<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tiger's Place]]></title><description><![CDATA[Tiger's Place]]></description><link>https://tigerabrodi.blog</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1662616452555/PLNLd_3Ma.png</url><title>Tiger&apos;s Place</title><link>https://tigerabrodi.blog</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 19 May 2026 03:06:21 GMT</lastBuildDate><atom:link href="https://tigerabrodi.blog/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Bipedal, humanoid, and the words for creature shapes in games]]></title><description><![CDATA[Bipedal vs quadrupedal: how many legs
This is about the number of legs the creature walks on.
Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids.
Quadrupedal. Four legs. Do]]></description><link>https://tigerabrodi.blog/bipedal-humanoid-and-the-words-for-creature-shapes-in-games</link><guid isPermaLink="true">https://tigerabrodi.blog/bipedal-humanoid-and-the-words-for-creature-shapes-in-games</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Mon, 18 May 2026 00:50:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/cefe068c-2788-4ab5-9e78-4f0f774d7df2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Bipedal vs quadrupedal: how many legs</h1>
<p>This is about the number of legs the creature walks on.</p>
<p><strong>Bipedal.</strong> Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids.</p>
<p><strong>Quadrupedal.</strong> Four legs. Dogs, horses, cows, lizards, most "beast" enemies.</p>
<p><strong>Hexapodal.</strong> Six legs. Insects, spiders' weird cousins.</p>
<p><strong>Octopodal.</strong> Eight legs. Spiders.</p>
<p>In game dev, the leg count matters because it changes how locomotion works. A bipedal walk cycle is a balance problem (the character is upright on two legs and falls over without active balance). A quadruped walk cycle is more stable but has way more legs to coordinate. Inverse kinematics (IK) for placing feet on uneven ground is harder the more legs you have.</p>
<p>When someone says "we need to handle this if they're bipedal," they usually mean the animation system, the balance system, or the IK foot placement is different.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/349a155a-00be-4855-8e74-1b17c7a0b9f2.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Humanoid: shaped like a person</h1>
<p>Humanoid is about overall body plan, not leg count.</p>
<p>A humanoid has:</p>
<ul>
<li><p>A head on top</p>
</li>
<li><p>A torso in the middle</p>
</li>
<li><p>Two arms with hands</p>
</li>
<li><p>Two legs with feet</p>
</li>
<li><p>Roughly human proportions</p>
</li>
</ul>
<p>Humans are humanoid. Elves, orcs, robots that look like people, aliens with two arms and two legs and a head, all humanoid.</p>
<p>A horse is bipedal? No, quadrupedal, AND not humanoid. A T-rex is bipedal but NOT humanoid (no arms in the human sense, weird body proportions).</p>
<p>Humanoid matters in game engines because they have specific tools for humanoid skeletons: motion capture libraries, animation retargeting (taking an animation made for one humanoid character and applying it to another), pre-built IK rigs. Unity's Humanoid Animation system, Unreal's Mannequin system, all assume the character has a humanoid skeleton.</p>
<p>If you make a non-humanoid character, you usually have to set up the rig from scratch.</p>
<h1>Anthropomorphic: animal that acts human</h1>
<p>A step further than humanoid. <strong>Anthropomorphic</strong> means an animal (or non-human thing) that's been redesigned with human characteristics: walking on two legs, having hands, wearing clothes, speaking.</p>
<p>Sonic the Hedgehog is anthropomorphic. Bugs Bunny is anthropomorphic. Most furry character designs are anthropomorphic.</p>
<p>The shape is humanoid (two arms, two legs, head on top). The species is animal. The behavior is human.</p>
<p>In games, anthropomorphic characters can usually use humanoid animation systems because their skeleton matches a human one closely enough.</p>
<h1>Other body plans</h1>
<p>Once you leave bipedal and quadrupedal, things get interesting.</p>
<p><strong>Serpentine.</strong> Snake-like. No legs. Movement is a slithering animation, often along a curve. Implementing snake movement is a whole topic on its own.</p>
<p><strong>Centauroid.</strong> Half humanoid, half quadruped. A centaur. Top half is a human torso with arms. Bottom half is a horse with four legs. Six limbs total.</p>
<p><strong>Tentacular.</strong> Octopus-like. Many flexible appendages instead of rigid limbs. Animation is usually done with bone chains and physics rather than IK.</p>
<p><strong>Avian.</strong> Bird-shaped. Bipedal but with wings instead of arms. Different gait, different balance, often flight to handle.</p>
<p><strong>Aquatic.</strong> Fish-shaped. No legs at all. Movement is based on body undulation. Often handled with vertex shaders for the swimming animation.</p>
<p><strong>Insectoid.</strong> Insect body plan. Multiple legs, segmented body, often antenna and mandibles.</p>
]]></content:encoded></item><item><title><![CDATA[Lerp and smoothstep, what they actually do]]></title><description><![CDATA[Intro
You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another.
The problem]]></description><link>https://tigerabrodi.blog/lerp-and-smoothstep-what-they-actually-do</link><guid isPermaLink="true">https://tigerabrodi.blog/lerp-and-smoothstep-what-they-actually-do</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Mon, 18 May 2026 00:23:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/5118b3cc-118f-479a-aaa2-d23f53ed67b9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Intro</h1>
<p>You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another.</p>
<h1>The problem</h1>
<p>You have a value at A. You want it at B. Snapping from A to B looks bad. Animating it manually is annoying.</p>
<p>Lerp and smoothstep do the in-between math for you. That's all they are.</p>
<h1>Lerp</h1>
<p>Lerp is short for "linear interpolation." Simpler word: "blend." Lerp blends between two values.</p>
<pre><code class="language-plaintext">lerp(a, b, t) = a + (b - a) * t
</code></pre>
<p>Three inputs:</p>
<ul>
<li><p><code>a</code> is the start value</p>
</li>
<li><p><code>b</code> is the end value</p>
</li>
<li><p><code>t</code> is how far between them, from 0 to 1</p>
</li>
</ul>
<p>When <code>t = 0</code>, you get <code>a</code>. When <code>t = 1</code>, you get <code>b</code>. When <code>t = 0.5</code>, you get the value halfway. When <code>t = 0.25</code>, you get a quarter of the way from <code>a</code> to <code>b</code>.</p>
<p>That's it. Blend <code>a</code> and <code>b</code> by some amount.</p>
<h1>What lerp is used for</h1>
<p><strong>Color blending.</strong> Lerp red and blue with <code>t = 0.5</code> to get purple.</p>
<p><strong>Position movement.</strong> Lerp position A and position B with <code>t = 0.3</code> to get a point 30% of the way from A to B.</p>
<p><strong>Volume fade.</strong> Lerp current volume toward 0 with <code>t = 0.1</code> every frame. Audio fades out smoothly.</p>
<p><strong>Camera follow.</strong> Each frame, lerp camera position toward player position by <code>t = 0.1</code>. Most game cameras do this.</p>
<p><strong>Brightness change.</strong> Lerp current brightness toward target brightness when the player walks into a dark room.</p>
<p>Lerp doesn't care what <code>a</code> and <code>b</code> are. Numbers, colors, positions, vectors. Anything that supports addition and multiplication works.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/37f9088e-7826-42dd-ad3a-e34efdb60ee4.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>The catch with lerp</h1>
<p>Lerp moves at constant speed. Same speed the whole way through.</p>
<p>Sometimes that's what you want. A simple slide. A simple fade.</p>
<p>Often it looks robotic. Real motion ramps up and slows down. A door opens slowly, swings through the middle, eases into the end. Lerp can't do that on its own.</p>
<p>That's where smoothstep comes in.</p>
<h1>Smoothstep</h1>
<p>Same idea as lerp (blend between two values), but the curve is soft at both ends instead of straight.</p>
<p>With lerp, you start at full speed and stop at full speed. With smoothstep, you start slow, speed up in the middle, slow down at the end.</p>
<pre><code class="language-plaintext">smoothstep(t) = t * t * (3 - 2 * t)
</code></pre>
<p>You don't need to memorize this. The result is an S-shaped curve. Flat at <code>t = 0</code> (no movement yet), flat at <code>t = 1</code> (movement done), steep in the middle (most of the change happens here).</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/fb905e99-78a3-4703-8953-24e8b1986d76.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>When to use which</h1>
<p>Use <strong>lerp</strong> for:</p>
<ul>
<li><p>Simple direct blending where the curve doesn't matter</p>
</li>
<li><p>Calling every frame with a small <code>t</code> for a smooth follow effect</p>
</li>
</ul>
<p>Use <strong>smoothstep</strong> for:</p>
<ul>
<li><p>One-shot animations (door opening, panel sliding in, fade)</p>
</li>
<li><p>Anywhere the start and end should feel natural instead of abrupt</p>
</li>
<li><p>Animation timelines where <code>t</code> goes from 0 to 1 over a fixed duration</p>
</li>
</ul>
<p>Rule of thumb: lerp every frame for follow behavior, smoothstep over a fixed duration for animation.</p>
<h1>The "lerp every frame" trick</h1>
<p>The most common use of lerp in games.</p>
<pre><code class="language-plaintext">camera.position = lerp(camera.position, player.position, 0.1)
</code></pre>
<p>This runs every frame. Each frame, the camera moves 10% of the way toward the player.</p>
<p>The result: the camera moves fast when far away, slow when close. It chases the player and decelerates naturally as it catches up. You didn't write any easing logic. The pattern produces it for free.</p>
<p>Use this for cameras, UI smoothing, value tracking. Anywhere one value should chase another.</p>
<p>Bigger <code>t</code> (like 0.3) is snappier. Smaller <code>t</code> (like 0.05) is lazier.</p>
]]></content:encoded></item><item><title><![CDATA[IK and FK, what they actually are]]></title><description><![CDATA[What is Kinematics
Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver]]></description><link>https://tigerabrodi.blog/ik-and-fk-what-they-actually-are</link><guid isPermaLink="true">https://tigerabrodi.blog/ik-and-fk-what-they-actually-are</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 10 May 2026 21:20:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/da14ff9b-f2cf-4658-89d2-cb6b2e324f44.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What is Kinematics</h1>
<p><strong>Kinematics</strong> is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inverse kinematics. The names literally describe direction of computation.</p>
<h1>FK: Forward Kinematics</h1>
<p>You rotate joints from the top down. The end of the chain (a hand, a foot) ends up wherever those rotations put it.</p>
<p>For an arm:</p>
<ol>
<li><p>Rotate the shoulder.</p>
</li>
<li><p>Rotate the elbow.</p>
</li>
<li><p>Rotate the wrist.</p>
</li>
<li><p>The hand is now in some position. You didn't pick where. It's the result.</p>
</li>
</ol>
<p>You're controlling each joint directly. The endpoint is whatever you get.</p>
<p>Think of a robotic arm where you press buttons to spin each motor. You don't tell it "put the gripper at this point." You tell each motor how to turn. The gripper ends up wherever those motor turns put it.</p>
<h1>IK: Inverse Kinematics</h1>
<p>The opposite. You say where the endpoint should go. The system figures out what every joint needs to do to get it there. This one is pretty cool once it clicks!</p>
<p>For the same arm:</p>
<ol>
<li><p>You set a target point in space (a doorknob, the ground, an enemy's chest).</p>
</li>
<li><p>You tell the system "put the hand here."</p>
</li>
<li><p>The system calculates: what shoulder rotation, elbow rotation, and wrist rotation make the hand land at that point?</p>
</li>
<li><p>The joints bend automatically.</p>
</li>
</ol>
<p>You only control the endpoint. The joints are the result.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/726d9c4d-1c9c-4d7a-a800-977bfd17a182.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Why both exist and when to use each</h1>
<p>The mental model is simple: <strong>FK plays the artist's vision, IK adapts to the world.</strong></p>
<p>FK is how authored animations work. An animator hand-crafted a walk, a punch, a spell cast. That motion has personality, hip sway, shoulder counter-rotation, the way the head bobs slightly. Dozens of subtle joint motions that make the character feel alive. All of this is stored as bone rotations and played back as FK at runtime. Cheap, expressive, but locked to whatever the animator made.</p>
<p>IK is what you reach for when the world has something to say about the pose. The ground is bumpy. The doorknob is at a different height than expected. The enemy keeps moving. You can't pre-bake any of this because you don't know it ahead of time. IK runs at runtime, takes a target point, and bends the chain to reach it.</p>
<p>If you tried to use only IK, three things break:</p>
<ol>
<li><p><strong>You lose the authored personality.</strong> IK only knows where the endpoint should be. It has no idea how the hips should sway or how the shoulders should counter-rotate. You'd get correct foot positions and a lifeless body. A robotic shuffle.</p>
</li>
<li><p><strong>IK has too many valid answers.</strong> The hand can reach the doorknob with the elbow up, down, or sideways. All technically solve the math. Without an FK pose to guide it, the IK picks weird answers. Elbows pointing the wrong way, knees bending sideways.</p>
</li>
<li><p><strong>It's expensive.</strong> FK is one matrix multiply per joint. IK is a math problem to solve. Doing IK on every joint every frame for every character would melt the CPU. You can afford a few IK solves (feet, hands, head). You can't afford full-body IK.</p>
</li>
</ol>
<p>If you tried to use only FK, you lose all world adaptation. The character's foot floats above the rock or clips through it. The hand misses the doorknob. The head looks straight ahead instead of tracking the player. The character feels disconnected from the scene.</p>
<p>So the rule:</p>
<ul>
<li><p><strong>FK</strong> carries the authored motion. It's the base layer, always playing whatever animation is active.</p>
</li>
<li><p><strong>IK</strong> adjusts specific bones at runtime to match the world. Foot placement, hand attachment, head look-at, aim.</p>
</li>
</ul>
<p>Most game characters use both, layered together. The walk animation plays as FK. Foot IK adjusts each foot to the ground beneath it. Head IK rotates the neck toward whatever the character is looking at. Hand IK keeps the gun in the right grip position. All on top of the same animation, all running at the same time.</p>
<p>A useful way to think about it: pure FK is a movie. The motion is fixed, the artist controls everything, the world is whatever was filmed. Pure IK is a puppet with strings. You pull the strings and the body follows mechanically. Real games are both at once: a movie playing, with strings on top to make the puppet react to a world that keeps changing.</p>
<h1>The vocabulary you'll hear</h1>
<p>These all show up around IK and FK. Quick definitions:</p>
<p><strong>Skeleton (or rig).</strong> The hierarchy of joints that controls the character. Like a stick figure underneath the visible mesh.</p>
<p><strong>Bone (or joint).</strong> One element of the skeleton. Each bone has a parent and rotates relative to it.</p>
<p><strong>Skinning.</strong> Attaching the visible 3D mesh to the skeleton. Each vertex of the mesh is "weighted" to one or more bones. Move a bone, the attached vertices follow.</p>
<p><strong>IK chain.</strong> The run of bones IK is solving. "Shoulder to hand" is a chain of 3 joints (shoulder, elbow, wrist). "Hip to foot" is a chain of 3 joints (hip, knee, ankle).</p>
<p><strong>IK target.</strong> The point in space the chain's endpoint should reach. Usually a small invisible object you move around in code.</p>
<p><strong>Pole target (or pole vector).</strong> A second control that tells the IK which way to bend. For an arm, the elbow could point in any direction while still putting the hand at the target. The pole target says "elbow should aim at this point." Without it, the elbow might point in weird directions.</p>
<p><strong>Constraint.</strong> A rule that limits joint movement. "The knee can only bend forward, not backward." Constraints keep IK from producing impossible poses.</p>
<p><strong>Two-bone IK.</strong> The simplest IK setup. Three joints, two bones. An arm. A leg. Has a clean math formula. Fast.</p>
<p><strong>FABRIK / CCD.</strong> Iterative IK solvers for longer chains, like a tail with 10 bones. They loop and refine the solution. Slower but flexible.</p>
<p><strong>Look-at constraint.</strong> A tiny IK that rotates one bone to point at a target. Used for head tracking and eye following.</p>
<p><strong>Retargeting.</strong> Taking an animation made for one skeleton and applying it to a different one. Works well when both skeletons follow the humanoid pattern.</p>
<p><strong>Procedural animation.</strong> Animation generated by code at runtime instead of pre-recorded. Heavy use of IK. Foot placement on terrain, head look-at, reaching for things.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/80c4f8e9-bc34-4b18-b5bf-52e99a11ca16.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Practical: what does this look like in code</h1>
<p>Imagine you have a humanoid rigged character in three.js or Babylon.js or Godot. The character has a skeleton with named bones (Hips, Spine, LeftShoulder, LeftElbow, LeftWrist, etc).</p>
<p><strong>FK example:</strong> play a pre-made walk animation.</p>
<pre><code class="language-plaintext">const walkAnimation = loadAnimation("walk.glb")
character.play(walkAnimation)
</code></pre>
<p>The animation file contains rotations per bone per frame. The engine applies them in order down the skeleton. The character walks. Pure FK.</p>
<p><strong>IK example:</strong> make the character look at the player.</p>
<pre><code class="language-plaintext">const headBone = character.getBone("Head")
const target = player.position
headBone.lookAt(target)  // simple one-bone IK
</code></pre>
<p>Or stick the character's hand on a doorknob:</p>
<pre><code class="language-plaintext">const handIK = createIKChain(character, "LeftShoulder", "LeftWrist")
handIK.target = doorknob.position
handIK.solve()  // calculates the new joint rotations
</code></pre>
<p>After this runs, the arm has bent to put the hand on the doorknob.</p>
<p><strong>Layered example:</strong> play a walk animation AND keep feet on uneven ground.</p>
<pre><code class="language-plaintext">character.play(walkAnimation)        // FK base motion
adjustFeetWithIK(character, terrain) // IK pass on top
</code></pre>
<p>The walk animation gives the legs their stepping motion. The IK pass overrides the foot positions to match the actual ground. The character's legs look natural even when the ground is bumpy.</p>
<p>This is the pattern most game characters use. FK animation as the base, IK as the layer that adapts to the world.</p>
<h1>What you can build with this</h1>
<ul>
<li><p><strong>Realistic foot placement</strong> on stairs, slopes, and rocks.</p>
</li>
<li><p><strong>Hand attachment</strong> to objects, weapons, ladders.</p>
</li>
<li><p><strong>Procedural look-at</strong> so characters react to the world (turning their head when something moves).</p>
</li>
<li><p><strong>Aim offset</strong> for shooting in any direction without baking 360 directional animations.</p>
</li>
<li><p><strong>Ragdoll physics</strong> combined with animation. The body falls like a doll, but specific limbs can still be controlled.</p>
</li>
<li><p><strong>Climbing systems</strong> where each grip is an IK target.</p>
</li>
<li><p><strong>Driving and riding</strong> where hands grip a wheel and feet hit pedals.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to make your textures fast on the GPU]]></title><description><![CDATA[Intro
Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more.
This post is about the less obvious]]></description><link>https://tigerabrodi.blog/how-to-make-your-textures-fast-on-the-gpu</link><guid isPermaLink="true">https://tigerabrodi.blog/how-to-make-your-textures-fast-on-the-gpu</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 10 May 2026 18:28:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/35b73118-d7c4-406f-9f45-1a4c5b2ffc50.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Intro</h1>
<p>Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more.</p>
<p>This post is about the less obvious stuff. KTX2, texture arrays, atlases, mipmaps, and a few other tricks that turn a slow texture pipeline into a fast one.</p>
<h1>The two problems textures cause</h1>
<p><strong>Memory.</strong> Textures sit in VRAM. The GPU reads from them every frame. More textures, more VRAM. Run out of VRAM and your game crashes on weaker devices.</p>
<p><strong>Bandwidth.</strong> Reading a texture pixel takes time. Reading from a smaller, compressed texture is faster than reading from a huge raw one. Modern GPUs are often bandwidth-limited, not compute-limited. Smaller and better-organized textures mean faster rendering.</p>
<p>Optimization comes down to: store less, organize better, sample faster.</p>
<h1>KTX2: ship textures already compressed for the GPU</h1>
<p>The single biggest win in your texture pipeline.</p>
<p>Normal pipeline with PNG or JPEG:</p>
<ol>
<li><p>PNG sits on disk (small, compressed for storage).</p>
</li>
<li><p>CPU loads it.</p>
</li>
<li><p>CPU decodes it to raw RGBA pixels.</p>
</li>
<li><p>CPU uploads those raw pixels to the GPU.</p>
</li>
<li><p>GPU stores raw pixels in VRAM.</p>
</li>
</ol>
<p>A 2048x2048 PNG might be 2 MB on disk. After CPU decoding, it's 16 MB of raw pixels in VRAM. The compression was for storage only. The GPU sees nothing of it.</p>
<p>KTX2 pipeline:</p>
<ol>
<li><p>KTX2 sits on disk, compressed in a format the GPU can read directly (BC7 on desktop, ASTC or ETC2 on mobile).</p>
</li>
<li><p>CPU loads it.</p>
</li>
<li><p>CPU uploads it directly to the GPU. No decoding step.</p>
</li>
<li><p>GPU stores it compressed in VRAM.</p>
</li>
<li><p>GPU decodes individual blocks on the fly when it samples.</p>
</li>
</ol>
<p>Same texture, roughly 4 MB on disk AND 4 MB in VRAM. Same image quality. 4x less memory. Less CPU work. Less bandwidth between CPU and GPU.</p>
<p>KTX2 is a container format. It holds the GPU compressed data. The actual compression formats inside (BC7, ASTC, ETC2) are what the GPU reads.</p>
<p>This isn't optional for serious work. PNG and JPEG are for storing photos for humans. KTX2 is for shipping textures to GPUs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/51736bcc-f24f-4c9b-b6ea-0c817c9c81e4.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Mipmaps: smaller versions for distant objects</h1>
<p>A texture is sampled at every pixel of every triangle it appears on. A texture covering a tree on the horizon might use 50 pixels of the screen. Sampling a 2048x2048 texture for 50 pixels is wasteful.</p>
<p>Mipmaps are pre-computed smaller versions of a texture. Each level is half the size of the previous one.</p>
<pre><code class="language-plaintext">level 0: 2048x2048 (full size)
level 1: 1024x1024
level 2: 512x512
level 3: 256x256
...
level 11: 1x1
</code></pre>
<p>When the GPU draws a triangle, it picks the mipmap level whose pixel size matches the triangle's screen size. Distant trees sample tiny mipmaps. Nearby objects sample the full texture.</p>
<p>Why this is fast:</p>
<ul>
<li><p>Smaller mipmap = less data to read = less bandwidth.</p>
</li>
<li><p>Smaller mipmap = better cache behavior. The GPU's texture cache keeps small mips fully loaded.</p>
</li>
<li><p>No aliasing. Without mipmaps, distant textures look noisy and shimmery as they sample random pixels of the full texture.</p>
</li>
</ul>
<p>Cost: about 33% more VRAM (all the smaller mips combined are roughly 1/3 the size of the original). You always want mipmaps. The memory cost is more than paid back by the bandwidth savings.</p>
<p>KTX2 stores mipmaps inside the same file. Generate them once during your build step, ship them with the texture, the GPU uses them automatically.</p>
<h1>The draw call problem</h1>
<p>Every time you switch which texture is bound, the CPU has to issue a state change to the GPU. State changes are slow. Many state changes per frame is a major source of CPU bottleneck.</p>
<p>If your scene has 100 objects with 100 different textures, that's at least 100 texture binds per frame. The GPU does the work fast. The CPU spends most of its time setting up the work.</p>
<p>The fix: stop binding 100 textures. Bind one and let the shader pick.</p>
<h1>Texture atlases: many textures in one big texture</h1>
<p>The simplest approach. Pack many small textures into one big texture. Each object samples a different region.</p>
<pre><code class="language-plaintext">+----------+----------+----------+
| brick    | grass    | wood     |
+----------+----------+----------+
| stone    | sand     | dirt     |
+----------+----------+----------+
| metal    | rust     | concrete |
+----------+----------+----------+
</code></pre>
<p>The shader gets UV coordinates that point to a sub-region of the big texture instead of the whole texture.</p>
<p>One bind for all 9 surface types. One draw call can render 9 different materials.</p>
<p>This works great for 2D games, UI, and games with many small simple textures. It has limits:</p>
<ul>
<li><p>Mipmaps don't work cleanly. The GPU might pick a small mip and start sampling pixels from the neighboring atlas tile. You get color bleeding.</p>
</li>
<li><p>Repeating (tiled) textures don't work. You can't have the texture wrap.</p>
</li>
<li><p>Hand-packing the atlas is annoying.</p>
</li>
</ul>
<p>For modern 3D games with PBR materials and tiled textures, atlases aren't enough. That's where texture arrays come in.</p>
<h1>Texture arrays: stacked layers</h1>
<p>A texture array looks like a single texture from the shader's perspective, but it has multiple layers stacked on top of each other. The shader picks which layer to sample.</p>
<pre><code class="language-plaintext">layer 0: brick texture (2048x2048)
layer 1: grass texture (2048x2048)
layer 2: wood texture (2048x2048)
...
layer N: concrete texture (2048x2048)
</code></pre>
<p>All layers are the same size and format. The shader samples it like:</p>
<pre><code class="language-plaintext">texture(myArray, vec3(uv.x, uv.y, layerIndex))
</code></pre>
<p>The third coordinate picks the layer.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/ef810963-91c1-4cdf-8347-5f2e7e8961af.png" alt="" style="display:block;margin:0 auto" />

