<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Krzyhau&apos;s Blog</title>
    <description>Rambling about tech, art, and everything in between.</description>
    <link>https://blog.krzyhau.pl</link>
    <atom:link href="https://blog.krzyhau.pl/feed.xml" rel="self" type="application/rss+xml" />
    <author>
      <name>Krzyhau</name>
      <email>contact@krzyhau.pl</email>
      <uri>https://krzyhau.pl/</uri>
    </author>
    
      <item>
        <title>The art of portal placement sparks - short research</title>
        <description>&lt;p&gt;One man’s game bug is another man’s tech-art project!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/banner.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;1-a-spark-of-betrayal&quot;&gt;1. A spark of betrayal&lt;/h2&gt;

&lt;p&gt;Portal placement in Portal 2 is one of those game mechanics that feel intuitive enough that people don’t put any thought into it. You shoot a portal and it appears on a wall - seems simple enough. However, in that split of a millisecond, the game performs a bunch of clever algorithms in order to detect the edges of a surface that was just shot, attempts to nudge the portal away from them and ensure the portal is properly placed on a portalable surface. All of that to make sure player’s intention is properly addressed - a wall is shot, so portal should appear on it, as long as it can fit there.&lt;/p&gt;

&lt;p&gt;Unfortunately, the more complex the system is, the more likelihood of a glitch to occur. Portal placement is no exception. Anyone who played this game for long enough can tell you how sometimes the game decides to simply ignore player’s intention and prevent the portal from being placed, despite there being enough space for it. You can imagine how betrayed someone can feel when they expect a portal to be opened, but instead they’re greeted with pesky spark particles, normally seen by shooting non-portalable walls. Outrageous and unfair!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/lies-deceptions.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For causal players, it’s not a big deal - they’ll shoot it again and it will usually work. However, for speedrunners, encountering this issue even once can feel like being punished by the game for no apparent reason. This naturally pushed them to find a way to prevent their portal shots from sparking. It was quickly noticed that some sections of portalable surfaces are more likely to cause a spark. It helped with consistency, but it was still a guesswork. If only there was a way to visualize those sections…&lt;/p&gt;

&lt;h2 id=&quot;2-the-beginnings-of-sparkmapping&quot;&gt;2. The beginnings of sparkmapping&lt;/h2&gt;

&lt;p&gt;On March 1st 2022, past &lt;a href=&quot;https://github.com/p2sr/SourceAutoRecord&quot;&gt;SourceAutoRecord&lt;/a&gt; developer &lt;a href=&quot;https://github.com/mlugg&quot;&gt;mlugg&lt;/a&gt; has created a feature called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pp_scan&lt;/code&gt; (portal placement scanner). The way it works is quite simple! For a defined 2D rectangle in the game’s world, a fake portal shot is being triggered through the original game’s code. Then, shot position is mapped to a pixel in a texture, and a result of a portal shot is stored as either green or red color, for successful or failed portal placement respectively. This process is repeated for every pixel in a target texture, which means that thousands, or sometimes even millions of portal shots have to be simulated in order to create a single image. The result gives you a precise picture of what part of a scanned surface would result in a spark.&lt;/p&gt;

&lt;p&gt;That’s where the art of &lt;strong&gt;sparkmapping&lt;/strong&gt; was born, with those images being dubbed &lt;strong&gt;sparkmaps&lt;/strong&gt;. Many people in the speedrunning community started to share not only sparkmaps for various places in the game, but also their feeling of contempt towards the game’s internal behavior.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/live-sparkmap-reaction.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Those images became a very useful resource - if anyone had trouble with their portals sparking at any spot, they could consult a sparkmap of that spot to know what’s the most preferable part of the surface to aim for.&lt;/p&gt;

&lt;p&gt;Now, it could be argued that an artistic value could also be spotted in them as well, especially with all of those shapes filled with repeating noise patterns that could be more commonly associated with glitch art. In fact, mlugg wasn’t sure whether their first results were a correct representation of a game state or just a graphical error.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/mlugg-message-2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Myself being very interested in internal behavior of Portal 2, and having several attempts at understanding how portal placement code works behind me (and even a successful one!) I was fascinated by this form of presenting its result. What can be learned from observing those patterns? How would they look like if it worked just a bit better? Can one learn to… read them?&lt;/p&gt;

&lt;h2 id=&quot;3-thinking-with-line-trace&quot;&gt;3. Thinking with… line trace?&lt;/h2&gt;

&lt;p&gt;Before we start looking at those weird shapes, I’m gonna give you a chapter filled with some nerdy explanations. It’ll be easier to understand sparkmaps if we understand what causes the sparks in the first place. I’ll try to simplify things whenever possible, since not everything is directly related to the topic we’re discussing, but just in case, I’ve put a massive amount of details in the P2SR Wiki article about &lt;a href=&quot;https://wiki.portal2.sr/Portal_Placement&quot;&gt;Portal Placement&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As previously mentioned, the game needs to determine if and where a portal should fit on a shot surface. Just by looking at a portalable surface, we could roughly tell that. But we’re humans. We’re naturals at pattern recognition (as presented by an image below)! Unfortunately, it’s a privilege computers don’t have, since they can’t see the world the same way we do.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/loss_placement.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We can try our best to develop algorithms to compensate that - image and geometry processing, shape recognition, multi-step collision resolution… but then suddenly our game needs more than 60th of a second to shoot a portal and player experiences a lag spike. That’s bad! The game needs to be able to see its own world and do it efficiently.&lt;/p&gt;

