Lab: Dotplots

How to 3D

Chapter 1: Points and Lines

Lab: Dotplots

In this chapter, you've been learning how to model objects as a collection of points and lines. Let's apply this knowledge by writing two renderers, one to plot a circle and one to plot a more interesting curve.

Circle

Your first challenge is to plot a circle. WebGL doesn't have a circle primitive, so the best you can do is plot a regular polygon with many sides. Follow these steps to create a circle regular polygon renderer.

Form a group of no more than three people.
Elect one group member to clone the lab repository, open the top-level repository directory in Visual Studio Code, open a terminal, move to the polydots folder, run npm install to download the dependencies, and run npm run start to start up a web server.
Open the command palette via View / Command Palette and enter Live Share: Start Collaboration Session. Share the link with all group members. Verify that all group members see the two dots.
Create a new function and name it generateCircle. You are going to factor out the vertex buffer and VAO creation to this separate function so that you can regenerate the circle without reinitializing the whole renderer.
Move the code in initialize that deals with positions, attributes, and vao to generateCircle. Leave the shader code in initialize because it won't change.
Just after the shader program is assigned in initialize, call generateCircle. You should see the two dots as before.
Make generateCircle accept parameters named n and radius.
Modify the positions array so that it contains n evenly spaced vertices on the perimeter of a circle of the given radius. You'll need to loop through the angles around the circle. A vertex's location can be computed by turning its angle and radius into Cartesian coordinates:
$$\begin{aligned} x &= \text{radius} \times \cos \theta \\ y &= \text{radius} \times \sin \theta \\ z &= 0 \end{aligned}$$
Call generateCircle with various parameters. Ensure that you can see circles of various sizes and roughnesses.
Add the following HTML just inside the opening body tag in index.html to give the user an interface for changing the sample rate and radius:
<div class="controls">
  <label for="n-input">n</label>
  <input id="n-input" type="text">
  <label for="radius-input">radius</label>
  <input id="radius-input" type="text">
</div>
<div class="controls">
  <label for="n-input">n</label>
  <input id="n-input" type="text">
  <label for="radius-input">radius</label>
  <input id="radius-input" type="text">
</div>
Add the following CSS at the bottom of style.css to position the controls atop the canvas:
.controls {
  position: absolute;
  top: 5px;
  right: 5px;
  display: grid;
  grid-template-columns: auto auto;
  grid-gap: 2px 5px;
  justify-items: end;
  z-index: 1;
}
.controls {
  position: absolute;
  top: 5px;
  right: 5px;
  display: grid;
  grid-template-columns: auto auto;
  grid-gap: 2px 5px;
  justify-items: end;
  z-index: 1;
}
Add the following global declarations to src/main.ts:
let nInput: HTMLInputElement;
let radiusInput: HTMLInputElement;
let nInput: HTMLInputElement;
let radiusInput: HTMLInputElement;
Make these globals refer to the page's text inputs by adding this code at the end of initialize:
// other getElementById calls...
nInput = document.getElementById('n-input') as HTMLInputElement;
radiusInput = document.getElementById('radius-input') as HTMLInputElement;

// ...

// other event listeners...
nInput.addEventListener('input', synchronize);
radiusInput.addEventListener('input', synchronize);
// other getElementById calls...
nInput = document.getElementById('n-input') as HTMLInputElement;
radiusInput = document.getElementById('radius-input') as HTMLInputElement;

// ...

// other event listeners...
nInput.addEventListener('input', synchronize);
radiusInput.addEventListener('input', synchronize);
Add this synchronize function, which will be called whenever a number in the text inputs changes:
function synchronize() {
  // Release any previous VAO and VBOs.
  vao?.destroy();
  attributes?.destroy();

  const n = parseInt(nInput.value);
  const radius = parseFloat(radiusInput.value);
  console.log(n, radius);
  // TODO: regenerate circle and redraw.
}
function synchronize() {
  // Release any previous VAO and VBOs.
  vao?.destroy();
  attributes?.destroy();

  const n = parseInt(nInput.value);
  const radius = parseFloat(radiusInput.value);
  console.log(n, radius);
  // TODO: regenerate circle and redraw.
}
Render the circle as a line loop instead of individual points.

Lissajous Curve

Your second challenge is to render Lissajous curves. Just like circles, they can be modeled through a set of parametric equations. Their coordinates are computed as functions of a variable \(t\):

$$\begin{aligned} x(t) &= a \times \sin(t) \\ y(t) &= b \times \sin(\text{ratio} \times t + \text{shift}) \\ z(t) &= 0 \end{aligned}$$

The domain of \(t\) is \([0, 2\pi]\). The position of the first vertex is \(\begin{bmatrix}x(0)&y(0)&z(0)\end{bmatrix}\). Complete the following steps to build a Lissajous renderer.

Copy over your index.html, style.css, and main.ts from the circle exercise to the lissadots folder. Ensure that you can render a circle before moving on.
Move in the terminal to the lissadots folder and start up a new server.
Rename generateCircle to generateLissajous.
Modify the input elements so there are five inputs with labels and IDs n, a, b, ratio, and shift.
Grab references to these five elements using getElementById.
Call synchronize whenever these inputs are modified.
Parse the values of these inputs in synchronize. Only parameter n is an integer. The others are reals.
Modify generateLissajous and its calls to generate Lissajous curves instead of a circle. The code will have a very similar structure to your circle generator. It just generates a different shape and relies on different free variables.

Submission

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

Elect one member of your group to record a short demo of your two renderers.
Show the parameters being modified and the shape being re-rendered.
Upload the video to a streaming service like YouTube or Vimeo.
Share a link to the video in a post on the #lab-gallery channel in Discord. Tag your group members.
Push your code to GitHub.

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

← Lecture: Dots