Reference for recovering an offline zone_lighter configuration from the stored per-zone .zonel lumel output, without access to the original build cfg. Developed while reversing the 2001-era snowballs lighter config and reused verbatim for the 2006 r2_* Ring continent configs (both summarized below).
The method is general to any NeL landscape lighting that fits the era combine formula and works from stored lumels alone; the specific era-tool tessellation subtleties in the "Analytic lattice" section apply when reversing a config for a lighter old enough to predate the modern refine path.
zone_lighter_2001)The canonical recovery, matching the shipped 2003 client lumels. Cfg: nel/tools/3d/zone_lighter_2001/cfg/snowballs_recovered.cfg, per-parameter evidence in comments.
| Parameter | Value | Notes |
|---|---|---|
sun_direction |
{ -0.095, +1.0, -0.50 } |
Pre-2001-10-26 direction; preserved as a commented-out line in the era cfg/properties.cfg. Trimmed identity fit locks it to 0.024° over 152,097 lumels. |
sky_contribution |
1 |
ON. Facing-away floor at ~190.5 pins this. |
sky_intensity |
0.8 |
Uniform 1.066 deficit ratio at SI=0.75 pins SI=0.8; 0.75 and 1.5 tested and rejected. |
shadow |
1 |
ON. |
softshadow |
1 |
Era polygonal softshadow. |
softshadow_blur_size |
1.0 (m) |
|
softshadow_fallof |
10.0 (m) |
Penumbra saturates beyond this range. |
softshadow_shape_vertex_count |
10 |
|
shadow_bias |
1.0 |
|
oversampling |
0 |
|
load_ig |
1 |
|
| Dependencies | 8-neighbour ring exactly | 5×5 wrecks 3_AK 86→65% blocks; solo drops to 4%. |
Reproduction: lowercase mirror of zones+igs, original-case shapes dir, client bank (data/tiles/bank.bank), envs NL_ZONE_LIGHTER_NO_NOISE=1 NL2001_ANALYTIC_LATTICE=1.
Gate: 96/96 reference zones, 88.08% blocks byte-identical / 98.10% values exact; best zones 96%+ blocks / 99.7% values.
zone_lighter)The parameters translated forward for the modern lighter (combine formula and sky model unchanged; shadows moved from era polygonal to zbuffer sun-camera). Cfg: nel/tools/3d/zone_lighter/cfg/snowballs_derived.cfg with @TOKEN@ paths.
| Parameter | Value | Notes |
|---|---|---|
sun_direction |
{ -0.095, +1.0, -0.50 } |
Identical to era. |
sky_contribution |
1 |
Identical. |
sky_intensity |
0.8 |
Identical. |
shadow |
1 |
Identical (softshadow toggle folded into sun_radius). |
sun_center |
{ 1599, -835, 0 } |
Map bbox center. |
sun_radius |
500 |
0.01 rad = saturated ~1 m penumbra at ~100 m occluders, matching the era saturation slope. Default 5000 (0.1 rad) over-blurs shadows past 10 m. |
sun_distance |
50000 |
Modern default. |
Gate: 96/96 reference zones via the LIGHT tier in snowballs_corpus.py: 91.81% exact / 98.88% ±3, 0 failed, no zone below 90%.
zone_lighter)Recovered from the 2006 originals in ryzom_setup_644.exe. The mainland configs were CVS-recovered (per Kaetemi) and are NOT improvised; the r2_* configs in the leveldesign workspace were improvised during OSS asset recovery, and the reversal confirmed those improvisers guessed all five sun directions right by borrowing ecosystem-matched mainland values. sky_intensity for r2_desert and r2_roots was corrected (leveldesign 9027698f).
| Continent | sun_direction |
sky_intensity |
Corpus exact / ±3 (zones) |
|---|---|---|---|
r2_desert |
{ -0.5, 0, -0.85 } |
0.25 (was 0.20 improvised) | 79.4% / 90.6% (351) |
r2_forest |
{ -0.776685, +0.216619, -0.59147 } |
0.20 | 77.8% / 89.0% (623) |
r2_jungle |
{ -0.776685, +0.216619, -0.59147 } |
0.20 | 68.8% / 84.4% (660) |
r2_lakes |
{ -0.5, 0, -0.85 } |
0.50 | 74.2% / 91.5% (550) |
r2_roots |
{ -0.5, 0, -0.85 } |
0.25 (was 0.50 improvised) | 76.0% / 83.4% (391) |
Sun camera: kept the CVS-recovered mainland production values sun_radius / sun_distance = 5000 / 50000 (0.1 rad, π/6 fov). Penumbra A/B on a low-sun jungle zone rules out >0.1 rad, can't resolve below 0.05 given the corpus resolution. sun_center improvised map-bbox values show no edge-of-map shadow displacement, confirmed consistent.
Sun fits converge <0.05° onto a mainland literal with decisive inlier volumes (roots: 44.9% correct basin vs 0.3% wrong basin). Sky peaks are sharp (roots: 74.4/77.4/74.4 exact at 0.24/0.25/0.26). Forest ecosystem shares the jungle bank; confirmed by fit quality.
Gate: 2575 ring zonels relit against 2006 originals, 0 failures. Residual taxonomy same as snowballs (cross-era codec skew, shadow-edge sampling, float tier).
The lighter (2001-onward, unchanged in its combine to the modern zbuffer variant) writes one uint8 lumel per patch texel as:
final = 255 · (shadow · clamp(−N·D − sky, 0, 1) + sky)
N = per-lumel normal (bezier tessellation, possibly noise-composed from tile displacement maps)D = sun direction (unit vector)sky = sky_intensity · sky_contribution_factor(N) — for the era formula, sky = SI · N_z · sin²(fPhi) where fPhi comes from the horizon tableshadow = 1 in open sky, 0 fully shadowed, fractional in penumbraThe key structural feature is the max: unshadowed sun-facing lumels evaluate to −N·D; unshadowed sun-away lumels floor at sky; shadow only enters when the sun term exceeds the sky floor.
Two probes and one baseline.
NL3D_ZONE_LIGHTER_DUMP_LUMELS (env in CZoneLighter::light) — dumps the exact per-lumel positions and normals the lighting pass uses. This is the input side of the reversal: the lighter's own view of the geometry, so you're fitting against the same normals the reference build saw (assuming the tessellation matches; see below).zone_lumel_dump (scratch tool) — extracts decompressed reference lumels via CZone::retrieve. This is the output side: what the original build wrote..zonel codec is lossy 4×4 block quantization. Load → retrieve → build → save on our own output survives only ~57% byte-exact (±22 tails in high-gradient blocks). A CORRECT model should therefore match refs at ~57%+ where right; every wrong model sits at ~1%, with the only fully-matching 4×4 blocks being trivially black. This is the noise floor to compare against.Always compare AFTER a run, not directly against raw model predictions: the codec noise dominates and both sides must share it. Whole-run compression roundtrip is idempotent enough (99.9%/±2 on refs) that block-percentage-exact is a stable signal at the true parameter values.
The identity line is the cheap discriminator: when shadow = 1 and dot − sky ∈ (0, 1), ref = −N·D exactly (sky cancels, no clamp active). Trimmed LSQ over the lumel set that sits within some tolerance of that identity gives a direction candidate with per-lumel evidence.
For every lumel (N, ref):
predict = -dot(N, D_candidate) * 255
residual = ref - predict
Trim outliers, refit.
Two arbitration signals to pick the correct basin among candidates:
a + w·N (an affine model of the observation against the lumel normal) to both the reference lumels and to your own clamped-Lambert output run through the same tool. Matching (slope, ambient) signatures across the two paired with mismatched directions means wrong D, right model — the ambient is picking up the sky floor, the slope is the sun term's magnitude. This was the compass that pointed at the correct basin on snowballs after the wrong-basin fit had wasted several passes.|D| should converge to 1.0000 within noise. |D| < 1 by more than the lumel discretization means the model is fitting through a distorted geometry (typically wrong tessellation; see analytic lattice below).
Once the sun direction is locked, the sky_contribution / sky_intensity are pinned by the sun-away lumels (unshadowed, −N·D ≤ 0), which floor at 255 · SI · N_z · sin²(fPhi).
N·k sky term is near 0 anyway. Rule sky-on in only when the sky-on grid shows a sharp peak that beats the sky-off model on the flat-open subset.Shadow enters as the multiplier on the sun term when −N·D > sky. The per-lumel shadow-factor field can be extracted from unshadowed vs partially-shadowed patches:
s = (ref − sky_component) / (dot − sky_component)
For fully-lit lumels s ≈ 1, for penumbra 0 < s < 1, for full shadow s = 0.
For the era polygonal softshadow the free parameters are blur_size, fallof, sample vertex count, and bias. Snowballs settled on blur = 1.0, fallof = 10.0, 10 vertices, bias = 1.0. Sensitivity is measurable: on snowballs 3_AK, sun_distance 50k → 500k changes 25% of lumels, sun_center map-center → origin changes 4%. The map bbox center is a defensible default for sun_center when no other signal is available.
For the modern zbuffer sun-camera the free parameters are sun_radius and sun_distance and their ratio maps to a penumbra angle. See "Modern derivation" below.
Zone lighting depends on the surrounding zones' geometry through the GI heightfield horizon term. Getting the dependency set wrong looks like a whole-model failure, not a per-parameter one.
The zone_dependencies tool that computed the reference dependency lists explicitly produced 8-neighbour sets; on any recovered lighter, ensure the same shape is emitted, and A/B against the alternatives if a whole-corpus fit stays low despite good per-parameter fits.
Applies to lighters resurrected against a modern NeL landscape library. The 2001 refineAll moved every tessellation vertex to EndPos == computeVertex == eval at threshold 0. The modern refine path leaves enforced-split vertices geomorphed at StartPos (linear parent midpoints), which diverges from CBezierPatch::eval by up to ~9 m per leaf in flat cases.
A resurrected tool reading Pos inherits this divergence in all three lumel-lighting consumers: pass-1 positions, pass-2 normals, and shadow-caster triangles. The reference lighter, running on era-CLandscape, never saw the divergence. Match refs by forcing the tessellation to analytic values:
NL2001_ANALYTIC_LATTICE=1 on zone_lighter_2001 replaces leaf vertex positions with pure eval(s,t) in all three consumers. Snowballs gate jumps 0.41% → 34% blocks on this flag alone.The modern nel/src/3d/zone_lighter.cpp is not affected — it reads face->V*->EndPos directly, which the modern refine path fills correctly. Only tools that read the geomorphed Pos hit this.
The bank references the per-tileset displacement (noise) maps under an authored path like R:\graphics\...\displace\bump*.tga. Offline lighting consumers compose the authored path verbatim; on Linux (or on any machine that isn't the bank author's), the load silently fails and CTileBank::getTileNoiseMap substitutes a static placeholder pattern. Every lumel normal then tessellates with wrong nonzero noise.
Fixed at NeL level (ryzomcore 508885216): when the authored path doesn't exist, fall back to CPath::lookup of the bare backslash-normalized filename. Empty filenames (e.g. snowballs' displacement-empty client bank) keep the old behavior. The client makes the same paths relative on load, which is why it never saw the failure.
Any offline lighting consumer needs this fix. Without it, all fits are against wrong normals; the snowballs recovered sun direction landed cleanly, but only after the noise maps loaded correctly.
For eras that lit with no displacement at all (snowballs shipped its client bank with all displacement tiles = 0), the additional env NL_ZONE_LIGHTER_NO_NOISE=1 short-circuits the noise composition entirely.
CPath case-sensitivity on LinuxCPath alternative-dir lookup concatenates the LOWERCASED filename, so mixed-case era assets (PI_PO_*.shape, *.zonel deps) silently fail on Linux. Prepare a lowercase symlink mirror (sb_lc/ and friends) for era data before running any offline tool that will resolve mixed-case names.
The 2001 tool loads dependency zones by literal dir+name+ext (no CPath) — dependency names in the dep cfg must match the filesystem case in that path.
The 2001 unpackLumelBlock dequantizes with exact integer /7 and /5 divisions; the modern lumel decoder uses >>8 LUT approximations that differ by ±1..2 per value. The block codec's basin flips amplify single-lumel deltas to whole-4×4 mismatches when you compare raw byte-percentages across the two decoders.
snowballs_zonel_gate.py) implements both quantizations for the corpus tester.Once the era-lighter cfg is recovered and validated at the era codec level, translate forward for the modern zone_lighter. The combine formula and sky model are unchanged; only shadows moved from era polygonal softshadow to the modern zbuffer sun-camera. Penumbra mapping:
blur_size / fallof beyond fallof — the 1 m / 10 m era default is a saturated ~1 m penumbra at ~100 m occluders and beyond.sun_radius / sun_distance.blur_size / fallof (rad) ≈ modern sun_radius / sun_distance. The modern default sun_radius = 5000, sun_distance = 50000 (0.1 rad) matches the era pre-saturation slope but over-blurs everything beyond 10 m.sun_radius = 500 (0.01 rad = the saturated ~1 m penumbra at ~100 m occluders). Improves every zone measured; picks up 5-10 percentage points on ±3 in some zones.Wire a LIGHT tier into the corpus tester: relight all reference zones with the derived modern cfg, tolerance-compare against the reference .zonel, and gate on ±3 ≥ threshold (snowballs gates 96%). Score both the era codec baseline and the modern derivation; the deltas taxonomize as cross-codec skew + float codegen tier + asset drift + shadow-edge sampling.
After a full parameter recovery, several residual sources remain, none of which are chase-worthy for parameter validation:
zone_lighter_2001 matches gcc/SSE within one block on snowballs 3_AK — confirmation that the residual is MSVC6 x87 codegen and micro-model detail (era tessellation averaged EndPos across multipatch binds; the analytic-lattice path uses own-patch eval). Chasing this means emulating MSVC6 codegen and the bind-averaging.|d|>10 drops 2.11%→0.77% with load_ig=0, mirroring T3-side zone-source drift on the same file.None of these are parametric. If a corpus fit stays significantly below the residual-taxonomy expectation, keep looking for the parameter miss (check the dependency ring first, then the noise-maps trap, then the tessellation).
For a fresh reversal on any lost lighting cfg (mainland-scale, Ring-scale, snowballs-scale):
getTileNoiseMap fix is present in your NeL tree (ryzomcore 508885216 or later).zone_lumel_dump) and probe-relight lumels (NL3D_ZONE_LIGHTER_DUMP_LUMELS).NL2001_ANALYTIC_LATTICE=1 (or the equivalent for the tool at hand).Traps to watch for at each step: silent noise-map placeholder, CPath lowercase folding, lumel codec skew, wrong-basin sun fits with plausible-looking residuals.