Lab: Triangles

How to 3D

Chapter 2: Triangles and Transformations

Lab: Triangles

In this chapter, you've been learning how to model objects as a collection of triangles that can be transformed. Let's apply this knowledge by writing a renderer that contorts a polygon into several different poses.

Form a group of no more than three people. Host, open your top-level repository directory in Visual Studio Code and run npm run dev to start up a web server. Then open the command palette via View / Command Palette and enter Live Share: Start Collaboration Session. Share the link with your group.

Transfit

Your first challenge is to transform a shape into a set of target shapes by transforming it through uniforms. Complete these steps:

Clone an old renderer to make a new renderer named transfit.
Remove any HTML input elements. This exercise has no controls.
Create a function named initializeOutlineVaos. Create six VAOs that will outline six horse-like shapes using these arrays for the positions:
const outlinePositions = [
  [-2, -2, 0, -1, -2, 0, -1, -1, 0, 0, -1, 0, 0, -2, 0, 1, -2, 0, 1,  0, 0, 2,  0, 0, 2,  1, 0, 1,  1, 0, 1,  2, 0, 0,  2, 0, 0,  0, 0, -2,  0, 0],
  [3, 4, 0, 1.5, 4, 0, 1.5, 5, 0, 0, 5, 0, 0, 4, 0, -1.5, 4, 0, -1.5, 6, 0, -3, 6, 0, -3, 7, 0, -1.5, 7, 0, -1.5, 8, 0, 0, 8, 0, 0, 6, 0, 3, 6, 0],
  [3.2679492235183716, -0.7320507764816284, 0.5, 4.133974611759186, -1.2320507764816284, 0.5, 4.633974611759186, -0.3660253882408142, 0.5, 5.5, -0.8660253882408142, 0.5, 5, -1.7320507764816284, 0.5, 5.866025388240814, -2.2320507764816284, 0.5, 6.866025388240814, -0.5, 0.5, 7.732050776481628, -1, 0.5, 8.232050776481628, -0.1339746117591858, 0.5, 7.366025388240814, 0.3660253882408142, 0.5, 7.866025388240814, 1.2320507764816284, 0.5, 7, 1.7320507764816284, 0.5, 6, 0, 0.5, 4.267949223518372, 1, 0.5],
  [4, 5, 0.5, 4, 5.5, 0.5, 5, 5.5, 0.5, 5, 6, 0.5, 4, 6, 0.5, 4, 6.5, 0.5, 6, 6.5, 0.5, 6, 7, 0.5, 7, 7, 0.5, 7, 6.5, 0.5, 8, 6.5, 0.5, 8, 6, 0.5, 6, 6, 0.5, 6, 5, 0.5],
  [14.732050776481628, 4.901923894882202, 0, 14.232050776481628, 6.200961947441101, 0, 13.366025388240814, 5.450961947441101, 0, 12.866025388240814, 6.75, 0, 13.732050776481628, 7.5, 0, 13.232050776481628, 8.799038052558899, 0, 11.5, 7.299038052558899, 0, 11, 8.598076105117798, 0, 10.133974611759186, 7.848076105117798, 0, 10.633974611759186, 6.549038052558899, 0, 9.767949223518372, 5.799038052558899, 0, 10.267949223518372, 4.5, 0, 12, 6, 0, 13, 3.401923894882202, 0],
  [10, 2.9999999999999996, 0, 11, 3, 0, 11, 1.4999999999999998, 0, 12, 1.5, 0, 12, 3, 0, 13, 3, 0, 13, 1.8369702788777518e-16, 0, 14, 3.6739405577555036e-16, 0, 14, -1.4999999999999996, 0, 13, -1.4999999999999998, 0, 13, -3, 0, 12, -3, 0, 12, 0, 0, 10, 0, 0]
];
const outlinePositions = [
  [-2, -2, 0, -1, -2, 0, -1, -1, 0, 0, -1, 0, 0, -2, 0, 1, -2, 0, 1,  0, 0, 2,  0, 0, 2,  1, 0, 1,  1, 0, 1,  2, 0, 0,  2, 0, 0,  0, 0, -2,  0, 0],
  [3, 4, 0, 1.5, 4, 0, 1.5, 5, 0, 0, 5, 0, 0, 4, 0, -1.5, 4, 0, -1.5, 6, 0, -3, 6, 0, -3, 7, 0, -1.5, 7, 0, -1.5, 8, 0, 0, 8, 0, 0, 6, 0, 3, 6, 0],
  [3.2679492235183716, -0.7320507764816284, 0.5, 4.133974611759186, -1.2320507764816284, 0.5, 4.633974611759186, -0.3660253882408142, 0.5, 5.5, -0.8660253882408142, 0.5, 5, -1.7320507764816284, 0.5, 5.866025388240814, -2.2320507764816284, 0.5, 6.866025388240814, -0.5, 0.5, 7.732050776481628, -1, 0.5, 8.232050776481628, -0.1339746117591858, 0.5, 7.366025388240814, 0.3660253882408142, 0.5, 7.866025388240814, 1.2320507764816284, 0.5, 7, 1.7320507764816284, 0.5, 6, 0, 0.5, 4.267949223518372, 1, 0.5],
  [4, 5, 0.5, 4, 5.5, 0.5, 5, 5.5, 0.5, 5, 6, 0.5, 4, 6, 0.5, 4, 6.5, 0.5, 6, 6.5, 0.5, 6, 7, 0.5, 7, 7, 0.5, 7, 6.5, 0.5, 8, 6.5, 0.5, 8, 6, 0.5, 6, 6, 0.5, 6, 5, 0.5],
  [14.732050776481628, 4.901923894882202, 0, 14.232050776481628, 6.200961947441101, 0, 13.366025388240814, 5.450961947441101, 0, 12.866025388240814, 6.75, 0, 13.732050776481628, 7.5, 0, 13.232050776481628, 8.799038052558899, 0, 11.5, 7.299038052558899, 0, 11, 8.598076105117798, 0, 10.133974611759186, 7.848076105117798, 0, 10.633974611759186, 6.549038052558899, 0, 9.767949223518372, 5.799038052558899, 0, 10.267949223518372, 4.5, 0, 12, 6, 0, 13, 3.401923894882202, 0],
  [10, 2.9999999999999996, 0, 11, 3, 0, 11, 1.4999999999999998, 0, 12, 1.5, 0, 12, 3, 0, 13, 3, 0, 13, 1.8369702788777518e-16, 0, 14, 3.6739405577555036e-16, 0, 14, -1.4999999999999996, 0, 13, -1.4999999999999998, 0, 13, -3, 0, 12, -3, 0, 12, 0, 0, 10, 0, 0]
];
Render the six outlines as line loops.
We've got to deal with the viewport distortion problem. WebGL only renders a unit cube, but the browser viewport might not be square. Replace your resizeCanvas function with these two functions:
function resizeCanvas() {
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  const aspectRatio = canvas.clientWidth / canvas.clientHeight;
  const size = 7;
  const center = [6, 3];
  if (aspectRatio >= 1) {
    clipFromWorld = ortho(center[0] - size * aspectRatio, center[0] + size * aspectRatio, center[1] - size, center[1] + size, -1, 1);
  } else {
    clipFromWorld = ortho(center[0] - size, center[0] + size, center[1] - size / aspectRatio, center[1] + size / aspectRatio, -1, 1);
  }
  render();
}

