Rendering 3d From Scratch Chapter 5 - The Screen Pt 2
Last time we covered a new mathematical tool to aid in our quest to draw 3d objects to the screen: the cross product. Here’s what it looks like in javascript:
Vec3.prototype.crossProduct = function(rhs) { return new Vec3( this.y * rhs.z - this.z * rhs.y, this.z * rhs.x - this.x * rhs.z, this.x * rhs.y - this.y * rhs.x ); }
You can see that similar to the dot product, it is just a few arithmetical operations. As a result, it is blazing fast, which is a good thing, because these operations will run hundreds or thousands of times per draw frame in a modern video game!
We know that, armed with this tool, we can determine the four corners of our camera plane. A methodology to do this was covered in the last article, so take a peek back at that if you need a refresher. We’ll create a class called “Camera” which will store the four corners of our plane (and a few other useful pieces of information). This is what it looks like in code:
function Camera(position, pointOfInterest, size) { this.position = position; this.poi = pointOfInterest; this.size = size; var lookDirection = pointOfInterest.subtract(position).normalize(); this.camRight = lookDirection.crossProduct(new Vec3(0, 1.0, 0)).normalize(); this.camUp = lookDirection.crossProduct(this.camRight).normalize(); var h = this.camRight.multiply(size); var v = this.camUp.multiply(size); this.viewCorners = [ pointOfInterest.subtract(h).subtract(v), pointOfInterest.subtract(h).add(v), pointOfInterest.add(h).add(v), pointOfInterest.add(h).subtract(v) ]; this.horizontalMag = this.viewCorners[3].subtract(this.viewCorners[0]).magnitude(); this.verticalMag = this.viewCorners[1].subtract(this.viewCorners[0]).magnitude(); this.plane = makePlaneFromTriangle(this.viewCorners[0], this.viewCorners[1], this.viewCorners[2]); }
And finally, while we’re on the topic of code, let’s cover a quick and dirty way to draw in a web browser. This is somewhat unrelated to our ultimate goal, but it will make demonstrating all this code in action a lot cooler. I’m going to use a class called ScreenBuffer.
function ScreenBuffer(canvasId) { this.canvas = document.getElementById(canvasId); this.width = this.canvas.width; this.height = this.canvas.height; this.ctx = this.canvas.getContext('2d'); this.buffer = this.ctx.getImageData(0, 0, this.width, this.height); this.pixels = this.buffer.data; } ScreenBuffer.prototype.setPixel = function(pixel, rgb) { var idx = (Math.round(pixel.y) * this.width + Math.round(pixel.x)) * 4; this.pixels[idx + 0] = rgb.r; this.pixels[idx + 1] = rgb.g; this.pixels[idx + 2] = rgb.b; this.pixels[idx + 3] = 255; } ScreenBuffer.prototype.draw = function() { this.ctx.putImageData(this.buffer, 0, 0); } ScreenBuffer.prototype.clear = function() { this.ctx.clearRect(0, 0, this.width, this.height); this.buffer = this.ctx.getImageData(0, 0, this.width, this.height); this.pixels = this.buffer.data; }
This code only requires that there is a canvas somewhere in your html dom. It gives the ability to set individual pixels in your canvas. Here’s an example of the code in action :-)
I encourage you to inspect the source on that metroid, and you’ll see that it’s just using the ScreenBuffer from above.
The only thing left to do is to fill in our shapes on screen. Next time, we’ll cover a cool algorithm for doing just that. That will be the final puzzle piece and then we’re ready for the grand finale.