《Unity Shader入门精要》学习笔记第7章 基础纹理_模型顶点的切线空间-程序员宅基地

技术标签: shader  unity  游戏引擎  游戏  游戏开发  

本文章用于帮助自己学习,因此只记录一些个人认为比较重要或者还不够熟悉的内容。
原作者:http://blog.csdn.net/candycat1992/article/

第七章 基础纹理

7.1单张纹理

在美术人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标 (texture-mapping coordinates)存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的 2D坐标。通常,这些坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,而v是纵向坐标。 因此,纹理映射坐标也被称为UV坐标。我在学习建模时,也经常要做展UV这个工作。

7.1.1实践

我们通常会使用一张纹理来代替物体的漫反射颜色。

Shader "Unity Shaders Book/Chapter 7/Single Texture"
{
    Properties
    {
        _Color("Color Tint",Color)=(1,1,1,1)
        //2D是纹理属性的声明方式。初始值是一个字符串后跟一个花括号
        //,“white”是内置纹理的名字, 也就是一个全白的纹理。 
        _MainTex("Main Tex",2D)="white"{}
        _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;
	            fixed4 _Specular;
	            //使用纹理名_ST 的方式来声明某个纹理的属性
	            //,让我们得到纹理缩放和平移值。 
	            //_MainTex_ST.xy存储的是缩放值,_MainTex_ST.zw存储的是偏移值。
	            float4 _MainTex_ST;
	            sampler2D _MainTex;
	            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;
	                // 使用纹理的属性_MainTex_ST来对顶点纹理坐标进行变换
	                //,先缩放后平移移。 
	                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
	                //或者直接用内置宏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));
	
	                //使用CG的tex2D函数对纹理采样,第一个参数是被采样的纹理名
	                //,第二个是float2类型纹理坐标,返回计算得到的纹素值
	                //结果和颜色_Color乘积作为材质的反射率albedo
	                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
	
					//材质的反射率和环境光相乘得到环境光部分,下面同理。 
	                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
	
	                fixed3 diffuse = _LightColor0.rbg * albedo * max(0, 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"
}

7.1.2纹理的属性

在我们向Unity中导入一张纹理资源后,可以在它的材质面板上调整其属性,如图所示:
在这里插入图片描述
纹理面板中的第一个属性是纹理类型。我们之所以要为导入的纹理选择合适的类型,是因为只有这样才能让Unity 知道我们的意图,为Unity Shader 传递正确的纹理,并在一些情况下可以让Unity 对该纹理进行优化。
Wrap Mode决定了当纹理坐标超过[0, 1]范围后将会如何被平铺。Wrap Mode有两种模式:
一种是Repeat在这种模式下,如果纹理坐标超过了 1,那么它的整数部分将会被舍弃,而直接使用小数部分进行采样,这样的结果是纹理将会不断重复
另一种是Clamp,在这种模式下,如果纹理坐标大于1,那么将会截取到1,如果小于0,那么将会截取到0
下图为两种模式下平铺一张纹理的效果:
在这里插入图片描述
下一个属性是Filter Mode属性,它决定了当纹理由于变换而产生拉伸时将会釆用哪种滤波模式
Filter Mode支持3种模式:Point, Bilinear以及Trilinear。它们得到的图片滤波效果依次提升,但需要耗费的性能也依次增大。纹理滤波会影响放大或缩小纹理时得到的图片质量。就好像我们把一张图放大到一定程度时,画面就逐渐变成了马赛克的效果。而滤波效果越好,就越不像马赛克。
在这里插入图片描述

7.2凹凸映射

7.2.1高度纹理

第一种技术是用一张高度图来实现凹凸映射。高度图中存储的是强度值(intensity),它用于表示模型表面局部的海拔高度。因此,颜色越浅表明该位置的表面越向外凸起,而颜色越深表明该位置越向里凹(根据美术常识,凸起的地方是亮面,受光照效果明显)。
这种方法的好处是非常直观,我们可以从高度图中明确地知道一个模型表面的凹凸情况,但缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要由像素的灰度值计算而得,因此需要消耗更多的性能。如下是一张高度图:
在这里插入图片描述

7.2.2法线纹理

而法线纹理中存储的就是表面的法线方向。由于法线方向的分量范围在[-1, 1],而像素的分量范围为[0,1],因此我们需要做一个映射,通常使用的映射就是:
在这里插入图片描述
对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理(object-space normal map)。
在实际制作中,往往会釆用另一种坐标空间,即模型顶点的切线空间 (tangent space)来存储法线。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴可由法线和切线叉积而得,也被称为是副切线(bitangent, b)或副法线,如图:
在这里插入图片描述
这种纹理被称为是切线空间的法线纹理(tangent-space normal map)。下图分别给出了模型空间和切线空间下的法线纹理:
在这里插入图片描述
切线空间下的法线纹理看起来几乎全部是浅蓝色的。 这是因为,每个法线方向所在的坐标空间是不一样的,即是表面每点各自的切线空间。这种法线纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向
在现代计算机图形学中,RGB是最常见的色彩编码方式,而法线贴图就是将法向量用颜色存在了图上,即用R通道储存X值,G通道是Y值,B通道是Z值。
也就是说,如果一个点的法线方向不变,那么在它的切线空间中,新的法线方向就是z轴方向,即值为(0,0, 1),经过映射后存储在纹理中就对应了 RGB(0.5, 0.5, 1)浅蓝色(因为rgb的数值区间是[0,1],法线的数值区间是[-1,1])。

看到这里,有些刚接触的人可能还是比较难以理解法线纹理的意义。用我自己的话来说,法线纹理这张图并不是真正的纹理,而是每个点在各自的切线空间中的法线方向,而这个法线方向并不是模型上这个点真正的法线方向,而是一个用于计算模拟凹凸光照效果的法线方向。在之前学习建模时,就一直会强调尽量控制模型的面数不要太多,不然会影响游戏性能。那么如何在游戏中实现逼真的光照效果呢?就需要模拟纹理的凹凸感。
比如,一个用砖块堆砌成的墙,他的模型一般是一个非常平的平面。而想要渲染出每个砖块凹凸的效果,以一个砖块为例,他的左侧边缘处是面向左侧凸起的,也就是这里的平面是向左侧旋转的,那么在法线纹理中对应的这个位置,他存储的法线方向就是由垂直于墙面的法线方向向左偏移一定角度,在计算光照时以这个法线信息去计算,就会达到砖块此处凸起的效果。

使用切线空间有更多优点:

  1. 自由度很高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型, 而应用到其他模型上效果就完全错误了。而切线空间下的法线纹理记录的是相对法线信息, 这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果
  2. 可进行UV动画。比如,我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果。原因同上。这种UV动画在水或者火山熔岩这种类型的物体上会经常用到。
  3. 可以重用法线纹理。比如,一个砖块,我们仅使用一张法线纹理就可以用到所有的6个面上。原因同上。
  4. 可压缩。由于切线空间下的法线纹理中法线的Z方向总是正方向,因此我们可以仅存储 XY方向,而推导得到Z方向。而模型空间下的法线纹理由于每个方向都是可能的,因此 必须存储3个方向的值,不可压缩。

7.2.3实践

由于法线纹理中存储的法线 是切线空间下的方向,因此我们通常有两种选择:
一种选择是在切线空间下进行光照计算,此时我们需要把光照方向、视角方向变换到切线空间下;
另一种选择是在世界空间下进行光照计算, 此时我们需要把釆样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。
效率上来说,第一种方法往往要优于第二种方法,但从通用性角度来说,第二种方法要优于第一种方法。在本节中将依次实现上述的两种方法。

1.在切线空间下计算

基本思路:在片元着色器中通过纹理釆样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算, 得到最终的光照结果。
为此,我们首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即我们需要知道从模型空间到切线空间的变换矩阵。这个变换矩阵的逆矩阵, 即从切线空间到模型空间的变换矩阵是非常容易求得的,我们在顶点着色器中按切线(x轴)、副切线(y轴)、法线(z轴)的顺序按列排列即可得到(数学原理详见4.6.2节)。而从切线空间到模型空间的矩阵就是它的逆矩阵。

Shader "Unity Shaders Book/Chapter 7/Normal Map In Tagent Space"
{
    Properties
    {
        _Color("Color Tint",Color)=(1,1,1,1)
        //2D是纹理属性的声明方式。初始值是一个字符串后跟一个花括号
        //,“white”是内置纹理的名字, 也就是一个全白的纹理。 
        _MainTex("Main Tex",2D)="white"{}
        //对于法线纹理_BumpMap,使用bump作为它的默认值。
        //bump是Unity内置的法线纹理,当没有提供任何法线纹理时
        //,bump就对应了模型自带的法线信息。
        _BumMap("Normal Map",2D)="bump"{}
        //_BumpScale用于控制凹凸程度,当它为0时
        //,意味着该法线纹理不会对光照产生任何影响。
        _BumpScale("Bump 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;
            fixed4 _Specular;
            //使用纹理名_ST 的方式来声明某个纹理的属性,让我们得到纹理缩放和平移值。 
            //_MainTex ST.xy存储的是缩放值,_MainTex_ST.zw存储的是偏移值。
            float4 _MainTex_ST;
            sampler2D _MainTex;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                //切线空间是由顶点法线和切线构建出的一个坐标空间
                //,因此需要得到顶点的切线信息。
                //和法线方向normal不同,tangent的类型是float4,不是float3, 
				//因为需要使用tangent.w分量来决定切线空间中的副切线坐标轴的方向性。
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                //由于使用了两张纹理
                //,因此需要存储两个纹理坐标,所以uv变量定义为float4类型。
				//其中xy分量存储了_MainTex的纹理坐标
				//,而zw分量存储了_BumpMap的纹理坐标。 
				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;
                //或者直接用内置宏o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                //第一个参数是纹理坐标,第二个参数是纹理名。 
                
                //计算副切线
		//		float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
				//把模型空间下切线方向、 副切线方向和法线方向按行排列来得到从模型空间到切线空间的变换矩阵rotation
		//		float3x3 rotation = float3x3(v.tangent.xyz,v.binormal,v.normal);
				//或者使用内置宏,直接计算得到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);

                //使用tex2D对法线纹理采样,第一个参数是被采样的纹理名
                //,第二个是纹理坐标,返回把法线经过映射后得到的像素值
                fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
                fixed3 tangentNormal;

				// 如果没有在Unity里把该法线纹理的类型设置成Normal map,需要反映射 
//				tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
//				tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
				
				// Or mark the texture as "Normal map", and use the built-in funciton
				//使用UnpackNormal得到正确的法线方向 
				tangentNormal = UnpackNormal(packedNormal);
				//利用_BumpScale 控制凹凸程度  
				tangentNormal.xy *= _BumpScale;
				//由于法线都是单位矢量,因此tangentNormal.z 可以由tangentNormal.xy计算而得
				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 * max(0, 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"
}

效果如下:
在这里插入图片描述

2.在世界空间下计算

这种方法的基本思想是:在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。最后,只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间下即可。

Shader "Unity Shaders Book/Chapter 7/Normal Map In WorldSpace"
{
    Properties
    {
        _Color("Color Tint",Color)=(1,1,1,1)
        _MainTex("Main Tex",2D)="white"{}
        //对于法线纹理_BumpMap,使用bump作为它的默认值。
        //bump是Unity内置的法线纹理,当没有提供任何法线纹理时
        //,bump就对应了模型自带的法线信息。
        _BumpMap("Normal Map",2D)="bump"{}
        //_BumpScale用于控制凹凸程度,当它为0时
        //,意味着该法线纹理不会对光照产生任何影响。
        _BumpScale("Bump 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;
            fixed4 _Specular;
            //使用纹理名_ST 的方式来声明某个纹理的属性,让我们得到纹理缩放和平移值。 
            //_MainTex ST.xy存储的是缩放值,_MainTex_ST.zw存储的是偏移值。
            float4 _MainTex_ST;
            sampler2D _MainTex;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                //切线空间是由顶点法线和切线构建出的一个坐标空间
                //,因此需要得到顶点的切线信息。
                //和法线方向normal不同,tangent的类型是float4,不是float3, 
				//因为需要使用tangent.w分量来决定切线空间中的副切线坐标轴的方向性。
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                //由于使用了两张纹理,因此需要存储两个纹理坐标
                //,所以uv变量定义为float4类型。
				//其中xy分量存储了_MainTex的纹理坐标
				//,而zw分量存储了_BumpMap的纹理坐标。 
				float4 uv : TEXCOORD0;
				//切线空间到世界空间的变换矩阵
				//因为一个插值寄存器最多只能存储float4大小变量
				//,因此3x4矩阵需要三个float4变量。 
				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;

				//计算世界空间下的顶点切线、副切线和法线的矢量表示(分别对应切线空间下xyz轴) 
 				fixed3 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));

                //使用tex2D对法线纹理采样,第一个参数是被采样的纹理名
                //,第二个是纹理坐标,返回把法线经过映射后得到的像素值
                //使用UnpackNormal得到正确的法线方向(对纹理解码) 
                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));

				//利用_BumpScale 控制凹凸程度  
				bump.xy *= _BumpScale;
				
				//由于法线都是单位矢量,因此bump.z 可以由bump.xy计算而得
				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"
}

效果同上。

7.3渐变纹理

在之前计算漫反射光照时,我们都是使用表面法线和光照方向的点积结果与材质的反射率相乘来得到表面的漫反射光照。但有时,我们需要更加灵活地控制光照结果。
在一篇论文中,作者提到了基于冷到暖色调(cool-to-warm tones)的着色技术,用来得到一种插画风格的渲染效果。使用这种技术,可以保证物体的轮廓线相比于之前使用的传统漫反射光照更加明显,而且能够提供多种色调变化。
如下图,使用不同的渐变纹理控制漫反射光照,左下角给出了每张图使用的渐变纹理。
在这里插入图片描述

Shader "Unity Shader Books/Chapter7/Ramp Texture"
{
    Properties
    {
        _RampTex ("Ramp Tex", 2D) = "white" {}
        _Color ("Color Tint",Color) = (1,1,1,1)
        _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"
            
            sampler2D _RampTex;
            float4 _RampTex_ST;
            fixed4 _Color;
            fixed4 _Specular;
            float _Gloss;
            

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
                
            };

            struct v2f
            {
                float3 worldPos : TEXCOORD0;
                float4 pos : SV_POSITION;
                float3 worldNormal : 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;
                
                //使用内置的TRANSFORM_TEX宏来计算经过平铺和偏移后的纹理坐标。
                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;
                //由于RampTex实际就是一个一维纹理(它在纵轴方向上颜色不变)
                /,因此纹理坐标的u和v方向都使用halfLamberto
                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"
}

看到这里可能会不理解渐变纹理是如何实现的,实际上渐变纹理的重点就是纹理采样的过程:

 fixed3 diffuseColor = tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb * _Color.rgb;

在上面的代码中,已经使用半兰伯特模型计算得到了范围被映射到[0,1]之间的半兰伯特部分halfLambert。然后使用halfLambert作为uv坐标的值(实际上v的坐标的值没有意义,因为纵轴方向上颜色不变),也就是说,光照越强,halfLambert的值就越大,采样的纹理横坐标就越接近1,也就是如下纹理坐标图的浅色位置,实现了渐变纹理的光照效果,反之亦然。
在这里插入图片描述

7.4遮罩纹理

什么是遮罩呢?简单来讲,遮罩允许我们可以保护某些区域,使它们免于某些修改
例如,在之前的实现中,我们都是把高光反射应用到模型表面的所有地方,即所有的像素都使用同样大小的高光强度和高光指数。但有时,我们希望模型表面某些区域的反光强烈一些,而某些区域弱一些。为了得到更加细腻的效果,我们就可以使用一张遮罩纹理来控制光照。
另一种常见的应用是在制作地形材质时需要混合多张图片,例如表现草地的纹理、表现石子的纹理、表现裸露土地的纹理等,使用遮罩纹理可以控制如何混合这些纹理。

7.4.1实践

Shader "Unlit/Chapter7-MaskTexture"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("Main Tex", 2D) = "white"{}
        _BumpMap("Normal",2D) = "bump"{}
        _BumpScale("Bump Scale",Float) = 1.0
        //_SpecularMask是高光反射遮罩纹理
        _SpecularMask("Specular Mask",2D) = "White"{}
        //_SpecularScale是用于控制遮罩影响度的系数
        _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
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                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 = saturate(sqrt(1.0 - dot(tangentNormal.xy, tangentNormal.xy)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rbg * albedo * max(0, dot(tangentNormal, tangentLightDir));

                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                //对遮罩纹理进行采样
                //由于本例使用的遮罩纹理中每个纹素的rgb分量其实都是一样的
                //, 表明了该点对应的高光反射强度,
                //在这里我们选择使用r分量来计算掩码值。
                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"
}

效果如下:
在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_46124783/article/details/114712417

智能推荐

python封装exe源码查看器_如何将python脚本封装成exe程序?-程序员宅基地

文章浏览阅读628次。我们在编写代码时候,,有没有想过怎么去运行这个代码,绝非是在编程软件里的预览哦。而是让用户去使用,绝对要成一个安装包,如果刚刚入门的小伙伴,肯定没有想过这些,因为大部分人,还处于在搭建代码的状态下,但是还是希望大家先了解下,自己没事的时候拾起来玩耍也不错哦~以下内容基于Python的第三方库pyinstaller进行的。项目地址pyinstallergitpyinstaller安装pipins..._封装的程序查看程序代码

和平精英怎么在服务器维护中登录,和平精英登录过于频繁怎么办 和平精英登录过于频繁解决方法...-程序员宅基地

文章浏览阅读2.9k次。和平精英于今日下午3点开服后,很多玩家都出现了无法登陆的问题。有些小伙伴表示,在登录过程中游戏出现了“登录过于频繁,请X分钟后再尝试!”的提醒。那么,和平精英登录过于频繁怎么办呢?下面就是和平精英登录过于频繁解决方法,快来看看吧!和平精英登录过于频繁怎么办和平精英登录过于频繁时,玩家需要耐心等待几分钟,之后就可以再次登陆了。众所周知,和平精英是绝地求生刺激战场的正式版,而且刺激战场已经于昨晚下线,...

PG库 boolean 自动转成int_pg数据库boolen转integer-程序员宅基地

文章浏览阅读1.1k次。CREATE OR REPLACE FUNCTION boolean_to_smallint(b boolean) RETURNS smallint AS $$ BEGIN RETURN (b::boolean)::bool::int; END; $$LANGUAGE plpgsql; CREATE CAST (boolean AS smallint) WITH FUNCTION boolean_to_smallint(boolean) AS implicit;在这里插入代码片..._pg数据库boolen转integer

C++中char *和char []的区别_c++程序中char* 和 char[]使用上的区别-程序员宅基地

文章浏览阅读1.3w次,点赞26次,收藏61次。以前一直觉得这两个有区别,但也没深究,今天写了个代码报了警告于是就看了看,总结如下。例如如下代码:#includeusing namespace std;int main(){ char *p1 = "abcd"; char p2[] = "1234"; return 0;}“abcd”是在编译时刻就确定的,而“1234”是在运行时刻赋值的。 但_c++程序中char* 和 char[]使用上的区别

【UWB】MSE 均方误差、RMSE 均方根误差_均方误差(mse)和方均根误差(rmse)算法-程序员宅基地

文章浏览阅读1.7k次。文章目录均方误差均方根误差Ref:均方误差在处理数据过程中,我们常需要用到均方误差(Mean Square Error, MSE)、均方根误差(Root Mean Square Error, RMSE)来对数据进行描述、统计。均方误差(MSE)是指参数估计值与参数真值之差平方的期望值。它是反映估计量与被估计量之间差异程度的一种度量,是衡量“平均误差”的一种较方便的方法。设 ttt 是根据子样确定的总体参数的一个估计量,(θ−????)2(\theta −????)^2(θ−t)2 的数学期望,称为估计_均方误差(mse)和方均根误差(rmse)算法

hdu 4359 Easy Tree DP(dp+组合计数)_[algorithm] easy tree-程序员宅基地

文章浏览阅读697次。题意:_[algorithm] easy tree

随便推点

360面试问题记录_面试官提问记录-程序员宅基地

文章浏览阅读110次。c++ 360面试问题记录关于简历简历上面面试官问的问题:1.先简单介绍一下自己2.看你简历上写的xx和xx公司,都是做哪方面业务的,简单介绍一下3.介绍一下你印象深刻的项目或者模块剩下的基本都是简历上写了什么问什么。会问到性能上面相关…然后有点记不清了技术问题1.vector是线程安全的吗?2.Vector跟数组的区别?什么时候用vector什么时候用数组3.动态指针4.平时用的数据结构都有哪些?5.GDB调试6.Git使用 svn使用7.流程图 类图用啥画的=======_面试官提问记录

php curl_init函数用法。_$curl = curl_init()-程序员宅基地

文章浏览阅读2.7k次。php curl_init函数用法使用PHP的cURL库可以简单和有效地去抓网页。你只需要运行一个脚本,然后分析一下你所抓取的网 页,然后就可以以程序的方式得到你想要的数据了。无论是你想从从一个链接上取部分数据,或是取一个XML文件并把其导入数据库,那怕就是简单的获取网页内 容,cURL 是一个功能强大的PHP库。PHP中的CURL函数库(Client URL Library Functi..._$curl = curl_init()

Java基础_java 获取今天日期-程序员宅基地

文章浏览阅读268次。Java基础内容,培训9天总结_java 获取今天日期

生产者消费者模型——C语言代码详解_生产者消费者代码-程序员宅基地

文章浏览阅读5.3k次,点赞3次,收藏22次。概念生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。321原则三种角色:生产者、消费者、仓库两种关系:生产者与生产者之间是互斥关系,消费者与消费者之间是互斥关系,生产者与消费者之间是同步与互斥关系。一个交易场_生产者消费者代码

javascript中的字符串、对象和数组的获取方式<基础>_js数组根据字符串获取值-程序员宅基地

文章浏览阅读1k次。温故而知新。字符串javascript中的字符串就是用”或者”“括起来的字符表示。如果符号”或”“本身就是字符串那么则用”” 或者”括起来转义字符\可以转义很多字符,比如\n表示换行,\t表示制表符,字符\本身也要转义,所以\表示的字符就是\。//例如 i'am "ok"'i\'am \"ok\"'//或者"i"+"'"+"am "+"'"+"ok"+"'"//转移字_js数组根据字符串获取值

Linux内核中list_for_each()和list_for_each_save()_dl_list_for_each-程序员宅基地

文章浏览阅读862次。Linux内核中list_for_each()和list_for_each_save()2018年08月22日 21:24:54 ibless 阅读数:41 注:彩色文本是转发者修改过,个人理解不一定正确,请多指教!。今天看Linux内核代码,看到了list_for_each_save()函数,想仔细的研究一下。下面是源代码:list_for_each()的定义: ..._dl_list_for_each

推荐文章

热门文章

相关标签