Appearance
渲染管线
渲染管线大概流程是什么?
渲染管线大概流程是什么?
一句话:渲染管线就是把 3D 场景一步步变成屏幕上 2D 像素的流程。 你可以把它想成工厂流水线:CPU 先决定“画什么”,GPU 再负责“怎么画成像素”。
最简单理解
游戏里有这些东西:
模型 Mesh
材质 Material
贴图 Texture
灯光 Light
相机 Camera
Shader渲染管线要做的事就是:
把这些 3D 数据处理成屏幕上的颜色也就是从:
一个 3D 世界变成:
一张屏幕图片大概流程
1. CPU 准备场景数据
CPU 会先整理这一帧要画什么。
比如:
有哪些物体
哪些物体在相机范围内
每个物体用什么材质
每个物体用什么 Shader
哪些灯光会影响它这个阶段还会做:
视锥剔除
遮挡剔除
渲染排序
合批
提交 DrawCallDrawCall 可以理解成:CPU 对 GPU 说:“帮我画这个东西。”
2. GPU 接收 DrawCall
GPU 收到命令后,开始进入图形管线。
它不是一次性直接画出图片,而是分很多步骤处理:
顶点阶段
图元阶段
光栅化阶段
片元阶段
测试混合阶段
输出屏幕3. 顶点着色器 Vertex Shader
模型本来是一堆顶点。
每个顶点一般有:
位置
法线
UV
颜色
切线
骨骼权重顶点着色器主要负责把顶点从模型空间一路变换到屏幕相关空间。
常见变换链路是:
模型空间 -> 世界空间 -> 观察空间 -> 裁剪空间面试里可以说:
顶点着色器负责处理每个顶点的位置变换,以及传递法线、UV 等数据给后续阶段。4. 图元装配 Primitive Assembly
GPU 会把顶点组装成图元。
最常见的是三角形:
3 个顶点 -> 1 个三角形因为 GPU 最擅长画三角形,所以游戏模型基本都是由大量三角形组成的。
5. 裁剪和剔除
不在相机视野里的部分不用画。
比如:
在视锥外的三角形
背对相机的三角形
被裁剪面切掉的部分这些会被裁掉或剔除,减少后续计算。
6. 光栅化 Rasterization
这是非常关键的一步。
三角形还是几何形状,屏幕却是一个个像素。 光栅化就是判断:
这个三角形覆盖了屏幕上的哪些像素它会把三角形转换成很多片元 Fragment。
片元可以先理解成:
候选像素注意:片元不一定最后真的能显示出来,因为后面还要经过深度测试、模板测试、混合等步骤。
7. 片元着色器 Fragment Shader
片元着色器负责算颜色。
它会根据这些信息计算最终颜色:
贴图颜色
材质参数
灯光
法线
阴影
透明度
环境光比如一个角色的衣服为什么是红色,金属为什么有高光,大部分都和这个阶段有关。
8. 深度测试 Depth Test
如果两个物体挡在同一个屏幕像素上,谁应该显示?
答案通常是:
离相机更近的显示
离相机更远的被挡住深度测试就是用来解决前后遮挡关系的。
9. 模板测试 Stencil Test
模板测试可以控制某些区域能不能被画。
常见用途:
描边
传送门
遮罩
特殊 UI 裁剪
角色透视效果10. 混合 Blending
透明物体需要和背景颜色混合。
比如玻璃、火焰、粒子特效:
最终颜色 = 当前颜色和背景颜色按透明度混合所以透明物体通常比不透明物体更麻烦,也更容易造成性能问题。
11. 写入帧缓冲 FrameBuffer
通过各种测试之后,最终颜色会写入帧缓冲。
帧缓冲可以理解成:
屏幕即将显示的那张图片后面可能还会做后处理:
Bloom
色调映射
抗锯齿
景深
运动模糊
颜色校正最后才显示到屏幕上。
Unity 里怎么对应
Unity 里你看到的是:
c
Camera
MeshRenderer
Material
Shader
Light
RenderPipelineUnity 会帮你组织渲染流程。
如果是 Built-in 管线,很多流程 Unity 内部固定好了。 如果是 URP / HDRP,也就是 SRP,流程可以被 Scriptable Render Pipeline 更灵活地控制。
但底层大方向仍然是:
CPU 提交渲染命令
GPU 处理顶点
GPU 光栅化三角形
GPU 计算像素颜色
通过深度、模板、混合
输出到屏幕C# 示例:观察 Unity 相机渲染前后时机
这个脚本不是手写整个 GPU 管线,而是帮你理解 Unity 一帧渲染时相机附近的调用顺序。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Camera 和 Debug
[RequireComponent(typeof(Camera))] // 要求当前 GameObject 必须带有 Camera 组件
public class RenderPipelineOrderLogger : MonoBehaviour // 定义一个观察 Unity 渲染时机的脚本
{ // 类开始
private void OnPreCull() // Unity 在相机剔除场景物体之前调用
{ // OnPreCull 函数开始
Debug.Log("1. OnPreCull:相机准备做剔除,决定哪些物体可能可见"); // 输出剔除前阶段说明
} // OnPreCull 函数结束
private void OnPreRender() // Unity 在相机开始渲染之前调用
{ // OnPreRender 函数开始
Debug.Log("2. OnPreRender:剔除基本完成,即将开始提交渲染"); // 输出渲染前阶段说明
} // OnPreRender 函数结束
private void OnRenderObject() // Unity 在相机渲染场景物体时调用
{ // OnRenderObject 函数开始
Debug.Log("3. OnRenderObject:可以在这里观察或执行额外绘制逻辑"); // 输出物体渲染阶段说明
} // OnRenderObject 函数结束
private void OnPostRender() // Unity 在相机完成渲染之后调用
{ // OnPostRender 函数开始
Debug.Log("4. OnPostRender:相机渲染结束,画面准备进入后续处理或显示"); // 输出渲染后阶段说明
} // OnPostRender 函数结束
} // 类结束面试高分回答
NOTE
渲染管线是把 3D 场景转换成屏幕像素的一套流程。大体上 CPU 先收集场景数据,做剔除、排序、合批,然后通过 DrawCall 把渲染命令提交给 GPU。GPU 先执行顶点着色器,把模型顶点从模型空间变换到裁剪空间,然后进行图元装配、裁剪、背面剔除和光栅化,把三角形转换成片元。片元着色器再根据材质、贴图、光照等计算颜色,之后经过深度测试、模板测试、混合,最终写入帧缓冲并显示到屏幕上。Unity 中 Built-in、URP、HDRP 的组织方式不同,但底层都离不开这些核心阶段。
顶点着色器做什么?
顶点着色器做什么?
一句话:顶点着色器负责处理“每一个顶点”,最核心的工作是把模型的顶点位置变换到屏幕能理解的空间。
先理解“顶点”是什么
一个 3D 模型本质上是由很多三角形组成的。 一个三角形由 3 个顶点组成。
每个顶点通常会带很多数据:
position:顶点位置
normal:法线方向
uv:贴图坐标
color:顶点颜色
tangent:切线
boneWeight:骨骼权重顶点着色器就是 GPU 上专门处理这些顶点的小程序。
它最核心做什么
顶点着色器最重要的任务是:坐标变换。
模型里的点一开始在模型自己的局部空间里,比如角色脚底在 (0, 0, 0)。 但屏幕要显示它,必须一步步变换:
模型空间 Object Space
-> 世界空间 World Space
-> 观察空间 View Space
-> 裁剪空间 Clip Space
-> 屏幕空间 Screen Space顶点着色器通常负责把顶点至少变换到:
裁剪空间 Clip Space后面的 GPU 阶段才能继续做裁剪、透视除法、光栅化。
它还会传递数据
顶点着色器不只算位置,还会把一些数据传给片元着色器。
比如:
UV 传下去,片元着色器才能采样贴图
法线传下去,片元着色器才能算光照
颜色传下去,片元着色器才能混合颜色
世界坐标传下去,片元着色器才能做特效所以可以理解成:
顶点着色器处理点的位置和顶点属性
片元着色器处理像素颜色它能做哪些效果
顶点着色器可以做很多“顶点级”的效果:
草被风吹动
水面波浪
旗帜飘动
角色骨骼动画蒙皮
顶点挤出描边
模型整体变形因为它改的是顶点位置,所以适合做整体形状变化。
它不能直接做什么
顶点着色器不能直接决定每个像素最终是什么颜色。 因为它处理的是顶点,不是像素。
像素颜色主要由:
Fragment Shader / Pixel Shader来算。
顶点着色器也不知道三角形内部每一个像素的信息。 三角形内部的像素,是在后面的光栅化阶段生成的。
C# 模拟:顶点着色器做坐标变换
下面用 C# 模拟“顶点从模型空间变到裁剪空间”的过程,帮助你理解顶点着色器的核心思想。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Vector3、Vector4、Matrix4x4 和 Camera
public class VertexShaderCpuSimulation : MonoBehaviour // 定义一个用 CPU 模拟顶点着色器坐标变换的组件
{ // 类开始
public Camera targetCamera; // 保存用于观察场景的相机
public Vector3 localVertex = new Vector3(1f, 1f, 1f); // 保存一个模型空间下的顶点位置
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
if (targetCamera == null) // 判断相机引用是否为空
{ // if 语句开始
targetCamera = Camera.main; // 如果没有手动指定相机,就尝试使用主相机
} // if 语句结束
Matrix4x4 modelMatrix = transform.localToWorldMatrix; // 获取模型矩阵,用来把模型空间变到世界空间
Matrix4x4 viewMatrix = targetCamera.worldToCameraMatrix; // 获取观察矩阵,用来把世界空间变到相机观察空间
Matrix4x4 projectionMatrix = targetCamera.projectionMatrix; // 获取投影矩阵,用来把观察空间变到裁剪空间
Vector4 vertex = new Vector4(localVertex.x, localVertex.y, localVertex.z, 1f); // 把 Vector3 顶点扩展成齐次坐标 Vector4
Vector4 worldPosition = modelMatrix * vertex; // 使用模型矩阵把顶点从模型空间变到世界空间
Vector4 viewPosition = viewMatrix * worldPosition; // 使用观察矩阵把顶点从世界空间变到观察空间
Vector4 clipPosition = projectionMatrix * viewPosition; // 使用投影矩阵把顶点从观察空间变到裁剪空间
Debug.Log("World Position = " + worldPosition); // 输出世界空间中的顶点位置
Debug.Log("View Position = " + viewPosition); // 输出观察空间中的顶点位置
Debug.Log("Clip Position = " + clipPosition); // 输出裁剪空间中的顶点位置
} // Start 函数结束
} // 类结束为什么叫“逐顶点执行”
如果一个模型有 1000 个顶点,那么顶点着色器大概会对这 1000 个顶点分别执行一次。
也就是说:
顶点越多,顶点着色器执行次数越多但 GPU 很擅长并行,所以可以同时处理大量顶点。
和片元着色器的区别
顶点着色器:
处理顶点
决定模型形状和位置
执行次数和顶点数有关片元着色器:
处理片元/像素候选
决定颜色、光照、贴图效果
执行次数和屏幕覆盖面积有关比如一个很大的墙面,顶点可能只有 4 个,但它覆盖了很多屏幕像素。 这种情况下,片元着色器压力可能比顶点着色器大很多。
面试高分回答
IMPORTANT
顶点着色器是 GPU 渲染管线中处理顶点的阶段,它会对每个顶点执行一次。它最核心的职责是做坐标变换,把顶点从模型空间经过世界、观察、投影变换到裁剪空间,让后续阶段能够进行裁剪、透视除法和光栅化。同时它也会把 UV、法线、顶点颜色、世界坐标等数据传递给片元着色器。顶点着色器适合做顶点动画、模型变形、骨骼蒙皮等顶点级效果,但它不直接决定最终像素颜色,像素颜色主要由片元着色器负责。
片元着色器做什么?
片元着色器做什么?
一句话:片元着色器负责处理“每一个片元”,最核心的工作是计算这个片元应该是什么颜色。
先理解片元是什么
三角形经过光栅化之后,会被拆成很多屏幕上的候选像素。
这些候选像素就叫:
Fragment,片元为什么不直接叫像素? 因为它还不一定真的显示到屏幕上。
它后面还要经过:
深度测试
模板测试
透明混合
写入帧缓冲只有通过这些步骤后,它才会变成屏幕上真正看到的像素。
片元着色器的核心工作
片元着色器主要做这几件事:
采样贴图
计算颜色
计算光照
处理透明度
处理特效
输出 RGBA 颜色比如一个角色衣服是红色,石头表面有纹理,金属有高光,火焰有透明渐变,这些很多都发生在片元着色器里。
它的输入通常有什么
片元着色器拿到的数据,很多来自顶点着色器,并且经过插值。
比如一个三角形三个顶点都有 UV:
c
左边顶点 UV = (0, 0)
右边顶点 UV = (1, 0)
上边顶点 UV = (0.5, 1)三角形内部每个片元的 UV 会被 GPU 自动插值出来。
常见输入有:
uv:贴图坐标
normal:法线方向
worldPos:世界坐标
vertexColor:顶点颜色
depth:深度
screenPos:屏幕坐标它的输出是什么
片元着色器通常输出颜色:
RGBA例如:
R:红色通道
G:绿色通道
B:蓝色通道
A:透明度但注意:片元着色器输出颜色,不代表这个颜色一定会显示。
因为后面可能被深度测试挡掉。
比如墙后面的怪物,片元着色器可能算了颜色,但深度测试发现它在墙后面,就不会写到屏幕上。
和顶点着色器的区别
顶点着色器:
处理顶点
决定模型画到哪里
执行次数和顶点数量有关片元着色器:
处理片元
决定表面是什么颜色
执行次数和屏幕覆盖面积有关所以一个只有 4 个顶点的大平面,如果铺满整个屏幕,顶点着色器只执行很少次,但片元着色器可能执行几十万甚至几百万次。
这也是为什么:
大面积透明特效
全屏后处理
复杂光照 Shader
大量 Overdraw会很吃 GPU。
Unity 里怎么理解
在 Unity 里,一个材质 Material 使用一个 Shader。
Shader 通常会包含:
Vertex Shader
Fragment Shader顶点着色器负责把模型顶点变换到屏幕相关空间。 片元着色器负责给表面算颜色。
C# 示例:用脚本给片元着色器传参数
在 Unity 里,C# 不能直接替代片元着色器,但可以给 Shader 传参数。 片元着色器会用这些参数计算颜色。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Renderer、Material 和 Color
public class FragmentShaderParameterExample : MonoBehaviour // 定义一个给材质 Shader 传参数的示例组件
{ // 类开始
private Renderer cachedRenderer; // 缓存当前物体的 Renderer 组件
private Material cachedMaterial; // 缓存当前物体运行时使用的材质实例
private void Awake() // Unity 在脚本实例加载时调用 Awake
{ // Awake 函数开始
cachedRenderer = GetComponent<Renderer>(); // 获取当前物体上的 Renderer 组件
cachedMaterial = cachedRenderer.material; // 获取材质实例,后续修改只影响当前物体
} // Awake 函数结束
private void Update() // Unity 每帧调用 Update
{ // Update 函数开始
float t = Mathf.PingPong(Time.time, 1f); // 生成一个在 0 到 1 之间来回变化的数值
Color color = Color.Lerp(Color.red, Color.blue, t); // 根据 t 在红色和蓝色之间做线性插值
cachedMaterial.SetColor("_BaseColor", color); // 把颜色参数传给 Shader,片元着色器会用它计算最终颜色
} // Update 函数结束
} // 类结束一个非常简单的片元着色器思想
虽然你现在主要写 C#,但面试里可以知道它的大概逻辑:
拿到 UV
根据 UV 采样贴图
拿到贴图颜色
结合灯光、透明度、材质参数
输出最终颜色也就是:
颜色 = 贴图颜色 × 材质颜色 × 光照结果性能上要注意什么
片元着色器很容易成为 GPU 瓶颈。
常见原因:
屏幕上覆盖面积太大
透明物体 Overdraw 太多
Shader 里采样贴图太多
光照计算太复杂
用了很多分支判断
全屏后处理太多比如粒子特效、雾、透明 UI、半透明技能特效,如果一层盖一层,就会让同一个屏幕像素被反复计算很多次。
这就是 Overdraw。
面试高分回答
NOTE
片元着色器是 GPU 渲染管线中处理片元的阶段,它在光栅化之后执行。光栅化会把三角形转换成大量片元,片元着色器会对每个片元执行一次,根据插值后的 UV、法线、世界坐标、材质参数、贴图和灯光等信息计算输出颜色。它通常负责贴图采样、光照计算、透明度处理和各种表面特效。片元着色器输出的颜色还要经过深度测试、模板测试和混合,最后才可能写入帧缓冲。简单说,顶点着色器决定模型画到哪里,片元着色器决定表面显示成什么颜色。
MVP 矩阵是什么?
MVP 矩阵是什么?
一句话:MVP 矩阵就是把一个模型顶点,从模型自己的坐标系,一步步变换到相机能看到、GPU 能裁剪和投影的位置。
MVP 分别是什么意思
MVP 是三个矩阵的组合:
M = Model Matrix,模型矩阵
V = View Matrix,观察矩阵
P = Projection Matrix,投影矩阵它们合起来做一件事:
把模型顶点变到裁剪空间常见公式是:
clipPos = P × V × M × localPos注意,这里虽然写成 P × V × M,但真正作用到顶点上的顺序是:
先 M
再 V
再 P因为矩阵乘法是从右往左作用到向量上的。
M:Model Matrix 是什么
Model Matrix 负责:
模型空间 -> 世界空间比如一个角色模型,它自己的本地坐标里,脚底可能在:
(0, 0, 0)但放到游戏世界里,它可能在:
(10, 0, 5)还可能旋转了 90 度,缩放了 2 倍。
这些信息就放在 Model Matrix 里:
位置 Position
旋转 Rotation
缩放 Scale所以 M 的作用是:
把模型自己的点,放到世界里的正确位置V:View Matrix 是什么
View Matrix 负责:
世界空间 -> 相机空间世界里有很多物体,但屏幕显示的是“从相机角度看到的画面”。
所以 V 的作用是:
把整个世界转换到相机视角下你可以这样理解:
不是相机真的把世界拉过来
而是数学上把世界变换成“以相机为中心”的坐标相机在哪里、朝哪里看,都会影响 View Matrix。
P:Projection Matrix 是什么
Projection Matrix 负责:
相机空间 -> 裁剪空间它决定:
近大远小
视野范围
近裁剪面
远裁剪面
正交投影还是透视投影如果是透视相机,就会有近大远小。 如果是正交相机,远近大小不会因为距离变化。
所以 P 的作用是:
把相机看到的 3D 空间,投影成 GPU 后续能处理的空间为什么要组合成 MVP
因为顶点着色器里,每个顶点都要经历这些变换。
如果每次都分开写:
c
worldPos = M × localPos
viewPos = V × worldPos
clipPos = P × viewPos也可以。
但通常可以先把矩阵合起来:
MVP = P × V × M然后直接:
clipPos = MVP × localPos这样更方便,也更适合 GPU 处理。
C# 示例:用 Unity 计算 MVP 变换
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Camera、Matrix4x4、Vector3、Vector4 和 Debug
public class MvpMatrixExample : MonoBehaviour // 定义一个演示 MVP 矩阵的 Unity 组件
{ // 类开始
public Camera targetCamera; // 保存用于观察物体的相机
public Vector3 localPoint = new Vector3(1f, 1f, 1f); // 保存一个模型空间中的点
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
if (targetCamera == null) // 判断是否没有手动指定相机
{ // if 语句开始
targetCamera = Camera.main; // 如果没有指定相机,就使用场景中的主相机
} // if 语句结束
Matrix4x4 modelMatrix = transform.localToWorldMatrix; // 获取模型矩阵 M,用来把模型空间变到世界空间
Matrix4x4 viewMatrix = targetCamera.worldToCameraMatrix; // 获取观察矩阵 V,用来把世界空间变到相机空间
Matrix4x4 projectionMatrix = targetCamera.projectionMatrix; // 获取投影矩阵 P,用来把相机空间变到裁剪空间
Matrix4x4 mvpMatrix = projectionMatrix * viewMatrix * modelMatrix; // 按 P * V * M 的顺序组合出 MVP 矩阵
Vector4 localPosition = new Vector4(localPoint.x, localPoint.y, localPoint.z, 1f); // 把模型空间点转换成齐次坐标,w 为 1 表示这是一个位置点
Vector4 clipPosition = mvpMatrix * localPosition; // 使用 MVP 矩阵把模型空间点直接变换到裁剪空间
Debug.Log("Local Position = " + localPosition); // 输出模型空间坐标
Debug.Log("Clip Position = " + clipPosition); // 输出裁剪空间坐标
} // Start 函数结束
} // 类结束最容易错的地方
矩阵乘法顺序不能乱。
下面这个是常见写法:
c
clipPos = P × V × M × localPos不能随便改成:
c
clipPos = M × V × P × localPos因为矩阵乘法一般不满足交换律:
A × B 不一定等于 B × A所以顺序错了,模型就会飞到奇怪的位置。
MVP 只适合变换位置吗?
主要用于变换顶点位置。
如果是方向,比如法线 Normal,就不能随便用完整 MVP。 法线通常要用模型矩阵相关的法线矩阵处理,尤其遇到非等比缩放时要用逆转置矩阵。
这个点面试里说出来会加分:
位置点用 MVP
法线方向不要直接用 MVPUnity Shader 里对应什么
Unity Shader 里常见类似:
c
UnityObjectToClipPos(vertex)它背后做的事情,本质就是:
把模型空间顶点变到裁剪空间也就是 MVP 变换。
面试高分回答
WARNING
MVP 矩阵是 Model、View、Projection 三个矩阵的组合。Model 矩阵把顶点从模型空间变到世界空间,View 矩阵把世界空间变到相机观察空间,Projection 矩阵把相机空间投影到裁剪空间。顶点着色器中通常会用 clipPos = P * V * M * localPos 把模型顶点变换到 GPU 后续裁剪和光栅化需要的空间。需要注意矩阵乘法顺序不能乱,实际作用顺序是先 Model,再 View,再 Projection。
世界空间、视图空间、裁剪空间分别是什么?
世界空间、视图空间、裁剪空间分别是什么?
一句话:世界空间回答“物体在世界哪里”,视图空间回答“相机怎么看它”,裁剪空间回答“GPU 要不要把它裁掉”。
先看整体流程
一个模型顶点从模型到屏幕,大概会经历:
模型空间 -> 世界空间 -> 视图空间 -> 裁剪空间 -> NDC -> 屏幕空间今天问的三个是中间很重要的:
世界空间 World Space
视图空间 View Space
裁剪空间 Clip Space它们经常和 MVP 矩阵一起出现:
c
worldPos = M * localPos
viewPos = V * worldPos
clipPos = P * viewPos世界空间是什么
世界空间就是整个游戏场景的统一坐标系。
比如 Unity 里:
transform.position通常就是世界空间位置。
如果一个怪物的位置是:
(10, 0, 5)意思就是它在整个游戏世界里的坐标是 (10, 0, 5)。
世界空间适合做:
角色和敌人的距离判断
物体在地图中的位置
寻路目标点
物理碰撞位置
特效生成位置
音效播放位置你可以理解成:
世界空间 = 游戏地图里的真实坐标视图空间是什么
视图空间也叫相机空间。
它是把世界坐标转换到“以相机为中心”的坐标系里。
也就是说,进入视图空间后:
相机相当于坐标原点
物体的位置变成相对相机的位置比如一个物体在相机前方、后方、左边、右边,就可以在视图空间里更容易判断。
视图空间适合做:
判断物体在相机前面还是后面
相机相关特效
屏幕方向相关计算
观察空间下的光照计算Unity 里的观察矩阵是:
c
Camera.worldToCameraMatrix它的作用就是:
世界空间 -> 视图空间裁剪空间是什么
裁剪空间是经过投影矩阵之后的空间。
也就是:
视图空间 -> 裁剪空间由投影矩阵 Projection Matrix 完成。
裁剪空间的重点不是“屏幕像素坐标”,而是:
GPU 用它来判断哪些顶点、三角形在可见范围内如果一个三角形完全在可见范围外,就可以被裁掉。 如果部分在范围内、部分在范围外,就可能被裁剪成新的图元。
裁剪空间之后还要经过:
透视除法 Perspective Divide也就是:
x / w
y / w
z / w然后才进入:
c
NDC,Normalized Device Coordinates,归一化设备坐标再经过视口变换,最后才变成屏幕像素坐标。
所以要记住:
裁剪空间不是屏幕空间三者怎么联系起来
一个模型顶点一开始在模型自己的坐标里:
c
localPos先用模型矩阵 M:
c
worldPos = M * localPos这一步得到世界空间位置。
再用观察矩阵 V:
c
viewPos = V * worldPos这一步得到相机视角下的位置。
再用投影矩阵 P:
c
clipPos = P * viewPos这一步得到裁剪空间位置。
合起来就是:
c
clipPos = P * V * M * localPos也就是 MVP 变换。
C# 示例:在 Unity 中打印三个空间坐标
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Camera、Vector3、Vector4、Matrix4x4 和 Debug
public class SpaceTransformExample : MonoBehaviour // 定义一个演示世界空间、视图空间、裁剪空间的组件
{ // 类开始
public Camera targetCamera; // 保存用于观察物体的相机
public Vector3 localPoint = new Vector3(0f, 1f, 0f); // 保存一个模型空间中的点
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
if (targetCamera == null) // 判断是否没有手动指定相机
{ // if 语句开始
targetCamera = Camera.main; // 如果没有指定相机,就使用场景中的主相机
} // if 语句结束
Matrix4x4 modelMatrix = transform.localToWorldMatrix; // 获取模型矩阵,用来把模型空间变到世界空间
Matrix4x4 viewMatrix = targetCamera.worldToCameraMatrix; // 获取观察矩阵,用来把世界空间变到视图空间
Matrix4x4 projectionMatrix = targetCamera.projectionMatrix; // 获取投影矩阵,用来把视图空间变到裁剪空间
Vector4 localPosition = new Vector4(localPoint.x, localPoint.y, localPoint.z, 1f); // 把模型空间点转换成齐次坐标
Vector4 worldPosition = modelMatrix * localPosition; // 使用模型矩阵计算世界空间坐标
Vector4 viewPosition = viewMatrix * worldPosition; // 使用观察矩阵计算视图空间坐标
Vector4 clipPosition = projectionMatrix * viewPosition; // 使用投影矩阵计算裁剪空间坐标
Debug.Log("World Space = " + worldPosition); // 打印世界空间坐标
Debug.Log("View Space = " + viewPosition); // 打印视图空间坐标
Debug.Log("Clip Space = " + clipPosition); // 打印裁剪空间坐标
} // Start 函数结束
} // 类结束容易混淆的点
世界空间不是模型自己的坐标。 模型自己的坐标叫模型空间或本地空间。
视图空间不是屏幕坐标。 它只是相机视角下的 3D 坐标。
裁剪空间也不是屏幕坐标。 它还要经过透视除法和视口变换,才会变成屏幕上的像素位置。
面试高分回答
NOTE
世界空间是整个场景统一使用的坐标系,表示物体在游戏世界中的真实位置;视图空间是以相机为参考的坐标系,表示物体相对相机的位置,由 View 矩阵把世界空间转换而来;裁剪空间是经过 Projection 矩阵后的空间,GPU 会在这个空间中判断图元是否处于可见范围内,超出范围的会被裁剪。顶点通常会经过 local -> world -> view -> clip 的变换,也就是 clipPos = P * V * M * localPos。
深度测试是什么?
深度测试是什么?
一句话:深度测试就是 GPU 用来判断“同一个屏幕像素上,哪个物体离相机更近”的机制。 近的留下,远的被挡住。
先用很简单的话理解
假设屏幕上有一个像素位置。
两个物体都投影到了这个像素:
绿色方块:离相机近
红色方块:离相机远那屏幕上应该显示谁?
答案是:
显示离相机更近的绿色方块
红色方块被挡住这个判断过程就是深度测试。
深度缓冲是什么
深度测试需要一个东西,叫:
Depth Buffer也叫:
Z Buffer它可以理解成一张和屏幕一样大的表。
屏幕上每个像素,都有一个对应的深度值:
像素 (100, 200) 当前最近深度 = 0.35
像素 (101, 200) 当前最近深度 = 0.42
像素 (102, 200) 当前最近深度 = 0.18这个深度值表示:
当前这个像素上,已经画过的最近片元离相机有多远深度测试怎么工作
当一个新的片元准备写到屏幕时,GPU 会做比较:
新片元的深度
和
Depth Buffer 里已有的深度如果新片元更近:
通过深度测试
可以显示
可能更新 Depth Buffer如果新片元更远:
深度测试失败
这个片元被丢弃
不会显示比如:
Depth Buffer 当前记录 = 0.35
新片元 depth = 0.20如果规则是“越小越近”,那 0.20 更近,所以通过。
再比如:
c
Depth Buffer 当前记录 = 0.35
新片元 depth = 0.700.70 更远,所以失败,被挡住。
ZTest 是什么
ZTest 决定比较规则。
常见规则有:
Less:新深度更小才通过
LessEqual:新深度更小或相等才通过
Greater:新深度更大才通过
Always:永远通过
Never:永远不通过游戏里不透明物体常见默认类似:
c
ZTest LessEqual意思是:
新片元比旧片元更近或一样近,就通过ZWrite 是什么
ZWrite 决定:
通过深度测试后,要不要把自己的深度写进 Depth Buffer不透明物体通常:
c
ZWrite On因为它应该挡住后面的东西。
透明物体通常:
c
ZWrite Off因为透明物体如果写入深度,可能会把后面的透明层错误挡掉。
所以透明物体经常需要:
从远到近排序
再进行透明混合深度测试发生在哪里
大概流程可以理解为:
三角形光栅化
生成片元
片元准备输出颜色
进行深度测试
通过后写入颜色缓冲
必要时写入深度缓冲但现代 GPU 还有一种优化叫:
c
Early-Z也就是提前做深度测试。
如果 GPU 能提前判断一个片元肯定被挡住,就可以在片元着色器执行前丢掉它。
这样可以减少很多无用的片元计算。
为什么它很重要
没有深度测试,画面就会出大问题。
比如远处的墙可能盖住近处的角色。 或者后画的物体永远盖住先画的物体。
深度测试解决的是:
前后遮挡关系让 3D 场景看起来符合真实空间。
C# 示例:用代码模拟深度测试思想
下面不是 GPU 真正的深度缓冲,而是用 C# 模拟“同一个像素上,保留最近深度”的逻辑。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour 和 Debug
public class DepthTestSimulation : MonoBehaviour // 定义一个用 C# 模拟深度测试的组件
{ // 类开始
private float currentDepth = 1f; // 保存当前像素已经记录的深度,1 表示很远
private string currentColor = "Background"; // 保存当前像素显示的颜色名称
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
TryDrawFragment(0.70f, "Red Far Object"); // 尝试绘制一个较远的红色片元
TryDrawFragment(0.25f, "Green Near Object"); // 尝试绘制一个较近的绿色片元
TryDrawFragment(0.90f, "Blue Very Far Object"); // 尝试绘制一个更远的蓝色片元
Debug.Log("Final Pixel Color = " + currentColor); // 输出最终这个像素留下来的颜色
} // Start 函数结束
private void TryDrawFragment(float newDepth, string newColor) // 定义尝试绘制片元的方法
{ // 方法开始
bool passDepthTest = newDepth <= currentDepth; // 判断新片元是否比当前记录的深度更近或一样近
if (passDepthTest == false) // 判断新片元是否没有通过深度测试
{ // if 语句开始
Debug.Log(newColor + " failed depth test"); // 输出这个片元被深度测试丢弃
return; // 直接返回,不更新颜色和深度
} // if 语句结束
currentDepth = newDepth; // 更新当前像素记录的最近深度
currentColor = newColor; // 更新当前像素最终显示的颜色
Debug.Log(newColor + " passed depth test"); // 输出这个片元通过了深度测试
} // 方法结束
} // 类结束Unity 中怎么理解
不透明物体一般会:
开启深度测试
开启深度写入
通常从前到后或按管线策略优化透明物体一般会:
开启深度测试
关闭深度写入
从远到近排序
做透明混合这就是为什么透明物体经常出现排序问题,比如:
玻璃叠玻璃显示不对
粒子特效互相穿插错误
半透明头发显示异常常见面试追问
为什么透明物体通常关闭 ZWrite?
因为透明物体不是完全挡住后面的东西。 如果它写入深度,后面的透明物体可能直接被深度测试丢掉,导致混合不正确。
为什么 Early-Z 能优化?
因为如果一个片元已经确定被前面的东西挡住,就没必要再执行复杂的片元着色器。 这样可以减少贴图采样、光照计算、特效计算的开销。
为什么 Overdraw 会影响性能?
因为同一个屏幕像素被很多层物体反复绘制。 每一层都可能执行片元着色器,透明特效尤其容易造成 Overdraw。
面试高分回答
WARNING
深度测试是 GPU 用来解决 3D 遮挡关系的机制。光栅化生成片元后,每个片元都会带有深度值,GPU 会把它和 Depth Buffer 中当前像素已经记录的深度进行比较。如果新片元更靠近相机,就通过深度测试,可以写入颜色缓冲,并在开启 ZWrite 时更新深度缓冲;如果更远,就会被丢弃。ZTest 控制比较规则,ZWrite 控制是否写入深度。它能保证近处物体挡住远处物体,也能通过 Early-Z 减少无效片元计算。透明物体通常关闭 ZWrite,并依赖从远到近排序和混合。
模板测试是什么?
模板测试是什么?
一句话:模板测试就是给屏幕上的像素做“标记”,之后只有符合标记规则的片元才允许继续绘制。
先用最简单的话理解
模板测试里的“模板”,可以想成一张和屏幕一样大的表。
屏幕上每个像素,都有一个模板值:
像素 A:Stencil = 0
像素 B:Stencil = 1
像素 C:Stencil = 1
像素 D:Stencil = 0然后 GPU 画某个片元时,会问:
这个像素位置的 Stencil 值符合规则吗?如果符合:
通过模板测试,继续渲染如果不符合:
模板测试失败,这个片元被丢弃所以模板测试本质上是:
屏幕像素级别的开关Stencil Buffer 是什么
Stencil Buffer 叫模板缓冲。
它和 Depth Buffer 类似,都是屏幕级缓冲。
区别是:
Depth Buffer 存深度值,用来判断远近遮挡
Stencil Buffer 存整数标记,用来判断能不能画模板缓冲通常每个像素是 8 bit,也就是可以存:
0 到 255你可以把它理解成:
每个屏幕像素都有一个小标签模板测试一般怎么工作
一般分两步。
第一步:先写模板值。
比如先画一个圆形遮罩区域,但这个遮罩本身不显示颜色,只是把它覆盖到的像素写成:
Stencil = 1第二步:再画真正的物体。
画物体的时候设置规则:
只有 Stencil == 1 的地方才允许显示于是物体就只会出现在刚才那个圆形区域里。
这就是很多遮罩、传送门、镜子效果的基础。
常见参数
模板测试里常见几个关键词:
Ref:参考值。
我要拿哪个值去和模板缓冲里的值比较比如:
Ref = 1Comp:比较规则。
模板缓冲里的值和 Ref 怎么比常见规则:
Always:永远通过
Equal:相等才通过
NotEqual:不相等才通过
Less:小于才通过
Greater:大于才通过Pass:模板测试通过后怎么处理模板值。
比如:
Keep:保持不变
Replace:替换成 Ref
Increment:加 1
Decrement:减 1还有一些失败时的操作,比如模板测试失败、深度测试失败时怎么处理,但零基础先记住 Ref、Comp、Pass 就够了。
和深度测试有什么区别
深度测试问的是:
这个片元是不是离相机更近?模板测试问的是:
这个片元所在像素的标记符不符合规则?所以:
Depth Test:解决前后遮挡
Stencil Test:解决区域限制和特殊遮罩比如一个角色站在墙后面,是否被墙挡住,这是深度测试管。 一个角色只在圆形区域里显示,这是模板测试管。
常见用途
模板测试在游戏里很常见:
UI Mask
小地图圆形遮罩
传送门
镜子
角色描边
墙后轮廓显示
区域高亮
技能范围裁剪
只让某些物体显示在指定区域比如传送门效果可以这样理解:
先把传送门形状写入 Stencil = 1
再只在 Stencil == 1 的地方渲染另一个场景画面C# 示例:模拟模板测试
下面用 C# 模拟一个很简单的模板测试: 先把中间一段像素写成 1,再让物体只在 Stencil == 1 的位置显示。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour 和 Debug
public class StencilTestSimulation : MonoBehaviour // 定义一个用 C# 模拟模板测试的组件
{ // 类开始
private int[] stencilBuffer = new int[10]; // 创建一个长度为 10 的模板缓冲,模拟屏幕上 10 个像素的模板值
private string[] colorBuffer = new string[10]; // 创建一个长度为 10 的颜色缓冲,模拟屏幕上 10 个像素最终显示的颜色
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
ClearBuffers(); // 清空模板缓冲和颜色缓冲
WriteStencilMask(3, 6, 1); // 把第 3 到第 6 个像素写成模板值 1
DrawObjectWithStencilTest(1, "Green Object"); // 只在模板值等于 1 的地方绘制绿色物体
PrintColorBuffer(); // 打印最终颜色缓冲结果
} // Start 函数结束
private void ClearBuffers() // 定义清空缓冲的方法
{ // 方法开始
for (int i = 0; i < stencilBuffer.Length; i++) // 遍历模板缓冲中的每一个像素
{ // for 循环开始
stencilBuffer[i] = 0; // 把当前像素的模板值清成 0
colorBuffer[i] = "."; // 把当前像素的颜色显示成空白符号
} // for 循环结束
} // 方法结束
private void WriteStencilMask(int startIndex, int endIndex, int stencilValue) // 定义写入模板遮罩的方法
{ // 方法开始
for (int i = startIndex; i <= endIndex; i++) // 遍历遮罩覆盖到的像素范围
{ // for 循环开始
stencilBuffer[i] = stencilValue; // 把当前像素的模板值写成指定值
} // for 循环结束
} // 方法结束
private void DrawObjectWithStencilTest(int referenceValue, string colorName) // 定义带模板测试的绘制方法
{ // 方法开始
for (int i = 0; i < colorBuffer.Length; i++) // 遍历屏幕上的每一个像素
{ // for 循环开始
bool passStencilTest = stencilBuffer[i] == referenceValue; // 判断当前像素的模板值是否等于参考值
if (passStencilTest == false) // 判断当前像素是否没有通过模板测试
{ // if 语句开始
continue; // 没通过就跳过,不写颜色
} // if 语句结束
colorBuffer[i] = colorName; // 通过模板测试后,把颜色写入颜色缓冲
} // for 循环结束
} // 方法结束
private void PrintColorBuffer() // 定义打印颜色缓冲的方法
{ // 方法开始
string result = string.Join(" | ", colorBuffer); // 把颜色缓冲中的所有像素拼成一行字符串
Debug.Log(result); // 输出最终屏幕像素显示结果
} // 方法结束
} // 类结束为什么渲染顺序很重要
模板测试通常依赖前一步已经写好了模板值。
所以顺序一般是:
先画遮罩物体,写 Stencil
再画目标物体,做 Stencil Test如果顺序反了:
目标物体先画
遮罩还没写那模板测试就没有正确依据,效果可能完全不对。
Unity 里怎么理解
在 Unity Shader 里,模板测试通常通过 Stencil 块控制。
核心思想类似:
c
Ref 1
Comp Equal
Pass Keep意思是:
参考值是 1
只有当前像素模板值等于 1 才通过
通过后保持模板值不变你不用一开始就死背语法,先把逻辑吃透:
先写标记
再按标记决定能不能画面试高分回答
TIP
模板测试是 GPU 渲染管线中的一种像素级测试,它依赖 Stencil Buffer。Stencil Buffer 会为屏幕上的每个像素保存一个整数标记,渲染片元时可以拿这个模板值和参考值 Ref 按照比较规则 Comp 进行比较。比较通过的片元才会继续渲染,失败的会被丢弃;同时还可以通过 Pass、Fail 等操作修改模板值。模板测试常用于 UI Mask、传送门、镜子、描边、区域裁剪等效果。它和深度测试不同,深度测试解决远近遮挡,模板测试解决区域标记和显示限制。
背面剔除是什么?
背面剔除是什么?
一句话:背面剔除就是不渲染背对相机的三角形。 因为很多模型是封闭的,背面的三角形你根本看不到,跳过它们可以减少 GPU 工作量。
先理解正面和背面
一个模型是由很多三角形组成的。
每个三角形都有:
正面 Front Face
背面 Back Face如果三角形的正面朝向相机,它通常会被渲染。 如果三角形的背面朝向相机,它通常会被剔除。
这就是:
c
Backface Culling中文叫:
背面剔除GPU 怎么知道哪边是正面
很多时候,GPU 不是直接看“法线画哪边”,而是看三角形顶点的绕序。
比如三角形三个顶点是:
A -> B -> C在屏幕上看,它们可能是:
顺时针
或
逆时针引擎或图形 API 会规定:
顺时针是正面
或
逆时针是正面符合规定绕序的一面就是正面。 反过来就是背面。
所以如果模型顶点绕序反了,可能会出现:
模型从外面看不见
从里面反而能看见这通常就是正反面判断反了。
为什么要背面剔除
因为大多数实体模型是封闭的。
比如:
角色
箱子
石头
建筑
武器
车辆你从外面看,只能看到朝向你的那一面。 背后的三角形被前面的表面挡住了,画它们没有意义。
所以开启背面剔除可以减少:
三角形处理
光栅化压力
片元着色器计算
Overdraw简单说:
看不到的背面,不画,省性能Cull Back、Cull Front、Cull Off 是什么
Cull Back:
剔除背面这是最常见的模式。 大多数不透明实体模型都用它。
Cull Front:
剔除正面比较少见,常用于一些特殊效果,比如反向描边、壳体效果、某些阴影或轮廓渲染。
Cull Off:
不剔除
正面和背面都画适合薄片类模型:
树叶
草
纸片
布料
头发片
UI 3D 面片因为这些东西没有真正厚度,如果只画正面,从背面看可能就消失了。
和法线有什么关系
视觉理解上,正面通常和法线方向一致。 比如一个面的法线朝外,那么外侧就是你希望看到的正面。
但严格说,背面剔除通常主要看:
三角形顶点绕序法线更多影响:
光照
阴影
反射
表面明暗所以要区分:
绕序决定正反面
法线决定光照方向当然,在建模软件里,法线和绕序通常会一起保持一致,所以你会感觉它们是一回事。
C# 示例:模拟判断一个面是否朝向相机
下面用 C# 模拟“一个面的法线是否朝向相机”。 这不是 GPU 真正的绕序剔除,但能帮助你理解背面剔除的思想。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Transform、Vector3 和 Debug
public class BackfaceCullingSimulation : MonoBehaviour // 定义一个模拟背面剔除判断的组件
{ // 类开始
public Transform cameraTransform; // 保存相机的 Transform 引用
public Vector3 faceNormal = Vector3.forward; // 保存当前面的本地法线方向
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
if (cameraTransform == null) // 判断相机引用是否为空
{ // if 语句开始
cameraTransform = Camera.main.transform; // 如果没有手动指定相机,就使用主相机的 Transform
} // if 语句结束
Vector3 worldNormal = transform.TransformDirection(faceNormal).normalized; // 把面的本地法线转换成世界空间方向
Vector3 toCamera = (cameraTransform.position - transform.position).normalized; // 计算从当前面指向相机的方向
float dot = Vector3.Dot(worldNormal, toCamera); // 用点乘判断法线是否大致朝向相机
bool isFrontFacing = dot > 0f; // 如果点乘大于 0,说明法线朝向相机这一侧
if (isFrontFacing) // 判断当前面是否是朝向相机的面
{ // if 语句开始
Debug.Log("Front Face:这个面朝向相机,通常会被渲染"); // 输出正面会被渲染的提示
} // if 语句结束
else // 如果当前面不是朝向相机的面
{ // else 语句开始
Debug.Log("Back Face:这个面背对相机,开启背面剔除时通常会被跳过"); // 输出背面会被剔除的提示
} // else 语句结束
} // Start 函数结束
} // 类结束Unity Shader 里怎么写
虽然你主要问 C#,但背面剔除通常在 Shader 里设置,面试知道这几个就够了:
Cull Back
Cull Front
Cull Off含义分别是:
剔除背面
剔除正面
关闭剔除,双面都画如果你做树叶、草、布料,经常会用:
Cull Off如果你做普通角色、箱子、建筑,通常用:
Cull Back常见问题
模型从某个角度突然看不见,可能是:
开了背面剔除
模型法线或绕序反了
模型是单面片
从背面看它自然不可见树叶从背面看消失,可能是:
树叶是单面模型
Shader 开了 Cull Back解决方式可能是:
改成双面模型
或者 Shader 使用 Cull Off
或者修正模型法线和绕序面试高分回答
NOTE
背面剔除是渲染管线中的一种优化,它会跳过背对相机的三角形。GPU 通常根据三角形顶点在屏幕上的绕序判断正面和背面,如果开启 Cull Back,背面三角形就不会继续进入后续渲染流程,从而减少光栅化和片元着色器的开销。大多数封闭不透明模型适合开启背面剔除;但像树叶、草、布料、纸片这种双面可见的薄片模型,通常需要 Cull Off。需要注意的是,背面剔除主要和三角形绕序有关,法线更多影响光照。
Alpha Test 和 Alpha Blend 区别是什么?
Alpha Test 和 Alpha Blend 区别是什么?
一句话:Alpha Test 是“透明度不够就直接丢掉”,Alpha Blend 是“根据透明度和背景颜色混合”。 所以:树叶、草、铁丝网常用 Alpha Test;玻璃、烟雾、粒子、UI 渐隐常用 Alpha Blend。
Alpha Test 是什么
Alpha Test 也叫:
Alpha Cutout
Alpha Clip
透明裁剪它会拿像素的 alpha 和一个阈值 cutoff 比较:
alpha >= cutoff:显示
alpha < cutoff:丢弃它不是半透明,而是非常干脆:
要么完全显示
要么完全不显示所以 Alpha Test 的边缘通常比较硬,像剪纸一样。
Alpha Blend 是什么
Alpha Blend 是真正的半透明混合。
它不会直接丢弃像素,而是用透明度把前景颜色和背景颜色混起来:
最终颜色 = 前景颜色 * alpha + 背景颜色 * (1 - alpha)比如 alpha = 0.3:
前景占 30%
背景占 70%所以它能做玻璃、烟雾、火焰、UI 淡入淡出这种柔和透明效果。
核心区别
Alpha Test:
硬透明
不做颜色混合
可以直接丢弃片元
边缘硬
通常可以写深度
适合树叶、草、铁丝网、破洞贴图Alpha Blend:
半透明
会和背景颜色混合
边缘柔和
通常关闭深度写入
需要排序
容易产生 Overdraw
适合玻璃、烟、火、粒子、UI深度方面的区别很重要
Alpha Test 因为结果只有显示或丢弃,所以显示出来的部分可以像普通不透明物体一样写入深度。
所以它通常可以:
ZWrite OnAlpha Blend 因为是半透明,后面的物体也可能需要被看见。 所以透明物体通常:
ZWrite Off但这会带来一个问题:
透明物体需要从远到近排序否则玻璃、烟雾、粒子可能显示顺序错误。
C# 示例:模拟 Alpha Test 和 Alpha Blend
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Color、Mathf 和 Debug
public class AlphaTestBlendSimulation : MonoBehaviour // 定义一个模拟 Alpha Test 和 Alpha Blend 的 Unity 组件
{ // 类开始
public float alpha = 0.4f; // 保存当前像素的透明度
public float cutoff = 0.5f; // 保存 Alpha Test 使用的裁剪阈值
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
bool alphaTestVisible = PassAlphaTest(alpha, cutoff); // 判断当前透明度是否能通过 Alpha Test
Color sourceColor = new Color(0f, 1f, 0f, alpha); // 创建前景颜色,绿色并带有 alpha 透明度
Color backgroundColor = new Color(1f, 0.6f, 0f, 1f); // 创建背景颜色,橙色并且完全不透明
Color blendedColor = AlphaBlend(sourceColor, backgroundColor); // 使用 Alpha Blend 计算混合后的颜色
Debug.Log("Alpha Test Visible = " + alphaTestVisible); // 输出 Alpha Test 是否显示
Debug.Log("Alpha Blend Color = " + blendedColor); // 输出 Alpha Blend 混合后的颜色
} // Start 函数结束
private bool PassAlphaTest(float pixelAlpha, float alphaCutoff) // 定义 Alpha Test 判断方法
{ // 方法开始
bool visible = pixelAlpha >= alphaCutoff; // 如果 alpha 大于等于阈值,就认为这个像素可见
return visible; // 返回这个像素是否通过 Alpha Test
} // 方法结束
private Color AlphaBlend(Color source, Color destination) // 定义 Alpha Blend 混合方法
{ // 方法开始
float a = Mathf.Clamp01(source.a); // 把前景 alpha 限制在 0 到 1 之间
float r = source.r * a + destination.r * (1f - a); // 根据 alpha 混合红色通道
float g = source.g * a + destination.g * (1f - a); // 根据 alpha 混合绿色通道
float b = source.b * a + destination.b * (1f - a); // 根据 alpha 混合蓝色通道
Color result = new Color(r, g, b, 1f); // 创建最终混合后的颜色
return result; // 返回混合结果
} // 方法结束
} // 类结束性能上怎么理解
Alpha Test 会 discard / clip 掉一部分片元。 它能避免被裁掉的地方继续写颜色,但过多 discard 也可能影响某些 GPU 优化。
Alpha Blend 最大的问题是 Overdraw。
比如一堆半透明烟雾叠在一起:
同一个屏幕像素可能被画很多次每一层都要混合颜色,所以粒子、烟雾、透明 UI 多了会很吃 GPU。
面试高分回答
NOTE
Alpha Test 是通过 alpha 和 cutoff 阈值比较来决定片元是否丢弃,结果只有完全显示或完全不显示,适合树叶、草、铁丝网这类硬边透明物体,通常可以写入深度。Alpha Blend 是根据 alpha 把当前片元颜色和背景颜色进行混合,可以表现真正的半透明,适合玻璃、烟雾、粒子和 UI 渐隐,但通常需要关闭 ZWrite,并且依赖从远到近排序,容易产生 Overdraw。简单说,Alpha Test 是裁剪,Alpha Blend 是混合。
Forward 和 Deferred 渲染区别是什么?
Forward 和 Deferred 渲染区别是什么?
一句话:Forward 是“画物体的时候顺便算光”,Deferred 是“先把物体信息存起来,再统一算光”。
Forward Rendering 是什么
Forward Rendering 叫前向渲染。
它的流程比较直接:
渲染一个物体
读取它的材质、贴图、法线
计算它受到哪些灯光影响
输出最终颜色也就是:
物体 + 灯光 -> 最终颜色如果场景里灯光很少,Forward 很简单、很直接、很适合。
Forward 的优点
Forward 的优点是:
流程简单
透明物体支持自然
MSAA 支持更方便
适合移动端
适合灯光少的场景
适合材质种类复杂的场景比如:
手游
卡通渲染
透明特效多的游戏
灯光数量不多的场景Forward 通常比较合适。
Forward 的缺点
Forward 最大的问题是:
灯光多时容易变贵因为物体渲染时要计算灯光。
如果一个物体受到很多灯影响,计算量可能增加。
你可以粗略理解成:
成本 ≈ 物体数量 × 影响它的灯光数量当然真实引擎会做优化,但面试里这样理解很清楚。
Deferred Rendering 是什么
Deferred Rendering 叫延迟渲染。
它分成两个大阶段。
第一阶段:几何阶段。
先不急着算最终光照,而是把物体表面信息写到几张缓冲图里。
这些缓冲图叫:
G-BufferG-Buffer 里通常会存:
Albedo 基础颜色
Normal 法线
Depth 深度
Metallic 金属度
Roughness 粗糙度
Specular 高光信息第二阶段:光照阶段。
再根据 G-Buffer 和灯光,在屏幕空间统一算光。
也就是:
G-Buffer + Lights -> 最终颜色Deferred 的优点
Deferred 最大优势是:
大量动态灯光更友好因为光照计算可以在屏幕空间做。
尤其是很多点光源、复杂灯光场景时,Deferred 往往比传统 Forward 更有优势。
适合:
大量动态灯光
复杂不透明场景
PC / 主机
写实风格场景
夜景、霓虹灯、室内多灯场景Deferred 的缺点
Deferred 也有代价。
第一,G-Buffer 占显存和带宽。
因为它不是只输出一张颜色图,而是要写多张图:
颜色图
法线图
深度图
材质参数图第二,透明物体不好处理。
透明物体需要看到后面的东西,还要做混合。 但 Deferred 的 G-Buffer 更适合“不透明表面”,所以透明物体通常还是要单独走 Forward。
第三,MSAA 通常更麻烦。
因为 Deferred 在屏幕空间做光照,多采样抗锯齿处理起来比 Forward 复杂。
核心区别表
Forward:
边画物体边算光
灯光少时简单高效
透明支持更自然
移动端常用
灯光多时压力变大
Deferred:
先写 G-Buffer
再统一算光
大量动态灯光更有优势
不透明场景更适合
透明、MSAA、带宽成本更麻烦Unity 里怎么理解
在 Unity 里,不同管线支持方式不一样。
Built-in 管线里可以选择:
Forward
DeferredURP 也有 Forward、Deferred、Forward+ 等方案。 HDRP 默认更偏向复杂光照和延迟/混合策略。
但不管 Unity 怎么封装,底层思想都可以记成:
Forward:物体阶段算光
Deferred:G-Buffer 阶段存信息,后面再算光C# 示例:查看当前相机渲染路径
注意:这个例子主要适合 Built-in 管线理解。 如果项目使用 URP / HDRP,实际渲染路径由 Render Pipeline Asset 控制,actualRenderingPath 不一定代表全部 SRP 细节。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Camera、RenderingPath 和 Debug
[RequireComponent(typeof(Camera))] // 要求当前 GameObject 必须带有 Camera 组件
public class RenderingPathLogger : MonoBehaviour // 定义一个打印相机渲染路径的组件
{ // 类开始
private Camera cachedCamera; // 缓存当前物体上的 Camera 组件
private void Awake() // Unity 在脚本实例加载时调用 Awake
{ // Awake 函数开始
cachedCamera = GetComponent<Camera>(); // 获取当前物体上的 Camera 组件
} // Awake 函数结束
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
RenderingPath selectedPath = cachedCamera.renderingPath; // 获取相机设置里选择的渲染路径
RenderingPath actualPath = cachedCamera.actualRenderingPath; // 获取 Unity 最终实际使用的渲染路径
Debug.Log("Selected Rendering Path = " + selectedPath); // 输出相机设置中选择的渲染路径
Debug.Log("Actual Rendering Path = " + actualPath); // 输出当前相机实际使用的渲染路径
} // Start 函数结束
} // 类结束怎么选
如果你的场景:
灯光少
透明物体多
移动端
需要 MSAA
材质变化复杂优先考虑 Forward。
如果你的场景:
大量动态点光源
不透明物体多
PC 或主机
写实光照复杂
希望光照统一在屏幕空间处理可以考虑 Deferred。
面试高分回答
NOTE
Forward Rendering 是前向渲染,它在渲染每个物体时直接计算该物体受到的光照,流程简单,对透明物体和 MSAA 支持更自然,适合灯光较少、移动端或透明物体较多的场景。Deferred Rendering 是延迟渲染,它先把不透明物体的颜色、法线、深度、材质参数等写入 G-Buffer,然后在屏幕空间统一计算光照,因此大量动态灯光时更有优势。但 Deferred 需要额外的 G-Buffer,显存和带宽开销更高,透明物体通常仍要走 Forward,MSAA 支持也更复杂。