Appearance
图形渲染岗
线性代数和空间变换必须会
面试回答:
TIP
线性代数和空间变换必须会,因为游戏里的移动、旋转、相机、碰撞、射线、渲染,本质都是空间计算。
你至少要能讲清:向量表示方向和距离,点乘判断夹角,叉乘求垂直方向,矩阵表示平移旋转缩放,四元数表示旋转,坐标空间负责把一个点从局部空间变到世界、视图、裁剪、屏幕空间。
加分说法:
“我不会只背 Unity API,比如 TransformPoint 或 WorldToScreenPoint。我会先判断当前数据在哪个空间,再判断目标需要哪个空间。角色技能范围、相机跟随、UI 血条跟随、Shader 里的 MVP 变换,本质都是空间变换问题。”
简单代码:世界坐标转屏幕 UI 坐标
c
using UnityEngine; // 引入 Unity 引擎命名空间,用来使用 Camera、Vector3、RectTransform
using UnityEngine.UI; // 引入 Unity UI 命名空间,用来处理 UI 相关组件
public class WorldHpBarFollow : MonoBehaviour // 定义血条跟随脚本,用来演示空间变换
{ // WorldHpBarFollow 类开始
public Transform target; // 保存世界中的目标对象,例如怪物头顶位置
public Camera worldCamera; // 保存渲染 3D 世界的相机
public RectTransform hpBar; // 保存血条 UI 的 RectTransform
public RectTransform canvasRoot; // 保存 Canvas 根节点的 RectTransform
private void LateUpdate() // LateUpdate 在 Update 后调用,适合做相机和 UI 跟随
{ // LateUpdate 方法开始
Vector3 worldPos = target.position + Vector3.up * 2f; // 计算血条要跟随的世界坐标位置
Vector3 screenPos = worldCamera.WorldToScreenPoint(worldPos); // 把世界坐标转换成屏幕坐标
Vector2 uiPos; // 声明一个 UI 局部坐标变量,用来接收转换结果
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRoot, screenPos, null, out uiPos); // 把屏幕坐标转换成 Canvas 内的局部坐标
hpBar.anchoredPosition = uiPos; // 把血条放到对应的 UI 坐标位置
} // LateUpdate 方法结束
} // WorldHpBarFollow 类结束渲染管线必须会画
面试回答:
渲染管线必须会画,而且要画得顺序准确。最简版本可以这样画:
场景对象 -> CPU 提交 DrawCall -> 顶点着色器 -> 裁剪 -> 光栅化 -> 片元着色器 -> 深度/模板测试 -> 混合 -> FrameBuffer -> 屏幕每一步怎么讲:
CPU 阶段负责准备场景、相机、灯光、材质,做剔除、排序、合批,然后提交 DrawCall。
顶点着色器负责处理每个顶点,最核心是用 MVP 矩阵把模型顶点变到裁剪空间。
光栅化负责把三角形变成片元,并插值 UV、颜色、法线等数据。
片元着色器负责计算每个片元颜色,比如采样贴图、计算光照、做法线贴图或溶解效果。
最后经过深度测试、模板测试、透明混合,写入 FrameBuffer,最终显示到屏幕。
加分句:
“我画渲染管线时会把 CPU 和 GPU 分开讲:CPU 负责收集和提交,GPU 负责顶点处理、光栅化、片元计算和输出合成。性能问题也能对应到管线阶段,比如 DrawCall 多影响 CPU 提交,Overdraw 高影响片元阶段。”
Shader 要能手写基础效果
面试回答:
TIP
Shader 要能手写基础效果,至少要会写 Unlit 纯色、纹理采样、半透明、顶点色、UV 滚动、Rim Light 这些。面试官不一定要求你写复杂 PBR,但会看你是否理解 Properties、SubShader、Pass、Vertex Shader、Fragment Shader 的职责。
加分说法:
“我至少能手写一个基础顶点片元 Shader。顶点 Shader 负责把模型顶点变到裁剪空间,片元 Shader 负责采样贴图并输出颜色。复杂效果比如 Rim、溶解、UV 滚动,本质都是在这个基础结构上扩展。”
基础纹理采样 Shader:
c
Shader "Custom/InterviewUnlitTexture" // 定义 Shader 名称,材质可以通过这个路径找到它
{ // Shader 代码块开始
Properties // 定义材质面板上可调的参数
{ // Properties 代码块开始
_MainTex ("Main Texture", 2D) = "white" {} // 定义主贴图,默认是白色贴图
_Color ("Tint Color", Color) = (1,1,1,1) // 定义颜色参数,用来给贴图染色
} // Properties 代码块结束
SubShader // 定义一套具体的渲染实现
{ // SubShader 代码块开始
Tags { "RenderType"="Opaque" } // 设置渲染类型为不透明物体
Pass // 定义一次渲染 Pass
{ // Pass 代码块开始
CGPROGRAM // 开始编写 CG/HLSL 代码
#pragma vertex vert // 指定顶点着色器函数名为 vert
#pragma fragment frag // 指定片元着色器函数名为 frag
#include "UnityCG.cginc" // 引入 Unity 常用 Shader 工具函数
sampler2D _MainTex; // 声明主贴图采样器
float4 _MainTex_ST; // 声明贴图缩放和平移参数
fixed4 _Color; // 声明颜色参数
struct appdata // 定义从模型传入顶点着色器的数据结构
{ // appdata 结构体开始
float4 vertex : POSITION; // 顶点位置,来自模型网格
float2 uv : TEXCOORD0; // 顶点 UV,来自模型网格
}; // appdata 结构体结束
struct v2f // 定义从顶点着色器传给片元着色器的数据结构
{ // v2f 结构体开始
float4 pos : SV_POSITION; // 裁剪空间位置,用来让 GPU 知道顶点在哪里
float2 uv : TEXCOORD0; // 传递给片元着色器的 UV
}; // v2f 结构体结束
v2f vert(appdata v) // 顶点着色器函数,每个顶点执行一次
{ // vert 函数开始
v2f o; // 创建输出结构体
o.pos = UnityObjectToClipPos(v.vertex); // 把模型空间顶点转换到裁剪空间
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 应用贴图缩放和平移,得到最终 UV
return o; // 返回顶点阶段输出
} // vert 函数结束
fixed4 frag(v2f i) : SV_Target // 片元着色器函数,每个片元执行一次
{ // frag 函数开始
fixed4 texColor = tex2D(_MainTex, i.uv); // 根据 UV 采样主贴图颜色
fixed4 finalColor = texColor * _Color; // 把贴图颜色和材质颜色相乘
return finalColor; // 输出最终像素颜色
} // frag 函数结束
ENDCG // 结束 CG/HLSL 代码
} // Pass 代码块结束
} // SubShader 代码块结束
} // Shader 代码块结束光照、阴影、后处理会深入追问
面试回答:
NOTE
光照、阴影、后处理会被深入追问,因为它们是渲染里最容易暴露底层理解的部分。面试官不是想听“我会开 Bloom、会开阴影”,而是想听你能不能讲清原理、伪影、性能成本和移动端取舍。
怎么回答:
光照:讲漫反射、高光、环境光、法线、视线、光线方向,进一步可以讲 Phong、Blinn-Phong、PBR、BRDF。
阴影:讲 Shadow Map,也就是先从光源视角渲染深度图,再从摄像机视角渲染时比较深度。继续追问会问阴影痤疮、Peter Panning、Bias、PCF、CSM、阴影距离。
后处理:讲它通常是全屏 Pass,把已经渲染好的颜色图、深度图、法线图作为输入,再做 Bloom、SSAO、景深、运动模糊、Color Grading。移动端要特别注意带宽、RenderTexture 分辨率和全屏采样次数。
加分句:
“光照算的是表面颜色,阴影算的是光源可见性,后处理是在整张屏幕图像上再加工。真正项目里我会根据设备性能做取舍,比如移动端减少实时光和实时阴影,控制后处理数量,降低 RenderTexture 分辨率。”
GPU 性能优化要懂
面试回答:
GPU 性能优化要懂,因为游戏画面卡不一定是 CPU 问题,很多时候是 GPU 在忙:像素填充太多、透明物体 Overdraw 太高、Shader 太复杂、阴影太贵、后处理太多、RenderTexture 分辨率过高。
面试要这样说:
NOTE
“我会先判断是不是 GPU 瓶颈,看 CPU Frame 和 GPU Frame。确认 GPU 高之后,再用 Frame Debugger、Profiler、RenderDoc 拆成本,看 DrawCall、SetPass、Overdraw、Shader 指令、贴图采样、阴影和后处理。优化时不会只凭感觉改,而是优化前后对比帧时间。”
常见优化点:
合批、GPU Instancing、SRP Batcher、减少透明叠加、降低阴影距离、减少后处理、降低 RenderTexture 分辨率、压缩贴图、简化移动端 Shader。
加分句:
“GPU 优化不是单纯降低画质,而是在画质、性能、发热和兼容性之间做取舍。真正有说服力的是能拿数据说明优化前多少 ms,优化后多少 ms。”
RenderDoc、Frame Debugger 是加分项
面试回答:
IMPORTANT
RenderDoc 和 Frame Debugger 是渲染方向的加分项,因为它们说明你不是靠猜来排查画面问题,而是能逐 DrawCall、逐 Pass、逐渲染状态分析。
Frame Debugger 更偏 Unity 视角,可以看 Unity 每一步提交了什么:哪个物体、哪个材质、哪个 Shader Pass、渲染到哪个 RenderTarget,适合排查渲染顺序、透明排序、合批、额外 Pass。
RenderDoc 更偏 GPU 视角,可以捕获一帧,查看纹理、Depth、RenderTarget、Buffer、Shader 输入输出和管线状态,适合排查黑屏、错贴图、深度异常、后处理错误、移动端渲染问题。
加分说法:
“我会先用 Frame Debugger 定位是哪一次 DrawCall 或 Pass 出问题,再用 RenderDoc 捕获这一帧,看 GPU 管线状态、贴图绑定、深度缓冲和 Shader 输入。这样不是凭感觉改,而是有证据地定位问题。”
PBR、IBL、BRDF 是加分项
面试回答:
NOTE
PBR、IBL、BRDF 是渲染方向加分项,因为它们说明你不只是会调材质参数,而是理解现代渲染里“材质为什么真实”。
PBR 是 Physically Based Rendering,物理基础渲染,核心是用更统一、更接近物理规律的方式描述材质,比如 BaseColor、Metallic、Roughness、Normal。
BRDF 是反射分布函数,用来描述一束光打到表面后,会往观察方向反射多少。PBR 里常见微表面模型会拆成 D、F、G:法线分布、菲涅尔、几何遮蔽。
IBL 是 Image Based Lighting,用环境贴图参与光照。它让物体不只受直接光影响,还能反射周围环境,所以金属、粗糙表面会更真实。
加分句:
“我理解 PBR 是整体渲染思想,BRDF 是它里面的核心反射模型,IBL 是用环境贴图补充间接光。实际项目里还要考虑贴图采样、预过滤、Shader 变体和移动端性能取舍。”
移动端渲染优化是高频点
面试回答:
移动端渲染优化是高频点,因为移动设备 GPU 性能、带宽、内存、电量和散热都有限。面试官想听的不是“我会降低画质”,而是你能不能定位瓶颈、拆解 GPU 成本,并做出合理取舍。
答题思路:
先真机测试,确认是 CPU 瓶颈还是 GPU 瓶颈。确认 GPU 瓶颈后,再看 DrawCall、SetPass、Overdraw、Shader 复杂度、贴图带宽、阴影、后处理、RenderTexture 分辨率。
常见优化点:
IMPORTANT
减少透明物体和粒子叠加,控制 Overdraw;使用合批、GPU Instancing、SRP Batcher 降低提交成本;简化移动端 Shader,减少采样、分支和复杂光照;降低阴影距离、级联数量和分辨率;谨慎使用 Bloom、SSAO、DOF 等全屏后处理;贴图用 ASTC、MipMap、图集,并控制尺寸。
加分句:
“移动端渲染优化不是单纯牺牲画质,而是在画质、性能、发热、电量和兼容性之间做取舍。优化前后我会用真机数据对比,比如 GPU Frame 从多少 ms 降到多少 ms,是否改善发热和掉帧。”
要能解释视觉效果背后的数学
面试回答:
WARNING
要能解释视觉效果背后的数学,因为 Shader 效果不是魔法,本质是用数学控制颜色、透明度、顶点位置、UV、深度或屏幕像素。
比如 Rim Light 背后是 dot(N, V),用法线和视线夹角判断边缘;溶解效果背后是噪声图加阈值裁剪;UV 滚动背后是 uv + speed * time;描边可能用法线外扩或深度边缘检测;Bloom 是亮部提取、模糊、再叠加。
加分说法:
“我解释视觉效果时会按四步讲:输入是什么,数学公式是什么,输出改变了什么,性能成本在哪里。比如 Rim Light 的输入是法线和视线方向,核心是 1 - dot(N,V),输出是边缘亮度,成本主要是额外计算和可能的叠加 Pass。”
简单 Rim Light 片元思路:
c
float3 normalDir = normalize(i.worldNormal); // 把世界空间法线归一化,保证点乘结果稳定
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos); // 计算从物体表面指向摄像机的视线方向
float dotNV = saturate(dot(normalDir, viewDir)); // 计算法线和视线夹角,saturate 限制到 0 到 1
float rim = 1.0 - dotNV; // 视线越贴近表面,rim 越大,边缘越明显
rim = pow(rim, _RimPower); // 用幂函数控制边缘光的范围和锐利程度
float3 finalColor = baseColor + rim * _RimColor.rgb; // 把边缘光颜色叠加到基础颜色上
return float4(finalColor, 1.0); // 输出最终颜色,并保持不透明项目中最好有自写 Shader 或渲染 Demo
面试回答:
IMPORTANT
项目中最好有自写 Shader 或渲染 Demo,因为它能证明你不是只会调 Unity 面板,而是真的理解渲染管线、顶点阶段、片元阶段、数学公式和性能代价。
Demo 不用特别大,但要能演示、能打开代码讲、能说明为什么这样写。比如角色 Rim Light、溶解死亡、UV 滚动水流、屏幕后处理灰度/描边、简化 Blinn-Phong 光照,都是很适合面试展示的小 Demo。
加分说法:
“我准备了一个自写 Shader Demo,能现场展示效果,也能解释顶点 Shader 负责什么、片元 Shader 负责什么、用到了哪些数学,比如点乘、噪声、插值、UV 偏移,并且能说明移动端上采样次数、透明和 Overdraw 的性能代价。”
简单 Rim Light 核心代码:
c
float3 normalDir = normalize(i.worldNormal); // 归一化世界空间法线,保证点乘计算稳定
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos); // 计算表面点指向摄像机的视线方向
float dotNV = saturate(dot(normalDir, viewDir)); // 计算法线和视线夹角,并限制到 0 到 1
float rim = 1.0 - dotNV; // 夹角越接近轮廓,rim 值越大
rim = pow(rim, _RimPower); // 用幂函数控制边缘光范围和锐利程度
float3 finalColor = baseColor + rim * _RimColor.rgb; // 把边缘光叠加到基础颜色上
return float4(finalColor, 1.0); // 输出最终颜色,并保持不透明