&lt;p&gt;This is where the concept of &lt;strong&gt;collision line tracing&lt;/strong&gt; comes in! It’s a nifty piece of gameplay logic which allows you to mathematically determine the physical properties of the game’s world along a given line, including where the line intersects and what it intersects with. It’s super cheap, especially in Source Engine, which is especially optimized for this operation, with most of the level geometry being built out of planes (and that’s just &lt;a href=&quot;https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection&quot;&gt;a couple of formulas&lt;/a&gt; away from what we need!). Using a bunch of these bad boys in a couple of discrete positions, you can gain a rough approximation of what’s going on in the game’s world, with essentially no cost.&lt;/p&gt;

&lt;p&gt;Portal placement is one of gameplay elements which heavily utilizes this technique - from a simple trace to determine where our portal shot lands after we shoot it, to hundreds of traces used to determine how the portal should fit on a surface. To visualize this, I hacked the game to draw lines for each line traced during portal placement and took a couple of screenshots. Here’s one of them (with more to come later!):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/traces_visualized1.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Very cool, but how do we use this technology? After all, we can’t just go and start blasting line traces everywhere. At some point, it would mitigate the performance benefits we’re getting from this approach. Developers had to be selective about which discrete points to pick in order to find information about the surface on which they’re trying to fit the portal.&lt;/p&gt;

&lt;p&gt;The approach they went for revolves around the corners of a presumed portal, which explains the X pattern that emerges in the screenshot above. Seems logical - corners cover the entire surface behind a portal, so if they’re on a portalable surface, the entire portal must also be on it! It makes perfect sense… until you realize edges are completely disregarded, and you can place portals clipping into corners like this just fine:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/questionable-portal.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Considering this is a bug that actually makes portal &lt;strong&gt;not&lt;/strong&gt; spark, let us acknowledge its irrelevance and talk more about those corners.&lt;/p&gt;

&lt;p&gt;We can easily detect if any walls are enclosing the surface by tracing a line slightly in front of the surface we just shot, going from the shot point to a potential corner point. Likewise, we can detect the edge of a solid surface by checking that line in reverse, but instead slightly within the surface. However, what about cases where the surface is flat - where both portalable and non-portalable surface is flush with each other?&lt;/p&gt;

&lt;p&gt;Interestingly, the first Portal game doesn’t even handle those cases, preventing a portal from being placed completely. This is why, in Portal, such cases of portalable surfaces are usually surrounded with invisible portal bumpers, to act as an enclosing wall for portal placement.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/portal1moment.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now, whether it was a way to make life easier for map designers, or to support unpredictable amount of irregular portalable surfaces (especially with addition of conversion gel, which is able to create portalable surface seemingly anywhere any time with no seam defined by geometry), an algorithm for detecting such portalable surface edge has been implemented for Portal 2, and it involves doing a bunch of tiny line traces towards the shot surface, across the space between a center point and all corners:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/traces_visualized2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Fortunately for game’s performance, it’s done in a smart way - instead of just blindly tracing lines to find the portal edge, a binary search is performed. This way, both position and orientation of a surface edge can be determined in just about 30 line traces! Unfortunately for the game’s intent, this binary search is not deep enough to be precise enough, introducing a bit of an error.&lt;/p&gt;

&lt;p&gt;Even worse, there’s a drastic issue with how edge orientation is determined. The idea is fine, and rather straightforward - a second point on the edge is found by doing yet another binary search, similar to the first one, but in a circle around the first point, and then both points are used to calculate orientation.&lt;/p&gt;

&lt;p&gt;The issue is in implementation. Here’s a C++ pseudocode I’ve created - try to figure out what’s wrong before reading further!&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// we can safely assume &quot;edgePoint + originToCornerOrthogonal * testDistance&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// is portalable, but &quot;edgePoint + originToCornerDirection * testDistance&quot; is not.&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;invalidSurfaceAngle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;validSurfaceAngle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;90.0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testAngle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;validSurfaceAngle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;invalidSurfaceAngle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Vector&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testPoint&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edgePoint&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cosf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testAngle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;originToCornerDirection&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testDistance&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sinf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testAngle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;originToCornerOrthogonal&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testDistance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IsPointOnPortalableSurface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testPoint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;validSurfaceAngle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testAngle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;invalidSurfaceAngle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testAngle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The intention was to do this search starting from 90 degrees, which after 10 iterations of a loop, would result in either of angle variables being close to the value we’re looking for, with an ~0.088 degrees margin of error. However, trigonometry functions take &lt;strong&gt;radians&lt;/strong&gt; as parameters, not &lt;strong&gt;degrees&lt;/strong&gt;. As a result, the initial angle for a binary search is 90 &lt;strong&gt;radians&lt;/strong&gt;, which is roughly 5156.62 degrees. Because of that, the algorithm has a good chance to spit out nonsense, and even the best outcomes have a potential error margin of 5 degrees - big enough to cause some real issues.&lt;/p&gt;

