【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

楼主: 鼯鼠

[转帖]着色器和效果

[复制链接]
 楼主| 发表于 2008-4-17 21:19:47 | 显示全部楼层

2.4 样例应用程序:散射光照(下)

阅读此文表明您已同意文末的声明

既然我们已经看到了实际的顶点着色器的代码,那么就让我们改变方式来看看应用程序的代码。这个应用程序有下列相关的全局变量:

IDirect3DVertexShader9* DiffuseShader = 0;

ID3DXConstantTable* DiffuseConstTable = 0;

ID3DXMesh* Teapot = 0;

D3DXHANDLE ViewMatrixHandle = 0;

D3DXHANDLE ViewProjMatrixHandle = 0;

D3DXHANDLE AmbientMtrlHandle = 0;

D3DXHANDLE DiffuseMtrlHandle = 0;

D3DXHANDLE LightDirHandle = 0;

D3DXMATRIX Proj;

我们有变量来代表顶点着色器及其常量表。我们有一个茶壶的网格变量,跟着是一集D3DXHANDLE,其变量名描述了了他们引用的变量:

Setup函数执行下列任务:

n 创建茶壶网格

n 编译顶点着色器

n 根据已编译代码创建顶点着色器

n 通过常量表获取着色器程序中的几个变量的句柄

n 通过常量表初始化这几个着色器变量

注意:对于本应用程序,我们的顶点结构不需要任何自由顶点格式不能描述的额外的分量。因此,在本例中,我们使用一个自由顶点格式来代替顶点声明。回想一下,自由顶点格式描述最终在内部被转换为一个顶点声明。

bool Setup()

{

HRESULT hr = 0;

//

// Create geometry:

//

D3DXCreateTeapot(Device, &Teapot, 0);

//

// Compile shader

//

ID3DXBuffer* shader = 0;

ID3DXBuffer* errorBuffer = 0;

hr = D3DXCompileShaderFromFile(

"diffuse.txt",

0,

0,

"Main", // entry point function name

"vs_1_1",

D3DXSHADER_DEBUG,

&shader,

&errorBuffer,

&DiffuseConstTable);

// output any error messages

if( errorBuffer )

{

::MessageBox(0, (char*)errorBuffer->GetBufferPointer(), 0, 0);

d3d::Release<ID3DXBuffer*>(errorBuffer);

}

if(FAILED(hr))

{

::MessageBox(0, "D3DXCompileShaderFromFile() - FAILED", 0, 0);

return false;

}

//

// Create shader

//

hr = Device->CreateVertexShader(

(DWORD*)shader->GetBufferPointer(),

&DiffuseShader);

if(FAILED(hr))

{

::MessageBox(0, "CreateVertexShader - FAILED", 0, 0);

return false;

}

d3d::Release<ID3DXBuffer*>(shader);

//

// Get Handles

//

ViewMatrixHandle = DiffuseConstTable->GetConstantByName(

0, "ViewMatrix");

ViewProjMatrixHandle = DiffuseConstTable->GetConstantByName(

0, "ViewProjMatrix");

AmbientMtrlHandle = DiffuseConstTable->GetConstantByName(

0, "AmbientMtrl");

DiffuseMtrlHandle = DiffuseConstTable->GetConstantByName(

0, "DiffuseMtrl");

LightDirHandle = DiffuseConstTable->GetConstantByName(

0, "LightDirection");

//

// Set shader constants:

//

 楼主| 发表于 2008-4-17 21:20:04 | 显示全部楼层

// Light direction:

D3DXVECTOR4 directionToLight(-0.57f, 0.57f, -0.57f, 0.0f);

DiffuseConstTable->SetVector(Device, LightDirHandle,

&directionToLight);

// Materials:

D3DXVECTOR4 ambientMtrl(0.0f, 0.0f, 1.0f, 1.0f);

D3DXVECTOR4 diffuseMtrl(0.0f, 0.0f, 1.0f, 1.0f);

DiffuseConstTable->SetVector(Device,AmbientMtrlHandle,&ambientMtrl);

DiffuseConstTable->SetVector(Device,DiffuseMtrlHandle,&diffuseMtrl);

DiffuseConstTable->SetDefaults(Device);

// Compute projection matrix.

D3DXMatrixPerspectiveFovLH(

&roj, D3DX PI * 0.25f,

(float)Width / (float)Height, 1.0f, 1000.0f);

return true;

}

Display函数非常简单。它检测用户输入(译者注:这里指的是用户输入的传入着色器程序的变量),并相应的更新视图矩阵。但是,因为我们在着色器中执行这个视图矩阵变换,所以我们还必须更新着色器中的视图矩阵变量。我们用常量表完成这件事情。

bool Display(float timeDelta)

{

if( Device )

{

//

// Update view matrix code snipped...

//

D3DXMATRIX V;

D3DXMatrixLookAtLH(&V, &position, &target, &up);

DiffuseConstTable->SetMatrix(Device, ViewMatrixHandle, &V);

D3DXMATRIX ViewProj =V *Proj;

DiffuseConstTable->SetMatrix(Device, ViewProjMatrixHandle,

&ViewProj);

//

// Render

//

Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,

0xffffffff, 1.0f, 0);

Device->BeginScene();

Device->SetVertexShader(DiffuseShader);

Teapot->DrawSubset(0);

Device->EndScene();

Device->resent(0, 0, 0, 0);

}

return true;

}

同样注意,就在DrawSubset调用之前,我们允许了这个我们希望使用的顶点着色器。

清理也需要被完成;我们简单的释放了这个已分配接口:

void Cleanup()

{

d3d::Release<ID3DXMesh*>(Teapot);

d3d::Release<IDirect3DVertexShader9*>(DiffuseShader);

d3d::Release<ID3DXConstantTable*>(DiffuseConstTable);

}

 楼主| 发表于 2008-4-19 16:33:23 | 显示全部楼层

2.5 样例应用程序:卡通渲染(上)

阅读此文表明您已同意文末的声明

作为第二个顶点着色器的例子,让我们编写两个顶点着色器,它们以卡通风格绘画的方式对网格着色(shade)和画轮廓(outline)。图17.2展示了这一点:

