# Unity 渲染：从模型到屏幕的一帧

适用：Unity 2022.3 LTS + URP 14，Forward Renderer 主线。

每次只看一站：改一个参数，观察画面，再解释为什么。不需要一次学完。交互版请双击同目录 index.html。

主线：场景更新 → 相机筛选 → URP 安排 Pass / 图形 API → 顶点 → 裁剪 / 投影 → 光栅化 / 插值 → 片元着色 → 深度 / 模板 / 混合 → 后处理 → UI / 显示。

这是阅读顺序。真实一帧包含多次 Draw 和多个目标；深度拒绝可以提前，CPU / GPU 也能跨帧重叠。

## 01 · 模型先是一包数据，还不是图片

Mesh 告诉 GPU「形状是什么」，Material / Shader 告诉它「怎么画」。贴图是被读取的数据，并不会自己贴到屏幕上。

一个立方体有 8 个几何角，但为了让每个面有独立的法线和 UV，常会用 24 个顶点记录、36 个索引，组成 12 个三角形。索引每 3 个一组，决定一个三角形连接哪 3 个顶点。静态资源通常已上传并留在 GPU 内存里，不是每帧都重新上传整个模型。

**输入：**顶点、三角形索引、UV、法线。**输出：**准备好可绘制的数据。

### 在网页试一个改动

切到「四棱锥」，看顶点连接发生变化；再开关棋盘贴图，看最终画面只改表面颜色。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Mesh / 导入设置 | 改顶点、索引、UV、法线；减少面数；设置 LOD | 轮廓、贴图定位、受光改变；少面数主要减少几何工作 |
| 纹理导入设置 | 按用途设 sRGB、压缩、最大尺寸、Mip Maps、Filter Mode | 影响颜色、显存、远处闪烁和清晰度 |
| Material / Shader | 选择 URP/Lit、Unlit 或自定义 Shader | 同一个模型可以画成金属、卡通、透明或纯色 |

### 回 Unity 动手

1. 新建 3D (URP) 空白项目，创建 Cube；给它新建一个 URP/Lit 材质。
2. 在 Project 里选材质，修改 Base Map 颜色。物体变色，但 Mesh 不变。
3. 选模型资源时查看 Mesh；注意几何角数量与顶点记录数量并不一定相同。

**容易混淆：**UV 是表面取样坐标，不是屏幕坐标。法线是受光方向，不是顶点移动方向。

**想一想：**给同一个 Mesh 换一份材质，哪些数据没有变？

**参考回答：**几何顶点和三角形索引没有变；使用的 Shader、贴图和材质参数可以变。

## 02 · CPU 先把这一帧的场景状态准备好

先确定角色、相机、灯光现在在哪里，再讨论如何画。Transform 是物体摆放，顶点变形是形状变化。

脚本、物理和动画在各自的时序里更新，Unity 随后为渲染准备场景数据。M（模型矩阵）把物体局部坐标变成世界坐标。骨骼蒙皮与 BlendShape 也会改变顶点，但实际在 CPU 还是 GPU 上执行，取决于平台和配置。CPU 准备与 GPU 执行可以跨帧重叠，并不是每一步都互相等到结束。

**输入：**上一帧状态 + 输入 + 动画。**输出：**这一帧的位置与渲染数据。

### 在网页试一个改动

改「物体旋转」，再把物体横向移开。整件物体的姿态和位置变了，贴图仍跟着表面走。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Transform / Animator | 移动、旋转、缩放；播放动画、调 BlendShape | 改变世界位置、姿态或形状 |
| C# 数据准备 | 更新材质参数、实例数据；组织 Renderer | 技能闪光、受击变色、成组实例化 |
| Bounds | 变形后检查 Renderer 的包围盒；需要时扩展 | 避免模型明明伸进屏幕，却被提前剔除 |

### 回 Unity 动手

1. 选中 Cube，修改 Transform 的 Position、Rotation、Scale。
2. 播放一个 Animator 动画时，区分物体根节点移动与骨骼顶点变形。
3. 如果做 Shader 顶点拉伸，额外检查原模型 Bounds 能否覆盖伸出的部分。

**容易混淆：**在顶点 Shader 里做风摆，不代表 Unity 的 CPU 剔除包围盒也跟着变化。

**想一想：**角色移动与草叶在 Shader 中摆动，分别优先找哪里？

**参考回答：**角色移动先看 Transform / 动画；草叶摆动先看顶点阶段的位移公式，同时检查 Bounds。

## 03 · 相机先决定「哪些物体值得画」

画面外的物体通常不必提交绘制。先按 Layer / Culling Mask 和视锥筛选，再进入后面的绘制流程。

视锥是相机能看见的空间范围。透视相机由 FOV、宽高比、Near/Far Clip 等定义；Unity 通常用物体包围盒做保守判断。遮挡剔除还可提前排除被墙挡住的物体，但需要满足系统的适用条件。CPU 物体剔除与 GPU 三角形背面剔除、深度测试是三个不同层次。

**输入：**场景里的 Renderer 与包围盒。**输出：**这台相机的可见候选物体。

### 在网页试一个改动