&lt;p&gt;What’s fascinating is that you can see results of that error for yourselves in the game right now! When console variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sv_portal_placement_debug&lt;/code&gt; is set to 1, the game draws a bunch of debug overlays when shooting a portal, which include lines showing what the algorithm thinks the edge of a surface is. Well… not sure I agree with the way it thinks, because those lines are often not even close to being angled properly:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/whatarethooose.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Naturally, developers predicted some kind of imprecisions would occur &lt;em&gt;somewhere&lt;/em&gt;. That’s why, in most cases, whenever a portal is bumped from a checked portal corner, a tiny value, internally called a “bump forgiveness”, is added to bumping distance. But some cases did not get a memo - more specifically, a case where portal is bumped away from a corner of a surface, and that’s where you’ll see the most of sparks. In fact, lack of any leeway in this case can make even the smallest errors lead to a portal sparking. And, if you recall, such error can also come from not only edge’s orientation, but position as well.&lt;/p&gt;

&lt;p&gt;I guess that’s it for nerdy explanations. Well, this is the part where I patch the game.&lt;/p&gt;
&lt;h2 id=&quot;3-the-part-where-i-patch-the-game&quot;&gt;3. The Part Where I Patch The Game.&lt;/h2&gt;

&lt;p&gt;Hello! This is the part where I patch the game! I’m using previously mentioned SourceAutoRecord plugin as a base for my modifications. One reverse-engineering session later, I gained signature scans for every place in memory that I want to change. More specifically, I wanted to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;include bump forgiveness value when portal is bumped from a surface corner.&lt;/li&gt;
  &lt;li&gt;increase the precision of broken binary searches for edge position and orientation detection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having those being applied dynamically by a plugin allows me to easily creates comparisons, and see how sparkmaps change when I enable or disable certain tweaks. I’m gonna go through a couple of curious cases, taking a set of four sparkmaps each to see all permutations of tweaks.&lt;/p&gt;

&lt;p&gt;Let’s start with a simple example - a flat surface with corner portal surface. It can be found in level called Laser Chaining.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/pp_scan_1.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As you can see, without tweaks, that entire corner is filled with a repeating pattern. It makes sense, as the precision loss is dependent on where the binary search starts, so one would expect it to repeat in some way. You can also see the repeating in both axes, as the phenomenon I described before happens for both top and side edge of a surface. The pattern is actually much more complex - it’s just what it converged into at this specific zoom level.&lt;/p&gt;

&lt;p&gt;A bump tweak is eliminating this pattern almost entirely, leaving only a small region of 6 units still affected. But it doesn’t even compare to a precision tweak, which removes the issue altogether with a sheer correctness of calculations.&lt;/p&gt;

&lt;p&gt;Curiously, with enough precision, a fundamental flaw of the edge orientation algorithm emerges. It can now incorrectly determine edge’s orientation if a corner of a portal ends up being exactly a tiny distance away from a corner of a surface (I invite you to consult previously posted pseudocode and try to figure out why, but if you just want to know, &lt;a href=&quot;assets/2026-08-01-portal2-sparkmaps/puzzle_solution.jpg&quot;&gt;here’s a very basic explanation that I’ve drawn in MS Paint&lt;/a&gt;). As a result, anything along a very thin diagonal line is detected as non-portallable due to the game thinking there isn’t enough space. It just goes to show how complicated, and thus, fragile, this whole thing is!&lt;/p&gt;

&lt;p&gt;Let’s check another sparkmap. This one in co-op level Bridge Testing has actually been named by the community - “Speech Bubble”. Go figure.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/pp_scan_2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There are two interesting characteristics about this one. An obvious one is the circular shape. Map designers has placed something called a portal placement helper on this surface, which is essentially a spherical region that will automatically snap a portal that was shot within it to a correct position. It’s a neat workaround for all of those bugs in critical places where you want your portal placement to always succeed, and it’s used quite frequently across the entire game. Honestly, I could write an entire separate blog post about those! (I’m not sure if I should…)&lt;/p&gt;

&lt;p&gt;Another unique trait is how stripes in spark regions are horizontal. Considering how vertical edges are all geometry based, and the game deals with them perfectly, we see oscillation in a pattern only by varying the distance from horizontal edges, hence why it changes vertically.&lt;/p&gt;

&lt;p&gt;Again, we see a proof of bump tweak mostly fixing the issue, but not entirely, and precision tweak being necessary to completely eliminate this issue. Weirdly enough, when you combine both of these tweaks, a small non-portalable dot appears in the bottom right sector of that surface. Apparently, in that spot, the game decides there is not enough space to fit a portal, because of the fact we’re bumping it that small extra distance. However, without precision tweak, the deviation happens to be helpful, preventing the portal from being bumped into an opposite wall. A weird combination, honestly.&lt;/p&gt;

&lt;p&gt;In any case, it seems like my tweaks are actually doing something! Could it be that I solved portal sparking?! Let’s check more extreme example, like this one from the level named Smooth Jazz:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/pp_scan_3.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Oh… well, it definitely seems to be less noisier after precision tweak…&lt;/p&gt;

&lt;p&gt;You see, in cases like these, the problem doesn’t lie in bugs in code, but in the approach itself. As hinted previously, small irregularities can make the algorithm give up, thinking it will never find enough space around given point, or even if it does, then the portal would be nudged too far away to feel natural. Funnily enough, in the case shown above, precision issues actually help the portal being nudged into a portallable surface just by sheer luck.&lt;/p&gt;

