本系列文章主要翻译和参考自《Real-Time 3D Rendering with DirectX and HLSL》一书(感谢原书作者),同时会加上一点个人理解和拓展,文章中如有错误,欢迎指正。
这里是书中的代码和资源。
纹理映射主要是指将贴图添加到3D模型表面的过程。这有点类似礼物包装,而你的包装纸就是一张2D的贴图。纹理映射是现代实时渲染技术的基础,并且可以用来实现很多很有意思的图形效果。
很多书或者文章中可能会把texture翻译成材质,在这里,我将保持unity Game Engine中的习惯,将texture统一翻译为纹理,material为材质。
本文索引:
一、 关于纹理映射的介绍
一般情况下,你应该会希望自己的模型拥有更多的细节,而不是像上一篇文章中的HelloShader和HelloStructs中输出的效果那样只有一个纯色。在前面的几篇文章中已经讲到过3D模型是由顶点组成的,而顶点间又会组成三角面片。这些顶点在最简单的情况下至少会描述这个点的坐标位置。当然也可能会描述一些其他信息,比如颜色,法线之类的。下图就是一个顶点上带有了位置和颜色信息的立方体。
左边的立方体中,每个顶点带有不同的颜色;而右边的立方体所有顶点都带有同样的颜色。显然,左边的立方体看起来细节要丰富的多。颜色使得立方体看起来更有趣并且也更易分辨出立方体的各个面。并且左边的立方体中每个像素的颜色与顶点位置相关。你的3D模型顶点数越多其表面的颜色细节就越丰富。然而,当摄像机视角拉近时,你将发现无论多少个顶点都无法达到高精度的以定义顶点颜色进行的渲染,同时,想要渲染出不同的颜色效果必然会考虑到顶点。由此,提出了纹理映射这个概念。
为了将纹理映射到三角面片上,你需要为每个顶点定义一个二维的纹理坐标。使用这个坐标你就可以找到纹理上坐标对应位置的颜色值。这个过程将会在像素着色器中执行,在该着色器中会为每个三角面片上的像素寻找其在纹理对应坐标位置上的颜色值。由此,就可以用三角面片顶点对应纹理坐标的方式取代单纯为顶点定义颜色这种渲染方式。
DirectX纹理坐标范围为[0,1],并且水平和垂直方向上都是这个范围,而原点则是图片的左上角。这两个轴一般被称为u(水平方向上)和v(垂直方向上)。下图展示了由两个三角形组成的四边形,并且高亮显示了其UV坐标。
注意 |
Direct3D支持一维、二维以及三维的纹理,并以纹理列和立方体纹理的形式提供。纹理坐标的维度取决于纹理的维度。 |
二、 一个实现纹理映射的effect
(1) Comment,the Preprocessor and Annotations:注释、预处理器以及注解
下面的代码段是一个实现了纹理映射的effect代码。首先需要在FX Composer下创建一个新的effect,然后将这些代码添加进去。
代码段Listing5.1 TextureMapping.fx
#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;
AddressU = WRAP;
};
/****************** Date 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);
}
}
在这个effect中可以看到多种语言的语法。
首先是shader中的注释。HLSL支持C语言风格的单行注释(//)也支持多行注释(/*注释内容*/)。
其次,在effect第一行中的宏命令#define FLIP_TEXTURE_Y 1,和C/C++中的使用方式是完全一样的。而HLSL中也确实包含很多预置的命令,包括#if,#else,#endif 以及 #include。
最后,我们再来看下在cbuffer中声明的WorldViewProjection常量。这个常量和在上一篇文章中声明的类似,但在其结尾增加了一个注解声明。注解是CPU端应用的注释,被括在一对尖括号当中。这个注释不会影响shader的执行,但是CPU中的应用会使用到。例如,关联到WorldViewProjection的UIWidget注解,将会控制FX Composer将如何处理这个shader常量。当被赋予None值时,NVIDIA FX Composer的属性面板中会将这个值从可见材质属性列表中移除。图5.3中显示了NVIDIA FX Composer的属性面板在两种情况下的显示结果。注意,即使这个值在属性面板中被隐藏了,他仍然会在CPU中更新。将这个值隐藏可以避免你对其做不必要的手动修改。
(2) Texture Objects and Samplers:纹理对象和采样器
在HLSL effect中要使用纹理涉及到三个步骤。
首先,你必须声明纹理对象(如下面的代码段中所示)。一个HLSL纹理的声明中可以指定具体的子类型(例如:Texture2D)或者其他更通用的纹理数据类型。纹理对象的声明不能包含在cbuffer中。
代码段Listing 5.2 TextureMapping.fx文件中纹理对象的声明
Texture2D ColorTexture
<
string ResourceName = "default_color.dds";
string UIName = "Color Texture";
string ResourceType = "2D";
>;
注意 |
在上面的代码段中有三个注解被关联到ColorTexture的声明中。所有的这些声明都是可选的,但在NVIDIA FX Composer这个编辑器环境下,这些注解会提升材质的使用效率。 |
UIName注解使你可以自定义显示在属性面板中纹理的名字。ResourceType定义了该纹理可以被赋值为 什么类型,而ResourceName允许当用户没有设置纹理时将会使用一个默认的纹理。 |
接下来,需要定义和初始化一个纹理采样器(参考代码段Listing5.3所示)。采样器控制如何从纹理中获取颜色值。Direct3D 10中引入了SamplerState数据类型,他直接映射了Direct3D中的C结构体,其成员变量包括filtering和texture address modes。
Listing5.3 TextureMapping.fx中的采样器
SamplerState ColorSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressU = WRAP;
};
最后,使用声明的采样器对象对纹理进行采样。这个步骤将在像素着色器中执行。(详见代码段Listing5.4)
代码段Listing 5.4 TextureMapping.fx文件中的像素着色器
float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
return ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
}
Sample函数的调用类似C++的对象成员函数引用。该函数的第一个参数是采样器对象,第二个参数是用来在纹理中查找颜色的纹理坐标。
(3) Texture Coordinates:纹理坐标
顶点流(vertex stream)中会提供在纹理上采样的坐标,而在shader中依靠VS_INPUT和VS_OUTPUT成员结构体进行顶点流数据传输。以下代码中注意观察新增加的变量TextureCoordinate及与其相对应的语义字符串TEXCOORD。
代码段Listing5.5 TextrueMapping.fx文件中的顶点着色器输入输出结构体定义
struct VS_INPUT
{
float4 ObjectPosition : POSITION;
float2 TextureCoordinate : TEXCOORD;
};
struct VS_OUTPUT
{
float4 Position : SV_POSITION;
float2 TextureCoordinate : TEXCOORD;
};
顶点着色器在调用了get_corrected_texture_coordinate()函数后将经过处理的纹理坐标传输到下一个渲染阶段。HLSL支持用户自定义的、C语言风格的辅助函数,而这里的这个协助函数主要在FLIP_TEXTURE_Y为非零值时将纹理坐标在垂直方向上进行翻转。这个步骤在NVIDIA FX Composer中是很必要的,因为这个软件使用OPENGL风格的纹理坐标来构建预置的这些3D模型(软件中提供的Sphere,Teapot,Torus以及平面)。OpenGL中纹理坐标的原点位于纹理的左下角,而DirectX中纹理坐标的原点位于纹理的左上角。因此,当你使用NVIDIA FX Composer软件中预置的几个模型时,你需要反转垂直方向的纹理坐标。如果你向
NVIDIA FX Composer中导入一个自定义的DirectX风格纹理坐标的模型,就可以不进行纹理的反转操作。
代码段Listing5.6 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
}
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;
}
(4) 效果查看
下图展示了这个effect的渲染效果
默认情况下不会显示这个贴图,需要在material的属性中对纹理贴图进行设置。
三、 纹理过滤
在我们讲解纹理坐标和采样的时候,你可能没有注意观察过纹理映射模型和通过一一对应将纹理上的元素渲染到屏幕上的区别。你的摄像机可能会以任意角度和距离来观察贴上了纹理的模型。试想一下,当你的摄像机逐渐靠近这个模型,光栅阶段将会决定模型的哪些像素会被传送到像素着色器中进行纹理坐标的插值。如果你的摄像机拍摄到的部分刚好位于没有纹理定义的像素上,也就是你正在试图渲染一个比原纹理分辨率更高的纹理贴图那应该如何处理?这部分文章的内容texture filtering纹理过滤决定了将赋予这些像素怎样的颜色值,这是因为这部分像素在本来的纹理贴图中已经不再能够找到对应的像素颜色了。
(1) Magnification 放大化
在需要将原模型放大查看的情况下,需要渲染的像素远比纹理已经提供的像素要多。Direct3D支持三种类型的过滤方式来决定这些中间像素的颜色:point filtering,linear interpolation,anisotropic filtering。
-
Point Filtering 点过滤
点过滤是所有过滤方式当中最快的一种,但是渲染的质量是最低的。也被称为近邻取样过滤(nearest-neighbor filtering),点过滤方式只是简单的使用最邻近像素中心的颜色来为当前像素赋值。下面左图所显示的就是点过滤方式生成的渲染结果,你可以看到像素以一块一块的方式渲染出来。
-
Linear Interpolation 线性过滤
线性过滤能够提供更好的渲染效果,当前像素点的颜色通过使用周围(包括垂直和水平方向上)带有纹理像素的点的颜色值进行插值计算得到。先用u方向上两个临近的像素进行插值再用v方向上两个临近的值进行插值从而计算出最终结果。图5.5右边显示的是经过线性插值计算出的结果,注意该图中显示效果要显得更平滑一些。 -
Anisotropic Filtering 各向异性过滤
各向异性用来处理当模型表面以十分倾斜的角度朝向摄像机而造成的纹理变形。各向异性过滤减少了变形并提升了渲染效果。这是使用代价最高的过滤方式,但所产生的效果是非常好的。下图展示了两种过滤方式的渲染效果区别。
(2) Minification 缩小化
缩小化是放大化的反面,用来处理当一个像素需要从原纹理的多个像素点中选取颜色的情况。这种情况通常发生在当摄像机离模型或者说纹理表面很远的情况下,这时Direct3D必须为这个像素选择一个合适的颜色值。缩小化与放大化有着同样的过滤方案选择,但相对来说,缩小化要更为复杂。比如使用线性插值方式的情况下,当前像素点的计算方式仍然是使用离他最近的周围四个纹理像素点来进行插值计算,但如果有更多的纹理像素点都与当前像素点关联,则都会被忽略,这种情况下的计算结果降低了渲染质量。
你可能已经想到可以用所有这些与当前像素点相关联的纹理像素点取平均值来计算,这样就可以采样任意规模的缩小化纹理值。但其实这种想法是不切实际的。然后,我们可以采用另一种替代方式,通过与计算技术mipmapping来实现。
(3) Mipmaps
mipmaps是原纹理不同版本的缩小版,一般会预先计算好并存储在同一个文件中。每个mip-level依次逐步除以2,直到变成1*1像素的纹理。下图展示了一张512*512像素的地球贴图拆分成9个mip-level后的结果。
当应用mipmap技术时,至少需要两个步骤来获取最终颜色。首先,需要为放大化或者缩小化选择mip-level。其次,通过选定的mip-level获取对应的颜色值。
点过滤或者线性过滤方式可以用来帮助选择mip-level。点过滤仅仅会选择最接近的mip-level,而线性过滤则会选择临近的两个mip-level。然后已选择的mip-level会通过点、线性、各向异性过滤法进行采样。如果使用的是线性过滤法则会对两个mip-level进行采样并将两个结果进行插值计算。这种技术将会获得最高的渲染效果。
最后,需要注意的是mipmap技术虽然提升了渲染质量,但也同时增加了近33%的内存占用率。
(4) SamplerState Filtering Options
让我们回顾一下TextureMapping.fx代码中的SamplerState对象(可参见下面的代码段),你可以看到这个对象中的Filter成员变量被赋予了MIN_MAG_MIP_LINEAR这个值。这个字符串设置采样器使用线性插值作为放大、缩小和mip-level的采样方式。Direct3D也允许为这些过程设置独立的值。Table5.1中提供这些变换的示例。你也可以在MSDN上的Direct3D文章中找到完整的列表。
Listing5.7 TextureMapping.fx中声明的SamplerState对象
SamplerState ColorSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressU = WRAP;
};
四、 Texture Addressing Modes:纹理寻址模式
你可能已经注意到在对SamplerState对象赋值的时候,还为下面这两个成员变量进行了赋值:AddressU = WARP 以及 AddressV = WRAP。这被称为寻址模式,用来控制当纹理坐标超出了[0, 1]范围时应该如何渲染这些像素点。Direct3D的寻址模式包括Wrap,Mirror,Clamp以及Border。
(1) Wrap 循环模式
在循环纹理寻址模式下,你的纹理会在低于0或高于1的地方不断进行重复。例如,为纹理坐标为(0.0, 0.0),(3.0, 0.0),(0.0, 3.0),(3.0, 3.0)的四边形进行纹理映射,结果显示贴图在每个轴向上重复了三次。如下图所示:
注意 |
每个重复的贴图周围的黑色边框不是在warping处理的过程中产生的。是为了分辨整数重复次数的边界。 |
(2) Mirror 镜像模式
镜像寻址模式类似于循环模式,只是镜像模式会以纹理所处的整形边界为镜像而不是简单的重复。下图显示了同样的纹理坐标下以镜像模式渲染的四边形。
(3) Clamp 拉伸模式
在拉伸模式下,纹理不会再被简单的重复平铺开来。整个纹理只会被使用一次,所有超出[0, 1]范围的像素都会以拉伸的形式从前一像素获取值。下图是使用拉伸模式渲染的模型,这个效果似的超出部分的贴图看起来好像被拖拉过。
注意 |
这里使用的纹理和之前两种模式使用的纹理不一样。这张纹理去掉了之前纹理的黑色边框,并进行了一些裁剪。如果继续使用之前那张纹理,那么超出部分的像素应该和边框一样都是黑色的。 |
(4) Border边框模式
和拉伸模式类似,边框寻址模式也只有使用一次纹理。但是在纹理的边缘部分并不会进行拖拉,边框颜色将会应用到超出纹理部分的像素中。如下图所示,这里的贴图也是使用最初我们使用的带黑色边框的那张,在边框模式下超出部分被渲染成了蓝色的边框色。
五、 扩展
(1) 更换纹理过滤方式查看效果
参考文章之前提供的表格,将纹理过滤方式做不同的修改,可以看到如下结果:
点过滤方式(MIN_MAG_MIP_POINT)与线性过滤方式(MIN_MAG_MAP_LINEAR)在将模式放到足够大之后看到的效果:
-
点过滤方式:
-
线性过滤方式:
对于模型表面倾斜角度大时各向异性过滤方式(ANISOTROPIC)与线性过滤方式(MIN_MAG_MAP_LINEAR)的渲染效果对比:
-
各向异性过滤方式:
-
线性过滤方式:
(2) 更换寻址方式查看效果
-
warp模式下的贴图显示:
-
mirror模式下的贴图显示:
-
clamp模式下的贴图显示:
-
border模式下的贴图显示:
六、 总结
在本文中,具体讲解了纹理映射的各个细节。了解了一个复杂纹理的映射效果并学习了更多的HLSL语法。也了解到了关于放大化、缩小化、mipmaping以及用于进行纹理采样的过滤处理过程。文章的最后,我们讲解了对于那些超过[0, 1]纹理坐标范围的像素点将如何进行处理。
纹理映射是学习现代3D渲染技术的基础,之后的文章内容将会基于前面的这几篇文章。
暂无评论内容