2.2:(a)使用卡通着色法着色的对象(注意着色间的尖锐过渡)。(b)增强卡通效果,轮廓边silhouette edge)被勾出。(c)使用标准散射光照着色的对象

注意:卡通渲染是一种特定类型的非写实渲染non-photorealistic rendering),有时被称作风格化渲染stylistic rendering)。

虽然卡通渲染不适用于所有游戏,例如激烈的第一人称射击游戏,但是它仍然可以增强一些希望表现卡通感觉类型游戏的气氛。此外,卡通渲染相当易于实现,并让我们得以漂亮的演示顶点着色器。

我们将卡通渲染分割为两步:

1. 卡通绘图典型的很少有在两种着色间强烈过渡的着色强度luminance)级别;我们称此为卡通着色(cartoon shading)。在图2.2a)中,我们看到只用了三种着色强度(亮、中、暗)对网格进行着色,而且其间的过渡是不连贯的——不像图2.2c),其明暗过渡是平滑的。

2. 卡通绘图也典型的有其轮廓边被勾出,如图2.2b)所示。

这两个步骤都需要其各自的顶点着色器。

2.5.1 卡通着色

要实现卡通着色,我们采用Lander20003月发表在Game Developer Magazine的文章“Shades of Disney: Opaquing a 3D World”中所描述的方法。它像这样工作:我们创建一个带强度级别的灰度纹理,它包含我们需要的不同的着色强度。图2.3显示了我们在样例程序中使用的这个纹理。

2.3:用来保存着色强度的着色纹理。注意观察不连续的着色间过渡和纹理着色强度必须从左到右增加。

然后在顶点着色器中,我们执行标准散射点积运算standard diffuse calculation dot product)来确定顶点法线N和光线向量L之间角度的余弦,用以确定顶点接收到多少光线:

s=L.N

如果s0,就表示光线向量和顶点法线之间的角度大于90度,也就表示该表面接收不到光线。因此,如果s0,我们就让s0。所以s [0, 1]

现在,在通常的散射光照模型中,我们使用s来标记颜色向量,这样顶点颜色就根据接收到的光照的量变暗:

diffuseColor = s(r, g, b, a)

但是,这将会导致从亮到暗之间着色的平滑过渡。这是与我们期望的卡通着色相反的。我们想要一个(对卡通渲染在两至四种着色间工作良好的)在一些不同着色间的不连续的过渡。

反其道而行之,不使用s来标记颜色向量,我们准备使用s作为早先提到的强度纹理的u纹理坐标——如图2.3

注意:标量scalars必定是一个有效的纹理坐标,因为s [0, 1],这是通常纹理坐标的区间。

按这种方式,顶点不会被平滑着色,而是不连续的。例如,强度纹理可能被分成3种着色,如图2.4所示:

2.4:那么,s [0, 0.33]的值使用shader0着色,s [ 0.330.66]的值使用shader1着色,s [0.66,1]的值使用shader2着色。当然,从这些着色的一种到另一种的过渡是不连续的,这就赋予了我们期望的效果。

注意:我们还为卡通着色关闭了纹理过滤,因为这种过滤会试图使着色过渡变平滑。这对于我们要求的不连续过渡是多余的。

 楼主| 发表于 2008-4-19 16:33:36 | 显示全部楼层

2.5.2 卡通着色的顶点着色器代码

我们现在呈现卡通着色的顶点着色器。这个着色器的主要任务只是根据s=L.N计算并设置纹理坐标。注意观察输出结构,我们已经增加了一个数据成员来存储已被计算过的纹理坐标。同时还需注意,我们仍然输出顶点颜色,虽然我们不修改它,不过当颜色被与强度纹理组合起来的时候,它呈现为被着色的。

// File: toon.txt

// Desc: Vertex shader that lights geometry so it appears to be

// drawn in a cartoon style.

//

// Globals

//

extern matrix WorldViewMatrix;

extern matrix WorldViewProjMatrix;

extern vector Color;

extern vector LightDirection;

static vector Black = {0.0f, 0.0f, 0.0f, 0.0f};

//

// Structures

//

struct VS_INPUT

{

vector position : POSITION;

vector normal : NORMAL;

};

struct VS_OUTPUT

{

vector position : POSITION;

float2 uvCoords : TEXCOORD;

vector diffuse : COLOR;

};

//

// Main

//

VS_OUTPUT Main(VS_INPUT input)

{

// zero out each member in output

VS_OUTPUT output = (VS_OUTPUT)0;

// transform vertex position to homogenous clip space

output.position = mul(input.position, WorldViewProjMatrix);

//

// Transform lights and normals to view space. Set w

// components to zero since we're transforming vectors.

// Assume there are no scalings in the world

// matrix as well.

//

LightDirection.w = 0.0f;

input.normal.w = 0.0f;

LightDirection = mul(LightDirection, WorldViewMatrix);

input.normal = mul(input.normal, WorldViewMatrix);

//

// Compute the 1D texture coordinate for toon rendering.

//

float u = dot(LightDirection, input.normal);

//

// Clamp to zero if u is negative because u

// negative implies the angle between the light

// and normal is greater than 90 degrees. And

// if that is true then the surface receives

// no light.

//

if(u < 0.0f)

u = 0.0f;

//

// Set other tex coord to middle.

//

float v = 0.5f;

output.uvCoords.x = u;

output.uvCoords.y = v;

// save color

output.diffuse = Color;

return output;

}

两点注解:

n 我们假设世界矩阵没有执行任何缩放。因为如果它执行,它就会弄乱乘以它的顶点的长度和方向。

n 我们总是设置v纹理坐标为纹理的中点。这意味着我们仅使用纹理中一条单一的线,那就是说我们可以使用1D强度纹理来代替2D的那个纹理。不管怎样,1D2D纹理都能工作。本例中,我们使用了2D纹理而不是1D纹理,这是没有什么特别的原因的。

 楼主| 发表于 2008-4-17 20:58:14 | 显示全部楼层

概览

在本章中,我们将描述高级顶点着色语言(High-Level Shading Language ,简称HLSL),我们用它编写以下三章的顶点和像素着色器。简单的说,顶点和像素着色器是我们编写并在图形卡的GPUgraphics processing unit)上执行小的自定义程序,它替换了固定功能管线的一部分。通过用我们自定义着色器程序替换固定功能管线的一个部件,我们获得了能达到的图形效果的最大自由度。我们不再受限于预定义的“固定的”操作。

