Devlog #1.4: Voxel-style Ambient Occlusion


Ambient Occlusion (AO) is a technique used to darken areas where light is naturally blocked by nearby geometry, helping create a stronger sense of depth and contact between objects. In practice, AO is often calculated by estimating how much of the surrounding hemisphere is visible from a given point on a surface.
There are two main types of AO algorithms:
- Static AO – Precomputed during asset creation, where occlusion is baked directly into the geometry or textures.
- Dynamic AO – Computed at runtime, usually based on changing or dynamic data.
Dynamic AO is commonly implemented with Screen-Space Ambient Occlusion (SSAO). This technique samples the depth buffer and uses the reconstructed geometry to estimate visibility for each pixel. The result is then applied as a shading factor across the screen.
However, in voxel-based or procedurally generated games, it’s often more efficient to calculate AO directly during voxel generation.
For reference, you can read a detailed breakdown of Minecraft-style AO here: 👉 0fps.net: Ambient Occlusion for Minecraft-like Worlds.
In my implementation, I simplified the method:
Instead of using 4 AO levels, I only use 3 (as shown in the picture).
For special case, if a vertex would normally have AO = 1, but there are voxels along its edges, I assign AO = 2.
🔶 Voxel AO Calculation
Each voxel requires checking up to 20 neighboring voxels to determine AO for all its vertices. For each face, specifically, you only need to check 8 neighboring voxels.
🔶 Storing AO in Vertex Data
I store the AO value in the w
component of the UV (TEXCOORD0).
Each face has 4 vertices, and I pack their AO values together so the shader can read them correctly.
Important: If the 4 vertices have different AO values stored incorrectly, the shader will display artifacts or “weird” shading.
🔶 Packing Strategy
Because UV is a half4, we can’t simply pack 4 AO values into a single floating-point component using integer bitmask tricks:
- Floating-point types (
half
,float
) store bits differently from integers (byte
,short
,int
). - Directly manipulating bits as floats can break the representation and produce incorrect results.
My solution:
- Pack AO values into a single byte, using 2 bits per vertex (supports up to 4 AO levels).
- Cast that byte into a half and store it in
UV.w
. - In the shader, cast it back to an integer and extract each 2-bit AO value per vertex.
🔶 Shader Implementation
Shader Graph doesn’t have a native cast function from half
→ int
, so I use a custom HLSL function to perform the cast.
I tried other methods, like manipulating float bits directly on CPU and GPU, but those only work reliably for static meshes (e.g., Mesh inside a MeshFilter).
With Graphics.DrawMesh
/ dynamic meshes rendering, bitwise float tricks start glitching, so the byte
→half
packing is more robust.
YNL - Vozel (Devlog)
Minecraft-inspired project with optimized voxels, procedural biomes, and advanced shaders in a fantastical world.
Status | In development |
Author | Yunasawa |
Tags | application, Unity, Voxel |
More posts
- Devlog #2.3: Burst-compatible biome proceducer1 day ago
- Devlog #2.2: Minecraft-like biome generation1 day ago
- Devlog #2.1: Procedural Noise in Action2 days ago
- Devlog #1.3: Hidden-face Culling11 days ago
- Devlog #1.2: Exploring Rendering Methods11 days ago
- Devlog #0.1: Control UIs, ToolTab & ToolViewDec 29, 2024
- Devlog #1.1: Base renderer & Multi-threadingNov 07, 2024
Leave a comment
Log in with itch.io to leave a comment.