Skip to content

In Memory Textures

Vinicius Reif Biavatti edited this page Oct 9, 2019 · 6 revisions

In Memory Textures

In this step, we will draw the wall based on some texture. This texture will be written inside the code so, it will be an integer matrix, where each number will identifier some color. This is the concept of memory texture. This step is not so hard, but I recommend you to understand about module (%) operator. This operator is used to get the position in some number interval, without the number gets out of the interval bounds.

Note: We will not load external textures in this step to make our work easier, but in other tutorial we will use external texture files to put in out RayCasting.

To start this step, we will add some new attributes for out data. The new root atributte will be a list of textures, and each list item will have some data of the textures like width, height, bitmap and colors list. Check the table to understand what each attribute is.

Attribute Description
Texture width The width of the bitmap
Texture height The height of the bitmap
Texture bitmap The bitmap (integer matrix) of the texture
Texture colors The list of the colors that correspond to the integer in the bitmap matrix position

Note: We will use just one texture, but you can add more to use in your map.

The addictional code will be:

// Data
let data = {
    // ...
    textures: [
        {
            width: 8,
            height: 8,
            bitmap: [
                [1,1,1,1,1,1,1,1],
                [0,0,0,1,0,0,0,1],
                [1,1,1,1,1,1,1,1],
                [0,1,0,0,0,1,0,0],
                [1,1,1,1,1,1,1,1],
                [0,0,0,1,0,0,0,1],
                [1,1,1,1,1,1,1,1],
                [0,1,0,0,0,1,0,0]
            ],
            colors: [
                "rgb(255, 241, 232)",
                "rgb(194, 195, 199)",
            ]
        }
    ]
}

The textured we used has 8x8 dimension, but you can create bigger textures. Don't forget that bigger textures will need more render processing. This texture represents a brick wall, but you can change the bitmap as you want.

The logic of the texture processing in RayCasting can be separated as topics. This logic will works in the rayCasting() function:

  • Get the texture X position based on each throwed ray coordinates
  • Change the wall drawer function to it uses the texture colors

After discover the wall height, we will get the x coordinate of the texture based on ray coordinates. This is necessary to discover what is the texture strip we will use to draw in our projection. We have to multiply the position by the texture width to make the texture has the same wall width. After it, we will use the module (%) operator to keep the x-axis inside the texture width interval. For example:

  1. Ray coordinates: ray.x = 15 and ray.y = 23
  2. Texture size: texture.width = 8
  3. Texture width position: position = (ray.x + ray.y) * texture.width, position = (15 + 23) * 8, position = 304
  4. Texture interval offset: position = position % texture.width, position = 304 % 8, position = 0
  5. The column zero 0 of our texture will be used for the strip render

In our code, we will add these lines after get the wall height value. The first line is for get the texture that will be processed by the map integer value position. The second line is the calc to discover the x coodinate of the texture:

// ...

// Wall height
let wallHeight = Math.floor(data.projection.halfHeight / distance);

// Get texture
let texture = data.textures[wall - 1];

// Calcule texture position
let texturePositionX = Math.floor((texture.width * (ray.x + ray.y)) % texture.width);

// ...

Note: Remember that we have to parse the value to integer because this value will be used to get the color in the texture bitmax (integer matrix). The function we used is Math.floor()

After it, we will create the function to draw the texture. The function will have a loop to iterate the texture height and some calcs to divide the texture height to discover the positions to draw the line. The parameters of this function are:

  • x: the x-axis coordinate to draw the strip
  • wallHeight: To know the strip height
  • texturePositionX: The position x of the texture we calculed before
  • texture: the texture we got from ray collide
/**
 * Draw texture
 * @param {*} x 
 * @param {*} wallHeight 
 * @param {*} texturePositionX 
 * @param {*} texture 
 */
function drawTexture(x, wallHeight, texturePositionX, texture) {
}

Now, we will define two variables inside the function. The first variable is yIncrementer to know what is the value we will increment to our y-axis render cursor. The second is the y cursor that will be used to draw the line.

  • yIncrementer: This value is calculed based in the wallHeight. We have to divide the wall height by the texture height to discover what the value we will increment to our y cursor. Remender we need to multiply the wall height by 2 (two) because the wall height is defined based in the projection halfHeight.
  • y: This is the render cursor. We divided the strip by the texture height and we will use this variable to control the position of the lines we need to draw to create our strip.
