纹理映射就是在3D object表面添加细节的过程。可以比喻成包装礼品的过程,包装纸就是一种2D纹理。纹理映射是现代渲染的基础,并用于多种有趣的图形技术中。
相比如上一章使用HelloShaders和HelloShaders effects产生的纯色效果,通常情况你会期望在3D objects中渲染更多的细节。正如之前所讲的,3D模型由vertices组成,并组织成三角形,这些vertices至少包含一个坐标位置。但可以包含更多的内容。进一步增加表面细节可以为每一个vertex提供一种颜色值。对比一下图5.1中的3D cubes。
图5.1 3D cubes with different colors for each vertex (left) and solid red (right).
左边的cube中,每个vertex带有一种不同的颜色值;而右边的cube中,每个vertex的颜色值都相同。很明显左边的cube显示了更多的细节。这些颜色值使得object更加有趣,并定义了cube的多个表面。此外,单个pixels的颜色值随着该pixels所处的位置相对于vertices的位置不同而变化(回想一下第一章,“Introducing DirectX”讨论的在rasterizer阶段的插值)。通过增加3D objects中带有颜色值的vertices数量,就可以更好的控制表面的颜色。然而,一旦要精细的显示,就无法得到足够的vertices来进行高质量的渲染,而且任何尝试都会迅速产生大量的vertices。解决这高质量渲染的办法是使用纹理映射。
要将纹理映射到一个三角形上,需要知道每个vertex的二维坐标。该坐标用于查找存储在2D纹理中的颜色值。查找过程是在pixel shader中进行的,用于确定三角形中每个pixel的颜色值。而且纹理坐标是通过三角形的vertices插值计算得到的,而不是通过vertex color。
DirectX的纹理坐标范围在水平和垂直轴上都是[0, 1],以左上角为起点。一般情况下水平轴命名为u,垂直轴叫v。如图5.2所示,一个纹理映射到一个四边形上(由两个三角形组成),并标出了每个vertex对应的纹理坐标。
图5.2 DirectX 2D texture coordinates. (Original texture from Reto Stöckli, NASA Earth Observatory.
Additional texturing by Nick Zuccarello, Florida Interactive Entertainment Academy.)
注意
Direct3D支持1D,2D,和3D纹理,以及texture arrays和texture cubes(将于第8章,“Gleaming the Cube”讲述)。需要查找的坐标数量与对应的纹理维数相关。
列表5.1是一种纹理映射effect的代码。与之前一样,在NVIDIA FX Composer中创建一种新的effect/material,并把列表中的代码拷贝过去。然后就可以一步一步的测试代码了。
列表5.1 TextureMapping.fx
/************* Resources *************/ #define FLIP_TEXTURE_Y 1 cbuffer CBufferPerObject { float4x4 WorldViewProjection : WORLDVIEWPROJECTION < string UIWidget = "None"; >; } RasterizerState DisableCulling { CullMode = NONE; }; Texture2D ColorTexture < string ResourceName = "default_color.dds"; string UIName = "Color Texture"; string ResourceType = "2D"; >; SamplerState ColorSampler { Filter = MIN_MAG_MIP_LINEAR; AddressU = WRAP; AddressV = WRAP; }; /************* Data Structures *************/ struct VS_INPUT { float4 ObjectPosition : POSITION; float2 TextureCoordinate : TEXCOORD; }; struct VS_OUTPUT { float4 Position : SV_Position; float2 TextureCoordinate : TEXCOORD; }; /************* Utility Functions *************/ float2 get_corrected_texture_coordinate(float2 textureCoordinate) { #if FLIP_TEXTURE_Y return float2(textureCoordinate.x, 1.0 - textureCoordinate.y); #else return textureCoordinate; #endif } /************* Vertex Shader *************/ VS_OUTPUT vertex_shader(VS_INPUT IN) { VS_OUTPUT OUT = (VS_OUTPUT)0; OUT.Position = mul(IN.ObjectPosition, WorldViewProjection); OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate); return OUT; } /************* Pixel Shader *************/ float4 pixel_shader(VS_OUTPUT IN) : SV_Target { return ColorTexture.Sample(ColorSampler, IN.TextureCoordinate); } /************* Techniques *************/ technique10 main10 { pass p0 { SetVertexShader(CompileShader(vs_4_0, vertex_shader())); SetGeometryShader(NULL); SetPixelShader(CompileShader(ps_4_0, pixel_shader())); SetRasterizerState(DisableCulling); } }
有很多编程语言结构都支持注释和预处理。先来看看代码的注释,HLSL支持C++风格的单行注释(// 注释)和多行注释(/* 注释 */)方式。
再看看代码最上方的宏定义,#define FLIP_TEXTURE_Y 1。这种宏定义与C/C++的宏定义行为完成一样。实际上,HLSL也有一些很熟悉的预处理命令,包括#if,#else,#endif,以及#include。
接下来开始研究在cubbfer中声明的shader常量WorldViewProjection。该常量与HelloShaders和HelloStructs中的作用一样,但在常量声明的末尾增加了一个备注。这些备注以尖括号的形式括起来,用于CPU端的应用程序访问,不会影响shader的运行,但是应用程序可以使用他们。例如,WorldViewProjection中的UIWidget备注,可以控制NVIDIA FC Composer如何对待该shader常量。由于UIWidget赋值为None,在NVIDIA FX Composer的material properties列表中无法看到WorldViewProjection常量。如图5.3所示,左图中没有UIWidget = “None”的备注,可以在Properties panel中看到WorldViewProjection常量,右图中有了该备注,所以无法看到WorldViewProjection常量。需要注意的是,只是在Properties panel看不到该常量,CPU依然会更新。隐藏该常量的意义在于,不需要手动编辑WorldViewProjection矩阵。
图5.3 NVIDIA FX Composer Properties panel showing the properties for TextureMapping.fx without
the UIWidget="None" annotation on WorldViewProjection (left) and with the annotation (right).
在HLSL effect中使用一种纹理需要三个步骤。第一步,必须声明texture object(如列表5.2所示)。声明一个HLSL的纹理可以使用显示类型(比如Texture2D)或者更通用的texture数据类型。Texture objects不能使用cubffers类型声明。
列表5.2 The Texture Object Declaration from TextureMapping.fx
Texture2D ColorTexture < string ResourceName = "default_color.dds"; string UIName = "Color Texture"; string ResourceType = "2D"; >;
注意
在列表5.2中,ColorTexture变量包含了三个备注,虽然说所有的备注都是可选的,但是这些备注在NVIDIA FX Composer中却很有用。
通过UIName备注可以在Properties panel中自定义变量名。ResourceType备注指定了可以texture的类型,而ResourceName则支持在用户没有指定纹理的时候使用默认的纹理。
第二步,必须声明并初始化一个texture sampler(纹理采样)(如列表5.3所示)。Samplers控制如何从一个纹理中获取颜色值。Direct3D 10介绍了SamplerState数据类型,并直接映射到对应的Direct3D C结构体的成员中,用于filtering(过滤)和texture address modes(纹理寻址模式)。马上就会讨论这些主题。
列表5.3 The Sampler Object Declaration from TexureMapping.fx
SamplerState ColorSampler { Filter = MIN_MAG_MIP_LINEAR; AddressU = WRAP; AddressV = WRAP; };
最后一步,就是使用声明的sampler object来进行纹理采样。这一步是在pixel shader的执行的(如列表5.4)
列表 5.4 The Pixel Shader from TextureMapping.fx
float4 pixel_shader(VS_OUTPUT IN) : SV_Target { return ColorTexture.Sample(ColorSampler, IN.TextureCoordinate); }
调用ColorTexture object的函数Sample()与C++风格的成员函数调用类似。Sample()函数的第一个参数是sampler object,第二个参数是texture中对应的坐标。
纹理采样的坐标来自vertex数据流,并与VS_INPUT和VS_OUTPUT中成员保持一致(如列表5.5所示)。注意下与2D TextureCoordinate成员关联的TEXCOORD语义。
列表5.5 The Vertex Shader Input and Output Data Structures from TextureMapping.fx
struct VS_INPUT { float4 ObjectPosition : POSITION; float2 TextureCoordinate : TEXCOORD; }; struct VS_OUTPUT { float4 Position : SV_Position; float2 TextureCoordinate : TEXCOORD; };
如列表5.6所示,vertex shader把输入的纹理坐标用做输出,但要先调用get_corrected_texture_coordinate()以得到正确的纹理坐标。HLSL支持自定义的C风格的辅助函数get_corrected_texture_coordinate(),如果FLIP_TEXTURE_Y非0,该函数只是简单的反转垂直方向的纹理坐标。这是必须的,因为NVIDIA FX Composer针对内置的3D模型(Sphere,Teapot,Torus,Plane)使用OpenGL风格的纹理坐标。在DirectX中纹理坐标以左上角为起点,而在OpenGL中以左下角为起点。因此,在对NVIDIA FX Composer中内置的模型进行shaders显示时,需要翻转垂直方向的纹理坐标。如果导入了一个自定义的模型,并且该模型具有DirectX风格的纹理坐标,只需要禁用FLIP_TEXTURE_Y宏定义。
列表5.6 The Vertex Shader and a Utility Function from TextureMapping.fx
float2 get_corrected_texture_coordinate(float2 textureCoordinate) { #if FLIP_TEXTURE_Y return float2(textureCoordinate.x, 1.0 - textureCoordinate.y); #else return textureCoordinate; #endif } /************* Vertex Shader *************/ VS_OUTPUT vertex_shader(VS_INPUT IN) { VS_OUTPUT OUT = (VS_OUTPUT)0; OUT.Position = mul(IN.ObjectPosition, WorldViewProjection); OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate); return OUT; }
图5.4显示了把纹理映射effect应用于一个sphere的输出结果。(注意:需要在Properties panel中修改ColorTexture的纹理映射图片)
图5.4 TextureMapping.fx applied to a sphere with a texture of Earth. (Original texture from Reto
Stöckli, NASA Earth Observatory. Additional texturing by Nick Zuccarello, Florida Interactive Entertainment
Academy.)