为了编写着色器程序,我们需要一种语言去编写它们。在DirectX 8.x中,着色器用低级着色器汇编语言编写。幸运的是,我们不再需要用汇编语言编写着色器了,因为DirectX 9已经支持高级着色器语言,我们可以用它编写着色器。使用HLSL在汇编语言之上编写着色器程序可从高级语言中受益,就像C++一样,它以超越汇编语言的方式编写应用程序,也就是说:

n 增加了生产力——用高级语言编程比用低级语言更快更容易。我们可以集中更多时间在算法上,而不是在编码上。

n 提高了可读性——高级语言程序更容易阅读,这意味着高级语言写的程序更易调试和维护。

n 编译器,时常,会生成比手写汇编更高效的汇编代码。

n 使用HLSL编译器,我们可以将代码编译为任意可用的着色器版本;使用汇编语言,我们可能需要为指定版本移植代码。

HLSL还和CC++语法相似,这样就缩短了学习的曲线。

最后,如果图形卡不支持顶点着色器和像素着色器的话,你就要为着色器例子程序切换到REF设备。

注意:像素着色器可以用软件模拟为软件顶点处理(software vertex processing)——D3DCREATE_SOFTWARE_VERTEX-PROCESSING

目标

n 学习如何编写和编译HLSL着色器程序

n 学习如何在应用程序和着色器程序之间交换数据

n 逐渐熟悉HLSL的语法、类型和内建函数

第一章 介绍高级着色语言
阅读此文表明您已同意文末的声明

概览
在本章中,我们将描述高级顶点着色语言(High-Level Shading Language ,简称HLSL),我们用它编写以下三章的顶点和像素着色器。简单的说,顶点和像素着色器是我们编写并在图形卡的GPU(graphics processing unit)上执行小的自定义程序,它替换了固定功能管线的一部分。通过用我们自定义着色器程序替换固定功能管线的一个部件,我们获得了能达到的图形效果的最大自由度。我们不再受限于预定义的“固定的”操作。

为了编写着色器程序,我们需要一种语言去编写它们。在DirectX 8.x中,着色器用低级着色器汇编语言编写。幸运的是,我们不再需要用汇编语言编写着色器了,因为DirectX 9已经支持高级着色器语言,我们可以用它编写着色器。使用HLSL在汇编语言之上编写着色器程序可从高级语言中受益,就像C++一样,它以超越汇编语言的方式编写应用程序,也就是说:

n 增加了生产力——用高级语言编程比用低级语言更快更容易。我们可以集中更多时间在算法上,而不是在编码上。

n 提高了可读性——高级语言程序更容易阅读,这意味着高级语言写的程序更易调试和维护。

n 编译器,时常,会生成比手写汇编更高效的汇编代码。

n 使用HLSL编译器,我们可以将代码编译为任意可用的着色器版本;使用汇编语言,我们可能需要为指定版本移植代码。

HLSL还和C或C++语法相似,这样就缩短了学习的曲线。

最后,如果图形卡不支持顶点着色器和像素着色器的话,你就要为着色器例子程序切换到REF设备。

注意:像素着色器可以用软件模拟为软件顶点处理(software vertex processing)——D3DCREATE_SOFTWARE_VERTEX-PROCESSING。

目标
n 学习如何编写和编译HLSL着色器程序

n 学习如何在应用程序和着色器程序之间交换数据

n 逐渐熟悉HLSL的语法、类型和内建函数



[声明]:本文译自Frank Luna的《Introduction to 3D Game Programming with DirectX 9.0》,限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;本文仅用于学习交流与参考用途,不得用于任何形式的商业用途;如需转载需事先征得作者本人和译者的同意,保持文章的完整性,并注明作者、译者和出处,对于违反以上条款造成的后果,译者对此不负任何责任。我的邮箱地址是Raymond_King123@hotmail.com,欢迎热爱3D图形和游戏,并有一定图形编程经验的朋友来信交流。

第一章 介绍高级着色语言
阅读此文表明您已同意文末的声明

概览
在本章中,我们将描述高级顶点着色语言(High-Level Shading Language ,简称HLSL),我们用它编写以下三章的顶点和像素着色器。简单的说,顶点和像素着色器是我们编写并在图形卡的GPU(graphics processing unit)上执行小的自定义程序,它替换了固定功能管线的一部分。通过用我们自定义着色器程序替换固定功能管线的一个部件,我们获得了能达到的图形效果的最大自由度。我们不再受限于预定义的“固定的”操作。

为了编写着色器程序,我们需要一种语言去编写它们。在DirectX 8.x中,着色器用低级着色器汇编语言编写。幸运的是,我们不再需要用汇编语言编写着色器了,因为DirectX 9已经支持高级着色器语言,我们可以用它编写着色器。使用HLSL在汇编语言之上编写着色器程序可从高级语言中受益,就像C++一样,它以超越汇编语言的方式编写应用程序,也就是说:

n 增加了生产力——用高级语言编程比用低级语言更快更容易。我们可以集中更多时间在算法上,而不是在编码上。

n 提高了可读性——高级语言程序更容易阅读,这意味着高级语言写的程序更易调试和维护。

n 编译器,时常,会生成比手写汇编更高效的汇编代码。

n 使用HLSL编译器,我们可以将代码编译为任意可用的着色器版本;使用汇编语言,我们可能需要为指定版本移植代码。

HLSL还和C或C++语法相似,这样就缩短了学习的曲线。

最后,如果图形卡不支持顶点着色器和像素着色器的话,你就要为着色器例子程序切换到REF设备。

注意:像素着色器可以用软件模拟为软件顶点处理(software vertex processing)——D3DCREATE_SOFTWARE_VERTEX-PROCESSING。

目标
n 学习如何编写和编译HLSL着色器程序

n 学习如何在应用程序和着色器程序之间交换数据

n 逐渐熟悉HLSL的语法、类型和内建函数