<p>Why texture arrays beat atlases (especially in the case of 3D games):</p>
<ul>
<li><p><strong>Mipmaps work normally.</strong> Each layer has its own full mipmap chain. No bleeding between layers.</p>
</li>
<li><p><strong>Wrapping works.</strong> A layer can tile and repeat like a normal texture. An atlas can't do this, because wrapping past UV 1.0 walks you into the neighboring texture instead of looping back.</p>
</li>
<li><p><strong>One bind for all layers.</strong> Same draw-call benefit as an atlas.</p>
</li>
<li><p><strong>You can pick the layer per-pixel.</strong> A terrain shader can blend between grass and rock based on height, sampling different layers in the same draw call.</p>
</li>
</ul>
<p>The catch: every layer must be the same size and format. If you have one 2048 texture and one 512 texture, you either upscale the small one (waste) or use separate arrays (defeats the point).</p>
<p>This is why teams normalize their texture sizes. All character albedo textures are 1024. All environment albedo textures are 2048. All UI icons are 256. Same size = stack them in arrays.</p>
<h1>Bindless textures: the modern endgame</h1>
<p>Texture arrays still need all layers to be the same size. Bindless textures break that limit.</p>
<p>With bindless, every texture in your scene gets a numeric ID. The shader takes the ID and samples directly. No binding state. No need to group textures by size or format. Any texture, any time, any draw call.</p>
<pre><code class="language-plaintext">sample(textureID = 47, uv)
</code></pre>
<p>This is what modern AAA renderers use. UE5 Nanite, idTech, Frostbite. All bindless.</p>
<p>WebGPU has limited bindless support today (working on it). When it lands properly in browsers, the texture optimization story for web games changes a lot.</p>
<p>For now, texture arrays are the most powerful tool you have in WebGPU.</p>
<h1>Compression formats: which one when</h1>
<p>KTX2 is the container. Inside, you pick a compression format. Each one has tradeoffs.</p>
<p><strong>BC7.</strong> Desktop standard. High quality, supports alpha, decent compression ratio. Use for desktop builds.</p>
<p><strong>BC6H.</strong> For HDR textures (skyboxes, light probes). Stores high dynamic range data efficiently.</p>
<p><strong>ASTC.</strong> Mobile standard. Adjustable block size (4x4 to 12x12) lets you trade quality for size. Use for iOS and modern Android.</p>
<p><strong>ETC2.</strong> Older mobile standard. Universally supported on Android. Use as a fallback for older devices.</p>
<p><strong>Basis Universal (UASTC / ETC1S).</strong> A "transcode anywhere" format. You ship one Basis-encoded file. At load time, the runtime transcodes it to whatever format the user's GPU supports (BC7 on desktop, ASTC on mobile, etc.). Slightly worse quality than format-native encoding, but you ship one file instead of three. KTX2 supports Basis natively.</p>
<p>For a web game targeting both desktop and mobile, Basis inside KTX2 is the easy answer. For a desktop-only build, BC7 inside KTX2 is the highest quality.</p>
<h1>Texture streaming: load only what's visible</h1>
<p>You don't need every texture loaded all the time. The player can only see what's in front of them.</p>
<p>Streaming means: load the high-res mips of textures the player can currently see, keep low-res mips for everything else, swap as the player moves.</p>
<p>A 2048x2048 mipmapped texture has about 22 MB total across all its mips. The full texture (top mip) alone is 16 MB. The smaller mips combined are about 6 MB. If the player is far from this object, you only need the smaller mips. Save 16 MB.</p>
]]></content:encoded></item><item><title><![CDATA[Convex hulls and why they matter for collision]]></title><description><![CDATA[The problem: collision is everywhere in games
Every game has to answer one question over and over: did this thing touch that thing?
Did the bullet hit the enemy? Did the player land on the platform? D]]></description><link>https://tigerabrodi.blog/convex-hulls-and-why-they-matter-for-collision</link><guid isPermaLink="true">https://tigerabrodi.blog/convex-hulls-and-why-they-matter-for-collision</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Fri, 08 May 2026 17:34:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/bdd649d5-bba8-4392-8298-0eb612016a5e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>The problem: collision is everywhere in games</h1>
<p>Every game has to answer one question over and over: did this thing touch that thing?</p>
<p>Did the bullet hit the enemy? Did the player land on the platform? Did the sword swing pass through the goblin? Did the car crash into the wall? Did the cursor click the button?</p>
<p>That question has a name. <strong>Collision detection.</strong></p>
<p>It sounds simple. It is not. A typical game runs collision checks thousands of times per frame, 60 times per second. If each check is slow, the whole game is slow. If each check is wrong, the player walks through walls or floats above the floor.</p>
<p>So game engines need a way to answer "did A touch B" that is both fast AND correct.</p>
<h1>The naive way: check every triangle</h1>
<p>Remember that 3D models are made of triangles. A character mesh might be 20,000 triangles. A wall might be 500 triangles.</p>
<p>The naive collision check is: for every triangle in the player, test if it overlaps any triangle in the wall. 20,000 × 500 = 10 million triangle vs triangle tests. Per pair of objects. Per frame.</p>
<p>If the scene has 100 objects, that's 100 × 100 × 10 million = 100 billion checks per frame. Your game runs at 0 fps. The CPU melts.</p>
<h1>The fix: collision shapes</h1>
<p>Game objects use a separate, simpler shape just for collision. The thing you see (the visual mesh) and the thing the engine uses for collision (the collision shape) are two different things.</p>
<p>The visual mesh of a character: 20,000 triangles, looks like a person.</p>
<p>The collision shape of the same character: maybe a single capsule (a cylinder with rounded ends). Or a box. Or a sphere. Or a small handful of simple shapes glued together.</p>
<p>Why? Because checking "did this capsule touch that box" is something you can do with a few math operations. Checking "did 20,000 triangles touch 500 triangles" is not.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/2e8cacce-a1fb-4d87-bf42-5af7491757b8.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>So what shapes can we use?</h1>
<p>Game engines have a small list of "primitive shapes" that are fast to test for collision:</p>
<ul>
<li><p><strong>Sphere.</strong> Two spheres collide if the distance between their centers is less than the sum of their radii. One math operation. Fastest collision check there is.</p>
</li>
<li><p><strong>Box.</strong> Slightly more math but still cheap.</p>
</li>
<li><p><strong>Capsule.</strong> Like a cylinder with rounded ends. Great for characters.</p>
</li>
<li><p><strong>Cylinder.</strong> Sometimes.</p>
</li>
<li><p><strong>Plane.</strong> For floors and walls.</p>
</li>
</ul>
<p>Most things in games can be simplified using these shapes or a mix of them. A character can be a capsule. A barrel can be a cylinder. A crate can be a box. A coin can be a small box.</p>
<p>But what if your shape isn't any of these? What if it's a weird custom shape, like a rock with bumps, or a sword, or a piece of architecture with multiple jutting parts?</p>
<p>That's where convex hulls come in.</p>
<h1>What "convex" actually means</h1>
<p>Before we talk about convex hulls, let's talk about what the word "convex" means. This is the word that tripped me up when I was learning this stuff.</p>
<p>A shape is <strong>convex</strong> if you can draw a straight line between any two points inside the shape, and the entire line stays inside the shape.</p>
<p>That's it. That's the whole definition. Pick any two points in the shape, connect them with a straight line, never leave the shape.</p>
<p>A circle is convex. Pick any two points inside it, draw a line, the line never leaves the circle.</p>
<p>A square is convex. Same test, same result.</p>
<p>A triangle is convex. A box is convex. A capsule is convex. Any shape made by stretching a balloon outward is convex.</p>
<p>A shape is <strong>concave</strong> (the opposite of convex) if you can find at least two points inside it where the line between them goes OUTSIDE the shape at some point.</p>
<p>A donut is concave. Pick a point on one side of the hole and a point on the other side. The line between them passes through the hole, which is outside the donut. So a donut is not convex.</p>
<p>A star shape is concave. A letter "L" is concave. A capital "C" is concave. Any shape with a dent, hole, or curve that bends inward is concave.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/164a4671-ba10-41b1-9fa2-e4e0d8808077.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Why convex shapes are a big deal for collision</h1>
<p>Here's the magic. Collision detection between two convex shapes is fast. Like, really fast. There are clean math algorithms (one famous one is called <a href="https://en.wikipedia.org/wiki/Gilbert%E2%80%93Johnson%E2%80%93Keerthi_distance_algorithm">GJK</a>) that can test "did these two convex shapes collide" in just a few microseconds.</p>
<p>But collision between a concave shape and anything else is hard. The math doesn't have a clean shortcut. You usually end up breaking the concave shape into pieces somehow, which gets messy.</p>
<p>So engines work hard to avoid concave shapes. They want every collision shape in the scene to be convex.</p>
<p>The reason: with convex shapes, the engine knows that the shape has no "hidden" parts. No tunnels. No notches. No pockets. The outside is fully outside. That property is what lets the math be clean.</p>
<h1>What a convex hull is</h1>
<p>A <strong>convex hull</strong> is the smallest convex shape that fully wraps around a given set of points or a given mesh.</p>
<p>Imagine you have a complicated mesh, like a treasure chest with a curved lid, handles sticking out, and detail bumps on every surface. That mesh is concave (it has dents and gaps).</p>
<p>Now imagine wrapping a tight rubber band or a piece of plastic wrap around the whole thing. The wrapper touches the outermost points but skips over the dents and notches. The result is a simpler shape that:</p>
<ul>
<li><p>Fully contains the original</p>
</li>
<li><p>Has no dents or holes</p>
</li>
<li><p>Is convex</p>
</li>
</ul>
<p>That's a convex hull. The "shrink-wrapped" version of your mesh.</p>
<p>In 2D, picture nails hammered into a board in a random pattern. Stretch a rubber band around all the nails. The shape of the rubber band is the convex hull of those nails.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/813a4747-75d3-4b25-9689-56c691063693.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Convex hulls in 3D</h1>
<p>Same idea. Take any 3D mesh, no matter how detailed. The convex hull is the smallest convex shape that fully wraps it.</p>
<p>A character mesh has thousands of triangles, with concave parts (the gap between legs, the curve of the armpit, the space between fingers). Its convex hull might be just 30 to 50 triangles, like a rough faceted balloon around the character.</p>
<p>Game engines compute the convex hull of a mesh once, save it, and use that for collision instead of the original mesh. You went from 20,000 triangles to 50 triangles, AND those 50 triangles form a convex shape that's fast to test against.</p>
<p>That's the trick. That's why convex hulls are such a big deal.</p>
<h1>What you give up</h1>
<p>Convex hulls aren't perfect. By definition they ignore dents and gaps in the original shape. So:</p>
<ul>
<li><p>The space between a character's legs gets filled in. The hull treats it as a solid.</p>
</li>
<li><p>A donut's hole gets filled. The donut becomes a flattened disc.</p>
</li>
<li><p>An "L" shaped piece of architecture becomes a triangle that fills the inner corner.</p>
</li>
</ul>
<p>For most things this is fine. A character with the leg gap filled in still collides correctly enough that the player can't tell. A donut prop in a game probably doesn't need bullets to fly through the hole.</p>
<p>But for some shapes you DO need the concave parts. A house with a doorway. A tunnel. A cup that things should be able to fall into. A maze.</p>
<h1>How engines handle truly concave shapes</h1>
<p>When you really need the concave parts, engines use one of these tricks:</p>
<p><strong>Convex decomposition.</strong> Break the concave shape into a bunch of smaller convex pieces. A donut becomes maybe 12 wedge-shaped chunks arranged in a ring. Collision is then "did anything touch any of these 12 convex pieces." Slower than one convex check but still fast. Tools like V-HACD do this automatically.</p>
<p><strong>Triangle mesh collision (only for static stuff).</strong> For things that never move (the ground, the level geometry, big buildings), engines do use the actual triangles, but with heavy spatial optimizations like a BVH (bounding volume hierarchy) so they only check the few triangles near the player. This works for static geometry but is too slow for moving objects.</p>
<p><strong>Compound shapes.</strong> Manually glue several primitive shapes together. A character might be a sphere for the head, a capsule for the torso, capsules for each limb. Each piece is convex, the whole assembly approximates the character.</p>
<p>The pattern is always the same: keep collision shapes convex, and if you can't, break them into multiple convex pieces.</p>
]]></content:encoded></item><item><title><![CDATA[Juice is the difference between a game that feels alive and one that doesn't]]></title><description><![CDATA[Intro
Think back to the last game you played that felt good.
What happened when you jumped? Did the camera move a little? Did your character squish for a moment before launching?
What happened when yo]]></description><link>https://tigerabrodi.blog/juice-is-the-difference-between-a-game-that-feels-alive-and-one-that-doesn-t</link><guid isPermaLink="true">https://tigerabrodi.blog/juice-is-the-difference-between-a-game-that-feels-alive-and-one-that-doesn-t</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Fri, 08 May 2026 01:38:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/9c2cfda0-ee01-4a5c-8c84-0f9cf200d2a1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Intro</h1>
<p>Think back to the last game you played that felt good.</p>
<p>What happened when you jumped? Did the camera move a little? Did your character squish for a moment before launching?</p>
<p>What happened when you took a hit? Did the screen flash white? Did time freeze for a tiny moment? Did the camera shake?</p>
<p>What happened when you fired a weapon? Did the camera kick back? Did empty shells fly out? Did the wall light up for one frame from the muzzle flash?</p>
<p>What happened when a flashbang went off? Did your hearing get muffled? Did the screen go white? Did your character flinch?</p>
<p>None of that is gameplay. None of it changes what's actually happening in the game. The bullet still hits. The jump still works. The explosion still does damage. But take those effects out and the game feels dead. Add them in and the game feels alive.</p>
<p>That's juice.</p>
<h1>What juice is</h1>
<p>Juice is the layer of feedback you put on top of every action. It's not the action itself. It's the response to the action. The screen shake. The brief freeze. The particles. The camera bump. The sound. The controller rumble. The slight slow motion. The squish and stretch.</p>
<p>Your brain reads these signals as "something just happened." Without them, actions feel weightless. With them, even a small button press feels like it mattered.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/fd5cade7-49d5-4098-9838-90d59181cce5.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Why it works</h1>
<p>In real life, every action gives your body feedback. Hit a wall and your hand hurts. Drop a glass and you hear it break. Jump and you feel your knees absorb the landing. Your brain has spent your whole life learning to read these signals.</p>
<p>Games are silent until you add feedback. So designers borrow from real life and from cartoons. A character squishes before it jumps because real things bend under force. A screen shakes during an explosion because real explosions vibrate everything around them. A character flashes red when hit because the brain reads "red flash = pain" right away.</p>
<p>You're not pretending to be realistic. You're giving the brain the signals it expects. That's why a small 2D pixel game with great juice can feel more grounded than a photorealistic 3D game without it.</p>
<hr />
<h1>The toolbox</h1>
<p>There are a few core types of juice. Most polished games combine many of them at the same time.</p>
<p><strong>Squash and stretch.</strong> Borrowed from animation. A ball gets short and wide when it hits the ground, then tall and thin when it launches. Apply this to anything: jumping, landing, taking damage, picking up an item, opening a chest. Just bending a shape for a few frames sells the impact.</p>
<p><strong>Screen shake.</strong> A small camera offset for a few frames after impact. Use it carefully. A small shake on hits feels powerful. Constant shake feels bad and makes people sick.</p>
<p><strong>Hit pause (also called hit stop).</strong> Freeze the whole game for 1 to 5 frames at the moment of impact. The brain reads the freeze as "this hit MATTERED." Used a lot in fighting games and any game with melee combat.</p>
<p><strong>Flash.</strong> Briefly tint the hit object white, red, or pink. One or two frames. Reads as "this got hit" right away.</p>
<p><strong>Particles.</strong> Small bursts of dots or fragments at the impact point. Sparks, dust, blood, leaves, debris, whatever fits the world. Even simple circles work.</p>
<p><strong>Camera bump.</strong> Like screen shake, but in a specific direction. The camera lurches the opposite way of the action. Used for recoil, explosions, big landings.</p>
<p><strong>Trails.</strong> Lines or fading shapes following fast moving objects. Sells speed.</p>
<p><strong>Sound layers.</strong> A single hit sound is fine. A hit sound + a low thump + a high crack + a tail of reverb is amazing. Stack a few short sounds together at slightly different pitches.</p>
<p><strong>Slow motion.</strong> Briefly drop the game to half or quarter speed when something dramatic happens. The kill in a stealth game. The final hit on a boss. The moment the player almost dies.</p>
<p><strong>Per-object slow motion.</strong> Even better than global slow motion. Slow down only the affected enemy for a moment. The enemy you killed crumples slowly while the rest of the world keeps moving normally.</p>
<p><strong>Color split (chromatic aberration).</strong> A small red and blue split at the screen edges during heavy impacts. Reads as "intensity."</p>
<p><strong>Controller rumble.</strong> Free real estate. A short rumble on impact, a longer one on landing, a heartbeat rumble at low health.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/aa3dbff3-d708-4294-9727-6fa79d1dbd32.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Layer everything</h1>
<p>One effect is never enough. Watch a great game in slow motion and look at a single bullet hit. It might trigger:</p>
<ul>
<li><p>Camera shake (3 frames)</p>
</li>
<li><p>Hit pause (2 frames)</p>
</li>
<li><p>White flash on the enemy (1 frame)</p>
</li>
<li><p>Red flash on the enemy (3 frames)</p>
</li>
<li><p>Particle burst at impact (10 frames)</p>
</li>
<li><p>Damage number floating up (30 frames)</p>
</li>
<li><p>Recoil on the gun (5 frames)</p>
</li>
<li><p>Empty shell flying out (60 frames)</p>
</li>
<li><p>Sound: gunshot + impact + casing tink</p>
</li>
</ul>
<h1>Ideas for your own game</h1>
<p>Things you can add in 30 minutes each that will instantly make your game feel better. Each one gives the player a specific feeling.</p>
<ul>
<li><p><strong>Squash on jump and stretch on landing.</strong> Just scale the character to 0.8 wide and 1.2 tall for a few frames. The feel: weight. The character has body. They push off the ground instead of just floating up.</p>
</li>
<li><p><strong>Hit pause on combat.</strong> Freeze the game for 2 frames every time the player lands a hit. The feel: heaviness. The hit lands like a hammer instead of passing through air. This single trick is the difference between strikes that feel weak and strikes that feel solid.</p>
</li>
<li><p><strong>Camera shake on impact.</strong> A 4-pixel random offset for 5 frames. Use a curve that fades out so it feels natural. The feel: force. Something powerful just happened and the world reacted to it.</p>
</li>
<li><p><strong>Damage numbers.</strong> Spawn a number above the enemy that drifts up and fades out. The feel: progress. Every hit is doing something measurable. People love watching numbers go up.</p>
</li>
<li><p><strong>Footstep particles.</strong> A few small dust particles at the player's feet. The feel: presence. The character is touching the ground, not gliding across it. Adds weight without changing how the character moves.</p>
</li>
<li><p><strong>UI bounce.</strong> When a button is pressed, briefly scale it to 0.9x then 1.1x then back to 1.0x. The feel: responsiveness. The button knows you clicked it. Menus feel alive instead of dead.</p>
</li>
<li><p><strong>Pickup squish.</strong> When the player grabs a coin or powerup, stretch it for one frame before it disappears. Add a sparkle particle. The feel: reward. Picking things up feels satisfying instead of mechanical.</p>
</li>
<li><p><strong>Death freeze.</strong> When something dies, freeze it for 5 frames before the death animation plays. Then explode it with particles. The feel: finality. That hit was THE hit. The enemy is dead and the moment is marked.</p>
</li>
<li><p><strong>Red flash on damage.</strong> A red overlay at 30% transparency for 4 frames. The feel: panic. The player instantly knows they got hit, even if they weren't looking at their health bar. Reads as danger before the brain has time to think.</p>
</li>
<li><p><strong>Trails on fast things.</strong> Bullets, projectiles, dashing characters. A thin fading line behind them. The feel: speed. The brain reads the trail and thinks "fast moving thing" before consciously seeing it.</p>
</li>
<li><p><strong>Lower the pitch on heavy hits.</strong> Same hit sound, but pitched down 20% on critical strikes. The feel: weight. A heavier sound is read by the brain as a heavier hit, even if the visual is identical. Stack this with hit pause and the strike feels enormous.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Just write your own libraries]]></title><description><![CDATA[Just write your own libraries
Libraries used to limit what you could do. If nothing fit your needs, you had to settle or slowly create it yourself. Now, that's changed. AI models are advanced enough f]]></description><link>https://tigerabrodi.blog/just-write-your-own-libraries</link><guid isPermaLink="true">https://tigerabrodi.blog/just-write-your-own-libraries</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Tue, 05 May 2026 17:56:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/88a64e1f-5480-4b97-b0f4-e06fc17354dd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Just write your own libraries</h1>
<p>Libraries used to limit what you could do. If nothing fit your needs, you had to settle or slowly create it yourself. Now, that's changed. AI models are advanced enough for you to make exactly what you want in just an afternoon.</p>
<hr />
<h1>The cost of using someone else's library</h1>
<p>Every library is decisions someone made for their use case, not yours. The API surface is bigger than you need. The types don't match your domain. The defaults are wrong. It pulls in dependencies you don't want. It updates and breaks something subtle. You patch around its limitations.</p>
<hr />
<h1>Writing your own</h1>
<p>You can make it tiny. Just the surface you actually use. Type-safe in your domain, composable with your code, self-documenting because it's shaped like your problem.</p>
<p>You can also build it for speed. When you write it yourself, you can target performance from the start. Add performance tests if it matters. Most generic libraries can't do this because they're optimizing for many use cases at once.</p>
<p>If something out there already does roughly what you want, use it as inspiration for the agent. Point at it, say "here's the API I like, here's the part I need, build me that." Or fork the relevant module out and have the agent simplify it, harden it, make it faster. You're not starting from a blank page, you're extracting the good parts.</p>
<hr />
<h1>TDD makes it safe</h1>
<p>Writing your own only feels safe if you know it works. TDD: write the test first, watch it fail, then implement.</p>
<p>When you write the test first, you describe the API you wish existed before committing to any implementation.</p>
<p>When you write the test after, the test is shaped by code that already exists. You write tests that pass. <strong>You don't really know if they'd catch a bug, because you wrote them with the answer in front of you.</strong></p>
<p>Watching the test fail first is the part that matters. A test that has never failed might just always pass for the wrong reason.</p>
<hr />
<h1>The tools that make TDD fast</h1>
<p><strong>Vitest</strong> for unit tests. Watch mode is sub-second. Testing stops feeling like a tax.</p>
<p><strong>Mock Service Worker (MSW)</strong> for mocking the network. It intercepts at the service worker level. Your code uses real fetch. The request is real until it hits the boundary, then MSW responds. No special branches, no injected mocks. The same setup works in unit tests, dev, and browser tests. MSW now mocks WebSockets too, which used to be painful.</p>
<p><strong>Vitest browser mode + Playwright</strong> for tests that need a real browser. Same Vitest API, same fast loop, but running in real Chromium.</p>
<hr />
<h1>The flow</h1>
<ol>
<li><p>Need some functionality.</p>
</li>
<li><p>Write a test for the API you want.</p>
</li>
<li><p>Have the agent implement it. Optionally point at an existing library as inspiration.</p>
</li>
<li><p>Iterate until the test passes and the API feels right.</p>
</li>
<li><p>Add performance tests if speed matters.</p>
</li>
<li><p>Move on.</p>
</li>
</ol>
<p>The output is a small file in your codebase that does exactly what you need. Your names, your types, no dependencies. If something changes, you change your own code. No version bumps, no upstream breaking changes, no opening GitHub issues on someone else's repo.</p>
<hr />
<h1>When to still use libraries</h1>
<p>This isn't "never use libraries again." Some libraries earn their place.</p>
<p><strong>Network effects matter.</strong> React has the entire ecosystem around it: tooling, types, jobs, documentation, every backend integration assumes it. The library itself is almost beside the point at that level. You're buying into a community, not a dependency.</p>
<p><strong>Some libraries do genuinely hard things.</strong> MSW intercepts at the service worker layer with all the protocol handling, lifecycle management, and edge cases that come with that. Replicating it would take weeks and you'd get something worse. The same goes for things like Playwright, Vitest, esbuild, the language runtime itself. These solve problems that are genuinely hard and that you do not want to own.</p>
<p>The point of this post isn't "build everything from scratch." It's "stop reaching for a library by default for things that should be 50 lines of code in your repo." Use libraries where they actually pull weight. Skip them for the long tail of utilities, helpers, wrappers, and glue where they cost more than they save.</p>
]]></content:encoded></item><item><title><![CDATA[Your game runs fine on desktop. Why does it crash on mobile?]]></title><description><![CDATA[Intro
You built a game on the web. It runs at 60 fps on your laptop. You open it on your phone and it stutters, overheats, or just crashes. Same code, same scene, completely different result.
This pos]]></description><link>https://tigerabrodi.blog/your-game-runs-fine-on-desktop-why-does-it-crash-on-mobile</link><guid isPermaLink="true">https://tigerabrodi.blog/your-game-runs-fine-on-desktop-why-does-it-crash-on-mobile</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Mon, 04 May 2026 16:35:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/d7bdfd20-28f4-417d-b4a9-14fb6057b80a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Intro</h1>
<p>You built a game on the web. It runs at 60 fps on your laptop. You open it on your phone and it stutters, overheats, or just crashes. Same code, same scene, completely different result.</p>
<p>This post explains why. By the end you'll understand the actual budgets that matter (triangles, textures, VRAM, draw calls, fill rate), what each one costs, and why mobile is harder than desktop on every single one of them.</p>
<h1>The pieces involved</h1>
<p>Before we compare desktop vs mobile, quick definitions of the parts that matter.</p>
<p><strong>CPU.</strong> The general purpose processor. Runs your JavaScript, your game logic, physics, AI. One thing at a time per core (mostly).</p>
<p><strong>GPU.</strong> The graphics processor. Designed to do thousands of small things in parallel. Draws every triangle, runs every shader, fills every pixel.</p>
<p><strong>RAM.</strong> The CPU's working memory. Where your code, variables, and game state live.</p>
<p><strong>VRAM.</strong> The GPU's working memory. Where textures, meshes, and shaders live so the GPU can read them fast. On desktop, this is often a separate chip with its own dedicated memory. On mobile, it's the same physical RAM as the CPU's.</p>
<p><strong>Bandwidth.</strong> How fast data can move between memory and the processor. Often more important than raw memory size, because if your processor can't get the data fast enough, it sits idle.</p>
<p><strong>Triangles.</strong> Every 3D shape in a game is made of triangles. A character might be 20,000 triangles. A whole scene might be a few million.</p>
<p><strong>Textures.</strong> Any image data the GPU reads. People usually picture textures as images wrapped around 3D models, but the term covers everything: sprites, particles, UI icons, fonts, normal maps, lookup tables, even the framebuffer itself. A 2048x2048 RGBA texture is about 16 MB uncompressed. Most scenes have hundreds of textures.</p>
<p><strong>Draw calls.</strong> Each time the CPU tells the GPU "draw this thing," that's a draw call. Each one has fixed overhead, regardless of how big the thing is.</p>
<p><strong>Fill rate.</strong> How fast the GPU can fill pixels with color. Limited by both the GPU's speed and the screen's resolution.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/8880f5ad-5b68-4b3a-9634-98a2c6ec060d.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Why mobile is fundamentally different</h1>
<p>The single biggest difference: <strong>memory architecture</strong>.</p>
<p>A gaming desktop has two separate memory pools. The CPU has its own RAM (16 GB or more). The GPU has its own dedicated VRAM (8 GB or more on a modern card, up to 24 GB on a flagship). They talk to each other over a fast bus called PCIe. Each processor reads from its own private memory at full speed.</p>
<p>A phone has one memory pool, <strong>shared between CPU and GPU</strong>. Your phone has, say, 6 to 12 GB total RAM depending on the model. The CPU and GPU both pull from the same pool. If your game eats 4 GB on textures, that's 4 GB the system, the OS, your game logic, and every other app on the phone don't get.</p>
<p>This is called <strong>unified memory</strong> (Apple's name) or <strong>shared memory architecture</strong>. It has some upsides (no copying data between CPU and GPU), but it has one massive downside: there's just way less of it for graphics.</p>
<p>A discrete GPU on a desktop might have 8 to 24 GB of VRAM. A flagship phone has maybe 2 to 5 GB practically available for graphics. Mid-range and older phones have less. The math is brutal.</p>
<h1>What actually costs VRAM</h1>
<p>When people say "I have a triangle budget," what they really mean is "I have a memory budget." Let's see where it goes.</p>
<p><strong>Textures dominate.</strong> A single 2048x2048 RGBA texture uncompressed is 16 MB. AAA games often ship characters with 4 or 5 textures at 2K each (color, normal, roughness, metallic, ambient occlusion), which adds up to ~80 MB per character. A scene with 50 unique objects on that budget can hit 4 GB easily.</p>
<p>But that's a choice, not a requirement. Plenty of great-looking games ship at 1K or even 512px textures and lean on art direction, lighting, and good materials instead of resolution. Cutting your texture size in half saves 4x the memory. Cut it again and you've saved 16x. For mobile, this is one of the biggest levers you have.</p>
<p><strong>Meshes are smaller but add up.</strong> A character mesh with 20,000 triangles is roughly 1 to 2 MB. A scene with hundreds of unique meshes might be 200 to 500 MB. Less than textures, still significant.</p>
<p><strong>Shaders, framebuffers, depth buffers, shadow maps.</strong> Each of these takes VRAM too. A 4K shadow map is 64 MB. The depth buffer for a 1080p screen is 8 MB. Modern rendering uses many of these.</p>
<p>So the actual breakdown for a game:</p>
<ul>
<li><p>60 to 80% textures</p>
</li>
<li><p>10 to 20% meshes</p>
</li>
<li><p>10 to 20% other (framebuffers, shadows, post-processing buffers, shaders)</p>
</li>
</ul>
<p>Triangle count itself is rarely the memory bottleneck. <strong>Textures are.</strong> This is why "what's your triangle budget" is the wrong first question. The right first question is "what's your texture memory budget."</p>
<h1>Use KTX2 (this is non-negotiable)</h1>
<p>KTX2 is the latest format for GPU-compressed textures. It stores texture data in formats the GPU can read directly, like BC7 for desktops and ASTC or ETC2 for mobile. Importantly, the texture remains compressed in VRAM. The GPU decodes blocks as needed, without decompressing the entire image.</p>
<p>Without KTX2, your pipeline looks like this: PNG on disk → CPU decodes it to raw RGBA pixels → upload millions of uncompressed pixels to the GPU → <strong>eat the full VRAM cost. A 2048x2048 PNG is maybe 2 MB on disk but 16 MB in VRAM.</strong></p>
<p>With KTX2 + BC7 (desktop) or ASTC (mobile): the same texture is roughly 4 MB on disk AND 4 MB in VRAM. Same image quality. 4x less memory. Less bandwidth. No CPU decoding work.</p>
<p>This is required for mobile. There is no path to good performance shipping raw PNGs on a phone. It's also a free win on desktop. Every modern engine and pipeline supports it. There's no reason not to use it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/eec36c04-91a4-486f-94bd-c2f0b116d73f.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Why triangle count still matters (just differently)</h1>
<p>Triangle count doesn't usually break VRAM. It breaks the CPU and GPU's ability to process them in time.</p>
<p>Each triangle has to:</p>
<ol>
<li><p>Be transformed by the vertex shader (CPU-side prep, GPU-side math).</p>
</li>
<li><p>Be tested for visibility (clipping, culling).</p>
</li>
<li><p>Be rasterized into pixels (GPU work).</p>
</li>
<li><p>Have each of its pixels shaded (more GPU work).</p>
</li>
</ol>
<p>A modern desktop GPU can chew through 30 to 100 million triangles per frame at 60 fps. A flagship phone GPU does maybe 5 to 15 million per frame. Mid-range phones do 1 to 3 million.</p>
<p>So if your scene has 5 million triangles, desktop laughs. Phone struggles. Same data, different processing speed.</p>
<p>The reason is power. Phone GPUs are tuned to fit in a battery-powered handheld that can't dissipate heat. They're efficient per watt, but they have a fraction of the raw throughput of a desktop card that draws 300 watts and has fans on it.</p>
<h1>Draw calls: the silent killer</h1>
<p>Each draw call has fixed CPU overhead. Doesn't matter if you're drawing one triangle or one million. The setup cost is per-call, not per-triangle.</p>
<p>On a desktop, you can issue maybe 5,000 to 10,000 draw calls per frame at 60 fps. On mobile, more like 100 to 500. The CPU on a phone is slower AND graphics commands are typically issued from a single thread on the web, so each draw call costs more relative to the budget.</p>
<p>This is why mobile-targeted games aggressively use <strong>batching</strong> (combining many objects into one draw call) and <strong>instancing</strong> (drawing the same mesh many times in one call). It's not optional. If your scene has 2,000 individual draw calls and you target mobile, you're already over budget before you've drawn anything.</p>
<h1>Fill rate and screen resolution</h1>
<p>The GPU has to color every pixel on screen, every frame. More pixels = more work.</p>
<p>A 1080p screen is about 2 million pixels. A desktop 4K monitor is 8 million. An iPhone Pro is around 3 million. So far so similar.</p>
<p>But mobile screens have a sneaky factor: they often run at higher pixel densities, so even though the <em>visible</em> size is small, the GPU is filling almost as many pixels as a desktop, with a fraction of the GPU power.</p>
<p>And many games use multiple full-screen passes (post-processing, bloom, screen-space reflections). Each pass runs the fragment shader on every pixel again. If you have 5 full-screen passes at 3 million pixels per pass, that's 15 million fragment shader invocations per frame just for post-processing.</p>
<p>This is called being <strong>fill rate bound</strong>. The GPU isn't slow because of triangles, it's slow because it's filling too many pixels. Mobile hits this faster because:</p>
<ul>
<li><p>Pixel density is high.</p>
</li>
<li><p>GPU fill rate is lower.</p>
</li>
<li><p>Heat throttles fill rate further the longer you play.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/bcaf66f4-05fb-4481-9dc1-ca739f56c808.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Heat and throttling: the hidden constraint</h1>
<p>Desktop and console GPUs rely on power outlets and cooling systems. Mobile GPUs depend on batteries and passive cases.  </p>
<p>In practice, a phone runs at full speed briefly, then heats up, causing the OS to reduce the GPU clock to prevent damage. This is thermal throttling.  </p>
<p>A game running at 60 fps initially and dropping to 35 fps later isn't faulty; it's throttled. The hardware can achieve 60 fps but can't maintain it without overheating.  </p>
<p>Mobile optimization is different. It's not about hitting 60 fps in a benchmark; it's about maintaining 60 fps for an hour without overheating. Sustained performance is more crucial than peak performance.</p>
<h1>Why iPhones are weirdly good (and weirdly bad)</h1>
<p>A quick note about iPhones. Apple's chips are the most powerful mobile GPUs available. The A17 Pro and M-series chips can compete with mid-range desktop GPUs in raw power. Modern Apple silicon includes hardware-accelerated ray tracing and 8 to 16 GB of shared memory. However, the same limits apply. The GPU shares memory with the CPU, and heat is still a problem. The screen has high pixel density, making the fill rate costly. Android devices vary widely. A top Samsung phone is similar to an iPhone, but an older, cheaper Android phone is much less powerful. When targeting "mobile," you're really aiming for "the weakest phone you'll accept," which is often much less capable than the iPhone you tested on.</p>
<h1>What this means for you</h1>
<p>If you're building a game on the web and want it to work on mobile, the rough budgets are:</p>
<table>
<thead>
<tr>
<th>Resource</th>
<th>Desktop budget</th>
<th>Mobile budget</th>
</tr>
</thead>
<tbody><tr>
<td>Triangles per frame</td>
<td>5 to 30 million</td>
<td>0.5 to 3 million</td>
</tr>
<tr>
<td>Texture memory</td>
<td>2 to 6 GB</td>
<td>200 to 800 MB</td>
</tr>
<tr>
<td>Draw calls per frame</td>
<td>2,000 to 10,000</td>
<td>100 to 500</td>
</tr>
<tr>
<td>Full-screen passes</td>
<td>5 to 10</td>
<td>1 to 3</td>
</tr>
<tr>
<td>Sustained GPU load</td>
<td>high</td>
<td>medium (heat)</td>
</tr>
</tbody></table>
<p>Same code running into a 10x smaller budget on every axis, while also having to share resources with the OS, the browser, and other apps.</p>
<p>When your desktop game crashes on mobile, the cause is almost always one of:</p>
<ol>
<li><p>Texture memory blew the VRAM budget.</p>
</li>
<li><p>Draw call count is too high for the CPU.</p>
</li>
<li><p>Fill rate is saturated by post-processing on a high-DPI screen.</p>
</li>
<li><p>Thermal throttling kicked in and frame time spiked.</p>
</li>
</ol>
<h1>What actually helps</h1>
<p>Concrete things that move the needle:</p>
<ul>
<li><p><strong>Ship textures as KTX2.</strong> Covered above. This alone can cut texture memory 4x to 8x. Required, not optional.</p>
</li>
<li><p><strong>Drop texture resolution on mobile.</strong> A texture at 1024x1024 instead of 2048x2048 is 4x cheaper. You probably won't notice the visual difference on a phone screen. You could probably even go much lower on moible 512x512 or 256x256.</p>
</li>
<li><p><strong>Batch and instance ruthlessly.</strong> 50 trees should be one draw call, not 50. Do the same on Desktop tbh wherever you can.</p>
</li>
<li><p><strong>Render at lower internal resolution.</strong> Phone screens are dense. An iPhone Pro packs 9 physical pixels into every 1 CSS pixel (devicePixelRatio of 3). Render at native density and the GPU shades 9x more pixels than a regular screen of the same size. Cap the pixel ratio instead: <code>renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5))</code>. Fewer pixels rendered, browser stretches to fit, fragment shader does 4x less work. Image looks slightly softer on a small phone screen. Nobody notices. Framerate and heat get massively better.</p>
</li>
<li><p><strong>Frustum cull and occlusion cull aggressively.</strong> Don't draw what isn't visible. Backface culling when possible too.</p>
</li>
<li><p><strong>LODs (level of detail).</strong> Draw simpler versions of meshes when they're far away. Have a thorough LOD system in place. You wanna aggressively do this on mobile. Octohedral impostors are great too for things medium-far away. For things very far away, they can just become flat images.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Redis is great]]></title><description><![CDATA[Introduction
I've used Redis for a lot of things over the years. Rate limiting, caching, distributed locking, tracking peak concurrency, ephemeral counters, queues, deduplication, leaderboards. Every ]]></description><link>https://tigerabrodi.blog/redis-is-great</link><guid isPermaLink="true">https://tigerabrodi.blog/redis-is-great</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 03 May 2026 20:07:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/b084c14b-1665-434c-8d6b-b0fc3d7591cc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>I've used Redis for a lot of things over the years. Rate limiting, caching, distributed locking, tracking peak concurrency, ephemeral counters, queues, deduplication, leaderboards. Every single time, it <em>just</em> worked.</p>
<p>This post is me appreciating Redis and explaining why it's so good at being a swiss army knife for backend systems.</p>
<h1>What Redis actually is</h1>
<p>Redis is an in-memory key-value store. That's the official description and it sells it short.</p>
<p>What it really is: a place to put data structures (strings, lists, sets, hashes, sorted sets, streams) and run atomic operations on them, fast, over a network.</p>
<p>Most people first encounter Redis as "a cache." It can be a cache. <strong>It's also way more than that.</strong></p>
<hr />
<h1>Why it's fast</h1>
<p>Redis stores everything in RAM. Reading from RAM is roughly 100,000x faster than reading from a spinning disk and roughly 1000x faster than an SSD. So Redis just doesn't pay the storage tax that databases pay.</p>
<p>Sub-millisecond response times for almost any operation. A typical Redis call from your application server takes well under a millisecond. You can call it many times per request and barely move your latency budget.</p>
<p>That's the foundation. Everything good about Redis builds on top of "RAM is fast."</p>
<hr />
<h1>Single-threaded, and why that's actually great</h1>
<p>Here's the thing that surprises people. Redis runs on a single thread for command execution. One CPU core handles all your commands, in the order they arrive.</p>
<p>This sounds like a downside. It's not. <em>It's the secret weapon.</em></p>
<p><strong>Why it's good:</strong></p>
<ol>
<li><p><strong>Every command is atomic by default.</strong> Because there's only one thread, no two commands can interleave. You don't need locks. You don't need transactions for most things. You just run a command and it either fully happened or didn't.</p>
</li>
<li><p><strong>No race conditions inside Redis itself.</strong> Multi-threaded systems spend huge amounts of complexity dealing with concurrent writes to the same data. Redis sidesteps the whole problem by not being concurrent.</p>
</li>
<li><p><strong>Predictable performance.</strong> No thread contention, no lock waits, no cache invalidation issues between cores. Just a tight loop processing commands one at a time.</p>
</li>
<li><p><strong>Simple mental model.</strong> When you write code that uses Redis, you don't need to think about what happens if two requests hit the same key at once. The answer is always "one of them goes first, the other goes second, the result is deterministic."</p>
</li>
</ol>
<p>The trade is throughput per server. Redis can't use 32 cores on a single instance. But it doesn't need to. A single Redis instance can do hundreds of thousands of operations per second on one core. For almost every workload, that's enough. If you outgrow it, you shard across multiple Redis instances.</p>
<p>Single-threaded isn't a limitation Redis works around. It's a deliberate design choice that simplifies everything.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/878c7253-2c4c-4964-b823-0306382c110c.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>What I've used Redis for</h1>
<h2>Caching</h2>
<p>The classic use. Database query is slow? Cache the result in Redis with a TTL (time to live). Next request hits Redis instead of the database.</p>
<pre><code class="language-plaintext">GET user:123:profile
→ if hit, return it
→ if miss, query DB, SET user:123:profile EX 300, return
</code></pre>
<p>Redis evicts the key automatically after 5 minutes. No cleanup code. Done.</p>
<h2>Rate limiting</h2>
<p>I want to limit a user to 100 requests per minute. Redis has <code>INCR</code> (increment) which is atomic. Combined with <code>EXPIRE</code>:</p>
<pre><code class="language-plaintext">key = "ratelimit:user:123"
count = INCR key
if count == 1: EXPIRE key 60
if count &gt; 100: reject
</code></pre>
<p>Three commands. The first time a user hits the endpoint, the counter starts and gets a 60-second TTL. Every subsequent request increments. After 60 seconds, the key disappears, the counter resets.</p>
<p>No race conditions. No "what if two requests hit at the exact same moment." Atomic INCR handles it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/26514fba-da73-4077-baa4-7f3004490540.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Distributed locking</h2>
<p>Multiple servers, all want to do the same job. Only one should do it at a time. Redis can be the gatekeeper:</p>
<pre><code class="language-plaintext">SET lock:job_xyz "server-A" NX EX 30
</code></pre>
<p><code>NX</code> = only set if the key doesn't already exist. <code>EX 30</code> = expire in 30 seconds (in case the server crashes before releasing the lock).</p>
<p>If the SET succeeds, you got the lock. If it fails, someone else has it. The 30-second expiry means a dead server doesn't hold the lock forever.</p>
<p>This pattern is called "Redlock" when done across multiple Redis instances for higher reliability, but the simple version above works for most use cases.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/80349f9b-1764-4649-be77-f78caf8cb558.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Tracking peak concurrency</h2>
<p>You want to know "how many users were online simultaneously at peak?" Redis sorted sets and counters make this trivial.</p>
<p>Use <code>INCR</code> when a user connects, <code>DECR</code> when they disconnect. Track the max value with another key:</p>
<pre><code class="language-plaintext">current = INCR concurrent_users
peak = GET peak_users
if current &gt; peak: SET peak_users current
</code></pre>
<p>The whole thing is atomic from your code's perspective. No "two users connected at the same time and we lost a count" bug.</p>
<h2>Queues</h2>
<p>Redis lists are LIFO/FIFO queues out of the box. <code>LPUSH</code> to add, <code>BRPOP</code> to read (blocking, so workers idle until something arrives).</p>
<pre><code class="language-plaintext">producer: LPUSH jobs "send_email:user_42"
worker:   BRPOP jobs 0   (blocks until something is in the queue)
</code></pre>
<p>This is the basis of many job queue systems (Celery, Sidekiq, BullMQ, etc.) before they grow into more complex setups. For simple workloads, a Redis list IS your queue.</p>
<h2>Deduplication</h2>
<p>Don't want to send the same notification twice? Use a Redis SET:</p>
<pre><code class="language-plaintext">SADD sent_notifications notification_id
→ returns 1 if newly added, 0 if already there
</code></pre>
<p>If the result is 1, send the notification. If it's 0, skip it. Atomic. Simple.</p>
<h2>Leaderboards</h2>
<p>Sorted sets (ZSET) are the perfect data structure for leaderboards. Every entry has a score. Redis keeps them sorted.</p>
<pre><code class="language-plaintext">ZADD leaderboard 9500 "player_42"
ZREVRANGE leaderboard 0 9 WITHSCORES   (top 10)
ZREVRANK leaderboard "player_42"        (their rank)
</code></pre>
<p>Top N, rank lookup, score updates, all O(log N). Building this with a SQL database is annoying. Redis makes it three commands.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/1e7ac5fa-4130-4b9c-b47b-a10576a992aa.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>The thing that ties it all together</h1>
<p>Notice the pattern across all those examples. Each one is:</p>
<ul>
<li><p>A specific data structure (string counter, set, list, sorted set)</p>
</li>
<li><p>An atomic operation on it</p>
</li>
<li><p>Often with a TTL so old data cleans itself up</p>
</li>
</ul>
<p>That's the Redis playbook. Pick the right data structure for the problem. Use built-in atomic operations. Let TTL handle cleanup.</p>
<p>You almost never write complex code on top of Redis. The complexity is already inside Redis. Your code is glue.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/1b75805c-fd53-4c84-bc4f-ae5a942226f6.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Persistence (the part people argue about)</h1>
<p>Redis is in-memory, but it can persist to disk. Two options:</p>
<ul>
<li><p><strong>RDB snapshots:</strong> periodic dump of the whole dataset to disk. Fast, but you can lose the last few minutes of data if Redis crashes.</p>
</li>
<li><p><strong>AOF (append-only file):</strong> every write command is logged to disk. Slower, but you lose almost nothing on crash.</p>
</li>
</ul>
<p>You can combine them. For most use cases (caching, rate limiting, locks) you don't really care if Redis loses data on crash, because the underlying truth is in your real database. Redis is a fast layer on top.</p>
<p>For use cases where Redis IS the source of truth (a session store, a leaderboard you don't want to rebuild), persistence matters more. I would still never use redis as a persistent store. It just gets messy.</p>
<h1>When NOT to use Redis</h1>
<p>Redis is great. It's not a database replacement.</p>
<ul>
<li><p><strong>Don't use it for primary data storage</strong> unless you understand the persistence tradeoffs. Memory is finite and disks are forever.</p>
</li>
<li><p><strong>Don't store huge values.</strong> Redis values can be megabytes, but you'll regret it. Keep keys and values small.</p>
</li>
<li><p><strong>Don't use it as a queue if you need delivery guarantees.</strong> Use a real message broker (RabbitMQ, Kafka, NATS) for that. Redis lists are great for fire-and-forget jobs but not for "this MUST get processed exactly once" workloads.</p>
</li>
</ul>
<h1>Summary</h1>
<ul>
<li><p>Redis is an in-memory key-value store with rich data structures.</p>
</li>
<li><p>It's fast because RAM is fast.</p>
</li>
<li><p>It's single-threaded, which is a feature: every command is atomic, simple to reason about, no race conditions inside Redis.</p>
</li>
<li><p>It's a swiss army knife: caching, rate limiting, locks, queues, leaderboards, deduplication, counters, all with the same primitives.</p>
</li>
<li><p>The pattern is always: pick a data structure, use built-in atomic ops, let TTL clean up.</p>
</li>
<li><p>Don't try to make it a database. Use it as a fast, ephemeral layer on top of one.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Decals in games are not talked about enough]]></title><description><![CDATA[What a decal is
A decal is a flat texture stuck onto an existing 3D surface. It doesn't add new geometry. It just paints something on top of what's already there.
Bullet hole on a concrete wall? Decal]]></description><link>https://tigerabrodi.blog/decals-in-games-are-not-talked-about-enough</link><guid isPermaLink="true">https://tigerabrodi.blog/decals-in-games-are-not-talked-about-enough</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 03 May 2026 17:48:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/959a7293-f2c7-4e8c-a646-76e5f51f4222.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What a decal is</h1>
<p>A decal is a flat texture stuck onto an existing 3D surface. It doesn't add new geometry. It just paints something on top of what's already there.</p>
<p>Bullet hole on a concrete wall? Decal. Tire skid mark on the road? Decal. Blood splatter on the floor? Decal. Graffiti on a building? Decal. Footprint in the snow? Decal.</p>
<p>The 3D world stays the same. The decal is a layer of paint on top.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/02420b55-7e1f-4822-bdee-b196546afdab.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Why this matters</h1>
<p>Without decals, every surface in a game would be locked to whatever the artist baked in. A bullet hole would have to be modeled into the wall geometry. Every wall, every variation, every surface you might shoot. Impossible.</p>
<p>Decals let the world react. The geometry stays static, but the surface tells a story. You shot here. A car drove here. Something bled here. Something exploded here.</p>
<p>Without decals, FPS games would feel dead. Walls wouldn't show you fired at them. Floors wouldn't show where the fight happened. The world wouldn't remember anything.</p>
<hr />
<h1>The two main approaches</h1>
<p>There are two common ways to draw decals. Both produce the same visual effect, but they work very differently.</p>
<p><strong>Approach 1: Mesh decals.</strong> Make a flat quad (4 vertices, 2 triangles), put it just barely above the surface, and texture it. Easy to understand. Problem: it floats. Walk close to it and you'll see Z-fighting (flickering as the GPU can't decide which surface is in front). Also, it doesn't conform to bumpy surfaces. A bullet hole on a curved barrel looks wrong because the quad is flat.</p>
<p><strong>Approach 2: Projected decals (also called deferred decals).</strong> Define a 3D box in world space. Anywhere this box overlaps with existing geometry, project the decal texture onto that geometry's surface. The decal wraps around bumps, corners, and curves. No Z-fighting. This is what modern games use.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/8547214e-b4bc-40d5-8aef-8de576061d5f.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>How projected decals work</h1>
<p>The idea is borrowed from how a movie projector throws an image onto a wall.</p>
<p>Imagine a 3D box. Inside that box, the GPU finds whatever surface is already there (the wall, the floor, whatever). For each pixel of that surface, the GPU samples the decal texture and blends it on top.</p>
<p>The trick: this happens in screen space during rendering. You're not modifying the original geometry. You're just adding a pass that says "for any pixel inside this box, multiply by this texture."</p>
<p>That's why they're called <strong>deferred decals</strong> when they happen during the deferred rendering pass. They piggyback on geometry the GPU already drew. Almost free in terms of performance.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/563db44d-09f6-47d7-9681-96d0d5d21f21.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>What decals can affect</h1>
<p>A decal isn't just color. Modern engines let decals affect multiple properties of the surface:</p>
<ul>
<li><p><strong>Albedo</strong> (the base color). The bullet hole is dark.</p>
</li>
<li><p><strong>Normal</strong> (the bumpiness). The bullet hole has a dent direction, so light catches it like it's recessed.</p>
</li>
<li><p><strong>Roughness</strong> (how shiny vs matte). Wet blood is glossy. Dry concrete is rough. A decal can change which.</p>
</li>
<li><p><strong>Metallic</strong> (whether it's metal). Less common but possible.</p>
</li>
<li><p><strong>Emissive</strong> (whether it glows). Useful for things like neon graffiti or sci-fi runes.</p>
</li>
</ul>
<p>So a single decal can simultaneously darken a wall, dent its lighting, and add wetness. All from one texture pass.</p>
<p>This is why decals look so good in modern engines. They're not just stickers. They're full material overlays.</p>
<hr />
<h1>Where decals shine</h1>
<p><strong>FPS games.</strong> Bullet holes, blood splatters, scorch marks. Every shot leaves evidence. The player feels the world reacting. CS, Call of Duty, every Counter-Strike, every Battlefield. All decal-heavy.</p>
<p><strong>Racing games.</strong> Tire marks on asphalt, mud splatters, wet trails. Forza and Gran Turismo lean on this hard.</p>
<p><strong>Horror games.</strong> Blood trails, claw marks, mysterious symbols on walls. Cheap to add, massively atmospheric.</p>
<p><strong>Open world.</strong> Cracked roads, weathering, posters, graffiti. Decals let artists add detail without bloating the geometry.</p>
<p><strong>Damage states.</strong> Bullet holes that build up over time on the same wall. The wall doesn't know it was shot. The decal system does.</p>
<hr />
<h1>Why decals are underappreciated</h1>
<p>Particles are flashy. Shaders are sexy. Decals just sit there, quietly making everything feel real. That's the whole point. You're not supposed to notice them.</p>
<p>But strip them out of any modern shooter and it would feel lifeless. The world wouldn't remember anything. Every fight would happen in a pristine museum.</p>
<p>Decals are the layer of memory the world has. Particles happen and disappear. Decals stay.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/ed25dbaf-6c90-4409-a378-dd5ba3e6b72d.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Common gotchas</h1>
<ul>
<li><p><strong>Limit overlap.</strong> Each decal is a render pass. Stack 50 bullet holes in the same spot and your frame rate drops. Most engines cap how many decals can affect a single pixel.</p>
</li>
<li><p><strong>They expire.</strong> Most games fade decals out after a while. Not just for performance. Stale blood splatters from 30 minutes ago break immersion.</p>
</li>
<li><p><strong>Surface direction matters.</strong> A bullet decal facing the wrong way looks like a sticker. The engine has to project it along the surface normal at the impact point.</p>
</li>
<li><p><strong>Skinned meshes are hard.</strong> A decal on a moving character is much harder than a decal on a static wall, because the surface deforms. Most games don't bother and apply decals only to static geometry.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[2D Rendering Concepts: A Reference]]></title><description><![CDATA[1. The Core Insight: 2D Is 3D With a Flat Camera
A modern 2D game is rendered by a 3D engine. Every sprite is a flat rectangle (a quad) in 3D space. Every tile is a quad. Every UI element is a quad. T]]></description><link>https://tigerabrodi.blog/2d-rendering-concepts-a-reference</link><guid isPermaLink="true">https://tigerabrodi.blog/2d-rendering-concepts-a-reference</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 03 May 2026 17:36:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/b6c56562-40ec-47fd-8ce2-9f8ae253c0b9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>1. The Core Insight: 2D Is 3D With a Flat Camera</h1>
<p>A modern 2D game is rendered by a 3D engine. Every sprite is a flat rectangle (a quad) in 3D space. Every tile is a quad. Every UI element is a quad. The camera looks at all those quads through an <strong>orthographic projection</strong>, which means distance doesn't change apparent size. A quad 10 units away looks the same size as one 100 units away.</p>
<p>That's the trick. The world is 3D, but the camera flattens depth out of the picture. The Z axis still exists for sorting purposes (which thing draws on top of which), but it doesn't affect how big things appear.</p>
<p>This is why engines like Three.js can power 2D games trivially. You set up an orthographic camera instead of a perspective camera, and the same engine that renders 3D scenes is now rendering "2D." No special 2D mode required. Switch the camera back to perspective and you've got a 2.5D game with depth perception.</p>
<p>This single architectural choice unlocks everything else. The entire stack can stay 3D underneath, while the game looks and behaves 2D on the surface.</p>
<hr />
<h1>2. Orthographic Projection</h1>
<p>There are two main camera projection types: perspective and orthographic.</p>
<p><strong>Perspective projection</strong> is how human eyes and real cameras work. Things farther away look smaller. Parallel lines converge at vanishing points.</p>
<p><strong>Orthographic projection</strong> doesn't do that. Things farther away look the same size as things up close. Parallel lines stay parallel forever. There's no vanishing point.</p>
<p>Orthographic is what makes a game look "2D." The world could be at varying depths in 3D space, but you'd never perceive the depth, because nothing changes size with distance.</p>
<hr />
<h1>3. Pixels-per-Unit (PPU)</h1>
<p>PPU is the conversion factor between <strong>world space</strong> (the units the game logic uses) and <strong>screen space</strong> (the pixels you actually see).</p>
<p>If your character sprite is 32 pixels tall and you decide "the character is 1 unit tall in the world," then 1 world unit = 32 pixels. PPU = 32.</p>
<p>This affects everything. A wall "5 units long" is 160 pixels. The camera's view of "10 units wide" is 320 pixels wide. Movement of "1 unit per second" is 32 pixels per second.</p>
<p>PPU is usually pinned to <strong>tile size</strong> in tile-based games. If your tiles are 16×16 pixels and your world coordinate system is "1 unit per tile," your PPU is 16. The choice of tile size determines the PPU, which determines how big everything appears, which determines how much world is visible at once.</p>
<p>Tile size is a stylistic choice as much as a technical one:</p>
<ul>
<li><p>8×8 tiles → Game Boy-era, almost no detail per tile</p>
</li>
<li><p>16×16 tiles → SNES-era, classic chunky pixel art</p>
</li>
<li><p>32×32 tiles → modern indie pixel art, more detail</p>
</li>
<li><p>64×64 tiles → approaching painterly territory</p>
</li>
</ul>
<p>Larger tiles = more detail, but fewer fit on screen. Smaller tiles = less detail, but more world visible.</p>
<hr />
<h1>4. Internal Resolution and Display Scaling</h1>
<p>The engine doesn't render directly to the player's screen. It renders to a <strong>fixed internal resolution</strong> first, then scales the result to whatever the actual display is.</p>
<p>Two resolutions in play:</p>
<ul>
<li><p><strong>Internal resolution:</strong> what the game <em>renders at</em>. Decided by the developer. Stays constant.</p>
</li>
<li><p><strong>Display resolution:</strong> what the <em>screen</em> is. Variable. Whatever the player has.</p>
</li>
</ul>
<p>Celeste renders at 320×180 internally. Always. On a 4K monitor it scales to 3840×2160 (12× scaling). On a 720p screen it scales to 1280×720 (4× scaling). Same internal render, different final scale.</p>
<p>This separation is crucial. The PPU stays fixed. The world's geometry stays fixed. The camera's view stays fixed. Only the final scaling step adapts to the screen.</p>
<p><strong>Integer scaling</strong> is the choice for pixel art games. Scale by whole multiples: 4×, 6×, 8×, 12×. Pixel art looks crisp because every game pixel becomes a clean block of screen pixels. The internal resolution is usually picked specifically to integer-scale into common displays cleanly. 320×180 hits 720p, 1080p, 1440p, and 4K perfectly.</p>
<p><strong>Non-integer scaling</strong> is for hi-res art (Hollow Knight style) where slight blur from interpolation is acceptable. The art is detailed enough that scaling artifacts are imperceptible.</p>
<hr />
<h1>5. Filtering Modes</h1>
<p>When the engine draws a sprite, it has to decide what to do when source pixels and screen pixels don't line up perfectly. The choice is called <strong>filtering mode</strong> and it's one of the highest-impact settings in 2D rendering.</p>
<p><strong>Nearest-neighbor filtering</strong> picks the single closest pixel from the source texture and uses that color. Hard edges, blocky, no blending. This is what pixel art needs. Without it, your chunky pixel sprites look soft and fuzzy.</p>
<p><strong>Bilinear filtering</strong> samples 4 surrounding source pixels and blends them based on the position. Smooth, soft, continuous. This is what hi-res illustrated art needs. Hard pixel edges become natural anti-aliased edges.</p>
<p>The choice is per-texture. Pixel art textures get nearest-neighbor. Hi-res textures get bilinear. Same engine, both kinds of art coexist by setting their filter mode independently.</p>
<p>Most engines default to bilinear because that's what most 3D textures want. Pixel art games have to explicitly set every texture to nearest-neighbor or the art renders blurry.</p>
<hr />
<h1>6. Mipmaps</h1>
<p>A <strong>mipmap</strong> is a pre-computed smaller version of a texture. The GPU stores the full-size texture plus half-size, quarter-size, eighth-size, etc.</p>
<p>When a 3D texture is rendered at varying sizes (closer or farther from the camera), the GPU picks the right mip level to avoid aliasing artifacts (shimmer, moiré patterns).</p>
<p>For pixel art games, mipmaps are usually <strong>disabled</strong>. The orthographic camera doesn't render anything at varying sizes, so there's no aliasing problem to solve. Worse, mipmaps would produce blurred small versions of pixel art, making sprites look muddy when they shouldn't change at all.</p>
<p>For hi-res 2D games with parallax depth, mipmaps stay <strong>enabled</strong>. Background objects rendered smaller than their source resolution would shimmer without mipmaps; with mipmaps, the GPU samples a pre-blurred version that's appropriate for the displayed size.</p>
<p>So: pixel art = mipmaps off. Hi-res with depth variation = mipmaps on.</p>
<hr />
<h1>7. Pixel Snapping</h1>
<p>Even with nearest-neighbor filtering, sprites positioned at sub-pixel coordinates can produce visual jitter. If a sprite's screen position changes from 342.0 to 342.1 to 342.2 frame by frame, nearest-neighbor sometimes snaps to 342, sometimes to 343. The character appears to jitter even when nothing is moving.</p>
<p>The fix: <strong>pixel snapping</strong>. Round all transforms to integer pixels every frame. Sub-pixel motion becomes stepped motion (a character moving 0.5 pixels per frame appears to teleport one pixel every other frame).</p>
<p>For pixel art, this stepped motion is desirable, it matches the chunky aesthetic. For hi-res art, it would look choppy, you want smooth sub-pixel motion.</p>
<p>So pixel snapping is on for pixel art, off for hi-res art. Same per-texture-style decision as filtering and mipmaps.</p>
<hr />
<h1>8. The Pixel Art Settings Stack</h1>
<p>For pixel art to look crisp, you need all six of these settings configured correctly:</p>
<ol>
<li><p>Internal resolution: small and fixed (e.g., 320×180)</p>
</li>
<li><p>PPU: matches your tile/asset size (e.g., 16 or 32)</p>
</li>
<li><p>Texture filter: nearest-neighbor</p>
</li>
<li><p>Mipmaps: disabled</p>
</li>
<li><p>Pixel snapping: on</p>
</li>
<li><p>Final scale to display: integer multiples only</p>
</li>
</ol>
<p>Get all six right, pixel art looks crisp on every screen at every zoom. Miss any one and the art looks mushy, blurred, or jittery.</p>
<p>For hi-res art (Hollow Knight style), the settings flip: bilinear filtering, mipmaps on, pixel snapping off, and the internal resolution is much higher. The architecture is the same; only the per-texture settings differ.</p>
<hr />
<h1>9. Drawing Order and Depth</h1>
<p>Every frame the engine has many things to draw. They overlap. The engine has to decide the order. Whatever's drawn last covers what was drawn before, so the ordering decides what the player sees.</p>
<p>Three main strategies for deciding draw order:</p>
<h2>Manual Layer Index</h2>
<p>Assign every object a number. Lower numbers draw first, higher numbers draw last. UI gets a high number (always on top). Background gets low (always behind). Simple, works for coarse categories.</p>
<h2>Y-Sort</h2>
<p>For top-down games, sort by Y position. Whoever is "lower" on screen (closer to the camera, semantically) draws on top. The character "in front of" a tree draws over the tree because their Y position is lower.</p>
<p>The Y position used for sorting is usually the object's <em>base</em>, the feet for a character, the trunk base for a tree. This handles overlap intuitively: walk in front of a bush and you cover it; walk behind and it covers you.</p>
<h2>Z Position</h2>
<p>In a 3D-engine 2D setup, every object has a Z coordinate. Use Z directly for sort order. Higher Z = closer to camera = drawn later. Same effect as manual layers, but native to 3D coordinates.</p>
<p>Most games combine these: coarse layer indexes for categories (background vs world vs UI), then Y-sort or Z within each layer for fine ordering.</p>
<h2>Painters Algorithm</h2>
<p>Painters algorithm just means "draw back-to-front." Like a real painter: first the sky, then the trees, then the person, then the foreground details. Each new draw covers (or partially covers, with transparency) what's already drawn.</p>
<p>This is the standard for 2D because it handles transparent sprites cleanly. The alternative (front-to-back with depth buffer) is faster for opaque pixels but doesn't handle transparency well. 2D games are heavy on transparency, so painters wins.</p>
<p>The cost is <strong>overdraw</strong>: pixels get drawn over multiple times. For 2D games, GPUs are fast enough that this doesn't matter.</p>
<hr />
<h1>10. Layered Rendering for Composition</h1>
<p>Most 2D games render in layers, each layer handling a different concern:</p>
<ul>
<li><p><strong>Far parallax:</strong> distant background, scrolls slowly</p>
</li>
<li><p><strong>Mid parallax:</strong> mid-distance scenery</p>
</li>
<li><p><strong>Near parallax:</strong> close background details</p>
</li>
<li><p><strong>Ground / playable layer:</strong> the world the character interacts with</p>
</li>
<li><p><strong>Characters and objects:</strong> entities, Y-sorted</p>
</li>
<li><p><strong>Foreground occluders:</strong> things drawn over the player (tree canopies, bridges)</p>
</li>
<li><p><strong>UI:</strong> always on top of everything</p>
</li>
</ul>
<p>Each layer is independent. Each can scroll at its own speed for parallax. Each can have its own filter, its own blend mode, its own collision data.</p>
<p>Composition happens at render time: the engine draws layers back-to-front, building up the final image. No single asset has to encode "grass with tree on top with character in front of it" because the layers handle composition implicitly.</p>
<p>This is the same pattern as layered tilemaps, applied at the rendering level. Separation of concerns scales: each layer is simple in isolation, and the visual richness comes from stacking them.</p>
<hr />
<h1>11. Performance Is About Draw Calls</h1>
<p>GPUs can draw millions of pixels per frame. That's not the bottleneck.</p>
<p>The bottleneck is <strong>draw calls</strong>: each separate "draw this thing" command sent from CPU to GPU has overhead. 1000 sprites = potentially 1000 draw calls = real performance cost.</p>
<p>The fix is <strong>batching</strong>: combining many separate draws into a single command. The GPU is told "here's one big mesh containing 1000 sprites' worth of vertices, here's one texture, draw it all in one go." One draw call. 1000 sprites rendered.</p>
<p>For batching to work, the sprites need to share:</p>
<ul>
<li><p>The same <strong>texture</strong> (same loaded image file)</p>
</li>
<li><p>The same <strong>shader</strong></p>
</li>
<li><p>The same <strong>blend mode</strong></p>
</li>
</ul>
<p>If any of these differ, the GPU has to break batching and start a new draw call.</p>
<p>This is why <strong>texture atlases matter for performance</strong>. Packing many unrelated sprites into one PNG means they all share the same texture, which means they can be batched. 100 sprites in one atlas: 1 draw call. 100 sprites in 100 separate PNGs: 100 draw calls.</p>
<p>A well-organized 2D game might have 5 to 20 draw calls per frame even with thousands of sprites visible. A poorly organized one might have 500+. The art looks identical; the framerate doesn't.</p>
<hr />
<h1>12. Frustum Culling</h1>
<p>Don't draw what you can't see. The engine checks each object against the camera's visible rectangle: is your bounding box inside? If not, skip you.</p>
<p>For tile-based worlds, this is trivial: convert the camera's view to tile coordinates, only draw tiles within those bounds. A 100×100 tile world shows maybe 30×20 tiles on screen at any time, so 9,400 of the 10,000 tiles are skipped per frame.</p>
<p>For free-floating sprites, the engine maintains spatial structures (quadtrees, grid lookups) to find visible objects fast. You usually don't write this code; engines provide it. You just need to make sure objects are registered with the engine's spatial system.</p>
<hr />
<h1>13. Blend Modes</h1>
<p>When a sprite's pixels get drawn, they have to combine with what's already on screen. The blend mode is the math rule for that combination.</p>
<p><strong>Normal (alpha blend):</strong> opaque pixels overwrite, transparent show through, semi-transparent mix proportionally. Default. Used for everything that's just "stacking layers."</p>
<p><strong>Additive:</strong> new pixel's color is <em>added</em> to existing color. Result is brighter. Used for fire, sparks, magic glows, lasers, lens flares. Real-world light adds, so additive blending makes glow effects look like actual light rather than colored stickers.</p>
<p><strong>Multiplicative:</strong> new pixel's color is <em>multiplied</em> by existing. Result is darker. Used for smoke, shadows, dust clouds, dimming effects. Real-world light filters work this way.</p>
<p>The blend mode and the texture have to be designed together. A fire texture authored for additive blending has bright shapes on dark/transparent backgrounds (the dark parts add nothing). The same texture used with normal blending would look like a black square with fire in it.</p>
<p>Particles especially need explicit blend modes: "fire" particles → additive, "smoke" particles → multiplicative or normal alpha, generic effects → normal.</p>
<hr />
<h1>14. Shaders</h1>
<p>A <strong>shader</strong> is a small program that runs on the GPU, once per pixel (or per vertex). The GPU has thousands of cores running it in parallel for different pixels. That's why per-pixel effects can run at 60fps over the whole screen, the work is parallelized at the hardware level.</p>
<p>The default 2D shader is dead simple: sample the texture at this UV coordinate, apply a tint, output the color.</p>
<p>Custom shaders enable stylized effects:</p>
<ul>
<li><p><strong>Outline shader:</strong> detect transparent pixels around opaque ones, draw an outline color. The 1-pixel outlines you see in many indie games.</p>
</li>
<li><p><strong>Color flash:</strong> lerp the texture color toward white based on a parameter. Use for hit feedback in combat.</p>
</li>
<li><p><strong>Dissolve:</strong> sample a noise pattern, hide pixels where the noise is below a threshold. Use for character death, scene transitions.</p>
</li>
<li><p><strong>Distortion:</strong> offset the UV by a noise pattern, making the sprite ripple. Use for water, heat, magical effects.</p>
</li>
<li><p><strong>Pixelation:</strong> quantize coordinates to a coarser grid before sampling. Use for stylization.</p>
</li>
</ul>
<p>Shaders are cheap and powerful because they run on already-existing pixel data. You're not creating new sprites or new geometry, you're just changing how the GPU outputs each pixel. A whole game's "feel" can shift dramatically with the right shaders applied.</p>
<p>This is also why GPU compute is faster than CPU compute for graphics work. Per-pixel parallelism. The CPU couldn't do millions of operations per frame; the GPU does it without breaking a sweat.</p>
<hr />
<h1>15. World Space vs Screen Space</h1>
<p>Two coordinate systems running simultaneously:</p>
<ul>
<li><p><strong>World space:</strong> the game's internal coordinates. The character is at (10, 5). The chest is at (20, 5). They're 10 units apart in the world. Measured in units, not pixels.</p>
</li>
<li><p><strong>Screen space:</strong> actual pixels. The character might be drawn at screen pixel (640, 360). The cursor is at (812, 425). Measured in pixels.</p>
</li>
</ul>
<p>PPU plus the camera's position together convert between them. World position (10, 5) at PPU 32 with camera centered, becomes screen pixel (640, 360) on a 1280×720 display.</p>
<p>UI elements typically live in screen space (the health bar is at screen position 10, 10, regardless of camera). World objects live in world space (the character is at world position 10, 5, regardless of where on screen they appear).</p>
<p>The engine mixes both correctly. You don't think about this much until something goes wrong (UI moves with the camera, world objects don't move at all). Then it's clear: something was put in the wrong coordinate system.</p>
<hr />
<h1>The Mental Model</h1>
<p>A 2D game is a <strong>collection of textured quads in 3D space</strong>, viewed through an <strong>orthographic camera</strong> that ignores depth for sizing. PPU converts world units to screen pixels. Internal resolution stays fixed; final scaling adapts to the display. Filtering, mipmaps, and pixel snapping are per-texture settings that determine whether art looks crisp or smooth. Drawing order is a system of layers, Y-sort, and Z, painted back-to-front. Performance comes from batching draws, which requires shared textures (atlases), shared shaders, and shared blend modes. Blend modes determine how pixels combine: normal stacks, additive brightens, multiplicative darkens. Shaders are tiny GPU programs that transform pixels at draw time, running in parallel across thousands of GPU cores.</p>
<p>The deeper truth: <strong>the rendering pipeline is fast and parallel by design, but only when you let it batch and stay out of its way.</strong> Most performance problems are organizational, not computational. Most visual quality problems are settings problems, not asset problems. The engine wants to do the right thing. Your job is to give it the right inputs.</p>
<p>The architectural elegance: by making 2D rendering "just 3D rendering with the camera changed," modern engines unify everything under one pipeline. Lighting, shaders, depth sorting, GPU compute, all available to 2D games for free. The line between "2D" and "3D" is a property of the camera, not the engine.</p>
]]></content:encoded></item><item><title><![CDATA[2D Asset Primitives: A Reference]]></title><description><![CDATA[1. Sprite
A 2D image with transparency, drawn in the world with a transform.
A sprite is the atomic unit of 2D games. The image data is just a PNG. What makes it a sprite is how it's used: positioned ]]></description><link>https://tigerabrodi.blog/2d-asset-primitives-a-reference</link><guid isPermaLink="true">https://tigerabrodi.blog/2d-asset-primitives-a-reference</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 03 May 2026 17:36:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/cf7c9bf9-8585-4279-b9a0-8dd700fcd266.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>1. Sprite</h1>
<p>A 2D image with transparency, drawn in the world with a transform.</p>
<p>A sprite is the atomic unit of 2D games. The image data is just a PNG. What makes it a sprite is <em>how it's used</em>: positioned somewhere in the world, optionally scaled, rotated, flipped, or tinted.</p>
<p>A sprite has:</p>
<ul>
<li><p>A texture (the image)</p>
</li>
<li><p>A position</p>
</li>
<li><p>A scale</p>
</li>
<li><p>A rotation</p>
</li>
<li><p>A flip flag (mirror horizontally or vertically)</p>
</li>
<li><p>A tint (a color multiplied over the pixels, useful for damage flashes)</p>
</li>
<li><p>A pivot (more on this below)</p>
</li>
</ul>
<p>When the engine "draws a sprite," it stamps the texture onto the screen with that transform applied.</p>
<p>Under the hood, a sprite is a flat rectangle (a quad) with the texture mapped onto it. In modern engines this rectangle lives in 3D space, and an orthographic camera renders it flat. You don't have to think about that day-to-day, but it explains why z-position, sprite sorting, and transparency artifacts behave the way they do.</p>
<p>A single sprite represents one frame, one pose, one moment. A tree, a coin, a bullet, a character standing still. To make something move, you need many sprites.</p>
<hr />
<h1>2. Spritesheet</h1>
<p>One image file containing multiple frames of a single animation, arranged in a grid.</p>
<p>If you want a character to walk, you need many sprites: one for each frame of the walk cycle. Rather than ship those as separate files, you pack them into one PNG laid out in a regular grid (often 4x4 = 16 frames, or 8x1 = 8 frames in a row).</p>
<p>The engine knows the frame size, the number of frames, the frame rate, and whether to loop. To play the animation it shows frame 1, then frame 2, then frame 3, on time, sampling a different sub-rectangle of the same texture each frame.</p>
<p>Why one file:</p>
<ul>
<li><p>Faster to load than many small files</p>
</li>
<li><p>One texture in GPU memory</p>
</li>
<li><p>One draw call per frame (the engine just shifts which sub-rect it samples)</p>
</li>
</ul>
<p>A spritesheet is <strong>raw material for an animation, not the animation itself</strong>. The animation is the combination of the spritesheet plus its metadata: frame size, frame count, FPS, loop behavior. The metadata can live in a JSON file alongside the PNG, embedded in the filename, or stamped directly into PNG <code>tEXt</code> chunks (yes, PNG files can carry text metadata invisibly).</p>
<p>Frame counts vary by style. Retro pixel art often uses 3 to 4 frames per cycle, which produces a "stepped" feel. Modern hand-drawn 2D often uses 8 to 12 frames for smoother motion. Cuphead uses 24 frames per second of full hand-drawn cel animation, which is essentially film animation. The number of frames is a stylistic choice, not a technical one.</p>
<hr />
<h1>3. Texture Atlas</h1>
<p>One image file containing many <em>unrelated</em> sprites packed together to save GPU memory and reduce draw calls.</p>
<p>Same file format as a spritesheet (a PNG with a grid of small images), but completely different purpose. An atlas is a junk drawer of sprites: the hero, a coin, a heart, a tree, a button. They have nothing to do with each other. They're packed together purely so the GPU has fewer textures to swap between.</p>
<table>
<thead>
<tr>
<th></th>
<th>Spritesheet</th>
<th>Atlas</th>
</tr>
</thead>
<tbody><tr>
<td>What's in it</td>
<td>Frames of one animation</td>
<td>Many unrelated sprites</td>
</tr>
<tr>
<td>Why</td>
<td>To play a sequence</td>
<td>To save memory and batch draws</td>
</tr>
<tr>
<td>How it's used</td>
<td>Cycle through frames in time</td>
<td>Look up sprite by name or coordinates</td>
</tr>
<tr>
<td>Layout</td>
<td>Usually a regular grid</td>
<td>Packed efficiently, sometimes irregular</td>
</tr>
</tbody></table>
<p>A spritesheet is a LEGO set: a deliberate collection designed to combine. An atlas is a junk drawer: arbitrary contents shoved together for convenience.</p>
<p>You can have both at once. A game might pack many static sprites and several spritesheets into one big atlas at build time for performance.</p>
<hr />
<h1>4. Pivot</h1>
<p>The point on a sprite that gets placed at the sprite's world position. Also the point of rotation and the point of scaling.</p>
<p>When you tell the engine "put this sprite at position (100, 200)," there's an implicit question: which point on the rectangle goes at (100, 200)? The center? The top-left? The bottom edge?</p>
<p>That point is the pivot. It determines:</p>
<ul>
<li><p><strong>Where the sprite sits.</strong> A grounded character with a center pivot ends up half-buried; the same sprite with a feet pivot stands correctly on the ground.</p>
</li>
<li><p><strong>What stays still during rotation.</strong> A sword pivoted at the handle swings like an actual sword; the same sword pivoted at the center spins like a propeller.</p>
</li>
<li><p><strong>What stays still during scaling.</strong> A character squashing on landing should keep their feet glued to the ground. Pivot at feet = squash compresses downward. Pivot at center = character lifts off the floor, which looks wrong.</p>
</li>
</ul>
<p>The pivot is a property of the sprite asset, usually stored as normalized coordinates (0 to 1 along each axis). <code>(0.5, 1.0)</code> is "horizontal center, vertical bottom" — the typical pivot for a grounded character.</p>
<p>The general rule: the pivot is wherever the sprite's logical position should anchor. Feet for grounded things. Top-center for hanging things. Center for projectiles and floating things. Where the hand grips it for a sword.</p>
<hr />
<h1>5. Anchor Points</h1>
<p>Named coordinates on a sprite where other things attach. Pivots, but more of them, with names.</p>
<p>A sprite has one pivot. But it can have many named "hotspots": the hand (where a sword attaches), the muzzle (where bullets spawn), the head (where a hat sits), the back (where a cape hangs).</p>
<p>These are anchor points. In 3D, the same concept is called a "socket." Same idea: a labeled coordinate that other game logic and other sprites can hook into.</p>
<p>Anchor metadata typically looks like:</p>
<pre><code class="language-plaintext">anchors = {
  hand: [0.7, 0.5],
  muzzle: [0.85, 0.45],
  head: [0.5, 0.1]
}
</code></pre>
<p>The complication: anchors often need to be <strong>per-frame</strong> rather than per-sprite. The hand isn't in the same pixel position across all 16 frames of an attack animation, the whole point of an attack animation is that the hand moves. So if a sword is "attached to the hand," the engine needs to know where the hand is on every frame.</p>
<p>The simple workaround is to bake attached items directly into the spritesheet (just draw the sword in the character's hand, no anchor needed). Cheaper, less flexible. You only need anchors when items can be swapped at runtime: weapon switching, customization, equipment systems.</p>
<hr />
<h1>6. Multi-Sprite Character</h1>
<p>A character assembled from multiple sprites layered via anchor points. Sometimes called a paper-doll character.</p>
<p>Instead of authoring "knight with iron sword and brown hat" as one sprite, you author the parts separately:</p>
<ul>
<li><p>Body (the base sprite)</p>
</li>
<li><p>Head (attached to a "neck" anchor on the body)</p>
</li>
<li><p>Hat (attached to a "head_top" anchor on the head)</p>
</li>
<li><p>Weapon (attached to the body's "hand" anchor)</p>
</li>
<li><p>Cape (attached to the body's "back" anchor)</p>
</li>
</ul>
<p>Each part is its own sprite (or its own spritesheet). They're combined at runtime via anchors.</p>
<p>The reason: <strong>combinatorial content for free</strong>. Five bodies times ten hats times eight weapons is 400 unique appearances from 23 sprites instead of 400 pre-rendered ones. This is how RPG character customization, roguelike enemy variety, and equipment-display systems work.</p>
<p>The cost is complexity: anchor metadata, layering rules, animation sync between parts, and per-frame anchor tracking if anchors move during animation. For a simple game where the character is always one fixed appearance, single-sprite is way easier. Multi-sprite is for when variety matters.</p>
<hr />
<h1>7. Tile</h1>
<p>A small image (typically 16x16, 32x32, or 64x64 pixels) designed to fit alongside other tiles in a grid to form a continuous picture.</p>
<p>Tiles are the LEGO bricks of 2D worlds. Each tile is just a small sprite, but designed so its edges line up with copies of itself or with other tiles. A grass tile. A dirt tile. A stone wall tile. A wooden floor tile.</p>
<p>Tiles solve the problem of building large 2D worlds without painting one giant image. A 100x100 tile world is just 10,000 numbers (the tile indices) instead of millions of pixels.</p>
<hr />
<h1>8. Tileset</h1>
<p>A deliberate collection of related tiles, packed in a grid PNG.</p>
<p>A tileset is a fixed asset. Each tile has an index (tile 0, tile 1, tile 2…). The world doesn't store images, it stores indices.</p>
<p>A tileset and an atlas can look identical if you open them in Photoshop. The difference is intent:</p>
<ul>
<li><p>An atlas packs <em>unrelated</em> sprites for memory efficiency.</p>
</li>
<li><p>A tileset packs <em>related</em> tiles deliberately designed to combine in a grid.</p>
</li>
</ul>
<p>Same file format, different purpose.</p>
<hr />
<h1>9. Tilemap</h1>
<p>A 2D array of tile indices that defines which tile goes where in the world.</p>
<pre><code class="language-plaintext">tilemap = [
  [0, 0, 0, 0, 0],
  [0, 1, 1, 1, 0],
  [0, 1, 2, 1, 0],
  [0, 1, 1, 1, 0],
  [0, 0, 0, 0, 0]
]
</code></pre>
<p>Read as: "fill with tile 0, with a 3x3 patch of tile 1 in the middle, and tile 2 dead center."</p>
<p>The world is data, not pixels. Trivially small. Trivially saved and loaded. Trivially modified at runtime ("change cell (3, 4) to dirt" is one assignment). Trivially generated procedurally.</p>
<p>The actual visible image you see when playing is generated at render time: the engine walks the array and draws the corresponding tile from the tileset at each grid position.</p>
<p>This separation between <strong>what the world is built from</strong> (tileset, art-heavy) and <strong>where each piece goes</strong> (tilemap, just data) is one of the biggest leverage points in 2D game design.</p>
<hr />
<h1>10. Layered Tilemaps</h1>
<p>Multiple tilemaps stacked at the same world coordinates, each handling a different concern.</p>
<p>A single layer of tiles is rarely enough. A real level needs:</p>
<ul>
<li><p>Ground (grass, dirt, water — the base)</p>
</li>
<li><p>Decoration (flowers, rocks, small details on top of ground)</p>
</li>
<li><p>Walls (obstacles, structures)</p>
</li>
<li><p>Overlay (tree canopies, awnings — drawn on top of characters)</p>
</li>
<li><p>Collision (invisible, marks which tiles are solid)</p>
</li>
</ul>
<p>Each is its own tilemap. The engine renders them back-to-front: ground, then decoration, then characters, then overlay. Composition happens at render time.</p>
<p>Layers also make collision data easy: a "collision layer" tilemap stores booleans (or simple types) saying "is this cell solid?" The engine queries it during physics. Visual data and collision data are separated because they're different concerns.</p>
<p>Most tile-based games use somewhere between 3 and 7 layers per level. Without layers, you'd need a tile for every possible combination ("grass with a flower," "grass with a rock," "grass with a flower and a rock"), which explodes combinatorially.</p>
<hr />
<h1>11. Autotile</h1>
<p>A system that automatically picks the right tile variant based on a cell's neighbors.</p>
<p>The problem: when grass meets dirt, the boundary needs to look smooth. You need tiles for "grass with dirt edge on top," "grass with dirt edge on left," "grass with dirt corner in the top-right," and so on. Painting these manually for every transition would be tedious and error-prone.</p>
<p>Autotiling solves it. The artist authors all the variants once. The system selects the right variant per cell at render time by looking at what the neighboring cells contain.</p>
<p>The designer's mental model becomes: "paint this region as dirt." The system handles every pixel-level detail of the boundary. Three main variants exist:</p>
<h2>Wang Tiles (corner-based)</h2>
<p>Each tile's <em>corners</em> are colored with one of two materials. With 4 corners and 2 possible colors per corner, there are 16 possible tiles. Enough to handle any boundary between two materials smoothly.</p>
<p>The engine's logic: for each cell, look at its 4 corners (each shared with an adjacent cell), determine which material is at each corner, and pick the matching tile from the 16-tile set. Corner-based selection guarantees seamless boundaries because corners are always shared between the cells that meet there.</p>
<h2>Blob Bitmask (8-neighbor)</h2>
<p>Each cell looks at all 8 of its neighbors (4 cardinal + 4 diagonal) and uses a bitmask to encode which are "the same material." 8 neighbors, 2 states each, gives 256 combinations, which collapse to <strong>47 visually distinct tiles</strong> (the famous "47-tile blob").</p>
<p>More tiles to author than Wang, but smoother and more organic transitions because edges and corners can curve more naturally.</p>
<h2>Dual-Grid (modern)</h2>
<p>The visual tiles are placed on a grid offset by half a tile from the world grid. Each visual tile straddles 4 world cells, so its 4 corners directly correspond to the 4 cells' material types. Same expressive power as 16-tile Wang, but smarter rendering means each tile gets reused more.</p>
<p>This is the technique many modern indie games are gravitating toward.</p>
<hr />
<h1>12. The 3+ Material Problem</h1>
<p>Wang tiles handle blending between two materials beautifully. They struggle when three or more materials need to meet at a single cell.</p>
<p>Real game worlds have grass meeting dirt meeting stone all at the same point. 2-corner Wang can't represent this directly because each corner is binary (material A or B). Three options exist:</p>
<ol>
<li><p><strong>Avoid 3-way meets in level design.</strong> Most games already do this by convention. Designers leave a buffer of one material between any third material. Players never notice. Pairwise Wang covers 95% of real game needs.</p>
</li>
<li><p><strong>Layering.</strong> Put one material as a base layer, others on upper layers with transparent edges. Where they overlap, you get the visual blend. Each layer is its own pairwise Wang set against transparency.</p>
</li>
<li><p><strong>Higher-corner Wang.</strong> 3-corner blending with 3 possible materials per corner gives 81 tiles per material set. Way more art to author. Used rarely.</p>
</li>
</ol>
<p>For most games, layering plus designer convention is the practical answer. Generating dedicated 3-corner blending sets is reserved for highest-quality painted-style games.</p>
<hr />
<h1>13. 9-Slice (also called 9-Patch)</h1>
<p>A way to render a single panel image at any size without distorting its borders.</p>
<p>Mostly relevant for engines where UI is rendered as textured quads inside the same renderer as the game world. (If you're using HTML/CSS for UI, this is what <code>border-image</code> is doing under the hood, and you don't need to think about it.)</p>
<p>The image is divided into 9 regions by two horizontal and two vertical slice lines:</p>
<pre><code class="language-plaintext">+---+--------+---+
| TL|  TOP   | TR|
+---+--------+---+
|LEFT|MIDDLE |RIGHT|
+---+--------+---+
| BL|  BOT   | BR|
+---+--------+---+
</code></pre>
<p>When rendered at a target size:</p>
<ul>
<li><p><strong>Corners</strong> stay at their original pixel size. They never stretch (preserving detail).</p>
</li>
<li><p><strong>Top and bottom edges</strong> stretch only horizontally.</p>
</li>
<li><p><strong>Left and right edges</strong> stretch only vertically.</p>
</li>
<li><p><strong>Middle</strong> stretches in both directions.</p>
</li>
</ul>
<p>The metadata is just four numbers: how many pixels in from each edge the slice lines are. Top: 16, right: 16, bottom: 16, left: 16. The engine computes the rest.</p>
<p>Result: one panel image renders correctly at any size. The corners stay crisp, the borders stay the right thickness, and only the (usually plain) middle stretches.</p>
<p>Common uses: dialogue boxes, inventory slots, buttons, health bars, window frames, speech bubbles, tooltips. Anything with a border that needs to flex with content.</p>
<hr />
<h1>14. Decals</h1>
<p>Sprites stamped onto the world as marks left behind by events.</p>
<p>A bullet hit a wall: bullet hole. A character walked through dirt: footprint. An explosion went off: scorch mark. A wound bled: blood splatter.</p>
<p>Decals are sprites (just PNGs), but used differently than entity sprites:</p>
<ul>
<li><p>They don't move</p>
</li>
<li><p>They don't have logic</p>
</li>
<li><p>They don't usually animate (some fade out over time)</p>
</li>
<li><p>They're often drawn between layers (above ground, below characters)</p>
</li>
<li><p>They're cheap to spawn in bulk</p>
</li>
</ul>
<p>The asset itself is just a sprite. The decal-ness is in the usage pattern, not the file format.</p>
<hr />
<h1>15. Particles</h1>
<p>Many tiny short-lived sprites driven by parameters, used together to produce effects.</p>
<p>Dust kicked up when landing. Sparks flying off a sword strike. Magic motes swirling around a wizard. Rain. Snow. Smoke. Fire. Explosions.</p>
<p>These all share a structure: many small things, each with its own velocity and lifetime, spawned together, fading out individually, but reading as one coherent effect.</p>
<p>A particle system is configured with parameters, not authored as art:</p>
<ul>
<li><p><strong>Spawn rate:</strong> particles per second</p>
</li>
<li><p><strong>Spawn shape:</strong> point, line, circle, arc</p>
</li>
<li><p><strong>Initial velocity:</strong> range of starting speeds and directions</p>
</li>
<li><p><strong>Lifetime:</strong> how long each particle lives</p>
</li>
<li><p><strong>Color over lifetime:</strong> start opaque, fade to transparent</p>
</li>
<li><p><strong>Scale over lifetime:</strong> start small, grow, shrink</p>
</li>
<li><p><strong>Texture:</strong> the sprite each particle uses</p>
</li>
<li><p><strong>Forces:</strong> gravity, wind, attraction</p>
</li>
</ul>
<p>The texture itself is usually tiny (8x8 to 32x32) and generic — a soft dot, a small star, a wisp shape. The fire particle and the dust particle might share the same texture; the difference is parameters (fire moves up, is orange-to-red, spawns densely; dust drifts, is gray, spawns once in a puff).</p>
<p>The art is minimal. The work is in the parameters. Tuning a particle system well is one of the highest-impact things you can do for game feel — almost every action in modern 2D action games triggers some particle effect, and the cumulative effect of all that subtle juice is what makes a game feel alive.</p>
<hr />
<h1>The mental model</h1>
<p>The asset primitives sort into a few rough categories:</p>
<p><strong>Atomic visual units:</strong> sprite, tile.</p>
<p><strong>Containers that hold many of those units:</strong> spritesheet (frames of one animation), texture atlas (unrelated sprites packed together), tileset (related tiles designed to combine).</p>
<p><strong>Coordinate systems on sprites:</strong> pivot (one logical anchor for position, rotation, scale), anchor points (named hotspots for attaching other things).</p>
<p><strong>Composition:</strong> multi-sprite character (assemble parts via anchors), tilemap (place tiles by index), layered tilemaps (stack tilemaps for ground, decoration, walls, collision).</p>
<p><strong>Smart selection:</strong> autotile (Wang, blob bitmask, dual-grid systems pick the right tile based on neighbors).</p>
<p><strong>Specialized usage patterns:</strong> 9-slice (stretchy UI panels), decals (stamped marks), particles (parameter-driven effects).</p>
<p>The deepest insight: <strong>2D worlds are built from tiny, repeatable, composable pieces.</strong> Tiles for the world. Sprites for the entities. Anchors for the connections. Tilemaps for the placement. Autotiling for the polish. Particles for the life. Each primitive solves a specific repetition or composition problem. Together they let you build worlds that are content-rich but data-light.</p>
<p>The engineering challenge isn't drawing any one of these things. The challenge is making thousands of them feel like they belong to the same game.</p>
]]></content:encoded></item><item><title><![CDATA[Skinning and Why Smooth Weights Matter]]></title><description><![CDATA[Introduction
You know what a 3D mesh is. You know what a skeleton is. There is a thing in the middle that connects them. That thing is skinning. And it makes or breaks how a character looks when it mo]]></description><link>https://tigerabrodi.blog/skinning-and-why-smooth-weights-matter</link><guid isPermaLink="true">https://tigerabrodi.blog/skinning-and-why-smooth-weights-matter</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 03 May 2026 17:23:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/aef45c03-5b2a-458b-bc9d-494484ced935.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>You know what a 3D mesh is. You know what a skeleton is. There is a thing in the middle that connects them. That thing is skinning. And it makes or breaks how a character looks when it moves.</p>
<h1>What Skinning Is</h1>
<p>A mesh is vertices in 3D space connected by triangles. A skeleton is a tree of bones. Without skinning these two are separate. Rotate the bones. The mesh sits there frozen.</p>
<p>Skinning is the link. It tells each vertex which bones to follow.</p>
<p>That is the whole job.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/8c84ac47-ef72-49e2-a73a-1a66b456108d.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Vertex Weights</h1>
<p>Skinning works through numbers called weights. Every vertex gets a weight for every bone that influences it. A value between 0 and 1.</p>
<p>Weight of 1. The bone fully controls that vertex. Weight of 0. The bone has zero influence. Anything between. Partial influence.</p>
<p>A vertex can be influenced by multiple bones at once. The weights from all bones on a vertex add up to 1.</p>
<p>Example. A vertex on the upper arm.</p>
<ul>
<li><p>Upper arm bone. 0.8</p>
</li>
<li><p>Shoulder bone. 0.2</p>
</li>
</ul>
<p>That vertex follows the upper arm mostly. The shoulder still pulls on it a little.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/84a993f0-cb4d-492f-acd8-ae5d432943f3.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Rigid vs Smooth Skinning</h1>
<p><strong>Rigid skinning.</strong> Each vertex bound to exactly one bone with weight 1. No blending. Good for robots. Bad for skin.</p>
<p><strong>Smooth skinning.</strong> Vertices near a joint get partial weights from bones on both sides. Influence blends across the seam. Good for anything organic.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/983e12e5-fb12-49ae-ba34-3c1e64bcfa7b.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>What This Looks Like At The Elbow</h1>
<p>Bend an elbow with rigid skinning. Upper arm vertices follow the upper arm bone. Forearm vertices follow the forearm bone. The two halves rotate independently. The mesh creases hard at the seam. Looks like folded paper. Sometimes the mesh pokes through itself.</p>
<p>Bend an elbow with smooth skinning. Vertices at the joint have weights split between both bones. Maybe 0.5 and 0.5. Move further from the joint and the weights shift toward whichever bone is closer.</p>
<p>When the elbow bends those middle vertices follow both bones partially. The result is a smooth curve. Skin appears to stretch and fold like real skin.</p>
<p>That is the entire point.</p>
]]></content:encoded></item><item><title><![CDATA[How spatial audio works in games]]></title><description><![CDATA[Introduction
A while ago I started wondering how games make sound feel like it's coming from a real place. Like in Counter-Strike when you can hear someone walking through a room two corridors over, o]]></description><link>https://tigerabrodi.blog/how-spatial-audio-works-in-games</link><guid isPermaLink="true">https://tigerabrodi.blog/how-spatial-audio-works-in-games</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Wed, 29 Apr 2026 17:56:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/72be7539-d299-48bd-bd6f-23ef8ed20efc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>A while ago I started wondering how games make sound feel like it's coming from a real place. Like in Counter-Strike when you can hear someone walking through a room two corridors over, or in Hellblade where voices feel like they're whispering right behind you. Turns out it's a whole layered system. Let me walk through it from the basics up.</p>
<h1>The most basic version: stereo panning</h1>
<p>The simplest "spatial audio" isn't really spatial at all. It just makes a sound louder in your left ear or your right ear depending on where the source is.</p>
<p>Sound is to the left of you → boost the left speaker, reduce the right. Brain interprets this as "sound is on my left."</p>
<p>That's it. Every game has done this since the 90s. It works, but it's crude. You can tell something is left or right, but not how far away, not whether it's behind you, not whether there's a wall between you and it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/b6d21b91-071b-4b64-a898-8e34e4064412.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Adding distance</h1>
<p>Sounds get quieter the further away they are. In games, you typically use a falloff curve:</p>
<pre><code class="language-plaintext">volume = 1 / distance²
</code></pre>
<p>Far sounds quiet, close sounds loud. Combined with stereo panning, you now have a basic 3D audio system. Sound to the left and far = quiet on the left speaker. Sound to the right and close = loud on the right speaker.</p>
<p>This is what most games shipped with for decades. It's "spatial" in a loose sense, but it's missing something huge.</p>
<hr />
<h1>The problem this misses</h1>
<p>Stand in a bathroom and clap. Stand in a forest and clap. Stand in a cathedral and clap.</p>
<p>The clap itself is identical in all three. But what your ears hear is completely different. Why?</p>
<p>Because sound <strong>bounces</strong>. The clap leaves your hands, hits the walls, hits the ceiling, hits the floor, hits the walls again. Each bounce arrives at your ears at slightly different times, with slightly different volumes, and slightly different frequencies (because surfaces absorb high frequencies more than low ones).</p>
<p>Your brain processes all those echoes without you thinking about it. That's how you can tell you're in a small tile room versus an open forest, just from a single clap. Your brain is doing a kind of acoustic ray tracing automatically.</p>
<p>Games that just do "louder on the left" miss all of this. The result is sound that feels detached from the space. You hear an enemy somewhere to your right, but the audio doesn't tell you whether they're in the same room or down a corridor.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/2b158899-a0e8-4ac8-8173-5d7cc7fb6ceb.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>What ray traced audio actually does</h1>
<p>The pitch: simulate sound the way real sound works. Treat sound waves like rays bouncing off walls.</p>
<p>For each sound source in the game:</p>
<ol>
<li><p>Shoot many rays out from the source in all directions.</p>
</li>
<li><p>Each ray bounces off surfaces it hits, losing some energy with each bounce.</p>
</li>
<li><p>Some rays eventually reach the listener (you).</p>
</li>
<li><p>Each ray that arrives carries info: how much energy it has left, how long it took to arrive, how its frequency was changed by the surfaces it bounced off.</p>
</li>
<li><p>Mix all those arriving rays together to produce the final sound.</p>
</li>
</ol>
<p>So if you're standing around a corner from a gunshot, no direct ray reaches you. But rays that bounced off the corridor walls do reach you, slightly delayed, slightly muffled. You hear the shot as a bouncy, indirect sound. That's the sound telling you "this is around the corner, not in front of you."</p>
<p>If you're in a cathedral, hundreds of rays bounce around the huge space and arrive at you over a long stretch of time. Long reverb tail. You can hear the size of the room.</p>
<p>If you're in a small tile bathroom, rays bounce quickly and aggressively because surfaces are hard and close. Short, sharp reverb.</p>
<p>Same source sound, wildly different results depending on the geometry around you. Just like real life.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/d65b07a5-7484-4fa3-8b3f-20a136c7988a.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Why "ray traced"</h1>
<p>Same reason graphics ray tracing has the name. You're literally tracing the path of a ray of energy through 3D space, hit by hit. Same algorithm as graphics ray tracing, just for sound waves instead of light.</p>
<p>The difference is that sound is way more forgiving than light. Sound diffracts (bends around corners) more than light does, and our ears are way less precise than our eyes. So you don't need millions of rays per source. A few hundred is often enough.</p>
<hr />
<h1>How your ears localize sound</h1>
<p>There's another piece I haven't mentioned yet. How does your brain know a sound is in front of you versus behind you, given that both ears hear it equally in both cases?</p>
<p>Your outer ear (the floppy bit, called the pinna) is shaped weirdly on purpose. It filters sound differently depending on which direction the sound came from. A sound from in front of you hits your ear ridges differently than a sound from behind. Your brain has learned, since you were a baby, to recognize these tiny frequency changes as "in front" versus "behind" versus "above" versus "below."</p>
<p>This is called the <strong>HRTF</strong>: head-related transfer function. It's basically a mathematical recording of how your specific head and ear shape filter sound from every direction.</p>
<p>Modern spatial audio runs incoming sound through an HRTF filter for each ray, simulating what the ray would sound like if it actually entered an ear shaped like a typical human ear. With headphones, this trick is shockingly effective. You can genuinely tell if a sound is above you, behind you, or in front of you, even though headphones only have two channels.</p>
<p>Without HRTF, ray traced audio still sounds cool, but you can't tell front from back. With HRTF layered on top, you get true 3D positioning.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/5265c912-7fe6-489c-857d-fd131c8b2aa8.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Putting it all together</h1>
<p>Modern spatial audio in games is layered:</p>
<ol>
<li><p><strong>Source sound</strong> (the gunshot, the footstep, the voice).</p>
</li>
<li><p><strong>Ray traced propagation</strong> (how does this sound travel through the geometry to reach the listener?).</p>
</li>
<li><p><strong>Per-ray frequency filtering</strong> (each surface absorbs different frequencies, so each bounce changes the sound's character).</p>
</li>
<li><p><strong>HRTF</strong> (final filter that makes the sound feel directional in 3D when heard through headphones).</p>
</li>
</ol>
<p>The game runs all four steps, in real time, for every sound source, every frame. Then mixes the results into what you hear.</p>
<hr />
<h1>The fanciness varies</h1>
<p>Not every game does the full ray traced version. There's a spectrum:</p>
<ul>
<li><p><strong>Simple occlusion checks.</strong> Shoot one ray from the source to the listener. If it hits a wall, muffle the sound. Fast, cheap, way better than nothing.</p>
</li>
<li><p><strong>Multi-bounce ray tracing.</strong> What I described above. Real reverb based on geometry.</p>
</li>
<li><p><strong>Baked acoustic response.</strong> Run the expensive simulation offline, save the result, look it up at runtime. Basically free at runtime, but doesn't react to dynamic geometry (closing a door doesn't change the sound).</p>
</li>
<li><p><strong>Full real-time simulation.</strong> What modern engines like Steam Audio and Microsoft's Project Acoustics aim for. Expensive but reacts to everything.</p>
</li>
</ul>
<p>Different games make different tradeoffs. Same underlying idea: sound should respect the geometry it travels through.</p>
<h1>Where you can hear this</h1>
<ul>
<li><p><strong>Counter-Strike 2</strong> uses ray traced audio for footsteps and gunfire. You can hear an enemy three rooms away through doorways with surprising accuracy.</p>
</li>
<li><p><strong>Hellblade: Senua's Sacrifice</strong> is famous for its binaural audio. Worth playing with headphones just for the audio.</p>
</li>
<li><p><strong>The Last of Us Part II</strong> uses occlusion and reverb extensively. Infected scuttling in the next room sounds like they're in the next room.</p>
</li>
<li><p><strong>Hitman</strong> lets you eavesdrop on conversations through walls because the audio system muffles distant voices realistically.</p>
</li>
</ul>
<hr />
<h1>Summary</h1>
<ul>
<li><p><strong>Stereo panning</strong>: louder in one ear or the other. Crude.</p>
</li>
<li><p><strong>Distance falloff</strong>: sounds get quieter further away. Combined with panning, gives basic 3D feel.</p>
</li>
<li><p><strong>Ray traced audio</strong>: shoot rays from each sound source. Let them bounce off geometry. Mix the rays that reach the listener. Now sound respects walls, corridors, room sizes, surface materials.</p>
</li>
<li><p><strong>HRTF</strong>: filter the result through a model of the human ear so your brain can tell front from back, up from down.</p>
</li>
<li><p><strong>Why it's powerful</strong>: same source sound produces wildly different results in different spaces, just like in real life.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[A visual guide to voxels]]></title><description><![CDATA[What's a voxel?
A voxel is to 3D what a pixel is to 2D.
A pixel is a tiny square in a 2D image. A grid of pixels makes a picture. Your screen is a grid of pixels. Zoom into any photo and you see them.]]></description><link>https://tigerabrodi.blog/a-visual-guide-to-voxels</link><guid isPermaLink="true">https://tigerabrodi.blog/a-visual-guide-to-voxels</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 26 Apr 2026 20:05:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/95b84fe3-aaf2-4b27-9ede-9ec81c979c7f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What's a voxel?</h1>
<p>A voxel is to 3D what a pixel is to 2D.</p>
<p>A <strong>pixel</strong> is a tiny square in a 2D image. A grid of pixels makes a picture. Your screen is a grid of pixels. Zoom into any photo and you see them.</p>
<p>A <strong>voxel</strong> is a tiny cube in a 3D space. A grid of voxels makes a 3D world. Minecraft is the famous example. Each block in Minecraft is essentially one voxel.</p>
<p>The word "voxel" comes from "volume element," same way "pixel" came from "picture element." That's the whole origin of the name.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/e0496141-f10e-44ef-bc4a-b7e3703716f7.png" alt="" style="display:block;margin:0 auto" />

<h1>What a voxel actually stores</h1>
<p>The cube shape is just for visualizing. A voxel is really a cell in a 3D grid that stores some data.</p>
<p>What data? Whatever you need:</p>
<ul>
<li><p>1 bit: solid or empty</p>
</li>
<li><p>1 byte: which material (stone, dirt, water, air)</p>
</li>
<li><p>More bytes: material plus light level plus temperature plus whatever</p>
</li>
</ul>
<p>In code, a voxel grid is just a 3D array:</p>
<pre><code class="language-js">voxels[x][y][z] = someValue
</code></pre>
<p>Every cell holds a value. The values together describe the world.</p>
<p>A useful way to think about it: voxels are not really cubes. They are points in a grid that happen to get drawn as cubes when you render them. <strong>The "cube" is a rendering choice, not a property of the voxel.</strong></p>
<h1>How big do voxel worlds get?</h1>
<p>Storage adds up fast.</p>
<p>A grid of 256 by 256 by 256 voxels has about 16 million cells. At 1 byte each, that's 16 MB. Manageable.</p>
<p>A grid of 1024 by 1024 by 1024 voxels has about 1 billion cells. At 1 byte each, that's 1 GB. Too much for most cases.</p>
<p>Two tricks games use to deal with this:</p>
<p><strong>Chunks.</strong> Split the world into smaller blocks of voxels. Minecraft uses 16 by 16 by 256 chunks. Only keep the chunks near the player in memory. Load them in as the player walks toward them, unload them as the player walks away.</p>
<p><strong>Sparse storage.</strong> Most voxels are usually empty (air). Don't store them at all. Use a tree structure called an <strong>octree</strong> that splits 3D space into 8 child cubes recursively, only storing the child cubes that contain something. Empty regions take almost no space.</p>
<p>These are engineering details, not voxel concepts. The core idea stays the same: a 3D grid of data.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/e29c17da-446a-4eea-b751-99c7108cd744.png" alt="" style="display:block;margin:0 auto" />

<h1>What voxels are good for</h1>
<p>Three main reasons people use voxels:</p>
<ol>
<li><p><strong>Destructible or modifiable worlds.</strong> Changing a voxel is easy. Flip a 1 to a 0 and there's a hole. With a normal triangle mesh, modifying geometry mid-game is much harder. Games like Teardown and Minecraft lean on this.</p>
</li>
<li><p><strong>Procedural generation.</strong> Filling a grid with numbers is something computers do well. You write a noise function, sample it at every grid point, and terrain appears. No artists needed.</p>
</li>
<li><p><strong>Medical and scientific data.</strong> MRI and CT scans naturally produce voxel grids. Each cell holds a density value from the scan. Same data structure, completely different field.</p>
</li>
</ol>
<hr />
<h1>What's hard about voxels?</h1>
<p>Two big challenges.</p>
<p><strong>Memory.</strong> As shown above, voxel grids are huge. Storing them is one problem. Sending them to the GPU each frame is another.</p>
<p><strong>Rendering.</strong> A GPU draws triangles. Voxels are not triangles. You have to convert your voxel grid into triangles before you can draw it. This conversion is called <strong>meshing</strong>, and it's where most of the complexity lives.</p>
<hr />
<h1>Three ways to render voxels</h1>
<p>There are three common approaches:</p>
<p><strong>1. Just draw cubes.</strong> Each solid voxel becomes a cube made of 12 triangles. Optimization: skip faces that are hidden inside the world. This is what Minecraft does. Output is intentionally blocky. Looks like Minecraft.</p>
<p><strong>2. Smooth the surface.</strong> Treat the voxel values as a continuous field. Find where the field crosses zero and draw a smooth surface there. This is what <strong>marching cubes</strong> does. Output is smooth, no visible cube faces. Used for natural terrain, caves, organic shapes.</p>
<p><strong>3. Ray march.</strong> Don't make triangles at all. For each pixel on screen, walk a ray through the voxel grid and find what it hits. Slower for big scenes but supports things triangles can't, like translucent fog or true volumetric effects.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/becc0cdc-89d0-42d7-81f6-1f203c986500.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>The mental shift</h1>
<p>A voxel is data. The cube is just one way to draw it.</p>
<p>Same voxel grid can become:</p>
<ul>
<li><p>Blocky Minecraft terrain</p>
</li>
<li><p>Smooth natural mountain</p>
</li>
<li><p>A semi transparent cloud</p>
</li>
<li><p>A medical scan visualization</p>
</li>
</ul>
<p>The voxels don't change. The rendering algorithm does.</p>
<p>If you remember nothing else: <strong>voxel = 3D pixel = a cell in a 3D grid storing some data</strong>.</p>
]]></content:encoded></item><item><title><![CDATA[Does my KTX2 texture need to be square?]]></title><description><![CDATA[Introduction
I hit an error like this:
KTX2 texture decoded to 512x286, which is incompatible with block size 4x4

I assumed the texture needed to be square (width = height). That was wrong.
Where the]]></description><link>https://tigerabrodi.blog/does-my-ktx2-texture-need-to-be-square</link><guid isPermaLink="true">https://tigerabrodi.blog/does-my-ktx2-texture-need-to-be-square</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 26 Apr 2026 17:46:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/16c94298-a82d-49b3-99a0-ab44c4e39a01.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>I hit an error like this:</p>
<pre><code class="language-plaintext">KTX2 texture decoded to 512x286, which is incompatible with block size 4x4
</code></pre>
<p>I assumed the texture needed to be <strong>square</strong> (width = height). That was wrong.</p>
<h1>Where the 4x4 comes from</h1>
<p>Block compression formats don't store individual pixels. They divide the image into <strong>4x4 pixel blocks</strong> and compress each block as a single unit.</p>
<p>For each 4x4 block, BC7 stores about 16 bytes that approximate all 16 pixels in that block. The GPU hardware knows how to decode any single pixel out of that block when sampling.</p>
<p>Because the unit of storage is a 4x4 block, the encoder physically cannot encode partial blocks. Every dimension of the input image must be <strong>a multiple of 4</strong>.</p>
<ul>
<li><p>512 x 512: fine. 128 blocks wide, 128 blocks tall.</p>
</li>
<li><p>1024 x 768: fine. Both divisible by 4.</p>
</li>
<li><p>512 x 286: broken. 286 / 4 = 71.5. You can't have half a block.</p>
</li>
</ul>
<p>286 isn't a multiple of 4. That's the actual problem.</p>
<h1>Why it's NOT about being square</h1>
<p>Square means width equals height. That's a completely separate thing from the block size constraint.</p>
<ul>
<li><p>512 x 512: square AND multiple of 4. Works.</p>
</li>
<li><p>1024 x 768: NOT square but multiple of 4. Works.</p>
</li>
<li><p>512 x 286: NOT square AND not multiple of 4. Fails.</p>
</li>
<li><p>511 x 511: SQUARE but not multiple of 4. Also fails.</p>
</li>
</ul>
<p>The constraint is "both dimensions divisible by 4." Aspect ratio doesn't matter at all.</p>
<p>This is where my confusion came from. Forcing all textures to 1:1 (square) is too aggressive. Banners, UI strips, sky backdrops, and tons of other legitimate textures are not square. The real fix is to round each dimension to the nearest multiple of 4 independently. 286 becomes 284 or 288. 512 stays 512.</p>
<h1>The actual fix</h1>
<p>Take the input dimensions, round each one to the nearest multiple of 4, resize the image, then run the BC encoder.</p>
<pre><code class="language-plaintext">512 x 286 → 512 x 284 (or 288)
</code></pre>
<p>A 2-pixel resize is invisible. Users won't notice. Pipeline doesn't crash. Done.</p>
]]></content:encoded></item><item><title><![CDATA[Indirect draws in WebGPU and why they're so powerful]]></title><description><![CDATA[Indirect draws in WebGPU
1. The two kinds of passes
Every frame, CPU records commands grouped into passes. Two kinds matter here.
Render pass = pixels come out. Runs the standard graphics pipeline:

V]]></description><link>https://tigerabrodi.blog/indirect-draws-in-webgpu-and-why-they-re-so-powerful</link><guid isPermaLink="true">https://tigerabrodi.blog/indirect-draws-in-webgpu-and-why-they-re-so-powerful</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 26 Apr 2026 16:41:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/a3ce6997-818c-43ad-86a5-85afbc7496f1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Indirect draws in WebGPU</h1>
<h1>1. The two kinds of passes</h1>
<p>Every frame, CPU records commands grouped into passes. Two kinds matter here.</p>
<p><strong>Render pass = pixels come out.</strong> Runs the standard graphics pipeline:</p>
<ul>
<li><p><strong>Vertex shader</strong>: runs once per vertex (corner of a triangle). Decides where each point ends up on screen.</p>
</li>
<li><p><strong>Fragment shader</strong>: runs once per pixel covered by a triangle. Decides what color that pixel is.</p>
</li>
</ul>
<p>Inside a render pass you issue draw calls. Output goes to a texture (your canvas, a shadow map, etc).</p>
<p><strong>Compute pass = numbers come out.</strong> Runs a compute shader. No vertices, no pixels, no triangles. Just custom logic running in parallel over a buffer. Output is whatever you write to storage buffers. Culling, physics, particle updates, GPU-side logic all live here.</p>
<p>Typical frame:</p>
<pre><code class="language-plaintext">commandEncoder
  → beginComputePass()    // prep (decide what to draw)
    dispatch(...)
    end()
  → beginRenderPass()     // actually draw it
    drawIndirect(...)
    end()
  → submit()
