Hey folks!
It’s been years since my last post on this site (game lists/downloads have stayed current though). I’ve built several new games since then, and now I’m back to talk raycasting. (Bella is doing great btw heh, picture of her below :D) Shes such a sweety.

Over the past few months I’ve been building a raycasting engine from scratch in javascript. I didn’t take many screenshots along the way, so this isn’t a blow-by-blow devlog, just a place to gather my thoughts as a sort of post-mortom. As someone who usually reaches for engines like Godot or Unity, rolling my own was surprisingly refreshing. I can shape it exactly to what I need.
Quick refresher: raycasting, popularized by id Software’s Wolfenstein 3D, is a rendering technique where you cast a ray for each vertical column of the screen. When a ray hits a wall, you render that slice of the wall using your available data (e.g., texture/column index).
But I go way beyond wolf3d in my renderer…
I support multi-height walls, variable-height floors and ceilings, far-wall rendering, (Above shorter nearer walls) and a per-pixel depth buffer. Sprites can appear above distant walls, and I have ‘counters’ (walls with a walkable floor on top), ‘bumps’ (prismatic floor rises you can step onto liek counters), and ‘holes’, deeper sub-floors with proper ravine transitions, plus liquids. Think Doom-style effects, but built entirely on DDA (Doom used sectors, not DDA), and much more.

DDA & the projection math (how this thing actually draws)
The hardest part wasn’t “making pixels go brr”, it was comprehending the projection math. Here’s the high level mental model I landed on, with the exact variables from my renderer.
1) From camera to ray
For each screen column screenColumnX, I build a ray on the camera plane:
camX = 2*(screenColumnX + 0.5)/WIDTH - 1
rayX = dirX + planeX * camX
rayY = dirY + planeY * camX
I also keep rayDirXRecip = 1/(rayX || 1e-9) and rayDirYRecip = 1/(rayY || 1e-9) around because DDA needs reciprocals a lot.
2) DDA = grid walking with constant cost
DDA (Digital Differential Analyzer) is a cheap way to march the ray through a tile grid one cell at a time. I set:
mapX = floor(player.x), mapY = floor(player.y)
deltaDistanceX = |1 / rayX|
deltaDistanceY = |1 / rayY|
stepX = rayX < 0 ? -1 : 1
stepY = rayY < 0 ? -1 : 1
sideDistanceX = (stepX < 0 ? (player.x - mapX) : (mapX + 1 - player.x)) * deltaDistanceX
sideDistanceY = (stepY < 0 ? (player.y - mapY) : (mapY + 1 - player.y)) * deltaDistanceY
Then loop: pick the smaller of sideDistanceX/Y, step in that axis, add deltaDistanceX/Y, and check the cell. When I hit a non-zero MAP[y][x], I’ve hit a wall; wallSide tells me if I crossed a vertical (X) or horizontal (Y) grid line.
This version uses Amanatides & Woo style parameters (1/|ray| strides) instead of the classic sqrt(1+(rayY/rayX)^2) form. Same result, fewer flops. More like voxels then a traditional raycaster.
(http://www.cse.yorku.ca/~amana/research/grid.pdf)

3) Perpendicular distance
The DDA gives me the distance to the grid line I just crossed (sideDistanceX or sideDistanceY). To turn that into the perpendicular distance used for correct wall height I do:
perpDist = (wallSide == 0 ? sideDistanceX - deltaDistanceX
: sideDistanceY - deltaDistanceY)
perpDist = max(0.01, perpDist)
Subtracting one deltaDistance* undoes the “one step too far” from earlier.
4) Projecting world to screen (why the wall lines have the right size)
Everything hangs on two ideas:
- The horizon is
HALF_HEIGHT. (Half the LOGICAL height, which is the height I used for the renderer, not the final output height, which is scaled up) - Your eye’s vertical position relative to a 2-unit wall turns into a scale:
eyeScale = HEIGHT * (2 - floorDepth - EYE) * 0.5
From similar triangles:
lineHeight = (HEIGHT / perpDist) | 0
bottomY = (HALF_HEIGHT + eyeScale / perpDist) | 0
topY = bottomY - lineHeight
That’s the canonical “sprite/wall projection” relationship in a raycaster, adapted for per-zone (My version of wolf3d style sectors but with variable floor and cieling heights, depths, and colors) floors (floorDepth) and player height EYE.
Tall walls / stacked segments

So that walls can be taller than 1 “unit” tall. 1 unit in my raycaster is 64 pixels... I split a “wall” struct into stacked slices (each one a “unit” or less on screen). For each slice I compute source srcY/srcH and draw via a function i amde called drawWallColumnImg(...). This is how I get multi-level / stacked geometry with correct UVs.

