Unity Shader 入门精要阅读笔记_进阶光照和高级纹理



前言

这次是更加复杂的光照


提示:以下是本篇文章正文内容,下面案例可供参考

一、更加复杂的光照

1.1 Unity的渲染路径

渲染路径(Rendering Path)决定了光照是如何应用到Unity Shader中的

主要有以下

  • 前向渲染路径(Forward Rendering Path)
  • 延迟渲染路径 (Deffered Rendering Path)

一般情况下,一个项目只用得到一种渲染路径,但是需要的话,可以在摄像机的渲染路径中重新设置,优先级比项目的高
在这里插入图片描述

1.1.1前向渲染路径

一次完整的前向渲染,需要渲染该对象的渲染图元,并且计算两个缓冲区(颜色和深度)的信息

对于每个逐像素的光源,都会执行一次以下流程,假如有多个逐像素光源,该物体就需要执行很多次pass,并且再帧缓冲中加起来得到最终结果
在这里插入图片描述

unity中的前向渲染路径有三种处理光照(怎么照亮物体)的方式

  • 逐顶点处理
  • 逐像素处理
  • 球谐函数处理

一个光源用那种处理模式取决于她的类型和渲染模式

  • 光源类型指光源是平行光还是其他的
  • 渲染模式指的是该光源是否重要(Important)

在前向渲染中,Unity会自动给所有的光源排个序,一定数量的光源会按照逐像素,顶多四个会按照逐顶点

判断规则:
在这里插入图片描述

在这里插入图片描述

  • 环境光和自发光计算只会在Bass中被计算一次
  • 对于前向渲染,通常会有一个Base pass和一个Additional Pass,Basspass智慧调用一次,而Additional Pass会被调用多次
  • Additional Pass 会有混合叠加(Blend one one)

在这里插入图片描述
在这里插入图片描述

1.1.2 延迟渲染路径

前向渲染有个问题。当场景中包含大量光源的时候,前向渲染的性能会激素下降

延迟渲染除了前向渲染的颜色和深度缓冲外,还有G缓冲,G是Geometry的缩写,存储了所关心的表面的其他信息,比如法线、位置、和光照计算的材质属性

延迟渲染主要包含了两个Pass

  • 第一个Pass 不进行任何光照计算,仅仅计算那些片元可见,当发现哪个片元可见,就把他存储到G缓冲区
  • 第二个pass,利用G缓冲区的各个片元信息,进行真正的光照计算

1.2 Unity的光源类型

平行光、点光源、聚光灯
(其实还有个面光源)

我们会在Unity Shader中访问他们的5个属性:位置、方向、颜色、强度、衰减
在这里插入图片描述

  • ,如果有多个平行光Unity会选择最亮的平行光传递给Base Pass 进行逐像素处理,其他的平行光会在Additional Pass 中计算
  • 如果场景中没有平行光,Bass Pass会当成全黑的处理

对于 Additional Pass,她的光照处理和Base Pass的是一样的。由于Additional Pass 处理的光源不仅仅是平行光,在计算光源的五个属性时,颜色和强度仍然可以使用_LightColor0,但是其他三个要根据光源类型。

  • 光源方向的计算:在这里插入图片描述
  • 衰减
    在这里插入图片描述
    Unity 使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减(Lookup Table , LUT),我们首先得到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样

1.3 Unity的光照衰减

Unity 使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减。

弊端如下

  • 需要预处理得到采样纹理
  • 不直观

纹理
(0,0)表明了与光源重合的点的衰减值,(1,1)点表明了在光源空间中最远位置的点的衰减值。
在这里插入图片描述
公式
在这里插入图片描述

1.4 Unity的阴影

Shadow Map 他把摄像机放在光源的位置上去看,看不到的地方就是阴影区域

我们使用一个额外的pass来判定shadowmap(本质上是一个深度图,记录了从光源方向看到的最近表面位置),这个Pass 就是LightMode设为ShadowCaster的Pass。当开启光源的阴影效果后,底层渲染引擎会先找到这个Pass,更新光源的阴影映射纹理

