Tyler Hallada - BlogMusings on technology, literature, and interesting topics
https://www.hallada.net/blog
Row Your Boat: How I made a boat physics simulation inside Oblivion Remastered<p>If creativity is borne out of constraints, creating mods for games must be one of the most creative things you can do as a programmer. It’s just so fun to hack a game engine to do something it was never supposed to do.</p>
<p>This blog post goes into depth describing a mod I made for the game <a href="https://elderscrolls.bethesda.net/en-US/oblivion-remastered">Oblivion Remastered</a>, a full graphics overhaul of the classic 2006 open-world RPG <a href="https://elderscrolls.bethesda.net/en/oblivion">Elder Scrolls IV: Oblivion</a> . The mod, <a href="https://www.nexusmods.com/oblivionremastered/mods/4273">Row Your Boat</a>, adds something the game engine was never supposed to support: a useable rowboat. The player can purchase the rowboat, row it on any waterway, drag it over land, summon it anywhere, add upgrades, all with a realistic boat physics simulation I developed inside the limited built-in scripting engine the game engine provides.</p>
<p><div class="video-container"><iframe width="560" height="315" src="https://www.youtube.com/embed/SE55cqIZNp4?si=h0yatrJK7QN-r9x-" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe></div></p>
<!--excerpt-->
<p>All of the scripts included in the mod are on my <a href="https://github.com/thallada/RowYourBoat">GitHub here</a>.</p>
<h3 id="the-game-release">The Game Release</h3>
<p>The Elder Scrolls series from <a href="https://bethesda.net/">Bethesda Softworks</a> is probably the most modded series of games ever. One of <a href="https://www.hallada.net/2022/10/05/modmapper-putting-every-skyrim-mod-on-a-map-with-rust.html">my previous projects involved indexing and mapping hundreds of thousands of mods for Skyrim</a> (the latest game in the series), so I know. A major reason why modding this series is so popular is because Bethesda releases the editor tools they used to create the games to the public for free. These tools lower the barrier of entry to modding and encourages complete beginners to try their hand at creating mods. I credit modding <a href="https://elderscrolls.bethesda.net/en/oblivion">Elder Scrolls IV: Oblivion</a> when I was a teenager with originally getting me interested in programming. The tool they released for the classic 2006 Oblivion is called the <a href="https://en.uesp.net/wiki/Oblivion_Mod:Construction_Set">Construction Set</a>. It gave modders the ability to create new items, quests, creatures, and modify pretty much anything else in the game engine Oblivion was built off of: <a href="http://www.gamebryo.com/">Gamebryo</a>.</p>
<p>When Oblivion Remastered was shadow-dropped on April 22, 2025 a lot of people doubted how moddable the remaster would be since <a href="https://www.virtuosgames.com/">Virtuos</a>, the developer behind the remaster, had to create a Frankenstein-meld of Gamebryo and <a href="https://www.unrealengine.com/">Unreal Engine</a> to accomplish the remaster. There were no modding tools released by Virtuos or Bethesda. For the first time, modding an Elder Scrolls game wasn’t officially supported.</p>
<p>Despite the challenges, a whole modding community sprung up overnight and began poking and prodding at the game that was released to see what was possible. The forums lit up in frenzy and <a href="https://discord.com/invite/ycKUrgFGMk">discord groups for sharing research developments</a> were established. It wasn’t long before the first mods started trickling in on the new <a href="https://www.nexusmods.com/games/oblivionremastered/mods">Oblivion Remastered site on Nexus Mods</a>. At first they were simple <a href="https://www.nexusmods.com/oblivionremastered/mods/15">desktop icon replacers</a> or <a href="https://www.nexusmods.com/oblivionremastered/mods/14">skip intro video mods</a> but they started to get more complex as modders figured out how to modify this new game engine.</p>
<p>The introduction of Unreal Engine was both a blessing and a curse for modding. On one hand, there is an existing community around modding games built on Unreal Engine with their own <a href="https://docs.ue4ss.com/">tools and scripting system</a>. But, on the other hand, it broke compatibility with pretty much every mod originally developed for classic Oblivion. It wasn’t going to be easy to port mods to the remaster. Modders would need to develop mods from scratch for the remaster.</p>
<p>Luckily, a lot of the modding tools that were used for the original game worked with the remaster after some tweaks. <a href="https://discord.com/channels/1364356029932109976/1370869854193582212">The community discovered that you could use the original Construction Set</a> (along with <a href="https://www.nexusmods.com/oblivion/mods/36370">the fantastic Construction Set Extender</a>) to create plugins that would load in Oblivion Remastered. New beta versions of <a href="https://github.com/TES5Edit/TES5Edit">xEdit</a> were released to support Oblivion Remastered (a GUI program for viewing and editing data inside mod plugins). A new version of <a href="https://github.com/ianpatt/obse64">Oblivion Script Extender for Oblivion Remastered (OBSE64)</a> was released (adds new scripting functions through reverse-engineering of the game engine). It was starting to look like it would be possible to create some truly complex mods in the remaster.</p>
<h3 id="the-idea">The Idea</h3>
<p><img src="/img/blog/waterfront-rumare.jpg" alt="Screenshot in Oblivion Remastered from the Imperial City Waterfront overlooking the Lake Rumare" /></p>
<p>In my own play-through of the game, I had just purchased the <a href="https://en.uesp.net/wiki/Oblivion:Shack_for_Sale">Imperial City Waterfront shack</a> as my character’s first home, and I was wandering around the waterfront right outside the shack and looking out over at the far shoreline of the <a href="https://en.uesp.net/wiki/Oblivion:Lake_Rumare">Lake Rumare</a>. I suddenly had the thought: wouldn’t it be great to row a boat over there? So much of <a href="https://en.uesp.net/wiki/Oblivion:Cyrodiil">Cyrodiil</a>, the province Oblivion is set in, is carved by rivers and lakes, and it’s bordered on two ends by ocean. All of this space on the map is only accessible through swimming, which is the slowest and most cumbersome way to travel in the game (it’s also slow on a horse, which is the only “vehicle” in the game). Why <em>shouldn’t</em> the player be able to take any one of those boats that litter the shorelines and row it anywhere?</p>
<h3 id="researching-the-original-boat-mod">Researching the Original Boat Mod</h3>
<p>In fact, I did remember downloading some sort of controllable boat mod for Oblivion way back in the day. A quick <a href="https://www.nexusmods.com/games/oblivion/mods?keyword=boat&sort=endorsements">search on old Oblivion Nexus Mods reveals a bunch of mods that allow the player to pilot boats</a>, so I knew it should be possible in theory. Out of all of these, <a href="https://www.nexusmods.com/oblivion/mods/3575">Jason1s Pilotable Pirate Ship</a> stood out to me. Impressively, the mod was published just a month after the initial release of the game in 2006. A lot of the other boat mods created later (reasonably) depended on and used <a href="https://obse.silverlock.org/">OBSE</a> functions for their functionality, but this mod used only the game’s built-in scripting language. OBSE64 for Oblivion Remastered isn’t yet far enough along yet to provide the same fancy functions those other mods used. So, I decided to look into the source files of Jason1’s mod to find out how he made a controllable boat with the same original limited scripting language I had to deal with.</p>
<p>
<figure>
<img alt="Screenshot from Jason1s Pilotable Pirate Ship mod" src="/img/blog/jason1-ship-mod.jpg" />
<figurecaption>
<em>
Screenshot from Jason1s Pilotable Pirate Ship mod by <a href="https://next.nexusmods.com/profile/Ioana?gameId=101">
Ioana</a>.
</em>
</figurecaption>
</figure>
</p>
<p>What I found in the scripts was fascinating. Without any native vehicle physics system, collision detection API, or even basic vector and trigonometric math functions, Jason1 was able to create clever workarounds that exploited other systems in the game to create controllable boats.</p>
<p>For example, take a look at this excerpt from the scripts which is a custom implementation of the trigonometric functions using a 7-term <a href="https://en.wikipedia.org/wiki/Taylor_series#Approximation_error_and_convergence">Taylor series approximation</a> of sine (since there is no built-in trig functions in the scripting language):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>;Calculate radians
Set r to (d * 3.14159265) / 180
Set r2 to (d2 * 3.14159265) / 180
;Calculate offsets with Taylor series expansion of sine
Set x to r - ((r * r * r) / 6) + ((r * r * r * r * r) / 120) - ((r * r * r * r * r * r * r) / 5040)
Set y to r2 - ((r2 * r2 * r2) / 6) + ((r2 * r2 * r2 * r2 * r2) / 120) - ((r2 * r2 * r2 * r2 * r2 * r2 * r2) / 5040)
</code></pre></div></div>
<p>To detect collisions, the script positions a creature that has an invisible box mesh underneath the player that spans the whole length of the boat. Unlike the static boat mesh, this creature mesh is a moveable if something collides with it (through the <a href="https://en.wikipedia.org/wiki/Havok_(software)">Havok physics engine</a> built into the game engine). So, if the player moves the boat forward or backward into terrain the invisible creature will get pushed upward to prevent clipping. Since you can query the Z (up-down) position of an object in the scripting language, the script can detect when the creature is getting pushed up by terrain and trigger a collision:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (BoatStartMove == 0)
...
set CollisionRef1 to player.PlaceAtMe aaCollider, 1, 0, 0
CollisionRef1.SetPos z, -5
CollisionRef1.SetScale 1.8
Set BoatStartMove to 1
endif
... ; later on in the inner loop:
if (CollisionRef1 != 0)
;Check whether collision creature has been pushed upwards
if (CollisionRef1.GetPos z > -5)
Message "The ship has run aground"
if (Reverse == 0)
Set ReverseSpeed to (-10 * BoatSpeed)
Set BoatSpeed to ReverseSpeed
Set BoatTurn to 0
Set Reverse to 1
MyShip.PlaySound3D TRPMineExplode
else
Set BoatSpeed to 0
endif
else
Set Reverse to 0
endif
CollisionRef1.SetPos x, BoatX
CollisionRef1.SetPos y, BoatY
If (CollisionRef1.GetPos z > 10)
CollisionRef1.SetPos z, 10
endif
CollisionRef1.SetAngle z, BoatAngle
endif
</code></pre></div></div>
<p>The script also positions another custom invisible box mesh around the player specifically designed to keep them on the deck of the boat and ensure they don’t fall through the boat into the water (it’s called <code class="language-plaintext highlighter-rouge">AntiGravRef</code> in the script).</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (BoatStartMove == 0)
set AntiGravRef to player.PlaceAtMe aaAntiGrav, 1, 0, 0
AntiGravRef.SetScale 2.0
AntiGravRef.SetRigidBodyMass 100
...
endif
... ; later on in the inner loop:
if (AntiGravRef != 0)
AntiGravRef.SetPos x, PlayerX
AntiGravRef.SetPos y, PlayerY
AntiGravRef.SetPos z, PlayerZ
endif
</code></pre></div></div>
<p>This system isn’t perfect. The mod’s readme mentions that “Collision detection works best when colliding head-on and is at it’s worst when colliding with several objects along the side (like the Titanic and the iceberg). If the collider gets really out of whack, it may be neccessary to rock the ship off the obstacle by selecting commands from the menu repeatedly. Dropping anchor can also help, once the obstacle is cleared. In short, it is best to not run into things.” And, there’s a lot of comments on the mod complaining about the general bugginess of this system. However, I still thought it was impressive what was accomplished within the limitations of what was available in the scripting language.</p>
<p>My initial plan was to simply port Jason1’s mod to Oblivion Remastered. However, I quickly realized that too many things were changed in the game engine to make Jason1’s solution feasible in the remaster.</p>
<h3 id="getting-meshes-to-appear-in-the-game">Getting Meshes to Appear in the Game</h3>
<p>One of the biggest hurdles of the modding the remaster was just getting items you added in a plugin to actually appear in the game. The <a href="https://en.uesp.net/wiki/Oblivion_Mod:Plugins">ESP files</a> created by the Construction Set allowed you to define new objects and their position in the game, but this was only in the old Gamebryo side of the game engine. There was a disconnect between that and the Unreal side of the game engine that prevented these objects from showing up in-game.</p>
<p>Luckily, <a href="https://next.nexusmods.com/profile/Godschildgaming?gameId=7587">Godschildgaming</a> created <a href="https://www.nexusmods.com/oblivionremastered/mods/1272">UE4SS TesSyncMapInjector</a> to solve this problem and create the glue needed between the ESP added objects and Unreal Engine. Godschildgaming created this utility through developing a <a href="https://docs.ue4ss.com/">UE4SS</a> plugin which allowed modifying the state of the Unreal Engine part of the game.</p>
<p>Any Oblivion Remastered mod that wanted to add new objects into the world would just need to add a dependency on UE4SS TesSyncMapInjector and create an INI or JSON config file that told TesSyncMapInjector what Unreal Engine model asset to use for each ESP object (referenced using their <a href="https://en.uesp.net/wiki/Oblivion_Mod:Formid">Formids</a>).</p>
<h3 id="moving-the-boat">Moving the Boat</h3>
<p>In Jason1’s Pilotable Pirate Ship mod, the player starts moving the boat by clicking on hull or wheel of the ship (“activating”) which opens a <a href="https://cs.uesp.net/wiki/MessageBox_Tutorial">MessageBox</a> with options for moving forward, backward, or stopping (“drop anchor”). The first thing I did was try to replicate this with a rowboat model in the game.</p>
<p>There was already a rowboat form in the game that was an activator type (<code class="language-plaintext highlighter-rouge">ACTI</code> form). It’s used in one of the main quests where the player needs to activate a rowboat that an NPC left on the shoreline. So, I duplicated that into my own activator object and hooked up a script to it that would show a similar message box when activated.</p>
<p>To actually move the boat in a script, all I needed was use the <a href="https://cs.uesp.net/wiki/SetPos"><code class="language-plaintext highlighter-rouge">SetPos</code></a> function on a reference to my boat. To move the boat continuously, I put the <code class="language-plaintext highlighter-rouge">SetPos</code> in a <a href="https://cs.uesp.net/wiki/GameMode"><code class="language-plaintext highlighter-rouge">GameMode</code></a> block which runs on every frame and keep adding/subtracting to the current X and Y positions (obtained with <a href="https://cs.uesp.net/wiki/GetPos"><code class="language-plaintext highlighter-rouge">GetPos</code></a>).</p>
<p>I found out later that I was lucky in having attempted to try to move an activator object every frame first. Moving objects like activators, NPCs, creatures, containers, lights, and misc items dropped from inventory every frame works fine in Oblivion Remastered. However, trying to move a static object every frame does not work (for example, the big pirate ship objects in the game, which are not activators). It only works if I <a href="cs.uesp.net/wiki/Disable"><code class="language-plaintext highlighter-rouge">Disable</code></a> and <a href="cs.uesp.net/wiki/Enable"><code class="language-plaintext highlighter-rouge">Enable</code></a> the static reference every other frame before moving it again. Since disabling a reference hides the mesh from the game, this causes the static reference to appear to flicker really badly as it moves. I still haven’t found a solution to this. I think I need to modify the Unreal Engine model asset to add a component that allows it to transform every frame, but the tools to edit the assets are really limited right now and I couldn’t figure it out. Luckily, everything I needed to move in my mod wasn’t one of these problematic static meshes (though, it would be nice to someday add a controllable full pirate ship in addition to a rowboat).</p>
<p>I was expecting to run into the problem Jason1 ran into with players clipping through the boat mesh and falling into the water while the boat was moving, but it turns out Unreal Engine actually handles this better! It had no problems with moving the player and keeping them on the deck while the boat was moving.</p>
<h3 id="turning-the-boat">Turning the Boat</h3>
<p>The other critical part of moving the boat is turning it so the player can dictate <em>where</em> the boat is moving. Jason1’s mod achieved this by locking the boat angle to the player’s view angle. So for example, when the player turned to look right, the boat would follow and turn right.</p>
<p>This is where the trigonometry comes into play. Instead of using the Taylor series that Jason1 used to approximate the sine, cosine, and tangent functions, I opted to use <a href="https://cs.uesp.net/wiki/Trigonometry_Functions#Galsiah_Version">this script made by Galsiah that I found on the CS Wiki</a> which claimed to be faster and more accurate than the Taylor series.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; Script originally by Galsiah.
; See: https://cs.uesp.net/wiki/Trigonometry_Functions#Galsiah_Version
set RYB.ang to (RYB.ang * RYB.degToRad)
set RYB.n to 1
if (RYB.ang > 4.7123)
set RYB.ang to (RYB.ang - 6.2832)
elseif (RYB.ang > 1.5708)
set RYB.ang to (RYB.ang - 3.1416)
set RYB.n to -1
endif
set RYB.t2 to (RYB.ang * RYB.ang)
set RYB.sin to RYB.n*(RYB.ang*(1 - RYB.t2*0.16605 + 0.00761*RYB.t2*RYB.t2))
set RYB.cos to RYB.n*(1 - RYB.t2*0.4967 + 0.03705*RYB.t2*RYB.t2)
set RYB.tan to (RYB.sin/RYB.cos)
</code></pre></div></div>
<p>This method employs multiple techniques like reducing the domain of the angles to <code class="language-plaintext highlighter-rouge">[−π/2,π/2]</code> (plus some wraparound logic) to prevent accuracy issues with large angles, efficient <a href="https://en.wikipedia.org/wiki/Horner%27s_method">Horner’s method-style evaluation</a> to reduce the number of calculations, and carefully chosen magic-number coefficients which were likely derived from <a href="https://en.wikipedia.org/wiki/Minimax_approximation_algorithm">minimax approximation</a> or <a href="https://en.wikipedia.org/wiki/Curve_fitting">curve fitting</a> in order to approximate the trigonometric functions as close as possible while also keeping the calculation fast.</p>
<p>With the ability to calculate sine and cosine, I could now calculate the next X and Y position of the boat given the current speed and the current angle of the boat:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set BoatX to BoatX + (sin * FrameBoatVelocity)
set BoatY to BoatY + (cos * FrameBoatVelocity)
...
BoatRef.SetPos x, BoatX
BoatRef.SetPos y, BoatY
</code></pre></div></div>
<p>To actually change the angle of the boat depending on the player look angle, I developed a similar solution to Jason1’s. Except, instead of locking the boat angle directly to player angle, I kept them independent and instead <em>gradually</em> modified the boat angle towards the player angle every frame. This made the turning feel much more natural and gave the rowboat realistic weight. The rate of turning also slows down as the boat speed slows down.</p>
<p>I also added a dead-zone a few degrees out from either side of the center line of the boat so if the player moves slightly it doesn’t cause the whole boat to move. This made turning the boat much more intentional and avoided the boat weaving too much side to side when the player was just attempting to go forward.</p>
<p>The turn rate also decays. So if the player stops turning the boat by looking directly ahead, the boat will naturally slow turning until it stops turning.</p>
<p>
<video width="100%" controls="">
<source src="/video/blog/rowboat-turning.mp4" type="video/mp4" />
Video of turning the rowboat on the water by looking left and right
</video>
</p>
<h3 id="detecting-collision">Detecting Collision</h3>
<p>Since the meshes are handled by Unreal Engine in Oblivion Remastered, I didn’t think the same method Jason1 used in his original mod would work for the remaster. I also wanted a better collision detection system since it sounded like there was a lot of issues with the method of using an invisible collision plane below the player.</p>
<p>At first, I tried spawning in two objects: one at the bow of the boat and another a short distance in front of the boat. And, then using a script to make the first object fire a projectile spell towards the second object. I could use <a href="https://cs.uesp.net/wiki/OnMagicEffectHit"><code class="language-plaintext highlighter-rouge">OnMagicEffectHit</code></a> on the second object to listen for the spell hitting it. If it didn’t hit within some time limit then I could assume the spell collided with something else (e.g. terrain) which means the boat collided with something. So in effect: literal <a href="https://en.m.wikipedia.org/wiki/Ray_casting">ray casting</a>.</p>
<p>This sort of worked, but a major problem with this approach was that I couldn’t find a way to make the spell casting silent and invisible. It was really annoying to see a constant stream of particles and hear the spell casting sound effects at the front of the boat while it was moving. I tried setting the spell effect visuals to <code class="language-plaintext highlighter-rouge">NONE</code> in the Construction Set but this seemed to cause the spell to not work at all in Oblivion Remastered.</p>
<p>Another issue with that approach was that the second object that receives the spell projectile needs to be a mesh that has collision so that the spell can actually collide with it instead of passing right through. But, I couldn’t find a single object in the game that was both invisible and had collision. The original game had invisible collision boxes that blocked the player from certain areas, but these boxes didn’t seem to have collision in Oblivion Remastered.</p>
<p>While looking for an object that had both collision and was invisible, I tried a rat with a permanent invisibility magic effect power. When I tried this I discovered that the game will always ensure that an actor is placed above objects or terrain to avoid clipping with them if you request to move the actor within an object or below terrain. I realized that I could abuse this behavior alone to detect collision since I could try to place an invisible and immobile actor in front of the boat, wait one frame, and then query the actual Z position of the actor to see where the game actually placed them. If the Z position returned is higher than the position I requested, then I know there is something in front of the boat that should cause a collision with the boat.</p>
<p>It took me a surprisingly long time to find an actor in the game that could be completely invisible and completely silent. The best solution I found was creating a new NPC that has the <a href="https://en.uesp.net/wiki/Oblivion:Vampire_Race">VampireRace</a> assigned to it. It seems like at some point in the game’s development there were plans to make vampires a different race in the game, but that was scrapped so that all races could contract vampirism instead. So, this race is not used in vanilla oblivion, and because of that there are no voice lines assigned to the race. This was important because NPC of other races would occasionally say idle dialogue lines or remark on things happening and break the illusion. I also set the scale of the NPC reference in the Construction Set to as small as the game engine would allow (like ant sized basically). I also used a bunch of other script functions to make the NPC invisible and turn off any AI that would cause it to react to the player or other actors in the area:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RYBColliderRef.SetActorAlpha 0.0
RYBColliderRef.SetActorRefraction 10.0
RYBColliderRef.AddSpell MG14JskarInvis
RYBColliderRef.SetActorValue Aggression 0
RYBColliderRef.SetActorValue Blindness 100
RYBColliderRef.ModActorValue Sneak 0
; RYBColliderRef.SetActorsAI 0 ; causes crash in 1.511.102.0
RYBColliderRef.SetDestroyed 1
</code></pre></div></div>
<p>
<video width="100%" controls="">
<source src="/video/blog/rowboat-collision-autorow.mp4" type="video/mp4" />
Video of the boat moving forward until it collides with a beach
</video>
</p>
<p>It’s still not a 100% perfect solution, since the vampire can make occasional splashing sounds when it hits the water which sounds quite glitchy when it happens every few frames. When I tried a <a href="https://en.uesp.net/wiki/Oblivion:Animals#Slaughterfish">slaughterfish</a>, the splashing sound didn’t happen but I couldn’t figure out how to silence the slaughterfish’s idle sounds which were more annoying.</p>
<p>To handle collisions while turning the boat, the invisible vampire (I call it the “collider” in my script) is spawned in the direction of travel. So, when moving in reverse, the collider is spawned behind the boat instead of in front.</p>
<p>I was concerned that moving an NPC every frame would cause a big performance hit. In practice, I don’t think I noticed any real effect on my machine. But, to be safe, I found the optimal frequency for placing the collider that minimized the how often it had to move while also still detecting collisions fast enough.</p>
<p>So that’s how I detect collision my mod: hanging a tiny invisible, mute, dumb vampire off the front of the boat until it bashes into something 😉.</p>
<h3 id="rowing-the-boat">Rowing the Boat</h3>
<p>While moving the boat automatically through the MessageBox menu was convenient, it was also cumbersome and awkward to interact with. I wanted to create a way to really make the player feel like they are rowing the boat for realism and ✨<em>immersion</em>✨.</p>
<p>Ideally the player would just hop on the boat and press some keybind to start moving forward. But, the built-in scripting language has no way to detect keypresses. OBSE64 may add this ability eventually, and mod developers have since found ways to trigger events on keypress through UE4SS scripts. At the time I made the mod, I decided to go with a spell instead.</p>
<p>In essence, the spell cast keybind would become the “go forward” keybind while the player was on the boat. I just had to create a new spell called “Row”, that when cast would trigger something in my script to tell it to start moving the boat forward. Oblivion was developed with custom-scripted spells in mind, so there are actually quite a few hooks built into the scripting language for magic effects. Specifically the blocktype <a href="https://cs.uesp.net/wiki/ScriptEffectStart">ScriptEffectStart</a> was very useful for this.</p>
<p>To allow the player to easily row backwards, I used the script function <a href="https://cs.uesp.net/wiki/IsSneaking">IsSneaking</a> to change the direction of rowing if the player was sneaking when the spell was cast.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>begin ScriptEffectStart
if (Player.IsSneaking)
set RYB.TriggerRowCast to 2
else
set RYB.TriggerRowCast to 1
endif
end
</code></pre></div></div>
<p>The script assigned to my custom Row spell above sets a variable in my main <a href="https://cs.uesp.net/wiki/Quest_scripts">quest script</a> (attached to the quest named <code class="language-plaintext highlighter-rouge">RYB</code>) to <code class="language-plaintext highlighter-rouge">1</code> or <code class="language-plaintext highlighter-rouge">2</code>. The quest script will detect the variable change and handle it by moving the boat forward or backwards respectively. Basically, this is really primitive function-calling between different scripts in the game.</p>
<p>To make rowing even more realistic, I also added a small <a href="https://en.uesp.net/wiki/Oblivion:Damage_Fatigue">Damage Fatigue</a> on self effect to the spell so rowing slowly drained the player’s fatigue over time (just like how running does in the game). This made it so the player couldn’t row indefinitely unless they used some other spell or potion to bolster their fatigue.</p>
<p>Both the Damage Fatigue effect and the <a href="https://en.uesp.net/wiki/Oblivion:Script_Effect">Script Effect</a> magic effects in the game cause sound and particle effects to play when cast. I found this annoying and distracting. When a spell in Oblivion has multiple magic effects assigned to it, it chooses the effect with the largest magnitude. So I just needed to find another magic effect in the game I could add that didn’t have any sounds or visual effects assigned to it and give it a large magnitude so my Row spell would use the null effects.</p>
<p>I initially went with the <a href="https://en.uesp.net/wiki/Oblivion:Darkness">Darkness</a> effect, which is an unused magic effect in vanilla oblivion that was cut during development (this was the magic effect being used <a href="https://youtu.be/SE55cqIZNp4">in my initial mod release video</a>). However, I found out from bug reports users sent after I initially released the mod that this magic effect has a bug that somehow broke Oblivion Remastered’s <a href="https://en.uesp.net/wiki/Oblivion:Night-Eye">Night Eye</a> effect. I suppose that serves me right for using a magic effect that was labelled in the Construction Set as “DO NOT USE”. Luckily, there was another unused magic effect in the game with no sound and visuals: <a href="https://en.uesp.net/wiki/Oblivion:Lock">Lock</a>. This one also had the bonus that it could be cast as “touch”. Normally that means the effect will only apply to actors the player is touching right in front of them, but for the purposes of my Row spell, this gave the spell the touch spell animation which – if you squinted – sort of looked like the player pushing two oars forward with both arms.</p>
<p>
<video width="100%" controls="">
<source src="/video/blog/row-spell.mp4" type="video/mp4" />
Video of rowing the boat forward by casting the Row spell
</video>
</p>
<h3 id="realistic-movement">Realistic Movement</h3>
<p>When the Row spell is cast by the player while they are on the boat, it doesn’t immediately shoot the boat forward at its maximum speed. Instead, the Row spell cast starts a timer where, for a short time period, it adds a small amount of “force” to the boat’s current velocity every game frame. This is to simulate the effect of oars pushing through the water. After constantly rowing for a while, velocity will accumulate until the boat reaches its maximum velocity.</p>
<p>Since this is an effect that applies every frame, I needed to account for players having different frame rates or variations in the frame rate. It wouldn’t make any sense if the boat was faster in lower graphics settings, or slower if the player entered an area with a lower frame rate. Infamously, Oblivion has this issue with its Havok physics engine. It’s tied to the game’s frame rate which often <a href="https://www.reddit.com/r/oblivion/comments/512ut0/is_fps_tied_to_physics_in_this_game/">causes bugs like objects erratically flying off shelves when the player enters an interior under high frame-rates</a>.</p>
<p>To fix this, all values in my mod applied every frame are smoothed over with a value I call <code class="language-plaintext highlighter-rouge">SmoothedDeltaTime</code> (<a href="https://docs.unity3d.com/ScriptReference/Time-smoothDeltaTime.html">borrowed term from Unity’s similar value</a>).</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set SecondsPassed to GetSecondsPassed
; Clamp extreme values
if (SecondsPassed < 0.001)
set SecondsPassed to 0.001
elseif (SecondsPassed > 0.1)
set SecondsPassed to 0.1
endif
; Exponentially smooth the delta time to adjust for frame rate changes
set SmoothedDeltaTime to ((1.0 - DeltaSmoothingFactor) * SmoothedDeltaTime) + (DeltaSmoothingFactor * SecondsPassed)
</code></pre></div></div>
<p><a href="https://cs.uesp.net/wiki/GetSecondsPassed"><code class="language-plaintext highlighter-rouge">GetSecondsPassed</code></a> returns the amount of time that has passed since the last frame.</p>
<p>This will smooth out frame-rate hitches so that the boat will not unexpectedly jerk forward if the player enters an area where assets are loading in and the frame rate dips low temporarily.</p>
<p>When the player stops rowing, the boat shouldn’t immediately stop moving. Instead the boat velocity gradually “decays” to 0 when no force is being applied. To make the decay more natural, I use exponential decay. However, since the script engine doesn’t have math functions like the <a href="https://en.wikipedia.org/wiki/Natural_logarithm">natural logarithm</a> or exponential functions, I had to approximate it with a constant value that I pre-computed.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; Speed values assume a frame rate of 60fps so readjust speed values to current frame rate
set FrameRowForce to BaseRowForce * (SmoothedDeltaTime / TargetDeltaTime)
if (Rowing == 0 && AutoRowing == 0 && BoatMoving == 2)
; Boat is moving, but not rowing. Decay the velocity using exponential decay.
; velocity = velocity * (1 - decay * time)
if (BaseBoatVelocity != 0)
; Calculate decay factor based on smoothed delta time
; Convert to time-based decay
; retention^(frames) = retention^(time/frameTime)
; We need to calculate r^(SmoothedDeltaTime/TargetDeltaTime)
; Approximation since we can't do pow():
; For small time steps, r^t ≈ 1 + t*ln(r)
; ln(0.98) ≈ -0.0202
set FrameVelocityDecay to 1 + (SmoothedDeltaTime / TargetDeltaTime) * (VelocityDecayLnRetentionFactor)
set BaseBoatVelocity to BaseBoatVelocity * FrameVelocityDecay
; Stop when very slow
if (BaseBoatVelocity > -0.2 && BaseBoatVelocity < 0.2)
set BaseBoatVelocity to 0
set BoatMoving to 0
</code></pre></div></div>
<p>Technically, my quest script doesn’t run every game frame though. The special quest variable <a href="https://cs.uesp.net/wiki/FQuestDelayTime"><code class="language-plaintext highlighter-rouge">fQuestDelayTime</code></a> configures how often the script is run while the game is running. To save CPU resources, I try to keep this to a high value when the player is not near the boat, but once they start moving the boat I ramp it down to a value that would run the script at roughly 60 times per second.</p>
<h3 id="dragging-the-boat">Dragging the Boat</h3>
<p>The largest and most prominent river in the game: the <a href="https://en.uesp.net/wiki/Oblivion:Niben_River">Niben River</a> is actually <a href="https://en.uesp.net/wiki/Oblivion:Niben_River#Lower_Niben">blocked by the city Layawiin</a> in the game, which makes it impossible to row the boat all the way on the river that goes from the Imperial City into the <a href="https://en.uesp.net/wiki/Oblivion:Topal_Bay">Topal Bay</a> at the bottom of the map. I knew I would need to develop an alternative way to move the boat for this reason when I set out to make this mod.</p>
<p>I thought it would be really cool if the player could hop out of the boat and then physically drag it over land. This would allow the player to drag it over the small strip of land blocking the river next to Layawiin and continue on rowing on the other side.</p>
<p>After perfecting the movement of the boat over water, I already had the necessary code to move the boat over land. I would just needed to tweak it to make it feel natural.</p>
<p>At this point in the project, I was heavily using <a href="https://claude.ai/">Claude</a> to help me out with the script. To be honest, as a non-game developer, a lot of the 3D math involved in this project was starting to get a bit over my head. But, Claude was an amazing tool at breaking it down for me in a way I could understand and served as a great super-powered <a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging">rubber duck</a> for debugging issues.</p>
<p>At some point while developing the boat dragging code with Claude, I had the great idea to suggest it create an <a href="https://www.anthropic.com/news/build-artifacts?subjects=announcements">artifact</a> by converting the OBScript code I was working on to the equivalent in JavaScript and display an interactable 2D visualization of the dragging simulation on a HTML canvas. This was <strong>super</strong> helpful in debugging a ton of issues with the dragging code because it tightened the feedback loop between making a change and then testing it out to see if it worked in the visualization. I spent a lot of time waiting to Oblivion Remastered to start up and load saves while working on this mod, so this was huge. I also told Claude to include lots of sliders for all the different variables in the dragging simulation so I could quickly tweak with them within the visualization and get the feel of the dragging really refined without even needing to load up the game.</p>
<p><a href="https://claude.ai/public/artifacts/23380c6b-c9a4-430d-bd86-781ae588739f"><img src="/img/blog/rowyourboat-dragging-visualization.jpg" alt="Screenshot of a Claude artifact web page with the title “Oblivion Boat Dragging Visualization” with a canvas displaying crude shapes representing a boat and rope and a bunch of “Dragging Parameters” slider inputs below" /></a></p>
<p>And, now that I have this artifact, it serves as great documentation for how the dragging code works! <a href="https://claude.ai/public/artifacts/23380c6b-c9a4-430d-bd86-781ae588739f">Try it out for yourself here</a>.</p>
<p>I will certainly be using LLMs to create visualizations of tricky simulations in the future. This is the sort of thing where I think AI could truly help 10x the speed and quality of code projects. To do this in the pre-LLMs days would have taken hours. Enough time that it just wouldn’t have felt worth it. But now that I can have an LLM spit it out in seconds, it would be dumb not to do it and reap the benefits of it.</p>
<p>The dragging code tries to simulate the player dragging the boat as if they were pulling a rope attached to the center of the boat. This allows the player to walk freely around the boat without it moving as long as they don’t make the rope taut by walking more than the rope’s length away from the center of the boat (the white circle in the visualization). Once they do, it will pull the boat with a force relative to how far away the player moved. The boat itself has friction with the ground which moderates this effect, since I wanted the dragging effect to feel slow and less practical than rowing it on water.</p>
<p>The boat also turns to face the bow towards the player. This makes it appear like the player is dragging the boat from its bow. The turning effect works very similarly to how the turning works on water with gradual ramp up and decay when the angle of difference enters the deadzone.</p>
<p>One thing the visualization does not show is how the boat behaves when dragged up or down hills (since it is only a 2D visualization). I wanted the pitch of the boat to change so that when the player drags it up a hill it pitches up to follow the slope of the terrain, and when they drag it downhill it would pitch down. Otherwise, the boat stuck out awkwardly horizontally from the side of hills while you were dragging it. It just looked unrealistic.</p>
<p>Since the boat doesn’t actually have any collision detection with the ground, it was quite a challenge to find a way to adjust the pitch to follow the terrain. I ultimately ended up using the player to detect the slope of the land. Since the player is pulling the boat, the boat follows the player’s path. I can query the player’s Z position to get the height of the terrain. While dragging, every 50 units or so of distance covered, the script records the current terrain height using the player Z position. Then 50 units later, it will compare the current terrain height to the height 50 units prior and use the difference to calculate the slope of the terrain. Using this angle, the boat will gradually pitch in the direction of this angle until it roughly matches the terrain’s slope. To avoid the boat clipping inside the terrain or floating too high off the terrain, the boat’s Z position is also raised or lowered relative to the terrain’s slope.</p>
<p><img src="/img/blog/rowboat-dragging.jpg" alt="Screenshot of dragging the rowboat up a steep hill, the boat is pitched up to match the slope of the terrain" /></p>
<p>The effect isn’t perfect, but it’s surprising how much of a difference it makes. It’s certainly close enough to make a convincing illusion that the boat is being dragged over land.</p>
<p>While dragging the boat, the player gains 200 pounds of encumbrance. This is to make the dragging more realistic since they shouldn’t be able to easily go off fighting monsters while shouldering an entire rowboat around the whole time. But, they could feasibly achieve that if they really wanted to and had the right <a href="https://en.uesp.net/wiki/Oblivion:Feather">Feather</a> spell, potion, or enchanted equipment.</p>
<p>The encumbrance is achieved by adding a special “Rowboat” item to the player’s inventory. The item is scripted so if it is dropped from the player’s inventory then the dragging stops. It also has the same rowboat model assigned to it through UE4SS TesSyncMapInjector so it even looks like a rowboat in the player’s inventory preview.</p>
<h3 id="summoning-the-boat">Summoning the Boat</h3>
<p>One of the first mods I downloaded for Oblivion Remastered was <a href="https://next.nexusmods.com/profile/PushTheWinButton?gameId=7587">PushTheWinButton</a>’s excellent <a href="https://www.nexusmods.com/oblivionremastered/mods/153">Horse Whistle - Summon and Follow</a> . There’s a reason pretty much every game these days that has horse mounts includes some sort of “whistle” mechanic that allows the player to summon their horse to their position immediately. While not exactly realistic, it’s just one of those things that smooths over gameplay so it’s not such a chore just to get playing.</p>
<p>For the rowboat to be a proper player vehicle, I knew I would also need to include some sort of summon ability. I implemented this with the same Row spell that is used to push the rowboat forward and backwards. If the player casts it while far enough away from the rowboat that they couldn’t feasibly be on it, then a MessageBox pops up instead. This message box includes two options: “Summon Boat” and “Place Boat Right Here”.</p>
<p>The difference between these two options is that “Summon Boat” tries to place the boat in somewhere in front of the player that obeys the terrain and objects around the player, whereas “Place Boat Right Here” ignores all of that and just places the boat exactly in front of the player even if it would clip with terrain or objects.</p>
<p>“Place Boat Right Here” was easy to implement since I just needed to use the same sin function I use for boat movement to find a spot in front of where the player is looking and then use <a href="https://cs.uesp.net/wiki/MoveTo"><code class="language-plaintext highlighter-rouge">MoveTo</code></a> and <code class="language-plaintext highlighter-rouge">SetPos</code> to move the boat to that spot.</p>
<p>“Summon Boat” was a bit more complicated since I needed to first scout out a good position by spawning in the same tiny vampire collider that I use for collision detection in front of the player, wait a frame, then query its position to get the final spot the game engine decided doesn’t clip with terrain or objects, then move the collider away and spawn in the boat. This worked in most cases, but there were a few spots I found in the world where the game decides to place the collider somewhere deep underground. So that’s why I kept the “Place Boat Right Here” as a back-up if that ever happens.</p>
<p>I also found that I needed to disable the boat and then re-enable it after moving it, otherwise sometimes the boat would weirdly not have any collision so the player could walk right through it and it would not be activatable.</p>
<h3 id="rocking-the-boat">Rocking the Boat</h3>
<p>Inspired by the classic Oblivion mod <a href="https://www.nexusmods.com/oblivion/mods/29649">QQuix - Rock rock rock your ship</a>, I wanted to add even more realism to the mod by adding a gentle rocking animation to the boat while it is in the water.</p>
<p>I achieved this (with a lot of help from Claude 🤖) by creating a system of combining three separate random sine wave oscillations:</p>
<ul>
<li>Primary wave: 30 degrees/second</li>
<li>Secondary wave: 45 degrees/second</li>
<li>Tertiary wave: 25 degrees/second</li>
</ul>
<p>Combining three random waves instead of a single makes the rocking more complex and unpredictable compared a single wave which would have resulted in monotonous, predictable motion.</p>
<p>The boat pitches by combining the primary and secondary waves:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set TargetRockPitchOffset to RockAmplitudePitch * (rockSin * 0.8 + rockSin2 * 0.2)
</code></pre></div></div>
<p>And rolls side-to-side by using a cosine function to create a 90-degree phase offset off the secondary and tertiary waves:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set TargetRockRollOffset to RockAmplitudeRoll * (rockCos3 * 0.7 + rockSin2 * 0.3)
</code></pre></div></div>
<p>And bobs up and down slightly by combining the primary and secondary waves:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set TargetRockZOffset to RockAmplitudeZ * (rockSin * 0.7 + rockSin2 * 0.3 + RockRandomPhase)
</code></pre></div></div>
<p>This all combines to create a fairly convincing boat rocking motion in the water:</p>
<p>
<video width="100%" controls="">
<source src="/video/blog/rocking-rowboat.mp4" type="video/mp4" />
Video of rowboat gently rocking in the water
</video>
</p>
<p>To increase the realism, the rocking motion gets amplified by how fast the boat is moving:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; Increase rocking amplitude based on speed
set TargetRockZOffset to TargetRockZOffset * (1 + (AbsoluteBoatVelocity / BoatMaxVelocity) * RockSpeedFactor)
set TargetRockPitchOffset to TargetRockPitchOffset * (1 + (AbsoluteBoatVelocity / BoatMaxVelocity) * RockSpeedFactor * 1.5)
set TargetRockRollOffset to TargetRockRollOffset * (1 + (AbsoluteBoatVelocity / BoatMaxVelocity) * RockSpeedFactor * 0.7)
</code></pre></div></div>
<p>And the rocking motion also gets amplified by bad weather by querying the wind speed with <a href="https://cs.uesp.net/wiki/GetWindSpeed"><code class="language-plaintext highlighter-rouge">GetWindSpeed</code></a> and adjusting the motion accordingly:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; Apply weather factor (based on wind speed)
set WindSpeed to GetWindSpeed
set TargetRockZOffset to TargetRockZOffset * (1 + (WindSpeed * RockWeatherFactor))
; Limit extreme Z offsets that would mess with boat-in-water detection
if (BoatZ + TargetRockZOffset < -RockMaxAbsoluteZ)
set TargetRockZOffset to -RockMaxAbsoluteZ - BoatZ
elseif (BoatZ + TargetRockZOffset > RockMaxAbsoluteZ)
set TargetRockZOffset to RockMaxAbsoluteZ - BoatZ
endif
set TargetRockPitchOffset to TargetRockPitchOffset * (1 + (WindSpeed * RockWeatherFactor))
set TargetRockRollOffset to TargetRockRollOffset * (1 + (WindSpeed * RockWeatherFactor))
</code></pre></div></div>
<p>To make the motion even more responsive, I integrated the player’s weight into the rocking motion.</p>
<p>
<video width="100%" controls="">
<source src="/video/blog/rocking-rowboat-player-weight.mp4" type="video/mp4" />
Video of player running back and forth along the boat and the boat pitching and rolling in response
</video>
</p>
<p>When the player is within a 3D boundary area above the deck of the boat, I translate the player’s position into the boat’s local coordinate system:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; Calculate player position relative to boat center
set PlayerRelativeX to PlayerX - BoatX
set PlayerRelativeY to PlayerY - BoatY
; Calculate boat's forward and right vectors using BoatAngle directly
; Forward vector (bow direction): sin(BoatAngle), cos(BoatAngle)
; Right vector (starboard direction): cos(BoatAngle), -sin(BoatAngle)
; PlayerLocalY: positive = toward bow, negative = toward stern
set PlayerLocalY to PlayerRelativeX * sin + PlayerRelativeY * cos
; PlayerLocalX: positive = toward starboard, negative = toward port
set PlayerLocalX to PlayerRelativeX * cos - PlayerRelativeY * sin
set PlayerLocalZ to PlayerZ - BoatZWithRock
</code></pre></div></div>
<p>Then I calculate a weight effect to apply to the pitch and roll that diminishes with the distance the player is from the center of the boat. I would normally use a square root function to calculate the player’s distance from the center of the boat, but since Oblivion’s scripting language doesn’t include a square root function, I had to use <a href="https://en.wikipedia.org/wiki/Newton%27s_method">Newton’s method</a> to find the approximate square root in two iterations:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; Calculate distance from boat center for falloff effect
set PlayerDistanceFromCenter to PlayerRelativeX * PlayerRelativeX + PlayerRelativeY * PlayerRelativeY
; Newton's method square root approximation (2 iterations)
set PlayerDistanceFromCenter to PlayerWeightMaxDistanceForward ; Initial guess
set PlayerDistanceFromCenter to (PlayerDistanceFromCenter + ((PlayerRelativeX * PlayerRelativeX + PlayerRelativeY * PlayerRelativeY) / PlayerDistanceFromCenter)) / 2
set PlayerDistanceFromCenter to (PlayerDistanceFromCenter + ((PlayerRelativeX * PlayerRelativeX + PlayerRelativeY * PlayerRelativeY) / PlayerDistanceFromCenter)) / 2
</code></pre></div></div>
<p>Using the distance from center I can then calculate an influence factor to apply to the pitch and roll on top of the randomized environmental pitch and roll (by adding these offsets):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; Calculate influence factor (1.0 at center, 0.0 at max distance)
; Limit effect area to a box approximately the area above the boat up 60 units (PlayerWeightMaxDistanceVertical)
if (PlayerDistanceFromCenter <= PlayerWeightMaxDistanceForward && PlayerLocalZ < PlayerWeightMaxDistanceVertical && PlayerLocalX < PlayerWeightMaxDistanceSide && PlayerLocalX > -PlayerWeightMaxDistanceSide)
set PlayerWeightInfluence to 1.0 - (PlayerDistanceFromCenter / PlayerWeightMaxDistanceForward)
else
set PlayerWeightInfluence to 0
endif
; Calculate target pitch offset based on player's fore/aft position
; Positive PlayerLocalY = toward bow (front) = boat should pitch forward (bow dips down)
; Negative PlayerLocalY = toward stern (back) = boat should pitch backward (stern dips down)
set TargetPlayerWeightPitchOffset to PlayerLocalY * PlayerWeightPitchFactor * PlayerWeightInfluence
; Calculate target roll offset based on player's port/starboard position
; Positive PlayerLocalX = toward starboard (right) = boat should roll starboard (startboard dips down)
; Negative PlayerLocalX = toward port (left) = boat should roll port (port dips down)
set TargetPlayerWeightRollOffset to -PlayerLocalX * PlayerWeightRollFactor * PlayerWeightInfluence
; Smooth the transition to prevent jarring movements
set PlayerWeightPitchOffset to PlayerWeightPitchOffset + ((TargetPlayerWeightPitchOffset - PlayerWeightPitchOffset) * PlayerWeightSmoothingFactor * SmoothedDeltaTime / TargetDeltaTime)
set PlayerWeightRollOffset to PlayerWeightRollOffset + ((TargetPlayerWeightRollOffset - PlayerWeightRollOffset) * PlayerWeightSmoothingFactor * SmoothedDeltaTime / TargetDeltaTime)
</code></pre></div></div>
<p>All of the rocking motion stops if the boat collides with land or if the player moves far enough away from the boat to save unnecessary processing. For players that want to minimize the performance impact, I also added a setting in the MessageBox menu to turn off the rocking animation.</p>
<h3 id="boat-upgrades">Boat Upgrades</h3>
<p>The rowboat itself is purchasable from <a href="https://en.uesp.net/wiki/Oblivion:Sergius_Verus">Sergius Verus</a> at the <a href="https://en.uesp.net/wiki/Oblivion:Three_Brothers_Trade_Goods">Three Brothers Trade Goods</a> in the Market District of the Imperial City. This was implemented similarly to how <a href="https://en.uesp.net/wiki/Oblivion:Buy_a_house_in_the_Imperial_City">buying houses in the game works</a>. You purchase a deed document from the trader which has a script attached to it with an <a href="https://cs.uesp.net/wiki/OnAdd"><code class="language-plaintext highlighter-rouge">OnAdd</code></a> block that triggers when it is added to the player’s inventory which then changes the owner of the house to the player and gives them the key. In the case of my rowboat, it just flips a variable in my script which makes the rowboat operable by the player and removes the for-sale sign next to the boat where it is docked in the <a href="https://en.uesp.net/wiki/Oblivion:Waterfront_District">Waterfront District</a>.</p>
<p>I thought it would be fun to also add purchasable upgrades to the rowboat that improve the experience; just like how you can purchase furniture upgrades to your house from traders. The upgrades I came up with for my rowboat are: a storage chest, a lamp, and a rope ladder that auto-deploys if the player falls overboard. In addition to these purchasable upgrades, I also included a free seat on the boat. The seat is just a standard stool in the game that I positioned so that it clips into the lip on the stern of the boat model.</p>
<p>Implementing these “attachments” for the boat ended up being one of the most challenging parts of developing this mod. The game sees them all as separate references. There’s no way to group them together with the rowboat as a single entity for the purposes of moving them in concert. I had to manually calculate and position each of the attachments relative to the boat’s current position.</p>
<p>I spent wayyy too long fiddling with the positioning of these attachments relative to the boat position. After I added the rocking animation to the boat, it got even more complicated since I had to account for the pitch and roll of the boat to determine the position of the attachments. I kept thinking I got it right, but all the attachments kept being <em>slightly</em> off of where they should be. I applied an extreme pitch and roll to the boat, which exaggerated the error while debugging this:</p>
<p>
<div class="row">
<figure>
<img alt="Screenshot of the rowboat pitched and tilted at an extreme angle and the lamp misplaced too far right and down from where it should be" src="/img/blog/rowyourboat-lamp-misplaced.jpg" />
</figure>
<figure>
<img alt="Screenshot of the rowboat pitched and tilted at an extreme angle and the chest and stool misplaced too far left and back from where they should be" src="/img/blog/rowyourboat-chest-seat-misplaced.jpg" />
</figure>
</div>
</p>
<p>Eventually I realized that the order that yaw, roll, and pitch were applied mattered. And confusingly, the order that these should be applied can vary between game engines. I had no idea which order Oblivion Remastered used, so I had to apply them in different orders until I finally found the correct order (Oblivion ended up being a ZXY kind of engine). Here’s the final code for calculating the position of the seat at the back of the boat:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; yaw
set RYB.TempX to RYB.SeatSideOffset * RYB.cos + RYB.SeatForwardOffset * RYB.sin
set RYB.TempY to RYB.SeatForwardOffset * RYB.cos - RYB.SeatSideOffset * RYB.sin
set RYB.TempZ to RYB.SeatZOffset
; roll
set RYB.OrigX to RYB.TempX
set RYB.OrigZ to RYB.TempZ
set RYB.TempX to RYB.OrigX * RYB.CosRoll - RYB.OrigZ * RYB.SinRoll
set RYB.TempZ to RYB.OrigX * RYB.SinRoll + RYB.OrigZ * RYB.CosRoll
; pitch
set RYB.OrigY to RYB.TempY
set RYB.OrigZ to RYB.TempZ
set RYB.TempY to RYB.OrigY * RYB.CosPitch + RYB.OrigZ * RYB.SinPitch
set RYB.TempZ to -RYB.OrigY * RYB.SinPitch + RYB.OrigZ * RYB.CosPitch
; to world coords
set RYB.SeatX to RYB.BoatX + RYB.TempX
set RYB.SeatY to RYB.BoatY + RYB.TempY
set RYB.SeatZ to RYB.BoatZ + RYB.TempZ + RYB.RockZOffset
</code></pre></div></div>
<h3 id="mod-release">Mod Release</h3>
<p>The finally released the mod on June 4th, 2025 <a href="https://www.nexusmods.com/oblivionremastered/mods/4273">on Nexus Mods</a> and <a href="https://www.reddit.com/r/oblivionmods/comments/1l3pg6z/row_your_boat_usable_rowboat_mod/">made a post on the r/oblivionmods subreddit</a>. It was fun seeing the response. A lot of people were excited to see a mod of this complexity released. I think they saw it as a sign that Oblivion Remastered was more mod-friendly than the doubters believed, and we would all see more sophisticated mods coming out for Oblivion Remastered soon. <a href="https://www.rockpapershotgun.com/oblivion-remastered-your-own-personal-rideable-rowboat-mod-sailing-around-cyrodiil-as-magical-mariner">Rock Paper Shotgun even featured my mod</a>, which was cool!</p>
<h3 id="the-mysterious-case-of-the-spontaneously-duplicating-rowboats">The Mysterious Case of The Spontaneously Duplicating Rowboats</h3>
<p>After release, I made a few updated versions that fixed various bugs that were reported by the community in the <a href="https://www.nexusmods.com/oblivionremastered/mods/4273?tab=bugs">bug tracker</a>. But, one bug that was <em>really</em> stumping me was the issue where players would report that sometimes their boat would spontaneously duplicate itself rendering both boats broken and unusable.</p>
<p>After a long session of digging through my scripts for any culprit code, I became convinced that this wasn’t an issue with my mod. I was beginning to think I was actually running into a bug of either one of my dependencies (like TesSyncMapInjector) or a bug in the game engine itself.</p>
<p>When I finally found a way to reproduce the issue in-game, I opened the console and selected both copies of the boat. The console reported that they had the same reference form id. Everything I knew about the original Oblivion game engine told me that this should be impossible. It seemed like the Unreal Engine side of the game engine had mistakenly duplicated the boat reference and this violation was breaking all of my scripts which were all built on the assumption that only one in-game object could have the same reference id.</p>
<p>Since this issue seemed to be on the Unreal Engine side of the game engine, I had no choice but to dive into the world of UE4SS lua scripting which would give me access to fiddle around with the internals of the Unreal Engine part of the game.</p>
<p>The <a href="[RowYourBoat/UE4SSScripts/main.lua at 53930b86f011110ec30569f261d51e7bdc8e21e3 · thallada/RowYourBoat · GitHub](https://github.com/thallada/RowYourBoat/blob/53930b86f011110ec30569f261d51e7bdc8e21e3/UE4SSScripts/main.lua)">full UE4SS script I wrote to fix the duplication issue is here</a>. Essentially, I used the <a href="https://docs.ue4ss.com/lua-api/global-functions/notifyonnewobject.html"><code class="language-plaintext highlighter-rouge">NotifyOnNewObject</code></a> to constantly keep track of new objects getting created in Unreal Engine. If any of the items matches the class for my rowboat or any of its attachments, then I immediately delete the duplicate copies. In my testing this seemed to work pretty well, and in most cases the cleanup happened so seamlessly the player would likely not notice it happening. With the duplicates deleted, my OBScript scripts continued to operate correctly.</p>
<p>I never truly found out the root-cause of the duplicating boats. The process I followed to consistently reproduce the bug involved:</p>
<ol>
<li>Move the boat outside the original cell it was placed in (outside the Imperial City Waterfront District).</li>
<li>Leave the boat in the new cell and then move the player to a new different cell far away from the boat.</li>
<li>Come back to the boat and row it back to the original cell it was placed in by the shack in the Waterfront District.</li>
</ol>
<p>So, I suspect it has something to do with Unreal Engine getting Construction Set placed references mixed up with references that have been moved by scripts outside their originally placed cell, and somehow duplicating the reference in the process.</p>
<p>I haven’t gotten any reports from users that the boat duplication bug is still happening after I released a new version with the UE4SS script. I still get the occasional user reporting crashes that happen, but it’s hard to prove what mod in their load order is really causing the crash, and many users report my mod because they see the log messages my script writes in their UE4SS logs. Personally, I didn’t experience any crashes with a bare-bones load order with just my mod and its dependencies installed.</p>
<h3 id="future-work">Future Work</h3>
<p>Unless I get infatuated with Oblivion modding again, I don’t think I’ll be adding anything more to the mod anytime soon. But, if I were to, I think there’s a lot more I could add to improve the mod:</p>
<ul>
<li>Add a controllable pirate ship with explorable interiors</li>
<li>Add a sailing mechanic like <a href="https://valheim.fandom.com/wiki/Boats#Sailing_with_the_wind">Valheim’s sailing</a> where you have to constantly adjust the sails to favor the current winds</li>
<li>Allow hiring ship crew to help you out on the deck</li>
<li>Add durability to the boats so crashing full-speed into a rock has consequences
<ul>
<li>Allow players to spend resources to repair the boat or pay someone to repair it for them</li>
</ul>
</li>
<li>Add animated oars that move through the water as the player rows</li>
<li>Add rowing sounds that play when the player is rowing</li>
<li>Add proper follower support by giving them a place to sit or stand on the boat while it’s moving</li>
<li>Boat crafting</li>
<li>Fix the boat floating above water in some interior/city worldspace cells
<ul>
<li>This would require a way to query the water height of the current cell. Hopefully OBSE64 eventually adds this 🤞.</li>
</ul>
</li>
<li>(probably a different mod) add a fishing mechanic so you can fish off the boat</li>
<li>(probably a different mod) add a cart mod
<ul>
<li>The dragging mechanic I added for this mod I think could be applied to carts on land. E.g. attaching a cart to your horse to get extra carrying capacity</li>
</ul>
</li>
</ul>
Sun, 24 Aug 2025 00:00:00 +0000
https://www.hallada.net/2025/08/24/row-your-boat.html
https://www.hallada.net/2025/08/24/row-your-boat.htmlModmapper: Putting every Skyrim mod on a map with Rust<p><a href="https://modmapper.com">Modmapper</a> is a website that I made that puts every mod
for the game <a href="https://en.wikipedia.org/wiki/The_Elder_Scrolls_V:_Skyrim">Elder Scrolls V:
Skyrim</a> uploaded to
<a href="https://www.nexusmods.com/">Nexus Mods</a> on an interactive map.</p>
<p><a href="https://modmapper.com" target="_blank">
<img src="/img/blog/modmapper.jpg" alt="Screenshot of modmapper.com" />
</a></p>
<p>You can view the map at <a href="https://modmapper.com">https://modmapper.com</a>.</p>
<p>Released in 2011, Skyrim is over a decade old now. But, its vast modding
community has kept it alive and relevant to this day. <a href="https://steamcharts.com/top/p.2">Skyrim is still in the
top 50 games being played on Steam in 2022</a> and
I think it’s no coincidence that <a href="https://www.nexusmods.com/games?">it’s also one of the most modded games
ever</a>.</p>
<!--excerpt-->
<p>The enormous and enduring modding community around the Elder Scrolls games is
why I have a special fondness for the series. I was 13 when I first got
interested in programming through <a href="https://www.nexusmods.com/users/512579?tab=user+files&BH=2">making mods for Elder Scrolls IV:
Oblivion</a>. I quickly
realized I got way more satisfaction out of modding the game than actually
playing it. I was addicted to being able to create whatever my mind imagined in
my favorite game.</p>
<p>I was working on mod for Skyrim earlier in the year<sup id="fnref:bazaarrealm" role="doc-noteref"><a href="#fn:bazaarrealm" class="footnote" rel="footnote">1</a></sup> and was
looking for the best places to put new buildings in the game world. I really
wanted areas of the game world off the beaten (heavily-modded) path. After over
a decade of modifications, there could be conflicts with hundreds of mods in any
area I chose which could cause issues like multiple buildings overlapping or
terrain changes causing floating rocks and trees.</p>
<p>
<div class="row">
<figure>
<img alt="Example of a conflict between two mods that both chose the
same spot to put a lamp post and sign post so they are clipping" src="/img/blog/modmapper-clipping-example2.jpg" />
<figurecaption>
<em>
Example of a conflict between two mods that both chose the same
spot to put a lamp post and sign post so they are clipping.
Screenshot by <a href="https://www.nexusmods.com/users/63732336">
AndreySG</a>.
</em>
</figurecaption>
</figure>
<figure>
<img alt="Example of a conflict between two mods that both chose the
same spot to put a building and rock so they are clipping" src="/img/blog/modmapper-clipping-example1.jpg" />
<figurecaption>
<em>
Conflict between a building and rock. Screenshot by <a href="https://www.reddit.com/user/LewdManoSaurus">
LewdManoSaurus</a>.
</em>
</figurecaption>
</figure>
<figure>
<img alt="Example of a conflict between two mods that both chose the
same spot to put a building and tree so they are clipping" src="/img/blog/modmapper-clipping-example3.jpg" />
<figurecaption>
<em>
Conflict between a building and a tree. Screenshot by <a href="https://www.nexusmods.com/skyrimspecialedition/users/51448566">
Janquel</a>.
</em>
</figurecaption>
</figure>
<figure>
<img alt="Example of a conflict between two mods that both chose the
same spot to put a woodcutting mill" src="/img/blog/modmapper-clipping-example4.jpg" />
<figurecaption>
<em>
Conflict between two woodcutting mills. Screenshot by <a href="https://www.nexusmods.com/skyrimspecialedition/users/51448566">
Janquel</a>.
</em>
</figurecaption>
</figure>
</div>
</p>
<p>Mod authors usually use a tool like
<a href="https://www.nexusmods.com/skyrim/mods/25859">TES5Edit</a> to analyze a group of
mod plugins to find conflicts and create patches to resolve them on a
case-by-case basis. But, I was unsatisfied with that. I wanted to be assured
that there would be no conflicts, or at least know the set of all possible mods
out there that could conflict so I could manually patch those few mods. There
was no good solution for finding conflicts across all mods though. Mod authors
would need to download every Skyrim mod ever and no one has time to download all
85,000+ Skyrim mods, and no one has the computer memory to load all of those in
TES5Edit at the same time.</p>
<p>Through that frustration, Modmapper was born with the mission to create a
database of all Skyrim mod exterior cell edits. With that database I can power
the website which visualizes how popular cells are in aggregate as well as allow
the user to drill down to individual cells, mods, or plugins to find potential
conflicts without ever having to download files themselves.</p>
<p>When I <a href="https://www.reddit.com/r/skyrimmods/comments/sr8k4d/modmapper_over_14_million_cell_edits_from_every/">released the website about 7 months
ago</a>
it made a big splash in the Skyrim modding community. No one had ever visualized
mods on a map like this before, and it gave everyone a new perspective on the
vast library of Skyrim mods. It was even <a href="https://www.pcgamer.com/skyrim-modmapper-is-a-weirdly-beautiful-way-to-manage-your-mods/">featured on the front page of PC
Gamer’s
website</a>.
Thirteen-year-old me, who regularly read the monthly PC Gamer magazine, would
have been astounded.</p>
<p><a href="https://www.pcgamer.com/skyrim-modmapper-is-a-weirdly-beautiful-way-to-manage-your-mods/" target="_blank">
<img src="/img/blog/modmapper-pcgamer.jpg" alt="Screenshot of PC Gamer article titled "Skyrim Modmapper is a weirdly
beautiful way to manage your mods" by Robert Zak published April 20,
2022" />
</a></p>
<p>The comments posted to the initial mod I posted on Nexus Mods<sup id="fnref:takedown" role="doc-noteref"><a href="#fn:takedown" class="footnote" rel="footnote">2</a></sup> for the
project were very amusing. It seemed to be blowing their minds:</p>
<blockquote>
<p>“Quite possibly this could be the best mod for
Skyrim. This hands-down makes everyone’s life easier to be able to see which of
their mods might be conflicting.” – <a href="/img/blog/modmapper-comment15.png">Nexus Mods comment by
lorddonk</a></p>
</blockquote>
<blockquote>
<p>“The 8th wonder of Skyrim. That’s a Titan’s work requiring a monk’s
perserverance. Finally, a place to go check (in)compatibilities !!! Voted.
Endorsed.” – <a href="/img/blog/modmapper-comment3.png">Nexus Mods comment by
jfjb2005</a></p>
</blockquote>
<blockquote>
<p>“They shall sing songs of your greatness! Wow, just wow.” – <a href="/img/blog/modmapper-comment7.png">Nexus Mods
comment by
LumenMystic</a></p>
</blockquote>
<blockquote>
<p>“Holy Batman Tits! Be honest….. You’re a Govt Agent and made this mod during
your “Terrorist Watch Shift” using a CIA super computer..” – <a href="/img/blog/modmapper-comment1.png">Nexus Mods
comment by toddrizzle</a></p>
</blockquote>
<blockquote>
<p>“What drugs are you on and can I have some?” – <a href="/img/blog/modmapper-comment11.png">Nexus Mods comment by
thappysnek</a></p>
</blockquote>
<blockquote>
<p>“This is madness! Author are some kind of overhuman?! GREAT work!”– <a href="/img/blog/modmapper-comment10.png">Nexus
Mods comment by TeodorWild</a></p>
</blockquote>
<blockquote>
<p>“You are an absolute legend. Bards will sing tales of your exploits” – <a href="/img/blog/modmapper-comment2.png">Nexus
Mods comment by burntwater</a></p>
</blockquote>
<blockquote>
<p>“I wanted to say something, but I’ll just kneel before thee and worship. This
would have taken me a lifetime. Amazing.” – <a href="/img/blog/modmapper-comment8.png">Nexus Mods comment by
BlueGunk</a></p>
</blockquote>
<blockquote>
<p>“Finally found the real dragonborn” – <a href="/img/blog/modmapper-comment6.png">Nexus Mods comment by
yag1z</a></p>
</blockquote>
<blockquote>
<p>“he is the messiah!” – <a href="/img/blog/modmapper-comment12.png">Nexus Mods comment by
Cursedobjects</a></p>
</blockquote>
<blockquote>
<p>“A god amongst men.” – <a href="/img/blog/modmapper-comment13.png">Nexus Mods comment by
TheMotherRobbit</a></p>
</blockquote>
<p>Apparently knowing how to program is now a god-like ability! This is the type of
feedback most programmers aspire to get from their users. I knew the tool was
neat and fun to build, but I didn’t realize it was <em>that</em> sorely needed by the
community.</p>
<p>Today, Modmapper has a sustained user-base of around 7.5k unique visitors a
month<sup id="fnref:analytics" role="doc-noteref"><a href="#fn:analytics" class="footnote" rel="footnote">3</a></sup> and I still see it mentioned in reddit threads or discord
servers whenever someone is asking about the places a mod edits or what mods
might be conflicting in a particular cell.</p>
<p>The rest of this blog post will delve into how I built the website and how I
gathered all of the data necessary to display the visualization.</p>
<h3 id="downloading-all-the-mods">Downloading ALL THE MODS!</h3>
<p><img src="/img/blog/allthemods.jpg" alt="Meme with the title "DOWNLOAD ALL THE MODS!"" /></p>
<p>In order for the project to work I needed to collect all the Skyrim mod plugin
files.</p>
<p>While there are a number of places people upload Skyrim mods, <a href="https://nexusmods.com">Nexus
Mods</a> is conveniently the most popular and has the vast
majority of mods. So, I would only need to deal with this one source. Luckily,
<a href="https://app.swaggerhub.com/apis-docs/NexusMods/nexus-mods_public_api_params_in_form_data/1.0">they have a nice API
handy</a>.</p>
<p><a href="https://github.com/thallada/modmapper">modmapper</a> is the project I created to
do this. It is a Rust binary that:</p>
<ul>
<li>Uses <a href="https://crates.io/crates/reqwest">reqwest</a> to make requests to <a href="https://nexusmods.com">Nexus
Mods</a> for pages of last updated mods.</li>
<li>Uses <a href="https://crates.io/crates/scraper">scraper</a> to scrape the HTML for
individual mod metadata (since the Nexus API doesn’t provide an endpoint to
list mods).</li>
<li>Makes requests to the Nexus Mods API to get file and download information for
each mod, using <a href="https://serde.rs/">serde</a> to parse the
<a href="https://en.wikipedia.org/wiki/JSON">JSON</a> responses.</li>
<li>Requests the content preview data for each file and walks through the list of
files in the archive looking for a Skyrim plugin file (<code class="language-plaintext highlighter-rouge">.esp</code>, <code class="language-plaintext highlighter-rouge">.esm</code>, or
<code class="language-plaintext highlighter-rouge">.esl</code>).</li>
<li>If it finds a plugin, it decides to download the mod. It hits the download API
to get a download link and downloads the mod file archive.</li>
<li>Then it extracts the archive using one of:
<a href="https://crates.io/crates/compress-tools">compress_tools</a>,
<a href="https://crates.io/crates/unrar">unrar</a>, or <a href="https://www.7-zip.org/">7zip</a> via
<a href="https://doc.rust-lang.org/std/process/struct.Command.html"><code class="language-plaintext highlighter-rouge">std::process::Commmand</code></a>
(depending on what type of archive it is).</li>
<li>With the ESP files (Elder Scrolls Plugin files) extracted, I then use my
<a href="https://github.com/thallada/skyrim-cell-dump">skyrim-cell-dump</a> library (more
on that later!) to extract all of the cell edits into structured data.</li>
<li>Uses <a href="https://crates.io/crates/seahash">seahash</a> to create a fast unique hash
for plugin files.</li>
<li>It then saves all of this data to a <a href="https://www.postgresql.org/">postgres</a>
database using the <a href="https://crates.io/crates/sqlx">sqlx crate</a>.</li>
<li>Uses extensive logging with the <a href="https://crates.io/crates/tracing">tracing
crate</a> so I can monitor the output and have
a history of a run to debug later if I discover an issue.</li>
</ul>
<p>It is designed to be run as a nightly <a href="https://en.wikipedia.org/wiki/Cron">cron</a>
job which downloads mods that have updated on Nexus Mods since the last run.</p>
<p>To keep costs for this project low, I decided to make the website entirely
static. So, instead of creating an API server that would have to be constantly
running to serve requests from the website by making queries directly to the
database, I would dump all of the data that the website needed from the database
to JSON files, then upload those files to <a href="https://aws.amazon.com/s3/">Amazon
S3</a> and serve them through the <a href="https://www.cloudflare.com/cdn/">Cloudflare
CDN</a> which has servers all over the world.</p>
<p>So, for example, every mod in the database has a JSON file uploaded to
<code class="language-plaintext highlighter-rouge">https://mods.modmapper.com/skyrimspecialedition/<nexus_mod_id>.json</code> and the
website frontend will fetch that file when a user clicks a link to that mod in
the UI.</p>
<p>The cost for S3 is pretty reasonable to me ($~3.5/month), and Cloudflare has a
<a href="https://www.cloudflare.com/plans/#price-matrix">generous free tier</a> that allows
me to host everything through it for free.</p>
<p>The server that I actually run <code class="language-plaintext highlighter-rouge">modmapper</code> on to download all of the mods is a
server I already have at home that I also use for other purposes. The output of
each run is uploaded to S3, and I also make a full backup of the database and
plugin files to <a href="https://www.dropbox.com">Dropbox</a>.</p>
<p>A lot of people thought it was insane that I downloaded every mod<sup id="fnref:adult-mods" role="doc-noteref"><a href="#fn:adult-mods" class="footnote" rel="footnote">4</a></sup>,
but in reality it wasn’t too bad once I got all the issues resolved in
<code class="language-plaintext highlighter-rouge">modmapper</code>. I just let it run in the background all day and it would chug
through the list of mods one-by-one. Most of the time ended up being spent
waiting while the Nexus Mod’s API hourly rate limit was reached on my
account.<sup id="fnref:rate-limit" role="doc-noteref"><a href="#fn:rate-limit" class="footnote" rel="footnote">5</a></sup></p>
<p>As a result of this project I believe I now have the most complete set of all
Skyrim plugins to date (extracted plugins only without other textures, models,
etc.)<sup id="fnref:plugin-collection" role="doc-noteref"><a href="#fn:plugin-collection" class="footnote" rel="footnote">6</a></sup>. Compressed, it totals around 99 GB, uncompressed: 191
GB.</p>
<p><a href="#finishing-the-collection-by-adding-all-skyrim-classic-mods">After I downloaded Skyrim Classic mods in addition to Skyrim Special
Edition</a>, here are
some counts from the database:</p>
<table>
<tbody>
<tr>
<td><strong>Mods</strong></td>
<td>113,028</td>
</tr>
<tr>
<td><strong>Files</strong></td>
<td>330,487</td>
</tr>
<tr>
<td><strong>Plugins</strong></td>
<td>534,831</td>
</tr>
<tr>
<td><strong>Plugin Cell Edits</strong></td>
<td>33,464,556</td>
</tr>
</tbody>
</table>
<h3 id="parsing-skyrim-plugin-files">Parsing Skyrim plugin files</h3>
<p>The Skyrim game engine has a concept of
<a href="https://en.uesp.net/wiki/Skyrim:Worldspaces">worldspaces</a> which are exterior
areas where the player can travel to. The biggest of these being, of course,
Skyrim itself (which, in the lore, is a province of the continent of
<a href="https://en.uesp.net/wiki/Lore:Tamriel">Tamriel</a> on the planet
<a href="https://en.uesp.net/wiki/Lore:Nirn">Nirn</a>). Worldspaces are recorded in a
plugin file as <a href="https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/WRLD">WRLD
records</a>.</p>
<p>Worldspaces are then chunked up into a square grid of cells. The Skyrim
worldspace consists of a little over 11,000 square cells. Mods that make a
changes to the game world have a record in the plugin (a <a href="https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/CELL">CELL
record</a>) with the
cell’s X and Y coordinates and a list changes in that cell.</p>
<p>There is some prior art (<a href="https://github.com/Ortham/esplugin">esplugin</a>,
<a href="https://github.com/TES5Edit/TES5Edit">TES5Edit</a>,
<a href="https://github.com/z-edit/zedit">zedit</a>) of open-source programs that could
parse Skyrim plugins and extract this data. However, all of these were too broad
for my purpose or relied on the assumption of being run in the context of a load
order where the master files of a plugin would also be available. I wanted a
program that could take a single plugin in isolation and skip through all of the
non-relevant parts of it and dump just the CELL and WRLD record data plus some
metadata about the plugin from the header as fast as possible.</p>
<p>After discovering <a href="https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format">the wonderful documentation on the UESP wiki about the Skyrim
mod file format</a>, I
realized this would be something that would be possible to make myself.
<a href="https://github.com/thallada/skyrim-cell-dump">skyrim-cell-dump</a> is a Rust
library/CLI program that accepts a Skyrim mod file and spits out the header
metadata of the plugin, the worlds edited/created, and all of the cells it
edits/creates.</p>
<p>Under the hood, it uses the <a href="https://crates.io/crates/nom">nom crate</a> to read
through the plugin until it finds the relevant records, then uses
<a href="https://crates.io/crates/flate2">flate2</a> to decompress any compressed record
data, and finally outputs the extracted data formatted to JSON with
<a href="https://crates.io/crates/serde">serde</a>.</p>
<p>Overall, I was pretty happy with this toolkit of tools and was able to quickly
get the data I needed from plugins. My only gripe was that I never quite figured
out how to properly do error handling with nom. If there was ever an error, I
didn’t get much data in the error about what failed besides what function it
failed in. I often had to resort to peppering in a dozen <code class="language-plaintext highlighter-rouge">dbg!()</code> statements to
figure out what went wrong.</p>
<p>I built it as both a library and binary crate so that I could import it in other
libraries and get the extracted data directly as Rust structs without needing to
go through JSON. I’ll go more into why this was useful later.</p>
<h3 id="building-the-website">Building the website</h3>
<p>Since I wanted to keep server costs low and wanted the site to be as fast as
possible for users, I decided pretty early on that the site would be purely
static HTML and JavaScript with no backend server. I decided to use the <a href="https://nextjs.org/">Next.js
web framework</a> with
<a href="https://www.typescriptlang.org/">TypeScript</a> since it was what I was familiar
with using in my day job. While it does have <a href="https://nextjs.org/docs/basic-features/pages#server-side-rendering">server-side rendering
support</a>
which would require running a backend <a href="https://nodejs.org/en/">Node.js</a> server,
it also supports a limited feature-set that can be <a href="https://nextjs.org/docs/advanced-features/static-html-export">exported as static
HTML</a>.</p>
<p>I host the site on <a href="https://pages.cloudflare.com/">Cloudflare pages</a> which is
available on their free tier and made deploying from Github commits a
breeze<sup id="fnref:cloudflare" role="doc-noteref"><a href="#fn:cloudflare" class="footnote" rel="footnote">7</a></sup>. The web code is in my <a href="https://github.com/thallada/modmapper-web">modmapper-web
repo</a>.</p>
<p>The most prominent feature of the website is the interactive satellite map of
Skyrim. Two essential resources made this map possible: <a href="https://srmap.uesp.net/">the map tile images
from the UESP skyrim map</a> and
<a href="https://www.mapbox.com/">Mapbox</a>.</p>
<p><a href="https://docs.mapbox.com/mapbox-gl-js/api/">Mapbox provides a JS library for its WebGL
map</a> which allows specifying a
<a href="https://docs.mapbox.com/mapbox-gl-js/example/map-tiles/">raster tile
source</a><sup id="fnref:3d-terrain" role="doc-noteref"><a href="#fn:3d-terrain" class="footnote" rel="footnote">8</a></sup>.</p>
<p>The <a href="https://en.uesp.net/wiki/UESPWiki:Skyrim_Map_Design">UESP team painstakingly loaded every cell in the Skyrim worldspace in the
Creation Kit and took a
screenshot</a>. Once I figured
out which image tiles mapped to which in-game cell it was relatively easy to put
a map together by plugging them into the Mapbox map as a raster tile source.</p>
<p>The heatmap overlaid on the map is created using a <a href="https://docs.mapbox.com/help/glossary/layer/">Mapbox
layer</a> that fills a cell with a
color on a gradient from green to red depending on how many edits that cell has
across the whole database of mods.</p>
<p><img src="/img/blog/modmapper-heatmap-closeup.jpg" alt="Screenshot closeup of modmapper.com displaying a grid of colored cells from
green to red overlaid atop a satellite map of
Skyrim" /></p>
<p>The sidebar on the site is created using <a href="https://reactjs.org/">React</a> and
<a href="https://redux.js.org/">Redux</a> and uses the
<a href="https://nextjs.org/docs/api-reference/next/router">next/router</a> to keep track
of which page the user is on with URL parameters.</p>
<p>
<div class="row">
<img alt="Screenshot of modmapper.com sidebar with a cell selected" src="/img/blog/modmapper-cell-sidebar.jpg" class="half-left" />
<img alt="Screenshot of modmapper.com sidebar with a mod selected" src="/img/blog/modmapper-mod-sidebar.jpg" class="half-right" />
</div>
</p>
<p>The mod search is implemented using
<a href="https://lucaong.github.io/minisearch/">MiniSearch</a> that asynchronously loads
the giant search indices for each game containing every mod name and id.</p>
<p><img src="/img/blog/modmapper-search.jpg" alt="Screenshot of modmapper.com with "trees" entered into the search bar with a
number of LE and SE mod results listed underneath in a
dropdown" /></p>
<p>One of the newest features of the site allows users to drill down to a
particular plugin within a file of a mod and “Add” it to their list. All of the
added plugins will be listed in the sidebar and the cells they edit displayed in
purple outlines and conflicts between them displayed in red outlines.</p>
<p><img src="/img/blog/modmapper-added-plugins.jpg" alt="Screenshot of modmapper.com with 4 Added Plugins and the map covered in purple
and red boxes" /></p>
<h3 id="loading-plugins-client-side-with-webassembly">Loading plugins client-side with WebAssembly</h3>
<p>A feature that many users requested after the initial release was being able to
load a list of the mods currently installed on their game and see which ones of
that set conflict with each other<sup id="fnref:second-announcement" role="doc-noteref"><a href="#fn:second-announcement" class="footnote" rel="footnote">9</a></sup>. Implementing this
feature was one of the most interesting parts of the project. Choosing to use
Rust made made it possible, since everything I was running server-side to
extract the plugin data could also be done client-side in the browser with the
same Rust code compiled to <a href="https://webassembly.org/">WebAssembly</a>.</p>
<p>I used <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a> to create
<a href="https://github.com/thallada/skyrim-cell-dump-wasm/">skyrim-cell-dump-wasm</a>
which exported the <code class="language-plaintext highlighter-rouge">parse_plugin</code> function from my
<a href="https://github.com/thallada/skyrim-cell-dump">skyrim-cell-dump</a> Rust library
compiled to WebAssembly. It also exports a <code class="language-plaintext highlighter-rouge">hash_plugin</code> function that creates a
unique hash for a plugin file’s slice of bytes using
<a href="https://crates.io/crates/seahash">seahash</a> so the site can link plugins a user
has downloaded on their hard-drive to plugins that have been downloaded by
modmapper and saved in the database.</p>
<p>Dragging-and-dropping the Skyrim Data folder on to the webpage or selecting the
folder in the “Open Skyrim Data directory” dialog kicks off a process that
starts parsing all of the plugin files in that directory in parallel using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">Web
Workers</a>.</p>
<p>I developed my own
<a href="https://github.com/thallada/modmapper-web/blob/4af628559030c3f24618b29b46d4a40af2f200a6/lib/WorkerPool.ts"><code class="language-plaintext highlighter-rouge">WorkerPool</code></a>
that manages creating a pool of available workers and assigns them to plugins to
process. The pool size is the number of cores on the user’s device so that the
site can process as many plugins in parallel as possible. After a plugin
finishes processing a plugin and sends the output to the redux store, it gets
added back to the pool and is then assigned a new plugin to process if there are
any<sup id="fnref:wasm-troubles" role="doc-noteref"><a href="#fn:wasm-troubles" class="footnote" rel="footnote">10</a></sup>.</p>
<p>Once all plugins have been loaded, the map updates by displaying all of the
cells edited in a purple box and any cells that are edited by more than one
plugin in a red box.</p>
<p><img src="/img/blog/modmapper-loaded-plugins.jpg" alt="Screenshot of modmapper.com with 74 Loaded Plugins and the map filled with
purple and red boxes" /></p>
<p>Users can also drag-and-drop or paste their <code class="language-plaintext highlighter-rouge">plugins.txt</code> file, which is the
file that the game uses to define the load order of plugins and which plugins
are enabled or disabled. Adding the <code class="language-plaintext highlighter-rouge">plugins.txt</code> sorts the list of loaded
plugins in the sidebar in load order and enables or disables plugins as defined
in the <code class="language-plaintext highlighter-rouge">plugins.txt</code>.</p>
<p><img src="/img/blog/modmapper-pluginstxt-dialog.jpg" alt="Screenshot of modmapper.com with the Paste plugins.txt dialog
open" /></p>
<p>Selecting a cell in the map will display all of the loaded cells that edit that
cell in the sidebar.</p>
<p><img src="/img/blog/modmapper-conflicted-cell.jpg" alt="Screenshot of modmapper.com with a conflicted cell selected on the map and 4
Loaded Plugins displayed" /></p>
<p>The ability to load plugins straight from a user’s hard-drive allows users to
map mods that haven’t even been uploaded to Nexus Mods.</p>
<h3 id="vortex-integration">Vortex integration</h3>
<p>The initial mod I released on the Skyrim Special Edition page of Nexus Mods was
<a href="https://www.reddit.com/r/skyrimmods/comments/svnz4a/modmapper_got_removed/">taken
down</a>
by the site admins since it didn’t contain an actual mod and they didn’t agree
that it qualified as a “Utility”.</p>
<p>Determined to have an actual mod page for Modmapper on Nexus Mods, I decided to
make a <a href="https://www.nexusmods.com/about/vortex/">Vortex</a> integration for
modmapper. Vortex is a mod manager made by the developers of Nexus Mods and they
allow creating extensions to the tool and have their own <a href="https://www.nexusmods.com/site">mod section for Vortex
extensions</a>.</p>
<p>With the help of <a href="https://www.nexusmods.com/skyrim/users/31179975">Pickysaurus</a>,
one of the community managers for Nexus Mods, I created a <a href="https://www.nexusmods.com/site/mods/371">Vortex integration
for Modmapper</a>. It adds a context menu
option on mods to view the mod in Modmapper with all of the cells it edits
selected in purple. It also adds a button next to every plugin file to view just
that plugin in Modmapper (assuming it has been processed by Modmapper).</p>
<p>
<div class="row">
<img alt="Screenshot of Vortex mod list with a mod context menu open which
shows a 'See on Modmapper' option" src="/img/blog/modmapper-vortex-mod-menu.jpg" class="half-left" />
<img alt="Screenshot of Vortex plugin list with 'See on Modmapper' buttons
on the right of each plugin row" src="/img/blog/modmapper-vortex-plugin-button.jpg" class="half-right" />
</div>
</p>
<p>To enable the latter part, I had to include <code class="language-plaintext highlighter-rouge">skyrim-cell-dump-wasm</code> in the
extension so that I could hash the plugin contents with <code class="language-plaintext highlighter-rouge">seahash</code> to get the
same hash that Modmapper would have generated. It only does this hashing when
you click the “See on Modmapper” button to save from excessive CPU usage when
viewing the plugin list.</p>
<p>After releasing the Vortex plugin, Pickysaurus <a href="https://www.nexusmods.com/skyrimspecialedition/news/14678">published a news article about
modmapper to the Skyrim Special Edition
site</a> which also got
a lot of nice comments ❤️.</p>
<h3 id="finishing-the-collection-by-adding-all-skyrim-classic-mods">Finishing the collection by adding all Skyrim Classic mods</h3>
<p>Skyrim is very silly in that it has <a href="https://ag.hyperxgaming.com/article/12043/every-skyrim-edition-released-over-the-last-decade">many
editions</a>.
But there was only one that split the modding universe into two: <a href="https://en.uesp.net/wiki/Skyrim:Special_Edition">Skyrim Special
Edition (SE)</a>.
It was released in October 2016 with a revamped game engine that brought some
sorely needed graphical upgrades. However, it also contained changes to how mods
worked, requiring all mod authors to convert their mods to SE. This created big
chasm in the library of mods, and Nexus Mods had to make a separate section for
SE-only mods.</p>
<p>When I started downloading mods in 2021, I started only with Skyrim SE mods,
which, at the time of writing, totals at over <a href="https://www.nexusmods.com/skyrimspecialedition/mods/">55,000 mods on Nexus
Mods</a>.</p>
<p>After releasing with just SE mods, many users requested that all of the classic
pre-SE Skyrim mods be added as well. This month, I finally finished downloading
all Skyrim Classic mods, which, at the time of writing, totals at over <a href="https://www.nexusmods.com/skyrim/mods/">68,000
mods on Nexus Mods</a>. That brings the
total downloaded and processed mods for Modmapper at over 113,000
mods<sup id="fnref:adult-mods:1" role="doc-noteref"><a href="#fn:adult-mods" class="footnote" rel="footnote">4</a></sup>!</p>
<h3 id="the-future">The future</h3>
<p>A lot of users had great feedback and suggestions on what to add to the site. I
could only implement so many of them, though. The rest I’ve been keeping track
of on <a href="https://trello.com/b/VdpTQ7ar/modmapper">this Trello board</a>.</p>
<p>Some of the headline items on it are:</p>
<ul>
<li>
<p>Add <a href="https://dbmap.uesp.net/">Solstheim map</a></p>
<p>Since map tiles images are available for that worldspace and because I have
already recorded edits to the worldspace in my database, it shouldn’t be too
terribly difficult.</p>
</li>
<li>
<p>Add <a href="https://www.modorganizer.org/">Mod Organizer 2</a> plugin</p>
<p>Lots of people requested this since it’s a very popular mod manager compared
to Vortex. MO2 supports python extensions so I created
<a href="https://github.com/thallada/skyrim-cell-dump-py">skyrim-cell-dump-py</a> to
export the Rust plugin processing code to a Python library. I got a bit stuck
on actually creating the plugin though, so it might be a while until I get to
that.</p>
</li>
<li>
<p>Find a way to display interior cell edits on the map</p>
<p>The map is currently missing edits to interior cells. Since almost all
interior cells in Skyrim have a link to the exterior world through a door
teleporter, it should be possible to map an interior cell edit to an exterior
cell on the map based on which cell the door leads out to.</p>
<p>That will require digging much more into the plugin files for more data, and
city worldspaces will complicate things further. Then there’s the question of
interiors with multiple doors to different exterior cells, or interior cells
nested recursively deep within many other interior cells.</p>
</li>
<li>
<p>Create a standalone <a href="https://www.electronjs.org">Electron</a> app that can run
outside the browser</p>
<p>I think this would solve a lot of the issues I ran into while developing the
website. Since Electron has a Node.js process running on the user’s computer
outside the sandboxed browser process, it gives me much more flexibility. It
could do things like automatically load a user’s plugin files. Or just load
plugins at all wihtout having to deal with the annoying dialog that lies to
the user saying they’re about to upload their entire Data folder hundreds of
gigabytes full of files to a server (I really wish the
<a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory">HTMLInputElement.webkitdirectory</a>
API would use the same underlying code as the <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API">HTML Drag and Drop
API</a>
which is a lot better).</p>
</li>
<li>
<p>Improving the search</p>
<p>The mod search feature struggles the most with the static generated nature of
the site. I found it very hard to pack all of the necessary info for the
search index for all 100k+ mods (index for both SE and LE is around 6 MB).
Asynchronously loading the indices with MiniSearch keeps it from freezing up
the browser, but it does take a very long time to fully load. I can’t help
think that there’s a better way to shard the indices somehow and only fetch
what I need based on what the user is typing into the search.</p>
</li>
</ul>
<p>To be clear, a lot of the Todos on the board are pipe-dreams. I may never get to
them. This project is sustained purely by my motivation and self-interests and
if something is too much of a pain to get working I’ll just drop it.</p>
<p>There will also be future Elder Scrolls games, and <a href="https://bethesda.net/en/game/starfield">future Bethesda games based
on roughly the same game engine</a>. It
would be neat to create similar database for those games as the modding
community develops in realtime.</p>
<p>Overall, I’m glad I made something of use to the modding community. I hope to
keep the site running for as long as people are modding Skyrim (until the
heat-death of the universe, probably).</p>
<p><br /></p>
<hr />
<h4 id="footnotes">Footnotes</h4>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:bazaarrealm" role="doc-endnote">
<p>Unfortunately, I basically lost interest on the mod after working on
Modmapper. I might still write a blog post about it eventually since I did a
lot of interesting hacking on the Skyrim game engine to try to add some
asynchronous multiplayer aspects. <a href="https://github.com/thallada/BazaarRealmPlugin">Project is here if anyone is curious in
the meantime</a>. <a href="#fnref:bazaarrealm" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:takedown" role="doc-endnote">
<p>I sadly only have screenshots for some of the comments on that mod since it
was eventually taken down by the Nexus Mod admins. See explanation about
that in the <a href="#vortex-integration">Vortex integration section</a>. <a href="#fnref:takedown" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:analytics" role="doc-endnote">
<p>As recorded by Cloudflare’s server side analytics, which may record a fair
amount of bot traffic. I suspect this is the most accurate number I can get
since most of my users probably use an ad blocker that blocks client-side
analytics. <a href="#fnref:analytics" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:adult-mods" role="doc-endnote">
<p>Every mod on Nexus Mods except for adult mods since the site restricts
viewing adult mods to only logged-in users and I wasn’t able to get my
scraping bot to log in as a user. <a href="#fnref:adult-mods" class="reversefootnote" role="doc-backlink">↩</a> <a href="#fnref:adult-mods:1" class="reversefootnote" role="doc-backlink">↩<sup>2</sup></a></p>
</li>
<li id="fn:rate-limit" role="doc-endnote">
<p>Apparently my mass-downloading did not go unnoticed by the Nexus Mod admins.
I think it’s technically against their terms of service to automatically
download mods, but I somehow got on their good side and was spared the
ban-hammer. I don’t recommend anyone else run modmapper themselves on the
entire site unless you talk to the admins beforehand and get the okay from
them. <a href="#fnref:rate-limit" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:plugin-collection" role="doc-endnote">
<p>If you would like access to this dataset of plugins to do some data-mining
please reach out to me at <a href="mailto:[email protected]">[email protected]</a>
(Note: only contains plugins files, no models, textures, audio, etc.). I
don’t plan on releasing it publicly since that would surely go against many
mod authors’ wishes/licenses. <a href="#fnref:plugin-collection" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:cloudflare" role="doc-endnote">
<p>I’m not sure I want to recommend anyone else use Cloudflare after <a href="https://www.theverge.com/2022/9/6/23339889/cloudflare-kiwi-farms-content-moderation-ddos">the whole
Kiwi Farms
debacle</a>.
I now regret having invested so much of the infrastructure in them. However,
I’m only using their free-tier, so at least I am a net-negative for their
business? I would recommend others look into
<a href="https://www.netlify.com/">Netlify</a> or <a href="https://www.fastly.com/">fastly</a> for
similar offerings to Cloudflare pages/CDN. <a href="#fnref:cloudflare" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:3d-terrain" role="doc-endnote">
<p>I also tried to add a <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/">raster Terrain-DEM
source</a>
for rendering the terrain in 3D. I got fairly far <a href="https://github.com/syncpoint/terrain-rgb">generating my own DEM RGB
tiles</a> from an upscaled <a href="https://i.imgur.com/9RErBDo.png">greyscale
heightmap</a> <a href="https://www.nexusmods.com/skyrim/mods/80692">constructed from the LAND
records in Skyrim.esm</a> (view it
<a href="https://www.dropbox.com/s/56lffk021riil6h/heightmap-4x_foolhardy_Remacri_rgb.tif?dl=0">here</a>).
But, it came out all wrong: <a href="/img/blog/modmapper-terrain-cliff.jpg">giant cliffs in the middle of the
map</a> and <a href="/img/blog/modmapper-bad-terrain.jpg">tiny spiky lumps with big
jumps in elevation at cell boundaries</a>.
Seemed like too much work to get right than it was worth it. <a href="#fnref:3d-terrain" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:second-announcement" role="doc-endnote">
<p><a href="https://www.reddit.com/r/skyrimmods/comments/ti3gjh/modmapper_update_load_plugins_in_your_load_order/">This was the announcement I posted to /r/skyrimmods for this feature</a> <a href="#fnref:second-announcement" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:wasm-troubles" role="doc-endnote">
<p>At first, I noticed a strange issue with re-using the same worker on
different plugins multiple times. After a while (~30 reuses per worker), the
processing would slow to a crawl and eventually strange things started
happening (I was listening to music in my browser and it started to pop and
crack). It seemed like the speed of processing increased exponentially to
the number of times the worker was reused. So, to avoid this, I had to make
the worker pool terminate and recreate workers after every plugin processed.
This ended up not being as slow as it sounds and worked fine. However, I
recently discovered that <a href="https://www.reddit.com/r/rust/comments/x1cle0/dont_use_wee_alloc_in_production_code_targeting/">wee_alloc, the most suggested allocator to use
with rust in wasm, has a memory leak and is mostly unmaintained
now</a>.
I switched to the default allocator and I didn’t run into the exponentially
slow re-use problem. For some reason, the first run on a fresh tab is always
much faster than the second run, but subsequent runs are still fairly stable
in processing time. <a href="#fnref:wasm-troubles" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
Wed, 05 Oct 2022 00:00:00 +0000
https://www.hallada.net/2022/10/05/modmapper-putting-every-skyrim-mod-on-a-map-with-rust.html
https://www.hallada.net/2022/10/05/modmapper-putting-every-skyrim-mod-on-a-map-with-rust.htmlGenerating icosahedrons and hexspheres in Rust<p>I’ve been trying to learn <a href="https://www.rust-lang.org/">Rust</a> lately, the hot new
systems programming language. One of the projects I wanted to tackle with the
speed of Rust was generating 3D polyhedron shapes. Specifically, I wanted to
implement something like the <a href="https://threejs.org/docs/#api/en/geometries/IcosahedronGeometry">Three.js
<code class="language-plaintext highlighter-rouge">IcosahedronGeometry</code></a>
in Rust. If you try to generate
<a href="https://en.wikipedia.org/wiki/Icosahedron">icosahedron</a>s in Three.js over any
detail level over 5 the whole browser will slow to a crawl. I think we can do
better in Rust!</p>
<p>Furthermore, I wanted to generate a hexsphere: a sphere composed of hexagon
faces and 12 pentagon faces, otherwise known as a truncated icosahedron or the
<a href="https://en.wikipedia.org/wiki/Goldberg_polyhedron">Goldberg polyhedron</a>. The
shape would be ideal for a game since (almost) every tile would have the same
area and six sides to defend or attack from. There’s a few <a href="https://www.robscanlon.com/hexasphere/">Javascript projects
for generating hexspheres</a>. Most of them
generate the shape by starting with a subdivided icosahedron and then truncating
the sides into hexagons. Though, there <a href="https://stackoverflow.com/questions/46777626/mathematically-producing-sphere-shaped-hexagonal-grid">exist other methods for generating the
hexsphere
shape</a>.</p>
<p><strong>Play around with all of these shapes in your browser at:
<a href="https://www.hallada.net/planet/">https://www.hallada.net/planet/</a>.</strong></p>
<p>So, how would we go about generating a hexsphere from scratch?</p>
<!--excerpt-->
<h3 id="the-icosahedron-seed">The Icosahedron Seed</h3>
<p>To start our sculpture, we need our ball of clay. The most basic shape that we
start with can be defined by its 20 triangle faces and 12 vertices: the regular
icosahedron. If you’ve ever played Dungeons and Dragons, this is the 20-sided
die.</p>
<p>To define this basic shape in Rust, we first need to define a few structs. The
most basic unit we need is a 3D vector which describes a single point in 3D
space with a X, Y, and Z float values. I could have defined this myself, but to
avoid having to implement a bunch of vector operations (like add, subtract,
multiply, etc.) I chose to import
<a href="https://docs.rs/cgmath/0.17.0/cgmath/struct.Vector3.html"><code class="language-plaintext highlighter-rouge">Vector3</code></a> from the
<a href="https://crates.io/crates/cgmath">cgmath crate</a>.</p>
<p>The next struct we need is <code class="language-plaintext highlighter-rouge">Triangle</code>. This will define a face between three
vertices:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Debug)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Triangle</span> <span class="p">{</span>
<span class="k">pub</span> <span class="n">a</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span>
<span class="k">pub</span> <span class="n">b</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span>
<span class="k">pub</span> <span class="n">c</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">impl</span> <span class="n">Triangle</span> <span class="p">{</span>
<span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">c</span><span class="p">:</span> <span class="nb">usize</span><span class="p">)</span> <span class="k">-></span> <span class="n">Triangle</span> <span class="p">{</span>
<span class="n">Triangle</span> <span class="p">{</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>We use <code class="language-plaintext highlighter-rouge">usize</code> for the three points of the triangle because they are indices
into a <a href="https://doc.rust-lang.org/std/vec/struct.Vec.html"><code class="language-plaintext highlighter-rouge">Vec</code></a> of <code class="language-plaintext highlighter-rouge">Vector3</code>s.</p>
<p>To keep these all together, I’ll define a <code class="language-plaintext highlighter-rouge">Polyhedron</code> struct:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Debug)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Polyhedron</span> <span class="p">{</span>
<span class="k">pub</span> <span class="n">positions</span><span class="p">:</span> <span class="nb">Vec</span><span class="o"><</span><span class="n">Vector3</span><span class="o">></span><span class="p">,</span>
<span class="k">pub</span> <span class="n">cells</span><span class="p">:</span> <span class="nb">Vec</span><span class="o"><</span><span class="n">Triangle</span><span class="o">></span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With this, we can define the regular icosahedron:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">Polyhedron</span> <span class="p">{</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">regular_isocahedron</span><span class="p">()</span> <span class="k">-></span> <span class="n">Polyhedron</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">t</span> <span class="o">=</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">+</span> <span class="p">(</span><span class="mf">5.0</span> <span class="k">as</span> <span class="nb">f32</span><span class="p">)</span><span class="nf">.sqrt</span><span class="p">())</span> <span class="o">/</span> <span class="mf">2.0</span><span class="p">;</span>
<span class="n">Polyhedron</span> <span class="p">{</span>
<span class="n">positions</span><span class="p">:</span> <span class="nd">vec!</span><span class="p">[</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">-</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">-</span><span class="mf">1.0</span><span class="p">,</span> <span class="o">-</span><span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="o">-</span><span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">t</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="n">t</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">,</span> <span class="o">-</span><span class="n">t</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="o">-</span><span class="n">t</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">-</span><span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">),</span>
<span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">-</span><span class="n">t</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span>
<span class="p">],</span>
<span class="n">cells</span><span class="p">:</span> <span class="nd">vec!</span><span class="p">[</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">11</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">7</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">10</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">11</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">9</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">11</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">11</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">7</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">11</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">10</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">),</span>
<span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
<span class="p">],</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="json-serialization">JSON Serialization</h3>
<p>To prove this works, we need to be able to output our shape to some format that
will be able to be rendered. Coming from a JS background, I’m only familiar with
rendering shapes with WebGL. So, I need to be able to serialize the shape to
JSON so I can load it in JS.</p>
<p>There’s an amazing library in Rust called
<a href="https://crates.io/crates/serde">serde</a> that will make this very
straightforward. We just need to import it and <code class="language-plaintext highlighter-rouge">impl Serialize</code> for all of our
structs.</p>
<p>The JSON structure we want will look like this. This is what Three.js expects
when initializing
<a href="https://threejs.org/docs/#api/en/core/BufferGeometry"><code class="language-plaintext highlighter-rouge">BufferGeometry</code></a>.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"positions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">[</span><span class="w">
</span><span class="mf">-0.8506508</span><span class="p">,</span><span class="w">
</span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="mf">0.5257311</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="err">...</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"cells"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">[</span><span class="w">
</span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="mi">2</span><span class="p">,</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="err">...</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>For the <code class="language-plaintext highlighter-rouge">"cells"</code> array, we’ll need to serialize <code class="language-plaintext highlighter-rouge">Triangle</code> into an array of 3
integer arrays:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">Serialize</span> <span class="k">for</span> <span class="n">Triangle</span> <span class="p">{</span>
<span class="k">fn</span> <span class="n">serialize</span><span class="o"><</span><span class="n">S</span><span class="o">></span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">serializer</span><span class="p">:</span> <span class="n">S</span><span class="p">)</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="nn">S</span><span class="p">::</span><span class="nb">Ok</span><span class="p">,</span> <span class="nn">S</span><span class="p">::</span><span class="n">Error</span><span class="o">></span>
<span class="k">where</span>
<span class="n">S</span><span class="p">:</span> <span class="n">Serializer</span><span class="p">,</span>
<span class="p">{</span>
<span class="k">let</span> <span class="n">vec_indices</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[</span><span class="k">self</span><span class="py">.a</span><span class="p">,</span> <span class="k">self</span><span class="py">.b</span><span class="p">,</span> <span class="k">self</span><span class="py">.c</span><span class="p">];</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">seq</span> <span class="o">=</span> <span class="n">serializer</span><span class="nf">.serialize_seq</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="n">vec_indices</span><span class="nf">.len</span><span class="p">()))</span><span class="o">?</span><span class="p">;</span>
<span class="k">for</span> <span class="n">index</span> <span class="k">in</span> <span class="n">vec_indices</span> <span class="p">{</span>
<span class="n">seq</span><span class="nf">.serialize_element</span><span class="p">(</span><span class="o">&</span><span class="n">index</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">seq</span><span class="nf">.end</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>I had some trouble serializing the <code class="language-plaintext highlighter-rouge">cgmath::Vector3</code> to an array, so I made my
own type that wrapped <code class="language-plaintext highlighter-rouge">Vector3</code> that could be serialized to an array of 3
floats.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Debug)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="k">pub</span> <span class="n">Vector3</span><span class="o"><</span><span class="nb">f32</span><span class="o">></span><span class="p">);</span>
<span class="k">impl</span> <span class="n">Serialize</span> <span class="k">for</span> <span class="n">ArraySerializedVector</span> <span class="p">{</span>
<span class="k">fn</span> <span class="n">serialize</span><span class="o"><</span><span class="n">S</span><span class="o">></span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">serializer</span><span class="p">:</span> <span class="n">S</span><span class="p">)</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="nn">S</span><span class="p">::</span><span class="nb">Ok</span><span class="p">,</span> <span class="nn">S</span><span class="p">::</span><span class="n">Error</span><span class="o">></span>
<span class="k">where</span>
<span class="n">S</span><span class="p">:</span> <span class="n">Serializer</span><span class="p">,</span>
<span class="p">{</span>
<span class="k">let</span> <span class="n">values</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[</span><span class="k">self</span><span class="na">.0</span><span class="py">.x</span><span class="p">,</span> <span class="k">self</span><span class="na">.0</span><span class="py">.y</span><span class="p">,</span> <span class="k">self</span><span class="na">.0</span><span class="py">.z</span><span class="p">];</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">seq</span> <span class="o">=</span> <span class="n">serializer</span><span class="nf">.serialize_seq</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="n">values</span><span class="nf">.len</span><span class="p">()))</span><span class="o">?</span><span class="p">;</span>
<span class="k">for</span> <span class="n">value</span> <span class="k">in</span> <span class="n">values</span> <span class="p">{</span>
<span class="n">seq</span><span class="nf">.serialize_element</span><span class="p">(</span><span class="o">&</span><span class="n">value</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">seq</span><span class="nf">.end</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And now <code class="language-plaintext highlighter-rouge">Polyhedron</code> needs to use this new type and implement <code class="language-plaintext highlighter-rouge">Serialize</code> for
the whole shape to get serialized:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Serialize,</span> <span class="nd">Debug)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Polyhedron</span> <span class="p">{</span>
<span class="k">pub</span> <span class="n">positions</span><span class="p">:</span> <span class="nb">Vec</span><span class="o"><</span><span class="n">ArraySerializedVector</span><span class="o">></span><span class="p">,</span>
<span class="k">pub</span> <span class="n">cells</span><span class="p">:</span> <span class="nb">Vec</span><span class="o"><</span><span class="n">Triangle</span><span class="o">></span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The actual serialization is done with:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">write_to_json_file</span><span class="p">(</span><span class="n">polyhedron</span><span class="p">:</span> <span class="n">Polyhedron</span><span class="p">,</span> <span class="n">path</span><span class="p">:</span> <span class="o">&</span><span class="n">Path</span><span class="p">)</span> <span class="p">{</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">json_file</span> <span class="o">=</span> <span class="nn">File</span><span class="p">::</span><span class="nf">create</span><span class="p">(</span><span class="n">path</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't create file"</span><span class="p">);</span>
<span class="k">let</span> <span class="n">json</span> <span class="o">=</span> <span class="nn">serde_json</span><span class="p">::</span><span class="nf">to_string</span><span class="p">(</span><span class="o">&</span><span class="n">polyhedron</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Problem serializing"</span><span class="p">);</span>
<span class="n">json_file</span>
<span class="nf">.write_all</span><span class="p">(</span><span class="n">json</span><span class="nf">.as_bytes</span><span class="p">())</span>
<span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't write to file"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>On the JS side, the <code class="language-plaintext highlighter-rouge">.json</code> file can be read and simply fed into either Three.js
or <a href="https://github.com/regl-project/reg">regl</a> to be rendered in WebGL (<a href="#rendering-in-webgl-with-regl">more on
that later</a>).</p>
<p><img src="/img/blog/icosahedron_colored_1.png" alt="Regular Icosahedron" /></p>
<h2 id="subdivided-icosahedron">Subdivided Icosahedron</h2>
<p>Now, we need to take our regular icosahedron and subdivide its faces N number of
times to generate an icosahedron with a detail level of N.</p>
<p>I pretty much copied must of <a href="https://github.com/mrdoob/three.js/blob/34dc2478c684066257e4e39351731a93c6107ef5/src/geometries/PolyhedronGeometry.js#L90">the subdividing code from
Three.js</a>
directly into Rust.</p>
<p>I won’t bore you with the details here, you can find the function
<a href="https://github.com/thallada/icosahedron/blob/9643757df245e29f5ecfbb25f9a2c06b3a4e1217/src/lib.rs#L160-L205">here</a>.</p>
<p><img src="/img/blog/icosahedron_colored_3.png" alt="Subdivided Icosahedron" /></p>
<h3 id="truncated-icosahedron">Truncated Icosahedron</h3>
<p>Now we get to the meat of this project. Transforming an icosahedron into a
hexsphere by
<a href="https://en.wikipedia.org/wiki/Truncation_%28geometry%29">truncating</a> the points
of the icosahedron into hexagon and pentagon faces.</p>
<p>You can imagine this operation as literally cutting off the points of the
subdivided icosahedron at exactly the midpoint between the point and it’s six or
five neighboring points.</p>
<p><img src="/img/blog/dodecahedron_in_icosahedron.png" alt="Image of biggest dodecahedron inside
icosahedron" />
(<a href="http://www.oz.nthu.edu.tw/~u9662122/DualityProperty.html">image source</a>)</p>
<p>In this image you can see the regular icosahedron (0 subdivisions) in wireframe
with a yellow shape underneath which is the result of all 12 points truncated to
12 pentagon faces, in other words: the <a href="https://en.wikipedia.org/wiki/Dodecahedron">regular
dodecahedron</a>.</p>
<p>You can see that the points of the new pentagon faces will be the exact center
of the original triangular faces. It should now make sense why truncating a
shape with 20 faces of 3 edges each results in a shape with 12 faces of 5 edges
each. Each pair multiplied still equals 60.</p>
<h4 id="algorithm">Algorithm</h4>
<p>There are many different algorithms you could use to generate the truncated
shape, but this is roughly what I came up with:</p>
<ol>
<li>
<p>Store a map of every icosahedron vertex to faces composed from that vertex
(<code class="language-plaintext highlighter-rouge">vert_to_faces</code>).</p>
</li>
<li>
<p>Calculate and cache the <a href="https://en.wikipedia.org/wiki/Centroid">centroid</a> of
every triangle in the icosahedron (<code class="language-plaintext highlighter-rouge">triangle_centroids</code>).</p>
</li>
<li>
<p>For every vertex in the original icosahedron:</p>
</li>
<li>
<p>Find the center point between all of centroids of all of the faces for that
vertex (<code class="language-plaintext highlighter-rouge">center_point</code>). This is essentially the original icosahedron point
but lowered towards the center of the polygon since it will eventually be the
center of a new flat hexagon face.</p>
<p><img src="/img/blog/hexagon_fan.png" alt="hexagon center point in red with original icosahedron faces fanning
out" /></p>
</li>
<li>
<p>For every triangle face composed from the original vertex:</p>
<p><img src="/img/blog/hexagon_fan_triangle_selected.png" alt="hexagon fan with selected triangle face in
blue" /></p>
</li>
<li>
<p>Sort the vertices of the triangle face so there is a vertex <code class="language-plaintext highlighter-rouge">A</code> in the center
of the fan like in the image, and two other vertices <code class="language-plaintext highlighter-rouge">B</code> and <code class="language-plaintext highlighter-rouge">C</code> at the edges
of the hexagon.</p>
</li>
<li>
<p>Find the centroid of the selected face. This will be one of the five or six
points of the new pentagon or hexagon (in brown in diagram below:
<code class="language-plaintext highlighter-rouge">triangleCentroid</code>).</p>
</li>
<li>
<p>Find the mid point between <code class="language-plaintext highlighter-rouge">AB</code> and <code class="language-plaintext highlighter-rouge">AC</code> (points <code class="language-plaintext highlighter-rouge">midAB</code> and <code class="language-plaintext highlighter-rouge">midAC</code> in
diagram).</p>
</li>
<li>
<p>With these mid points and the face centroid, we now have two new triangles
(in orange below) that form one-fifth or one-sixth of the final pentagon or
hexagon face. Add the points of the triangle to the <code class="language-plaintext highlighter-rouge">positions</code> array. Add
the two new triangles composed from those vertices as indexes into the
<code class="language-plaintext highlighter-rouge">positions</code> array to the <code class="language-plaintext highlighter-rouge">cells</code> array. We need to compose the pentagon or
hexagon out of triangles because in graphics everything is a triangle, and
this is the simplest way to tile either shape with triangles:</p>
<p><img src="/img/blog/hexagon_fan_construct.png" alt="hexagon fan " /></p>
</li>
<li>
<p>Go to step 5 until all faces of the icosahedron vertex have been visited.<br />
Save indices to all new triangles in the <code class="language-plaintext highlighter-rouge">cells</code> array, which now form a
complete pentagon or hexagon face, to the <code class="language-plaintext highlighter-rouge">faces</code> array.</p>
<p><img src="/img/blog/hexagon_tiling.png" alt="hexagons tiling on icosahedron faces" /></p>
</li>
<li>
<p>Go to step 3 until all vertices in the icosahedron have been visited. The
truncated icosahedron is now complete.</p>
</li>
</ol>
<p><img src="/img/blog/hexsphere_colored_3.png" alt="colored hexsphere of detail level 3" /></p>
<h4 id="code">Code</h4>
<p>The <code class="language-plaintext highlighter-rouge">truncate</code> function calls out to a bunch of other functions, so <a href="https://github.com/thallada/icosahedron/blob/9643757df245e29f5ecfbb25f9a2c06b3a4e1217/src/lib.rs#L227">here’s a
link to the function within the context of the whole
file</a>.</p>
<h3 id="calculating-normals">Calculating Normals</h3>
<p>It took me a surprisingly long time to figure out how to compute
<a href="https://en.wikipedia.org/wiki/Normal_(geometry)">normals</a> for the truncated
icosahedron. I tried just using an out-of-the-box solution like
<a href="https://github.com/mikolalysenko/angle-normals/blob/master/angle-normals.js">angle-normals</a>
which could supposedly calculate the normal vectors for you, but they came out
all wrong.</p>
<p><img src="/img/blog/bad_hexsphere_normals.png" alt="hexsphere with bad normals" /></p>
<p>So, I tried doing it myself. Most tutorials on computing normal vectors for a
mesh assume that it is tiled in a particular way. But, my algorithm spins around
icosahedron points in all different directions, and so the triangle points are
not uniformly in clockwise or counter-clockwise order.</p>
<p>I could have sorted these points into the correct order, but I found it easier
to instead just detect when the normal was pointing the wrong way and just
invert it.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">compute_triangle_normals</span><span class="p">(</span><span class="o">&</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">origin</span> <span class="o">=</span> <span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">);</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="k">self</span><span class="py">.cells</span><span class="nf">.len</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">vertex_a</span> <span class="o">=</span> <span class="o">&</span><span class="k">self</span><span class="py">.positions</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.a</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">vertex_b</span> <span class="o">=</span> <span class="o">&</span><span class="k">self</span><span class="py">.positions</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.b</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">vertex_c</span> <span class="o">=</span> <span class="o">&</span><span class="k">self</span><span class="py">.positions</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.c</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">e1</span> <span class="o">=</span> <span class="n">vertex_a</span> <span class="o">-</span> <span class="n">vertex_b</span><span class="p">;</span>
<span class="k">let</span> <span class="n">e2</span> <span class="o">=</span> <span class="n">vertex_c</span> <span class="o">-</span> <span class="n">vertex_b</span><span class="p">;</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">no</span> <span class="o">=</span> <span class="n">e1</span><span class="nf">.cross</span><span class="p">(</span><span class="n">e2</span><span class="p">);</span>
<span class="c1">// detect and correct inverted normal</span>
<span class="k">let</span> <span class="n">dist</span> <span class="o">=</span> <span class="n">vertex_b</span> <span class="o">-</span> <span class="n">origin</span><span class="p">;</span>
<span class="k">if</span> <span class="n">no</span><span class="nf">.dot</span><span class="p">(</span><span class="n">dist</span><span class="p">)</span> <span class="o"><</span> <span class="mf">0.0</span> <span class="p">{</span>
<span class="n">no</span> <span class="o">*=</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">let</span> <span class="n">normal_a</span> <span class="o">=</span> <span class="k">self</span><span class="py">.normals</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.a</span><span class="p">]</span><span class="na">.0</span> <span class="o">+</span> <span class="n">no</span><span class="p">;</span>
<span class="k">let</span> <span class="n">normal_b</span> <span class="o">=</span> <span class="k">self</span><span class="py">.normals</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.b</span><span class="p">]</span><span class="na">.0</span> <span class="o">+</span> <span class="n">no</span><span class="p">;</span>
<span class="k">let</span> <span class="n">normal_c</span> <span class="o">=</span> <span class="k">self</span><span class="py">.normals</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.c</span><span class="p">]</span><span class="na">.0</span> <span class="o">+</span> <span class="n">no</span><span class="p">;</span>
<span class="k">self</span><span class="py">.normals</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.a</span><span class="p">]</span> <span class="o">=</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">normal_a</span><span class="p">);</span>
<span class="k">self</span><span class="py">.normals</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.b</span><span class="p">]</span> <span class="o">=</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">normal_b</span><span class="p">);</span>
<span class="k">self</span><span class="py">.normals</span><span class="p">[</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="py">.c</span><span class="p">]</span> <span class="o">=</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">normal_c</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">for</span> <span class="n">normal</span> <span class="k">in</span> <span class="k">self</span><span class="py">.normals</span><span class="nf">.iter_mut</span><span class="p">()</span> <span class="p">{</span>
<span class="o">*</span><span class="n">normal</span> <span class="o">=</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">normal</span><span class="na">.0</span><span class="nf">.normalize</span><span class="p">());</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="assigning-random-face-colors">Assigning Random Face Colors</h3>
<p>Finally, all that’s left to generate is the face colors. The only way I could
figure out how to individually color a shape’s faces in WebGL was to pass a
color per vertex. The issue with this is that each vertex of the generated
shapes could be shared between many different faces.</p>
<p>How can we solve this? At the cost of memory, we can just duplicate a vertex
every time it’s used by a different triangle. That way no vertex is shared.</p>
<p>This can be done after a shape has been generated with shared vertices.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">unique_vertices</span><span class="p">(</span><span class="o">&</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="n">Polyhedron</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">triangle</span> <span class="k">in</span> <span class="n">other</span><span class="py">.cells</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">vertex_a</span> <span class="o">=</span> <span class="n">other</span><span class="py">.positions</span><span class="p">[</span><span class="n">triangle</span><span class="py">.a</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">vertex_b</span> <span class="o">=</span> <span class="n">other</span><span class="py">.positions</span><span class="p">[</span><span class="n">triangle</span><span class="py">.b</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">vertex_c</span> <span class="o">=</span> <span class="n">other</span><span class="py">.positions</span><span class="p">[</span><span class="n">triangle</span><span class="py">.c</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">normal_a</span> <span class="o">=</span> <span class="n">other</span><span class="py">.normals</span><span class="p">[</span><span class="n">triangle</span><span class="py">.a</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">normal_b</span> <span class="o">=</span> <span class="n">other</span><span class="py">.normals</span><span class="p">[</span><span class="n">triangle</span><span class="py">.b</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">normal_c</span> <span class="o">=</span> <span class="n">other</span><span class="py">.normals</span><span class="p">[</span><span class="n">triangle</span><span class="py">.c</span><span class="p">]</span><span class="na">.0</span><span class="p">;</span>
<span class="k">self</span><span class="py">.positions</span><span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">vertex_a</span><span class="p">));</span>
<span class="k">self</span><span class="py">.positions</span><span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">vertex_b</span><span class="p">));</span>
<span class="k">self</span><span class="py">.positions</span><span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">vertex_c</span><span class="p">));</span>
<span class="k">self</span><span class="py">.normals</span><span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">normal_a</span><span class="p">));</span>
<span class="k">self</span><span class="py">.normals</span><span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">normal_b</span><span class="p">));</span>
<span class="k">self</span><span class="py">.normals</span><span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">normal_c</span><span class="p">));</span>
<span class="k">self</span><span class="py">.colors</span>
<span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)));</span>
<span class="k">self</span><span class="py">.colors</span>
<span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)));</span>
<span class="k">self</span><span class="py">.colors</span>
<span class="nf">.push</span><span class="p">(</span><span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)));</span>
<span class="k">let</span> <span class="n">added_index</span> <span class="o">=</span> <span class="k">self</span><span class="py">.positions</span><span class="nf">.len</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">self</span><span class="py">.cells</span>
<span class="nf">.push</span><span class="p">(</span><span class="nn">Triangle</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">added_index</span> <span class="o">-</span> <span class="mi">2</span><span class="p">,</span> <span class="n">added_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">added_index</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">self</span><span class="py">.faces</span> <span class="o">=</span> <span class="n">other</span><span class="py">.faces</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With unique vertices, we can now generate a random color per face with the <a href="https://crates.io/crates/rand">rand
crate</a>.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">assign_random_face_colors</span><span class="p">(</span><span class="o">&</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">rng</span> <span class="o">=</span> <span class="nn">rand</span><span class="p">::</span><span class="nf">thread_rng</span><span class="p">();</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="k">self</span><span class="py">.faces</span><span class="nf">.len</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">face_color</span> <span class="o">=</span> <span class="nn">Vector3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">rng</span><span class="nf">.gen</span><span class="p">(),</span> <span class="n">rng</span><span class="nf">.gen</span><span class="p">(),</span> <span class="n">rng</span><span class="nf">.gen</span><span class="p">());</span>
<span class="k">for</span> <span class="n">c</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="k">self</span><span class="py">.faces</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="nf">.len</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">face_cell</span> <span class="o">=</span> <span class="o">&</span><span class="k">self</span><span class="py">.cells</span><span class="p">[</span><span class="k">self</span><span class="py">.faces</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">c</span><span class="p">]];</span>
<span class="k">self</span><span class="py">.colors</span><span class="p">[</span><span class="n">face_cell</span><span class="py">.a</span><span class="p">]</span> <span class="o">=</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">face_color</span><span class="p">);</span>
<span class="k">self</span><span class="py">.colors</span><span class="p">[</span><span class="n">face_cell</span><span class="py">.b</span><span class="p">]</span> <span class="o">=</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">face_color</span><span class="p">);</span>
<span class="k">self</span><span class="py">.colors</span><span class="p">[</span><span class="n">face_cell</span><span class="py">.c</span><span class="p">]</span> <span class="o">=</span> <span class="nf">ArraySerializedVector</span><span class="p">(</span><span class="n">face_color</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="binary-serialization">Binary Serialization</h3>
<p>Now that we have to duplicate vertices for individual face colors, the size of
our JSON outputs are getting quite big:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr>
<td>icosahedron_r1_d6.json</td>
<td>28 MB</td>
</tr>
<tr>
<td>icosahedron_r1_d7.json</td>
<td>113 MB</td>
</tr>
<tr>
<td>hexsphere_r1_d5.json</td>
<td>42 MB</td>
</tr>
<tr>
<td>hexsphere_r1_d6.json</td>
<td>169 MB</td>
</tr>
</tbody>
</table>
<p>Since all of our data is just floating point numbers, we could reduce the size
of the output considerably by using a binary format instead.</p>
<p>I used the <a href="https://docs.rs/byteorder/1.3.2/byteorder/">byteorder</a> crate to
write out all of the <code class="language-plaintext highlighter-rouge">Vec</code>s in my <code class="language-plaintext highlighter-rouge">Polyhedron</code> struct to a binary file in
little-endian order.</p>
<p>The binary format is laid out as:</p>
<ol>
<li>1 32 bit unsigned integer specifying the number of vertices (<code class="language-plaintext highlighter-rouge">V</code>)</li>
<li>1 32 bit unsigned integer specifying the number of triangles (<code class="language-plaintext highlighter-rouge">T</code>)</li>
<li><code class="language-plaintext highlighter-rouge">V</code> * 3 number of 32 bit floats for every vertex’s x, y, and z coordinate</li>
<li><code class="language-plaintext highlighter-rouge">V</code> * 3 number of 32 bit floats for the normals of every vertex</li>
<li><code class="language-plaintext highlighter-rouge">V</code> * 3 number of 32 bit floats for the color of every vertex</li>
<li><code class="language-plaintext highlighter-rouge">T</code> * 3 number of 32 bit unsigned integers for the 3 indices into the vertex
array that make every triangle</li>
</ol>
<p>The <code class="language-plaintext highlighter-rouge">write_to_binary_file</code> function which does all that is
<a href="https://github.com/thallada/icosahedron/blob/9643757df245e29f5ecfbb25f9a2c06b3a4e1217/src/bin.rs#L13">here</a>.</p>
<p>That’s a lot better:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr>
<td>icosahedron_r1_d6.bin</td>
<td>9.8 MB</td>
</tr>
<tr>
<td>icosahedron_r1_d7.bin</td>
<td>11 MB</td>
</tr>
<tr>
<td>hexsphere_r1_d5.bin</td>
<td>14 MB</td>
</tr>
<tr>
<td>hexsphere_r1_d6.bin</td>
<td>58 MB</td>
</tr>
</tbody>
</table>
<p>On the JavaScript side, the binary files can be read into <code class="language-plaintext highlighter-rouge">Float32Array</code>s like
this:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">fetch</span><span class="p">(</span><span class="nx">binaryFile</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">())</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">buffer</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">DataView</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">numVertices</span> <span class="o">=</span> <span class="nx">reader</span><span class="p">.</span><span class="nx">getUint32</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">numCells</span> <span class="o">=</span> <span class="nx">reader</span><span class="p">.</span><span class="nx">getUint32</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">shape</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">positions</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Float32Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="nx">numVertices</span> <span class="o">*</span> <span class="mi">3</span><span class="p">),</span>
<span class="na">normals</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Float32Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">,</span> <span class="nx">numVertices</span> <span class="o">*</span> <span class="mi">12</span> <span class="o">+</span> <span class="mi">8</span><span class="p">,</span> <span class="nx">numVertices</span> <span class="o">*</span> <span class="mi">3</span><span class="p">),</span>
<span class="na">colors</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Float32Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">,</span> <span class="nx">numVertices</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">+</span> <span class="mi">8</span><span class="p">,</span> <span class="nx">numVertices</span> <span class="o">*</span> <span class="mi">3</span><span class="p">),</span>
<span class="na">cells</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Uint32Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">,</span> <span class="nx">numVertices</span> <span class="o">*</span> <span class="mi">36</span> <span class="o">+</span> <span class="mi">8</span><span class="p">,</span> <span class="nx">numCells</span> <span class="o">*</span> <span class="mi">3</span><span class="p">),</span>
<span class="p">})</span>
</code></pre></div></div>
<h3 id="rendering-in-webgl-with-regl">Rendering in WebGL with Regl</h3>
<p>I was initially rendering the shapes with Three.js but switched to
<a href="https://github.com/regl-project/regl">regl</a> because it seemed like a more
direct abstraction over WebGL. It makes setting up a WebGL renderer incredibly
easy compared to all of the dozens cryptic function calls you’d have to
otherwise use.</p>
<p>This is pretty much all of the rendering code using regl in my <a href="https://github.com/thallada/planet">3D hexsphere and
icosahedron viewer project</a>.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">drawShape</span> <span class="o">=</span> <span class="nx">hexsphere</span> <span class="o">=></span> <span class="nx">regl</span><span class="p">({</span>
<span class="na">vert</span><span class="p">:</span> <span class="s2">`
precision mediump float;
uniform mat4 projection, view;
attribute vec3 position, normal, color;
varying vec3 fragNormal, fragPosition, fragColor;
void main() {
fragNormal = normal;
fragPosition = position;
fragColor = color;
gl_Position = projection * view * vec4(position, 1.0);
}`</span><span class="p">,</span>
<span class="na">frag</span><span class="p">:</span> <span class="s2">`
precision mediump float;
struct Light {
vec3 color;
vec3 position;
};
uniform Light lights[1];
varying vec3 fragNormal, fragPosition, fragColor;
void main() {
vec3 normal = normalize(fragNormal);
vec3 light = vec3(0.1, 0.1, 0.1);
for (int i = 0; i < 1; i++) {
vec3 lightDir = normalize(lights[i].position - fragPosition);
float diffuse = max(0.0, dot(lightDir, normal));
light += diffuse * lights[i].color;
}
gl_FragColor = vec4(fragColor * light, 1.0);
}`</span><span class="p">,</span>
<span class="na">attributes</span><span class="p">:</span> <span class="p">{</span>
<span class="na">position</span><span class="p">:</span> <span class="nx">hexsphere</span><span class="p">.</span><span class="nx">positions</span><span class="p">,</span>
<span class="na">normal</span><span class="p">:</span> <span class="nx">hexsphere</span><span class="p">.</span><span class="nx">normals</span><span class="p">,</span>
<span class="na">color</span><span class="p">:</span> <span class="nx">hexsphere</span><span class="p">.</span><span class="nx">colors</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">elements</span><span class="p">:</span> <span class="nx">hexsphere</span><span class="p">.</span><span class="nx">cells</span><span class="p">,</span>
<span class="na">uniforms</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">"</span><span class="s2">lights[0].color</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span>
<span class="dl">"</span><span class="s2">lights[0].position</span><span class="dl">"</span><span class="p">:</span> <span class="p">({</span> <span class="nx">tick</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">t</span> <span class="o">=</span> <span class="mf">0.008</span> <span class="o">*</span> <span class="nx">tick</span>
<span class="k">return</span> <span class="p">[</span>
<span class="mi">1000</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">cos</span><span class="p">(</span><span class="nx">t</span><span class="p">),</span>
<span class="mi">1000</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sin</span><span class="p">(</span><span class="nx">t</span><span class="p">),</span>
<span class="mi">1000</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sin</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span>
<span class="p">]</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>
<p>I also imported <a href="https://github.com/regl-project/regl-camera">regl-camera</a> which
handled all of the complex viewport code for me.</p>
<p>It was fairly easy to get a simple renderer working quickly in regl, but I
couldn’t find many examples of more complex projects using regl. Unfortunately,
the project looks a bit unmaintained these days as well. If I’m going to
continue with rendering in WebGL, I think I will try out
<a href="https://www.babylonjs.com/">Babylon.js</a> instead.</p>
<h3 id="running-in-webassembly">Running in WebAssembly</h3>
<p>Since rust can be compiled down to wasm and then run in the browser, I briefly
tried getting the project to run completely in the browser.</p>
<p>The <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a> tool made it pretty easy
to get started. My main struggle was figuring out an efficient way to get the
megabytes of generated shape data into the JavaScript context so it could be
rendered in WebGL.</p>
<p>The best I could come up with was to export all of my structs into flat
<code class="language-plaintext highlighter-rouge">Vec<f32></code>s and then create <code class="language-plaintext highlighter-rouge">Float32Array</code>s from the JS side that are views into
wasm’s memory.</p>
<p>To export:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">fill_exports</span><span class="p">(</span><span class="o">&</span><span class="k">mut</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">position</span> <span class="k">in</span> <span class="o">&</span><span class="k">self</span><span class="py">.positions</span> <span class="p">{</span>
<span class="k">self</span><span class="py">.export_positions</span><span class="nf">.push</span><span class="p">(</span><span class="n">position</span><span class="na">.0</span><span class="py">.x</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_positions</span><span class="nf">.push</span><span class="p">(</span><span class="n">position</span><span class="na">.0</span><span class="py">.y</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_positions</span><span class="nf">.push</span><span class="p">(</span><span class="n">position</span><span class="na">.0</span><span class="py">.z</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">for</span> <span class="n">normal</span> <span class="k">in</span> <span class="o">&</span><span class="k">self</span><span class="py">.normals</span> <span class="p">{</span>
<span class="k">self</span><span class="py">.export_normals</span><span class="nf">.push</span><span class="p">(</span><span class="n">normal</span><span class="na">.0</span><span class="py">.x</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_normals</span><span class="nf">.push</span><span class="p">(</span><span class="n">normal</span><span class="na">.0</span><span class="py">.y</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_normals</span><span class="nf">.push</span><span class="p">(</span><span class="n">normal</span><span class="na">.0</span><span class="py">.z</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">for</span> <span class="n">color</span> <span class="k">in</span> <span class="o">&</span><span class="k">self</span><span class="py">.colors</span> <span class="p">{</span>
<span class="k">self</span><span class="py">.export_colors</span><span class="nf">.push</span><span class="p">(</span><span class="n">color</span><span class="na">.0</span><span class="py">.x</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_colors</span><span class="nf">.push</span><span class="p">(</span><span class="n">color</span><span class="na">.0</span><span class="py">.y</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_colors</span><span class="nf">.push</span><span class="p">(</span><span class="n">color</span><span class="na">.0</span><span class="py">.z</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">for</span> <span class="n">cell</span> <span class="k">in</span> <span class="o">&</span><span class="k">self</span><span class="py">.cells</span> <span class="p">{</span>
<span class="k">self</span><span class="py">.export_cells</span><span class="nf">.push</span><span class="p">(</span><span class="n">cell</span><span class="py">.a</span> <span class="k">as</span> <span class="nb">u32</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_cells</span><span class="nf">.push</span><span class="p">(</span><span class="n">cell</span><span class="py">.b</span> <span class="k">as</span> <span class="nb">u32</span><span class="p">);</span>
<span class="k">self</span><span class="py">.export_cells</span><span class="nf">.push</span><span class="p">(</span><span class="n">cell</span><span class="py">.c</span> <span class="k">as</span> <span class="nb">u32</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And then the wasm <code class="language-plaintext highlighter-rouge">lib.rs</code>:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">byteorder</span><span class="p">::{</span><span class="n">LittleEndian</span><span class="p">,</span> <span class="n">WriteBytesExt</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">js_sys</span><span class="p">::{</span><span class="n">Array</span><span class="p">,</span> <span class="n">Float32Array</span><span class="p">,</span> <span class="n">Uint32Array</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">wasm_bindgen</span><span class="p">::</span><span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">web_sys</span><span class="p">::</span><span class="n">console</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">icosahedron</span><span class="p">;</span>
<span class="nd">#[cfg(feature</span> <span class="nd">=</span> <span class="s">"wee_alloc"</span><span class="nd">)]</span>
<span class="nd">#[global_allocator]</span>
<span class="k">static</span> <span class="n">ALLOC</span><span class="p">:</span> <span class="nn">wee_alloc</span><span class="p">::</span><span class="n">WeeAlloc</span> <span class="o">=</span> <span class="nn">wee_alloc</span><span class="p">::</span><span class="nn">WeeAlloc</span><span class="p">::</span><span class="n">INIT</span><span class="p">;</span>
<span class="nd">#[wasm_bindgen(start)]</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">main_js</span><span class="p">()</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="p">(),</span> <span class="n">JsValue</span><span class="o">></span> <span class="p">{</span>
<span class="nd">#[cfg(debug_assertions)]</span>
<span class="nn">console_error_panic_hook</span><span class="p">::</span><span class="nf">set_once</span><span class="p">();</span>
<span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
<span class="nd">#[wasm_bindgen]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Hexsphere</span> <span class="p">{</span>
<span class="n">positions</span><span class="p">:</span> <span class="n">Float32Array</span><span class="p">,</span>
<span class="n">normals</span><span class="p">:</span> <span class="n">Float32Array</span><span class="p">,</span>
<span class="n">colors</span><span class="p">:</span> <span class="n">Float32Array</span><span class="p">,</span>
<span class="n">cells</span><span class="p">:</span> <span class="n">Uint32Array</span><span class="p">,</span>
<span class="p">}</span>
<span class="nd">#[wasm_bindgen]</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">shape_data</span><span class="p">()</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="n">Array</span><span class="p">,</span> <span class="n">JsValue</span><span class="o">></span> <span class="p">{</span>
<span class="k">let</span> <span class="n">radius</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">;</span>
<span class="k">let</span> <span class="n">detail</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">hexsphere</span> <span class="o">=</span> <span class="nn">icosahedron</span><span class="p">::</span><span class="nn">Polyhedron</span><span class="p">::</span><span class="nf">new_truncated_isocahedron</span><span class="p">(</span><span class="n">radius</span><span class="p">,</span> <span class="n">detail</span><span class="p">);</span>
<span class="n">hexsphere</span><span class="nf">.compute_triangle_normals</span><span class="p">();</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">unique_hexsphere</span> <span class="o">=</span> <span class="nn">icosahedron</span><span class="p">::</span><span class="nn">Polyhedron</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="n">unique_hexsphere</span><span class="nf">.unique_vertices</span><span class="p">(</span><span class="n">hexsphere</span><span class="p">);</span>
<span class="n">unique_hexsphere</span><span class="nf">.assign_random_face_colors</span><span class="p">();</span>
<span class="n">unique_hexsphere</span><span class="nf">.fill_exports</span><span class="p">();</span>
<span class="k">let</span> <span class="n">positions</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nn">Float32Array</span><span class="p">::</span><span class="nf">view</span><span class="p">(</span><span class="o">&</span><span class="n">unique_hexsphere</span><span class="py">.export_positions</span><span class="p">)</span> <span class="p">};</span>
<span class="k">let</span> <span class="n">normals</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nn">Float32Array</span><span class="p">::</span><span class="nf">view</span><span class="p">(</span><span class="o">&</span><span class="n">unique_hexsphere</span><span class="py">.export_normals</span><span class="p">)</span> <span class="p">};</span>
<span class="k">let</span> <span class="n">colors</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nn">Float32Array</span><span class="p">::</span><span class="nf">view</span><span class="p">(</span><span class="o">&</span><span class="n">unique_hexsphere</span><span class="py">.export_colors</span><span class="p">)</span> <span class="p">};</span>
<span class="k">let</span> <span class="n">cells</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nn">Uint32Array</span><span class="p">::</span><span class="nf">view</span><span class="p">(</span><span class="o">&</span><span class="n">unique_hexsphere</span><span class="py">.export_cells</span><span class="p">)</span> <span class="p">};</span>
<span class="nf">Ok</span><span class="p">(</span><span class="nn">Array</span><span class="p">::</span><span class="nf">of4</span><span class="p">(</span><span class="o">&</span><span class="n">positions</span><span class="p">,</span> <span class="o">&</span><span class="n">normals</span><span class="p">,</span> <span class="o">&</span><span class="n">colors</span><span class="p">,</span> <span class="o">&</span><span class="n">cells</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With wasm-pack, I could import the wasm package, run the <code class="language-plaintext highlighter-rouge">shape_data()</code>
function, and then read the contents as any other normal JS array.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">rust</span> <span class="o">=</span> <span class="k">import</span><span class="p">(</span><span class="dl">"</span><span class="s2">../pkg/index.js</span><span class="dl">"</span><span class="p">)</span>
<span class="nx">rust</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">module</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">shapeData</span> <span class="o">=</span> <span class="nx">module</span><span class="p">.</span><span class="nx">shape_data</span><span class="p">()</span>
<span class="kd">const</span> <span class="nx">shape</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">positions</span><span class="p">:</span> <span class="nx">shapeData</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
<span class="na">normals</span><span class="p">:</span> <span class="nx">shapeData</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
<span class="na">colors</span><span class="p">:</span> <span class="nx">shapeData</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span>
<span class="na">cells</span><span class="p">:</span> <span class="nx">shapeData</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="p">})</span>
</code></pre></div></div>
<p>I could side-step the issue of transferring data from Rust to JavaScript
entirely by programming literally everything in WebAssembly. But the bindings
from rust wasm to the WebGL API are still way too complicated compared to just
using regl. Plus, I’d have to implement my own camera from scratch.</p>
<h3 id="the-stats">The Stats</h3>
<p>So how much faster is Rust than JavaScript in generating icosahedrons and
hexspheres?</p>
<p>Here’s how long it took with generating shapes in JS with Three.js in Firefox
versus in native Rust with a i5-2500K 3.3 GHz CPU.</p>
<table>
<thead>
<tr>
<th>Shape</th>
<th>JS generate time</th>
<th>Rust generate time</th>
</tr>
</thead>
<tbody>
<tr>
<td>Icosahedron detail 6</td>
<td>768 ms</td>
<td>28.23 ms</td>
</tr>
<tr>
<td>Icosahedron detail 7</td>
<td>4.25 s</td>
<td>128.81 ms</td>
</tr>
<tr>
<td>Hexsphere detail 6</td>
<td>11.37 s</td>
<td>403.10 ms</td>
</tr>
<tr>
<td>Hexsphere detail 7</td>
<td>25.49 s</td>
<td>1.85 s</td>
</tr>
</tbody>
</table>
<p>So much faster!</p>
<h3 id="todo">Todo</h3>
<ul>
<li>
<p>Add a process that alters the shape post-generation. Part of the reason why I
decided to fan the hexagon faces with so many triangles is that it also allows
me to control the height of the faces better. This could eventually allow me
to create mountain ranges and river valleys on a hexsphere planet. Stretching
and pulling the edges of the polygon faces in random directions could add
variation and make for a more organic looking hexsphere.</p>
</li>
<li>
<p>Conversely, it would be nice to be able to run a process post-generation that
could reduce the number of triangles by tiling the hexagons more efficiently
when face elevation isn’t needed.</p>
</li>
<li>
<p>Add parameters to the generation that allows generating sections of the
hexsphere / icosahedron. This will be essential for rendering very detailed
polyhedrons since at a certain detail level it becomes impossible to render
the entire shape at once.</p>
<p>In WebGL, figure out what part of the shape is in the current viewport and
pass these parameters to the generation.</p>
</li>
<li>
<p>Render the shapes in a native Rust graphics library instead of WebGL. I’m
curious how much slower WebGL is making things.</p>
</li>
<li>
<p>Parallelize the generation. Right now the generation is very CPU bound and
each subdivide/truncate iteration is mostly independent from each other, so I
think I could get some decent speed-up by allowing the process to run on
multiple cores. Perhaps the <a href="https://github.com/rayon-rs/rayon">rayon</a> crate
could make this pretty straightforward.</p>
</li>
<li>
<p>Find some way to avoid unique vertices. The size of the shape is <em>much</em> bigger
because of this. There might be a way to keep shared vertices while also
having a separate color per face by using texture mapping.</p>
</li>
<li>
<p>In the renderer, implement face selection (point and click face and show an
outline around selected face).</p>
</li>
<li>
<p>In the renderer, implement fly-to-face zooming: given a face, fly the camera
around the sphere in an orbit and then zoom in on the face.</p>
</li>
</ul>
Sat, 01 Feb 2020 00:00:00 +0000
https://www.hallada.net/2020/02/01/generating-icosahedrons-and-hexspheres-in-rust.html
https://www.hallada.net/2020/02/01/generating-icosahedrons-and-hexspheres-in-rust.htmlStudio-Frontend: Developing Frontend Separate from edX Platform<p><em>This is a blog post that I originally wrote for the <a href="https://engineering.edx.org/">edX engineering
blog</a>.</em></p>
<p>At the core of edX is the <a href="https://github.com/edx/edx-platform">edx-platform</a>, a
monolithic Django code-base 2.7 times the size of Django itself.
<!--excerpt--></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-------------------------------------------------------------------------------
Language Files Lines Code Comments Blanks
-------------------------------------------------------------------------------
ActionScript 1 118 74 23 21
Autoconf 10 425 237 163 25
CSS 55 17106 14636 1104 1366
HTML 668 72567 36865 30306 5396
JavaScript 1500 463147 352306 55882 54959
JSON 91 14583 14583 0 0
JSX 33 2595 2209 62 324
LESS 1 949 606 232 111
Makefile 1 65 49 8 8
Markdown 23 287 287 0 0
Mustache 1 1 1 0 0
Python 3277 559255 442756 29254 87245
ReStructuredText 48 4252 4252 0 0
Sass 424 75559 55569 4555 15435
Shell 15 929 505 292 132
SQL 4 6283 5081 1186 16
Plain Text 148 3521 3521 0 0
TypeScript 20 88506 76800 11381 325
XML 364 5283 4757 231 295
YAML 36 1630 1361 119 150
-------------------------------------------------------------------------------
Total 6720 1317061 1016455 134798 165808
-------------------------------------------------------------------------------
</code></pre></div></div>
<p>35% of the edx-platform is JavaScript. While it has served edX well since its
inception in 2012, reaching over 11 million learners in thousands of courses on
<a href="https://www.edx.org/">edX.org</a> and many more millions on all of the <a href="https://openedx.atlassian.net/wiki/spaces/COMM/pages/162245773/Sites+powered+by+Open+edX">Open edX
instances across the
world</a>,
it is starting to show its age. Most of it comes in the form of <a href="http://backbonejs.org/">Backbone.js
apps</a> loaded by <a href="http://requirejs.org/">RequireJS</a> in
Django <a href="http://www.makotemplates.org/">Mako templates</a>, with
<a href="https://jquery.com/">jQuery</a> peppered throughout.</p>
<p>Many valiant efforts are underway to modernize the frontend of edx-platform
including replacing RequireJS with Webpack, Backbone.js with
<a href="https://reactjs.org/">React</a>, and ES5 JavaScript and CoffeeScript with ES6
JavaScript. Many of these efforts <a href="https://www.youtube.com/watch?v=xicBnbDX4AY">were covered in detail at the last Open edX
conference</a> and in <a href="https://open-edx-proposals.readthedocs.io/en/latest/oep-0011-bp-FED-technology.html">Open edX
Proposal 11: Front End Technology
Standards</a>.
However, the size and complexity of the edx-platform means that these kind of
efforts are hard to prioritize, and, in the meantime, frontend developers are
forced to <a href="https://openedx.atlassian.net/wiki/spaces/FEDX/pages/264700138/Asset+Compilation+Audit+2017-11-01">wait over 10
minutes</a>
for our home-grown asset pipeline to build before they can view changes.</p>
<p>There have also been efforts to incrementally modularize and extract parts of
the edx-platform into separate python packages that could be installed as
<a href="https://docs.djangoproject.com/en/2.0/ref/applications/">Django apps</a>, or even
as separately deployed
<a href="https://en.wikipedia.org/wiki/Microservices">microservices</a>. This allows
developers to work independently from the rest of the organization inside of a
repository that they own, manage, and is small enough that they could feasibly
understand it entirely.</p>
<p>When my team was tasked with improving the user experience of pages in
<a href="https://studio.edx.org/">Studio</a>, the tool that course authors use to create
course content, we opted to take a similar architectural approach with the
frontend and create a new repository where we could develop new pages in
isolation and then integrate them back into the edx-platform as a plugin. We
named this new independent repository
<a href="https://github.com/edx/studio-frontend">studio-frontend</a>. With this approach,
our team owns the entire studio-frontend code-base and can make the best
architectural changes required for its features without having to consult with
and contend with all of the other teams at edX that contribute to the
edx-platform. Developers of studio-frontend can also avoid the platform’s slow
asset pipeline by doing all development within the studio-frontend repository
and then later integrating the changes into platform.</p>
<h2 id="react-and-paragon">React and Paragon</h2>
<p>When edX recently started to conform our platform to the <a href="https://www.w3.org/WAI/intro/wcag">Web Content
Accessibility Guidelines 2.0 AA (WCAG 2.0
AA)</a>, we faced many challenges in
retrofitting our existing frontend code to be accessible. Rebuilding Studio
pages from scratch in studio-frontend allows us to not only follow the latest
industry standards for building robust and performant frontend applications, but
to also build with accessibility in mind from the beginning.</p>
<p>The Javascript community has made great strides recently to <a href="https://reactjs.org/docs/accessibility.html">address
accessibility issues in modern web
apps</a>. However, we had trouble
finding an open-source React component library that fully conformed to WCAG 2.0
AA and met all of edX’s needs, so we decided to build our own:
<a href="https://github.com/edx/paragon">Paragon</a>.</p>
<p>Paragon is a library of building-block components like buttons, inputs, icons,
and tables which were built from scratch in React to be accessible. The
components are styled using the <a href="https://github.com/edx/edx-bootstrap">Open edX theme of Bootstrap
v4</a> (edX’s decision to adopt Bootstrap is
covered in
<a href="https://open-edx-proposals.readthedocs.io/en/latest/oep-0016-bp-adopt-bootstrap.html">OEP-16</a>).
Users of Paragon may also choose to use the
<a href="https://github.com/edx/paragon#export-targets">themeable</a> unstyled target and
provide their own Bootstrap theme.</p>
<p><img src="/img/blog/paragon-modal-storybook.jpg" alt="Paragon's modal component displayed in
Storybook" /></p>
<p>Studio-frontend composes together Paragon components into higher-level
components like <a href="https://github.com/edx/studio-frontend/blob/master/src/accessibilityIndex.jsx">an accessibility
form</a>
or <a href="https://github.com/edx/studio-frontend/blob/master/src/index.jsx">a table for course assets with searching, filtering, sorting, pagination,
and upload</a>.
While we developed these components in studio-frontend, we were able to improve
the base Paragon components. Other teams at edX using the same components were
able to receive the same improvements with a single package update.</p>
<p><img src="/img/blog/studio-frontend-assets-table.jpg" alt="Screenshot of the studio-frontend assets table inside of
Studio" /></p>
<h2 id="integration-with-studio">Integration with Studio</h2>
<p>We were able to follow the typical best practices for developing a React/Redux
application inside studio-frontend, but at the end of the day, we still had to
somehow get our components inside of existing Studio pages and this is where
most of the challenges arose.</p>
<h2 id="webpack">Webpack</h2>
<p>The aforementioned move from RequireJS to Webpack in the edx-platform made it
possible for us to build our studio-frontend components from source with Webpack
within edx-platform. However, this approach tied us to the edx-platform’s slow
asset pipeline. If we wanted rapid development, we had to duplicate the
necessary Webpack config between both studio-frontend and edx-platform.</p>
<p>Instead, studio-frontend handles building the development and production Webpack
builds itself. In development mode, the incremental rebuild that happens
automatically when a file is changed takes under a second. The production
JavaScript and CSS bundles, which take about 25 seconds to build, are published
with every new release to
<a href="https://www.npmjs.com/package/@edx%2Fstudio-frontend">NPM</a>. The edx-platform
<code class="language-plaintext highlighter-rouge">npm install</code>s studio-frontend and then copies the built production files from
<code class="language-plaintext highlighter-rouge">node_modules</code> into its Django static files directory where the rest of the
asset pipeline will pick it up.</p>
<p>To actually use the built JavaScript and CSS, edx-platform still needs to
include it in its Mako templates. We made a <a href="https://github.com/edx/edx-platform/blob/master/common/djangoapps/pipeline_mako/templates/static_content.html#L93-L122">Mako template
tag</a>
that takes a Webpack entry point name in studio-frontend and generates script
tags that include the necessary files from the studio-frontend package. It also
dumps all of the initial context that studio-frontend needs from the
edx-platform Django app into <a href="https://github.com/edx/edx-platform/blob/master/cms/templates/asset_index.html#L36-L56">a JSON
object</a>
in a script tag on the page that studio-frontend components can access via a
shared id. This is how studio-frontend components get initial data from Studio,
like which course it’s embedded in.</p>
<p>For performance, modules that are shared across all studio-frontend components
are extracted into <code class="language-plaintext highlighter-rouge">common.min.js</code> and <code class="language-plaintext highlighter-rouge">common.min.css</code> files that are included
on every Studio template that has a studio-frontend component. User’s browsers
should cache these files so that they do not have to re-download libraries like
React and Redux every time they visit a new page that contains a studio-frontend
component.</p>
<h2 id="css-isolation">CSS Isolation</h2>
<p>Since the move to Bootstrap had not yet reached the Studio part of the
edx-platform, most of the styling clashed with the Bootstrap CSS that
studio-frontend components introduced. And, the Bootstrap styles were also
leaking outside of the studio-frontend embedded component <code class="language-plaintext highlighter-rouge">div</code> and affecting
the rest of the Studio page around it.</p>
<p><img src="/img/blog/studio-frontend-style-isolation.jpg" alt="Diagram of a studio-frontend component embedded inside of
Studio" /></p>
<p>We were able to prevent styles leaking outside of the studio-frontend component
by scoping all CSS to only the <code class="language-plaintext highlighter-rouge">div</code> that wraps the component. Thanks to the
Webpack <a href="https://github.com/postcss/postcss-loader">postcss-loader</a> and the
<a href="https://github.com/ledniy/postcss-prepend-selector">postcss-prepend-selector</a>
we were able to automatically scope all of our CSS selectors to that <code class="language-plaintext highlighter-rouge">div</code> in
our build process.</p>
<p>Preventing the Studio styles from affecting our studio-frontend component was a
much harder problem because it means avoiding the inherently cascading nature of
CSS. A common solution to this issue is to place the 3rd-party component inside
of an
<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"><code class="language-plaintext highlighter-rouge">iframe</code></a>
element, which essentially creates a completely separate sub-page where both CSS
and JavaScript are isolated from the containing page. Because <code class="language-plaintext highlighter-rouge">iframe</code>s
introduce many other performance and styling issues, we wanted to find a
different solution to isolating CSS.</p>
<p>The CSS style <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/all"><code class="language-plaintext highlighter-rouge">all:
initial</code></a> allows
resetting all properties on an element to their initial values as defined in the
CSS spec. Placing this style under a wildcard selector in studio-frontend
allowed us to reset all inherited props from the legacy Studio styles without
having to enumerate them all by hand.</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">*</span> <span class="p">{</span>
<span class="nl">all</span><span class="p">:</span> <span class="n">initial</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>While this CSS property doesn’t have broad browser support yet, we were able to
polyfill it thanks to postcss with the
<a href="https://github.com/maximkoretskiy/postcss-initial">postcss-initial</a> plugin.</p>
<p>However, this resets the styles to <em>nothing</em>. For example, all <code class="language-plaintext highlighter-rouge">div</code>s are
displayed inline. To return the styles back to to some sane browser default we
had to re-apply a browser default stylesheet. You can read more about this
technique at
<a href="https://github.com/thallada/default-stylesheet">default-stylesheet</a>.</p>
<p>From there, Bootstrap’s
<a href="https://getbootstrap.com/docs/4.0/content/reboot/">reboot</a> normalizes the
browser-specific styling to a common baseline and then applies the Bootstrap
styles conflict-free from the surrounding CSS cascade.</p>
<p>There’s a candidate recommendation in CSS for a <a href="https://www.w3.org/TR/css-contain-1/"><code class="language-plaintext highlighter-rouge">contains</code>
property</a>, which will “allow strong,
predictable isolation of a subtree from the rest of the page”. I hope that it
will provide a much more elegant solution to this problem once browsers support
it.</p>
<h2 id="internationalization">Internationalization</h2>
<p>Another major challenge with separating out the frontend from edx-platform was
that most of our internationalization tooling was instrumented inside the
edx-platform. So, in order to display text in studio-frontend components in the
correct language we either had to pass already-translated strings from the
edx-platform into studio-frontend, or set-up translations inside
studio-frontend.</p>
<p>We opted for the latter because it kept the content close to the code that used
it. Every display string in a component is stored in a
<a href="https://github.com/edx/studio-frontend/blob/master/src/components/AssetsTable/displayMessages.jsx">displayMessages.jsx</a>
file and then imported and referenced by an id within the component. A periodic
job extracts these strings from the project, pushes them up to our translations
service <a href="https://www.transifex.com/">Transifex</a>, and pulls any new translations
to store them in our NPM package.</p>
<p>Because Transifex’s <code class="language-plaintext highlighter-rouge">KEYVALUEJSON</code> file format does not allow for including
comments in the strings for translation, <a href="https://github.com/efischer19">Eric</a>
created a library called <a href="https://github.com/efischer19/reactifex">reactifex</a>
that will send the comments in separate API calls.</p>
<p>Studio includes the user’s language in the context that it sends a
studio-frontend component for initialization. Using this, the component can
display the message for that language if it exists. If it does not, then it will
display the original message in English and <a href="https://github.com/edx/studio-frontend/blob/master/src/utils/i18n/formattedMessageWrapper.jsx">wrap it in a <code class="language-plaintext highlighter-rouge">span</code> with <code class="language-plaintext highlighter-rouge">lang="en"</code>
as an
attribute</a>
so that screen-readers know to read it in English even if their default is some
other language.</p>
<p>Read more about studio-frontend’s internationalization process in <a href="https://github.com/edx/studio-frontend/blob/master/src/data/i18n/README.md">the
documentation that Eric
wrote</a>.</p>
<h2 id="developing-with-docker">Developing with Docker</h2>
<p>To normalize the development environment across the whole studio-frontend team,
development is done in a Docker container. This is a minimal Ubuntu 16.04
container with specific version of Node 8 installed and its only purpose is to
run Webpack. This follows the pattern established in <a href="https://open-edx-proposals.readthedocs.io/en/latest/oep-0005-arch-containerize-devstack.html">OEP-5: Pre-built
Development
Environments</a>
for running a single Docker container per process that developers can easily
start without installing dependencies.</p>
<p>Similar to edX’s <a href="https://github.com/edx/devstack">devstack</a> there is a Makefile
with commands to start and stop the docker container. The docker container then
immediately runs <a href="https://github.com/edx/studio-frontend/blob/master/package.json#L12"><code class="language-plaintext highlighter-rouge">npm run
start</code></a>,
which runs Webpack with the
<a href="https://github.com/webpack/webpack-dev-server">webpack-dev-server</a>. The
webpack-dev-server is a node server that serves assets built by Webpack.
<a href="https://github.com/edx/studio-frontend/blob/master/config/webpack.dev.config.js#L94">Studio-frontend’s Webpack
config</a>
makes this server available to the developer’s host machine
at <code class="language-plaintext highlighter-rouge">http://localhost:18011</code>.</p>
<p>With <a href="https://webpack.js.org/concepts/hot-module-replacement/">hot-reload</a>
enabled, developers can now visit that URL in their browser, edit source files
in studio-frontend, and then see changes reflected instantly in their browser
once Webpack finishes its incremental rebuild.</p>
<p>However, many studio-frontend components need to be able to talk to the
edx-platform Studio backend Django server. Using <a href="https://docs.docker.com/compose/networking/#use-a-pre-existing-network">docker’s network connect
feature</a>
the studio-frontend container can join the developer’s existing docker devstack
network so that the studio-frontend container can make requests to the docker
devstack Studio container at <code class="language-plaintext highlighter-rouge">http://edx.devstack.studio:18010/</code> and Studio can
access studio-frontend at <code class="language-plaintext highlighter-rouge">http://dahlia.studio-fronend:18011/</code>.</p>
<p>The webpack-dev-server can now <a href="https://github.com/edx/studio-frontend/blob/master/config/webpack.dev.config.js#L101">proxy all
requests</a>
to Studio API endpoints (like <code class="language-plaintext highlighter-rouge">http://localhost:18011/assets</code>)
to <code class="language-plaintext highlighter-rouge">http://edx.devstack.studio:18010/</code>.</p>
<h2 id="developing-within-docker-devstack-studio">Developing within Docker Devstack Studio</h2>
<p>Since studio-frontend components will be embedded inside of an existing Studio
page shell, it’s often useful to develop on studio-frontend containers inside of
this set-up. <a href="https://github.com/edx/studio-frontend#development-inside-devstack-studio">This can be
done</a>
by setting a variable in the devstack’s <code class="language-plaintext highlighter-rouge">cms/envs/private.py</code>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">STUDIO_FRONTEND_CONTAINER_URL</span> <span class="o">=</span> <span class="s">'http://localhost:18011'</span>
</code></pre></div></div>
<p>This setting is checked in the Studio Mako templates wherever studio-frontend
components are embedded. If it is set to a value other than <code class="language-plaintext highlighter-rouge">None</code>, then the
templates will request assets from that URL instead of the Studio’s own static
assets directory. When a developer loads a Studio page with an embedded
studio-frontend component, their studio-frontend webpack-dev-server will be
requested at that URL. Similarly to developing on studio-frontend in isolation,
edits to source files will trigger a Webpack compilation and the Studio page
will be hot-reloaded or reloaded to reflect the changes automatically.</p>
<p>Since the studio-frontend JS loaded on <code class="language-plaintext highlighter-rouge">localhost:18010</code> is now requesting the
webpack-dev-server on <code class="language-plaintext highlighter-rouge">localhost:18011</code>,
an <a href="https://github.com/edx/studio-frontend/blob/master/config/webpack.dev.config.js#L98"><code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin</code> header</a>
has to be configured on the webpack-dev-server to get around CORS violations.</p>
<p><img src="/img/blog/studio-frontend-docker-devstack.jpg" alt="Diagram of studio-frontend's docker container communicating to Studio inside
of the devstack_default docker
network" /></p>
<h2 id="deploying-to-production">Deploying to Production</h2>
<p><a href="https://github.com/edx/studio-frontend#releases">Each release of
studio-frontend</a> will upload
the <code class="language-plaintext highlighter-rouge">/dist</code> files built by Webpack in production mode to
<a href="https://www.npmjs.com/package/@edx/studio-frontend">NPM</a>. edx-platform
requires a particular version of studio-frontend in its
<a href="https://github.com/edx/edx-platform/blob/master/package.json#L7"><code class="language-plaintext highlighter-rouge">package.json</code></a>.
When a new release of edx-platform is made, <code class="language-plaintext highlighter-rouge">paver update_assets</code> will run
which will copy all of the files in the
<code class="language-plaintext highlighter-rouge">node_modules/@edx/studio-frontend/dist/</code> to the Studio static folder.
Because <code class="language-plaintext highlighter-rouge">STUDIO_FRONTEND_CONTAINER_URL</code> will be <code class="language-plaintext highlighter-rouge">None</code> in production, it will be
ignored, and Studio pages will request studio-frontend assets from that static
folder.</p>
<h2 id="future">Future</h2>
<p>Instead of “bringing the new into the old”, we’d eventually like to move to a
model where we “work in the new and bring in the old if necessary”. We could
host studio-frontend statically on a completely separate server which talks to
Studio via a REST (or <a href="https://graphql.org/">GraphQL</a>) API. This approach would
eliminate the complexity around CSS isolation and bring big performance wins for
our users, but it would require us to rewrite more of Studio.</p>
Thu, 26 Apr 2018 00:00:00 +0000
https://www.hallada.net/2018/04/26/studio-frontend.html
https://www.hallada.net/2018/04/26/studio-frontend.htmlIsso Comments<p>I’ve been meaning to add a commenting system to this blog for a while, but I
couldn’t think of a good way to do it. I implemented my own commenting system on
my <a href="https://github.com/thallada/personalsite">old Django personal site</a>. While I
enjoyed working on it at the time, it was a lot of work, especially to fight the
spam. Now that my blog is hosted statically on Github’s servers, I have no way
to host something dynamic like comments.
<!--excerpt--></p>
<p><a href="http://disqus.com/">Disqus</a> seems to be the popular solution to this problem
for other people that host static blogs. The way it works is that you serve a
javascript client script on the static site you own. The script will make AJAX
requests to a separate server that Disqus owns to retrieve comments and post new
ones.</p>
<p>The price you pay for using Disqus, however, is that <a href="https://replyable.com/2017/03/disqus-is-your-data-worth-trading-for-convenience/">they get to sell all of
the data that you and your commenters give
them</a>.
That reason, plus the fact that I wanted something more DIY, meant this blog has
gone without comments for a few years.</p>
<p>Then I discovered <a href="https://github.com/posativ/isso">Isso</a>. Isso calls itself a
lightweight alternative to <a href="http://disqus.com/">Disqus</a>. Isso allows you to
install the server code on your own server so that the comment data never goes
to a third party. Also, it does not require logging into some social media
account just to comment. Today, I installed it on my personal AWS EC2 instance
and added the Isso javascript client script on this blog. So far, my experience
with it has been great and it performs exactly the way I expect.</p>
<p>I hit a few snags while installing it, however.</p>
<h2 id="debian-package">Debian Package</h2>
<p><strong>I don’t recommend using the Debian package anymore as it frequently goes out
of date and breaks on distribution upgrades. See bottom edit.</strong></p>
<p>There is a very handy <a href="https://github.com/jgraichen/debian-isso">Debian package</a>
that someone has made for Isso. Since my server runs Ubuntu 16.04, and Ubuntu is
based off of Debian, this is a package I can install with my normal ubuntu
package manager utilities. There is no PPA to install since the package is in
the <a href="https://packages.ubuntu.com/xenial/isso">main Ubuntu package archive</a>. Just
run <code class="language-plaintext highlighter-rouge">sudo apt-get install isso</code>.</p>
<p>I got a bit confused after that point, though. There seems to be no
documentation I could find about how to actually configure and start the server
once you have installed it. This is what I did:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo cp</span> /etc/default/isso /etc/isso.d/available/isso.cfg
<span class="nb">sudo ln</span> <span class="nt">-s</span> /etc/isso.d/available/isso.cfg /etc/isso.d/enabled/isso.cfg
</code></pre></div></div>
<p>Then you can edit <code class="language-plaintext highlighter-rouge">/etc/isso.d/available/isso.cfg</code> with your editor of choice to
<a href="https://posativ.org/isso/docs/configuration/server/">configure the Isso server for your
needs</a>. Make sure to set
the <code class="language-plaintext highlighter-rouge">host</code> variable to the URL for your static site.</p>
<p>Once you’re done, you can run <code class="language-plaintext highlighter-rouge">sudo service isso restart</code> to reload the server
with the new configuration. <code class="language-plaintext highlighter-rouge">sudo service isso status</code> should report <code class="language-plaintext highlighter-rouge">Active
(running)</code>.</p>
<p>Right now, there should be a <a href="http://gunicorn.org/">gunicorn</a> process running
the isso server. You can check that with <code class="language-plaintext highlighter-rouge">top</code> or running <code class="language-plaintext highlighter-rouge">ps aux | grep
gunicorn</code>, which should return something about “isso”.</p>
<h2 id="nginx-reverse-proxy">Nginx Reverse Proxy</h2>
<p>In order to map the URL “comments.hallada.net” to this new gunicorn server, I
need an <a href="https://www.nginx.com/resources/admin-guide/reverse-proxy/">nginx reverse
proxy</a>.</p>
<p>To do that, I made a new server block: <code class="language-plaintext highlighter-rouge">sudo vim
/etc/nginx/sites-available/isso</code> which I added:</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
<span class="kn">listen</span> <span class="s">[::]:80</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">comments.hallada.net</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Script-Name</span> <span class="n">/isso</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
<span class="kn">proxy_pass</span> <span class="s">http://localhost:8000</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then I enabled this new server block with:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo ln</span> <span class="nt">-s</span> /etc/nginx/sites-available/isso /etc/nginx/sites-enabled/isso
<span class="nb">sudo </span>systemctl restart nginx
</code></pre></div></div>
<h2 id="dns-configuration">DNS Configuration</h2>
<p>I added a new A record for “comments.hallada.net” that pointed to my server’s IP
address to the DNS configuration for my domain (which I recently switched to
<a href="https://aws.amazon.com/route53/">Amazon Route 53</a>).</p>
<p>After the DNS caches had time to refresh, visiting <code class="language-plaintext highlighter-rouge">http://comments.hallada.net</code>
would hit the new <code class="language-plaintext highlighter-rouge">isso</code> nginx server block, which would then pass the request
on to the gunicorn process.</p>
<p>You can verify if nginx is getting the request by looking at
<code class="language-plaintext highlighter-rouge">/var/log/nginx/access.log</code>.</p>
<h2 id="adding-the-isso-script-to-my-jekyll-site">Adding the Isso Script to my Jekyll Site</h2>
<p>I created a file called <code class="language-plaintext highlighter-rouge">_includes/comments.html</code> with the contents that <a href="https://posativ.org/isso/docs/quickstart/#integration">the
Isso documentation</a>
provides. Then, in my post template, I simply included that on the page where I
wanted the comments to go:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"card"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"row clearfix"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"column full"</span><span class="nt">></span>
<span class="nt"><script </span><span class="na">data-isso=</span><span class="s">"https://comments.hallada.net/"</span>
<span class="na">src=</span><span class="s">"https://comments.hallada.net/js/embed.min.js"</span><span class="nt">></script></span>
<span class="nt"><section</span> <span class="na">id=</span><span class="s">"isso-thread"</span><span class="nt">></span>
<span class="nt"><noscript></span>Javascript needs to be activated to view comments.<span class="nt"></noscript></span>
<span class="nt"></section></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>Another thing that was not immediately obvious to me is that the value of the
<code class="language-plaintext highlighter-rouge">name</code> variable in the Isso server configuration is the URL path that you will
need to point the Isso JavaScript client to. For example, I chose <code class="language-plaintext highlighter-rouge">name = blog</code>,
so the <code class="language-plaintext highlighter-rouge">data-isso</code> attribute on the script tag needed to be
<code class="language-plaintext highlighter-rouge">http://comments.hallada.net/blog/</code>.</p>
<h2 id="the-uncaught-referenceerror">The Uncaught ReferenceError</h2>
<p><strong>You won’t need to fix this if you install Isso from PIP! See bottom edit.</strong></p>
<p>There’s <a href="https://github.com/posativ/isso/issues/318">an issue</a> with that Debian
package that causes a JavaScript error in the console when trying to load the
Isso script in the browser. I solved this by uploading the latest version of the
Isso <code class="language-plaintext highlighter-rouge">embeded.min.js</code> file to my server, which I put at
<code class="language-plaintext highlighter-rouge">/var/www/html/isso/embeded.min.js</code>. Then I modified the nginx server block to
serve that file when the path matches <code class="language-plaintext highlighter-rouge">/isso</code>:</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
<span class="kn">listen</span> <span class="s">[::]:80</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">comments.hallada.net</span><span class="p">;</span>
<span class="kn">root</span> <span class="n">/var/www/html</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Script-Name</span> <span class="n">/isso</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
<span class="kn">proxy_pass</span> <span class="s">http://localhost:8000</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="n">/isso</span> <span class="p">{</span>
<span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="nv">$uri</span><span class="s">.php?</span><span class="nv">$args</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now requesting <code class="language-plaintext highlighter-rouge">http://comments.hallada.net/isso/embeded.min.js</code> would return
the newer script without the bug.</p>
<h2 id="sending-emails-through-amazon-simple-email-service">Sending Emails Through Amazon Simple Email Service</h2>
<p>I already set up <a href="https://aws.amazon.com/ses/">Amazon’s SES</a> in my <a href="http://www.hallada.net/2017/08/30/making-mailing-list-jekyll-blog-using-sendy.html">last
blog
post</a>.
To get Isso to use SES to send notifications about new comments, create a new
credential in the SES UI, and then set the <code class="language-plaintext highlighter-rouge">user</code> and <code class="language-plaintext highlighter-rouge">password</code> fields in the
<code class="language-plaintext highlighter-rouge">isso.cfg</code> to what get’s generated for the IAM user. The SES page also has
information for what <code class="language-plaintext highlighter-rouge">host</code> and <code class="language-plaintext highlighter-rouge">port</code> to use. I used <code class="language-plaintext highlighter-rouge">security = starttls</code> and
<code class="language-plaintext highlighter-rouge">port = 587</code>. Make sure whatever email you use for <code class="language-plaintext highlighter-rouge">from</code> is a verified email in
SES. Also, don’t forget to add your email as the <code class="language-plaintext highlighter-rouge">to</code> value.</p>
<h2 id="enabling-https-with-lets-encrypt">Enabling HTTPS with Let’s Encrypt</h2>
<p><a href="https://letsencrypt.org/">Let’s Encrypt</a> allows you to get SSL certificates for
free! I had already installed the certbot/letsencrypt client before, so I just
ran this to generate a new certificate for my new sub-domain
“comments.hallada.net”:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>letsencrypt certonly <span class="nt">--nginx</span> <span class="nt">-d</span> comments.hallada.net
</code></pre></div></div>
<p>Once that successfully completed, I added a new nginx server block for the https
version at <code class="language-plaintext highlighter-rouge">/etc/nginx/sites-available/isso-https</code>:</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span> <span class="s">http2</span><span class="p">;</span>
<span class="kn">listen</span> <span class="s">[::]:443</span> <span class="s">ssl</span> <span class="s">http2</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">comments.hallada.net</span><span class="p">;</span>
<span class="kn">root</span> <span class="n">/var/www/html</span><span class="p">;</span>
<span class="kn">ssl_certificate</span> <span class="n">/etc/letsencrypt/live/comments.hallada.net/fullchain.pem</span><span class="p">;</span>
<span class="kn">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/comments.hallada.net/privkey.pem</span><span class="p">;</span>
<span class="kn">ssl_trusted_certificate</span> <span class="n">/etc/letsencrypt/live/comments.hallada.net/fullchain.pem</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Script-Name</span> <span class="n">/isso</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
<span class="kn">proxy_pass</span> <span class="s">http://localhost:8000</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="n">/isso</span> <span class="p">{</span>
<span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="nv">$uri</span><span class="s">.php?</span><span class="nv">$args</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And, I changed the old http server block so that it just permanently redirects
to the https version:</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
<span class="kn">listen</span> <span class="s">[::]:80</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">comments.hallada.net</span><span class="p">;</span>
<span class="kn">root</span> <span class="n">/var/www/html</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">return</span> <span class="mi">301</span> <span class="s">https://comments.hallada.net</span><span class="nv">$request_uri</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then I enabled the https version:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo ln</span> <span class="nt">-s</span> /etc/nginx/sites-available/isso-https /etc/nginx/sites-enabled/isso-https
<span class="nb">sudo </span>systemctl restart nginx
</code></pre></div></div>
<p>I checked that I didn’t get any errors visiting <code class="language-plaintext highlighter-rouge">https://comments.hallada.net/</code>,
and then changed my Jekyll include snippet so that it pointed at the <code class="language-plaintext highlighter-rouge">https</code>
site instead of <code class="language-plaintext highlighter-rouge">http</code>.</p>
<p>Now you can securely leave a comment if you want to yell at me for writing the
wrong thing!</p>
<h2 id="edit-5282019">EDIT 5/28/2019:</h2>
<p>I don’t recommend using the Debian package anymore since it frequently goes out
of date and breaks when upgrading your Linux distribution.</p>
<p>Instead, follow the <a href="https://posativ.org/isso/docs/install/">Isso docs</a> by
creating a <a href="https://virtualenv.pypa.io/en/latest/">virtualenv</a> and then run <code class="language-plaintext highlighter-rouge">pip
install isso</code> and <code class="language-plaintext highlighter-rouge">pip install gunicorn</code> from within the virtualenv. Then, when
creating <a href="https://github.com/jgraichen/debian-isso/blob/master/debian/isso.service">a systemd
service</a>,
make sure to point to the gunicorn executable in that virtualenv (e.g.<br />
<code class="language-plaintext highlighter-rouge">/opt/isso/bin/gunicorn</code>). It should load and run Isso from the same virtualenv.</p>
Wed, 15 Nov 2017 00:00:00 +0000
https://www.hallada.net/2017/11/15/isso-comments.html
https://www.hallada.net/2017/11/15/isso-comments.htmlMaking a Mailing List for a Jekyll Blog Using Sendy<p>When my beloved <a href="https://en.wikipedia.org/wiki/Google_Reader">Google Reader</a> was
discontinued in 2013, I stopped regularly checking RSS feeds. Apparently, <a href="https://trends.google.com/trends/explore?date=all&q=rss">I am
not alone</a>. It seems
like there’s a new article every month arguing either that <a href="https://hn.algolia.com/?q=&query=rss%20dead&sort=byPopularity&prefix&page=0&dateRange=all&type=story">RSS is dead or RSS
is not dead
yet</a>.
Maybe RSS will stick around to serve as a cross-site communication backbone, but
I don’t think anyone will refute that RSS feeds are declining in consumer use.
Facebook, Twitter, and other aggregators are where people really go. However, I
noticed that I still follow some small infrequent blogs through mailing lists
that they offer. I’m really happy to see an email sign up on blogs I like,
because it means I’ll know when they post new content in the future. I check my
email regularly unlike my RSS feeds.
<!--excerpt--></p>
<p>Even though I’m sure my blog is still too uninteresting and unheard of to get
many signups, I still wanted to know what it took to make a blog mailing list.
RSS is super simple for website owners, because all they need to do is dump all
of their content into a specially formatted XML file, host it, and let RSS
readers deal with all the complexity. In my blog, <a href="https://github.com/thallada/thallada.github.io/blob/master/feed.xml">I didn’t even need a
Jekyll
plugin</a>.
Email is significantly more difficult. With email, the website owner owns more
of the complexity. And, spam filters make it unfeasible to roll your own email
server. A couple people can mark you as spam, and BAM: now you are blacklisted
and you have to move to a new IP address. This is why most people turn to a
hosted service like <a href="https://mailchimp.com/">Mailchimp</a>. Though, I was
dissatisfied with that because of the <a href="https://mailchimp.com/pricing/">high costs and measly free
tier</a>.</p>
<p><a href="https://aws.amazon.com/ses/">Amazon Simple Email Service (SES)</a> deals with all
the complexity of email for you and is also
<a href="https://aws.amazon.com/ses/pricing/">cheap</a>. In fact, it’s free unless you have
more than 62,000 subscribers or post way more than around once a month, and even
after that it’s a dime for every 1,000 emails sent. Frankly, no one can really
compete with what Amazon is offering here.</p>
<p>Okay, so that covers sending the emails, but what about collecting and storing
subscriptions? SES doesn’t handle any of that. I searched around a long time for
something simple and free that wouldn’t require me setting up a server <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. I
eventually ended up going with <a href="https://sendy.co/">Sendy</a> because it looked like
a well-designed product exactly for this use case that also handled drafting
emails, email templates, confirmation emails, and analytics. It costs a one-time
fee of $59 and I was willing to fork that over for quality software. Especially
since most other email newsletter services require some sort of monthly
subscription that scales with the number of emails you are sending.</p>
<p>Unfortunately, since Sendy is self-hosted, I had to add a dynamic server to my
otherwise completely static Jekyll website hosted for free on <a href="https://pages.github.com/">Github
Pages</a>. You can put Sendy on pretty much anything
that runs PHP and MySQL including the cheap <a href="https://aws.amazon.com/ec2/instance-types/">t2.micro Amazon EC2 instance
type</a>. If you are clever, you might
find a cheaper way. I already had a t2.medium for general development,
tinkering, and hosting, so I just used that.</p>
<p>There are many guides out there for setting up MySQL and Apache, so I won’t go
over that. But, I do want to mention how I got Sendy to integrate with
<a href="https://nginx.org/en/">nginx</a> which is the server engine I was already using. I
like to put separate services I’m running under different subdomains of
my domain hallada.net even though they are running on the same server and IP
address. For Sendy, I chose <a href="http://list.hallada.net">list.hallada.net</a> <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.
Setting up another subdomain in nginx requires <a href="https://askubuntu.com/a/766369">creating a new server
block</a>. There’s <a href="https://gist.github.com/refringe/6545132">a great Gist of a config for
powering Sendy using nginx and
FastCGI</a>, but I ran into so many
issues with the subdomain that I decided to use nginx as a proxy to the Apache
mod_php site running Sendy. I’ll just post my config here:</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
<span class="kn">listen</span> <span class="s">[::]:80</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">list.hallada.net</span><span class="p">;</span>
<span class="kn">root</span> <span class="n">/var/www/html/sendy</span><span class="p">;</span>
<span class="kn">index</span> <span class="s">index.php</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/l/</span> <span class="p">{</span>
<span class="kn">rewrite</span> <span class="s">^/l/([a-zA-Z0-9/]+)</span>$ <span class="n">/l.php?i=</span><span class="nv">$1</span> <span class="s">last</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="n">/t/</span> <span class="p">{</span>
<span class="kn">rewrite</span> <span class="s">^/t/([a-zA-Z0-9/]+)</span>$ <span class="n">/t.php?i=</span><span class="nv">$1</span> <span class="s">last</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="n">/w/</span> <span class="p">{</span>
<span class="kn">rewrite</span> <span class="s">^/w/([a-zA-Z0-9/]+)</span>$ <span class="n">/w.php?i=</span><span class="nv">$1</span> <span class="s">last</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="n">/unsubscribe/</span> <span class="p">{</span>
<span class="kn">rewrite</span> <span class="s">^/unsubscribe/(.*)</span>$ <span class="n">/unsubscribe.php?i=</span><span class="nv">$1</span> <span class="s">last</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="n">/subscribe/</span> <span class="p">{</span>
<span class="kn">rewrite</span> <span class="s">^/subscribe/(.*)</span>$ <span class="n">/subscribe.php?i=</span><span class="nv">$1</span> <span class="s">last</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$remote_addr</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
<span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:8080/sendy/</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Basically, this proxies all of the requests through to Apache which I configured
to run on port 8080 by changing the <code class="language-plaintext highlighter-rouge">Listen</code> directive in
<code class="language-plaintext highlighter-rouge">/etc/apache2/ports.conf</code>.</p>
<p>I also had to add <code class="language-plaintext highlighter-rouge">RewriteBase /sendy</code> to the end of the <code class="language-plaintext highlighter-rouge">.htcaccess</code> file in
the sendy directory (which, for me, was in <code class="language-plaintext highlighter-rouge">/var/www/html/sendy</code>). This
basically forces Sendy to use urls that start with <code class="language-plaintext highlighter-rouge">http://list.hallada.net</code>
instead of <code class="language-plaintext highlighter-rouge">http://list.hallada.net/sendy</code> which I thought was redundant since I
am dedicating the whole subdomain to sendy.</p>
<p>A perplexing issue I ran into was that Gmail accounts were completely dropping
(not even bouncing!) any emails I sent to them if I used my personal email
<code class="language-plaintext highlighter-rouge">[email protected]</code> as the from address. I switched to <code class="language-plaintext highlighter-rouge">[email protected]</code> for
the from address and emails went through fine after that <sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">3</a></sup>. <a href="https://forums.aws.amazon.com/thread.jspa?messageID=802461&#802461">The issue seems
unresolved</a>
as of this post.</p>
<p>Lastly, I needed to create a form on my website for readers to sign up for the
mailing list. Sendy provides the HTML in the UI to create the form, which I
<a href="https://github.com/thallada/thallada.github.io/blob/master/_includes/mail-form.html">tweaked a
little</a>
and placed in a <a href="https://jekyllrb.com/docs/includes/">Jekyll includes template
partial</a> that I could include on both the
post layout and the blog index template. I refuse to pollute the internet with
yet another annoying email newsletter form that pops up while you are trying to
read the article, so you can find my current version at the bottom of this
article where it belongs <sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">4</a></sup>.</p>
<p>All in all, setting up a mailing list this way wasn’t too bad except for the part
where I spent way too much time fiddling with nginx configs. But, I always do
that, so I guess that’s expected.</p>
<p>As for the content of the newsletter, I haven’t figured out how to post the
entirety of a blog post into the HTML format of an email as soon as I commit a
new post yet. So, I think for now I will just manually create a new email
campaign in Sendy (from an email template) that will have a link to the new
post, and send that.</p>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>It would be interesting to look into creating a <a href="https://www.google.com/forms/about/">Google
Form</a> that submits rows to a <a href="https://www.google.com/sheets/about/">Google
Sheet</a> and then triggering a <a href="https://aws.amazon.com/lambda/">AWS
Lambda</a> service that iterates over the rows
using something like <a href="https://developers.google.com/sheets/api/quickstart/python">the Google Sheets Python
API</a> and sending
an email for every user using the <a href="http://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-api.html">Amazon SES
API</a>
(<a href="https://github.com/pankratiev/python-amazon-ses-api">python-amazon-ses-api</a>
might also be useful there). <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>I ran into a hiccup <a href="http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-domain-procedure.html">verifying this domain for Amazon
SES</a>
using the <a href="https://www.namecheap.com/">Namecheap</a> advanced DNS settings
because it only allowed me to set up one MX record, but I already had one
for my root hallada.net domain that I needed. So, I moved to <a href="https://aws.amazon.com/route53/">Amazon’s Route
53</a> instead <sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">5</a></sup> which made setting up the
<a href="http://docs.aws.amazon.com/ses/latest/DeveloperGuide/easy-dkim.html">DKIM
verification</a>
really easy since Amazon SES gave a button to create the necessary DNS
records directly in my Route 53 account. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:4" role="doc-endnote">
<p>Obviously a conspiracy by Google to force domination of Gmail. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:5" role="doc-endnote">
<p>Yes, I really hate those pop-ups. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:3" role="doc-endnote">
<p>As <a href="https://www.washingtonpost.com/business/is-amazon-getting-too-big/2017/07/28/ff38b9ca-722e-11e7-9eac-d56bd5568db8_story.html">Amazon continues its plan for world
domination</a>
it appears I’m moving more and more of my personal infrastructure over to
Amazon as well… <a href="#fnref:3" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
Wed, 30 Aug 2017 00:00:00 +0000
https://www.hallada.net/2017/08/30/making-mailing-list-jekyll-blog-using-sendy.html
https://www.hallada.net/2017/08/30/making-mailing-list-jekyll-blog-using-sendy.htmlProximity Structures: Playing around with PixiJS<p>I’ve been messing around with a library called <a href="http://www.pixijs.com/">PixiJS</a>
which allows you to create WebGL animations which will fall back to HTML5 canvas
if WebGL is not available in the browser. I mostly like it because the API is
similar to HTML5 canvas which <a href="https://github.com/thallada/thallada.github.io/blob/master/js/magic.js">I was already familiar
with</a>. I
can’t say that I like the PixiJS API and documentation that much, though. For
this project, I mostly just used a small portion of it to create <a href="http://www.goodboydigital.com/pixi-webgl-primitives/">WebGL (GPU
accelerated) primitive
shapes</a> (lines and
circles).
<!--excerpt--></p>
<p><strong>Play with it here</strong>: <a href="http://proximity.hallada.net">http://proximity.hallada.net</a></p>
<p><strong>Read/clone the code here</strong>: <a href="https://github.com/thallada/proximity-structures">https://github.com/thallada/proximity-structures</a></p>
<p><a href="http://proximity.hallada.net"><img src="/img/blog/proximity-structures.gif" alt="The animation in
action" /></a></p>
<p>The idea was inspired by
<a href="https://thumb9.shutterstock.com/display_pic_with_logo/3217643/418838422/stock-vector-abstract-technology-futuristic-network-418838422.jpg">all</a>
<a href="https://ak5.picdn.net/shutterstock/videos/27007555/thumb/10.jpg">those</a>
<a href="https://ak9.picdn.net/shutterstock/videos/10477484/thumb/1.jpg">countless</a>
<a href="https://ak3.picdn.net/shutterstock/videos/25825727/thumb/1.jpg">node</a>
<a href="https://t4.ftcdn.net/jpg/00/93/24/21/500_F_93242102_mqtDljufY7CNY0wMxunSbyDi23yNs1DU.jpg">network</a>
<a href="https://ak6.picdn.net/shutterstock/videos/12997085/thumb/1.jpg">graphics</a> that
I see all the time as stock graphics on generic tech articles.</p>
<p>This was really fun to program. I didn’t care much about perfect code, I just
kept hacking one thing onto another while watching the instantaneous feedback of
the points and lines responding to my changes until I had something worth
sharing.</p>
<h3 id="details">Details</h3>
<p>The majority of the animation you see is based on
<a href="https://en.wikipedia.org/wiki/Inbetweening">tweening</a>. Each point has an origin
and destination stored in memory. Every clock tick (orchestrated by the almighty
<a href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame</a>),
the main loop calculates where each point should be in the path between its
origin and destination based on how long until it completes its “cycle”. There
is a global <code class="language-plaintext highlighter-rouge">cycleDuration</code>, defaulted to 60. Every frame increments the cycle
counter by 1 until it reaches 60, at which point it folds over back to 0. Every
point is assigned a number between 1 and 60. This is its start cycle. When the
global cycle counter equals a point’s start cycle number, the point has reached
its destination and a new target destination is randomly chosen.</p>
<p>Each point is also randomly assigned a color. When a point is within
<code class="language-plaintext highlighter-rouge">connectionDistance</code> of another point in the canvas, a line is drawn between the
two points, their colors are averaged, and the points’ colors become the average
color weighted by the distance between the points. You can see clusters of
points converging on a color in the animation.</p>
<p>Click interaction is implemented by modifying point target destinations within a
radius around the click. Initially, a mouse hover will push points away.
Clicking and holding will draw points in, progressively growing the effect
radius in the process to capture more and more points.</p>
<p>I thought it was really neat that without integrating any physics engine
whatsoever, I ended up with something that looked sort of physics based thanks
to the tweening functions. Changing the tweening functions that the points use
seems to change the physical properties and interactions of the points. The
elastic tweening function makes the connections between the points snap like
rubber bands. And, while I am not drawing any explicit polygons, just points and
lines based on proximity, it sometimes looks like the points are coalescing into
some three-dimensional structure.</p>
<p>I’ll probably make another procedural animation like this in the future since it
was so fun. Next time, I’ll probably start from the get-go in ES2015 (or ES7,
or ES8??) and proper data structures.</p>
Mon, 07 Aug 2017 00:00:00 +0000
https://www.hallada.net/2017/08/07/proximity-structures.html
https://www.hallada.net/2017/08/07/proximity-structures.htmlGenerating Random Poems with Python<p>In this post, I will demonstrate how to generate random text using a few lines
of standard python and then progressively refine the output until it looks
poem-like.</p>
<p>If you would like to follow along with this post and run the code snippets
yourself, you can clone <a href="https://github.com/thallada/nlp/">my NLP repository</a>
and run <a href="https://github.com/thallada/nlp/blob/master/edX%20Lightning%20Talk.ipynb">the Jupyter
notebook</a>.</p>
<p>You might not realize it, but you probably use an app everyday that can generate
random text that sounds like you: your phone keyboard.
<!--excerpt--></p>
<p><img src="/img/blog/phone_keyboard.jpg" alt="Suggested next words UI feature on the iOS
keyboard" /></p>
<p>Just by tapping the next suggested word over and over, you can generate text. So how does it work?</p>
<h2 id="corpus">Corpus</h2>
<p>First, we need a <strong>corpus</strong>: the text our generator will recombine into new
sentences. In the case of your phone keyboard, this is all the text you’ve ever
typed into your keyboard. For our example, let’s just start with one sentence:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">corpus</span> <span class="o">=</span> <span class="s">'The quick brown fox jumps over the lazy dog'</span>
</code></pre></div></div>
<h2 id="tokenization">Tokenization</h2>
<p>Now we need to split this corpus into individual <strong>tokens</strong> that we can operate
on. Since our objective is to eventually predict the next word from the previous
word, we will want our tokens to be individual words. This process is called
<strong>tokenization</strong>. The simplest way to tokenize a sentence into words is to split
on spaces:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">words</span> <span class="o">=</span> <span class="n">corpus</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">' '</span><span class="p">)</span>
<span class="n">words</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="s">'The'</span><span class="p">,</span> <span class="s">'quick'</span><span class="p">,</span> <span class="s">'brown'</span><span class="p">,</span> <span class="s">'fox'</span><span class="p">,</span> <span class="s">'jumps'</span><span class="p">,</span> <span class="s">'over'</span><span class="p">,</span> <span class="s">'the'</span><span class="p">,</span> <span class="s">'lazy'</span><span class="p">,</span> <span class="s">'dog'</span><span class="p">]</span>
</code></pre></div></div>
<h2 id="bigrams">Bigrams</h2>
<p>Now, we will want to create <strong>bigrams</strong>. A bigram is a pair of two words that
are in the order they appear in the corpus. To create bigrams, we will iterate
through the list of the words with two indices, one of which is offset by one:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bigrams</span> <span class="o">=</span> <span class="p">[</span><span class="n">b</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">words</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="n">words</span><span class="p">[</span><span class="mi">1</span><span class="p">:])]</span>
<span class="n">bigrams</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[(</span><span class="s">'The'</span><span class="p">,</span> <span class="s">'quick'</span><span class="p">),</span>
<span class="p">(</span><span class="s">'quick'</span><span class="p">,</span> <span class="s">'brown'</span><span class="p">),</span>
<span class="p">(</span><span class="s">'brown'</span><span class="p">,</span> <span class="s">'fox'</span><span class="p">),</span>
<span class="p">(</span><span class="s">'fox'</span><span class="p">,</span> <span class="s">'jumps'</span><span class="p">),</span>
<span class="p">(</span><span class="s">'jumps'</span><span class="p">,</span> <span class="s">'over'</span><span class="p">),</span>
<span class="p">(</span><span class="s">'over'</span><span class="p">,</span> <span class="s">'the'</span><span class="p">),</span>
<span class="p">(</span><span class="s">'the'</span><span class="p">,</span> <span class="s">'lazy'</span><span class="p">),</span>
<span class="p">(</span><span class="s">'lazy'</span><span class="p">,</span> <span class="s">'dog'</span><span class="p">)]</span>
</code></pre></div></div>
<p>How do we use the bigrams to predict the next word given the first word?</p>
<p>Return every second element where the first element matches the <strong>condition</strong>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">condition</span> <span class="o">=</span> <span class="s">'the'</span>
<span class="n">next_words</span> <span class="o">=</span> <span class="p">[</span><span class="n">bigram</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">bigram</span> <span class="ow">in</span> <span class="n">bigrams</span>
<span class="k">if</span> <span class="n">bigram</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="n">condition</span><span class="p">]</span>
<span class="n">next_words</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="s">'quick'</span><span class="p">,</span> <span class="s">'lazy'</span><span class="p">]</span>
</code></pre></div></div>
<p>We have now found all of the possible words that can follow the condition “the”
according to our corpus: “quick” and “lazy”.</p>
<pre>
(<span style="color:blue">The</span> <span style="color:red">quick</span>) (quick brown) ... (<span style="color:blue">the</span> <span style="color:red">lazy</span>) (lazy dog)
</pre>
<p>Either “<span style="color:red">quick</span>” or “<span style="color:red">lazy</span>” could be the next word.</p>
<h2 id="trigrams-and-n-grams">Trigrams and N-grams</h2>
<p>We can partition our corpus into groups of threes too:</p>
<pre>
(<span style="color:blue">The</span> <span style="color:red">quick brown</span>) (quick brown fox) ... (<span style="color:blue">the</span> <span style="color:red">lazy dog</span>)
</pre>
<p>Or, the condition can be two words (<code class="language-plaintext highlighter-rouge">condition = 'the lazy'</code>):</p>
<pre>
(The quick brown) (quick brown fox) ... (<span style="color:blue">the lazy</span> <span style="color:red">dog</span>)
</pre>
<p>These are called <strong>trigrams</strong>.</p>
<p>We can partition any <strong>N</strong> number of words together as <strong>n-grams</strong>.</p>
<h2 id="conditional-frequency-distributions">Conditional Frequency Distributions</h2>
<p>Earlier, we were able to compute the list of possible words to follow a
condition:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">next_words</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="s">'quick'</span><span class="p">,</span> <span class="s">'lazy'</span><span class="p">]</span>
</code></pre></div></div>
<p>But, in order to predict the next word, what we really want to compute is what
is the most likely next word out of all of the possible next words. In other
words, find the word that occurred the most often after the condition in the
corpus.</p>
<p>We can use a <strong>Conditional Frequency Distribution (CFD)</strong> to figure that out! A
<strong>CFD</strong> can tell us: given a <strong>condition</strong>, what is <strong>likelihood</strong> of each
possible outcome.</p>
<p>This is an example of a CFD with two conditions, displayed in table form. It is
counting words appearing in a text collection (source: nltk.org).</p>
<p><img src="http://www.nltk.org/images/tally2.png" alt="Two tables, one for each condition: "News" and "Romance". The first column of
each table is 5 words: "the", "cute", "Monday", "could", and "will". The second
column is a tally of how often the word at the start of the row appears in the
corpus." /></p>
<p>Let’s change up our corpus a little to better demonstrate the CFD:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">words</span> <span class="o">=</span> <span class="p">(</span><span class="s">'The quick brown fox jumped over the '</span>
<span class="s">'lazy dog and the quick cat'</span><span class="p">).</span><span class="n">split</span><span class="p">(</span><span class="s">' '</span><span class="p">)</span>
<span class="k">print</span> <span class="n">words</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="s">'The'</span><span class="p">,</span> <span class="s">'quick'</span><span class="p">,</span> <span class="s">'brown'</span><span class="p">,</span> <span class="s">'fox'</span><span class="p">,</span> <span class="s">'jumped'</span><span class="p">,</span> <span class="s">'over'</span><span class="p">,</span> <span class="s">'the'</span><span class="p">,</span> <span class="s">'lazy'</span><span class="p">,</span> <span class="s">'dog'</span><span class="p">,</span> <span class="s">'and'</span><span class="p">,</span> <span class="s">'the'</span><span class="p">,</span> <span class="s">'quick'</span><span class="p">,</span> <span class="s">'cat'</span><span class="p">]</span>
</code></pre></div></div>
<p>Now, let’s build the CFD. I use
<a href="https://docs.python.org/2/library/collections.html#defaultdict-objects"><code class="language-plaintext highlighter-rouge">defaultdicts</code></a>
to avoid having to initialize every new dict.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">defaultdict</span>
<span class="n">cfd</span> <span class="o">=</span> <span class="n">defaultdict</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">defaultdict</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="mi">0</span><span class="p">))</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">words</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span><span class="p">):</span> <span class="c1"># loop to the next-to-last word
</span> <span class="n">cfd</span><span class="p">[</span><span class="n">words</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">lower</span><span class="p">()][</span><span class="n">words</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">].</span><span class="n">lower</span><span class="p">()]</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="c1"># pretty print the defaultdict
</span><span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="nb">dict</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="nb">dict</span><span class="p">(</span><span class="n">cfd</span><span class="p">).</span><span class="n">items</span><span class="p">()}</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">'and'</span><span class="p">:</span> <span class="p">{</span><span class="s">'the'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'brown'</span><span class="p">:</span> <span class="p">{</span><span class="s">'fox'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'dog'</span><span class="p">:</span> <span class="p">{</span><span class="s">'and'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'fox'</span><span class="p">:</span> <span class="p">{</span><span class="s">'jumped'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'jumped'</span><span class="p">:</span> <span class="p">{</span><span class="s">'over'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'lazy'</span><span class="p">:</span> <span class="p">{</span><span class="s">'dog'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'over'</span><span class="p">:</span> <span class="p">{</span><span class="s">'the'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'quick'</span><span class="p">:</span> <span class="p">{</span><span class="s">'brown'</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
<span class="s">'the'</span><span class="p">:</span> <span class="p">{</span><span class="s">'lazy'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">'quick'</span><span class="p">:</span> <span class="mi">2</span><span class="p">}}</span>
</code></pre></div></div>
<p>So, what’s the most likely word to follow <code class="language-plaintext highlighter-rouge">'the'</code>?</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">max</span><span class="p">(</span><span class="n">cfd</span><span class="p">[</span><span class="s">'the'</span><span class="p">])</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">'quick'</span>
</code></pre></div></div>
<p>Whole sentences can be the conditions and values too. Which is basically the way
<a href="http://www.cleverbot.com/">cleverbot</a> works.</p>
<p><img src="/img/blog/cleverbot.jpg" alt="An example of a conversation with Cleverbot" /></p>
<h2 id="random-text">Random Text</h2>
<p>Lets put this all together, and with a little help from
<a href="http://www.nltk.org/">nltk</a> generate some random text.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">nltk</span>
<span class="kn">import</span> <span class="nn">random</span>
<span class="n">TEXT</span> <span class="o">=</span> <span class="n">nltk</span><span class="p">.</span><span class="n">corpus</span><span class="p">.</span><span class="n">gutenberg</span><span class="p">.</span><span class="n">words</span><span class="p">(</span><span class="s">'austen-emma.txt'</span><span class="p">)</span>
<span class="c1"># NLTK shortcuts :)
</span><span class="n">bigrams</span> <span class="o">=</span> <span class="n">nltk</span><span class="p">.</span><span class="n">bigrams</span><span class="p">(</span><span class="n">TEXT</span><span class="p">)</span>
<span class="n">cfd</span> <span class="o">=</span> <span class="n">nltk</span><span class="p">.</span><span class="n">ConditionalFreqDist</span><span class="p">(</span><span class="n">bigrams</span><span class="p">)</span>
<span class="c1"># pick a random word from the corpus to start with
</span><span class="n">word</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">choice</span><span class="p">(</span><span class="n">TEXT</span><span class="p">)</span>
<span class="c1"># generate 15 more words
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">15</span><span class="p">):</span>
<span class="k">print</span> <span class="n">word</span><span class="p">,</span>
<span class="k">if</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">cfd</span><span class="p">:</span>
<span class="n">word</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">choice</span><span class="p">(</span><span class="n">cfd</span><span class="p">[</span><span class="n">word</span><span class="p">].</span><span class="n">keys</span><span class="p">())</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">break</span>
</code></pre></div></div>
<p>Which outputs something like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>her reserve and concealment towards some feelings in moving slowly together .
You will shew
</code></pre></div></div>
<p>Great! This is basically what the phone keyboard suggestions are doing. Now how
do we take this to the next level and generate text that looks like a poem?</p>
<h2 id="random-poems">Random Poems</h2>
<p>Generating random poems is accomplished by limiting the choice of the next word
by some constraint:</p>
<ul>
<li>words that rhyme with the previous line</li>
<li>words that match a certain syllable count</li>
<li>words that alliterate with words on the same line</li>
<li>etc.</li>
</ul>
<h2 id="rhyming">Rhyming</h2>
<h3 id="written-english--spoken-english">Written English != Spoken English</h3>
<p>English has a highly <strong>nonphonemic orthography</strong>, meaning that the letters often
have no correspondence to the pronunciation. E.g.:</p>
<blockquote>
<p>“meet” vs. “meat”</p>
</blockquote>
<p>The vowels are spelled differently, yet they rhyme <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>.</p>
<p>So if the spelling of the words is useless in telling us if two words rhyme,
what can we use instead?</p>
<h3 id="international-phonetic-alphabet-ipa">International Phonetic Alphabet (IPA)</h3>
<p>The IPA is an alphabet that can represent all varieties of human pronunciation.</p>
<ul>
<li>meet: /mit/</li>
<li>meat: /mit/</li>
</ul>
<p>Note that this is only the IPA transcription for only one <strong>accent</strong> of English.
Some English speakers may pronounce these words differently which could be
represented by a different IPA transcription.</p>
<h2 id="syllables">Syllables</h2>
<p>How can we determine the number of syllables in a word? Let’s consider the two
words “poet” and “does”:</p>
<ul>
<li>“poet” = 2 syllables</li>
<li>“does” = 1 syllable</li>
</ul>
<p>The vowels in these two words are written the same, but are pronounced
differently with a different number of syllables.</p>
<p>Can the IPA tell us the number of syllables in a word too?</p>
<ul>
<li>poet: /ˈpoʊət/</li>
<li>does: /ˈdʌz/</li>
</ul>
<p>Not really… We cannot easily identify the number of syllables from those
transcriptions. Sometimes the transcriber denotes syllable breaks with a <code class="language-plaintext highlighter-rouge">.</code> or
a <code class="language-plaintext highlighter-rouge">'</code>, but sometimes they don’t.</p>
<h3 id="arpabet">Arpabet</h3>
<p>The Arpabet is a phonetic alphabet developed by ARPA in the 70s that:</p>
<ul>
<li>Encodes phonemes specific to American English.</li>
<li>Meant to be a machine readable code. It is ASCII only.</li>
<li>Denotes how stressed every vowel is from 0-2.</li>
</ul>
<p>This is perfect! Because of that third bullet, a word’s syllable count equals
the number of digits in the Arpabet encoding.</p>
<h3 id="cmu-pronouncing-dictionary-cmudict">CMU Pronouncing Dictionary (CMUdict)</h3>
<p>A large open source dictionary of English words to North American pronunciations
in Arpanet encoding. Conveniently, it is also in NLTK…</p>
<h3 id="counting-syllables">Counting Syllables</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">string</span>
<span class="kn">from</span> <span class="nn">nltk.corpus</span> <span class="kn">import</span> <span class="n">cmudict</span>
<span class="n">cmu</span> <span class="o">=</span> <span class="n">cmudict</span><span class="p">.</span><span class="nb">dict</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">count_syllables</span><span class="p">(</span><span class="n">word</span><span class="p">):</span>
<span class="n">lower_word</span> <span class="o">=</span> <span class="n">word</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span>
<span class="k">if</span> <span class="n">lower_word</span> <span class="ow">in</span> <span class="n">cmu</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">max</span><span class="p">([</span><span class="nb">len</span><span class="p">([</span><span class="n">y</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="n">x</span> <span class="k">if</span> <span class="n">y</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="ow">in</span> <span class="n">string</span><span class="p">.</span><span class="n">digits</span><span class="p">])</span>
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">cmu</span><span class="p">[</span><span class="n">lower_word</span><span class="p">]])</span>
<span class="k">print</span><span class="p">(</span><span class="s">"poet: {}</span><span class="se">\n</span><span class="s">does: {}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">count_syllables</span><span class="p">(</span><span class="s">"poet"</span><span class="p">),</span>
<span class="n">count_syllables</span><span class="p">(</span><span class="s">"does"</span><span class="p">)))</span>
</code></pre></div></div>
<p>Results in:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>poet: 2
does: 1
</code></pre></div></div>
<h2 id="buzzfeed-haiku-generator">Buzzfeed Haiku Generator</h2>
<p>To see this in action, try out a haiku generator I created that uses Buzzfeed
article titles as a corpus. It does not incorporate rhyming, it just counts the
syllables to make sure it’s <a href="https://en.wikipedia.org/wiki/Haiku">5-7-5</a>. You can view the full code
<a href="https://github.com/thallada/nlp/blob/master/generate_poem.py">here</a>.</p>
<p><img src="/img/blog/buzzfeed.jpg" alt="Buzzfeed Haiku Generator" /></p>
<p>Run it live at:
<a href="http://mule.hallada.net/nlp/buzzfeed-haiku-generator/">http://mule.hallada.net/nlp/buzzfeed-haiku-generator/</a></p>
<h2 id="syntax-aware-generation">Syntax-aware Generation</h2>
<p>Remember these?</p>
<p><img src="/img/blog/madlibs.jpg" alt="Example Mad Libs: "A Visit to the Dentist"" /></p>
<p>Mad Libs worked so well because they forced the random words (chosen by the
players) to fit into the syntactical structure and parts-of-speech of an
existing sentence.</p>
<p>You end up with <strong>syntactically</strong> correct sentences that are <strong>semantically</strong>
random. We can do the same thing!</p>
<h3 id="nltk-syntax-trees">NLTK Syntax Trees!</h3>
<p>NLTK can parse any sentence into a <a href="http://www.nltk.org/book/ch08.html">syntax
tree</a>. We can utilize this syntax tree
during poetry generation.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">stat_parser</span> <span class="kn">import</span> <span class="n">Parser</span>
<span class="n">parsed</span> <span class="o">=</span> <span class="n">Parser</span><span class="p">().</span><span class="n">parse</span><span class="p">(</span><span class="s">'The quick brown fox jumps over the lazy dog.'</span><span class="p">)</span>
<span class="k">print</span> <span class="n">parsed</span>
</code></pre></div></div>
<p>Syntax tree output as an
<a href="https://en.wikipedia.org/wiki/S-expression">s-expression</a>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(S
(NP (DT the) (NN quick))
(VP
(VB brown)
(NP
(NP (JJ fox) (NN jumps))
(PP (IN over) (NP (DT the) (JJ lazy) (NN dog)))))
(. .))
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">parsed</span><span class="p">.</span><span class="n">pretty_print</span><span class="p">()</span>
</code></pre></div></div>
<p>And the same tree visually pretty printed in ASCII:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> S
________________________|__________________________
| VP |
| ____|_____________ |
| | NP |
| | _________|________ |
| | | PP |
| | | ________|___ |
NP | NP | NP |
___|____ | ___|____ | _______|____ |
DT NN VB JJ NN IN DT JJ NN .
| | | | | | | | | |
the quick brown fox jumps over the lazy dog .
</code></pre></div></div>
<p>NLTK also performs <a href="http://www.nltk.org/book/ch05.html">part-of-speech tagging</a>
on the input sentence and outputs the tag at each node in the tree. Here’s what
each of those mean:</p>
<table>
<tbody>
<tr>
<td><strong>S</strong></td>
<td>Sentence</td>
</tr>
<tr>
<td><strong>VP</strong></td>
<td>Verb Phrase</td>
</tr>
<tr>
<td><strong>NP</strong></td>
<td>Noun Phrase</td>
</tr>
<tr>
<td><strong>DT</strong></td>
<td>Determiner</td>
</tr>
<tr>
<td><strong>NN</strong></td>
<td>Noun (common, singular)</td>
</tr>
<tr>
<td><strong>VB</strong></td>
<td>Verb (base form)</td>
</tr>
<tr>
<td><strong>JJ</strong></td>
<td>Adjective (or numeral, ordinal)</td>
</tr>
<tr>
<td><strong>.</strong></td>
<td>Punctuation</td>
</tr>
</tbody>
</table>
<p>Now, let’s use this information to swap matching syntax sub-trees between two
corpora (<a href="https://github.com/thallada/nlp/blob/master/syntax_aware_generate.py">source for the generate
function</a>).</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">syntax_aware_generate</span> <span class="kn">import</span> <span class="n">generate</span>
<span class="c1"># inserts matching syntax subtrees from trump.txt into
# trees from austen-emma.txt
</span><span class="n">generate</span><span class="p">(</span><span class="s">'trump.txt'</span><span class="p">,</span> <span class="n">word_limit</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(SBARQ
(SQ
(NP (PRP I))
(VP (VBP do) (RB not) (VB advise) (NP (DT the) (NN custard))))
(. .))
I do not advise the custard .
==============================
I do n't want the drone !
(SBARQ
(SQ
(NP (PRP I))
(VP (VBP do) (RB n't) (VB want) (NP (DT the) (NN drone))))
(. !))
</code></pre></div></div>
<p>Above the line is a sentence selected from a corpus of Jane Austen’s <em>Emma</em>.
Below it is a sentence generated by walking down the syntax tree and finding
sub-trees from a corpus of Trump’s tweets that match the same syntactical
structure and then swapping the words in.</p>
<p>The result can sometimes be amusing, but more often than not, this approach
doesn’t fare much better than the n-gram based generation.</p>
<h3 id="spacy">spaCy</h3>
<p>I’m only beginning to experiment with the <a href="https://spacy.io/">spaCy</a> Python
library, but I like it a lot. For one, it is much, much faster than NLTK:</p>
<p><img src="/img/blog/spacy_speed.jpg" alt="spaCy speed comparison" /></p>
<p><a href="https://spacy.io/docs/api/#speed-comparison">https://spacy.io/docs/api/#speed-comparison</a></p>
<p>The <a href="https://spacy.io/docs/api/">API</a> takes a little getting used to coming from
NLTK. It doesn’t seem to have any sort of out-of-the-box solution to printing
out syntax trees like above, but it does do <a href="https://spacy.io/docs/api/tagger">part-of-speech
tagging</a> and <a href="https://spacy.io/docs/api/dependencyparser">dependency relation
mapping</a> which should accomplish
about the same. You can see both of these visually with
<a href="https://demos.explosion.ai/displacy/">displaCy</a>.</p>
<h2 id="neural-network-based-generation">Neural Network Based Generation</h2>
<p>If you haven’t heard all the buzz about <a href="https://en.wikipedia.org/wiki/Artificial_neural_network">neural
networks</a>, they are a
particular technique for <a href="https://en.wikipedia.org/wiki/Machine_learning">machine
learning</a> that’s inspired by our
understanding of the human brain. They are structured into layers of nodes which
have connections to other nodes in other layers of the network. These
connections have weights which each node multiplies by the corresponding input
and enters into a particular <a href="https://en.wikipedia.org/wiki/Activation_function">activation
function</a> to output a single
number. The optimal weights for solving a particular problem with the network
are learned by training the network using
<a href="https://en.wikipedia.org/wiki/Backpropagation">backpropagation</a> to perform
<a href="https://en.wikipedia.org/wiki/Gradient_descent">gradient descent</a> on a
particular <a href="https://en.wikipedia.org/wiki/Loss_function">cost function</a> that
tries to balance getting the correct answer while also
<a href="https://en.wikipedia.org/wiki/Regularization_(mathematics)">generalizing</a> the
network enough to perform well on data the network hasn’t seen before.</p>
<p><a href="https://en.wikipedia.org/wiki/Long_short-term_memory">Long short-term memory
(LSTM)</a> is a type of
<a href="https://en.wikipedia.org/wiki/Recurrent_neural_network">recurrent neural network
(RNN)</a> (a network with
cycles) that can remember previous values for a short or long period of time.
This property makes them remarkably effective at a multitude of tasks, one of
which is predicting text that will follow a given sequence. We can use this to
continually generate text by inputting a seed, appending the generated output to
the end of the seed, removing the first element from the beginning of the seed,
and then inputting the seed again, following the same process until we’ve
generated enough text from the network (<a href="http://www.cs.utoronto.ca/~ilya/pubs/2011/LANG-RNN.pdf">paper on using RNNs to generate
text</a>).</p>
<p>Luckily, a lot of smart people have done most of the legwork so you can just
download their neural network architecture and train it yourself. There’s
<a href="https://github.com/karpathy/char-rnn">char-rnn</a> which has some <a href="http://karpathy.github.io/2015/05/21/rnn-effectiveness/">really exciting
results for generating texts (e.g. fake
Shakespeare)</a>. There’s
also <a href="https://github.com/larspars/word-rnn">word-rnn</a> which is a modified
version of char-rnn that operates on words as a unit instead of characters.
Follow <a href="/2017/06/20/how-to-install-tensorflow-on-ubuntu-16-04.html">my last blog post on how to install TensorFlow on Ubuntu
16.04</a> and
you’ll be almost ready to run a TensorFlow port of word-rnn:
<a href="https://github.com/hunkim/word-rnn-tensorflow">word-rnn-tensorflow</a>.</p>
<p>I plan on playing around with NNs a lot more to see what kind of poetry-looking
text I can generate from them.</p>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Fun fact: They used to be pronounced differently in Middle English during
the invention of the printing press and standardized spelling. The <a href="https://en.wikipedia.org/wiki/Great_Vowel_Shift">Great
Vowel Shift</a> happened
after, and is why they are now pronounced the same. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
Tue, 11 Jul 2017 00:00:00 +0000
https://www.hallada.net/2017/07/11/generating-random-poems-with-python.html
https://www.hallada.net/2017/07/11/generating-random-poems-with-python.htmlHow to Install TensorFlow on Ubuntu 16.04 with GPU Support<p>I found the <a href="https://www.tensorflow.org/install/install_linux">tensorflow
documentation</a> rather lacking
for installation instructions, especially in regards to getting GPU support.
I’m going to write down my notes from wrangling with the installation here for
future reference and hopefully this helps someone else too.
<!--excerpt--></p>
<p>This will invariably go out-of-date at some point, so be mindful of the publish
date of this post. Make sure to cross-reference other documentation that has
more up-to-date information.</p>
<h2 id="assumptions">Assumptions</h2>
<p>These instructions are very specific to my environment, so this is what I am
assuming:</p>
<ol>
<li>You are running Ubuntu 16.04. (I have 16.04.1)
<ul>
<li>You can check this in the output of <code class="language-plaintext highlighter-rouge">uname -a</code></li>
</ul>
</li>
<li>You have a 64 bit machine.
<ul>
<li>You can check this with <code class="language-plaintext highlighter-rouge">uname -m</code>. (should say <code class="language-plaintext highlighter-rouge">x86_64</code>)</li>
</ul>
</li>
<li>You have an NVIDIA GPU that has CUDA Compute Capability 3.0 or higher.
<a href="https://developer.nvidia.com/cuda-gpus">NVIDIA documentation</a> has a full table
of cards and their Compute Capabilities. (I have a GeForce GTX 980 Ti)
<ul>
<li>You can check what card you have in Settings > Details under the label
“Graphics”</li>
<li>You can also check by verifying there is any output when you run <code class="language-plaintext highlighter-rouge">lspci |
grep -i nvidia</code></li>
</ul>
</li>
<li>You have a linux kernel version 4.4.0 or higher. (I have 4.8.0)
<ul>
<li>You can check this by running <code class="language-plaintext highlighter-rouge">uname -r</code></li>
</ul>
</li>
<li>You have gcc version 5.3.1 or higher installed. (I have 5.4.0)
<ul>
<li>You can check this by running <code class="language-plaintext highlighter-rouge">gcc --version</code></li>
</ul>
</li>
<li>You have the latest <a href="https://i.imgur.com/8osspXj.jpg">proprietary</a> NVIDIA
drivers installed.
<ul>
<li>You can check this and install it if you haven’t in the “Additional
Drivers” tab in the “Software & Updates” application (<code class="language-plaintext highlighter-rouge">update-manager</code>).
(I have version 375.66 installed)</li>
</ul>
</li>
<li>You have the kernel headers installed.
<ul>
<li>Just run <code class="language-plaintext highlighter-rouge">sudo apt-get install linux-headers-$(uname -r)</code> to install them
if you don’t have them installed already.</li>
</ul>
</li>
<li>You have Python installed. The exact version shouldn’t matter, but for the
rest of this post I’m going to assume you have <code class="language-plaintext highlighter-rouge">python3</code> installed.
<ul>
<li>You can install <code class="language-plaintext highlighter-rouge">python3</code> by running <code class="language-plaintext highlighter-rouge">sudo apt-get install python3</code>. This
will install Python 3.5.</li>
<li>Bonus points: you can install Python 3.6 by following <a href="https://askubuntu.com/a/865569">this
answer</a>, but Python 3.5 should be fine.</li>
</ul>
</li>
</ol>
<h2 id="install-the-cuda-toolkit-80">Install the CUDA Toolkit 8.0</h2>
<p>NVIDIA has <a href="http://docs.nvidia.com/cuda/cuda-installation-guide-linux/">a big scary documentation
page</a> on this, but I
will summarize the only the parts you need to know here.</p>
<p>Go to the <a href="https://developer.nvidia.com/cuda-downloads">CUDA Toolkit Download</a>
page. Click Linux > x86_64 > Ubuntu > 16.04 > deb (network).</p>
<p>Click download and then follow the instructions, copied here:</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">sudo dpkg -i cuda-repo-ubuntu1604_8.0.61-1_amd64.deb</code></li>
<li><code class="language-plaintext highlighter-rouge">sudo apt-get update</code></li>
<li><code class="language-plaintext highlighter-rouge">sudo apt-get install cuda</code></li>
</ol>
<p>This will install CUDA 8.0. It installed it to the directory
<code class="language-plaintext highlighter-rouge">/usr/local/cuda-8.0/</code> on my machine.</p>
<p>There are some <a href="http://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#post-installation-actions">post-install
actions</a>
we must follow:</p>
<ol>
<li>Edit your <code class="language-plaintext highlighter-rouge">~/.bashrc</code>
<ul>
<li>Use your favorite editor <code class="language-plaintext highlighter-rouge">gedit ~/.bashrc</code>, <code class="language-plaintext highlighter-rouge">nano ~/.bashrc</code>, <code class="language-plaintext highlighter-rouge">vim
~/.bashrc</code>, whatever.</li>
</ul>
</li>
<li>Add the following lines to the end of the file:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># CUDA 8.0 (nvidia) paths</span>
<span class="nb">export </span><span class="nv">CUDA_HOME</span><span class="o">=</span>/usr/local/cuda-8.0
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span>/usr/local/cuda-8.0/bin<span class="k">${</span><span class="nv">PATH</span>:+:<span class="k">${</span><span class="nv">PATH</span><span class="k">}}</span>
<span class="nb">export </span><span class="nv">LD_LIBRARY_PATH</span><span class="o">=</span>/usr/local/cuda-8.0/lib64<span class="k">${</span><span class="nv">LD_LIBRARY_PATH</span>:+:<span class="k">${</span><span class="nv">LD_LIBRARY_PATH</span><span class="k">}}</span>
</code></pre></div> </div>
</li>
<li>Save and exit.</li>
<li>Run <code class="language-plaintext highlighter-rouge">source ~/.bashrc</code>.</li>
<li>Install writable samples by running the script <code class="language-plaintext highlighter-rouge">cuda-install-samples-8.0.sh
~/</code>.
<ul>
<li>If the script cannot be found, the above steps didn’t work :(</li>
<li>I don’t actually know if the samples are absolutely required for what I’m
using CUDA for, but it’s recommended according to NVIDIA, and compiling
them will output a nifty <code class="language-plaintext highlighter-rouge">deviceQuery</code> binary which can be ran to test if
everything is working properly.</li>
</ul>
</li>
<li>Make sure <code class="language-plaintext highlighter-rouge">nvcc -V</code> outputs something.
<ul>
<li>If an error, the above steps 1-4 didn’t work :(</li>
</ul>
</li>
<li><code class="language-plaintext highlighter-rouge">cd ~/NVIDIA_CUDA-8.0_Samples</code>, cross your fingers, and run <code class="language-plaintext highlighter-rouge">make</code>
<ul>
<li>The compile will take a while</li>
<li>My compile actually errored near the end with an error about <code class="language-plaintext highlighter-rouge">/usr/bin/ld:
cannot find -lnvcuvid</code> I <em>think</em> that doesn’t really matter because the
binary files were still output.</li>
</ul>
</li>
<li>Try running <code class="language-plaintext highlighter-rouge">~/NVIDIA_CUDA-8.0_Samples/bin/x86_64/linux/release/deviceQuery</code>
to see if you get any output. Hopefully you will see your GPU listed.</li>
</ol>
<h2 id="install-cudnn-v51">Install cuDNN v5.1</h2>
<p><a href="https://askubuntu.com/a/767270">This AskUbuntu answer</a> has good instructions.
Here are the instructions specific to this set-up:</p>
<ol>
<li>Visit the <a href="https://developer.nvidia.com/cudnn">NVIDIA cuDNN page</a> and click
“Download”.</li>
<li>Join the program and fill out the survey.</li>
<li>Agree to the terms of service.</li>
<li>Click the link for “Download cuDNN v5.1 (Jan 20, 2017), for CUDA 8.0”</li>
<li>Download the “cuDNN v5.1 Library for Linux” (3rd link from the top).</li>
<li>Untar the downloaded file. E.g.:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/Downloads
<span class="nb">tar</span> <span class="nt">-xvf</span> cudnn-8.0-linux-x64-v5.1.tgz
</code></pre></div> </div>
</li>
<li>Install the cuDNN files to the CUDA folder:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>cuda
<span class="nb">sudo cp</span> <span class="nt">-P</span> include/<span class="k">*</span> /usr/local/cuda-8.0/include/
<span class="nb">sudo cp</span> <span class="nt">-P</span> lib64/<span class="k">*</span> /usr/local/cuda-8.0/lib64/
<span class="nb">sudo chmod </span>a+r /usr/local/cuda-8.0/lib64/libcudnn<span class="k">*</span>
</code></pre></div> </div>
</li>
</ol>
<h2 id="install-libcupti-dev">Install libcupti-dev</h2>
<p>This one is simple. Just run:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>libcupti-dev
</code></pre></div></div>
<h2 id="create-a-virtualenv">Create a Virtualenv</h2>
<p>I recommend using
<a href="https://virtualenvwrapper.readthedocs.io/en/latest/index.html">virtualenvwrapper</a>
to create the tensorflow virtualenv, but the TensorFlow docs still have
<a href="https://www.tensorflow.org/install/install_linux#InstallingVirtualenv">instructions to create the virtualenv
manually</a>.</p>
<ol>
<li><a href="https://virtualenvwrapper.readthedocs.io/en/latest/install.html">Install
virtualenvwrapper</a>.
Make sure to add <a href="https://virtualenvwrapper.readthedocs.io/en/latest/install.html#shell-startup-file">the required
lines</a>
to your <code class="language-plaintext highlighter-rouge">~/.bashrc</code>.</li>
<li>Create the virtualenv:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkvirtualenv <span class="nt">--python</span><span class="o">=</span>python3 tensorflow
</code></pre></div> </div>
</li>
</ol>
<h2 id="install-the-tensorflow-with-gpu-support">Install the TensorFlow with GPU support</h2>
<p>If you just run <code class="language-plaintext highlighter-rouge">pip install tensorflow</code> you will not get GPU support. To
install the correct version you will have to install from a <a href="https://www.tensorflow.org/install/install_linux#python_35">particular
url</a>. Here is the
install command you will have to run to install TensorFlow 1.2 for Python 3.5
with GPU support:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.2.0-cp35-cp35m-linux_x86_64.whl
</code></pre></div></div>
<p>If you need a different version of TensorFlow, you can edit the version number
in the URL. Same with the Python version (change <code class="language-plaintext highlighter-rouge">cp35</code> to <code class="language-plaintext highlighter-rouge">cp36</code> to install for
Python 3.6 instead, for example).</p>
<h2 id="test-that-the-installation-worked">Test that the installation worked</h2>
<p>Save this script from <a href="https://www.tensorflow.org/tutorials/using_gpu#logging_device_placement">the TensorFlow
tutorials</a>
to a file called <code class="language-plaintext highlighter-rouge">test_gpu.py</code>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Creates a graph.
</span><span class="k">with</span> <span class="n">tf</span><span class="p">.</span><span class="n">device</span><span class="p">(</span><span class="s">'/cpu:0'</span><span class="p">):</span>
<span class="n">a</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">constant</span><span class="p">([</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mf">3.0</span><span class="p">,</span> <span class="mf">4.0</span><span class="p">,</span> <span class="mf">5.0</span><span class="p">,</span> <span class="mf">6.0</span><span class="p">],</span> <span class="n">shape</span><span class="o">=</span><span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">],</span> <span class="n">name</span><span class="o">=</span><span class="s">'a'</span><span class="p">)</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">constant</span><span class="p">([</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mf">3.0</span><span class="p">,</span> <span class="mf">4.0</span><span class="p">,</span> <span class="mf">5.0</span><span class="p">,</span> <span class="mf">6.0</span><span class="p">],</span> <span class="n">shape</span><span class="o">=</span><span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span> <span class="n">name</span><span class="o">=</span><span class="s">'b'</span><span class="p">)</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
<span class="c1"># Creates a session with log_device_placement set to True.
</span><span class="n">sess</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">Session</span><span class="p">(</span><span class="n">config</span><span class="o">=</span><span class="n">tf</span><span class="p">.</span><span class="n">ConfigProto</span><span class="p">(</span><span class="n">log_device_placement</span><span class="o">=</span><span class="bp">True</span><span class="p">))</span>
<span class="c1"># Runs the op.
</span><span class="k">print</span><span class="p">(</span><span class="n">sess</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">c</span><span class="p">))</span>
</code></pre></div></div>
<p>And then run it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python test_gpu.py
</code></pre></div></div>
<p>You should see your GPU card listed under “Device mapping:” and that each task
in the compute graph is assigned to <code class="language-plaintext highlighter-rouge">gpu:0</code>.</p>
<p>If you see “Device mapping: no known devices” then something went wrong and
TensorFlow cannot access your GPU.</p>
Tue, 20 Jun 2017 00:00:00 +0000
https://www.hallada.net/2017/06/20/how-to-install-tensorflow-on-ubuntu-16-04.html
https://www.hallada.net/2017/06/20/how-to-install-tensorflow-on-ubuntu-16-04.htmlGenerating Realistic Satellite Imagery with Deep Neural Networks<p>I’ve been doing a lot of experimenting with <a href="https://github.com/jcjohnson/neural-style">neural-style</a>
the last month. I think I’ve discovered a few exciting applications of the
technique that I haven’t seen anyone else do yet. The true power of this
algorithm really shines when you can see concrete examples.
<!--excerpt--></p>
<p>Skip to the <strong>Applications</strong> part of this post to see the outputs from my
experimentation if you are already familiar with DeepDream, Deep Style, and all
the other latest happenings in generating images with deep neural networks.</p>
<h3 id="background-and-history">Background and History</h3>
<p>On <a href="https://medium.com/backchannel/inside-deep-dreams-how-google-made-its-computers-go-crazy-83b9d24e66df#.g4t69y8wy">May 18, 2015 at 2 a.m., Alexander
Mordvintsev</a>,
an engineer at Google, did something with deep neural networks that no one had
done before. He took a net designed for <em>recognizing</em> objects in images and used
it to <em>generate</em> objects in images. In a sense, he was telling these systems
that mimic the human visual cortex to hallucinate things that weren’t really
there. The <a href="https://i.imgur.com/6ocuQsZ.jpg">results</a> looked remarkably like LSD
trips or what a <a href="https://www.reddit.com/r/deepdream/comments/3cewgn/an_artist_suffering_from_schizophrenia_was_told/">schizophrenic person sees on a blank
wall</a>.</p>
<p>Mordvintsev’s discovery quickly gathered attention at Google once he posted
images from his experimentation on the company’s internal network. On June 17,
2015, <a href="http://googleresearch.blogspot.com/2015/06/inceptionism-going-deeper-into-neural.html">Google posted a blog post about the
technique</a>
(dubbed “Inceptionism”) and how it was useful for opening up the notoriously
black-boxed neural networks using visualizations that researchers could examine.
These machine hallucinations were key for identifying the features of objects
that neural networks used to tell one object from another (like a dog from a
cat). But the post also revealed the <a href="https://goo.gl/photos/fFcivHZ2CDhqCkZdA">beautiful
results</a> of applying the algorithm
iteratively on it’s own outputs and zooming out at each step.</p>
<p>The internet exploded in response to this post. And once <a href="http://googleresearch.blogspot.com/2015/07/deepdream-code-example-for-visualizing.html?m=1">Google posted the code
for performing the
technique</a>,
people began experimenting and sharing <a href="https://www.reddit.com/r/deepdream">their fantastic and creepy
images</a> with the world.</p>
<p>Then, on August, 26, 2015, a paper titled <a href="http://arxiv.org/abs/1508.06576">“A Neural Algorithm of Artistic
Style”</a> was published. It showed how one could
identify which layers of deep neural networks recognized stylistic information
of an image (and not the content) and then use this stylistic information in
Google’s Inceptionism technique to paint other images in the style of any
artist. A <a href="https://github.com/jcjohnson/neural-style">few</a>
<a href="https://github.com/kaishengtai/neuralart">implementations</a> of the paper were
put up on Github. This exploded the internet again in a frenzy. This time, the
images produced were less like psychedelic-induced nightmares but more like the
next generation of Instagram filters (<a href="https://www.reddit.com/r/deepdream/comments/3jwl76/how_anyone_can_create_deep_style_images/">reddit
how-to</a>).</p>
<p>People began to wonder <a href="http://www.hopesandfears.com/hopes/culture/is-this-art/215039-deep-dream-google-art">what all of this
meant</a>
to <a href="http://kajsotala.fi/2015/07/deepdream-today-psychedelic-images-tomorrow-unemployed-artists/">the future of
art</a>.
Some of the results produced where <a href="https://raw.githubusercontent.com/jcjohnson/neural-style/master/examples/outputs/tubingen_starry.png">indistinguishable from the style of dead
artists’
works</a>.
Was this a demonstration of creativity in computers or just a neat trick?</p>
<p>On November, 19, 2015, <a href="http://arxiv.org/abs/1511.06434">another paper</a> was
released that demonstrated a technique for generating scenes from convolutional
neural nets (<a href="https://github.com/Newmu/dcgan_code">implementation on Github</a>).
The program could generate random (and very realistic) <a href="https://github.com/Newmu/dcgan_code/raw/master/images/lsun_bedrooms_five_epoch_samples.png">bedroom
images</a>
from a neural net trained on bedroom images. Amazingly, it could also generate
<a href="https://github.com/Newmu/dcgan_code/blob/master/images/lsun_bedrooms_five_epochs_interps.png">the same bedroom from any
angle</a>.
It could also <a href="https://github.com/Newmu/dcgan_code/blob/master/images/turn_vector.png">produce images of the same procedurally generated face from any
angle</a>.
Theoretically, we could use this technology to create <em>procedurally generated
game art</em>.</p>
<p>The main thing holding this technology back from revolutionizing procedurally
generated video games is that it is not real-time. Using
<a href="https://github.com/jcjohnson/neural-style">neural-style</a> to apply artistic
style to a 512 by 512 pixel content image could take minutes even on the
top-of-the-line GTX Titan X graphics card. Still, I believe this technology has
a lot of potential for generating game art even if it can’t act as a real-time
filter.</p>
<h3 id="applications-generating-satellite-images-for-procedural-world-maps">Applications: Generating Satellite Images for Procedural World Maps</h3>
<p>I personally know very little machine learning, but I have been able to produce
a lot of interesting results by using the tool provided by
<a href="https://github.com/jcjohnson/neural-style">neural-style</a>.</p>
<p>Inspired by <a href="http://blog.kaelan.org/randomly-generated-world-map/">Kaelan’s procedurally generated world
maps</a>, I wanted to extend
the idea by generating realistic satellite images of the terrain maps. The
procedure is simple: take a <a href="/assets/kaelan_terrain1.png">generated terrain map</a>
and apply the style of a <a href="/assets/uk_satellite.jpg">real-world satellite image</a>
on it using neural-style.</p>
<p><img src="/assets/satellite_terrain1_process.png" alt="Output of generated map plus real-world satellite
imagery" /></p>
<p>The generated output takes on whatever terrain is in the satellite image. Here
is an output processing one of Kaelan’s maps with a <a href="/assets/svalbard_satellite.jpg">arctic satellite
image</a>:</p>
<p><img src="/assets/kaelan_terrain2.jpg" alt="Kaelan's terrain map" />
<img src="/assets/satellite_terrain2.png" alt="Output of terrain map plus arctic satellite imagery" /></p>
<p>And again, with one of Kaelan’s desert maps and a <a href="/assets/desert_satellite.jpg">satellite image of a
desert</a>:</p>
<p><img src="/assets/kaelan_terrain3.jpg" alt="Kaelan's desert terrain map" />
<img src="/assets/satellite_terrain3.png" alt="Output of terrain map plus desert satellite imagery" /></p>
<p>It even works with <a href="http://blog.kaelan.org/hexagon-world-map-generation/">Kaelan’s generated hexagon
maps</a>. Here’s an island
hexagon map plus a <a href="/assets/volcano_satellite.jpg">satellite image of a volcanic
island</a>:</p>
<p><img src="/assets/kaelan_hex_terrain.jpg" alt="Kaelan's island hexagon map" />
<img src="/assets/satellite_hex_terrain.png" alt="Output of hexagon map plus island satellite
imagery" /></p>
<p>This image even produced an interesting three-dimensional effect because of the
volcano in the satellite image.</p>
<p>By the way, this also works with minecraft maps. Here’s a minecraft map I found
on the internet plus a <a href="/assets/river_satellite.png">satellite image from Google
Earth</a>:</p>
<p><img src="/assets/minecraft_map.jpg" alt="Minecraft map" />
<img src="/assets/satellite_minecraft_map.png" alt="Output of minecraft map plus river satellite
imagery" /></p>
<p>No fancy texture packs or 3-D rendering needed :).</p>
<p>Here is the Fallout 4 grayscale map plus a
<a href="/assets/boston_aerial.jpg">satellite image of Boston</a>:</p>
<p><img src="/assets/fallout4_map.png" alt="Fallout 4 grayscale map" />
<img src="/assets/satellite_fallout4_map.png" alt="Output of Fallout 4 map plus Boston satellite
imagery" /></p>
<p>Unfortunately, it puts the built-up dense part of the city in the wrong part of
the geographic area. But, this is understandable since we gave the algorithm no
information on where that is on the map.</p>
<p>We can also make the generated terrain maps look like old hand-drawn maps using
neural-style. With Kaelan’s terrain map as the
content and <a href="/assets/cyrodiil_ingame.jpg">the in-game Elder Scrolls IV Oblivion map of
Cyrodiil</a> as the style we get this:</p>
<p><img src="/assets/kaelan_terrain1.png" alt="Kaelan's terrain map" />
<img src="/assets/cyrodiil_terrain1.png" alt="Output of terrain map plus map of Cyrodiil" /></p>
<p>It looks cool, but the water isn’t conveyed very clearly (e.g. makes deep water
look like land). Neural-style seems to work better when there is lots of color
in both images.</p>
<p>Here is the output of the hex terrain plus satellite map above and the Cyrodiil
map which looks a little cleaner:</p>
<p><img src="/assets/satellite_hex_terrain.png" alt="Satellite-like hex terrain map" />
<img src="/assets/cyrodiil_satellite_hex_terrain.png" alt="Output of hex terrain plus satellite and map of
Cyrodiil" /></p>
<p>I was interested to see what neural-style could generate from random noise, so I
rendered some clouds in GIMP and ran it with a satellite image of <a href="/assets/mexico_city.jpg">Mexico City
from Google Earth</a> (by the way, I’ve been getting high
quality Google Earth shots from
<a href="https://earthview.withgoogle.com">earthview.withgoogle.com</a>).</p>
<p><img src="/assets/blurry_clouds.png" alt="Random clouds" />
<img src="/assets/random_mexico_city.png" alt="Output of random clouds and Mexico City" /></p>
<p>Not bad for a neural net without a degree in urban planning.</p>
<p>I also tried generating on random noise with a satellite image of <a href="/assets/treatment_plant.jpg">a water
treatment plant in Peru</a></p>
<p><img src="/assets/blurry_clouds2.png" alt="Random clouds" />
<img src="/assets/random_treatment_plant.png" alt="Output of random clouds and water treatment
plant" /></p>
<h3 id="applications-more-fun">Applications: More Fun</h3>
<p>For fun, here are some other outputs that I liked.</p>
<p><a href="/assets/boston_skyline.jpg">My photo of Boston’s skyline as the content</a> and
<a href="/assets/starry_night.jpg">Vincent van Gogh’s The Starry Night as the style</a>:</p>
<p><img src="/assets/starry_boston.png" alt="Output of Boston skyline and starry night" /></p>
<p><a href="/assets/standing_forest.jpg">A photo of me</a> (by Aidan Bevacqua) and <a href="/assets/forrest_autumn.jpg">Forrest in
the end of Autumn by Caspar David Friedrich</a>:</p>
<p><img src="/assets/dead_forest_standing.png" alt="Output of me and Forrest in the end of
Autumn" /></p>
<p><a href="/assets/sitting_forest.jpg">Another photo of me by Aidan</a> in the same style:</p>
<p><img src="/assets/dead_forest_sitting.png" alt="Output of me and Forrest in the end of Autumn" /></p>
<p><a href="/assets/mountain_view.jpg">A photo of me on a mountain</a> (by Aidan Bevacqua) and
<a href="/assets/pixels.png">pixel art by Paul Robertson</a></p>
<p><img src="/assets/mountain_view_pixels.png" alt="Output of me on a mountain and pixel art" /></p>
<p><a href="/assets/copenhagen_park.jpg">A photo of a park in Copenhagen I took</a> and a
painting similar in composition, <a href="/assets/avenue_poplars.jpg">Avenue of Poplars at Sunset by Vincent van
Gogh</a>:</p>
<p><img src="/assets/poplars.png" alt="Output of park in Copenhagen and Avenue of Poplars at
Sunset" /></p>
<p><a href="/assets/shenandoah_mountains.jpg">My photo of the Shenandoah National Park</a> and
<a href="/assets/halo_ring_mountains.jpg">this halo graphic from GMUNK</a>
(<a href="http://www.gmunk.com/filter/Interactive/ORA-Summoners-HALO">GMUNK</a>):</p>
<p><img src="/assets/halo_shenandoah.png" alt="Output of Shenandoah mountains and halo ring
mountains" /></p>
<p><a href="/assets/me.png">A photo of me by Aidan</a> and a <a href="/assets/stained_glass.jpg">stained glass
fractal</a>:</p>
<p><img src="/assets/stained_glass_portrait.png" alt="Output of me and a stained glass fractal" /></p>
<p>Same photo of me and some <a href="/assets/pockets.jpg">psychedelic art by GMUNK</a></p>
<p><img src="/assets/pockets_portrait.png" alt="Output of me and psychedelic art" /></p>
<p><a href="/assets/nyc.jpg">New York City</a> and <a href="/assets/rainforest.jpg">a rainforest</a>:</p>
<p><img src="/assets/jungle_nyc.png" alt="Output of New York City and a rainforest" /></p>
<p><a href="/assets/kowloon.jpg">Kowloon Walled City</a> and <a href="/assets/ngs_map.jpg">a National Geographic
Map</a>:</p>
<p><img src="/assets/kowloon_ngs.png" alt="Output of Kowloon and NGS map" /></p>
<p><a href="/assets/side_portrait.jpg">A photo of me by Aidan</a> and <a href="/assets/head_lioness.jpg">Head of Lioness by
Theodore Gericault</a>:</p>
<p><img src="/assets/lion_portrait.png" alt="Output of photo of me and " /></p>
<p><a href="/assets/forest_hill.jpg">Photo I took of a Norwegian forest</a> and <a href="/assets/mountain_brook.jpg">The Mountain
Brook by Albert Bierstadt</a>:</p>
<p><img src="/assets/mountain_brook_hill.png" alt="Output of Norwegian forest and The Mountain
Brook" /></p>
<h3 id="limitations">Limitations</h3>
<p>I don’t have infinite money for a GTX Titan X, so I’m stuck with using OpenCL on
my more-than-a-few-generations-old AMD card. It takes about a half-hour to
generate one 512x512 px image in my set-up (which makes the feedback loop for
correcting mistakes <em>very</em> long). And sometimes the neural-style refuses to run
on my GPU (I suspect it runs out of VRAM), so I have to run it on my CPU which
takes even longer…</p>
<p>I am unable to generate bigger images (though
<a href="https://github.com/jcjohnson/neural-style/issues/36#issuecomment-142994812">the author has been able to generate up to 1920x1010
px</a>).
As the size of the output increases the amount of memory and time to generate
also increases. And, it’s not practical to just generate thumbnails to test
parameters, because increasing the image size will probably generate a very
different image since all the other parameters stay the same even though they
are dependent on the image size.</p>
<p>Some people have had success running these neural nets on GPU spot instances in
AWS. It would be certainly cheaper than buying a new GPU in the short-term.</p>
<p>So, I have a few more ideas for what to run, but it will take me quite a while
to get through the queue.</p>
Wed, 06 Jan 2016 00:00:00 +0000
https://www.hallada.net/2016/01/06/neural-style.html
https://www.hallada.net/2016/01/06/neural-style.html