把「横向位置」推到边缘，看物体进入或离开视锥；关闭「相机包含主物体」模拟 Layer 被过滤。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Camera | 改 Culling Mask、FOV、裁剪距离、正交大小 | 控制这台相机看哪些层、看到多大范围 |
| LOD Group | 设置切换距离 / 屏幕占比 | 远处换低模；减少几何工作 |
| Occlusion Culling | 烘焙并验证遮挡剔除 | 适合部分静态遮挡场景；不是所有地图都收益明显 |

### 回 Unity 动手

1. 为 Cube 新建一个 Layer，在 Main Camera 的 Culling Mask 中去掉这个层。
2. Cube 消失后，恢复勾选；再修改 Camera FOV。
3. 开启 Scene 视图中的相机视锥辅助，移动 Cube 对照可见范围。

**容易混淆：**看不见先检查启用、Renderer、Layer、视锥、Bounds。相机主画面筛选与阴影投射的筛选不同：物体本身不在画面中，也可能留下阴影。

**想一想：**Culling Mask 没选中一个物体的 Layer，改它的 Base Color 有用吗？

**参考回答：**这台相机没有提交它的绘制，改颜色无法让它出现在这台相机的画面里。

## 04 · URP 安排 Pass，再向图形 API 提交命令

一帧是很多次绘制的组合。URP 决定何时画阴影、物体、透明、后处理；一次 Draw 也不等于一个 GameObject。

教程按 URP 14 的 Forward Renderer 主线讲。可能先绘制阴影贴图、深度或法线预通道，再绘制不透明、天空盒、透明和后处理；是否存在这些 Pass 取决于功能与配置。Material 的 Queue、Shader Pass 的 LightMode、Renderer 的过滤共同影响一次绘制是否被选中。CPU 用 D3D / OpenGL ES / Vulkan 等 API 设置 GPU 状态并提交命令，Unity 通常替你做这些工作。

**输入：**可见物体 + 材质 + Renderer 设置。**输出：**Draw / 状态切换 / Render Target 命令。

### 在网页试一个改动

开关「阴影」和「透明片」，看下方 Pass 列表多出或少掉相应工作；一件物体可能在阴影和主画面中各画一次。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| 材质 Queue / Shader Pass | 配置 Queue、LightMode 与绘制状态 | 安排不透明、透明或指定用途的 Pass |
| Renderer Feature / Render Pass | 在合适的 RenderPassEvent 插入绘制 / 全屏处理 | 描边、指定 Layer 重绘、局部效果；需要相应输入与目标 |
| SRP Batcher / Instancing | 规范 UnityPerMaterial CBUFFER；验证批处理兼容性 | 前者减少 CPU 状态设置成本，不保证减少 Draw 次数；后者可合并兼容实例 |

### 回 Unity 动手

1. Window → Analysis → Frame Debugger，Enable，选一帧。
2. 从事件列表逐项看 Render Target、Shader、Mesh、材质与状态。
3. 比较开启 / 关闭主光阴影前后的事件；记录多出的阴影工作。

**容易混淆：**这里的 Pass 列表是概念示意，不是读取你的 Unity 帧。不同渲染路径、平台与功能会有不同事件。

**想一想：**降低 Draw Call 是否一定能解决 GPU 填充瓶颈？

**参考回答：**不能。Draw 次数更关联提交与状态成本；透明覆盖、大量像素着色和后处理仍可能让 GPU 很忙。

## 05 · 顶点 Shader 把 3D 位置投到画面上

M 摆物体，V 换到相机视角，P 做投影。顶点 Shader 输出的是裁剪坐标；之后还有裁剪、除以 w 和视口映射。

概念顺序是 pClip = P × V × M × pLocal。裁剪坐标有 x、y、z、w 四个分量；图元先按可见范围裁剪，再用 xyz / w 得到 NDC，最后映射到屏幕。透视相机让远物体看起来更小。顶点阶段还把 UV、世界位置、法线等交给后续阶段插值。矩阵符号、NDC 深度范围、Y 方向与 reversed-Z 会随平台变化，Unity 的转换函数处理这些差异。

**输入：**局部坐标 + 顶点属性。**输出：**裁剪坐标 + 待插值属性。

### 在网页试一个改动

提高「顶点拉伸」，看轮廓真正改变；调 FOV，看投影改变。颜色参数不能替代顶点位移。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| HLSL Vertex / ShaderGraph Vertex | 改 Position；做风摆、波浪、顶点挤出 | 改变轮廓和空间位置；不会自动生成新三角形 |
| Camera 投影 | 调 FOV / Orthographic Size / Viewport Rect | 改变构图、透视与画面映射 |
| 坐标与法线变换 | 用 TransformObjectToHClip 等 URP 函数 | 避免坐标空间混用；非均匀缩放下法线需要正确变换 |

### 回 Unity 动手

1. 在附带的 FrameLab.shader 中找到 vert。
2. 把 _Warp 从 0 改为 0.3，观察 Cube 上方顶点横向偏移。
3. 同时修改 Camera FOV，区分模型变形与镜头投影变化。

**容易混淆：**顶点位移必须在 ShadowCaster / DepthOnly 等相关 Pass 中一致处理，否则阴影或深度可能与主画面错位。本教程附带的最小 Unlit Shader 只有主颜色 Pass。

**想一想：**想让角色轮廓膨胀，应该优先改 Vertex 还是 Fragment？

