UnityShader-基础纹理 纹理映射(texture mapping):使用一张图片来控制模型外观
纹理映射坐标(texture-mapping):定义了该顶点在纹理中对应的2D坐标,通常使用二维变量(u, v)表示,也被称为UV坐标,范围被归一化到[0, 1]
纹理采样时使用的纹理坐标不一定在[0, 1]范围内,会根据平铺模式决定如何采样
在Unity中,纹理空间符合OpenGL传统,原点位于纹理左下角
纹理的处理过程可以用以下图片内容描述:
单张纹理 Shader实现:
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 Shader "AABB/Single Texture" { Properties { _Color ("Color Tint", Color) = (1 , 1 , 1 , 1 ) _MainTex ("Main Tex", 2 D) = "white" {} _Specular ("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss ("Gloss", Range(8.0 , 256 )) = 20 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #include "UnityCG.cginc" #include "Lighting.cginc" #pragma vertex vert #pragma fragment frag 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 = TRANSFORM_TEX(v.texcoord, _MainTex); 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 * saturate(dot (worldNormal, worldLightDir)); 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" }
纹理的属性设置:
Alpha from Grayscale:勾选后,透明通道的值将会由每个像素的灰度值生成
Wrap Mode:决定纹理坐标超过[0, 1]后将会如何被平铺。有两种模式:
Repeat:如果纹理坐标超过1,则整数部分被舍弃,直接使用小数部分进行采样,结果是纹理不断重复
Clamp:如果纹理大于1,则被截取到1,如果小于0,则被截取到0
Filter Mode:决定纹理由于变换而产生拉伸时将会采用哪种滤波模式。支持3种模式:Point, Bilinear, Trilinear。得到的图片滤波效果依次提升,但是耗费的性能也会增加
Point:使用最近邻(nearest neighbor)滤波,在放大或者缩小时,采样像素数目只有一个
Bilinear:线性滤波,会找到4个邻近像素进行插值混合,通常选择该模式
Trilinear:会在多级渐远纹理之间进行混合,单张纹理下和Bilinear效果相同
多级渐进纹理(mipmapping): 将原纹理提前用滤波处理来得到很多更小的图像,得到多层较小纹理,当物体远离摄像机时,直接使用较小纹理进行计算。在Unity中通过勾选Generate Mip Maps来实现
注:这里我的理解是,假设原来拥有128x128大小的纹理和平面,两者刚好贴合,当平面因为远离摄像机而面积变小后(经过了透视除法,x, y,z的坐标值被除以w),经过屏幕映射而包围的像素数目减少,假设为64x64像素大小,此时采集128x128大小的纹理时,中间会间隔一个像素无法被采集到,因此容易出现马赛克或者锯齿,丢失细节(在LearnOpenGL中说还会导致内存浪费,我暂时不能理解),因此我们可以提前将纹理以合适的方式处理好(而不一定是默认插值方式),并生成对应的小纹理,这样采样时就能够控制细节。
纹理最大尺寸(Max Size):如果导入纹理超过设置的最大尺寸,则会将纹理缩放为该最大分辨率。导入纹理大小最好时2的幂,有利于存储
纹理模式(Format):影响纹理的精度和占用内存
凹凸映射(bump mapping) 目的:使用一张纹理来修改模型表面的法线,以提供更多的细节,让模型看起来凹凸不平
主要实现方法分为高度纹理(height map)和法线纹理(normal map)
高度纹理 高度纹理使用一张高度图来实现凹凸映射,高度图中存储强度值,用于表示模型表面局部的海拔高度
缺点:计算复杂,不能直接得到表面法线,需要由灰度值计算获得
法线纹理 存储表面的法线方向,法线和像素之间的映射关系为: $$ pixel = \frac{normal + 1}{2} $$ 法线纹理按照坐标系的不同分为**模型空间下的法线纹理(object-space normal map)和 切线空间下的法线纹理(tangent-space normal map)**。一般选择后者,因为切线空间下的法线纹理记录的是相对法线信息,能够适应完全不同的网格。
切线空间:
原点为顶点,z轴是顶点的法线方向,x轴是切线方向,y轴是副切线方向
在切线空间下计算:
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 Shader "Normal Map In Tangent Space" { Properties { _Color("Color Tint", Color) = (1 , 1 , 1 , 1 ) _MainTex("Main Tex", 2 D) = "white" {} _BumpMap("Normal Map", 2 D) = "bump" {} _BumpScale("Bump Scale", Float) = 1.0 _Specular("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss("Gloss", Range(8.0 , 256 )) = 20.0 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #include "Lighting.cginc" #pragma vertex vert #pragma fragment frag 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 * saturate(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 Shader "Normal Map In World Space" { Properties { _Color("Color Tint", Color) = (1 , 1 , 1 , 1 ) _MainTex("Main Tex", 2 D) = "white" {} _BumpMap("Normal Map", 2 D) = "bump" {} _BumpScale("Bump Scale", Float) = 1.0 _Specular("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss("Gloss", Range(8.0 , 256 )) = 20.0 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #include "Lighting.cginc" #pragma vertex vert #pragma fragment frag 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在某些平台上使用了DXT5nm的压缩格式,因此需要针对这种格式进行解码,在DXT5nm格式的法线纹理中,纹素的a通道对应法线的x分量,g通道对应法线的y分量,r和b通道被舍弃,法线的z分量由xy推导而得 ,这样可以减少法线纹理占用的内存空间
Create from Grayscale:用于从高度图中生成法线纹理,设置后可以调整Bumpiness和Filtering来控制凹凸程度和计算方式
渐变纹理(ramp texture) 可以用于控制漫反射光照效果,生成类似于卡通风格的图像
实现代码:
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 Shader "Ramp Texture" { Properties { _Color("Color Tint", Color) = (1 , 1 , 1 , 1 ) _RampTex("Ramp Tex", 2 D) = "white" {} _Specular("Specular", Color) = (1 , 1 , 1 , 1 ) _Gloss("Gloss", Range(8.0 , 256 )) = 20 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #include "Lighting.cginc" #pragma vertex vert #pragma fragment frag 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模式,以防止对纹理进行采样时由于浮点数精度造成的问题
遮罩纹理(mask texture) 遮罩允许我们可以保护某些区域,使它们免于某些修改。
用于控制某些区域的反光强度,得到更加细腻的效果
控制多种纹理的混合
流程:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值来于某种表面属性相乘,可以保证像素级别地控制模型表面的各种性质
实现代码:
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 Shader "Mask Texture" { Properties { _Color ("Color Tint", Color) = (1 , 1 , 1 , 1 ) _MainTex ("Main Tex", 2 D) = "white" {} _BumpMap ("Normal Map", 2 D) = "bump" {} _BumpScale("Bump Scale", Float) = 1.0 _SpecularMask ("Specular Mask", 2 D) = "white" {} _SpecularScale ("Specular 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" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float _BumpScale; sampler2D _SpecularMask; float _SpecularScale; 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 * 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" }