diff options
| author | yum <yum.food.vr@gmail.com> | 2026-04-01 11:27:22 -0700 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2026-04-01 11:27:22 -0700 |
| commit | 7bcd5e31b69f8b0284d886eba660e8b26740c9bf (patch) | |
| tree | 6aa83323640d42620881d07902ed8619830f0d83 | |
| parent | 125f059267830d8f81694edb3ca83c6257f608f3 (diff) | |
add histogram preserving triplanar projection article
| -rwxr-xr-x | index.md | 511 |
1 files changed, 497 insertions, 14 deletions
@@ -2,9 +2,303 @@ pagetitle: yummers lang: en --- +# histogram-preserving tri-planar projection +31 March 2026 + +I've been messing around with Burley's "On Histogram-Preserving Blending for +Randomized Texture Tiling" ([link](https://jcgt.org/published/0008/04/02/)) for +a couple days. The core idea is to pre-process images into a "Gaussianized" +form where the histogram of the image's colors follows a Gaussian distribution. +Once in Gaussian form, there is a closed-form way to blend multiple samples with +barycentric weights such that the Gaussian's variance is preserved (Equation 2 +in the paper). Finally, you can run the blended colors through a lookup table +(LUT) to get a result in the original image's color space. The results are +outstanding. (These ideas build on those laid out by Heitz and +Neyret in an earlier paper. I will reference Heitz a few times.) + + + +It was love at first sight - you can use this to seamlessly tile large areas +with textures that themselves don't even need to be seamless. However, the +method uses 4 taps per pixel (3 overlapping hexagons per pixel, plus 1 3D +lookup table tap). + +I've been thinking about terrains for a week or two, since I need to make a +large-scale environment for a project. I really like the idea of using +tri-planar projection for grass, stone etc., but I've never been satisfied with +the quality I get from it. It always creates this awful loss of contrast +between layers and creates weird ghosting artifacts. + +Wait a minute, isn't that kind of what Heitz's technique addresses? + +It turns out that yeah, you can use the exact same machinery described by Heitz +and Burley to perform histogram-preserving tri-planar projection. +You just use standard tri-planar projection to get barycentric +coordinates instead of playing with a UV-space triangle grid. Results are shown +below. + + + +I also noticed that the gamma term described in Burley's Equation 5 can +significantly reduce contrast. At low values, where ghosting is more visible, +contrast is better preserved; at high values, it's more diminished. + + + + +Perhaps blending in YCbCr would ameliorate the loss in contrast, but I haven't +tried that yet. + +The astute reader might find that just increasing contrast after the blend +would produce a similar result, and I'm inclined to agree. The only possible +advantage that this method has is that it doesn't demand fine-tuning. + +# using linux as a desktop os in 2026 +9 Feb 2026 + +About a month ago, my PC's boot drive died. I had been running Windows 11 with +moderate dissatisfaction for a few months, so I decided to switch over to +Linux as my primary OS. These are some notes on that process. My +motivation is to give an accurate portrayal of what to expect out of the +switching process and the day-to-day operation. + +TLDR: The Linux desktop is *way* better in 2026 than it was in 2016. Native app +support is far more common, and Proton is really good. If dual booting was not +still necessary for VR, I would wholeheartedly recommend it. + +## Dual boot setup + +I knew immediately that I'd be dual booting. My memory told me that some apps +just would not work well, and the virtualization tax is high, so I'd want a +native Windows install. So I made my first mistake: I installed Linux, *then* +Windows. The opposite order is far more streamlined. So I just overwrote my +install with Win11. I left half my drive as unallocated space for the Linux +install. + +After rebooting normally to make sure Windows was really working, it was time +to install Linux. My new install would not let me get into BIOS - my keyboard +inputs did not work. There are one-time flags you can set via shell (PowerShell +and BASH) to do various boot-related tasks without keyboard input. To get into +BIOS: + +* PowerShell: `shutdown /r /fw /t 0` +* bash: `sudo systemctl reboot --firmware-setup` + +My keyboard did work once in the BIOS, so I was then able to enter my Linux +bootable USB. I had some trouble getting the bootable USB to work. I had to +install the media via Rufus's `dd` mode instead of the default. + +I installed my distro as normal in the unallocated space, then rebooted. GRUB +showed up, showing my Linux install as default and the Windows boot manager +below. Somewhat unsurprisingly, my keyboard didn't work in GRUB. I heard that +disabling fast boot and +[xhci](https://en.wikipedia.org/wiki/Extensible_Host_Controller_Interface) +handoff in the BIOS can help, but this only temporarily helped before the issue +resurfaced and then resolved itself. My current config has fast boot off and +xhci handoff off. + +My solution to the pre-BIOS/GRUB keyboard issue is just to use shell commands +to reboot. To get from Windows to Linux, I just reboot as normal since Linux +has prio by default in my install. To go from Linux to Windows, I installed +`efibootmgr`, ran it to get the numeric ID of the Windows boot manager (0000), +then crafted this one liner: + +* bash: `sudo efibootmgr --bootnext 0000 && reboot` + +I use ctrl+R to find it every time I need to reboot. + +> Sidebar: this method does not play nicely with Windows updates. Since +> Windows needs to reboot 19 times to do anything, and each reboot takes you +> into Linux, you'll be stuck booting back into Windows manually. Next time +> Windows demands an update, I'll probably just unplug my PC from the wall. + +## Linux setup + +I'm using the Ubuntu 2024 LTS as my distro. My first point of confusion getting +started was the apparent surfeit of package managers: apt (the standard), +snap (canonical's thing), and flatpak (some semi popular community thing). +snap and flatpak are sandboxed by default, which is really just a massive +fucking pain in the ass for GUI apps. So I use apt wherever possible, and raw +.deb files for the rest. + +### Audio + +Audio's a little scuffed, but seems like we've mostly gotten on the pulse audio +train (thank God). + +For whatever reason, my motherboard's audio output sets itself to 39% volume. I +have to use `alsamixer` to increase this to 100%. + +I used `pavucontrol` to disable irrelevant speakers and mics s.a. monitor speakers. + +### Firefox + +Firefox comes pre-installed on Ubuntu. Firefox is slowly going the way of +Windows, but [Just The Browser](https://justthebrowser.com/) has some easy one +liners to de-shittify it. Waterfox is also interesting, but I haven't tried it +yet. + +### Discord + +I used the raw .deb to install Discord. It will ask you to manually update +every few days. I wrote this shell script to speed that up: + +```bash +#!/usr/bin/env bash +# updisc: update discord + +set -o errexit +set -o xtrace + +cd $HOME/Downloads +wget --content-disposition "https://discord.com/api/download/stable?platform=linux&format=deb" +latest=$(ls -v | grep discord | tail -n1) +sudo dpkg -i "$latest" +``` + +### Spotify + +The snap works fine for this. Installed it through the App Center (Canonical's +app store). + +(Preachy note: Spotify kind of sucks. Avoid using their auto generated +playlists. Spotify has something called the Perfect Fit Program which +commissions and pushes music to listeners based on non-public preference data. +Artists involved in this program are not well compensated. Read about it in Liz +Pelly's +[expose](https://harpers.org/archive/2025/01/the-ghosts-in-the-machine-liz-pelly-spotify-musicians/).) + +### Steam + +I use the raw .deb to install Steam. I tried the flatpak at first, but the +sandboxing doesn't play nicely with proton. Steam will keep itself up to date +so the raw .deb is fine. + +### Games + +I had some issues with graphics drivers in certain games and had to roll back +my driver from 590 to 570. You can list your driver with `nvidia-smi`, and +install some other version (e.g. 570) with `sudo apt install +nvidia-driver-570`. It will ask for a password - this only has to be entered +once after reboot, after which the driver will be trusted forever. + +If your game uses Easy Anti Cheat and Proton, you'll need to install the Proton +EasyAntiCheat runtime. It should be listed in your library by default. + +Proton has a heavy FPS hit vs. Windows native (like 30%), but I'm not a +competitive gamer and my computer is very over-built, so I don't care. + +### Blender + +I downloaded the LTS .tar.xz from the website and put it in my bin directory. I +think it's probably smarter to use Steam for this. Do *not* use the snap +version - it won't let you install addons from the web. + +If you use an NDOF input device like a spacemouse, install +[spacenavd](https://github.com/FreeSpacenav/spacenavd) via apt. You might have +to relaunch blender. + +Otherwise, pretty much identical experience. + +### Unity + +Superficially, Unity basically just works. Install the hub using the official +Unity3D [documentation](https://docs.unity3d.com/hub/manual/InstallHub.html#install-hub-linux). + +My problems with Unity so far are: + +* Slow shader compile times. +* Unity uses OpenGL by default on Linux, and Vulkan is very crashy in my + experience. + * OpenGL uses a different depth buffer format than DX11/DX12, making it hard + to develop for that platform on the OpenGL version. +* GPU profiler doesn't work out of the box, showing 1 ms for every frame. +* Weird permissions issues if you just mount a project created in Windows. Had + to copy it over. +* Have to delete Library/ if the project was created/used on Windows. + (Shouldn't be a big deal. Just slows down first time startup.) +* Slow scrolling performance in Inspector pane +* Dragging sliders in game mode is not smooth, like it is in Windows + +You can use ALCOM/vrc-get to create VRChat projects. Get it [from +github](https://github.com/vrc-get/vrc-get). + +### Adobe + +I've already been on Krita (and GIMP before that), which natively supports +Linux. No problems there. + +Substance painter is a massive issue. Adobe claims to have a native Ubuntu build, +and even sell it through Steam. However, it simply did not launch on my +system. It was missing half a dozen shared object files (.so), and after +manually fixing that it still fails to launch. + +Thankfully the fuckwits at Adobe couldn't be bothered to strip their binary: + +``` +$ file ./Adobe\ Substance\ 3D\ Painter +./Adobe Substance 3D Painter: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=cf49a257fa3bcf0f40d860a8a45a67c873571421, with debug_info, not stripped +$ $ du -h ./Adobe\ Substance\ 3D\ Painter +305M ./Adobe Substance 3D Painter +``` + +So the next time I have a free afternoon I'll be looking through that. + +I did try ArmorPaint, but it is very clearly still quite early in development. +I do not think that it's a viable alternative to Substance Painter yet. (For +example: you cannot drag and drop in textures, nor can you import more than one +at a time. Functional, sure, but barely.) To its credit: unlike Substance +Painter, it actually launches. + +## Pleasant surprises + +Linux is way, way more polished than I remember it. Back in college I was +fucking around with Arch (and didn't know what I was doing) so I was expecting +a far more painful setup process. Instead it was very seamless. Audio works +well. There are very few weird audio/graphical bugs. NVIDIA drivers are easy to +install. Gaming basically just works - I have yet to encounter a game which I +can't play (I've only tried maybe a dozen). + +The amount of native app support is really heartwarming. Skipping over the big +apps like Firefox and Steam, here are some smaller apps I was surprised to see +native support for: + +* OBS (video recording/streaming tool) +* Factorio (indie game) +* r2modman (mod manager) +* Chatterino (twitch chat app) +* PureRef (artist reference app) +* Lorien (infinite canvas drawing app) + +## Complaints + +Canonical uses the "yes/maybe later" pattern which degrades the notion of +consent. Nearly every large firm does it now since Google about-faced a couple +years ago, but it is still a grave degradation of user rights that shouldn't be +glossed over. + +SteamVR does not work for me. My knuckles did not have an internally +consistent coordinate system, preventing me from completing room +calibration. +I was never able to figure this out. This~~, along with Unity's crashy behavior +on Vulkan,~~* is what's keeping me from just deleting Windows. + +\* I've actually been able to use the OpenGL build to do graphical programming + without issue for a few weeks now. So, not an issue. + +## Conclusions + +The Linux desktop is really good. You should give it a try. + # hemi-octahedral impostors 14 Jan 2026 +*Note: this blog post is only like half way complete. I may or may not circle +back to it. The stuff on octahedral mappings is all finished, but the +impostor application below is not.* + Ryan Brucks published [an article](https://shaderbits.com/blog/octahedral-impostors) describing "octahedral impostors" in 2018. The basic idea is to to take photos of some @@ -14,10 +308,11 @@ those photos in a particle.  -Why octahedrons? The octahedral mapping is simply one way to convert between a -flat coordinate system and a spherical coordinate system. It is notable because -it does not use any trig functions, making it suitable for use in realtime -graphics. +## But why octahedrons? + +The octahedral mapping is simply one way to convert between a flat coordinate +system and a spherical coordinate system. It is notable because it does not use +any trig functions, making it suitable for use in realtime graphics. This is what an octahedron looks like: @@ -48,14 +343,14 @@ Viewed head on, we can see a very beautifully symmetric unwrapping:  -Note that we never actually did any rotations, so there's no trig! Here's the +Note that we never actually did any rotations, so there no trig! Here's the same procedure in code: ```c // Convert unit octahedron to a [-1,1] x [-1,1] patch on xz plane. float3 octahedron_to_plane(float3 p) { if (p.y >= 0) { - // Project upper hemispher onto xz plane. + // Project upper hemisphere onto xz plane. p.y = 0; return p; } @@ -78,15 +373,10 @@ float3 octahedron_to_plane(float3 p) { float l1_norm = abs(p.x) + abs(p.y) + abs(p.z); p /= l1_norm; // Then unwrap. - if (p.y >= 0) { - // Project upper hemispher onto xz plane. - p.y = 0; - return p; + if (p.y < 0) { + p.x = sign(p.x) * (1 - abs(p.x)); + p.z = sign(p.z) * (1 - abs(p.z)); } - // First, reflect the lower hemisphere's points about their diagonal. - p.x = sign(p.x) * (1 - abs(p.x)); - p.z = sign(p.z) * (1 - abs(p.z)); - // Then project onto the xz plane. p.y = 0; return p; } @@ -96,8 +386,201 @@ Here's a quick demo showing what that norm conversion does to a unit sphere:  +Going from plane to octahedron is just the same thing backwards: + +```c +// Convert a [-1,1] x [-1,1] patch on xz plane to a unit sphere. +float3 plane_to_octahedron(float3 p) { + float l1_norm = abs(p.x) + abs(p.z); + if (l1_norm > 1) { + // Reflect lower hemisphere's point about their diagonal. + p.x = sign(p.x) * (1 - abs(p.x)); + p.z = sign(p.z) * (1 - abs(p.z)); + } + p.y = 1 - l1_norm; + return normalize(p); +} +``` + If you'd like more discussion on this topic, I recommend the spherical geometry section in [the PBR book](https://www.pbr-book.org/4ed/Geometry_and_Transformations/Spherical_Geometry#x3-OctahedralEncoding).) +## The hemi octahedron + +We might only want to map the upper hemisphere to a plane. In that case, we can +first note that in the standard octahedral mapping, the inner diamond of the +[-1,1] x [-1,1] square gets mapped to the upper hemisphere. So all we have to +do is first remap our input to that diamond via a scale and 45 degree rotation, map it, +then rotate it back. The code is still very simple: + +```c +// Convert unit sphere to a [-1,1] x [-1,1] patch on xz plane. +float3 hemi_octahedron_to_plane(float3 p) { + // Rotate 45° and scale to fit square into diamond + float x_rot = (p.x + p.z) * 0.5; + float z_rot = (p.z - p.x) * 0.5; + p.x = x_rot; + p.z = z_rot; + + float l1_norm = abs(p.x) + abs(p.y) + abs(p.z); + p /= l1_norm; + if (p.y < 0) { + p.x = sign(p.x) * (1 - abs(p.x)); + p.z = sign(p.z) * (1 - abs(p.z)); + } + p.y = 0; + + // Rotate back. + x_rot = p.x - p.z; + z_rot = p.x + p.z; + p.x = x_rot; + p.z = z_rot; + + return p; +} +``` + +Here is that transform, visualized: + + + +If we didn't do that scale and rotate, this is what it would look like: + + + +I will leave the plane -> hemi-octahedron code as an exercise for the reader. + +## Impostor v1 + +With this mapping, we can write some code to spawn cameras at the lattice +points of an octahedral-mapped hemisphere, pointing in at some target object, +and generate an atlas of images taken at different angles: + + + + + +We can then write a naive particle shader which computes its nearest lattice +point and simply renders that image. + +We can simply compute the direction from the camera to the particle's center, +map that to 2D using the hemi-octahedral mapping, then find the nearest lattice +point by rounding. We can also rotate the particle to the same orientation that +the photo was taken at to avoid any weird behavior when viewed top down. + +That looks like this: + + + +The popping is pretty awful! Can we do better? + +## Impostor v2 + +Brucks describes a "virtual frame projection" method. I'll let him explain it: + +> Looking back to the 'virtual grid mesh' above, we can see that for any triangle on the grid, it has 3 vertices. So if we want to blend smoothly across this grid, we need to be able to identify the 3 nearest frames. And remember how using a sprite caused messed up projection of just one frame? Well it turns out the same thing happens when you try to reuse the projection from one frame for another! This is a pain. So you actually have to render a virtual frame projection for the other 2 frames to simulate their geometry. While using the mesh UVs for one projection and 'solving' the other two does work, it falls apart for lower (~8x8) frame counts because the angular difference can be so great between cards that you see the card start to clip at grazing angles (not shown in any videos yet). As a compromise, the shader does not use ANY UVs right now. It solves all 3 frames using virtual frame projection in the vertex shader and then uses a traditional sprite vertex shader. The only downside is at close distances you occasionally see some minor clipping on the edge but it is much more acceptable this way. + +Lost? Me too! I found this paragraph extremely confusing - it's what motivated +me to write this article. + +As near as I can tell, what he's describing is that you retrieve +the nearest 3 lattice points and do a barycentric interpolation. He's also +trying to clarify that you can't just use the uvs from one lattice point to +sample another - you have to calculate each lattice point's uvs separately. +(I suppose that that level of optimization-first thinking is required when +you're building for Fortnite!) + +You then render the blended color that on a standard facing quad primitive. + +To start, I calculate the ray from the camera to the origin of the particle's +coordinate system. I use that position for my barycentric interpolation. + +That looks like this: + + + +Huh. Looks a lot worse than his demo. What are we doing wrong? + +Could it just be our choice of mesh that makes our results look bad? Here's Suzanne: + + + +Maybe it looks a little better? + +The mesh that Brucks shows off in his blog post has radial symmetry and smooth +normals, which might be responsible. + +You can also see some artifacts appearing in open space. This was caused by a +couple things: + +1. The other mesh was toggled on when I generated my impostor atlas. +2. The bounding sphere around my mesh had very little padding. +3. The particle can rotate, and if you don't clip the parts outside the + impostor's bounding sphere, you can wind up rendering them. + +3 is crucial - with that correction in place, you can pack your atlas +pretty tightly. Here's Suzanne with that correction in place: + + + +Here's the atlas. Pretty tight packing - could probably be optimized a little +further though: + + + +## Impostor v3 + +After stepping away for a bath, the issue occurred to me. + +I was calculating the lattice point based on the direction from the camera to +the particle center. With barycentric interpolation in place, we would be +better off using a per-pixel ray intersection with the impostor's bounding +sphere. Concretely: we want to sample the lattice points whose cameras have a +direction most closely matching the standard view direction. This is found by +simply going from the particle's bounding sphere origin to the surface along +`-viewDir`, projecting that to 2d, then rounding to lattice points as normal. + +This seems to help a bit, but it's not night and day. I didn't capture any +videos here, but the next gen uses this tech. + +## Impostor v4 + +So far we've only been rendering pre-lit images of our subject on an unlit +particle. Can we do better? What if we captured the albedo, normal, metallic +gloss, and position, then lit it with a standard surface shader? + +The results look a bit better - specular is much better approximated now: + + + +## Impostor v5 + +I continued to spin my wheels for a couple days. I re-read Brucks' article +several more times, and came to a couple conclusions: + +1. He is using the camera-origin ray, not a per pixel view direction ray. +2. He is doing some form of parallax occlusion mapping to limit popping. + +I found his description on +[this video](https://www.youtube.com/watch?v=6rsXe6kKTC4) useful: + +> This version blends the three nearest frames using a single parallax offset (similar to a bump offset). This is the version of impostors used in FNBR on PC and Consoles. It was used on mobile originally but switched back to single frame at last minute since we were compositing them into HLODs and thus rendering lots of them. + +That single parallax offset is explained by this image: + + + +My buggy implementation looks promising - see how the eyes are much sharper +now? + + + +It is still very, very poppy, unlike Brucks' demo. I must be doing something +wrong. + +*Note: I stepped away from this project and don't plan to revisit it soon. If I +do, I'll post updates in a followup and link to it from here.* + # 6 wave dispersion relations with derivatives 21 Sep 2025 |
