Skip to content
Vinicius Reif Biavatti edited this page Apr 16, 2021 · 9 revisions

Projection

In the basic tutorial, we drawed the lines in the entire screen but, in old games, there aren't good performance computers to render 640x480 images so, the stripes was drawn in a little panel and in the render step, it was resized based on the scale for a original monitor size. This method loses some quality, but it can be very fast in relation of the computer performance. In the intermediary tutorial, we will learn how to render textures on the wall. The method we will use needs more computer processing so, we will implement a scale projection from now.

Note: The code used for this tutorial is the result code of the basic tutorial so, if you dont completed the basic tutorial, do it to continue.

For this step, we will change some code parts, beginning from the attributes. We will create other object called projection that will have the correct render sizes and the screen will keep the original sizes like before. The canvas will have the original size, but the logic of the code will use the projection size to draw the stripes.

The projection sizes will be defined using the scale new attribute of the screen object. Check this table:

Attribute Description Value
Projection width The projection width calculed from the screen scale Screen width / scale
Projection height The projection height calculed from the screen scale Screen height / scale
Projection half width The half width of projection Projection width / 2
Projection half height The half height of projection Projection height / 2
Screen scale The scale of the screen 1 to n

In the code, the attributes are:

// Data
let data = {
    screen: {
        // ...
        scale: 4
    },
    projection: {
        width: null,
        height: null,
        halfWidth: null,
        halfHeight: null
    },
// ...

Note: I this case I used the value 4 (four) to the scale, but you can use the value you want. The value 1 (one) is the best definition.

We use null values for the projection because these values will be calculed based on screen attributes. Now, we will make the calcs of the attributes created before. Other thing we will do is define the incrementAngle of the rays of RayCasting to consider the projection width.

// Calculated data
// ...
data.projection.width = data.screen.width / data.screen.scale;
data.projection.height = data.screen.height / data.screen.scale;
data.projection.halfWidth = data.projection.width / 2;
data.projection.halfHeight = data.projection.height / 2;
data.rayCasting.incrementAngle = data.player.fov / data.projection.width;

Good! Now, we have to set the scale of the canvas to render the image with the correct size we want. If we dont do this step, the rendered image will have the projection size. In the canvas creation step, we will add the scale configuration to the screen context. Also in this step, we will call the translate method from the context to set the definition for our lines. It will fix the lines with the scale 1 (one) because the canvas calculates from the half of a pixel to render the lines.

Important: The translate function will fix some pixels when the scale is 1 (one). If you don't use the 1 (one) scale for your implementation, you don't need to set the translate. This can change the pixel rendering and create some borders in the screen. Some draw steps will need to be refactored. In the other tutorials, I used scale 4 (four) for the implementation to guarantee we didn't need to change parts of the code.

// Canvas context
const screenContext = screen.getContext("2d");
screenContext.scale(data.screen.scale, data.screen.scale);
screenContext.translate(0.5, 0.5);

In the rayCasting() function, some parts need to be changed. The first part is the for loop of each ray. This loop considers the original width of the screen, but now we have to consider the projection width.

/**
 * Raycasting logic
 */
function rayCasting() {
    let rayAngle = data.player.angle - data.player.halfFov;
    for(let rayCount = 0; rayCount < data.projection.width; rayCount++) {
        // ...
    }
}

In the wallHeight variable, we need to use the half projection height to get the result so, and in the draw step we need to use the projection size too:

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

// ...

// Draw
drawLine(rayCount, 0, rayCount, data.projection.halfHeight - wallHeight, "cyan");
drawLine(rayCount, data.projection.halfHeight - wallHeight, rayCount, data.projection.halfHeight + wallHeight, "red");
drawLine(rayCount, data.projection.halfHeight + wallHeight, rayCount, data.projection.height, "green");

To finish this step, we will change the clearScreen() function too, to make it uses the projection size:

/**
 * Clear screen
 */
function clearScreen() {
    screenContext.clearRect(0, 0, data.projection.width, data.projection.height);
}

With our scale done, we can go to the memory texture step.