**参考回答：**改 Vertex 的位置（或增加几何）。Fragment 主要决定已有覆盖区域的颜色，通常不能直接移动轮廓。

## 06 · 光栅化：找出三角形覆盖哪些采样点

三角形不是直接变成一块图片。光栅器检查覆盖，给被覆盖的采样位置生成片元，再把三个顶点的属性插进来。

单采样可以先想成在每个像素中心检查覆盖；MSAA 则会使用多个覆盖 / 深度采样点，实际片元着色频率依配置而定。片元是「候选贡献」，不等于最终屏幕像素：可能被深度、裁切等拒绝，也可能与其他片元混合。UV、位置等通常用透视校正插值；不是简单把三顶点值随便平均。光栅器本身通常不是我们编写的 Shader。

**输入：**投影后的三角形。**输出：**片元与插值后的属性。

### 在网页试一个改动

切到低分辨率，观察三角形边缘的台阶；关闭「背面剔除」，看进入处理的三角形增多。点击图中任意位置查看覆盖情况。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Cull / Render Face | 选择 Back、Front、Off | 单面或双面；Off 可能增加片元工作 |
| Render Scale / AA | 改渲染分辨率；选择 MSAA 等抗锯齿策略 | 清晰度与像素 / 采样成本改变；MSAA 主要改善几何边缘 |
| 网格拓扑 / LOD | 减少三角形；避免大量很小的三角形 | 降低部分几何与光栅开销，收益需要实测 |

### 回 Unity 动手

1. 在 URP Asset 修改 Render Scale，比较 1.0 与 0.7 的 Game 画面。
2. 在 URP/Lit 材质中比较 Render Face 的 Front 与 Both（若当前材质支持）。
3. 使用抗锯齿时检查渲染路径与目标是否支持；不要将 MSAA 当作所有闪烁的通用解。

**容易混淆：**教程的「栅格」是可见三角形的覆盖示意，默认选最近表面用于阅读；真实 GPU 在深度拒绝前可能生成多个重叠片元。

**想一想：**三角形更少，片元一定更少吗？

**参考回答：**不一定。同样占满屏幕的两个大三角形，仍能覆盖几乎所有屏幕像素。

## 07 · 片元 Shader 读取表面数据

UV 像查表地址：告诉 Shader 到贴图的哪里取样。Base Color 乘贴图色，得到基础表面颜色。

一个片元拿到插值属性后，可以采样贴图、乘颜色、生成图案、做溶解判断。纹理采样受 Wrap、Filter、Mip、压缩与色彩空间影响。Normal Map 通常修改着色用的法线，让光看起来像遇到凹凸；它通常不会增加几何或改变剪影。PBR 还用到 Metallic、Smoothness 等表面参数，下一步再把它们与光照一起计算。

**输入：**插值后的 UV、法线、位置等。**输出：**表面颜色与其他着色输入。

### 在网页试一个改动

改「UV 平铺」，棋盘密度变化但轮廓不变；改「基础颜色」，表面整体换色。点击像素查看插值 UV。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Base Map / Color / UV | 采样贴图；平铺、偏移；染色 | 换肤、纹理滚动、伤害变色 |
| Normal / Metallic / Smoothness | 改着色法线与反射参数 | 凹凸感、金属 / 非金属、反射锐度 |
| Alpha Clip / clip() | 按阈值丢弃片元 | 树叶、栅栏、溶解；它与半透明混合不同 |

### 回 Unity 动手

1. 在 URP/Lit 材质设置 Base Map 的 Tiling；观察贴图重复。
2. 增加一张 Normal Map，导入类型设为 Normal map；侧面观察轮廓仍不变。
3. 打开 Alpha Clipping 并调 Threshold，留意孔洞边缘和阴影 Pass 是否一致。

**容易混淆：**把 UV 当屏幕坐标、把 Normal Map 当实际几何，都会让调试方向偏离。

**想一想：**想让水面贴图向右流动，但网格不动，优先改哪里？

**参考回答：**修改采样 UV 的偏移，例如 uv + float2(speed * time, 0)，属于表面取样相关工作。

## 08 · 光照把「表面是什么」变成「看起来有多亮」

面朝向光就亮，背向光就暗。阴影还需要回答：从灯光看过来，前面有没有别的物体挡住？

最简单的漫反射可以理解为 max(dot(N,L),0)。真实 URP/Lit 还会计算 BRDF、距离衰减、主光与附加光、环境 / 烘焙光照、反射与阴影。常见阴影映射先从灯光视角绘制深度，再在主画面中比较距离；阴影贴图是另一次绘制产物，不能只凭这一帧屏幕颜色推导。网页用简化漫反射、Blinn 高光和几何射线遮挡帮助理解，不能当作 URP PBR 或阴影贴图算法的精确复现。

**输入：**表面参数 + 灯光 + 阴影 / 环境数据。**输出：**该片元的线性颜色。

### 在网页试一个改动

旋转「灯光方向」，看同一个面受光改变；开关阴影，看地面遮挡改变。法线、灯光和颜色共同决定结果。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Light / URP Asset | 改方向、强度、阴影距离、分辨率、级联、Bias | 明暗、阴影清晰度、条纹 / 悬浮问题；成本也会变化 |
| Material / ShaderGraph | 调整 Smoothness、Metallic；自定义卡通阶梯光照 | 塑料 / 金属感、反射表现、NPR 风格 |
| Lightmap / Probe | 烘焙静态光；动态物体采样探针 | 减少部分实时光照成本；场景与物体需正确配置 |

