《Unity Shader入门精要》笔记:中级篇(2)

  3.2 游戏引擎技术
  • 本篇博客主要为个人学习所编写读书笔记,不用于任何商业用途,以及不允许任何人以任何形式进行转载。
  • 本篇博客会补充一些扩展内容(例如其他博客链接)。
  • 本篇博客还会提供一些边读边做的效果截图。文章内所有数学公式都由Latex在线编辑器生成。
  • 本篇博客主要提供一个“glance”,知识点的总结。如有需要请到书店购买正版。
  • 博客提及所有官方文档基于2022.2版本,博客会更新一些书中的旧的知识点到2022.2版本。
  • 如有不对之处欢迎指正。
  • 我创建了一个游戏制作交流群:637959304 进群密码:(CSGO的拆包密码)欢迎各位大佬一起学习交流,不限于任何平台(U3D、UE、COCO2dx、GamesMaker等),以及欢迎编程,美术,音乐等游戏相关的任何人员一起进群学习交流。

  • 该部分关于环境映射,反射折射,菲涅尔原理等内容在我的HLSL博客中也有详细的介绍。

高级纹理

  • 立方体纹理(Cubemap):用六张图像形成一个立方纹理来实现映射。
  • 天空盒子(Skybox):游戏中用于模拟背景的一种方式方法。天空盒子是在所有不透明物体之后渲染的,其背后使用的网格是一个立方体或者细分后的球体。
  • 创建用于环境映射的立方纹理:
    1、用一些特殊布局的纹理创建。该方法支持边缘修正、光滑反射(glossy relfection)和HDR功能
    2、手动创建一个Cubemap,然后把6张图传入
    3、用脚本生成。通过利用Unity提供的Camera.RenderToCubemap函数来实现
  • 反射:反射效果使得物体的表面看起来更像是金属。模拟反射效果需要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。
Shader "Custom/Shader06"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _ReflectColor("Reflection Color",Color) = (1,1,1,1)
        _ReflectAmount("Reflect Amount",Range(0,1)) = 1
        _Cubemap("Reflection Cubemap",Cube) = "_Skybox"{}
    }
    SubShader
    {
        Pass { 
			Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
            #pragma multi_compile_fwdbase
			
			#include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "UnityCG.cginc"

            fixed4 _Color;
			fixed4 _ReflectColor;
            fixed _ReflectAmount;
            samplerCUBE _Cubemap;

            struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
                float3 worldViewDir : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float3 worldNormal : TEXCOORD3;
                float3 worldRef1 : TEXCOORD4; 
                SHADOW_COORDS(5)
			};

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
                o.worldNormal = UnityObjectToWorldNormal(v.normal);  
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);

                o.worldRef1 = reflect(-o.worldViewDir,o.worldNormal);

                TRANSFER_SHADOW(o);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 worldViewDir = normalize(i.worldViewDir);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0,dot(worldNormal,worldLightDir));

                fixed3 reflection = texCUBE(_Cubemap,i.worldRef1).rgb * _ReflectColor.rgb;
//这里是官方自带的衰减因子atten,上一篇里面没注意到,教程里面没自己写了
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
//书里面没解释lerp,这里简单的释义为,当_ReflectAmount是diffuse和reflection的权重,当_ReflectAmount为0只有漫反射,当_ReflectAmount为1只有环境的反射
                fixed3 color = ambient + lerp(diffuse,reflection,_ReflectAmount) * atten;

                return fixed4(color,1.0);

            }
            ENDCG

        }
    }
    FallBack "Diffuse"
}
image 193 - 《Unity Shader入门精要》笔记:中级篇(2)
材质来自官方插件商店免费8K skybox
  • 折射:当给定入射角可以使用斯涅尔定律(Snell’s Law)来计算反射角,即计算光线与法线的夹角θ2。对于一个透明物体来说,折射关系会发生两次,一次是入射,一次是出射,但仅仅模拟一次入射得到的视觉效果也还不错,且计算出射比较复杂,所以实际应用中往往只是用一次出射即可。
  • η为折射率,常用折射率列表:传送门
image 192 - 《Unity Shader入门精要》笔记:中级篇(2)
Shader "Custom/Shader06"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _RefractColor("Refraction Color",Color) = (1,1,1,1)
        _RefractAmount("Refract Amount",Range(0,1)) = 1
