高级光照

1 Blinn-Phong光照

使用半程向量与法向量之间的角度代替视线与反射向量之间的角度,其它与Phong光照一致。

优点:

  • 计算快
  • 当角度超过90度时不会出现异常的光照(Phong会产生异常的光照,因为cos超过90度为负数)

2 Gamma校正

人眼感知的非线性:相当于物理亮度的2次幂

显示器的非线性:相当于物理亮度的2.2次幂(CRT显示器)

但是显示器的衰减+人眼的衰减,就使得人眼看起来的亮度不太正常,同时由于很多光照的计算都是在线性空间内进行的,所以需要进行Gamma校正,使显示器显示的颜色是线性的。

在OpenGL中,通常有两种方法来完成Gamma校正:

  • 使用OpenGL内建的sRGB帧缓冲

    1
    glEnable(GL_FRAMEBUFFER_SRGB);
  • 自己在片元着色器中手动Gamma校正

    1
    fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));

3 sRGB纹理

如果我们在屏幕上取图片,那么这个图片是线性的(经过Gamma校正),如果我们拿这个图片再进行一次渲染,那么这个图片再经过一次Gamma校正,会变得不正常,OpenGL提供了一种解决方案,即GL_SRGB和GL_SRGB_ALPHA内部纹理格式,这些图片不会再进行一次Gamma校正。

1
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

4 衰减

真实的物理世界中光照的衰减和距离的平方成反比,因此下列公式在Gamma校正后使用是没有问题的,可以产生可信的效果:

但是如果没有Gamma校正,那么线性函数更适合用于光照衰减的计算。(见左下和右上)

因为显示器的非线性替代了光照衰减的非线性,使得两者效果相似。

5 阴影映射(Shadow Mapping)

以光的位置为视角进行渲染,看得到的被点亮,看不到的就在阴影之中。

主要实现方法是利用帧缓冲的深度缓冲,以光源的位置为视角渲染深度贴图。然后在正式渲染的时候使用深度贴图,将片元位置映射到光空间中,对同样的x和y,对比z和深度贴图的值,如果z值大于深度贴图,那么说明此片元在阴影中,shadow值置为1,否则在阴影外,shadow值置为0,shadow值与blinn-phong的漫反射、镜面反射交互作用,得到最终片元的值。

阴影贴图的改进

如果出现阴影失真(Shadow Acne),如上图所示,在地板上和正方体上出现了很短的阴影条,一般是因为多个片元从同一个深度值进行采样,因此可以使用阴影偏移(Shadow Bias)解决这个问题,即在比较z值和深度值时加入一个偏移:

1
2
float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;

有些坡度大的仍然会产生阴影失真,一个更可靠的方法是根据表面朝向光线的角度更改偏移量:

1
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

效果:

悬浮

使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值):

这叫悬浮(Peter Panning),解决这个问题的一个方法是使用正面剔除,即只使用背面的z值来对深度值进行比较,正面剔除对于不同的场景需要进行更详细的考虑。

6 万向阴影贴图

由于上一节的阴影贴图是具有方向性的,只能对一个区域的物体渲染阴影,得到一个窗口的阴影贴图,因此考虑全局的阴影,需要使用深度立方体贴图,想象一个点光源对所有方向放出光,一个窗口显然是不够用的,考虑一个包围点光源的立方体,光源对所有方向的深度贴图都可以对应到正方体内部的一个面,这样我们只需要6个面就可以完成全局的阴影贴图。

与单方向的阴影贴图不同的是,这里需要使用几何着色器,指定将图形渲染到立方体的哪一个面,另外,不需要再将片元坐标转换到光空间窗口进行深度值比对,只需要确定片元坐标到光源的方向向量,求出向量的长度即片元的深度,再获取这个方向上深度贴图的closestDepth值,这个值由于是0到1的值,需要再乘以一个far_plane的值才能得到可与实际深度对比的深度值,将片元深度与closestDepth进行对比,得到shadow值,在阴影中为1,否则为0。再对blinn-phong的参数作用,确认片元的最终颜色。

最终效果如下:

光照

本文整理自LearnOpenGL

1 颜色

在数字设备上,颜色通常由RGB三个值的组合表示,三个值可以组合成很多颜色。

