Unity Shader 之 基础纹理
基础纹理
纹理的目的就是使用一张图片来控制模型的外观。使用纹理映射(texture mapping)技术,我们可以把一张图“粘”在模型表面,逐纹素(texel)地控制模型的颜色。
建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates)存储在每个顶点上。纹理映射坐标(UV坐标,u为横向坐标,v为纵向坐标)定义了该顶点在纹理中对应的2D坐标。
UV坐标通常都会被归一化到[0,1]范围内。而Unity中用的坐标为下图,与OpenGL中的坐标相同,而DirectX的坐标原点在左上角。
单张纹理
我们通常会用一张纹理来代替物体的漫反射颜色。
1 Shader "Unity My Shader/Diffuse Texture" 2 { 3 Properties 4 { 5 _Color("Color", Color) = (1,1,1,1) 6 _MainTex ("Main Tex", 2D) = "white" {} 7 _Specular ("Specular", Color) = (1,1,1,1) 8 _Gloss("Gloss", Range(8.0, 256)) = 20 9 } 10 SubShader 11 { 12 Pass 13 { 14 Tags{"LightMode"="ForwardBase"} 15 16 CGPROGRAM 17 #pragma vertex vert 18 #pragma fragment frag 19 20 #include "UnityCG.cginc" 21 #include "Lighting.cginc" 22 23 fixed4 _Color; 24 sampler2D _MainTex; 25 float4 _MainTex_ST; 26 fixed4 _Specular; 27 float _Gloss; 28 29 struct a2v 30 { 31 float4 vertex : POSITION; 32 float3 normal : NORMAL; 33 // 将模型的第一组纹理坐标存储到该变量中 34 float3 texcoord : TEXCOORD0; 35 }; 36 37 struct v2f 38 { 39 float4 pos : SV_POSITION; 40 float3 worldPos : TEXCOORD0; 41 float3 worldNormal : TEXCOORD1; 42 float2 uv : TEXCOORD2; 43 }; 44 45 v2f vert (a2v v) 46 { 47 v2f o; 48 49 o.pos = UnityObjectToClipPos(v.vertex); 50 // 模型坐标顶点转换世界坐标顶点 51 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 52 // 模型坐标法线转换世界坐标法线 53 o.worldNormal = UnityObjectToWorldNormal(v.normal); 54 // 对顶点纹理坐标进行变换,最终得到uv坐标。 55 // 方法原理 o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 56 //_MainTex_ST 是纹理的属性值,写法是固定的为 纹理名+_ST 57 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 58 59 return o; 60 } 61 62 fixed4 frag (v2f i) : SV_Target 63 { 64 // 法线方向 65 fixed3 worldNormal = normalize(i.worldNormal); 66 // 光照方向 67 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); 68 // 视角方向 69 fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); 70 // 对纹理进行采样,返回为计算得到的纹素值,与_Color的乘积作为反射率 71 fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; 72 // 环境光 73 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 74 // 漫反射 75 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); 76 // Blinn模型 计算 77 fixed3 halfDir = normalize(worldViewDir + worldLightDir); 78 // 高光反射 79 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); 80 // 相加后输出颜色 81 return fixed4(ambient + diffuse + specular, 1); 82 } 83 ENDCG 84 } 85 } 86 }
凹凸映射
目的:使用一张纹理来修改模型表面的法线,以便为模型提供更多细节。不会真的改变模型的顶点位置,只是让模型看起来好像“凹凸不平”。
实现凹凸映射的两种方法:高度映射(height mapping)和法线映射(normal mapping)
- 高度映射:使用高度纹理(height map)模拟表面位移(displacement),然后得到一个修改后的法线值。
- 法线映射:使用法线纹理(normal map)来直接存储表面法线。
高度纹理
高度图:存储强度值(intensity),表示模型表面局部的海拔高度。颜色越浅表明该位置的表明越向外凸起,越深越向内凹陷。
缺点:计算时不能直接得到表面法线,得同过灰度值计算得到,消耗性能。
法线纹理
法线纹理中存储的就是表面的法线方向。由于法线方向的分量范围在[-1,1],而像素的分量范围是[0,1],所以需要映射一下: pixel = (normal + 1) / 2
反之: normal = pixel * 2 - 1
效果如下:
不开启凹凸效果 开启凹凸效果(切线空间)
开启凹凸效果(世界空间)
Shader代码如下:
在切线空间下的凹凸纹理实现
1 Shader "Unity My Shader/aotu Texture" 2 { 3 Properties 4 { 5 _Color("Color", Color) = (1,1,1,1) 6 _MainTex ("Main Tex", 2D) = "white" {} 7 _BumpTex ("Bump Tex", 2D) = "Bump" {} 8 _BumpScale ("Bump Scale", Float) = 1 9 _Specular ("Specular", Color) = (1,1,1,1) 10 _Gloss("Gloss", Range(8.0, 256)) = 20 11 } 12 SubShader 13 { 14 Pass 15 { 16 Tags{"LightMode"="ForwardBase"} 17 18 CGPROGRAM 19 #pragma vertex vert 20 #pragma fragment frag 21 22 #include "UnityCG.cginc" 23 #include "Lighting.cginc" 24 25 fixed4 _Color; 26 sampler2D _MainTex; 27 float4 _MainTex_ST; 28 sampler2D _BumpTex; 29 float4 _BumpTex_ST; 30 float _BumpScale; 31 fixed4 _Specular; 32 float _Gloss; 33 34 struct a2v 35 { 36 float4 vertex : POSITION; 37 // 将模型的法线方向存储到变量中 38 float3 normal : NORMAL; 39 // 将模型的第一组纹理坐标存储到变量中 40 float3 texcoord : TEXCOORD0; 41 // 将模型的顶点切线方向存储到变量中,float4的原因是用w来决定切线空间的第三个坐标轴——福切线的方向性。 42 float4 tangent : TANGENT; 43 }; 44 45 struct v2f 46 { 47 float4 pos : SV_POSITION; 48 float3 lightDir : TEXCOORD0; 49 fixed3 viewDir : TEXCOORD1; 50 // 存储两个uv坐标 _MainTex 与 _BumpTex 51 float4 uv : TEXCOORD2; 52 }; 53 54 v2f vert (a2v v) 55 { 56 v2f o; 57 58 o.pos = UnityObjectToClipPos(v.vertex); 59 60 // 计算福切线方向 叉乘获得与垂直于法线和切线平面的福切线方向,v.tangent.w 用来抉择福切线的方向(因为有两个方向) 61 // 方法原理 float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w; 62 // 模型空间到切线空间的变换矩阵 63 // 方法原理 float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal); 64 TANGENT_SPACE_ROTATION; 65 66 // ObjSpaceLightDir(模型空间的光照方向),转换成切线空间的光照方向。 67 o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz; 68 // 同上,转换的是视角方向 69 o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz; 70 71 // 对顶点纹理坐标进行变换,最终得到uv坐标。 72 // 方法原理 o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 73 //_MainTex_ST 是纹理的属性值,写法是固定的为 纹理名+_ST 74 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); 75 o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpTex); 76 77 return o; 78 } 79 80 fixed4 frag (v2f i) : SV_Target 81 { 82 // 切线空间的光照方向 83 fixed3 tangentLightDir = normalize(i.lightDir); 84 // 切线空间的视角方向 85 fixed3 tangentViewDir = normalize(i.viewDir); 86 87 // 对法线纹理进行采样 88 fixed4 packedNormal = tex2D(_BumpTex, i.uv.zw); 89 // 转换映射 90 // 方法原理 tangentNormal.xy = (packedNormal.xy * 2 - 1); 91 fixed3 tangentNormal = UnpackNormal(packedNormal); 92 tangentNormal.xy *= _BumpScale; 93 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); 94 95 // 对纹理进行采样,返回为计算得到的纹素值,与_Color的乘积作为反射率 96 fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; 97 // 环境光 98 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 99 // 漫反射 100 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir)); 101 // Blinn模型 计算 102 fixed3 halfDir = normalize(tangentViewDir + tangentLightDir); 103 // 高光反射 104 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss); 105 // 相加后输出颜色 106 return fixed4(ambient + diffuse + specular, 1); 107 } 108 ENDCG 109 } 110 } 111 }
在世界空间下计算
1 _BumpTex ("Bump Tex", 2D) = "Bump" {} 2 _BumpScale ("Bump Scale", Float) = 1 3 _Specular ("Specular", Color) = (1,1,1,1) 4 _Gloss("Gloss", Range(8.0, 256)) = 20 5 } 6 SubShader 7 { 8 Pass 9 { 10 Tags{"LightMode"="ForwardBase"} 11 12 CGPROGRAM 13 #pragma vertex vert 14 #pragma fragment frag 15 16 #include "UnityCG.cginc" 17 #include "Lighting.cginc" 18 19 fixed4 _Color; 20 sampler2D _MainTex; 21 float4 _MainTex_ST; 22 sampler2D _BumpTex; 23 float4 _BumpTex_ST; 24 float _BumpScale; 25 fixed4 _Specular; 26 float _Gloss; 27 28 struct a2v 29 { 30 float4 vertex : POSITION; 31 // 将模型的法线方向存储到变量中 32 float3 normal : NORMAL; 33 // 将模型的第一组纹理坐标存储到变量中 34 float3 texcoord : TEXCOORD0; 35 // 将模型的顶点切线方向存储到变量中,float4的原因是用w来决定切线空间的第三个坐标轴——福切线的方向性。 36 float4 tangent : TANGENT; 37 }; 38 39 struct v2f 40 { 41 float4 pos : SV_POSITION; 42 float4 TtoW0 : TEXCOORD0; 43 float4 TtoW1 : TEXCOORD1; 44 float4 TtoW2 : TEXCOORD2; 45 float4 uv : TEXCOORD3; 46 }; 47 48 v2f vert (a2v v) 49 { 50 v2f o; 51 52 o.pos = UnityObjectToClipPos(v.vertex); 53 54 // 世界空间顶点坐标 55 float3 worldPos = mul(unity_ObjectToWorld, v.vertex); 56 // 世界空间法线方向 57 float3 worldNormal = UnityObjectToWorldNormal(v.normal); 58 // 世界空间切线方向 59 float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); 60 // 世界空间福切线方向 61 float3 worldBinormal = cross(worldTangent, worldNormal) * v.tangent.w; 62 63 // 对顶点纹理坐标进行变换,最终得到uv坐标。 64 // 方法原理 o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 65 //_MainTex_ST 是纹理的属性值,写法是固定的为 纹理名+_ST 66 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); 67 o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpTex); 68 69 // 类矩阵形式保存 70 o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); 71 o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); 72 o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); 73 74 return o; 75 } 76 77 fixed4 frag (v2f i) : SV_Target 78 { 79 // 构建世界空间的顶点坐标 80 float3 worldPos = fixed3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w); 81 // 世界空间的光照方向 82 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); 83 // 世界空间的视角方向 84 fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos)); 85 86 // 对法线纹理进行采样 87 fixed4 packedNormal = tex2D(_BumpTex, i.uv.zw); 88 // 转换映射 89 // 方法原理 tangentNormal.xy = (packedNormal.xy * 2 - 1); 90 fixed3 bump = UnpackNormal(packedNormal); 91 bump.xy *= _BumpScale; 92 bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy))); 93 // 把法线变换到世界空间下(法线原为切线空间),通过使用点乘操作来实现矩阵的每一行和法线相乘得到 94 bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); 95 96 // 对纹理进行采样,返回为计算得到的纹素值,与_Color的乘积作为反射率 97 fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; 98 // 环境光 99 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 100 // 漫反射 101 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, worldLightDir)); 102 // Blinn模型 计算 103 fixed3 halfDir = normalize(worldViewDir + worldLightDir); 104 // 高光反射 105 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss); 106 // 相加后输出颜色 107 return fixed4(ambient + diffuse + specular, 1); 108 } 109 ENDCG 110 } 111 } 112 }
遮罩纹理
作用:遮罩允许我们可以保护某些区域,使它们免于某些修改
流程:采样得到遮罩纹理的纹素值,使用其中摸个(或某几个)通道的值来与某种表面的属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该属性影响
漫反射+高光反射+凹凸纹理+遮罩
Shader如下
1 Shader "Unity My Shader/aotu Texture" 2 { 3 Properties 4 { 5 _Color("Color", Color) = (1,1,1,1) 6 _MainTex ("Main Tex", 2D) = "white" {} 7 _BumpTex ("Bump Tex", 2D) = "Bump" {} 8 _BumpScale ("Bump Scale", Float) = 1 9 _SpecularMask ("Specular Mask Tex", 2D) = "white" {} 10 _SpecularScale ("Specular Scale", Float) = 1 11 _Specular ("Specular", Color) = (1,1,1,1) 12 _Gloss("Gloss", Range(8.0, 256)) = 20 13 } 14 SubShader 15 { 16 Pass 17 { 18 Tags{"LightMode"="ForwardBase"} 19 20 CGPROGRAM 21 #pragma vertex vert 22 #pragma fragment frag 23 24 #include "UnityCG.cginc" 25 #include "Lighting.cginc" 26 27 fixed4 _Color; 28 sampler2D _MainTex; 29 float4 _MainTex_ST; 30 sampler2D _BumpTex; 31 float4 _BumpTex_ST; 32 float _BumpScale; 33 sampler2D _SpecularMask; 34 float4 _SpecularMask_ST; 35 float _SpecularScale; 36 fixed4 _Specular; 37 float _Gloss; 38 39 struct a2v 40 { 41 float4 vertex : POSITION; 42 // 将模型的法线方向存储到变量中 43 float3 normal : NORMAL; 44 // 将模型的第一组纹理坐标存储到变量中 45 float3 texcoord : TEXCOORD0; 46 // 将模型的顶点切线方向存储到变量中,float4的原因是用w来决定切线空间的第三个坐标轴——福切线的方向性。 47 float4 tangent : TANGENT; 48 }; 49 50 struct v2f 51 { 52 float4 pos : SV_POSITION; 53 float2 uv : TEXCOORD0; 54 float3 lightDir : TEXCOORD1; 55 float3 viewDir : TEXCOORD2; 56 }; 57 58 v2f vert (a2v v) 59 { 60 v2f o; 61 62 o.pos = UnityObjectToClipPos(v.vertex); 63 64 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); 65 66 TANGENT_SPACE_ROTATION; 67 68 o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); 69 o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)); 70 71 return o; 72 } 73 74 fixed4 frag (v2f i) : SV_Target 75 { 76 // 世界空间的光照方向 77 fixed3 tangentLightDir = normalize(i.lightDir); 78 // 世界空间的视角方向 79 fixed3 tangentViewDir = normalize(i.viewDir); 80 81 // 对法线纹理进行采样 82 fixed4 packedNormal = tex2D(_BumpTex, i.uv); 83 fixed3 tangentNormal = UnpackNormal(packedNormal); 84 tangentNormal.xy *= _BumpScale; 85 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); 86 87 // 对纹理进行采样,返回为计算得到的纹素值,与_Color的乘积作为反射率 88 fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; 89 // 环境光 90 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 91 // 漫反射 92 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir)); 93 // Blinn模型 计算 94 fixed3 halfDir = normalize(tangentViewDir + tangentLightDir); 95 // 对高光反射的r通道计算掩码值 96 fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale; 97 // 高光反射 添加遮罩 98 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask; 99 // 相加后输出颜色 100 return fixed4(ambient + diffuse + specular, 1); 101 } 102 ENDCG 103 } 104 } 105 }
(完)