【Unity Shader】高级光照与阴影总结:渲染路径、多光源、透明阴影

本文从工程视角总结 URP 光照与阴影的关键机制:光照由哪些部分组成、Forward/Deferred 的差异如何影响 Shader 设计、多光源如何进入 Shader、阴影的计算链路、以及透明阴影为什么难。目标是把"写 Shader 时必须知道的输入与坑位"一次梳理清楚,便于长期复用。


0. 什么时候必须理解这些机制

以下场景:

  • 自定义光照/风格化(Toon/Ramp、边缘光、各向异性高光、假 SSS、特殊 BRDF)
  • 主光 + 多点光 + 聚光的正确叠加(含衰减、阴影)
  • 透明相关复杂效果(玻璃/薄膜/双面透明/自遮挡/透明写深度)
  • 需要同时正确 Cast/Receive(投影/受影)
  • 性能优化(多灯循环、阴影采样、透明 overdraw)

1. URP 的"光照"在算什么

1.1 直射光(Direct Lighting)

直射光来自场景灯(Directional/Point/Spot)的贡献,通常包含:

  • 漫反射 Diffuse:决定明暗与体积感
  • 镜面反射 Specular:决定高光与材质质感
  • 阴影 Shadow:决定"是否被遮挡"后的衰减

最小 Lambert(用于说明结构):

text 复制代码
diffuse = albedo * lightColor * max(0, dot(N, L))
  • N:单位法线(常用世界空间)
  • L:单位光方向(约定为"指向光源"的方向或其等价形式)
  • albedo:材质本色(BaseMap/颜色)
  • lightColor:灯颜色×强度

镜面反射常见两条路线:

  • 传统:Blinn-Phong(便宜、易控)
  • PBR:GGX/Disney BRDF(URP Lit/Shader Graph Lit 的主路径)

1.2 间接光(Indirect Lighting)

用于"没灯也不全黑",典型来源:

  • 环境光(Skybox/环境颜色)
  • Light Probe(动态物体的烘焙环境)
  • Lightmap(静态物体烘焙 GI)
  • Reflection Probe(反射环境 / IBL)
  • SH(球谐)

1.3 自发光(Emission)

物体自发光,不依赖灯光。常用于灯牌/能量特效,并配合 Bloom 强化观感。

工程上常见的合成结构:Final = (Direct × Shadow) + Indirect + Emission


2. 渲染路径(Rendering Path)与 Shader 的关系

2.1 Forward(前向)

特征:渲染对象时就把光照算完。

  • 主光 + 额外光(循环或多 Pass 叠加)
  • 透明几乎总走 Forward(混合与排序决定)

工程含义:

  • 多灯会线性变贵(每像素循环更多灯)
  • 透明 overdraw 叠加成本更高

2.2 Deferred(延迟)

特征:先写 GBuffer(材质信息),再统一算灯。

  • 对不透明的"多动态灯"通常更划算
  • 代价:更多缓冲带宽/显存;透明通常仍走 Forward

2.3 写 Shader 的视角总结

  • Forward:更适合做"自定义光照",但要面对多灯循环成本
  • Deferred:更像"填材质表(GBuffer)",想大改光照逻辑更麻烦
  • 透明:即使项目启用 Deferred,透明材质也往往要按 Forward 规则设计

3. 多光源如何进入 URP Shader

3.1 不是"场景所有灯"都会进入 Shader

URP 会对每个可见物体做光源剔除与限制,常见约束:

  • 距离/范围
  • Layer Mask
  • URP Asset:是否开启 Additional Lights
  • 最大额外灯数量(URP Asset)
  • 额外灯按 Per-Vertex / Per-Pixel 计算模式

结论:Shader 里拿到的 Additional Lights 是"精选列表",不是全场灯

3.2 Main Light 与 Additional Lights(工程习惯)

  • Main Light:通常 1 盏(常见为最重要的 Directional),通常带阴影
  • Additional Lights:点光/聚光等,数量受限;是否带阴影取决于配置与成本权衡

3.3 叠加方式(概念)

每盏灯贡献一份 diffuse/specular,最终累加:

  • N·L 决定明暗
  • 点光/聚光还会有距离/角度衰减
  • 阴影系数一般乘到该灯的直射贡献上(主光最常见)

