Shader "Unity Shader Books/Chapter 7/Single Texture"
{
Properties
{
_Color ("Color Tint",Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20
}
SubShader
{
Pass
{
Tags {"LightrMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;//其中存储了纹理的缩放和偏移值,分别可以通过.xy和.zw得到
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;//使用tex2D函数对纹理采样,返回纹素值,乘以颜色属性作为材质反射率albedo
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;//将albedo和环境光照相乘得到环境光
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));//使用albedo计算漫反射结果
//计算高光反射
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"
}
完成上面代码的编写并赋给场景中准备好的胶囊体,并准备一张普通的jpg砖墙图片赋值给 Texture 属性,我们就可以看到其呈现的效果:
对于我们选择的纹理(jpg图片),我们可以在 Inspector 窗口中检视它的属性:
凹凸映射是纹理的另一种常见应用。他的目的是使用一张纹理来修改模型表面的法线,丰富模型视觉上的效果但不改变顶点的实际位置
主要有两种方法来进行凹凸映射:一种是高度映射,一种是法线映射
高度映射的方式是使用一张高度纹理(height map)来模拟表面位移,然后得到一个修改后的法线值。 height map 中存储的是强度值 intensity ,用于表示模型表面局部的海拔高度:
就如上面这张 height map 所示,其颜色越浅表明模型越向外凸起,颜色越深表明模型越向内凹陷
使用这种方法好处是可以直观的知道模型表面的凹凸情况,但这一方式计算复杂,实时计算时不能直接得到表面法线,需要由像素的灰度值计算而得,会消耗更多的性能
高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息
法线映射的方式是使用一张法线纹理(normal map)直接存储表面法线
由于法线方向的分量范围是[-1,1],像素的分量范围是[0,1],因此我们需要做一个映射,通常为:
因此我们在 shader 中进行纹理采样后要得到法线方向需要做相应的反映射:
除此以外,我们还需要考虑法线纹理中存储法线方向是在哪个坐标空间中的。这里我们可以采用两种坐标空间:一种是模型自身的模型空间,另一种是模型顶点的切线空间,因此也有了对应的纹理称谓:模型空间的法线纹理和切线空间的法线纹理:
(由于选取了不同空间在映射后对应了不同的RGB值,同一张法线纹理呈现不同的颜色)
我们往往会使用切线空间下的法线纹理(因为优点很多)
在开始编写前,还有一些我们需要注意的地方:我们需要在计算光照模型时统一各个方向矢量所在的坐标空间。通常我们有两种选择,一种是在切线空间下进行光照关照计算,此时我们需要将光照方向和视角方向转换到切线空间下;一种是在世界空间下进行光照计算,此时需要将纹理采样得到的法线方向转换到世界空间下
下分别实现两种方法
Shader "Unity Shaders Book/Chapter7/Normal Map In Tangent Space"
{
Properties
{
_Color("Color Tint",Color) = (1,1,1,1)
_MainTex("Texture", 2D) = "white" {}
_BumpMap("Normal map",2D) = "bump" {} //法线纹理属性,使用内置的法线纹理"bump"
_BumpScale("Bump Scale",Float) = 1.0 //用于控制凹凸程度,为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;
float4 _MainTex_ST;//其中存储了纹理的缩放和偏移值,分别可以通过.xy和.zw得到
sampler2D _BumpMap;
float4 _BumpMap_ST;//和_MainTex_ST类似,用于获得纹理的属性(平铺和偏移系数)
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT; //tangent.w决定切线空间中第三个坐标轴 ———— 副切线的方向性
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);
//在uv中存储使用的两个纹理坐标
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 binarmal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
//建立将向量从模型空间变换到切线空间的变换矩阵
//float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
//通过Unity内置变量直接计算得到rotation变换矩阵
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;
//如果法线纹理没有设置成Normal Map类型,则手动进行反映射
tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//如果法线方向设置为了Normal Map类型,则使用内置函数计算正确的法线方向
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;//使用tex2D函数对纹理采样,返回纹素值,乘以颜色属性作为材质反射率albedo
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;//将albedo和环境光照相乘得到环境光
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));//使用albedo计算漫反射结果
//计算高光反射
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"
}
完成上面的代码后,我们可以在材质面板中为胶囊体添加 normal map ,使得胶囊体呈现视觉上的凹凸感,面板中的 Bump Scale 属性可以调整凹凸程度
Shader "Unity Shaders Book/Chapter7/Normal Map In World Space"
{
Properties
{
_Color("Color Tint",Color) = (1,1,1,1)
_MainTex("Texture", 2D) = "white" {}
_BumpMap("Normal map",2D) = "bump" {} //法线纹理属性,使用内置的法线纹理"bump"
_BumpScale("Bump Scale",Float) = 1.0 //用于控制凹凸程度,为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;
float4 _MainTex_ST;//其中存储了纹理的缩放和偏移值,分别可以通过.xy和.zw得到
sampler2D _BumpMap;
float4 _BumpMap_ST;//和_MainTex_ST类似,用于获得纹理的属性(平铺和偏移系数)
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT; //tangent.w决定切线空间中第三个坐标轴 ———— 副切线的方向性
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
//依次存储从切线空间到世界空间变换矩阵的每一行(世界空间下的顶点位置存储在w分量中)
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//在uv中存储使用的两个纹理坐标
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;
float3 worldNormal = UnityObjectToWorldNormal(v.vertex);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
//计算切线空间到世界空间的变换矩阵
//将世界空间下的顶点位置的xyz分量分别存储在TtoW变量的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;//使用tex2D函数对纹理采样,返回纹素值,乘以颜色属性作为材质反射率albedo
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;//将albedo和环境光照相乘得到环境光
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, LightDir));//使用albedo计算漫反射结果
//计算高光反射
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 函数针对不同的压缩格式进行正确的采样
值得一提的是,当纹理类型标识为 Normal Map 后,下面会出现一个复选框 “Create from Grayscale”,这一选项用于从高度图中生成法线纹理(勾选后, Unity 会根据高度图生成一张切线空间下的法线纹理)
其中, Bumpiness 用于控制凹凸程度, Filtering 决定计算凹凸程度的方式
Shader "Unity Shaders Book/Chapter7/Ramp Texture"
{
Properties
{
_Color ("Tint Color",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);//使用Unity内置宏计算平铺和偏移后的纹理坐标
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(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"
}
遮罩允许我们保护某些区域,使他们免于某些修改
遮罩纹理大体的使用流程为:通过采样,得到遮罩纹理的纹素值,使用其中某几个通道的值与某种表面属性相乘,这样当该通道为0时,可以保护表现不受该属性的影响
Shader "Unity Shaders Book/Chapter7/Mask Texture"
{
Properties
{
_Color ("Tint Color",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;
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;//使用tex2D函数对纹理采样,返回纹素值,乘以颜色属性作为材质反射率albedo
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;//将albedo和环境光照相乘得到环境光
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));//使用albedo计算漫反射结果
//计算使用遮罩纹理的高光反射
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四个通道来存储不同的属性