推荐:用 NSDT设计器 快速搭建可编程3D场景。
昨天我在 WebGL 沙箱项目的评论中收到 Jon 的一个问题:
嗨, 布兰登,以你的演示为起点,我尝试显示一个金字塔,但到目前为止我只能看到它的四个面之一。 如果我使用 gl.LINES 而不是 gl.TRIANGLES,那么我只能看到一个三角形。 我对将纹理坐标混合到 vertArray 中的方式也有点困惑。 您能解释一下这些坐标是如何在着色器中排序的吗?
老实说,我不知道我是否是世界上解释这一点的最佳人选,但我会尝试一下,因为关于此的特定于 WebGL 的信息似乎非常少。 为了简单起见,大多数教程更喜欢将数组分开,但这对于性能来说并不是最佳的。 这些概念的工作方式与 OpenGL 几乎相同,但很高兴看到它们在你正在使用的环境中使用。 它需要对通常不适用于 Javascript 的主题有一些基本的了解,例如计算字节,但是一旦掌握了它的窍门,它就不会太难。
首先要注意的是,WebGL API 不知道任何有关顶点、纹理坐标、法线或其他类似内容的信息。 它所知道的是数字列表。 它知道如何在图形内存中保存数字列表,并且知道如何告诉着色器开始渲染这些数字,但实际上仅此而已。 你是通过着色器代码和 gl.vertexAttribPointer() 告诉系统数字含义的人。
var values = [
-1.0, -1.0, 0.0, // Vertex 1
0.0, 1.0, 0.0, // Vertex 2
1.0, -1.0, 0.0 // Vertex 3
];
你们以前可能都见过,对吧? 这次要注意的是,除了注释和我们构建换行符的方式之外,没有任何东西可以分隔顶点。 该行可以准确地写成这样:
var values = [ -1.0, -1.0, 0.0, 0.0, 1.0, 0.0, 1.0, -1.0, 0.0 ];
当然,现在对于我们人类来说阅读起来要困难得多,但是对于计算机来说却是完全一样的。 无论哪种情况,我们都将数据推送到顶点缓冲区中,如下所示:
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(values), gl.STATIC_DRAW);
请注意 Float32Array 位,我们稍后会讨论它。 不过,最终结果是 vertBuffer 现在指向显卡上某处的数字数组。 那么计算机如何知道如何解释这些看似随机的值呢? gl.vertexAttribPointer!
在渲染之前,我们必须调用如下代码:
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.vertexAttribPointer(shader_attrib_position, 3, gl.FLOAT, false, 12, 0);
这告诉系统它需要知道的关于如何读取顶点的一切(更准确地说,如何告诉着色器关于顶点的信息…)。 这里我们要注意 4 点: “3”告诉 WebGL 每个点由多少个值组成。 由于位置是由 x、y 和 z 坐标定义的,因此我们将其指定为“3”。 如果你在 WebGL 代码中进行 2D 渲染,可以轻松地在此处传递 2,并且可以根据情况给它 1 到 4 之间的任何值。 接下来,我们告诉它我们正在传递什么样的数字。 在本例中它们是浮点型,因此我们传入 gl.FLOAT。 还有 gl.BYTE 和 gl.SHORT (以及其他的,但我们不要把事情搞得太复杂)。 这告诉 WebGL 两件非常重要的事情:首先,如何将值暴露给着色器,其次每个值有多大。 现在这很重要,对于那些不熟悉字节打包的人来说,让我们快速回顾一下:
- gl.BYTE = 1 字节(显然!),可以保存从 -127 到 127 的值
- gl.SHORT = 2字节,取值范围-32767到32767
- gl.FLOAT = 4字节,一大堆的值范围。
在大多数情况下,除非你有一些非常具体的需求,否则你将希望坚持使用 gl.FLOAT。 这里使用 Float 也与我们在创建缓冲区时使用 Float32Array 一致。 如果我们要使用其他类型之一,你将需要使用不同的数组类型创建缓冲区。
重要的是,根据上面的 vertexAttribPointer 调用,我们传递了 3 个浮点数。 3 * 4 = 12,所以每个位置占用12个字节。 而且,嘿! 我们在那次通话中也有 12! 这称为Stride(步幅),它告诉系统每个顶点的起点相距多远(以字节为单位)。 如果我们的顶点包含的信息不仅仅是位置,这非常有用。 假设由于某种原因我们的顶点被这样定义:
var values = [
-1.0, -1.0, 0.0, 999.0, // Vertex 1
0.0, 1.0, 0.0, 999.0, // Vertex 2
1.0, -1.0, 0.0, 999.0 // Vertex 3
];
这些 999 可能代表顶点数据的其他部分,但它们与位置没有任何关系。 如果我们继续使用上面相同的 vertexAttribPointer 值,我们的顶点将如下所示:
[-1, -1, 0] [999, 0, 1] [0, 999, 1] [-1, 0, 999]
我们可以轻松地“忽略”第四个值,从而返回到我们想要的顶点位置,如下所示:
gl.vertexAttribPointer(shader_attrib_position, 3, gl.FLOAT, false, 16, 0);
请注意,我们仍然指示 WebGL 读取 3 个浮点,但步幅已更改为 16 以考虑第 4 个不需要的浮点(4 个值 * 每个值 4 个字节 = 16 个字节)。
最后,我们在末尾添加 0,它告诉 WebGL 开始读取数组中的值的深度。 这也是以字节为单位给出的,在本例中为 0,因为我们想从第一个值开始。 如果我们想从第二个数字开始,我们会这样做:
gl.vertexAttribPointer(shader_attrib_position, 3, gl.FLOAT, false, 16, 4);
现在我们的顶点位置将如下所示:
[-1, 0, 999] [1, 0, 999] [-1, 0, 999]
这对于将多个网格打包到单个缓冲区中非常有用,因为我们可以在任何点开始渲染,但它对于交错(Interleaved)数据也很有用。 “交错”意味着将多种类型的数据打包到一个数组中,例如位置和纹理坐标数据。 大多数教程会将它们显示为单独的数组,如下所示:
var pos = [1, 1, 1, 2, 2, 2, 3, 3, 3]; // (x, y, z), (x, y, z)...
var tex = [0, 1, 0, 1, 0, 1]; // (s, t), (s, t)...
但将它们全部作为一个数组通常会更高效、更容易,如下所示:
var verts = [
1, 1, 1, 0, 1, // (x, y, z), (s, t)...
2, 2, 2, 0, 1,
3, 3, 3, 0, 1,
];
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
当我们为此数组设置 vertexPointers 时,我们需要对 vertexAttribPointer 进行两次调用。
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.vertexAttribPointer(shader_attrib_position, 3, gl.FLOAT, false, 20, 0);
gl.vertexAttribPointer(shader_attrib_texcoord, 2, gl.FLOAT, false, 20, 12);
让我们看看一下这意味着什么:第一个属性(顶点位置)由 3 个浮点组成,从字节 0 开始,间隔 20 个字节。第二个属性(顶点UV)由 2 个浮点组成,间隔 20 个字节,从字节开始 12 个(或 3 个浮点数)。 就是这样! 一个缓冲区中有两种属性类型! 我们可以根据需要(在合理范围内)对尽可能多的属性执行此操作。 包含位置、纹理坐标、法线、副法线和顶点权重的单个缓冲区并不罕见!
我真诚地希望这一切都是有意义的,而不仅仅是一堆图形胡言乱语。 我在这里根本没有讨论着色器方面的内容,但其他教程对此进行了很好的介绍。
原文链接:WebGL交错缓冲区 — BimAnt