//如何光线所在介质折射率和折射光线所在截止的折射率比值,一般前者都为空气,折射率为1,后者折射率查表
        _RefractRatio("Refract Ratio",Range(0.1,2)) = 0.5
        _Cubemap("Reflection Cubemap",Cube) = "_Skybox"{}
    }
    SubShader
    {
        Pass { 
			Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
            #pragma multi_compile_fwdbase
			
			#include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "UnityCG.cginc"

            fixed4 _Color;
			fixed4 _RefractColor;
            fixed _RefractAmount;
            fixed _RefractRatio;
            samplerCUBE _Cubemap;

            struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
                float3 worldViewDir : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float3 worldNormal : TEXCOORD3;
                float3 worldRefr : TEXCOORD4; 
                SHADOW_COORDS(5)
			};

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
                o.worldNormal = UnityObjectToWorldNormal(v.normal);  
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
//折射计算必须进行归一化
                o.worldRefr = refract(-normalize(o.worldViewDir),normalize(o.worldNormal),_RefractRatio);

                TRANSFER_SHADOW(o);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 worldViewDir = normalize(i.worldViewDir);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0,dot(worldNormal,worldLightDir));

                fixed3 refraction = texCUBE(_Cubemap,i.worldRefr).rgb * _RefractColor.rgb;

                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

                fixed3 color = ambient + lerp(diffuse,refraction,_RefractAmount) * atten;

                return fixed4(color,1.0);

            }
            ENDCG

        }
    }
    FallBack "Diffuse"
}

image 194 - 《Unity Shader入门精要》笔记:中级篇(2)
折射比率为1时
image 195 - 《Unity Shader入门精要》笔记:中级篇(2)
折射比率1.65
image 196 - 《Unity Shader入门精要》笔记:中级篇(2)
带入萤石的折射率
  • 菲涅尔反射(Fresnel reflection):当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部,发生折射或者散射。被反射的光和入射光之间存在一定比率关系,这个比率关系可以通过菲涅尔等式进行计算。
  • Schlick菲涅尔近似等式:v是视角方向,n是表面发现。
image - 《Unity Shader入门精要》笔记:中级篇(2)
  • Empricial菲涅尔近似等式:bias、scale,power是控制项。
  • 重要,重要,重要!这里应当是书里原文公式抄错了,power应该在外面,害我调了好久代码没发现问题在哪里,所以图二才是正确的公式!
image 1 1024x67 - 《Unity Shader入门精要》笔记:中级篇(2)
image 3 1024x77 - 《Unity Shader入门精要》笔记:中级篇(2)
//唯一的不同处在于这里,直接用了第一个近似式,去除了之前的reflect变量
fixed3 reflection = texCUBE(_Cubemap,i.worldRef1).rgb;
fixed fresnel = _FresnelScale + (1-_FresnelScale) * pow(1-dot(worldLightDir,worldNormal),5);
image 2 - 《Unity Shader入门精要》笔记:中级篇(2)
_FresnelScale=0.633
//Empricial菲涅尔近似等式,利用这三个参数可以更好的控制边缘的效果
fixed Empricial = max(0,min(1,_Empricial_bias+_Empricial_scale * pow(1-dot(worldLightDir,worldNormal),_Empricial_power)));
image 4 - 《Unity Shader入门精要》笔记:中级篇(2)
bias=5,scale=0.16,power=-1

渲染纹理

  • 渲染目标纹理(Render Rarget Texture,RTT):利用GPU把整个三维场景渲染到中间缓冲中。(传统使用的是帧缓冲或后备缓冲)
  • 多重渲染目标(Multiple Render Target,MRT):利用GPU把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。
  • 渲染纹理(Render Texture):有两种方式。1、在Project目录下创建渲染纹理,然后把某个摄像机的渲染目标设置成该渲染纹理,渲染结果就会实时更新到渲染纹理中。 2、屏幕后使用GrabPass命令或OnRenderImage函数来获取当前屏幕图像,Unity会把它放到一张和屏幕分辨率等同的渲染纹理中,然后可以在自定义的Pass中把他们当成普通的纹理来处理,从而实现屏幕特效。
  • 镜子效果:使用一张纹理贴图绑定给摄像机(camera里有个选项),然后再采样该纹理贴图就可以得到摄像机视角的图片了。把材质绑定在平面上就可以实现镜子下过。
  • 玻璃效果
Shader "Example/Shader08"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BumpMap ("Normal Map", 2D) = "bump" {}
        _CubeMap ("Environment CubeMap",Cube) = "_Skybox"{}
//控制扭曲程度
        _Distortion("Distortion",Range(0,100)) = 10
        _RefractionAmount("Refract Amount",Range(0.0,1.0)) = 1.0
    }
    SubShader
    {
        Tags{"Queue" = "Transparent" "Rendertype" = "Opaque"}

        

        GrabPass{"_RefractionTex"}
        

        Pass 
        {
            CGPROGRAM

            #pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            samplerCUBE _CubeMap;
            float4 _Distortion;
            fixed _RefractionAmount;
            sampler2D _RefractionTex;
            float4 _RefractionTex_TexelSize;

            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; 
                float4 scrPos : TEXCOORD5;
			};

            v2f vert(a2v v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.scrPos = ComputeGrabScreenPos(o.pos);
			 
			 	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 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                fixed3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.zw));

                float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
                fixed3 refrCol = tex2D(_RefractionTex,i.scrPos.xy / i.scrPos.w).rgb;

                bump = normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump)));
                fixed3 reflDir = reflect(-worldViewDir,bump);
                fixed4 texColor = tex2D(_MainTex,i.uv.xy);
                fixed3 reflCol = texCUBE(_CubeMap,reflDir).rgb * texColor.rgb;

                fixed3 finalColor = reflCol * (1-_RefractionAmount) + reflCol * _RefractionAmount;

                return fixed4(finalColor,1);
            }
            ENDCG
        }
        
    }
}

  • 程序纹理:由计算机生成的图像。这种的好处是可以用参数来控制纹理的外观等各种属性。具体可以去Shadertoy里膜拜各路神仙。
  • 程序材质:本质上一样,但是使用的纹理是程序纹理。程序纹理一般在Substance Designer中进行生成。

