diff --git a/README.md b/README.md
index c9d8c303..b4315436 100644
--- a/README.md
+++ b/README.md
@@ -56,9 +56,10 @@ unsupported versions rather than silently relabelling data.
sessions; Source-qualified remains selectable. See [RFC 0007](docs/rfcs/0007-illustrative-planet-looks.md) for review
status and limits, and the [rollout review](docs/PLANET_ROLLOUT_REVIEW_20260925.md)
for local evidence and outstanding delivery steps.
- Saturn's Illustrative look also uses the recovered Look Lab color response and
- inspection lighting to reveal polar colors. Its body card offers Sun-directed
- lighting for seasonal illumination and moon shadows at the modeled date.
+ Saturn's Illustrative look keeps the recovered Look Lab color response and defaults
+ to sunlight shared with its rings and moons. Shadows follow the modeled Sun as the
+ camera moves. Its body card also offers an explicit Look Lab inspection light to
+ reveal polar colors, with moon-transit shadows disabled in that mode.
- **Earth Look Lab appearance (candidate):** the latest published Sites Earth
joins Illustrative look: NASA July 2004 surface, dark-ocean grading, 1.6 display
exposure, and the recovered cloud-volume/texture-lighting recipe. Source-qualified
diff --git a/apps/web/index.html b/apps/web/index.html
index c0b525c8..c9151c78 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -286,8 +286,8 @@
-
-
Look Lab uses an inspection light near the camera. Choose Sun-directed for seasonal illumination and moon shadows.
+
+
The Sun lights Saturn, its rings and moons at the modeled date. Look Lab replaces Saturn's sunlight with a camera-relative inspection light and hides moon-transit shadows.
diff --git a/apps/web/js/illustrativeAppearance.js b/apps/web/js/illustrativeAppearance.js
index 292a2bf5..0f45162d 100644
--- a/apps/web/js/illustrativeAppearance.js
+++ b/apps/web/js/illustrativeAppearance.js
@@ -84,7 +84,7 @@ export function illustrativeDescription(body, state = {}) {
: 'Focus or zoom in to load this look. ';
const light=body==='Saturn'&&status==='ready'?(state.saturnLighting==='look-lab'
? 'Look Lab inspection lighting reveals surface color and polar detail. Lighting and ring shadows are illustrative; Sun-directed restores illumination and moon shadows for the modeled date. '
- : 'Saturn uses Sun-directed lighting with the Look Lab color response; an unlit pole remains dark. '):'';
+ : 'The Sun lights Saturn, its rings and moons at the modeled date. The Look Lab color response is retained; shadows follow the Sun and an unlit pole remains dark. '):'';
return `${readiness}${light}Illustrative look suppresses the registered surface for this planet. Solar System Scope / INOVE · CC BY 4.0. Artistic color and reconstructed coverage; not a registered observation, calibrated color or current weather. Measured terrain relief stays suspended while this look is selected, including loading, failure, and when the map is not retained.`;
}
diff --git a/apps/web/js/orrery.js b/apps/web/js/orrery.js
index b253e2bb..8e5b1dc2 100644
--- a/apps/web/js/orrery.js
+++ b/apps/web/js/orrery.js
@@ -166,7 +166,7 @@ const state = (store.orrery = {
showOrbits: true, showSky: true, showConst: false, showLabels: true, showSunEq: false, useTextures: true, galaxy: false,
earthNight: true, earthWeather: true, earthIce: false, earthCloudSource: 'composite', venusRadar: false,
appearanceStatus: {}, planetLook: 'illustrative', illustrativeStatus: {},
- saturnLighting: 'look-lab',
+ saturnLighting: 'sun-directed',
illustrativeDemandBodies: /** @type {string[]} */ ([]),
illustrativeVisibleFocused: /** @type {string[]} */ ([]),
earthLookStatus: 'deferred', earthLookPhase: 0,
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 9657be61..6d3d3407 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -37,11 +37,13 @@ adds the default fresh-session artistic mode for seven planets. Source-qualified
remains selectable; the artistic maps carry no geographic, color or observation qualification.
Measured terrain stays suspended for the whole time Illustrative look is selected,
including loading, failure, and eviction, not only while an artistic map is shown.
-Saturn's artistic material uses the recovered Look Lab color response. Its default
-inspection light follows the camera to reveal polar detail; Sun-directed lighting
-remains selectable without changing positions, epoch, scale or IAU orientation.
-Inspection ring shading uses the same light as the globe; real-Sun moon transit
-shadows are shown only in Sun-directed mode. Source-qualified keeps Sun lighting.
+Saturn's artistic material uses the recovered Look Lab color response and defaults
+to Sun-directed illumination, shared with its rings and moons. Camera movement MUST
+NOT change the Sun direction or shadow geometry. Moon lighting uses physical offsets,
+not enlarged display positions. The optional Look Lab inspection light follows the
+camera for both globe and rings; real-Sun moon transit shadows are disabled only in
+that mode. Neither mode changes positions, epoch, scale or IAU orientation.
+Source-qualified keeps Sun lighting.
Venus with Magellan radar keeps that registered ground and draws the artistic
atmosphere above it. Moons retain their existing materials. The Solar System Sun follows
[RFC 0010](rfcs/0010-approved-sun-look.md): the approved illustrative v2 recipe
diff --git a/docs/rfcs/0007-illustrative-planet-looks.md b/docs/rfcs/0007-illustrative-planet-looks.md
index 16f4e9fa..5adc7849 100644
--- a/docs/rfcs/0007-illustrative-planet-looks.md
+++ b/docs/rfcs/0007-illustrative-planet-looks.md
@@ -65,10 +65,13 @@ It also retained astronomical sunlight, whereas the lab used a 22-degree
camera-relative key light. At dates when the north pole is unlit, those views
cannot show comparable polar colors even with identical image bytes.
-Saturn now uses the recovered material in Illustrative look. Its body card offers
-Look Lab inspection lighting (default) and Sun-directed lighting. The inspection
-light illuminates both globe and rings consistently. It does not reuse real-Sun
-moon-transit shadows; those return with Sun-directed lighting. No positions,
+Saturn uses the recovered material in Illustrative look. Sun-directed lighting is
+the default: the globe, rings and moons share the modeled Sun as their light source.
+Orbiting the camera changes the view of the shadows, not their direction. Moon
+lighting uses physical offsets rather than the enlarged display positions.
+The body card retains optional Look Lab inspection lighting. That light follows
+the camera for both globe and rings and disables real-Sun moon-transit shadows.
+It is an explicit appearance inspection mode, not the solar-system default. No positions,
rotation, physical/display sizes or simulation time change. Other planets and
Saturn's Source-qualified path retain their existing materials and lighting.
Map loading/failure retains the original fallback. No texture bytes, new polar
@@ -79,8 +82,10 @@ The recovered source is recorded in
[the Sites v7 provenance](../validation/sites-earth-v7/source.json).
`tools/saturn_look_probe.mjs` checks literal lab color samples through the real
GPU sphere shader, including linear composition, and checks both poles at four
-viewing angles plus Sun-directed rollback. Runtime tests verify the material
-cannot leak to moons, pending maps, disabled textures or Source-qualified.
+viewing angles plus the default Sun-directed mode. Runtime tests verify solar
+illumination stays fixed while the camera moves and that a synthetic moon transit
+is preserved. They also verify the material cannot leak to moons, pending maps,
+disabled textures or Source-qualified.
### Existing appearance controls
diff --git a/tests/web/helpers/orreryHarness.mjs b/tests/web/helpers/orreryHarness.mjs
index 86f24e13..f83977ec 100644
--- a/tests/web/helpers/orreryHarness.mjs
+++ b/tests/web/helpers/orreryHarness.mjs
@@ -199,7 +199,7 @@ export async function orreryHarness(t, options = {}) {
orreryAnimate: node({ checked: true }), orreryNow: node(),
};
if (options.controls) {
- nodes.orrerySaturnControls=node();nodes.orrerySaturnLighting=node({value:'look-lab'},'select');
+ nodes.orrerySaturnControls=node();nodes.orrerySaturnLighting=node({value:'sun-directed'},'select');
for(const id of ['InspectSun','PhysicalStatus','SolarControls','SolarMode','SolarPlay','SolarRestart','SolarTime','SolarEpoch','SunLookControls','SolarArchiveControls','SunResolution','SunProminences','SunLookPlay','SunLookRestart','SunLookStatus','Terrain','Optics'])nodes[`orrery${id}`]=node();
if(options.phenomenonImage)nodes.orreryPlanetPhenomena=node();
for (const id of ["Backend", "MetadataEpoch", "ScaleStatus", "SelectedEpoch", "SelectionStatus", "Detail", "Labels", "Positions", "Search", "ObjectGroup", "FocusSelected", "Time", "Size", "TrueScale", "Speed", "SpeedLabel", "SpeedExtras", "SpeedEntry", "SpeedUnit", "SpeedPresets", "ShowOrbits", "ShowSky", "ShowConst", "ShowLabels", "ShowSunEq", "ShowSmall", "ShowMoons", "DeepSky", "Textures", "PlanetLook", "EarthNight", "EarthWeather", "EarthIce", "EarthLayerStatus", "IceLegend", "IceLegendCaption", "TopDown", "Anchor", "FreeFly", "Galaxy", "Local"]) {
diff --git a/tests/web/saturnLookRuntime.test.mjs b/tests/web/saturnLookRuntime.test.mjs
index d2127c41..5508d04d 100644
--- a/tests/web/saturnLookRuntime.test.mjs
+++ b/tests/web/saturnLookRuntime.test.mjs
@@ -2,6 +2,62 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {orreryHarness} from './helpers/orreryHarness.mjs';
import {BODY} from '../../apps/web/js/bodyData.js';
+import {MOONS} from '../../apps/web/js/moons.js';
+import {MOON_ELEMENTS} from '../../apps/web/js/moonelements.js';
+import {moonOffsetAU} from '../../apps/web/js/moonorbits.js';
+import {SYSTEM_ORDER} from '../../apps/web/js/systemContract.js';
+
+test('fresh Saturn uses the Sun for its globe, rings and moons, independent of the camera',async t=>{
+ // Synthetic alignment, not a claimed historical transit: retain real moon offsets
+ // and place Saturn opposite Tethys so its shadow must reach the globe.
+ const tethys={...MOONS.find(m=>m.n==='Tethys'),...MOON_ELEMENTS.Tethys};
+ const h=await orreryHarness(t,{controls:true,catalogues:'ready',useProductAppearanceDefault:true,
+ illustrativeMap:async()=>({width:2048,height:1024,close(){}}),
+ systemPositions(unix){
+ const offset=moonOffsetAU(tethys,unix),distance=Math.hypot(...offset);
+ return Float64Array.from(SYSTEM_ORDER.flatMap((name,i)=>name==='Saturn'
+ ?offset.map(value=>-9.5*value/distance):name==='Sun'?[0,0,0]:[i+1,0,0]));
+ }});
+ await h.enterOrrery();t.after(()=>h.leaveOrrery());h.setAnimate(false);
+ await h.settleCatalogues();h.input('orreryAnchor','Saturn','change');
+ await h.settle();await h.settle();
+ assert.equal(h.state.saturnLighting,'sun-directed');
+ assert.equal(h.nodes.orrerySaturnLighting.value,'sun-directed');
+ const body=h.state.bodies.find(b=>b.name==='Saturn'),position=[body.x_au,body.y_au,body.z_au];
+ const direction=pos=>pos.map(value=>-value/Math.hypot(...pos));
+ const near=(actual,expected)=>expected.forEach((value,i)=>assert.ok(Math.abs(actual[i]-value)<1e-6));
+ const paint=()=>{const start=h.gpuSubmissions.length;h.check('orreryTextures',true);return h.gpuSubmissions.slice(start);};
+ const surface=frame=>frame.find(draw=>draw.uniforms.u_bodyRadiusKm===BODY.Saturn.radiusKm&&draw.uniforms.u_mode===0).uniforms;
+ const epoch=h.state.renderUnix,positions=JSON.stringify(h.state.bodies);
+ const checkSolar=()=>{
+ const frame=paint(),globe=surface(frame);
+ near(globe.u_light,direction(position));
+ assert.equal(globe.u_saturnLook,1,'recovered colors remain enabled under sunlight');
+ assert.equal(globe.u_useTex,1);
+ assert.ok(globe.u_moonShadowCount>0,'the synthetic Tethys transit must not be suppressed');
+ const ring=frame.find(draw=>draw.kind==='arrays'&&draw.uniforms.u_prad!==undefined
+ &&draw.uniforms.u_center.every((value,i)=>Math.abs(value-position[i])<1e-6));
+ assert.ok(ring,'Saturn rings must be submitted');
+ assert.deepEqual(ring.uniforms.u_light,globe.u_light,'globe and ring shadows share the solar direction');
+ for(const name of ['Tethys','Titan']){
+ const moon=h.moons.find(m=>m.n===name),offset=moonOffsetAU(moon,epoch);
+ const draw=frame.find(d=>d.uniforms.u_bodyRadiusKm===moon.r&&d.uniforms.u_mode===0);
+ assert.ok(draw,`${name} must be drawn`);
+ near(draw.uniforms.u_light,direction(position.map((value,i)=>value+offset[i])));
+ assert.equal(draw.uniforms.u_saturnLook,0);
+ }
+ return globe;
+ };
+ const initial=checkSolar();h.state.az+=Math.PI/2;h.state.el=-.8;h.state.radius*=1.2;
+ const orbited=checkSolar();
+ for(const key of ['u_light','u_lightObj','u_moonShadowCount','u_moonShadowPos[0]','u_moonShadowAxis[0]'])
+ assert.deepEqual(orbited[key],initial[key],`${key} must not follow the camera`);
+ h.input('orrerySaturnLighting','look-lab','change');
+ const inspection=surface(paint());assert.notDeepEqual(inspection.u_light,initial.u_light);
+ assert.equal(inspection.u_moonShadowCount,0,'inspection remains an explicit separate mode');
+ h.input('orrerySaturnLighting','sun-directed','change');checkSolar();
+ assert.equal(h.state.renderUnix,epoch);assert.equal(JSON.stringify(h.state.bodies),positions);
+});
test('only a ready illustrative Saturn map receives the recovered display material',async t=>{
let complete;
@@ -13,6 +69,7 @@ test('only a ready illustrative Saturn map receives the recovered display materi
const saturn=draw=>draw.uniforms.u_bodyRadiusKm===BODY.Saturn.radiusKm&&draw.uniforms.u_mode===0;
const paint=()=>{const start=h.gpuDraws.length;h.check('orreryTextures',h.state.useTextures);return h.gpuDraws.slice(start);};
assert.ok(paint().filter(saturn).every(draw=>draw.uniforms.u_saturnLook===0),'pending maps keep the existing fallback');
+ h.input('orrerySaturnLighting','look-lab','change');
complete({width:2048,height:1024,close(){}});await h.settle();await h.settle();
const epoch=h.state.renderUnix,positions=JSON.stringify(h.state.bodies),frame=paint();
assert.ok(frame.some(draw=>saturn(draw)&&draw.uniforms.u_saturnLook===1&&draw.uniforms.u_useTex===1));