function ortho(left: number, right: number, bottom: number, top: number, near: number = -1, far: number = 1) {
  return new Float32Array([
    2 / (right - left), 0, 0, 0,
    0, 2 / (top - bottom), 0, 0,
    0, 0, 2 / (near - far), 0,
    -(right + left) / (right - left),
    -(top + bottom) / (top - bottom),
    (near + far) / (near - far),
    1,
  ]);
}
function resizeCanvas() {
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  const aspectRatio = canvas.clientWidth / canvas.clientHeight;
  const size = 7;
  const center = [6, 3];
  if (aspectRatio >= 1) {
    clipFromWorld = ortho(center[0] - size * aspectRatio, center[0] + size * aspectRatio, center[1] - size, center[1] + size, -1, 1);
  } else {
    clipFromWorld = ortho(center[0] - size, center[0] + size, center[1] - size / aspectRatio, center[1] + size / aspectRatio, -1, 1);
  }
  render();
}

function ortho(left: number, right: number, bottom: number, top: number, near: number = -1, far: number = 1) {
  return new Float32Array([
    2 / (right - left), 0, 0, 0,
    0, 2 / (top - bottom), 0, 0,
    0, 0, 2 / (near - far), 0,
    -(right + left) / (right - left),
    -(top + bottom) / (top - bottom),
    (near + far) / (near - far),
    1,
  ]);
}
This code creates what's called an orthographic projection matrix. We'll learn more about this matrix in a couple of chapters. Change your vertex shader to apply this matrix like so:
uniform mat4 clipFromWorld;

