
How to Build a Hades‑Style Encounter Flow Generator in Unity
August 22, 2026
Real‑Time Volumetric Fog in Unity Using 3D Textures and Compute Shaders
August 24, 2026Lightmap baking is one of the most expensive offline processes in Unity. Traditional CPU baking (Progressive CPU Lightmapper) is extremely slow for large scenes and high-resolution maps.
To overcome this, many studios build GPU-accelerated custom baking tools using compute shaders, RTX hardware, or hybrid CPU/GPU workloads.
This guide explores how GPU lightmap baking works, how to design a custom Unity-based GPU baker, how data flows from scene geometry to compute shaders, and how to generate a final lightmap texture.
1. What Is GPU Lightmap Baking?
GPU baking is a technique where the heavy calculations of lightmap generation (ray casting, irradiance gathering, global illumination, denoising) are performed on the GPU instead of the CPU.
A GPU baking system usually handles:
- Ray generation on the GPU
- Sampling direct and indirect lighting
- Bouncing rays using BVH acceleration structures
- Accumulating results into texels
- Denoising using compute filters
- Writing out final textures to disk
With massive parallelism, GPUs can deliver performance gains of:
10x–100x faster baking compared to CPU‑based lightmappers.
2. How Unity Lightmap Baking Internally Works
Unity’s lightmap generation process follows these general steps:
- Unwrap meshes for lightmapping (UV charting)
- Generate lightmap texels and world positions
- Cast rays (direct + indirect) per texel
- Accumulate lighting into the lightmap buffer
- Denoise + pack maps
- Save the final texture assets
The main expensive part is step 3 and 4, ray casting and lighting accumulation — which we will port to the GPU.
3. Designing a Custom GPU Lightmap Baking Architecture
A minimal GPU lightmap baker consists of these components:
- Texel extractor – generates world-space positions for UV texels
- Compute shader raycaster – shoots rays from texels
- GPU buffers for geometry, materials, light data
- Irradiance accumulator – gathers and averages samples
- Denoiser (optional)
- Texture writer – writes results into a Texture2D
Below is a simplified architecture diagram:
Unity Meshes → Texel Generator → Compute Shader (Ray Tracing) → Irradiance Buffer → Denoise → TextureWriter → Lightmap PNG
4. Step-by-Step Implementation in Unity
4.1 Extract Texels for Lightmap Baking
For each mesh with lightmap UVs, we must convert UV coordinates to world-space positions based on interpolated barycentric coordinates.
Sample C# extractor:
public struct TexelData
{
public Vector3 worldPos;
public Vector3 normal;
public int lightmapIndex;
public Vector2 uv;
}
public static List<TexelData> ExtractTexels(Mesh mesh, Transform t, int resolution)
{
List<TexelData> texels = new();
Vector2[] uvs = mesh.uv2;
if (uvs == null || uvs.Length == 0) return texels;
for (int i = 0; i < mesh.triangles.Length; i += 3)
{
int a = mesh.triangles[i];
int b = mesh.triangles[i + 1];
int c = mesh.triangles[i + 2];
Vector2 uva = uvs[a];
Vector2 uvb = uvs[b];
Vector2 uvc = uvs[c];
// Iterate texels inside triangle's UV space
for (int x = 0; x < resolution; x++)
for (int y = 0; y < resolution; y++)
{
Vector2 uv = new Vector2(x / (float)resolution, y / (float)resolution);
if (!PointInTriangle(uv, uva, uvb, uvc)) continue;
// Interpolate world position
Vector3 pos = BarycentricInterpolate(mesh, t, a, b, c, uv, uva, uvb, uvc);
texels.Add(new TexelData
{
worldPos = pos,
normal = t.TransformDirection(mesh.normals[a]),
uv = uv
});
}
}
return texels;
}
This builds the CPU-side texel list, which will be uploaded to the GPU.
4.2 Upload Geometry to GPU
We need mesh vertices, normals, indices, material IDs, and lights.
Store these in StructuredBuffers:
- Vertex buffer
- Triangle buffer
- Material buffer
- Light buffer
ComputeBuffer vertexBuffer;
ComputeBuffer triangleBuffer;
ComputeBuffer lightBuffer;
void UploadData()
{
vertexBuffer = new ComputeBuffer(verts.Length, sizeof(float) * 3);
vertexBuffer.SetData(verts);
triangleBuffer = new ComputeBuffer(tris.Length, sizeof(int) * 3);
triangleBuffer.SetData(tris);
lightBuffer = new ComputeBuffer(lights.Length, sizeof(float) * 8);
lightBuffer.SetData(lights);
}
5. Compute Shader for GPU Ray Casting
This is the core: each texel generates samples (rays) toward the scene.
We use Unity compute shaders (HLSL).
A minimal compute kernel:
// File: LightmapRaytrace.compute
#pragma kernel CSMain
StructuredBuffer<float3> TexelPositions;
StructuredBuffer<Triangle> Triangles;
RWStructuredBuffer<float3> Irradiance;
[numthreads(64,1,1)]
void CSMain(uint id : SV_DispatchThreadID)
{
float3 pos = TexelPositions[id];
float3 accumulated = 0;
int sampleCount = 32; // example sample count
for (int i = 0; i < sampleCount; i++)
{
float3 dir = RandomHemisphereDirection(normal);
accumulated += TraceRay(pos, dir);
}
Irradiance[id] = accumulated / sampleCount;
}
In real GPU bakers, TraceRay() uses BVH acceleration instead of naive triangle iteration.
6. Integrating Compute Shader With C#
public ComputeShader raytraceShader;
ComputeBuffer texelBuffer;
ComputeBuffer irradianceBuffer;
public Texture2D BakeLightmap(List<TexelData> texels, int width, int height)
{
texelBuffer = new ComputeBuffer(texels.Count, System.Runtime.InteropServices.Marshal.SizeOf(typeof(TexelData)));
texelBuffer.SetData(texels);
irradianceBuffer = new ComputeBuffer(texels.Count, sizeof(float) * 3);
int kernel = raytraceShader.FindKernel("CSMain");
raytraceShader.SetBuffer(kernel, "TexelPositions", texelBuffer);
raytraceShader.SetBuffer(kernel, "Irradiance", irradianceBuffer);
int groups = Mathf.CeilToInt(texels.Count / 64.0f);
raytraceShader.Dispatch(kernel, groups, 1, 1);
// Read results back
Vector3[] result = new Vector3[texels.Count];
irradianceBuffer.GetData(result);
return PackToTexture(result, texels, width, height);
}
7. Packing Irradiance Into a Lightmap Texture
After computing lighting per texel, we convert data into a regular Texture2D lightmap.
Texture2D PackToTexture(Vector3[] samples, List<TexelData> texels, int width, int height)
{
Texture2D lightmap = new Texture2D(width, height, TextureFormat.RGBAHalf, false);
Color[] pixels = new Color[width * height];
for (int i = 0; i < texels.Count; i++)
{
int x = (int)(texels[i].uv.x * width);
int y = (int)(texels[i].uv.y * height);
pixels[y * width + x] = new Color(
samples[i].x,
samples[i].y,
samples[i].z,
1.0f
);
}
lightmap.SetPixels(pixels);
lightmap.Apply();
return lightmap;
}
8. Adding a GPU Denoiser
GPU bakers almost always need denoisers due to Monte Carlo sampling noise.
You can use:
- Compute Shader Bilateral Filter
- Wavelet Filter
- SVGF (Spatiotemporal Variance-Guided Filtering)
- Optix Denoiser (if using native plugin)
Example bilateral filter kernel:
// LightmapDenoise.compute
#pragma kernel Denoise
RWTexture2D<float4> Lightmap;
[numthreads(8,8,1)]
void Denoise(uint2 id : SV_DispatchThreadID)
{
float3 sum = 0;
float wsum = 0;
for (int x = -2; x <= 2; x++)
for (int y = -2; y <= 2; y++)
{
float3 col = Lightmap[id + uint2(x,y)].rgb;
float w = 1; // Add bilateral weighting here
sum += col * w;
wsum += w;
}
Lightmap[id] = float4(sum / wsum, 1.0);
}
9. Exporting the Lightmap Texture to Disk
public static void SaveLightmap(Texture2D tex, string path)
{
byte[] png = tex.EncodeToPNG();
System.IO.File.WriteAllBytes(path, png);
#if UNITY_EDITOR
UnityEditor.AssetDatabase.Refresh();
#endif
}
10. Optional: Hybrid RTX Ray Tracing via Native Plugin
For real Hades-, UE5-, or Unity Enemies-level baking speed, you can integrate:
- NVIDIA OptiX
- DirectX Raytracing (DXR)
- Vulkan RT
This requires native plugins (C++ DLL), but the flow stays the same: Unity sends mesh + texel data → plugin → GPU returns irradiance values.
11. Summary
Custom GPU lightmap baking inside Unity is absolutely achievable and dramatically faster than CPU Progressive baking.
A GPU baker requires:
- UV texel extraction
- Geometry & lighting buffers
- Compute shader raycaster
- Irradiance accumulation
- Denoising (optional)
- Texture packing & exporting
This architecture gives you:
- 10x–100x baking speed
- Fully customizable GI behavior
- Integration with procedural tools
- The ability to run baking in‑game
With this foundation, you can extend it toward hybrid RTX baking, unfolded UV debugging tools, and even in-editor real-time GI previews.



