Rendering

Sky & environment

Meep's physically-based Hosek-Wilkie analytic sky model, runtime cubemap-to-SH3 capture, and the pure-JS Monte-Carlo path tracer used for offline reference renders and probe baking.

Meep provides two independent pieces of sky/environment infrastructure: an analytic sky model used for real-time sky colour and light-probe irradiance, and a pure-JavaScript path tracer used for offline rendering and as the back-end of the light-probe bake. Both live under @woosh/meep-engine/src/engine/graphics/sh3/.

Hosek-Wilkie analytic sky

The sky model is a JavaScript port of the Hosek-Wilkie physically-based sky model. It takes a sun direction, turbidity (atmospheric haze), ground albedo, and an overcast factor, and returns sky radiance for any view direction.

Two functions form the public surface:

import {
    sky_hosek_precompute,
    sky_hosek_compute_irradiance_by_direction
} from "@woosh/meep-engine/src/engine/graphics/sh3/sky/hosek/sky_hosek_compute_irradiance_by_direction.js";

// Precompute 9 XYZ Hosek coefficients + radiance scale for the given sky state.
// Call once per sky change (e.g. time of day update).
const mCoeffsXYZ = new Float32Array(27); // 3 channels × 9 coefficients
const mRadXYZ    = new Float32Array(3);  // overall average radiance

sky_hosek_precompute(
    mCoeffsXYZ, mRadXYZ,
    sunDirection,  // vec3, normalised
    turbidity,     // 1–10
    rgbAlbedo,     // vec3, linear colour space
    overcast        // 0–1
);

// Sample sky radiance in a given direction.
const out = new Float32Array(3); // RGB result
sky_hosek_compute_irradiance_by_direction(
    out, mCoeffsXYZ, mRadXYZ, sunDirection,
    dir_x, dir_y, dir_z
);

sky_hosek_precompute interpolates precomputed quintic Bernstein-polynomial datasets (kHosekCoeffsX/Y/Z, kHosekRadX/Y/Z from data.js) over turbidity and solar elevation. Results are in XYZ colour space and automatically converted to linear RGB by sky_hosek_compute_irradiance_by_direction. Two overrides handle special cases: when the sun is below the horizon, the sun-scattering terms (C, E, F, H in the Hosek 9-parameter model) are faded out and the zenith darkened; when overcast > 0, the model blends toward the CIE standard overcast sky.

The helper make_sky_hosek wraps both calls into a single closure suitable for use as the __background_sampler of a PathTracedScene:

import { make_sky_hosek }
    from "@woosh/meep-engine/src/engine/graphics/sh3/path_tracer/make_sky_hosek.js";

const skyFn = make_sky_hosek(
    [0.3, 0.9, 0.1],  // sun direction
    3,                  // turbidity
    0,                  // overcast
    [0.1, 0.1, 0.1]    // ground albedo
);
// skyFn(result, result_offset, direction, direction_offset) → writes RGB into result[]

Runtime cubemap capture and SH3 projection

fromCubeRenderTarget reads all six faces of a Three.js WebGLCubeRenderTarget and projects them into an L2 spherical-harmonics (SH3) coefficient array in one pass. The result is a Float64Array of 27 values (9 coefficients × 3 colour channels), suitable for use as light-probe data:

import { fromCubeRenderTarget }
    from "@woosh/meep-engine/src/engine/graphics/sh3/fromCubeRenderTarget.js";

// cubeRenderTarget: THREE.WebGLCubeRenderTarget, already rendered
const data = new Uint8Array(resolution * resolution * 4);
const coefficients = fromCubeRenderTarget(data, renderer, cubeRenderTarget);
// coefficients: Float64Array[27]

The function weights each pixel by its solid-angle contribution (4 / (|n|³), the standard sphere-sampling correction for a unit cube), normalises by 4π / total_weight, and applies sh3_dering_optimize_positive to suppress ringing artefacts.

WebGLCubeProbeRenderer uses this function directly: it positions a Three.js CubeCamera at each probe location, renders the scene once per probe using the GPU, reads the result back with readRenderTargetPixels, and projects to SH3. This is the faster of the two bake back-ends; it captures only direct illumination and single-bounce reflections, not true multi-bounce GI.

Pure-JS path tracer

The path tracer in src/engine/graphics/sh3/path_tracer/ is a complete Monte-Carlo renderer written in plain JavaScript. It is the engine used by PathTracerProbeRenderer for light probe baking and can also produce offline reference images.

Architecture

ClassRole
PathTracercore path-tracing loop
PathTracedScenescene graph: meshes, lights, BVH, background sampler
PathTracedMeshone mesh instance (geometry + material + transform + per-mesh BVH)
BufferedGeometryBVHper-geometry BVH built from a THREE.BufferGeometry

PathTracedScene maintains a two-level BVH: a top-level BVH over mesh AABBs (Morton-coded and optimized with treelet restructuring), and a per-mesh BVH for triangle intersection. The top-level structure is rebuilt via scene.optimize() after adding or removing meshes.

Tracing a path

PathTracer.path_trace traces one path from a ray:

import { PathTracer }
    from "@woosh/meep-engine/src/engine/graphics/sh3/path_tracer/PathTracer.js";

const tracer = new PathTracer();
const irradiance = new Float32Array(3);

tracer.path_trace(
    irradiance, // output RGB
    ray,        // Ray3 or [ox, oy, oz, dx, dy, dz, tMax]
    1,          // min_bounce (before Russian roulette applies)
    5,          // max_bounce
    Math.random,
    scene
);

Each bounce: trace the BVH for the nearest hit, sample the surface material (colour and scattered direction), shadow-test toward each scene light (directional and point), accumulate the light contribution weighted by the cosine lobe, and advance the ray. Russian roulette terminates the path with probability 0.5 after min_bounce bounces.

Populating the scene

populate_path_traced_scene_from_ecd traverses the ECS dataset and adds every ShadedGeometry + Transform entity to the scene:

import { populate_path_traced_scene_from_ecd }
    from "@woosh/meep-engine/src/engine/graphics/sh3/path_tracer/populate_path_traced_scene_from_ecd.js";

const scene = new PathTracedScene();
populate_path_traced_scene_from_ecd(ecd, scene);
scene.optimize(); // rebuild top-level BVH

The Hosek sky can be used as the background by assigning make_sky_hosek(...) to scene.__background_sampler.

Practical limits

The path tracer runs entirely on the CPU, single-threaded. At 4096 samples per probe with 5 bounces, a scene with several hundred probes will take seconds to minutes in the browser. For interactive work, use WebGLCubeProbeRenderer; reserve the path tracer for final bake quality or offline tooling.