
U3D 渲染技术专题
U3D 渲染技术专题
DrawCall 优化详解
DrawCall 的数量直接影响游戏的渲染性能。每一个 DrawCall 都需要 CPU 去处理,并消耗 GPU 的渲染资源。当 DrawCall 数量过多时,会导致 CPU 和 GPU 的负担加重,进而影响游戏的帧率和流畅度。特别是在移动设备上,由于硬件性能的限制,过多的 DrawCall 更容易导致游戏卡顿和掉帧。
注意
注意
using UnityEngine; public class MaterialPropertyBlockColorTest : MonoBehaviour { public Renderer[] targetRenderers; // 拖入3个物体的Renderer public Color[] colors = { Color.red, Color.green, Color.blue }; void Start() { if (targetRenderers == null || targetRenderers.Length != 3) { Debug.LogError("请拖入3个Renderer!"); return; } // 确保所有物体使用相同的材质实例 Material sharedMaterial = targetRenderers[0].sharedMaterial; for (int i = 0; i < targetRenderers.Length; i++) { targetRenderers[i].sharedMaterial = sharedMaterial; // 强制共享材质 } // 用 MaterialPropertyBlock 设置不同颜色 for (int i = 0; i < targetRenderers.Length; i++) { MaterialPropertyBlock block = new MaterialPropertyBlock(); block.SetColor("_BaseColor", colors[i]); targetRenderers[i].SetPropertyBlock(block); Debug.Log($"物体 {i} 颜色设置为: {colors[i]}"); } } }
SpriteAtlas atlas = Resources.Load<SpriteAtlas>("PathToAtlas"); Sprite sprite = atlas.GetSprite("SpriteName"); Image image = GetComponent<Image>(); image.sprite = sprite;
// 同一个材质,开启 GPU Instancing MaterialPropertyBlock block = new MaterialPropertyBlock(); block.SetColor("_Color", Color.red); renderer.SetPropertyBlock(block); // 不打破 instancing
#pragma multi_compile_instancing UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_BUFFER_END(Props)
using UnityEngine; public class MeshCombineTest : MonoBehaviour { public MeshFilter[] meshFilters; void Start() { if (meshFilters == null || meshFilters.Length == 0) return; // 1. 创建 CombineInstance 数组 CombineInstance[] combines = new CombineInstance[meshFilters.Length]; for (int i = 0; i < meshFilters.Length; i++) { combines[i].mesh = meshFilters[i].sharedMesh; // 关键修改:使用 localToWorldMatrix 保留位置/旋转/缩放 combines[i].transform = meshFilters[i].transform.localToWorldMatrix; // 2. 禁用原始 MeshRenderer(避免重复渲染) MeshRenderer renderer = meshFilters[i].GetComponent<MeshRenderer>(); if (renderer != null) renderer.enabled = false; } // 3. 创建合并后的 Mesh Mesh combinedMesh = new Mesh(); combinedMesh.CombineMeshes(combines); // 合并所有 Mesh // 4. 赋值给当前 GameObject MeshFilter myFilter = GetComponent<MeshFilter>(); if (myFilter == null) myFilter = gameObject.AddComponent<MeshFilter>(); myFilter.mesh = combinedMesh; // 5. 确保有 Renderer 并复制第一个材质(简单处理) MeshRenderer myRenderer = GetComponent<MeshRenderer>(); if (myRenderer == null) myRenderer = gameObject.AddComponent<MeshRenderer>(); if (meshFilters.Length > 0 && meshFilters[0].GetComponent<MeshRenderer>() != null) { myRenderer.material = meshFilters[0].GetComponent<MeshRenderer>().sharedMaterial; } } }
public class UICanvasRenderer : MonoBehaviour { private CanvasRenderer canvasRenderer; void Start() { canvasRenderer = GetComponent<CanvasRenderer>(); canvasRenderer.sortingOrder = 1; } } public class UIMask : MonoBehaviour { public RectTransform maskRect; void Start() { Mask mask = gameObject.AddComponent<Mask>(); mask.showMaskGraphic = false; mask.rectTransform = maskRect; } }
渲染相关术语
Drawcall:是 CPU 对底层图形绘制接⼝的调⽤,命令 GPU 执⾏渲染操作,渲染流程采⽤流⽔线实现,CPU 和 GPU 并⾏⼯作,它们之间通过命令缓冲区连接,CPU 向其中发送渲染命令,GPU 接收并执⾏对应的渲染命令。DrawCall 的数量直接影响游戏的渲染性能。
合批:动态合批与静态合批其本质是将多次绘制请求,在条件允许的情况下进行合并处理,减少 CPU 对 GPU 绘制请求的次数,达到提高性能的目的。
动态合批和静态合批细节
动态批处理:如果动态物体共用着相同的材质,那么 Unity 会自动对这些物体进行批处理。动态批处理一切都是自动的,不需要用户做任何操作,而且物体是可以移动的,但是限制很多。
静态批处理:自由度很高,限制很少,但可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了。
图集:图集是将很多零碎的 2D 小图整合成一张大图,方便 Unity 渲染合批,降低渲染消耗。
Shader:着色器,是一种较为短小的程序片段,告诉图形硬件如何计算和输出图像。简单的说 Shader 就是可编程图形管线的算法片段。主要分为 Vertex Shader (顶点着色器) 和 Fragment Shader (片元着色器)。
渲染管线:也称为渲染流水线。渲染管线(Rendering Pipeline)是计算机图形学中将 3D 场景转换为 2D 图像的过程。它是一系列步骤的集合,负责处理几何数据、光照、纹理等信息,最终生成屏幕上显示的图像。渲染管线是实时图形渲染(如游戏)和离线渲染(如电影特效)的核心。
渲染管线之应用阶段:这个阶段主要在 CPU 中,从 CPU 中整理好渲染数据,并将数据发送给 GPU。
渲染管线之几何阶段:通过对输入的渲染图元进行处理,这一阶段将会输出屏幕空间的二维顶点坐标,深度值等信息并传递给下一阶段。
渲染管线之光栅化阶段:将点线面等几何概念实体化成像素,这一阶段会使用上一阶段传递的数据渲染出最终的图像。
渲染管线阶段划分详细讲解
空间名称 | 变换名称 | 变换矩阵 | 作用 |
---|---|---|---|
模型空间 | 模型变换 | 模型矩阵 | 定义模型的几何形状 |
世界空间 | 视图变换 | 视图矩阵 | 将模型放置在场景中的正确位置 |
视图空间 | 投影变换 | 投影矩阵 | 将场景转换到相机的视角 |
裁剪空间 | 视口变换 | 视口变换矩阵 | 将 3D 场景投影到 2D 平面上 |
屏幕空间 | - | - | 将投影后的 2D 坐标映射到屏幕上的像素位置 |
冯光照模型计算公式
Phong 光照模型的总光照公式如下:
其中:
环境光 (Ambient Light):
漫反射光 (Diffuse Light):
镜面反射光 (Specular Light):
最终光照公式为:
兰伯特光照模型计算公式
在光照计算中,Diffuse 是指光照与物体表面之间的交互,其中光线在表面上均匀地反射。漫反射光照模型的计算遵循 兰伯特定律,也叫兰伯特光照模型(Lambertian reflection),它的特点是反射的光强度与入射光与表面法线的夹角有关。
Diffuse 光照计算公式如下:
U3D 中 PBR 材质用法
GPU 工作原理
GPU 通过大规模并行架构、专用硬件单元和高效内存管理,实现了图形渲染和通用计算的高性能处理。其核心原理是:
GPU 渲染流水线
Vertex Shader 工作原理
Vertex Shader(顶点着色器)是图形渲染管线中的一个阶段,负责对每个顶点进行处理。它的主要作用是对场景中所有的顶点进行变换、光照计算、纹理坐标的转换等工作,最终生成屏幕上的图形。它是渲染管线中的第一步,通常在 GPU 上执行。顶点着色器的计算过程:
延迟渲染
我们知道,正向渲染(Forward Rendering),或称正向着色(Forward Shading),是渲染物体的一种非常直接的方式,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推。传统的正向渲染思路是,先进行着色,再进行深度测试。 其主要缺点就是光照计算跟场景复杂度和光源个数有很大关系。假设有 n 个物体, m 个光源,且每个每个物体受所有光源的影响,那么复杂度就是 O(m*n)。
正向渲染简单直接,也很容易实现,但是同时它对程序性能的影响也很大,因为对每一个需要渲染的物体,程序都要对每个光源下每一个需要渲染的片段进行迭代,如果旧的片段完全被一些新的片段覆盖,最终无需显示出来,那么其着色计算花费的时间就完全浪费掉了。
可以将延迟渲染( Deferred Rendering) 理解为先将所有物体都先绘制到**屏幕空间的缓冲区(即 Gbuffer, Geometric Buffer,几何缓冲区)**中,再逐光源对该缓冲区 进行着色的过程,从而避免了因计算被深度测试丢弃的⽚元的着色而产⽣的不必要的开销。也就是说,延迟渲染基本思想是,先执行深度测试,再进行着色计算,将本来在物理空间(三维空间)进行光照计算放到了像素空间(二维空间)进行处理。对应于正向渲染 O(m*n)的 复杂度,经典的延迟渲染复杂度为 O(n+m)。
逐顶点光照和逐像素光照
shader 里如何来体现光照?
光的颜色 * 【baseColor/Diffuse/Albedo】 = 颜色
光的颜色 * 漫反射强度 = 漫反射光的颜色
光的颜色 * 镜面反射的强度 = 镜面反射光的颜色
逐顶点光照:在顶点着色器的时候,计算每个顶点的光照颜色。计算光照次数少,性能好,效果差。
逐像素光照:在片元着色器的时候,baseColor * 插值的光照颜色。计算光照的次数多,性能差,效果好。
MipMap
MipMap(多级渐远纹理)是一种常用的图形技术,特别是在 3D 渲染中,用来优化纹理的加载和显示效果。MipMap 由一系列尺寸逐渐减小的纹理图像组成,这些纹理图像是原始纹理的逐级缩小版本。每一级的图像都会是上一级图像尺寸的一半,直到达到最低的尺寸(通常是 1x1 像素)。
MipMap 是通过为一个纹理生成多个尺寸逐渐减小的版本来工作。例如,假设你有一张 512x512 像素的纹理,那么它的 MipMap 版本可能会包含以下几个级别:
Level 0:512x512
Level 1:256x256
Level 2:128x128
Level 3:64x64
Level 4:32x32
Level 5:16x16
Level 6:8x8
Level 7:4x4
Level 8:2x2
Level 9:1x1
这些不同级别的图像可以在不同的渲染距离和场景条件下使用,以提高性能和视觉质量。
详细内容
光照贴图优点
LightMap:就是指在三维软件里打好光,然后渲染把场景各表面的光照输出到贴图上,最后又通过引擎贴到场景上,这样就使物体有了光照的感觉。优点:
Alpha Blend工作原理
Alpha Blend(透明混合)是一种常用于图形渲染中的技术,用于将两个图像或颜色根据透明度(Alpha 通道)进行混合,从而实现半透明效果。它广泛应用于图形和游戏开发中,特别是在处理透明物体(如玻璃、烟雾或人物阴影)时。工作原理如下:
计算公式:最终显示颜色 = 源像素透明度 x 源像素颜色 +(1 - 源像素透明度)x 目标像素颜色