### 回 Unity 动手

1. 场景保留一盏 Directional Light、一块 Plane 与 Cube；打开主光阴影。
2. 旋转 Directional Light，比较 Cube 明暗与地面投影。
3. 在 Frame Debugger 里找到阴影目标，再找到主画面使用阴影的绘制。

**容易混淆：**网页阴影用射线判遮挡；真正 URP 常用阴影贴图。Bias、级联、过滤这些问题要回 Unity 的真实帧验证。

**想一想：**用了 Unlit 材质，调主光为什么物体可能仍然不变？

**参考回答：**Unlit 主颜色计算通常不使用场景光照；具体是否使用阴影等取决于这个 Shader 的实现。

## 09 · 深度决定谁挡住谁，模板决定哪里能画

ZTest 是「这次允许画吗」，ZWrite 是「画完把深度记下来吗」。它们是两件事。

深度缓冲为采样位置保存深度，帮助处理前后遮挡。教程用常规 0 近、1 远；Unity 一些平台使用 reversed-Z（1 近、0 远），不要照搬教程数值推断平台。GPU 在满足条件时可提前做深度测试 / 拒绝（Early-Z），并非永远先运行 Fragment 再检查。Stencil 是另一个整数标记测试，可做区域遮罩；它不会自己知道哪个物体近。

**输入：**片元颜色 + 深度 + 现有缓冲。**输出：**通过测试的贡献；更新深度等缓冲。

### 在网页试一个改动

把「深度测试」改为 Always，观察后画的地面盖住部分物体；开启「模板圆形遮罩」，看只在标记区域保留颜色。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| ZTest | LEqual 等比较；特殊效果用 Always 等 | 正常遮挡、透视轮廓 / X-Ray；Always 会失去正常遮挡 |
| ZWrite | 不透明通常 On；透明通常 Off | 是否影响后续遮挡；关掉不代表自己不做 ZTest |
| Stencil | 先写标记，再设 Ref / Comp / Pass | 门户、局部区域效果、UI 遮罩；注意位分配与管线占用 |

### 回 Unity 动手

1. 打开 Frame Debugger，选择 Cube 的 Draw，查看 ZTest、ZWrite。
2. 复制附带 Shader 做实验，把 ZTest LEqual 改为 Always；用遮挡物对照。
3. 完成后恢复设置；Stencil 需要有明确的写标记 Pass 与测试 Pass。

**容易混淆：**网页圆形模板遮罩为概念实验：先生成整数标记，再测试；不是由 Unity 自动创建的圆形 UI Mask。

**想一想：**ZWrite Off，物体是不是一定能穿透墙？

**参考回答：**不是。它仍可能执行 ZTest，被墙的已有深度拒绝。想改变遮挡要看深度测试、绘制时机与目标。

## 10 · 透明片元与已有颜色混在一起

半透明通常在不透明之后画。用当前片元颜色与背景色按比例混合，而不是仅仅把 Alpha 改小就结束。

直观的 straight-alpha 混合是 C = Src × α + Dst × (1−α)。这是 RGB 示例，目标 Alpha 有自己的混合规则。Premultiplied 与 Additive 用不同因子。透明物体通常 ZWrite Off、仍做深度测试，并按物体距离近似从后往前画；物体相交、内部三角形次序会让排序不完美。多个透明层反复覆盖屏幕，容易造成 Overdraw 与带宽 / 着色成本。

**输入：**透明片元颜色 + 目标原色。**输出：**混合后的颜色。

### 在网页试一个改动

打开「透明片」，把 Alpha 从 0 推到 1：先透出背景，再逐渐变成青色片；被 Cube 挡住的部分仍做深度测试。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| URP/Lit Surface Type | 设 Transparent，选择 Alpha / Premultiply / Additive 等 | 玻璃、烟雾、加法发光；必须匹配 Shader 输出与混合因子 |
| 排序与深度策略 | 控制 Queue / Sorting；必要时设计深度预通道等方案 | 改善特定重叠问题；单改 Queue 不能解决所有相交透明 |
| 粒子与特效面积 | 减少叠层、屏幕占比、过度复杂片元 | 降低填充 / Overdraw；低模烟雾仍可很昂贵 |

### 回 Unity 动手

1. 新建 URP/Lit 材质，Surface Type = Transparent，选择 Alpha。
2. 把 Base Color 的 Alpha 调低，给一个位于 Cube 前方的 Quad。
3. 检查 Render Face 与排序，打开 Frame Debugger 看透明 Draw 前后的画面。

**容易混淆：**透明不等于折射；Alpha Blend 只混合颜色，不会自动让背景按玻璃曲率扭曲。

**想一想：**只有两个三角形的巨大透明 Quad，为什么也可能很贵？

**参考回答：**几何少，但覆盖面积大；多个层会让大量像素重复着色、测试、混合并读写颜色目标。

## 11 · 后处理拿到整张图，再改画面

物体 Shader 负责一次绘制，后处理通常读取已经画出的图。它可以统一改变全屏，而不必逐个修改材质。

