First Shape
This post is part of a course on geometric modeling at the Summer Liberal Arts Institute for Computer Science held at Carleton College in 2021.
Throughout this course, we will be making 3D shapes with code and displaying them. Probably we should start with a simple shape. What’s the simplest shape you can think of? There are several reasonable answers to this question. You might be thinking of a cube. How many faces does a cube have? How many vertices? How many edges?
Even simpler than a cube is a square. It’s not 3D, but it’s still a shape. It has 1 face, 4 vertices, and 4 edges.
Even simpler than a square is a triangle. It’s got 1 face, 3 vertices, and 3 edges.
Could we go any simpler? With just 2 vertices, we get a line segment. With just 1 vertex, we get a point. Neither of these really feels like a shape. Let’s call triangle the winner and make this simplest of shapes.
ASCII Triangle
Let’s draw a triangle first using ASCII art. In ASCII art, we place letters, numbers, and punctuation in a grid so that they look like something. We need a place to write the code that draws these symbols. Create a file named render.js
in your project and enter this text to plot a triangle:
console.log(" * ");
console.log(" / \ ");
console.log("*---*");
In index.html
, change the <body>
element to look like this:
<body>
🥨
<script src="render.js"></script>
</body>
Save both files. If you are using the CS50 IDE, click the Pop Out Into New Window button on the top right. If you are using Visual Studio Code, reload the page in your browser. Do you see your triangle? Probably not. That’s because console.log
isn’t meant for putting content in a web page. Rather, it puts messages in your browser’s developer console. You need to find a way to open this developer console. Every browser has a slightly different way. Often you can right-click on the page, click Inspect, and find the tab labeled Console. Try that.
Do you see the triangle now? If your browser is like mine, you’re probably seeing something strange. Two of the three edges are present, but not the third. That’s because the backslash character has special meaning inside double quotation marks. If you want a normal backslash to appear, you need to put two of them:
console.log(" * ");
console.log(" / \\");
console.log("*---*");
Save and reload. You should now see a complete triangle.
Pixel Triangle
Usually we build images out of pixels, not ASCII characters. The process of turning a shape into a pixelated image is called rendering. To render a triangle and all of our more complex shapes later on, we will make heavy use of THREE.js, a library of code written in JavaScript by others for rendering 3D scenes. You have already linked in the THREE.js library with the two <script>
elements in <head>
in the HTML file you created while setting up your project.
We first set up a scene. Replace the console.log
instructions in render.js
with these lines:
// Create a rendering canvas that fills the window.
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Create a scene to hold all our shapes.
const scene = new THREE.Scene();
// Create a camera sitting at (0, 0, 15) and looking at (0, 0, 0).
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 15;
// Populate the scene with shapes.
// TODO
// Render the scene.
renderer.render(scene, camera);
In a moment, you’ll replace TODO
with more commands.
When you reload the page, you will see a black rectangle. By default, THREE.js fills the canvas with a black background color. You will see a white border around the canvas. The browser has added a margin around the <body>
element. Let’s get rid of it. Create a new file named style.css
and add this text:
body {
margin: 0;
}
This is a stylesheet, a file that holds the visual styling information for a webpage. Apply the stylesheet to index.html
by adding this element somewhere within <head>
:
<link href="style.css" rel="stylesheet" type="text/css">
Also remove the pretzel code (🥨
) from index.html
. With the margin and the pretzel gone, you should the black canvas fill the entire viewport of the browser.
Let’s fill that empty canvas with an orange triangle. Our first step is to create a list of all the xyz-coordinates for the triangle’s vertices. Make a right-triangle whose hypotenuse passes through the origin with this code:
const positions = [
-1, -1, 0,
1, -1, 0,
-1, 1, 0,
];
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
Replace the // TODO
line with this code.
The second step is to create a material that describes the triangle’s appearance. THREE.js provides a number of different materials. We start with the basic material that assigns a uniform color to every pixel within a shape:
const material = new THREE.MeshBasicMaterial({
color: 'orange',
});
The final step is combine the geometry and the material together into a mesh and add the mesh to the scene:
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
You should now see an orange triangle in your browser. There’s not much you can do with it, but there are some things:
- Try changing the values in the
positions
array. - Change the camera’s z-coordinate.
- Change the material’s color.
Currently you have to change the code to alter the rendering. It’d be nice if some things could be animated or changed interactively.
Rotate
Let’s make the triangle spin on its own. One way to rotate a mesh is to assign its rotation around a particular axis directly, like this:
mesh.rotation.y = 45.0;
Try adding that line to your code.
This indeed changes the rotate, but the triangle is still frozen in time. To make it spin continuously, we must add a routine that grows the angle of rotation. This routine must run constantly to make the scene feel alive. This animate
function rotates the triangle a wee bit and also schedules it to be called again in the near future:
function animate() {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
This is feeling alive, but it would feel even more alive if the user could interact with the triangle using the mouse.
Trackball
To allow the user to rotate or zoom in the browser, we add trackball controls. In arcade games, a trackball is actual hardware; it’s a ball embedded in the cabinet. The player spins the trackball to move the character. In THREE.js, the trackball is an imaginary (and therefore invisible) ball that fills the browser window. The user spins it by clicking and dragging on the window with the mouse.
Create a trackball and update the camera on each frame with the following code:
const controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 2;
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
Replace the previous rotation code and definition of animate
with this new code.
Try clicking and dragging. The triangle should rotate. What happens when you spin it more than 90 degrees?
Try zooming in and out by scrolling. What happens you zoom out a lot? When you zoom in a lot?
Challenge
Now it’s your turn to do some thinking on your own. Adjust your renderer to produce an image that looks like this:
The positions
array can hold the coordinates of many triangles, like this:
const positions = [
// First triangle
x0, y0, z0, // vertex 0
x1, y1, z1, // vertex 1
x2, y2, z2, // vertex 2
// Second triangle
x3, y3, z3, // vertex 3
x4, y4, z4, // vertex 4
x5, y5, z5, // vertex 5
// More triangles and vertices...
];
Notice how the positions
array is flat rather than an array of arrays.
Just a heads-up. The positions must be listed in counter-clockwise order to be visible. If they are not visible when you first load the page, try rotating the shape and looking at the back side. If the triangles suddenly appear, then you have ordered them clockwise.
You will probably run into issues. Your instructor wants to help you work through them. Consider also browing the THREE.js documentation.