2.1 图形渲染管线 (Rendering Pipeline) 第二章:WebGL 核心概念领域 - 2.1 图形渲染管线 (Rendering Pipeline) 详解 2.1.1 渲染管线概述 渲染管线可以被视为一系列顺序执行的阶段,每个阶段接收前一阶段的输出作为输入,并进行特定的数据处理和转换。WebGL 的渲染管线是一个高度可配置的流程,允许开发者通过编写着色器程序来定制管线的某些阶段,从而实现各种视觉效果。 概括来说,WebGL 渲染管线主要包括以下几个核心阶段: 顶点着色器 (Vertex Shader):处理输入的顶点数据,执行顶点变换、光照计算等操作。 图元装配 (Primitive Assembly):将顶点着色器处理后的顶点数据组织成几何图元(如点、线、三角形)。
渲染管线可以被视为一系列顺序执行的阶段,每个阶段接收前一阶段的输出作为输入,并进行特定的数据处理和转换。WebGL 的渲染管线是一个高度可配置的流程,允许开发者通过编写着色器程序来定制管线的某些阶段,从而实现各种视觉效果。
概括来说,WebGL 渲染管线主要包括以下几个核心阶段:
顶点着色器 (Vertex Shader):处理输入的顶点数据,执行顶点变换、光照计算等操作。
图元装配 (Primitive Assembly):将顶点着色器处理后的顶点数据组织成几何图元(如点、线、三角形)。
光栅化 (Rasterization):将几何图元转换为屏幕上的像素片段 (Fragments)。
片段着色器 (Fragment Shader):为每个像素片段计算最终颜色,并可以进行纹理采样、光照计算等操作。
逐片段操作 (Per-Fragment Operations):执行深度测试、裁剪、混合等操作,最终决定像素是否可见以及如何与其他像素混合。
下面我们将逐个阶段进行详细解析,并结合代码实践来加深理解。
顶点着色器是渲染管线的第一个可编程阶段。它以顶点作为输入单位,每个顶点都会被顶点着色器独立处理一次。顶点数据通常包括顶点的位置坐标、法线、颜色、纹理坐标等属性。
顶点着色器的主要任务包括:
模型视图变换 (Model-View Transformation):将顶点从模型局部坐标系转换到世界坐标系,再转换到相机坐标系(也称为观察坐标系)。这涉及到模型矩阵和视图矩阵的乘法运算。
投影变换 (Projection Transformation):将顶点从相机坐标系转换到裁剪坐标系。投影变换负责将 3D 场景投影到 2D 平面上,并定义了视锥体的形状(透视投影或正交投影)。
顶点属性处理:可以修改顶点的颜色、法线、纹理坐标等属性,并将这些属性传递给后续的管线阶段。
剪裁坐标计算:顶点着色器必须输出裁剪坐标 gl_Position,这是后续管线阶段进行剪裁和投影的关键。
代码实践 - 顶点着色器示例 (GLSL):
#version 300 es in vec4 a_position; // 输入顶点位置属性 in vec3 a_color; // 输入顶点颜色属性 uniform mat4 u_modelMatrix; // 模型矩阵 uniform mat4 u_viewMatrix; // 视图矩阵 uniform mat4 u_projectionMatrix; // 投影矩阵 out vec3 v_color; // 输出颜色到片段着色器 void main() { // 模型视图投影变换 gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_position; // 将顶点颜色传递给片段着色器 v_color = a_color; }
代码详解:
#version 300 es: 指定 GLSL ES 3.0 版本。
in vec4 a_position;: 声明输入属性 a_position,类型为 vec4 (四维向量),用于接收顶点位置数据。in 关键字表示输入属性。
in vec3 a_color;: 声明输入属性 a_color,类型为 vec3 (三维向量),用于接收顶点颜色数据。
uniform mat4 u_modelMatrix;, uniform mat4 u_viewMatrix;, uniform mat4 u_projectionMatrix;: 声明 Uniform 变量,用于接收模型矩阵、视图矩阵和投影矩阵。uniform 关键字表示 Uniform 变量,其值在一次绘制调用中保持不变。mat4 表示 4x4 矩阵。
out vec3 v_color;: 声明输出变量 v_color,类型为 vec3,用于将顶点颜色数据传递给片段着色器。out 关键字表示输出变量。
void main() { ... }: 顶点着色器的主函数。
gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_position;: 执行模型视图投影变换,计算裁剪空间坐标,并将结果赋值给内置输出变量 gl_Position。注意矩阵乘法的顺序,从右向左依次应用模型矩阵、视图矩阵和投影矩阵。
v_color = a_color;: 将输入的顶点颜色属性 a_color 赋值给输出变量 v_color,传递给片段着色器。
图元装配阶段接收顶点着色器处理后的顶点数据,并将这些顶点组织成几何图元。几何图元的类型由绘制调用时的参数指定,常见的图元类型包括:
点 (Points):将每个顶点作为一个单独的点绘制。
线 (Lines):将每两个顶点连接成一条线段。
三角形 (Triangles):将每三个顶点组成一个三角形。
图元装配阶段会根据指定的图元类型,将顶点连接成相应的几何形状。例如,如果指定绘制三角形,则每三个顶点会被组合成一个三角形。
代码实践 - JavaScript WebGL 设置 (图元类型):
// ... 获取 WebGL 上下文,创建程序,绑定属性和 Uniform ... // 设置顶点数据 (简化示例,假设已经有顶点数据) const positions = [ 0.0, 0.5, 0.0, // 顶点 1 -0.5, -0.5, 0.0, // 顶点 2 0.5, -0.5, 0.0 // 顶点 3 ]; const colors = [ 1.0, 0.0, 0.0, // 红色 0.0, 1.0, 0.0, // 绿色 0.0, 0.0, 1.0 // 蓝色 ]; // ... 创建缓冲区,上传顶点数据 ... // 绘制三角形 gl.drawArrays(gl.TRIANGLES, 0, 3); // 使用 TRIANGLES 图元类型,从顶点索引 0 开始,绘制 3 个顶点 // 可以尝试其他图元类型,例如绘制线段 // gl.drawArrays(gl.LINES, 0, 6); // 需要提供 6 个顶点来绘制两条线段 // gl.drawArrays(gl.POINTS, 0, 3); // 绘制三个点
代码详解:
gl.drawArrays(mode, first, count): WebGL 绘制调用的核心函数。
mode: 指定图元类型,例如 gl.TRIANGLES (三角形), gl.LINES (线段), gl.POINTS (点)。
first: 指定从哪个顶点索引开始绘制。
count: 指定绘制多少个顶点。
图元装配阶段示意图 (Mermaid):
光栅化阶段接收图元装配阶段生成的几何图元,并将这些图元转换为屏幕上的像素片段 (Fragments)。对于每个图元,光栅化器会确定哪些像素被该图元覆盖,并为每个被覆盖的像素生成一个片段。
光栅化阶段的主要任务包括:
图元遍历:遍历几何图元覆盖的像素区域。
片段生成:为每个被覆盖的像素生成一个片段。片段包含像素的位置信息 (屏幕坐标)、深度信息以及顶点着色器传递过来的插值属性 (如颜色、纹理坐标等)。
插值计算:对于三角形等面片图元,需要根据顶点属性进行插值计算,得到每个片段的属性值。例如,如果顶点着色器输出了顶点颜色,光栅化器会根据三角形面片上的位置对顶点颜色进行插值,得到每个片段的颜色值。
代码实践 - 光栅化概念 (GLSL 片段着色器接收插值属性):
在顶点着色器中我们输出了 v_color 变量:
// 顶点着色器 (部分代码) out vec3 v_color; void main() { // ... v_color = a_color; }
在片段着色器中,我们可以接收这个插值后的 v_color 变量:
#version 300 es precision mediump float; // 设置浮点数精度 in vec3 v_color; // 接收顶点着色器传递的颜色 (插值后) out vec4 fragColor; // 输出片段颜色 void main() { // 直接使用插值后的颜色作为片段颜色 fragColor = vec4(v_color, 1.0); // 将 RGB 颜色转换为 RGBA,alpha 设为 1.0 (不透明) }
代码详解:
precision mediump float;: 设置浮点数精度为中等精度。在片段着色器中通常需要指定精度,以平衡性能和精度。
in vec3 v_color;: 声明输入变量 v_color,类型为 vec3,用于接收顶点着色器传递过来的颜色数据。注意,这里的 v_color 是经过光栅化阶段插值后的值。
out vec4 fragColor;: 声明输出变量 fragColor,类型为 vec4,用于指定片段的最终颜色。gl_FragColor 在 GLSL ES 3.0 中被 fragColor 替代。
fragColor = vec4(v_color, 1.0);: 将插值后的顶点颜色 v_color 转换为 vec4 类型,并设置 alpha 通道为 1.0,作为片段的最终颜色输出。
光栅化阶段示意图 (Mermaid):
片段着色器是渲染管线的第二个可编程阶段,也是最重要的阶段之一。它以光栅化阶段生成的像素片段作为输入单位,每个片段都会被片段着色器独立处理一次。
片段着色器的主要任务包括:
纹理采样:从纹理图像中读取纹素 (Texel) 数据,用于实现纹理贴图效果。
光照计算:根据光照模型和材质属性,计算片段的颜色。
颜色混合:可以进行颜色混合操作,例如将纹理颜色与光照颜色混合。
丢弃片段:可以根据条件丢弃某些片段,不进行后续的逐片段操作。
输出片段颜色:片段着色器必须输出片段的最终颜色 fragColor (或 gl_FragColor 在 GLSL ES 1.0/2.0 中),这将传递给后续的逐片段操作阶段。
代码实践 - 片段着色器示例 (纹理采样):
#version 300 es precision mediump float; in vec2 v_texCoord; // 接收顶点着色器传递的纹理坐标 (插值后) uniform sampler2D u_texture; // 纹理 Uniform 变量 out vec4 fragColor; void main() { // 纹理采样 vec4 textureColor = texture(u_texture, v_texCoord); // 将纹理颜色作为片段颜色 fragColor = textureColor; }
代码详解:
in vec2 v_texCoord;: 声明输入变量 v_texCoord,类型为 vec2,用于接收顶点着色器传递过来的纹理坐标数据 (经过光栅化阶段插值)。
uniform sampler2D u_texture;: 声明 Uniform 变量 u_texture,类型为 sampler2D,用于接收 2D 纹理对象。sampler2D 是纹理采样器的类型。
vec4 textureColor = texture(u_texture, v_texCoord);: 使用内置函数 texture() 进行纹理采样。texture() 函数接收纹理采样器 u_texture 和纹理坐标 v_texCoord 作为参数,返回采样得到的纹素颜色值。
fragColor = textureColor;: 将纹理采样得到的颜色 textureColor 作为片段的最终颜色输出。
代码实践 - 顶点着色器示例 (传递纹理坐标):
为了配合上面的片段着色器示例,我们需要在顶点着色器中计算并传递纹理坐标:
#version 300 es in vec4 a_position; in vec2 a_texCoord; // 输入纹理坐标属性 uniform mat4 u_modelMatrix; uniform mat4 u_viewMatrix; uniform mat4 u_projectionMatrix; out vec2 v_texCoord; // 输出纹理坐标到片段着色器 void main() { gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_position; // 将纹理坐标传递给片段着色器 v_texCoord = a_texCoord; }
代码详解:
in vec2 a_texCoord;: 声明输入属性 a_texCoord,类型为 vec2,用于接收顶点纹理坐标数据。
out vec2 v_texCoord;: 声明输出变量 v_texCoord,类型为 vec2,用于将纹理坐标数据传递给片段着色器。
v_texCoord = a_texCoord;: 将输入的纹理坐标属性 a_texCoord 赋值给输出变量 v_texCoord,传递给片段着色器。
片段着色器阶段示意图 (Mermaid):
逐片段操作阶段是渲染管线的最后阶段,它接收片段着色器输出的片段颜色,并执行一系列测试和混合操作,最终决定片段是否可见以及如何与其他像素混合。
逐片段操作的主要步骤包括:
裁剪测试 (Scissor Test):根据裁剪矩形区域,丢弃区域外的片段。
模板测试 (Stencil Test):根据模板缓冲区的值,决定是否丢弃片段。
深度测试 (Depth Test):将片段的深度值与深度缓冲区的值进行比较,决定是否丢弃片段。深度测试用于实现遮挡关系,确保物体的前后顺序正确。
混合 (Blending):将片段的颜色与颜色缓冲区中已有的颜色进行混合,实现透明效果。
抖动 (Dithering):通过抖动算法,减少颜色量化误差,提高颜色表现力。
颜色缓冲区写入:如果片段通过了所有测试,则将其颜色写入颜色缓冲区。
代码实践 - 深度测试和混合设置 (JavaScript WebGL):
// ... 获取 WebGL 上下文 ... // 开启深度测试 gl.enable(gl.DEPTH_TEST); // 设置深度测试函数 (默认 gl.LESS,即深度值小于缓冲区值时通过测试) // gl.depthFunc(gl.LESS); // 开启混合 gl.enable(gl.BLEND); // 设置混合函数 (常用的 Alpha 混合) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // 设置混合方程 (默认 gl.FUNC_ADD) // gl.blendEquation(gl.FUNC_ADD); // 清除颜色缓冲区和深度缓冲区 gl.clearColor(0.0, 0.0, 0.0, 1.0); // 黑色背景 gl.clearDepth(1.0); // 深度缓冲区最大值 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // ... 设置顶点数据,着色器,Uniform ... // ... 执行绘制调用 gl.drawArrays() ...
代码详解:
gl.enable(gl.DEPTH_TEST);: 开启深度测试。
gl.depthFunc(func);: 设置深度测试函数。gl.LESS (默认值):如果片段深度值小于深度缓冲区的值,则通过测试。常用的函数还有 gl.GREATER, gl.EQUAL, gl.LEQUAL, gl.GEQUAL, gl.NOTEQUAL, gl.ALWAYS, gl.NEVER。
gl.enable(gl.BLEND);: 开启混合。
gl.blendFunc(sfactor, dfactor);: 设置混合函数,定义源颜色和目标颜色如何混合。gl.SRC_ALPHA (源颜色 Alpha 值), gl.ONE_MINUS_SRC_ALPHA (1 减去源颜色 Alpha 值) 是常用的 Alpha 混合参数。
gl.blendEquation(mode);: 设置混合方程,定义颜色混合的运算方式。gl.FUNC_ADD (默认值):源颜色和目标颜色相加。常用的方程还有 gl.FUNC_SUBTRACT, gl.FUNC_REVERSE_SUBTRACT。
gl.clearColor(r, g, b, a);: 设置清除颜色缓冲区的颜色。
gl.clearDepth(depth);: 设置清除深度缓冲区的深度值。
gl.clear(mask);: 清除指定的缓冲区。gl.COLOR_BUFFER_BIT 清除颜色缓冲区,gl.DEPTH_BUFFER_BIT 清除深度缓冲区。
逐片段操作阶段示意图 (Mermaid):
流程图解释:
顶点数据 (Vertex Data):作为渲染管线的输入,包括顶点位置、颜色、法线、纹理坐标等属性。
顶点着色器 (Vertex Shader):处理顶点数据,进行模型视图投影变换,计算顶点属性,输出裁剪坐标和顶点属性。
图元装配 (Primitive Assembly):将顶点组织成几何图元 (点、线、三角形)。
光栅化 (Rasterization):将几何图元转换为像素片段,并进行属性插值。
片段着色器 (Fragment Shader):为每个片段计算最终颜色,进行纹理采样、光照计算等操作。
逐片段操作 (Per-Fragment Operations):执行裁剪测试、模板测试、深度测试、混合等操作,决定片段是否可见。
帧缓冲区 (Framebuffer):存储渲染结果,包括颜色缓冲区、深度缓冲区等。最终图像会显示在颜色缓冲区中。
最终图像 (Final Image):渲染管线的最终输出,显示在屏幕上的 2D 图像。
注意: 顶点着色器和片段着色器是可编程阶段,开发者可以通过编写 GLSL 代码来定制这两个阶段的功能。其他阶段是固定管线阶段,其行为由硬件和 WebGL API 控制。
管线阶段顺序执行:顶点着色器 -> 图元装配 -> 光栅化 -> 片段着色器 -> 逐片段操作。
可编程阶段:顶点着色器和片段着色器是可编程的,允许开发者自定义顶点和片段的处理逻辑。
数据流转:数据在管线中流动,每个阶段接收前一阶段的输出,并产生下一阶段的输入。
核心概念:顶点、图元、片段、着色器、缓冲区、状态等概念是理解渲染管线的基石。
实践重要性:通过编写着色器代码和 WebGL JavaScript 代码,可以更深入地理解渲染管线的工作方式,并掌握 WebGL 图形编程的核心技能。