✔ ShaderLab
└── defining a Shader object
└── defining a SubShader
└── defining a Pass
└── adding shader programs
└── commands(用于配置状态)
✔ HLSL in Unity
└── 核心语言:变量声明、矩阵变换、光照计算、纹理采样等
Stencil
是哪个层级的?
-
属于 ShaderLab 的
Pass
块中Commands
层 -
和
ZTest
、ZWrite
是同级的像素级状态控制指令 -
是在 GPU rasterization 后、fragment shader 执行前 执行的
层级 | 负责什么 |
---|---|
ShaderLab |
包裹整个 Shader 的结构与状态配置 |
└── Commands |
设置渲染管线的状态(Stencil、ZTest) |
└── Stencil |
控制像素是否允许渲染/写入 |
CGPROGRAM |
包含实际执行的 vertex/fragment shader |
HLSLCode |
完整的计算逻辑、光照、纹理等 |

By default, the GPU writes to all channels (RGBA).
Offset 的本质是什么?
它不是视觉上的移动,而是让 GPU 对"Z 值"加一个偏移,从而影响 ZTest 和 ZWrite 的判断结果。

"RenderType"="Opaque" 有什么用?
这是 Unity 在做 后处理、阴影、光照、剔除等内部优化分类时的标记。
它不是决定 Shader 渲染行为的关键,而是:
给 Unity 的渲染系统或后处理系统一个分类提示,比如:
-
是否参与阴影贴图渲染
-
是否进入后处理剔除流程
-
SRP Batcher 是否批处理它等
Rasterization(像素生成)
↓
Stencil Test ←←←← 你设置的这段代码起作用在这里!
↓(通过才继续)
Depth Test (ZTest)
↓
Fragment Shader
↓
Color/Depth Buffer 写入

Stencil Buffer 在每一帧的开始阶段(或每个相机渲染阶段)会被清空,默认值是 0。
三个关键阶段(Stencil 操作类型):
阶段 | 触发条件 | 含义说明 |
---|---|---|
Pass |
stencil test ✅ 且 depth test ✅ | 片元通过 stencil + 深度测试 |
Fail |
stencil test ❌ | stencil 不通过 |
ZFail |
stencil ✅ 但 depth ❌ | stencil 通过,但被深度测试剔除 |
操作选项说明(你图中提到的每一项)
操作名 | 中文含义 | 数值行为 & 举例 |
---|---|---|
Keep |
保持原值 | 不修改 stencil buffer(最常见) |
Zero |
写入 0 | 抹除区域常用 |
Replace |
写入 Ref 值 | 最常用的写 stencil 的方式 |
IncrSat |
增加 1(饱和) | 254 → 255 → 255 (不会超) |
DecrSat |
减少 1(饱和) | 1 → 0 → 0 (不会负) |
Invert |
位取反 | 00000001 → 11111110 = 254 |
IncrWrap |
增加 1(循环) | 255 + 1 → 0 (超出变 0) |
DecrWrap |
减少 1(循环) | 0 - 1 → 255 (下溢变 255) |
应用建议:
操作组合 | 用途 |
---|---|
Comp Always + Pass Replace |
写入 stencil mask 区域 |
Comp Equal + Pass Keep |
只让特定 stencil 区域可见(不写入) |
Pass IncrSat + ZFail DecrSat |
分层地刷 stencil 计数(如地形穿插特效) |
Pass Zero |
清除模板值区域 |


关掉 ZWrite, 依然可以写入颜色缓冲区,只要你的片元通过了 ZTest 且启用了 ColorMask 和 Blend。
设置 | 行为 |
---|---|
ZTest Always |
所有片元都通过测试 → 能继续执行 Fragment Shader、写颜色 |
ZTest LEqual |
通过 Z 比较判断是否绘制(常用) |
ZTest Never |
所有片元都不通过测试 → Fragment 不执行 → 不写颜色 |
❗ZTest Off (非法) |
Unity 实际上不会接受这个值,结果和 ZTest Never 类似,物体消失 |

Unity 渲染顺序排序规则(按优先级):
-
RenderQueue 值(数值小的先渲染)
-
Material 的 Shader Pass(Tag)顺序
-
Material 实例 ID(material 被创建的先后)
-
MeshRenderer 的 Sorting Layer / Order(如果启用 SRP)
-
对象在 Hierarchy 中的顺序(具体实现非公开,但确实存在影响)
-
可能的优化批处理合并顺序(例如 SRP Batcher)
Designed for both realistic and stylized lighting
Basic physically-based rendering (PBR)
Easier to customize for custom lighting models such as cel shading
Unlike traditional ray tracing, which calculates exact intersections with geometric primitives, ray marching relies on signed distance functions (SDFs) to determine the distance from a point in space to the nearest surface.
Ray marching is a rendering technique that determines the intersection of rays with objects in a scene by advancing the ray in steps, guided by a Signed Distance Function (SDF).
Advancement: The ray advances by the evaluated distance. This process repeats until the distance is below a small threshold (indicating a surface hit) or a maximum number of steps or distance is reached (indicating no intersection).
Shading: Once an intersection is found, lighting calculations are performed to determine the color of the pixel.
This method is efficient because it avoids unnecessary calculations and can handle complex, procedurally defined scenes without explicit geometry.
LDR = Low Dynamic Range,低动态范围图像
通常指的是 8-bit 每通道 的图像格式,比如:
-
.png
(常见的 UI 图像) -
.jpg
(压缩的贴图) -
Unity 中大多数非 HDR 图像默认就是 LDR
"LDR 转码"可能指什么?
情况 1⃣:在渲染流程中转换为 HDR 使用
即:将 LDR 图像内容提升到 HDR 流程中,比如做后期处理、Bloom 等
这时,Unity 或 Shader 中需要将 LDR 色值(0~1):
-
转换为 线性空间
-
有时还要乘上曝光系数、Gamma 校正等

