├── README.md ├── demos ├── README.md ├── ball.html ├── terrain.html └── tetrahedron.html └── zengine.js /README.md: -------------------------------------------------------------------------------- 1 | zengine.js 2 | ========== 3 | 4 | #### A JavaScript 3D Rendering Engine 5 | 6 | There are many JavaScript 3D libraries out there, such as [THREE.js](https://threejs.org/), but I wanted to challenge myself to write the neatest, most simple code that accomplishes rendering objects in 3D to a 2D screen. Ignoring comments, all the code that was necessary to build this to its current functionality is under 100 lines! 7 | 8 | --- 9 | 10 | ### Installation 11 | 12 | Simply include the source in your application's HTML, no downloading required: 13 | 14 | ```html 15 | 16 | ``` 17 | 18 | or you can use the shorter `git.io` redirect: 19 | 20 | ```html 21 | 22 | ``` 23 | 24 | --- 25 | 26 | ### Usage 27 | 28 | The main use of this library is obviously the rendering capabilities. This is covered below. However functions that are required to render are also available for use as part of the library. Some examples of these include: a dot product function, transformation matricies and distance functions. Feel free to use these but at the time of writing, no documentation has been made for them. 29 | 30 | **Prerequisites:** 31 | 32 | - All angles are in **degrees**. 33 | - All distances are in arbitary units - relative to each other. 34 | - The coordinate system has the `y-axis` going straight ahead, `x-axis` to the right and `z-axis` going straight up. 35 | 36 | The main function - `zengine.render()` - renders a world from the perspective of a camera to a HTML5 Canvas Element. 37 | 38 | It has the format: 39 | 40 | ```javascript 41 | zengine.render(world, cam, canvas, wireframe, horizon, light); 42 | ``` 43 | 44 | *Note that `wireframe`, `horizon` and `light` can be set to their default values of: `false`, `Infinity` (not actually because the filtering step is skipped for efficiency) and the camera's point of view's vector by not passing them or through `undefined`*. 45 | 46 | The `world` is described by an array of faces. 47 | 48 | Each face is itself an object with attributes: 49 | 50 | Attribute | Meaning 51 | ---------- | ------------------------------------- 52 | `verts` | array of vertexes as objecs (e.g. `{x: 0, y: 0, z: 0}`) 53 | `vect` | the face's unit vector (e.g. `{x: 0, y: 1, z: 0}`) 54 | `col` | color - if using shading, an object with attributes `h, s, l` else any CSS string 55 | 56 | This can be summarised by the following general-case format. 57 | 58 | ```javascript 59 | world = [{verts: [{x: ,y: ,z: }, {x: ,y: ,z: }, ...], vect: {x: ,y: ,z: }, col: }, ...] 60 | ``` 61 | 62 | The `cam` parameter is an object with attributes: 63 | 64 | Attribute | Meaning 65 | ------------- | ------------------------------------------ 66 | `x`, `y`, `z` | cooridinate in 3D Cartesian Geometry 67 | `yaw` | rotation left to right 68 | `pitch` | rotation up and down 69 | `roll` | rotation about the "forward" axis 70 | `fov` | the, horizontal, field of view, in degrees 71 | 72 | This can be seen in the following general-case format. 73 | 74 | ```javascript 75 | cam = {x: ,y: ,z: ,yaw: ,pitch: ,roll: ,fov: } 76 | ``` 77 | 78 | The `canvas` parameter should be a HTML Canvas Element Object. 79 | 80 | *Calling this function will blank the canvas before drawing to it.* 81 | 82 | The `wireframe` parameter takes a boolean indicating whether or not to draw just the outlines of each face. This also speeds up the rendering as face ordering is no longer required, and drawing to the Canvas is marginally faster. 83 | 84 | The `horizon` parameter takes a distance, in units relative to the world, for how far you can see. The purpose of this is to speed up rendering. If left `undefined`, defaults to infinity. 85 | 86 | The `light` parameter is an object with attributes: 87 | 88 | Attribute | Meaning 89 | -----------------|------------------------------------------- 90 | `yaw`, `pitch` | components of a spherical direction vector 91 | `min_saturation` | minimum saturation percent 92 | `min_lightness` | minimum lightness percent 93 | 94 | Where the `min_*` attributes are to be given as decimals in the range `0` to `1`. 95 | 96 | --- 97 | 98 | ### Examples 99 | 100 | The [demo folder](https://github.com/joeiddon/zengine/tree/master/demos) contains example code. You can view the code either by cloning this whole repository with 101 | 102 | ```shell 103 | git clone https://github.com/joeiddon/zengine.git 104 | ``` 105 | or just use the GitHub web app. 106 | 107 | To actually view each example, host locally, or view in GitHub Pages [here](https://joeiddon.github.io/zengine/demos). 108 | 109 | --- 110 | 111 | ### Known Bugs 112 | 113 | Some bugs that need fixing, but I haven't got around to: 114 | 115 | - Proper face ordering - currently done by distance to face centroids; should be done by casting a ray through the two faces. Or could use a more low level canvas rendering context, like WebGL, in order to implement a simple z-buffer. 116 | - Rotation around the x-axis is not right-hand for some reason - probably a dodgy rotation matrix calculation. Can't easily be flipped as now lots of code relies on it being wrong! Ha! 117 | 118 | --- 119 | 120 | ### Uses 121 | 122 | These are some applications of this library: 123 | 124 | - [the demos](https://joeiddon.github.io/zengine/demos) 125 | - [maze game](https://joeiddon.github.io/3d_maze_game) 126 | - [apocalombie](https://joeiddon.github.io/apocalombie) 127 | - [blocks](https://joeiddon.github.io/blocks) 128 | 129 | If you would like to read about these projects more, I have posts on each of them [here on my website](https://joeiddon.github.io/projects/javascript). 130 | -------------------------------------------------------------------------------- /demos/README.md: -------------------------------------------------------------------------------- 1 | demos 2 | ===== 3 | 4 | View the result of the above code by following their respective links: 5 | 6 | - [ball.html](https://joeiddon.github.io/zengine/demos/ball) 7 | - [terrain.html](https://joeiddon.github.io/zengine/demos/terrain) 8 | - [tetrahedron.html](https://joeiddon.github.io/zengine/demos/tetrahedron) 9 | -------------------------------------------------------------------------------- /demos/ball.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | 176 | 177 | -------------------------------------------------------------------------------- /demos/terrain.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 48 |

