最简WebGL教程,仅需 75 行代码

做者:Avik Das

翻译:疯狂的技术宅javascript

原文:https://avikdas.com/2020/07/0...html

未经容许严禁转载前端

现代 OpenGL(以及名为WebGL的扩展)与我过去学习的传统 OpenGL 有很大不一样。我了解栅格化的工做原理,因此对这些概念很满意。可是我所阅读的每篇教程都介绍了抽象和辅助函数,这使我很难理解哪些部分是 OpenGL API 的真正核心。java

明确地说,在实际的应用程序中,把位置数据和渲染功能分离到单独的类这样的抽象很重要。可是,这些抽象把代码分布到了多个区域,而且因为模板的重复以及逻辑单元之间的数据传递而致使大量的开销。而个人最佳学习方式是线性代码流,其中每一行都是手头主题的核心。程序员

首先,本文要归功于我所学过的教程。从这个基础开始,我剥离了全部抽象,直到有了一个“最小可行的程序”为止。但愿这将帮助你使用现代OpenGL入门。这就咱们要作的:web

image.png

一个等边三角形,顶部为绿色,左下为黑色,右下为红色,中间有过渡颜色面试

初始化

要使用 WebGL,须要用 canvas 进行绘制。你确定会想包括一些经常使用的 HTML 骨架、某些样式等,可是 canvas 才是最关键的。加载 DOM 后,咱们将可以用 Javascript 访问画布。canvas

<canvas id="container" width="500" height="500"></canvas>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    // 全部的 Javascript 代码将会出如今这里
  });
</script>

咱们能够经过画布的可访问性得到 WebGL 的渲染上下文,并将其初始化为透明色。 OpenGL 的世界中的颜色是RGBA,每一个份量都在 01 之间。透明色是用于在从新绘制场景的帧的开始时绘制画布的颜色。segmentfault

const canvas = document.getElementById('container');
const gl = canvas.getContext('webgl');

gl.clearColor(1, 1, 1, 1);

在实际的程序中,还能够进行更多的初始化。须要特别注意的是启用了“深度缓冲区(depth buffer)”,这将容许基于 Z 坐标对几何图形进行排序。对于只包含一个三角形的最简程序,咱们将会忽略这种状况。服务器

编译着色器

OpenGL 的核心是栅格化框架,在这里咱们能够决定如何实现除栅格化以外的全部内容。这须要在 GPU 上至少运行两段代码:

  1. 为输入所执行的顶点着色器,每一个输入都会对应输出一个3D位置(其实是齐次坐标中的4D)。
  2. 为屏幕上的每一个像素所执行的片断着色器,负责输出这个像素应该是哪一种颜色。

在这两个步骤之间,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 代码中有一些须要提到的变量:

  1. 一个名为 position属性。属性本质上是一个输入,而且为每一个这样的输入调用着色器。
  2. 一种称为 colorvarying。这既是顶点着色器的输出(每一个顶点着色器都有一个),也是片断着色器的输入。值被传递到片断着色器时,将根据栅格化的属性对值进行插值计算。
  3. gl_Position 值。本质上是顶点着色器的输出,如任何存在变化的值。这很特别,由于它用于肯定须要去绘制哪些像素。

还有一个称为 uniform 的变量类型,该变量类型在屡次调用顶点着色器时将会保持不变。这些 uniform 用于变换矩阵之类的属性,对于单个几何图形上的顶点来讲,它们都是恒定的。

接下来,咱们用片断着色器执行相同的操做,将其编译并发送到 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)的抽象。我仍在试图彻底弄清楚它的工做原理,可是最终,咱们将会使用抽象来进行如下操做:

  1. 将一系列字节存储在 CPU 的内存中。
  2. 用经过 gl.createBuffe() 建立的惟一缓冲区和 gl.ARRAY_BUFFER 的绑定点(binding point)将字节传输到 GPU 的内存。

尽管在顶点着色器中每一个输入变量(属性)都有一个 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 的“剪辑空间(clip space)”。我不会介绍剪辑空间的详细信息(它们与同构坐标有关),可是如今,X 和Y 在 -1+1 之间变化。因为顶点着色器仅按原样传递输入数据,所以能够直接在剪辑空间中指定坐标。

接下来,咱们还会把缓冲区与顶点着色器中的变量之一相关联:

  1. 从上面建立的程序中获取 position 变量的句柄。
  2. 告诉 OpenGL 从 gl.ARRAY_BUFFER 绑定点读取数据,每批 3 个,其特殊参数如 offsetstride 为零。
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 行代码放在一块儿进行研究。

image.png

完整的处理流程:首先建立着色器,经过 VBO 将数据传输到 GPU,把二者关联在一块儿,而后 GPU 在再将全部内容组装成最终的图像。

最后的步骤,尽管通过了简化,但完整描述了三角形所需的步骤顺序

对我而言,学习 OpenGL 的难点在于得到屏幕上最基本图像所需的大量模板。因为栅格化框架要求咱们提供 3D 渲染功能,而且与 GPU 的通讯很是冗长,因此有不少概念须要预先学习。我但愿本文所展现的基础知识比其余教程更简单!

前端刷题神器

扫码进入前端面试星球🌍,解锁刷题神器,还能够获取800+道前端面试题一线常见面试高频考点

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


相关文章
相关标签/搜索