前言
纹理最初的目的就是使用一张图片来控制模型的外观。
在美术人员建模的时候,通常会在建模软件中利用纹理展开技术,把纹理映射坐标存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。
通常这些坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,而v是纵向坐标,因此纹理映射坐标也被称为UV坐标。
但顶点UV坐标的范围通常都被归一化到[0,1]范围内。纹理采样时使用的纹理坐标不一定是在[0,1]范围内。实际上,这种不在[0,1]范围内的纹理坐标又是会非常有用,与之关系紧密的是纹理的平铺模式,它将决定渲染引擎在遇到不在[0,1]范围内的纹理坐标时如何进行纹理采样。
单张纹理
我们通常会使用一张纹理来代替物体的漫反射颜色。
实践
我们使用Blinn-Phong光照模型来计算光照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
|
Shader "Unity Shaders Book/Chapter7/SingleTexture" { Properties { _Color ("Color Tint",Color) = (1,1,1,1) _MainTex ("Main Tex",2D) = "white"{} _Specular("Specular",Color) = (1,1,1,1) _Gloss("Gloss",Range(8.0,256)) = 20 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; o.uv = v.texcoord.xy * _MainTex_ST.xy+_MainTex_ST.zw; return o; } fixed4 frag(v2f i):SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir =normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo; fixed3 diffuse = _LightColor0.rgb * albedo*max(0,dot(worldNormal,worldLightDir)); fixed3 viewDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 halfDir = normalize(worldLightDir+viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal,halfDir)),_Gloss); return fixed4(ambient+diffuse+specular,1.0); }
ENDCG } } Fallback "Specular" }
|
Unity中纹理的基本知识点
纹理的基本设置
Wrap Mode
,它决定了当纹理坐标超过[0,1]范围后将会如何被平铺。Wrap Mode有两种模式:
一种是Repeat
,在这种模式下,如果纹理坐标超过了1,那么它的整数部分将会被舍弃,而直接使用小数部分进行采样,这样的结果是纹理将会不断重复。
另一种是Clamp
,在这种模式下,如果纹理坐标大于1,那么将会被截取到1,如果小于0,将会截取到0。
纹理缩放更加复杂的原因是在于我们往往需要处理抗锯齿
问题,一个方法就是使用多级渐远纹理技术(mipmapping)
。
多级渐远纹理技术将原纹理提前用滤波处理来得到很多更小的图像,形成一个图像金字塔,每一层都是对上一层图像降采样的结果。这样在实时运行时,就可以快速得到结果像素。
在Unity中,我们可以在纹理导入面板中,勾选Generate Mip Maps即可开启多级渐远纹理技术
,同时,我们还可以选择生成多级渐远纹理时是否使用线性空间(用于伽马校正)以及采用的滤波器等。
对于Filter Mode
(滤波模式)(假设同时使用多级渐远纹理技术)
Point模式使用了最近邻滤波,在放大或缩小时,它的采样像素数目通常只有一个,因此图像会看起来有种像素风的效果。
Bilinear滤波使用了线性滤波,对于每个目标像素,他会找4个临近数据,然后对他们进行线性插值混合后得到最终像素,因此图像看起来像被模糊了。
Trilinear滤波几乎和Bilinear一样,但是他还会在多级渐远纹理之间进行混合,如果一张纹理没有使用多级渐远纹理技术,那么Trilinear得到的结果和Bilinear就是一样的。
纹理的最大尺寸和纹理模式
如果导入纹理大小超过Max Texture Size中的设置值,那么Unity将会把该纹理缩放为这个最大分辨率,理想情况下,导入的纹理可以是正方形的,但长宽大小应该是2的幂
,否则(NPOT)将会占用更多的内存空间,而且GPU读取该纹理的速度也会有所下降。
Format决定了Unity内部使用哪种格式来存储该纹理。使用的纹理精度越高,占用的内存空间越大,但是得到的效果也越好(如果开启多级渐远技术也会增加纹理的内存占用)。所以对于一些不需要使用很高精度的纹理(例如用于漫反射颜色的纹理),我们应该尽量使用压缩格式。(Compression/Compressed)
凹凸映射
凹凸映射的目的是使用一张纹理来修改模型表面的法线
,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来高低不平,但是可以从模型的轮廓看出破绽。
两种方法使用凹凸映射
一:使用一张高度纹理
来模拟表面位移,然后得到一个修改后的法线值,这种方法也被称为高度映射
。
二:使用一张法线纹理
来直接存储表面法线,这种方法又被称为法线映射
。
高度纹理
高度图中存储的是强度值,它用于表示模型表面局部的海拔高度,因此,颜色越浅表明该位置的表面越向外凸起,而颜色越深,表明该位置越向里凹。优点是非常直观,缺点是计算更加复杂,在实时计算时不能直接得到表面法线,需要由像素灰度值计算而得,因此需要消耗更多性能
。
高度图通常会和发现映射一起使用,用于给出表面凹凸的额外信息,我们通常会使用法线映射来修改光照
。
法线纹理
法线纹理中存储的就是表面的法线方向。由于法线方向分量范围在[-1,1],而像素分量范围为[0,1],所以需要一个映射
pixel=2normal+1
这也意味着,我们在Shader中对法线纹理进行纹理采样后,还需要对结果进行一次反映射过程,来得到原先的法线方向。
normal=pixel×2−1
对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中——模型空间的法线纹理
然鹅,我们往往会选择另一种坐标空间——模型的切线空间
来存储法线。对于模型的每个顶点,它们都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向,x轴是顶点的切线方向,y轴可由法线和切线叉积而得,也被称为副切线或者副法线。
下面是切线空间示意图
这种纹理被称为切线空间的法线纹理
。
下面是模型空间法线纹理和切线空间法线纹理示意图
模型空间下的法线纹理看起来是“五颜六色的”,这是因为所有法线所在坐标空间都是同一个模型空间,即个点法线方向不同,经过映射后存储到纹理后对应颜色也不同。
切线空间下的法线纹理几乎都是蓝色的,因为每个法线所在坐标空间都是不同的切线空间,这种法线纹理存储了每个点在各自切线空间中法线扰动方向——z轴方向,如果一个点的法线方向不变,值为(0,0,1),所以映射过来都是蓝色。
这些蓝色部分表明法线纹理中的法线和模型本身法线一样,不需要改变。
模型空间法线纹理和切线空间法线纹理对比
模型空间法线纹理
- 实现简单,更加直观。
- 在纹理坐标的缝合处和尖锐的边角部分,可见突变(缝隙)少,可以提供平滑的边界。
- 记录的是绝对法线信息,仅可用于创建它的那个模型。
- 不可以进行UV动画
- 不可压缩,法线纹理每个方向都有可能,因此必须存储3个方向的值,不可压缩。
切线空间法线纹理
- 自由度高,可以用于不同的模型。
- 可以进行UV动画
- 可压缩,由于切线空间下的法线纹理中的法线的Z方向总是正方向,因此我们可以仅存储XY方向,然后推导Z方向。
实践
- 在切线空间下进行光照计算,效率比第二种方法高,因为可以在顶点着色器中就可以完成对光照方向和视角方向的变换。而第二种方法要先对法线纹理进行采样。所以变换过程必须在片元着色器计算,意味着一次矩阵操作。
- 在世界空间下进行光照计算,通用性比第一种方法高,有时需要在世界空间下进行一些计算。具体就是在世界空间计算光照模型,在片元着色器把法线方向从切线空间变换到世界空间下(在顶点着色器把这个矩阵传递给片元着色器),最后在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间即可。尽管这种方法需要更多的计算,但是在需要使用Cubemap进行环境映射等情况下,我们就需要使用这种方法。
在切线空间下进行计算
基本思路:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向,光照方向等进行计算。为此要现在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间,即模型空间到切线空间的变换矩阵。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
|
Shader "Unity Shaders Book/Chapter 7/Normal Map In TangentSpace" { Properties { _Color("Color Tint",Color) = (1,1,1,1) _MainTex("Main Tex",2D) = "White"{} _BumpMap("Normal Map",2D) = "bump"{} _BumpScale("Bump Scale",Float) = 1.0 _Specular("Specular",Color) = (1,1,1,1) _Gloss("Gloss",Range(8.0,256)) = 20 } SubShader { Pass { Tags{"LightMode"="ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "Lighting.cginc" #include "UnityCG.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; float3 lightDir : TEXCOORD1; float3 viewDir : TEXCOORD2; };
v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; TANGENT_SPACE_ROTATION; o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 tangentLightDir = normalize(i.lightDir); fixed3 tangentViewDir = normalize(i.viewDir); fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw); fixed3 tangentNormal; tangentNormal = UnpackNormal(packedNormal); tangentNormal.xy *= _BumpScale; tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir)); fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss); return fixed4(ambient + diffuse + specular,1.0); }
ENDCG } } Fallback "Specular" }
|
在世界空间下计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
|
Shader "Unlit/Chapter-NormalMapWorldSpace" { Properties { _Color("Color Tint",Color) = (1,1,1,1) _MainTex("Main Tex",2D) = "White"{} _BumpMap("Normal Map",2D) = "bump"{} _BumpScale("Bump Scale",Float) = 1.0 _Specular("Specular",Color) = (1,1,1,1) _Gloss("Gloss",Range(8.0,256)) = 20 } SubShader { Pass { Tags{"LightMode"="ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "Lighting.cginc" #include "UnityCG.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; float4 TtoW0 : TEXCOORD1; float4 TtoW1 : TEXCOORD2; float4 TtoW2 : TEXCOORD3; };
v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); fixed3 worldBinormal = cross(worldNormal,worldTangent)*v.tangent.w; o.TtoW0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x); o.TtoW1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y); o.TtoW2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z); return o; } fixed4 frag(v2f i) : SV_Target { float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos)); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.zw)); bump.xy *= _BumpScale; bump.z = sqrt(1.0 - saturate(dot(bump.xy,bump.xy))); bump = normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump)));
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(bump,lightDir)); fixed3 halfDir = normalize(lightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(bump,halfDir)),_Gloss); return fixed4(ambient + diffuse + specular,1.0); }
ENDCG } } Fallback "Specular" }
|
Unity中的法线纹理类型
当把法线纹理标识成Normal map,可以使用Unity的内置函数UnpackNormal来得到正确的法线方向。
当我们需要使用那些包含了法线映射的内置Unity Shader时,必须把使用的法线纹理按上面的方式标识成Normal map才能得到正确结果。
这可以让Unity根据不同平台对纹理进行压缩。
渐变纹理
一种常见用法就是使用渐变纹理来控制漫反射光照结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
Shader "Unity Shaders Book/Chapter 7/Ramp Texture" { Properties { _Color("Color Tint",Color) = (1,1,1,1) _RampTex("Ramp Tex",2D) = "White"{} _Specular("Specular",Color) = (1,1,1,1) _Gloss("Gloss",Range(8.0,256)) = 20 } SubShader { Tags{"LightMode"="ForwardBase"}
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color; sampler2D _RampTex; float4 _RampTex_ST; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; };
v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _RampTex); return o; }
fixed4 frag (v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed halfLambert = 0.5*dot(worldNormal,worldLightDir)+0.5;
fixed3 diffuseColor = tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb*_Color.rgb; fixed3 diffuse = _LightColor0.rgb * diffuseColor; fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 halfDir = normalize(worldLightDir+viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss); return fixed4(ambient + diffuse +specular,1.0); } ENDCG } } Fallback "Specular" }
|
需要注意,我们需要把渐变纹理的Wrap Mode设置为Clamp模式,防止对纹理进行采样时由于浮点精度而造成的问题。
使用Repeat模式会舍弃整数部分,只保留小数部分,有时会造成部分黑点。
换成Clamp模式即可解决这种问题。
遮罩纹理
遮罩允许我们可以保护某些区域,使他们免于修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
|
Shader "Unity Shaders Book/Chapter7/MaskTexture" { Properties { _Color("Color Tint",Color) = (1,1,1,1) _MainTex("Main Tex",2D) = "white"{} _BumpMap("Normal Map",2D) = "bump"{} _BumpScale("Bump Scale",Float) = 1.0 _SpecularMask("Specular Mask",2D) = "white"{} _SpecularScale("Specular Scale",Float) = 1.0 _Specular ("Specular",Color) = (1,1,1,1) _Gloss("Gloss",Range(8.0,256)) = 20 } SubShader { Tags{"LightMode"="ForwardBase"} Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color; sampler2D _MainTex; sampler2D _BumpMap; float _BumpScale; sampler2D _SpecularMask; float _SpecularScale; float4 _MainTex_ST; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 lightDir : TEXCOORD1; float3 viewDir : TEXCOORD2; };
v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; TANGENT_SPACE_ROTATION; o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; return o; }
fixed4 frag (v2f i) : SV_Target { fixed3 tangentLightDir = normalize(i.lightDir); fixed3 tangentViewDir = normalize(i.viewDir); fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap,i.uv)); tangentNormal.xy *= _BumpScale; tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir)); fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); fixed specularMask = tex2D(_SpecularMask,i.uv).r*_SpecularScale; fixed3 specular = _LightColor0.rgb * _Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss) * specularMask; return fixed4(ambient + diffuse + specular,1.0); } ENDCG } } Fallback "Specular" }
|
其他遮罩纹理
在真实的游戏制作过程中,遮罩纹理已经不止限于保护某些区域使他们免于某些修改,而是可以存储任何我们希望逐像素控制的表面属性。我们会充分利用一张纹理RGBA四个通道,用于存储不同属性。