Vulkan Schnee 0.0.1
High-performance rendering engine
Loading...
Searching...
No Matches
Spherical Harmonics Probe Baking

SH probes capture indirect diffuse lighting at discrete points in a scene. At runtime, each object samples the nearest probe to receive ambient illumination from surrounding surfaces, lights, and the sky.

The pipeline has three stages: bake in Blender → export via glTF → load in the engine.

Source files:

  • BlenderPlugins/plugin/SphericalHarmonicsBaking.py - Baking operator
  • BlenderPlugins/ExporterPlugin/__init__.py - glTF scene extension hook
  • Engine/src/Engine/Renderer/SHProbeGrid.h - Engine-side data structures
  • Engine/src/Engine/Renderer/SHProbeGrid.cpp - Binary file loading
  • Engine/src/Engine/Ecs/FrameProcessing.cpp - glTF extension parsing

Blender Setup

1. Create a ProbeVolumes collection

In the Blender Outliner, create a collection named exactly ProbeVolumes. Add one or more closed mesh objects to this collection. Each mesh defines a region where probes will be placed.

The volume meshes do not render in the final export. Any mesh topology works as long as the volume is closed (watertight normals pointing outward).

Multiple meshes in the collection are treated as separate volumes — probes are placed inside the union of all volumes.

2. Probe placement rules

The baking script places probes only at positions that satisfy both conditions:

  • The position is inside at least one volume mesh (ray-cast test)
  • The position is not inside any other scene mesh (walls, floors, solid geometry)

Probes inside solid geometry are pruned automatically. If a probe location falls inside a wall due to cell alignment, it is skipped.


Baking Settings

Click Bake SH in Volumes in 3D Viewport > Sidebar (N) > Vulkan Schnee Engine > Lightmapping > Baking. A dialog appears with these parameters:

Parameter Default Description
Probe Volume Collection ProbeVolumes Name of the Blender collection containing volume meshes
Min Cell Size (m) 0.5 Smallest octree cell size. Cells this small are placed near scene geometry.
Max Cell Size (m) 16.0 Largest octree cell size. Open areas (no nearby geometry) get one probe per 16m cell.
Panorama Resolution 128 Width of each per-probe equirectangular render. Height = Width / 2.
Max SH Band (L) 2 Maximum SH band. L=2 produces 9 coefficients (L0 + L1 + L2).
Cycles Samples 16 Samples per probe panorama.
Reuse Cycles Render Data enabled Keeps Cycles render data/BVH between probe renders.
Fast SH Cycles Settings enabled Reduces bounce limits, disables caustics, and disables motion blur for SH baking.
Direct Light Scale 0.0 Optional direct Blender light injection into SH. Keep 0 when the engine renders direct lights dynamically.
Shadow Direct Lights enabled Ray-tests direct light contribution against scene geometry.
Direct Light Bands L0-L1 (Stable) Maximum SH band used for analytic direct lights.

Adaptive octree placement

The baking system builds a sparse adaptive octree over the probe volumes. Cells that overlap scene geometry are subdivided down to Min Cell Size. Cells with no nearby geometry remain at Max Cell Size. This concentrates probes where lighting varies (near surfaces, in corners) and uses fewer probes in open space.

Example for a 10m × 10m × 4m room:

  • Min=0.5m → 8 probes across 4m height near walls/floor/ceiling
  • Max=4m → 1 probe per 4m slab in the room center
  • Typical result: 200-400 probes vs. ~3200 for a uniform 0.5m grid

Render settings

The operator temporarily overrides Cycles render settings for each probe:

  • Render engine: Cycles (required for panorama camera)
  • 16 samples with adaptive sampling when fast settings are enabled (minimum 4 samples, threshold 0.2)
  • Denoiser: disabled (not needed at 128×64)
  • Output format: 16-bit EXR (uncompressed, for speed)
  • GPU rendering: enabled if available
  • Persistent Cycles render data: enabled by default

Original scene render settings are restored after baking completes.

Prerequisites

SH baking requires scipy. Install it from the Vulkan Schnee Engine panel:

3D Viewport > Sidebar (N) > Vulkan Schnee Engine > Dependencies > Install Dependencies

Or manually in Blender's Python environment:

pip install scipy

Running the Bake

  1. Save the .blend file (required — output path is relative to it)
  2. Click Bake SH in Volumes in Vulkan Schnee Engine > Lightmapping > Baking
  3. Adjust parameters in the dialog
  4. Click OK

The operator renders one Cycles panorama per probe location and projects the result onto L0–L2 SH basis functions. Progress is printed to the system console (Window > Toggle System Console).

Output: //sh_coeffs/probes.shprobe (relative to the .blend file)

For a scene with multiple named volumes, only one probes.shprobe is written per bake run. To bake separate volumes independently, run the operator multiple times with different collection names and manually rename the output files between runs.

Debug baking

Debug: Bake SH at Cursor bakes a single probe at the 3D cursor position. Output goes to //sh_coeffs/debug_probe.txt (text) and //sh_coeffs/probes.shprobe (binary, single probe). Useful for verifying that the render pipeline and scipy integration work before running a full bake.


Exporting

