128 lines
5.7 KiB
Markdown
128 lines
5.7 KiB
Markdown
# Camera & input
|
|
|
|
Two bricks drive the viewpoint: the scene's **active `Camera`** (view/projection matrices
|
|
built every frame) and the unified **`InputState`** (keyboard/mouse, cross-frame
|
|
semantics). The orbital **`CameraController`** bridges the two.
|
|
|
|
## 1. The active camera
|
|
|
|
The scene holds a single camera, read by the engine every frame to write the view/projection
|
|
matrices into the frame buffer (aspect recomputed from the window size).
|
|
|
|
```rust
|
|
use wsg_lib::resources::Camera;
|
|
use glam::Vec3;
|
|
|
|
app.scene.set_camera(Camera::new(
|
|
Vec3::new(3.0, 2.0, 3.0), // eye position
|
|
Vec3::ZERO, // target point
|
|
Vec3::Y, // "up" vector
|
|
));
|
|
```
|
|
|
|
- **Default**: position `(0, 0, 3)`, looking at the origin, 45° vertical fov, near 0.1,
|
|
far 100 — frames a unit cube with no tuning.
|
|
- `Camera::with_perspective(fov, near, far)` adjusts the projection (fov in radians).
|
|
- Read: `app.scene.camera()`; direct mutation: `app.scene.camera_mut()`.
|
|
- The `up` field matters: the orbital camera forces it to `+Y` (level horizon).
|
|
|
|
> The matrices use the **WebGPU** convention (NDC depth `[0,1]`) — do not replace
|
|
> `projection_matrix` with an OpenGL `[-1,1]` projection, the near part of the frustum would
|
|
> be clipped.
|
|
|
|
## 2. The orbital controller
|
|
|
|
`CameraController` represents the viewpoint in spherical coordinates around a target:
|
|
`yaw` (azimuth around +Y), `pitch` (elevation, bounded to ±~83°), `distance` (radius,
|
|
bounded to `[0.1, 100]`), `target` (target point).
|
|
|
|
```rust
|
|
use wsg_lib::resources::CameraController;
|
|
|
|
let mut ctrl = CameraController::default(); // target at origin, distance 3, front view
|
|
ctrl.orbit(dx, dy); // mouse drag: yaw/pitch (bounded pitch, no poles)
|
|
ctrl.zoom(scroll_y); // wheel: zoom (positive scroll = move closer)
|
|
ctrl.reset(); // back to the default framing
|
|
ctrl.apply_to(app.scene.camera_mut()); // write the framing into the active camera (do this EVERY frame)
|
|
```
|
|
|
|
`CameraController::from_camera(&cam)` rebuilds a controller from an existing camera
|
|
(useful to start the orbit from a manual framing).
|
|
|
|
Two public fields tune the feel of the camera (defaults in parentheses):
|
|
|
|
| Field | Meaning | Default |
|
|
|-------|---------|---------|
|
|
| `orbit_sensitivity` | radians of yaw per pixel of mouse delta | `0.005` (~110° per full window width) |
|
|
| `zoom_factor` | multiplicative distance change per wheel notch (`distance *= factor^scroll`) | `0.9` (10% per notch) |
|
|
|
|
The exact wiring snippet (orbit + zoom + reset + `1`/`2`/`3` presets, driven from
|
|
`app.input`) is in [`demo.rs`](../../lib/examples/demo.rs), `update()` section.
|
|
|
|
## 3. The unified input state
|
|
|
|
`app.input` (public field of `App`) is fed by winit events and **rotated** automatically
|
|
every frame (`begin_frame`/`end_frame` around your `update`). Three semantics per control:
|
|
|
|
| Semantics | Methods | Meaning |
|
|
|------------|----------|---------|
|
|
| **pressed** | `key_pressed(code)`, `mouse_button_pressed(btn)` | true **only** on the frame the key/button was just pressed |
|
|
| **held** | `key_held(code)`, `mouse_button_held(btn)` | true while the key/button stays down |
|
|
| **released** | `key_released(code)`, `mouse_button_released(btn)` | true **only** on the release frame |
|
|
|
|
Plus: `mouse_position() -> (f32, f32)`, `mouse_delta() -> (f32, f32)` (accumulated over the
|
|
frame, reset between frames), `scroll_delta() -> (f32, f32)` (wheel, in **line/notch units** —
|
|
`PixelDelta` events are normalized by /32 so one physical wheel notch ≈ 1.0 on every backend).
|
|
|
|
> **Button-gated orbit**: `mouse_delta()` returns movement *whenever* the mouse moves. For a
|
|
> classic arc-rotate camera, apply it only while a button is held — that is what the `demo` does:
|
|
> `if app.input.mouse_button_held(MouseButton::Left) { self.camera.orbit(dx, dy); }`.
|
|
> Free-movement orbit (no button) is also possible, just drop the condition.
|
|
|
|
`KeyCode` values are winit's physical codes (`winit::keyboard::KeyCode`); mouse buttons are
|
|
`winit::event::MouseButton`. The library does not re-export them: if your code mentions
|
|
them, add `winit = "0.30"` to your own dependencies (as the examples do). Input-less
|
|
applications (like `simple`/`cube`) don't need winit: `app.input` remains usable, only
|
|
`KeyCode` comparisons require the import.
|
|
|
|
```rust
|
|
use winit::event::MouseButton;
|
|
use winit::keyboard::KeyCode;
|
|
|
|
fn update(&mut self, app: &mut wsg_lib::App) {
|
|
// Orbit (left-drag gated) + zoom driven by the mouse (excerpts from demo):
|
|
let (dx, dy) = app.input.mouse_delta();
|
|
if app.input.mouse_button_held(MouseButton::Left) {
|
|
self.camera.orbit(dx, dy);
|
|
}
|
|
let (_, sy) = app.input.scroll_delta();
|
|
self.camera.zoom(sy);
|
|
|
|
// R: reset — key_pressed fires once, not on key-repeat.
|
|
if app.input.key_pressed(KeyCode::KeyR) {
|
|
self.camera.yaw = 0.6;
|
|
self.camera.pitch = 0.35;
|
|
self.camera.distance = 6.5;
|
|
}
|
|
self.camera.apply_to(app.scene.camera_mut());
|
|
}
|
|
```
|
|
|
|
> **Gamepad**: the API is reserved (`InputState` will pass through `DeviceEvent`s) but not
|
|
> implemented yet — deferred, see [ROADMAP](../ROADMAP.md).
|
|
|
|
## 4. Common recipes
|
|
|
|
| Need | Recipe |
|
|
|--------|--------|
|
|
| Standard orbital camera | `CameraController` + `mouse_delta`/`scroll_delta` (snippet above) |
|
|
| FPS camera (WASD) | `key_held(KeyCode::KeyW)` in `update` → move `camera.position`/`target`; override `render()` if needed |
|
|
| Changing the orbit target | `ctrl.target = subject_position;` (following an object) |
|
|
| View presets | `key_pressed(Digit1/2/3)` → write yaw/pitch/distance (from the `demo`) |
|
|
| Tuning the camera speed | `ctrl.orbit_sensitivity = 0.003;` (slower orbit), `ctrl.zoom_factor = 0.95;` (gentler zoom) |
|
|
|
|
## Links
|
|
|
|
- [User README](README.md) · [Lights](lights.md) · [Examples](examples.md)
|
|
- [Root README](../../README.md) · [ARCHI_APP](../tech/ARCHI_APP.md)
|