[声明]:本文译自Frank Luna的《Introduction to 3D Game Programming with DirectX 9.0》,限于译者水平,文中难免错漏之处,欢迎各位网友批评指正;本文仅用于学习交流与参考用途,不得用于任何形式的商业用途;如需转载需事先征得作者本人和译者的同意,保持文章的完整性,并注明作者、译者和出处,对于违反以上条款造成的后果,译者对此不负任何责任。我的邮箱地址是Raymond_King123@hotmail.com,欢迎热爱3D图形和游戏,并有一定图形编程经验的朋友来信交流。

 楼主| 发表于 2008-4-17 21:00:27 | 显示全部楼层

1.1编写HLSL着色器

阅读此文表明您已同意文末的声明

我们可以直接把HLSL着色器代码作为一长串字符串编写进我们的应用程序源文件中。但是,更加方便和模块化的方法是把着色器的代码从应用程序代码中分离出来。因此,我们可以在记事本里编写我们的着色器代码,并将之保存进一般的ASCII文本文件中。然后再使用D3DXCompileShaderFromFile函数(1.2.2)编译我们的着色器。

作为介绍,下面给出一个简单的用HLSL编写的顶点着色器,它的源文件是从文本文件Transform.txt生成而来。完整的项目可以在Transform标题下找到。顶点着色器通过一个组合视图和投影矩阵变换(transform)顶点,并且设置顶点颜色的漫反射成分(the diffuse color component of the vertex)为蓝色。

注意:本例使用顶点着色器作为示例,但是别急管顶点着色器做了什么,它们会在下一章中讲述。至于现在,你要做的就是熟悉HLSL程序的语法和格式。

/////////////////////////////////////////////////////////////////////
//
// File: transform.txt
//
// Author: Frank D. Luna (C) All Rights Reserved
//
// System: AMD Athlon 1800+ XP, 512 DDR, Geforce 3, Windows XP,
//
				MSVC++ 7.0
//
// Desc: Vertex shader that transforms a vertex by the view and
//
				projection transformation, and sets the vertex color to blue.
//
/////////////////////////////////////////////////////////////////////

//
// Globals
//

// Global variable to store a combined view and projection
// transformation matrix.
				We initialize this variable
// from the application.
matrix ViewProjMatrix;

// Initialize a global blue color vector.
vector Blue = {0.0f, 0.0f, 1.0f, 1.0f};

//
// Structures
//

// Input structure describes the vertex that is input
// into the shader.
				Here the input vertex contains
// a position component only.
struct VS_INPUT
{

					vector position
					: POSITION;
};

// Output structure describes the vertex that is
// output from the shader. Here the output
// vertex contains a position and color component.
struct VS_OUTPUT
{

					vector position : POSITION;

					vector diffuse
					: COLOR;
};

//
// Main Entry Point, observe the main function
// receives a copy of the input vertex through
// its parameter and returns a copy of the output
// vertex it computes.
//

VS_OUTPUT Main(VS_INPUT input)
{

					// zero out members of output

					VS_OUTPUT output = (VS_OUTPUT)0;


					// transform to view space and project

					output.position
					= mul(input.position, ViewProjMatrix);


					// set vertex diffuse color to blue

					output.diffuse = Blue;


					//Output the projected and colored vertex.

					return output;
}

1.1.1全局变量

首先,我们初始化了两个全局变量:

matrix ViewProjMatrix;
vector Blue = {0.0f, 0.0f, 1.0f, 1.0f};
 楼主| 发表于 2008-4-17 21:03:09 | 显示全部楼层

第一个变量,ViewProjMatrix,是matrix类型,它是一个HLSL内建的4?矩阵类型。这个变量存储视图和投影矩阵的组合,这样它就同事描述了这两个变换。这种方式使我们仅需使用一个向量矩阵乘法(vector-matrix multiplicatio),而不是两个。要注意的是,着色器源代码的任何地方都没有初始化这个变量。那是因为我们它是我们在应用程序的源代码中进行设置的——而不是在着色器中。应用程序和着色器直接的通信是常用操作,详见1.2.1节。
第二个变量,Blue,是内建的vector类型,它是一个4D数组。我们简单的将其成分(译者注:这里的成分指的是顶点颜色的漫反射成分)初始化为蓝,将其视为一个RGBA颜色数组。
1.1.2输入和输出结构

在全局变量声明之后,我们定义了两个特殊的结构,我们叫它们“输入”(input)和“输出”(output)结构。对于顶点着色器而言,这些结构分别定义了我们的着色器要输入和输出的顶点数据(vertex data)。

struct VS_INPUT
{
vector position : POSITION;
};

struct VS_OUTPUT
{
vector position : POSITION;
vector diffuse : COLOR;
};
注意:像素数据(pixel data)由像素着色器的输入和输出结构定义。

本例中,我们输入进顶点着色器的顶点仅包括一个位置成分(position component)。我们输出的着色器包含了一个位置成分和一个颜色成分。

特殊的冒号代表了一种语义,它用于指定变量的用途。这类似于自由顶点格式(flexible vertex format ,简称FVF)的顶点结构。例如,在VS_INPUT中,有一个成员:


vector position : POSITION;
" : POSITION"的语法表示向量position是用于描述输入的顶点的位置的。又例如,在VS_OUTPUT中,我们有:


vector diffuse : COLOR;


这里,": COLOR"表示向量diffuse是用于描述输出顶点的颜色的的。我们将在下两章中详细讨论可用的用途标识符(usage identifier)。

注意:从底层的观点来看,与变量语法的语义相关联的是硬件寄存器。也就是说,输入变量与输入寄存器关联,输出变量与输出寄存器关联。例如,VS_INPUT的position成员与顶点输入位置寄存器(the vertex input position register)相连。类似的,diffuse与一个特定的顶点颜色输出寄存器(vertex output color register)相连。

 楼主| 发表于 2008-4-17 21:03:44 | 显示全部楼层

1.3 变量类型

阅读此文表明您已同意文末的声明

注意:除了下列各小节中描述的类型外,HLSL还有一些内建的对象类型(如:纹理对象texture object))。但是,由于这些对象类型主要用于效果框架,我们将对其延迟到4讨论。

1.3.1 数值类型

HLSL支持下列数值类型scalar type):

n
bool——Truefalse值。注意,HLSL提供了truefalse关键字

n
int——32位有符号整型