生活中的物体的颜色是它所反射的颜色,白色的阳光是所有可见颜色的集合,物体仅反射代表物体颜色的部分。

图形中将光源的颜色与物体的颜色值相乘,得到的就是这个物体所反射的颜色。

1
2
3
glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);

为了保证灯的顶点数据、数据指针和颜色在接下来的过程中不受影响,接下来为灯设置一个专用的VAO和shader。

物体箱子的FragmentShader颜色设置两个uniform变量:lightColor和objectColor。

2 基础光照

基础光照模型

经验光照模型Phone光照模型由三个分量组成:环境(Ambient),漫反射(Diffuse),镜面(Specular)光照。

Phone光照模型

  • 环境光照(Ambient Lighting):环境的光照,没有方向,布满整个环境。
  • 漫反射光照(Diffuse Lighting):光源对物体的方向性影响,物体越是正对着光源则越亮。
  • 镜面光照(Specular Lighting):有光泽物体上面出现的亮点,相比于物体颜色更倾向于光的颜色。
环境光照

要得到环境光照,我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:

1
2
3
4
5
6
7
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
漫反射光照

通过计算法向量和光源方向的夹角大小来反映漫反射的强度,通过计算两个归一化向量之间的点乘来表示夹角大小。

漫发射大小

1
2
3
4
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

片段着色器的计算都是在世界空间坐标中进行的,所以法向量需要转换为世界空间坐标,但是并不能直接通过顶点的转换矩阵进行转换,这样可能会造成法向量不垂直于表面(不等比缩放后),诀窍是使用一个专门定制的法线矩阵(Normal Matrix)进行变换,定义为model矩阵左上角的逆矩阵的转置矩阵

1
Normal = mat3(transpose(inverse(model))) * aNormal;

其中矩阵求逆运算是开销比较大的运算,应该尽可能避免在着色器中进行矩阵求逆,最好是用CPU计算出发现矩阵,再通过uniform传值给着色器。

镜面光照

镜面光照是通过计算视线方向和反射向量的角度差来反映光照强度的,夹角越小,镜面光的影响越大。同时乘以一个镜面强度变量,给镜面高光一个中等亮度颜色,让他不要产生过度的影响。

1
float specularStrength = 0.5;

basic_lighting_specular_theory

计算过程如下:

1
2
3
4
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

其中32是高光的反光度(Shininess),一个物体的反光度越高,反射光的能力越强,散射越少,高光点越小。

shininess

如果把Phong光照模型在顶点着色器中实现,那么顶点少得多,更高效,但是片元的颜色是通过对顶点进行插值得到的,这种光照的真实感不会非常真实,除非使用了大量顶点。这种叫Gouraud Shading,而不是Phong Shading,Phong光照能产生更平滑的光照效果。

3 材质

不同物体对光有不同的反应,因此每个物体可以定义一个材质(Material)属性。

1
2
3
4
5
6
7
8
9
#version 330 core
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};

uniform Material material;

由此光照的计算变成如下,也就是除了角度决定的强度以外,需要再乘以材质对应的光照分量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void main()
{
// 环境光
vec3 ambient = lightColor * material.ambient;

// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = lightColor * (diff * material.diffuse);

// 镜面光
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = lightColor * (spec * material.specular);

vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}

填充结构体还需要对结构体的每个分量单独设置:

1
2
3
4
lightingShader.setVec3("material.ambient",  1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.diffuse", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);
lightingShader.setFloat("material.shininess", 32.0f);

这样得到的结果太亮了,三种分量都全力反射,所以应该针对每个分量设置强度,例如光源的环境光强度可以设置为一个小一点的值,对环境光进行限制:

1
vec3 ambient = vec3(0.1) * material.ambient;

对每个分量都设置的话,建立一个结构体会方便很多:

1
2
3
4
5
6
7
8
9
struct Light {
vec3 position; // 光源位置

vec3 ambient;
vec3 diffuse;
vec3 specular;
};

uniform Light light;

由此着色器需要进行修改:

1
2
3
vec3 ambient  = light.ambient * material.ambient;
vec3 diffuse = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);

4 光照贴图

贴图+光照=光照贴图,简单易懂,就是在物体贴图上添加光照。