&lt;p&gt;We could dive into this rabbit hole much deeper, try to find viable alternatives for a portal placement algorithm, like pre-baked portal placement maps or mini physics solver… However, just for now, I’m going to accept what we have, appreciate that it works most of the time, and admire the abstraction it forms when it fails. (yes I’m lazy 🙃)&lt;/p&gt;

&lt;p&gt;With that being said, here’s an absolute masterpiece created by portal placement algorithm in Portal Stories: Mel. The world record holder of this Portal 2 mod, &lt;a href=&quot;https://www.twitch.tv/burger40&quot;&gt;Burger40&lt;/a&gt;, has humbly named it “The Firestorm Bong”:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2026-08-01-portal2-sparkmaps/firestorm-bong.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;4-i-call-this-piece-spark&quot;&gt;4. I call this piece… “Spark”.&lt;/h2&gt;

&lt;p&gt;I hope that, after slightly prolonged explanation and a short exploration of portal sparks, you can see why I find them so fascinating! They’re a window to the game’s internal behavior, a capture of what the game sees and what we can’t, a unique perspective of thinking about the virtual world which is not only natural, but necessary for a machine, but abstract and alien to us humans! I find it incredibly important, especially in the age of machine learning algorithms, to think about methods of portraying how computers think.&lt;/p&gt;

&lt;p&gt;As a final homage to this niche piece of technology, I’ll leave you with what’s probably one of my favorite videos yet, where I bring the art of portal sparking to its extremes, and create a live recording of sparking surfaces, as seen by the game’s algorithm:&lt;/p&gt;

&lt;div class=&quot;youtube-video&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/BLdkiCwBoWg&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
</description>
        <pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate>
        <link>https://blog.krzyhau.pl//portal2-sparkmaps</link>
        <link href="https://blog.krzyhau.pl/portal2-sparkmaps"/>
        <guid isPermaLink="true">https://blog.krzyhau.pl/portal2-sparkmaps</guid>
      </item>
    
      <item>
        <title>FEZ spectrogram images - personal endeavor</title>
        <description>&lt;p&gt;I’ve generated a crisp-clean render of images hidden within FEZ original soundtrack. All it took was a couple of scripts and… smuggling of audio tracks from France?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/banner.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;1-adventure&quot;&gt;1. Adventure&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://store.steampowered.com/app/224760/FEZ/&quot;&gt;FEZ&lt;/a&gt; is a game that has been close to my heart since I played it. I remember my younger self slowly figuring out all the meta puzzles and falling in love with the formula of metroid-brainia. To this day, I look back with joy to the day I ran back from school just to make it in time to collect an anti-cube from the clock tower. Shortly speaking, FEZ heavily influenced me both as a gamer and a game developer. So, not surprisingly, every once in a while, I’ll try to join my passion for completely random technical bullshittery with some of the puzzles from this fantastic game (some of which are yet to be solved to this day!)&lt;/p&gt;