n
half——16位浮点数

n
float——32位浮点数

n
double——64位浮点数

注意:某些平台可能不支持inthalfdouble。如果是这种情况,这些类型将用float模拟。

1.3.2 向量类型

HLSL有下列内建向量类型vector type):

n
vector——各分量为float类型的4D向量

n
vector<T, n>——一个n维向量,其每个分量为数值scalar类型Tn维必须在14之间。这里是一个2D double向量:

vector<double, 2> vec2;

我们可以使用数组下标的语法访问向量的一个分量。例如,要设置向量vec的第i个分量,我们可以写成:

vec = 2.0f;

此外,我们可以像访问结构的成员一样访问向量vec的一个分量,使用已定义的分量名xyzwrgba

vec.x = vec.r = 1.0f;
vec.y = vec.g = 2.0f;
vec.z = vec.b = 3.0f;
vec.w = vec.a = 4.0f;

名称为rgba的分量分别对应xyzw的分量。当使用向量来表示颜色的时候,RGBA符号更符合这个向量代表一种颜色的事实。

作为选择,我们可以使用其它一些预定义的分别用来代表2D3D4D向量的类型:

float2 vec2;
float3 vec3;
float4 vec4;

考虑向量u = (ux, uy, uz, uw),假设我们要拷贝u的所有分量到一个像v = (ux, uy, uy, uw)这样的向量v。最直接的方法可能是单独按需从uv拷贝每个分量。但不管怎样,HLSL提供了一种特殊的语法做这类无序拷贝的工作,它叫做“鸡尾酒”(swizzles):

vector u = {l.0f, 2.0f, 3.0f, 4.0f};
vector v = {0.0f, 0.0f, 5.0f, 6.0f};


v = u.xyyw; // v = {1.0f, 2.0f, 2.0f, 4.0f}

拷贝数组时,我们不必拷贝完每个分量。例如,我们可以仅拷贝xy分量,代码段举例如下:

vector u = {1.0f, 2.0f, 3.0f, 4.0f};
vector v = {0.0f, 0.0f, 5.0f, 6.0f};
v.xy = u; // v = {l.0f, 2.0f, 5.0f, 6.0f}
 楼主| 发表于 2008-4-17 21:06:00 | 显示全部楼层

1.3.3 矩阵类型
HLSL有下列内建矩阵类型:

n matrix——一个4?矩阵,其各项类型为float

n matrix<T, m, n>——一个m譶矩阵,其各项类型为数值类型T。矩阵维数m和n必须在1至4之间。这里是一个2?整型矩阵的例子:

matrix<int, 2, 2> m2x2;
作为选择,我们可以定义一个m譶矩阵,其m和n在1至4之间,使用下列语法:

floatmxn matmxn;
实例:

float2x2 mat2x2;
float3x3 mat3x3;
float4x4 mat4x4;
float2x4 mat2x4;
注意:类型不必须是float类型——我们可以使用其它类型。举例来说,我们可以用整型,写成这样:

int2x2 i2x2;
int2x2 i3x3;
int2x2 i2x4;
我们可以用二维数组的下标语法访问矩阵中的项。例如,要设置矩阵M的第i,j个项,我们可以写成:

M [j] = value;
此外,我们可以像访问结构的成员那样访问矩阵M的项。下列条目已定义:

以1为基数的:

M._11 = M._12 = M._13 = M._14 = 0.0f;
M._21 = M._22 = M._23 = M._24 = 0.0f;
M._31 = M._32 = M._33 = M._34 = 0.0f;
M._41 = M._42 = M._43 = M._44 = 0.0f;
以0为基数的:

M._m00 = M._m01 = M._m02 = M._m03 = 0.0f;
M._m10 = M._m11 = M._m12 = M._m13 = 0.0f;
M._m20 = M._m21 = M._m22 = M._m23 = 0.0f;
M._m30 = M._m31 = M._m32 = M._m33 = 0.0f;
有时,我们想要访问矩阵中一个特定的行。我们可以用一维数组的下标语法来做。例如,要引用矩阵M中第i行的向量,我们可以写:

vector ithRow = M; // get the ith row vector in M
注意:可以使用两种语法在HLSL中初始化变量:

vector u = {0.6f, 0.3f, 1.0f, 1.0f};
vector v = {1.0f, 5.0f, 0.2f, 1.0f};
也可以,等价的,使用构造风格的语法:

vector u = vector(0.6f, 0.3f, 1.0f, 1.0f);
vector v = vector(1.0f, 5.0f, 0.2f, 1.0f);
其它一些例子:

float2x2 f2x2 = float2x2(1.0f, 2.0f, 3.0f, 4.0f);
int2x2 m = {1, 2, 3, 4};
int n = int(5);
int a = {5};
float3 x = float3(0, 0, 0);
1.3.4 数组
我们可以用类似C++的语法声明特定类型的一个数组。例如:

float M[4][4];
half p[4];
vector v[12];
1.3.5 结构
结构的定义和在C++里一样。但是,HLSL里的结构不能有成员函数。这是一个HLSL里的结构的例子:

struct MyStruct
{
matrix T;
vector n;
float f;
int x;
bool b;
};
MyStruct s; // instantiate
s.f = 5.0f; // member access
1.3.6 typedef关键字
HLSL的typedef关键字功能和C++里的完全一样。例如,我们可以给类型vector<float, 3>用下面的语法命名:

typedef vector<float, 3> point;
然后,不用写成:

vector<float, 3> myPoint;
……我们只需这样写:

point myPoint;
这里是另外两个例子,它展示了如何对常量和数组类型使用typedef关键字:

typedef const float CFLOAT;
typedef float point2[2];
1.3.7 变量前缀
下列关键字可以做变量声明的前缀:

n static——如果全局变量带static关键字前缀,就表示它不是暴露于着色器之外的。换句话说,它是着色器局部的。如果一个局部变量以static关键字为前缀,它就有和C++中static局部变量相同的行为。也就是说,该变量在函数首次执行时被一次性初始化,然后在所有函数调用中维持其值。如果变量没有被初始化,它就自动初始化为0。

static int x = 5;
n uniform——如果变量以uniform关键字为前缀,就意味着此变量在着色器外面被初始化,比如被C++应用程序初始化,然后再输入进着色器。

