VertexAttributes

How to 3D

Chapter 1: Points and Lines

VertexAttributes

Recall that vertex properties are the locations, color, and other data that are measured at each individual vertex in a model. You must migrate the attributes from arrays in RAM to a vertex buffer object (VBO) in VRAM and managed by the graphics driver. WebGL provides functions to do this: createBuffer, bindBuffer, bufferData, bufferSubData, and deleteBuffer. These functions are similar to malloc, memcpy, and free in C.

Many graphics engines provide a reusable VBO abstraction so that the high-level code doesn't get bogged down in low-level technical details. We do the same here so that you can get a renderer up and running quickly. In your project, create file lib/vertex-attributes.ts and copy in this code:

export class VertexAttribute {
  name: string;
  nvertices: number;
  ncomponents: number;
  buffer: WebGLBuffer;
 
  constructor(name: string, nvertices: number, ncomponents: number, floats: Float32Array, usage: number = gl.STATIC_DRAW) {
    this.name = name;
    this.nvertices = nvertices;
    this.ncomponents = ncomponents;

    this.buffer = gl.createBuffer()!;
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
    gl.bufferData(gl.ARRAY_BUFFER, floats, usage);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  update(floats: Float32Array) {
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, floats);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  destroy() {
    gl.deleteBuffer(this.buffer);
  }
}

export class VertexAttributes {
  nvertices: number;
  indexBuffer: WebGLBuffer | null;
  attributes: VertexAttribute[];
  indexCount: number;

  constructor() {
    this.nvertices = -1;
    this.indexBuffer = null;
    this.indexCount = 0;
    this.attributes = [];
  }

  addAttribute(name: string, nvertices: number, ncomponents: number, floats: Float32Array, usage: number = gl.STATIC_DRAW) {
    if (this.nvertices >= 0 && nvertices != this.nvertices) {
      throw "Attributes must have same number of vertices.";
    }

    this.nvertices = nvertices;
    let attribute = new VertexAttribute(name, nvertices, ncomponents, floats, usage);
    this.attributes.push(attribute);

    return attribute;
  }

  addIndices(ints: Uint32Array, usage: number = gl.STATIC_DRAW) {
    this.indexCount = ints.length;
    this.indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, ints, usage);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
  }

  destroy() {
    for (let attribute of this.attributes) {
      attribute.destroy();
    }

    if (this.indexBuffer) {
      gl.deleteBuffer(this.indexBuffer);
    }
  }

  [Symbol.iterator]() {
    return this.attributes.values();
  }

  get vertexCount(): number {
    return this.nvertices;
  }
}
export class VertexAttribute {
  name: string;
  nvertices: number;
  ncomponents: number;
  buffer: WebGLBuffer;
 
  constructor(name: string, nvertices: number, ncomponents: number, floats: Float32Array, usage: number = gl.STATIC_DRAW) {
    this.name = name;
    this.nvertices = nvertices;
    this.ncomponents = ncomponents;

    this.buffer = gl.createBuffer()!;
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
    gl.bufferData(gl.ARRAY_BUFFER, floats, usage);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  update(floats: Float32Array) {
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, floats);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  destroy() {
    gl.deleteBuffer(this.buffer);
  }
}

export class VertexAttributes {
  nvertices: number;
  indexBuffer: WebGLBuffer | null;
  attributes: VertexAttribute[];
  indexCount: number;

  constructor() {
    this.nvertices = -1;
    this.indexBuffer = null;
    this.indexCount = 0;
    this.attributes = [];
  }

  addAttribute(name: string, nvertices: number, ncomponents: number, floats: Float32Array, usage: number = gl.STATIC_DRAW) {
    if (this.nvertices >= 0 && nvertices != this.nvertices) {
      throw "Attributes must have same number of vertices.";
    }

    this.nvertices = nvertices;
    let attribute = new VertexAttribute(name, nvertices, ncomponents, floats, usage);
    this.attributes.push(attribute);

    return attribute;
  }

  addIndices(ints: Uint32Array, usage: number = gl.STATIC_DRAW) {
    this.indexCount = ints.length;
    this.indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, ints, usage);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
  }

  destroy() {
    for (let attribute of this.attributes) {
      attribute.destroy();
    }

    if (this.indexBuffer) {
      gl.deleteBuffer(this.indexBuffer);
    }
  }

  [Symbol.iterator]() {
    return this.attributes.values();
  }

  get vertexCount(): number {
    return this.nvertices;
  }
}

The VertexAttribute class manages a vertex buffer for a single property, like position or color. The VertexAttributes class manages a bundle of properties as well as an optional index buffer. Most of your dealings will be with VertexAttributes in this course, but not VertexAttribute.

To create a new VBO with two vertices, each having a position and color, we first make buffers holding the raw data:

const positions = new Float32Array([
  0.0, 0.0, 0,     // vertex 0 is at the origin
  0.5, 0.5, 0,     // vertex 1 is northeast
]);

const colors = new Float32Array([
  1, 0, 0,         // vertex 0 is red
  0, 0, 1,         // vertex 1 is blue
]);
const positions = new Float32Array([
  0.0, 0.0, 0,     // vertex 0 is at the origin
  0.5, 0.5, 0,     // vertex 1 is northeast
]);

const colors = new Float32Array([
  1, 0, 0,         // vertex 0 is red
  0, 0, 1,         // vertex 1 is blue
]);

Plain JavaScript arrays are wild. They can contain data of any type and can change size. WebGL needs to run predictably and quickly and therefore doesn't accept plain arrays. However, it will accept a typed array, which is a compact buffer of binary data. Float32Array holds 4-byte floats, Uint32Array 4-byte integers, and Uint16Array 2-byte integers.

From these typed arrays we create and populate an instance of VertexAttributes like this:

const attributes = new VertexAttributes();
attributes.addAttribute('position', 2, 3, positions);
attributes.addAttribute('color', 2, 3, colors);
const attributes = new VertexAttributes();
attributes.addAttribute('position', 2, 3, positions);
attributes.addAttribute('color', 2, 3, colors);

The strings position and color are human-friendly names for the properties. The 2 in the addAttribute calls means there are two vertices, and the 3 means each vertex attribute has three components. For the position, these are the x-, y-, and z-coordinates. For the color, these are the red, green, and blue intensities. We must pass these numbers because the array is flat; it has no explicit structure separating one vertex's data from another.

Add these two snippets of code to the initialize function in main.ts under the comment about initializing graphics state. In general, the global gl must be assigned before calling any WebGL functions. We also need to import the class at the top of the script with this line:

import { VertexAttributes } from 'lib/vertex-attributes.js';
import { VertexAttributes } from 'lib/vertex-attributes.js';

Import statements are always needed when we reference types or functions from other files. From this point forward, we won't explicitly mention them, but you will still need to add them. Note that the extension is .js. The TypeScript compiler turns TypeScript into JavaScript but by design avoids altering the behavior of the program. One thing it doesn't do is modify paths.

This code puts the model of a line segment in VRAM. But it doesn't draw anything. Before we can draw, we need some shader programs.

← Verbs and NounsShader Program →