</code></pre>
<p>Compute pass writes into buffers. Render pass reads those same buffers. That's the handoff.</p>
<h1>2. How a normal draw works (at the hardware level)</h1>
<p>When CPU calls <code>renderPass.draw(1000, 5)</code>, here's what happens under the hood:</p>
<ol>
<li><p>The driver packs a command like <code>CMD_DRAW indexCount=1000 instanceCount=5</code> and queues it for the GPU.</p>
</li>
<li><p>The GPU has a unit at the front called the <strong>command processor</strong>. It reads the command stream one entry at a time.</p>
</li>
<li><p>Command processor sees CMD_DRAW, reads the numbers (1000, 5) baked into the command, kicks off the work: vertex fetch, vertex shader, rasterization, fragment shader, framebuffer write.</p>
</li>
</ol>
<p>Key point: <strong>those numbers (1000, 5) were baked in when CPU recorded the command.</strong></p>
<h1>3. What indirect changes</h1>
<p>Normal draw: CPU bakes the numbers (count, instance count) into the command.</p>
<p>Indirect draw: CPU points at a buffer instead. The command looks like this:</p>
<pre><code class="language-plaintext">CMD_DRAW_INDIRECT bufferAddress=0x... offset=0
</code></pre>
<p>No counts in the command itself.</p>
<p>When the command processor hits this, it does one extra step: read 16 or 20 bytes from that buffer. Those bytes ARE the draw arguments. Then it draws normally.</p>
<p>Same shaders, same rasterization, same output. Only difference: the numbers came from GPU memory.</p>
<p><strong>Why it matters:</strong> something else on the GPU can write those numbers. Usually a compute shader.</p>
<h1>4. The buffer layout</h1>
<p>When a compute shader fills in draw args, it has to write the right fields in the right order. So you need to know what's in that buffer.</p>
<p>Each draw's args take a fixed number of bytes:</p>
<ul>
<li><p><strong>drawIndirect</strong> (16 bytes): <code>vertexCount, instanceCount, firstVertex, firstInstance</code></p>
</li>
<li><p><strong>drawIndexedIndirect</strong> (20 bytes): <code>indexCount, instanceCount, firstIndex, baseVertex, firstInstance</code></p>
</li>
</ul>
<p>Pack as many as you want, one after another. The <code>offset</code> you pass to <code>drawIndirect</code> tells the GPU where to start reading.</p>
<pre><code class="language-plaintext">[draw0: 20 bytes][draw1: 20 bytes][draw2: 20 bytes]
 offset=0         offset=20         offset=40