4. 阴影原理:Shadow Map 的计算链路

4.1 阴影的本质

阴影是一个可见性系数:

  • shadowAttenuation ∈ [0,1]
  • 1 表示光照得到,0 表示被遮挡

4.2 Shadow Map(阴影贴图)流程

  1. 从光源视角渲染场景深度 → Shadow Map
  2. 主渲染时把像素位置变换到光空间 → shadowCoord
  3. 采样 Shadow Map 并比较深度
  4. 得到 shadowAttenuation
  5. 乘到直射光贡献:direct *= shadowAttenuation

4.3 高频坑位

  • Shadow Bias / Normal Bias:避免自阴影痘痘(acne)与"漂浮"(peter panning)
  • PCF:软阴影多点采样,成本与质量权衡明显
  • Cascades:定向光级联阴影(近清晰远省)
  • 阴影分辨率/距离:画质与性能强相关

5. Cast vs Receive:你的 Shader 到底需要做什么

5.1 Receive Shadows(受影)

在主渲染 pass 中:

  • 计算/获取 shadowCoord
  • 采样 Shadow Map 得到 shadowAttenuation
  • 把它乘到直射光贡献上

如果你写的是 Unlit/自定义 Lit 却不接阴影采样:

  • 物体阴影区域不会变暗
  • 视觉上更像"贴在屏幕上"

5.2 Cast Shadows(投影)

Shadow Map 生成阶段需要渲染你的物体深度:

  • 通常需要一个 ShadowCaster pass(概念上与 Built-in 一致)

不写/不兼容 ShadowCaster pass 的常见后果:

  • 物体不投影
  • 或透明物体投影与预期不符(取决于你在 ShadowCaster 中如何处理 alpha)

6. 透明物体的阴影为什么难(重点)

透明阴影困难的根源是信息量不足:

  • Shadow Map 擅长回答"挡 / 不挡"(近似二值遮挡)
  • 透明需要回答"挡多少 / 透多少",并且可能需要多层累积甚至彩色透光

6.1 Shadow Map 只记录"最近深度"

同一方向上每个 texel 只存最近一层深度:

  • 不透明:最近层挡住即可,模型匹配
  • 透明:可能多层薄膜/玻璃,每层只挡一部分,应该沿光线做累积
    • 但 Shadow Map 不保存多层信息 → 无法真实累计透光

6.2 透明依赖混合与排序,阴影生成阶段通常不混合

透明主渲染依赖 Color Buffer 混合;阴影图阶段通常只写深度,不做颜色混合。

因此"半透明程度"难以自然映射到阴影测试模型中。

6.3 常见工程妥协(URP)

  • 透明不投影(最常见)
  • 用 Cutout(clip)投影(树叶/栅栏最稳)
  • ShadowCaster pass 里做近似:clip 或用 alpha 近似遮光
  • 用假阴影(blob shadow/投影贴花)替代真实投影

7. 自定义 Shader 的最低实现建议

7.1 仅特效(Unlit)

  • 默认可以不做阴影
  • 若希望"受影":采样 shadowAttenuation 并乘到直射贡献(或整体颜色)
  • 若希望"投影":补齐 ShadowCaster pass(透明要明确策略:不投影 / clip / 近似)

7.2 自定义 Lit(自己算光照)

建议从"基本正确"开始扩展:

  1. Main Light 的 diffuse(Lambert/Toon 都可)
  2. 主光阴影 shadowAttenuation
  3. 再扩展高光、Additional Lights 循环、GI/Probe 等

7.3 透明材质的投影策略

优先明确需求:是否真的需要"半透明阴影"?

  • 树叶/网格:Cutout(clip)投影最稳定
  • 玻璃/薄膜:常见策略是不投影或用假阴影

8. 性能与质量的常见权衡点

  • 多光源:额外灯数量上限、Per-Pixel vs Per-Vertex
  • 阴影:分辨率、距离、级联数、PCF 采样数
  • 透明:overdraw、排序、是否写深度(稳定 vs 正确)
  • 自定义效果:优先复用 URP 的库函数与管线数据,减少重复实现与兼容成本

