【Shader入门精要】第七章——基础纹理

Shader入门精要项目资源:

https://github.com/candycat1992/Unity_Shaders_Book

一、凹凸映射、法线纹理

法线纹理是存储法线数据的,即模型上每一个像素的法线;

有两种存储方法:

  • 存的是模型空间下的法线
  • 存的是切线空间下的法线

区别:模型空间下的法线是相对于模型空间坐标系的,切线空间下的法线是相对于顶点切线空间坐标系的。

具体说明:

因为我们在使用法线的时候,都是作用于一个顶点(或片元),使用用模型空间方式存储的法线贴图的话,这些法线是绝对法线数据,因为是相对于模型空间坐标系的,此时,如果你想用模型下的法线贴图,你就没办法重用到多个不同模型身上,因为法线是相对于模型空间的,而不是相对于顶点的(或片元),所以必须为每一个模型都做一个法线贴图。如果是切线空间的法线贴图,因为里面的法线是相对于顶点的(或片元),所以可以提供给不同模型使用,因为是相对顶点的(或片元)!顶点随你怎么在模型空间改变位置,法线也会跟着改变,可以想象成这个法线是顶点的子物体(法线就是一根线),而前者就不会跟随顶点变换了。

因此,切线空间下的法线纹理是常用的,下面介绍的范例也全是介绍切线空间下的法线纹理用法。

  • 什么叫顶点切线空间?

在模型空间下,切线是两个相邻顶点的连线(射线),每一个顶点上都有它的切线,切线是由自身和相邻顶点构成的;

顶点切线空间的X轴是顶点切线、Z轴是顶点法线、Y轴是顶点副切线(或副法线)

顶点副切点垂直于顶点切线和顶点法线构成的平面的,可发现,垂直于这个平面的是有两种方向选择的,我们用顶点切线的w值进行控制它的方向。

每一个顶点都有一个切线空间,每一个顶点的切线空间都不相同,切线空间的原点即是顶点自身。

范例-1 在切线空间下使用切线空间下的法线纹理,实现凹凸映射+漫反射+高光反射效果

思路:在顶点着色器计算出切线空间下的光源向量和观察向量,传递给片元着色器,然后在片元着色器取出法线纹理的法线后,要经过UnpackNormal处理,并且要乘以一个权重控制法线影响度(凹凸程度),之后就是很普通的导入公式计算漫反射和高光反射。