</code></pre>
<p>Two buffer usage flags to remember:</p>
<ul>
<li><p><code>INDIRECT</code> (always required)</p>
</li>
<li><p><code>STORAGE</code> (add this if a compute shader writes to the buffer)</p>
</li>
</ul>
<p>Forget either flag and WebGPU will reject the call. You don't need to memorize the field layout, just know it exists when you sit down to write the compute shader.</p>
<h1>5. The barrier</h1>
<p>GPUs run work in parallel. If your compute pass writes to a buffer and your render pass reads from it, the GPU needs to know "finish the writes before the reads start." That's a <strong>barrier</strong>.</p>
<p>WebGPU inserts barriers automatically between passes. You don't write them manually. They just need to exist, and they do.</p>
<h1>6. A real frame, step by step</h1>
<p>The full chain in a GPU driven frame:</p>
<pre><code class="language-plaintext">CPU submits three commands:
  CMD_DISPATCH       → run the compute pass
  CMD_BARRIER        → wait for compute writes to land
  CMD_DRAW_INDIRECT  → run the render pass

GPU command processor does:
  1. Read CMD_DISPATCH.
     Spawn compute shader invocations.
     Compute shader decides which objects survive culling,
     writes their instance data into a buffer,
     writes instanceCount into the indirect args buffer.

  2. Read CMD_BARRIER.
     Wait for those writes to be visible to subsequent reads.

  3. Read CMD_DRAW_INDIRECT.
     Read 20 bytes from the indirect args buffer.
     See instanceCount=3000 (written by the compute shader).
     Spawn vertex shaders, rasterize, run fragment shaders.
