要澄清的是,在实际应用中,诸如将这些位置和渲染功能划分为单独的类之类的抽象很重要。但是,由于逻辑单元之间的样板和数据传输,这些抽象将代码分散在不同的区域,并增加了冗余。我发现以线性代码流研究某个主题最方便,其中每一行都直接与此主题相关。
首先,我要感谢所使用的教程的创建者。以它为基础,我摆脱了所有抽象,直到获得了“最小可行程序”。我希望它可以帮助您开始使用现代OpenGL。这是我们将要做的:
等边三角形,顶部为绿色,左下方为黑色,右下方为红色,点之间插有颜色。黑色三角形的稍微亮些的版本[在Habré中翻译]。
初始化
在WebGL中,我们需要
canvas
绘制。当然,您肯定需要添加所有常用的HTML样板,样式等,但是canvas是最重要的。加载DOM之后,我们可以使用Javascript访问画布。
<canvas id="container" width="500" height="500"></canvas>
<script>
document.addEventListener('DOMContentLoaded', () => {
// All the Javascript code below goes here
});
</script>
通过访问画布,我们可以获得WebGL渲染上下文并初始化其透明颜色。OpenGL世界中的颜色存储为RGBA,每个组件的值都从
0
到1
。透明色是用于在每帧开始时绘制画布并重新绘制场景的颜色。
const canvas = document.getElementById('container');
const gl = canvas.getContext('webgl');
gl.clearColor(1, 1, 1, 1);
在实际程序中,初始化可以而且应该更详细。特别要提到的是包含了一个深度缓冲区,该缓冲区允许您基于Z坐标对几何进行排序,而对于仅由一个三角形组成的简单程序,我们就不会这样做。
编译着色器
OpenGL的核心是栅格化框架,在该框架中,我们必须决定如何实施除栅格化以外的所有内容。因此,必须在GPU中执行至少两个阶段的代码:
- 一个顶点着色器,用于处理所有输入数据并为每个输入输出一个3D位置(实际上是统一坐标中的4D位置)。
- 片段着色器,用于处理屏幕上的每个像素,渲染应使用该像素绘制的颜色。
在这两个阶段之间,OpenGL从顶点着色器获取几何图形,并确定该几何图形覆盖哪些屏幕像素。这是光栅化阶段。
两种着色器通常都是用GLSL(OpenGL着色语言)编写的,然后将其编译为GPU的机器代码。然后将机器代码传递到GPU,以便可以在渲染过程中执行它。我不会详细介绍GLSL,因为我只想展示其基础知识,但是该语言与C足够接近,足以让大多数程序员熟悉。
首先,我们编译顶点着色器并将其传递给GPU。在下面显示的片段中,着色器源代码存储为字符串,但可以从其他位置加载。最后,该字符串将传递到WebGL API。
const sourceV = `
attribute vec3 position;
varying vec4 color;
void main() {
gl_Position = vec4(position, 1);
color = gl_Position * 0.5 + 0.5;
}
`;
const shaderV = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shaderV, sourceV);
gl.compileShader(shaderV);
if (!gl.getShaderParameter(shaderV, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shaderV));
throw new Error('Failed to compile vertex shader');
}
值得在这里解释GLSL代码中的一些变量:
- (attribute)
position
. , , . - Varying
color
. ( ) . . -
gl_Position
. , , varying-. , ,
还有一个统一变量类型,它在所有顶点着色器调用中都是常量。此类制服用于变换矩阵之类的属性,对于一个几何元素的所有顶点而言,该常数将保持不变。
接下来,我们对片段着色器执行相同的操作-将其编译并将其传输到GPU。请注意,
color
片段着色器现在可以读取顶点着色器中的变量。
const sourceF = `
precision mediump float;
varying vec4 color;
void main() {
gl_FragColor = color;
}
`;
const shaderF = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shaderF, sourceF);
gl.compileShader(shaderF);
if (!gl.getShaderParameter(shaderF, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shaderF));
throw new Error('Failed to compile fragment shader');
}
此外,顶点着色器和片段着色器都链接到一个OpenGL程序中。
const program = gl.createProgram();
gl.attachShader(program, shaderV);
gl.attachShader(program, shaderF);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
throw new Error('Failed to link program');
}
gl.useProgram(program);
我们告诉GPU我们要执行上述着色器。现在,我们要做的就是创建传入数据,然后让GPU处理这些数据。
将传入数据发送到GPU
传入的数据将存储在GPU内存中并在那里进行处理。与其对每个传入数据进行一次单独的绘制调用(一次将一个数据块传输一次),不如将所有传入数据全部传输到GPU并从那里读取。(旧的OpenGL在各个元素上传递数据,这会降低性能。)
OpenGL提供了一种称为“顶点缓冲对象(VBO)”的抽象。我仍在弄清楚它是如何工作的,但是最终我们将做以下工作来使用它:
- 将数据序列存储在中央处理器(CPU)存储器中。
- 通过使用
gl.createBuffer()
和锚点 创建的唯一缓冲区将字节传输到GPU内存gl.ARRAY_BUFFER
。
对于顶点着色器中输入数据(属性)的每个变量,我们将有一个VBO,尽管可以对输入数据的多个元素使用一个VBO。
const positionsData = new Float32Array([
-0.75, -0.65, -1,
0.75, -0.65, -1,
0 , 0.65, -1,
]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positionsData, gl.STATIC_DRAW);
通常,我们用应用程序能够理解的坐标定义几何,然后在顶点着色器中使用一组变换将它们映射到OpenGL剪辑空间。我不会详细介绍截断空间(它与齐次坐标相关联),而您只需要知道X和Y在-1到+1范围内变化即可。由于顶点着色器仅按原样传递输入,因此我们可以直接在剪切空间中设置坐标。
然后,我们还将缓冲区绑定到顶点着色器中的变量之一。在代码中,我们执行以下操作:
- 我们
position
从上面创建的程序中获取变量描述符。 - 我们指示OpenGL以
gl.ARRAY_BUFFER
特定参数(例如,偏移量和步幅为0)从锚点读取数据(以3为一组)。
const attribute = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(attribute);
gl.vertexAttribPointer(attribute, 3, gl.FLOAT, false, 0, 0);
值得注意的是,我们可以通过这种方式创建一个VBO并将其绑定到顶点着色器属性,因为我们要依次执行这些功能。如果要分离这两个函数(例如,一次创建所有VBO,然后将它们绑定到单独的属性),那么在将每个VBO映射到相应的属性之前,我们需要每次调用
gl.bindBuffer(...)
。
渲染!
最后,在正确准备好GPU内存中的所有数据之后,我们可以告诉OpenGL清除屏幕并运行程序来处理我们准备好的阵列。作为栅格化步骤的一部分(确定哪些像素被顶点覆盖),我们告诉OpenGL将3个一组的顶点视为三角形。
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
使用这种线性方案,程序将一次性执行。在任何实际应用中,我们都将以结构化的方式存储数据,并在数据发生变化时将其发送到GPU,并在每一帧进行渲染。
总而言之,以下是在屏幕上显示第一个三角形所需的最少概念集的图表。但是,即使该方案也大大简化了,所以最好编写本文介绍的75行代码并进行研究。
显示三角形所需的最终高度简化的步骤序列
对我而言,学习OpenGL的最难部分是在屏幕上显示最简单的图像所需的大量模板。由于栅格化框架要求我们提供3D渲染功能,并且与GPU的通信量很大,因此必须直接研究许多概念。希望本文以比其他教程更简单的方式向您展示了基础知识。
也可以看看:
- “每个人的WebGL ”
- “ WebGL应用程序性能”
- “五个惊人的WebGL演示”
也可以看看:
- “每个人的WebGL ”
- “ WebGL应用程序性能”
- “五个惊人的WebGL演示”