Shader "MilkShader/Sevent/NormalMapTangentSpace"
{
	Properties
	{
		//主纹理贴图
		_MainTex ("Texture", 2D) = "white" {}
		//自定义颜色值
		_Color ("Color",Color)=(1,1,1,1)
		//高光反射颜色
		_Specular("Specular", Color)=(1,1,1,1)
		//高光反射光泽度
		_Gloss("Gloss",Range(8.0,256))=20
		//切线空间下的法线纹理
		_BumpMap("Bump", 2D) = "bump"{}
		//法线纹理影响系数
		_BumpScale("Bump Scale", Float) = 1.0
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			//开启前置渲染
			Tags{
				"LightMode" = "ForwardBase"
			}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			//光照内置变量如:_LightColor0 所在的头文件
			#include "Lighting.cginc"

			//可以直接用"UnityCG.cginc"里面的appdata_tan来作为顶点着色器的输入结构体
			//包含顶点、法线、切线、模型自带的纹理坐标
			struct appdata
			{
				float4 vertex : POSITION;	//顶点
				float3 normal : NORMAL;		//法线
				float4 tangent : TANGENT;	//切线
				float2 texcoord : TEXCOORD0;//模型自带的纹理坐标
			};
									
			//顶点着色器输出结构体、同时也是片元着色器的输入结构体
			struct v2f
			{
				float4 uv : TEXCOORD0;		//经过主纹理缩放系数和偏转系数处理后的纹理坐标、
				float4 vertex : SV_POSITION;//裁剪空间下的顶点坐标、
				float3 lightDir : TEXCOORD1;//切线空间下的光源向量、			
				float3 viewDir : TEXCOORD2; //切线空间下的观察向量
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;//主纹理(缩放系数,偏移系数) (x,y)是缩放,(z,w)是偏移
			fixed4 _Specular;			
			fixed4 _Color;
			sampler2D _BumpMap;
			float4 _BumpMap_ST;//法线纹理的缩放偏移系数
			float _Gloss;
			float _BumpScale;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				//利用xy存储主纹理坐标, 利用zw存储法线纹理坐标
				//o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				//同上式子操作:对模型纹理坐标进行(根据主纹理缩放系数和偏移系数)的缩放和偏移得到主纹理的纹理坐标
				o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);

				//o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
				//同上式子操作:对模型纹理坐标进行(根据切线纹理缩放系数和偏移系数)的缩放和偏移得到切线纹理的纹理坐标
				o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
				
				//副法线(副切线)= 切线和法线的叉积 * 切线w值(w值是控制方向的!)
				float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
				//模型空间转切线空间的矩阵
				float3x3 objectToTangentMat = float3x3(v.tangent.xyz, binormal, v.normal);
				//光源向量转切线空间
				o.lightDir = mul(objectToTangentMat, ObjSpaceLightDir(v.vertex)).xyz;
				//观察向量转切线空间
				o.viewDir = mul(objectToTangentMat, ObjSpaceViewDir(v.vertex)).xyz;
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed3 tangentLightDir = normalize(i.lightDir);
				fixed3 tangentViewDir = normalize(i.viewDir);
				//i.uv.zw是法线纹理的纹理坐标,根据这个纹理坐标从法线纹理进行采样像素颜色值rgba(fixed4)
				fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
				//需要进行一个转换!因为法线纹理上存储的法线数据是经过了压缩才能存入纹理的,具体博客会讲。
				fixed3 tangentNormal = UnpackNormal(packedNormal);
				//再乘以它的法线影响系数(对XY进行乘法运算即可)
				tangentNormal.xy *= _BumpScale;
				//z轴是根据XY推理出的,因为法线的Z轴绝对是正数,而且法线是一个单位矢量即满足x^2+y^2+z^2 = 1
				tangentNormal.z = sqrt(1- saturate(dot(tangentNormal.xy,tangentNormal.xy)));
			
				//上面到此已经知道了切线空间下的法线、光源向量、观察向量,可导入公式求出漫反射、高光反射
				fixed3 albedo = tex2D(_MainTex, i.uv.xy).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(saturate(dot(tangentNormal, halfDir)), _Gloss);

				return fixed4(ambient+diffuse+specular, 1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

 1、关于UnpackNormal(fixed4)的知识点

对从法线纹理中取出来的法线进行解压缩处理,因为法线(x,y,z)的x,y,z范围都是在[-1,1],而纹理保存的是颜色值(r,g,b,a),r,g,b,a范围都是在[0,1],为了将法线能正确保存入纹理,必须要经过一个压缩:

color = normal/2 + 1/2  (法线(x,y,z)进行这个算法将x,y,z调整到[0,1]范围变成color存入法线纹理)

我们通过tex2D(_BumpTex, i.uv.zw)获取到法线纹理中的颜色值(fixed4)之后,是经过压缩的法线,需要通过UnpackNormal函数进行解压缩,这个函数做的事情是:

normal = color * 2 - 1  ( color(r,g,b)都会进行这个算法将r,g,b调整到[-1,1]范围还原成法线 ) 

由于,法线是一个单位矢量,且Z轴是正数,存储时可以只存法线的x值,y值,然后依靠xy值推出z值。

公式: z  =  sqrt(1 - (x*x+y*y)) 

因此,有一个叫DXT5nm格式的法线纹理,这种格式的法线纹理会只存储法线的xy值,z值靠xy推导出来,以此来达到节省内存开销。

从DXT5nm格式的法线纹理取出法线后,需要进行如下解压缩过程:

normal.xy = color.wy * 2 - 1;

normal.z = sqrt(1 - (normal.x^2 + normal.y^2)) = sqrt( 1 - dot(normal.xy,normal.xy));

UnpackNormal(fixed4)源码如下:(UnityCG.cginc头文件)

//DXT5nm格式的解压缩过程
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
    fixed3 normal;
    normal.xy = packednormal.wy * 2 - 1;
    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

//貌似Unity2017.2.f3版本开始多了个BC5,其实都差不多,只是将法线的x存储在了x,上面的DXT5nm是存于w
// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1)
// Note neutral texture like "bump" is (0, 0, 1, 1) to work with both plain RGB normal and DXT5nm/BC5
fixed3 UnpackNormalmapRGorAG(fixed4 packednormal)
{
    // This do the trick
   packednormal.x *= packednormal.w;//这种是让这个函数即支持DXT5nm也支持BC5
    //若是DXT5nm, x = 1, w = 法线x,那么x * w = 1 * 法线x = 法线x
    //若是BC5, x = 法线x, w = 1, 那么 x * w = 法线x * 1 = 法线x

    fixed3 normal;
    normal.xy = packednormal.xy * 2 - 1;
    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
    return packednormal.xyz * 2 - 1;
#else
    return UnpackNormalmapRGorAG(packednormal);
#endif
}

 


 2、关于模型空间转切线空间转换矩阵(可选看)

M(o2t) 模型空间转切线空间转换矩阵,M(t2o) 切线空间转模型空间转换矩阵

由于模型空间和切线空间之间的差别就是平移和旋转,而由于空间转换的对象是坐标系,对坐标系进行平移是没有意义的,因此只剩下一个旋转差别,旋转矩阵是一个正交矩阵,正交矩阵的逆矩阵是其转置矩阵,因此我们可以求出M(t2o),再求其转置矩阵得到其逆矩阵M(o2t)。

(为什么旋转矩阵是一个正交矩阵?为什么正交矩阵的逆矩阵是其转置矩阵?不在本文解释,可查资料了解。)

因此,我们可以求M(t2o)切线空间转模型空间转换矩阵!

求出模型空间下的切线空间X轴向量表示、Y轴向量表示、Z轴向量表示,就求出了切线转模型的矩阵。

模型空间下的切线空间X轴向量 = 顶点切线(简称:tagent)

模型空间下的切线空间Y轴向量 = 顶点副法线(副切线)(简称: binormal)

模型空间下的切线空间Z轴向量 = 顶点法线  (简称: normal)

PS:切线空间是以顶点为坐标系原点的,切线空间都是相对于顶点而言,每一个顶点都有一个切线空间!

[fixed3x3] M(t2o)的第一列是tagent, 第二列是binormal, 第三列是normal

(为什么这样就求出了M(t2o)?)解惑请看https://blog.csdn.net/qq_39574690/article/details/98440963

因此,它的转置矩阵(将列变成行) MT(t2o) 第一行是 tagent, 第二行是binormal, 第三行是normal

因此 M(o2t) =  逆M(t2o) = MT(t2o),得出

	//副法线(副切线)= 切线和法线的叉积 * 切线w值(w值是控制方向的!)
	float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
	//模型空间转切线空间的矩阵
	float3x3 objectToTangentMat = float3x3(v.tangent.xyz, binormal, v.normal);

float3x3是按行写入,即第一行为tangent, 第二行为binormal, 第三行为normal


范例-2 在世界空间下使用切线空间下的法线纹理,实现凹凸映射+漫反射+高光反射效果(可选看)

思路:由于要在世界空间下使用,所以我们要转换切线空间下的法线到世界空间下,因此我们要求出切线空间转世界空间转换矩阵M(t2w),在片元着色器中,用M(t2w)进行转换法线,之后就是很普通地求出世界光源向量、世界观察向量,再导入公式即可。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "MilkShader/Sevent/NormalMapWorldSpace"
{
	Properties{
		_MainTex("Main Texture", 2D) = "while"{}
		_BumpMap("Bump Map", 2D) = "while"{}
		_Specular("Specular", Color) = (1,1,1,1)
		_Color ("Color", Color) = (1,1,1,1)
		_BumpScale("Bump Scale", Float) = 1.0
		_Gloss ("Gloss", Range(8.0,256)) = 20
	}

	SubShader{
		Tags{
			"RenderType" = "Opaque"
		}
		LOD 100
		Pass{
			Tags{ "LightMode" = "ForwardBase"}
			CGPROGRAM
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#pragma vertex vert
			#pragma fragment frag
			
			float _Gloss;
			fixed4 _Color;
			fixed4 _Specular;
			float _BumpScale;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _BumpMap;
			float4 _BumpMap_ST;

			struct a2v{
				float4 pos : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 uv : TEXCOORD0;				
			};
			struct v2f{
				float4 pos : SV_POSITION;
				float4 uv :TEXCOORD0;
				float4 TtoW0 : TEXCOORD1;//存储的是切线空间转世界空间矩阵的第一行
				float4 TtoW1 : TEXCOORD2;//存储的是切线空间转世界空间矩阵的第二行
				float4 TtoW2 : TEXCOORD3;//存储的是切线空间转世界空间矩阵的第三行
			};

			v2f vert(a2v i){
				v2f o;
				o.pos = UnityObjectToClipPos(i.pos);
				o.uv.xy = TRANSFORM_TEX(i.uv, _MainTex);
				o.uv.zw = TRANSFORM_TEX(i.uv, _BumpMap);

				float3 worldPos = mul(unity_ObjectToWorld, i.pos).xyz;				
				float3 worldTangent = UnityObjectToWorldDir(i.tangent.xyz);//x
				float3 worldNormal = UnityObjectToWorldNormal(i.normal);//z
				float3 worldBinormal = cross(worldNormal, worldTangent) * i.tangent.w;//y
			
				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{
				fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
				fixed3 tangentNormal = UnpackNormal(packedNormal);
				tangentNormal.xy *= _BumpScale;
				tangentNormal.z = sqrt(1-dot(tangentNormal.xy,tangentNormal.xy));
				fixed3 worldNormal = normalize(half3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));
				float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				fixed3 halfDir = normalize(worldLightDir + worldViewDir);
				
				fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal, worldLightDir));
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);

				return fixed4(ambient + diffuse + specular,1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

1、关于切线空间转世界空间转换矩阵M(t2w)

与M(t2o)类似,M(t2w)第一列、第二列、第三列分别是世界空间下的切线空间X轴矢量表示、Y轴矢量表示、Z轴矢量表示。

其中,世界空间下的切线空间X轴矢量表示 = 世界切线,世界空间下的切线空间Y轴矢量表示 = 世界副法线(副切线),世界空间下的切线空间Z轴矢量表示 = 世界法线

float3 worldPos = mul(unity_ObjectToWorld, i.pos).xyz;				
float3 worldTangent = UnityObjectToWorldDir(i.tangent.xyz);//x
float3 worldNormal = UnityObjectToWorldNormal(i.normal);//z
float3 worldBinormal = cross(worldNormal, worldTangent) * i.tangent.w;//y
			
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);

如上片段代码,注意:o.TtoW0,o.TtoW1, o .TtoW2分别是M(t2w)第一行,第二行,第三行;

第一行的第四列是世界顶点坐标x值,第二行的第四列是世界顶点坐标y值,第三行的第四列是世界顶点坐标z值

M(t2w)是3X3矩阵,因为变换的是向量,在上面三个变量的前三列才是M(t2w)的内容。

而为什么第四列要这样子存一个世界顶点坐标呢?因为要合理利用插值寄存器的空间,插值寄存器存储的是float4!一个TEXCOORDx就是一个插值寄存器,所以尽量使用这个语义时要想好怎么用。

fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);	
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1-dot(tangentNormal.xy,tangentNormal.xy));
fixed3 worldNormal = normalize(half3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));	
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);

