Expose authored sun, point, and spot lights in render snapshots

This commit is contained in:
Emil
2026-09-24 01:35:20 +03:00
parent bf89ba6f3d
commit 674e3e812b
7 changed files with 311 additions and 13 deletions
@@ -42,9 +42,9 @@ Tasks 1–3 define the shared interfaces. Integrate Task 2's descriptor ABI befo
**Files:** Modify `include/faset/render/renderer.hpp`, `src/authoring/schema.cpp`, `src/player/SceneView.cpp`, `tests/authoring_tests.cpp`, `tests/runtime_player_tests.cpp`, and relevant Manual authoring examples.
**Interfaces:** Introduce `SunLight { stable_id, direction, color, intensity, casts_shadow }`, `LocalLight { Kind::Point|Spot, stable_id, position, direction, color, intensity, range, inner_angle, outer_angle, casts_shadow, shadow_priority }`, and `CameraFrustum { view, projection, near_plane, far_plane, perspective }`. Add `std::optional<SunLight> Snapshot::sun`, `std::vector<LocalLight> Snapshot::local_lights`, and `std::optional<CameraFrustum> Snapshot::camera_frustum` after existing aggregate fields; retain `Snapshot::light_direction`. `SceneView::build` fills camera data for 3D scenes and picks the first enabled directional by stable ID.
**Interfaces:** Introduce `SunLight { stable_id, direction, color, intensity, casts_shadow }`, `LocalLight { Kind::Point|Spot, stable_id, position, direction, color, intensity, range, inner_angle, outer_angle, casts_shadow, shadow_priority }`, and `CameraFrustum { view, projection, near_plane, far_plane, perspective }`. Add `std::optional<SunLight> Snapshot::sun`, `std::vector<LocalLight> Snapshot::local_lights`, `bool Snapshot::authored_lights_present` (default false), and `std::optional<CameraFrustum> Snapshot::camera_frustum` after existing aggregate fields; retain `Snapshot::light_direction`. `SceneView::build` fills camera data for 3D scenes, sets authored-light presence even for disabled/future-version light components, and picks the first enabled directional by stable ID. A legacy sun is synthesized only when both sun and authored-light presence are absent.
- [ ] **Step 1: Write failing schema/extraction tests.** Construct a version-1 scene with directional, point, and spot entities in one order and reversed order. Assert all local IDs/properties agree; authored sun color/intensity are preserved; no-light scene retains the default legacy sun; extra directionals emit a diagnostic; invalid `range <= 0`, `inner_angle > outer_angle`, nonfinite color/transform, and unknown kind identify the entity/field. An essential assertion is:
- [ ] **Step 1: Write failing schema/extraction tests.** Construct a version-1 scene with directional, point, and spot entities in one order and reversed order. Assert all local IDs/properties agree; authored sun color/intensity are preserved; a no-light scene has `authored_lights_present == false` and retains the default legacy sun, while a local-only or explicitly disabled-sun scene has the flag true and no sun. Extra directionals emit a diagnostic; invalid `range <= 0`, `inner_angle > outer_angle`, nonfinite color/transform, and unknown kind identify the entity/field. An essential assertion is:
```cpp
auto a = view.build(scene_with_three_lights(), 16.f / 9.f);
auto b = view.build(reordered_scene_with_three_lights(), 16.f / 9.f);
@@ -54,14 +54,14 @@ Tasks 1–3 define the shared interfaces. Integrate Task 2's descriptor ABI befo
"Light ordering follows stable IDs, not entity array order");
```
- [ ] **Step 2: Run the focused tests red.** Run `cmake --preset linux-debug` and `cmake --build --preset linux-debug --target faset_authoring_tests faset_player_tests --parallel 4`; missing typed fields should fail compilation. If compilation succeeds, `ctest --test-dir build/linux-debug --output-on-failure --no-tests=error -R '^(authoring|player_scene_contracts)$'` must fail on a new behavioral assertion. Record the expected failure rather than assuming the build itself must be red.
- [ ] **Step 3: Add schema defaults and extraction.** Keep builtin version 1; use `fields.value` for additive fields, normalize directions after the world transform, validate finite/color/range/cone values, sort by stable ID, and preserve the legacy fallback. Set `camera_frustum` from the same unjittered view/projection used to form `view_projection`. Update direct C++ API examples to construct one point and one spot light.
- [ ] **Step 3: Add schema defaults and extraction.** Keep builtin version 1; use `fields.value` for additive fields, normalize directions after the world transform, validate finite/color/range/cone values, sort by stable ID, and preserve the legacy fallback only when no authored light component exists. Set `camera_frustum` from the same unjittered view/projection used to form `view_projection`. Update direct C++ API examples to construct one point and one spot light.
```cpp
struct CameraFrustum {
Mat4 view{identity}, projection{identity};
float near_plane{0.1f}, far_plane{1000.f};
bool perspective{true};
};
// Snapshot::light_direction remains available to callers without Snapshot::sun.
// Snapshot::light_direction remains a fallback only if authored_lights_present is false.
```
- [ ] **Step 4: Run focused tests green.** `ctest --test-dir build/linux-debug --output-on-failure -R '^(authoring|player_scene_contracts)$'` passes, including old version-1 scenes and exported-scene decoding.
- [ ] **Step 5: Commit** `Expose authored sun, point, and spot lights in render snapshots`.
@@ -12,7 +12,7 @@ P3 supplies one sun plus point and spot lights, camera-fitted cascaded sun shado
Keep `faset.light` at builtin schema version 1 and add optional fields with defaults: `kind` (`directional`, `point`, `spot`, default `directional`), `enabled` (true), `color` (white), `intensity` (1, nonnegative), `range` (10, positive, for local lights), `inner_angle` (0.35 radians), `outer_angle` (0.70 radians, strictly below π/2), `casts_shadow` (true), and `shadow_priority` (integer 0). Existing fields and component IDs remain valid. Cross-field validation requires `0 <= inner_angle <= outer_angle`; local range, color channels, intensity, transforms, and cone directions must be finite. Invalid authored values return a diagnostic with entity and field rather than nonfinite GPU data. Missing new fields use the schema defaults.
`Snapshot` retains `light_direction` for existing direct-render clients. Add an optional `SunLight`, a vector of `LocalLight`, and an optional explicit `CameraFrustum` containing unjittered view/projection matrices, near/far distances, and projection kind. `SceneView` populates these from the authoring scene. Each light carries the stable entity/component identity, transformed position or normalized direction, color, intensity, range, cone angles, and shadow options. The first enabled directional light by stable ID is the sun; extra directionals produce a visible diagnostic until a later multi-sun design exists. If there is no authored directional light, the renderer synthesizes the legacy sun from `Snapshot::light_direction`. Local lights are ordered by stable ID to prevent reordering from changing atlas allocation or results. Point attenuation tends smoothly to zero at `range`; a spot multiplies it by a smooth inner-to-outer cone factor. The shader handles zero distance and invalid normals without NaN output.
`Snapshot` retains `light_direction` for existing direct-render clients. Add an optional `SunLight`, a vector of `LocalLight`, an `authored_lights_present` flag (default false), and an optional explicit `CameraFrustum` containing unjittered view/projection matrices, near/far distances, and projection kind. `SceneView` populates these from the authoring scene and sets the flag if any authored light component exists, including a disabled or future-version component. Each light carries the stable entity/component identity, transformed position or normalized direction, color, intensity, range, cone angles, and shadow options. The first enabled directional light by stable ID is the sun; extra directionals produce a visible diagnostic until a later multi-sun design exists. The renderer synthesizes the legacy sun from `Snapshot::light_direction` only when `sun` is absent **and** `authored_lights_present` is false. Thus an old scene without light components keeps its previous appearance, while a local-only scene or explicitly disabled sun does not receive an unintended directional light. Low-level callers may set the flag to request a dark scene without authoring metadata. Local lights are ordered by stable ID to prevent reordering from changing atlas allocation or results. Point attenuation tends smoothly to zero at `range`; a spot multiplies it by a smooth inner-to-outer cone factor. The shader handles zero distance and invalid normals without NaN output.
Explicit camera frustum data is required for four cascades. If a low-level caller supplies only the legacy `view_projection`, the renderer uses one bounded legacy-compatible sun shadow view and reports `effective_sun_cascades = 1`; it never silently claims CSM. 2D sprite-only snapshots do not incur shadow work. The future temporal stage may jitter the main raster projection, but the shadow planner consumes the unjittered `CameraFrustum` exclusively.