本文从工程视角总结 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(阴影贴图)流程
- 从光源视角渲染场景深度 → Shadow Map
- 主渲染时把像素位置变换到光空间 →
shadowCoord - 采样 Shadow Map 并比较深度
- 得到
shadowAttenuation - 乘到直射光贡献:
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 生成阶段需要渲染你的物体深度:
- 通常需要一个
ShadowCasterpass(概念上与 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(自己算光照)
建议从"基本正确"开始扩展:
- Main Light 的 diffuse(Lambert/Toon 都可)
- 主光阴影
shadowAttenuation - 再扩展高光、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 的等价核心通常是这三步:
- 算 shadowCoord :
TransformWorldToShadowCoord(positionWS) - 采主光阴影 :
MainLightRealtimeShadow(shadowCoord) - 乘到你的主光贡献上(或直接乘到颜色上)
示意(概念级):
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 里是宏为主。逻辑完全一样,只是接口换了。