屏幕空间的阴影映射技术(Screenspace Shadow Map),SSM原本时延迟渲染中产生阴影的方法

  • 先会调用LightMode为ShadowCaster的Pass 获得SM,和摄像机的深度纹理
  • 根据光源的SM和摄像机的深度纹理来获得屏幕空间的阴影图
  • 两个做对比,摄像机的深度图记录的表面深度大于转换到阴影映射纹理的深度值,这里就是阴影区域
  • tip:我们需要把表面坐标从模型空间变到屏幕空间下,进行采样

总结

  • 接受阴影:在Shader中对阴影映射纹理进行采样
  • 投射阴影:把该物体加入到光源的阴影映射纹理的计算中

1.4.1 不透明物体的阴影

Mesh Renderer Cast Shadows 和 Receive Shadows 来设置。
开启了Cast Shadows ,Unity会把该物体加入光源的阴影映射纹理的计算,这是一个 LightMode为 ShadowCaster的Pass来实现的

对于自定义的shader, LightMode为 ShadowCaster的Pass是定义在Fallback内的,而我们要自定义阴影投射代码。具体流程如下

  • 顶点着色器的输出结构体添加内置宏:SHADOW_COORDS
    声明一个用于对阴影纹理采样的坐标,参数为下一个可用的插值寄存器的索引值在这里插入图片描述

  • 在顶点着色器内加入另一个内置宏:TRANSFER_SHADOW
    用于计算阴影纹理坐标
    在这里插入图片描述

  • 在片元着色器中计算阴影值,使用了内置宏SHADOW_ATTENUATION
    在这里插入图片描述

  • 把阴影值和漫反射以及高光颜色相乘即可

1.4.2 统一管理光照衰减和阴影

光照筛检和阴影对物体最终的渲染的结果本质相同,把光照因子和阴影值通光照结果相乘得到最终的渲染结果
即:UNITY_LIGHT_ATTENUATION
在这里插入图片描述

  • 第一个参数atten会被他自己声明,用来存储输出的结果
  • 第二个参数是结构体v2f,这个参数会传递给SHADOW_ATTENUATION ,计算阴影值
  • 第三个参数是世界空间坐标,采样得到光照衰减

二、高级纹理

2.1 立方体纹理

可以用于天空盒子,和环境映射。

对立方体纹理采样,需要提供一个三维的纹理坐标,这个三维坐标表示了我们再世界坐标下的一个3D方向。
在这里插入图片描述

天空盒子不用多说。

2.1.1 环境映射

对于反射主要有两个要点:

  • 在顶点着色器中计算该顶点处的反射方向,计算入射光线的方向
    在这里插入图片描述

  • 在片元着色器中采样cubemap
    在这里插入图片描述
    对于折射,要注意到斯涅尔定律
    在这里插入图片描述
    在这里插入图片描述

  • 首先还是要计算透射入射方向
    在这里插入图片描述
    reflect ratio 指的是不同介质投射比,作为参数输入

  • 然后做采样
    在这里插入图片描述
    对于菲涅尔反射
    菲尼尔效应描述了一种现象,当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部,繁盛折射或者散射,这个悲反射的光和入射光之间存在一定的比率关系,具体的例子就是远处的水看不到水底,近处的水可以看得到。

以下时两个菲尼尔近似等式
在这里插入图片描述

Shader "Unity Shaders Book/Chapter 10/Fresnel" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
		_Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" "Queue"="Geometry"}
		
		Pass { 
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			#pragma multi_compile_fwdbase
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Color;
			fixed _FresnelScale;
			samplerCUBE _Cubemap;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldPos : TEXCOORD0;
  				fixed3 worldNormal : TEXCOORD1;
  				fixed3 worldViewDir : TEXCOORD2;
  				fixed3 worldRefl : TEXCOORD3;
 	 			SHADOW_COORDS(4)
			};
			
			v2f vert(a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				
				o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
				
				o.worldRefl = 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;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb;
				
				fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);
				
				fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
				
				fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;
				
				return fixed4(color, 1.0);
			}
			
			ENDCG
		}
	} 
	FallBack "Reflective/VertexLit"
}

