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

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

使用深度和法线纹理

  • 深度纹理原理:该纹理是一张渲染纹理,存储一个高精度的深度值,其范围为[0,1],通常非线性分布。可以使用透视投影类型的摄像机,在模型从顶点变换到其次裁剪坐标系下,利用MVP变换矩阵得到顶点的最后一步,用投影矩阵来变换顶点,以此来得到深度信息。
    Unity中深度纹理可以来自真正的深度缓存,也可以由一个单独的Pass得到,取决于渲染路径和硬件。
//Unity中可以直接通过代码获取深度纹理,同理也可以设置法线:DepthTextureMode.Normals
//首先设置摄像机模式
camera.depthTextureMode = DepthTextureMode.Depth;
//然后再Shader代码中访问
_CameraDepthTexture

运动模糊

  • 速度映射图-更加广泛处理运动模糊的技术。该图中存储了每个像素的速度,然后使用这个速度来决定模糊的方向和大小。
    生成方法:1、把场景所有物体的速度渲染到一张纹理中,缺点是要修改场景中所有物体的Shader代码。 2、利用深度纹理在片元着色器中为每个像素计算其在世界空间下的位置,使用前一帧的视角*投影矩阵对顶点坐标进行变换,得到该位置在前一帧的NDC坐标,然后计算前一帧和当前帧的位置差,生成像素的速度。优点是可以在一个屏幕后处理步骤中完成整个效果模拟,缺点是需要在片元着色器中进行两次矩阵乘法的操作,对性能会有所影响。
//在.cs文件中设置摄像机的深度纹理
void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;
//前一帧的图像矩阵
		previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
}

void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			material.SetFloat("_BlurSize", blurSize);

			material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
//当前的图像矩阵,得到当前摄像机的视角矩阵和投影矩阵,相乘之后取逆,得到当前视角*投影矩阵的逆矩阵并传递给材质。
			Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
			Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
			material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
			previousViewProjectionMatrix = currentViewProjectionMatrix;

			Graphics.Blit (src, dest, material);
		} 
        else {
			Graphics.Blit(src, dest);
		}
}

//片元着色器的处理
fixed4 frag(v2f i) : SV_Target {
			// 得到当前像素的深度纹理
			float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
			//构建NDC坐标H,即把深度值重新映射回NDC
			float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
			float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
			float4 worldPos = D / D.w;
			
			float4 currentPos = H;
			float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
			previousPos /= previousPos.w;
			//根据像素前后位置的不同计算速度

			float2 velocity = (currentPos.xy - previousPos.xy)/2.0f;
			
			float2 uv = i.uv;
			float4 c = tex2D(_MainTex, uv);
			uv += velocity * _BlurSize;
			for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
				float4 currentColor = tex2D(_MainTex, uv);
				c += currentColor;
			}
			c /= 3;
			
			return fixed4(c.rgb, 1.0);
		}

全局雾效

  • Unity内置的雾效可以产生基于距离的线性或指数雾效。
  • 基于屏幕后处理的全局雾效:根据深度纹理来重建每个像素在世界空间下的位置。
//c#关键计算雾效部分
void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
//计算近裁剪平面的四个角对应的向量,并存储在矩阵中
			Matrix4x4 frustumCorners = Matrix4x4.identity;

			float fov = camera.fieldOfView;
			float near = camera.nearClipPlane;
			float aspect = camera.aspect;
//该像素的世界坐标=摄像机在空间下的位置+以及世界空间下该像素相对于摄像机的偏移量
//偏移量=linearDepth*interpolatedRay,linearDepth由深度纹理得到的线性深度值,interpolatedRay由顶点着色器输出并插值后得到的射线。
//interpolatedRay是对近裁剪平面4个角的某个特定向量的插值,首先进行toTop和toRight,起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量。计算公式如下:
			float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
			Vector3 toRight = cameraTransform.right * halfHeight * aspect;
			Vector3 toTop = cameraTransform.up * halfHeight;
//进行四个方向的计算
			Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
			float scale = topLeft.magnitude / near;

			topLeft.Normalize();
			topLeft *= scale;

			Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
			topRight.Normalize();
			topRight *= scale;

			Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
			bottomLeft.Normalize();
			bottomLeft *= scale;

			Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
			bottomRight.Normalize();
			bottomRight *= scale;

			frustumCorners.SetRow(0, bottomLeft);
			frustumCorners.SetRow(1, bottomRight);
			frustumCorners.SetRow(2, topRight);
			frustumCorners.SetRow(3, topLeft);

			material.SetMatrix("_FrustumCornersRay", frustumCorners);

			material.SetFloat("_FogDensity", fogDensity);
			material.SetColor("_FogColor", fogColor);
			material.SetFloat("_FogStart", fogStart);
			material.SetFloat("_FogEnd", fogEnd);

			Graphics.Blit (src, dest, material);
		} 
        else {
			Graphics.Blit(src, dest);
		}
	}