49 |
50 | 51 |

hills: 2

52 | 53 |

width: 32

54 | 55 |

length: 32

56 | 57 |

hill height: 8

58 | 59 |

speed: 256

60 | 61 |

light yaw speed: 0

62 | 63 |

light pitch speed: 0

64 | 65 |

66 |

67 |
68 | 69 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /demos/tetrahedron.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /zengine.js: -------------------------------------------------------------------------------- 1 | /* 2 | zengine.js - 3D Rendering Software designed to work with the HTML5 Canvas 3 | Copyright (c) 2018 Joe Iddon. All right reserved. 4 | 5 | This library is free software; you are free to redistribute it and/or 6 | modify it provided appropriate credit is given to the original author. 7 | 8 | GitHub Repository (includes README.md w/documentation): 9 | https://github.com/joeiddon/zengine 10 | 11 | Author's website: 12 | http://joeiddon.github.io/ 13 | */ 14 | 15 | 'use strict'; 16 | 17 | let zengine = { 18 | render: function(world, cam, canvas, wireframe, horizon, light){ 19 | let ctx = canvas.getContext('2d'); 20 | ctx.clearRect(0, 0, canvas.width, canvas.height); 21 | 22 | //create cartesian unit vector representations from polar light and cam vects 23 | let cam_vect = this.polar_to_cart(this.to_rad(cam.yaw), this.to_rad(cam.pitch)); 24 | let light_vect = light ? this.polar_to_cart(this.to_rad(light.yaw), this.to_rad(light.pitch)) : 0; 25 | 26 | //temporary inclusion until all current uses are updated to include unit vects 27 | let has_vects = world[0].vect != undefined; 28 | 29 | //add some extra attrs. to each face 30 | for (var f = 0; f < world.length; f++){ 31 | world[f].centr = this.centroid(world[f].verts); 32 | world[f].dist = this.distance(cam, world[f].centr); 33 | world[f].c_vect = {x: (world[f].centr.x - cam.x) / world[f].dist, 34 | y: (world[f].centr.y - cam.y) / world[f].dist, 35 | z: (world[f].centr.z - cam.z) / world[f].dist}; 36 | } 37 | 38 | //only keep faces that are: 39 | // - before the horizon 40 | // - facing the camera 41 | // - have at least one vertex in front of camera. 42 | world = world.filter(f => 43 | (!horizon || f.dist < horizon) && 44 | (wireframe || !has_vects || this.dot_prod(f.c_vect, f.vect) < 0) && 45 | f.verts.some(c => this.dot_prod({x: c.x-cam.x, 46 | y: c.y-cam.y, 47 | z: c.z-cam.z}, cam_vect) > 0)); 48 | 49 | //order the faces in the world (furthest to closest) 50 | if (!wireframe) world.sort((a, b) => b.dist - a.dist); 51 | 52 | for (let f = 0; f < world.length; f++){ 53 | //todo: just have more stacked .map calls rather than chunk it 54 | 55 | //align 3d coordinates to camera view angle 56 | let acs = world[f].verts.map(this.translate(-cam.x, -cam.y, -cam.z)) 57 | .map(this.z_axis_rotate(this.to_rad(cam.yaw))) 58 | .map(this.y_axis_rotate(this.to_rad(cam.roll))) 59 | .map(this.x_axis_rotate(this.to_rad(cam.pitch))) 60 | .map(this.translate(cam.x, cam.y, cam.z)); 61 | 62 | //convert the 3d coordinates to yaw, pitch angles from cam center line 63 | let cas = acs.map(c => ({y: this.to_deg(Math.atan2(c.x - cam.x, c.y - cam.y)), 64 | p: this.to_deg(Math.atan2(c.z - cam.z, c.y - cam.y))})); 65 | 66 | //convert angles to 2d canvas coordinates 67 | let cos = cas.map(a => ({x: canvas.width/2 + (a.y * (canvas.width/cam.fov)), 68 | y: canvas.height/2 - (a.p * (canvas.width/cam.fov))})); 69 | 70 | //draw the face on the canvas 71 | ctx.strokeStyle = wireframe ? 'white' : 'black'; 72 | ctx.beginPath(); 73 | ctx.moveTo(cos[0].x, cos[0].y); 74 | for (let i = 1; i < cos.length; i++){ 75 | ctx.lineTo(cos[i].x, cos[i].y); 76 | } 77 | ctx.closePath(); ctx.stroke(); 78 | if (!wireframe){ 79 | if (has_vects){ 80 | let angle = -this.dot_prod(light_vect || world[f].c_vect /*cam_vect*/, world[f].vect); 81 | if (angle < 0) angle = 0; 82 | let s = world[f].col.s * (light ? (light.min_saturation+ (1 -light.min_saturation) * angle) : angle); 83 | let l = world[f].col.l * (light ? (light.min_lightness + (1 -light.min_lightness ) * angle) : angle); 84 | ctx.fillStyle = 'hsl('+world[f].col.h+','+s+'%,'+l+'%)'; 85 | } else { 86 | ctx.fillStyle = world[f].col; 87 | } 88 | ctx.fill(); 89 | } 90 | } 91 | }, 92 | 93 | centroid: function(verts){ 94 | let l = verts.length; 95 | let c = {x: 0, y: 0, z: 0}; 96 | for (let i = 0; i < l; i++) 97 | for (let k in c) c[k] += verts[i][k]; 98 | return {x: c.x/l, y: c.y/l, z: c.z/l}; 99 | }, 100 | 101 | translate: (x, y, z) => (v => ({x: v.x + x, y: v.y + y, z: v.z + z})), 102 | to_deg: (r) => r * (180 / Math.PI), 103 | to_rad: (d) => d * (Math.PI / 180), 104 | distance: (c1, c2) => ((c2.x - c1.x)**2 + (c2.y - c1.y)**2 + (c2.z - c1.z)**2) ** 0.5, 105 | dot_prod: (v1, v2) => v1.x * v2.x + v1.y * v2.y + v1.z * v2.z, 106 | polar_to_cart: (y, p) => ({x: Math.sin(y) * Math.cos(p), 107 | y: Math.cos(y) * Math.cos(p), 108 | z: Math.sin(p)}), 109 | cross_prod: (v1, v2) => ({x: v1.y * v2.z - v1.z * v2.y, 110 | y: v1.z * v2.x - v1.x * v2.z, 111 | z: v1.x * v2.y - v1.y * v2.x}), 112 | x_axis_rotate: (r) => (v => ({x: v.x, 113 | y: v.y * Math.cos(r) + v.z * Math.sin(r), 114 | z: -v.y * Math.sin(r) + v.z * Math.cos(r)})), 115 | y_axis_rotate: (r) => (v => ({x: v.x * Math.cos(r) + v.z * Math.sin(r), 116 | y: v.y, 117 | z: -v.x * Math.sin(r) + v.z * Math.cos(r)})), 118 | z_axis_rotate: (r) => (v => ({x: v.x * Math.cos(r) - v.y * Math.sin(r), 119 | y: v.x * Math.sin(r) + v.y * Math.cos(r), 120 | z: v.z})) 121 | }; 122 | --------------------------------------------------------------------------------