I come for theory help with a classic raycast algorithm (currently, based on DDA). The problem is not with the algorithm itself, but with the floor rendering code.
I left the code of my raycast render method below (actual DDA implementation is left out, I think that's working as expected). The basic idea it uses PLAYER_HEIGHT (position of camera, currently set to HORIZON), and the formula PLAYER_HEIGHT/(y- HORIZON). In the canvas, the bottom pixel of y is equal to SCREEN_HEIGHT, and goes “up” until it reaches HORIZON - 1. Canvas element pixels go from 0,0 (top left corner) to screen_width, screen_height (bottom right corner). That's the distance that will be used for interpolating the floor coordinates x and y according to the vector direction of the player and a camera plane perpendicular to the direction vector.
function render(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, currentTime: number) {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
movePlayer(deltaTime);
renderingActions(deltaTime);
// Clear the canvas
ctx.fillStyle = "black";
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
const buffer = FLOOR_TEXTURE_IMAGE_DATA.data;
const rayIncrement = FOV / SCREEN_WIDTH;
const wallsArray: {wallHeight: number,distance:number}[] = [];
const rayDirsArray: {rayDirX: number,rayDirY: number, distance: number, normRayAngle: number, wallHeight: number, side:number, wallStart:number}[] = [];
const dirX = Math.cos(normalizeAngle(playerAngle));
const dirY = Math.sin(normalizeAngle(playerAngle));
const planeX = -dirY ;
const planeY = dirX;
nearPlaneX1 = dirX - planeX;
nearPlaneX2 = dirX + planeX;
nearPlaneY1 = dirY - planeY;
nearPlaneY2 = dirY + planeY;
const dotDirLeft = (dirX * nearPlaneX1 + dirY * nearPlaneY1);
const dotDirRight = (dirX * nearPlaneX2 + dirY * nearPlaneY2);
const angleLeft = Math.acos(dotDirLeft);
const angleRight = Math.acos(dotDirRight);
const normalizedLeftX = nearPlaneX1/Math.sqrt((nearPlaneX1*nearPlaneX1) + (nearPlaneY1*nearPlaneY1));
const normalizedLeftY = nearPlaneY1/Math.sqrt((nearPlaneX1*nearPlaneX1) + (nearPlaneY1*nearPlaneY1));
const normalizedRightX = nearPlaneX2/Math.sqrt((nearPlaneX2*nearPlaneX2) + (nearPlaneY2*nearPlaneY2));
const normalizedRightY = nearPlaneY2/Math.sqrt((nearPlaneX2*nearPlaneX2) + (nearPlaneY2*nearPlaneY2));
for (let x = 0; x < SCREEN_WIDTH; x++) {
const rayAngle = playerAngle - FOV / 2 + x * rayIncrement;
const {distance, rayDirX, rayDirY, normRayAngle, side} = castRay(rayAngle);
// Calculate wall height once
const wallHeight = Math.min(SCREEN_HEIGHT,
(SCREEN_HEIGHT * WALL_SCALE) / (distance * DISTANCE_SCALE / WORLD_SCALE));
const wallStart = (HORIZON - wallHeight) / 2
wallsArray.push({wallHeight, distance});
rayDirsArray.push({rayDirX, rayDirY, distance, normRayAngle, wallHeight, side, wallStart});
}
for(let y = SCREEN_HEIGHT ; y > HORIZON; y--) {
const yDistance = ((HORIZON / (y - HORIZON)));
const floorXDirInc = yDistance*(normalizedRightX - normalizedLeftX)/ (SCREEN_WIDTH);
const floorYDirInc = yDistance*(normalizedRightY - normalizedLeftY) / (SCREEN_WIDTH);
floorYStart = worldToGrid(playerY) + yDistance*(normalizedLeftY);
floorXStart = worldToGrid(playerX) + yDistance*(normalizedLeftX);
for(let x = 0; x < SCREEN_WIDTH; x++){
const cellX = Math.floor(Math.abs(floorXStart));
const cellY = Math.floor(Math.abs(floorYStart));
floorXStart+= floorXDirInc;
floorYStart+=floorYDirInc;
const pixelDestIndex = (y * SCREEN_WIDTH + x) * 4;
if (debug) {
console.log(`FloorStartX ${floorXStart} FloorStartY ${floorYStart}`)
console.log(`CellX ${cellX} CellY ${cellY}`)
console.log(`WORLD posX ${playerX} posY${playerY} GRID posX ${worldToGrid(playerX)} posY ${worldToGrid(playerY)}`)
console.log(`FloorXInd ${floorXDirInc} FloorYInc ${floorYDirInc}`)
console.log(`MapRatio ${mapRatio} MapAcc ${mapAcc}`)
debug = false;
}
if((cellX + cellY) % 2){
buffer[pixelDestIndex] = 0; // Using fixed color for testing
buffer[pixelDestIndex + 1] = 0;
buffer[pixelDestIndex + 2] = 0;
buffer[pixelDestIndex + 3] = 255;
}else
buffer[pixelDestIndex] = 255; // Using fixed color for testing
buffer[pixelDestIndex + 1] = 255;
buffer[pixelDestIndex + 2] = 255;
buffer[pixelDestIndex + 3] = 255;
}
}
ctx.putImageData(FLOOR_TEXTURE_IMAGE_DATA, 0,0);
if(RENDER_WALLS)
wallsArray.forEach((wallProperties, index) => {
const brightness = Math.max(255 - (wallProperties.distance * DISTANCE_SCALE / WORLD_SCALE) * BRIGHT_FACTOR, 0);
ctx.fillStyle = `rgb(${brightness}, 0, 0)`;
ctx.fillRect(index, (SCREEN_HEIGHT - wallProperties.wallHeight) / 2, 1, wallProperties.wallHeight);
})
requestAnimationFrame((time) => render(canvas, ctx, time));
}
The main problem is in my rendering floor technique. New tiles “appear” below the walls. The desired effect would be, if there is a wall in a map position (7,4) the floor tiles around that should be always the same. But right now, as the player moves, “new” tiles slide from behind the wall, so depending on the player position and angle, near to (7,4) the floor tile may be appear white or blue, so it is not “in sync”.
I think the problem is the distance of the wall I get using DDA and the distance related to the difference between the current row in the screen space and my HORIZON is different, they are not “in sync” and it is normal because the are two different “types” of distances? I'm lost here. Thank you all for taking your time with this.