Automatic .shprobe detection

The glTF exporter plugin automatically includes .shprobe files during export — no manual configuration is needed. In File > Export > glTF 2.0, ensure Export Engine Properties is enabled (it is by default).

At export time, the gather_scene_hook scans sh_coeffs/ next to the .blend file. For each .shprobe file found, it:

  1. Reads the 68-byte SHPR v3 binary header to extract metadata (probe count, root grid dimensions, origin, root cell size, default scale, default ambient)
  2. Adds an entry to the shProbeGrids array in the scene-level VULKAN_SCHNEE_engine_properties extension
  3. Copies the .shprobe file to sh_coeffs/<filename> relative to the glTF output directory

glTF extension JSON

The scene-level extension looks like this for a single volume:

{
"extensions": {
"VULKAN_SCHNEE_engine_properties": {
"shProbeGrids": [
{
"shProbePath": "sh_coeffs/probes.shprobe",
"volumeName": "probes",
"probeCount": 312,
"gridDimensions": [40, 8, 40],
"gridOrigin": [-10.0, 0.0, -10.0],
"gridSpacing": 0.5,
"defaultScale": 1.0,
"defaultAmbient": 0.1
}
]
}
}
}

gridOrigin and grid axes are in engine coordinate space (Y-up). The baking script converts from Blender's Z-up coordinate system automatically:

Multi-volume export

Place multiple .shprobe files in sh_coeffs/. The exporter adds one entry per file to shProbeGrids, sorted by filename. The engine loads all entries on scene load and queries them in order — the first grid containing the world position wins.

"shProbeGrids": [
{ "shProbePath": "sh_coeffs/interior.shprobe", "volumeName": "interior", ... },
{ "shProbePath": "sh_coeffs/exterior.shprobe", "volumeName": "exterior", ... }
]

Export directory layout

After export, the directory structure next to the .gltf file:

scene.gltf
scene.bin
sh_coeffs/
probes.shprobe

Engine Loading

When a glTF scene is loaded, FrameProcessing.cpp checks the scene-level VULKAN_SCHNEE_engine_properties extension for a shProbeGrids array. Each entry is parsed into an SHProbeGridInfo struct and the referenced .shprobe file is loaded via SHProbeGrid::loadFromFile().

Probe data is uploaded to a GPU storage buffer (SHProbeBuffer). Index 0 is always the neutral fallback probe (hardcoded SH coefficients matching the shader defaults). Objects outside all probe volumes receive this fallback.

Per-object probe assignment uses RenderingDataManager::queryProbeIndex(worldPos), which iterates loaded grids and calls SHProbeGrid::getProbeIndex() on each. The first grid returning a valid index (≥ 0) wins. Invalid occupancy grid cells (value -1) also fall back to index 0.


.shprobe Binary Format

SHPR version 3 format, little-endian:

Offset Size Type Field
0 4 uint32 Magic: 0x52485053 ("SHPR")
4 4 uint32 Version: 3
8 4 uint32 Probe count
12 4 uint32 Root grid dim X
16 4 uint32 Root grid dim Y
20 4 uint32 Root grid dim Z
24 4 uint32 Octree node count
28 4 uint32 Root index count
32 4 uint32 Max octree depth
36 4 uint32 Reserved
40 4 float Grid origin X
44 4 float Grid origin Y
48 4 float Grid origin Z
52 4 float Root cell size (meters)
56 4 float Min cell size (meters)
60 4 float Default SH scale
64 4 float Default ambient term
68 probeCount × 160 - Probe data (9 × vec4 coefficients + scale + ambient + 8 bytes padding)
after probes rootIndexCount × 4 int32[] Root index grid (-1 = empty root cell)
after root index grid nodeCount × 16 uint32[4][] Sparse octree nodes: childMask, firstChild, dataIndex, flags

Root index grid flat index: x + y * rootDimX + z * rootDimX * rootDimY. Leaf octree nodes store the probe index in dataIndex; empty leaves use 0xFFFFFFFF.


Material Parameters

The VS Movable Diffuse shader node in Blender has two parameters that control how SH data is applied at runtime. Set them per-material in the shader node graph:

Parameter Default Description
SH Scale 1.0 Multiplier for all SH coefficients. Lower it only for intentional artistic dimming.
Ambient Term 0.1 Constant added to diffuse after SH evaluation. Prevents fully dark areas in uncovered regions.

These values are exported with the material via the VULKAN_SCHNEE_materials extension and read by the engine shader pipeline.


Practical Settings

Interior room (living room, corridor):

Min Cell Size: 0.5m
Max Cell Size: 4.0m
Panorama Resolution: 128

Outdoor environment (courtyard, terrain):

Min Cell Size: 2.0m
Max Cell Size: 32.0m
Panorama Resolution: 128

Large interior with open areas (warehouse, hall):

Min Cell Size: 0.5m
Max Cell Size: 8.0m
Panorama Resolution: 128

Increasing Panorama Resolution beyond 128 has diminishing returns — SH order 2 captures only low-frequency lighting, so detail in the panorama averages out during projection. Use higher resolution only when L_MAX is set to 3 or 4.


Related Documentation