├── canvas-raycaster ├── Player.js ├── README.md ├── trace.css ├── trace.js ├── input.js ├── Level.js ├── index.html └── RayCaster.js └── README.md /canvas-raycaster/Player.js: -------------------------------------------------------------------------------- 1 | function Player(s) { 2 | this.health = 100; 3 | this.speed = { 4 | forward : s, 5 | backward: .8 * s, 6 | turn : 2 * s 7 | }; 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # canvas-raycaster 2 | > NOTE: This example repository has been archived. The example code can now be found at [mdn/museum/canvas-raycaster](https://github.com/mdn/museum/tree/main/canvas-raycaster) and the live example at [Canvas Raycaster Demo](http://mdn.github.io/museum/canvas-raycaster/). 3 | -------------------------------------------------------------------------------- /canvas-raycaster/README.md: -------------------------------------------------------------------------------- 1 | # canvas-raycaster 2 | > NOTE: This example repository has been archived. The example code can now be found at [mdn/museum/canvas-raycaster](https://github.com/mdn/museum/tree/main/canvas-raycaster) and the live example at [Canvas Raycaster Demo](http://mdn.github.io/museum/canvas-raycaster/). 3 | -------------------------------------------------------------------------------- /canvas-raycaster/trace.css: -------------------------------------------------------------------------------- 1 | .window { 2 | z-index: 10; 3 | position: absolute; 4 | left: 10px; 5 | top: 10px; 6 | width: 30%; 7 | color: #00FF00; 8 | background-color: #001100; 9 | opacity: .80; 10 | border: 2px solid #000000; 11 | font-family: "Lucida Console", "Monaco", "Courier New", Courier, mono; 12 | font-size: small; 13 | } 14 | ul { 15 | margin: 0px; 16 | padding: 0px; 17 | } 18 | li { 19 | list-style-type: none; 20 | } -------------------------------------------------------------------------------- /canvas-raycaster/trace.js: -------------------------------------------------------------------------------- 1 | var MAX_LINES = 12; 2 | var begin = ''; 5 | 6 | function trace(msg) { 7 | var output_window = document.getElementById("trace"); 8 | var lines = output_window.innerHTML.toLowerCase(); 9 | var lineList; 10 | 11 | if (lines.length > 0) { 12 | lineList = lines.substring(begin.length, lines.length - end.length).split(middle); 13 | while (lineList.length >= MAX_LINES) { lineList.shift(); } 14 | lineList.push(msg); 15 | } 16 | else { 17 | lineList = [ msg ]; 18 | } 19 | 20 | output_window.innerHTML = begin +lineList.join(middle) +end; 21 | } -------------------------------------------------------------------------------- /canvas-raycaster/input.js: -------------------------------------------------------------------------------- 1 | var KEY = { 2 | D: 68, 3 | W: 87, 4 | A: 65, 5 | S:83, 6 | RIGHT:39, 7 | UP:38, 8 | LEFT:37, 9 | DOWN:40, 10 | Q:81 11 | }; 12 | 13 | var input = { 14 | right: false, 15 | up: false, 16 | left: false, 17 | down: false, 18 | quit: false 19 | }; 20 | 21 | function press(evt) { 22 | evt.preventDefault(); 23 | var code = evt.keyCode; 24 | switch(code) { 25 | case KEY.RIGHT: 26 | case KEY.D: input.right = true; break; 27 | 28 | case KEY.UP: 29 | case KEY.W: input.up = true; break; 30 | 31 | case KEY.LEFT: 32 | case KEY.A: input.left = true; break; 33 | 34 | case KEY.DOWN: 35 | case KEY.S: input.down = true; break; 36 | 37 | case KEY.Q: input.quit = true; break; 38 | } 39 | } 40 | 41 | function release(evt) { 42 | var code = evt.keyCode; 43 | switch(code) { 44 | case KEY.RIGHT: 45 | case KEY.D: input.right = false; break; 46 | 47 | case KEY.UP: 48 | case KEY.W: input.up = false; break; 49 | 50 | case KEY.LEFT: 51 | case KEY.A: input.left = false; break; 52 | 53 | case KEY.DOWN: 54 | case KEY.S: input.down = false; break; 55 | 56 | case KEY.Q: break; 57 | 58 | default: trace('unrecognized key code: ' +code); break; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /canvas-raycaster/Level.js: -------------------------------------------------------------------------------- 1 | function Level() { 2 | this.CELLTYPE_OPEN = -1; 3 | this.CELL_SIZE = 64; // using multiple of 2 for optimization 4 | this.CELL_SIZE_SHIFT = 6; // x >> 6 = Math.floor(x/64) 5 | this.CELL_HALF = this.CELL_SIZE >> 1; // must be integer 6 | 7 | this.cellCount = { _x:0, _y:0 }; 8 | this.dimension = { _x:0, _y:0 }; 9 | this.spawnPoint = { _x:0, _y:0 }; 10 | this.colors = { ground:'#000000', sky:'#FFFFFF', wallsNear:0, wallsFar:0 }; 11 | 12 | this.map; 13 | this.viewExtent; 14 | this.walltypes; 15 | 16 | this.parseMap = function(mapString, cols, rows) { 17 | this.cellCount._x = cols; 18 | this.cellCount._y = rows; 19 | this.dimension._x = this.cellCount._x * this.CELL_SIZE; 20 | this.dimension._y = this.cellCount._y * this.CELL_SIZE; 21 | 22 | var parsedOk = false; 23 | 24 | if (mapString.length != this.cellCount._x * this.cellCount._y) { 25 | trace("map size not equal to level dimensions"); 26 | } 27 | 28 | else { 29 | this.walltypes = "@#%&"; 30 | this.colors.ground = '#444455'; 31 | this.colors.sky = '#66AAFF'; 32 | this.colors.wallsNear = new Array(0xDD1111, 0x11DD11, 0x1111DD, 0x6611CC); 33 | this.colors.wallsFar = new Array(0x110000, 0x001100, 0x000011, 0x110022); 34 | this.viewExtent = this.CELL_SIZE * 3; 35 | var spawnChar = "P"; 36 | 37 | this.map = new Array(); 38 | for (var row = 0; row < this.cellCount._y; row++) { 39 | var r = new Array(); 40 | for (var col = 0; col < this.cellCount._x; col++) { 41 | var type = this.CELLTYPE_OPEN; 42 | var c = mapString.charAt(row * this.cellCount._x + col); 43 | if (c == spawnChar) { 44 | type = this.CELLTYPE_OPEN; 45 | this.spawnPoint._x = col * this.CELL_SIZE + this.CELL_HALF; 46 | this.spawnPoint._y = row * this.CELL_SIZE + this.CELL_HALF; 47 | } 48 | else { 49 | var i = this.walltypes.indexOf(c); 50 | if (i > -1) { type = i; } 51 | } 52 | r.push(type); 53 | } 54 | this.map.push(r); 55 | } 56 | parsedOk = true; 57 | } 58 | 59 | return parsedOk; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /canvas-raycaster/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ray-caster 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 93 | 94 | 102 | 103 | 104 | 105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /canvas-raycaster/RayCaster.js: -------------------------------------------------------------------------------- 1 | 2 | function RayCaster(canvas, w, h, z, level, player, inputBuffer) { 3 | 4 | this.QUAD_I = Math.PI * .5; 5 | this.QUAD_II = Math.PI; 6 | this.QUAD_III = Math.PI * 1.5; 7 | this.TO_RADS = Math.PI / 180; 8 | this.TO_DEGS = 180 / Math.PI; 9 | this.INFINITY = 10000; 10 | this.RES = { w:w, h:h, hh:h * .5 }; 11 | this.FOV = 60 * this.TO_RADS; 12 | this.SLIVER_ARC = this.FOV / this.RES.w; 13 | this.TABLE_ENTRIES = Math.ceil(Math.PI * 2 / this.SLIVER_ARC); 14 | 15 | this.TABLE_INV_SIN; 16 | this.TABLE_INV_COS; 17 | this.TABLE_TAN; 18 | this.TABLE_INV_TAN; 19 | this.QUAD_BOUNDARIES; 20 | this.TABLE_VIEW_CORRECTION; 21 | this.TABLE_REFLECTANCE_LATITUDE; 22 | this.TABLE_REFLECTANCE_LONGITUDE; 23 | this.TABLE_HEX = [ 24 | '00','01','02','03','04','05','06','07','08','09','0a','0b','0c','0d','0e','0f', 25 | '10','11','12','13','14','15','16','17','18','19','1a','1b','1c','1d','1e','1f', 26 | '20','21','22','23','24','25','26','27','28','29','2a','2b','2c','2d','2e','2f', 27 | '30','31','32','33','34','35','36','37','38','39','3a','3b','3c','3d','3e','3f', 28 | '40','41','42','43','44','45','46','47','48','49','4a','4b','4c','4d','4e','4f', 29 | '50','51','52','53','54','55','56','57','58','59','5a','5b','5c','5d','5e','5f', 30 | '60','61','62','63','64','65','66','67','68','69','6a','6b','6c','6d','6e','6f', 31 | '70','71','72','73','74','75','76','77','78','79','7a','7b','7c','7d','7e','7f', 32 | '80','81','82','83','84','85','86','87','88','89','8a','8b','8c','8d','8e','8f', 33 | '90','91','92','93','94','95','96','97','98','99','9a','9b','9c','9d','9e','9f', 34 | 'a0','a1','a2','a3','a4','a5','a6','a7','a8','a9','aa','ab','ac','ad','ae','af', 35 | 'b0','b1','b2','b3','b4','b5','b6','b7','b8','b9','ba','bb','bc','bd','be','bf', 36 | 'c0','c1','c2','c3','c4','c5','c6','c7','c8','c9','ca','cb','cc','cd','ce','cf', 37 | 'd0','d1','d2','d3','d4','d5','d6','d7','d8','d9','da','db','dc','dd','de','df', 38 | 'e0','e1','e2','e3','e4','e5','e6','e7','e8','e9','ea','eb','ec','ed','ee','ef', 39 | 'f0','f1','f2','f3','f4','f5','f6','f7','f8','f9','fa','fb','fc','fd','fe','ff']; 40 | this.PALETTE; 41 | this.CENTERLINE_SHIFT = 0; 42 | 43 | this.camera = { position: { _x:-1, _y:-1}, direction: 0 } 44 | this.idle = false; 45 | this.sliverWidth = z * 2; 46 | this.canvas = canvas; 47 | this.canvas.lineWidth = this.sliverWidth; 48 | this.level = level; //new Level(); 49 | this.player = player; //new Player(8); 50 | this.keysPressed = inputBuffer;//new Array(false, false, false, false); 51 | 52 | this.update = function() { 53 | if (!this.idle) { 54 | this.blank(this.RES.w, this.RES.h, this.RES.hh, this.level.colors.sky, this.level.colors.ground); 55 | this.cast(); 56 | } 57 | this.processInput(); 58 | } 59 | 60 | this.loadMap = function(m, x, y) { 61 | var parseOk = this.level.parseMap(m, x, y); 62 | if (parseOk) { 63 | this.buildPalette(); 64 | this.camera.position._x = this.level.spawnPoint._x; 65 | this.camera.position._y = this.level.spawnPoint._y; 66 | this.camera.direction = 0; 67 | trace("player spawned at [" +this.camera.position._x +" " +this.camera.position._y +"]"); 68 | } 69 | return parseOk; 70 | } 71 | 72 | this.cast = function() { 73 | var hit_latitude = { _x:0, _y:0, type:this.level.CELLTYPE_OPEN }; 74 | var hit_longitude = { _x:0, _y:0, type:this.level.CELLTYPE_OPEN }; 75 | var distance = { _x:0, _y:0 }; 76 | var step = { _x:0, _y:0 }; 77 | var mapScale = this.RES.h / this.level.dimension._y; 78 | var wallHeight = this.RES.h; 79 | 80 | var wallHalfHeight; 81 | var wallScale; 82 | var wallTop; 83 | var wallCenter; 84 | var wallBottom; 85 | 86 | var brightness; 87 | var rlu; 88 | var C; 89 | var sliverColor; 90 | 91 | // cast a ray for every sliver of our Field Of View (from -this.FOV/2 to this.FOV/2), 92 | // looking for both latitudinal (E-W) and longitudinal (N-S) intersections. 93 | // the closest intersection will determine how to render the sliver. 94 | var rayDirection = this.camera.direction - Math.round(this.RES.w * .5) + 1; 95 | if (rayDirection < 0) { rayDirection += this.TABLE_ENTRIES; } 96 | for (var currentSliver = 0; currentSliver < this.RES.w; currentSliver += this.sliverWidth) { 97 | rayDirection += this.sliverWidth; 98 | if (rayDirection >= this.TABLE_ENTRIES) { rayDirection = 0; } 99 | 100 | // look for intersections with latitudinal boundaries (running east-west) 101 | if (rayDirection >= this.QUAD_BOUNDARIES[0] && rayDirection < this.QUAD_BOUNDARIES[2]) { 102 | this.cast_north(hit_latitude, distance, step, rayDirection); 103 | } 104 | else { 105 | this.cast_south(hit_latitude, distance, step, rayDirection); 106 | } 107 | 108 | // look for intersections with longitudinal boundaries (running north-south) 109 | if (rayDirection >= this.QUAD_BOUNDARIES[1] && rayDirection < this.QUAD_BOUNDARIES[3]) { 110 | this.cast_west(hit_longitude, distance, step, rayDirection); 111 | } 112 | else { 113 | this.cast_east(hit_longitude, distance, step, rayDirection); 114 | } 115 | 116 | // compare distances and draw nearest intersection 117 | if (distance._x < distance._y) { 118 | // draw a latitudinal wall sliver (east-west wall) 119 | distance._x *= this.TABLE_VIEW_CORRECTION[currentSliver]; 120 | wallScale = this.level.CELL_SIZE / distance._x; 121 | rlu = rayDirection - this.camera.direction; 122 | if (rlu < 0) { rlu += this.TABLE_ENTRIES; } 123 | else if (rlu >= this.TABLE_ENTRIES) { rlu -= this.TABLE_ENTRIES; } 124 | brightness = 1 - Math.min(1, distance._x / this.level.viewExtent); 125 | brightness *= this.TABLE_REFLECTANCE_LATITUDE[rlu]; 126 | C = this.PALETTE[hit_latitude.type]; 127 | sliverColor = '#' + 128 | this.TABLE_HEX[ Math.round(C.r.delta * brightness + C.r.far) ] + 129 | this.TABLE_HEX[ Math.round(C.g.delta * brightness + C.g.far) ] + 130 | this.TABLE_HEX[ Math.round(C.b.delta * brightness + C.b.far) ]; 131 | } 132 | else { 133 | // draw a longitudinal wall sliver (north-south wall) 134 | distance._y *= this.TABLE_VIEW_CORRECTION[currentSliver]; 135 | wallScale = this.level.CELL_SIZE / distance._y; 136 | rlu = rayDirection - this.camera.direction; 137 | if (rlu < 0) { rlu += this.TABLE_ENTRIES; } 138 | else if (rlu >= this.TABLE_ENTRIES) { rlu -= this.TABLE_ENTRIES; } 139 | brightness = 1 - Math.min(1, distance._y / this.level.viewExtent); 140 | brightness *= this.TABLE_REFLECTANCE_LONGITUDE[rlu]; 141 | C = this.PALETTE[hit_longitude.type]; 142 | sliverColor = '#' + 143 | this.TABLE_HEX[ Math.round(C.r.delta * brightness + C.r.far) ] + 144 | this.TABLE_HEX[ Math.round(C.g.delta * brightness + C.g.far) ] + 145 | this.TABLE_HEX[ Math.round(C.b.delta * brightness + C.b.far) ]; 146 | } 147 | wallCenter = Math.round(this.RES.hh + this.CENTERLINE_SHIFT*wallScale); 148 | wallHalfHeight = (wallHeight * wallScale) >> 1; 149 | wallTop = Math.max(0, wallCenter - wallHalfHeight); 150 | wallBottom = Math.min(this.RES.h, wallCenter + wallHalfHeight); 151 | this.drawSliver(currentSliver, wallTop, wallBottom, sliverColor); 152 | } 153 | 154 | } 155 | 156 | this.cast_north = function(hit, distance, step, ray) { 157 | // casting northward (0 - 180 degrees), Y is increasing 158 | var cellBoundY = this.camera.position._y >> this.level.CELL_SIZE_SHIFT; 159 | hit._y = (cellBoundY+1) << this.level.CELL_SIZE_SHIFT; 160 | hit._x = this.camera.position._x + ((hit._y - this.camera.position._y) * this.TABLE_INV_TAN[ray]); 161 | step._x = this.level.CELL_SIZE * this.TABLE_INV_TAN[ray]; 162 | step._y = this.level.CELL_SIZE; 163 | 164 | var casting = true; 165 | while (casting) { 166 | // is current hit point out of bounds? 167 | if ( (hit._x < 0) || (hit._x >= this.level.dimension._x) ) { 168 | distance._x = this.INFINITY; 169 | casting = false; 170 | } 171 | else { 172 | // is there a wall at the cell boundary north of the hitpoint? 173 | // walltype = this.level.map[row][col]; 174 | hit.type = this.level.map[((hit._y + this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT)][(hit._x >> this.level.CELL_SIZE_SHIFT)]; 175 | if (hit.type != this.level.CELLTYPE_OPEN) { 176 | distance._x = (hit._y - this.camera.position._y) * this.TABLE_INV_SIN[ray]; 177 | casting = false; 178 | } 179 | // if still in bounds but south of an empty cell, then cast further north 180 | else { 181 | hit._x += step._x; 182 | hit._y += step._y; 183 | } 184 | } 185 | } 186 | } 187 | 188 | this.cast_south = function(hit, distance, step, ray) { 189 | // casting southward (180 - 360 degrees), Y is decreasing 190 | var cellBoundY = this.camera.position._y >> this.level.CELL_SIZE_SHIFT; 191 | hit._y = cellBoundY << this.level.CELL_SIZE_SHIFT; 192 | hit._x = this.camera.position._x + ((hit._y - this.camera.position._y) * this.TABLE_INV_TAN[ray]); 193 | step._x = -this.level.CELL_SIZE * this.TABLE_INV_TAN[ray]; 194 | step._y = -this.level.CELL_SIZE; 195 | 196 | var casting = true; 197 | while (casting) { 198 | // is current hit point out of bounds? 199 | if ( (hit._x < 0) || (hit._x >= this.level.dimension._x) ) { 200 | distance._x = this.INFINITY; 201 | casting = false; 202 | } 203 | else { 204 | // is there a wall at the cell boundary south of the hitpoint? 205 | // walltype = this.level.map[row][col]; 206 | hit.type = this.level.map[((hit._y - this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT)][(hit._x >> this.level.CELL_SIZE_SHIFT)]; 207 | if (hit.type != this.level.CELLTYPE_OPEN) { 208 | distance._x = (hit._y - this.camera.position._y) * this.TABLE_INV_SIN[ray]; 209 | casting = false; 210 | } 211 | // if still in bounds but north of an empty cell, then cast further south 212 | else { 213 | hit._x += step._x; 214 | hit._y += step._y; 215 | } 216 | } 217 | } 218 | } 219 | 220 | this.cast_west = function(hit, distance, step, ray) { 221 | // casting westward (90 - 270 degrees), X is decreasing 222 | var cellBoundX = this.camera.position._x >> this.level.CELL_SIZE_SHIFT; 223 | hit._x = cellBoundX << this.level.CELL_SIZE_SHIFT; 224 | hit._y = this.camera.position._y + ((hit._x - this.camera.position._x) * this.TABLE_TAN[ray]); 225 | step._x = -this.level.CELL_SIZE; 226 | step._y = -this.level.CELL_SIZE * this.TABLE_TAN[ray]; 227 | 228 | var casting = true; 229 | while (casting) { 230 | // is current hit point out of bounds? 231 | if ( (hit._y < 0) || (hit._y >= this.level.dimension._y) ) { 232 | distance._y = this.INFINITY; 233 | casting = false; 234 | } 235 | else { 236 | // is there a wall at the cell boundary west of the hitpoint? 237 | // walltype = this.level.map[row][col]; 238 | hit.type = this.level.map[(hit._y >> this.level.CELL_SIZE_SHIFT)][((hit._x - this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT)]; 239 | if (hit.type != this.level.CELLTYPE_OPEN) { 240 | distance._y = (hit._x - this.camera.position._x) * this.TABLE_INV_COS[ray]; 241 | casting = false; 242 | } 243 | // if still in bounds but east of an empty cell, then cast further west 244 | else { 245 | hit._x += step._x; 246 | hit._y += step._y; 247 | } 248 | } 249 | } 250 | } 251 | 252 | this.cast_east = function(hit, distance, step, ray) { 253 | // casting eastward (0-90, 270-360 degrees), X is increasing 254 | var cellBoundX = this.camera.position._x >> this.level.CELL_SIZE_SHIFT; 255 | hit._x = (cellBoundX+1) << this.level.CELL_SIZE_SHIFT; 256 | hit._y = this.camera.position._y + ((hit._x - this.camera.position._x) * this.TABLE_TAN[ray]); 257 | step._x = this.level.CELL_SIZE; 258 | step._y = this.level.CELL_SIZE * this.TABLE_TAN[ray]; 259 | 260 | var casting = true; 261 | while (casting) { 262 | // is current hit point out of bounds? 263 | if ( (hit._y < 0) || (hit._y >= this.level.dimension._y) ) { 264 | distance._y = this.INFINITY; 265 | casting = false; 266 | } 267 | else { 268 | // is there a wall at the cell boundary east of the hitpoint? 269 | // walltype = this.level.map[row][col]; 270 | hit.type = this.level.map[hit._y >> this.level.CELL_SIZE_SHIFT][(hit._x + this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT]; 271 | if (hit.type != this.level.CELLTYPE_OPEN) { 272 | distance._y = (hit._x - this.camera.position._x) * this.TABLE_INV_COS[ray]; 273 | casting = false; 274 | } 275 | // if still in bounds but west of an empty cell, then cast further east 276 | else { 277 | hit._x += step._x; 278 | hit._y += step._y; 279 | } 280 | } 281 | } 282 | } 283 | 284 | this.blank = function(w, h, hh, sky, ground) { 285 | // clear drawings from previous update (pen resets to [0, 0]), 286 | this.canvas.clearRect(0, 0, w, h); 287 | // draw fresh background of sky and ground 288 | this.canvas.fillStyle = sky; 289 | this.canvas.fillRect(0, 0, w, hh); 290 | this.canvas.fillStyle = ground; 291 | this.canvas.fillRect(0, hh, w, h); 292 | } 293 | 294 | this.drawSliver = function(x, t, b, c) { 295 | // draw a vertical 1-pixel wide sliver of wall 296 | var xc = x + this.sliverWidth * .5; 297 | this.canvas.beginPath(); 298 | this.canvas.strokeStyle = c; 299 | this.canvas.moveTo(xc, t); 300 | this.canvas.lineTo(xc, b); 301 | this.canvas.closePath(); 302 | this.canvas.stroke(); 303 | } 304 | 305 | this.processInput = function() { 306 | this.idle = true; 307 | 308 | if (this.keysPressed.left) { 309 | // rotate this.camera counter-clockwise 310 | this.idle = false; 311 | trace('turning left'); 312 | this.camera.direction -= this.player.speed.turn; 313 | if (this.camera.direction < 0) { this.camera.direction += this.TABLE_ENTRIES; } 314 | } 315 | if (this.keysPressed.right) { 316 | // rotate this.camera clockwise 317 | this.idle = false; 318 | trace('turning right'); 319 | this.camera.direction += this.player.speed.turn; 320 | if (this.camera.direction >= this.TABLE_ENTRIES) { this.camera.direction -= this.TABLE_ENTRIES; } 321 | } 322 | if (this.keysPressed.up) { 323 | // ensure next step will take this.camera into empty cell 324 | this.idle = false; 325 | trace('moving forward'); 326 | var newX = this.camera.position._x + this.player.speed.forward / this.TABLE_INV_COS[this.camera.direction]; 327 | var newY = this.camera.position._y + this.player.speed.forward / this.TABLE_INV_SIN[this.camera.direction]; 328 | var row = newY >> this.level.CELL_SIZE_SHIFT; 329 | var col = newX >> this.level.CELL_SIZE_SHIFT; 330 | if (this.level.map[row][col] == this.level.CELLTYPE_OPEN) { 331 | this.camera.position._x = newX; 332 | this.camera.position._y = newY; 333 | } 334 | } 335 | if (this.keysPressed.down) { 336 | // ensure next step will take this.camera into empty cell 337 | this.idle = false; 338 | trace('moving backward'); 339 | var newX = this.camera.position._x - this.player.speed.backward / this.TABLE_INV_COS[this.camera.direction]; 340 | var newY = this.camera.position._y - this.player.speed.backward / this.TABLE_INV_SIN[this.camera.direction]; 341 | var row = newY >> this.level.CELL_SIZE_SHIFT; 342 | var col = newX >> this.level.CELL_SIZE_SHIFT; 343 | if (this.level.map[row][col] == this.level.CELLTYPE_OPEN) { 344 | this.camera.position._x = newX; 345 | this.camera.position._y = newY; 346 | } 347 | } 348 | } 349 | 350 | this.buildPalette = function() { 351 | // for each walltype color pair, 352 | // extract the r,g,b components for shading use later 353 | // 354 | // 24-bit color: 355 | // rrrrrrrrggggggggbbbbbbbb 356 | // 24 16 8 0 357 | // 358 | // extraction: 359 | // r = c >> 16 : shift out the green and blue 360 | // g = (c & 0x00FF00) >> 8 : mask out the red, shift out the blue 361 | // b = c & 0x0000FF : mask out the red and green 362 | // combination: 363 | // c = (r << 16) + (g << 8) + b : shift the components into place and combine 364 | 365 | this.PALETTE = new Array(); 366 | // the palette will be used to interp from dark to light (far to near), 367 | // so delta is set in this direction 368 | for (var i = 0; i < this.level.walltypes.length; i++) { 369 | // grab wallcolor near and wallcolor far 370 | var wcn = this.level.colors.wallsNear[i]; 371 | var wcf = this.level.colors.wallsFar[i]; 372 | 373 | // extract rgb components for near and far 374 | var rn = (wcn & 0xff0000) >> 16; 375 | var rf = (wcf & 0xff0000) >> 16; 376 | //var rn = wcn >> 16; 377 | //var rf = wcf >> 16; 378 | var gn = (wcn & 0x00ff00) >> 8; 379 | var gf = (wcf & 0x00ff00) >> 8; 380 | var bn = wcn & 0x0000ff; 381 | var bf = wcf & 0x0000ff; 382 | 383 | // assemble object and store in lookup table for use later 384 | var C = { 385 | r : { near:rn, far:rf, delta:rn-rf}, 386 | g : { near:gn, far:gf, delta:gn-gf}, 387 | b : { near:bn, far:bf, delta:bn-bf} 388 | }; 389 | this.PALETTE[i] = C; 390 | } 391 | } 392 | 393 | this.buildTables = function() { 394 | // precompute values for expensive math ops 395 | // we already know the field of view and horizontal screen res, 396 | // and thus the degrees of view spanned by a single sliver of res, 397 | // so we compute the trig values for enough slivers to cover 360 deg. 398 | 399 | // initialize the tables 400 | this.TABLE_INV_SIN = new Array(); 401 | this.TABLE_INV_COS = new Array(); 402 | this.TABLE_TAN = new Array(); 403 | this.TABLE_INV_TAN = new Array(); 404 | this.QUAD_BOUNDARIES = new Array(); 405 | this.TABLE_REFLECTANCE_LATITUDE = new Array(); 406 | this.TABLE_REFLECTANCE_LONGITUDE = new Array(); 407 | 408 | // define some unit circle constants 409 | var PI_1over2 = Math.PI * 1 / 2; // 90 degrees 410 | var PI_1over1 = Math.PI * 1; // 180 degrees 411 | var PI_3over2 = Math.PI * 3 / 2; // 270 degrees 412 | var PI_2over1 = Math.PI * 2; // 360 degrees 413 | 414 | // walk around the unit circle, jotting down trig values along the way. 415 | // we need to look out for horizontal and vertical asymptotes, where tangent 416 | // goes to infinity, and substitute a grossly underestimated value that 417 | // won't break our calculations. 418 | // also, when we cross an asymptote, we'll record the index i 419 | // QUAD_ 420 | var quadrant = 0; 421 | var angle = 0; 422 | for (var i = 0; i < this.TABLE_ENTRIES; i++) { 423 | var cosine = Math.cos(angle); 424 | var sine = Math.sin(angle); 425 | var absCosine = Math.abs(cosine); 426 | var absSine = Math.abs(sine); 427 | 428 | if (absCosine == 0 || absSine == 1) { 429 | // 90 or 270 degrees 430 | this.TABLE_TAN[i] = -this.INFINITY; 431 | this.TABLE_INV_TAN[i] = 0; 432 | if (quadrant == 1) { this.TABLE_INV_COS[i] = -this.INFINITY; } 433 | else { this.TABLE_INV_COS[i] = this.INFINITY; } 434 | this.TABLE_INV_SIN[i] = 1 / sine; 435 | this.QUAD_BOUNDARIES[quadrant] = i; 436 | quadrant++; 437 | } 438 | else if (absCosine == 1 || absSine == 0) { 439 | // 0 or 180 degrees 440 | this.TABLE_TAN[i] = 0; 441 | this.TABLE_INV_TAN[i] = this.INFINITY; 442 | if (quadrant == 0) { this.TABLE_INV_SIN[i] = this.INFINITY; } 443 | else { this.TABLE_INV_SIN[i] = -this.INFINITY; } 444 | this.TABLE_INV_COS[i] = 1 / cosine; 445 | this.QUAD_BOUNDARIES[quadrant] = i; 446 | quadrant++; 447 | } 448 | else { 449 | // no asymptotes to worry about 450 | this.TABLE_TAN[i] = sine / cosine; 451 | this.TABLE_INV_TAN[i] = cosine / sine; 452 | this.TABLE_INV_COS[i] = 1 / cosine; 453 | this.TABLE_INV_SIN[i] = 1 / sine; 454 | } 455 | 456 | // for specular lighting, 457 | // precalculate the cosine of the angle between 458 | // every ray and the surface normal of: 459 | // 1) a latitudinal (horizontal) surface 460 | // 2) a longitudinal (vertical) surface 461 | // the calculation requires that the angle be [0,PI/2] 462 | var h = 0; 463 | var v = 0; 464 | switch (quadrant-1) { 465 | case 0: 466 | h = PI_1over2 - angle; 467 | v = angle; 468 | break; 469 | 470 | case 1: 471 | h = angle - PI_1over2; 472 | v = PI_1over1 - angle; 473 | break; 474 | 475 | case 2: 476 | h = PI_3over2 - angle; 477 | v = angle - PI_1over1; 478 | break; 479 | 480 | case 3: 481 | h = angle - PI_3over2; 482 | v = PI_2over1 - angle; 483 | break; 484 | } 485 | this.TABLE_REFLECTANCE_LATITUDE[i] = Math.sin( Math.min(PI_1over2, Math.max(0, h)) ); 486 | this.TABLE_REFLECTANCE_LONGITUDE[i] = Math.cos( Math.min(PI_1over2, Math.max(0, v)) ); 487 | 488 | angle += this.SLIVER_ARC; 489 | } 490 | 491 | // pre-compute view correction values for each sliver 492 | this.TABLE_VIEW_CORRECTION = new Array(); 493 | var FOVangle = this.SLIVER_ARC * (-Math.round(this.FOV*.5)); 494 | for (var sliver = 0; sliver < this.RES.w; sliver++) { 495 | this.TABLE_VIEW_CORRECTION[sliver] = Math.cos(FOVangle); // minimal fish-eye 496 | //this.TABLE_VIEW_CORRECTION[sliver] = 1 / Math.cos(FOVangle); // extra fish-eye! cool. 497 | FOVangle += this.SLIVER_ARC; 498 | } 499 | } 500 | 501 | this.buildTables(); 502 | 503 | } --------------------------------------------------------------------------------