//shader部分
Shader "Example/Shade10" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_FogDensity ("Fog Density", Float) = 1.0
		_FogColor ("Fog Color", Color) = (1, 1, 1, 1)
		_FogStart ("Fog Start", Float) = 0.0
		_FogEnd ("Fog End", Float) = 1.0
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		float4x4 _FrustumCornersRay;
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		sampler2D _CameraDepthTexture;
		half _FogDensity;
		fixed4 _FogColor;
		float _FogStart;
		float _FogEnd;
		
		struct v2f {
			float4 pos : SV_POSITION;
			half2 uv : TEXCOORD0;
			half2 uv_depth : TEXCOORD1;
//存储插值之后的像素向量
			float4 interpolatedRay : TEXCOORD2;
		};
//原文中顶点着色器发部分工作是对于平台差异化进行了处理
		v2f vert(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);
			
			o.uv = v.texcoord;
			o.uv_depth = v.texcoord;
			
			#if UNITY_UV_STARTS_AT_TOP
			if (_MainTex_TexelSize.y < 0)
				o.uv_depth.y = 1 - o.uv_depth.y;
			#endif
			
			int index = 0;
			if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
				index = 0;
			} else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
				index = 1;
			} else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
				index = 2;
			} else {
				index = 3;
			}

			#if UNITY_UV_STARTS_AT_TOP
			if (_MainTex_TexelSize.y < 0)
				index = 3 - index;
			#endif
			
			o.interpolatedRay = _FrustumCornersRay[index];
				 	 
			return o;
		}
//产生雾效的部分
		fixed4 frag(v2f i) : SV_Target {
//重建像素在世界空间中的位置,
			float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
			float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
//基于高度的雾效模拟
			float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
			fogDensity = saturate(fogDensity * _FogDensity);
			
			fixed4 finalColor = tex2D(_MainTex, i.uv);
			finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
			
			return finalColor;
		}
		
		ENDCG
		
		Pass {
			ZTest Always Cull Off ZWrite Off
			     	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment frag  
			  
			ENDCG  
		}
	} 
	FallBack Off
}

image 13 1024x573 - 《Unity Shader入门精要》笔记:高级篇(2)

边缘检测

  • 使用Roberts算子进行边缘检测,Gx:{{-1,0},{0,1}} Gy{{0,-1},{1,0}}
//c#部分
using UnityEngine;

public class EdgeDetectNormalsAndDepth : PostEffectsBase {

	public Shader edgeDetectShader;
	private Material edgeDetectMaterial = null;
	public Material material {  
		get {
			edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
			return edgeDetectMaterial;
		}  
	}

	[Range(0.0f, 1.0f)]
//为1时只显示物体的描边
	public float edgesOnly = 0.0f;

	public Color edgeColor = Color.black;

	public Color backgroundColor = Color.white;
//控制对深度+法线纹理采样时的距离,从视觉效果来说,该值越大,描边越宽
	public float sampleDistance = 1.0f;
//控制领域深度值或者法线值相差多少时会被认定为一条边界
	public float sensitivityDepth = 1.0f;

	public float sensitivityNormals = 1.0f;
	
	void OnEnable() {
		GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
	}

//默认情况下,该函数会在所有透明和不透明的Pass中执行完毕后被立即调用,加上该属性则不会影响透明物体的Pass,该例子中意义为希望只对不透明的物体进行描边处理
	[ImageEffectOpaque]
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			material.SetFloat("_EdgeOnly", edgesOnly);
			material.SetColor("_EdgeColor", edgeColor);
			material.SetColor("_BackgroundColor", backgroundColor);
			material.SetFloat("_SampleDistance", sampleDistance);
			material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));

			Graphics.Blit(src, dest, material);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

//Shader

Shader "Example/Shader10" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_EdgeOnly ("Edge Only", Float) = 1.0
		_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
		_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
		_SampleDistance ("Sample Distance", Float) = 1.0
		_Sensitivity ("Sensitivity", Vector) = (1, 1, 1, 1)
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		fixed _EdgeOnly;
		fixed4 _EdgeColor;
		fixed4 _BackgroundColor;
		float _SampleDistance;
		half4 _Sensitivity;
		
		sampler2D _CameraDepthNormalsTexture;
		
		struct v2f {
			float4 pos : SV_POSITION;
//第一组存储屏幕颜色的采样纹理,以备平台差异化处理使用。剩余4组存储了使用Roberts算子时需要采样的纹理坐标。
			half2 uv[5]: TEXCOORD0;
		};
		  
		v2f vert(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);
			
			half2 uv = v.texcoord;
			o.uv[0] = uv;
			
			#if UNITY_UV_STARTS_AT_TOP
			if (_MainTex_TexelSize.y < 0)
				uv.y = 1 - uv.y;
			#endif
			
			o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
			o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
			o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
			o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;
					 
			return o;
		}