画面常先存入颜色 Render Target（离屏目标），再由全屏 Pass 处理。HDR 允许颜色值超过 1，Tonemapping 把高动态范围映到显示范围；Exposure 调整亮度。Bloom 通常提取亮区域、模糊、再合成。网页只实现曝光与可切换的 Reinhard 示例曲线，不是 URP 的 ACES / Neutral，也没有假装实现 Bloom。线性计算与显示编码应区分；本演示最后转换为 sRGB 供 Canvas 显示。

**输入：**场景颜色；部分效果还要深度 / 法线等。**输出：**经过曝光、色调映射等的图像。

### 在网页试一个改动

提高「曝光」，对照色调映射开关：不映射容易截到亮白，映射后会压缩高亮。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Global Volume / Camera | 加 Color Adjustments、Tonemapping、Bloom；Camera 开启 Post Processing | 统一调色、亮部泛光、画面风格 |
| Renderer Feature | 按插入位置读取 / 写入正确目标，声明所需输入 | 全屏染色、描边、景深类效果；避免同一目标读写冲突 |
| Render Scale / RT 格式 | 选择分辨率、HDR、MSAA 与目标格式 | 显存与带宽变化；0.7 倍宽高约为 0.49 倍像素，不代表帧率翻倍 |

### 回 Unity 动手

1. GameObject → Volume → Global Volume，新建 Profile。
2. Add Override 加 Color Adjustments，勾 Post Exposure；Main Camera 勾 Post Processing。
3. 加 Tonemapping，比较 None / Neutral / ACES；需要时在 URP 与 Camera 配置允许 HDR。

**容易混淆：**教程采用 URP 14；附带资料指向其 RTHandle / Blitter 用法。Unity 6 的 RenderGraph 教程不能直接当作这版代码复制。

**想一想：**想只给被选角色画描边，是不是只能用全屏后处理？

**参考回答：**不是。可以用额外几何 Pass、指定对象重绘，或先写遮罩再全屏处理；选择取决于效果与成本。

## 12 · 合成 UI，提交显示：这帧才来到屏幕

颜色目标不一定就是显示器。完成需要的合成后，结果才进入最终显示流程；一帧到此结束，下一帧继续。

Screen Space Overlay Canvas 通常在相机渲染之后叠到屏幕，常规相机后处理一般不会影响它。Screen Space Camera 与 World Space 则参与相机 / 场景绘制，顺序与效果取决于配置。相机堆叠、RenderTexture、抗锯齿 Resolve、最终 Blit 和显示编码也会改变最后的事件。交换链 / Present 把渲染结果交给显示系统，显示时机还受 VSync 等影响。网页 UI 标签在颜色处理后覆盖，只模拟 Overlay 的主线关系。

**输入：**相机结果 + 其他相机 / UI。**输出：**显示系统呈现的图像。

### 在网页试一个改动

开关「叠加 UI」，然后回上一步调曝光：画面亮度变了，叠加文字保持原样。低分辨率时场景变粗，后加的 UI 仍清楚。

### 开发者能改什么

| 改哪里 | 操作 | 效果与代价 |
| --- | --- | --- |
| Canvas Render Mode | 选择 Overlay、Camera、World Space；设置排序 | HUD、场景内名字牌、是否跟随相机效果 |
| Camera Stack / Target Texture | 组合相机，或先绘制到 RenderTexture | 小地图、角色展示、多个视角合成；额外相机有额外工作 |
| VSync / 帧预算 | 分析 CPU、GPU 与显示等待；到目标设备测量 | 稳定呈现；降低像素工作不一定改善 CPU 瓶颈 |

### 回 Unity 动手

1. 新建 UI Canvas，Render Mode = Screen Space - Overlay，放一段文字。
2. 提高 Volume 的 Post Exposure，看场景与 Overlay UI 的区别。
3. 用 Frame Debugger 跟到最终绘制；用 Profiler 与目标设备确认真正的瓶颈。

**容易混淆：**画面相同不代表成本相同。网页不测 Unity GPU 时间，任何示意计数都不能当真机性能数据。

**想一想：**角色全屏后处理正常，Overlay 血条没变色，需要先检查什么？

**参考回答：**先检查 Canvas 的渲染模式和绘制顺序；Overlay 可能在后处理之后才绘制，未进入该效果输入。

## 整帧地图：每个过程在 Unity 中的入口

网页版地图的每个过程可点击，弹出下面的概要。示例为局部片段，需要放在标明的作用域并具备相应引用 / 声明；未在 Unity 实际编译或运行。

### CPU：准备与安排

#### 场景 / 动画更新

先用脚本与动画准备这一帧的位置、姿态和参数。

**对应入口：**MonoBehaviour.Update / LateUpdate；Transform；Animator.SetFloat / SetTrigger。动画参数需要先在 Animator Controller 中定义。

C# · 放在 MonoBehaviour.Update 中；speed 为自定义 float 字段

```
transform.position += Vector3.right * speed * Time.deltaTime;
```

**效果：**移动物体；LateUpdate 常用于跟随相机。角色姿态由 Animator、蒙皮与 BlendShape 等处理。

**准确边界：**移动 Transform 改的是物体摆放；在 Shader 中移动顶点改的是形状。物理更新还要看 FixedUpdate 的时序。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/ScriptReference/MonoBehaviour.LateUpdate.html)

#### 相机筛选

