阿卡内酱
阿卡内酱
发布于 2026-04-14 / 0 阅读
0
0

09.复杂光照

9.1 Unity渲染路径

主要有三种:前向渲染路径 (Forward Rendering Path) 、延迟渲染路径 (Deferred Rendering Path) 和顶点照明渲染路径 (Vertex Lit Rendering Path)

渲染路径的选择决定了shader所能获得的数据量、类型和所能使用的内置函数等等。

表9.1 LightMode标签支持的渲染路径设置选项

标 签 名

描 述

Always

不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照

ForwardBase

用于前向渲染 。该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和Lightmaps

ForwardAdd

用于前向渲染 。该Pass会计算额外的逐像素光源,每个Pass对应一个光源

Deferred

用于延迟渲染 。该Pass会渲染G缓冲(G-buffer)

ShadowCaster

把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中

PrepassBase

用于遗留的延迟渲染 。该Pass会渲染法线和高光反射的指数部分

PrepassFinal

用于遗留的延迟渲染 。该Pass通过合并纹理、光照和自发光来渲染得到最后的颜色

Vertex、VertexLMRGBM和VertexLM

用于遗留的顶点照明渲染

1、前向渲染路径:
有ForwardBase和ForwardAdd两种,

比较

ForwardBase

ForwardAdd

负责计算的光照

亮度最高的平行光Directional+所有逐顶点光源+环境光、自发光、球谐光照(SH)、光照贴图

除亮度最高的平行光外的平行光(平行光也是逐像素的)+其他所有逐像素光源

调用

每个渲染对象必定调用一次,1/物体

每个逐像素光源影响物体时执行一次,N 次/物体(N = 影响该物体的非主逐像素光源数)

混合模式

通常 Blend Off 或 Blend One Zero或不写

只能为Blend One One

阴影

可开启主光的阴影映射,通过 SHADOW_COORDSTRANSFER_SHADOWSHADOW_ATTENUATION 等宏实现

每个光源可独立开启阴影,但需要在片元着色器中为该光源单独采样阴影纹理

顶点计算

支持逐顶点光源,在顶点着色器内计算多个顶点光总和

没有顶点光源计算,所有光照在片元着色器中进行(逐像素)

颜色累加

最终颜色直接输出到 framebuffer

颜色通过加法混合累加到已有像素上,因此需使用 漫反射项+高光项,无需重新计算环境/自发光

预编译指令

# pragma multi_ compile_fwdbase

# pragma multicompile_fwdadd,若要为点光源与聚光灯实现阴影效果则使用# pragma multi_compile fwdadd_fullshadows代替

片元着色器颜色计算值

需要计算所有基础光照,包括环境光ambient,漫反射diffuse和高光specular

只需要计算该光源的漫反射diffuse与高光贡献specular

一般首先定义一个或多个作为ForwardBase的Pass,然后定义一个Pass来作为ForwardAdd

光源处理方式:

  • 场景中最亮的平行光总是按逐像素处理的。

  • 渲染模式被设置成Not Important 的光源,会按逐顶点或者SH处理。

  • 渲染模式被设置成Important 的光源,会按逐像素处理。

  • 如果根据以上规则得到的逐像素光源数量小于Quality Setting 中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。

2、顶点照明渲染路径
低性能渲染,计算逐顶点光照

3、延迟渲染路径
通过G-Buffer来降低渲染开销,但实现的效果有限。

  • 不支持真正的抗锯齿(anti-aliasing)功能。

  • 不能处理半透明物体。

  • 对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。

官方的渲染路径比较文档:http://docs.unity3d.com/Manual/RenderingPaths.html

9.2 Unity的光照类型及前向渲染实现

9.2.1 光源类型

平行光光照范围没有限制且无衰减,而且默认光线会穿透所有物体

9.2.2 前向渲染实现

代码:采用Blinn-Phong模型,直接复制第六章的基础Blinn-Phong实现代码即可,两个Pass除了Tags和预编译的不同,其余皆相同
第一个Pass:ForwardBase
Tags与预编译指令:因为变量unity_WorldToLight