![Lighting in Unity: A Practical Guide for Game Developers There’s something satisfying about watching a flat, boring plane turn into a mountain range. That’s exactly what a height map does in Unity. With a simple grayscale image, you can shape landscapes, add depth to materials, and create worlds that feel real instead of flat. It’s one of those tools that looks technical at first, but once you understand it, it becomes surprisingly simple and powerful. In this guide, we’ll break down what a height map is, how it works in Unity, how to use it for terrain, how it differs from normal maps, and how to control it with C#. What Is a Height Map? A height map is a grayscale image where each pixel represents elevation. Black = lowest height White = highest height Gray = values in between Think of it like a topographic map, but simplified into brightness levels. Unity reads this image and uses the brightness values to push parts of a surface up or down. The result? Hills, valleys, cliffs, and surface details created from a simple image. Height Maps in Unity Terrain The most common use of height maps in Unity is terrain generation. Unity’s Terrain system allows you to import a height map and automatically generate a 3D landscape from it. How to Import a Height Map into Terrain Create a Terrain: GameObject → 3D Object → Terrain Select the Terrain object. Open the Terrain Inspector. Choose Import Raw under the heightmap settings. Select your grayscale RAW file. Once imported, Unity converts the grayscale values into elevation data. If your height map is smooth, you’ll get rolling hills. If it has sharp contrast, you’ll get steep cliffs. Height Map Resolution Matters Resolution affects how detailed your terrain will be. A low-resolution height map creates blocky terrain. A high-resolution height map creates smoother, more detailed landscapes. However, higher resolution also increases memory usage and processing cost. If you're building for mobile, balance detail with performance. Height Maps vs Normal Maps This is where many beginners get confused. Height Map Actually changes geometry (in terrain or displacement). Creates real depth. More performance cost if geometry changes. Normal Map Does NOT change geometry. Fakes lighting to simulate bumps. Much cheaper performance-wise. If you need real terrain shape, use a height map. If you just want surface detail like cracks or scratches, a normal map is usually better. Using Height Maps in Materials (Parallax & Displacement) Height maps are not limited to terrain. You can also use them in materials. In Unity’s Standard Shader (or URP/HDRP equivalents), height maps can be used for: Parallax Mapping – creates depth illusion without changing geometry. Displacement Mapping – actually modifies mesh vertices (HDRP). For example, if you apply a brick texture, adding a height map can make the mortar appear recessed and bricks raised. Creating Height Maps You can create height maps using: Photoshop or GIMP (grayscale images) Blender (baked displacement maps) World Machine or Gaea (terrain generation tools) Procedural generation with code The key is keeping it grayscale and avoiding compression artifacts. Generating a Height Map with Code You can also generate terrain procedurally using Perlin Noise. This is common in open-world or survival games. Here’s a simple example: [csharp] using UnityEngine; public class TerrainGenerator : MonoBehaviour { public Terrain terrain; public int depth = 20; public int width = 256; public int height = 256; public float scale = 20f; void Start() { terrain.terrainData = GenerateTerrain(terrain.terrainData); } TerrainData GenerateTerrain(TerrainData terrainData) { terrainData.heightmapResolution = width + 1; terrainData.size = new Vector3(width, depth, height); terrainData.SetHeights(0, 0, GenerateHeights()); return terrainData; } float[,] GenerateHeights() { float[,] heights = new float[width, height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { heights[x, y] = Mathf.PerlinNoise(x / scale, y / scale); } } return heights; } } [/csharp] This script generates a terrain using Perlin Noise, which creates natural-looking hills and variation. Controlling Height Strength Sometimes height maps look too extreme. Other times they look flat. In terrain settings, you can adjust: Terrain height (Y scale) Brush strength (when sculpting manually) In materials, you can adjust height intensity inside the shader settings. Small adjustments make a big difference. Subtle depth often looks more realistic than exaggerated displacement. Common Problems and Fixes Terrain Looks Blocky Increase heightmap resolution or smooth the terrain. Edges Look Stretched Make sure your height map is square and uses proper dimensions (like 512x512 or 1024x1024). Lighting Looks Strange Check your normal settings and ensure lighting is baked or set correctly. When to Use Height Maps Use height maps when: You need large landscapes. You want realistic terrain shaping. You’re building procedural worlds. You need true geometric depth. Skip them when: You only need small surface detail. Performance is extremely limited. Final Thoughts Height maps are one of those tools that feel technical at first, but once you use them, they become creative tools. You’re not just editing numbers. You’re sculpting mountains. Carving valleys. Designing the shape of a world. Start simple. Import a grayscale image. Adjust the scale. Play with noise. Watch how small changes affect the landscape. Once you understand height maps, Unity stops feeling like a flat engine. It starts feeling like a world builder.](https://unityqueen.com/wp-content/uploads/2026/02/Lighting-in-Unity-150x150.jpg)