2.2 渲染纹理

GPU允许我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理(RTT)。与之相关的时多重渲染目标(MRT),这种技术指的是GPU允许我们把场景同时渲染到多个RTT中,而不需要为每个渲染目标单独渲染完整的场景。

Unity里,专门为RTT定义了一种专门的纹理类型,渲染纹理。

实现镜子效果
设置一个render texture ,用一个摄像机照射想要镜子中出现的画面,然后设置摄像机的target texure 为该render texture。

镜子的实现原理就是使用一个渲染纹理作为输入,然后水平翻转并且直接显示到物体上。

Shader "Unity Shaders Book/Chapter 10/Mirror" {
	Properties {
		_MainTex ("Main Tex", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" "Queue"="Geometry"}
		
		Pass {
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			sampler2D _MainTex;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
			};
			
			v2f vert(a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				
				o.uv = v.texcoord;
				// Mirror needs to filp x
				o.uv.x = 1 - o.uv.x;
				
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				return tex2D(_MainTex, i.uv);
			}
			
			ENDCG
		}
	} 
 	FallBack Off
}

实现玻璃效果
当我们在Shader中定义了一个GrabPass后,Unity会把当前屏幕的画面绘制在一张纹理中

一些先验tip:
在这里插入图片描述
结论成立

这个代码有些地方挺难看懂、、

Shader "Unity Shaders Book/Chapter 10/Glass Refraction" {
//shader需要的属性
	Properties {
	//玻璃的材质纹理、法线纹理、环境纹理、折射的扭曲程度
		_MainTex ("Main Tex", 2D) = "white" {}
		_BumpMap ("Normal Map", 2D) = "bump" {}
		_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {}
		_Distortion ("Distortion", Range(0, 100)) = 10
		_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
	}
	SubShader {
		// We must be transparent, so other objects are drawn before this one.
		//Transparent可以确保其他所有的不透明物体都已经被渲染到了屏幕上
		//Opaque在使用着色器替换的时候,该物体可以在被需要时正确渲染
		Tags { "Queue"="Transparent" "RenderType"="Opaque" }
		
		// This pass grabs the screen behind the object into a texture.
		// We can access the result in the next pass as _RefractionTex
		//定义了一个抓取屏幕图像的pass,这个pass定义了一个字符串,该字符串内部的名称决定了抓取到的屏幕图像会被存入哪个纹理
		GrabPass { "_RefractionTex" }
		
		Pass {		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _BumpMap;
			float4 _BumpMap_ST;
			samplerCUBE _Cubemap;
			float _Distortion;
			fixed _RefractAmount;
			sampler2D _RefractionTex;
			float4 _RefractionTex_TexelSize;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT; 
				float2 texcoord: TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float4 scrPos : TEXCOORD0;
				float4 uv : TEXCOORD1;
				float4 TtoW0 : TEXCOORD2;  
			    float4 TtoW1 : TEXCOORD3;  
			    float4 TtoW2 : TEXCOORD4; 
			};
			
			v2f vert (a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//内置的函数得到对应被抓取得屏幕图像的采样坐标
				//即屏幕坐标
				o.scrPos = ComputeGrabScreenPos(o.pos);
				//计算了采样坐标
				o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);

				float3 worldPos = mul(_Object2World, v.vertex).xyz;  
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  
				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  
				fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 
				
				//我们要在片元着色器中把法线方向从切线空间变到世界空间下,对cube采样
				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));
				
				// Get the normal in tangent space
				fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));	
				
				// Compute the offset in tangent space
				//对屏幕图像进行采样,一个1x1的图片
				//结合先验知识看
				float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
				i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
				// 得到真正的屏幕坐标,透视除法
				fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
				
				// Convert the normal to world space
				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 - _RefractAmount) + refrCol * _RefractAmount;
				
				return fixed4(finalColor, 1);
			}
			
			ENDCG
		}
	}
	
	FallBack "Diffuse"
}

猜你喜欢

转载自blog.csdn.net/woshi_wst/article/details/128756172