纹理动画

  • Unity内置时间变量:
image 5 1024x254 - 《Unity Shader入门精要》笔记:中级篇(2)
  • 帧序列动画
Shader "Example/Shader08" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Image Sequence", 2D) = "white" {}
    	_HorizontalAmount ("Horizontal Amount", Float) = 4
    	_VerticalAmount ("Vertical Amount", Float) = 4
    	_Speed ("Speed", Range(1, 100)) = 30
	}
	SubShader {
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
		
		Pass {
			Tags { "LightMode"="ForwardBase" }
			
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			
			CGPROGRAM
			
			#pragma vertex vert  
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			float _HorizontalAmount;
			float _VerticalAmount;
			float _Speed;
			  
			struct a2v {  
			    float4 vertex : POSITION; 
			    float2 texcoord : TEXCOORD0;
			};  
			
			struct v2f {  
			    float4 pos : SV_POSITION;
			    float2 uv : TEXCOORD0;
			};  
			
			v2f vert (a2v v) {  
				v2f o;  
				o.pos = UnityObjectToClipPos(v.vertex);  
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
				return o;
			}  
//播放关键帧动画要计算每个时刻要播放关键帧在纹理中的位置
			fixed4 frag (v2f i) : SV_Target {
				float time = floor(_Time.y * _Speed);  
//行索引,列索引,通过时间计算
				float row = floor(time / _HorizontalAmount);
				float column = time - row * _HorizontalAmount;
//Unity中纹理坐标顺序从下到上,而序列帧纹理顺序从上至下所以要用-row
				half2 uv = i.uv + half2(column, -row);
				uv.x /=  _HorizontalAmount;
				uv.y /= _VerticalAmount;

				fixed4 c = tex2D(_MainTex, uv);
				c.rgb *= _Color;
				
				return c;
			}
			
			ENDCG
		}  
	}
	FallBack "Transparent/VertexLit"
}

  • 顶点动画
v2f vert(a2v v) 
{
				v2f o;
				
				float4 offset;
				offset.yzw = float3(0.0, 0.0, 0.0);
				offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
				o.pos = UnityObjectToClipPos(v.vertex + offset);
				
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv +=  float2(0.0, _Time.y * _Speed);
				
				return o;
}
image 6 1024x246 - 《Unity Shader入门精要》笔记:中级篇(2)
  • 广告牌技术(Billboarding):也属于顶点动画的一种,根据视角方向来旋转视图,让其看起来总是朝向摄像机。
  • 广告牌技术就是让法线永远垂直指向观察者的视角,因此需要更改包含法线在内的正交的基向量,以此来重新构建坐标系的方向。在计算三个基向量时,需要首先固定其中一个,法线或者向上的基向量。下面的例子是固定发现时候的情况。
Shader "Example/Billboard" {
	Properties {
		_MainTex ("Main Tex", 2D) = "white" {}
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
//同于约束垂直方向的程度
		_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 
	}
	SubShader {
//DisableBatching设置是否使用批处理,顶点动画如果使用批处理那么可能会丢失自己的模型空间
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
		
		Pass { 
			Tags { "LightMode"="ForwardBase" }
			
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			Cull Off
		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed4 _Color;
			fixed _VerticalBillboarding;
			
			struct a2v {
				float4 vertex : POSITION;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
			};
			
			v2f vert (a2v v) {
				v2f o;
				
				float3 center = float3(0, 0, 0);
				float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));
//_VerticalBillboarding =1法线方向为固定视角方向,_VerticalBillboarding =0为向上方向为固定视角方向。然后需要对计算得到的法线方向进行归一化操作来得到单位矢量
				float3 normalDir = viewer - center;
				normalDir.y =normalDir.y * _VerticalBillboarding;
				normalDir = normalize(normalDir);
//cross是计算叉乘的意思
				float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
				float3 rightDir = normalize(cross(upDir, normalDir));
				upDir = normalize(cross(normalDir, rightDir));
				
				float3 centerOffs = v.vertex.xyz - center;
				float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
              
				o.pos = UnityObjectToClipPos(float4(localPos, 1));
				o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target {
				fixed4 c = tex2D (_MainTex, i.uv);
				c.rgb *= _Color.rgb;
				
				return c;
			}
			
			ENDCG
		}
	} 
	FallBack "Transparent/VertexLit"
}

LEAVE A COMMENT