在上面代码在片元着色器中,获取到了切线空间下的法线tangentNormal后,通过M(t2w)·tangentNormal得到worldNormal

矩阵点积就是将矩阵第一行与法线[4x1]第一列进行点积得到世界法线x值,y值和z值以此类推。

通过获取  float3( i.TtoW0.w, i.TtoW1.w, i.TtoW2.w ) 是世界顶点坐标


 二、渐变纹理

渐变纹理-漫反射是对半兰伯特-漫反射的扩展,也是由提出半兰伯特-漫反射的valve公司提出的。

  • 兰伯特漫反射公式:diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(normal, lightDir))
  • 半兰伯特漫反射公式: diffuse = _LightColor0.rgb * _Diffuse.rgb * (0.5 * dot(normal, lightDir) + 0.5)

其中,(0.5 * dot(normal, lightDir) + 0.5)是对dot(normal, lightDir)进行的一个范围映射,dot(normal, lightDir)范围是[-1,1],(0.5 * dot(normal, lightDir) + 0.5)范围是[0,1],所以为什么半兰伯特比兰伯特更亮,就是因为没有了负数,背光面也是比较明亮的。

而渐变纹理-漫反射是用(0.5 * dot(normal, lightDir) + 0.5)作为采样渐变纹理时的纹理坐标(X,Y),其中X,Y都是这个(0.5 * dot(normal, lightDir) + 0.5),你可以想象成(0.5 * dot(normal, lightDir) + 0.5)是一个根据光线与法线的夹角从0°变到90°,这个值就会从1逐渐变为0,也就是纹理坐标会从(1,1)渐变到(0,0),那么采样就是从纹理的右上角不断往左下角进行采样,如下图渐变纹理

