Skip to content

图形渲染岗

线性代数和空间变换必须会

linear-algebra-space-transform-must-know

面试回答:

TIP

线性代数和空间变换必须会,因为游戏里的移动、旋转、相机、碰撞、射线、渲染,本质都是空间计算。

你至少要能讲清:向量表示方向和距离,点乘判断夹角,叉乘求垂直方向,矩阵表示平移旋转缩放,四元数表示旋转,坐标空间负责把一个点从局部空间变到世界、视图、裁剪、屏幕空间。

加分说法:

“我不会只背 Unity API,比如 TransformPointWorldToScreenPoint。我会先判断当前数据在哪个空间,再判断目标需要哪个空间。角色技能范围、相机跟随、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 类结束

渲染管线必须会画

render-pipeline-must-draw

面试回答:

渲染管线必须会画,而且要画得顺序准确。最简版本可以这样画:

场景对象 -> CPU 提交 DrawCall -> 顶点着色器 -> 裁剪 -> 光栅化 -> 片元着色器 -> 深度/模板测试 -> 混合 -> FrameBuffer -> 屏幕

每一步怎么讲:

CPU 阶段负责准备场景、相机、灯光、材质,做剔除、排序、合批,然后提交 DrawCall

顶点着色器负责处理每个顶点,最核心是用 MVP 矩阵把模型顶点变到裁剪空间。

光栅化负责把三角形变成片元,并插值 UV、颜色、法线等数据。

片元着色器负责计算每个片元颜色,比如采样贴图、计算光照、做法线贴图或溶解效果。

最后经过深度测试、模板测试、透明混合,写入 FrameBuffer,最终显示到屏幕。

加分句:

“我画渲染管线时会把 CPU 和 GPU 分开讲:CPU 负责收集和提交,GPU 负责顶点处理、光栅化、片元计算和输出合成。性能问题也能对应到管线阶段,比如 DrawCall 多影响 CPU 提交,Overdraw 高影响片元阶段。”

Shader 要能手写基础效果

shader-basic-effects-handwrite

面试回答:

TIP

Shader 要能手写基础效果,至少要会写 Unlit 纯色纹理采样半透明顶点色UV 滚动Rim Light 这些。面试官不一定要求你写复杂 PBR,但会看你是否理解 PropertiesSubShaderPassVertex ShaderFragment 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 代码块结束

光照、阴影、后处理会深入追问

lighting-shadow-postprocess-deep-questions

面试回答:

NOTE

光照、阴影、后处理会被深入追问,因为它们是渲染里最容易暴露底层理解的部分。面试官不是想听“我会开 Bloom、会开阴影”,而是想听你能不能讲清原理、伪影、性能成本和移动端取舍。

怎么回答:

光照:讲漫反射、高光、环境光、法线、视线、光线方向,进一步可以讲 PhongBlinn-PhongPBRBRDF

阴影:讲 Shadow Map,也就是先从光源视角渲染深度图,再从摄像机视角渲染时比较深度。继续追问会问阴影痤疮、Peter Panning、Bias、PCF、CSM、阴影距离。

后处理:讲它通常是全屏 Pass,把已经渲染好的颜色图、深度图、法线图作为输入,再做 Bloom、SSAO、景深、运动模糊、Color Grading。移动端要特别注意带宽、RenderTexture 分辨率和全屏采样次数。

加分句:

“光照算的是表面颜色,阴影算的是光源可见性,后处理是在整张屏幕图像上再加工。真正项目里我会根据设备性能做取舍,比如移动端减少实时光和实时阴影,控制后处理数量,降低 RenderTexture 分辨率。”

GPU 性能优化要懂

gpu-performance-optimization-must-know

面试回答:

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 是加分项

renderdoc-frame-debugger-bonus

面试回答:

IMPORTANT

RenderDocFrame 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 是加分项

pbr-ibl-brdf-bonus

面试回答:

NOTE

PBRIBLBRDF 是渲染方向加分项,因为它们说明你不只是会调材质参数,而是理解现代渲染里“材质为什么真实”。

PBR 是 Physically Based Rendering,物理基础渲染,核心是用更统一、更接近物理规律的方式描述材质,比如 BaseColorMetallicRoughnessNormal

BRDF 是反射分布函数,用来描述一束光打到表面后,会往观察方向反射多少。PBR 里常见微表面模型会拆成 DFG:法线分布、菲涅尔、几何遮蔽。

IBL 是 Image Based Lighting,用环境贴图参与光照。它让物体不只受直接光影响,还能反射周围环境,所以金属、粗糙表面会更真实。

加分句:

“我理解 PBR 是整体渲染思想,BRDF 是它里面的核心反射模型,IBL 是用环境贴图补充间接光。实际项目里还要考虑贴图采样、预过滤、Shader 变体和移动端性能取舍。”

移动端渲染优化是高频点

mobile-rendering-optimization-high-frequency

面试回答:

移动端渲染优化是高频点,因为移动设备 GPU 性能、带宽、内存、电量和散热都有限。面试官想听的不是“我会降低画质”,而是你能不能定位瓶颈、拆解 GPU 成本,并做出合理取舍。

答题思路:

先真机测试,确认是 CPU 瓶颈还是 GPU 瓶颈。确认 GPU 瓶颈后,再看 DrawCallSetPassOverdraw、Shader 复杂度、贴图带宽、阴影、后处理、RenderTexture 分辨率。

常见优化点:

IMPORTANT

减少透明物体和粒子叠加,控制 Overdraw;使用合批、GPU Instancing、SRP Batcher 降低提交成本;简化移动端 Shader,减少采样、分支和复杂光照;降低阴影距离、级联数量和分辨率;谨慎使用 Bloom、SSAO、DOF 等全屏后处理;贴图用 ASTC、MipMap、图集,并控制尺寸。

加分句:

“移动端渲染优化不是单纯牺牲画质,而是在画质、性能、发热、电量和兼容性之间做取舍。优化前后我会用真机数据对比,比如 GPU Frame 从多少 ms 降到多少 ms,是否改善发热和掉帧。”

要能解释视觉效果背后的数学

visual-effects-math-explanation

面试回答:

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

project-custom-shader-render-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); // 输出最终颜色,并保持不透明

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0