先筛掉这台相机不需要提交到主画面的物体。

**对应入口：**Camera.cullingMask 选 Layer；fieldOfView、nearClipPlane / farClipPlane 改可见范围。GeometryUtility.TestPlanesAABB 可辅助检查 Bounds 与视锥。

C# · cam 为 Camera，layer 为已存在的层编号

```
cam.cullingMask &= ~(1 << layer); // 排除该层
cam.fieldOfView = 50f;
```

**效果：**控制相机看到哪些层，以及构图与可见范围。Unity 根据 Renderer Bounds 等数据执行实际剔除。

**准确边界：**手动 TestPlanesAABB 只是辅助判断，不会替换 URP 的剔除流程。主画面剔除与阴影投射筛选也不同。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Camera-cullingMask.html)

#### URP 安排 Pass

Renderer Feature 决定自定义工作插入到一帧的哪个位置。

**对应入口：**ScriptableRendererFeature.Create 初始化自定义 ScriptableRenderPass；AddRenderPasses 调用 renderer.EnqueuePass；renderPassEvent 控制插入时机。

C# · 初始化自定义 pass 时，以及 AddRenderPasses 中的片段

```
pass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
// 在 AddRenderPasses 中：
renderer.EnqueuePass(pass);
```

**效果：**在不透明之后加描边、指定对象重绘或其他自定义效果。Execute 中才实现具体绘制命令。

**准确边界：**管线的 Render Pass 与 Shader 文件里的 Pass 是不同对象。URP 14 获取相机目标应在合适时序处理，例如 SetupRenderPasses；不要照搬 Unity 6 的 RenderGraph 写法。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/renderer-features/how-to-fullscreen-blit.html)

#### 图形 API 命令

通常让 Unity 把绘制命令转成 D3D、OpenGL ES 或 Vulkan 等平台调用。

**对应入口：**在 ScriptableRenderPass.Execute 中使用 CommandBuffer；cmd.DrawRenderer 绘制指定 Renderer，context.ExecuteCommandBuffer 提交命令。

C# · 自定义 Pass.Execute 内；target 与 mat 为已设置的 Renderer / Material

```
var cmd = CommandBufferPool.Get("DrawExample");
cmd.DrawRenderer(target, mat, 0, 0); // 子网格0 / 材质Pass0
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
```

**效果：**用指定材质重画某个物体，例如描边遮罩。命令记录与 GPU 真正执行是不同时间。

**准确边界：**普通物体交给 MeshRenderer + 材质即可。DrawRenderer 不会自动提供完整的 URP 光照数据；例子可先使用 Unlit 材质，Lit 需要正确的管线绘制配置。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Rendering.CommandBuffer.DrawRenderer.html)

### GPU：每次 Draw 的主线

#### 顶点

在顶点函数中修改位置，再输出裁剪坐标。

**对应入口：**Shader Pass 的 #pragma vertex vert 指定顶点函数；URP Core.hlsl 提供 TransformObjectToHClip。ShaderGraph 对应 Vertex 的 Position。

HLSL · vert 内；已包含 Core.hlsl，已定义 input / output / _Warp

```
float3 p = input.positionOS.xyz;
p.x += _Warp * (p.y + 0.5);
output.positionCS = TransformObjectToHClip(p);
```

**效果：**顶点拉伸、风摆、波浪、轮廓挤出；几何轮廓会改变。

**准确边界：**顶点变形后检查 Bounds；ShadowCaster、DepthOnly 等相关 Pass 也要保持同样的变形。顶点位移不会自动增加三角形。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/urp-shaders/birp-urp-custom-shader-upgrade-guide.html)

#### 裁剪 / 投影

Shader 输出 SV_POSITION 后，GPU 执行图元裁剪、除以 w 和视口映射。

**对应入口：**Camera.fieldOfView、nearClipPlane、farClipPlane 与 rect 控制投影和画面区域；顶点函数输出带 SV_POSITION 语义的 float4。

C# · cam 为透视 Camera；参数按场景单位选择

```
cam.nearClipPlane = 0.3f;
cam.farClipPlane = 100f;
cam.rect = new Rect(0f, 0f, 1f, 1f);
```

**效果：**调整可见远近范围，或把相机画面放到屏幕的一部分。正交相机的画面大小使用 orthographicSize。

**准确边界：**通常不在 vert 里手动除以 w；正确输出裁剪坐标即可。Camera.WorldToScreenPoint 可在 C# 侧检查某个世界点的屏幕位置。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Camera.html)

#### 光栅化 / 插值

GPU 自动找覆盖采样点，并插值 UV、法线等传给片元函数。

**对应入口：**ShaderLab 的 Cull 控制三角形背面剔除；URP Asset 的 Render Scale / MSAA 控制分辨率与采样。通常没有一个 C# API 让你手动逐像素光栅化。

ShaderLab · 写在 Shader 的 Pass 中，位于 HLSLPROGRAM 外

```
Cull Back // 剔除背面
// 改为 Cull Off：双面都进入后续处理
```

**效果：**Cull Off 能显示单面网格的背面，但可能增加工作；降低 Render Scale 会减少内部像素。

**准确边界：**UV 等属性通常做透视校正插值。MSAA 支持与收益取决于渲染路径、目标和平台，不是对所有闪烁都有效。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/api/UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset.html)