</code></pre>
<p>CPU submitted 3 commands. Never touched individual objects. GPU decided how many to draw.</p>
<h1>7. Why this is a win</h1>
<h2>a) CPU per-object work disappears</h2>
<p>Normal renderer looks like this:</p>
<pre><code class="language-js">for (const obj of scene.objects) {       // CPU loops
  if (isInFrustum(obj)) {                 // CPU tests
    renderPass.setBindGroup(obj.bg);      // CPU sets state
    renderPass.draw(obj.indexCount);      // CPU issues draw
  }
}
</code></pre>
<p>100k objects = CPU loops 100k times. Even skipped objects cost a check. More objects = more CPU work.</p>
<p>GPU driven + indirect: loop is gone. CPU dispatches one compute pass and one indirect draw regardless of object count. 1k objects or 1M objects look identical from the CPU side.</p>
<h2>b) Driver overhead shrinks</h2>
<p>Every draw call has cost beyond drawing: validation, state setup, command translation, tracking. This cost is per-call, not per-triangle.</p>
<p>1000 draws of 10 triangles each is way more CPU-expensive than 1 draw of 10,000 triangles. Same GPU work, much more driver overhead.</p>
<p>Indirect + instancing collapses many draws into few. Less overhead per frame.</p>
<h2>c) No readback, no stall</h2>
<p>Traditional CPU culling sometimes needs data that lives on the GPU (animated bone positions, depth buffer for occlusion). CPU has to ask GPU for that data and wait for it. The wait is a <strong>stall</strong>. The transfer is a <strong>readback</strong>.</p>
<p>Readbacks are slow. Milliseconds sometimes. A 60fps frame budget is 16ms. One bad readback can wreck it.</p>
<p>With GPU driven rendering, CPU never asks. Data lives on the GPU. Compute shader uses it directly, writes results to another GPU buffer, render pass reads that buffer. CPU is never in the loop.</p>
<h1>8. The remaining problem: many different meshes</h1>
<p>A single draw call (indirect or not) draws <strong>one mesh</strong>. It uses the currently bound vertex buffer, index buffer, shader, and textures, all set by the CPU before the call.</p>
<p>Indirect lets the GPU decide <em>how many</em> of that mesh to draw (the <code>instanceCount</code>). It does NOT let the GPU decide <em>which</em> mesh. The mesh is locked in by whatever the CPU bound right before the draw.</p>
<p>So if you have 1000 trees and 5000 grass blades, that's 2 indirect draws. Fine. The GPU decides the per-frame counts.</p>
<p>But if you have 500 <em>different</em> meshes (tree, rock, grass, mushroom, stump, flower...), you need 500 indirect draws, because each one needs different bound geometry:</p>
<pre><code class="language-js">for (const meshType of allMeshTypes) {  // 500 iterations on CPU
  renderPass.setVertexBuffer(meshType.vbo);
  renderPass.setIndexBuffer(meshType.ibo);
  renderPass.setBindGroup(0, meshType.bg);
  renderPass.drawIndexedIndirect(argsBuffer, meshType.argsOffset);
}
</code></pre>
<p>The args are GPU authored, but CPU still walks a list of 500 to swap geometry between draws.</p>
<p>Better than 500 normal draws (you skipped the per-object loop). Still not ideal at extreme scale.</p>
<p>That's what multi-draw indirect fixes next.</p>
<h1>9. Multi-draw indirect</h1>
<p>Quick refresher on vertex buffers first.</p>
<p>A <strong>vertex buffer</strong> is just a chunk of GPU memory holding the points (vertices) that make up a mesh. A tree mesh has, say, 512 vertices. Those 512 points sit in a vertex buffer. When you draw the tree, the GPU reads from that buffer to know where the triangles are.</p>
<p>Each mesh normally has its own vertex buffer. To draw a different mesh, the CPU has to swap which buffer is bound. That swap is the slow part.</p>
<p>Now the trick.</p>
<pre><code class="language-plaintext">renderPass.multiDrawIndexedIndirect(argsBuffer, offset, countBuffer, countOffset, maxCount)
</code></pre>
<p><strong>Step 1: put every mesh into ONE big vertex buffer.</strong></p>
<p>Instead of 500 separate buffers (one per mesh), you concatenate all of them into one big buffer:</p>
<pre><code class="language-plaintext">big vertex buffer:
[tree verts][rock verts][grass verts][mushroom verts][...]
 0           512          640          704
