技术美术——基础纹理
常规纹理
纹理的一种使用方式就是作为常规纹理,可以理解为一张照片。在这里纹理的作用是代替 物体的漫反射系数 ,这里我们会再之前的 Blinn-Phong 高光反射 shader 的基础上实现一个基础的纹理 shader。这里可以知道常规纹理会参与 环境光 ,物体 漫反射 的计算。在基本光照模型中,由于没有相关材质贴图,所以在计算公式中
C = (c * m) * max(0 , n * l )
m 的值取为 1,不会影响到漫反射光的计算。当 m 值存在时,也就是有常规纹理的时候,光照计算中的 m 就要从常规纹理图中读取,读取方法见下面 albedo 的计算方法。
Shader "Unlit/MainTex0"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
_SpecularCol("Specular Col", Color) = (1,1,1,1)
_SpecularStrength("Specular Strength", Range(8.0, 256))=10
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
half4 _SpecularCol;
float _SpecularStrength;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 posWorld : SV_POSITION;
float2 uv : TEXCOORD1;
float3 worldNormal : NORMAL;
float3 worldPos : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.posWorld = UnityObjectToClipPos(v.vertex); // 顶点位置从模型空间转到裁剪空间
o.worldNormal = UnityObjectToWorldDir(v.normal); // 顶点法线从模型空间转到世界空间
o.worldPos = UnityObjectToWorldDir(v.vertex); // 顶点从模型空间转到世界空间
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 使用纹理属性的_ST对顶点纹理坐标进行变换
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half3 worldNormal = normalize(i.worldNormal); // 顶点法线方向
half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // 主光源放方向
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos); // 视角方向
half3 halfDir = normalize(viewDir + worldLightDir); // 半角方向
half3 NdotL = max(0.0, dot(worldNormal, worldLightDir)); // Lambert
half3 NdotV = max(0.0, dot(worldNormal, halfDir)); // BlinnPhong
half3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; // 漫反射系数,从纹理采样漫反射颜色作为其值
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光
// 漫反射光 = 入射光线强度 * 材质的漫反射系数 * 取值为正数(表面法线方向 · 光源方向)
half3 diffuse = _LightColor0.rgb * albedo * pow(NdotL * 0.5 + 0.5, 2.0); // half lambert
half3 specular = _LightColor0.rgb * _SpecularCol.rgb * pow(NdotV, _SpecularStrength); // blinn phong
return fixed4(diffuse + specular + ambient, 1);
}
ENDCG
}
}
}
凹凸映射
凹凸映射的作用就是使用一张纹理来修改模型表面的法线,提供更多的细节效果。主要有两种方法:
高度映射
一张高度纹理来模拟表面位移,然后得到一个修改后的法线值。
法线映射
一张法线纹理来直接存储表面法线。
通常我们将高度映射和法线映射当成一种技术,但是两者的本质是有所区别的。
法线纹理
法线纹理中存储的是表面的法线方向,由于发现分量的取值范围在 [-1, 1] 之间,而像素的分量范围在 [0, 1] ,因此两者的转换还需要做一个映射:
pixel = (normal + 1) / 2
反映射后就能得到原先的法线方向
normal = pixel * 2 - 1
法线的坐标空间
模型空间的法线纹理
模型顶点的法线定义在模型空间中,一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称之为 模型空间的法线纹理。
切线空间的法线纹理
实际应用中,我们常用模型顶点的 切线空间 存储法线。对于模型的每个顶点,都有属于自己的切线空间,模型的顶点就是切线空间的原点,顶点的法线作切线空间的 z 轴 (n),顶点的切线方向作 x 轴 (t),顶点的 y 轴可由 x、z 叉乘求得,也被称为 副切线 或 副法线 (b),这就是 切线空间的法线纹理。
计算方式
由于法线纹理中存储的是切线空间下的方向,因此我们通常有两种选择:
- 在切线空间下进行光照计算,需要将光照方向、视角方向变换到切线空间下。
- 在世界空间下进行光照计算,需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。
效率上考虑,第一种优于第二种,因为整个空间的变换过程在顶点着色器中完成。
通用性考虑,第二种优于第一种,因为有时需要在世界空间进行一次矩阵操作。
切线空间计算
思路:1. 在顶点着色器里将模型空间下的光源方向、视角方向转到切线空间;2. 在片元着色器里直接对法线纹理进行采样,得到切线空间下的法线数值;3. 进行光照计算。
Shader "Unlit/MainBumpT"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_BumpTex("BumpTex",2D) = "white" {}
_BumpStrength("Bump Strength", Float) = 1.0
_Color("Color Tint", Color) = (1,1,1,1)
_Specular("Specular Color", Color) = (1,1,1,1)
_SpecularStrength("Specular Strength", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" "LightModel"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
half _BumpStrength;
half4 _Color;
half4 _Specular;
half _SpecularStrength;
struct appdata
{
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
float3 normal : NORMAL;
};
struct v2f
{
float4 uv : TEXCOORD0; // flaot4 因为使用了两张纹理贴图,所以成 flaot4 了
float4 posWorld : SV_POSITION;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.posWorld = UnityObjectToClipPos(v.vertex); // 顶点坐标从模型空间转到裁剪空间
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // MainTex 的 Tiling 和 Offset
o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw; // BumpTex 的 Tiling 和 offset
float3 binnormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w; // 得到模型空间下的副切线(.w 是取一个方向作为切线方向)
float3x3 rotation = float3x3(v.tangent.xyz, binnormal, v.normal); // 一个切线空间 (顶点切线方向作为 x,叉乘值作为 y, 顶点法线作为 z)
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); // 模型空间下的光照方向转到切线空间
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)); // 模型空间下的视角方向转到切线空间
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half3 tangentLightDir = normalize(i.lightDir); // 归一化光照方向
half3 tangentViewDir = normalize(i.viewDir); // 归一化视角方向
half3 halfDir = normalize(tangentLightDir + tangentViewDir); // 归一化半角方向
half4 packNormal = tex2D(_BumpTex,i.uv.zw); // 对法线贴图进行采样
half3 tangentNormal;
tangentNormal=UnpackNormal(packNormal);
tangentNormal.xy*=_BumpStrength; // 调整法线缩放,控制凹凸强度
tangentNormal.z = sqrt(1.0 - max(0.0, dot(tangentNormal.xy, tangentNormal.xy)));
half3 NdotL = max(0.0, dot(tangentNormal, tangentLightDir)); // lambert
half3 RdotL = max(0.0, dot(tangentNormal, halfDir)); // blinn phong
half3 albedo = tex2D(_MainTex,i.uv) * _Color.rgb; // 漫反射系数(从贴图中读取)
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光
half3 diffuse = _LightColor0.rgb * albedo * pow(NdotL*0.5 +0.5, 2.0); // half lambert
half3 specular = _LightColor0.rgb * _Specular.rgb * pow(RdotL, _SpecularStrength);
return half4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
}
世界空间计算
思路:1. 在顶点着色器里将模型空间下的顶点位置、顶点法线、顶点切线、顶点副切线转到世界空间,并创建法线从切线空间转世界空间的 矩阵,用于后面采样法线纹理将法线从切线空间转到世界空间;2. 计算世界空间下的光照方向、视角方向,对法线纹理进行采样,并且将其从切线空间转到世界空间;3. 进行光照计算。
Shader "Unlit/MainBumpW"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_BumpTex("BumpTex", 2D) = "white" {}
_BumpScale("Bump Scale", Float) = 1.0
_Color("Color", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_SpecularPower("Specular Power", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"= "ForwardBase"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float4 _Color;
float4 _Specular;
float _BumpScale;
float _SpecularPower;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 uv : TEXCOORD0;
float4 posWorld : SV_POSITION;
float4 TtoW0:TEXCOORD1;
float4 TtoW1:TEXCOORD2;
float4 TtoW2:TEXCOORD3;
};
v2f vert (appdata v)
{
v2f o;
o.posWorld = UnityObjectToClipPos(v.vertex); // 顶点坐标从模型空间转到裁剪空间
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // _MainTex 缩放平移属性
o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw; // _BumpTex 缩放平移属性
half3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点位置从模型空间转到世界空间
half3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线从模型空间转到世界空间
half3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); // 顶点切线从模型空间转到世界空间
half3 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
{
half3 worldPos = half3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
half3 lightDir = UnityWorldSpaceLightDir(worldPos);
half3 viewDir = UnityWorldSpaceViewDir(worldPos);
half3 halfDir = normalize(lightDir + viewDir);
half3 bump = UnpackNormal(tex2D(_BumpTex, i.uv.zw)); // 从法线贴出中读取法线信息
bump.xy*=_BumpScale; // 缩放法线贴图
bump.z = sqrt(1.0 - dot(bump.xy, bump.xy));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));// 切线空间转世界空间
half NdotL = max(0.0, dot(bump, lightDir)); // lambert
half NdotV = max(0.0, dot(bump, halfDir)); // blinn phong
half3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; // 读取漫反射系数
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光
half3 diffuse = _LightColor0.rgb * albedo * pow(NdotL * 0.5 + 0.5, 2.0);
half3 specular = _LightColor0.rgb * _Specular.rgb * pow(NdotV, _SpecularPower);
return half4(diffuse + specular + ambient, 1.0);
}
ENDCG
}
}
}
渐变纹理
如常规纹理一样,我们在渲染中使用纹理是为了定义一个物体的颜色。纹理其实可以用于存储任何表面属性,一种常见的作法就是使用纹理渐变来控制漫反射光照结果。其核心部分是纹理采样计算部分,使用了 Half Lambert 作为采样UV。
注意将纹理的 Wrap mode 设为 Clamp
Shader "Unlit/gradient"
{
Properties
{
_RampTex ("Ramp Texture", 2D) = "white" {}
_Color("Color Tint", Color) = (1,1,1,1)
_Specular("Specular Color", Color) = (1,1,1,1)
_SpecularPower("Specular Power", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" "LightModel"="ForwardBase" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _RampTex;
float4 _RampTex_ST;
float4 _Color;
float4 _Specular;
Float _SpecularPower;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 posWorld : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
float2 uv : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
o.posWorld = UnityObjectToClipPos(v.vertex); // 顶点坐标从模型空间转到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal); // 法线从模型空间转到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 位置从模型空间转到世界空间
o.uv = v.texcoord.xy * _RampTex_ST.xy + _RampTex_ST.zw; // UV 的缩放和平移属性
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half3 worldNormal = normalize(i.worldNormal); // 归一化法线方向
half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 归一化主光源方向
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // 归一化视角方向
half3 halfDir = normalize(worldLightDir + worldViewDir); // 归一化半角方向
half NdotL = max(0.0, dot(worldNormal, worldLightDir) * 0.5 + 0.5); // lambert
half NdotV = max(0.0, dot(worldNormal, halfDir));
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // 环境光
half3 diffuseColor = tex2D(_RampTex, half2(NdotL, NdotL)).rgb * _Color.rgb; // 反射颜色 = 使用渐变纹理来采样半兰伯特光照生成的纹理
half3 diffuse = _LightColor0.rgb * diffuseColor;
half3 specular = _LightColor0.rgb * _Specular.rgb * pow(NdotV, _SpecularPower);
return half4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
遮罩纹理
遮罩纹理可以让我们更加自由的控制模型表面的性质。遮罩纹理的使用一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该属性的影响。这里样例使用的高光的 mask ,并且是纹理的 r 通道。
Shader "Unlit/mask"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
_BumpTex("BumpTex", 2D) = "whire" {}
_BumpScale("BumptScale",Float) = 1.0
_SpecularMask("Specular Mask", 2D) = "white" {}
_SpecularScale("Specular Scale", Float) = 1.0
_Specular("Specualr", Color) = (1,1,1,1)
_SpecularPower("Specular Power", Range(8.0, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" "LightModel"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularScale;
float4 _Specular;
float _SpecularPower;
struct appdata
{
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float4 posWorld : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.posWorld = UnityObjectToClipPos(v.vertex); // 顶点坐标从模型空间转世界空间
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // _MainTex 的缩放和平移
o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw; // _BumpTex 的缩放和平移
float3 binormal = cross(normalize(v.normal), normalize(v.tangent)) * v.tangent.w; // 副切线
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal); // 变换矩阵
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz; // 主光源方向从模型空间转到切线空间
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz; // 视角方向从模型空间转到切线空间
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half3 tangentLightDir = normalize(i.lightDir); // 切线空间下主光源方向
half3 tangentViewDir = normalize(i.viewDir); // 切线空间下的视角方向
half3 halfDir = normalize(tangentLightDir + tangentViewDir); // 半角方向
half3 tangentNormal = UnpackNormal(tex2D(_BumpTex, i.uv.zw)); // 从法线贴图读取法线信息
tangentNormal.xy *= _BumpScale; // 缩放法线
tangentNormal.z = sqrt(1.0 - max(0.0, dot(tangentNormal.xy, tangentNormal.xy))); // 计算法线的 z 值
half NdotL = max(0.0, dot(tangentNormal, tangentLightDir)); // lambert
half NdotV = max(0.0, dot(tangentNormal, halfDir)); // blinn-phong
half3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb; // 从材质贴图计算漫反射率
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光
half3 diffuse = _LightColor0.rgb * albedo * (NdotL * 0.5 +0.5); // 计算漫反射光
half specularMask = tex2D(_SpecularMask, i.uv).r * _Specular.rgb; // 从高光贴图中读取高光反射值
half3 specular = _LightColor0.rgb * _Specular.rgb * specularMask * pow(NdotV, _SpecularPower);
return half4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
}
拓展 :Mask 的应用还有很大的空间,可以试着举例出来:UV 顶点动画。
总结
漫反射系数 | 法线系数 | |
---|---|---|
无纹理 | 1 | 模型法线 |
常规纹理 | tex2D(_MainTex,i.uv).rgb * _Color.rgb | 模型法线 |
法线纹理 | tex2D(_MainTex,i.uv) * _Color.rgb | tex2D(_BumpTex,i.uv.zw) |
渐变纹理 | tex2D(_RampTex, half2(NdotL, NdotL)).rgb * _Color.rgb | 模型法线 |
遮罩纹理 | tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb | (tex2D(_BumpTex, i.uv.zw) |