因为 LDR 渲染没有亮度爆炸 (所有颜色都压缩在 0~1),如果用 One One
累加容易溢出导致伪高光。
而 HDR 渲染可以存储 >1 的颜色 ,就允许你用 Blend One One
来叠加多个高亮源,实现自然的"亮区溢出"效果。

Shader 中的 HDR 模式是 针对渲染管线,不是贴图文件本身
所谓的"LDR 模式"和"HDR 模式"并不是直接判断图片是不是 HDR 图片,而是这个 Shader 是否运行在 HDR 渲染目标上(例如后处理的 RT 支持高亮度值 >1)
#pragma multi_compile __ UNITY_HDR_ON
如果你在 ForwardAdd / Deferred / PostEffect 中使用 HDR Framebuffer(比如 RenderTexture 设置了 HDR 格式如 ARGBHalf),那么 UNITY_HDR_ON 就会被启用。

图片格式会直接影响 Shader 中是否能产生或承载 HDR 效果,但它的作用不是决定 HDR 渲染"开不开",而是决定:
贴图本身是否能存储和输出 HDR 色值(即 >1.0 的亮度信息)
渲染流程中的 HDR 有两个维度
类型 | 控制作用 | 举例 |
---|---|---|
渲染目标是否 HDR | 控制最终 framebuffer 是否允许 HDR 值 | 摄像机开启 allowHDR ,RenderTexture 用 ARGBHalf |
贴图格式是否 HDR | 控制图片是否存储了 HDR 内容 | .exr 图,高光范围 >1.0 |
贴图格式 | 是否支持 HDR | 说明 |
---|---|---|
.png , .jpg , .tga |
❌ 否 | LDR,颜色范围最大为 0~1 |
.hdr , .exr |
✅ 是 | 支持 float 数据,值可 >1(如阳光 10.0) |
-
用了
.png
图片,即使你后面col * 100.0
也不叫 HDR 图。因为原始值已经被压缩死了。 -
你用了
.exr
并且在导入时设置为RGBAHalf
,它才是真正的 HDR 图源。
Shader 中怎么才能体现 HDR 效果?
必须三者都满足:
-
✅ 渲染目标是 HDR
摄像机
allowHDR
开启,或者你使用了RenderTextureFormat.ARGBHalf
-
✅ 贴图本身是 HDR 图(如 .exr)
导入时设置格式为
RGBAHalf
或RGBAFloat
-
✅ Shader 中不被 Gamma 或 ToneMapping 限制
-
使用
Blend One One
-
不用 saturate/clamp
-
最终走 PostProcess 才看到溢光 Bloom
-
位数(每通道) | 存储内容 | 亮度范围 | 示例格式 |
---|---|---|---|
8-bit | 存整数 0 ~ 255 | 实际映射为 0.0 ~ 1.0 | PNG, JPG |
16-bit float(half) | 存浮点值(可大于 1) | 支持 HDR,如 3.2, 10.0 | .exr , ARGBHalf |
32-bit float(full) | 更高精度 HDR | 如 0.0001 ~ 100000 | .exr , ARGBFloat |

Unity 不会 也不能真正 把 .jpg
/ .png
这样的 LDR 图片"变成" HDR 图片,原因在于:
✅ LDR 图(如 .jpg/.png)在磁盘上就已经失去了 HDR 信息,Unity 无法凭空生成。
HDR 图片资源 和LDR 图片资源 在 Unity 中的显示会有明显不一样的效果 ,但前提是你使用的是"支持 HDR 的渲染管线与设置"。否则,即使你导入了 HDR 图,也会被当作普通贴图对待而失去意义。
📌 如果你只是把 HDR 图(如
.exr
)当作一个普通贴图MainTex
,贴在一个物体上,不使用 HDR 相机、不经过后期处理(如 Bloom),那么:
➤ 它的显示效果会看起来和 .png
几乎没区别!
这是因为:
-
屏幕是 LDR 输出设备(
0~1
),高亮值被裁剪或 tone map 掉了 -
你没使用
HDR 渲染目标
→ 无法表达高于 1.0 的值 -
Shader 最终输出的是
SV_Target
限制在[0,1]
在HDR 渲染 + 后期(Bloom)等开启时,HDR 图效果显著不同!
在如下条件下,HDR 图像的高亮值会被正确渲染出来:
✅ 条件一:相机 Allow HDR
开启
在 Unity 相机设置中勾选:
[✔] Allow HDR
✅ 条件二:使用了 Bloom / Glow 等后处理(Post Processing)
例如使用 Unity 的 Post Processing Stack 中的 Bloom ,当图片中有亮度 >1
的区域时,会:
-
显示明显的溢光(Bloom)
-
显示能量更高的区域发光
-
呈现出一种"高动态"的感觉
都是刚导入unity的图片--默认格式
.hdr .jpg
延迟渲染之所以要"分多个 Render Target",是因为它先要"收集物体的物理属性",再统一"光照计算",所以必须把各种表面信息分别存起来。
Forward 渲染是"画一个人 → 给他涂光影 → 下一个人",
而 Deferred 渲染是"先画所有人的轮廓、材质、法线 → 最后一次性打灯照亮整个场景"。
Deferred 渲染的第一步就是把你所有看得见的物体的这些"物理表面信息"写进几个专用的缓冲区(Render Targets)里,这组缓冲我们称为:
✅ G-Buffer(Geometry Buffer)

