topical media & game development

talk show tell print

graphic-canvas-example-raycaster-raycaster.js / js



  
  function RayCaster(canvas, w, h, z, level, player, inputBuffer) {
  
      this.QUAD_I = Math.PI * .5;
      this.QUAD_II = Math.PI;
      this.QUAD_III = Math.PI * 1.5;
      this.TO_RADS = Math.PI / 180;
      this.TO_DEGS = 180 / Math.PI;
      this.INFINITY = 10000;
      this.RES = { w:w, h:h, hh:h * .5 };
      this.FOV = 60 * this.TO_RADS;
      this.SLIVER_ARC = this.FOV / this.RES.w;
      this.TABLE_ENTRIES = Math.ceil(Math.PI * 2 / this.SLIVER_ARC);
  
      this.TABLE_INV_SIN;
      this.TABLE_INV_COS;
      this.TABLE_TAN;
      this.TABLE_INV_TAN;
      this.QUAD_BOUNDARIES;
      this.TABLE_VIEW_CORRECTION;
      this.TABLE_REFLECTANCE_LATITUDE;
      this.TABLE_REFLECTANCE_LONGITUDE;
           this.TABLE_HEX = [
                  '00','01','02','03','04','05','06','07','08','09','0a','0b','0c','0d','0e','0f',
                  '10','11','12','13','14','15','16','17','18','19','1a','1b','1c','1d','1e','1f',
                  '20','21','22','23','24','25','26','27','28','29','2a','2b','2c','2d','2e','2f',
                  '30','31','32','33','34','35','36','37','38','39','3a','3b','3c','3d','3e','3f',
                  '40','41','42','43','44','45','46','47','48','49','4a','4b','4c','4d','4e','4f',
                  '50','51','52','53','54','55','56','57','58','59','5a','5b','5c','5d','5e','5f',
                  '60','61','62','63','64','65','66','67','68','69','6a','6b','6c','6d','6e','6f',
                  '70','71','72','73','74','75','76','77','78','79','7a','7b','7c','7d','7e','7f',
                  '80','81','82','83','84','85','86','87','88','89','8a','8b','8c','8d','8e','8f',
                  '90','91','92','93','94','95','96','97','98','99','9a','9b','9c','9d','9e','9f',
                  'a0','a1','a2','a3','a4','a5','a6','a7','a8','a9','aa','ab','ac','ad','ae','af',
                  'b0','b1','b2','b3','b4','b5','b6','b7','b8','b9','ba','bb','bc','bd','be','bf',
                  'c0','c1','c2','c3','c4','c5','c6','c7','c8','c9','ca','cb','cc','cd','ce','cf',
                  'd0','d1','d2','d3','d4','d5','d6','d7','d8','d9','da','db','dc','dd','de','df',
                  'e0','e1','e2','e3','e4','e5','e6','e7','e8','e9','ea','eb','ec','ed','ee','ef',
                  'f0','f1','f2','f3','f4','f5','f6','f7','f8','f9','fa','fb','fc','fd','fe','ff'];
      this.PALETTE;
      this.CENTERLINE_SHIFT = 0;
  
      this.camera = { position: { _x:-1, _y:-1}, direction: 0 }
           this.idle = false;
           this.sliverWidth = z * 2;
      this.canvas = canvas;
           this.canvas.lineWidth = this.sliverWidth;
      this.level = level; //new Level();
      this.player = player; //new Player(8);
      this.keysPressed = inputBuffer;//new Array(false, false, false, false);
      
      this.update = function() {
                  if (!this.idle) {
                          this.blank(this.RES.w, this.RES.h, this.RES.hh, this.level.colors.sky, this.level.colors.ground);
                          this.cast();
                  }
                  this.processInput();
      }
      
      this.loadMap = function(m, x, y) {
          var parseOk = this.level.parseMap(m, x, y);
          if (parseOk) {
              this.buildPalette();
              this.camera.position._x = this.level.spawnPoint._x;
              this.camera.position._y = this.level.spawnPoint._y;
              this.camera.direction = 0;
              trace("player spawned at [" +this.camera.position._x +" " +this.camera.position._y +"]");
          }
          return parseOk;
      }
      
      this.cast = function() {
          var hit_latitude = { _x:0, _y:0, type:this.level.CELLTYPE_OPEN };
          var hit_longitude = { _x:0, _y:0, type:this.level.CELLTYPE_OPEN };
          var distance = { _x:0, _y:0 };
          var step = { _x:0, _y:0 };
          var mapScale = this.RES.h / this.level.dimension._y;
          var wallHeight = this.RES.h;
          
          var wallHalfHeight;
          var wallScale;
          var wallTop;
          var wallCenter;
          var wallBottom;
          
          var brightness;
          var rlu;
          var C;
          var sliverColor;
  
          // cast a ray for every sliver of our Field Of View (from -this.FOV/2 to this.FOV/2),
          // looking for both latitudinal (E-W) and longitudinal (N-S) intersections.
          // the closest intersection will determine how to render the sliver.
          var rayDirection = this.camera.direction - Math.round(this.RES.w * .5) + 1;
          if (rayDirection < 0) { rayDirection += this.TABLE_ENTRIES; }
          for (var currentSliver = 0; currentSliver < this.RES.w; currentSliver += this.sliverWidth) {
              rayDirection += this.sliverWidth;
              if (rayDirection >= this.TABLE_ENTRIES) { rayDirection = 0; }
              
              // look for intersections with latitudinal boundaries (running east-west)
              if (rayDirection >= this.QUAD_BOUNDARIES[0] && rayDirection < this.QUAD_BOUNDARIES[2]) {
                  this.cast_north(hit_latitude, distance, step, rayDirection);
              }
              else {
                  this.cast_south(hit_latitude, distance, step, rayDirection);
              }
              
              // look for intersections with longitudinal boundaries (running north-south)
              if (rayDirection >= this.QUAD_BOUNDARIES[1] && rayDirection < this.QUAD_BOUNDARIES[3]) {
                  this.cast_west(hit_longitude, distance, step, rayDirection);
              }
              else {
                  this.cast_east(hit_longitude, distance, step, rayDirection);
              }
          
              // compare distances and draw nearest intersection
              if (distance._x < distance._y) {
                  // draw a latitudinal wall sliver (east-west wall)
                  distance._x *= this.TABLE_VIEW_CORRECTION[currentSliver];
                  wallScale = this.level.CELL_SIZE / distance._x;
                  rlu = rayDirection - this.camera.direction;
                  if (rlu < 0) { rlu += this.TABLE_ENTRIES; }
                  else if (rlu >= this.TABLE_ENTRIES) { rlu -= this.TABLE_ENTRIES; }
                  brightness = 1 - Math.min(1, distance._x / this.level.viewExtent);
                  brightness *= this.TABLE_REFLECTANCE_LATITUDE[rlu];
                  C = this.PALETTE[hit_latitude.type];
                  sliverColor = '#' +
                                                         this.TABLE_HEX[ Math.round(C.r.delta * brightness + C.r.far) ] +
                                this.TABLE_HEX[ Math.round(C.g.delta * brightness + C.g.far) ] +
                                this.TABLE_HEX[ Math.round(C.b.delta * brightness + C.b.far) ];
              }
              else {
                  // draw a longitudinal wall sliver (north-south wall)
                  distance._y *= this.TABLE_VIEW_CORRECTION[currentSliver];
                  wallScale = this.level.CELL_SIZE / distance._y;
                  rlu = rayDirection - this.camera.direction;
                  if (rlu < 0) { rlu += this.TABLE_ENTRIES; }
                  else if (rlu >= this.TABLE_ENTRIES) { rlu -= this.TABLE_ENTRIES; }
                  brightness = 1 - Math.min(1, distance._y / this.level.viewExtent);
                  brightness *= this.TABLE_REFLECTANCE_LONGITUDE[rlu];
                  C = this.PALETTE[hit_longitude.type];
                  sliverColor = '#' +
                                                         this.TABLE_HEX[ Math.round(C.r.delta * brightness + C.r.far) ] +
                                this.TABLE_HEX[ Math.round(C.g.delta * brightness + C.g.far) ] +
                                this.TABLE_HEX[ Math.round(C.b.delta * brightness + C.b.far) ];
              }
              wallCenter = Math.round(this.RES.hh + this.CENTERLINE_SHIFT*wallScale);
              wallHalfHeight = (wallHeight * wallScale) >> 1;
              wallTop = Math.max(0, wallCenter - wallHalfHeight);
              wallBottom = Math.min(this.RES.h, wallCenter + wallHalfHeight);
              this.drawSliver(currentSliver, wallTop, wallBottom, sliverColor);
          }
          
      }
      
      this.cast_north = function(hit, distance, step, ray) {
          // casting northward (0 - 180 degrees), Y is increasing
          var cellBoundY = this.camera.position._y >> this.level.CELL_SIZE_SHIFT;
          hit._y = (cellBoundY+1) << this.level.CELL_SIZE_SHIFT;
          hit._x = this.camera.position._x + ((hit._y - this.camera.position._y) * this.TABLE_INV_TAN[ray]);
          step._x = this.level.CELL_SIZE * this.TABLE_INV_TAN[ray];
          step._y = this.level.CELL_SIZE;
          
          var casting = true;
          while (casting) {
              // is current hit point out of bounds?
              if ( (hit._x < 0) || (hit._x >= this.level.dimension._x) ) {
                  distance._x = this.INFINITY;
                  casting = false;
              }
              else {
                  // is there a wall at the cell boundary north of the hitpoint?
                  // walltype = this.level.map[row][col];
                  hit.type = this.level.map[((hit._y + this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT)][(hit._x >> this.level.CELL_SIZE_SHIFT)];
                  if (hit.type != this.level.CELLTYPE_OPEN) {
                      distance._x = (hit._y - this.camera.position._y) * this.TABLE_INV_SIN[ray];
                      casting = false;
                  }
                  // if still in bounds but south of an empty cell, then cast further north
                  else {
                      hit._x += step._x;
                      hit._y += step._y;
                  }
              }
          }
      }
      
      this.cast_south = function(hit, distance, step, ray) {
          // casting southward (180 - 360 degrees), Y is decreasing
          var cellBoundY = this.camera.position._y >> this.level.CELL_SIZE_SHIFT;
          hit._y = cellBoundY << this.level.CELL_SIZE_SHIFT;
          hit._x = this.camera.position._x + ((hit._y - this.camera.position._y) * this.TABLE_INV_TAN[ray]);
          step._x = -this.level.CELL_SIZE * this.TABLE_INV_TAN[ray];
          step._y = -this.level.CELL_SIZE;
          
          var casting = true;
          while (casting) {
              // is current hit point out of bounds?
              if ( (hit._x < 0) || (hit._x >= this.level.dimension._x) ) {
                  distance._x = this.INFINITY;
                  casting = false;
              }
              else {
                  // is there a wall at the cell boundary south of the hitpoint?
                  // walltype = this.level.map[row][col];
                  hit.type = this.level.map[((hit._y - this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT)][(hit._x >> this.level.CELL_SIZE_SHIFT)];
                  if (hit.type != this.level.CELLTYPE_OPEN) {
                      distance._x = (hit._y - this.camera.position._y) * this.TABLE_INV_SIN[ray];
                      casting = false;
                  }
                  // if still in bounds but north of an empty cell, then cast further south
                  else {
                      hit._x += step._x;
                      hit._y += step._y;
                  }
              }
          }
      }
      
      this.cast_west = function(hit, distance, step, ray) {
          // casting westward (90 - 270 degrees), X is decreasing
          var cellBoundX = this.camera.position._x >> this.level.CELL_SIZE_SHIFT;
          hit._x = cellBoundX << this.level.CELL_SIZE_SHIFT;
          hit._y = this.camera.position._y + ((hit._x - this.camera.position._x) * this.TABLE_TAN[ray]);
          step._x = -this.level.CELL_SIZE;
          step._y = -this.level.CELL_SIZE * this.TABLE_TAN[ray];
          
          var casting = true;
          while (casting) {
              // is current hit point out of bounds?
              if ( (hit._y < 0) || (hit._y >= this.level.dimension._y) ) {
                  distance._y = this.INFINITY;
                  casting = false;
              }
              else {
                  // is there a wall at the cell boundary west of the hitpoint?
                  // walltype = this.level.map[row][col];
                  hit.type = this.level.map[(hit._y >> this.level.CELL_SIZE_SHIFT)][((hit._x - this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT)];
                  if (hit.type != this.level.CELLTYPE_OPEN) {
                      distance._y = (hit._x - this.camera.position._x) * this.TABLE_INV_COS[ray];
                      casting = false;
                  }
                  // if still in bounds but east of an empty cell, then cast further west
                  else {
                      hit._x += step._x;
                      hit._y += step._y;
                  }
              }
          }
      }
      
      this.cast_east = function(hit, distance, step, ray) {
          // casting eastward (0-90, 270-360 degrees), X is increasing
          var cellBoundX = this.camera.position._x >> this.level.CELL_SIZE_SHIFT;
          hit._x = (cellBoundX+1) << this.level.CELL_SIZE_SHIFT;
          hit._y = this.camera.position._y + ((hit._x - this.camera.position._x) * this.TABLE_TAN[ray]);
          step._x = this.level.CELL_SIZE;
          step._y = this.level.CELL_SIZE * this.TABLE_TAN[ray];
          
          var casting = true;
          while (casting) {
              // is current hit point out of bounds?
              if ( (hit._y < 0) || (hit._y >= this.level.dimension._y) ) {
                  distance._y = this.INFINITY;
                  casting = false;
              }
              else {
                  // is there a wall at the cell boundary east of the hitpoint?
                  // walltype = this.level.map[row][col];
                  hit.type = this.level.map[hit._y >> this.level.CELL_SIZE_SHIFT][(hit._x + this.level.CELL_HALF) >> this.level.CELL_SIZE_SHIFT];
                  if (hit.type != this.level.CELLTYPE_OPEN) {
                      distance._y = (hit._x - this.camera.position._x) * this.TABLE_INV_COS[ray];
                      casting = false;
                  }
                  // if still in bounds but west of an empty cell, then cast further east
                  else {
                      hit._x += step._x;
                      hit._y += step._y;
                  }
              }
          }
      }
      
      this.blank = function(w, h, hh, sky, ground) {
          // clear drawings from previous update (pen resets to [0, 0]),
                    this.canvas.clearRect(0, 0, w, h);
          // draw fresh background of sky and ground
                    this.canvas.fillStyle = sky;
                    this.canvas.fillRect(0, 0, w, hh);
                    this.canvas.fillStyle = ground;
                    this.canvas.fillRect(0, hh, w, h);
      }
      
      this.drawSliver = function(x, t, b, c) {
          // draw a vertical 1-pixel wide sliver of wall
                    var xc = x + this.sliverWidth * .5;
                    this.canvas.beginPath();
                    this.canvas.strokeStyle = c;
                    this.canvas.moveTo(xc, t);
                    this.canvas.lineTo(xc, b);
                    this.canvas.closePath();
                    this.canvas.stroke();
      }
      
      this.processInput = function() {
                   this.idle = true;
                   
          if (this.keysPressed.left) {
              // rotate this.camera counter-clockwise
                                  this.idle = false;
                                  trace('turning left');
              this.camera.direction -= this.player.speed.turn;
              if (this.camera.direction < 0) { this.camera.direction += this.TABLE_ENTRIES; }
          }
          if (this.keysPressed.right) {
              // rotate this.camera clockwise
                                  this.idle = false;
                                  trace('turning right');
              this.camera.direction += this.player.speed.turn;
              if (this.camera.direction >= this.TABLE_ENTRIES) { this.camera.direction -= this.TABLE_ENTRIES; }
          }
          if (this.keysPressed.up) {
              // ensure next step will take this.camera into empty cell
                                  this.idle = false;
                                  trace('moving forward');
              var newX = this.camera.position._x + this.player.speed.forward / this.TABLE_INV_COS[this.camera.direction];
              var newY = this.camera.position._y + this.player.speed.forward / this.TABLE_INV_SIN[this.camera.direction];
              var row = newY >> this.level.CELL_SIZE_SHIFT;
              var col = newX >> this.level.CELL_SIZE_SHIFT;
              if (this.level.map[row][col] == this.level.CELLTYPE_OPEN) {
                  this.camera.position._x = newX;
                  this.camera.position._y = newY;
              }
          }
          if (this.keysPressed.down) {
              // ensure next step will take this.camera into empty cell
                                  this.idle = false;
                                  trace('moving backward');
              var newX = this.camera.position._x - this.player.speed.backward / this.TABLE_INV_COS[this.camera.direction];
              var newY = this.camera.position._y - this.player.speed.backward / this.TABLE_INV_SIN[this.camera.direction];
              var row = newY >> this.level.CELL_SIZE_SHIFT;
              var col = newX >> this.level.CELL_SIZE_SHIFT;
              if (this.level.map[row][col] == this.level.CELLTYPE_OPEN) {
                  this.camera.position._x = newX;
                  this.camera.position._y = newY;
              }
          }
      }
  
      this.buildPalette = function() {
          // for each walltype color pair,
          // extract the r,g,b components for shading use later
          //
          // 24-bit color:
          //   rrrrrrrrggggggggbbbbbbbb
          // 24      16       8       0
          //
          // extraction:
          //   r = c >> 16              : shift out the green and blue
          //   g = (c & 0x00FF00) >> 8  : mask out the red, shift out the blue
          //   b = c & 0x0000FF         : mask out the red and green
          // combination:
          //   c = (r << 16) + (g << 8) + b  : shift the components into place and combine
          
          this.PALETTE = new Array();
          // the palette will be used to interp from dark to light (far to near),
          // so delta is set in this direction
          for (var i = 0; i < this.level.walltypes.length; i++) {
              // grab wallcolor near and wallcolor far
              var wcn = this.level.colors.wallsNear[i];
              var wcf = this.level.colors.wallsFar[i];
              
              // extract rgb components for near and far
              var rn = (wcn & 0xff0000) >> 16;
              var rf = (wcf & 0xff0000) >> 16;
              //var rn = wcn >> 16;
              //var rf = wcf >> 16;
              var gn = (wcn & 0x00ff00) >> 8;
              var gf = (wcf & 0x00ff00) >> 8;
              var bn = wcn & 0x0000ff;
              var bf = wcf & 0x0000ff;
              
              // assemble object and store in lookup table for use later
              var C = {
                  r : { near:rn, far:rf, delta:rn-rf},
                  g : { near:gn, far:gf, delta:gn-gf},
                  b : { near:bn, far:bf, delta:bn-bf}
              };
              this.PALETTE[i] = C;
          }
      }
      
      this.buildTables = function() {
          // precompute values for expensive math ops
          // we already know the field of view and horizontal screen res,
          // and thus the degrees of view spanned by a single sliver of res,
          // so we compute the trig values for enough slivers to cover 360 deg.
          
          // initialize the tables
          this.TABLE_INV_SIN = new Array();
          this.TABLE_INV_COS = new Array();
          this.TABLE_TAN = new Array();
          this.TABLE_INV_TAN = new Array();
          this.QUAD_BOUNDARIES = new Array();
          this.TABLE_REFLECTANCE_LATITUDE = new Array();
          this.TABLE_REFLECTANCE_LONGITUDE = new Array();
          
          // define some unit circle constants
          var PI_1over2 = Math.PI * 1 / 2; //  90 degrees
          var PI_1over1 = Math.PI * 1;     // 180 degrees
          var PI_3over2 = Math.PI * 3 / 2; // 270 degrees
          var PI_2over1 = Math.PI * 2;     // 360 degrees
          
          // walk around the unit circle, jotting down trig values along the way.
          // we need to look out for horizontal and vertical asymptotes, where tangent
          // goes to infinity, and substitute a grossly underestimated value that
          // won't break our calculations.
          // also, when we cross an asymptote, we'll record the index i
          // QUAD_
          var quadrant = 0;
          var angle = 0;
          for (var i = 0; i < this.TABLE_ENTRIES; i++) {
              var cosine = Math.cos(angle);
              var sine = Math.sin(angle);
              var absCosine = Math.abs(cosine);
              var absSine = Math.abs(sine);
              
              if (absCosine == 0 || absSine == 1) {
                  // 90 or 270 degrees
                  this.TABLE_TAN[i] = -this.INFINITY;
                  this.TABLE_INV_TAN[i] = 0;
                  if (quadrant == 1) { this.TABLE_INV_COS[i] = -this.INFINITY; }
                  else               { this.TABLE_INV_COS[i] = this.INFINITY; }
                  this.TABLE_INV_SIN[i] = 1 / sine;
                  this.QUAD_BOUNDARIES[quadrant] = i;
                  quadrant++;
              }
              else if (absCosine == 1 || absSine == 0) {
                  // 0 or 180 degrees
                  this.TABLE_TAN[i] = 0;
                  this.TABLE_INV_TAN[i] = this.INFINITY;
                  if (quadrant == 0) { this.TABLE_INV_SIN[i] = this.INFINITY; }
                  else               { this.TABLE_INV_SIN[i] = -this.INFINITY; }
                  this.TABLE_INV_COS[i] = 1 / cosine;
                  this.QUAD_BOUNDARIES[quadrant] = i;
                  quadrant++;
              }
              else {
                  // no asymptotes to worry about
                  this.TABLE_TAN[i] = sine / cosine;
                  this.TABLE_INV_TAN[i] = cosine / sine;
                  this.TABLE_INV_COS[i] = 1 / cosine;
                  this.TABLE_INV_SIN[i] = 1 / sine;
              }
              
              // for specular lighting,
              // precalculate the cosine of the angle between
              // every ray and the surface normal of:
              //   1) a latitudinal (horizontal) surface
              //   2) a longitudinal (vertical) surface
              // the calculation requires that the angle be [0,PI/2]
              var h = 0;
              var v = 0;
              switch (quadrant-1) {
                  case 0:
                  h = PI_1over2 - angle;
                  v = angle;
                  break;
                  
                  case 1:
                  h = angle - PI_1over2;
                  v = PI_1over1 - angle;
                  break;
                  
                  case 2:
                  h = PI_3over2 - angle;
                  v = angle - PI_1over1;
                  break;
                  
                  case 3:
                  h = angle - PI_3over2;
                  v = PI_2over1 - angle;
                  break;
              }
              this.TABLE_REFLECTANCE_LATITUDE[i]  = Math.sin( Math.min(PI_1over2, Math.max(0, h)) );
              this.TABLE_REFLECTANCE_LONGITUDE[i] = Math.cos( Math.min(PI_1over2, Math.max(0, v)) );
              
              angle += this.SLIVER_ARC;
          }
          
          // pre-compute view correction values for each sliver
          this.TABLE_VIEW_CORRECTION = new Array();
          var FOVangle = this.SLIVER_ARC * (-Math.round(this.FOV*.5));
          for (var sliver = 0; sliver < this.RES.w; sliver++) {
              this.TABLE_VIEW_CORRECTION[sliver] = Math.cos(FOVangle); // minimal fish-eye
              //this.TABLE_VIEW_CORRECTION[sliver] = 1 / Math.cos(FOVangle); // extra fish-eye! cool.
              FOVangle += this.SLIVER_ARC;
          }
      }
      
      this.buildTables();
      
  }
  


(C) Æliens 20/2/2008

You may not copy or print any of this material without explicit permission of the author or the publisher. In case of other copyright issues, contact the author.