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 Color 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;
}

Now we need to render our buffer in our screen. For this we need to create the last function of this tutorial step called by renderBuffer(). This function will be called from the main loop, after all of the process function. If we just draw the buffer directally in the screen, it will not be resized in relation of the scale. To correct this, we will create a canvas in memory and draw the buffer there, and after it, draw this canvas in the screen.

Note: Create a canvas in memory is necessary because the function we will use to render the buffer image is not a draw function. Just draw functions take the scale for the draws. If we put our image in some canvas first, we will be able to use the drawImage() function to take the scale for this draw.

Lets create the function, and create the canvas with the projection dimension. After it, we will get the context of this canvas to use the putImageData() function to put our buffered image into the it. After it, we will use the drawImage() function of the screen context to draw the canvas generated image.

/**
 * Render buffer
 */
function renderBuffer() {
    let canvas = document.createElement('canvas');
    canvas.width = data.projection.width;
    canvas.height = data.projection.height;
    canvas.getContext('2d').putImageData(data.projection.imageData, 0, 0);
    screenContext.drawImage(canvas, 0, 0);
}

/**
 * Main loop
 */
function main() {
    mainLoop = setInterval(function() {
        // ...
        renderBuffer();
    }, data.render.dalay);
}

Good! If you run the application now, we will check that the image will be smoothed. To resolve it, we will disable this property of our screen context. The name of this property is imageSmoothingEnabled.

// Canvas context
// ...
screenContext.imageSmoothingEnabled = false;

If we test it, we will discover that the player will move so fast. This is because the buffer implementation gave us performance. To fix it, we will need to change the player speed. Remember you can use values you like for this.

// Data
let data = {
    // ...
    player: {
        // ...
        speed: {
            movement: 0.02,
            rotation: 0.7
        }
    }
    // ...
}

Well done! We finished the buffer step. It is for me one of the most important tutorial steps of the wiki because it is so important for the performance of the rendering. For the next things we will do, this is pre-requisite for a good processing.

Code

Check the result code of this step below:

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

/**
 * 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;
}

/**
 * Draw pixel on buffer
 * @param {number} x 
 * @param {number} y 
 * @param {RGBA Object} 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;
}

/**
 * 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);
    }
}

/**
 * Main loop
 */
function main() {
    mainLoop = setInterval(function() {
        clearScreen();
        movePlayer();
        rayCasting();
        renderBuffer();
    }, data.render.dalay);
}

/**
 * Render buffer
 */
function renderBuffer() {
    let canvas = document.createElement('canvas');
    canvas.width = data.projection.width;
    canvas.height = data.projection.height;
    canvas.getContext('2d').putImageData(data.projection.imageData, 0, 0);
    screenContext.drawImage(canvas, 0, 0);
}

/**
 * Parse image data to a Color 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;
}