in vec3 position;

void main() {
  gl_Position = clipFromWorld * vec4(position, 1.0);
}
uniform mat4 clipFromWorld;

in vec3 position;

void main() {
  gl_Position = clipFromWorld * vec4(position, 1.0);
}
Finally, upload the matrix as a uniform in render:
shaderProgram.setUniformMatrix4fv('clipFromWorld', clipFromWorld);
shaderProgram.setUniformMatrix4fv('clipFromWorld', clipFromWorld);
You should see your six horses centered in the viewport. The one on the bottom-left should look like it's made of squares—with no distortion.
Add an rgb uniform. Use the uniform instead of hardcoding the color in the fragment shader. Render all six outlines in black.
Create a function named initializeFillVao and create geometry for a triangulated horse. Use these positions:
[
  -2.0, -2.0, 0, // vertex 0
  -1.0, -2.0, 0, // vertex 1
  -1.0, -1.0, 0, // vertex 2
   0.0, -1.0, 0, // vertex 3
   0.0, -2.0, 0, // vertex 4
   1.0, -2.0, 0, // vertex 5
   1.0,  0.0, 0, // vertex 6
   2.0,  0.0, 0, // vertex 7
   2.0,  1.0, 0, // vertex 8
   1.0,  1.0, 0, // vertex 9
   1.0,  2.0, 0, // vertex 10
   0.0,  2.0, 0, // vertex 11
   0.0,  0.0, 0, // vertex 12
  -2.0,  0.0, 0  // vertex 13
]
[
  -2.0, -2.0, 0, // vertex 0
  -1.0, -2.0, 0, // vertex 1
  -1.0, -1.0, 0, // vertex 2
   0.0, -1.0, 0, // vertex 3
   0.0, -2.0, 0, // vertex 4
   1.0, -2.0, 0, // vertex 5
   1.0,  0.0, 0, // vertex 6
   2.0,  0.0, 0, // vertex 7
   2.0,  1.0, 0, // vertex 8
   1.0,  1.0, 0, // vertex 9
   1.0,  2.0, 0, // vertex 10
   0.0,  2.0, 0, // vertex 11
   0.0,  0.0, 0, // vertex 12
  -2.0,  0.0, 0  // vertex 13
]
Here are the same vertices labeled by their index:
Triangulate the horse in some fashion, and add the resulting indices to the vertex attributes.
Render the horse as filled triangles. Use a color different from the outlines, and render the filled horse behind the outlines.
Edit the vertex shader to include uniforms radians, offsets, and factors.
Rotate, scale, and translate the position property in the shader using the uniforms. Apply the rotation first, the scale second, and the translation third. The order matters. Before any transformations, the origin is at the center of the horsey's bounding box, where the neck meets the back.
Draw the filled horse five more times to fit the target outlines. Use the same filled VAO and shader; change only the uniforms.

Blender Model

Making models by hand is not sustainable. We need to develop some 3D modeling skills. Find some tutorials and make some interesting object with fewer than 100 triangles. Learn about extrusion and loop cuts. Focus only on the geometry, not color or texture.

Submission

To receive credit for your lab work, follow these steps:

Non-host, download the transfit folders before the host closes the session.
Share a screenshot of your renderer and a screenshot of your Blender model in a post in the #lab channel in Discord. Tag your group members with @.
Push your code to your individual GitHub repository that your instructor made for you.

Only labs submitted on time will be granted credit. Late labs or forgot-to-submits are not accepted because Monday at noon is when your instructor has time to grade.

← Lecture: Triangles