Hello Cornflower

How to 3D

Chapter 1: Points and Lines

Hello Cornflower

Every graphics concept that we discuss in this course will be made concrete in code. We need a place to write this code. Let's make that place right now as we write our first renderer. It will display a cornflower blue rectangle, like this:

Repository

Your instructor has created a single Git repository for your graphics code this semester. All code will be stored in this single repository. At this point, the repository is only on GitHub. You must clone it to your local computer by following these steps:

Visit the course GitHub organization using the link on Canvas.
Find your repository and copy its URL.
Open Visual Studio Code.
Select View / Command Palette.
Enter the command Git: Clone and paste in the URL. Then select a folder that will hold your cloned repository. Since the repository is empty, the clone will be empty.

You only need to clone your repository once. As the semester progresses, you will make changes to the folder you just made and push them back up to GitHub.

Global Configuration

You will place many renderers in your repository throughout the semester. Let's perform some global configuration now that will impact all of these future renderers.

Open the empty project root folder in Visual Studio Code. Always open this project root to work on your renderers. If you open a parent or child folder, you will encounter issues with Git and sharing.

Create a file named .gitignore and add these lines:

.DS_Store
node_modules
.DS_Store
node_modules

These are the names of files that should never be added to version control. The first is created by macOS to cache thumbnail versions of any image files. These thumbnails are shown on the file icons in your operating system's file explorer. The folder node_modules holds all the library code that your project depends on. It doesn't go in version control because it is big, and its contents can be downloaded at anytime from the Node.js package registry. By placing .gitignore in the project root, it will recursively apply to all subfolders.

Let's place in the root an HTML file that displays a table of contents of all your renderers. Paste this HTML in index.html:

HTML
<!DOCTYPE html>
<html>
<head>
  <title>CS 488</title>
</head>
<body>
  <h1>Renderers</h1>
  <ul>
    <li><a href="/apps/hello-cornflower/">hello-cornflower</a></li>
  </ul>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <title>CS 488</title>
</head>
<body>
  <h1>Renderers</h1>
  <ul>
    <li><a href="/apps/hello-cornflower/">hello-cornflower</a></li>
  </ul>
</body>
</html>

Each renderer we add will get its own list item and link.

To make the folder a Node.js project, it needs a configuration file named package.json. Create the package.json and add this configuration:

{
  "name": "cs488",
  "type": "module",
  "scripts": {
    "dev": "vite --open",
    "build": "tsc && vite build",
    "sandbox": "tsc && node build/sandbox.js"
  }
}
{
  "name": "cs488",
  "type": "module",
  "scripts": {
    "dev": "vite --open",
    "build": "tsc && vite build",
    "sandbox": "tsc && node build/sandbox.js"
  }
}

This file describes the project, lists its dependencies, and defines shortcuts for shell commands that we may want to run. This configuration provides three commands: dev, build, and sandbox. The dev command is the most important one for this course. It starts up a local web server and opens index.html file in a web browser. We'll run it soon, but first we must install some dependencies.

Our project depends on vite and tsc. The utility vite (pronounced VEET) is a bundler that packs the code, HTML, stylesheets, and images of our project into a standalone web page. The utility tsc is the TypeScript compiler that translates TypeScript into JavaScript. Install the dependencies by executing this command in the terminal:

npm install vite vite-plugin-checker typescript
npm install vite vite-plugin-checker typescript

Inspect package.json. See the dependencies that have been added to it? Notice the node_modules folder that has been created? It will be kept out of version control thanks to the .gitignore file. The file package-lock.json has also been created. It tracks the version numbers of the dependencies we are currently using, and it should go into version control to ensure our collaborators use the same versions.

We are ready to start a web server for the project. Run this command in the terminal:

npm run dev
npm run dev

The table of contents page should open automatically in the default web browser. The one link on that page doesn't go anywhere yet because we haven't made the renderer. Before we do that, the TypeScript compiler needs to be configured. Create tsconfig.json with this JSON configuration:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "nodenext",
    "skipLibCheck": true,
    "moduleResolution": "nodenext",
    "outDir": "./build",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "noFallthroughCasesInSwitch": true,
    "paths": {
      "lib/*": ["./lib/*"],
    }
  },
  "include": ["apps", "lib"]
}
{
  "compilerOptions": {
    "target": "es2020",
    "module": "nodenext",
    "skipLibCheck": true,
    "moduleResolution": "nodenext",
    "outDir": "./build",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "noFallthroughCasesInSwitch": true,
    "paths": {
      "lib/*": ["./lib/*"],
    }
  },
  "include": ["apps", "lib"]
}

You may see an error from Visual Studio Code saying the no TypeScript files were found. We don't have any yet. Let's add a very small one.

We're using TypeScript in this course to discover errors early. Every renderer we write we'll give a global variable named gl—which is short for graphics library. We need to declare the type of gl so the TypeScript compiler can check that we're calling its methods correctly. Create a folder named lib to hold the code shared by all our renderers. Create lib/globals.d.ts and paste in this type declaration:

interface Window {
  gl: WebGL2RenderingContext;
}
  
declare const gl: WebGL2RenderingContext;
interface Window {
  gl: WebGL2RenderingContext;
}
  
declare const gl: WebGL2RenderingContext;