&lt;p&gt;One of these puzzles are hidden within the game’s soundtrack. Its composer, Disasterpeace, approached the game’s creator, Phil Fish, with an idea of hiding images within audio tracks (about which he wrote &lt;a href=&quot;http://disasterpeace.com/blog/fez.postmortem.html&quot;&gt;on his own blog&lt;/a&gt;). The idea is to have an image encoded into a very specific sound wave, so that when it’s viewed as a &lt;a href=&quot;https://en.wikipedia.org/wiki/Spectrogram&quot;&gt;spectrogram&lt;/a&gt;, the image appears. Quite fun thing to do for something like a meta puzzle or an &lt;a href=&quot;https://en.wikipedia.org/wiki/Alternate_reality_game&quot;&gt;ARG&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a small example of such thing that I made using an online converter. I’ve put both the original image and the image produced by previewing the encoded audio file in a spectrogram view in Audacity below, as well as the audio itself. Just be warned, this audio does awful things to your ears, so lower down the volume before trying to play it back.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/check-this-shit-comparison.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;audio src=&quot;assets/2025-02-05-fez-spectrograms-adventure/check-this-shit-audio.wav&quot; controls=&quot;&quot; preload=&quot;&quot;&gt;&lt;/audio&gt;

&lt;p&gt;I’ve decided that recovering those images from the soundtrack is a fun idea for a side project! However, just opening a spectrogram viewer and screenshotting every track doesn’t sound like something I’d enjoy doing (also, it was done multiple times before me already). What I’d like is to automate this process, so I can easily iterate on my tweaks and apply them to all tracks at once, then see how clean of images I can recover with a bunch of tweaks and slight post-processing. On top of that, well, once I have these images, I might do some kind of artsy thing with them! 👀&lt;/p&gt;

&lt;h2 id=&quot;2-puzzle&quot;&gt;2. Puzzle&lt;/h2&gt;

&lt;p&gt;To start up, I focused on figuring out the best method of extraction. I wanted the output to be as clean and detailed as possible. And here’s where the first problem emerged - while Disasterpeace tried to insert them in places where there’s as little noise as possible, in some places the generated sound is mixing with a background noise from the soundtrack, resulting in a complete mess…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/what-a-mess.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I tried different methods - audio mixing, image processing etc. -  but there’s only so much you can do without &lt;em&gt;additional data&lt;/em&gt;… hmm, so what kind of &lt;em&gt;additional data&lt;/em&gt; could help here?&lt;/p&gt;

&lt;p&gt;You see, there’s this cool thing called &lt;strong&gt;phase cancellation&lt;/strong&gt;. As a general simplification - sound waves work like math:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;When you play two sound waves together, you get one that is a mix of two: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A + B = C&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Likewise, if you have that mixed audio and play it along with one of the original waves, but with an &lt;em&gt;inverted phase&lt;/em&gt;, you get the other one in isolation: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C - B = A&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means that, if I had the same exact soundtrack but &lt;strong&gt;unpuzzlified&lt;/strong&gt; (as in, without spectrogram images in it), I could invert its phase, then mix it with the &lt;strong&gt;puzzlified&lt;/strong&gt; version (the one with spectrogram images) and get a pure audio file of just spectrogram images with absolutely zero additional noise! This meant it was time to begin the hunt for the unpuzzlified version of the OST!&lt;/p&gt;
&lt;h2 id=&quot;3-beyond&quot;&gt;3. Beyond&lt;/h2&gt;

&lt;p&gt;I’m actually not the first person to come up with this idea. A Reddit user named Supersaiyan_IV &lt;a href=&quot;https://www.reddit.com/r/Fez/comments/1ynlh1/fez_ost_image_analysis_idea/&quot;&gt;has made a post&lt;/a&gt; about their version of the OST also being unpuzzlified, and using it to decode clearer versions of hidden images. Interestingly enough, they obtained it through a Humble Indie Bundle 11 offer. Unfortunately for me, these offers are time-limited, and I’m already about 11 years late for it as of writing this article. However, if the unpuzzlified version appeared in something as typical as Humble Bundle, then surely it can be easily obtained somewhere else… right?&lt;/p&gt;

&lt;p&gt;In my hunt, I used the track &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;03 - Beyond&lt;/code&gt; as a reference - the hidden image in this track is right at the beginning and it has an amplitude big enough to be heard quite easily as a high-pitched noise. You can listen for the difference yourself (headphones are recommended):&lt;/p&gt;

&lt;audio src=&quot;assets/2025-02-05-fez-spectrograms-adventure/beyond-puzzlified.wav&quot; controls=&quot;&quot; preload=&quot;&quot;&gt;&lt;/audio&gt;
&lt;audio src=&quot;assets/2025-02-05-fez-spectrograms-adventure/beyond-unpuzzlified.wav&quot; controls=&quot;&quot; preload=&quot;&quot;&gt;&lt;/audio&gt;

&lt;p&gt;Here are all the sources of the soundtrack that I managed to find:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://music.disasterpeace.com/album/fez-ost&quot;&gt;Disasterpeace Bandcamp album page&lt;/a&gt; - when bought, you get access to variety of audio formats (FLAC, WAV, MP3 and so on…). Unfortunately, all puzzlified.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/224763/FEZ_Original_Soundtrack/&quot;&gt;Steam’s store page&lt;/a&gt; - gives you two versions, FLAC and MP3. Both of them puzzlified. However, there are witnesses which claim Steam was distributing an unpuzzlified version for a short moment.&lt;/li&gt;
  &lt;li&gt;Officially released vinyl record (it has no rights to &lt;a href=&quot;https://coloredvinylrecords.com/disasterpeace-fez.html&quot;&gt;look this cool&lt;/a&gt;) - based on &lt;a href=&quot;https://www.youtube.com/watch?v=A8ENJhW1YqY&quot;&gt;a video of a rip I found on YouTube&lt;/a&gt; I can tell it has unpuzzlified soundtrack pressed onto it, and presumably it came with a digital version as well, but these days it’s incredibly hard to come by&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Humble Indie Bundle 11&lt;/strong&gt; - it had unpuzzlified soundtrack, but as I said before, I’m 11 years too late… I could ask someone who bought it that way to give me the files (I was actually talking with someone who did when researching this project), but I was eager to see if there’s a way to obtain the unpuzzlified soundtrack through more &lt;em&gt;legal&lt;/em&gt; means these days.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;All the popular streaming services (YouTube, SoundCloud, Apple Music etc.)&lt;/strong&gt; - surprisingly, they all have unpuzzlified versions of the soundtrack! However, in most of them, the tracks are compressed (which would influence the phase cancellation process) or there’s no legal way of ripping those tracks out of these services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, my search has led me to &lt;a href=&quot;https://www.qobuz.com/us-en/album/fez-original-game-soundtrack-disasterpeace/vhpqon0wdtloa&quot;&gt;Qobuz&lt;/a&gt; - some kind of french streaming service I’ve never heard of. It had FEZ soundtrack on it, and it allowed download of raw, lossless music track files. And despite the fact I couldn’t be sure these tracks are unpuzzlified, I had a reason to believe so: it seemed like it was distributed by a music label which was in close association with IDOL - a music distributor who distributed FEZ soundtrack on other streaming services.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/found-it.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There was really only a one way to find out if they had what I was looking for, and that was to simply put some money on a table. Unfortunately, I had some slight difficulties of making a purchase, since Qobuz is region-locked, and it wasn’t letting me register an account in Poland, nor it accepted my transactions. But that’s what you’ve got social connections for! I’ve contacted a French colleague of mine from the &lt;a href=&quot;https://www.youtube.com/watch?v=MZi1dXwCqG8&quot;&gt;old Portal 2 TASing days&lt;/a&gt; to act as my FEZ OST smuggler and spend 5 euros for a soundtrack of a game he barely heard of (I still owe him a beer lol). After a short exchange of words and making fun of French language… I’ve got it! I became a proud &lt;strong&gt;legal&lt;/strong&gt; owner of unpuzzlified FEZ Original Soundtrack! 🥳&lt;/p&gt;

&lt;p&gt;First thing I did - I downloaded one of the tracks to perform phase cancellation in Audacity, and my goodness, I’ve never seen this image being as crisp as this (well, apart from &lt;a href=&quot;https://www.youtube.com/watch?v=EyfOh2za-po&quot;&gt;the original&lt;/a&gt;). If you compare it with a screenshot I posted earlier in this post, the difference is shocking!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/heehee.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;4-progress&quot;&gt;4. Progress&lt;/h2&gt;

&lt;p&gt;As I said, I wanted to automate the isolation process, so that I could tweak the parameters and achieve the best looking result for all images at once.&lt;/p&gt;

&lt;p&gt;To achieve this, I first prepared a small database of where in the soundtrack spectrogram images are located - track name, time, duration and frequency range. It was as daunting as it sounds… For about an hour or so, I was in a loop of opening a track, looking for a hidden image, measuring the time span and frequency range, then writing it down. I could’ve automated it, but I’ve decided that fine-tuning the algorithm and filtering out bad apples would take longer than doing it manually.&lt;/p&gt;

&lt;p&gt;Ultimately, I ended up with this list:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;01 Adventure &amp;gt; (none)
02 Puzzle &amp;gt; 2:08.900 - 2:12.250, 800-20000Hz
03 Beyond &amp;gt; 0:00.000 - 0:06.500, 800-20000Hz
04 Progress &amp;gt; 4:10.400 - 4:16.000, 1200-13000Hz
05 Beacon &amp;gt; 2:29.140 - 2:32.340, 500-20000Hz
06 Flow &amp;gt; 0:02.000 - 0:05.190, 500-20000Hz
07 Formations &amp;gt; (none)
08 Legend &amp;gt; 1:15.370 - 1:18:270, 100-20000Hz
09 Compass &amp;gt; 2:37.350 - 2:44:050, 120-20000Hz
10 Forgotten &amp;gt; 2:32:110 - 2:34:360, 800-20000Hz
11 Sync &amp;gt; (none)
12 Glitch &amp;gt; (none)
13 Fear &amp;gt; 3:29.410 - 3:32.050, 800-20000Hz
14 Spirit &amp;gt; 2:48.130 - 2:52.180, 900-20000Hz
15 Nature &amp;gt; (none)
16 Knowledge &amp;gt; (none)
17 Death &amp;gt; (none)
18 Memory &amp;gt; 1:13:630 - 1:16.270, 800-11000Hz
19 Pressure &amp;gt; (none)
20 Nocturne &amp;gt; (none)
21 Age &amp;gt; 2:46.450 - 2:56.200, 90-20000Hz
22 Majesty &amp;gt; 3:15.360 - 3:21:810, 700-20000Hz
23 Continuum &amp;gt; 2:29.010 - 2:35.860, 450-20000Hz
24 Home &amp;gt; (none)
25 Reflection &amp;gt; 8:13.900 - 8:20.480, 800-21000Hz
26 Love &amp;gt; 1:02.870 - 1:09.090, 450-20000Hz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Thanks to recent technological advancements (and fortunately for my lazy butt) I didn’t have to write a custom parser or manually rewrite the list into a more usable format, and I could just let AI create a JSON file for me! Honestly, saying that we live in the most convenient times for technical experiments like these is an understatement.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/yo-mr-chat.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Once I had the JSON file, it was pretty straight-forward as well. I’ve decided to use Python - I cannot put enough emphasis on how easy it is to prototype stuff with it these days. Even though I started knowing barely anything about specific libraries I’d need for the job, with massive amounts of information both within official documentations and on StackOverflow, as well as being assisted by tools like GitHub Copilot, I’ve gotten from nothing to a finished product within a couple of hours.&lt;/p&gt;

&lt;p&gt;The created script does the following for each track:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;loads both &lt;strong&gt;puzzlified&lt;/strong&gt; and &lt;strong&gt;unpuzzlified&lt;/strong&gt; versions of the track,&lt;/li&gt;
  &lt;li&gt;performs &lt;strong&gt;phase cancellation&lt;/strong&gt; on them, isolating a track which contains purely the sound generated from the image,&lt;/li&gt;
  &lt;li&gt;cuts the audio using the database I previously created and saves it to a file,&lt;/li&gt;
  &lt;li&gt;generates a spectrogram image using Fourier transform algorithm from &lt;a href=&quot;https://librosa.org/&quot;&gt;librosa&lt;/a&gt; and then plots it using &lt;a href=&quot;https://matplotlib.org/&quot;&gt;matplotlib&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, there were some interesting edge cases that I had to handle that I think are worth mentioning, mainly:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Track &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;09 Compass&lt;/code&gt; is actually a few seconds longer in a puzzlified version, to fit the image in the silent part at the end of the audio file. I simply left that part unmodified, since, logically, there was no audio to perform phase cancellation on in the first place.&lt;/li&gt;
  &lt;li&gt;Both tracks &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10 Forgotten&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;14 Spirit&lt;/code&gt; have the aplitude of the audio lowered down in the puzzlified version to make the hidden images more visible (oh irony). I had to figure out how much it was lowered down to be able to make a perfect phase cancellation. &lt;strong&gt;15dB&lt;/strong&gt; seemed to be the magic number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the script done, it was just a matter of fine-tuning… lots of fine-tuning. I tried my best to find a set of Fourier transform parameters giving the cleanest results, but I also relied on doing some math on the resulting data set to tweak brightness and contrast across different frequencies (the resulting images were much brighter on lower frequencies). Apart from that, I’ve also tried some random ideas in hope of improving the quality, including:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Using different parameters for specific ranges of frequencies - I observed that some parts of the spectrogram image were becoming a little bit sharper when using specific parameters for Fourier transform. I abandoned this idea due to some weird technical issues that I didn’t bother to fix, but it might be worth exploring.&lt;/li&gt;
  &lt;li&gt;Randomize parameters to generate a set of outputs, then return an average of them - I actually kept this in the project. The resulting image is less noisy, with a small cost of being slightly blurrier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have any ideas on how I could get more clearer images from these audio tracks (not including obvious approaches like standard de-noising algorithms) then I’d like to hear them! But for now, I’ll stick with what I’ve got right now and move on to the artistic part of the project!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/heehee2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;5-nature&quot;&gt;5. Nature&lt;/h2&gt;

&lt;p&gt;Once the technical part was done, I could do anything with those images - having a computer do the heavy lifting for me means I could go creative and represent the data in any way I wanted.&lt;/p&gt;

&lt;p&gt;What I aimed for was an effect similar to contact sheets - I imagined images rendered in gray-scale, arranged one next to another, with some additional cool metadata written around them on a frame. This kind of old-school lost media vibes seem rather fitting, and doesn’t sound that hard to replicate in the most basic form.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/contact-sheet-example.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is the moment where I nerd out over &lt;strong&gt;matplotlib&lt;/strong&gt; for a second - WOW, what an amazing library! I thought that I’d need a separate image processing library to present all additional details properly, but it turned out it allowed me to do all the key things I wanted while being fully configurable! It also feels like I only glanced over the tip of the iceberg, so I might need to have a rendezvous with it at some point and just plot a bunch of useless graphs.&lt;/p&gt;

&lt;p&gt;With some tweaking, I made the script spit out an absolute beauty of an image! The labels not only add up to the vibe I wanted to achieve, but they are actually informative as well: they tell you where in the audio track you can find those images - both in time and frequency. Honestly, it’s something I’d happily put on a poster or a t-shirt!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/fez_spectrogram_images.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;6-love&quot;&gt;6. Love&lt;/h2&gt;

&lt;p&gt;This is where I leave this project to collect the dust. I’ve put everything that I’ve created for it in a &lt;a href=&quot;https://github.com/Krzyhau/fez-spectrogram-reader&quot;&gt;GitHub repository&lt;/a&gt;, both code and output files (available to download from Releases tab) - go wild with them!&lt;/p&gt;

&lt;p&gt;To wrap it all up, I’ve decided to make it look even cooler by slapping a quick 3D scene in Blender. It’s probably one of the coolest things I’ve done in a while.&lt;/p&gt;

&lt;p&gt;Let’s hope I’ll make something even cooler soon enough, about which I’ll make sure to write a blog post… whatever that will be!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-05-fez-spectrograms-adventure/fez-spectrograms-blender-render.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</description>
        <pubDate>Wed, 05 Feb 2025 00:00:00 +0000</pubDate>
        <link>https://blog.krzyhau.pl//fez-spectrograms-adventure</link>
        <link href="https://blog.krzyhau.pl/fez-spectrograms-adventure"/>
        <guid isPermaLink="true">https://blog.krzyhau.pl/fez-spectrograms-adventure</guid>
      </item>
    
      <item>
        <title>Creating my own blog - manifesto</title>
        <description>&lt;p&gt;I’ve created a small blog. This is it! You’re reading it! Heyo! 👋&lt;/p&gt;

&lt;p&gt;Every once in a while, I do something cool (well, for me at least). Then, I have an urge to post it somewhere, but I don’t have a place to do it. I could record a video about it and post on &lt;a href=&quot;https://www.youtube.com/@Krzyhau&quot;&gt;my YouTube channel&lt;/a&gt; but not everything deserves to be presented in a low-effort video form in my eye. I could probably slap it somewhere on a public chat within a certain community, and actually that’s what I’ve been doing most of the time. But then, I’m kind of spread out between communities in my creative work, and some work don’t even have a suitable place to be posted that way.&lt;/p&gt;

&lt;p&gt;I don’t really have a place to call home, to post whatever crazy idea I manage to realize into existence, send it away into the void of the Internet and let some random strangers admire it. It’s kind of discouraging, to the point most of my small projects haven’t been posted anywhere, and I can’t flex with them to friends, co-workers or random people I meet during events.&lt;/p&gt;

&lt;p&gt;I always wanted to make a small blog which would fulfill such purpose, but never gotten around doing it. However, this weekend, when I lost motivation to do literally anything more productive, I’ve done it! So, to test this idea out, here’s a write-up of how I did it.&lt;/p&gt;
&lt;h2 id=&quot;the-plan&quot;&gt;The plan&lt;/h2&gt;

&lt;p&gt;Here’s what I expected my blog to be:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Minimalistic&lt;/strong&gt; - no unnecessary bloat. I wanted to allow readers to get straight to the point and focus on what matters - the juicy content.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Easy to setup and manage&lt;/strong&gt; - why bother having to host an entire CMS like WordPress if all I want to do is to have some text written up and displayed online?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Easy to use&lt;/strong&gt; - I want to be able to get a quick access to the insides of the blog and send new content into it, regardless of where I am or what device I’m using.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Configurable&lt;/strong&gt; - While I could use some external websites for blogging like Medium.com, I like to have as much control over the platform I’m posting on as possible. I don’t care about exposure in this case, I just want to post random stuff!&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Preservable&lt;/strong&gt; - When I ever decide to switch the back-end to something else, I want the process to be as swift as possible.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Interactive&lt;/strong&gt; - A nice bonus for being able to read some random ramblings written by a polish guy is to be able to comment below them to tell him what a bumbling buffoon he is!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-execution&quot;&gt;The execution&lt;/h2&gt;

&lt;p&gt;While I could’ve written my own simple CMS for this blog, it’s an entire project on itself, and I want to start posting stuff rather than have it blocked by ambition. So I went for rather simple yet quite interesting combo.&lt;/p&gt;

&lt;p&gt;For start, I’m hosting this blog on &lt;a href=&quot;https://pages.github.com/&quot;&gt;GitHub Pages&lt;/a&gt;. Fantastic feature - you can turn any repository into a working website, all for free! While I am paying for a custom domain, GitHub can give you your own (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[username].github.io&lt;/code&gt;). Probably a better use case would be a landing page for your project (it’s amazing how awful GitHub is to navigate for casual users - the number of times I have to explain how one can download released executable from a GitHub repository is kind of surprising) but I’ve seen people host their blogs on it, so why shouldn’t I?!&lt;/p&gt;

&lt;p&gt;What’s more important is that GitHub Pages is powered by &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; in a background, so I can take advantage of an ease of writing stuff in &lt;a href=&quot;https://www.markdownguide.org/basic-syntax/&quot;&gt;Markdown&lt;/a&gt;. My posts written in this quite simple format can then be generated into a static HTML pages based on a previously prepared template. This is fantastic for two reasons. One is that I could easily migrate to any other solution, whether it’s another instance of Jekyll lying on a VPS or a completely custom solution made from scratch (because I fully expect to go crazy with such idea at some point in the future). But more important benefit is that I can use &lt;a href=&quot;https://obsidian.md/&quot;&gt;Obsidian&lt;/a&gt; to easily write the posts in a WYSIWYG fashion. It’s a tool that I’ve already used for making my own personal notes, so it’s quite convenient for me to introduce it into another part of my life!
Look! Here’s me writing this very page in Obsidian right now!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-02-blog-creation-manifesto/writing-recursive.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Being a good static content generator, Jekyll gives you tools to fully customize the looks of your website. For now, I’ve decided to just &lt;del&gt;shamelessly steal&lt;/del&gt; acquire a default &lt;a href=&quot;https://github.com/pages-themes/hacker&quot;&gt;Hacker&lt;/a&gt; theme and heavily customize it to my needs (in fact, I’d argue there’s not a lot left from that theme). I ended up with something fairly simple and hopefully easy to consume, but I might tweak it in the future if I change my mind. I guess I’ll leave a screenshot of how it’s looking right now for historic preservation:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-02-blog-creation-manifesto/first-screenshot.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;With all of that set up, the last thing that’s missing is the interactivity part. You may think that, being a static website, it’ll be difficult to set up any kind of comment section system. Apparently not!&lt;/p&gt;

&lt;p&gt;After a little bit of googling, I found &lt;a href=&quot;https://utteranc.es/&quot;&gt;utterances&lt;/a&gt;, a small JavaScript widget adding a comment section based on GitHub Issues. Since I’m already hosting blog on a GitHub repository, it’s the exact puzzle piece I needed! So now, not only you can completely ignore this entire post by scrolling all the way down, but also post a mean comment about how much you dislike my content (also with an ability to be formatted in Markdown!), and even use emoji reactions!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;assets/2025-02-02-blog-creation-manifesto/comment-screenshot.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I’m already quite happy with this setup and I’m eager to write some stuff to fill it in with &lt;strong&gt;the juice™️&lt;/strong&gt;!  So, uh… I guess I’ll see ya in the next post!&lt;/p&gt;
</description>
        <pubDate>Sun, 02 Feb 2025 00:00:00 +0000</pubDate>
        <link>https://blog.krzyhau.pl//blog-creation-manifesto</link>
        <link href="https://blog.krzyhau.pl/blog-creation-manifesto"/>
        <guid isPermaLink="true">https://blog.krzyhau.pl/blog-creation-manifesto</guid>
      </item>
    
  </channel>
</rss>
