Skip to content
Vinicius Reif Biavatti edited this page Oct 11, 2019 · 4 revisions

Buffer

I think this is the most important tutorial of the wiki. We didn't make this before to focusing in the simplest way to create the RayCasting. With the buffer, we will obtain performance to render our application, and it will enable to us to use each pixel processing instead of intervals (lines). If we don't use buffer for pixel processing, the renderization will be so slow.

The buffer is just an image data that will be used for draws, and after every draw, just the image buffered will be rendered. In this tutorial, we will need to change all drawer functions we have to use the buffer instead of the screen context.

The attribute we will define to be the buffer will be the projection image data and it is represented by a pixel array. This array has 4 (four) positions for every pixel we have, and each position represents the RGBA (Red, Green, Blue and Alpha) values. In this case, we will need to manipulate this array to draw the pixels inside. To know what is the correct position in relation of the projection as a matrix, we will use this formula to access the indexes:

Formula: index = 4 * (x + y * data.projection.width)

The multiplied 4 is the offset for the RGBA values and the y * data.projection.width is the offset for each y-axis position.

The first thing we will do is define the buffer. We will define two more attrbutes for our data.projection object. The first is the image data reference, and the second is the buffer.

Note: The image data is not the buffer. This is a couple of information we need to draw in the canvas. The buffer is the imageData.data that is the pixel array we will manipulate.

// Data
let data = {
    // ...
    projection: {
        // ...
        imageData: null,
        buffer: null
    }
    // ...
}

So now we will define the new attributes after the canvas creation. The imageData will be a created image of our screen context. The buffer will be the data of this image.

// Buffer
data.projection.imageData = screenContext.createImageData(data.projection.width, data.projection.height);
data.projection.buffer = data.projection.imageData.data;

The next thing we will do is change the drawer functions. Note that there aren't functions to draw lines, rects, etc, in the image data so, we will need to create by ourself. The second thing we will create is a color object with RGBA attributes.

/**
 * Color object
 * @param {number} r 
 * @param {number} g 
 * @param {number} b 
 * @param {number} a 
 */
function Color(r, g, b, a) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
}

This is necessary because we will not use CSS color representation more. The buffer works with integer values with 0 to 255 interval.

For our first draw function, we will create a pixel drawer. This function will be called by drawPixel(x, y, color) and it work is to put the color in the correct position of the buffer. In this step, we will use the formula we checked above.

/**
 * Draw pixel on buffer
 * @param {number} x 
 * @param {number} y 
 * @param {Color} color 
 */
function drawPixel(x, y, color) {
    let offset = 4 * (Math.floor(x) + Math.floor(y) * data.projection.width);
    data.projection.buffer[offset  ] = color.r;
    data.projection.buffer[offset+1] = color.g;
    data.projection.buffer[offset+2] = color.b;
    data.projection.buffer[offset+3] = color.a;
}

After define the function, we will change the implementation of the other drawer function. To be easier, we will re-create these functions to use the buffer. The first function we will work is drawLine(). The header of the new line drawer function will be drawLine(x, y1, y2, color).

Note: We will not define the second x for the line drawer function because in RayCasting we don't need to create diagonal lines.

This function will draw pixels from the y1 to y2 in the x-axis specified as parameter. To do this we will need a for loop and we will use our new drawPixel() function.

/**
 * Draw line in the buffer
 * @param {Number} x 
 * @param {Number} y1 
 * @param {Number} y2 
 * @param {Color} color 
 */
function drawLine(x1, y1, y2, color) {
    for(let y = y1; y < y2; y++) {
        drawPixel(x1, y, color);
    }
}

After it, we will change the drawers of rayCasting() function to use the correct parameters of the new drawLine() function.

// Draw
drawLine(rayCount, 0, data.projection.halfHeight - wallHeight, new Color(0, 0, 0, 255));
drawTexture(rayCount, wallHeight, texturePositionX, texture);
drawLine(rayCount, data.projection.halfHeight + wallHeight, data.projection.height, new Color(95, 87, 79, 255));

Check that we have to change the implementation of the drawTexture() function too so, in this function we will call our drawLine() function instead of draw the line with native function.

/**
 * Draw texture
 * @param {*} x 
 * @param {*} wallHeight 
 * @param {*} texturePositionX 
 * @param {*} texture 
 */
function drawTexture(x, wallHeight, texturePositionX, texture) {
    // ...
    let color = null;
    for(let i = 0; i < texture.height; i++) {
        // ...
        drawLine(x, y, Math.floor(y + (yIncrementer + 0.5)), color);
        y += yIncrementer;
    }
}

Note that the color of the texture uses the CSS format color. We will need to change it too to use our Color object instead. In the parseImageData() function we will change it.

/**
 * Parse image data to a css rgb array
 * @param {array} imageData 
 */
function parseImageData(imageData) {
    let colorArray = [];
    for (let i = 0; i < imageData.length; i += 4) {
        colorArray.push(new Color(imageData[i], imageData[i + 1], imageData[i + 2], 255));
    }
    return colorArray;
}

Clone this wiki locally