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) 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 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 hello-cornflower project, create file src/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: number[], 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, new Float32Array(floats), usage);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  update(floats: number[]) {
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(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: number[], 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: number[], 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, new Uint32Array(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: number[], 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, new Float32Array(floats), usage);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  update(floats: number[]) {
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(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: number[], 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: number[], 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, new Uint32Array(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. You'll deal directly with VertexAttributes in this course, but not VertexAttribute.

To create a new VBO with two vertices, each having a position and color, you create an instance of VertexAttributes like this:

const positions = [
  0.0, 0.0, 0,     // vertex 0
  0.5, 0.5, 0,     // vertex 1
];

const colors = [
  1, 0, 0,         // vertex 0
  0, 0, 1,         // vertex 1
];

const attributes = new VertexAttributes();
attributes.addAttribute('position', 2, 3, positions);
attributes.addAttribute('color', 2, 3, colors);
const positions = [
  0.0, 0.0, 0,     // vertex 0
  0.5, 0.5, 0,     // vertex 1
];

const colors = [
  1, 0, 0,         // vertex 0
  0, 0, 1,         // vertex 1
];

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 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.

Add this 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. You'll also need to import the class at the top of the script with this line:

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

Import statements are always needed when you 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.

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

← Verbs and NounsShader Program →