Skip to content

leftspace89/lightmapbaker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

D187LightMapBaker

Bake real shadow/lighting lightmaps from a world's own lights and inject them into an already-compiled .World00p in place — no recompile. The engine's shaders\rigid\Solid\diffuse_spec_lightmap.fx shader supports lightmaps; this tool bakes and wires them up.

Showcase

Lightmap Studio

Lightmap Studio — live perspective viewport (fly + FPS-walk camera), realtime light editing with 3D gizmos, per-surface bake selection, and a one-click bake.

Lightmap Studio in action — viewport, light gizmos, and bake panel.

Baked soft shadows + ambient occlusion in-engine.

Baked atlas pages

Baked multi-page lightmap atlas — each tile is one surface's soft-shadow + ambient-occlusion lighting, packed proportional to its world area.

What it does

  1. Reads the world's render geometry + its light rig (LightPoint/Area/ Directional/Spot) + GI/light blocker polys. Sky/cloud and engine shadow-volume surfaces are excluded from the occluder set (a finite skybox above the level would otherwise cast a giant fake shadow over the interior).
  2. Unwraps each surface into non-overlapping lightmap charts with xatlas (worlds often lump a whole multi-directional structure into one surface, which a single-plane projection can't unwrap without overlap). Vertices are duplicated at chart seams.
  3. Bakes a multi-page lightmap atlas with embree ray-traced soft shadows + ambient occlusion, using each light's own colour/intensity/radius.
  4. Generates lightmap-variant .Mat00 materials (diffuse on UV1, atlas on the emissive UV2) and rebuilds the lit surfaces into the 64B lightmap vertex layout (expanded chart-cut verts + reindexed triangles, render BSP tree kept valid), splicing the new render section back into the world.
  5. Stages the world + atlas pages + materials into the game tree.

Usage

--src (the world to lightmap) and --base-tree (the loose game data dir containing Materials\..., used to read each material's base texture) are required.

# quick test bake (~35s)
python build_lightmap_bake.py --src World.World00p --base-tree path\to\Game \
    --name MyMap --fast

# high-quality bake (~3-4 min) — the tuned settings
python build_lightmap_bake.py --src World.World00p --base-tree path\to\Game \
    --name MyMap --quality

# a preset, with an individual override
python build_lightmap_bake.py --src World.World00p --base-tree path\to\Game \
    --name MyMap --quality --pages 8

Presets

  • --fast — quick iteration: 2 pages, low samples, 1024px max tile.
  • --quality — the tuned high-quality settings: 6 pages, 16 shadow samples, 24 AO samples, sharp sun (0.35° disk) for crisp shadows, 2048px max tile for floor detail, low ambient + light denoise for shadow contrast.

Both presets also set --spec-intensity 0 (kills lightmap-unmodulated specular glare). SurfaceFlags are left as the base material's value.

Any individual flag (--pages, --ao-samples, --ambient, …) overrides the preset. With no preset, the built-in BakeOpts defaults apply (a medium quality). Notable extra flags: --bias (shadow-ray offset; raise to kill self-shadow acne), --spec-intensity, --dir-transform (debug: remap the directional emit axis to A/B-test the engine's convention).

Output is staged locally under ./output/, mirroring the game tree:

output/Worlds/<World>.World00p
output/Data_Bg/Lightmap/<name>/LM_*.dds        (atlas pages)
output/Data_Bg/Lightmap/<name>/LM_*/...Mat00   (lightmap materials)

Copy the contents of output/ into the game root when you're happy with it (override the location with --stage). Run python build_lightmap_bake.py -h for every setting (atlas size, pages, ambient, exposure, sun/point boost, AO, soft-shadow samples, sun angle, denoise, dilate, workers, paths).

GI/light blockers in the target world are used automatically as bake occluders (no flag needed) — they seal light leaks. If the world has none (0 blockers), there's nothing to seal.

GUI - Lightmap Studio

A PySide6 + moderngl front-end for everything the CLI does, modelled on tools/skin_edit. Live perspective viewport with WASD + RMB fly camera, realtime light preview (sun + point/spot Lambert with the same windowed attenuation the baker uses), project hierarchy (lights grouped by kind, plus surfaces with material + LM flag), per-light property pane, a full bake panel mapped to every CLI flag, one-click bake (runs in a QProcess, stdout streamed to the log dock), and on success an auto-load of the freshly baked world with its LM_0 atlas bound for the Atlas shading mode.

py lightmap_studio.py [--base-tree DIR] [WORLD.World00p]
# or:  lightmap_studio.bat World.World00p

Viewport controls

Input Action
RMB drag mouse-look
WASD move forward / strafe (hold RMB for proper FPS feel)
Q / E drop / rise (world up)
Shift sprint
Wheel (no RMB) adjust fly speed
Wheel (with RMB) dolly along forward
F frame the whole world
T snap to top-down

Shading modes (toolbar dropdown)

Mode What you see
Unlit (height) render_preview.py look: blue→green→orange height gradient
Sun N·L only the sun, dot-product against face normals (shadowed)
Realtime Lit per-material diffuse × (ambient + sun-with-shadow-map + per-pixel point/spot Lambert)
Flat grey mid-grey shaded by normal, useful for reading geometry
Atlas per-material diffuse × bound LM_0.dds via UV2 (for inspecting a baked result)

Materials: each surface's .Mat00 is read from --base-tree, its tDiffuseMap decoded (DXT1/3/5) and bound on UV1. Missing materials fall back to neutral grey. Megatextures are downsampled to 1024px on upload to keep VRAM in check. Edit Base tree in the Bake panel and the world re-loads with the new path's materials.

Sun shadow map: a 2048² ortho shadow map of the strongest directional light is re-baked when the sun (or geometry) changes; the main pass samples it with 3×3 PCF for soft edges. Toggle via the Shadows checkbox.

Light editing (preview only — does not save back to the .World00p)

Select a light in the project tree; the right panel becomes editable spinboxes for position, color, intensity, radius, engine euler X/Y/Z (X = pitch, where 90° = straight down — same convention as tools/world00p/fear_sun_rotation.py and tools/blender_addon/format/coord.py; the quaternion is stored as truth and direction + right + up are re-derived via quat_rotate) and FOV. Edits mutate the light in memory and re-render gizmos + shadow map + lighting live. The + Light button drops a new directional/point/spot/area at the world center; − Light removes the selected one. These edits affect what you SEE but the bake still runs against the on-disk world — there is no round-trip writer yet.

Translate gizmo: when a light is selected, three colored axis arrows (X red / Y green / Z blue) appear at its position, sized to a constant on-screen length. Left-click + drag an arrow to slide the light along that world axis; the editor spinboxes update live. The arrows render on top (depth test off) so they stay visible through geometry.

WorldModel placement: render geometry whose surfaces belong to a movable WorldModel slot (cranes, trains, sky cubes, animated props…) is stored in LOCAL space and placed at runtime by its owning object's Pos + Rotation. The studio replicates tools/blender_addon's import logic: builds a slot name table from world_models.bsp_name_entries, votes each surface to its owning slot by vertex-position lookup, then applies the owner's quat + pos per vertex. Without this fix, movable models cluster at the origin.

Live bake preview (WYSIWYG): ambient / exposure / sun boost / point boost in the Bake panel are the single source of truth — editing them updates the realtime viewport instantly, so what you see is what the bake will produce. The toolbar Ambient/Exposure sliders mirror the panel.

Walk mode (FPS): toolbar Walk Mode (or F1) drops a player into the world. WASD walks, Shift sprints, Space jumps. Gravity + a downward ray-cast against world geometry (via trimesh/embreex) keeps the player on the floor. The Character tab in the right dock tunes height, move/jump speed, run multiplier, gravity, FOV, step-up height and feet offset; values persist to tools/lightmapbaker/studio_settings.ini so they survive restarts.

Light gizmos (always on, toggle via the "Gizmos" checkbox)

  • Directional / sun — long colored arrow across the world along its emit direction + a fainter ground-projection arrow + ring glyph at the source.
  • Point — sphere at Pos + three orthogonal LightRadius rings.
  • Spot — wedge from Pos to a FovX×FovY rectangle at LightRadius.
  • Area — rectangle from local right/up.
  • Selecting a light in the project tree highlights it in bright yellow (double-click to focus the camera on it).

Helpers

  • _probe_lights.py <world> — list a world's light rig.

  • _probe_render_surfaces.py <world> — dump render-section layout; confirm lit surfaces are 56B (promotable) and whether it's already lightmapped.

  • _verify_lm.py <staged_world.World00p> — integrity check + atlas-page PNG.

  • build_lightmap_test.py — inject a flat solid-colour test atlas (plumbing check, no bake).

  • render_preview.py — pre-bake preview PNGs of the world geometry with every light overlaid (sun arrows with direction + euler, point lights with their falloff radius rings, spot cones drawn to FovX/FovY at LightRadius, area rectangles). Top-down + two side views, painter's-sorted, height-shaded.

    python render_preview.py --src World.World00p --out images/preview --res 1400
    # or pick views: top, side_x, side_z (comma list)
    python render_preview.py --src World.World00p --views top,side_z
  • lm_inspect.py — inspect/debug a baked world's lightmap (decodes the DXT1 atlas, no engine needed). Three modes:

    # plan view of the baked floor + sun/shadow-cast direction arrow
    # (sanity-check shadow direction)
    python lm_inspect.py topdown \
        --world  output/Worlds/MyMap.World00p \
        --lmdir  output/Data_Bg/Lightmap/MyMap \
        --out    images/td.png
    
    # dump one surface's atlas tile (autocontrast) to inspect its baked lighting
    python lm_inspect.py tile \
        --world output/Worlds/MyMap.World00p \
        --lmdir output/Data_Bg/Lightmap/MyMap \
        --surface 5 --out images/surf5.png
    
    # per-surface lit-check: mean normal, N·(toward-sun) (lit if >0), mean baked
    # luminance — flags surfaces that face the sun but baked dark, or vice versa
    python lm_inspect.py faces \
        --world output/Worlds/MyMap.World00p \
        --lmdir output/Data_Bg/Lightmap/MyMap

Layout

File Role
build_lightmap_bake.py main driver: bake → materials → inject → stage
lightmap_baker.py xatlas unwrap + embree bake (pack, raster, soft shadows, AO, denoise, multiprocessing)
inject_lightmaps.py rebuild surfaces to 64B layout (chart-cut verts + reindex) + splice render section
lightmap_assets.py LTMI .Mat00 read/write + DXT1 encode
world_lights.py parse the light rig from the object section (+ quat→euler)
lm_inspect.py inspect/debug a baked lightmap (topdown / tile / faces)
render_preview.py pre-bake top/side PNG previews with light gizmos
lightmap_studio.py GUI front-end (PySide6 + moderngl). Subpackage in studio/.
studio/ GUI subpackage: main_window, viewport, scene, fly_camera, gl_shaders, math3d, textures, bake_runner.
worldparser/ vendored copy of the world00p binary parser (format/model/sections/roundtrip). Source of truth is tools/world00p/.

Requires

Install everything with:

pip install -r requirements.txt

That pulls numpy trimesh embreex xatlas pillow (core baker) plus PySide6 moderngl (the GUI) — embreex is the fast raycaster, xatlas does the non-overlapping lightmap unwrap, pillow is for the verify/preview/inspect PNGs. (If xatlas is missing the baker falls back to a single-plane unwrap, which only works for genuinely planar surfaces.) If you only use the CLI you can skip the PySide6/moderngl lines.

Lighting troubleshooting

The whole bake comes out dark / flat / no sun, but the realtime preview looks bright. Almost always the world's GI/light-blocker box is sealing out the sky. Those invisible blocker brushes seal light leaks indoors, but most maps have one big box over the level that walls off the sun → the whole map bakes to flat ambient. It's off by default now; only turn it on (--use-blockers, or the studio's "Use GI blocker box" checkbox) for genuinely sealed indoor maps. (The realtime preview looks bright because its shadow map is too coarse to capture the box — it's the bake that's telling the truth.)

Other dark-bake causes, in rough order:

Symptom Cause Fix
Flat/dark everywhere, ~no sun GI blocker box (above) leave --use-blockers OFF
Whole map dim but shaded low --ambient (e.g. --fast uses 0.15) use --quality or raise --ambient
Only part of the map lit low sun blocked by terrain/rim raise the sun's elevation (edit the light's pitch in the studio)
Trees cast no shadow canopy too transparent lower Foliage transmit (0=solid crisp tree shadows, default)
Under trees too dark canopy fully solid raise Foliage transmit toward 1 for softer/dappled
One specific thing over-shadows a big occluder add its material to Exclude occluders (e.g. cliff_5)
A few objects render fully black in-game (but fine in the studio) their base shader had no matching *_lightmap.fx in the engine, so the generated material failed to load fixed: variants are now mapped by capability to a shader that actually ships (specular_bumpdiffuse_spec_normalmap_lightmap, diffuse_speculardiffuse_spec_lightmap, …). Re-bake any world baked before this fix
Colours look wrong/blue-grey n/a ambient now auto-tints to the lights — a red light gives a red ambient

The bake log prints a surface-selection report (what's promoted / skipped / kept-unbaked and why) and an occluder report — check those first when a result surprises you.

Notes

  • GI/light blocker polys are off by default as occluders (they usually seal out the sun); enable with --use-blockers for sealed indoor maps. The baker warns if an enabled blocker volume looks like a sky-sealing box.
  • Sky/cloud (...\Sky\..., Sky_Day*, CloudPlane*) and engine shadowvolume surfaces are auto-excluded from the occluder set — a finite skybox sits above the level and would otherwise cast a huge fake shadow over the interior (e.g. a central tower baking fully dark).
  • Each surface is unwrapped into non-overlapping charts (xatlas); vertices are duplicated at chart seams and triangle indices rewritten in place (same order, so the render BSP tree stays valid). A single surface's chart-cut vertex count must stay under 65536 (uint16 indices); the injector errors clearly if not.
  • Lightmap atlas binds to the material's tEmissiveMap (UV2); the base texture stays on tDiffuseMap (UV1). The shader multiplies them, so a black atlas = full shadow.
  • Each base material is converted to an existing *_lightmap.fx shader family, chosen by capability (normalmap→diffuse_spec_normalmap_lightmap, env→diffuse_spec_env_lightmap, specular→diffuse_spec_lightmap, else diffuse_lightmap) so normal/env/spec aren't lost. The engine only ships the diffuse_spec* / diffuse_alpha* / translucent_* lightmap variants — it has no specular_lightmap / specular_bump_lightmap / diffuse_specular_ lightmap, so the baker never emits those (doing so rendered the surface black in-game). Self-lit / transparent blends (emissive, additive, multiply, glass, water, decal, …) have no opaque lightmap form, so they're kept un-baked (render normally, just no baked shadow). specular.fx maps to the diffuse-spec family. The base material's properties are carried over; tEmissiveMap is added. Unknown shaders fall back to "base props + emissive".
  • Point and spot lights do cast shadows (occlusion ray surface→light per sample). If point-light shadows look absent it's usually the light setup — high intensity saturating to white, many overlapping lights filling each other's shadows, or a large soft radius. Tune --point-boost, --shadow-samples, --light-soft.
  • Point/spot attenuation uses a smooth windowed falloff (1-(d/r)²)² (reaches most of LightRadius then rolls off), not a hard (1-d/r)². FovX/FovY are degrees; FarZ is a fixed projection field (constant 300 in retail), not the reach — reach is LightRadius.
  • Spot projection cookies: if a spot has a Texture (projected cookie/gobo, e.g. Tex\Lights\Light_01_P.dds), the baker samples it and shapes the baked pool to that texture (a circle), so baked light + shadow stay inside the lit area instead of spilling into the FovX×FovY rectangle. Auto-detected; resolved relative to --base-tree. Spots without a texture use the rectangular frustum with a soft edge. (The engine renders the cookie's illumination dynamically but casts no shadow for it — the baker supplies the shadow.)
  • Hard/blocky "stair-step" light edges are lightmap resolution, not the falloff: --fast packs big floors at low texel density. Use --quality (or raise --max-tile/--pages); lm_inspect can show units-per-texel.
  • ~4 workers is the sweet spot (embreex is already internally multithreaded).

License

MIT © leftspace89

About

D187 (S2 SonSilah) LightMap texture baker.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages