Skip to content

Floorcasting

Vini edited this page May 6, 2024 · 29 revisions

Floorcasting

Floorcasting is the name of the technique to draw floors in the Raycasting projection by some texture. If you don't want to render a texture in the floor, the Floorcasting is not needed because you just need to draw color lines from the end of the wall to the end of the projection. So, lets check how to create the Floorcasting.

There are some techniques (vertical strip render, horizontal strip render, etc...) that can be used to render the Floorcasting, but for this tutorial I will use the easiest one that I found (vertical strip render). It is not the best technique since the processing used is not so optimized.

To make the Floorcasting, we will start to iterate from the next pixel after the wall to the last pixel of the projection. For each pixel, we will calculate the distance of this pixel, and get the coordinates using this distance to discover the tile in the map. With the tile we can find the texture that will be used for this pixel. These steps are:

For each pixel under the wall until the end of the projection:

  1. Calculate the distance of this pixel
  2. Calculate the coordinates by the distance
  3. Find the tile in the map by the coordinates
  4. Get the texture of this tile
  5. Find the color in this texture by the coordinates
  6. Draw the pixel

It will be made for each column in the rendering processor (For each x coordinate). The image below explains a little bit about the iteration and the calc to get the distance

Floorcasting explanation

The first thing to do in the code is to create the function to render the floor, and call this function instead the drawLine() function we are calling to render the floor. The function will need as arguments the x coordinate to know witch column in the projection is the target, the wallHeight to know the position for the next pixel until the last pixel of the projection and the rayAngle to calculate the distance and the position of the tile.

/**
 * Floorcasting
 * @param {*} x1 
 * @param {*} wallHeight 
 * @param {*} rayAngle 
 */
function drawFloor(x1, wallHeight, rayAngle) {
}

So now, we will create the iteration between the pixels:

function drawFloor(x1, wallHeight, rayAngle) {
    start = data.projection.halfHeight + wallHeight + 1;
    for(y = start; y < data.projection.height; y++) {
    }
}

Good. Know, I will explain the formula we will use to calculate the distance of each pixel in the projection plane. This formula generates a distance value in the range of the pixel between the half height of the projection and the height of the projection. By knowing that the last pixel will have 1 of distance because this pixel is the closest pixel of the player, and the pixel in the half height on the projection will have the infinite distance, we can make the range using this formula:

  • Formula: pixel distance = projection height / (2 * pixel_y - projection height)
  • Code: distance = data.projection.height / (2 * y - data.projection.height)

For example, this formula will generate these values for a projection plane with height 20:

height = 20

pixel 11:  10.0
pixel 12:  5.0
pixel 13:  3.3333333333333335
pixel 14:  2.5
pixel 15:  2.0
pixel 16:  1.6666666666666667
pixel 17:  1.4285714285714286
pixel 18:  1.25
pixel 19:  1.1111111111111112
pixel 20:  1.0

Note: We didn't start to calculate the distance by the first pixel in the projection plane since the pixel in the middle of the height will have infinite distance, and the pixels before this will have negative distancies. The Floorcasting just render the floor, so only the pixels after the rendered wall will be processed (calculations always after the half height of the projection plane).

Note: These distance values for each pixel will always be the same, so, you can use some in-memory table with these values pre-calculated to save processing.

Ok, now lets coding this math part into our Floorcasting code. The first thing is to calc using the formula:

function drawFloor(x1, wallHeight, rayAngle) {
    start = data.projection.halfHeight + wallHeight + 1;
    for(y = start; y < data.projection.height; y++) {
        // Create distance and calculate it
        distance = data.projection.height / (2 * y - data.projection.height)
    }
}

Ok, by knowing just the distance is not the enough to get the position of the pixel in the map. We have to discover the location (x, y) of the pixel. To do this, we can get the Cosine and Sine values from the rayAngle to discover the incrementers (direction) to that location. So, before the loop starts, lets define the cos and sin of the ray. And after it, we can just multiply these values with the distance to get the location of the casted floor.

function drawFloor(x1, wallHeight, rayAngle) {
    start = data.projection.halfHeight + wallHeight + 1;
    directionCos = Math.cos(degreeToRadians(rayAngle))
    directionSin = Math.sin(degreeToRadians(rayAngle))
    for(y = start; y < data.projection.height; y++) {
        // Create distance and calculate it
        distance = data.projection.height / (2 * y - data.projection.height)

        // Get the tile position
        tilex = distance * directionCos
        tiley = distance * directionSin
    }
}

Ok, but it is still not working. We calculated the distance and the position from the zero coords, not relative from the player coords. To offset with the player coords and get the exact location, just sum the tile coords with the player coords. This is necessary because if you are in the 0,0 position in the map, it will work fine, but if you move the player, the position of the floor will be the same, not the real from the player.

function drawFloor(x1, wallHeight, rayAngle) {
    start = data.projection.halfHeight + wallHeight + 1;
    directionCos = Math.cos(degreeToRadians(rayAngle))
    directionSin = Math.sin(degreeToRadians(rayAngle))
    for(y = start; y < data.projection.height; y++) {
        // Create distance and calculate it
        distance = data.projection.height / (2 * y - data.projection.height)

        // Get the tile position
        tilex = distance * directionCos
        tiley = distance * directionSin
        tilex += data.player.x
        tiley += data.player.y
    }
}

Now we have the correct coords to get the tile in the map. So, let's do that! To get the tile just access the map matrix and get by positions. To get the coords, we have to round our values with Math.floor(). After get the tile, we will get the texture of this tile too. For guarantee, we will create a validation to avoid get wrong textures (If I discover the reason of errors here, I will update this part of the tutorial to make correctly).

function drawFloor(x1, wallHeight, rayAngle) {
    start = data.projection.halfHeight + wallHeight + 1;
    directionCos = Math.cos(degreeToRadians(rayAngle))
    directionSin = Math.sin(degreeToRadians(rayAngle))
    for(y = start; y < data.projection.height; y++) {
        // Create distance and calculate it
        distance = data.projection.height / (2 * y - data.projection.height)

        // Get the tile position
        tilex = distance * directionCos
        tiley = distance * directionSin
        tilex += data.player.x
        tiley += data.player.y
        tile = data.map[Math.floor(tiley)][Math.floor(tilex)]

        // Get texture
        texture = data.floorTextures[tile]
        if(!texture) {
            continue
        }
    }
}

Note: I am getting the texture from the data.floorTextures array that I will create later to store the floor textures

After get the texture, we have to get the color. To get this, we will be sure to keep the values inside the range of the texture using mod (%) and we will multiply the values by the texture size, to repeat by the correct size of the tile in the map. Again, we have to remmember the Math.floor to cast the values to integer before get the color. And to finish the Floorcasting, we just need to draw the color into the projection by the x coordinate got from argument and the y coordinate got from the for iterator

Note: Remmember that the texture color array is not a matrix, so we need to calculate the coordinates position first using: x + y * width

// Get texture
// ...

// Define texture coords
texture_x = (Math.floor(tilex * texture.width)) % texture.width
texture_y = (Math.floor(tiley * texture.height)) % texture.height

// Get pixel color
color = texture.data[texture_x + texture_y * texture.width];
drawPixel(x1, y, color)

To start drawing the floor, lets import the texture like the other parts of the tutorial. For this, I created a new texture array in data object with the floor textures only, and I changed the loadTexture() function to load the floorTextures too. The texture I used to test the FloorCasting is the texture below:

Image: test_floor.png

floor texture

First I added the texture in the HTML file to load this

File: Raycasting.html

<body>
    <img id="texture" src="texture.png" style="display: none">
    <img id="background" src="background.png" style="display: none">
    <img id="tree" src="sprite.png" style="display: none">
    <img id="floor-texture" src="floor.png" style="display: none;"> <!-- Floor texture -->
</body>

After add, I changed the data with the new floorTextures array to organize the data of the algorithm

data = {
    // ...
    floorTextures: [
        {
            width: 16,
            height: 16,
            id: "floor-texture",
            data: null
        }
    ],
    // ...
}

And after this, lets change the texture loader routine to load the floor textures too

/**
 * Load textures
 */
function loadTextures() {
    for(let i = 0; i < data.textures.length; i++) {
        if(data.textures[i].id) {
            data.textures[i].data = getTextureData(data.textures[i]);
        }
    }
    for(let i = 0; i < data.floorTextures.length; i++) {
        if(data.floorTextures[i].id) {
            data.floorTextures[i].data = getTextureData(data.floorTextures[i]);
        }
    }
}

So nice! Now, we have just to change the rayCasting() function to draw the floor, not just a line:

// Draw
drawBackground(rayCount, 0, data.projection.halfHeight - wallHeight, data.backgrounds[0]);
drawTexture(rayCount, wallHeight, texturePositionX, texture);
drawFloor(rayCount, wallHeight, rayAngle)

Let's test the result!

wrong floorcasting result

As you can see there is something going wrong. The first thing is the blank line after the wall. This happens because the routine that fix the "canvas half pixel" in html5. To fix it, we just need to increase the fixer in the wall drawing function. This avoid that the first pixel that we will check in the Floorcasting is a wall pixel.

function drawTexture(x, wallHeight, texturePositionX, texture) {
    // ...
    drawLine(x, y, Math.floor(y + (yIncrementer + 2)), color); // Changed to 2 instead of 0.5
    // ...
}

And the other big problem is the fact that the floor looks has some inverted fisheye effect. This happens because we are using the pixel distance without consider the ray angle and the player angle. To fix it, we can use trigonometry. The image below shows that the distance used for each pixel is the value of the adjacent side of the right triangle. We have to make the opposite of the (Fisheye fix) in the other tutorial. We have to use the hypotenuse distance of it. So, checking the SOH CAH TOA formulas we can discover that we can use the COS formula again to fix it

Inverse fish eye in floor casting

Like the fisheye correction, lets fix the distance using the SOH CAH TOA formula. The formula we can use to fix this is the cossine formula like below:

hypotenuse = x
angle = 30
adjacent_side = 10

// Formula
COS(angle) = adjacent_side / hypotenuse

// Rotate formula to solve the problem
COS(angle) = adjacent_side / hypotenuse
COS(angle) * hypotenuse = (adjacent_side / hypotenuse) * hypotenuse // (Add multiplier)
COS(angle) * hypotenuse = adjacent_side // (Remove redundant)
(COS(angle) * hypotenuse) / COS(angle) = adjacent_side / COS(angle) // (Add  divider)
hypotenuse = adjacent_side / COS(angle) // (Remove redundant) Right!

So now, we have the formula. To fix that we just need to apply this for the distance of the pixel. For the angle, we need to remmember that this angle needs to be the angle of the ray, without the angle of the player, so, the code will be:

  • Formula: adjacent side = hypotenuse * COS(ray angle - player angle)
  • Code: distance = distance * Math.cos(rayAngle - data.player.angle)

Let's add this to the source code!

function drawFloor(x1, wallHeight, rayAngle) {
    // ...

    // Create distance and calculate it
    distance = data.projection.height / (2 * y - data.projection.height)
    distance = distance / Math.cos(degreeToRadians(playerAngle) - degreeToRadians(rayAngle)) // Inverse fisheye fix

    // ...
}

Note: We can use this same logic to render the Ceilcasting. We have only to adapt to the ceil intead of the floor.

Now it is done! I changed the texture to be better, but I recommend the previous texture to test the result since this has the bordes to help check the position of it in relation of the floor. The next texture is this:

Image: floor.png

Floor texture

And the result of our code now is this!

Floorcasting result

The result code for Floorcasting is:

/**
 * Floorcasting
 * @param {*} x1 
 * @param {*} wallHeight 
 * @param {*} rayAngle 
 */
function drawFloor(x1, wallHeight, rayAngle) {
    start = data.projection.halfHeight + wallHeight + 1;
    directionCos = Math.cos(degreeToRadians(rayAngle))
    directionSin = Math.sin(degreeToRadians(rayAngle))
    playerAngle = data.player.angle
    for(y = start; y < data.projection.height; y++) {
        // Create distance and calculate it
        distance = data.projection.height / (2 * y - data.projection.height)
        distance = distance / Math.cos(degreeToRadians(playerAngle) - degreeToRadians(rayAngle))

        // Get the tile position
        tilex = distance * directionCos
        tiley = distance * directionSin
        tilex += data.player.x
        tiley += data.player.y
        tile = data.map[Math.floor(tiley)][Math.floor(tilex)]
        
        // Get texture
        texture = data.floorTextures[tile]

        if(!texture) {
            continue
        }

        // Define texture coords
        texture_x = (Math.floor(tilex * texture.width)) % texture.width
        texture_y = (Math.floor(tiley * texture.height)) % texture.height
        
        // Get pixel color
        color = texture.data[texture_x + texture_y * texture.width];
        drawPixel(x1, y, color)
    }
}