9. 再补充一个:Built-in(老管线)阴影三大宏:干什么、怎么用

这三件套的目标只有一个:
把阴影坐标从顶点传到片元,然后在片元里得到阴影衰减系数(0~1)。

9.1 SHADOW_COORDS(n)

作用 :在 v2f 里声明一个用于存阴影坐标的插值变量(本质是 TEXCOORDn)。

用法:

hlsl 复制代码
struct v2f
{
    float4 pos : SV_POSITION;
    SHADOW_COORDS(1)   // 占用 TEXCOORD1 来存阴影坐标
    float2 uv : TEXCOORD0;
};

9.2 TRANSFER_SHADOW(o)

作用 :在顶点着色器里,把阴影坐标算出来并写进 v2f

用法:

hlsl 复制代码
v2f vert(appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    TRANSFER_SHADOW(o);  // 把阴影坐标塞到 o 的 SHADOW_COORDS 里
    o.uv = v.uv;
    return o;
}

9.3 SHADOW_ATTENUATION(i)

作用:在片元着色器里采样阴影贴图,得到阴影衰减(0~1)。

  • 1:完全受光
  • 0:完全在阴影里
  • 中间值:PCF 软阴影的过渡

用法:

hlsl 复制代码
fixed4 frag(v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);

    half shadow = SHADOW_ATTENUATION(i);  // 关键:阴影系数
    col.rgb *= shadow;                    // 最常见:让阴影区域变暗

    return col;
}

Built-in 阴影三宏最小可运行骨架(仅示意)

hlsl 复制代码
#include "UnityCG.cginc"
#include "AutoLight.cginc"   // 阴影相关宏通常在这里/相关 include 中

struct appdata { float4 vertex:POSITION; float2 uv:TEXCOORD0; };
struct v2f { float4 pos:SV_POSITION; float2 uv:TEXCOORD0; SHADOW_COORDS(1) };

v2f vert(appdata v){
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    TRANSFER_SHADOW(o);
    return o;
}

fixed4 frag(v2f i):SV_Target{
    fixed4 col = tex2D(_MainTex, i.uv);
    half shadow = SHADOW_ATTENUATION(i);
    col.rgb *= shadow;
    return col;
}

这套宏的本质就是"声明阴影坐标 → 顶点写入 → 片元采样得到 shadow"。


9.4 URP 里怎么做同样的事(等价替代)

URP 不建议你用上面那套宏(很多时候直接不存在)。URP 的等价核心通常是这三步:

  1. 算 shadowCoordTransformWorldToShadowCoord(positionWS)
  2. 采主光阴影MainLightRealtimeShadow(shadowCoord)
  3. 乘到你的主光贡献上(或直接乘到颜色上)

示意(概念级):

hlsl 复制代码
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

float4 shadowCoord = TransformWorldToShadowCoord(positionWS);
half shadow = MainLightRealtimeShadow(shadowCoord);
lighting.rgb *= shadow;

URP 里是函数为主,Built-in 里是宏为主。逻辑完全一样,只是接口换了。

相关推荐
浪客川2 小时前
godot-rust入门案例
rust·游戏引擎·godot
RPGMZ2 小时前
RPGMakerMZ游戏引擎 地图角色顶部显示称号
javascript·游戏引擎·rpgmz·rpgmakermz
harrain19 小时前
拟合模型与虚幻引擎
游戏引擎·数字孪生·虚幻
努力长头发的程序猿1 天前
在Unity2d中,根据Y轴决定渲染顺序(URP项目适用)
unity
DaLiangChen1 天前
Unity 精准 Mesh 点击检测:穿透遮挡 + 单击双击识别
unity·游戏引擎
迪普阳光开朗很健康2 天前
Unity中new() 和实例化有什么区别?
unity·游戏引擎
mxwin2 天前
Unity Shader 极坐标特效 从数学原理到实战案例
unity·游戏引擎·shader·uv
魔士于安2 天前
unity 圆盘式 太空飞船
游戏·unity·游戏引擎·贴图·模型
陈言必行2 天前
Unity 之 Addressables 加载失败:路径变量未替换导致的 404 错误分析与解决
unity·游戏引擎