├── 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 = '
- ';
3 | var middle = '
- ';
4 | var end = '
';
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 | }
--------------------------------------------------------------------------------