Building a 3D engine in HTML5
Marius Gundersen @GundersenMarius
September 2011
Marius Gundersen @GundersenMarius
September 2011
<canvas width="320" height="240" id="c"><canvas> <script> var canvas = document.getElementById("c"); var ctx = canvas.getContext("2d"); ctx.fillStyle="red"; ctx.lineStyle="black"; ctx.fillRect(128, 96, 64, 48); ctx.strokeRect(128, 96, 64, 48); </script>
var world = { vertices:[ {x:100, y:100, z: 500}, {x:-100, y:100, z: 500}, {x:-100, y:-100, z: 500}, {x:100, y:-100, z: 500}, {x:100, y:100, z: 300}, {x:-100, y:100, z: 300}, {x:-100, y:-100, z: 300}, {x:100, y:-100, z: 300}, ] };
var camera = { depth: 350, screen: Demo.ctx, width: Demo.canvas.width, height: Demo.canvas.height, offsetX: Demo.canvas.width/2, offsetY: Demo.canvas.height/2 }
for each vertex project onto screen draw on screen
function render(world, camera){ for(var i=0; i<world.vertices.length; i++){ var vertex = world.vertices[i]; var scale = camera.depth / vertex.z; var posX = scale * vertex.x + camera.offsetX; var posY = scale * vertex.y + camera.offsetY; var size = scale * 10; camera.screen.fillRect(posX-size/2, posY-size/2, size, size); } }
var camera = { x: 0, y: 0, z: 0, depth: 350, screen: Demo.ctx, width: Demo.canvas.width, height: Demo.canvas.height, offsetX: Demo.canvas.width/2, offsetY: Demo.canvas.height/2 }
for each vertex offset from camera project onto screen draw on screen
... var vertex = world.vertices[i]; var dx = vertex.x - camera.x; var dy = vertex.y - camera.y; var dz = vertex.z - camera.z; var scale = camera.depth / dz; var posX = scale * dx + camera.offsetX; var posY = scale * dy + camera.offsetY; var size = scale * 10; ...
... var dz = vertex.z - camera.z; if(dz > 0){ var scale = camera.depth / dz; var posX = scale * dx + camera.offsetX; var posY = scale * dy + camera.offsetY; var size = scale * 10; camera.screen.fillRect(posX - size / 2, posY - size / 2, size, size); } ...
for each vertex offset from camera rotate around each axis if in front of camera project onto screen draw on screen
... var dx = vertex.x - camera.x; var dy = vertex.y - camera.y; var dz = vertex.z - camera.z; var d1x = Math.cos(camera.ry)*dx + Math.sin(camera.ry)*dz; var d1y = dy; var d1z = Math.cos(camera.ry)*dz - Math.sin(camera.ry)*dx; if(d1z > 0){ ...
... var d1x = Math.cos(camera.ry)*dx + Math.sin(camera.ry)*dz; var d1y = dy; var d1z = Math.cos(camera.ry)*dz - Math.sin(camera.ry)*dx; var d2x = d1x; var d2y = Math.cos(camera.rx)*d1y - Math.sin(camera.rx)*d1z; var d2z = Math.cos(camera.rx)*d1z + Math.sin(camera.rx)*d1y; var d3x = Math.cos(camera.rz)*d2x + Math.sin(camera.rz)*d2y; var d3y = Math.cos(camera.rz)*d2y - Math.sin(camera.rz)*d2x; var d3z = d2z; if(d3z > 0){ ...
for each vertex offset from camera rotate around each axis if in front of camera project onto screen draw image on screen
var sprite = new Image(); sprite.src = "gfx/tree.png"; ... if(d3z > 0){ var scale = camera.depth / d3z; var posX = scale * d3x + camera.offsetX; var posY = scale * d3y + camera.offsetY; var width = scale * sprite.width; var height = scale * sprite.height; camera.screen.drawImage(sprite, 0, 0, sprite.width, sprite.height, posX - width / 2, posY - height / 2, width, height); }
for each vertex offset from camera rotate around each axis if in front of camera project onto screen store in list sort list for each item in list draw image on screen
var toDraw = []; for(var i=0; i<world.vertices.length; i++){ ... if(d3z > 0){ ... toDraw.push({ posX: posX - width / 2, posY: posY - height / 2, posZ: scale, width: width, height: height }); } }
toDraw.sort(function(a, b){ return a.posZ - b.posZ; }); for(var i=0; i<toDraw.length; i++){ var item = toDraw[i]; camera.screen.drawImage(sprite, 0, 0, sprite.width, sprite.height, item.posX, item.posY, item.width, item.height); }
var world = { vertices:[ {x:140, y:20, z: 0, ry:Math.PI*1, sprite:{ w:48, h:48, y:0 }}, ...
for each vertex offset from camera rotate around each axis if in front of camera project onto screen calculate angle store in list sort list for each item in list find image tile draw image on screen
toDraw.push({ posX: posX - width / 2, posY: posY - height / 2, posZ: scale, width: width, height: height, sprite: vertex.sprite, ry: camera.ry - vertex.ry, });
for(var i=0; i<toDraw.length; i++){ var item = toDraw[i]; var angle = (item.ry)%(Math.PI*2); while(angle < 0) angle += Math.PI*2; var ratio = angle / (Math.PI*2) * sprite.width; var x = (Math.round(ratio / item.sprite.w)*item.sprite.w); x %= sprite.width; camera.screen.drawImage(sprite, x, item.sprite.y, item.sprite.w, item.sprite.h, item.posX, item.posY, item.width, item.height); }
var world = { lines:[ {p1: 0, p2: 1}, {p1: 1, p2: 2}, {p1: 2, p2: 3}, {p1: 3, p2: 0} ], vertices:[ {x:100, y:100, z: 100}, {x:-100, y:100, z: 100}, {x:-100, y:-100, z: 100}, {x:100, y:-100, z: 100}, ],
for each vertex offset from camera rotate around each axis project onto screen store in vertex for each line if both vertices are in front of camera store line in list sort list for each line in list draw line
for(var i=0; i<world.vertices.length; i++){ var vertex = world.vertices[i]; .. vertex.posX = scale * d3x + camera.offsetX; vertex.posY = scale * d3y + camera.offsetY; vertex.posZ = scale; } for(var i=0; i<world.lines.length; i++){ var line = world.lines[i]; if(line.p1.posZ > 0 && line.p2.posZ > 0){ toDraw.push({ posX1: line.p1.posX, posY1: line.p1.posY, posX2: line.p2.posX, posY2: line.p2.posY, posZ: (line.p1.posZ + line.p2.posZ)/2 }); } }
toDraw.sort(function(a, b){ return a.posZ - b.posZ; }); for(var i=0; i<toDraw.length; i++){ camera.screen.beginPath(); camera.screen.moveTo(toDraw[i].posX1, toDraw[i].posY1); camera.screen.lineTo(toDraw[i].posX2, toDraw[i].posY2); camera.screen.stroke(); }
var world = { triangles:[ {p1: 2, p2: 1, p3: 0, c:"#D00"}, {p1: 0, p2: 3, p3: 2, c:"#D00"}, ] vertices:[ {x:100, y:100, z: 100}, {x:-100, y:100, z: 100}, {x:-100, y:-100, z: 100}, {x:100, y:-100, z: 100}, ],
for each vertex offset from camera rotate around each axis project onto screen store in vertex for each triangle if all vertices are in front of camera store triangle in list sort list for each triangle in list draw triangle
for(var i=0; i<world.triangles.length; i++){ var triangle = world.triangles[i]; if(triangle.p1.posZ > 0 && triangle.p2.posZ > 0 && triangle.p3.posZ > 0){ toDraw.push({ posX1: triangle.p1.posX, posY1: triangle.p1.posY, posX2: triangle.p2.posX, posY2: triangle.p2.posY, posX3: triangle.p3.posX, posY3: triangle.p3.posY, posZ: (triangle.p1.posZ + triangle.p2.posZ + triangle.p3.posZ)/3, color: triangle.c }); } }
for(var i=0; i<toDraw.length; i++){ camera.screen.beginPath(); camera.screen.fillStyle = toDraw[i].color; camera.screen.moveTo(toDraw[i].posX1, toDraw[i].posY1); camera.screen.lineTo(toDraw[i].posX2, toDraw[i].posY2); camera.screen.lineTo(toDraw[i].posX3, toDraw[i].posY3); camera.screen.fill(); }
for each vertex offset from camera rotate around each axis project onto screen store in vertex for each triangle if all vertices are in front of camera if polygon faces camera store triangle in list sort list for each triangle in list draw triangle
var vector1 = {dx: triangle.p1.x - triangle.p2.x, dy: triangle.p1.y - triangle.p2.y, dz: triangle.p1.z - triangle.p2.z}; var vector2 = {dx: triangle.p3.x - triangle.p2.x, dy: triangle.p3.y - triangle.p2.y, dz: triangle.p3.z - triangle.p2.z}; var crossProduct = {dx: vector1.dy*vector2.dz - vector1.dz*vector2.dy, dy: vector1.dz*vector2.dx - vector1.dx*vector2.dz, dz: vector1.dx*vector2.dy - vector1.dy*vector2.dx};
var cameraVector = {dx: (camera.x - (triangle.p1.x + triangle.p2.x + triangle.p3.x)/3), dy: (camera.y - (triangle.p1.y + triangle.p2.y + triangle.p3.y)/3), dz: (camera.z - (triangle.p1.z + triangle.p2.z + triangle.p3.z)/3)}; var dp = crossProduct.dx * cameraVector.dx + crossProduct.dy * cameraVector.dy + crossProduct.dz * cameraVector.dz; if(dp > 0){ toDraw.push({ ...
for each vertex offset from camera rotate around each axis project onto screen store in vertex for each triangle if all vertices are in front of camera if polygon faces camera calculate shade store triangle in list sort list for each triangle in list draw triangle draw shade
var dp = crossProduct.dx * cameraVector.dx + crossProduct.dy * cameraVector.dy + crossProduct.dz * cameraVector.dz; var length1 = Math.sqrt(cameraVector.dx * cameraVector.dx + cameraVector.dy * cameraVector.dy + cameraVector.dz * cameraVector.dz); var length2 = Math.sqrt(crossProduct.dx * crossProduct.dx + crossProduct.dy * crossProduct.dy + crossProduct.dz * crossProduct.dz); dp = dp/length1/length2; if(dp > 0){ toDraw.push({ ... color: triangle.c, shade: 1-dp });
camera.screen.lineTo(toDraw[i].posX3, toDraw[i].posY3); camera.screen.fill(); camera.screen.fillStyle = "rgba(0, 0, 0,"+toDraw[i].shade+")"; camera.screen.fill();
perspective: 600px; transform: rotateY(0.25turn) translateZ(100px);
MariusGundersen.net
@gundersenMarius