Pass {
    // Pass for ambient light & first pixel light (directional light)
    Tags { "LightMode"="ForwardBase" }

    CGPROGRAM
    // Apparently need to add this declaration 
    #pragma multi_compile_fwdbase
    #include "AutoLight.cginc"

片元着色器:得到环境光ambient、漫反射diffuse与高光specular,然后再计算光的衰减,因为平行光无衰减,于是这里令衰减值attenuation为1

// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
...
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

// The attenuation of directional light is always 1
fixed atten = 1.0;

return fixed4(ambient + (diffuse + specular) * atten, 1.0);

第二个Pass:ForwardAdd
Tags与预编译指令:

Pass {
    // Pass for other pixel lights
    Tags { "LightMode"="ForwardAdd" }

    Blend One One

    CGPROGRAM

    // Apparently need to add this declaration 
    #pragma multi_compile_fwdadd

片元着色器
计算光源方向:如果当前渲染的光源类型为平行光,则引擎会定义USING_DIRECTIONAL_LIGHT,且_WorldSpaceLightPos0就是光源方向。否则_WorldSpaceLightPos0为光源位置。

#ifdef USING_DIRECTIONAL_LIGHT
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
#endif

处理光源衰减:对于平行光衰减依旧为1
Unity选择了使用一张纹理作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。我们首先得到该点在光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值。关于Unity中衰减纹理的细节可以参见9.3节.

#ifdef USING_DIRECTIONAL_LIGHT
    fixed atten = 1.0;
#else
    #if defined (POINT)
	    float3 lightCoord = mul(unity_WorldToLight, i.worldPos).xyz;
	    fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
	#elif defined (SPOT)
	    float4 lightCoord = mul(unity_WorldToLight, i.worldPos);
	    fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
	#else
	    fixed atten = 1.0;
	#endif
#endif

9.3 Unity的光照衰减

9.3.1 通过纹理获取光照

通过对预处理得到的纹理采样_LightTexture0来得到衰减值,一般是采样纹理对角线上的值,(0,0)是离光源最近的,(1,1)是离光源最远的
1、使用unity_WorldToLight来转换到光源空间中的位置
2、使用dot来对距离值取平方,不用开方消耗性能。
3、使用宏UNITY_ATTEN_CHANNEL来得到衰减纹理的衰减值所在的通道

9.3.2 通过数学公式计算得到

例如:

float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
atten = 1.0 / distance; // linear attenuation

9.4 Unity的阴影

9.4.1 阴影实现技术

Shadow Map技术:将摄像机放置到与光源重合的位置上,摄像机看不见的地方就为该光源的阴影区域。Unity使用的就是这种技术。

前向渲染的Shadow Map: 若平行光开启了阴影,Unity会为其计算它的阴影映射纹理(shadowmap),本质为一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。

ShadowCaster-LightMode:在Pass的Tags中,可为设置Tags的LightMode为ShadowCaster。这个Pass的渲染目标是阴影映射纹理(或深度纹理)。Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。
因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Unity Shader中找到LightMode 为ShadowCaster 的Pass,如果没有,它就会在Fallback 指定的Unity Shader中继续寻找,直到找到为止或者不再有Fallback,如果仍然没有找到,该物体就无法向其他物体投射阴影(但它仍然可以接收来自其他物体的阴影)。当找到了一个LightMode 为ShadowCaster 的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。一般Fallback的终点为VertexLit,其实现了ShadowCaster。

屏幕空间的阴影映射技术 (Screenspace Shadow Map):新的Unity所使用的技术,需要显卡支持MRT。当使用了屏幕空间的阴影映射技术时,Unity首先会通过调用LightMode 为ShadowCaster 的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。

9.4.2 不透明物体的阴影

组件信息:
在Light组件可以设置一个光源是否能投射阴影。
对于物体的Mesh Renderer组件,可以设置Cast Shadow属性来决定该物体是否能够向其他物体投射阴影。Two Sided属性可以针对类似于Plane这样的只渲染一面的物体,或者是一些镂空的,但是背面被剔除的物体,也可以两个面都能投射阴影;勾选Receive Shadows则可以使其接收到其他物体投射的阴影。

代码初步实现接收其他物体的投影
因为ShadowCaster的Pass是实现向其他物体投影的,所以需要再额外写代码实现接收其他物体的投影
首先先复制上一节的代码,下面的代码皆在ForwardBase的Pass内进行修改
预编译:

#include "AutoLight.cginc"。

结构体定义:在v2f中添加SHADOW_COORDS宏,括号内参数是下一个可用的插值寄存器的索引值(为下一个未被使用的TEXCOORD的尾号值)

struct v2f {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
    SHADOW_COORDS(2)
};

顶点着色器:使用TRANSFER_SHADOW宏计算shadow的uv坐标
注意!!!!!,若要使用TRANSFER_SHADOW,则a2v的坐标值必须命名为vertex,且顶点着色器函数的参数必须命名为v,且v2f的坐标值必须命名为pos!!!

v2f vert(a2v v) {
    v2f o;  
    ...     
    // Pass shadow coordinates to pixel shader
    TRANSFER_SHADOW(o);

    return o;
}

片元着色器:

// Use shadow coordinates to sample shadow map
fixed shadow = SHADOW_ATTENUATION(i);

return fixed4(ambient + (diffuse + specular) * atten * shadow, 1);

总结:

  • SHADOW_COORDS :实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量。

  • TRANSFER_SHADOW :实现会根据平台不同而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了UNITYNO_SCREENSPACE SHADOWS 来得到),TRANSFER_SHADOW 会调用内置的ComputeScreenPos函数来计算_ShadowCoord;如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW 会把顶点坐标从模型空间变换到光源空间后存储到_ShadowCoord中。

  • SHADOW_ATTENUATION:负责使用_ShadowCoord对相关的纹理进行采样,得到阴影信息。

9.4.4 完整一步实现衰减与阴影

只需要在两个Pass内都使用UNITY_LIGHT_ATTENUATION宏就可以同时得到衰减以及阴影值。
代码:两个Pass内都这样实现
预编译:

#include "Lighting.cginc"
#include "AutoLight.cginc"

结构体:

struct v2f {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
    SHADOW_COORDS(2)
};

顶点着色器:

v2f vert(a2v v) {
    v2f o;
    ...
    TRANSFER_SHADOW(o);

    return o;
}

片元着色器:第一个参数atten不需要我们声明,UNITY_LIGHT_ATTENUATION会帮我们声明。第二个参数为

fixed4 frag(v2f i) : SV_Target {
    ...

    // UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

    return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

UNITY_LIGHT_ATTENUATION解释:

  • 第一个参数atten:所获得的值为衰减值与阴影值得乘积。不需要我们声明,UNITY_LIGHT_ATTENUATION会帮我们声明。

  • 第二个参数为v2f结构体,用以计算阴影值。

  • 第三个参数用以获取光照衰减。且此参数为三维向量!

9.4.5 透明度物体的阴影

对于AlphaTest:
按照上面的步骤实现阴影接收,最后的atten乘在diffuse上即可。
但为了实现镂空的情况下的正确的阴影投射,Fallback需要设置为Transparent/Cutout/VertexLit,普通的VertexLit是针对非透明物体的

对于AlphaBlend:
因为对于半透明物体的阴影处理特别复杂且消耗性能,所以unity内置的所有半透明shader都不会实现投影与阴影接收等阴影效果。但可以在Shader内将Fallback设置为一些不透明的shader比如VertexLit、Diffuse来强制生成投影。
且此时需要将SubShader的Tags的Queue设置为“Geometry+10”,因为Unity底层默认会直接跳过Transparent队列的阴影接收计算!


评论