漫反射贴图

首先需要将纹理储存在Material结构体中,将vec3漫反射颜色替换成sampler2D不透明类型用以储存纹理。

1
2
3
4
5
6
7
struct Material {
sampler2D diffuse; // 作者这里把环境光删掉了,并用漫反射贴图作为环境光
vec3 specular;
float shininess;
};
...
in vec2 TexCoords;

然后只需要从纹理中采样片段的漫反射颜色值即可:

1
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

用漫反射贴图作为环境光材质颜色:

1
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));

纹理坐标需要通过顶点着色器传递到片元着色器中:

1
2
3
4
5
6
7
8
9
10
11
12
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
...
out vec2 TexCoords;

void main()
{
...
TexCoords = aTexCoords;
}

然后把纹理单元赋值到material.diffuse这个uniform采样器中,绑定箱子纹理到这个纹理单元中,最后绘制箱子。

1
2
3
4
lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);

漫反射贴图效果

镜面光贴图

注意到木头不应该有这么强的镜面高光,而钢铁边缘应该显示镜面高光,因此有一个专门用于镜面高光的纹理贴图,用来作为镜面高光的蒙板,在这部分有高光,其他部分没有高光,这个蒙板可以直接对漫反射贴图进行剪切处理并且黑白化来生成,黑色部分乘以高光为0,因此不反射镜面高光,白色部分为vec3(1.0),全力反射镜面高光。

镜面高光蒙板

同样地,需要在绘制之前绑定合适的纹理单元,着色器材质中把vec3改成sampler2D,最后计算光照如下:

1
2
3
4
vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
FragColor = vec4(ambient + diffuse + specular, 1.0);

一个有趣的hack

添加一个叫做放射光贴图(Emission Map)的东西,它是一个储存了每个片段的发光值(Emission Value)的贴图。发光值是一个包含(假设)光源的物体发光(Emit)时可能显现的颜色,这样的话物体就能够忽略光照条件进行发光(Glow)。游戏中某个物体在发光的时候,你通常看到的就是放射光贴图(比如 机器人的眼,或是箱子上的灯带)。将这个纹理(作者为 creativesam)作为放射光贴图添加到箱子上,产生这些字母都在发光的效果:参考解答最终效果

放射光贴图

由此,我们可以自由操控光的反射,忽略光照条件,自定义自己的光照效果。

5 投光物

将光投射(Cast)到物体的光源叫做投光物(Light Caster)。

平行光

所有光都互相平行,称为定向光(Directional Light)。

平行光

把光源的position改成direction就可以实现平行光。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;

vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-light.direction);
...
}

不要忘记定义光源的方向

1
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);

如果把光定义为vec4那么可以利用w分量决定定向光还是位置光源(Positional Light Source),w为1时,变换和投影可以作用到光源上,为点光源的位置向量;w为0时,为方向光源。

点光源

光源处于世界中某一个位置的光源,朝着所有方向发光,但是光线会随着距离逐渐衰减。

点光源

衰减

随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。常用的衰减强度公式如下:

这里$d$代表片段距光源的距离,三个可配置项为常数项$K_c$,一次项$K_l$和二次项$K_q$。

  • 常数项通常保持为1.0,保证分母大于等于分子
  • 一次项以线性方程减少强度
  • 二次项会和距离的平方相乘,让光源以二次递减的方式减少强度

下图显示了在100距离内衰减的效果:

衰减函数

Ogre3D的Wiki有一些经验设置数据可供参考。

那么现在只需要一个距离$d$和三个预设值就可以计算距离衰减强度了,算出来后分别乘以环境光、漫反射和镜面光颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Light {
vec3 position;

vec3 ambient;
vec3 diffuse;
vec3 specular;

float constant;
float linear;
float quadratic;
};
...
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
...
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;

聚光

聚光是位于环境中某个位置的光源只朝一个特定方向而不是所有方向照射光线,这样只有在聚光方向的特定半径内的物体才会被照亮。OpenGL中聚光使用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径。

聚光

  • LightDir: 从片段指向光源的向量。
  • SpotDir: 聚光指向的方向。
  • $\phi$: 切光角,边缘与聚光方向的夹角。
  • $\theta$: LightDir向量和SpotDir向量之间的夹角,即片元方向与聚光方向的夹角。

