{"id":1412,"date":"2023-08-31T04:07:05","date_gmt":"2023-08-31T04:07:05","guid":{"rendered":"https:\/\/timallanwheeler.com\/blog\/?p=1412"},"modified":"2023-08-31T04:30:45","modified_gmt":"2023-08-31T04:30:45","slug":"toom-with-non-euclidean-geometry","status":"publish","type":"post","link":"https:\/\/timallanwheeler.com\/blog\/2023\/08\/31\/toom-with-non-euclidean-geometry\/","title":{"rendered":"TOOM with Non-Euclidean Geometry"},"content":{"rendered":"\n<p>This month continues our foray into the world of DOOM and C software renderers. In the last post, we moved from a blocky, axis-aligned world to general 2D geometry. In this post, not only will we manipulate the 3rd dimension, thereby reaching DOOM-geometry-parity, we will leverage our raycasting approach to unlock something the original DOOM couldn&#8217;t do.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Variable Floor and Ceiling Heights<\/h2>\n\n\n\n<p>In Wolfenstein 3D, the floors are always at z = 0 and the ceilings are always at z = 1. DOOM could adjust these floor and ceiling heights, and its players were thus able to ascend stairs, traverse next to chasms, and generally enjoy a much richer gaming environment.<\/p>\n\n\n\n<p>This gif shows the basic z-manipulation I&#8217;m talking about:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toomed_variable_z4.gif\" alt=\"\" class=\"wp-image-1414\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Here we&#8217;re adjusting the floor and ceiling height of our corridor. When we raise the floor, we need to render a wall texture between our floor and the raised corridor floor. When we lower the floor, we need to render a wall texture between the lowered floor and the normal floor on the other end.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"423\" height=\"249\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image.png\" alt=\"\" class=\"wp-image-1415\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image.png 423w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-300x177.png 300w\" sizes=\"auto, (max-width: 423px) 100vw, 423px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Changing the ceiling height can also expose a bit of wall texture:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"432\" height=\"204\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-1.png\" alt=\"\" class=\"wp-image-1416\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-1.png 432w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-1-300x142.png 300w\" sizes=\"auto, (max-width: 432px) 100vw, 432px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Our renderer has to be able to model these floor sections in order to keep track which of piece of floor has which floor and ceiling height, and we have to keep track of the textures to draw when they are exposed.<\/p>\n\n\n\n<p><a href=\"https:\/\/doomwiki.org\/wiki\/Sector\">Like DOOM<\/a>, I will denote regions of the map with a particular floor and ceiling height (and later, floor and ceiling textures) to be called a <em>sector.<\/em> Sectors will naturally be closed polygons in our top-down 2D map. <\/p>\n\n\n\n<p>Here you can see a top-down view of the TOOM level editor, showing the same region as rendered above. The red triangle is the camera, which looks toward the right.  <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-2.png\" alt=\"\" class=\"wp-image-1417\" style=\"width:339px;height:182px\" width=\"339\" height=\"182\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-2.png 431w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-2-300x161.png 300w\" sizes=\"auto, (max-width: 339px) 100vw, 339px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Here is the same shot, filled in a bit to make it easier to interpret:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"359\" height=\"199\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-3.png\" alt=\"\" class=\"wp-image-1418\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-3.png 359w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-3-300x166.png 300w\" sizes=\"auto, (max-width: 359px) 100vw, 359px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>As covered in the last post,  the world is defined using a triangle mesh. Our corridor, for example, is comprised of two triangles. The sector for that corridor must be referenced by the lines along the corridor&#8217;s boundary:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"359\" height=\"197\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-4.png\" alt=\"\" class=\"wp-image-1419\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-4.png 359w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-4-300x165.png 300w\" sizes=\"auto, (max-width: 359px) 100vw, 359px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The sector itself just stores the z heights right now. It doesn&#8217;t know which edges it corresponds to. Instead, we have the side geometry refer to the sector it should correspond to. This approach is again, <a href=\"https:\/\/doomwiki.org\/wiki\/Sidedef\">very much like DOOM<\/a>.<\/p>\n\n\n\n<p>We are using a Doubly Connected Edge List (DCEL) to represent our triangle mesh. Edges in a DCEL are bi-directional . If we think of edges as right-handed, and an edge A-&gt;B as being on the right-hand side of AB, then we can just have every directed edge at the boundary of a sector keep track of which sector  it helps enclose.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"496\" height=\"276\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-5.png\" alt=\"\" class=\"wp-image-1421\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-5.png 496w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-5-300x167.png 300w\" sizes=\"auto, (max-width: 496px) 100vw, 496px\" \/><\/figure>\n<\/div>\n\n\n<p>The sides that refer to the corridor are yellow, whereas the sides that refer to the primary sector are orange. When we raycast from the camera down the corridor, we can check for z changes when we traverse over edges where the sector changes. Here we see a raycast with two locations where the sector heights might change:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"498\" height=\"275\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-6.png\" alt=\"\" class=\"wp-image-1422\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-6.png 498w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-6-300x166.png 300w\" sizes=\"auto, (max-width: 498px) 100vw, 498px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The data structures for this are pretty simple. A directed edge may have a SideInfo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct SideInfo {\n    u16 flags;\n    u16 sector_id;\n    TextureInfo texture_info_lower;\n    TextureInfo texture_info_middle;\n    TextureInfo texture_info_upper;\n};<\/code><\/pre>\n\n\n\n<p>A SideInfo is simply a struct that contains a lower, middle, and upper texture, the index of the sector to refer to, and some flags. Right now, the only flag I use is one to determine whether a wall is passable, and if so, we can avoid rendering the middle texture.<\/p>\n\n\n\n<p>The TextureInfo entry is pretty simple as well:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct TextureInfo {\n    u16 texture_id; \/\/ Index in the texture atlas\n    i16 x_offset;   \/\/ Texture x offset\n    i16 y_offset;   \/\/ Texture y offset\n};<\/code><\/pre>\n\n\n\n<p>And Sectors are just basic data as well:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct Sector\n{\n    u16 flags;\n    f32 z_floor;\n    f32 z_ceil;\n};<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Raycasting with Varying Heights<\/h2>\n\n\n\n<p>With these data structures in-hand, rendering becomes a little more complicated. We continue to cast our ray out, as before. Now, however, we need to keep track of how much we&#8217;ve already drawn.<\/p>\n\n\n\n<p>To illustrate, let&#8217;s look at a horizontal cross-section of geometry:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"707\" height=\"348\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-7.png\" alt=\"\" class=\"wp-image-1423\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-7.png 707w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-7-300x148.png 300w\" sizes=\"auto, (max-width: 707px) 100vw, 707px\" \/><\/figure>\n<\/div>\n\n\n<p>Here I have drawn the area swept out as we cast a single ray to the right, down a single pixel-column. The light purple boxes show all of the regions in our field-of-view where we need to render ceiling pixels, the dark purple boxes show where we need to render floor pixels, and the green boxes show where we need to render wall pixels.<\/p>\n\n\n\n<p>When we start raycasting, we start having not drawn any pixels. The bounds on where we can draw contains the whole vertical pixel column, between zero and the number of pixels:<\/p>\n\n\n\n<p>\\[y_\\text{lo} = 0 \\qquad y_\\text{hi} = \\text{SCREEN_SIZE_Y}\\]\n\n\n\n<p>We start to traverse the mesh, and hit our first sector change:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"672\" height=\"342\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_661.png\" alt=\"\" class=\"wp-image-1434\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_661.png 672w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_661-300x153.png 300w\" sizes=\"auto, (max-width: 672px) 100vw, 672px\" \/><\/figure>\n<\/div>\n\n\n<p>At this point we calculate the y-location (i.e., location in the vertical pixel column) of the ends of the floor and ceiling. We render the floor and ceiling into our pixel column. We&#8217;re already pretty used to this trigonometry by now.<\/p>\n\n\n\n<p>\\[\\begin{aligned} y_\\text{ceil} &amp; = \\frac{\\text{SCREEN_SIZE_Y}}{2} + \\gamma \\left( z_\\text{ceil} &#8211; z_\\text{camera} \\right) \\\\ y_\\text{floor} &amp; = \\frac{\\text{SCREEN_SIZE_Y}}{2} + \\gamma \\left( z_\\text{floor} &#8211; z_\\text{camera} \\right)\\end{aligned}\\]\n\n\n\n<p>where<\/p>\n\n\n\n<p>\\[\\gamma = \\frac{\\text{SCREEN_SIZE_Y}}{\\text{y field of view}} \\cdot \\frac{\\text{cam length}}{\\text{ray length}}\\]\n\n\n\n<p>The y yield of view is how many units up the camera views for every unit we raycast outward. I&#8217;m using about 0.84. The camera length is close to 1, and adjusts for the fact that our screen is flat and not curved, so rays travelling left or right of center have to have the length adjusted. Other than that, its just a function of the heights and how far our ray has traveled.<\/p>\n\n\n\n<p>In this case, the next sector has an increase in the ceiling height and a decrease in the floor height. That means  we do not have any local wall textures to render. We set \\(y_\\text{lo} = y_\\text{floor}\\) and \\(y_\\text{hi} = y_\\text{ceil}\\) and keep on raycasting:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"674\" height=\"327\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_663.png\" alt=\"\" class=\"wp-image-1436\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_663.png 674w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_663-300x146.png 300w\" sizes=\"auto, (max-width: 674px) 100vw, 674px\" \/><\/figure>\n<\/div>\n\n\n<p>At the next sector transition, we once again compute \\(y_\\text{floor}\\) and \\(y_\\text{ceil}\\). This time, however, the ceiling drops and the floor rises, so we also have wall segments to render. We compute their pixel extents in the same way:<\/p>\n\n\n\n<p>\\[\\begin{aligned} y_\\text{upper} &amp; = \\frac{\\text{SCREEN_SIZE_Y}}{2} + \\gamma \\left( z_\\text{upper} &#8211; z_\\text{camera} \\right) \\\\ y_\\text{lower} &amp; = \\frac{\\text{SCREEN_SIZE_Y}}{2} + \\gamma \\left( z_\\text{lower} &#8211; z_\\text{camera} \\right)\\end{aligned}\\]\n\n\n\n<p>where \\(z_\\text{upper}\\) and \\(z_\\text{lower}\\) are the new sector ceiling and floor heights, respectively.<\/p>\n\n\n\n<p>When we continue our raycast, we find that the next sector transition only increases the ceiling height. The new \\(y_\\text{floor}\\) is greater than \\(y_\\text{lo}\\), so we render the patch of floor. The new \\(y_\\text{ceil}\\) is not any lower than our current \\(y_\\text{hi}\\), due to the ceiling being parallel to our camera view, so we don&#8217;t need to render anything there. <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"670\" height=\"322\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_665.png\" alt=\"\" class=\"wp-image-1438\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_665.png 670w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_665-300x144.png 300w\" sizes=\"auto, (max-width: 670px) 100vw, 670px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>We finally proceed and hit a closed sector. We render the last bit of floor and then render its center texture:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"676\" height=\"325\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-11.png\" alt=\"\" class=\"wp-image-1439\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-11.png 676w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-11-300x144.png 300w\" sizes=\"auto, (max-width: 676px) 100vw, 676px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>So that&#8217;s how the renderer works when you have variable z heights and upper, middle, and lower textures. Each raycast has to do more work to determine what to draw, and it keeps track of where it is within its y pixel bounds to make sure we don&#8217;t mistakenly draw something on top of what we&#8217;ve already rendered.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rendering Textures<\/h2>\n\n\n\n<p>In <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/05\/29\/billboard-sprites\/\">this previous blog post<\/a>, I covered sprites, and how they use the <a href=\"https:\/\/doomwiki.org\/wiki\/Picture_format\">DOOM picture format<\/a>. In short, sprites like monsters are stored in vertical pixel strips. Why? Because when we render the vertical portions of the world, we&#8217;re rendering convenient vertical sections of these sprites. The vertical orientation forms a right-triangle, which makes the math of figuring out which pixel to draw more efficient. DOOM uses the same format for wall textures (both upper and lower). I ended up doing the same thing.<\/p>\n\n\n\n<p> So, wall textures are the same as sprites. What about horizontal textures? We&#8217;d like to draw textures on our ceilings and floors too:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"634\" height=\"356\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-12.png\" alt=\"\" class=\"wp-image-1441\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-12.png 634w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-12-300x168.png 300w\" sizes=\"auto, (max-width: 634px) 100vw, 634px\" \/><\/figure>\n<\/div>\n\n\n<p>(Yeah, these don&#8217;t fit thematically. I haven&#8217;t loaded my own floor and ceiling textures yet. These just come from DOOM.)<\/p>\n\n\n\n<p>We can again leverage <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/04\/30\/textures-for-wolfenstein\/\">our previous work<\/a> to figure out how to draw floor and ceiling textures. Unlike the vertically-oriented wall textures, these &#8220;flat&#8221; textures are most efficiently drawn horizontally across the screen. As such, in DOOM they are called <em><a href=\"https:\/\/doomwiki.org\/wiki\/Flat\">flats<\/a><\/em>. <\/p>\n\n\n\n<p>Our render is most adept at rendering things vertically. It works on one pixel column at a time. So how do we hope to render anything horizontally?<\/p>\n\n\n\n<p>Well, the raycasts start with the leftmost pixel column and then work their way to the right. I keep track of <em>active spans<\/em>, which are horizontal sections of the screen that need to eventually be rendered with a single flat at a single floor height. Whenever we need to start a new span, we close out the previous one and render it. <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toom_active_spans.gif\" alt=\"\" class=\"wp-image-1442\"\/><\/figure>\n<\/div>\n\n\n<p> <\/p>\n\n\n\n<p>The gif above shows how the renderer produces a scene. We draw vertical sections as we raycast, but start\/maintain horizontal spans that only get rendered (1) when another span in the same y-coordinate starts or (2) we are done raycasting.<\/p>\n\n\n\n<p>Each active span contains all of the information needed to render it efficienctly, once it needs to be rendered:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct ActiveSpanData {\n    \/\/ Location of ray_dir_lo's intersection with the ground.\n    common::Vec2f hit;\n    \/\/ Shift 'hit' by one pix column moved toward hi's intersection.\n    common::Vec2f step;\n    u16 sector_id;\n    u16 flat_id;\n    bool is_active;\n    \/\/ The camera x pixel index where this span starts.\n    int x_start;\n    \/\/ The most recent camera x pixel where this span currently ends.\n    int x_end;\n};<\/code><\/pre>\n\n\n\n<p>We can do the computation work of calculating the hit and step values when we first create the span. We set x_start at that time, and initially set x_end to the same value. If we go to render the same sector  in the same location in the next column, we increment x_end.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Colors and Light Levels<\/h2>\n\n\n\n<p>You may have noticed that these images get darker the further we&#8217;re looking. This is an intentional effect from DOOM that enhances the sense of depth.<\/p>\n\n\n\n<p>This is easiest to see without the floor textures:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"810\" height=\"235\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-13.png\" alt=\"\" class=\"wp-image-1444\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-13.png 810w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-13-300x87.png 300w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-13-768x223.png 768w\" sizes=\"auto, (max-width: 810px) 100vw, 810px\" \/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Conceptually, this is really simple. Render pixels darker when they are further away. <\/p>\n\n\n\n<p>The <em>light level<\/em> of a pixel column drops off as the raycast distance increases. Or rather, the darkness level increases with the raycast distance.<\/p>\n\n\n\n<p>Pixel values are written to the screen as RGB values. How does one take an RGB value and make it darker? Well, white is 0xFFFFFF and black is 0x000000, so the simplest way is to decrease each channel by some percentage. To do it &#8220;more correctly&#8221;, you can convert your RGB color into the <a href=\"https:\/\/en.wikipedia.org\/wiki\/CIELAB_color_space\">Lab colorspace<\/a>, reduce its lightness, and then convert it back. That is an expensive operation.<\/p>\n\n\n\n<p>The original DOOM didn&#8217;t have access to RGB values. Or rather, it didn&#8217;t have access to all of them. According to the <a href=\"https:\/\/fabiensanglard.net\/gebbdoom\/\">DOOM Black Book<\/a>, PC video games of the 90s had 320&#215;200 resolution with 1 byte per pixel drawn from a <a href=\"https:\/\/doomwiki.org\/wiki\/PLAYPAL\">256-color palette<\/a>. This meant that programmers could set the 256 colors in the palette, and drawing a pixel to the screen effectively meant indexing into this palette by setting a 1-byte pixel index rather than a full RGB value.<\/p>\n\n\n\n<p>So how does one change a pixel&#8217;s brightness?<\/p>\n\n\n\n<p>The DOOM developers precomputed mappings from pixel values at full brightness to pixel values at reduced brightness. There were many such <a href=\"https:\/\/doomwiki.org\/wiki\/COLORMAP\">colormaps<\/a>, 32 that were computed for all light levels from 100% brightness all the way down to 0%. There were even some additional colormaps, such as an inverted colormap used when the player becomes invulnerable:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"623\" height=\"351\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-14.png\" alt=\"\" class=\"wp-image-1445\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-14.png 623w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-14-300x169.png 300w\" sizes=\"auto, (max-width: 623px) 100vw, 623px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>All wall textures and flats store pixel indices rather than RGB values. These pixel indices pass through the chosen colormap and then index into the palette to get the RGB value. In the original DOOM system, indexing into the palette was done for you by the video card. I&#8217;ll be doing it myself here.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>u8 palette_index = patch.post_data&#91;column_offset];\npalette_index = colormap.map&#91;palette_index];\nu32 abgr = *(u32*)(palette.colors + 3 * palette_index) | 0xFF000000;<\/code><\/pre>\n\n\n\n<p> The palette stores sequential BGR values, so we just grab the section we need and then overwrite the FF for the alpha channel.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct Palette {\n    u8 rgbs&#91;768];\n};\n\nstruct Colormap {\n    u8 map&#91;256];\n};<\/code><\/pre>\n\n\n\n<p>As a final note, there is one additional lighting effect that helps make the map more legible. Like the trick <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/04\/01\/wolfenstein-3d-raycasting-in-c\/\">used in Wolfenstein<\/a>, walls closer to being x-aligned are drawn 1 point brighter than normal, and walls closer to being y-aligned are drawn 1 point darker than normal. Furthermore, each sector has its own baseline light level. This lets the map creator make some areas naturally darker, and allows for some nice floor shadow effects:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"636\" height=\"355\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-15.png\" alt=\"\" class=\"wp-image-1448\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-15.png 636w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-15-300x167.png 300w\" sizes=\"auto, (max-width: 636px) 100vw, 636px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">DOOM Levels<\/h2>\n\n\n\n<p>Ah, that last image gave it away. The geometry changes and lighting changes now bring us more-or-less to parity with the original DOOM. We can load and traverse the original DOOM levels!<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toom_e1m1_walkthrough_4.gif\" alt=\"\" class=\"wp-image-1449\"\/><\/figure>\n<\/div>\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toom_e1m1_walkthrough_5.gif\" alt=\"\" class=\"wp-image-1450\"\/><\/figure>\n<\/div>\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toom_e1m1_walkthrough_6.gif\" alt=\"\" class=\"wp-image-1451\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>This rendering is done using the raycasting method, via our DCEL, rather than using DOOM&#8217;s BSP. This means we can totally avoid loading <a href=\"https:\/\/doomwiki.org\/wiki\/SEGS\">SEGS<\/a>, <a href=\"https:\/\/doomwiki.org\/wiki\/SSECTORS\">SSECTORS<\/a>, <a href=\"https:\/\/doomwiki.org\/wiki\/NODES\">NODES<\/a>, and the <a href=\"https:\/\/doomwiki.org\/wiki\/BLOCKMAP\">BLOCKMAP<\/a>. <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"650\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-16-1024x650.png\" alt=\"\" class=\"wp-image-1452\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-16-1024x650.png 1024w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-16-300x191.png 300w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-16-768x488.png 768w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-16.png 1173w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n<\/div>\n\n\n<p>E1M1 loaded into the TOOM editor.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Work was required to be able to load the geometry into our DCEL correctly. Mainly in being able to properly flip mesh edges in order to be able to produce edges that match the map geometry, and then constrain them. I might cover that in a future post.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Portals<\/h2>\n\n\n\n<p>Okay, we&#8217;ve reached geometric parity with DOOM. Is there something this DCEL approach can do that DOOM cannot?<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toom_portal8.gif\" alt=\"\" class=\"wp-image-1467\"\/><\/figure>\n<\/div>\n\n\n<p>So how does that work?<\/p>\n\n\n\n<p>When we raycast or when the player walks around, we traverse between faces in our triangle mesh. We can create a portal by linking two edges in the triangle mesh and adding the necessary logic such that, when traversed, we come out the linked edge instead.<\/p>\n\n\n\n<p>Below we see the top-down view of the portal setup playing out. The red triangle shows the player camera and its approximate view cone. <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toom_portal6.gif\" alt=\"\" class=\"wp-image-1456\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The player movement for when we traverse a portal is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Suppose we are crossing the line segment AB from pos to pos_next,\n\/\/ where pos_next now lies on AB.\nVec2f v_face = b - a;\n\n\/\/ Calculate where along the segment we intersected.\nf32 v_face_len = Norm(v_face);\nf32 x_along_texture = v_face_len - Norm(pos_next - a);\n\n\/\/ Update our quarter edge using the destination quarter edge\n\/\/ stored in the side info.\ncamera_state-&gt;qe = mesh.Tor(side_info-&gt;qe_portal);\n\n\/\/ The vector along the new face is\nVec2f c = mesh.GetVertex(side_info-&gt;qe_portal);\nVec2f d = mesh.GetVertex(mesh.Sym(side_info-&gt;qe_portal));\nVec2f cd = d - c;\ncd = Normalize(cd);\n\n\/\/ Our new position is:\ncamera_state-&gt;pos = c + x_along_texture * cd;\n\nf32 angle_ab = std::atan2(v_face.y, v_face.x);\nf32 angle_cd = std::atan2(cd.y, cd.x);\nf32 angle_rot = CounterClockwiseAngleDistance(angle_ab, angle_cd) + M_PI;\nf32 cos_rot = std::cos(angle_rot);\nf32 sin_rot = std::sin(angle_rot);\n\n\/\/ Now to get the new direction, we need to rotate out of the new face\ndir = {cos_rot * dir.x - sin_rot * dir.y,\n       sin_rot * dir.x + cos_rot * dir.y};\n\n\/\/ We also need to rotate our speed\nvel = {cos_rot * vel.x - sin_rot * vel.y,\n       sin_rot * vel.x + cos_rot * vel.y};<\/code><\/pre>\n\n\n\n<p>Geometrically, we are figuring out the rotation angle between our portals (which could be precomputed) and then rotating our movement direction and speed when we traverse. <\/p>\n\n\n\n<p>Note that the +pi term compensates for the fact that the destination portal side is oriented 180 degrees with respect to the source portal side.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"977\" height=\"399\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_681.png\" alt=\"\" class=\"wp-image-1462\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_681.png 977w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_681-300x123.png 300w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/Selection_681-768x314.png 768w\" sizes=\"auto, (max-width: 977px) 100vw, 977px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>When we raycast through portals, we have properly account for any height changes on the receiving sector. We want to view the other side as if we were the appropriate height:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>camera_z += (sector_dst-&gt;z_floor - sector_src-&gt;z_floor);<\/code><\/pre>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"638\" height=\"357\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/toom_portal7.gif\" alt=\"\" class=\"wp-image-1464\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>There is a bit of extra bookkeeping we have to do with portals &#8211; namely that for flats to render correctly we have to update the camera position to the place it would be as if the portal did not exist and we were peering toward our destination:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"843\" height=\"400\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-18.png\" alt=\"\" class=\"wp-image-1468\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-18.png 843w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-18-300x142.png 300w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/08\/image-18-768x364.png 768w\" sizes=\"auto, (max-width: 843px) 100vw, 843px\" \/><\/figure>\n<\/div>\n\n\n<p>We then have to recompute some of the vectors used to create spans. This is all because flats are rendered on a fixed grid, and we compute where we are in that grid based on the camera location. We have to fake the camera being in the proper location &#8220;behind&#8221; where the portal surface is. Please take a look at the source code for the nitty-gritty.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>There is a lot of neat gamemap stuff that one can do with portals and the non-Euclidean geometry they unlock. Some of it is more blatant, like the first portal, and some of it can be quite subtle, like a hallways that is shorter or longer than expected. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>This blog post covered adding variable z heights for floors and ceilings, which then required being able to render wall sections revealed at floor or ceiling height changes. We rendered those vertically, because that is most efficient. We then figured out a way to render horizontal textures too, which were most efficient to render in horizontal strips. We added some lighting effects. We then had everything we needed to represent the original DOOM level geometry. We concluded with an overview of portals &#8211; sides in our mesh that are linked together such that crossing one results in you exiting out the other.<\/p>\n\n\n\n<p>I made the code for <a href=\"https:\/\/github.com\/tawheeler\/toom\/releases\/tag\/v1.0.0\">TOOM<\/a> and <a href=\"https:\/\/github.com\/tawheeler\/TOOMED\/releases\/tag\/v1.0.0\">TOOMED<\/a> available in separate repos. TOOM is the &#8220;game&#8221;, written in pure C without graphics libraries. TOOMED is the TOOM Editor, written in C++. It basically doesn&#8217;t use graphics libraries either, but it does use Dear ImGui with an accelerated backend, as well as have more liberal data structures for easier manipulation of game data and assets.<\/p>\n\n\n\n<p>That&#8217;s more or less it for TOOM, for now. There are a few things the renderer doesn&#8217;t support that DOOM has, such as <a href=\"https:\/\/doomwiki.org\/wiki\/Animated_flat\">animated nukage<\/a>, pickups, or proper handling for partially-transparent walls. There obviously isn&#8217;t any gameplay. No shooting or doors or anything like that. Those things are neat, of course, but I feel like I already understand how they work, and it&#8217;d just be pretty tedious work for me to get TOOM running with all that. Maybe I&#8217;ll do it, but for now I&#8217;m thinking I&#8217;ll be turning to other interests. Thanks for reading!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Note on Efficiency<\/h2>\n\n\n\n<p>One of the reasons that this TOOM project interested me in the first place was the promise that I might learn how to program something &#8220;for real&#8221; and really have to care about performance. I admire folks like <a href=\"https:\/\/caseymuratori.com\/\">Casey Muratori<\/a> and <a href=\"http:\/\/number-none.com\/blow\/blog\/\">Jonathan Blow<\/a> who ardently advocate for efficient programming, and that software engineers should understand what goes on under modern abstractions.<\/p>\n\n\n\n<p>I tried to keep TOOM running at over 30fps. I was able to track this using a profiling technique covered by Casey in his <a href=\"https:\/\/www.computerenhance.com\/\">performance-aware programming class<\/a>. <\/p>\n\n\n\n<p>For a moderately-expensive scene in E1M1, TOOM clocks in at about 130fps:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Frame dt:   28.9459ms\nTotal time: 7.6722ms, 130.3 fps (CPU freq 1992267920)\n  Poll:   32405 (0.21%)\n  Tick:   7097 (0.05%)\n  Render: 15245367 (99.74%)\n  DKBS:   249 (0.00%)<\/code><\/pre>\n\n\n\n<p>As we can see, 99.74% of the time is spent rendering. That is to be expected, since that is where all the work is.<\/p>\n\n\n\n<p>Here the frame dt is the 30Hz enforced by my monitor, because SDL is syncing to my monitor refresh rate. This is with a 640 x 320 window, which is double the resolution of the original DOOM. Yes, this is like 30 years after DOOM came out, but I am nevertheless quite happy with these results.<\/p>\n\n\n\n<p>TOOMED, however, is a bit slower:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Total time: 35.2314ms (CPU freq 1991906430)\n  GetRightHandedness  &#91; 25801]:     792837 (1.13%)\n  RenderPatchColumn   &#91;  1760]:    8381240 (11.94%)\n  RenderSpan          &#91;   318]:    3498674 (4.99%)\n  Raycast             &#91;   640]:   35425448 (50.48%, 72.23% w\/children)\n  Raycast_EdgeChecks  &#91;  8600]:    2322161 (3.31%, 4.43% w\/children)\n  Raycast_WithValidSideInfo&#91;  2720]:    3599229 (5.13%, 17.32% w\/children)\n  RenderScene         &#91;     1]:     146851 (0.21%, 77.17% w\/children)<\/code><\/pre>\n\n\n\n<p>It takes about 4.6x as long for TOOMED to render the same scene, giving it a frame rate of about 28fps. That is not great.<\/p>\n\n\n\n<p>Yes, TOOMED is doing a little more, in that it also renders the top-down GUI. However, that GUI stuff is pretty negligible. I believe the real culprit here more-or-less boils down to two things: memory layout and dictionary lookups.<\/p>\n\n\n\n<p>In TOOM, everything is accessed directly in the binary save file. Entries are all nicely indexed and can be quickly accessed. In TOOMED, since we&#8217;re editing things, we store them in std::vectors and std::maps, which results in disparate structures spread out in different locations. In many cases it simply takes more effort for the CPU to get access to what it needs. Furthermore, dictionary lookups, while being O(1), are still doing more work than a simple array access. The delta here, 4.6x, is pretty substantial, and this is the sort of takeaway that I think serious gamedev folk mean when they say paying attention to your data structures and memory strategies matters.<\/p>\n\n\n\n<p>As a final fun learning point, my raycast method was initially recursive. That is, every time we step our ray and transfer over a triangle edge, I&#8217;d recurse down in order to process the next triangle step. I figured that this would make it easier to later back-track and render things like monsters or item pickups. However, TOOMED was running _extremely_ slowly, around 70ms per frame. Changing from recursion to a loop cut that time in about half. Its the same amount of work, theoretically, only now we don&#8217;t have to make a bunch of copies every time we push to the stack.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This month continues our foray into the world of DOOM and C software renderers. In the last post, we moved from a blocky, axis-aligned world to general 2D geometry. In this post, not only will we manipulate the 3rd dimension, thereby reaching DOOM-geometry-parity, we will leverage our raycasting approach to unlock something the original DOOM [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1412","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/1412","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/comments?post=1412"}],"version-history":[{"count":30,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/1412\/revisions"}],"predecessor-version":[{"id":1480,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/1412\/revisions\/1480"}],"wp:attachment":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/media?parent=1412"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/categories?post=1412"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/tags?post=1412"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}