不知不觉,以从事unity游戏开发将近十年了。一路前行,终会站在巨人的肩膀上。愿你我都安好。
直奔主题吧。知其然,更知其所以然。
从unity运行到看到游行画面的整个工作流来说,分为两个计算处理阶段。
CPU 计算处理阶段(应用阶段)
-
概述
- CPU 计算处理的阶段,借鉴 Shader 渲染管线流程,把它称为应用阶段。
- 应用阶段,首先从游戏一帧也就是一个 Update 说明。主要是 CPU 处理准备模型网格,模型材质信息阶段。然后将这些数据通过 draw call 发送给 GPU,完成一次 CPU 处理。
-
准备模型的过程
-
模型在场景的位置的计算
- 在这里,模型要通过物理碰撞,包括触发器和刚体组件的碰撞检测,有的还有物理材质,寻路网格的参与。
-
模型动画的计算
- 通过关键帧计算插值的补间骨骼位置,然后通过模型顶点在骨骼的权重进行顶点变化,这个过程也是复杂且性能要求高的。
-
遮挡剔除
- 这里包括相机视锥模型的剔除和遮挡剔除。
- 同时,为了减少优化模型顶点的合批的计算量,又出了个 LOD 技术,根据相机的远近切换模型精度,以减少计算量。
- 最终完成一帧内,摄像机内可见物体的所有模型网格。
-
-
优化处理
-
网格和 draw call
- 但是这些网格还不是最终的网格。对于 GPU 来说,一个网格和一个材质球就是一个 draw call,这个后面会说明。
- 所以要想 GPU 更快的计算,CPU 就要减少 draw call 的数量。那么使用同一材质球的各个模型怎么一个 draw call,即使用一个材质球完成,而不是一个个的模型使用相同的着色器去渲染呢?
-
动态合批和静态合批
- 这样动态合批和静态合批应运而生,这些合批的最终结果是将使用相同材质的模型合成一个大网格,这样将多个相同材质的网格合并成一个大网格使用同一个 CPU 计算准备好的材质球的所有数据信息一起推送到 GPU 完成一次 draw call。
- 至于为什么相同材质合并一个网格使用同一个着色器处理成一个 draw call,将在后面 GPU 计算处理阶段介绍。如果使用相同材质的合批被打断,就会产生新的模型和材质
-
局限性
- 但是这些优化还是有很多局限性。静态网格说白了就是在编辑器里游戏运行前将静态不变的场景物体合并成一个个多个批次的大网格,因而不适合运动形变物体对象。
- 动态合批正好适合动态物体,但是物体模型又有 300 顶点甚至更少顶点的限制。。。怎么办呢?GPU Instancing 也应运而生,能通过 GPU 实例化多个位置变换的物体,绕过 CPU 合批的局限性,但是不支持 SkinnedMeshRenderer 蒙皮骨骼。我是真的服了。。。Unity 感觉在一直补坑的道路上越走越远。
- 接着后面又出了个 GPU Skinning 技术,说是技术,其实就是在补坑。我是不明白不统一处理这些问题,简直反人类。
-
GPU 计算处理阶段(渲染阶段)
概述
在经过 CPU 的应用阶段后,GPU 会处理 CPU 在一帧内计算处理的所有 draw call。一个 draw call 是 GPU 处理一个网格数据的流水线操作。这里的网格是由 CPU 合批后的大网格数据,下文将对此进行详细介绍。
Draw Call 工作流程简述
Shader 加载
要了解 GPU 的处理逻辑,首先需要了解其整个处理流程。当 GPU 接收到一个 draw call 指令时,会将与材质球相关联的 shader 加载进 GPU,设置 shader 的状态(如深度缓冲区、颜色缓冲区、材质属性等),并加载所需数据(模型网格数据、纹理数据、变量初始化数据)。
纹理和属性将存放到缓冲区
-
Uniform Buffers(统一缓冲区)
统一缓冲区用于存储 Shader 中的全局变量或属性,如颜色、光照参数、材质属性等。每次渲染调用时,统一缓冲区会被传递给 GPU,并在 Shader 中访问这些属性。MaterialPropertyBlock 可以用来修改这些属性,而无需改变整个材质。 -
Texture Units(纹理单元)
纹理单元用于管理 Shader 中绑定的纹理资源。当通过 MaterialPropertyBlock 修改纹理时,它切换的是 Shader 中使用的纹理单元,而不是改变材质本身绑定的纹理。 -
Constant Buffers(常量缓冲区)
常量缓冲区类似于统一缓冲区,但它们用于存储在渲染过程中保持不变的一组常量值。MaterialPropertyBlock 可以修改这些常量值,但修改仅在当前渲染调用中有效,不会影响其他实例化的对象。 -
Structured Buffers(结构化缓冲区)
结构化缓冲区用于存储和处理复杂的数据结构,特别是在计算着色器或高级渲染效果中。虽然 MaterialPropertyBlock 通常不会直接操作结构化缓冲区,但理解这些缓冲区在 Shader 中的作用有助于理解属性的存储和传递过程。
通常,Shader 包含一个顶点函数(#pragma vertex vert
)和一个片元函数(#pragma fragment frag
),有时还包括曲面着色器和几何着色器。这些函数对应渲染管线中的顶点着色器和片元着色器。
纹理处理
Shader 加载完成后,GPU 会为 Shader 分配对应的纹理单元,用于存储纹理对象。一个纹理对象可以是单个图片纹理(如 _Texture("Texture", 2D)
),也可以是一组纹理数组(如 _TextureArray("Texture Array", 2DArray)
)。这些纹理通过材质球绑定后会被加载进来。
流水线计算处理
-
顶点着色器
首先通过顶点着色器(调用顶点函数),将模型从模型空间转换到平面的裁切空间。 -
曲面着色器和几何着色器
如果存在曲面着色器和几何着色器,则依次执行这些着色器。 -
三角裁切、三角形遍历、光栅处理
- 三角裁切:对模型进行三角裁切。
- 三角形遍历:将模型裁切空间中的三角形映射到屏幕像素。
- 光栅处理:进行深度测试、透明测试等处理。
-
片元着色器
最后调用片元函数,执行片元着色器,完成