eng doc
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Lights — user documentation
|
||||
|
||||
The **lighting side** of the scene: the light model, shadows, and emissive materials.
|
||||
|
||||
| Page | Topic |
|
||||
|------|-------|
|
||||
| [Lights](lights.md) | Scene-global lights (directional/point/spot/ambient), `MAX_LIGHTS`, packed indices |
|
||||
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall, tuning |
|
||||
| [Emissive + Exposure](emissive-exposure.md) | Emissive materials (HDR glow) and runtime exposure |
|
||||
|
||||
Example folder: [`lib/examples/lights/`](../../../lib/examples/lights/README.md)
|
||||
(`shadow`, `shadow_test`, `spot_test`, `emissive`).
|
||||
|
||||
## Links
|
||||
|
||||
- [User documentation index](../README.md) · [Quickstart](../quickstart.md) · [Examples](../examples.md)
|
||||
@@ -0,0 +1,114 @@
|
||||
# Emissive + Exposure
|
||||
|
||||
## Principle
|
||||
|
||||
Two complementary features (Step 22):
|
||||
|
||||
| Feature | Effect | Cost |
|
||||
|---------|--------|------|
|
||||
| **Exposure** (6.1) | Multiplies luminance before the tone-mapping curve | Zero when HDR is inactive |
|
||||
| **Emissive** (6.2) | Adds an emitted color (independent of the lights) | Zero when `emissive = [0,0,0,0]` |
|
||||
|
||||
## Exposure
|
||||
|
||||
### API
|
||||
|
||||
```rust
|
||||
// Initialization (optional, default = 1.0)
|
||||
let app = AppBuilder::new()
|
||||
.with_hdr(ToneMapper::Aces)
|
||||
.with_exposure(1.5) // start brighter
|
||||
.build().await?;
|
||||
|
||||
// Runtime (in update())
|
||||
app.set_exposure(app.exposure() * 1.1); // +1 "stop"
|
||||
app.set_exposure(1.0); // reset
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
- Exposure is a **multiplier** applied to the HDR texture before the tone-mapping curve.
|
||||
- `exposure = 2.0` → the image is 2× brighter (like opening a camera's aperture).
|
||||
- `exposure = 0.5` → the image is 2× darker.
|
||||
- Clamped to `[0.01, 10.0]` to avoid degenerate values.
|
||||
- **Only has an effect when HDR is active** (`with_hdr(...)`). In LDR the value is ignored.
|
||||
|
||||
### Keyboard (demo)
|
||||
|
||||
| Key | Effect |
|
||||
|-----|--------|
|
||||
| `+` | ×1.1 (brighter) |
|
||||
| `-` | ÷1.1 (darker) |
|
||||
| `0` | Reset to 1.0 |
|
||||
|
||||
## Emissive
|
||||
|
||||
### API
|
||||
|
||||
```rust
|
||||
use wsg_lib::resources::Material;
|
||||
|
||||
// Create a material with emissivity
|
||||
let mut mat = /* ... */;
|
||||
mat.emissive = [1.0, 0.3, 0.1, 1.5]; // orange, intensity 1.5 (> 1.0 = HDR glow)
|
||||
```
|
||||
|
||||
### Format
|
||||
|
||||
`emissive = [r, g, b, intensity]`:
|
||||
|
||||
- **rgb**: the emission color (same space as the vertex base color)
|
||||
- **a (intensity)**: the multiplier. `1.0` = normal color, `> 1.0` = highlight (only visible in HDR)
|
||||
|
||||
### Shader formula
|
||||
|
||||
```
|
||||
final_color = lit + base_color * emissive.rgb * emissive.a
|
||||
```
|
||||
|
||||
- The emission is **additive**: visible even in total darkness (no light needed).
|
||||
- It is **independent of shadows**: an emissive object casts no shadow and is not shadowed.
|
||||
- `emissive = [0,0,0,0]` (default) → no change (non-regression guaranteed).
|
||||
|
||||
### Use cases
|
||||
|
||||
| Use | Value |
|
||||
|-----|-------|
|
||||
| LED / indicator | `[0, 1, 0, 1.0]` (green, normal intensity) |
|
||||
| Flame / sun | `[1, 0.8, 0.2, 3.0]` (orange, HDR glow) |
|
||||
| Neon | `[0, 0.5, 1, 2.5]` (cyan, glow) |
|
||||
| Inactive | `[0, 0, 0, 0]` (default) |
|
||||
|
||||
### Keyboard (demo)
|
||||
|
||||
| Key | Effect |
|
||||
|-----|--------|
|
||||
| `E` | Toggle orange glow on the sphere/cylinder |
|
||||
|
||||
## Interactions
|
||||
|
||||
| Combination | Result |
|
||||
|-------------|--------|
|
||||
| Emissive + HDR + ACES | Soft glow, highlights roll off (the nicest) |
|
||||
| Emissive + LDR | Clamped to 1.0 (no glow, but the color is visible in the dark) |
|
||||
| Emissive + shadows | The emissive object is NOT shadowed (emission bypasses the shadow term) |
|
||||
| Exposure + Emissive | Exposure also amplifies the emission (consistent: everything is in the HDR texture) |
|
||||
|
||||
## Non-regression
|
||||
|
||||
- **Emissive**: `[0,0,0,0]` by default → the shader adds `base * 0 * 0 = 0` → no change.
|
||||
- **Exposure**: `1.0` by default → `pow(color, 1/1) = color` → no change.
|
||||
- Both are **opt-in**: without `with_hdr(...)` and `emissive != 0`, the pipeline is identical
|
||||
to the previous state.
|
||||
|
||||
## Limitations (MVP)
|
||||
|
||||
- Emissive is **per material**, not per vertex (no emission gradient within a mesh).
|
||||
- Emissive is **static** at material creation (changing `mat.emissive` requires re-registering
|
||||
the material via `add_material`).
|
||||
- No **bloom** (Step 23): the HDR glow is visible but not "blurred" / spread.
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](../README.md) · [HDR & tone mapping](../effects/hdr.md) · [Examples](../examples.md)
|
||||
- [Root README](../../../README.md)
|
||||
@@ -0,0 +1,84 @@
|
||||
# Lights
|
||||
|
||||
Lights are **scene-global**: a single list is packed into the frame uniforms every frame, and
|
||||
**all** entities receive their lighting (per-material lights are out of the current scope).
|
||||
|
||||
## Model
|
||||
|
||||
- Bounded capacity: **`MAX_LIGHTS = 8`** lights in total (directional + point + spot
|
||||
combined). Adding beyond that returns an error.
|
||||
- **Default**: one white directional light along **+Z** (from the surface point toward the
|
||||
light) + white ambient. This default exactly reproduces the historical single-light
|
||||
rendering — your scene "just works" with no configuration.
|
||||
- Ambient (`set_ambient`) is a global hemispherical term, independent of the lights.
|
||||
|
||||
## Adding lights
|
||||
|
||||
```rust
|
||||
use glam::Vec3;
|
||||
|
||||
// Directional: `dir` points FROM the surface point TOWARD the light.
|
||||
app.scene
|
||||
.add_directional_light(Vec3::new(1.0, 1.2, 1.0).normalize(), [1.0, 0.98, 0.92], 1.5)
|
||||
.unwrap();
|
||||
|
||||
// Point: world position, tint, intensity, attenuation radius (linear down to 0).
|
||||
app.scene
|
||||
.add_point_light(Vec3::new(0.5, 1.6, 1.8), [1.0, 0.7, 0.3], 1.2, 6.0)
|
||||
.unwrap();
|
||||
|
||||
// Spot: position, cone axis (FROM the light TOWARD the scene), tint, intensity, radius,
|
||||
// half-angle in radians (penumbra smoothed at the edge).
|
||||
app.scene.add_spot_light(
|
||||
Vec3::new(-2.5, 2.2, 1.0), // position
|
||||
Vec3::new(2.5, -2.2, -1.0).normalize(), // axis, toward the scene
|
||||
[0.3, 1.0, 0.5], // green tint
|
||||
1.4, 8.0, 0.45, // intensity, radius, half-angle (~26°)
|
||||
).unwrap();
|
||||
```
|
||||
|
||||
These three calls are the ones in the [`demo`](../../../lib/examples/effects/demo.rs) example;
|
||||
[`cube.rs`](../../../lib/examples/meshes/cube.rs) shows a point + a spot on top of the default
|
||||
directional, and [`spot_test.rs`](../../../lib/examples/lights/spot_test.rs) isolates a single spot
|
||||
(ambient nearly zero).
|
||||
|
||||
Global settings:
|
||||
|
||||
| Method | Effect |
|
||||
|---------|--------|
|
||||
| `set_ambient([r, g, b])` | hemispherical ambient color (default white) |
|
||||
| `clear_lights()` | empties the list — only ambient will light the scene (useful for a flat look without switching to unlit) |
|
||||
| `set_lights(Lights)` | replaces the whole list (batch reset) |
|
||||
| `lights()` | reads the current list |
|
||||
|
||||
## ⚠️ Packed indices (important for shadows)
|
||||
|
||||
Lights are stacked in the GPU array **by type, in order**:
|
||||
|
||||
```
|
||||
index 0 .. n_dir-1 : directional
|
||||
index n_dir .. +n_point-1 : point
|
||||
index … .. +n_spot-1 : spot
|
||||
```
|
||||
|
||||
Two consequences:
|
||||
|
||||
1. **Index 0 is the default +Z directional** (the one `Lights::new()` pre-loads),
|
||||
not your first added light. This is a classic pitfall — see
|
||||
[Shadows](shadows.md).
|
||||
2. If you want **your** light to be the only one (and thus at index 0), clear the list
|
||||
first: `app.scene.clear_lights();` then `add_*_light(…)` (this is the technique in
|
||||
[`shadow_test.rs`](../../../lib/examples/lights/shadow_test.rs)).
|
||||
|
||||
## Intensities and tints
|
||||
|
||||
- `color` is an RGB in `[0..1]`; `intensity` is an unbounded multiplier.
|
||||
- Local lights (point/spot) attenuate **linearly** — intensity drops to zero at `radius`.
|
||||
Beyond the radius, the light contributes nothing.
|
||||
- The `standard` shader accumulates ambient + all lights (no mutual occlusion between
|
||||
lights; the spot cone culling happens at the fragment).
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](../README.md) · [Shadows](shadows.md) · [Materials & textures](../meshes/materials.md)
|
||||
- [Root README](../../../README.md) · [ARCHI_RENDU](../../tech/ARCHI_RENDU.md)
|
||||
@@ -0,0 +1,79 @@
|
||||
# Shadows (shadow mapping)
|
||||
|
||||
Shadows are **off by default** and are enabled by designating **a single** casting light:
|
||||
|
||||
```rust
|
||||
app.scene.set_shadow_caster(Some(index)); // packed index — see the pitfall below
|
||||
app.scene.set_shadow_caster(None); // shadows off (default)
|
||||
```
|
||||
|
||||
Only a **directional or spot** light can cast shadows. A **point** light index disables the
|
||||
shadow pass (cubemap shadows are out of scope).
|
||||
|
||||
## ⚠️ The packed-index pitfall
|
||||
|
||||
`set_shadow_caster` takes the light's index **in the packed array** (directionals first,
|
||||
then point, then spot — recalled in [Lights](lights.md)).
|
||||
|
||||
**Index 0 is the default +Z directional** pre-loaded by `Lights::new()`, not necessarily
|
||||
your light. Symptom of a wrong index: the shadow camera looks in an unexpected direction and
|
||||
misaligned objects occlude each other (blackened objects, ghost shadows).
|
||||
|
||||
Two ways to avoid it:
|
||||
|
||||
1. **Clear the list before adding yours** — your light becomes index 0:
|
||||
|
||||
```rust
|
||||
app.scene.clear_lights(); // removes the default +Z
|
||||
app.scene.add_directional_light(dir, [1.0, 0.98, 0.92], 1.6).unwrap();
|
||||
app.scene.set_shadow_caster(Some(0)); // now it really is YOUR light
|
||||
```
|
||||
|
||||
This is the technique in [`shadow_test.rs`](../../../lib/examples/lights/shadow_test.rs).
|
||||
|
||||
2. **Count the indices** — if you keep the default light and add yours, it lands at index 1:
|
||||
|
||||
```rust
|
||||
app.scene.add_directional_light(toward_light, [1.0, 0.98, 0.92], 1.5).unwrap(); // → index 1
|
||||
app.scene.set_shadow_caster(Some(1)); // this is the demo's warm light that casts
|
||||
```
|
||||
|
||||
This is the technique in [`demo.rs`](../../../lib/examples/effects/demo.rs).
|
||||
|
||||
## How it works (to understand the limits)
|
||||
|
||||
Each frame, if a caster is active, the engine runs **two passes** (technical details in
|
||||
[FRAME_LOOP](../../tech/FRAME_LOOP.md)):
|
||||
|
||||
1. **Shadow pass**: the scene is rendered as seen *from the light* (depth-only
|
||||
`shadow_shader.wgsl` shader) into a 1024² `Depth32Float` shadow map (size configurable
|
||||
via `SHADOW_MAP_SIZE`), with a depth bias (slope-scaled + constant) to avoid shadow acne.
|
||||
2. **Color pass**: the `standard` fragment shader re-projects each fragment into light space
|
||||
and compares its depth against the map via a **3×3 PCF** (softened shadow edges).
|
||||
|
||||
Things to know:
|
||||
|
||||
- **Directional light**: the shadow frustum is orthographic, centered on the scene center
|
||||
(`SHADOW_SCENE_CENTER`, radius `SHADOW_SCENE_RADIUS = 5.0` by default). Objects **far from
|
||||
the origin** may fall outside the frustum and stop casting.
|
||||
- **Spot light**: the light's cone naturally bounds the shadow.
|
||||
- Only one light casts at a time (no multi-light shadows).
|
||||
- Shadows only affect meshes rendered by `standard` in lit mode — a renderer in unlit mode
|
||||
(see [Materials & textures](../meshes/materials.md)) receives none.
|
||||
|
||||
## Tuning shadow rendering
|
||||
|
||||
The constants `SHADOW_MAP_SIZE`, `SHADOW_DEPTH_BIAS`, `SHADOW_SCENE_RADIUS`,
|
||||
`SHADOW_SCENE_CENTER` are exposed in `wsg_lib::utils` (defaults: 1024, 0.006, 5.0, origin).
|
||||
|
||||
Tuning tips:
|
||||
|
||||
- **Speckled shadow edges (acne)**: raise the bias.
|
||||
- **Peter-panning** (shadow detached from the object): lower the bias.
|
||||
- **Shadow clipped at the scene edge**: raise the frustum radius (directional).
|
||||
- **Shadows too blurry, want them crisper**: raise the map size.
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](../README.md) · [Lights](lights.md) · [Examples](../examples.md)
|
||||
- [Root README](../../../README.md) · [FRAME_LOOP](../../tech/FRAME_LOOP.md)
|
||||
Reference in New Issue
Block a user