</code></pre>
<p>The tree's vertices live at the start. The rock's vertices live right after. And so on. Every mesh is a slice of the same big buffer.</p>
<p><strong>Step 2: each draw points at its slice.</strong></p>
<p>Each draw arg has fields called <code>baseVertex</code> and <code>firstIndex</code>. They mean "start reading from this position in the big buffer." So:</p>
<pre><code class="language-plaintext">draw 0: baseVertex=0     → tree slice
draw 1: baseVertex=512   → rock slice
draw 2: baseVertex=640   → grass slice
</code></pre>
<p>Same buffer for all of them. Different starting positions. No swapping needed.</p>
<p><strong>Step 3: GPU writes the args.</strong></p>
<p>A compute shader figures out what's visible. For each visible mesh, it writes a draw arg pointing at the right slice. It also writes the total number of draws into a small buffer called the <strong>count buffer</strong>.</p>
<p><strong>Step 4: one CPU call does everything.</strong></p>
<pre><code class="language-plaintext">multiDrawIndexedIndirect(argsBuffer, offset, countBuffer, countOffset, maxCount)
</code></pre>
<ul>
<li><p><code>argsBuffer</code> = all the draw args the compute shader wrote</p>
</li>
<li><p><code>countBuffer</code> = how many of them are real (GPU decided)</p>
</li>
<li><p><code>maxCount</code> = upper safety cap</p>
</li>
</ul>
<p>GPU reads the count. Issues that many draws. Each one pulls geometry from its own slice of the big buffer. CPU never loops, never swaps state, never finds out how many draws happened.</p>
<p>Pair this with bindless textures (so different draws can use different textures without the CPU swapping those either) and you have a fully GPU driven renderer.</p>
<p>This is how UE5 Nanite and modern AAA engines work. Flat CPU cost, massive scenes.</p>
<h1>10. WebGPU status</h1>
<p><code>multi-draw-indirect</code> is an optional feature:</p>
<pre><code class="language-js">if (adapter.features.has('multi-draw-indirect')) {
  device = await adapter.requestDevice({
    requiredFeatures: ['multi-draw-indirect']
  });
}
</code></pre>
<p>Shipping in Chrome, solid in Dawn. Not universal on the web yet. For native targets (wgpu, Dawn native) it's production ready.</p>
<p><a href="https://developer.chrome.com/blog/new-in-webgpu-131">More info.</a></p>
<h1>11. Summary in order</h1>
<ol>
<li><p><strong>Two kinds of passes.</strong> Render pass = draws pixels. Compute pass = runs logic that writes into buffers.</p>
</li>
<li><p><strong>Normal draw.</strong> CPU picks the numbers (how many indices, how many instances) and bakes them into the command.</p>
</li>
<li><p><strong>Indirect draw.</strong> Same draw, but the numbers live in a GPU buffer. Command processor reads them right before drawing. CPU no longer picks the numbers.</p>
</li>
<li><p><strong>Barrier.</strong> Makes sure the compute pass finishes writing before the render pass starts reading. WebGPU adds it for you.</p>
</li>
<li><p><strong>Typical frame.</strong> Compute pass decides what's visible and writes the draw args. Render pass issues one indirect draw using those args. CPU never touches individual objects.</p>
</li>
<li><p><strong>The win.</strong> CPU cost stops growing with scene size. Fewer driver calls. No CPU/GPU sync stalls.</p>
</li>
<li><p><strong>The catch.</strong> One indirect draw = one mesh. 500 different meshes still means 500 CPU calls.</p>
</li>
<li><p><strong>Multi-draw indirect.</strong> Pack all meshes into one big vertex buffer. GPU writes args pointing at different slices. One CPU call issues thousands of draws, count and contents both GPU decided. The core trick behind modern engines like UE5 Nanite.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Trigonometry for game devs, starting from zero]]></title><description><![CDATA[Introduction
This guide assumes you know nothing. We're starting from the actual beginning. By the end, you'll understand what trig is, why it matters for games, and you'll know the handful of formula]]></description><link>https://tigerabrodi.blog/trigonometry-for-game-devs-starting-from-zero</link><guid isPermaLink="true">https://tigerabrodi.blog/trigonometry-for-game-devs-starting-from-zero</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Fri, 24 Apr 2026 15:38:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/b9914b51-2b99-43ea-ac37-dbcddec824b3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>This guide assumes you know nothing. We're starting from the actual beginning. By the end, you'll understand what trig is, why it matters for games, and you'll know the handful of formulas that do 90% of the work.</p>
<p>Take it slow. Read each section, sit with it, move on when it feels okay. It's fine if it takes a few passes. As a high school dropout myself, I sometimes struggle with basic concepts for much longer than others. Take it as inspiration.</p>
<h1>What does "trigonometry" even mean?</h1>
<p>The word comes from two Greek words: "trigon" (triangle) and "metry" (measuring). So "trigonometry" literally means "triangle measuring."</p>
<p>That's its origin. Ancient mathematicians studied triangles a lot because triangles show up everywhere: in buildings, in star positions, in distances across rivers you can't walk. They figured out rules for how the sides and angles of triangles relate to each other. Those rules became trigonometry.</p>
<p>People usually shorten it to <strong>trig</strong>. Same word. Less typing.</p>
<p>For game dev, we barely use triangles. We use trig for something else: dealing with <strong>angles</strong> and <strong>directions</strong>. But the math came from triangles, which is why the function names (sin, cos, tan) feel weird and unrelated to what we actually do with them. Just know that the names are historical baggage. They don't really describe what these functions do in code.</p>
<h1>What's an angle?</h1>
<p>An angle is how much something is turned.</p>
<p>Imagine you're standing up, facing forward. If you turn your whole body to face right, you've turned 90 degrees. If you keep going and face backward, you've turned 180 degrees. Keep going, face left, that's 270 degrees. Keep going, you're back to facing forward, 360 degrees. Full circle.</p>
<p>So:</p>
<ul>
<li><p>90 degrees = quarter turn</p>
</li>
<li><p>180 degrees = half turn</p>
</li>
<li><p>270 degrees = three quarter turn</p>
</li>
<li><p>360 degrees = full turn, back where you started</p>
</li>
</ul>
<p>That's degrees. You've probably seen the little symbol before: 90°.</p>
<p>Angles don't just describe how much you turned. They describe any direction you can point. "Straight up" is an angle. "Slightly to the right of straight up" is an angle. Every direction has an angle.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/60a97924-6d0a-4a9d-8783-03bdddd7d85f.png" alt="" style="display:block;margin:0 auto" />

<h1>What's a direction?</h1>
<p>A direction is where something is pointing or going.</p>
<p>In 2D (flat, like on a piece of paper), a direction is a pair of numbers. For example:</p>
<ul>
<li><p>(1, 0) means "1 step right, 0 steps up." So it's pointing right.</p>
</li>
<li><p>(0, 1) means "0 steps right, 1 step up." Pointing up.</p>
</li>
<li><p>(-1, 0) means "-1 step right" which means 1 step LEFT. So pointing left.</p>
</li>
<li><p>(0, -1) means "1 step down." Pointing down.</p>
</li>
</ul>
<p>That pair of numbers is called a <strong>vector</strong>. Don't let the word scare you. A vector is just two numbers that describe "how much horizontal, how much vertical."</p>
<p>Vectors can point anywhere:</p>
<ul>
<li><p>(1, 1) points up and to the right at a diagonal.</p>
</li>
<li><p>(3, 4) also points up and to the right, but much further.</p>
</li>
</ul>
<h1>Angles vs directions: the same thing, two ways</h1>
<p>Here's a question. Say something is pointing up and to the right at exactly 45 degrees. How do you describe that?</p>
<p><strong>As an angle</strong>, you'd say: "It's at 45 degrees."</p>
<p><strong>As a direction vector</strong>, you'd say: "It's pointing (0.707, 0.707)." (Those weird numbers are just what happens when you split "diagonal" into horizontal and vertical parts.)</p>
<p>Both descriptions mean the exact same thing. You can describe any direction in two ways:</p>
<ol>
<li><p><strong>As a single angle.</strong> One number. Simple to store, easy to say "turn this 10 more degrees."</p>
</li>
<li><p><strong>As a direction vector.</strong> Two numbers (x, y). Easier to use when you want to actually move something.</p>
</li>
</ol>
<p>Games constantly need to switch between these two ways of describing things. An enemy has a rotation angle (for drawing it facing a way), but when it fires a bullet, the bullet needs a direction vector (to move in). Different jobs, different representations.</p>
<p><strong>Trig is the bridge between these two representations.</strong> That's honestly what trig is for, in game dev. Angle to vector. Vector to angle. Back and forth.</p>
<p>Now, the rest of this guide is just learning the specific tools for that bridge.</p>
<h1>The one picture that makes it all work: the unit circle</h1>
<p>Draw a circle. Make its center at the spot (0, 0), which we call the <strong>origin</strong>. Make its size such that it reaches exactly 1 step in every direction. This is called the "unit circle" because "unit" means "one."</p>
<p>Every point on the edge of this circle can be described by an angle. Start from the rightmost point of the circle (that's angle 0). Walk counter clockwise around the circle. As you walk, the angle grows. Quarter way up, you're at 90 degrees. Top of the circle, wait no, quarter way up is the top (because we start from the right and go counter clockwise). Let me restart that.</p>
<p>Start at the rightmost point. Angle is 0.</p>
<ul>
<li><p>Walk counter clockwise to the top: angle is 90°.</p>
</li>
<li><p>Keep walking to the leftmost point: angle is 180°.</p>
</li>
<li><p>Keep walking to the bottom: angle is 270°.</p>
</li>
<li><p>Keep walking back to the start: angle is 360° (same as 0°).</p>
</li>
</ul>
<p>Every point on the circle has an angle. And every point on the circle also has a position: an (x, y) pair.</p>
<p>Here's the huge insight, the one thing that matters in all of trig for games:</p>
<p><strong>There's a simple formula that converts any angle into its matching (x, y) position on the unit circle.</strong></p>
<p>That formula uses two functions called <strong>cos</strong> and <strong>sin</strong>. They look like this:</p>
<pre><code class="language-plaintext">x = cos(angle)
y = sin(angle)
</code></pre>
<p>So if you have any angle, those two functions hand you back the x and y position on the unit circle for that angle. That's it. That's their whole job in game dev.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/7a375f3b-e6b4-4e73-b3f5-e8a6483ce4ea.png" alt="" style="display:block;margin:0 auto" />

<p>You don't have to understand WHY cos and sin work. You don't need to derive them. You just need to know what they do:</p>
<ul>
<li><p>Give <code>cos</code> an angle, it gives you back the x coordinate on the unit circle.</p>
</li>
<li><p>Give <code>sin</code> an angle, it gives you back the y coordinate on the unit circle.</p>
</li>
</ul>
<hr />
<p>Don't get confused by <em>θ. It's theta. Just greek letter for angle.</em></p>
<h1>Quick side note: radians vs degrees</h1>
<p>Here's an annoying thing. When you use cos and sin in actual code, they usually don't want degrees. They want a different unit called <strong>radians</strong>.</p>
<p>Radians are just another way to measure angles. Like how you can measure distance in meters or feet. Same thing, different numbers.</p>
<ul>
<li><p>Full turn = 360 degrees = <strong>2π radians</strong></p>
</li>
<li><p>Half turn = 180 degrees = <strong>π radians</strong></p>
</li>
<li><p>Quarter turn = 90 degrees = <strong>π/2 radians</strong></p>
</li>
</ul>
<p>π (pi) is a famous number that equals roughly 3.14. So a full turn in radians is about 6.28. A half turn is about 3.14. Weird, but that's the system code uses.</p>
<p>Why radians instead of degrees? Because the math libraries were built that way for good reasons we don't need to care about. Just accept it.</p>
<p>If you prefer thinking in degrees (most people do at first), your game engine has a helper to convert:</p>
<ul>
<li><p>Unity: <code>Mathf.Deg2Rad</code> multiplies degrees to turn them into radians.</p>
</li>
<li><p>Godot: <code>deg_to_rad(90)</code> returns about 1.57.</p>
</li>
</ul>
<p>Or do it manually:</p>
<pre><code class="language-plaintext">radians = degrees * π / 180
</code></pre>
<p>For the rest of this guide I'll sometimes write angles as degrees (like 90°) for readability, but remember that when you actually type code, you'll need to convert them to radians first.</p>
<h1>Using cos and sin to move things in a direction</h1>
<p>Finally, the game dev part.</p>
<p>Say your player is facing at 30 degrees (30° from pointing right, rotating counter clockwise). They press the shoot button. You want a bullet to fly out in the direction they're facing.</p>
<p>Here's the code:</p>
<pre><code class="language-plaintext">angle = 30 degrees (converted to radians)
direction_x = cos(angle)
direction_y = sin(angle)
</code></pre>
<p>Now you have a direction vector. It's pointing exactly where the player is facing. To actually move the bullet, you multiply the direction by some speed and add it to the bullet's position every frame:</p>
<pre><code class="language-plaintext">bullet.x = bullet.x + direction_x * speed
bullet.y = bullet.y + direction_y * speed
</code></pre>
<p>That's it. That's a bullet flying in any direction you want. You just used trig.</p>
<p><strong>This is the pattern you'll use all the time.</strong> "I have an angle, I need a direction." Answer: (cos, sin).</p>
<h1>Making something orbit around a point</h1>
<p>Want a moon to orbit a planet?</p>
<p>The moon's position needs to be "at distance r from the planet, at some angle that keeps changing." Here's how:</p>
<pre><code class="language-plaintext">angle = 0  // start angle
every frame:
    angle = angle + 0.01  // slowly increase angle each frame
    moon.x = planet.x + cos(angle) * r
    moon.y = planet.y + sin(angle) * r
</code></pre>
<p>What's happening: (cos, sin) gives you a point on the unit circle (a circle of size 1). Multiplying by r scales the circle up to size r. Adding the planet's position shifts the circle so it's centered on the planet instead of (0, 0). As the angle keeps growing, the moon walks around the circle.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/7c1a8382-2786-4ef7-917e-ae2c5ab5bbc2.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[Signed Distance Functions (SDFs) and why they are cool]]></title><description><![CDATA[First, what problem are we even solving?
Before we talk about SDFs, let's talk about how 3D graphics normally work.
Take a round ball in any video game. Looks smooth, right? It's not. If you zoom in c]]></description><link>https://tigerabrodi.blog/signed-distance-functions-sdfs-and-why-they-are-cool</link><guid isPermaLink="true">https://tigerabrodi.blog/signed-distance-functions-sdfs-and-why-they-are-cool</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Thu, 23 Apr 2026 14:27:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/b7f80deb-90e9-4146-bb83-e97ea7daf7ee.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>First, what problem are we even solving?</h1>
<p>Before we talk about SDFs, let's talk about how 3D graphics normally work.</p>
<p>Take a round ball in any video game. Looks smooth, right? It's not. If you zoom in close enough, you'll find out that ball is actually hundreds of flat triangles arranged in a ball shape. That's how almost every 3D object in games works. Everything is triangles.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/c7542ba1-88fb-4a34-8f89-a25d99c580fc.png" alt="" style="display:block;margin:0 auto" />

<p>Why triangles? Because graphics cards (GPUs) are really, really fast at drawing triangles. They can draw billions per second. So game engines built everything around triangles.</p>
<p>But triangles have some big problems:</p>
<ol>
<li><p><strong>Up close, you see the edges.</strong> A "smooth" ball is never actually smooth. It's always made of flat faces.</p>
</li>
<li><p><strong>Combining shapes is a nightmare.</strong> Want to merge two spheres into one blob? You'd have to manually delete and rebuild triangles. Engines have whole complicated systems just for this.</p>
</li>
<li><p><strong>Each shape uses memory.</strong> Every triangle stores coordinates. Detailed models can be millions of triangles.</p>
</li>
<li><p><strong>You can't do infinity.</strong> Want a cube repeated endlessly in every direction? You'd need endless memory. Not possible.</p>
</li>
</ol>
<p>SDFs solve every one of these problems. But to understand how, we have to think about what a shape IS in a completely different way.</p>
<h1>The big idea: a shape is a question</h1>
<p>Forget triangles for a minute. I want you to think of a shape as something that answers a question.</p>
<p>Imagine there's a ball floating in space. You stand somewhere in the room and you ask the ball: "how far am I from your surface?"</p>
<p>The ball gives you a number:</p>
<ul>
<li><p>If you're standing outside the ball, you get a positive number (like +5)</p>
</li>
<li><p>If you're standing inside the ball, you get a negative number (like -2)</p>
</li>
<li><p>If you're touching the ball's surface exactly, you get zero</p>
</li>
</ul>
<p>That's it. That's what an SDF is. Just that. A function that takes a point and gives you a distance.</p>
<p>Let me break down the name:</p>
<ul>
<li><p><strong>Signed</strong> means the number can have a plus or minus sign. That's how it tells you "inside" vs "outside".</p>
</li>
<li><p><strong>Distance</strong> just means how far.</p>
</li>
<li><p><strong>Function</strong> is the math word for "a rule that takes inputs and gives outputs."</p>
</li>
</ul>
<p>So a Signed Distance Function is just: a rule. Point goes in, distance comes out.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/0b87b956-6ade-4f7d-a171-7ca34dcf15bd.png" alt="" style="display:block;margin:0 auto" />

<h1>Wait, so where is the shape stored?</h1>
<p>This is the part that takes a minute to click. It's also the coolest part.</p>
<p><strong>The shape isn't stored anywhere.</strong> There are no triangles. There's no data saying "here are the points of the ball." There's only the rule.</p>
<p>The shape exists as "all the points in space where the rule gives back zero." That's the definition.</p>
<p>If this feels weird, that's normal. You're used to thinking of shapes as solid things you can touch or see. SDFs treat shapes as pure instructions. You never actually "build" the ball. You just write a rule that happens to describe a ball.</p>
<h1>A real example so it feels concrete</h1>
<p>The simplest SDF is a sphere sitting at the center of space. Here's the rule:</p>
<pre><code class="language-plaintext">distance = length(point) - radius
</code></pre>
<p>Let me explain every word.</p>
<ul>
<li><p><code>point</code> is wherever you're asking about. It can be any spot in 3D space.</p>
</li>
<li><p><code>length(point)</code> means "how far is this point from the center of space (the origin)?" It's just the straight line distance. Pythagoras stuff. If the point is 5 steps from the center, length is 5.</p>
</li>
<li><p><code>radius</code> is how big the sphere is. If the sphere has radius 3, it reaches 3 units in every direction from the center.</p>
</li>
<li><p><code>distance</code> is what the rule gives back.</p>
</li>
</ul>
<p>Let's walk through three specific cases to make this click:</p>
<p><strong>Case 1:</strong> The point is 5 units away from the center of space. The sphere's radius is 3.</p>
<pre><code class="language-plaintext">length(point) = 5
distance = 5 - 3 = +2
</code></pre>
<p>Answer: you're 2 units OUTSIDE the sphere.</p>
<p><strong>Case 2:</strong> The point is 1 unit away from the center. Same sphere.</p>
<pre><code class="language-plaintext">length(point) = 1
distance = 1 - 3 = -2
</code></pre>
<p>Answer: you're 2 units INSIDE the sphere.</p>
<p><strong>Case 3:</strong> The point is exactly 3 units from the center.</p>
<pre><code class="language-plaintext">distance = 3 - 3 = 0
</code></pre>
<p>Answer: you're right ON the surface.</p>
<p>That's a full sphere. One line of math. No triangles. No stored data. Just a rule that can answer any question about that sphere.</p>
<h1>Why nothing is stored, and how pixels get their color</h1>
<p>OK so remember: the sphere is just a rule. <code>distance = length(point) - radius</code>. A formula. Three words of math.</p>
<p>The computer stores the formula. It does NOT store the sphere.</p>
<p><strong>The sphere gets "built" fresh every single frame, one pixel at a time.</strong></p>
<p>Here's how it actually works. Your screen has pixels. Let's say 1920 across, 1080 down. That's about 2 million pixels. For each frame of the game, every single pixel needs a color.</p>
<p>The computer goes pixel by pixel. For each pixel, it does this:</p>
<ol>
<li><p>Imagine a straight line (a "ray") going from your eye, through the pixel on the screen, out into the 3D world behind it.</p>
</li>
<li><p>Walk along that ray, one step at a time, asking the rule: "how far to the nearest surface from here?"</p>
</li>
<li><p>The rule answers with a distance. Say it says "5 units." That means the ray can safely jump 5 units forward without hitting anything. So we jump 5 units.</p>
</li>
<li><p>Ask again. "2 units." Jump 2.</p>
</li>
<li><p>Ask again. "0.3 units." Jump 0.3.</p>
</li>
<li><p>Ask again. "Basically zero." BOOM. The ray has hit a surface. This pixel is touching the sphere.</p>
</li>
<li><p>Paint this pixel the sphere's color.</p>
</li>
</ol>
<p>Then the computer moves to the NEXT pixel and does the whole process again. And again. 2 million times. Every frame.</p>
<p>That's the whole game. A flipbook of "ask the rule, walk, ask again, walk, hit something, color a pixel."</p>
<p><strong>Why is nothing stored?</strong> Because it doesn't need to be. The formula can answer any question about the sphere on demand. Storing the sphere somewhere would just be a copy of what the rule already tells us. Wasted memory.</p>
<p>It's like if I asked you "what's 7 times 8?" You don't need to have the answer written down somewhere. You know the rule for multiplication. You can compute the answer whenever I ask. The answer exists in the moment you need it, then it's gone.</p>
<p><strong>The sphere is 7 times 8.</strong> It's not a stored number. It's a computation that happens when asked. The result appears on your screen, and the moment the frame is done, the sphere is gone until next frame, when we rebuild it from scratch, one pixel at a time, at 60 times per second.</p>
]]></content:encoded></item><item><title><![CDATA[How Computers Draw 3D Worlds]]></title><description><![CDATA[Step 0. The Problem
A 3D scene inside a computer is just math. Points in space. Lines between them. Colors. Materials.
But your screen is flat. It is a grid of tiny squares called pixels. Each pixel c]]></description><link>https://tigerabrodi.blog/how-computers-draw-3d-worlds</link><guid isPermaLink="true">https://tigerabrodi.blog/how-computers-draw-3d-worlds</guid><dc:creator><![CDATA[Tiger Abrodi]]></dc:creator><pubDate>Sun, 19 Apr 2026 22:00:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/3c0a72df-3ccb-467b-9f0f-66d3a46d91f5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Step 0. The Problem</h1>
<p>A 3D scene inside a computer is just math. Points in space. Lines between them. Colors. Materials.</p>
<p>But your screen is flat. It is a grid of tiny squares called pixels. Each pixel can only show one color at a time.</p>
<p>So the big question is this: <strong>how do we turn a 3D scene (math) into a 2D image (pixels)?</strong></p>
<p>That process is called <strong>rendering</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/9c3e1e4a-eec7-402a-a41a-672fd3bb99a9.png" alt="" style="display:block;margin:0 auto" />

<p>There are different ways to solve this problem. Each way is a trade between <strong>speed</strong> and <strong>realism</strong>.</p>
<hr />
<h1>1. Rasterization</h1>
<p>This is the oldest and fastest method. Almost every video game you have ever played uses it.</p>
<h2>The core idea</h2>
<p>Take every 3D triangle in the scene. Figure out which pixels on the screen it covers. Color those pixels.</p>
<p>That is it. That is the whole trick.</p>
<p>3D models are built from triangles (thousands or millions of them). Rasterization goes through each triangle and asks: <strong>"which pixels does this triangle touch?"</strong> Then it fills those pixels in.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/427fcce8-c17b-4784-88c2-f3410bc7bae3.png" alt="" style="display:block;margin:0 auto" />

<h2>Why it is fast</h2>
<p>The computer only looks at the geometry. It does not care about light bouncing around the room. It does not care about reflections. It just projects triangles onto the screen and fills pixels.</p>
<p>This is something graphics cards (GPUs) are extremely good at. They can do this for millions of triangles, sixty or more times per second.</p>
<h2>The big problem</h2>
<p>Rasterization knows <strong>where</strong> things are. But it does not know <strong>how light behaves</strong>.</p>
<p>If you want a shadow, the game has to fake it. If you want a reflection in a puddle, the game has to fake it. If you want soft light bouncing off a red wall onto a white floor (making the floor slightly pink), the game has to fake it.</p>
<p>Games have gotten very good at faking these things over the years. But they are still fakes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/d22d8ba8-ed7f-4d12-b1ac-e58c32377148.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>2. Ray Tracing (the one you care about)</h1>
<p>This one is interesting. It's also the one that took a moment for me to grasp.</p>
<h2>Start with how real light works</h2>
<p>In the real world, light comes out of a source (sun, lamp, fire). It travels in straight lines. It bounces off surfaces. Some of those bounces eventually hit your eyes. That is how you see things.</p>
<p>Every color you see is a photon (a tiny particle of light) that bounced around the world and ended up in your eye.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/cadefb7e-440d-4ea6-a881-844ac8bbc8b1.png" alt="" style="display:block;margin:0 auto" />

<h2>The naive idea</h2>
<p>What if we simulated this inside the computer? Shoot millions of light rays from every lamp. Let them bounce around. Wait for some of them to hit a virtual "eye" (the camera).</p>
<p>This would work. It would look perfect.</p>
<p>But it would be insanely wasteful. Most light rays from a lamp never reach your eye. They go off into empty space or get absorbed. You would be simulating trillions of rays and almost none of them matter.</p>
<h2>The clever flip</h2>
<p>Here is the key insight that makes ray tracing actually work:</p>
<p><strong>Do it backwards.</strong></p>
<p>Instead of shooting rays from the lamp and hoping they hit the camera, shoot rays from the <strong>camera</strong> and see what they hit.</p>
<p>Why? Because we only care about light that reaches the eye anyway. So start from the eye. Go backwards. Trace the light ray in reverse until it finds a light source.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/116e8daa-e158-4c93-a3e2-d711302d497e.png" alt="" style="display:block;margin:0 auto" />

<h2>How a single ray actually works</h2>
<p>Imagine you are the computer. You want to figure out the color of one pixel on the screen.</p>
<ol>
<li><p>You shoot an imaginary ray from the camera, through that pixel, into the 3D scene.</p>
</li>
<li><p>The ray travels in a straight line until it hits something. Let's say it hits a shiny red apple.</p>
</li>
<li><p>Now you ask: "how much light is hitting this exact spot on the apple?"</p>
</li>
<li><p>To answer that, you shoot <strong>more rays</strong> from that spot. One ray toward each light source to check: "is this spot in shadow, or does light reach it?" If something is blocking the path to the lamp, this spot is in shadow. If not, light reaches it.</p>
</li>
<li><p>If the surface is shiny, you also shoot a ray in the reflection direction, to see what is being reflected.</p>
</li>
<li><p>If the surface is glass, you shoot a ray that bends through it (refraction).</p>
</li>
<li><p>Each of those new rays might hit another surface, which might also be shiny or glassy, so you shoot <strong>more rays</strong> from there. And so on.</p>
</li>
<li><p>After a few bounces, you stop. You collect all the information. You calculate the final color for that pixel.</p>
</li>
</ol>
<p>Then you do this for every single pixel on the screen. Millions of them.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/11028494-f5cf-41c8-b078-4bb242b4d793.png" alt="" style="display:block;margin:0 auto" />

<h2>Why this looks so good</h2>
<p>Because you are actually simulating how light behaves.</p>
<ul>
<li><p><strong>Shadows happen naturally.</strong> If a shadow ray gets blocked, the spot is in shadow. No fakery.</p>
</li>
<li><p><strong>Reflections happen naturally.</strong> A reflection ray just bounces and shows you what is there.</p>
</li>
<li><p><strong>Glass and water happen naturally.</strong> Rays bend through them.</p>
</li>
<li><p><strong>Colored light bouncing off walls happens naturally.</strong> The ray picks up the color of what it touches.</p>
</li>
</ul>
<p>Everything comes from the same simple rule: <strong>follow the ray and see what it hits.</strong></p>
<h2>Why it is slow</h2>
<p>Every pixel needs at least one ray. But usually many more (for reflections, shadows, bounces). A 4K screen has 8 million pixels. You might need 10, 50, even hundreds of rays per pixel for a clean image.</p>
<p>That is a lot of rays. For a long time, ray tracing was only used for movies, where one frame could take hours to render. Real-time ray tracing (in games) only became possible recently, because GPUs got special hardware to accelerate it (starting around 2018 with Nvidia's RTX cards).</p>
<hr />
<h1>3. Path Tracing</h1>
<p>Path tracing is ray tracing taken to its logical extreme.</p>
<h2>The difference</h2>
<p>Regular ray tracing usually handles a few specific effects: sharp reflections, sharp shadows, refraction. It still cheats on some things, like soft indirect light.</p>
<p>Path tracing says: <strong>no cheats. Simulate every possible light path.</strong></p>
<p>When a ray hits a surface, path tracing does not just shoot one reflection ray. It shoots rays in many directions, because in reality light bounces off a rough surface in all directions, not just one. Then each of those rays bounces again. And again.</p>
<p>This captures something called <strong>global illumination</strong>: the way light bounces many times and fills a room with soft, indirect light.</p>
<h2>The trade-off</h2>
<p>Path tracing is even slower than basic ray tracing. Because you are shooting <strong>many</strong> rays per bounce, and tracking many bounces.</p>
<p>It also produces noisy images at first (grainy, like an old photo). You need a lot of rays per pixel to smooth it out. Modern games use AI denoisers to clean up the noise quickly.</p>
<p>But when it works, it is the most realistic rendering possible. It is what movies like Pixar's films use. And games like Cyberpunk 2077 and Alan Wake 2 now offer path tracing modes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/8826fcff-3352-4e66-b352-c124786e0eda.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>4. Lumen</h1>
<p>Lumen is not a new rendering method. It is a <strong>system</strong> built by Epic Games for Unreal Engine 5.</p>
<h2>What it does</h2>
<p>Lumen tries to give you global illumination (that soft, bouncing, realistic light from path tracing) without the massive cost of full path tracing.</p>
<p>It is a clever hybrid. It uses a mix of tricks:</p>
<ul>
<li><p>It uses simplified versions of the scene (lower detail) to trace rays quickly.</p>
</li>
<li><p>It uses <strong>screen-space</strong> tricks: looking at what is already on the screen to figure out reflections.</p>
</li>
<li><p>It uses ray tracing for things that need it.</p>
</li>
<li><p>It caches (saves) light calculations and reuses them across frames, so it does not redo work every frame.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/60bb3fa2fffc4c13b9cd5935/62458a02-5471-4fc7-bd61-9b5dccb61288.png" alt="" style="display:block;margin:0 auto" />

<h2>Why it matters</h2>
<p>Full path tracing is too expensive for most games on most hardware. Lumen gets you most of the visual benefit (bouncing light, soft shadows, dynamic reflections) at a fraction of the cost. It makes realistic lighting possible on normal gaming PCs and consoles.</p>
<p>It is less accurate than true path tracing. But it is fast enough to run in real time.</p>
]]></content:encoded></item></channel></rss>