feat(camera): orbital CameraController + final demo example
Add resources::CameraController (Etapes 15.C): spherical yaw/pitch/distance/ target with orbit() (mouse drag), zoom() (wheel, clamped), reset(), and apply_to(&mut Camera). Add Scene::camera_mut() for in-place per-frame edits. New lib/examples/demo.rs: all six primitives on a textured ground, standard Phong material, shadow-casting directional + point + spot lights, and a live orbital view driven by the unified input (drag=orbit, wheel=zoom, R=reset, 1/2/3=front/side/top presets) plus slow primitive rotation. 6 unit tests for CameraController.
This commit is contained in:
@@ -0,0 +1,227 @@
|
|||||||
|
//! **WSG `demo`** — the final showcase example (Étape 15, sous-volt 15.C).
|
||||||
|
//!
|
||||||
|
//! Combines everything built throughout the library into one declarative scene:
|
||||||
|
//!
|
||||||
|
//! * a **ground plane** plus one of each procedural primitive from `math::primitives`
|
||||||
|
//! (`cube`, `uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) placed around it,
|
||||||
|
//! * a **procedural texture** per mesh (checker / stripe grids, no assets on disk),
|
||||||
|
//! * the **standard** Phong material wired to those textures,
|
||||||
|
//! * an **orbital camera** driven live by the unified input state (Étape 15.B):
|
||||||
|
//! moving the mouse orbits (yaw/pitch), the wheel zooms (distance),
|
||||||
|
//! * `R` resets the view, keys `1`/`2`/`3` jump to front / side / top presets,
|
||||||
|
//! * a **directional** light (the shadow caster) + a **point** light + a **spot** light,
|
||||||
|
//! so the shadow of the cube and the colored light halos are all visible,
|
||||||
|
//! * the primitives slowly rotate in `update`, so depth, lighting and shadows read clearly.
|
||||||
|
//!
|
||||||
|
//! Doc (this header) follows the English convention used for examples; internal comments stay
|
||||||
|
//! concise and French where helpful. Run with:
|
||||||
|
//!
|
||||||
|
//! `cargo run -p wsg-lib --example demo`
|
||||||
|
|
||||||
|
use glam::{Quat, Vec3};
|
||||||
|
use winit::keyboard::KeyCode;
|
||||||
|
use wsg_lib::AppHandler;
|
||||||
|
use wsg_lib::app::AppBuilder;
|
||||||
|
use wsg_lib::math::{Transform, cone, cube, cylinder, icosphere, plane, torus, uv_sphere};
|
||||||
|
use wsg_lib::resources::{CameraController, Texture};
|
||||||
|
use wsg_lib::utils::WsgError;
|
||||||
|
|
||||||
|
/// Generates an 8×8 RGBA checkerboard (white / brick) as raw bytes for `Texture::from_rgba8`.
|
||||||
|
fn checkerboard_rgba() -> Vec<u8> {
|
||||||
|
const SIZE: u32 = 8;
|
||||||
|
let mut rgba = Vec::with_capacity((SIZE * SIZE * 4) as usize);
|
||||||
|
for y in 0..SIZE {
|
||||||
|
for x in 0..SIZE {
|
||||||
|
let even = (x + y) % 2 == 0;
|
||||||
|
let (r, g, b) = if even { (235, 235, 228) } else { (150, 90, 70) };
|
||||||
|
rgba.extend_from_slice(&[r, g, b, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a vertical stripe texture (blue / cyan), useful to make rotation visible on rounded
|
||||||
|
/// bodies (sphere / cylinder) via the UV seams.
|
||||||
|
fn stripes_rgba() -> Vec<u8> {
|
||||||
|
const W: u32 = 32;
|
||||||
|
const H: u32 = 16;
|
||||||
|
let mut rgba = Vec::with_capacity((W * H * 4) as usize);
|
||||||
|
for _y in 0..H {
|
||||||
|
for x in 0..W {
|
||||||
|
let band = (x / 4) % 2 == 0;
|
||||||
|
let (r, g, b) = if band { (40, 90, 190) } else { (120, 210, 235) };
|
||||||
|
rgba.extend_from_slice(&[r, g, b, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Demo handler: holds the orbital controller plus a slow rotation angle.
|
||||||
|
struct Demo {
|
||||||
|
camera: CameraController,
|
||||||
|
angle: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Horizontal radius at which the primitives sit around the origin.
|
||||||
|
const ORBIT_RADIUS: f32 = 1.7;
|
||||||
|
/// Vertical offset so the meshes stand on the ground plane (y = 0).
|
||||||
|
const STAND_HEIGHT: f32 = 0.5;
|
||||||
|
|
||||||
|
/// Lays out one primitive (already scaled/positioned) at an angle around the origin.
|
||||||
|
fn place(label: &str, mesh: &str, app: &mut wsg_lib::App, index: usize) {
|
||||||
|
let a = index as f32 / 6.0 * std::f32::consts::TAU;
|
||||||
|
let mut tf = Transform::identity();
|
||||||
|
tf.translation = Vec3::new(a.cos() * ORBIT_RADIUS, STAND_HEIGHT, a.sin() * ORBIT_RADIUS);
|
||||||
|
tf.rotation = Quat::from_rotation_y(a); // face the center
|
||||||
|
app.scene
|
||||||
|
.add_entity_with_transform(label, mesh, tf)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppHandler for Demo {
|
||||||
|
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||||||
|
// 1. Shader + material base.
|
||||||
|
app.scene
|
||||||
|
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (device, queue) = {
|
||||||
|
let ctx = app.context();
|
||||||
|
(ctx.device.clone(), ctx.queue.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Procedural textures, one material per pattern.
|
||||||
|
let checker =
|
||||||
|
Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap();
|
||||||
|
app.scene.add_texture("checker_texture", checker).unwrap();
|
||||||
|
app.scene
|
||||||
|
.add_material_texture("ground_mat", "standard", "checker_texture")
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.add_material_texture("solid_mat", "standard", "checker_texture")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let stripes =
|
||||||
|
Texture::from_rgba8(&device, &queue, 32, 16, &stripes_rgba(), "stripes").unwrap();
|
||||||
|
app.scene.add_texture("stripes_texture", stripes).unwrap();
|
||||||
|
app.scene
|
||||||
|
.add_material_texture("stripes_mat", "standard", "stripes_texture")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 3. Ground plane (large, thin, textured).
|
||||||
|
app.scene
|
||||||
|
.create_mesh("ground_mesh", plane(9.0, 9.0, 1, 1), Some("ground_mat"))
|
||||||
|
.unwrap();
|
||||||
|
app.scene.add_entity("ground", "ground_mesh").unwrap();
|
||||||
|
|
||||||
|
// 4. One mesh per primitive, each assigned to a textured (or stripe) material.
|
||||||
|
app.scene
|
||||||
|
.create_mesh("cube_mesh", cube(0.8), Some("solid_mat"))
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.create_mesh("sphere_mesh", uv_sphere(0.55, 32, 20), Some("stripes_mat"))
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.create_mesh("ico_mesh", icosphere(0.5, 2), Some("solid_mat"))
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.create_mesh("cyl_mesh", cylinder(0.4, 0.9, 32), Some("stripes_mat"))
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.create_mesh("cone_mesh", cone(0.45, 0.9, 32), Some("solid_mat"))
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.create_mesh("torus_mesh", torus(0.42, 0.16, 24, 16), Some("solid_mat"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
place("cube_e", "cube_mesh", app, 0);
|
||||||
|
place("sphere_e", "sphere_mesh", app, 1);
|
||||||
|
place("ico_e", "ico_mesh", app, 2);
|
||||||
|
place("cyl_e", "cyl_mesh", app, 3);
|
||||||
|
place("cone_e", "cone_mesh", app, 4);
|
||||||
|
place("torus_e", "torus_mesh", app, 5);
|
||||||
|
|
||||||
|
// 5. Lights: a shadow-casting directional + a warm point + a green spot.
|
||||||
|
// Start from the default list (directional +Z) so we keep it and add the rest.
|
||||||
|
let toward_light = Vec3::new(1.0, 1.2, 1.0).normalize();
|
||||||
|
app.scene
|
||||||
|
.add_directional_light(toward_light, [1.0, 0.98, 0.92], 1.5)
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.add_point_light(Vec3::new(0.5, 1.6, 1.8), [1.0, 0.7, 0.3], 1.2, 6.0)
|
||||||
|
.unwrap();
|
||||||
|
app.scene
|
||||||
|
.add_spot_light(
|
||||||
|
Vec3::new(-2.5, 2.2, 1.0),
|
||||||
|
Vec3::new(2.5, -2.2, -1.0).normalize(),
|
||||||
|
[0.3, 1.0, 0.5],
|
||||||
|
1.4,
|
||||||
|
8.0,
|
||||||
|
0.45,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The first directional light (packed index 0) casts shadows.
|
||||||
|
app.scene.set_shadow_caster(Some(0));
|
||||||
|
app.scene.set_ambient([0.14, 0.14, 0.16]);
|
||||||
|
|
||||||
|
// 6. Active camera, driven by the orbital controller (position, distance, preset target).
|
||||||
|
self.camera.yaw = 0.6;
|
||||||
|
self.camera.pitch = 0.35;
|
||||||
|
self.camera.distance = 6.5;
|
||||||
|
self.camera.target = Vec3::ZERO;
|
||||||
|
self.camera.apply_to(app.scene.camera_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||||
|
// ---- Orbital camera from unified input ----
|
||||||
|
// Moving the mouse orbits (yaw/pitch); the wheel zooms (distance).
|
||||||
|
let (dx, dy) = app.input.mouse_delta();
|
||||||
|
self.camera.orbit(dx, dy);
|
||||||
|
let (_, sy) = app.input.scroll_delta();
|
||||||
|
self.camera.zoom(sy);
|
||||||
|
|
||||||
|
// R: reset the view. Keys 1/2/3: front / side / top presets.
|
||||||
|
if app.input.key_pressed(KeyCode::KeyR) {
|
||||||
|
// Keep the target but restore a pleasing default framing.
|
||||||
|
self.camera.yaw = 0.6;
|
||||||
|
self.camera.pitch = 0.35;
|
||||||
|
self.camera.distance = 6.5;
|
||||||
|
}
|
||||||
|
if app.input.key_pressed(KeyCode::Digit1) {
|
||||||
|
self.camera.yaw = 0.0;
|
||||||
|
self.camera.pitch = 0.25;
|
||||||
|
self.camera.distance = 6.5;
|
||||||
|
}
|
||||||
|
if app.input.key_pressed(KeyCode::Digit2) {
|
||||||
|
self.camera.yaw = std::f32::consts::FRAC_PI_2;
|
||||||
|
self.camera.pitch = 0.15;
|
||||||
|
self.camera.distance = 6.5;
|
||||||
|
}
|
||||||
|
if app.input.key_pressed(KeyCode::Digit3) {
|
||||||
|
self.camera.yaw = 0.0;
|
||||||
|
self.camera.pitch = 1.25;
|
||||||
|
self.camera.distance = 8.0;
|
||||||
|
}
|
||||||
|
self.camera.apply_to(app.scene.camera_mut());
|
||||||
|
|
||||||
|
// ---- Slow rotation of the primitives so lighting/shadow read clearly ----
|
||||||
|
self.angle += 0.008;
|
||||||
|
let base = *app
|
||||||
|
.scene
|
||||||
|
.entity_transform("cube_e")
|
||||||
|
.expect("cube entity present");
|
||||||
|
let mut tf = base;
|
||||||
|
tf.rotation = Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.4);
|
||||||
|
app.scene.set_entity_transform("cube_e", tf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pollster::main]
|
||||||
|
async fn main() -> Result<(), WsgError> {
|
||||||
|
let app = AppBuilder::new().title("WSG Demo").build().await?;
|
||||||
|
app.run(Demo {
|
||||||
|
camera: CameraController::default(),
|
||||||
|
angle: 0.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -98,3 +98,179 @@ impl Camera {
|
|||||||
glam::camera::rh::proj::directx::perspective(self.fov, aspect, self.near, self.far)
|
glam::camera::rh::proj::directx::perspective(self.fov, aspect, self.near, self.far)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Vertical pitch clamp (radians) applied by [`CameraController`] so the camera never flips over the
|
||||||
|
/// poles. Kept a little under ±90°.
|
||||||
|
pub const PITCH_LIMIT: f32 = 1.45; // ~83°
|
||||||
|
|
||||||
|
/// Orbital camera controller (Étape 15, sous-volt 15.C).
|
||||||
|
///
|
||||||
|
/// Represents the viewpoint spherically around a `target`: `yaw` (rotation around the world-up axis),
|
||||||
|
/// `pitch` (elevation above/below the horizontal), `distance` (radius) and the look-at `target`.
|
||||||
|
/// [`CameraController::apply_to`] writes these into a [`Camera`] each frame, so the controller stays
|
||||||
|
/// decoupled from `Camera`'s own position/target/up representation.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use wsg_lib::resources::{Camera, CameraController};
|
||||||
|
/// # use glam::Vec3;
|
||||||
|
/// let cam = Camera::new(Vec3::new(3.0, 2.0, 3.0), Vec3::ZERO, Vec3::Y);
|
||||||
|
/// let mut ctrl = CameraController::from_camera(&cam);
|
||||||
|
/// ctrl.orbit(0.1, -0.05); // drag: yaw/pitch
|
||||||
|
/// ctrl.zoom(-1.0); // wheel: distance
|
||||||
|
/// let mut cam2 = cam;
|
||||||
|
/// ctrl.apply_to(&mut cam2); // write back into the active camera
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct CameraController {
|
||||||
|
/// Rotation around the world-up (+Y) axis, in radians.
|
||||||
|
pub yaw: f32,
|
||||||
|
/// Elevation angle above (+) / below (-) the horizontal, in radians, clamped to ±[`PITCH_LIMIT`].
|
||||||
|
pub pitch: f32,
|
||||||
|
/// Distance from the camera position to the `target` (orbit radius).
|
||||||
|
pub distance: f32,
|
||||||
|
/// World-space point the camera looks at and orbits around.
|
||||||
|
pub target: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CameraController {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
yaw: 0.0,
|
||||||
|
pitch: 0.0,
|
||||||
|
distance: 3.0,
|
||||||
|
target: Vec3::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sensitivity of the orbit drag (radians of yaw per pixel of horizontal mouse delta).
|
||||||
|
pub const DEFAULT_ORBIT_SENSITIVITY: f32 = 0.01;
|
||||||
|
/// Multiplicative zoom factor applied per unit of vertical scroll.
|
||||||
|
pub const DEFAULT_ZOOM_FACTOR: f32 = 0.9;
|
||||||
|
|
||||||
|
impl CameraController {
|
||||||
|
/// Builds a controller that reproduces an existing camera's framing by extracting yaw/pitch/
|
||||||
|
/// distance from `position - target` in spherical coordinates.
|
||||||
|
pub fn from_camera(camera: &Camera) -> Self {
|
||||||
|
let offset = camera.position - camera.target;
|
||||||
|
let distance = offset.length().max(f32::EPSILON);
|
||||||
|
// Y-up convention: pitch = asin(y / r), yaw measured from +Z toward +X.
|
||||||
|
let pitch = offset
|
||||||
|
.y
|
||||||
|
.atan2((offset.x * offset.x + offset.z * offset.z).sqrt());
|
||||||
|
let yaw = offset.x.atan2(offset.z);
|
||||||
|
Self {
|
||||||
|
yaw,
|
||||||
|
pitch: pitch.clamp(-PITCH_LIMIT, PITCH_LIMIT),
|
||||||
|
distance,
|
||||||
|
target: camera.target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the world-space eye position from the current yaw/pitch/distance around `target`.
|
||||||
|
pub fn position(&self) -> Vec3 {
|
||||||
|
let cp = self.pitch.cos();
|
||||||
|
let dir = Vec3::new(cp * self.yaw.sin(), self.pitch.sin(), cp * self.yaw.cos());
|
||||||
|
self.target + dir * self.distance
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the current framing into a [`Camera`]: sets its `position` (spherical away from
|
||||||
|
/// `target`), its look-at `target`, and forces `up` to world +Y so the horizon stays level.
|
||||||
|
pub fn apply_to(&self, camera: &mut Camera) {
|
||||||
|
camera.position = self.position();
|
||||||
|
camera.target = self.target;
|
||||||
|
camera.up = Vec3::Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies an orbit drag (mouse delta in pixels): `dx` rotates yaw, `dy` rotates pitch
|
||||||
|
/// (inverted so dragging up tilts the view up). Pitch is clamped to ±[`PITCH_LIMIT`].
|
||||||
|
pub fn orbit(&mut self, dx: f32, dy: f32) {
|
||||||
|
self.yaw -= dx * DEFAULT_ORBIT_SENSITIVITY;
|
||||||
|
self.pitch = (self.pitch + dy * DEFAULT_ORBIT_SENSITIVITY).clamp(-PITCH_LIMIT, PITCH_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zooms in/out by an exponential factor on the vertical wheel scroll (`scroll_y`): positive
|
||||||
|
/// scroll zooms in (distance shrinks). Clamped to a sane `[0.1, 100]` range.
|
||||||
|
pub fn zoom(&mut self, scroll_y: f32) {
|
||||||
|
if scroll_y == 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let factor = DEFAULT_ZOOM_FACTOR.powf(scroll_y);
|
||||||
|
self.distance = (self.distance * factor).clamp(0.1, 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the controller to its default framing (origin target, `distance` 3, level view).
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_positions_level_front() {
|
||||||
|
let ctrl = CameraController::default();
|
||||||
|
let p = ctrl.position();
|
||||||
|
assert!((p - Vec3::new(0.0, 0.0, 3.0)).length() < 1e-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn orbit_changes_yaw_and_clamps_pitch() {
|
||||||
|
let mut ctrl = CameraController::default();
|
||||||
|
ctrl.orbit(100.0, 0.0); // yaw rotation
|
||||||
|
let p1 = ctrl.position();
|
||||||
|
assert!((p1.x.abs()) > 0.1, "yaw should swing around +Y");
|
||||||
|
assert!(ctrl.pitch == 0.0);
|
||||||
|
// Pitch clamped to ±PITCH_LIMIT even with a huge drag.
|
||||||
|
ctrl.orbit(0.0, 1_000.0);
|
||||||
|
assert!((ctrl.pitch - PITCH_LIMIT).abs() < 1e-5);
|
||||||
|
ctrl.orbit(0.0, -2_000.0);
|
||||||
|
assert!((ctrl.pitch + PITCH_LIMIT).abs() < 1e-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zoom_inout_clamped() {
|
||||||
|
let mut ctrl = CameraController::default();
|
||||||
|
ctrl.zoom(1.0);
|
||||||
|
assert!(ctrl.distance < 3.0, "positive scroll zooms in");
|
||||||
|
ctrl.zoom(-10.0);
|
||||||
|
assert!(ctrl.distance > 3.0);
|
||||||
|
ctrl.zoom(10_000.0);
|
||||||
|
assert!(ctrl.distance >= 0.1 - 1e-5);
|
||||||
|
ctrl.zoom(-10_000.0);
|
||||||
|
assert!(ctrl.distance <= 100.0 + 1e-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_from_camera_reproduces_framing() {
|
||||||
|
let cam = Camera::new(Vec3::new(3.0, 2.0, 3.0), Vec3::new(1.0, 1.0, 0.0), Vec3::Y);
|
||||||
|
let ctrl = CameraController::from_camera(&cam);
|
||||||
|
let mut back = cam.clone();
|
||||||
|
ctrl.apply_to(&mut back);
|
||||||
|
// Target preserved; position matches up to float error for a non-pole framing.
|
||||||
|
assert!((back.target - cam.target).length() < 1e-4);
|
||||||
|
assert!((back.position - cam.position).length() < 1e-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_restores_defaults() {
|
||||||
|
let mut ctrl = CameraController::default();
|
||||||
|
ctrl.orbit(100.0, 50.0);
|
||||||
|
ctrl.zoom(3.0);
|
||||||
|
assert!(ctrl.yaw != 0.0);
|
||||||
|
ctrl.reset();
|
||||||
|
assert!((ctrl.yaw).abs() < 1e-6);
|
||||||
|
assert!(ctrl.distance == 3.0);
|
||||||
|
assert!(ctrl.target == Vec3::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_to_enforces_world_up() {
|
||||||
|
let ctrl = CameraController::default();
|
||||||
|
let mut cam = Camera::new(Vec3::ZERO, Vec3::ZERO, Vec3::X); // odd up
|
||||||
|
ctrl.apply_to(&mut cam);
|
||||||
|
assert!(cam.up == Vec3::Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub mod uniform;
|
|||||||
pub mod vertex;
|
pub mod vertex;
|
||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub use camera::Camera;
|
pub use camera::{Camera, CameraController, PITCH_LIMIT};
|
||||||
pub use lights::Lights;
|
pub use lights::Lights;
|
||||||
pub use material::Material;
|
pub use material::Material;
|
||||||
pub use mesh::Mesh;
|
pub use mesh::Mesh;
|
||||||
|
|||||||
@@ -262,6 +262,12 @@ impl Scene {
|
|||||||
&self.camera
|
&self.camera
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the scene's active camera, for in-place per-frame edits
|
||||||
|
/// (e.g. [`CameraController::apply_to`](crate::resources::CameraController) during `update`).
|
||||||
|
pub fn camera_mut(&mut self) -> &mut Camera {
|
||||||
|
&mut self.camera
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds a directional light (direction **from the surface toward the light**, color, intensity).
|
/// Adds a directional light (direction **from the surface toward the light**, color, intensity).
|
||||||
/// Lights are global to the scene and uploaded into the frame uniforms each frame (Phase 4.2,
|
/// Lights are global to the scene and uploaded into the frame uniforms each frame (Phase 4.2,
|
||||||
/// Étape 12). Returns `Err` if the scene would exceed `MAX_LIGHTS` (capacity is bounded; no
|
/// Étape 12). Returns `Err` if the scene would exceed `MAX_LIGHTS` (capacity is bounded; no
|
||||||
|
|||||||
Reference in New Issue
Block a user