5) Texture addressing & side flipping
To render tetxures, I find where the ray actually hit the wall in world space:
hitX = player.x + perpDist * rayX
hitY = player.y + perpDist * rayY
Use the fractional coordinate perpendicular to the wall to pick the texture column:
- If we hit a vertical wall (
wallSide==0), usefrac(hitY). - If we hit a horizontal wall, use
frac(hitX).
Flip the column for consistent facing (N/E/S/W) based on stepX/stepY, then:
textureColumnX = (fraction * TEXTURE_HEIGHT) | 0 // square atlas columns
I select the actual texture id per side via WALL_MAP[hitTextureId].textures[wallSideDirection][seg], then look up a pre-shaded variant using SHADE_LEVELS and nearestIndexInAscendingOrder.
This keeps textures from being accidentally mirrored, so any text stays readable, by flipping UVs based on the wall face, ensuring what you see matches the source image. Because I know the hit side (N/E/S/W), I can also choose side-specific textures and even support things like portals, or rendering the “below” of walls above me. Many raycasters treat these as ‘mid textures’ drawn in the middle of a cell; I render them on grid boundaries instead.
6) Floors & ceilings with row-distance LUTs
Floors/ceilings are projected by rows rather than by wall hits.
The core identity is:
dy = (rowY - HALF_HEIGHT) // pixels offset from horizon
dist(rowY) = eyeScale / dy // world distance at that row
I precompute these in a function called rebuildRowDistLUT():
ROW_DIST[y]for floors (positive, below horizon)CIELING_ROW_DIST[y]for ceilings (negative sign so it marches upward)- Per-zone overrides
ROW_DIST_BY_ZONE[z][y]andCILEING_DIST_BY_ZONE[z][y]to respect differentfloorDepth/ceilingHeight.
To convert those distances into world coords along the ray cheaply, I use:
invDot = 1 / (dirX*rayX + dirY*rayY)
wx += rayX * deltaDist * invDot
wy += rayY * deltaDist * invDot
That keeps the floors/ceilings perfectly aligned with walls at any height without doing fresh divisions per row.
I render them by stepping along the ray (ray-march style), which keeps the math aligned with walls and the code easy to reason about. I could halve draw calls by skipping strict per-column rendering, but I chose clarity. Performance is still strong because an x/y z-buffer stops drawing floors/ceilings where they’d be occluded, so we skip a lot of work.

speaking of which…
7) Z-buffer & occlusion
I keep a per-pixel depth buffer pixelHeightBuffer[y*WIDTH + x] and write the wall’s perpDist for each covered pixel. That lets me:
- Cull ceiling rows when a nearer wall is in front (
castCielingchecksdist >= getPixelDepth(...)). - Correctly depth-sort sprites later.
- Correctly mask sprites later.
- Avoid overdrawing floor spans already behind a wall.
I also do an in-column occlusion pass (preprocessOcclusion) to drop hidden wall segments before drawing.

8) Fog & haze
My raycaster has fog and atmospheric effects. Unlike most raycasters. This not only allows me to squeeze out more perf (i don’t need to render stuff beyond fog) but gives it a more modern look.

Fog is distance-based with a soft start:
start = player.sightDist * FOG_START_FRAC
end = player.sightDist
t = clamp((dist - start) / (end - start), 0, 1)
I draw a 1-px-wide band (drawFogBand) over each wall column with globalAlpha = t * 0.85, tinted by the zone’s fogColor. There’s also a full-screen “haze” gradient for mood.
You can find an open source version of the engine here:
Heads-up: this open-source repo hasn’t been updated to the new renderer yet (I rewrote it this past week), so parts are messy. And it lack smuch of the new functionality (spirtes over walls, cielings over short walls in tall zones, walls etc) I’m prioritizing porting the new code over. (Edit: its now ported over)
https://github.com/Untrustedlife/InvasionEngine
It comes with a map editor so you can easily make new maps, and of course its designed to keep game code and engine code seperated.
How was it?
The early version came together quickly, only like a week; But adding tall walls , far walls over near walls, and the depth buffer was the real challenge. I’ve rewritten the renderer multiple times, and the build from this last week finally supports the flashy bits like tall sprites behind distant short walls. And zone aware cielings that render over short walls in those zones etc.
I was able to hone my javascript skills, and especially my engine optimization skills. I felt like I was actually working within limitations, channeling my inner John Carmac, unlike with commercial game engines.
It was food for that part of my soul deep down that wishes i was around in the early days of game development instead of living in the ashes of the devastation triple A publishers have wrought in the modern day.
I might do this again and make my own sorta minecrafty voxel engine at some point, that would be fun, or a true sector based engine like Doom.
Who knows really, the skys the limit!
What Now?
This engine is flexible enough for whatever I throw at it, and the minimal overdraw keeps performance strong, even with hundreds of sprites. Short term idea is to fix bugs, restore ceiling-over-wall occlusion, add floor-over-wall occlusion, tighten sprite masking, expand wall/floor texturing, and add proper skyboxes.
On the project side, I’ve got a few ideas: an ‘80s-style horror game where you play the villain, and a lightweight (Or even hardcore) Arena-like RPG. A lot of this is already doable without new engine work, so I may make a new game first and iterate after.
It all runs in the broswer, which gives me a lot of advantages over most things on itchio these days. And the fact that its my own focused engine helps too.
I had a lot of fun working on this project and as of posting thsi i’ve even updated it more! Heh
In the meantime, you can play Realmchild Invasion (The game featured in all the screenshots here using my new engine and updated renderer) here: untrustedlife.com/realmchildinvasion


Leave a Reply