手电筒

Light结构体

1
2
3
4
5
6
struct Light {
vec3 position;
vec3 direction;
float cutOff;
...
};

传输数据到着色器中

1
2
3
lightingShader.setVec3("light.position",  camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));

计算光照

1
2
3
4
5
6
7
8
float theta = dot(lightDir, normalize(-light.direction));

if(theta > light.cutOff)
{
// 执行光照计算
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

效果如下:

手电筒

平滑边缘

为了使边缘平滑,使用内圆锥和外圆锥,让光从内圆锥逐渐变暗直到外圆锥,强度值通过如下公式进行计算:

其中$\phi$为内圆锥切光角的余弦值,$\gamma$为外圆锥切光角的余弦值,$\theta$为片元的角度余弦值。

1
2
3
4
5
6
7
8
float theta     = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); // 使用clamp函数限制intensity的范围为0到1
...
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
...

平滑边缘

适用于恐怖游戏。

渲染流水线

本文整理自《Unity Shader入门精要》以及LearnOpenGL

综述

渲染流水线的工作任务在于由一个三维场景出发,生成一张二维图像。通常由CPU和GPU共用完成。

《RTR》把整个渲染流程概念化为三个阶段:

  1. 应用阶段(CPU)

    即准备好场景数据,进行粗粒度的剔除,设置每个模型的渲染状态(材质、纹理、shader等),输出渲染图元。

  2. 几何阶段(GPU)

    对每个渲染图元进行逐顶点、逐多边形的操作,把顶点坐标变换到屏幕坐标,输出二维顶点坐标以及每个顶点相应的数据。

  3. 光栅化阶段(GPU)

    对逐顶点数据进行纹理以及颜色插值,再进行逐像素的其他处理,产生屏幕上的像素,渲染出最终的图像。

渲染流水线中CPU与GPU之间的通信

渲染流水线的起点是CPU,所以CPU与GPU之间的通信主要出现在应用阶段:

  1. 把数据加载到显存当中:硬盘->内存->显存
  2. 设置渲染状态:设置着色器、光照、材质
  3. 调用Draw Call:CPU发往GPU的渲染命令,GPU根据渲染状态和传输的数据跑完整个GPU流水线,输出屏幕像素

GPU流水线

pipeline

GPU的渲染流水线如上图,GPU从显存中读取顶点数据,进入几何阶段。其中屏幕映射、三角形设置、三角形遍历都是GPU固定实现的(Fixed-function),不可编程或者配置。

几何阶段中包含以下子流水线阶段:

1.顶点着色器(Vertex Shader)

输入顶点数据,对顶点做坐标变换(模拟水或者布料),把顶点坐标从模型空间转换到齐次裁剪空间(视锥体确定的空间,即经过MVP矩阵变换后的空间),做透视除法(xyz除以齐次坐标)后得到归一化后的设备坐标NDC(Normalized Device Coordinates),在OpenGL是(-1,1),再计算顶点颜色;

2.曲面细分着色器(Tessellation Shader)

可选,用于细分图元;

3.几何着色器(Geometry Shader)

可选,用于执行逐图元的着色操作,或用于产生更多的图元;

4.裁剪(Clipping)

将部分在视野内的图元裁剪成两部分,舍弃在NDC立方体外的部分,只将在NDC立方体内的图元传播到下一个环节,这步不可编程,是硬件上的固定操作,但可以进行配置;

5.屏幕映射(Screen Mapping)

把每个图元的x和y坐标转换到屏幕坐标系(和屏幕分辨率有关系)下,不改变z坐标,由此形成窗口坐标系(屏幕坐标系+z坐标);

OpenGL和DirectX的屏幕坐标是反向的

光栅化阶段包含以下子流水线阶段:

1.三角形设置(Triangle Setup)

计算光栅化一个三角网格所需要的信息,由于上一个阶段输出顶点以及额外的信息,并没有包含三角形的边界信息,这一步主要得到三角形边界的表示方式;

2.三角形遍历(Triangle Traversal)

