前端实现OpenGl

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

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

初始化

要使用 WebGL,需要用 canvas 进行绘制。你肯定会想包括一些常用的 HTML 骨架、某些样式等,但是 canvas 才是最关键的。加载 DOM 后,我们将能够用 Javascript 访问画布。

document.addEventListener('DOMContentLoaded',() =>{// 所有的 Javascript 代码将会出现在这里});document.addEventListener('DOMContentLoaded',() =>{// 所有的 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);

在实际的程序中,还可以进行更多的初始化。需要特别注意的是启用了“深度缓冲区(depth buffer)”,这将允许基于 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 代码中有一些需要提到的变量:

一个名为 position 的属性。属性本质上是一个输入,并且为每个这样的输入调用着色器。

一种称为 color 的 varying。这既是顶点着色器的输出(每个顶点着色器都有一个),也是片段着色器的输入。值被传递到片段着色器时,将根据栅格化的属性对值进行插值计算。

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)的抽象。我仍在试图完全弄清楚它的工作原理,但是最终,我们将会使用抽象来进行以下操作:

将一系列字节存储在 CPU 的内存中。

用通过 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 之间变化。由于顶点着色器仅按原样传递输入数据,因此可以直接在剪辑空间中指定坐标。

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

从上面创建的程序中获取 position 变量的句柄。

告诉 OpenGL 从 gl.ARRAY_BUFFER 绑定点读取数据,每批 3 个,其特殊参数如 offset 和 stride为零。

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 的通信非常冗长,所以有很多概念需要预先学习。我希望本文所展示的基础知识比其他教程更简单

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容