3D Pogel Game
textures & characters
A performance optimized, 3D perspective-improved raycaster engine for your browser. Complete with mouselook, sprites, textures, lighting, objects and so much more!
Raycasting & 3D
The first question might be…is it actually 3D? That depends. You could argue that even polygonal 3D rendering is just a 2D representation of three-dimensional space. And so is this raycaster! While it has clearly more limitations than a polygonal world, it’s still in essence the illusion of a 3D world, created in 2D pixels from a set of coordinates.
This article is going to be a long one! I tried putting fun animated pictures at key points that makes it easier to skim.
Or just skip the words and play the game! It’s way fun and also less reading!
3D perspective & mouselook
One of the most obvious limitations of column-based rendering a 3D world is the absence of proper looking up and down. After all, traditional raycaster games and even early poly games like Doom had you looking straight ahead pretty much all the time.
Modern 3D games really spoil us in this regard: being able to look around freely contributes so much to an immersive, real-feeling world. And that’s really hard to get un-used to. So the challenge was: Can I re-create a modern “mouselook” feel into this engine?
Step 1: Just change the height of the ceiling and floor
The basic concept is simple enough: When you move the mouse to look up, simply make the sky taller and the floor smaller. When looking down, make the sky smaller and the floor larger.
Of course reality doesn’t really look like that. You get an uncanny feeling of being “really zoomed in”, even when you are not. That’s because there is no vertical perspective, you’re just moving the view port up and down.
Step 2: Skew the whole image!
We’re calculating the world column by column, so there’s not really a way skew a column to fake perspective. But what we can do is treat the entire output as an image, and skew that image!
After each frame is generated in step 1, the frame is an array of pixels, and we can now operate on a row- instead of a column-basis! We know how far up and down we are looking, so all we have to is progressively shorten each row of pixels as we look up and down, and fill it with filler-pixels at the beginning and end.
This sounds simple, but the math for this is still a bit tricky! While it’s easy(ish) to determine how many pixels to remove from each row, figuring out how to distribute the those pixels from each row to keep the perspective intact over each scan-line required some mental gymnastics.
Step 3: Hide the skew!
Credit where credit is due: I cite all the code I adapted and used in code-comments! Check out the repo for more details!
Sprites, textures & renderer
Of course no 3D world is complete without inhabitants!
Sprites are calculated and in their own space, and superimposed over the background world. Sprites support 4 views depending on its angle relative to the player. To determine if a sprite is behind another sprite, or a wall, a rudimentary z-buffer is used!
The characters models are adapted from my first Pogel-game, and stored as JSON in external texture-files. They loaded and placed into the world dynamically, and have basic movement logic. Moving sprites will turn 90 degrees if they hit a wall, and reverse direction when they hit the player!
Textures, renderer and output modes_
The ASCII output can run in 3 render modes: Solid Walls, Textures Only and Shaded Texture. Walls are textured with a sampler:
- The sampler figures out which pixel to get from a texture based on which part of the texture corresponds to the wall coordinates from the game-world
- Textures can be sampled at different sizes: Blocks can have 2x (or any times) the size, and repeat textures within the block, increasing the resolution
While I like the look of ASCII characters as textures, they tend to be a illegible if rendered with really large pixels, and so I wrote in the ability to not only convert the text ASCII chars into solid solid ASCII chars, but also adjust the distance shading of those chars on depth.
Shading (or: almost lighting)_
I thought it might be kind of neat to have a—sorta—directional light source to give the whole world a little more depth. Depending on the distance from the player, each world-column is rendered at a slightly brightness. Walls facing a certain direction are shaded one step lighter than walls facing the other.
In order to re-use this for any texture, solid wall, or sprites, I wrote a method that assigns the correct depth- texture- directional values for each pixel as they are requested by the engine.
Performance, assets & objects
- Caching and preserving certain player-, object- and world-states, such as angles, z-buffers, distance calculations. The goal is calculate everything only once!
- Frequent calculations (especially π, and various calculations involving π) are cached in constants
- Lookup-tables for some recurring calculations involving looking up and down
Math.flooretc. with bitwise operators
- Combining various operations into single loops: super-impose objects and sprites onto the background in the same output loop, instead of rendering each space on top the other
Because this is an entire game engine, level data and textures are loaded from files, and can be switched on the fly!
The engine also supports what I call floor objects: blocks that are neither walls nor infinite planes like ceilings and the sky. In the world they could represent water or holes, depending on their shading. They work by:
- Calculating the distance to the front and the back of a block.
- Overlaying and combining them onto the background-space
- Keeping them out of the z-buffer, so they don’t hide sprites placed behind them!
Input and output
- i/o is written from scratch! Walking, strafing, running and jumping included, and of course proper mouselook!
- I quickly figured out that console.log-ing is super expensive. Instead, debug output is printed to a special div on screen with its own function. I always find it really interesting when you have find other solutions to problems that have already been solved!
At some point I decided I would make a game that took place in a grocery store. Since shelves themselves have depths, the textures for the shelves would have to have some dimension to them. My idea was to serve a different texture (with fake perspective) based on the player angel is looking at it. The effect is subtle here, but I think it really helps sell the illusion!
This one is actually quite simple: Every few frames, load a different texture of a block. This code is alreay in the engine, but has not yet been implemented yet!