//计算对角线上两个纹理值的插值,返回值为0代表有边间,1代表没有。
		half CheckSame(half4 center, half4 sample) {
//得到法线和深度值
			half2 centerNormal = center.xy;
			float centerDepth = DecodeFloatRG(center.zw);
			half2 sampleNormal = sample.xy;
			float sampleDepth = DecodeFloatRG(sample.zw);
//把两个采样点的对应值相减并取绝对值,再乘以灵敏度,把差异值的每个分量相加再和一个阈值比较。根据比较结果进行是否有边界的判定
			half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
			int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
			float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
			int isSameDepth = diffDepth < 0.1 * centerDepth;
			
			return isSameNormal * isSameDepth ? 1.0 : 0.0;
		}
		
		fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
			half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
			half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
			half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
			half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
			
			half edge = 1.0;
			
			edge *= CheckSame(sample1, sample2);
			edge *= CheckSame(sample3, sample4);
			
			fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
			fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
			
			return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
		}
		
		ENDCG
		
		Pass { 
			ZTest Always Cull Off ZWrite Off
			
			CGPROGRAM      
			
			#pragma vertex vert  
			#pragma fragment fragRobertsCrossDepthAndNormal
			
			ENDCG  
		}
	} 
	FallBack Off
}

image 14 - 《Unity Shader入门精要》笔记:高级篇(2)
image 15 1024x653 - 《Unity Shader入门精要》笔记:高级篇(2)

卡通风格渲染

  • 卡通风格=轮廓线+(分界明显的纯色区域)高光
  • 5种类型的轮廓线渲染:
    1、基于观察角度和表面法线的轮廓线渲染。简单快速,局限性大,描边效果不尽人意。
    2、过程式几何轮廓线渲染。快速有效,使用表面平滑模型,不适用于立方体等类型的模型。
    3、基于图像处理的轮廓线渲染。适用任何模型,但深度和法线变化很小的模型无法被检测出来。
    4、基于轮廓边缘的轮廓线渲染。无法控制轮廓线的风格渲染,出现动画连贯性的问题。
    5、混合渲染方法,混合上述1-4中的若干方法进行渲染。
  • 添加高光
Shader "Example/Shader07" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
//控制漫反射色调的渐变纹理
		_Ramp ("Ramp Texture", 2D) = "white" {}
//轮廓线宽度,颜色
		_Outline ("Outline", Range(0, 1)) = 0.1
		_OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
		_Specular ("Specular", Color) = (1, 1, 1, 1)
		_SpecularScale ("Specular Scale", Range(0, 0.1)) = 0.01
	}
    SubShader {
		Tags { "RenderType"="Opaque" "Queue"="Geometry"}
		
		Pass {
			NAME "OUTLINE"
			//只渲染背面,以此来确定轮廓
			Cull Front
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			
			float _Outline;
			fixed4 _OutlineColor;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			}; 
			
			struct v2f {
			    float4 pos : SV_POSITION;
			};
			
			v2f vert (a2v v) {
				v2f o;
				
				float4 pos = mul(UNITY_MATRIX_MV, v.vertex); 
				float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
				normal.z = -0.5;
				pos = pos + float4(normalize(normal), 0) * _Outline;
				o.pos = mul(UNITY_MATRIX_P, pos);
				
				return o;
			}
			//沿着色器渲染整个背面
			float4 frag(v2f i) : SV_Target { 
				return float4(_OutlineColor.rgb, 1);               
			}
			
			ENDCG
		}
		
		Pass {
			Tags { "LightMode"="ForwardBase" }
			//渲染正面
			Cull Back
		
			CGPROGRAM
		
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
		
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			#include "UnityShaderVariables.cginc"
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _Ramp;
			fixed4 _Specular;
			fixed _SpecularScale;
		
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			}; 
		
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
				SHADOW_COORDS(3)
			};
			
			v2f vert (a2v v) {
				v2f o;
				
				o.pos = UnityObjectToClipPos( v.vertex);
				o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
				o.worldNormal  = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				TRANSFER_SHADOW(o);
				
				return o;
			}
			
			float4 frag(v2f i) : SV_Target { 
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
				
				fixed4 c = tex2D (_MainTex, i.uv);
				fixed3 albedo = c.rgb * _Color.rgb;
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				fixed diff =  dot(worldNormal, worldLightDir);
				diff = (diff * 0.5 + 0.5) * atten;

//与之前主要的区别在这里:在漫反射部分使用了渐变纹理
				fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
				
				fixed spec = dot(worldNormal, worldHalfDir);
				fixed w = fwidth(spec) * 2.0;
//高光部分用step函数来特判_SpecularScale=0时完全消除高光反射
				fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);
				
				return fixed4(ambient + diffuse + specular, 1.0);
			}
		
			ENDCG
		}
	}
	FallBack "Diffuse"
}
image 16 - 《Unity Shader入门精要》笔记:高级篇(2)
image 17 - 《Unity Shader入门精要》笔记:高级篇(2)
image 18 - 《Unity Shader入门精要》笔记:高级篇(2)
image 19 - 《Unity Shader入门精要》笔记:高级篇(2)

LEAVE A COMMENT