n extern——如果变量以extern关键字为前缀,就意味着该变量可在着色器外被访问,比如被C++应用程序。仅全局变量可以以extern关键字为前缀。不是static的全局变量默认就是extern。

n shared——如果变量以shared关键字为前缀,就提示效果框架(参见19章):变量将在多个效果间被共享。仅全局变量可以以shared为前缀。

n volatile——如果变量以volatile关键字为前缀,就提示效果框架(参见19章):变量将被时常修改。仅全局变量可以以volatile为前缀。

n const——HLSL中的const关键字和C++里的意思一样。也就是说,如果变量以const为前缀,那此变量就是常量,并且不能被改变。

const float pi = 3.14f;

 楼主| 发表于 2008-4-19 16:34:17 | 显示全部楼层

2.5 样例应用程序:卡通渲染(下)

阅读此文表明您已同意文末的声明

2.5.3 轮廓勾勒

要完成卡通效果,我们还需要勾勒outline轮廓边silhouette edge)。这比卡通着色复杂一点。

2.5.3.1 边的表示法

我们将一个网格的一条边表示为一个四元组(从两个三角形构建)——参见图2.5

2.5:表示边的四元组

我们选择四元组有两个原因:我们可以通过调整四元组的维容易的改变边的厚度,并且我们可以渲染退化的四元组来隐藏某些边,也即轮廓边。在Direct3D中,我们从两个三角形来构建一个四元组。退化四元组degenerate quad)是从两个退化三角形构建而来的四元组。退化三角形degenerate triangle)是一个面积为零的三角形,或者换句话说,是一个三点位于一线上的三角形。如果我们传入一个退化三角形到渲染管线,则该三角形显示为空。这是很有用的,因为如果我们希望隐藏特定三角形,我们可以简单的退化它而不需要实际的从三角形列表(顶点缓冲)移除它。回想一下,我们只需要显示轮廓边——而不是网格的每一条边。

当我们首先创建一条边的时候,我们指定其四个顶点,并使其退化,这意味着边将会被隐藏(渲染时不显示)。

2.6:由两个三角形共用边描述的退化四元组

注意图2.6中的两个顶点v0v1,我们设置其顶点法线向量为零向量。然后当我们将边的顶点送入顶点着色器的时候,顶点着色器将会检测顶点是否位于轮廓边上;如果是,则顶点着色器将按顶点法线的方向偏移顶点位置的标量。观察法线向量为零的顶点,它不会被偏移。

因此,我们最终以一个非退化四元组non-degenerate quad)来表示轮廓边,如图2.7所示。

2.7:位于轮廓边上的顶点v2v3被按照其各自的顶点法线n2n3进行偏移。观察顶点v0v1仍然保持在其固定位置,因为其顶点法线等于零向量,因此对于它们来说没有偏移发生。按这种方式,四元组成功的重新生成来表示轮廓边。

备注:如果我们没有设置顶点v0v1的顶点法线为零向量,那么那些顶点就同样会被偏移。但是如果偏移描述轮廓边的所有四个顶点,那么我们仅是平移了该退化四元组。通过保持顶点v0v1固定并仅仅偏移顶点v2v3,我们重新生成了四元组。

2.5.3.2 轮廓边测试

若两个表面face0face1以与视图方向相异的方向共享边所在表面,则该边为轮廓边。也就是说,如果一个表面是前面front facing)而另一个表面是后面back facing),那么这条边就是一条轮廓边。图2.8给出了一个轮廓边和一个非轮廓边的例子。

2.8:在(a)中,由v0 v1定义的共享边的一个表面是前面,而共享边另一个表面是后面,因此该边是轮廓边。在(b)中,由v0 v1定义的这两个共享边的表面都是正面,因此该边不是轮廓边。

接下来,为了检测一个顶点是否在轮廓边上,我们必须以每个顶点为基础了解face0 face1的法线向量。我们的边的顶点数据结构反映如下:

struct VS_INPUT

{

vector position : POSITION;

vector normal : NORMAL0;

vector faceNormal1 : NORMAL1;

vector faceNormal2 : NORMAL2;

};

前两个分量很直接,但让我们看看两个额外的法线向量,它们是faceNormal1faceNormal2。这些向量描述了两个表面的表面法线,共享边的顶点位于这两个表面的共享边上,这两个表面是face0face1

实际检测顶点是否在共享边上的数学如下。假设我们在视图空间中,令v为一原点指向检测顶点的向量——图2.8,令n0face0的表面法线且n1face0的表面法线,若下面的不等式为真,则顶点位于轮廓边上:

1v.n0)(v.n1)<0

若两点积符号相异,则不等式为真,使得不等式左边为负。回想一下点积的性质,两个点积的符号相异意味着一个表面是前面而另一个是后面。

现在,考虑一条边只有一个三角形共享它的情况,如图2.9,其法线将会被存储在faceNormal1中。

2.9:顶点v0v1定义的边只有一个表面共享它

我们定义这种边为轮廓边。要确保顶点着色器将这种边作为轮廓边处理,我们要让faceNormal2 = -faceNormal1。因此,反向的表面法线和不等式(1)为真,表示该边为一轮廓边。

2.5.3.3 边的生成

生成网格的边是微不足道的;我们简单的遍历网格的每个表面并为表面上每条边计算一个四元组(退化的,如图2.6所示)。

注意:每个表面有三条边,因为每个三角形有三条边。

对于每条边的迭代,我们也许要知道共享边的两个表面。表面之一是边所在的三角形。例如,如果要计算第1个表面的一条边,那么第1个表面共享该边。共享该边的另一个表面可以使用网格的邻接信息找到。

 楼主| 发表于 2008-4-19 16:34:30 | 显示全部楼层

2.5.4 轮廓边顶点着色器代码

我们现在呈现渲染轮廓边的顶点着色器代码。这个着色器的主要任务就是确定传入的顶点是否在轮廓边上。如果是,顶点着色器就按顶点法线的方向偏移顶点一定量的数值。

// File: outline.txt

// Desc: Vertex shader renders silhouette edges.

//

// Globals

//

extern matrix WorldViewMatrix;

extern matrix ProjMatrix;