Nothing amazing happens, but the error about no TypeScript files should go away.

By default, Vite only shows type errors from TypeScript when we fully build the project into a web app. It doesn't show errors when we are developing our code and testing it through a local web server—which is exactly when we want to see these errors. The vite-plugin-checker makes these errors visible in the browser as we write code. Enable this checker by creating file vite.config.js in the top-level project folder with this content:

JavaScript
import checker from 'vite-plugin-checker';

export default {
  plugins: [
    checker({
      typescript: true,
    }),
  ],
};
import checker from 'vite-plugin-checker';

export default {
  plugins: [
    checker({
      typescript: true,
    }),
  ],
};

Our global configuration is mostly complete. Later on we'll add more dependencies and configuration, but let's get our first renderer rendering.

To stay organized, we'll put each renderer that we make in its own folder, and we'll put all renderer folders in a folder called apps. Create the apps folder in the project root.

Renderer

Within apps, create a subfolder named hello-cornflower. In it, create a file named index.html and add this HTML code:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello, Cornflower</title>
    <link rel="icon" href="data:,">
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script type="module" src="main.ts"></script>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <title>Hello, Cornflower</title>
    <link rel="icon" href="data:,">
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script type="module" src="main.ts"></script>
  </body>
</html>

The canvas element is the rectangle that will get filled with pixels. The script element loads in the TypeScript code—which will be translated into JavaScript during the build process. The link tag loads in a stylesheet that alters the appearance of the page elements.

Click on the link to the hello-cornflower renderer in your web browser. The page is blank. In the developer console, you likely see errors. Both main.ts and style.css are missing. Let's add these files.

In the apps/hello-cornflower folder, create a file named main.ts and add this HTML code:

let canvas: HTMLCanvasElement;

async function initialize() {
  canvas = document.getElementById('canvas') as HTMLCanvasElement;
  window.gl = canvas.getContext('webgl2') as WebGL2RenderingContext;

  // Initialize other graphics state as needed.

  // Event listeners
  window.addEventListener('resize', () => resizeCanvas());

  resizeCanvas();  
}

function render() {
  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.clearColor(0.392, 0.584, 0.929, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
}

function resizeCanvas() {
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  render();
}

window.addEventListener('load', () => initialize());
let canvas: HTMLCanvasElement;

async function initialize() {
  canvas = document.getElementById('canvas') as HTMLCanvasElement;
  window.gl = canvas.getContext('webgl2') as WebGL2RenderingContext;

  // Initialize other graphics state as needed.

  // Event listeners
  window.addEventListener('resize', () => resizeCanvas());

  resizeCanvas();  
}

function render() {
  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.clearColor(0.392, 0.584, 0.929, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
}

function resizeCanvas() {
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  render();
}

window.addEventListener('load', () => initialize());

The initialize function in main.ts runs exactly once, as the application starts up. It grabs a reference to the page's canvas element and gets from it a WebGL context, an environment for executing WebGL commands. This context is stored as the gl object whose type we declared earlier.

The function render colors in the pixels of the framebuffer and runs periodically throughout the lifetime of the renderer. Currently render only clears the canvas to the color cornflower. The first three numbers passed to clearColor are the red, green, and blue intensities that mix to make cornflower. The fourth number is the color's opacity. A value of 1 means that the color is completely opaque, that we can't see anything behind it.

The resizeCanvas function is called whenever the window changes size. It resizes the canvas so that it matches the window and re-renders the scene.

After creating this file, the renderer should now show a cornflower box. You may need to reload the page to see it. Probably the box is small. We want our renderers to fill the browser window. We'll achieve that by adding a global stylesheet.

Create at the project root a folder named public to store static assets like images and stylesheets. Create file public/style.css with these styles:

body {
  margin: 0;
}

#canvas {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  height: 100vh;
}
body {
  margin: 0;
}

#canvas {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  height: 100vh;
}

This stylesheet removes the whitespace margin around the page's body element and makes the canvas fill the browser window. The hello-cornflower/index.html file we created earlier loads in this stylesheet using a link tag.

We've done it. Our first renderer is complete. And a little boring. Experiment by changing the clear color in main.ts. Try changing the opacity. Confirm that as soon as you save the file, Vite automatically reloads the page in the browser.

Synchronizing to GitHub

The files we've been editing are only in the local clone of your repository. We need to commit the changes and then push them up to GitHub. We should do this after every work session, even if our code is broken or our task isn't done. By commiting and pushing regularly, our work is backed up and accessible to others on the project, like our instructor.

Commit and push your code by following these steps.

Open the source control management panel in Visual Studio Code by selecting View / Source Control. The panel lists all the files that are new to the repository or that have changes since your last commit.
Hover your mouse over each file and click the + button to add the changed file to your commit. Or, to add all changes, hover over the the Changes heading and click the + button.
Add a short statement describing the changes in the message box. For example, we might write Add cornflower renderer or Fix missing triangles or Refactor particle system
Click the check button. The changes are committed only to your local repository.
Click Sync Changes. Or click the … icon and select Pull, Push / Push.

Visit your repository on github.com and make sure you can see the changes. If you can't see them, then your instructor can't either.

To create a new renderer, repeat only the steps in the Renderer section. The global configuration needs to be completed only once.

← Software SetupVerbs and Nouns →