|
| 1 | +Title: WebGL2 属性(Attributes) |
| 2 | +Description: WebGL 中的 attributes 是什么? |
| 3 | +TOC: 属性(Attributes) |
| 4 | + |
| 5 | +本文旨在帮助你建立对 WebGL 中属性状态是如何设置的一个直观理解。 |
| 6 | +另有[关于纹理单元的类似文章](webgl-texture-units.html)以及[framebuffer 的文章](webgl-framebuffers.html)。 |
| 7 | + |
| 8 | +前置知识建议阅读:[WebGL 是如何工作的](webgl-how-it-works.html) 和 |
| 9 | +[WebGL 着色器和 GLSL](https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html)。 |
| 10 | + |
| 11 | +## Attributes(属性) |
| 12 | + |
| 13 | +在 WebGL 中,attributes 是传入顶点着色器的输入,数据来自 buffer。 |
| 14 | +每当调用 `gl.drawArrays` 或 `gl.drawElements` 时,WebGL 会执行用户提供的顶点着色器 N 次。 |
| 15 | +每次迭代,attributes 定义了如何从绑定到它们的 buffer 中提取数据, |
| 16 | +并将其传递给顶点着色器中的属性变量。 |
| 17 | + |
| 18 | +如果用 JavaScript 来模拟实现,它们可能像这样: |
| 19 | + |
| 20 | +```js |
| 21 | +// pseudo code |
| 22 | +const gl = { |
| 23 | + arrayBuffer: null, |
| 24 | + vertexArray: { |
| 25 | + attributes: [ |
| 26 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 27 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 28 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 29 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 30 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 31 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 32 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 33 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 34 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 35 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 36 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 37 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 38 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 39 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 40 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 41 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, |
| 42 | + ], |
| 43 | + elementArrayBuffer: null, |
| 44 | + }, |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +如上所示,总共有 16 个 attributes。 |
| 49 | + |
| 50 | +当你调用 `gl.enableVertexAttribArray(location)` 或 `gl.disableVertexAttribArray`,可以将其理解为如下操作: |
| 51 | + |
| 52 | +```js |
| 53 | +// pseudo code |
| 54 | +gl.enableVertexAttribArray = function(location) { |
| 55 | + const attrib = gl.vertexArray.attributes[location]; |
| 56 | + attrib.enable = true; |
| 57 | +}; |
| 58 | + |
| 59 | +gl.disableVertexAttribArray = function(location) { |
| 60 | + const attrib = gl.vertexArray.attributes[location]; |
| 61 | + attrib.enable = false; |
| 62 | +}; |
| 63 | +``` |
| 64 | + |
| 65 | +换句话说,location 就是 attribute 的索引。 |
| 66 | + |
| 67 | +类似地,`gl.vertexAttribPointer` 用来设置 attribute 的几乎所有其他属性。 |
| 68 | +实现可能如下所示: |
| 69 | + |
| 70 | +```js |
| 71 | +// pseudo code |
| 72 | +gl.vertexAttribPointer = function(location, size, type, normalize, stride, offset) { |
| 73 | + const attrib = gl.vertexArray.attributes[location]; |
| 74 | + attrib.size = size; |
| 75 | + attrib.type = type; |
| 76 | + attrib.normalize = normalize; |
| 77 | + attrib.stride = stride ? stride : sizeof(type) * size; |
| 78 | + attrib.offset = offset; |
| 79 | + attrib.buffer = gl.arrayBuffer; // !!!! <----- |
| 80 | +}; |
| 81 | +``` |
| 82 | + |
| 83 | +注意,调用 `gl.vertexAttribPointer` 时,`attrib.buffer` 会设置为当前的 `gl.arrayBuffer`。 |
| 84 | +`gl.arrayBuffer` 如上述伪代码所示,通过调用 `gl.bindBuffer(gl.ARRAY_BUFFER, someBuffer)` 设置。 |
| 85 | + |
| 86 | + |
| 87 | +```js |
| 88 | +// pseudo code |
| 89 | +gl.bindBuffer = function(target, buffer) { |
| 90 | + switch (target) { |
| 91 | + case ARRAY_BUFFER: |
| 92 | + gl.arrayBuffer = buffer; |
| 93 | + break; |
| 94 | + case ELEMENT_ARRAY_BUFFER; |
| 95 | + gl.vertexArray.elementArrayBuffer = buffer; |
| 96 | + break; |
| 97 | + ... |
| 98 | +}; |
| 99 | +``` |
| 100 | +
|
| 101 | +接下来是顶点着色器。在顶点着色器中你声明属性,例如: |
| 102 | +
|
| 103 | +```glsl |
| 104 | +#version 300 es |
| 105 | +in vec4 position; |
| 106 | +in vec2 texcoord; |
| 107 | +in vec3 normal; |
| 108 | + |
| 109 | +... |
| 110 | + |
| 111 | +void main() { |
| 112 | + ... |
| 113 | +} |
| 114 | +``` |
| 115 | +
|
| 116 | +当你使用 `gl.linkProgram(someProgram)` 将顶点着色器和片段着色器链接时, |
| 117 | +WebGL(驱动/GPU/浏览器)会自行决定每个属性使用哪个索引(location)。 |
| 118 | +除非你手动分配位置(见下文),否则你无法预知它们会选哪个索引。 |
| 119 | +因此你需要通过 `gl.getAttribLocation` 查询它们: |
| 120 | +
|
| 121 | +```js |
| 122 | +const positionLoc = gl.getAttribLocation(program, 'position'); |
| 123 | +const texcoordLoc = gl.getAttribLocation(program, 'texcoord'); |
| 124 | +const normalLoc = gl.getAttribLocation(program, 'normal'); |
| 125 | +``` |
| 126 | +
|
| 127 | +假设 `positionLoc` = `5`,意味着在执行顶点着色器时(即调用 `gl.drawArrays` 或 `gl.drawElements`), |
| 128 | +WebGL 期待你已经为 attribute 5 设置好了正确的 `type`、`size`、`offset`、`stride`、`buffer` 等。 |
| 129 | +
|
| 130 | +注意:在调用 `gl.linkProgram` *之前*,你可以使用`gl.bindAttribLocation(program, location, nameOfAttribute) `指定位置,例如: |
| 131 | +
|
| 132 | +```js |
| 133 | +// Tell `gl.linkProgram` to assign `position` to use attribute #7 |
| 134 | +gl.bindAttribLocation(program, 7, 'position'); |
| 135 | +``` |
| 136 | +
|
| 137 | +如果使用的是GLSL ES 3.00着色器,您也可以直接在着色器中指定要使用的location位置,例如: |
| 138 | +
|
| 139 | +```glsl |
| 140 | +layout(location = 0) in vec4 position; |
| 141 | +layout(location = 1) in vec2 texcoord; |
| 142 | +layout(location = 2) in vec3 normal; |
| 143 | + |
| 144 | +... |
| 145 | +``` |
| 146 | +
|
| 147 | +使用 `bindAttribLocation` 看起来更符合 [D.R.Y. 原则](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), |
| 148 | +不过你可以根据个人偏好选择不同的方式。 |
| 149 | +
|
| 150 | +## 完整的属性状态 |
| 151 | +
|
| 152 | +上面的描述中省略了一点:每个 attribute 实际上都有一个默认值。 |
| 153 | +这在实际应用中较少使用,所以之前没有提及。 |
| 154 | +
|
| 155 | +```js |
| 156 | +attributeValues: [ |
| 157 | + [0, 0, 0, 1], |
| 158 | + [0, 0, 0, 1], |
| 159 | + ... |
| 160 | +], |
| 161 | +vertexArray: { |
| 162 | + attributes: [ |
| 163 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, |
| 164 | + divisor: 0, }, |
| 165 | + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, |
| 166 | + divisor: 0, }, |
| 167 | + ... |
| 168 | +``` |
| 169 | +
|
| 170 | +你可以通过一系列 `gl.vertexAttribXXX` 函数设置每个 attribute 的值。 |
| 171 | +当 `enable` 为 `false` 时,会使用这些值;当 `enable` 为 `true`,则会从分配的 `缓冲区buffer` 中读取数据。 |
| 172 | +
|
| 173 | +<a id="vaos"></a> |
| 174 | +## 顶点数组对象(VAO) |
| 175 | +
|
| 176 | +```js |
| 177 | +const vao = gl.createVertexArray(); |
| 178 | +``` |
| 179 | +
|
| 180 | +这会创建一个如上伪代码中 `gl.vertexArray` 所示的对象。 |
| 181 | +调用 `gl.bindVertexArray(vao)` 将你创建的 VAO 设为当前 VAO: |
| 182 | +
|
| 183 | +```js |
| 184 | +// pseudo code |
| 185 | +gl.bindVertexArray = function(vao) { |
| 186 | + gl.vertexArray = vao ? vao : defaultVAO; |
| 187 | +}; |
| 188 | +``` |
| 189 | +
|
| 190 | +这样你就可以在当前 VAO 中设置所有 `attributes` 和 `ELEMENT_ARRAY_BUFFER`。 |
| 191 | +当你要绘制某个图形时,只需调用一次 `gl.bindVertexArray` 即可设置所有属性。 |
| 192 | +否则你可能需要为每个属性分别调用 `gl.bindBuffer`、`gl.vertexAttribPointer`、`gl.enableVertexAttribArray`。 |
| 193 | +
|
| 194 | +由此可见,使用 VAO 是很有价值的。 |
| 195 | +不过,要正确使用 VAO 需要更好的组织结构。 |
| 196 | +
|
| 197 | +举个例子,假设你想用 `gl.TRIANGLES` 和一个着色器绘制一个立方体, |
| 198 | +再用 `gl.LINES` 和另一个着色器重新绘制它。 |
| 199 | +
|
| 200 | +假设用三角形绘制时要做光照处理,因此顶点着色器声明了这些属性: |
| 201 | +
|
| 202 | +```glsl |
| 203 | +#version 300 es |
| 204 | +// lighting-shader |
| 205 | +// 用于绘制三角形的着色器 |
| 206 | + |
| 207 | +in vec4 a_position; |
| 208 | +in vec3 a_normal; |
| 209 | +``` |
| 210 | +
|
| 211 | +然后使用这些位置和法线向我在[第一篇光照文章](webgl-3d-lighting-directional.html)中做的那样。 |
| 212 | +
|
| 213 | +对于不需要光照的线条,您需要使用纯色,可以参照本教程系列[第一页](webgl-fundamentals.html)中的基础着色器实现方式。 |
| 214 | +声明一个uniform的颜色变量。 这意味着在顶点着色器中只需处理位置数据即可 |
| 215 | +
|
| 216 | +```glsl |
| 217 | +#version 300 es |
| 218 | +// solid-shader |
| 219 | +// shader for cube with lines |
| 220 | + |
| 221 | +in vec4 a_position; |
| 222 | +``` |
| 223 | +
|
| 224 | +我们并不知道 WebGL 为每个 shader 分配的 attribute 位置是多少。 |
| 225 | +假设 lighting-shader 的分配结果是: |
| 226 | +
|
| 227 | +``` |
| 228 | +a_position location = 1 |
| 229 | +a_normal location = 0 |
| 230 | +``` |
| 231 | +
|
| 232 | +solid-shader只有一个attribute属性。 |
| 233 | +
|
| 234 | +``` |
| 235 | +a_position location = 0 |
| 236 | +``` |
| 237 | +
|
| 238 | +显然,在切换着色器时需要重新设置属性。 |
| 239 | +一个着色器期望 `a_position` 的数据出现在attribute 0,另一个着色器期望它出现在attribute 1。 |
| 240 | +
|
| 241 | +重新设置属性是一件麻烦事。更糟的是,使用 VAO 的初衷就是避免这些重复操作。 |
| 242 | +为了解决这个问题,我们需要在链接程序之前使用 bindAttribLocation 显式指定位置: |
| 243 | +
|
| 244 | +重新设置属性是一件麻烦事。更糟的是,使用 VAO 的初衷就是避免这些重复操作。 |
| 245 | +为了解决这个问题,我们需要在链接程序之前使用 `bindAttribLocation` 显式指定位置。 |
| 246 | +
|
| 247 | +我们告诉 WebGL: |
| 248 | +
|
| 249 | +```js |
| 250 | +gl.bindAttribLocation(solidProgram, 0, 'a_position'); |
| 251 | +gl.bindAttribLocation(lightingProgram, 0, 'a_position'); |
| 252 | +gl.bindAttribLocation(lightingProgram, 1, 'a_normal'); |
| 253 | +``` |
| 254 | +
|
| 255 | +务必在**调用 `gl.linkProgram` 之前**执行以上操作。 |
| 256 | +这样 WebGL 在链接着色器时就会使用我们指定的位置。 |
| 257 | +现在这两个着色器就可以使用相同的 `VAO`。 |
| 258 | +
|
| 259 | +## 最大属性数量 |
| 260 | +
|
| 261 | +WebGL2 要求至少支持 16 个 attribute,但具体设备 / 浏览器 / 驱动可能支持更多。 |
| 262 | +你可以通过下面的方式获取实际支持数量: |
| 263 | +
|
| 264 | +```js |
| 265 | +const maxAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); |
| 266 | +``` |
| 267 | +
|
| 268 | +如果你打算使用超过 16 个 attributes,建议检查支持数量, |
| 269 | +并在设备不足时提示用户,或者降级使用更简单的着色器。 |
0 commit comments