Merge commit 'b191ae0' into feat/p1-p3-integration
Native and manual checks / native (ubuntu-24.04) (push) Failing after 31s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / manual (push) Successful in 28s
Native and manual checks / native (windows-2025) (push) Canceled after 0s

This commit is contained in:
Emil
2026-09-24 03:01:51 +03:00
28 changed files with 3042 additions and 121 deletions
+92
View File
@@ -0,0 +1,92 @@
# Add lights to a 3D scene
Add a **Light** component to a scene entity. The entity's transform places a point
or spot light; its rotation aims a spot light along local negative Z. A directional
light uses the entity's orientation. Light colors and intensity contribute to the
mesh's linear PBR illumination before tone mapping. Sprites and UI retain their
unlit tint.
The version-1 `faset.light` component has three `kind` values:
| Kind | Position and direction | Useful fields |
| --- | --- | --- |
| `directional` | Direction from the entity transform | `color`, `intensity`, `casts_shadow` |
| `point` | Position from the entity transform; illuminates every direction | `color`, `intensity`, `range` |
| `spot` | Position and local negative-Z direction | `color`, `intensity`, `range`, `inner_angle`, `outer_angle` |
Angles are radians. A spot's inner angle must not exceed its outer angle. Intensity
must be nonnegative and range positive. `enabled: false` keeps the component in the
scene without contributing light. The `shadow_priority` integer is reserved for the
bounded local-shadow scheduler; it does not change brightness.
In the current rendering checkpoint, one enabled directional light can cast the
existing single-map shadow. Point and spot lights illuminate meshes but do not yet
cast shadows. The [P3 lighting plan](https://github.com/emil28092005/Faset_Engine/blob/main/docs/superpowers/plans/2026-09-24-p3-lighting.md)
tracks cascades and the bounded local-shadow atlas. A scene with no Light component
keeps the legacy white sun so older projects retain their appearance. Adding any
Light component, even a disabled one, turns off that compatibility fallback. If
several directionals are enabled, Faset chooses the one with the smallest stable
entity ID and reports a diagnostic for the others.
## Author a point light through MCP
Use `faset_schema` to inspect the current field IDs, then send a `faset_scene_edit`
batch with the document ID, current revision, and target entity ID. For example:
```json
{
"document": "REPLACE_WITH_DOCUMENT_ID",
"revision": 4,
"idempotency_key": "add-red-point-light",
"operations": [{
"op": "component.add",
"entity": "REPLACE_WITH_ENTITY_ID",
"type": "faset.light",
"fields": {
"kind": "point",
"color": [1, 0.15, 0.1, 1],
"intensity": 8,
"range": 6
}
}]
}
```
Move the entity with its Transform component. `component.add` fills any omitted
light fields from the version-1 schema; use `component.set` for later edits. See
[MCP and command line](mcp.md) for revision and retry handling.
## Supply lights directly from C++
When building a `faset::render::Snapshot` yourself, set
`authored_lights_present` to suppress the compatibility sun in a local-only scene.
Provide a stable ID for each light so future shadow scheduling remains independent
of submission order.
```cpp
faset::render::Snapshot snapshot;
snapshot.authored_lights_present = true;
faset::render::LocalLight point;
point.kind = faset::render::LocalLight::Kind::Point;
point.stable_id = "level/torch";
point.position = {-2, 1.5f, 0};
point.color = {1, 0.3f, 0.1f, 1};
point.intensity = 8;
point.range = 6;
snapshot.local_lights.push_back(point);
faset::render::LocalLight spot;
spot.kind = faset::render::LocalLight::Kind::Spot;
spot.stable_id = "level/lamp";
spot.position = {2, 3, 0};
spot.direction = {0, -1, 0};
spot.inner_angle = 0.25f;
spot.outer_angle = 0.55f;
spot.intensity = 5;
spot.range = 9;
snapshot.local_lights.push_back(spot);
```
The renderer submits at most 128 local lights per frame in stable-ID order. Later
P3 work adds explicit overflow diagnostics and measured light-list optimization.
@@ -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.