/**
 * Draw texture
 * @param {*} x 
 * @param {*} wallHeight 
 * @param {*} texturePositionX 
 * @param {*} texture 
 */
function drawTexture(x, wallHeight, texturePositionX, texture) {
    let yIncrementer = (wallHeight * 2) / texture.height;
    let y = data.projection.halfHeight - wallHeight;
}

Now we just need to create the loop to iterate the texture height and draw the strip lines usign the cursor. Inside the loop, we need to get the color that will be used for the strip. To get the color, we will check the texture bitmap using the coordinates we have.

/**
 * Draw texture
 * @param {*} x 
 * @param {*} wallHeight 
 * @param {*} texturePositionX 
 * @param {*} texture 
 */
function drawTexture(x, wallHeight, texturePositionX, texture) {
    let yIncrementer = (wallHeight * 2) / texture.height;
    let y = data.projection.halfHeight - wallHeight;

    for(let i = 0; i < texture.height; i++) {
        screenContext.strokeStyle = texture.colors[texture.bitmap[i][texturePositionX]];
    }
}

All right! The next step is draw the line. Now we have the color, the strip cursor and the incrementer.

/**
 * Draw texture
 * @param {*} x 
 * @param {*} wallHeight 
 * @param {*} texturePositionX 
 * @param {*} texture 
 */
function drawTexture(x, wallHeight, texturePositionX, texture) {
    let yIncrementer = (wallHeight * 2) / texture.height;
    let y = data.projection.halfHeight - wallHeight;

    for(let i = 0; i < texture.height; i++) {
        screenContext.strokeStyle = texture.colors[texture.bitmap[i][texturePositionX]];
        screenContext.beginPath();
        screenContext.moveTo(x, y);
        screenContext.lineTo(x, y + (yIncrementer + 0.5));
        screenContext.stroke();
        y += yIncrementer;
    }
}

Note: The plus value '0.5' in the yIncrementer is used to draw the line in the entire pixel. HTML5 canvas considers the half of the pixel for drawing. If you remove it, the textures can shows some white pixels after render.

The last thing to do is just change the wall drawer of our rayCasting() function to use the drawTexture() function. Let's enjoy and change the old colors we used to render the ceiling and floor to better colors.

// ...

// Draw
drawLine(rayCount, 0, rayCount, data.projection.halfHeight - wallHeight, "black");
drawTexture(rayCount, wallHeight, texturePositionX, texture);
drawLine(rayCount, data.projection.halfHeight + wallHeight, rayCount, data.projection.height, "rgb(95, 87, 79)");

// ...

Congratulations! We can text our texture processing now running the code!

Result

Scale: 4

Example of RayCasting with scale four for projection with textures on the walls

Scale: 1

Example of RayCasting with scale one for projection with textures on the walls

Code

The entire code of this step is:

/**
 * Raycasting logic
 */
function rayCasting() {

    // ...

    // Get texture
    let texture = data.textures[wall - 1];

    // Calcule texture position
    let texturePositionX = Math.floor((texture.width * (ray.x + ray.y)) % texture.width);

    // Draw
    drawLine(rayCount, 0, rayCount, data.projection.halfHeight - wallHeight, "black");
    drawTexture(rayCount, wallHeight, texturePositionX, texture);
    drawLine(rayCount, data.projection.halfHeight + wallHeight, rayCount, data.projection.height, "rgb(95, 87, 79)");

    // ...

}

/**
 * Draw texture
 * @param {*} x 
 * @param {*} wallHeight 
 * @param {*} texturePositionX 
 * @param {*} texture 
 */
function drawTexture(x, wallHeight, texturePositionX, texture) {
    let yIncrementer = (wallHeight * 2) / texture.height;
    let y = data.projection.halfHeight - wallHeight;

    for(let i = 0; i < texture.height; i++) {
        screenContext.strokeStyle = texture.colors[texture.bitmap[i][texturePositionX]];
        screenContext.beginPath();
        screenContext.moveTo(x, y);
        screenContext.lineTo(x, y + (yIncrementer + 0.5));
        screenContext.stroke();
        y += yIncrementer;
    }
}

Well done. The next tutorial will teach how to use external textures.