检查每个像素是否被三角网格所覆盖,如果被覆盖,则生成一个片元,这样一个过程就是三角形遍历,也称扫描变换,并且会使用三角网格的顶点信息对所有像素进行插值(深度等),最终输出片元序列,还不是一个真正意义上的像素,而是包含了很多状态的集合(包含屏幕坐标、深度信息、顶点信息、法线以及纹理坐标等),这些状态用于计算像素的最终颜色;

3.片元着色器(Fragment Shader/Pixel Shader)

输入片元信息,输出像素真正的颜色值,此阶段包含纹理采样等渲染技术,但是一般仅能影响单个片元。

4.逐片元操作(Per-Fragment Operations/Output-Merger)

这一阶段决定片元的可见性,主要过程如图:

逐片元操作

模板测试(Stencil Test)

stencil testing

对每个片元,对比它的模板缓冲的值与参考值,并且按照一定规则通过测试,如果通过测试,那么此片元可以进入下一个阶段,通常用于渲染阴影、镜面、轮廓等。

大体步骤如下:

  • 启用模板缓冲写入 glStencilMask(0xFF)
  • 渲染物体,根据物体更新模板缓冲内容(例如物体轮廓的模板缓冲置为1)glStencilFunc
  • 禁用模板缓冲写入 glStencilMask(0x00)
  • 渲染其它气体,这次根据模板缓冲内容丢弃特定片段(例如不绘制原物体轮廓部分) glStencilFunc

OpenGL相关函数

1
2
3
glStencilMask(0xFF); // 位遮罩(bitmask),每一位写入模板缓冲时都与它进行位加运算,效果为允许
// 写入
glStencilMask(0x00); // 0x00,进行位加运算后还是0x00,原模板缓冲不变,效果为禁止写入
1
2
3
4
glStencilFunc(GLenum func, GLint ref, GLuint mask) // 指定通过模板测试条件,通过测试则
// 直接被绘制,func为比较函数,ref为比较的参考值,mask为位遮罩,比较前对两边先进行位加运算,
// (ref & mask)(stencil & mask),再进行比较
glStencilFunc(GL_EQUAL, 1, 0xFF)) // 当缓冲区等于1时,通过模板测试
1
2
3
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass) // 指定测试通过或失败时执行的动作,
// sfail为模板测试失败时,dpfail为只有深度测试失败时,dppass为两个测试都通过时,执行的动作
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // 字面意思,replace为替换为ref值
深度测试(Depth Test)

通常用于隐藏面消除的z-buffer算法,通过比较片元的深度来判断片元是否通过深度测试,如果通过测试可以进入下一个测试阶段,最终可绘制出来,此阶段也是可以高度配置的,可用于透明效果的实现。

混合(Blend)

用于半透明物体或者透明物体的实现,如果没有混合操作,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色,开了混合后,GPU会取出源色和目标颜色,将两种颜色进行混合。

测试的顺序与性能

通常测试是在片元着色器后进行的,但是这样会浪费计算成本,很多片元着色器计算得到的值并不会显示在最终屏幕上,所以大多数GPU会尽可能在片元着色器之前进行这些测试,以提高性能,但是这样有可能与片元着色器中的操作,例如透明度测试,发生冲突,GPU会判断冲突,如果有冲突,则会禁用提前测试,这也是透明度测试会导致性能下降的原因。

关于Draw Call造成的性能影响

CPU和GPU并行工作

CPU和GPU通过命令缓冲区(Command Buffer)进行协同工作,即一个命令队列,CPU向其中添加命令,GPU从其中读取命令。

Draw Call对性能的影响及优化方法

每次调用Draw Call,CPU需要向GPU发送很多内容,包括数据、状态、命令等,这阶段CPU需要完成很多工作,例如检查渲染状态,待CPU完成这些工作后,GPU才开始渲染,但是GPU的渲染速度通常快于CPU的速度,如果Draw Call太多,CPU会把大量时间花费在提交Draw Call上,造成过载。

那么把大量Draw Call合并成一个Draw Call可以减少Draw Call,这就是批处理(Batching),通常通过在CPU内存中合并静态物体的网格来达到目的。

游戏开发中减少Draw Call的开销,需要注意:

  1. 避免使用大量很小的网格,同时尽量合并它们;
  2. 避免使用过多的材质;