Cacomania: Canvas Textured raycaster

Cacomania

Canvas Textured raycaster

Guido Krömer - 5. January 2013 - Tags: , ,

Welcome to the second part of my little raycaster series. In this part I am going to explain how to render the scene with textured walls. How to render an untextured scene can be read in the previous part.

Raycaster @ Chrome 23 with a scale of 3
Just a screen shot of the raycaster running @ Chrome version 23.

The textures used in the tutorial are from the Freedoom project.

Gray theory

Drawing textured walls is not so hard, instead of drawing a vertical line with the given size on the screen a slice of the image, corresponding with the wall, gets drawn with the given height.

For each ray the corresponding part of the texture gets drawn
In the graphic above the corresponding part of the wall texture hit by a ray gets drawn with a height determined by the length of the ray.

Implementation

The slice which has to be drawn gets determined by the this.getLine() method. If the ray hits a wall/tile the part of the wall will be stored into the lineElement.part field. The image which the slice belongs to will be stored into the lineElement.texture field. The this.getLine() method is, as mentioned in previous tutorial, a modified versions of Bresenham's line algorithm.

this.getLine = function(x1, y1, x2, y2, lineElement) {
  var dx = Math.abs(x2 - x1);
  var dy = Math.abs(y2 - y1);
  var sx = (x1 < x2) ? 1 : -1;
  var sy = (y1 < y2) ? 1 : -1;
  var err = dx - dy;
  var e2;
  var perviousTileX = 0;
  var perviousTileY = 0;

  while (!((x1 == x2) && (y1 == y2))) {
    e2 = err << 1;
    if (e2 > -dy) {
      err -= dy;
      x1 += sx;
    }
    else if (e2 < dx) {
      err += dx;
      y1 += sy;
    }

    var mapX = Math.floor(x1 / TILE);
    var mapY = Math.floor(y1 / TILE);

    if (this.map[mapY][mapX]) {
      lineElement.y = y1;
      lineElement.x = x1;
      lineElement.texture = textures[this.map[mapY][mapX]];
      lineElement.north = perviousTileX == mapX;
      lineElement.part = lineElement.north ? x1 - (mapX * TILE) : y1 - (mapY * TILE)
      return;
    }
    perviousTileX = mapX;
    perviousTileY = mapY;
  }
}

The textures array gets filled through the this.loadTextures() method. The first step is determining which textures are used in the tile based map, here is a little space for optimization since all textures until the max texture id gets loaded and not only the actually used ones.

this.loadTextures = function() {
    var maxTexture = 0;
    for (var y = 0; y < this.map.length; y++) {
        for (var x = 0; x < this.map[y].length; x++) {
            maxTexture = Math.max(maxTexture, this.map[y][x]);
        }
    }

    for (var i = 0; i <= maxTexture; i++) {
        var texture = new Image();
        texture.src = 'Textures/' + i + '.png';
        textures.push(texture)
    }
}

All textures are stored in the "Textures" sub folder, the id/number of the texture corresponds to the file name: The really naive organisation structure of the textures
This way is really naive, but good enough if the number of textures is low.

Drawing a slice of the texture on the canvas screen can be done using drawImage() method, instead of telling the method where the image should be drawn on screen it is possible to define which part of an image should be drawn where on the screen with which width and height. How the third overload of the drawImage() method work is really good explained, here. The slice drew by the raycaster is one pixel with, remember a ray gets cast for each pixel in the width. This is the call of the method for drawing a single ray: ctx.drawImage(texture, Math.floor(lineElement.part * (texture.width / TILE)), 0, 1, texture.height, i, SCR_H_HALF - wallFactor, 1, wallFactor * 2)

But just drawing textured walls does not make a good feel of depth, as like in the untextured version the walls should be drawn darker when they are far away from the player, therefore above each texture slice a black line gets drawn. The line will be drawn with an alpha value, the larger the value is the more less transparent the wall overlay will be drawn.

ctx.globalAlpha = lineElement.dist / 1000 * (lineElement.north ? 1 : 1.5);
ctx.fillStyle = "black";
ctx.beginPath();
ctx.moveTo(i, SCR_H_HALF - wallFactor);
ctx.lineTo(i, SCR_H_HALF + wallFactor);
ctx.closePath();
ctx.stroke();

Rendering a textured floor or ceiling might be a little overkill since each pixel must be drawn manually, which is slow using the 2D canvas element. But the feel or depth can be improved by drawing a gradient as ceiling and floor, which gets darker in the rear. This is what the this.drawBackgound() method does. Here is a little bit space for improvement, too. Instead of drawing the gradient again and again this should be done just one time.

 this.drawBackgound = function(ctx) {
  var grd = ctx.createLinearGradient(0,SCR_H_HALF,0,0);
  grd.addColorStop(0,"black");
  grd.addColorStop(1,"grey");
  ctx.fillStyle = grd;
  ctx.fillRect(0, 0, SCR_W, SCR_H_HALF)

  grd = ctx.createLinearGradient(0,SCR_H_HALF,0,SCR_H);
  grd.addColorStop(0,"black");
  grd.addColorStop(1,"grey");
  ctx.fillStyle = grd;
  ctx.fillRect(0, SCR_H_HALF, SCR_W, SCR_H);
}

A little bit more retro plz

I think filtered textures destroy the retro feeling, this can be archived by disabling the image smoothing in Webkit based browsers or Mozilla Firefox. For gaining access the canvas context out of the draw method the GameManager class had to be quick and dirty extended with a new getter for the canvas context.

this.getCtx = function() {
    return ctx;
}

Now the init() method is able to access the context object and disable the image smoothing in both browsers.

var ctx = gameManager.getCtx();
ctx.webkitImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;

Raycaster with image smoothing enabled (right) and disabled (left)
The splitted image above shows one half with enabled smoothing and the other without. Pixel can be so beautiful :) .

Scaling the raycaster up and rendering at a lower resolution improves the performance significant, but the retro-effect gets boosted, too. The first screen shot was taken using Google's Chrome which soothes the upscaled image, the second one with the Firefox which show nice large pixel. Raycaster in 5 differen scale factors/resolutions (1,2,3,5 and 10)
The scale factors are: 1, 2, 3, 5 and 10. Textured raycaster with different scaling on firefox, scaling 1,2,3,5,10 and 20!
The used scale factors are: 1, 2, 3, 5, 10 and 20. But I have to admit that a scale factor of 20 is really extreme.

Here is the whole raycaster code, the rest can be found in the Gist as usual.

That's it guys, I hope you enjoyed my little article about raycasting. A demo is located here.