我们可以修改这个渐变纹理来进行控制每一个片元的漫反射颜色!如下图改了纹理。

渐变纹理-漫反射公式: diffuse = _LightColor0.rgb * _Diffuse.rgb * tex2D(_RampTex, fixed2(h, h))

其中h = (0.5 * dot(normal, lightDir) + 0.5), 也可理解为h是表面辐照度,即物体上一个点的反射光线强度

用人话说就是在真实世界情况下,你拿着一个光源,照着一个物体,你看到比较亮的地方(那些点)就会去取渐变纹理的偏右上角的颜色值,而较暗的地方就会去取渐变纹理偏左下角的颜色值。

(注意:这是在OpenGL渲染空间下才是如此(左下角为(0,0)),DirectX渲染空间是左上角为(0,0) 貌似差别不大)

事实上,半兰伯特-漫反射 也可看成是一种特殊的渐变纹理-漫反射,即这个渐变纹理是由白色渐变到黑色的,你可以把半兰伯特-漫反射写成:

半兰伯特漫反射公式: diffuse = _LightColor0.rgb * _Diffuse.rgb * tex2D(_RampTex, fixed2(h, h))

其中h = (0.5 * dot(normal, lightDir) + 0.5), _RampTex是(从右往左)由白到黑的一维纹理(必须是由白到黑的一维纹理

因为h 就是半兰伯特-漫反射的一个系数范围[0,1],当h为1时,在渐变纹理上会从这个一维纹理采样出(1,1,1),当h逐渐为0靠拢时,采样出的颜色值就会往(0,0,0)靠拢,即把1变成了(1,1,1) 低纬度转高纬度。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "MilkShader/Sevent/Chapter7-RampTexture"
{
	Properties
	{
		_RampTex ("RampTex", 2D) = "white" {}
		_Color ("Color", Color) = (1,1,1,1)
		_Specular ("Specular", Color) = (1,1,1,1)
		_Gloss ("Gloss", Range(8,256)) = 20
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag			
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"

			fixed4 _Color;
			fixed4 _Specular;
			sampler2D _RampTex;
			float4 _RampTex_ST;
			float _Gloss;

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				float3 normal :NORMAL;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;				
				float4 vertex : SV_POSITION;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
			};			
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _RampTex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				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 diffuse = _LightColor0.rgb * _Color.rgb * (0.5 * dot(worldNormal, worldLightDir) + 0.5);

				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + worldViewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);

				return fixed4(ambient + diffuse + specular, 1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

三、遮罩纹理

代码实现:(在切线空间下计算)凹凸映射+漫反射+高光反射

在本例,遮罩纹理中存储的R值是一个控制高光反射颜色值强弱的系数(权重),即我们可以通过这个遮罩纹理进行调整其纹理中的每一个像素点的R值,就可以控制每一个片元的高光反射颜色值的强弱!下面代码中还会加一个控制R值影响程度的Float变量_SpecularScale,当这个为1时,遮罩纹理R值就会百分百地作用于高光反射颜色值。当为0时,遮罩纹理R值就不生效了,高光反射颜色没有变化。

Shader "MilkShader/Sevent/Chapter7-MaskTextureMilk"
{
	Properties
	{
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Texture", 2D) = "white" {}			
		_BumpMap("Normal Map", 2D) = "bump"{}
		_BumpScale("Bump Scale", Float) = 1.0
		_Specular("Specular",Color)= (1,1,1,1)
		_SpecularMask("Specular Mask",2D) = "white"{}
		_SpecularScale ("Specular Scale", Float ) = 1.0		
		_Gloss("Gloss",Range(8,256)) = 20
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100		
		Pass
		{
			Tags{ "LightMode" = "ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag			
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float4 uv : TEXCOORD0;
				float4 tangent : TANGENT;
				float3 normal : NORMAL;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;				
				float4 vertex : SV_POSITION;
				float3 lightDir : TEXCOORD1;
				float3 viewDir : TEXCOORD2;
			};

			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST; //三个纹理坐标都用主纹理的缩放和偏移
			sampler2D _BumpMap;
			//float4 _BumpMap_ST; //节省一个插值寄存器
			float _BumpScale;
			fixed4 _Specular;
			sampler2D _SpecularMask;
			//float4 _SpecularMask_ST; //节省一个插值寄存器
			float _SpecularScale;
			float _Gloss;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
				float3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;
				float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal.xyz);
				o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
				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-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(tangentNormal + tangentViewDir);
				fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale; //遮罩系数 = 遮罩纹理的R通道 * 遮罩权重
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal, halfDir)), _Gloss) * specularMask;//在原高光反射公式上,乘以一个遮罩系数
								
				return fixed4(ambient + diffuse + specular, 1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

遮罩纹理在上例只用到了一个R通道,事实上一个遮罩纹理应该四个通道都会合理利用,每一个通道或多通道组合来代表一种控制某个结果的系数,例如上例就是R通道来控制高光反射颜色值强弱的。

关于更多的用法,在后面高级纹理篇解释。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值