#### 片元着色

片元函数读取贴图和属性，计算这一片元贡献的颜色。

**对应入口：**#pragma fragment frag 指定片元函数；SAMPLE_TEXTURE2D 读取贴图。URP/Lit / ShaderGraph Lit 再把表面参数交给光照计算。

HLSL · frag 内；贴图、采样器、UV 与颜色参数已声明

```
half4 color = SAMPLE_TEXTURE2D(
    _BaseMap, sampler_BaseMap, input.uv);
return color * _BaseColor;
```

**效果：**染色、UV 流动、溶解、卡通光照。ShaderGraph 的 Base Color、Normal、Metallic、Smoothness 控制对应表面参数。

**准确边界：**这是 Unlit 取样示例；它不自动计算灯光与阴影。clip() 会丢弃片元，Alpha Blend 则把颜色与目标混合，二者不同。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/urp-shaders/birp-urp-custom-shader-upgrade-guide.html)

#### 测试 / 混合

测试决定这片元能否贡献；混合决定它与已有颜色如何合成。

**对应入口：**ShaderLab 状态：ZTest 比较深度，ZWrite 控制深度写入，Stencil 测试整数标记，Blend 控制颜色合成。

ShaderLab · 单个 Pass 中的普通不透明状态

```
ZTest LEqual
ZWrite On
Blend One Zero
```

**效果：**正常不透明遮挡。区域遮罩可另设 Stencil { Ref 1 Comp Equal Pass Keep }，并先用另一个 Pass 写入标记 1。

**准确边界：**ZWrite Off 不代表穿墙；ZTest 仍会检查已有深度。满足条件时 GPU 可以提前做深度拒绝，实际顺序不是永远先着色再测试。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/Manual/SL-ZTest.html)

#### 写入目标

通过测试的颜色与深度写进当前 Render Target，它可能是一张离屏纹理。

**对应入口：**URP 自定义 Pass 在 OnCameraSetup 等合适位置调用 ConfigureTarget(RTHandle)；ConfigureClear 设置清理。相机也能通过 Camera.targetTexture 输出到 RenderTexture。

C# · 自定义 Pass.OnCameraSetup 中；colorHandle 已分配并正确管理

```
ConfigureTarget(colorHandle);
ConfigureClear(ClearFlag.Color, Color.black);
```

**效果：**将遮罩、描边输入或其他中间结果写到单独目标。用双参数 ConfigureTarget 可指定颜色与深度附件。

**准确边界：**分配 / 释放目标、尺寸与格式都要正确处理；同一颜色目标同时采样与写入有风险。片元返回 SV_Target 颜色，具体写到哪张图由当前绑定目标决定。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/renderer-features/how-to-fullscreen-blit.html)

### 把多个结果组合起来

#### 阴影 / 深度等前序结果

先为后面的绘制生成需要的阴影、深度或法线数据。

**对应入口：**投影阴影通常使用 LightMode = ShadowCaster 的 Shader Pass；深度预通道用 DepthOnly；深度法线使用当前管线请求的 Pass（如 DepthNormalsOnly）。

ShaderLab · ShadowCaster Pass 的用途标签；Pass 仍需自己的绘制实现

```
Tags { "LightMode" = "ShadowCaster" }
```

**效果：**让管线识别阴影绘制用途。在编辑器打开主光阴影，并检查 Renderer 的 Cast Shadows / Receive Shadows。深度 / 法线预通道是否存在取决于配置。

**准确边界：**仅加标签不会生成正确阴影代码；还需处理灯光视角变换与偏移等。主颜色做了顶点变形，相关阴影 / 深度 Pass 也需同步。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/urp-shaders/urp-shaderlab-pass-tags.html)

#### 主画面

URP 挑选主颜色用途的 Shader Pass，绘制不透明物体等内容。

**对应入口：**Forward 常用 UniversalForward；UniversalForwardOnly 可强制使用 Forward 的绘制用途；基础 Unlit 可用 SRPDefaultUnlit。Material 绑定 MeshRenderer。

ShaderLab · 实现了 Forward 着色的 Pass 内

```
Tags { "LightMode" = "UniversalForward" }
// 基础 Unlit 例子则可用 SRPDefaultUnlit
```

**效果：**画出角色、场景等主颜色。URP/Lit 已有相应 Pass，不必为普通物体手写完整管线。

**准确边界：**这里的 LightMode 标记绘制用途，不等于渲染顺序；Queue、过滤、Renderer 和排序也参与决定是否 / 何时绘制。用 Frame Debugger 确认实际事件。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/urp-shaders/urp-shaderlab-pass-tags.html)

#### 透明

通常先有不透明背景，再绘制透明物体并混合颜色。

**对应入口：**URP/Lit 材质设 Surface Type = Transparent，选 Alpha / Premultiply / Additive。自定义 Shader 要同时配置透明 Queue 与匹配的深度、混合状态。

ShaderLab · straight-alpha 透明 Shader 的 SubShader / Pass 片段

```
// SubShader：
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" }
// Pass：
ZTest LEqual
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
```

**效果：**做烟雾、半透明面片等；RGB 按 Alpha 与已有目标色混合。片元输出的 Alpha 也必须正确。

