乐趣区

关于前端:最简WebGL教程仅需-75-行代码

作者:Avik Das

翻译:疯狂的技术宅

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

未经容许严禁转载

古代 OpenGL(以及名为 WebGL 的扩大)与我过来学习的传统 OpenGL 有很大不同。我理解栅格化的工作原理,所以对这些概念很称心。然而我所浏览的每篇教程都介绍了形象和辅助函数,这使我很难了解哪些局部是 OpenGL API 的真正外围。

明确地说,在理论的应用程序中,把地位数据和渲染性能拆散到独自的类这样的形象很重要。然而,这些形象把代码散布到了多个区域,并且因为模板的反复以及逻辑单元之间的数据传递而导致大量的开销。而我的最佳学习形式是线性代码流,其中每一行都是手头主题的外围。

首先,本文要归功于我所学过的教程。从这个根底开始,我剥离了所有形象,直到有了一个“最小可行的程序”为止。心愿这将帮忙你应用古代 OpenGL 入门。这就咱们要做的:

一个等边三角形,顶部为绿色,左下为彩色,右下为红色,两头有过渡色彩

初始化

要应用 WebGL,须要用 canvas 进行绘制。你必定会想包含一些罕用的 HTML 骨架、某些款式等,然而 canvas 才是最要害的。加载 DOM 后,咱们将可能用 Javascript 拜访画布。

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

<script>
  document.addEventListener('DOMContentLoaded', () => {// 所有的 Javascript 代码将会呈现在这里});
</script>

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

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 行代码放在一起进行钻研。

残缺的解决流程:首先创立着色器,通过 VBO 将数据传输到 GPU,把两者关联在一起,而后 GPU 在再将所有内容组装成最终的图像。

最初的步骤,只管通过了简化,但残缺形容了三角形所需的步骤程序

对我而言,学习 OpenGL 的难点在于取得屏幕上最根本图像所需的大量模板。因为栅格化框架要求咱们提供 3D 渲染性能,并且与 GPU 的通信十分简短,所以有很多概念须要事后学习。我心愿本文所展现的基础知识比其余教程更简略!

前端刷题神器

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


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

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

欢送持续浏览本专栏其它高赞文章:

  • 深刻了解 Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13 个帮你进步开发效率的古代 CSS 框架
  • 疾速上手 BootstrapVue
  • JavaScript 引擎是如何工作的?从调用栈到 Promise 你须要晓得的所有
  • WebSocket 实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30 分钟用 Node.js 构建一个 API 服务器
  • Javascript 的对象拷贝
  • 程序员 30 岁前月薪达不到 30K,该何去何从
  • 14 个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把 HTML 转成 PDF 的 4 个计划及实现

  • 更多文章 …
退出移动版