文章目录
前言
这次是更加复杂的光照
提示:以下是本篇文章正文内容,下面案例可供参考
一、更加复杂的光照
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"
}