**准确边界：**不要只改材质 Alpha 或 _Surface 就假定全部状态正确。普通透明排序无法解决所有相交物体；半透明也不会自动实现折射。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/Manual/SL-Blend.html)

#### 后处理

通过 Volume 调整整张相机图像，或用全屏自定义 Pass 处理它。

**对应入口：**Camera 开启 Post Processing；Global Volume 的 Profile 加 Color Adjustments、Tonemapping 等 Override。C# 可以修改对应参数。

C# · volume 已引用 Global Volume；Profile 已有 Color Adjustments

```
if (volume.profile.TryGet<ColorAdjustments>(out var settings))
    settings.postExposure.Override(1f);
```

**效果：**统一增加曝光，或调整调色 / 色调映射。自定义全屏效果可使用 Renderer Feature、RTHandle 与 Blitter。

**准确边界：**ColorAdjustments 位于 UnityEngine.Rendering.Universal 命名空间。需要勾选参数 Override；全屏效果的输入、目标与插入位置要匹配。

[Unity 官方参考](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/api/UnityEngine.Rendering.Universal.ColorAdjustments.html)

#### UI / 最终合成

按 Canvas 模式与相机设置，将 UI 和相机结果组合起来。

**对应入口：**Canvas.renderMode 选择 Overlay、Camera 或 World Space；相机堆叠在 URP Camera 的 Renderer / Stack 等设置中配置。

C# · canvas 为 Canvas 引用

```
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
```

**效果：**叠加清楚的屏幕 HUD。Screen Space Camera / World Space 可用于随相机或位于场景中的 UI。

**准确边界：**Overlay 通常在相机后处理之后绘制，因此不会自动受相机后处理影响。相机堆叠与 Canvas 模式会改变合成关系。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Canvas-renderMode.html)

#### 显示

Unity 与显示系统负责提交最终画面；通常不需要手动调用 Present。

**对应入口：**用 Application.targetFrameRate 请求帧率，用 Quality Settings 的 VSync 设置协调呈现；实际规则取决于平台。

C# · 例如在初始化时设置期望帧率

```
Application.targetFrameRate = 60;
```

**效果：**表达目标帧率，配合 CPU / GPU 帧预算与显示刷新率观察稳定性。

**准确边界：**这行不是“现在显示一帧”的命令。桌面开启 VSync 时 targetFrameRate 通常会被忽略；移动端通常忽略 vSyncCount，依 targetFrameRate 等规则控制。

[Unity 官方参考](https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Application-targetFrameRate.html)

## 常见词

| 术语 | 直白理解 | 准确边界 |
| --- | --- | --- |
| Mesh | 几何数据 | 顶点属性和三角形连接关系。 |
| Material | 一份绘制配方 | Shader 加具体贴图、颜色等参数。 |
| Shader | GPU 程序及绘制定义 | Vertex / Fragment 负责可编程计算；ShaderLab 还声明 Pass 与状态。 |
| Renderer | 把物体纳入渲染的组件 / 管线部件 | MeshRenderer 与 URP Renderer 指不同层次，结合上下文区分。 |
| Pass | 一次用途明确的绘制阶段 | 可指 Shader Pass 或管线 Render Pass；它们不是同一个对象。 |
| Draw Call | 一条绘制命令 | 可画一个网格的一部分或多个实例。 |
| Render Target | 颜色 / 深度等写入目标 | 可能是离屏纹理，也可能是用于最终显示的目标。 |
| Fragment | 一个候选片元 | 可被拒绝、混合；不等于最终屏幕像素。 |
| Depth | 前后距离记录 | ZTest 比较，ZWrite 写入；范围 / 方向与平台有关。 |
| Stencil | 整数标记缓冲 | 可先标记区域，再测试是否允许画。 |
| Overdraw | 同一屏幕位置重复绘制 | 透明叠层常见；不是只靠三角形数量判断。 |
| SRP Batcher | 减少兼容 Draw 的 CPU 设置成本 | 不保证合并成更少 Draw；不要与 GPU Instancing 混用概念。 |

## 最小 Shader 的使用

把 FrameLab.shader 复制到空白 URP 项目的 Assets，创建材质并选择 Learning/FrameLabUnlit，赋给 Unity Cube。修改 Top Shear 对照顶点位移；修改 Base Map 与 Tiling 对照片元取样。

它是单 Pass、Opaque 的 Unlit 教学例子，不参与真实光照 / 阴影；没有 ShadowCaster / DepthOnly。尚未在 Unity 编译或运行。不要直接替换生产项目的材质。

## 演示边界

网页用 CPU 软件光栅演示图形概念，不读取你的 Unity 工程。使用简化漫反射、Blinn 高光、射线遮挡和 Reinhard 曲线；不精确复现 URP PBR、阴影贴图、MSAA、纹理 Mip、GPU 并行或真实帧耗时。

## 官方参考

- URP 14 渲染结构：https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/rendering-in-universalrp.html
- Frame Debugger：https://docs.unity3d.com/2022.3/Documentation/Manual/FrameDebugger.html
- 自定义 URP Shader：https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/urp-shaders/birp-urp-custom-shader-upgrade-guide.html
- ZTest：https://docs.unity3d.com/2022.3/Documentation/Manual/SL-ZTest.html
- URP 14 Renderer Feature：https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/renderer-features/how-to-fullscreen-blit.html