static vector Black = {0.0f, 0.0f, 0.0f, 0.0f};

//

// Structures

//

struct VS_INPUT

{

vector position : POSITION;

vector normal : NORMAL0;

vector faceNormal1 : NORMAL1;

vector faceNormal2 : NORMAL2;

};

struct VS_OUTPUT

{

vector position : POSITION;

vector diffuse : COLOR;

};

//

// Main

//

VS_OUTPUT Main(VS_INPUT input)

{

// zero out each member in output

VS_OUTPUT output = (VS_OUTPUT)0;

// transform position to view space

input.position = mul(input.position, WorldViewMatrix);

// Compute a vector in the direction of the vertex

// from the eye. Recall the eye is at the origin

// in view space - eye is just camera position.

vector eyeToVertex = input.position;

// transform normals to view space. Set w

// components to zero since we're transforming vectors.

// Assume there are no scalings in the world

// matrix as well.

input.normal.w = 0.0f;

input.faceNormal1.w = 0.0f;

input.faceNormal2.w = 0.0f;

input.normal = mul(input.normal, WorldViewMatrix);

input.faceNormal1 = mul(input.faceNormal1, WorldViewMatrix);

input.faceNormal2 = mul(input.faceNormal2, WorldViewMatrix);

// compute the cosine of the angles between

// the eyeToVertex vector and the face normals.

float dot0 = dot(eyeToVertex, input.faceNormal1);

float dot1 = dot(eyeToVertex, input.faceNormal2);

// if cosines are different signs (positive/negative)

// then we are on a silhouette edge. Do the signs

// differ?

if( (dot0 * dot1) < 0.0f )

{

// yes, then this vertex is on a silhouette edge,

// offset the vertex position by some scalar in the

// direction of the vertex normal.

input.position += 0.1f * input.normal;

}

// transform to homogeneous clip space

output.position = mul(input.position, ProjMatrix);

// set outline color

output.diffuse = Black;

return output;

}

 楼主| 发表于 2008-4-19 16:35:18 | 显示全部楼层

第三章 介绍像素着色器

像素着色器是在对每个像素进行光栅化处理期间在图形卡的GPU上执行的程序。(不像顶点着色器,Direct3D不会以软件模拟像素着色器的功能。)它实际上替换了固定功能管线的多纹理化阶段the multitexturing stage),并赋予我们直接操纵单独的像素和访问每个像素的纹理坐标的能力。这种对像素和纹理坐标的直接访问使我们可以达成各种特效,例如:多纹理化multitexturing)、每像素光照per pixel lighting)、景深depth of field)、云状物模拟cloud simulation)、焰火模拟fire simulation)、混杂阴影化技巧sophisticated shadowing technique)。

图形卡支持的像素着色器的版本可以通过D3DCAPS9结构的PixelShaderVersion成员和D3DPS_VERSION宏进行检查。下列代码片断展示了这点:

// If the device's supported version is less than version 2.0

if( caps.PixelShaderVersion < D3DPS_VERSION(2, 0) )

// Then pixel shader version 2.0 is not supported on this device.

目标

n 获得对多纹理化概念的基本理解

n 学习如何编写、创建并使用像素着色器

n 学习如何使用像素着色器实现多纹理化效果

 楼主| 发表于 2008-4-19 16:35:43 | 显示全部楼层

3.1 多纹理化概览

阅读此文表明您已同意文末的声明

多纹理化Multitexturing)可能是能用像素着色器实现的最简单的技巧了。此外,因为像素着色器替换多纹理化阶段,那么接下来我们应该对多纹理化“是什么”和“做什么”有一个最基本的理解。本节介绍多纹理化的简明概览。

当我们一开始讨论纹理化texturing)的时候(译者注:对纹理化的讨论不在译文范围内,具体可参照原书第六章,本文是原书“第四部分:着色器和效果”的第三章),我们忽略了固定功能管线中对多纹理化的讨论,这有两个原因:第一,多纹理化是有一点棘手的过程,我们考虑到这在当时是一个高级话题;此外,固定功能多纹理化阶段被新的和更强有力的像素着色器替换掉了。因此花时间在已经过时的固定功能纹理化阶段上是无意义的。

多纹理化后面的思想有一点和混合blending)相关。在第七章中(译者注:原书第七章不在译文范围内)我们了解到:可以将正要被光栅化的像素与之前写入后台缓冲的像素进行混合来达成一种特效。我们推广这种相同的思想到多个纹理multiple texture)。也就是说,我们一次允许几个纹理,然后定义这些纹理如何被混合在一起以到达一种特殊效果。多纹理化的一个通常的用法是执行光照。作为在顶点处理阶段使用Direct3D的光照模型的替代,我们使用一种叫做“光照贴图”(light map)的特殊纹理贴图texture map),它编码encode)表面是如何被光照的(译者注:这句话的意思是“它定义表面是如何被光照的”)。例如,假设我们希望一盏聚光灯spotlight)照在一个大木箱上,我们要么可以定义一个D3DLIGHT9结构的聚光灯,要么可以将代表木箱的纹理贴图与代表聚光灯的光照映射混合在一起,如图3.1所示。

18.1:使用多纹理化渲染一个通过聚光灯照亮的木箱。这里我们通过将相应的纹理像素(texels)相乘来将这两个纹理组合起来。

注意:用第七章里的混合,结果图像依赖于纹理被混合的方式。在固定功能管线的多纹理化阶段,混合方程式被纹理渲染状态texture render state)控制。用像素着色器,我们可以以可编程的方式在代码中写出混合函数的简单表达式。这使我们可以以任何我们想要的方式混合纹理。我们将在讨论为本章准备的样例应用程序时详细讨论纹理混合。

混合多个纹理(本例中是两个)来照亮木箱比起Direct3D的光照来有两个好处:

n 光照是是预先在聚光灯的光照贴图里计算好的。因此,光照不需要在运行时被计算,这节省了处理时间。当然,只有静态对象和静态灯光的光照可以被预先计算。

n 因为光照贴图是预先计算好的,我们能够使用比Direct3D的(光照)模型多的多的更加精确的和复杂的光照模型。(在更加真实的场景中的更好的光照结果。)