✅ 为什么不能把所有数据塞进一个 Target?
因为:
-
Render Target 通常是 RGBA(4 通道 float)
-
一个 Target 最多装下 4 个 float 值
-
法线 是 3D 向量(3 个 float)
-
颜色 + 粗糙度 + 金属度 是 4 个 float
-
加起来根本装不下
这叫:
✅ MRT(Multiple Render Targets,多渲染目标输出)
(RT = RGBA4 通道)
在延迟渲染路径下 ,半透明物体不是"完全不能渲染",而是:
✅ 不能参与延迟光照计算 ,但可以在延迟路径后以 Forward(前向)方式额外渲染补上透明部分。
这点是延迟渲染架构的一个"混合渲染设计"。
为什么延迟渲染不支持半透明物体 参与主流程光照?
核心原因:
延迟渲染中光照计算发生在 每个屏幕像素上(Screen-Space),但:
半透明物体的特性 | 延迟渲染难以支持的点 |
---|---|
颜色和光照需要与背后的物体混合 | GBuffer 只能记录一个片元(最前的那一个) |
多个物体沿深度重叠、需逐层混合 | 延迟渲染只保存每像素最前面的信息 |
常用 Alpha Blending(如加法/乘法) | 无法在 GBuffer 中保留多个深度层 |
因此,半透明物体无法写入 GBuffer 并延迟光照处理。
Unity 是怎么解决的?
Unity 的做法是:
✔ 延迟渲染管线结构:
-
Opaque(不透明)物体 → Deferred Pass:主流程,输出 GBuffer,执行光照计算
-
Transparent(半透明)物体 → Forward Pass:绕开延迟流程,单独计算光照+混合
你在使用延迟渲染时,其实透明物体仍然被渲染,只是:
-
不进 GBuffer,不参与延迟的光照阶段
-
走的是 ForwardBase Pass
-
支持 Alpha Blending,手动计算光照
所以你可以看到半透明的水面、玻璃、粒子效果 仍然存在。
如何验证?
你可以在 Frame Debugger 打开一个延迟渲染项目,然后查看:
-
GBuffer Pass 写入的对象是 Opaque 类型
-
半透明物体(带 Alpha 或透明队列)会在 GBuffer 后走 Forward Pass
-
Shader 的 Tag 里写着
"LightMode" = "ForwardBase"
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "LightMode" = "ForwardBase" }
Blend SrcAlpha OneMinusSrcAlpha
这会让你的 Shader:
在延迟渲染中作为 Forward Pass 插入
得到单独计算的光照(但性能取决于光源数量)
所谓"正常显示"的意思是:
肉眼可见的内容,在屏幕上确实绘制出来了,半透明的效果也看起来"对",没有消失,也能混合、贴图、反射、水波等等------这就是"看起来正常"。
比如:
-
玻璃材质确实半透明,看得见背后
-
粒子烟雾确实飘着,看起来和 Forward 渲染没什么不同
-
水面波动有 Fresnel 高光,看起来很漂亮
问题 1:延迟渲染的 GBuffer 无法支持多个片元(透明层)
-
延迟渲染在 GBuffer 中,每个像素只能存储:
-
一个颜色(albedo)
-
一个法线
-
一个深度
-
一个高光参数......
-
-
但:
-
透明叠加(如玻璃 + 水 + 烟雾)需要 多层数据
-
延迟渲染只能记录最前面的那一层
-
结果:
❌ 后面的透明层被彻底"丢弃",你看到的只是最前面的一层,无法正确混合
无法使用透明混合(Blend)
-
延迟渲染阶段是全屏后处理(屏幕 Quad)
-
这个阶段已经不是 per-object 渲染了
-
Unity 的延迟光照 Pass 不支持混合模式(Blend)
-
而 Alpha Blend 依赖深度排序和混合
结果:
透明的加法、乘法、Alpha 衰减等效果全部无法执行
问题 4:多个透明物体重叠会严重错误(没有 Painter's Algorithm)
-
Forward 渲染透明是靠"按从后往前绘制"来模拟层叠
-
Deferred 中 GBuffer 阶段是"按深度近处优先"
-
所以即使你硬塞透明物体进去,ZTest 会挡掉后面的半透明层
定义一个"半透明物体"的本质是:
👉 这个物体的最终像素颜色,依赖于它后面的像素的存在,它需要和后面合成。
只要存在这种依赖,"你就不是个能走延迟管线的老实物体",而必须被分出去单独处理。
Unity 判断 Shader 是否应该 转入 Forward 路径而不是延迟光照,主要依据以下"硬条件":
条件 | 解释 |
---|---|
Blend 指令是否存在 |
✅ 一旦存在混合操作(如 Blend SrcAlpha OneMinusSrcAlpha ),将强制切换到 Forward |
渲染队列(Queue)是否是 Transparent 类别 | ✅ 若 Queue = Transparent (3000+),一般也会走 Forward |
ZWrite 是否关闭 |
关闭 ZWrite 是常见透明标志,和 Blend 搭配使用 |
Alpha 通道是否参与输出 |
只写透明色(例如输出 alpha < 1)也会被视为"透明物体" |
光照模式 LightMode 是否兼容 Deferred |
Deferred 只接受特定的 LightMode(ForwardAdd 会强制转 Forward) |
cs
Tags { "RenderType" = "Opaque", "Queue" = "Geometry", "LightMode" = "Deferred" }
Blend SrcAlpha OneMinusSrcAlpha // ❗ 触发转 Forward
ZWrite Off
// 即使你写了 LightMode = Deferred,Unity 仍会忽略这一 Pass 的延迟处理
UNITY_LIGHT_ATTENUATION
是 Unity 在其内置光照宏系统中用于处理光照衰减与阴影遮蔽的一个宏定义,它会根据当前的渲染模式(如 ForwardBase / ForwardAdd)自动计算光源的衰减(Attenuation)以及是否处于阴影中。
atten = SHADOW_ATTENUATION(i); // 阴影遮蔽系数,0 表示全阴影,1 表示无遮挡
atten *= LIGHT_ATTENUATION(i); // 距离、角度等造成的光照衰减
它已经包含了:
-
距离衰减(Distance Attenuation)
-
点光/聚光角度衰减(Spotlight attenuation)
-
阴影遮蔽(Shadow attenuation)
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
atten:输出变量,用于存储光照衰减结果。
i:插值结构体(v2f),它包含阴影坐标、光照坐标等。
i.worldPos:当前像素的世界空间位置。
一般你在 ForwardBase/ForwardAdd 的片元光照计算中,写自己的 Lambert 或 Blinn-Phong 模型前会调用这个宏
cs
#define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
第 1 行:将世界坐标变换到光源空间
第 2 行:获取阴影遮蔽值
第 3 行:采样光照衰减贴图
-
dot(lightCoord, lightCoord)
→ 求的是lightCoord
的平方长度,即从光源到该点的距离平方。 -
.rr
→ 是个技巧,把这个 scalar 值构造成一个float2(x, x)
,作为 UV 坐标采样。 -
tex2D(_LightTexture0, ...)
→ 从点光源的衰减图中查出该距离下的强度。 -
* shadow
→ 乘以阴影遮蔽因子,得到最终的光照影响系数。
worldPos\] → 计算到光源距离 → 从 _LightTexture0 里查衰减值 ↓ 是否处于阴影中(shadow) ↓ final attenuation = 衰减 × 阴影 此宏最终给你一个 float 值 `atten`,你通常这么用 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); finalColor \*= atten \* _LightColor0.rgb; "光源空间"(Light Space 或 Light Coordinate Space)是指**以光源为参考系**建立的一套坐标系,常用于计算阴影、光照衰减等效果。 * **世界空间**:描述的是"每个物体在整个世界中的位置"。 * **光源空间**:描述的是"每个物体在你手电筒坐标系里的位置"。 你拿着手电筒扫出去,其实就是把物体从"世界空间"转换到"手电筒空间"里。 在图形学中,矩阵是 4×4 的,而位置是 `float3`。为了让坐标能参与 4×4 矩阵的乘法(比如变换到光源空间),我们必须补一个第 4 分量: float4(worldPos, 1.0) // 意思是:这个是一个"位置点" 它是**齐次坐标的 w 分量**,决定了这个向量是"点"还是"方向": * `float4(x, y, z, **1**)` → 表示一个位置点,参与平移。 * `float4(x, y, z, **0**)` → 表示一个方向向量,不参与平移(比如法线、切线)。 \| R R R T \| \| R R R T \| \| R R R T \| \| 0 0 0 1 \| 如果乘 float4(p, 1),会带上 T,也就是会平移位置。 如果乘 float4(n, 0),就不会有 T,适用于方向(如法线不应被平移)。 点到光源的距离在光源空间里就是 `lightCoord` 的长度。 每个像素点被阴影"遮住"的程度 * 如果这个值是 **1.0** ,那说明 *the fragment is fully lit*, 完全没有被阴影挡住。 * 如果是 **0.0** ,那就是 *完全处在阴影里* ------ the light source is blocked by something. * 而中间值,比如 `0.4`,就表示 *partially shadowed*,这在软阴影(soft shadow)或者模糊阴影(PCF filtering)中很常见。 * 首先把世界坐标 `worldPos` 映射到光源的空间里 ------ > *transform the position from world space to light space*,就是用一个矩阵变换。 * 然后从 shadow map(阴影贴图)里查一下这个位置对应的深度(depth)值。 * 比较当前片元的深度和 shadow map 中的值: > *if the fragment's depth is greater than what the light "remembers"*, 那说明它被挡住了,在阴影中。 * 得出一个遮蔽程度,再输出回来作为 `shadow` 值。 * Unity 会从每一个支持阴影的光源(directional, spot, point)出发, * 以"光源"为摄像机,**渲染一次整个场景(只写深度)**, * 将**最前面的每个物体的深度** 写入一张纹理 → 这就是 **shadow map**。  | 光源类型 | 纹理名(Shader 内部变量) | 类型 | |-------------|---------------------|-------------------| | Directional | `_ShadowMapTexture` | `sampler2D`(平台相关) | | Spot Light | `_ShadowMapTexture` | `sampler2D` | | Point Light | `_ShadowCubemap` | `samplerCUBE` | UNITY_SAMPLE_SHADOW(tex, coord) 这类宏内部会封装: 从 shadow map 中采样深度 和当前 fragment 的深度做比较 根据结果返回遮蔽程度(shadow attenuation)   --------------- 内部源码的变量名----fallback选择-------透明区域不投射出投影   * `Transparent/Cutout/VertexLit` 是 Unity 内置的一个 Shader 名称(可以认为是一个备用方案)。 * 该内置 Shader 的行为包含 **透明区域剔除(AlphaTest)**,即只渲染不透明区域。 * 它本身的 Tag 就是 `RenderType = "TransparentCutOut"`,并设置了 Queue = "AlphaTest"。 * `RenderType = "TransparentCutOut"`:告诉 Unity 这是一个剪裁式透明的 Shader,供 Unity 内部的 **Shader 分类、投影剔除、光照通道优化、阴影行为等机制**使用。 * `Queue = "AlphaTest"`(对应值为 2450):指定渲染顺序排在普通不透明物体之后但在普通透明物体之前。 | 自定义 Tag 行为 | Fallback Shader 行为 | 联系说明 | |---------------------------------------|-----------------------------------------|------------------------------------------------| | 设置 `RenderType = "TransparentCutOut"` | Unity fallback Shader 也设置该 RenderType | 会被 Unity 用于判断是否投射阴影(透明区域不投射) | | 设置 `Queue = "AlphaTest"` | **fallback Shader 所在的渲染队列也是 AlphaTest** | 确保了半透明剪裁效果的正确排序渲染 | | 无需手动写 Pass for Shadow | fallback Shader 中已含有适当的 Pass | 你自定义 Shader 不写 shadowcaster Pass 也能投影(只投不透明部分) | ---------- `Queue = "AlphaTest"-` 2450,, 普通不透明物体\< 渲染顺序 \<普通透明物体 ```cs Shader "Unlit/004-Shadow2" { Properties { _MainTex("MainTex", 2D) = "white" {} _Color("Diffuse",Color) = (1,1,1,1) _Specular("Specular", Color) = (1,1,1,1) _Gloss("Gloss", Range(8.0, 256)) = 20 _Cutoff("Alpha Cutoff", Range(0,1)) = 0.5 } SubShader { Tags { "RenderType" = "TransparentCutOut" "Queue"="AlphaTest" "IgnoreProjector" = "True"} LOD 100 Pass { Tags{"LightMode" = "ForwardBase"} CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Color; fixed4 _Specular; float _Gloss; fixed _Cutoff; sampler2D _MainTex; float4 _MainTex_ST; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal:NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos:TEXCOORD1; float3 vertexLight : TEXCOORD2; SHADOW_COORDS(3) //仅仅是阴影 float2 uv : TEXCOORD4; }; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.uv, _MainTex); //仅仅是阴影 TRANSFER_SHADOW(o); return o; } fixed4 frag (v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed4 texColor = tex2D(_MainTex, i.uv); clip(texColor.a - _Cutoff); fixed3 diffuse = texColor * _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal,worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss); //fixed shadow = SHADOW_ATTENUATION(i); //这个函数计算包含了光照衰减已经阴影,因为ForwardBase逐像素光源一般是方向光,衰减为1,atten在这里实际是阴影值 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4((ambient + (diffuse + specular) * atten + i.vertexLight), 1); } ENDCG } Pass { Tags{"LightMode" = "ForwardAdd"} Blend One One CGPROGRAM #pragma multi_compile_fwdadd_fullshadows #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Color; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex:POSITION; float3 normal :NORMAL; }; struct v2f { float4 pos :SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; LIGHTING_COORDS(2,3) //包含光照衰减以及阴影 }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; //包含光照衰减以及阴影 TRANSFER_VERTEX_TO_FRAGMENT(o); return o; } fixed4 frag(v2f i):SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0,dot(worldNormal,worldLightDir)); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(viewDir,halfDir)),_Gloss); //fixed atten = LIGHT_ATTENUATION(i); 包含光照衰减以及阴影 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4((diffuse+ specular)*atten,1.0); } ENDCG } Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile_shadowcaster #pragma multi_compile_instancing // allow instanced shadow pass for most of the shaders #include "UnityCG.cginc" struct v2f { V2F_SHADOW_CASTER; float2 uv : TEXCOORD1; UNITY_VERTEX_OUTPUT_STEREO }; uniform float4 _MainTex_ST; v2f vert( appdata_base v ) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } uniform sampler2D _MainTex; uniform fixed _Cutoff; uniform fixed4 _Color; float4 frag( v2f i ) : SV_Target { fixed4 texcol = tex2D( _MainTex, i.uv ); clip( texcol.a*_Color.a - _Cutoff ); SHADOW_CASTER_FRAGMENT(i) } ENDCG } } //FallBack "Diffuse" //FallBack "Transparent/Cutout/VertexLit" } ``` 叫 **Alpha Cutoff** 是因为它用的是贴图的 **alpha 通道(透明度)** 和一个 **cutoff(阈值)** 进行比较来裁剪片元是否可见。 clip(texColor.a - _Cutoff); 这句话表示: 如果 alpha \< _Cutoff,就丢弃当前片元,不会进入颜色缓冲。 * **Alpha** :使用 `texColor.a`,即纹理的透明度。 * **Cutoff**:一个阈值,比如 0.5,低于它的被视为"完全透明"。 * 结合起来,**Alpha Cutoff** 就是 "透明度裁剪阈值"。 ------------------- ------------------  为什么偏要从appdata,所谓的应用层额外特意拿数据,,为什么unity不自己准备好,我后面会用到的东西自己就加上啊,为什么偏偏要自己额外写一遍 **片元受不受"阴影遮挡"影响** :只和 `UNITY_LIGHT_ATTENUATION` 是否调用有关,**ForwardBase 不需要写 ShadowCaster 也能"看见"阴影图**。 > "既然我要用顶点、法线、UV,在 `vert()` 里一用就能知道,为什么 Unity 不直接自动从 CPU 传进来?" 但这恰恰是因为 **Shader 不是解释型语言,也不是脚本,而是硬件并行编译执行的程序**,它的运行方式比一般语言更"低级",更接近显卡指令集。  #### **Shader 编译阶段 ≠ Unity 脚本解释阶段** * Shader 编译是静态的,Unity 没法在运行时 "扫描你用了什么" 然后自动改结构体再回填绑定,这**效率极低且不稳定**。 * 顶点着色器运行时是 GPU 的"入口函数",输入结构必须明确定义,不接受"模糊猜测"。 #### **没有反射 + 不支持动态结构** * Shader 没有像 C# 那样的反射机制去"动态读取我后面函数里用到了什么",因为: * Shader 编译器不具备完整的语义分析能力来动态判断哪些字段你"可能"用到。 * 每个顶点属性都要被严格绑定到 `Vertex Attribute Index`(如 layout(location = x)),不能自动乱排。  在 Shader 编译期,GPU **必须已经知道** ------每一个顶点的数据结构是怎样的,**它不会、也不能在运行时动态调整。**   ### ❌ 动态做法会失败的例子: 假设 GPU 自动判断你在 `frag()` 里用了 `normal`,它想"帮你加进去",结果会出现: * 顶点数据还没上传,但你用到了没声明的字段 → **未定义行为** * 有些模型根本没法线(比如 UI 精灵)→ **程序崩溃** * 数据传进去了但结构不对 → **整个 buffer 混乱,图像错位** | 特性 | Shader on GPU | C# on CPU | |------------|-------------------------------------------------|--------------------------| | **运行模式** | 并行、静态、数据驱动 | 串行、动态、控制驱动 | | **执行对象** | 每个顶点 / 每个像素一个线程(千级并发) | 整个程序一个主线程(或少数多线程) | | **数据来源** | Vertex Buffer(结构固定、字段必须手动声明) | 堆 / 栈 / 动态分配对象 | | **变量识别方式** | 编译时静态匹配语义标签(如 `POSITION`, `NORMAL`) | 运行时根据类型系统和对象引用 | | **函数执行结构** | 没有 call stack,只有入口和执行块(`vertex()`、`fragment()`) | 有调用栈、方法嵌套、递归等 | | **语言特性** | 无动态内存、无对象、无类型推断 | 支持 new、GC、反射、虚函数、多态等 | | **内存布局** | 所有数据结构(如 appdata)必须已知并匹配绑定 | 内存可以动态分配/引用/释放 | | **适合的模型** | 数据流处理:图像、顶点、像素 | 控制流逻辑:UI、AI、系统调用 | | **中间代码结构** | 编译为 GPU 平台目标指令(如 HLSL → DXBC/SPIR-V) | 编译为 IL → JIT 编译为 CPU 机器码 |   ### 🧱 什么是 VBO(Vertex Buffer Object)? VBO 是显卡上的一块连续显存,用来存储**每个顶点所需的数据** ,包括位置、法线、UV、颜色等等。 它相当于一张"表格",每一行是一个顶点,每一列是一个属性字段。   不,**你在 `appdata` 中写的字段顺序不会影响 VBO 的数据排列** 。 真正决定数据排列顺序的是:**Unity 引擎内部的约定布局**,而不是你 Shader 中结构体的顺序。 Shader 中的语义标签(如 `POSITION`, `NORMAL`, `TEXCOORD0`)**才是匹配关键**,它告诉 GPU:"我这个字段要绑定哪个通道/位置的数据"。 struct appdata { float2 uv : TEXCOORD0; float4 vertex : POSITION; float3 normal : NORMAL; }; 这也完全合法,Unity 会照样从: POSITION → mesh.vertices NORMAL → mesh.normals TEXCOORD0 → mesh.uv 中取值。只要语义写对,顺序不重要。  #### Stack(栈) * 自动分配 * 存放:**函数调用链、局部变量、返回地址** * 后进先出(LIFO) * 由系统线程自动维护 void Foo() { int x = 10; // x 在栈上 } → 当 Foo() 被调用,x 被压入栈帧;Foo() 返回后 x 被销毁 #### Heap(堆) * 手动分配(C# 自动使用 new) * 存放:**对象、引用类型、数组等大型数据** * 程序员可控生命周期(由垃圾回收 GC 回收) class MyClass { public int val; } MyClass a = new MyClass(); // a.val 在堆上 | 层级 | 栈和堆是如何存在的 | |-------------------|------------------------------------------------| | **C#(.NET 层)** | 编译器将值类型放栈、引用类型放堆 | | **操作系统层** | 程序启动时为每个线程分配栈空间(一般几 MB),堆则在进程地址空间的大区块中动态申请 | | **硬件层(CPU 内存访问)** | Stack/Heap 都是内存中的连续区域,CPU 通过栈指针(ESP/RSP)或堆指针访问 | #### ✅ 栈的底层本质: * 使用硬件寄存器 `RSP`(x64)或 `ESP`(x86)指向栈顶地址 * 调用函数时: * 返回地址、参数、局部变量 → 入栈 * 返回时全部弹出 栈的地址是**向下增长的** #### ✅ 堆的底层本质: * 堆是内存中的一块自由区域 * `.NET GC` 会向操作系统申请堆块(用 VirtualAlloc / malloc) * GC 跟踪对象引用关系,释放不再被引用的内存块 | 特性 | Stack | Heap | |------|----------------|------------------| | 分配效率 | 🚀 极快(寄存器压栈) | 🐢 较慢(动态分配 + GC) | | 生命周期 | 自动管理(函数退出销毁) | 需手动管理或依赖 GC | | 所在内存 | 操作系统分配的栈段 | 操作系统分配的堆段 | | 硬件依赖 | 依赖 CPU 的栈指针寄存器 | 无专属硬件,但走虚拟地址分配 | Shader 编译不是解释执行,而是要变成 GPU 的机器指令(SPIR-V, DXIL),并生成固定布局的输入签名(Input Signature) `_LightTexture0` 是 Unity 生成的 **点光源或聚光灯的衰减查找表** `UNITY_SHADOW_ATTENUATION(input, worldPos)` 返回的是 **是否被阴影遮挡** 这种写法只适用于: * **点光源(Point Light)** * **聚光灯(Spot Light)** 不适用于: * 方向光(Directional Light)→ 衰减恒为 `1.0` 方向光的"距离衰减"恒为 1,但方向光可以有"阴影遮蔽"! ### 🔍 拆解 `UNITY_LIGHT_ATTENUATION`: 这个宏实际上包含了两个部分: `fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); ` `fixed atten = distanceAtten * shadow; ` 其中: * ✅ 对 **点光 / 聚光**: * `distanceAtten = tex2D(_LightTexture0, ...)` * `shadow = 通过 shadow map 查询` * ✅ 对 **方向光**: * `distanceAtten = 1.0`(恒定) * `shadow = 通过 shadow map 查询` ❌ **不会** 。它只能**接受阴影(receive shadow)**,不能投射阴影。 ### `UNITY_LIGHT_ATTENUATION` 的作用 它的工作是: * 使用当前物体的位置去采样 shadow map * 根据当前是否**处在阴影区** 决定 `shadow` 值是多少 * → 是 **"是否被遮挡"的接收者** ### 什么才控制 "投射阴影"? 要让物体在场景中**投射阴影**,你必须: #### 🔧 使用专用的 Pass: Pass { Tags { "LightMode" = "ShadowCaster" } } Unity 在烘焙实时阴影贴图时,只会读取带有 ShadowCaster 标签的 Pass #### ✳️ 所以: | 功能 | 需要的机制 | |------------------|-----------------------------------------------| | 接收阴影(显示阴影贴图遮蔽) | `UNITY_LIGHT_ATTENUATION` + `TRANSFER_SHADOW` | | 投射阴影(把自己投影到别的物体) | **必须写 `ShadowCaster` Pass** | 每个 Pass 都是一次独立的绘制调用(Draw Call) SIMD(Single Instruction, Multiple Data) if (i \< 3) { do A } else { do B } 那这就像一半士兵要向左走,一半向右走------GPU 只能"串行执行这两个分支",然后再把结果拼起来。性能损失极大,这就是所谓的 分支发散(branch divergence)。 ### 🔁 那么,为什么不能让 GPU 更灵活一些? #### 因为灵活就意味着硬件更复杂,性能就下降了: * 做控制分支需要更多的寄存器、更多的判断逻辑单元(这就是 CPU 的结构) * 而 GPU 要的不是判断,是**批量执行相同任务的爆发力** * 它不希望你搞"例外",它希望你扔给它成千上万个"同样"的操作,它就能飞起来 现代 GPU 使用 **Warp(NVIDIA)或 Wavefront(AMD)** 概念------即: > 例如 32 条线程被打包成一个 Warp,一起执行一条指令。 这是所谓的 **SIMD(一条指令,多条数据)** 什么是分支发散(branch divergence) if (lightIndex == 0) color += CalculateMainLight(); else color += CalculateOtherLight(); 此时: Warp 内部分线程 lightIndex == 0,另一部分不等于 GPU 无法在一个时钟周期内执行两个路径 于是,它执行的不是并行,而是序列化执行: 首先只激活 lightIndex == 0 的线程,其余线程"空等" 然后只激活 else 分支线程,其余线程再次"空等" 💡 此时你就失去了并行性。Warp 的吞吐能力瞬间砍半或更糟。 ### 相比之下,CPU 为什么可以做得好? 因为 CPU 为每个线程提供: * 独立的指令控制单元(每核一个 PC) * 分支预测器(branch predictor) * 指令乱序执行(Out-of-Order Execution) * 高速缓存配合复杂的数据依赖调度 🔁 它能动态应对每个线程的**不确定路径和控制逻辑** ,但代价是:**只有几核,不能做万线程并发。** 而 GPU 的哲学是:**牺牲灵活性,换吞吐率。** 没有 `SHADOW_COORDS(idx)`(即没有生成 `_ShadowCoord` 变量): ❌ **这个物体将无法接收来自其他物体投射的阴影**。 🎯 没有 `DECLARE_LIGHT_COORDS(idx)` 会有什么影响? ✅ 不影响投射阴影 ✅ 不影响接收阴影 ❌ **会影响主光源衰减与方向计算**(具体视渲染模式而定) ### 🔍 更具体地说: #### 1. 在 **平行光(Directional Light)** 下: > 不需要 `_LightCoord`,因为光照方向是全局统一的,不依赖每像素插值 因此: * 即使你没有 `DECLARE_LIGHT_COORDS(idx)`,主光源方向仍可以使用内置变量 `_WorldSpaceLightPos0` 直接计算光照(如 Blinn-Phong) * 没什么问题 ✅ 这个 `ShadowCaster` Pass 控制的是 **这个物体自身投射出的阴影**。 ✅ 会被所有能产生阴影的光源类型使用:**平行光(Directional)** 、**点光源(Point)** 、**聚光灯(Spot)**。 ✅ 其工作对象是 Light 在 "**开启阴影投射** " 且此物体允许投影 的条件下,**在 light 视角渲染阴影图时调用此 Pass。** * 不渲染颜色 * 只写入深度(或编码后的深度) * 会进行 `clip()`,剔除透明区域 * 不涉及任何 Blinn/Phong 光照模型 * 只在某些渲染阶段调用 ### 为什么像阴影、GI、反射、屏幕空间模糊等**看起来是"跨像素"的效果能成立?** > ✅ 这是因为:**"跨物体信息"不是在 Shader 运行时直接传递的,而是通过中间 Buffer 在"不同渲染阶段"间接建立起来的。** > ✅ **如果多个 Shader 都有 ShadowCaster Pass,那是不是每个都"写一张 ShadowMap"?会不会有多个全局表?** #### 🚩 直接回答: > ❌ **不是每个 Shader 或物体都创建一张 ShadowMap。只有每个光源(Light)会拥有一张(或一组)ShadowMap。** > > ✅ 所有带有 `ShadowCaster` Pass 的物体,都会被**批量绘制进同一个光源的 ShadowMap**中。 ### 🔍 ShadowMap 是按"光源"分配的,而不是按"物体"或"Shader"分配的: | 单位 | ShadowMap 分配 | |-----------|-----------------------| | 每个物体 | ❌ 不会单独分配 ShadowMap | | 每种 Shader | ❌ 不会单独分配 ShadowMap | | ✅ 每个光源 | ✅ 会创建一张(或多张)ShadowMap | 例如: * 一个 Directional Light:会创建一个正交投影的 ShadowMap(如 4096×4096) * 一个 Point Light:创建一个 **立方体贴图(CubeMap)**,表示 6 个方向 * Spot Light:用透视投影写入一个 2D ShadowMap ### 📌 多个 Shader 的 `ShadowCaster` 是怎么一起工作的? #### ✅ Unity 会在渲染 ShadowMap 时: 1. 遍历场景中所有 **启用了阴影投射** 的物体(MeshRenderer 的 CastShadows) 2. 查找该物体的材质 → 对应 Shader → 找到其中的 `ShadowCaster` Pass 3. 使用这个 Pass,把该物体绘制进当前光源的 ShadowMap(统一 Buffer) 这意味着: * 无论这个 Shader 是用于角色、建筑、草、特效,只要有 `ShadowCaster`,都会被打包渲染进 **"当前光源的 ShadowMap"** * ShadowMap 只是一个"深度图表面",你不可能在里面"写别人的名字"------你只是把自己当前遮挡的位置画进去 你可以把 ShadowMap 理解成: > 📋 "每个光源持有一本自己的观察记录本(ShadowMap),它不管你是谁、用什么 Shader,只要你挡住了我,我就记在本子里。" 这本子就是 ShadowMap,供后面所有物体查询。 ✅ 是 **Unity 引擎在 CPU 端识别 Pass** ,不是 GPU 来判断。 ✅ 识别的唯一硬性标志是 `Tags { "LightMode" = "ShadowCaster" }` ❗ 不加这个 Tag,哪怕你写了完整代码,也**永远不会被 Shadow 渲染阶段调用**。 在 Unity 的渲染管线中(无论是内置还是 URP/HDRP),**Shader 的各个 Pass 是被 Unity 在 CPU 端查表调度的**。 ### `LightMode = "ShadowCaster"` 是唯一的硬性识别点 这是 Unity 的渲染器用来匹配 Shader Pass 的"关键字标识符"。 | 功能 | 必须 Tag 名称 | |-------------|--------------------------------| | 主光照 Pass | `"LightMode" = "ForwardBase"` | | 附加光源 Pass | `"LightMode" = "ForwardAdd"` | | 投射阴影 Pass | `"LightMode" = "ShadowCaster"` | | 烘焙贴图 Pass | `"LightMode" = "Meta"` | | 自定义后处理 Pass | URP/HDRP 需要注册管线中的 Feature | > ✅ 如果你没有写这个 Tag,Unity 完全不会认出这是个投影用的 Pass,即使内容完全照写。  #### `TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)` ##### 🔧 功能: > 把当前顶点的位置变换为光源视角的裁剪空间坐标,并做一个 **法线方向的偏移(bias)** 来防止 **Peter-panning**(阴影浮空)。 #### 为什么要显式写 `#pragma target 2.0`? * Unity 默认最低目标是 Shader Model 2.0,但有些平台(尤其是移动端)**需要你显式指定** * 如果不写,有些较老的 GPU 可能会尝试降级编译失败 * 确保跨平台可运行(尤其是 Android) 在 ShadowCaster 这样的 Pass 中非常重要,因为场景中树木/草地等对象很多,阴影投射阶段 Instancing 帮助非常大 `UNITY_MATRIX_MVP` 是在 ShadowCaster Pass 中被重定向为:LightProjection \* LightView \* Model  ### 真相:**Unity 在不同 Pass 下,给你的 `UNITY_MATRIX_MVP` 是不同的!** 这是 Unity 引擎在 CPU 端做的 **Uniform 注入机制** 。 具体机制如下: | 当前 Shader Pass | `UNITY_MATRIX_MVP` 实际上传入的矩阵(由 C++ 引擎注入) | |-------------------------|------------------------------------------------------| | ForwardBase, ForwardAdd | 摄像机视角的 ViewProjection 矩阵 × 模型矩阵 | | ShadowCaster | 光源视角的 ViewProjection 矩阵 × 模型矩阵(= Shadow VP \* Model) | | DepthOnly | 摄像机的投影矩阵 × 模型矩阵(用于 ZPrePass) | | Meta | 光照贴图烘焙摄像机的矩阵 | ### 采样是怎么发生的? 在 Fragment Shader 里你常见这样的代码: `float4 col = tex2D(_MainTex, uv); ` 这就是: > **从贴图中取出"uv 对应的颜色值"作为这个像素点的代表色。** 但实际情况是: * 一张贴图是离散存储的(1 像素 = 1 texel) * `uv` 是连续的(float2),你可能正好落在两个 texel 之间 * 所以 GPU 会做: * 最近邻采样(Nearest) * 双线性插值(Bilinear) * 三线性 + mipmap(Trilinear) 但无论哪种方式,**最终只是"一个采样点 → 一个值"**