WebGL
是从过去的OpenGL
扩展而来的,与过去的OpenGL
非常不同。我了解光栅化是如何工作的,所以我对这些概念很满意。然而,我读过的每一篇教程都引入了抽象和帮助函数,这使得我更难理解哪些部分是openglapi的真正核心。
明确地说,像将位置数据和呈现功能分离到不同的类中这样的抽象在实际应用程序中非常重要。但是,这些抽象将代码分散到多个区域,并且由于在逻辑单元之间传递样板文件和传递数据而带来开销。我学到最好的方法是线性的代码流,每一行都是手头主题的核心。
首先,我使用的教程值得称赞。从这个基础开始,我剥离了所有的抽象,直到我有了一个“最小可行的程序”。希望这将帮助您开始使用现代OpenGL
。以下是我们制作的:
一个等边三角形,顶部为绿色,左下为黑色,右下为红色,中间插入颜色。
一个略为丰富多彩的黑色三角形
对于WebGL,我们需要一个画布来绘制。您肯定希望包含所有常见的HTML模板、一些样式等,但是画布是最关键的。加载DOM之后,我们就可以使用Javascript访问画布了。
<canvas id="container" width="500" height="500"></canvas> <script> document.addEventListener('DOMContentLoaded', () => { // 下面所有的Javascript代码都在这里 }); </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着色语言)编写的,然后将GLSL
编译为GPU
的机器代码。然后机器代码被发送到GPU
,这样它就可以在渲染过程中运行了。我不会花太多时间在GLSL
上,因为我只是想展示一些基本知识,但是这种语言与C语言非常接近,大多数程序员都很熟悉。
首先,我们编译一个顶点着色器并将其发送到GPU
。在这里,着色器的源代码存储在字符串中,但可以从其他地方加载。最终,字符串被发送到webglapi
。
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('编译顶点着色器失败'); } 复制代码
这里,GLSL
代码中有几个变量值得调用:
一种叫做位置
的属性。属性本质上是一个输入,并且为每个这样的输入调用着色器。
一种叫做颜色
的变化。这是顶点着色器的输出(每个输入一个),也是碎片着色器的输入。当该值传递给片段着色器时,该值将根据栅格化的属性进行插值。
gl位置值
。本质上是顶点着色器的输出,就像任何变化的值一样。这一个是特别的,因为它是用来确定哪些像素需要绘制。
还有一个称为uniform
的变量类型,它将在多次调用顶点着色器时保持不变。这些统一用于像变换矩阵这样的属性,对于单个几何体上的所有顶点,变换矩阵都是常量。
接下来,我们对fragment shader
执行相同的操作,编译并将其发送到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('未能编译片段着色器'); } 复制代码
最后,顶点和片段着色器都链接到一个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
的内存中并从那里进行处理。而不是对每一个输入进行单独的draw
调用(一次只传输一个相关数据),而是将整个输入传输到GPU
并从那里读取。(传统的OpenGL
一次只传输一个数据块,导致性能下降。)
OpenGL
提供了一种称为顶点缓冲对象(VBO)
的抽象。我仍在弄清楚所有这些是如何工作的,但最终,我们将使用抽象来完成以下工作:
在CPU
内存中存储一个字节序列。
使用使用创建的唯一缓冲区将字节传输到GPU的内存gl.createBuffer()
和一个绑定点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。因为顶点着色器只是按原样传递输入数据,所以我们可以直接在片段空间中指定坐标。
接下来,我们还要将缓冲区与顶点着色器中的一个变量相关联。在这里,我们:
从我们上面创建的程序中获取位置变量的句柄。
告诉OpenGL
从gl.ARRAY_BUFFER
绑定点,每批3个,具有特定参数,如偏移量和步长为零。
const attribute = gl.getAttribLocation(program, 'position'); gl.enableVertexAttribArray(attribute); gl.vertexAttribPointer(attribute, 3, gl.FLOAT, false, 0, 0); 复制代码
请注意,我们可以通过这种方式创建VBO
并将其与顶点着色器属性相关联,因为我们一个接一个地执行这两个操作。如果我们将这两个函数分开(例如一次性创建所有vbo
,然后将它们与单个属性相关联),则需要调用gl.bindBuffer(…)
将每个VBO
与其相应的属性关联。
最后,在GPU内存中的所有数据按照我们想要的方式设置好后,我们可以告诉OpenGL清除屏幕并在我们设置的数组上运行程序。作为栅格化的一部分(确定哪些像素被顶点覆盖),我们告诉OpenGL将3个一组的顶点作为三角形处理。
gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, 3); 复制代码
我们以线性方式设置的方式确实意味着程序可以一次性运行。在任何实际应用中,我们都会以结构化的方式存储数据,当数据发生变化时将其发送给GPU,并在每一帧中进行绘图。
把所有的东西放在一起,下面的图表显示了在屏幕上显示第一个三角形所需的最小概念集。即使这样,这个图也被大大简化了,所以最好的办法是将本文中介绍的75行代码放在一起并加以研究。
整个步骤包括:创建着色器,通过vbo将数据传输到GPU,将两者关联在一起,然后GPU将所有内容组合成最终图像。
虽然大大简化了,但需要一系列步骤来展示这个令人垂涎的三角形
学习OpenGL最困难的部分是在屏幕上获得最基本图像所需的大量样板文件。因为光栅化框架要求我们提供三维渲染功能,与GPU的通信是冗长的。
入门 WebGL