备注:多纹理化阶段典型的用于实现静态对象的完全光照引擎(full lighting engine)。例如,我们可以用一个纹理贴图保存对象的颜色,比如木箱的纹理贴图。然后我们可以用一个散射光照贴图diffuse light map)保存散射表面着色diffuse surface shade),一个单独的镜面光照贴图保存镜面表面着色,一个雾状物贴图(fog map)保存覆盖在表面的雾状物的总量,还有可以用一个详细贴图(detail map)保存小的、高访问率的表面的细节。当所有这些纹理被组合起来,只需到这些预先计算的纹理中检索,就可以有效的照亮、着色并且增加细节到场景中去。

注意:聚光灯光照贴图是最基本的光照贴图的一个微不足道的例子。典型的,特定的程序用于在给定的场景和光源下生成光照贴图。生成光照贴图超越了本书的范围。有兴趣的读者可以参考Alan WattFabio Policarpo3D Games: Real-time Rendering and Software Technology中描述的光照贴图。

 楼主| 发表于 2008-4-19 16:36:19 | 显示全部楼层

3.1.1 允许多个纹理

回忆一下,纹理是用IDirect3DDevice9::SetTexture方法设置,而采样器状态(sampler state)是用IDirect3DDevice9::SetSamplerState方法设置,原型如下:

HRESULT IDirect3DDevice9::SetTexture(

DWORD Stage, // specifies the texture stage index

IDirect3DBaseTexture9 *pTexture

);

HRESULT IDirect3DDevice9::SetSamplerState(

DWORD Sampler, // specifies the sampler stage index

D3DSAMPLERSTATETYPE Type,

DWORD Value

);

注意:一个特定的采样器阶段索引i与第i个纹理阶段(texture stage)相关联。也就是说,第i个采样器阶段指定了第iset)纹理的采样器状态。

纹理/采样器阶段索引标识了我们希望设置的纹理/采样器的纹理/采样器阶段。因此,我们可以允许多个纹理并通过使用不同的阶段索引设置其相应的采样器状态。在本书前面的部分中,我们总是指定0,来指示第一个阶段,因为我们一次仅使用一个纹理。所以例如,假设我们要允许三个纹理,我们像这样使用阶段0,12

// Set first texture and corresponding sampler states.

Device->SetTexture( 0, Tex1);

Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);

Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

Device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);

// Set second texture and corresponding sampler states.

Device->SetTexture( 1, Tex2);

Device->SetSamplerState(1, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);

Device->SetSamplerState(1, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

Device->SetSamplerState(1, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);

// Set third texture and corresponding sampler states.

Device->SetTexture( 2, Tex3);

Device->SetSamplerState(2, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);

Device->SetSamplerState(2, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

Device->SetSamplerState(2, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);

这段代码允许Tex1, Tex2Tex3,并设置每个纹理的过滤模式。

3.1.2 多纹理坐标

回忆一下第六章,对于每个3D三角形,我们要在纹理上定义一个相应的三角形以映射到该3D三角形。我们通过对每个顶点增加纹理坐标完成这一点。因此,构成一个三角形的每三个顶点定义了一个在纹理上的对应的三角形。

因为我们现在需要使用多个纹理,对于每三个顶点定义的一个三角形,我们需要在每个被允许的纹理上定义一个相应的三角形。我们做这件事通过增加额外的纹理坐标的集合到每个顶点——每个顶点一集,而且相应地,每个允许的纹理。举个例子,如果我们混合三个纹理到一起,那么每个顶点必须有三集纹理坐标以索引到三个允许纹理。因此,一个三纹理的多纹理化顶点结构看起来可能像这样:

struct MultiTexVertex

{

MultiTexVertex(float x, float y, float z,

float u0, float v0,

float u1, float v1,

float u2, float v2)

{

_x = x; _y = y; _z = z;

_u0 = u0; _v0 = v0;

_u1 = u1; _v1 = v1;

_u2 = u2; _v2 = v2;

}

float _x, _y, _z;

float _u0, _v0; // Texture coordinates for texture at stage 0.

float _u1, _v1; // Texture coordinates for texture at stage 1.

float _u2, _v2; // Texture coordinates for texture at stage 2.

static const DWORD FVF;

};

const DWORD MultiTexVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX3;

注意,指定自由顶点格式标记D3DFVF_TEX3表明顶点结构包含三集纹理坐标。固定功能管线支持最多八集纹理坐标。要使用多于八集,你必须使用顶点声明和可编程顶点管线。

注意:在较新的像素着色器版本中,我们可以使用一集纹理坐标以索引到多个纹理,并因此消除了对多个纹理坐标的需要。当然这得假设每个纹理阶段使用相同的纹理坐标。如果每个阶段的纹理坐标不同,则我们仍然需要多纹理坐标。

 楼主| 发表于 2008-4-19 16:37:01 | 显示全部楼层

3.2 像素着色器输入和输出

两样东西要输入到像素着色器:颜色和纹理坐标。两样都是以每像素为单位的。

注意:回想一下,顶点颜色是在图元的面(face of primitive)间进行插值的。

每个像素的纹理坐标就是简单的 (u , v) 坐标,它指定了像素正准备被映射到的纹理的纹理单元。Direct3D按每个像素来计算颜色和纹理坐标,从顶点颜色和顶点纹理坐标,在进入像素着色器之前。输入到像素着色器的颜色和纹理坐标的数量依赖于顶点着色器输出的颜色和纹理坐标的数量。例如,如果一个顶点着色器输出了两个颜色和三个纹理坐标,那么Direct3D将会按每个像素计算两个颜色和三个纹理坐标并且把它们把它们输入到像素着色器。我们使用带语意的语法semantic syntax,译者注:参见译文第一章)映射这些输入颜色和纹理坐标进我们的着色器程序的变量里。使用前面的例子,我们可以这样写:

struct PS_INPUT

{

vector c0 : COLOR0;

vector c1 : COLOR1;

float2 t0 : TEXCOORD0;

float2 t1 : TEXCOORD1;

float2 t2 : TEXCOORD2;

};

对于输出,像素着色器输出一个单一的计算过的该像素的颜色值:

struct PS_OUTPUT

{

vector finalPixelColor : COLOR0;

};

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-2-6 09:56

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表