Appearance
Unity 渲染
Built-in、URP、HDRP 区别是什么?
Built-in、URP、HDRP 区别是什么?
一句话:Built-in 是老的传统管线,URP 是通用高性能管线,HDRP 是高画质写实管线。
Built-in 是什么
Built-in Render Pipeline 是 Unity 传统内置渲染管线。
它是 Unity 很早期就有的默认渲染系统。 很多老项目、老插件、老教程都是基于 Built-in 的。
它的特点是:
稳定
资料多
兼容老项目
但可定制性比较弱适合:
维护老项目
小型项目
旧插件依赖多的项目
不想迁移管线的项目Built-in 里常见:
Forward Rendering
Deferred Rendering
Surface Shader
传统后处理但是它不像 URP / HDRP 那样是 Scriptable Render Pipeline,整体扩展方式没那么现代。
URP 是什么
URP 全称是:
c
Universal Render Pipeline中文常叫:
通用渲染管线它的目标是:
一套管线,尽量覆盖多平台URP 特点是:
性能比较好
跨平台能力强
适合移动端
适合中小型项目
比 Built-in 更现代
比 HDRP 更轻量适合:
手游
Switch
VR
中小型项目
卡通风格项目
跨平台项目
性能敏感项目URP 的画面上限比 Built-in 更现代,但通常不如 HDRP 那么追求极限写实。
HDRP 是什么
HDRP 全称是:
c
High Definition Render Pipeline中文常叫:
高清渲染管线它的目标是:
高端平台
高画质
写实渲染HDRP 支持很多高级效果:
更复杂的 PBR
体积雾
体积光
屏幕空间反射
更强后处理
更真实的光照模型
更复杂的材质系统适合:
PC
主机
影视预演
工业可视化
建筑可视化
高画质写实项目但代价是:
性能成本更高
学习成本更高
配置更复杂
不适合低端移动设备三者核心区别
Built-in:
老管线
稳定
老项目兼容好
扩展性一般
适合维护老项目URP:
新管线
通用
性能好
跨平台
适合移动端和大多数项目HDRP:
新管线
高画质
写实
性能要求高
适合 PC 和主机高端项目怎么选择
如果是老项目:
优先继续 Built-in因为迁移成本可能很高。
如果是新手游或跨平台项目:
优先 URP因为它性能和兼容性更平衡。
如果是 PC / 主机写实大作:
考虑 HDRP因为它的画面上限更高。
迁移要注意什么
从 Built-in 迁移到 URP 或 HDRP,不是点一下就完事。
可能需要处理:
材质丢失
Shader 不兼容
后处理变化
灯光表现变化
阴影效果变化
自定义渲染代码重写
插件不兼容所以项目早期就要决定渲染管线。 项目做到一半再换,成本会很痛。
C# 示例:判断当前项目使用哪种渲染管线
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour 和 Debug
using UnityEngine.Rendering; // 引入 UnityEngine.Rendering 命名空间,用来访问 GraphicsSettings 和 RenderPipelineAsset
public class RenderPipelineDetector : MonoBehaviour // 定义一个检测当前渲染管线的 Unity 组件
{ // 类开始
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
RenderPipelineAsset pipelineAsset = GraphicsSettings.currentRenderPipeline; // 获取当前项目正在使用的渲染管线资源
if (pipelineAsset == null) // 判断当前渲染管线资源是否为空
{ // if 语句开始
Debug.Log("Current Pipeline = Built-in Render Pipeline"); // 如果为空,说明当前使用的是 Built-in 内置渲染管线
return; // 已经判断完成,直接返回
} // if 语句结束
string pipelineName = pipelineAsset.GetType().Name; // 获取当前渲染管线资源的类型名称
Debug.Log("Current Pipeline Asset Type = " + pipelineName); // 输出当前渲染管线资源类型名称
if (pipelineName.Contains("Universal")) // 判断类型名称是否包含 Universal
{ // if 语句开始
Debug.Log("Current Pipeline = URP"); // 如果包含 Universal,通常说明当前使用的是 URP
} // if 语句结束
else if (pipelineName.Contains("HDRenderPipeline")) // 判断类型名称是否包含 HDRenderPipeline
{ // else if 语句开始
Debug.Log("Current Pipeline = HDRP"); // 如果包含 HDRenderPipeline,通常说明当前使用的是 HDRP
} // else if 语句结束
else // 如果既不是 URP,也不是 HDRP
{ // else 语句开始
Debug.Log("Current Pipeline = Custom SRP or Unknown SRP"); // 输出当前可能是自定义 SRP 或未知 SRP
} // else 语句结束
} // Start 函数结束
} // 类结束面试高分回答
NOTE
Built-in 是 Unity 传统内置渲染管线,稳定、资料多、兼容老项目,但扩展性和现代渲染能力较弱。URP 是 Universal Render Pipeline,目标是通用和高性能,适合移动端、跨平台和大多数中小型项目。HDRP 是 High Definition Render Pipeline,目标是高端平台和高画质写实渲染,支持更复杂的光照、后处理和材质效果,但性能成本和配置复杂度更高。简单说,Built-in 适合老项目,URP 适合通用跨平台,HDRP 适合高端写实画质。
ShaderLab 大概结构是什么?
ShaderLab 大概结构是什么?
一句话:ShaderLab 是 Unity Shader 的“外壳语言”,负责描述属性、渲染状态、SubShader、Pass;真正的顶点/片元计算通常写在里面的 HLSL/CG 代码块里。
整体结构
ShaderLab 大概长这样:
Shader
Properties
SubShader
Tags
Pass
渲染状态
HLSL/CG 代码
Fallback你可以这样理解:
Shader:整个文件外壳
Properties:暴露到材质面板的参数
SubShader:一套渲染实现方案
Pass:一次具体绘制
HLSL/CG:真正写顶点和片元逻辑的地方Shader 是什么
最外层是:
Shader "名字"它决定这个 Shader 在 Unity 里显示的路径。
比如:
Shader "Custom/SimpleColor"在材质选择 Shader 时,就能看到:
Custom -> SimpleColorProperties 是什么
Properties 是给 Unity Inspector 面板看的。
比如你想让美术能调颜色、贴图、透明度,就写在这里。
常见属性:
Color:颜色
2D:贴图
Float:浮点数
Range:滑动条
Vector:向量Properties 里的名字,通常要和 HLSL/CG 里的变量名对应。
比如:
_Color
_MainTexSubShader 是什么
SubShader 是一套渲染方案。
一个 Shader 可以写多个 SubShader。 Unity 会根据当前平台、显卡能力、管线支持情况,选择能运行的那个。
比如:
第一个 SubShader:高级平台用
第二个 SubShader:低端平台兜底用现在很多项目只写一个 SubShader,但你要知道它可以有多个。
Tags 是什么
Tags 用来告诉 Unity 这个 Shader 属于哪类渲染对象。
常见:
RenderType
Queue
LightMode比如:
Queue = Transparent表示这个对象走透明队列。
比如:
RenderType = Opaque表示它是不透明物体。
Pass 是什么
Pass 可以理解为:
一次绘制过程一个 SubShader 里可以有多个 Pass。
比如描边效果常见做法:
第一个 Pass:画正常模型
第二个 Pass:画放大的背面轮廓但要注意:
Pass 越多,渲染开销通常越高因为它可能导致物体被画多次。
渲染状态是什么
Pass 里可以设置渲染状态,比如:
Cull
ZWrite
ZTest
Blend
Stencil它们决定 GPU 怎么画。
比如:
Cull Back:剔除背面
ZWrite On:写入深度
ZTest LEqual:深度测试规则
Blend SrcAlpha OneMinusSrcAlpha:透明混合这些不是颜色计算逻辑,而是 GPU 渲染规则。
HLSLPROGRAM / CGPROGRAM 是什么
ShaderLab 只是外层结构。 真正的顶点和片元逻辑通常写在:
HLSLPROGRAM
ENDHLSL或者老写法:
CGPROGRAM
ENDCG里面会写:
顶点函数 vert
片元函数 frag
#pragma vertex vert
#pragma fragment frag简单 ShaderLab 示例
下面每一行都加注释,你可以先看结构,不用急着完全背语法。
c
Shader "Custom/SimpleColor" // 定义 Shader 的名字和在 Unity 菜单中的路径
{ // Shader 外壳开始
Properties // 定义暴露到材质 Inspector 面板的属性
{ // Properties 属性块开始
_Color ("Color", Color) = (1, 1, 1, 1) // 定义一个颜色属性,默认是白色
} // Properties 属性块结束
SubShader // 定义一套渲染实现方案
{ // SubShader 块开始
Tags { "RenderType" = "Opaque" } // 设置渲染类型标签,表示这是不透明物体
LOD 100 // 设置 Shader 细节等级,Unity 可根据质量等级选择
Pass // 定义一次具体绘制过程
{ // Pass 块开始
Cull Back // 剔除背面,避免绘制背对相机的三角形
ZWrite On // 开启深度写入,让物体可以正确遮挡后面的物体
ZTest LEqual // 使用小于等于规则进行深度测试
HLSLPROGRAM // HLSL 代码块开始
#pragma vertex vert // 指定顶点着色器函数名是 vert
#pragma fragment frag // 指定片元着色器函数名是 frag
float4 _Color; // 声明和 Properties 中对应的颜色变量
struct appdata // 定义顶点输入结构
{ // appdata 结构体开始
float4 vertex : POSITION; // 接收模型顶点位置
}; // appdata 结构体结束
struct v2f // 定义顶点着色器传给片元着色器的数据结构
{ // v2f 结构体开始
float4 vertex : SV_POSITION; // 保存裁剪空间位置
}; // v2f 结构体结束
v2f vert(appdata v) // 定义顶点着色器函数
{ // vert 函数开始
v2f o; // 创建输出结构体变量
o.vertex = UnityObjectToClipPos(v.vertex); // 把模型空间顶点变换到裁剪空间
return o; // 返回顶点着色器输出数据
} // vert 函数结束
float4 frag(v2f i) : SV_Target // 定义片元着色器函数,并输出颜色
{ // frag 函数开始
return _Color; // 返回材质面板中设置的颜色
} // frag 函数结束
ENDHLSL // HLSL 代码块结束
} // Pass 块结束
} // SubShader 块结束
Fallback "Diffuse" // 如果当前 Shader 不支持,就回退到 Diffuse
} // Shader 外壳结束面试高分回答
ShaderLab 是 Unity 用来组织 Shader 的外层描述语言。它最外层是 Shader,里面可以有 Properties、SubShader、Fallback。Properties 用来把颜色、贴图、数值暴露到材质面板;SubShader 表示一套渲染实现方案,一个 Shader 可以有多个 SubShader 以适配不同平台;Pass 表示一次具体绘制过程,里面可以设置 Cull、ZWrite、ZTest、Blend、Stencil 等渲染状态。真正的顶点着色器和片元着色器逻辑通常写在 HLSLPROGRAM 或 CGPROGRAM 代码块中。简单说,ShaderLab 管结构和渲染状态,HLSL/CG 管具体计算。
Pass 是什么?
Pass 是什么?
一句话:Pass 就是 Shader 里的一次具体绘制过程。 一个 Pass 会设置一套渲染状态,然后执行一套顶点着色器和片元着色器,把物体画一遍。
放在 ShaderLab 结构里看
大概是:
Shader
SubShader
Pass
Pass
PassSubShader 是一套渲染方案。 Pass 是这套方案里的某一次绘制。
比如一个 Shader 可能有:
第一个 Pass:正常画模型
第二个 Pass:画描边
第三个 Pass:写阴影
第四个 Pass:写深度所以你可以先记住:
Pass = 画一遍Pass 里通常有什么
一个 Pass 里常见内容有:
渲染状态
顶点着色器
片元着色器
LightMode 标签
Stencil 设置
Blend 设置
Depth 设置
Cull 设置比如:
c
Cull Back
ZWrite On
ZTest LEqual
Blend SrcAlpha OneMinusSrcAlpha这些都是告诉 GPU:
要不要剔除背面
要不要写深度
深度怎么比较
透明怎么混合为什么需要多个 Pass
因为有些效果不是画一遍就够。
比如描边效果:
Pass 1:正常画角色
Pass 2:把模型稍微放大,画成黑色轮廓再比如阴影:
Pass 1:正常画到屏幕
Pass 2:写入 ShadowMap再比如深度预写:
Pass 1:只写深度
Pass 2:再正常算颜色所以多 Pass 的本质是:
同一个物体为了不同目的,被渲染多次Pass 和 DrawCall 的关系
它们不是完全同一个概念,但关系很近。
简单理解:
一个物体一个 Pass,通常至少会产生一次绘制提交如果一个物体有多个 Pass:
这个物体就可能被画多次所以多 Pass 往往会增加:
DrawCall
GPU 渲染开销
片元计算压力
带宽消耗尤其在移动端,要谨慎使用多 Pass。
LightMode 是什么
LightMode 是 Pass 的标签,用来告诉渲染管线:
这个 Pass 是干什么的比如 URP 里常见:
c
UniversalForward
ShadowCaster
DepthOnly
DepthNormals可以理解成:
c
UniversalForward:正常前向渲染
ShadowCaster:用于投射阴影
DepthOnly:只写深度渲染管线会根据当前阶段,选择对应 LightMode 的 Pass 来执行。
ShaderLab 示例:两个 Pass 的描边 Shader
下面代码不是让你死背,而是看结构:一个 Pass 画描边,一个 Pass 画本体。每一行都加了注释。
c
Shader "Custom/TwoPassOutline" // 定义 Shader 名字,显示在 Unity 的 Shader 选择菜单里
{ // Shader 外壳开始
Properties // 定义材质面板里可以调的属性
{ // Properties 属性块开始
_Color ("Color", Color) = (1, 1, 1, 1) // 定义模型本体颜色,默认白色
_OutlineColor ("Outline Color", Color) = (0, 0, 0, 1) // 定义描边颜色,默认黑色
} // Properties 属性块结束
SubShader // 定义一套渲染实现方案
{ // SubShader 块开始
Tags { "RenderType" = "Opaque" } // 设置这个 Shader 的渲染类型为不透明
Pass // 第一个 Pass 开始,用来画描边
{ // 第一个 Pass 块开始
Cull Front // 剔除正面,只画背面,常用于外扩描边
ZWrite On // 开启深度写入,让描边参与深度遮挡
HLSLPROGRAM // HLSL 代码块开始
#pragma vertex vert // 指定顶点着色器函数名为 vert
#pragma fragment frag // 指定片元着色器函数名为 frag
float4 _OutlineColor; // 声明描边颜色变量
struct appdata // 定义顶点输入结构
{ // appdata 结构体开始
float4 vertex : POSITION; // 接收模型顶点位置
}; // appdata 结构体结束
struct v2f // 定义顶点到片元的数据结构
{ // v2f 结构体开始
float4 vertex : SV_POSITION; // 保存裁剪空间位置
}; // v2f 结构体结束
v2f vert(appdata v) // 定义顶点着色器函数
{ // vert 函数开始
v2f o; // 创建输出数据
v.vertex.xyz *= 1.05; // 简单把模型顶点向外放大,形成描边外壳
o.vertex = UnityObjectToClipPos(v.vertex); // 把模型空间顶点变换到裁剪空间
return o; // 返回顶点着色器输出
} // vert 函数结束
float4 frag(v2f i) : SV_Target // 定义片元着色器函数
{ // frag 函数开始
return _OutlineColor; // 返回描边颜色
} // frag 函数结束
ENDHLSL // HLSL 代码块结束
} // 第一个 Pass 块结束
Pass // 第二个 Pass 开始,用来画模型本体
{ // 第二个 Pass 块开始
Cull Back // 剔除背面,只画模型正面
ZWrite On // 开启深度写入
HLSLPROGRAM // HLSL 代码块开始
#pragma vertex vert // 指定顶点着色器函数名为 vert
#pragma fragment frag // 指定片元着色器函数名为 frag
float4 _Color; // 声明本体颜色变量
struct appdata // 定义顶点输入结构
{ // appdata 结构体开始
float4 vertex : POSITION; // 接收模型顶点位置
}; // appdata 结构体结束
struct v2f // 定义顶点到片元的数据结构
{ // v2f 结构体开始
float4 vertex : SV_POSITION; // 保存裁剪空间位置
}; // v2f 结构体结束
v2f vert(appdata v) // 定义顶点着色器函数
{ // vert 函数开始
v2f o; // 创建输出数据
o.vertex = UnityObjectToClipPos(v.vertex); // 把模型空间顶点变换到裁剪空间
return o; // 返回顶点着色器输出
} // vert 函数结束
float4 frag(v2f i) : SV_Target // 定义片元着色器函数
{ // frag 函数开始
return _Color; // 返回模型本体颜色
} // frag 函数结束
ENDHLSL // HLSL 代码块结束
} // 第二个 Pass 块结束
} // SubShader 块结束
} // Shader 外壳结束常见误区
不要以为 Pass 越多越高级。 Pass 多通常意味着物体被多画几遍,效果可能更复杂,但开销也更高。
也不要把 Pass 和 SubShader 混在一起:
c
SubShader:一套方案
Pass:方案里的一次绘制面试高分回答
NOTE
Pass 是 ShaderLab 中的一次具体渲染步骤。一个 SubShader 可以包含多个 Pass,每个 Pass 可以设置自己的渲染状态,比如 Cull、ZWrite、ZTest、Blend、Stencil,并且包含对应的顶点着色器和片元着色器代码。渲染管线会根据 Pass 的标签,例如 LightMode,决定在某个阶段执行哪个 Pass。多 Pass 可以实现描边、阴影、深度预写等效果,但通常也意味着同一个物体会被绘制多次,可能增加 DrawCall 和 GPU 开销。
Render Queue 是什么?
Render Queue 是什么?
一句话:Render Queue 是 Unity 用来控制物体渲染先后顺序的队列编号。 数字越小越早画,数字越大越晚画。
为什么需要 Render Queue
一帧画面里有很多东西:
天空盒
地面
角色
树叶
玻璃
粒子
UI这些东西不能完全乱画。
比如:
天空应该先画
不透明物体一般较早画
透明物体通常较晚画
UI 或 Overlay 通常最后画所以 Unity 用 Render Queue 给它们排队。
常见队列值
Unity 常见队列大概是:
Background:1000
Geometry:2000
AlphaTest:2450
Transparent:3000
Overlay:4000你可以这样记:
1000:背景
2000:不透明物体
2450:透明裁剪物体
3000:半透明物体
4000:覆盖层Geometry 是什么
Geometry 通常是不透明物体的默认队列。
比如:
地面
墙
角色身体
箱子
石头
建筑这些通常:
ZWrite On
参与深度写入
遮挡关系比较稳定不透明物体一般较早画,有利于深度缓冲提前挡掉后面的片元,减少 Overdraw。
AlphaTest 是什么
AlphaTest 常用于硬裁剪透明。
比如:
树叶
草
铁丝网
破洞贴图它不是半透明混合,而是:
alpha 达标就显示
alpha 不达标就丢弃所以它通常放在 Geometry 后面,Transparent 前面。
Transparent 是什么
Transparent 用于真正半透明物体。
比如:
玻璃
烟雾
火焰
粒子
透明 UI透明物体通常比较麻烦,因为它要和背景颜色混合。
所以透明物体经常需要:
从远到近排序
ZWrite Off
Blend On如果透明物体顺序错了,可能出现:
玻璃遮挡异常
粒子层级错乱
半透明物体显示不对Overlay 是什么
Overlay 通常用于非常靠后的渲染内容。
比如:
覆盖效果
某些 UI
特殊调试显示
始终显示在前面的效果它的队列值很大,所以会比较晚渲染。
Render Queue 和深度测试不是一回事
这个非常重要。
Render Queue 控制的是:
谁先画,谁后画Depth Test 控制的是:
同一个像素上,谁离相机更近所以不要混淆:
Render Queue:渲染顺序
Depth Test:遮挡判断
Blend:颜色混合一个物体就算后画,如果深度测试失败,也可能显示不出来。 一个透明物体即使后画,如果排序不对,也可能混合错误。
C# 示例:修改材质 Render Queue
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Renderer、Material 和 Debug
public class RenderQueueExample : MonoBehaviour // 定义一个演示 Render Queue 的 Unity 组件
{ // 类开始
private Renderer cachedRenderer; // 缓存当前物体的 Renderer 组件
private Material cachedMaterial; // 缓存当前物体的材质实例
private void Awake() // Unity 在脚本实例加载时调用 Awake
{ // Awake 函数开始
cachedRenderer = GetComponent<Renderer>(); // 获取当前物体上的 Renderer 组件
cachedMaterial = cachedRenderer.material; // 获取运行时材质实例,避免直接改共享材质
} // Awake 函数结束
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
cachedMaterial.renderQueue = 3000; // 把当前材质设置到 Transparent 队列附近
Debug.Log("Render Queue = " + cachedMaterial.renderQueue); // 输出当前材质的 Render Queue 数值
} // Start 函数结束
} // 类结束ShaderLab 里怎么写
在 ShaderLab 里通常通过 Tags 设置:
c
Shader "Custom/QueueExample" // 定义 Shader 名字
{ // Shader 外壳开始
SubShader // 定义一套渲染方案
{ // SubShader 块开始
Tags { "Queue" = "Transparent" } // 把这个 Shader 放到 Transparent 渲染队列
Pass // 定义一次绘制过程
{ // Pass 块开始
} // Pass 块结束
} // SubShader 块结束
} // Shader 外壳结束也可以写偏移:
Queue = Geometry+1
Queue = Transparent-10意思是:
比 Geometry 稍晚一点
比 Transparent 稍早一点常见坑
不要为了“让物体显示在前面”就乱调 Render Queue。 如果深度测试还开着,它仍然可能被挡住。
不要把普通不透明物体放到很靠后的透明队列。 这可能破坏渲染优化,还可能增加 Overdraw。
透明物体不只看 Render Queue,还要看:
c
ZWrite
ZTest
Blend
排序方式
相机距离面试高分回答
NOTE
Render Queue 是 Unity 控制渲染顺序的队列编号,数值越小越早渲染,数值越大越晚渲染。常见队列有 Background 1000、Geometry 2000、AlphaTest 2450、Transparent 3000、Overlay 4000。不透明物体通常在 Geometry 队列并写入深度,Alpha Test 常用于树叶、草这种硬裁剪材质,Transparent 用于玻璃、粒子等半透明物体,通常需要排序和混合。需要注意,Render Queue 只控制渲染先后,真正的遮挡还要看深度测试,透明效果还要看 Blend 和 ZWrite。
ZWrite 和 ZTest 分别是什么?
ZWrite 和 ZTest 分别是什么?
一句话:ZTest 决定片元能不能通过深度测试,ZWrite 决定通过后要不要把深度写进深度缓冲。
先理解深度缓冲
屏幕上每个像素都有一个深度值,存在:
Depth Buffer它记录这个像素目前最近的物体深度。
比如:
当前像素记录的深度 = 0.4
新来的片元深度 = 0.2如果规则是“越小越近”,那 0.2 更靠近相机,通常可以通过。
ZTest 是什么
ZTest 控制的是:
新片元的深度,怎么和 Depth Buffer 里已有深度比较它问的是:
这个片元能不能画?常见规则:
ZTest Less:新片元更近才通过
ZTest LEqual:新片元更近或一样近才通过
ZTest Greater:新片元更远才通过
ZTest Always:永远通过
ZTest Never:永远不通过最常见的是:
ZTest LEqual意思是:
比当前记录更近,或者一样近,就通过ZWrite 是什么
ZWrite 控制的是:
片元通过 ZTest 后,要不要把自己的深度写入 Depth Buffer它问的是:
我要不要把这个深度记录下来?常见设置:
ZWrite On
ZWrite OffZWrite On:
通过深度测试后,把自己的深度写入 Depth Buffer这样后面的物体再画到同一个像素时,就会拿它的深度来比较。
ZWrite Off:
通过深度测试后,不更新 Depth Buffer也就是说它可以显示颜色,但不留下深度记录。
二者核心区别
ZTest:
控制能不能通过深度比较ZWrite:
控制通过后要不要写入深度缓冲所以顺序可以这样理解:
先 ZTest
再 ZWrite更完整一点:
片元来了
先和 Depth Buffer 做 ZTest
如果失败,片元丢弃
如果通过,可能写颜色
如果 ZWrite On,再更新 Depth Buffer不透明物体通常怎么设置
不透明物体通常:
ZTest LEqual
ZWrite On原因是:
不透明物体应该正常遮挡后面的东西比如墙挡住角色,箱子挡住地面,这些都依赖深度写入。
透明物体通常怎么设置
透明物体通常:
ZTest LEqual
ZWrite Off
Blend On原因是透明物体虽然要考虑遮挡关系,但它不应该完全挡死后面的透明层。
比如玻璃:
它自己要显示
但后面的东西也应该能透出来如果透明物体 ZWrite On,可能会导致后面的透明物体被深度测试直接丢掉,透明混合就错了。
ShaderLab 示例
c
Shader "Custom/ZWriteZTestExample" // 定义 Shader 名字
{ // Shader 外壳开始
SubShader // 定义一套渲染方案
{ // SubShader 块开始
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } // 设置为透明类型,并放入透明渲染队列
Pass // 定义一次具体绘制过程
{ // Pass 块开始
ZTest LEqual // 开启深度测试,深度更近或相等时通过
ZWrite Off // 关闭深度写入,让透明物体不把后面的透明层直接挡掉
Blend SrcAlpha OneMinusSrcAlpha // 开启常见 Alpha 混合
HLSLPROGRAM // HLSL 代码块开始
#pragma vertex vert // 指定顶点着色器函数名为 vert
#pragma fragment frag // 指定片元着色器函数名为 frag
struct appdata // 定义顶点输入结构
{ // appdata 结构体开始
float4 vertex : POSITION; // 接收模型空间顶点位置
}; // appdata 结构体结束
struct v2f // 定义顶点到片元的数据结构
{ // v2f 结构体开始
float4 vertex : SV_POSITION; // 保存裁剪空间位置
}; // v2f 结构体结束
v2f vert(appdata v) // 定义顶点着色器函数
{ // vert 函数开始
v2f o; // 创建输出结构体
o.vertex = UnityObjectToClipPos(v.vertex); // 把模型空间顶点变换到裁剪空间
return o; // 返回顶点着色器输出
} // vert 函数结束
float4 frag(v2f i) : SV_Target // 定义片元着色器函数
{ // frag 函数开始
return float4(0, 0.6, 1, 0.5); // 返回半透明蓝色
} // frag 函数结束
ENDHLSL // HLSL 代码块结束
} // Pass 块结束
} // SubShader 块结束
} // Shader 外壳结束常见坑
如果透明物体开了:
ZWrite On可能出现:
前面的透明物体把后面的透明物体挡没了如果用了:
ZTest Always它会无视深度关系,永远通过。 适合某些 UI、轮廓、调试效果,但普通物体乱用会导致遮挡错误。
如果 ZWrite Off,不代表物体不会被遮挡。 它仍然可以 ZTest LEqual,也就是:
它会被前面的不透明物体挡住
但它自己不写深度面试高分回答
NOTE
ZTest 是深度测试规则,用来决定当前片元的深度如何和深度缓冲中已有深度比较,比较通过才允许继续渲染。ZWrite 是深度写入开关,用来决定片元通过深度测试后,是否把自己的深度写入 Depth Buffer。不透明物体通常使用 ZTest LEqual 和 ZWrite On,这样可以正常遮挡后面的物体;透明物体通常使用 ZTest LEqual 和 ZWrite Off,这样它仍然会被前方物体遮挡,但不会把后面的透明层直接挡掉。
法线贴图原理是什么?
法线贴图原理是什么?
一句话:法线贴图用一张贴图保存“每个像素的法线方向”,让光照计算发生变化,从而让低模表面看起来像有凹凸细节。
先理解它解决什么问题
一个低模墙面可能只有几个顶点,真实几何很平。
如果只靠模型顶点法线,光照也会很平:
表面没什么细节
砖缝、划痕、凹凸都看不出来但如果真的把每个砖缝都建成模型,顶点数会暴涨。
所以法线贴图的思路是:
模型几何不变
但每个像素用不同的法线参与光照这样就能用低模表现高模细节。
法线贴图存的不是颜色
普通贴图的 RGB 是颜色。
法线贴图的 RGB 不是给你看的颜色,而是编码后的方向:
R 表示 X 方向
G 表示 Y 方向
B 表示 Z 方向因为贴图颜色范围是:
0 到 1但法线方向通常需要:
-1 到 1所以 Shader 里通常会做一次转换:
normal = rgb * 2 - 1比如:
RGB = (0.5, 0.5, 1.0)
转换后大概是 (0, 0, 1)这表示法线大致朝外。
为什么法线贴图常常是蓝紫色
因为切线空间法线默认大多接近:
(0, 0, 1)编码到 RGB 后就是:
(0.5, 0.5, 1.0)也就是:
红色中等
绿色中等
蓝色很高所以看起来就偏蓝紫色。
TBN 是什么
法线贴图里的方向通常是在:
切线空间 Tangent Space但光照计算很多时候需要在:
世界空间 World Space所以需要一个矩阵把法线从切线空间转到世界空间。
这个矩阵就是:
TBN它由三个方向组成:
Tangent:切线
Bitangent:副切线
Normal:模型法线你可以理解成:
TBN 是把法线贴图里的局部方向,转换到模型真实朝向上的工具法线贴图到底改变了什么
它改变的是:
光照计算用的法线方向它不改变:
模型顶点位置
模型真实轮廓
碰撞体形状
真实几何凹凸所以从正面看,凹凸感很明显。 但从侧面看,模型轮廓还是平的。
这也是法线贴图和真实建模的区别。
C# 示例:给材质设置法线贴图
下面是 Unity 里通过 C# 给材质设置法线贴图的例子。
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Renderer、Material 和 Texture2D
public class NormalMapSetter : MonoBehaviour // 定义一个给材质设置法线贴图的组件
{ // 类开始
public Texture2D normalMap; // 保存要设置到材质上的法线贴图
public float normalStrength = 1f; // 保存法线贴图强度
private Renderer cachedRenderer; // 缓存当前物体的 Renderer 组件
private Material cachedMaterial; // 缓存当前物体的材质实例
private void Awake() // Unity 在脚本实例加载时调用 Awake
{ // Awake 函数开始
cachedRenderer = GetComponent<Renderer>(); // 获取当前物体上的 Renderer 组件
cachedMaterial = cachedRenderer.material; // 获取运行时材质实例,避免直接修改共享材质
} // Awake 函数结束
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
if (normalMap == null) // 判断是否没有指定法线贴图
{ // if 语句开始
return; // 如果没有法线贴图,就直接返回
} // if 语句结束
cachedMaterial.SetTexture("_BumpMap", normalMap); // 把法线贴图设置到常见的法线贴图属性上
cachedMaterial.SetFloat("_BumpScale", normalStrength); // 设置法线贴图强度
cachedMaterial.EnableKeyword("_NORMALMAP"); // 启用材质中的法线贴图关键字
} // Start 函数结束
} // 类结束Unity 里要注意什么
法线贴图导入时不要当普通颜色图。
在 Unity 里通常要把 Texture Type 设置成:
c
Normal map否则 Unity 可能不会按法线贴图方式解码,光照就会不对。
常见问题:
凹凸方向反了
光照很怪
接缝明显
法线强度过大
贴图导入类型错误
切线方向不一致和高度图、置换贴图区别
法线贴图:
不改变真实几何
只改变光照法线
性能较好高度图:
存高度信息
可以用于视差、地形、置换等置换贴图:
可能真的移动顶点或细分后的表面
能改变轮廓
成本更高面试高分回答
IMPORTANT
法线贴图是一种用贴图保存像素级法线方向的技术。它不会改变模型真实顶点和轮廓,而是在片元着色阶段读取法线贴图,把 RGB 解码成法线方向,再通过 TBN 矩阵从切线空间转换到世界空间或视图空间,参与光照计算。这样低模表面也能产生丰富的明暗变化,看起来像有凹凸细节。它常用于砖墙、皮肤、衣服、金属划痕等细节表现,但不能改变真实碰撞和几何轮廓。
PBR 基本思想是什么?
PBR 基本思想是什么?
一句话:PBR 就是用更接近真实物理规律的方式描述材质和光照,让材质在不同环境下表现更稳定、更可信。
PBR 是什么
PBR 全称是:
Physically Based Rendering中文叫:
基于物理的渲染它不是某一个具体 Shader,而是一套思想:
用更符合真实世界光照规律的参数来描述材质比如以前可能会靠经验乱调:
颜色亮一点
高光强一点
反射多一点PBR 更希望用接近真实材质的参数:
基础颜色
金属度
粗糙度
法线
环境反射
AO让材质在不同光照环境下都尽量合理。
核心思想一:材质参数更物理化
PBR 常见参数有:
Base Color:基础颜色
Metallic:金属度
Roughness:粗糙度
Normal:法线
AO:环境遮蔽
Emission:自发光这些参数都比较容易对应真实世界。
比如:
木头是非金属
铁是金属
磨砂塑料粗糙度高
镜子粗糙度低核心思想二:金属和非金属不一样
在 PBR 里,金属和非金属的光照表现不同。
非金属:
有明显漫反射颜色
比如木头、皮肤、石头、塑料金属:
漫反射很弱
主要靠反射环境颜色
比如金、铁、铜、铝所以 Metallic 很重要。
Metallic = 0:非金属
Metallic = 1:金属中间值通常用于过渡或特殊材质。
核心思想三:粗糙度决定高光形状
Roughness 控制表面粗糙程度。
低粗糙度:
表面光滑
高光小而亮
反射清晰
像镜子、抛光金属高粗糙度:
表面粗糙
高光大而散
反射模糊
像磨砂塑料、粗糙石头所以粗糙度不是简单控制“亮不亮”,而是控制:
反射和高光有多集中核心思想四:能量守恒
PBR 很重要的一条原则是:
反射出去的光不能比进来的光更多这叫:
能量守恒比如一束光打到物体上,可能分成两部分:
漫反射
镜面反射如果镜面反射很强,漫反射通常就不能无限强。 否则物体会看起来像自己凭空发光,不真实。
核心思想五:环境光照很重要
PBR 很依赖环境反射,也就是:
IBL
Image Based Lighting比如一个金属球,它真正好不好看,很大程度取决于它反射了什么环境。
所以 PBR 项目里常见:
天空盒
反射探针
环境贴图
HDR 环境光没有好的环境光,PBR 材质也可能显得很假。
Unity 里怎么理解
Unity 的 Standard Shader、URP Lit、HDRP Lit 都是 PBR 思路。
常见材质参数:
Base Map
Base Color
Metallic
Smoothness
Normal Map
Occlusion
Emission注意 Unity 里经常用:
Smoothness而不是 Roughness。
它们关系大概是:
Smoothness = 1 - Roughness也就是说:
Smoothness 越高,越光滑
Roughness 越高,越粗糙C# 示例:设置一个 PBR 材质参数
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Renderer、Material、Color 和 Texture2D
public class PbrMaterialExample : MonoBehaviour // 定义一个演示 PBR 材质参数的 Unity 组件
{ // 类开始
public Color baseColor = Color.white; // 保存基础颜色,决定非金属材质的主要颜色
public float metallic = 0f; // 保存金属度,0 表示非金属,1 表示金属
public float smoothness = 0.5f; // 保存光滑度,越高高光越集中
public Texture2D normalMap; // 保存法线贴图,用来增加表面光照细节
private Renderer cachedRenderer; // 缓存当前物体上的 Renderer 组件
private Material cachedMaterial; // 缓存当前物体运行时使用的材质实例
private void Awake() // Unity 在脚本实例加载时调用 Awake
{ // Awake 函数开始
cachedRenderer = GetComponent<Renderer>(); // 获取当前物体上的 Renderer 组件
cachedMaterial = cachedRenderer.material; // 获取材质实例,避免直接修改共享材质
} // Awake 函数结束
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
cachedMaterial.SetColor("_BaseColor", baseColor); // 设置 URP Lit 常用的基础颜色属性
cachedMaterial.SetFloat("_Metallic", metallic); // 设置金属度属性
cachedMaterial.SetFloat("_Smoothness", smoothness); // 设置光滑度属性
if (normalMap != null) // 判断是否指定了法线贴图
{ // if 语句开始
cachedMaterial.SetTexture("_BumpMap", normalMap); // 设置法线贴图属性
cachedMaterial.EnableKeyword("_NORMALMAP"); // 启用法线贴图关键字
} // if 语句结束
} // Start 函数结束
} // 类结束常见误区
PBR 不等于自动真实。
想让 PBR 好看,还需要:
正确的贴图
合理的灯光
正确的色彩空间
反射探针
环境光
曝光和后处理
统一的材质规范如果参数乱填,比如:
木头 Metallic = 1
石头 Smoothness 拉满
皮肤像镜子一样反光那还是会很假。
面试高分回答
IMPORTANT
PBR 是基于物理的渲染思想,它用更接近真实世界的材质参数和光照规律来计算表面表现。常见参数包括 Base Color、Metallic、Roughness 或 Smoothness、Normal、AO 等。PBR 会区分金属和非金属材质,使用粗糙度控制高光和反射的集中程度,并遵循能量守恒原则,避免反射光凭空变多。同时 PBR 通常结合 IBL、反射探针和环境光,让材质在不同光照环境下表现更稳定。简单说,PBR 不是某个单独效果,而是一套让材质更符合真实物理规律的渲染方法。
Shadow Map 是什么?
Shadow Map 是什么?
一句话:Shadow Map 就是“从光源视角看到的深度图”,用它判断一个点有没有被别的物体挡住,从而决定这个点是不是在阴影里。
先用一句生活化的话理解
你可以把光源想成一台相机。
如果光源“看得到”某个点:
这个点能被光照到如果光源“看不到”某个点:
说明它被别的物体挡住了
这个点就在阴影里Shadow Map 就是把“光源看到的最近深度”提前记录下来。
Shadow Map 的基本流程
它通常分两遍。
第一遍:从光源视角渲染。
把光源当成相机
从光源方向看场景
不渲染颜色
只记录深度得到一张深度纹理:
Shadow Map这张图里每个像素存的是:
光源能看到的最近物体深度第二遍:从真正摄像机视角渲染。
正常渲染画面
对每个像素判断它是否在阴影里判断方法是:
把当前像素位置转换到光源空间
算出它距离光源的深度
拿这个深度和 Shadow Map 里记录的深度比较如果当前点更远:
说明光源前面已经有东西挡住它
所以它在阴影里核心判断公式
可以粗略理解为:
当前点到光源的深度 > Shadow Map 记录的深度那就说明:
当前点被遮挡
在阴影里如果深度差不多:
光源能直接看到它
它是受光的为什么 Shadow Map 是深度图,不是颜色图
普通贴图存颜色:
RGBShadow Map 存的是深度:
Depth它不关心物体是什么颜色,只关心:
从光源方向看,最近的表面在哪里所以 Shadow Map 本质是:
光源视角下的 Depth Buffer为什么会有阴影锯齿
因为 Shadow Map 是一张有限分辨率的纹理。
比如只有:
1024 x 1024但场景很大,阴影覆盖范围也很大。 那一个 Shadow Map 像素可能对应世界里很大一块区域。
结果阴影边缘就会出现:
锯齿
块状
抖动
模糊不准提高分辨率可以改善,但会增加显存和渲染成本。
为什么需要 Bias
Shadow Map 很容易遇到一个问题:
自己遮住自己这叫:
Shadow Acne
阴影痤疮原因是深度精度有限。
一个点理论上应该刚好等于 Shadow Map 里的深度,但因为精度误差,可能被判断成:
稍微更远于是它错误地进入阴影。
Bias 的作用就是:
给深度比较一点点偏移
避免自己错误遮住自己但 Bias 太大又会出现另一个问题:
Peter Panning
阴影和物体分离看起来像阴影飘起来了。
PCF 是什么
PCF 全称是:
Percentage Closer Filtering它的作用是柔化阴影边缘。
不是只采样 Shadow Map 一个点,而是采样周围多个点:
有些点在阴影里
有些点不在阴影里
最后算一个平均结果这样阴影边缘就不会那么硬。
级联阴影是什么
方向光照很远,比如太阳光。 如果只用一张 Shadow Map 覆盖整个视野,近处阴影质量可能很差。
所以常用:
Cascaded Shadow Maps
级联阴影它把摄像机视野分成几段:
近处一张 Shadow Map
中距离一张 Shadow Map
远处一张 Shadow Map近处用更高精度,远处可以低一点。 这样近处阴影会更清晰。
Unity 里怎么理解
Unity 中开启阴影通常需要:
Light 开启 Shadows
物体 Renderer 开启 Cast Shadows
接收阴影的物体开启 Receive Shadows
项目质量设置里配置阴影距离和分辨率常见参数:
Shadow Resolution
Shadow Distance
Shadow Bias
Normal Bias
Shadow Cascades这些都和 Shadow Map 的质量和性能有关。
C# 示例:设置灯光阴影参数
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Light、LightShadows 和 Debug
[RequireComponent(typeof(Light))] // 要求当前 GameObject 必须带有 Light 组件
public class ShadowMapLightSettingsExample : MonoBehaviour // 定义一个演示灯光阴影设置的组件
{ // 类开始
private Light cachedLight; // 缓存当前物体上的 Light 组件
private void Awake() // Unity 在脚本实例加载时调用 Awake
{ // Awake 函数开始
cachedLight = GetComponent<Light>(); // 获取当前物体上的 Light 组件
} // Awake 函数结束
private void Start() // Unity 在脚本启用后第一次 Update 前调用 Start
{ // Start 函数开始
cachedLight.shadows = LightShadows.Soft; // 开启软阴影,让 Unity 使用阴影贴图并进行过滤
cachedLight.shadowStrength = 0.8f; // 设置阴影强度,数值越大阴影越明显
cachedLight.shadowBias = 0.05f; // 设置阴影 Bias,用来减少自阴影瑕疵
cachedLight.shadowNormalBias = 0.4f; // 设置法线方向 Bias,用来进一步减少阴影痤疮
Debug.Log("Shadow settings applied"); // 输出阴影设置完成的信息
} // Start 函数结束
} // 类结束常见坑
Shadow Map 分辨率太低:
阴影边缘锯齿明显Shadow Distance 太大:
同样分辨率要覆盖更大范围
近处阴影会变糊Bias 太小:
容易出现阴影痤疮Bias 太大:
阴影和物体脱离透明物体:
投射阴影和接收阴影通常需要特殊处理面试高分回答
NOTE
Shadow Map 是一种基于深度图的阴影技术。它会先从光源视角渲染场景,只记录深度,生成一张 Shadow Map。之后从摄像机视角正常渲染时,把当前像素的位置转换到光源空间,拿当前深度和 Shadow Map 中记录的最近深度比较。如果当前点比 Shadow Map 中的深度更远,说明它被更靠近光源的物体挡住了,所以处于阴影中。Shadow Map 的常见问题包括阴影锯齿、Shadow Acne 和 Peter Panning,通常通过提高分辨率、PCF 过滤、Bias、Normal Bias 和级联阴影来改善。
后处理是什么?
后处理是什么?
后处理 Post Processing,就是“场景已经渲染成一张画面之后,再对这张画面做二次加工”。
你可以把它理解成:游戏先把场景拍成一张照片,然后再给这张照片加滤镜。它通常不改变模型本身,而是改变最终屏幕效果。
基本流程
- 相机先把 3D 场景渲染到一张图里。
- 这张图通常叫 Color Buffer 或 Render Texture。
- 后处理 Shader 读取这张图。
- 对每个像素做处理。
- 最后输出到屏幕。
常见后处理效果
Bloom:亮的地方向外发光。
Color Grading:整体调色,比如偏暖、偏冷、电影感。
Tonemapping:把 HDR 高亮颜色压到屏幕能显示的范围。
Depth of Field:景深,远处或近处模糊。
Motion Blur:运动模糊。
Vignette:暗角。
Anti-Aliasing:抗锯齿。
SSAO:屏幕空间环境遮蔽,让角落和缝隙更有阴影感。
Unity 里怎么用?
URP / HDRP 里一般用 Volume 系统:
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour 和 Debug
using UnityEngine.Rendering; // 引入渲染命名空间,用来使用 Volume 和 VolumeProfile
using UnityEngine.Rendering.Universal; // 引入 URP 命名空间,用来使用 Bloom
public class PostProcessingBloomExample : MonoBehaviour // 定义一个控制 Bloom 后处理强度的组件
{ // 类开始
public Volume volume; // 保存场景中的 Volume 组件引用
public float bloomIntensity = 2f; // 保存想要设置的 Bloom 强度
private void Start() // Unity 会在脚本第一次 Update 之前调用 Start
{ // Start 函数开始
if (volume == null) // 判断是否没有指定 Volume
{ // if 语句开始
Debug.LogWarning("Volume is missing"); // 输出警告,提示缺少 Volume
return; // 没有 Volume 就直接结束函数
} // if 语句结束
VolumeProfile profile = volume.profile; // 获取 Volume 使用的 Profile
if (profile == null) // 判断 Profile 是否为空
{ // if 语句开始
Debug.LogWarning("Volume Profile is missing"); // 输出警告,提示缺少 Profile
return; // 没有 Profile 就直接结束函数
} // if 语句结束
bool hasBloom = profile.TryGet<Bloom>(out Bloom bloom); // 尝试从 Profile 中获取 Bloom 效果
if (hasBloom == false) // 判断是否没有找到 Bloom
{ // if 语句开始
Debug.LogWarning("Bloom is missing in Volume Profile"); // 输出警告,提示 Profile 中没有 Bloom
return; // 没有 Bloom 就直接结束函数
} // if 语句结束
bloom.active = true; // 启用 Bloom 后处理效果
bloom.intensity.Override(bloomIntensity); // 覆盖 Bloom 的强度参数
Debug.Log("Bloom intensity updated"); // 输出设置完成的信息
} // Start 函数结束
} // 类结束面试高分回答
TIP
后处理是在场景渲染完成后,对颜色缓冲或 Render Texture 再进行全屏处理的一类效果。它通常通过全屏 Shader Pass 读取上一阶段画面,并结合深度、法线、运动向量等缓冲,计算 Bloom、调色、景深、运动模糊、抗锯齿、SSAO 等效果。它不改变模型几何,而是改变最终画面表现。因为很多后处理是全屏采样,所以分辨率、采样次数和效果数量都会直接影响性能,移动端尤其要控制开销。
RenderTexture 有什么用?
RenderTexture 有什么用?
RenderTexture 可以理解成一张“可以被相机画进去的 Texture”。
普通 Texture 多半是提前做好的图片,比如 PNG、TGA、贴图资源;RenderTexture 是运行时生成的画面,比如让一个相机把角色、小地图、监控画面、镜子画面先渲染到 RenderTexture 里,然后再把这张 RenderTexture 显示到 UI 或贴到模型材质上。
SVG 可下载路径:C:\Users\Administrator\Documents\Codex\2026-07-04\struct-class\outputs\rendertexture-usage-csharp.svg
一句话理解
RenderTexture = GPU 上的一张临时画布。
相机可以把画面画到这张画布上,Shader、UI、材质又可以把它当成普通贴图来用。
常见用途
小地图:用一个俯视相机把地图画到 RenderTexture,再显示到 UI 上。
角色预览:背包、商城、换装界面里,用单独相机渲染角色模型。
监控屏:游戏里的电视、监控器、摄像头画面。
镜子 / 传送门:另一个相机从特殊角度渲染画面,再贴到平面上。
后处理:先把画面渲染到 RenderTexture,再进行模糊、泛光、调色等处理。
截图 / 缩略图:把某个模型或场景渲染成一张图。
Unity 示例:相机画到 RenderTexture,再显示到 RawImage
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Camera、RenderTexture 和 MonoBehaviour
using UnityEngine.UI; // 引入 UnityEngine.UI 命名空间,用来使用 RawImage
public class RenderTextureExample : MonoBehaviour // 定义一个演示 RenderTexture 用法的脚本组件
{ // 类开始
public Camera renderCamera; // 保存专门负责渲染画面的相机
public RawImage previewImage; // 保存用来显示 RenderTexture 的 UI 图片
public int textureWidth = 512; // 设置 RenderTexture 的宽度
public int textureHeight = 512; // 设置 RenderTexture 的高度
private RenderTexture renderTexture; // 保存运行时创建出来的 RenderTexture
private void Start() // Unity 会在脚本第一次 Update 之前调用 Start
{ // Start 函数开始
renderTexture = new RenderTexture(textureWidth, textureHeight, 24); // 创建一张带 24 位深度缓冲的 RenderTexture
renderTexture.Create(); // 在 GPU 上真正创建这张 RenderTexture 资源
renderCamera.targetTexture = renderTexture; // 让相机不再直接画到屏幕,而是画到 RenderTexture
previewImage.texture = renderTexture; // 让 RawImage 把这张 RenderTexture 显示到 UI 上
} // Start 函数结束
private void OnDestroy() // Unity 会在对象销毁时调用 OnDestroy
{ // OnDestroy 函数开始
if (renderCamera != null) // 判断相机引用是否还存在
{ // if 语句开始
renderCamera.targetTexture = null; // 解除相机和 RenderTexture 的绑定
} // if 语句结束
if (renderTexture != null) // 判断 RenderTexture 是否已经创建
{ // if 语句开始
renderTexture.Release(); // 释放 GPU 上的 RenderTexture 资源
Destroy(renderTexture); // 销毁 Unity 对象层面的 RenderTexture
} // if 语句结束
} // OnDestroy 函数结束
} // 类结束为什么它很重要?
因为很多高级画面效果,本质都是“先画到一张中间图,再处理这张图”。
比如后处理不是直接改场景里的模型,而是先拿到相机渲染完的画面,然后对整张画面做模糊、泛光、调色。这个“中间图”经常就是 RenderTexture。
性能注意
RenderTexture 很有用,但不是免费的。
分辨率越高,占用显存越多。
更新越频繁,GPU 压力越大。
如果小地图不需要每帧刷新,可以降低刷新频率。
如果只是 UI 预览,不一定需要 1024 或 2048,很多时候 512 就够了。
动态创建的 RenderTexture 用完要 Release,否则可能造成显存浪费。
面试高分回答
RenderTexture 是一种可以作为渲染目标的纹理。普通 Texture 主要用于采样,而 RenderTexture 可以让 Camera 或渲染管线把画面先渲染进去,然后再作为纹理给 UI、材质、Shader 或后处理使用。它常用于小地图、角色预览、监控屏、镜子、传送门、截图和后处理。需要注意的是,RenderTexture 通常占用 GPU 显存,分辨率和刷新频率都会影响性能,运行时创建后也要正确释放。
GPU Instancing 如何减少 DrawCall?
GPU Instancing 如何减少 DrawCall?
GPU Instancing 的核心是:很多物体长得一样,只是位置、旋转、缩放、颜色不同,那就不要一个一个提交 DrawCall,而是“同一个 Mesh + 同一个 Material,一次提交一批实例数据”。
普通渲染像这样:1000 棵同样的草,CPU 可能要向 GPU 提交很多次绘制命令。GPU Instancing 像这样:CPU 只告诉 GPU:“这个草模型画 1000 次,每次用不同的矩阵和颜色。”于是 DrawCall 数量就降下来了。
它到底减少了什么?
减少的是 CPU 向 GPU 提交绘制命令的次数,也就是 DrawCall 数量。
它不是减少三角形数量,也不是减少像素着色开销。1000 棵草还是要画 1000 棵,只是 CPU 不用每棵草都单独发一次命令。
底层理解
没有 Instancing 时:
CPU 提交第 1 个物体:设置 Mesh、Material、Transform,然后 Draw。
CPU 提交第 2 个物体:设置 Mesh、Material、Transform,然后 Draw。
CPU 提交第 3 个物体:继续重复。
有 Instancing 时:
CPU 设置一次 Mesh。
CPU 设置一次 Material。
CPU 传一批实例数据,比如每个实例的位置矩阵、颜色、缩放。
GPU 根据 instance id 自己取对应数据,把同一个模型画很多遍。
适合什么场景?
草、树、石头、子弹、金币、弹壳、特效碎片、地面装饰物、重复怪物、重复建筑组件。
一句话:大量相同 Mesh、相同 Material,只是位置或少量参数不同的物体。
Unity C# 示例:用 Graphics.DrawMeshInstanced
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 Mesh、Material、Matrix4x4 和 MonoBehaviour
public class GpuInstancingExample : MonoBehaviour // 定义一个演示 GPU Instancing 的脚本组件
{ // 类开始
public Mesh mesh; // 保存要被重复绘制的同一个网格模型
public Material material; // 保存要被重复使用的同一个材质
public int count = 100; // 保存要绘制的实例数量
private Matrix4x4[] matrices; // 保存每个实例自己的变换矩阵
private void Start() // Unity 会在脚本第一次 Update 之前调用 Start
{ // Start 函数开始
material.enableInstancing = true; // 开启材质的 GPU Instancing 支持
matrices = new Matrix4x4[count]; // 创建矩阵数组,用来保存每个实例的位置旋转缩放
for (int i = 0; i < count; i++) // 循环生成每一个实例的数据
{ // for 循环开始
Vector3 position = new Vector3(i % 10 * 2f, 0f, i / 10 * 2f); // 计算当前实例的位置
Quaternion rotation = Quaternion.identity; // 设置当前实例不旋转
Vector3 scale = Vector3.one; // 设置当前实例使用默认缩放
matrices[i] = Matrix4x4.TRS(position, rotation, scale); // 把位置旋转缩放组合成一个矩阵
} // for 循环结束
} // Start 函数结束
private void Update() // Unity 每帧调用 Update
{ // Update 函数开始
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, count); // 一次 DrawCall 批量绘制多个实例
} // Update 函数结束
} // 类结束注意点
Graphics.DrawMeshInstanced 一批最多通常是 1023 个实例,更多数量要分批,或者使用 DrawMeshInstancedIndirect。
Mesh 必须相同。
Material 必须相同。
Shader Pass 要支持 Instancing。
如果每个对象用了不同材质,就很难合批。
如果你频繁修改普通 Material 参数,可能会打断 Instancing。
如果只是颜色、强度这种每个实例不同的小参数,应该用实例化属性或 MaterialPropertyBlock。
面试高分回答
NOTE
GPU Instancing 是一种减少 DrawCall 的渲染优化方式。它适用于大量使用相同 Mesh 和相同 Material 的物体。普通渲染会让 CPU 为每个物体单独提交绘制命令,而 Instancing 会把每个物体的 Transform、颜色等实例数据打包成数组,一次提交给 GPU。GPU 在顶点着色阶段通过 instance id 读取对应实例的数据,把同一个 Mesh 绘制多次。它主要减少 CPU 提交 DrawCall 和渲染状态切换的开销,但不会减少实际需要绘制的三角形和像素数量。
SRP Batcher 需要满足什么条件?
SRP Batcher 需要满足什么条件?
SRP Batcher 是 Unity 在 URP / HDRP 里的 CPU 渲染优化机制。它的重点不是把 DrawCall 变少,而是让 CPU 在提交 DrawCall 时少做重复的材质参数绑定、Shader 参数设置、渲染状态切换。
一句话:DrawCall 可能还在,但每个 DrawCall 更便宜了。
条件一:必须使用 SRP 管线
SRP Batcher 里的 SRP 指的是 Scriptable Render Pipeline。
所以它需要:
使用 URP。
或者使用 HDRP。
或者使用自定义 SRP。
Built-in 内置渲染管线不走 SRP Batcher。
在 URP 里,还要在 URP Pipeline Asset 里开启 SRP Batcher。
条件二:Shader 必须兼容 SRP Batcher
这是最关键的条件。
Shader 里的材质属性必须统一放进一个叫 UnityPerMaterial 的常量缓冲区里。
每个物体相关的数据,比如对象矩阵,要放在 UnityPerDraw 这类 Unity 规定的缓冲区里。
简单理解:
UnityPerMaterial:这个材质自己的参数,比如颜色、金属度、粗糙度。
UnityPerDraw:这个物体自己的参数,比如模型矩阵、世界矩阵。
如果 Shader 把材质属性乱放,SRP Batcher 就很难高效缓存这些数据。
Shader 示例
c
CBUFFER_START(UnityPerMaterial) // 开始声明材质级别的常量缓冲区,SRP Batcher 要求材质属性集中放这里
float4 _BaseColor; // 声明基础颜色,每个材质可以有自己的颜色
float _Metallic; // 声明金属度参数,每个材质可以有自己的金属度
float _Smoothness; // 声明光滑度参数,每个材质可以有自己的光滑度
CBUFFER_END // 结束材质级别的常量缓冲区声明条件三:尽量使用相同 Shader Variant
SRP Batcher 最喜欢这种情况:
很多物体使用同一个 Shader。
可以是不同 Material。
但最好走的是同一个 Shader Variant。
Shader Variant 可以理解成“同一个 Shader 编译出来的不同版本”。比如一个材质开了某个关键字,另一个材质没开,就可能变成不同 Variant。Variant 越乱,CPU 切换成本就越高。
条件四:不要随便用 MaterialPropertyBlock
MaterialPropertyBlock 很常用,可以给不同 Renderer 设置不同参数,而不复制 Material。
但是它可能让对象不走 SRP Batcher 的高效路径,尤其是你频繁改材质属性时。
所以面试里可以这样说:
如果只是少量对象需要单独改颜色,MaterialPropertyBlock 很方便。
如果是大量对象追求 SRP Batcher 性能,要小心使用它,最好用 Frame Debugger 或 Profiler 验证。
SRP Batcher 和 GPU Instancing 的区别
SRP Batcher:DrawCall 可能没少,但 CPU 设置每个 DrawCall 的成本降低。
GPU Instancing:相同 Mesh + 相同 Material 的物体,可以一次 DrawCall 画很多个实例。
Dynamic Batching:Unity 尝试把小网格合并后一起画。
所以它们不是一回事。
怎么检查有没有生效?
可以用 Unity 的 Frame Debugger。
看某个 DrawCall 是否显示 SRP Batcher 兼容。
如果不兼容,Unity 通常会告诉你原因,比如 Shader 属性没有放进正确的 CBUFFER。
面试高分回答
IMPORTANT
SRP Batcher 是 URP / HDRP 中用于降低 CPU 渲染提交开销的机制。它要求项目使用 SRP 管线,并在管线资源中开启 SRP Batcher。Shader 也必须兼容,也就是材质属性要集中放在 UnityPerMaterial 常量缓冲区中,对象级数据放在 Unity 规定的 per draw 缓冲区中。它的优化点不是减少 DrawCall 数量,而是让材质数据可以长期缓存在 GPU 端,CPU 在连续绘制相同 Shader Variant 的对象时不需要反复绑定大量参数,从而降低 SetPass 和状态切换成本。
透明物体排序有什么问题?
透明物体排序有什么问题?
透明物体最大的问题是:它的最终颜色依赖绘制顺序。
不透明物体一般可以写入深度缓冲,谁离相机近,谁就挡住后面的像素。但透明物体通常要做颜色混合,不能简单地把后面的东西完全挡掉,所以经常需要“从远到近”绘制。
为什么透明物体需要排序?
透明物体通常是这样混合的:
最终颜色 = 当前透明物体颜色 × 透明度 + 背景颜色 × 剩余透明度
也就是说,透明物体画出来的颜色,要依赖“它背后已经画好的颜色”。
所以如果你先画近处透明物体,再画远处透明物体,结果可能错。
正确思路通常是:
先画不透明物体。
再画透明物体。
透明物体尽量从远到近画。
为什么排序会出问题?
因为 Unity 很多时候是按“物体中心点到相机的距离”来排序透明物体。
但是中心点排序很粗糙。
一个透明物体可能很大。
两个透明物体可能互相穿插。
一个物体的一部分在前面,另一部分在后面。
粒子系统里每个粒子也可能互相遮挡。
这时候只按“整个物体”排序,就无法保证每个像素都正确。
最典型的问题:交叉透明物体
比如两个半透明玻璃片互相穿插。
从相机看过去:
左边可能是蓝色玻璃在前。
右边可能是红色玻璃在前。
如果你让蓝色整体先画,右边可能错。
如果你让红色整体先画,左边可能错。
这就是透明排序的本质难题:有些情况根本不能靠“对象级排序”完美解决。
Unity 里常见表现
透明材质互相闪烁。
角色半透明头发显示不对。
玻璃后面的玻璃颜色怪。
粒子特效前后关系错乱。
水面、烟雾、能量罩互相遮挡异常。
UI 和 3D 透明物体层级不符合预期。
为什么透明物体通常 ZWrite Off?
透明物体如果写入深度,就可能把后面的透明物体提前挡掉。
比如你先画一个半透明玻璃,它写入了深度。后面另一个透明玻璃再画时,深度测试发现自己在后面,就不画了。这样就看不到后面的透明层了。
所以透明材质常见配置是:
ZWrite Off // 关闭深度写入,避免透明物体把后面的透明层直接挡掉
ZTest LEqual // 保留深度测试,避免被不透明物体挡住时还强行显示
Blend SrcAlpha OneMinusSrcAlpha // 使用常见 Alpha 混合,让当前颜色和背景颜色按透明度混合怎么解决或缓解?
能不用半透明就不用半透明。
比如树叶、铁丝网、破洞布料,优先考虑 Alpha Test / Cutout。Cutout 是要么显示,要么丢弃,不需要复杂透明混合,排序问题少很多。
把大透明物体拆小。
因为 Unity 按物体中心排序,物体越大,排序越容易错。拆成多个小网格后,排序粒度更细,错误会少一些。
手动调整 Render Queue。
比如让某些透明物体更早或更晚绘制。
使用 Sorting Layer / Order in Layer。
这对 2D、Sprite、UI、特定透明对象很有用。
特殊效果用特殊方案。
比如头发、烟雾、水、玻璃、粒子,有时要用深度预写、双 Pass、屏幕空间效果,甚至 Order Independent Transparency 这类更复杂算法。
Unity C# 示例:手动调整透明材质队列
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour 和 Renderer
public class TransparentQueueExample : MonoBehaviour // 定义一个演示透明物体渲染队列的脚本
{ // 类开始
public Renderer targetRenderer; // 保存需要调整渲染队列的 Renderer
public int renderQueue = 3100; // 设置目标渲染队列,透明队列通常从 3000 开始
private void Start() // Unity 会在脚本第一次 Update 之前调用 Start
{ // Start 函数开始
if (targetRenderer == null) // 判断是否没有指定 Renderer
{ // if 语句开始
Debug.LogWarning("Target Renderer is missing"); // 输出警告,提示缺少 Renderer
return; // 没有 Renderer 就直接结束函数
} // if 语句结束
Material material = targetRenderer.material; // 获取当前 Renderer 的材质实例
material.renderQueue = renderQueue; // 设置材质的渲染队列,数值越大越晚绘制
Debug.Log("Transparent render queue updated"); // 输出设置完成的信息
} // Start 函数结束
} // 类结束面试高分回答
WARNING
透明物体排序的问题来自 Alpha Blend 对绘制顺序的依赖。透明物体通常关闭深度写入,只保留深度测试,然后从远到近绘制,让前景颜色和已经绘制好的背景颜色混合。但实际项目中 Unity 往往按 Renderer 的中心点距离排序,这种对象级排序无法处理大型透明物体、互相交叉的透明物体、粒子互相穿插等情况,所以会出现透明层级错误、闪烁和遮挡异常。解决时可以优先使用 Cutout,拆分网格,调整 Render Queue、Sorting Layer,或者针对复杂透明效果使用深度预写、多 Pass 或更高级的透明算法。
Overdraw 是什么?
Overdraw 是什么?
Overdraw 就是“同一个屏幕像素被画了很多次”。
比如屏幕上某个像素位置,先画了背景,又画了烟雾,又画了火焰,又画了半透明 UI。最后你看到的可能只是最上面那层,但 GPU 前面几层也可能都算过了,这些重复计算就是 Overdraw。
为什么它会影响性能?
GPU 渲染一个像素时,不只是“涂一下颜色”这么简单。
它可能要执行片元着色器,采样贴图,计算透明混合,读取深度,写入颜色缓冲。一个像素如果被画 5 次,这些工作就可能重复做 5 次。
所以 Overdraw 高,本质上是在浪费 GPU 的像素填充能力,也常说浪费 Fill Rate。
最容易产生 Overdraw 的地方
粒子特效:烟雾、火焰、爆炸、魔法光效,经常是很多半透明面片叠在一起。
UI:多个全屏半透明面板、背景遮罩、渐变图、装饰图叠加。
透明物体:玻璃、水面、能量罩、半透明角色、树叶草丛。
后处理:很多后处理是全屏 Pass,每多一次就是整屏再画一遍。
大面积空白贴图:比如一个特效贴图只有中间一点火花,但整张透明图很大,透明区域也可能参与绘制。
为什么不透明物体问题小一点?
不透明物体通常会写入深度缓冲。
近处物体写入深度后,后面更远的像素可能被深度测试提前丢弃,不需要继续执行完整片元计算。
但透明物体通常是:
ZWrite Off // 关闭深度写入,避免透明物体直接挡掉后面的透明层
Blend SrcAlpha OneMinusSrcAlpha // 开启 Alpha 混合,让当前颜色和背景颜色按透明度混合因为透明物体要看到后面的东西,所以不能像不透明物体那样简单挡住后面。这样多层透明叠加时,每一层都可能要画,Overdraw 就高了。
怎么排查 Overdraw?
Unity Scene 视图里可以看 Overdraw 模式。
Frame Debugger 可以看哪些对象反复覆盖屏幕。
Profiler 的 GPU 模块可以看渲染耗时。
RenderDoc 可以更细地分析每个 Pass、每次绘制和像素覆盖情况。
移动端还要特别注意,因为移动 GPU 对 Fill Rate 很敏感,Overdraw 高容易掉帧、发热、耗电。
怎么优化?
减少透明层数。
减少粒子数量和粒子尺寸。
远处粒子降低发射率。
用更紧的 Sprite Mesh,避免巨大透明空白区域。
UI 上不要堆太多全屏半透明图。
能用 Cutout 的地方,少用 Alpha Blend。
特效贴图尽量裁掉无用透明边缘。
大型透明物体拆分或降低复杂度。
后处理控制数量和分辨率。
C# 示例:根据距离降低粒子数量,减少远处 Overdraw
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Camera 和 ParticleSystem
public class ParticleOverdrawLimiter : MonoBehaviour // 定义一个根据距离降低粒子发射率的脚本
{ // 类开始
public ParticleSystem particle; // 保存需要被优化的粒子系统
public Camera targetCamera; // 保存用于计算距离的相机
public float nearRate = 80f; // 保存近距离时的粒子发射率
public float farRate = 10f; // 保存远距离时的粒子发射率
public float nearDistance = 5f; // 保存近距离阈值
public float farDistance = 30f; // 保存远距离阈值
private void Update() // Unity 每帧调用 Update
{ // Update 函数开始
if (particle == null) // 判断粒子系统是否为空
{ // if 语句开始
return; // 没有粒子系统就直接返回
} // if 语句结束
if (targetCamera == null) // 判断相机是否为空
{ // if 语句开始
targetCamera = Camera.main; // 尝试使用场景中的主相机
} // if 语句结束
if (targetCamera == null) // 判断是否仍然找不到相机
{ // if 语句开始
return; // 没有相机就无法计算距离,直接返回
} // if 语句结束
float distance = Vector3.Distance(transform.position, targetCamera.transform.position); // 计算粒子和相机之间的距离
float t = Mathf.InverseLerp(nearDistance, farDistance, distance); // 把距离转换成 0 到 1 的插值比例
float rate = Mathf.Lerp(nearRate, farRate, t); // 根据距离计算当前发射率
ParticleSystem.EmissionModule emission = particle.emission; // 获取粒子系统的发射模块
emission.rateOverTime = rate; // 设置粒子每秒发射数量
} // Update 函数结束
} // 类结束面试高分回答
IMPORTANT
Overdraw 是指同一个屏幕像素被重复绘制多次。它常发生在透明物体、粒子、UI 和全屏后处理里。因为透明混合通常不能像不透明物体那样依赖深度写入提前剔除,所以多层透明叠加时,每一层都可能执行片元着色和混合,导致 GPU Fill Rate 压力变大。优化时可以减少透明层数、降低粒子数量、裁剪透明贴图空白区域、使用更紧的 Sprite Mesh、减少全屏 UI 叠加,并通过 Unity 的 Overdraw 视图、Frame Debugger、Profiler 或 RenderDoc 定位问题。
如何定位 GPU 性能瓶颈?
如何定位 GPU 性能瓶颈?
定位 GPU 瓶颈不要靠猜,要按流程来:先确认是不是 GPU 慢,再判断慢在“像素、Shader、阴影、后处理、几何、带宽”哪一类。
第一步:先确认是不是 GPU 瓶颈
一帧总耗时超过预算,不一定是 GPU 的问题。
60 FPS 的预算大约是 16.67 ms。
30 FPS 的预算大约是 33.33 ms。
如果 CPU 一帧 25 ms,GPU 一帧 8 ms,那主要是 CPU 瓶颈。
如果 CPU 一帧 8 ms,GPU 一帧 25 ms,那才是 GPU 瓶颈。
Unity 里可以看:
Profiler 的 GPU Usage。
Profiler 的 CPU Timeline。
Frame Debugger。
RenderDoc。
移动端还可以用 Android GPU Inspector、Xcode GPU Frame Capture、Snapdragon Profiler、Mali Graphics Debugger 等工具。
第二步:用“开关实验”快速判断类型
降低分辨率,如果帧率明显上升,通常说明像素压力大,可能是 Overdraw、后处理、透明、Fill Rate 问题。
关闭后处理,如果帧率明显上升,说明 Bloom、SSAO、景深、运动模糊、抗锯齿等全屏 Pass 比较贵。
关闭阴影,如果帧率明显上升,说明 Shadow Map、阴影距离、级联阴影、阴影分辨率比较贵。
关闭粒子和透明物体,如果帧率明显上升,说明 Overdraw 或透明排序相关渲染压力较大。
换成简单 Shader,如果帧率明显上升,说明片元着色器、贴图采样、PBR、法线贴图、分支计算比较贵。
降低模型面数,如果帧率明显上升,说明顶点压力、骨骼动画、蒙皮或几何量比较高。
第三步:常见 GPU 瓶颈类型
像素瓶颈:分辨率高、Overdraw 高、透明层太多、全屏 UI 叠加。
Shader 瓶颈:片元 Shader 复杂、贴图采样多、分支多、PBR 计算重。
阴影瓶颈:实时光太多、Shadow Map 分辨率高、级联阴影多、阴影距离太远。
后处理瓶颈:Bloom、SSAO、景深、运动模糊、抗锯齿、多次全屏 Pass。
几何瓶颈:模型面数高、顶点 Shader 重、骨骼动画多、蒙皮角色多。
带宽瓶颈:HDR、MSAA、高分辨率 RenderTexture、多 Render Target、频繁读写大纹理。
Unity C# 示例:打印 CPU / GPU 帧耗时
c
using UnityEngine; // 引入 UnityEngine 命名空间,用来使用 MonoBehaviour、Debug 和 FrameTimingManager
public class GpuFrameTimeLogger : MonoBehaviour // 定义一个打印 CPU 和 GPU 帧耗时的脚本
{ // 类开始
private FrameTiming[] timings = new FrameTiming[1]; // 创建一个数组,用来接收最近一帧的耗时信息
private float timer = 0f; // 保存计时器,用来控制日志输出频率
private void Update() // Unity 每帧调用 Update
{ // Update 函数开始
FrameTimingManager.CaptureFrameTimings(); // 请求 Unity 捕获当前帧的 CPU 和 GPU 时间
timer += Time.unscaledDeltaTime; // 使用不受时间缩放影响的时间累加计时器
if (timer < 1f) // 判断是否还没到一秒
{ // if 语句开始
return; // 没到一秒就直接返回,避免每帧刷屏
} // if 语句结束
timer = 0f; // 重置计时器
uint count = FrameTimingManager.GetLatestTimings(1, timings); // 获取最近一帧的耗时数据
if (count == 0) // 判断当前平台是否没有返回耗时数据
{ // if 语句开始
Debug.Log("Frame timing is not available on this platform"); // 输出提示,说明当前平台可能不支持
return; // 没有数据就直接返回
} // if 语句结束
double cpuMs = timings[0].cpuFrameTime; // 读取 CPU 一帧耗时,单位是毫秒
double gpuMs = timings[0].gpuFrameTime; // 读取 GPU 一帧耗时,单位是毫秒
Debug.Log($"CPU Frame: {cpuMs:F2} ms, GPU Frame: {gpuMs:F2} ms"); // 打印 CPU 和 GPU 的帧耗时
} // Update 函数结束
} // 类结束注意一个面试坑
DrawCall 多,不一定就是 GPU 瓶颈。
DrawCall 很多时,常见瓶颈反而在 CPU,因为 CPU 要提交很多渲染命令。
GPU 瓶颈更关注:GPU Frame Time、片元数量、Overdraw、Shader 复杂度、阴影、后处理、显存带宽。
面试高分回答
TIP
定位 GPU 性能瓶颈时,我会先用 Profiler 确认 GPU Frame Time 是否超过帧预算,避免把 CPU 问题误判成 GPU 问题。确认是 GPU 瓶颈后,我会用控制变量法排查:降低分辨率判断 Fill Rate 和 Overdraw,关闭后处理判断全屏 Pass,关闭阴影判断 Shadow Map,关闭透明和粒子判断 Overdraw,替换简单 Shader 判断片元计算和采样成本,降低模型复杂度判断顶点压力。最后用 Frame Debugger 或 RenderDoc 找到具体昂贵的 Pass、DrawCall、RenderTexture 或 Shader。这样不是凭感觉优化,而是用数据定位瓶颈。