3.4: Rendering an Empty Scene
- Download code
- 03-tracer.zip
- Live example
- examples/03-tracer/index.html
So, we have vectors, colors, rays, a scene, a camera… now we’re going to create the actual ray-tracer, hook that up to our HTML canvas, and render something.
We don’t have any shapes yet, so all we’ll be able to render is empty space, but we can control the background color of our scene so we should at least be able to check that the empty sky in our empty world is the right color.
Creating the renderer
Create a new file called modules/renderer.js
, that looks like this:
// modules/renderer.js
import { Color } from './color.js';
import { Vector } from './vector.js';
import { Camera } from './camera.js';
import { Scene } from './scene.js';
class Renderer {
#canvasWidth;
#canvasHeight;
constructor(canvasWidth, canvasHeight) {
this.#canvasWidth = canvasWidth;
this.#canvasHeight = canvasHeight;
}
render(scene, callback) {
var started = new Date().valueOf();
for (let pixelY = 0; pixelY < this.#canvasHeight; pixelY++) {
for (let pixelX = 0; pixelX < this.#canvasWidth; pixelX++) {
let sceneX = (pixelX / this.#canvasWidth) - 0.5;
let sceneY = (pixelY / this.#canvasHeight) - 0.5;
let pixelColor = scene.trace(sceneX, sceneY);
callback(pixelX, pixelY, pixelColor);
}
}
var duration = (new Date().valueOf() - started);
console.log(`Render completed in ${duration / 1000} seconds`);
}
}
export { Renderer, Scene, Camera, Color, Vector };
The main purpose of the Renderer
class is to translate HTML canvas pixel coordinates into floating-point world coordinates, and then call scene.trace
once for each pixel on our canvas.
The renderer includes a variable called
step
; this controls how many pixels we’ll render in each loop. The default valuestep=1
will render every pixel in the canvas, but this can take a long time - and if we’re trying to useconsole.log
to debug our code, it’ll slow things to a crawl. Settingstep
to 10 or even100
will render solid 10x10 or 100x100 blocks of pixels, which speeds things up considerably.
Notice that the renderer doesn’t actually update the canvas
directly; instead, for each pixel that’s rendered, it’ll call a callback
function and pass the x
, y
, color
and step
values.
Connecting it all together
Update the main.js
file in your project with this code:
// main.js
import { Renderer, Camera, Scene, Vector, Color } from './modules/renderer.js';
let canvas = document.getElementById('my-canvas');
let ctx = canvas.getContext('2d');
let renderer = new Renderer(canvas.width, canvas.height);
function paintPixel(x, y, color) {
ctx.fillStyle = color.html;
ctx.fillRect(x, y, 1, 1);
}
let camera = new Camera(new Vector(-4, 1, -5), Vector.O);
let background = Color.Blue;
let scene = new Scene(camera, background);
renderer.render(scene, paintPixel);
In this file, we create a scene containing a camera and a background color, then create a Renderer
, and trace that scene, using a callback function that renders each pixel (or pixel block) to our HTML canvas element.
Checklist:
You should have a project structure now that looks like this:
- index.html
- style.css
- main.js
- modules/
Download code: examples/03.renderer.zip
Run this code: examples/03-tracer/index.html
If everything works, you’ll get a solid block of bright blue sky:
Try changing the background
color on line 4 – for example, to make our sky midnight dark blue, try:
let background = new Color(0, 20, 55);
Review & Recap
- The
Renderer
class is the connection between the browser’s HTML and canvas elements, which work in 2D screen coordinates based on pixels, and the ray-tracing engine, which works in 3D world coordinates based on vectors. - Any ray in our scene that doesn’t hit a shape will end up as
scene.background
- and because we don’t have any shapes yet, all we can do is draw empty skies.
- Download code
- 03-tracer.zip
- Live example
- examples/03-tracer/index.html