diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..dd6f0cc13
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Exclude website from GitHub language statistics
+website/** linguist-documentation
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5d0f591b1..7d1931b56 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,4 +1,4 @@
-name: Rapier CI build
+name: CI build
on:
push:
diff --git a/.run/camera.run.xml b/.run/camera.run.xml
index 83047fb03..d9543616c 100644
--- a/.run/camera.run.xml
+++ b/.run/camera.run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/clippy.run.xml b/.run/clippy.run.xml
index d45dd3ff8..071ca0c32 100644
--- a/.run/clippy.run.xml
+++ b/.run/clippy.run.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/.run/cube (web).run.xml b/.run/cube (web).run.xml
index 1c2bdcd79..8358d1aea 100644
--- a/.run/cube (web).run.xml
+++ b/.run/cube (web).run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/cube.run.xml b/.run/cube.run.xml
index 742019341..02372b492 100644
--- a/.run/cube.run.xml
+++ b/.run/cube.run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.run/instancing2d (web).run.xml b/.run/instancing2d (web).run.xml
index 4e0c9b710..8807eb58e 100644
--- a/.run/instancing2d (web).run.xml
+++ b/.run/instancing2d (web).run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/instancing2d.run.xml b/.run/instancing2d.run.xml
index 4e7f39704..cb683a029 100644
--- a/.run/instancing2d.run.xml
+++ b/.run/instancing2d.run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.run/lines.run.xml b/.run/lines.run.xml
index f50f9e17a..d0b8a458b 100644
--- a/.run/lines.run.xml
+++ b/.run/lines.run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/lines2d (web).run.xml b/.run/lines2d (web).run.xml
new file mode 100644
index 000000000..86012f102
--- /dev/null
+++ b/.run/lines2d (web).run.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/lines2d.run.xml b/.run/lines2d.run.xml
new file mode 100644
index 000000000..e86801512
--- /dev/null
+++ b/.run/lines2d.run.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/multi_light (web).run.xml b/.run/multi_light (web).run.xml
new file mode 100644
index 000000000..34651e93f
--- /dev/null
+++ b/.run/multi_light (web).run.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/multi_light.run.xml b/.run/multi_light.run.xml
new file mode 100644
index 000000000..f2586fba0
--- /dev/null
+++ b/.run/multi_light.run.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/multi_windows.run.xml b/.run/multi_windows.run.xml
index fc4695794..4e13ced08 100644
--- a/.run/multi_windows.run.xml
+++ b/.run/multi_windows.run.xml
@@ -1,7 +1,9 @@
-
+
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/obj.run.xml b/.run/obj.run.xml
index 3e3722522..b4435a8d8 100644
--- a/.run/obj.run.xml
+++ b/.run/obj.run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/planar_lines (web).run.xml b/.run/points2d (web).run.xml
similarity index 81%
rename from .run/planar_lines (web).run.xml
rename to .run/points2d (web).run.xml
index 68d55ccd6..3a7c123f2 100644
--- a/.run/planar_lines (web).run.xml
+++ b/.run/points2d (web).run.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/.run/planar_polylines.run.xml b/.run/points2d.run.xml
similarity index 84%
rename from .run/planar_polylines.run.xml
rename to .run/points2d.run.xml
index d8f414eec..b5323952e 100644
--- a/.run/planar_polylines.run.xml
+++ b/.run/points2d.run.xml
@@ -1,7 +1,7 @@
-
+
-
+
diff --git a/.run/planar_polylines (web).run.xml b/.run/polylines2d (web).run.xml
similarity index 80%
rename from .run/planar_polylines (web).run.xml
rename to .run/polylines2d (web).run.xml
index f440850eb..941565af0 100644
--- a/.run/planar_polylines (web).run.xml
+++ b/.run/polylines2d (web).run.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/.run/planar_lines.run.xml b/.run/polylines2d.run.xml
similarity index 84%
rename from .run/planar_lines.run.xml
rename to .run/polylines2d.run.xml
index 56311223c..74b9f8aa1 100644
--- a/.run/planar_lines.run.xml
+++ b/.run/polylines2d.run.xml
@@ -1,7 +1,7 @@
-
+
-
+
@@ -17,4 +17,4 @@
-
\ No newline at end of file
+
diff --git a/.run/primitives.run.xml b/.run/primitives.run.xml
index 616ed1ed7..3169b5c8a 100644
--- a/.run/primitives.run.xml
+++ b/.run/primitives.run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/primitives2d.run.xml b/.run/primitives2d.run.xml
index e2fcd077f..bd035536b 100644
--- a/.run/primitives2d.run.xml
+++ b/.run/primitives2d.run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.run/procedural (web).run.xml b/.run/procedural (web).run.xml
index 55b022c2e..78a2cebc4 100644
--- a/.run/procedural (web).run.xml
+++ b/.run/procedural (web).run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/procedural.run.xml b/.run/procedural.run.xml
index 3354fe253..d791845dc 100644
--- a/.run/procedural.run.xml
+++ b/.run/procedural.run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.run/rectangle.run.xml b/.run/rectangle.run.xml
index d932374b6..1d79b76d2 100644
--- a/.run/rectangle.run.xml
+++ b/.run/rectangle.run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/stereo.run.xml b/.run/stereo.run.xml
index a5ec022e5..63426f5a2 100644
--- a/.run/stereo.run.xml
+++ b/.run/stereo.run.xml
@@ -1,7 +1,9 @@
+
+
@@ -9,11 +11,10 @@
-
-
+
\ No newline at end of file
diff --git a/.run/test.run.xml b/.run/test.run.xml
index 3596f66c1..a52390b4c 100644
--- a/.run/test.run.xml
+++ b/.run/test.run.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/.run/ui (web).run.xml b/.run/ui (web).run.xml
index edffa6336..448f77eb4 100644
--- a/.run/ui (web).run.xml
+++ b/.run/ui (web).run.xml
@@ -1,5 +1,6 @@
+
diff --git a/.run/ui.run.xml b/.run/ui.run.xml
index 8d79c5c31..d7b81abe3 100644
--- a/.run/ui.run.xml
+++ b/.run/ui.run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ad0cbb2e..752750253 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,203 @@
+# v0.39.0
+
+Major API overhaul: scene separation from window, glam math library, simplified transform API, and 2D/3D naming conventions.
+
+## Breaking Changes
+
+### Scene Separation from Window
+- **Scenes are no longer owned by the Window** - Create scenes independently and pass them to render
+- **New render methods**: `window.render_3d(&mut scene, &mut camera)` and `window.render_2d(&mut scene, &mut camera)`
+- **Removed**: `window.add_cube()`, `window.add_sphere()`, etc. - use `scene.add_cube()` instead
+- **Removed**: `window.scene()` and `window.scene_mut()` - manage scenes directly
+- Cameras must now be created and managed by user code
+
+### 3D Type Renaming
+- `SceneNode` → `SceneNode3d`
+- `Object` → `Object3d`
+- `GpuMesh` → `GpuMesh3d`
+- `MeshManager` → `MeshManager3d`
+- `MaterialManager` → `MaterialManager3d`
+- `PointRenderer` → `PointRenderer3d`
+- `PolylineRenderer` → `PolylineRenderer3d`
+- `Camera` trait → `Camera3d` trait
+
+### Math Library: nalgebra → glam
+- **Switched** from `nalgebra` to `glamx` (glam wrapper) for all public APIs
+- Key type changes:
+ - `Point3` → `Vec3`
+ - `Vector3` → `Vec3`
+ - `UnitQuaternion` → `Quat`
+ - `Translation3` → `Vec3`
+ - `Isometry3` → `Pose3`
+ - `Point2` / `Vector2` → `Vec2`
+ - `UnitComplex` → `f32` (just use angle in radians directly)
+- Common conversions:
+ - `Vector3::y_axis()` → `Vec3::Y`
+ - `Point3::origin()` → `Vec3::ZERO`
+ - `UnitQuaternion::from_axis_angle(&Vector3::y_axis(), angle)` → `Quat::from_axis_angle(Vec3::Y, angle)`
+
+### Simplified Transform API
+- `prepend_to_local_rotation(&rot)` → `rotate(rot)`
+- `prepend_to_local_translation(&t)` → `translate(t)`
+- `set_local_rotation(r)` → `set_rotation(r)`
+- `set_local_translation(t)` → `set_position(t)`
+- `local_rotation()` → `rotation()`
+- `local_translation()` → `position()`
+- Rotation values passed by value, not reference
+
+### 2D API Renaming ("planar" → "2d")
+- `PlanarSceneNode` → `SceneNode2d`
+- `PlanarCamera` → `Camera2d`
+- `FixedView` (planar) → `FixedView2d`
+- `Sidescroll` → `PanZoomCamera2d`
+- `draw_planar_line()` → `draw_line_2d()`
+- `draw_planar_point()` → `draw_point_2d()`
+- `add_rectangle()` / `add_circle()` now on `SceneNode2d`
+- Modules renamed: `planar_camera` → `camera2d`, `planar_polyline_renderer` → `polyline_renderer2d`
+
+### Camera Renaming
+- `ArcBall` → `OrbitCamera3d`
+- `FirstPerson` → `FirstPersonCamera3d`
+- `FirstPersonStereo` → `FirstPersonStereoCamera3d`
+- `FixedView` → `FixedViewCamera3d`
+
+### Color API
+- Colors now use `[f32; 3]` arrays instead of `Point3`
+- `set_color(r, g, b)` now takes a `[f32; 3]` and you can use color constants directly: `set_color(RED)`
+- `set_lines_color(Some(Point3::new(1.0, 0.0, 0.0)))` → `set_lines_color(Some(RED))`
+- New `color` module with CSS named color constants (re-exported in prelude)
+
+### parry3d Now Optional
+- `parry3d` moved behind `parry` feature flag
+- `parry3d` now uses glam directly (no nalgebra conversion needed)
+- `add_trimesh()` requires `parry` feature
+- Examples `procedural` and `decomp` require `--features parry`
+
+### Lighting API
+- **Replaced** `Light::Absolute(Point3)` and `Light::StickToCamera` enum with new `Light` struct
+- **Removed** `window.set_light()` - use `scene.add_light()` instead
+- Lights are now scene nodes, not window-level configuration
+
+### Other Breaking Changes
+- `SceneNode::new_empty()` → `SceneNode3d::empty()`
+- `SceneNode::unlink()` → `SceneNode3d::detach()`
+- Index buffers always use `u32` (removed `vertex_index_u32` feature)
+- `instant` crate replaced with `web_time`
+- Camera modules merged: `camera2d` and `camera3d` now under single `camera` module
+- **Removed** `Window::set_frame_limit()` method
+- `set_background_color(r, g, b)` → `set_background_color(Color)`
+- `draw_line(a, b, color, width)` → `draw_line(a, b, color, width, perspective)`
+
+## Migration Guide
+
+### Basic 3D scene (before):
+```rust
+use kiss3d::window::Window;
+use nalgebra::{UnitQuaternion, Vector3};
+
+#[kiss3d::main]
+async fn main() {
+ let mut window = Window::new("Example").await;
+ let mut cube = window.add_cube(1.0, 1.0, 1.0);
+ cube.set_color(1.0, 0.0, 0.0);
+
+ let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.01);
+ while window.render().await {
+ cube.prepend_to_local_rotation(&rot);
+ }
+}
+```
+
+### Basic 3D scene (after):
+```rust
+use kiss3d::prelude::*;
+
+#[kiss3d::main]
+async fn main() {
+ let mut window = Window::new("Example").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+
+ let mut cube = scene.add_cube(1.0, 1.0, 1.0);
+ cube.set_color(RED);
+
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.01);
+ while window.render_3d(&mut scene, &mut camera).await {
+ cube.rotate(rot);
+ }
+}
+```
+
+### Basic 2D scene (before):
+```rust
+use kiss3d::window::Window;
+use nalgebra::UnitComplex;
+
+let mut window = Window::new("2D").await;
+let mut rect = window.add_rectangle(50.0, 100.0);
+let rot = UnitComplex::new(0.01);
+while window.render().await {
+ rect.prepend_to_local_rotation(&rot);
+}
+```
+
+### Basic 2D scene (after):
+```rust
+use kiss3d::prelude::*;
+
+let mut window = Window::new("2D").await;
+let mut camera = PanZoomCamera2d::default();
+let mut scene = SceneNode2d::empty();
+
+let mut rect = scene.add_rectangle(50.0, 100.0);
+while window.render_2d(&mut scene, &mut camera).await {
+ rect.rotate(0.01);
+}
+```
+
+## New Features
+
+### Multiple Lights Support
+- Scene now supports up to 8 lights (instead of just one)
+- **New light types**: `Light::point()`, `Light::directional()`, `Light::spot()`
+- Lights attach to scene nodes via `scene.add_light(light)`
+- Convenience methods: `add_point_light()`, `add_directional_light()`, `add_spot_light()`
+- Lights inherit transforms from parent scene nodes
+- Light properties: `color`, `intensity`, `enabled`, `attenuation_radius`
+- Builder pattern: `Light::point(100.0).with_color(RED).with_intensity(5.0)`
+
+### Ambient Light Control
+- `window.set_ambient(f32)` - set ambient light intensity
+- `window.ambient()` - get current ambient light intensity
+
+### Color Module
+- New `kiss3d::color` module with all CSS named colors
+- Example: `color::RED`, `color::LIME_GREEN`, `color::STEEL_BLUE`
+- New `Color` struct (alias of `Rgba`) for colors with alpha channel
+
+### Subdivision Control for Primitives
+- `add_sphere_with_subdiv(r, ntheta, nphi)` - control sphere tessellation
+- `add_cone_with_subdiv(r, h, nsubdiv)` - control cone segments
+- `add_cylinder_with_subdiv(r, h, nsubdiv)` - control cylinder segments
+- `add_capsule_with_subdiv(r, h, ntheta, nphi)` - control capsule tessellation
+
+### Optional Serde Support
+- New `serde` feature flag for serialization support
+- Enable with: `kiss3d = { version = "0.39", features = ["serde"] }`
+
+### Default Cameras
+- `OrbitCamera3d::default()` - starts at (0, 0, -2) looking at origin
+- `PanZoomCamera2d::default()` - centered at origin with 2x zoom
+
+### Multi-Window Support Improvements
+- Better handling of multiple windows in the same application
+
+### Line Rendering
+- `draw_line()` now takes a `perspective` parameter to control size scaling with distance
+- 2D lines now render on top of 2D surfaces
+
+---
+
# v0.38.0
Switch from OpenGL (glow/glutin) to wgpu for cross-platform GPU support.
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 60b6a1794..000000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,312 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Project Overview
-
-Kiss3d is a "Keep It Simple, Stupid" 3D graphics engine for Rust. It's designed for simplicity and ease of use rather than feature completeness or performance. The library allows developers to draw simple geometric figures with minimal code and supports both native platforms and WASM without code changes.
-
-**Key Principle**: One-liner features (from the user's point of view) are strongly preferred. Keep things simple.
-
-## Dependencies
-
-- **nalgebra 0.34**: Used for all math operations (vectors, matrices, transformations)
-- **parry3d 0.25**: Physics/geometry library (successor to ncollide3d)
-- **glow 0.16**: OpenGL bindings
-- **glutin 0.32** (native only): Window creation and OpenGL context management
-- **egui 0.32** (optional feature): Immediate mode GUI integration
-- **wasm-bindgen** (WASM only): Browser integration
-
-If users have nalgebra in their project, they must use version 0.34 to match kiss3d's version.
-
-## Feature Flags
-
-- **egui**: Optional immediate mode GUI integration (enables `egui` and `egui_glow` dependencies)
-- **vertex_index_u32**: Use 32-bit vertex indices instead of 16-bit (useful for large meshes)
-
-Enable features in Cargo.toml:
-```toml
-[dependencies]
-kiss3d = { version = "0.37", features = ["egui"] }
-```
-
-## Build and Test Commands
-
-### Building
-```bash
-# Check for errors (fast)
-cargo check
-
-# Build the project
-cargo build
-
-# Build with release optimizations
-cargo build --release
-
-# Check/build for WASM
-cargo check --target wasm32-unknown-unknown
-cargo build --target wasm32-unknown-unknown --release
-```
-
-### Testing
-```bash
-# Run all tests
-cargo test
-
-# Run a specific test
-cargo test test_name
-
-# Run tests with output visible
-cargo test -- --nocapture
-```
-
-### Running Examples
-```bash
-# Run a specific example (native)
-cargo run --example cube
-cargo run --example procedural
-cargo run --example custom_material
-
-# Run example with egui feature
-cargo run --example ui --features egui
-
-# Run example with timeout (useful for quick testing)
-timeout 3 cargo run --example cube
-
-# Run with backtrace for debugging
-RUST_BACKTRACE=1 cargo run --example cube
-RUST_BACKTRACE=full cargo run --example cube
-
-# Run example for WASM
-cargo run --example cube --target wasm32-unknown-unknown
-```
-
-There are 36+ examples in the `examples/` directory demonstrating various features including:
-- Basic shapes: `cube`, `primitives`, `procedural`
-- Interaction: `event`, `mouse_events`, `camera`
-- Advanced rendering: `custom_material`, `texturing`, `wireframe`, `post_processing`
-- Scene management: `group`, `add_remove`, `instancing3d`
-- UI integration: `ui` (requires `egui` feature)
-- File loading: `obj`
-
-### Linting
-```bash
-# Run clippy for linting
-cargo clippy
-```
-
-### Documentation
-```bash
-# Build and open documentation
-cargo doc --open
-```
-
-## Architecture
-
-### Core Modules
-
-**window** (`src/window/`)
-- `Window`: Main entry point for the 3D engine. Handles rendering loop, event processing, and scene management.
-- `Canvas`: Platform abstraction layer
- - `gl_canvas.rs`: Native OpenGL canvas implementation (glutin-based)
- - `webgl_canvas.rs`: WebGL canvas for WASM targets
-- Window owns the scene graph, cameras, renderers, and manages the render loop.
-
-**scene** (`src/scene/`)
-- `SceneNode`: Hierarchical scene graph nodes for 3D objects. Each node can have:
- - Local and world transforms (Isometry3) and scales
- - Children nodes (forming a hierarchy)
- - An optional `Object` to render
- - Parent references (using Weak pointers to avoid cycles)
-- `PlanarSceneNode`: 2D scene graph nodes for overlay rendering
-- `Object`: Wraps a `GpuMesh` and `Material` for rendering
-
-**resource** (`src/resource/`)
-- `GpuMesh`: GPU-side mesh data with vertices, indices, normals, and UVs. All geometry data is stored in GPU buffers via `GPUVec`.
-- `RenderMesh`: CPU-side mesh descriptor used for procedural generation (converted to `GpuMesh`)
-- `MeshManager`: Singleton that manages shared mesh resources
-- `Material`/`MaterialManager`: Shader programs and material properties
-- `Texture`/`TextureManager`: Texture loading and management
-- `FramebufferManager`: Manages render targets for post-processing
-
-**procedural** (`src/procedural/`)
-- Procedural mesh generation copied from ncollide3d
-- Generators for basic shapes: cuboid, sphere, cone, cylinder, capsule, quad
-- Path-based shape generation (extrusion, bezier curves)
-- `RenderMesh`: High-level mesh descriptor with flexible index buffers
-- All procedural functions return `RenderMesh` which can be added to scenes via `SceneNode::add_render_mesh()`
-
-**camera** (`src/camera/`)
-- `Camera` trait: Defines camera interface
-- `ArcBall`: Default orbital camera (scroll to zoom, click+drag to rotate/pan)
-- `FirstPerson`: FPS-style camera
-- Custom cameras can be implemented via the `Camera` trait
-
-**renderer** (`src/renderer/`)
-- `LineRenderer`: Renders 3D lines
-- `PointRenderer`: Renders 3D points
-- `EguiRenderer`: Integrates egui immediate mode GUI (when `egui` feature is enabled)
-
-**builtin** (`src/builtin/`)
-- Built-in shader programs (vertex and fragment shaders)
-- Default materials and effects
-
-**loader** (`src/loader/`)
-- Mesh loading from files (OBJ, etc.)
-
-**event** (`src/event/`)
-- Event handling abstraction for keyboard, mouse, window events
-- Platform-agnostic event types
-
-**text** (`src/text/`)
-- Text rendering using rusttype
-- Font loading and glyph rasterization
-
-**post_processing** (`src/post_processing/`)
-- Post-processing effects framework
-- Effects are applied after scene rendering
-
-**context** (`src/context/`)
-- OpenGL context management and state tracking
-
-### Cross-Platform Support
-
-The `#[kiss3d::main]` procedural macro (in `kiss3d-macro/`) enables writing platform-agnostic code.
-
-**Implementation Details** (`kiss3d-macro/src/lib.rs`):
-- The macro validates that it's applied to an async `fn main()` with no parameters
-- Expands to two different entry points based on target architecture:
- - Native: Calls `::kiss3d::pollster::block_on(__kiss3d_async_main())`
- - WASM: Calls `::kiss3d::wasm_bindgen_futures::spawn_local(__kiss3d_async_main())`
-- The original async function becomes `__kiss3d_async_main()`
-
-**Usage**:
-
-```rust
-#[kiss3d::main]
-async fn main() {
- let mut window = Window::new("Title");
- while window.render().await {
- // Render loop
- }
-}
-```
-
-**Native**: `pollster::block_on` runs the async function synchronously
-**WASM**: `wasm_bindgen_futures::spawn_local` integrates with browser's `requestAnimationFrame`
-
-The `window.render().await` call:
-- Native: Returns immediately each frame
-- WASM: Yields to browser's event loop, returns when next frame is ready
-
-Users should NOT add `pollster` or `wasm_bindgen_futures` to their dependencies - they are re-exported by kiss3d.
-
-### Important Type Relationships
-
-```
-Window
-├── SceneNode (3D root)
-│ ├── Children: Vec
-│ └── Object (optional)
-│ ├── GpuMesh (vertices, indices, normals, UVs on GPU)
-│ └── Material (shader + properties)
-├── PlanarSceneNode (2D overlay root)
-├── Camera (Rc>)
-├── Renderers (LineRenderer, PointRenderer, TextRenderer)
-└── Managers (MeshManager, MaterialManager, TextureManager, FramebufferManager)
-```
-
-### Scene Graph Pattern
-
-The scene graph uses `Rc>` internally:
-- `Rc` for shared ownership between parent and children
-- `RefCell` for interior mutability
-- Parent references use `Weak>` to prevent cycles
-- Transforms are hierarchical (parent transforms affect children)
-
-### Mesh Creation Flow
-
-1. Generate `RenderMesh` using `procedural` module OR load from file
-2. Add to scene: `window.add_cube()` or `scene_node.add_render_mesh(mesh)`
-3. Internally: `RenderMesh` → `GpuMesh` (uploads to GPU)
-4. `MeshManager` caches meshes to avoid duplication
-5. `SceneNode` wraps `GpuMesh` + `Material` in an `Object`
-
-## Common Patterns
-
-### Adding Geometry
-```rust
-// High-level API (recommended)
-let mut cube = window.add_cube(1.0, 1.0, 1.0);
-let mut sphere = window.add_sphere(0.5);
-
-// Lower-level: add to specific scene node
-let mesh = procedural::sphere(0.5, 32, 32, true);
-let mut node = scene_node.add_render_mesh(mesh);
-
-// Custom mesh from TriMesh
-let trimesh = parry3d::shape::TriMesh::new(...);
-let mut node = scene_node.add_trimesh(trimesh, Vector3::from_element(1.0));
-```
-
-### Transforming Objects
-```rust
-// Set position
-node.set_local_translation(Translation3::new(1.0, 2.0, 3.0));
-
-// Set rotation
-let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.1);
-node.set_local_rotation(rot);
-
-// Incremental transforms
-node.append_translation(&Translation3::new(0.1, 0.0, 0.0));
-node.prepend_to_local_rotation(&rot);
-
-// Set scale
-node.set_local_scale(2.0, 1.0, 2.0);
-```
-
-### Event Handling
-```rust
-for event in window.events().iter() {
- match event.value {
- WindowEvent::Key(button, Action::Press, _) => {
- // Handle key press
- }
- WindowEvent::MouseButton(button, Action::Press, _) => {
- // Handle mouse button
- }
- _ => {}
- }
-}
-```
-
-## Version History Context
-
-### v0.37.0 (Current)
-- Latest release with all v0.36.0 features stabilized
-- Egui integration improvements
-- Bug fixes and stability improvements
-
-### v0.36.0 (Major Breaking Changes)
-- Replaced `ncollide3d` with `parry3d` (ncollide's successor)
-- Updated `nalgebra` from 0.30 to 0.34
-- Renamed `Mesh` → `GpuMesh` to clarify it's GPU-side data
-- Copied procedural mesh generation from ncollide into kiss3d
-- Introduced `RenderMesh` type for CPU-side mesh description
-- Switched to async API with `#[kiss3d::main]` macro for cross-platform support
-- Replaced conrod with egui for UI (optional feature)
-- Updated glutin to 0.32 for window management
-
-Key migration points for users:
-- Use `parry3d::shape::TriMesh` instead of `ncollide3d::procedural::TriMesh`
-- Use `kiss3d::procedural` instead of importing from ncollide
-- Methods return `GpuMesh` instead of `Mesh`
-- Must use `#[kiss3d::main]` and async render loop
-
-## Code Style Notes
-
-- Use `na::` for nalgebra imports (aliased as `na`)
-- Heavy use of `Rc>` for shared mutable state
-- Platform-specific code uses `#[cfg(target_arch = "wasm32")]` and `#[cfg(not(target_arch = "wasm32"))]`
-- Allowed clippy lints: `module_inception`, `too_many_arguments`, `type_complexity`
diff --git a/Cargo.toml b/Cargo.toml
index c3bd704e1..29386aaa0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
authors = ["Sébastien Crozet "]
name = "kiss3d"
-version = "0.38.0"
+version = "0.39.0"
autoexamples = true
description = "Keep it simple, stupid, 2D and 3D graphics engine for Rust."
@@ -30,26 +30,27 @@ path = "src/lib.rs"
[features]
egui = ["dep:egui", "dep:egui-wgpu"]
-vertex_index_u32 = []
recording = ["dep:ffmpeg-the-third"]
+parry = ["dep:parry3d"]
+serde = ["dep:serde", "glamx/serde", "bitflags/serde", "rgb/serde"]
[dependencies]
-bitflags = { version = "2", features = ["serde"] }
+bitflags = "2"
bytemuck = { version = "1", features = ["derive"] }
-egui = { version = "0.32", optional = true, default-features = true }
-egui-wgpu = { version = "0.32", optional = true }
+egui = { version = "0.33", optional = true, default-features = true }
+egui-wgpu = { version = "0.33", optional = true }
either = "1"
image = "0.25"
-instant = { version = "0.1", features = ["wasm-bindgen"] }
+web-time = "1"
kiss3d-macro = { version = "0.36.0", path = "kiss3d-macro" }
log = "0.4"
-nalgebra = { version = "0.34", features = ["bytemuck"] }
+glamx = { version = "0.1", features = ["bytemuck"] }
ffmpeg-the-third = { version = "4", optional = true }
num-traits = "0.2"
-parry3d = "0.25"
+parry3d = { version = "0.25", optional = true }
+rgb = "0.8"
rusttype = { version = "0.9", features = ["gpu_cache"] }
-serde = "1"
-serde_derive = "1"
+serde = { version = "1", features = ["derive"], optional = true }
wgpu = "25"
winit = "0.30"
@@ -82,6 +83,18 @@ web-sys = { version = "0.3", features = [
[dev-dependencies]
env_logger = "0.11"
-nalgebra = { version = "0.34", features = ["rand", "bytemuck"] }
parry2d = "0.25"
-rand = "0.9"
\ No newline at end of file
+rand = "0.9"
+
+[[example]]
+name = "decomp"
+required-features = ["parry"]
+
+[[example]]
+name = "procedural"
+required-features = ["parry"]
+
+[patch.crates-io]
+#glamx = { path = "../../glamx" }
+#parry3d = { path = "../../parry/crates/parry3d" }
+#parry2d = { path = "../../parry/crates/parry2d" }
\ No newline at end of file
diff --git a/README.md b/README.md
index b95f018b4..90b5d3f46 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,20 @@
-# Kiss3d
+
+
+
-
+
+
+
+
+
+
+
+
+
-[](https://crates.io/crates/kiss3d)
-[](https://docs.rs/kiss3d)
-[](https://github.com/sebcrozet/kiss3d)
-
-Keep It Simple, Stupid 3d graphics engine.
+Keep It Simple, Stupid 3D and 2D graphics engine.
This library is born from the frustration that today’s 3D
graphics library are either:
@@ -31,35 +37,32 @@ with as little friction as possible.
* First-person camera available as well, and user-defined cameras are possible.
* Render boxes, spheres, cones, cylinders, quads and lines simply
* Change an object's color or texture.
-* Change an object's transform (we use [nalgebra](http://nalgebra.org) to do that).
+* Change an object's transform.
* Create basic post-processing effects.
As an example, creating a scene with a red, rotating cube with a light attached
to the camera is as simple as:
```rust
-extern crate kiss3d;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use kiss3d::na::{UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: cube").await;
- let mut c = window.add_cube(1.0, 1.0, 1.0);
-
- c.set_color(1.0, 0.0, 0.0);
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 2.0, -2.0));
- window.set_light(Light::StickToCamera);
+ let mut c = scene.add_cube(1.0, 1.0, 1.0).set_color(RED);
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
- while window.render().await {
- c.prepend_to_local_rotation(&rot);
+ while window.render_3d(&mut scene, &mut camera).await {
+ c.rotate(rot);
}
}
-
```
This code works on **both native platforms and WASM** without any changes! The `#[kiss3d::main]`
@@ -89,8 +92,6 @@ Simply add the following to your `Cargo.toml` file:
[dependencies]
kiss3d = "0.37"
```
-Note: If your project already uses nalgebra, you'll need the same version used by `kiss3d`, or you may run into compatibility issues.
-
### Optional Features
#### Recording
@@ -108,28 +109,6 @@ Kiss3d supports recording your 3D scene to MP4 video files. This feature require
kiss3d = { version = "0.37", features = ["recording"] }
```
-**Example usage:**
-```rust
-use kiss3d::window::{Window, RecordingConfig};
-
-#[kiss3d::main]
-async fn main() {
- let mut window = Window::new("Recording Example").await;
-
- // Start recording (optionally skip frames to reduce file size)
- let config = RecordingConfig::new().with_frame_skip(2);
- window.begin_recording_with_config(config);
-
- // Render your scene
- for _ in 0..120 {
- window.render().await;
- }
-
- // Save to MP4 (30 fps)
- window.end_recording("output.mp4", 30).unwrap();
-}
-```
-
#### Egui Integration
For immediate mode GUI support, enable the `egui` feature:
@@ -137,12 +116,3 @@ For immediate mode GUI support, enable the `egui` feature:
[dependencies]
kiss3d = { version = "0.37", features = ["egui"] }
```
-
-
-## Contributions
-I’d love to see people improving this library for their own needs. However, keep in mind that
-**kiss3d** is KISS. One-liner features (from the user point of view) are preferred.
-
-## Acknowledgements
-
-Thanks to all the Rustaceans for their help, and their OpenGL bindings.
diff --git a/assets/kiss3d-logo-small.png b/assets/kiss3d-logo-small.png
new file mode 100644
index 000000000..6c9912e8d
Binary files /dev/null and b/assets/kiss3d-logo-small.png differ
diff --git a/assets/kiss3d-logo.af b/assets/kiss3d-logo.af
new file mode 100644
index 000000000..88af3d217
Binary files /dev/null and b/assets/kiss3d-logo.af differ
diff --git a/assets/kiss3d-logo.png b/assets/kiss3d-logo.png
index 96021a388..80fee999d 100644
Binary files a/assets/kiss3d-logo.png and b/assets/kiss3d-logo.png differ
diff --git a/examples/add_remove.rs b/examples/add_remove.rs
index cf93c0a17..c775ab2ca 100644
--- a/examples/add_remove.rs
+++ b/examples/add_remove.rs
@@ -1,23 +1,22 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: add_remove").await;
- let mut c = window.add_cube(1.0, 1.0, 1.0);
- let mut added = true;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 0.0, -2.0));
- window.set_light(Light::StickToCamera);
+ let mut cube = scene.add_cube(1.0, 1.0, 1.0);
+ let mut added = true;
- while window.render().await {
+ while window.render_3d(&mut scene, &mut camera).await {
if added {
- window.remove_node(&mut c);
+ cube.remove();
} else {
- c = window.add_cube(1.0, 1.0, 1.0);
- c.set_color(1.0, 0.0, 0.0);
+ cube = scene.add_cube(1.0, 1.0, 1.0).set_color(RED);
}
added = !added;
diff --git a/examples/camera.rs b/examples/camera.rs
index 34b3c5866..e6832d886 100644
--- a/examples/camera.rs
+++ b/examples/camera.rs
@@ -1,22 +1,15 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::camera::{ArcBall, FirstPerson};
-use kiss3d::event::{Action, Key, WindowEvent};
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::Point3;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
- let eye = Point3::new(10.0f32, 10.0, 10.0);
- let at = Point3::origin();
- let mut first_person = FirstPerson::new(eye, at);
- let mut arc_ball = ArcBall::new(eye, at);
+ let eye = Vec3::new(10.0f32, 10.0, 10.0);
+ let at = Vec3::ZERO;
+ let mut first_person = FirstPersonCamera3d::new(eye, at);
+ let mut arc_ball = OrbitCamera3d::new(eye, at);
let mut use_arc_ball = true;
let mut window = Window::new("Kiss3d: camera").await;
- window.set_light(Light::StickToCamera);
+ let mut scene = SceneNode3d::empty();
while !window.should_close() {
// rotate the arc-ball camera.
@@ -37,29 +30,14 @@ async fn main() {
}
}
- window.draw_line(
- &Point3::origin(),
- &Point3::new(1.0, 0.0, 0.0),
- &Point3::new(1.0, 0.0, 0.0),
- 2.0,
- );
- window.draw_line(
- &Point3::origin(),
- &Point3::new(0.0, 1.0, 0.0),
- &Point3::new(0.0, 1.0, 0.0),
- 2.0,
- );
- window.draw_line(
- &Point3::origin(),
- &Point3::new(0.0, 0.0, 1.0),
- &Point3::new(0.0, 0.0, 1.0),
- 2.0,
- );
+ window.draw_line(Vec3::ZERO, Vec3::X, RED, 2.0, false);
+ window.draw_line(Vec3::ZERO, Vec3::Y, GREEN, 2.0, false);
+ window.draw_line(Vec3::ZERO, Vec3::Z, BLUE, 2.0, false);
if use_arc_ball {
- window.render_with_camera(&mut arc_ball).await;
+ window.render_3d(&mut scene, &mut arc_ball).await;
} else {
- window.render_with_camera(&mut first_person).await;
+ window.render_3d(&mut scene, &mut first_person).await;
}
}
}
diff --git a/examples/cube.rs b/examples/cube.rs
index 8421507ff..0d9d744a6 100644
--- a/examples/cube.rs
+++ b/examples/cube.rs
@@ -1,23 +1,20 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
env_logger::init();
let mut window = Window::new("Kiss3d: cube").await;
- let mut c = window.add_cube(1.0, 1.0, 1.0);
-
- c.set_color(1.0, 0.0, 0.0);
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 2.0, -2.0));
- window.set_light(Light::StickToCamera);
+ let mut c = scene.add_cube(1.0, 1.0, 1.0).set_color(RED);
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
- while window.render().await {
- c.prepend_to_local_rotation(&rot);
+ while window.render_3d(&mut scene, &mut camera).await {
+ c.rotate(rot);
}
}
diff --git a/examples/custom_material.rs b/examples/custom_material.rs
index 19a541a9a..db81a7e95 100644
--- a/examples/custom_material.rs
+++ b/examples/custom_material.rs
@@ -1,14 +1,5 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::camera::Camera;
-use kiss3d::context::Context;
-use kiss3d::light::Light;
-use kiss3d::resource::vertex_index::VERTEX_INDEX_FORMAT;
-use kiss3d::resource::{GpuData, GpuMesh, Material, RenderContext};
-use kiss3d::scene::{InstancesBuffer, ObjectData};
-use kiss3d::window::Window;
-use na::{Isometry3, Matrix3, Translation3, UnitQuaternion, Vector3};
+use kiss3d::prelude::vertex_index::VERTEX_INDEX_FORMAT;
+use kiss3d::prelude::*;
use std::any::Any;
use std::cell::RefCell;
use std::rc::Rc;
@@ -16,18 +7,23 @@ use std::rc::Rc;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: custom_material").await;
- let mut c = window.add_sphere(1.0);
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, 6.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
let material = Rc::new(RefCell::new(
- Box::new(NormalMaterial::new()) as Box
+ Box::new(NormalMaterial::new()) as Box
));
+ let mut c = scene
+ .add_sphere(1.0)
+ .set_material(material)
+ .translate(Vec3::new(0.0, 0.0, 2.0));
- c.set_material(material);
- c.append_translation(&Translation3::new(0.0, 0.0, 2.0));
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
-
- while window.render().await {
- c.prepend_to_local_rotation(&rot);
+ while window.render_3d(&mut scene, &mut camera).await {
+ c.rotate(rot);
}
}
@@ -52,6 +48,10 @@ struct ObjectUniforms {
pub struct NormalMaterialGpuData {
frame_uniform_buffer: wgpu::Buffer,
object_uniform_buffer: wgpu::Buffer,
+ /// Cached frame bind group
+ frame_bind_group: Option,
+ /// Cached object bind group
+ object_bind_group: Option,
}
impl NormalMaterialGpuData {
@@ -75,6 +75,8 @@ impl NormalMaterialGpuData {
Self {
frame_uniform_buffer,
object_uniform_buffer,
+ frame_bind_group: None,
+ object_bind_group: None,
}
}
}
@@ -240,30 +242,25 @@ impl NormalMaterial {
}
}
-impl Material for NormalMaterial {
+impl Material3d for NormalMaterial {
fn create_gpu_data(&self) -> Box {
Box::new(NormalMaterialGpuData::new())
}
- fn render(
+ fn prepare(
&mut self,
pass: usize,
- transform: &Isometry3,
- scale: &Vector3,
- camera: &mut dyn Camera,
- _: &Light,
- data: &ObjectData,
- mesh: &mut GpuMesh,
- _instances: &mut InstancesBuffer,
+ transform: Pose3,
+ scale: Vec3,
+ camera: &mut dyn Camera3d,
+ _lights: &LightCollection,
+ _data: &ObjectData3d,
gpu_data: &mut dyn GpuData,
- context: &mut RenderContext,
+ _viewport_width: u32,
+ _viewport_height: u32,
) {
let ctxt = Context::get();
- if !data.surface_rendering_active() {
- return;
- }
-
// Downcast gpu_data to our specific type
let gpu_data = gpu_data
.as_any_mut()
@@ -274,8 +271,8 @@ impl Material for NormalMaterial {
let (view, proj) = camera.view_transform_pair(pass);
let frame_uniforms = FrameUniforms {
- view: view.to_homogeneous().into(),
- proj: proj.into(),
+ view: view.to_mat4().to_cols_array_2d(),
+ proj: proj.to_cols_array_2d(),
};
ctxt.write_buffer(
&gpu_data.frame_uniform_buffer,
@@ -284,33 +281,33 @@ impl Material for NormalMaterial {
);
// Update object uniforms
- let formatted_transform = transform.to_homogeneous();
- let formatted_scale = Matrix3::from_diagonal(&Vector3::new(scale.x, scale.y, scale.z));
+ let formatted_transform = transform.to_mat4();
+ let formatted_scale = Mat3::from_diagonal(scale);
// Pad mat3x3 to mat3x4 for proper alignment
let scale_padded: [[f32; 4]; 3] = [
[
- formatted_scale[(0, 0)],
- formatted_scale[(1, 0)],
- formatted_scale[(2, 0)],
+ formatted_scale.x_axis.x,
+ formatted_scale.x_axis.y,
+ formatted_scale.x_axis.z,
0.0,
],
[
- formatted_scale[(0, 1)],
- formatted_scale[(1, 1)],
- formatted_scale[(2, 1)],
+ formatted_scale.y_axis.x,
+ formatted_scale.y_axis.y,
+ formatted_scale.y_axis.z,
0.0,
],
[
- formatted_scale[(0, 2)],
- formatted_scale[(1, 2)],
- formatted_scale[(2, 2)],
+ formatted_scale.z_axis.x,
+ formatted_scale.z_axis.y,
+ formatted_scale.z_axis.z,
0.0,
],
];
let object_uniforms = ObjectUniforms {
- transform: formatted_transform.into(),
+ transform: formatted_transform.to_cols_array_2d(),
scale: scale_padded,
_padding: [0.0; 4],
};
@@ -320,6 +317,37 @@ impl Material for NormalMaterial {
bytemuck::bytes_of(&object_uniforms),
);
+ // Cache bind groups
+ gpu_data.frame_bind_group =
+ Some(self.create_frame_bind_group(&gpu_data.frame_uniform_buffer));
+ gpu_data.object_bind_group =
+ Some(self.create_object_bind_group(&gpu_data.object_uniform_buffer));
+ }
+
+ fn render(
+ &mut self,
+ _pass: usize,
+ _transform: Pose3,
+ _scale: Vec3,
+ _camera: &mut dyn Camera3d,
+ _lights: &LightCollection,
+ data: &ObjectData3d,
+ mesh: &mut GpuMesh3d,
+ _instances: &mut InstancesBuffer3d,
+ gpu_data: &mut dyn GpuData,
+ render_pass: &mut wgpu::RenderPass<'_>,
+ _context: &RenderContext,
+ ) {
+ if !data.surface_rendering_active() {
+ return;
+ }
+
+ // Downcast gpu_data to our specific type
+ let gpu_data = gpu_data
+ .as_any_mut()
+ .downcast_mut::()
+ .expect("NormalMaterial requires NormalMaterialGpuData");
+
// Ensure mesh buffers are on GPU
mesh.coords().write().unwrap().load_to_gpu();
mesh.normals().write().unwrap().load_to_gpu();
@@ -342,46 +370,25 @@ impl Material for NormalMaterial {
None => return,
};
- // Create bind groups with per-object buffers
- let frame_bind_group = self.create_frame_bind_group(&gpu_data.frame_uniform_buffer);
- let object_bind_group = self.create_object_bind_group(&gpu_data.object_uniform_buffer);
-
- // Create render pass
- {
- let mut render_pass = context
- .encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("custom_material_render_pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: context.color_view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
- view: context.depth_view,
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- }),
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
- render_pass.set_pipeline(&self.pipeline);
- render_pass.set_bind_group(0, &frame_bind_group, &[]);
- render_pass.set_bind_group(1, &object_bind_group, &[]);
-
- render_pass.set_vertex_buffer(0, coords_buf.slice(..));
- render_pass.set_vertex_buffer(1, normals_buf.slice(..));
- render_pass.set_index_buffer(faces_buf.slice(..), VERTEX_INDEX_FORMAT);
-
- render_pass.draw_indexed(0..mesh.num_indices(), 0, 0..1);
- }
+ // Use cached bind groups
+ let frame_bind_group = gpu_data
+ .frame_bind_group
+ .as_ref()
+ .expect("prepare() must be called before render()");
+ let object_bind_group = gpu_data
+ .object_bind_group
+ .as_ref()
+ .expect("prepare() must be called before render()");
+
+ render_pass.set_pipeline(&self.pipeline);
+ render_pass.set_bind_group(0, frame_bind_group, &[]);
+ render_pass.set_bind_group(1, object_bind_group, &[]);
+
+ render_pass.set_vertex_buffer(0, coords_buf.slice(..));
+ render_pass.set_vertex_buffer(1, normals_buf.slice(..));
+ render_pass.set_index_buffer(faces_buf.slice(..), VERTEX_INDEX_FORMAT);
+
+ render_pass.draw_indexed(0..mesh.num_indices(), 0, 0..1);
}
}
diff --git a/examples/custom_mesh.rs b/examples/custom_mesh.rs
index cb205a1aa..6c14cff34 100644
--- a/examples/custom_mesh.rs
+++ b/examples/custom_mesh.rs
@@ -1,37 +1,32 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::resource::GpuMesh;
-use kiss3d::window::Window;
-use na::{Point3, UnitQuaternion, Vector3};
-use std::cell::RefCell;
-use std::rc::Rc;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: custom_mesh").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- let a = Point3::new(-1.0, -1.0, 0.0);
- let b = Point3::new(1.0, -1.0, 0.0);
- let c = Point3::new(0.0, 1.0, 0.0);
+ let a = Vec3::new(-1.0, -1.0, 0.0);
+ let b = Vec3::new(1.0, -1.0, 0.0);
+ let c = Vec3::new(0.0, 1.0, 0.0);
let vertices = vec![a, b, c];
- let indices = vec![Point3::new(0, 1, 2)];
+ let indices = vec![[0, 1, 2]];
- let mesh = Rc::new(RefCell::new(GpuMesh::new(
+ let mesh = Rc::new(RefCell::new(GpuMesh3d::new(
vertices, indices, None, None, false,
)));
- let mut c = window.add_mesh(mesh, Vector3::new(1.0, 1.0, 1.0));
-
- c.set_color(1.0, 0.0, 0.0);
- c.enable_backface_culling(false);
-
- window.set_light(Light::StickToCamera);
+ let mut c = scene
+ .add_mesh(mesh, Vec3::new(1.0, 1.0, 1.0))
+ .set_color(RED)
+ .enable_backface_culling(false);
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
- while window.render().await {
- c.prepend_to_local_rotation(&rot);
+ while window.render_3d(&mut scene, &mut camera).await {
+ c.rotate(rot);
}
}
diff --git a/examples/custom_mesh_shared.rs b/examples/custom_mesh_shared.rs
index 400b3c0c7..d05378a60 100644
--- a/examples/custom_mesh_shared.rs
+++ b/examples/custom_mesh_shared.rs
@@ -1,51 +1,44 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::resource::{GpuMesh, MeshManager};
-use kiss3d::window::Window;
-use na::{Point3, UnitQuaternion, Vector3};
-use std::cell::RefCell;
-use std::rc::Rc;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: custom_mesh_shared").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- let a = Point3::new(-1.0, -1.0, 0.0);
- let b = Point3::new(1.0, -1.0, 0.0);
- let c = Point3::new(0.0, 1.0, 0.0);
+ let a = Vec3::new(-1.0, -1.0, 0.0);
+ let b = Vec3::new(1.0, -1.0, 0.0);
+ let c = Vec3::new(0.0, 1.0, 0.0);
let vertices = vec![a, b, c];
- let indices = vec![Point3::new(0, 1, 2)];
+ let indices = vec![[0, 1, 2]];
- let mesh = Rc::new(RefCell::new(GpuMesh::new(
+ let mesh = Rc::new(RefCell::new(GpuMesh3d::new(
vertices, indices, None, None, false,
)));
- // XXX: it would be better to do: MeshManager::add(Rc....) directly.
- MeshManager::get_global_manager(|mm| mm.add(mesh.clone(), "custom_mesh"));
-
- let mut c1 = window
- .add_geom_with_name("custom_mesh", Vector3::new(1.0, 1.0, 1.0))
- .unwrap();
- let mut c2 = window
- .add_geom_with_name("custom_mesh", Vector3::new(1.0, 1.0, 1.0))
- .unwrap();
-
- c1.set_color(1.0, 0.0, 0.0);
- c2.set_color(0.0, 1.0, 0.0);
-
- c1.enable_backface_culling(false);
- c2.enable_backface_culling(false);
-
- window.set_light(Light::StickToCamera);
-
- let rot1 = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
- let rot2 = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), -0.014);
-
- while window.render().await {
- c1.prepend_to_local_rotation(&rot1);
- c2.prepend_to_local_rotation(&rot2);
+ // TOTO: it would be better to do: MeshManager::add(Rc....) directly.
+ MeshManager3d::get_global_manager(|mm| mm.add(mesh.clone(), "custom_mesh"));
+
+ let mut c1 = scene
+ .add_geom_with_name("custom_mesh", Vec3::new(1.0, 1.0, 1.0))
+ .unwrap()
+ .set_color(RED)
+ .enable_backface_culling(false);
+ let mut c2 = scene
+ .add_geom_with_name("custom_mesh", Vec3::new(1.0, 1.0, 1.0))
+ .unwrap()
+ .set_color(GREEN)
+ .enable_backface_culling(false);
+
+ let rot1 = Quat::from_axis_angle(Vec3::Y, 0.014);
+ let rot2 = Quat::from_axis_angle(Vec3::Y, -0.014);
+
+ while window.render_3d(&mut scene, &mut camera).await {
+ c1.rotate(rot1);
+ c2.rotate(rot2);
}
}
diff --git a/examples/decomp.rs b/examples/decomp.rs
index 0a4695433..b44dce625 100644
--- a/examples/decomp.rs
+++ b/examples/decomp.rs
@@ -1,20 +1,12 @@
-extern crate instant;
-extern crate kiss3d;
-extern crate nalgebra as na;
-extern crate rand;
-
-use instant::Instant;
-use kiss3d::light::Light;
-use kiss3d::loader::obj;
-use kiss3d::window::Window;
-use na::Vector3;
-use parry3d::shape::TriMesh;
-use parry3d::transformation;
-use parry3d::transformation::vhacd::VHACDParameters;
+use kiss3d::parry3d::shape::TriMesh;
+use kiss3d::parry3d::transformation;
+use kiss3d::parry3d::transformation::vhacd::VHACDParameters;
+use kiss3d::prelude::*;
use rand::random;
use std::env;
use std::path::Path;
use std::str::FromStr;
+use web_time::Instant;
fn usage(exe_name: &str) {
println!("Usage: {} obj_file scale clusters concavity", exe_name);
@@ -42,12 +34,17 @@ async fn main() {
let scale: f32 = FromStr::from_str(&args.next().unwrap()[..]).unwrap();
let concavity: f32 = FromStr::from_str(&args.next().unwrap()[..]).unwrap();
- let scale = Vector3::from_element(scale);
+ let scale = Vec3::splat(scale);
/*
* Create the window.
*/
let mut window = Window::new("Kiss3d: convex decomposition").await;
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
/*
* Convex decomposition.
@@ -56,8 +53,9 @@ async fn main() {
let mtl_path = Path::new("none");
let teapot = obj::parse_file(obj_path, mtl_path, "none").unwrap();
- let mut m = window.add_obj(obj_path, mtl_path, scale);
- m.set_surface_rendering_activation(false);
+ scene
+ .add_obj(obj_path, mtl_path, scale)
+ .set_surface_rendering_activation(false);
let mut total_time = 0.0f64;
for &(_, ref mesh, _) in teapot.iter() {
@@ -68,27 +66,28 @@ async fn main() {
.indices
.as_split()
.iter()
- .map(|idx| [idx.x.x, idx.y.x, idx.z.x])
+ .map(|idx| [idx[0][0], idx[1][0], idx[2][0]])
.collect();
+ let coords = trimesh.coords.clone();
let begin = Instant::now();
let params = VHACDParameters {
concavity,
..VHACDParameters::default()
};
- let decomp =
- transformation::vhacd::VHACD::decompose(¶ms, &trimesh.coords, &idx, true);
+ let decomp = transformation::vhacd::VHACD::decompose(¶ms, &coords, &idx, true);
let elapsed = begin.elapsed();
total_time =
elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 / 1000000000.0;
- for (vtx, idx) in decomp.compute_exact_convex_hulls(&trimesh.coords, &idx) {
+ for (vtx, idx) in decomp.compute_exact_convex_hulls(&coords, &idx) {
let r = random();
let g = random();
let b = random();
if let Ok(trimesh) = TriMesh::new(vtx, idx) {
- let mut m = window.add_trimesh(trimesh, scale);
- m.set_color(r, g, b);
+ scene
+ .add_trimesh(trimesh, scale, true)
+ .set_color(Color::new(r, g, b, 1.0));
}
}
}
@@ -103,7 +102,5 @@ async fn main() {
* Rendering.
*
*/
- window.set_light(Light::StickToCamera);
-
- while window.render().await {}
+ while window.render_3d(&mut scene, &mut camera).await {}
}
diff --git a/examples/event.rs b/examples/event.rs
index cf9f81917..22fd1b219 100644
--- a/examples/event.rs
+++ b/examples/event.rs
@@ -1,34 +1,32 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::event::{Action, WindowEvent};
-use kiss3d::window::Window;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: events").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
- while window.render().await {
+ while window.render_3d(&mut scene, &mut camera).await {
for mut event in window.events().iter() {
match event.value {
WindowEvent::Key(button, Action::Press, _) => {
println!("You pressed the button: {:?}", button);
- println!("Do not try to press escape: the event is inhibited!");
+ println!("Do not try to press escape: the event is inhibited!");
event.inhibited = true // override the default keyboard handler
}
WindowEvent::Key(button, Action::Release, _) => {
println!("You released the button: {:?}", button);
- println!("Do not try to press escape: the event is inhibited!");
+ println!("Do not try to press escape: the event is inhibited!");
event.inhibited = true // override the default keyboard handler
}
WindowEvent::MouseButton(button, Action::Press, mods) => {
- println!("You pressed the mouse button: {:?}", button);
- println!("You pressed the mouse button with modifiers: {:?}", mods);
+ println!("You pressed the mouse button: {:?}", button);
+ println!("You pressed the mouse button with modifiers: {:?}", mods);
// dont override the default mouse handler
}
WindowEvent::MouseButton(button, Action::Release, mods) => {
- println!("You released the mouse button: {:?}", button);
- println!("You released the mouse button with modifiers: {:?}", mods);
+ println!("You released the mouse button: {:?}", button);
+ println!("You released the mouse button with modifiers: {:?}", mods);
// dont override the default mouse handler
}
WindowEvent::CursorPos(x, y, _) => {
diff --git a/examples/group.rs b/examples/group.rs
index 73604a72a..4f03364a2 100644
--- a/examples/group.rs
+++ b/examples/group.rs
@@ -1,36 +1,30 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Translation3, UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
- let mut window = Window::new("Kiss3d: cube").await;
-
- let mut g1 = window.add_group();
- let mut g2 = window.add_group();
+ let mut window = Window::new("Kiss3d: group").await;
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- g1.append_translation(&Translation3::new(2.0f32, 0.0, 0.0));
- g2.append_translation(&Translation3::new(-2.0f32, 0.0, 0.0));
+ let mut g1 = scene.add_group().set_position(Vec3::new(2.0, 0.0, 0.0));
+ let mut g2 = scene.add_group().set_position(Vec3::new(-2.0, 0.0, 0.0));
g1.add_cube(1.0, 5.0, 1.0);
g1.add_cube(5.0, 1.0, 1.0);
+ g1.set_color(RED);
g2.add_cube(1.0, 5.0, 1.0);
g2.add_cube(1.0, 1.0, 5.0);
+ g2.set_color(GREEN);
- g1.set_color(1.0, 0.0, 0.0);
- g2.set_color(0.0, 1.0, 0.0);
-
- window.set_light(Light::StickToCamera);
-
- let rot1 = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
- let rot2 = UnitQuaternion::from_axis_angle(&Vector3::x_axis(), 0.014);
+ let rot1 = Quat::from_axis_angle(Vec3::Y, 0.014);
+ let rot2 = Quat::from_axis_angle(Vec3::X, 0.014);
- while window.render().await {
- g1.prepend_to_local_rotation(&rot1);
- g2.prepend_to_local_rotation(&rot2);
+ while window.render_3d(&mut scene, &mut camera).await {
+ g1.rotate(rot1);
+ g2.rotate(rot2);
}
}
diff --git a/examples/instancing2d.rs b/examples/instancing2d.rs
index 78264effe..7f530fe08 100644
--- a/examples/instancing2d.rs
+++ b/examples/instancing2d.rs
@@ -1,18 +1,11 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::camera::FixedView;
-use kiss3d::planar_camera::Sidescroll;
-use kiss3d::scene::PlanarInstanceData;
-use kiss3d::window::Window;
-use na::{Matrix2, Point2, UnitComplex};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: instancing 2D").await;
- let mut rect = window.add_rectangle(50.0, 150.0);
-
- let rot_rect = UnitComplex::new(0.014);
+ let mut camera = PanZoomCamera2d::new(Vec2::ZERO, 0.1);
+ let mut scene = SceneNode2d::empty();
+ let rot_rect = 0.014;
let mut instances = vec![];
let count = 100;
@@ -25,9 +18,9 @@ async fn main() {
let color = [ii / count as f32, jj / count as f32, 1.0, 1.0];
let mut lines_color = color.map(|c| 1.0 - c);
lines_color[3] = 1.0;
- instances.push(PlanarInstanceData {
- position: Point2::new((ii - shift) * 150.0, (jj - shift) * 150.0),
- deformation: Matrix2::new(1.0, ii * 0.004, jj * 0.004, 1.0),
+ instances.push(InstanceData2d {
+ position: Vec2::new((ii - shift) * 150.0, (jj - shift) * 150.0),
+ deformation: Mat2::from_cols_array(&[1.0, ii * 0.004, jj * 0.004, 1.0]),
color,
lines_color: Some(lines_color),
..Default::default()
@@ -35,17 +28,13 @@ async fn main() {
}
}
- rect.set_instances(&instances);
- rect.set_lines_width(2.0, true);
- rect.set_points_size(10.0, true);
-
- let mut camera2d = Sidescroll::new();
- let mut camera3d = FixedView::new();
+ let mut rect = scene
+ .add_rectangle(50.0, 150.0)
+ .set_instances(&instances)
+ .set_lines_width(2.0, true)
+ .set_points_size(10.0, true);
- while !window.should_close() {
- rect.prepend_to_local_rotation(&rot_rect);
- window
- .render_with_cameras(&mut camera3d, &mut camera2d)
- .await;
+ while window.render_2d(&mut scene, &mut camera).await {
+ rect.append_rotation(rot_rect);
}
}
diff --git a/examples/instancing3d.rs b/examples/instancing3d.rs
index 063ee8140..a574d726f 100644
--- a/examples/instancing3d.rs
+++ b/examples/instancing3d.rs
@@ -1,16 +1,16 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::scene::InstanceData;
-use kiss3d::window::Window;
-use na::{Matrix3, Point3, UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
env_logger::init();
let mut window = Window::new("Kiss3d: instancing 3D").await;
- let mut c = window.add_cube(1.0, 1.0, 1.0);
+ let mut camera =
+ OrbitCamera3d::new(Vec3::new(200.0, 200.0, 200.0), Vec3::new(75.0, 75.0, 75.0));
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(1000.0))
+ .set_position(Vec3::new(200.0, 200.0, 200.0));
+ let mut c = scene.add_cube(1.0, 1.0, 1.0);
let mut instances = vec![];
for i in 0..100 {
@@ -19,16 +19,16 @@ async fn main() {
let ii = i as f32;
let jj = j as f32;
let kk = k as f32;
- let color = [ii / 100.0, jj / 100.0, kk / 100.0 + 0.1, 1.0];
- instances.push(InstanceData {
- position: Point3::new(ii, jj, kk) * 1.5,
+ let color = Color::new(ii / 100.0, jj / 100.0, kk / 100.0 + 0.1, 1.0);
+ instances.push(InstanceData3d {
+ position: Vec3::new(ii, jj, kk) * 1.5,
color,
#[rustfmt::skip]
- deformation: Matrix3::new(
+ deformation: Mat3::from_cols_array(&[
1.0, ii * 0.004, kk * 0.004,
ii * 0.004, 1.0, jj * 0.004,
kk * 0.004, jj * 0.004, 1.0,
- ),
+ ]),
..Default::default()
});
}
@@ -37,11 +37,9 @@ async fn main() {
c.set_instances(&instances);
- window.set_light(Light::StickToCamera);
-
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
- while window.render().await {
- c.prepend_to_local_rotation(&rot);
+ while window.render_3d(&mut scene, &mut camera).await {
+ c.rotate(rot);
}
}
diff --git a/examples/lines.rs b/examples/lines.rs
index 38705cd0c..565bda374 100644
--- a/examples/lines.rs
+++ b/examples/lines.rs
@@ -1,30 +1,21 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Point2, Point3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: lines").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- window.set_light(Light::StickToCamera);
-
- while window.render().await {
- let a = Point3::new(-0.1, -0.1, 0.0);
- let b = Point3::new(0.0, 0.1, 0.0);
- let c = Point3::new(0.1, -0.1, 0.0);
-
- window.draw_line(&a, &b, &Point3::new(1.0, 0.0, 0.0), 2.0);
- window.draw_line(&b, &c, &Point3::new(0.0, 1.0, 0.0), 2.0);
- window.draw_line(&c, &a, &Point3::new(0.0, 0.0, 1.0), 2.0);
+ while window.render_3d(&mut scene, &mut camera).await {
+ let a = Vec3::new(-0.4, -0.4, 0.0);
+ let b = Vec3::new(0.0, 0.4, 0.0);
+ let c = Vec3::new(0.4, -0.4, 0.0);
- window.draw_planar_line(
- &Point2::new(-100.0, -200.0),
- &Point2::new(100.0, -200.0),
- &Point3::new(1.0, 1.0, 1.0),
- 2.0,
- );
+ window.draw_line(a, b, RED, 4.0, true);
+ window.draw_line(b, c, GREEN, 4.0, true);
+ window.draw_line(c, a, BLUE, 4.0, true);
}
}
diff --git a/examples/lines2d.rs b/examples/lines2d.rs
new file mode 100644
index 000000000..c350700eb
--- /dev/null
+++ b/examples/lines2d.rs
@@ -0,0 +1,18 @@
+use kiss3d::prelude::*;
+
+#[kiss3d::main]
+async fn main() {
+ let mut window = Window::new("Kiss3d: 2D lines").await;
+ let mut camera = PanZoomCamera2d::default();
+ let mut scene = SceneNode2d::empty();
+
+ while window.render_2d(&mut scene, &mut camera).await {
+ let a = Vec2::new(-200.0, -200.0);
+ let b = Vec2::new(0.0, 200.0);
+ let c = Vec2::new(200.0, -200.0);
+
+ window.draw_line_2d(a, b, RED, 2.0);
+ window.draw_line_2d(b, c, GREEN, 2.0);
+ window.draw_line_2d(c, a, BLUE, 2.0);
+ }
+}
diff --git a/examples/mouse_events.rs b/examples/mouse_events.rs
index 3a2a8b04c..cafd171c4 100644
--- a/examples/mouse_events.rs
+++ b/examples/mouse_events.rs
@@ -1,57 +1,52 @@
-//! Test of kiss3d's planar camera. Just moves a cross around the screen whenever the mouse is clicked. Shows conversions between co-ordinate systems.
-extern crate kiss3d;
-extern crate nalgebra as na;
+//! Demonstrates mouse events and coordinate system conversions.
+//!
+//! A small cross follows the cursor, and clicking places a persistent cross at that location.
+use kiss3d::prelude::*;
-use kiss3d::event::{Action, WindowEvent};
-use kiss3d::light::Light;
-use kiss3d::planar_camera::*;
-use kiss3d::window::Window;
-
-/// main program
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Mouse events").await;
- let mut camera = kiss3d::planar_camera::PlanarFixedView::new();
- window.set_light(Light::StickToCamera);
- let draw_colour = na::Point3::new(0.5, 1.0, 0.5);
- let mut last_pos = na::Point2::new(0.0f32, 0.0f32);
- let mut sel_pos = na::Point2::new(0.0f32, 0.0f32);
- while window
- .render_with(None, Some(&mut camera), None, None)
- .await
- {
+ let mut camera = PanZoomCamera2d::default();
+ let mut scene = SceneNode2d::empty();
+
+ let cursor_color = RED;
+ let click_color = GREEN;
+
+ let mut cursor_pos = Vec2::new(0.0f32, 0.0f32);
+ let mut click_positions: Vec = Vec::new();
+
+ while window.render_2d(&mut scene, &mut camera).await {
+ let window_size = Vec2::new(window.size()[0] as f32, window.size()[1] as f32);
+
for event in window.events().iter() {
match event.value {
- WindowEvent::FramebufferSize(x, y) => {
- println!("frame buffer size event {}, {}", x, y);
- }
WindowEvent::MouseButton(button, Action::Press, modif) => {
println!("mouse press event on {:?} with {:?}", button, modif);
- let window_size =
- na::Vector2::new(window.size()[0] as f32, window.size()[1] as f32);
- sel_pos = camera.unproject(&last_pos, &window_size);
- println!(
- "conv {:?} to {:?} win size {:?} ",
- last_pos, sel_pos, window_size
- );
- }
- WindowEvent::Key(key, action, modif) => {
- println!("key event {:?} on {:?} with {:?}", key, action, modif);
+ let world_pos = camera.unproject(cursor_pos, window_size);
+ click_positions.push(world_pos);
+ println!("placed cross at {:?} (screen: {:?})", world_pos, cursor_pos);
}
WindowEvent::CursorPos(x, y, _modif) => {
- last_pos = na::Point2::new(x as f32, y as f32);
- }
- WindowEvent::Close => {
- println!("close event");
+ cursor_pos = Vec2::new(x as f32, y as f32);
}
_ => {}
}
}
- const CROSS_SIZE: f32 = 10.0;
- let up = na::Vector2::new(CROSS_SIZE, 0.0);
- window.draw_planar_line(&(sel_pos - up), &(sel_pos + up), &draw_colour, 2.0);
- let right = na::Vector2::new(0.0, CROSS_SIZE);
- window.draw_planar_line(&(sel_pos - right), &(sel_pos + right), &draw_colour, 2.0);
+ // Draw cross following cursor
+ let cursor_world_pos = camera.unproject(cursor_pos, window_size);
+ draw_cross(&mut window, cursor_world_pos, 40.0, cursor_color, 8.0);
+
+ // Draw persistent crosses at click positions
+ for &pos in &click_positions {
+ draw_cross(&mut window, pos, 25.0, click_color, 4.0);
+ }
}
}
+
+fn draw_cross(window: &mut Window, pos: Vec2, size: f32, color: Color, thickness: f32) {
+ let h = Vec2::new(size, 0.0);
+ let v = Vec2::new(0.0, size);
+ window.draw_line_2d(pos - h, pos + h, color, thickness);
+ window.draw_line_2d(pos - v, pos + v, color, thickness);
+}
diff --git a/examples/multi_light.rs b/examples/multi_light.rs
new file mode 100644
index 000000000..b3c8a8562
--- /dev/null
+++ b/examples/multi_light.rs
@@ -0,0 +1,59 @@
+use kiss3d::prelude::*;
+
+#[kiss3d::main]
+async fn main() {
+ env_logger::init();
+ let mut window = Window::new("Kiss3d: multi-light").await;
+ window.set_background_color(Color::new(0.05, 0.05, 0.1, 1.0));
+ window.set_ambient(0.1);
+
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+
+ // Add some geometry
+ scene
+ .add_cube(5.0, 0.2, 5.0)
+ .set_position(Vec3::new(0.0, -1.0, 0.0))
+ .set_color(GRAY);
+
+ let mut cube = scene
+ .add_cube(1.0, 1.0, 1.0)
+ .set_position(Vec3::new(-1.0, 0.0, 0.0))
+ .set_color(WHITE);
+
+ scene
+ .add_sphere(0.5)
+ .set_position(Vec3::new(1.0, 0.0, 0.0))
+ .set_color(WHITE);
+
+ // Add a directional green light from above
+ scene
+ .add_light(
+ Light::directional(Vec3::NEG_Y)
+ .with_color(GREEN)
+ .with_intensity(2.0),
+ )
+ .set_position(Vec3::new(0.0, 2.0, 0.0));
+
+ // Add a red point light using add_light with a custom Light
+ let mut red_light = scene
+ .add_light(Light::point(20.0).with_color(RED).with_intensity(5.0))
+ .set_position(Vec3::new(-3.0, 2.0, 0.0));
+
+ // Add a blue point light
+ let mut blue_light = scene
+ .add_light(Light::point(20.0).with_color(BLUE).with_intensity(5.0))
+ .set_position(Vec3::new(3.0, 2.0, 0.0));
+
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.01);
+ let mut time = 0.0f32;
+
+ while window.render_3d(&mut scene, &mut camera).await {
+ cube.rotate(rot);
+
+ // Animate the lights in a circular pattern
+ time -= 0.02;
+ red_light.set_position(Vec3::new(-3.0 * time.cos(), 3.0 * time.sin(), 2.0));
+ blue_light.set_position(Vec3::new(2.0, 3.0 * time.sin(), -3.0 * time.cos()));
+ }
+}
diff --git a/examples/multi_windows.rs b/examples/multi_windows.rs
index c56bd2e7a..d2eb22b6e 100644
--- a/examples/multi_windows.rs
+++ b/examples/multi_windows.rs
@@ -1,16 +1,40 @@
-extern crate kiss3d;
-
-use kiss3d::window::Window;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
- for _ in 0..5 {
- let mut window = Window::new("Kiss3d window").await;
- window.add_cube(1.0, 1.0, 1.0);
- let mut i = 0;
- while i < 100 && window.render().await {
- i += 1;
+ let mut window1 = Some(Window::new("Kiss3d multi-window 1").await);
+ let mut window2 = Some(Window::new("Kiss3d multi-window 2").await);
+
+ let mut camera1 = OrbitCamera3d::default();
+ let mut scene1 = SceneNode3d::empty();
+ scene1
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 2.0, -2.0));
+
+ let mut camera2 = OrbitCamera3d::default();
+ let mut scene2 = SceneNode3d::empty();
+ scene2
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 2.0, -2.0));
+
+ let mut c1 = scene1.add_cube(1.0, 1.0, 1.0).set_color(RED);
+ let mut c2 = scene2.add_cube(1.0, 1.0, 1.0).set_color(GREEN);
+
+ // Only exit when both windows have been closed.
+ while window1.is_some() || window2.is_some() {
+ if let Some(window) = &mut window1 {
+ if !window.render_3d(&mut scene1, &mut camera1).await {
+ window1 = None;
+ }
+ }
+
+ if let Some(window) = &mut window2 {
+ if !window.render_3d(&mut scene2, &mut camera2).await {
+ window2 = None;
+ }
}
- window.close();
+
+ c1.rotate(Quat::from_axis_angle(Vec3::Y, 0.05));
+ c2.rotate(Quat::from_axis_angle(Vec3::X, -0.05));
}
}
diff --git a/examples/obj.rs b/examples/obj.rs
index 1cfdf2a95..15e0fddc2 100644
--- a/examples/obj.rs
+++ b/examples/obj.rs
@@ -1,39 +1,36 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Translation3, UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
use std::f32;
use std::path::Path;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: obj").await;
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, -0.7), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, -10.0));
// Teapot
let obj_path = Path::new("examples/media/teapot/teapot.obj");
let mtl_path = Path::new("examples/media/teapot");
- let mut teapot = window.add_obj(obj_path, mtl_path, Vector3::new(0.001, 0.001, 0.001));
- teapot.append_translation(&Translation3::new(0.0, -0.05, -0.2));
+ let mut teapot = scene
+ .add_obj(obj_path, mtl_path, Vec3::new(0.001, 0.001, 0.001))
+ .set_position(Vec3::new(0.0, -0.05, -0.2));
// Rust logo
let obj_path = Path::new("examples/media/rust_logo/rust_logo.obj");
let mtl_path = Path::new("examples/media/rust_logo");
- let mut rust = window.add_obj(obj_path, mtl_path, Vector3::new(0.05, 0.05, 0.05));
- rust.prepend_to_local_rotation(&UnitQuaternion::from_axis_angle(
- &Vector3::x_axis(),
- -f32::consts::FRAC_PI_2,
- ));
- rust.set_color(0.0, 0.0, 1.0);
-
- window.set_light(Light::StickToCamera);
+ let mut rust = scene
+ .add_obj(obj_path, mtl_path, Vec3::new(0.05, 0.05, 0.05))
+ .set_rotation(Quat::from_axis_angle(Vec3::X, -f32::consts::FRAC_PI_2))
+ .set_color(BLUE);
- let rot_teapot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
- let rot_rust = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), -0.014);
+ let rot_teapot = Quat::from_axis_angle(Vec3::Y, 0.014);
+ let rot_rust = Quat::from_axis_angle(Vec3::Y, -0.014);
- while window.render().await {
- teapot.prepend_to_local_rotation(&rot_teapot);
- rust.prepend_to_local_rotation(&rot_rust);
+ while window.render_3d(&mut scene, &mut camera).await {
+ teapot.rotate(rot_teapot);
+ rust.prepend_rotation(rot_rust);
}
}
diff --git a/examples/planar_lines.rs b/examples/planar_lines.rs
deleted file mode 100644
index 02ce102bc..000000000
--- a/examples/planar_lines.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Point2, Point3};
-
-#[kiss3d::main]
-async fn main() {
- let mut window = Window::new("Kiss3d: planar lines").await;
-
- window.set_light(Light::StickToCamera);
-
- while window.render().await {
- let a = Point2::new(-200.0, -200.0);
- let b = Point2::new(0.0, 200.0);
- let c = Point2::new(200.0, -200.0);
-
- window.draw_planar_line(&a, &b, &Point3::new(1.0, 0.0, 0.0), 2.0);
- window.draw_planar_line(&b, &c, &Point3::new(0.0, 1.0, 0.0), 2.0);
- window.draw_planar_line(&c, &a, &Point3::new(0.0, 0.0, 1.0), 2.0);
- }
-}
diff --git a/examples/points.rs b/examples/points.rs
index cbd41401b..157665e40 100644
--- a/examples/points.rs
+++ b/examples/points.rs
@@ -1,30 +1,21 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Point2, Point3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: points").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- window.set_light(Light::StickToCamera);
-
- while window.render().await {
- let a = Point3::new(-0.1, -0.1, 0.0);
- let b = Point3::new(0.0, 0.1, 0.0);
- let c = Point3::new(0.1, -0.1, 0.0);
- let red = Point3::new(1.0, 0.0, 0.0);
- let blue = Point3::new(0.0, 1.0, 0.0);
- let green = Point3::new(0.0, 0.0, 1.0);
-
- window.draw_point(&a, &red, 5.0);
- window.draw_point(&b, &blue, 15.0);
- window.draw_point(&c, &green, 25.0);
+ while window.render_3d(&mut scene, &mut camera).await {
+ let a = Vec3::new(-0.1, -0.1, 0.0);
+ let b = Vec3::new(0.0, 0.1, 0.0);
+ let c = Vec3::new(0.1, -0.1, 0.0);
- window.draw_planar_point(&Point2::new(-50.0, -200.0), &red, 5.0);
- window.draw_planar_point(&Point2::new(0.0, -200.0), &blue, 15.0);
- window.draw_planar_point(&Point2::new(50.0, -200.0), &green, 25.0);
+ window.draw_point(a, RED, 5.0);
+ window.draw_point(b, GREEN, 15.0);
+ window.draw_point(c, BLUE, 25.0);
}
}
diff --git a/examples/points2d.rs b/examples/points2d.rs
new file mode 100644
index 000000000..aaab41b79
--- /dev/null
+++ b/examples/points2d.rs
@@ -0,0 +1,18 @@
+use kiss3d::prelude::*;
+
+#[kiss3d::main]
+async fn main() {
+ let mut window = Window::new("Kiss3d: points 2D").await;
+ let mut camera = PanZoomCamera2d::new(Vec2::new(0.0, -200.0), 1.0);
+ let mut scene = SceneNode2d::empty();
+
+ while window.render_2d(&mut scene, &mut camera).await {
+ let a = Vec2::new(-50.0, -200.0);
+ let b = Vec2::new(0.0, -200.0);
+ let c = Vec2::new(50.0, -200.0);
+
+ window.draw_point_2d(a, RED, 5.0);
+ window.draw_point_2d(b, GREEN, 15.0);
+ window.draw_point_2d(c, BLUE, 25.0);
+ }
+}
diff --git a/examples/polyline_strip.rs b/examples/polyline_strip.rs
index 94e3ccfd8..b7547a1b4 100644
--- a/examples/polyline_strip.rs
+++ b/examples/polyline_strip.rs
@@ -3,50 +3,49 @@
//! Demonstrates drawing a polyline around a cube with configurable width,
//! similar to bevy_polyline's linestrip example.
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::renderer::Polyline;
-use kiss3d::window::Window;
-use na::{Isometry3, Point3, UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: polyline strip").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- window.set_light(Light::StickToCamera);
- window.set_background_color(0.2, 0.2, 0.25);
+ window.set_background_color(Color::new(0.2, 0.2, 0.25, 1.0));
// Add a cube to show the polyline around
- let mut cube = window.add_cube(1.0, 1.0, 1.0);
- cube.set_color(0.5, 0.55, 1.0);
+ let mut cube = scene
+ .add_cube(1.0, 1.0, 1.0)
+ .set_color(Color::new(0.5, 0.55, 1.0, 1.0));
// Create base polyline once (vertices in local space)
- let mut polyline = Polyline::new(vec![
- Point3::new(-0.5, -0.5, -0.5),
- Point3::new(0.5, -0.5, -0.5),
- Point3::new(0.5, 0.5, -0.5),
- Point3::new(-0.5, 0.5, -0.5),
- Point3::new(-0.5, 0.5, 0.5),
- Point3::new(0.5, 0.5, 0.5),
- Point3::new(0.5, -0.5, 0.5),
- Point3::new(-0.5, -0.5, 0.5),
+ let mut polyline = Polyline3d::new(vec![
+ Vec3::new(-0.5, -0.5, -0.5),
+ Vec3::new(0.5, -0.5, -0.5),
+ Vec3::new(0.5, 0.5, -0.5),
+ Vec3::new(-0.5, 0.5, -0.5),
+ Vec3::new(-0.5, 0.5, 0.5),
+ Vec3::new(0.5, 0.5, 0.5),
+ Vec3::new(0.5, -0.5, 0.5),
+ Vec3::new(-0.5, -0.5, 0.5),
])
- .with_color(1.0, 0.0, 0.0)
+ .with_color(RED)
.with_width(5.0)
.with_depth_bias(0.0002); // Slight depth bias to render in front of the cube
let mut angle = 0.0f32;
- while window.render().await {
+ while window.render_3d(&mut scene, &mut camera).await {
// Rotate the cube
angle += 0.01;
- let rotation = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), angle);
- cube.set_local_rotation(rotation);
+ let rotation = Quat::from_axis_angle(Vec3::Y, angle);
+ cube.set_rotation(rotation);
// Apply same rotation to polyline via transform
- polyline.transform = Isometry3::from_parts(Default::default(), rotation);
+ polyline.transform.rotation = rotation;
window.draw_polyline(&polyline);
}
diff --git a/examples/polylines.rs b/examples/polylines.rs
index e9be84d4f..da247d48a 100644
--- a/examples/polylines.rs
+++ b/examples/polylines.rs
@@ -6,14 +6,7 @@
//!
//! Use the UI panel to toggle perspective mode for the polylines.
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::camera::ArcBall;
-use kiss3d::light::Light;
-use kiss3d::renderer::Polyline;
-use kiss3d::window::Window;
-use na::{Point3, Vector3};
+use kiss3d::prelude::*;
// Simulation parameters
const NUM_BODIES: usize = 128;
@@ -25,14 +18,14 @@ const SUBSTEPS: usize = 2; // Physics substeps per frame
/// A body in the simulation
struct Body {
- position: Vector3,
- velocity: Vector3,
+ position: Vec3,
+ velocity: Vec3,
mass: f32,
}
/// Trail storing position history as a ring buffer
struct Trail {
- positions: Vec>,
+ positions: Vec,
head: usize,
len: usize,
}
@@ -40,13 +33,13 @@ struct Trail {
impl Trail {
fn new() -> Self {
Self {
- positions: vec![Point3::origin(); TRAIL_LENGTH],
+ positions: vec![Vec3::ZERO; TRAIL_LENGTH],
head: 0,
len: 0,
}
}
- fn push(&mut self, pos: Point3) {
+ fn push(&mut self, pos: Vec3) {
self.positions[self.head] = pos;
self.head = (self.head + 1) % TRAIL_LENGTH;
if self.len < TRAIL_LENGTH {
@@ -55,7 +48,7 @@ impl Trail {
}
/// Copy trail points into destination vector (avoids allocation)
- fn copy_to(&self, dest: &mut Vec>) {
+ fn copy_to(&self, dest: &mut Vec) {
dest.clear();
if self.len == 0 {
return;
@@ -76,7 +69,7 @@ impl Trail {
}
/// HSL to RGB conversion
-fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Point3 {
+fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Vec3 {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let h_prime = h / 60.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
@@ -96,7 +89,7 @@ fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Point3 {
(c, 0.0, x)
};
- Point3::new(r + m, g + m, b + m)
+ Vec3::new(r + m, g + m, b + m)
}
/// Simple pseudo-random number generator (xorshift)
@@ -126,8 +119,8 @@ fn init_bodies() -> Vec {
// Add a central massive body
bodies.push(Body {
- position: Vector3::zeros(),
- velocity: Vector3::zeros(),
+ position: Vec3::ZERO,
+ velocity: Vec3::ZERO,
mass: 100.0,
});
@@ -137,11 +130,11 @@ fn init_bodies() -> Vec {
let radius = 1.0 + rng.next_f32() * 8.0;
let height = (rng.next_f32() - 0.5) * 1.0;
- let position = Vector3::new(radius * angle.cos(), height, radius * angle.sin());
+ let position = Vec3::new(radius * angle.cos(), height, radius * angle.sin());
// Calculate orbital velocity for roughly circular orbit
let orbital_speed = (G * 100.0 / radius).sqrt() * (0.9 + rng.next_f32() * 0.2);
- let tangent = Vector3::new(-angle.sin(), 0.0, angle.cos());
+ let tangent = Vec3::new(-angle.sin(), 0.0, angle.cos());
let velocity = tangent * orbital_speed;
let mass = 0.01 + rng.next_f32() * 0.1;
@@ -157,14 +150,14 @@ fn init_bodies() -> Vec {
}
/// Initialize polylines with colors and widths for each body
-fn init_polylines() -> Vec {
+fn init_polylines() -> Vec {
let mut rng = Rng::new(42);
let mut polylines = Vec::with_capacity(NUM_BODIES);
// Central body (yellow star)
polylines.push(
- Polyline::new(Vec::with_capacity(TRAIL_LENGTH))
- .with_color(1.0, 1.0, 0.5)
+ Polyline3d::new(Vec::with_capacity(TRAIL_LENGTH))
+ .with_color(Color::new(1.0, 1.0, 0.5, 1.0))
.with_width(3.0),
);
@@ -184,8 +177,8 @@ fn init_polylines() -> Vec {
let line_width = 0.5 + rng.next_f32() * 9.5;
polylines.push(
- Polyline::new(Vec::with_capacity(TRAIL_LENGTH))
- .with_color(color.x, color.y, color.z)
+ Polyline3d::new(Vec::with_capacity(TRAIL_LENGTH))
+ .with_color(Color::new(color.x, color.y, color.z, 1.0))
.with_width(line_width),
);
}
@@ -194,8 +187,8 @@ fn init_polylines() -> Vec {
}
/// Compute gravitational acceleration on body i from all other bodies
-fn compute_acceleration(bodies: &[Body], i: usize) -> Vector3 {
- let mut acceleration = Vector3::zeros();
+fn compute_acceleration(bodies: &[Body], i: usize) -> Vec3 {
+ let mut acceleration = Vec3::ZERO;
let pos_i = bodies[i].position;
for (j, body_j) in bodies.iter().enumerate() {
@@ -204,7 +197,7 @@ fn compute_acceleration(bodies: &[Body], i: usize) -> Vector3 {
}
let offset = body_j.position - pos_i;
- let dist_sq = offset.norm_squared() + EPSILON * EPSILON;
+ let dist_sq = offset.length_squared() + EPSILON * EPSILON;
let dist = dist_sq.sqrt();
let force_mag = G * body_j.mass / dist_sq;
acceleration += offset / dist * force_mag;
@@ -218,8 +211,7 @@ fn physics_step(bodies: &mut [Body], dt: f32) {
let n = bodies.len();
// Store accelerations
- let accelerations: Vec> =
- (0..n).map(|i| compute_acceleration(bodies, i)).collect();
+ let accelerations: Vec = (0..n).map(|i| compute_acceleration(bodies, i)).collect();
// Update positions
for (i, body) in bodies.iter_mut().enumerate() {
@@ -227,8 +219,7 @@ fn physics_step(bodies: &mut [Body], dt: f32) {
}
// Compute new accelerations
- let new_accelerations: Vec> =
- (0..n).map(|i| compute_acceleration(bodies, i)).collect();
+ let new_accelerations: Vec = (0..n).map(|i| compute_acceleration(bodies, i)).collect();
// Update velocities
for (i, body) in bodies.iter_mut().enumerate() {
@@ -239,14 +230,17 @@ fn physics_step(bodies: &mut [Body], dt: f32) {
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: N-Body Polyline Simulation").await;
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- window.set_light(Light::StickToCamera);
- window.set_background_color(0.02, 0.02, 0.05);
+ window.set_background_color(Color::new(0.02, 0.02, 0.05, 1.0));
// Set up camera looking at the simulation (user can control it)
- let eye = Point3::new(0.0, 15.0, 25.0);
- let at = Point3::origin();
- let mut camera = ArcBall::new(eye, at);
+ let eye = Vec3::new(0.0, 15.0, 25.0);
+ let at = Vec3::ZERO;
+ let mut camera = OrbitCamera3d::new(eye, at);
// Initialize simulation
let mut bodies = init_bodies();
@@ -256,10 +250,10 @@ async fn main() {
let mut polylines = init_polylines();
// UI state
- #[allow(unused_mut)] // Silence warning appearing when the "egui" feature isn’t.
+ #[allow(unused_mut)] // Silence warning appearing when the "egui" feature isn't.
let mut perspective_mode = false;
- while window.render_with_camera(&mut camera).await {
+ while window.render_3d(&mut scene, &mut camera).await {
// Draw UI
#[cfg(feature = "egui")]
window.draw_ui(|ctx| {
@@ -279,7 +273,7 @@ async fn main() {
// Update trails with current positions
for (i, body) in bodies.iter().enumerate() {
- trails[i].push(Point3::from(body.position));
+ trails[i].push(body.position);
}
// Draw trails as polylines (reusing pre-allocated polylines)
diff --git a/examples/planar_polylines.rs b/examples/polylines2d.rs
similarity index 75%
rename from examples/planar_polylines.rs
rename to examples/polylines2d.rs
index d2ed306f3..b6a9ce0ed 100644
--- a/examples/planar_polylines.rs
+++ b/examples/polylines2d.rs
@@ -7,14 +7,7 @@
//! - Right mouse button + drag: Pan the camera
//! - Scroll wheel: Zoom in/out
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::planar_camera::Sidescroll;
-use kiss3d::planar_polyline_renderer::PlanarPolyline;
-use kiss3d::window::Window;
-use na::{Point2, Point3, Vector2};
+use kiss3d::prelude::*;
// Simulation parameters
const NUM_BODIES: usize = 64;
@@ -26,14 +19,14 @@ const SUBSTEPS: usize = 4; // Physics substeps per frame
/// A body in the 2D simulation
struct Body {
- position: Vector2,
- velocity: Vector2,
+ position: Vec2,
+ velocity: Vec2,
mass: f32,
}
/// Trail storing position history as a ring buffer
struct Trail {
- positions: Vec>,
+ positions: Vec,
head: usize,
len: usize,
}
@@ -41,13 +34,13 @@ struct Trail {
impl Trail {
fn new() -> Self {
Self {
- positions: vec![Point2::origin(); TRAIL_LENGTH],
+ positions: vec![Vec2::ZERO; TRAIL_LENGTH],
head: 0,
len: 0,
}
}
- fn push(&mut self, pos: Point2) {
+ fn push(&mut self, pos: Vec2) {
self.positions[self.head] = pos;
self.head = (self.head + 1) % TRAIL_LENGTH;
if self.len < TRAIL_LENGTH {
@@ -56,7 +49,7 @@ impl Trail {
}
/// Copy trail points into destination vector (avoids allocation)
- fn copy_to(&self, dest: &mut Vec>) {
+ fn copy_to(&self, dest: &mut Vec) {
dest.clear();
if self.len == 0 {
return;
@@ -77,7 +70,7 @@ impl Trail {
}
/// HSL to RGB conversion
-fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Point3 {
+fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Vec3 {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let h_prime = h / 60.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
@@ -97,7 +90,7 @@ fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Point3 {
(c, 0.0, x)
};
- Point3::new(r + m, g + m, b + m)
+ Vec3::new(r + m, g + m, b + m)
}
/// Simple pseudo-random number generator (xorshift)
@@ -127,8 +120,8 @@ fn init_bodies() -> Vec {
// Add a central massive body at origin
bodies.push(Body {
- position: Vector2::zeros(),
- velocity: Vector2::zeros(),
+ position: Vec2::ZERO,
+ velocity: Vec2::ZERO,
mass: 10000.0,
});
@@ -137,11 +130,11 @@ fn init_bodies() -> Vec {
let angle = rng.next_f32() * TAU;
let radius = 50.0 + rng.next_f32() * 250.0;
- let position = Vector2::new(radius * angle.cos(), radius * angle.sin());
+ let position = Vec2::new(radius * angle.cos(), radius * angle.sin());
// Calculate orbital velocity for roughly circular orbit
let orbital_speed = (G * 10000.0 / radius).sqrt() * (0.8 + rng.next_f32() * 0.4);
- let tangent = Vector2::new(-angle.sin(), angle.cos());
+ let tangent = Vec2::new(-angle.sin(), angle.cos());
let velocity = tangent * orbital_speed;
let mass = 1.0 + rng.next_f32() * 10.0;
@@ -157,14 +150,14 @@ fn init_bodies() -> Vec {
}
/// Initialize polylines with colors and widths for each body
-fn init_polylines() -> Vec {
+fn init_polylines() -> Vec {
let mut rng = Rng::new(42);
let mut polylines = Vec::with_capacity(NUM_BODIES);
// Central body (yellow)
polylines.push(
- PlanarPolyline::new(Vec::with_capacity(TRAIL_LENGTH))
- .with_color(1.0, 1.0, 0.5)
+ Polyline2d::new(Vec::with_capacity(TRAIL_LENGTH))
+ .with_color(Color::new(1.0, 1.0, 0.5, 1.0))
.with_width(4.0),
);
@@ -183,8 +176,8 @@ fn init_polylines() -> Vec {
let line_width = 1.0 + rng.next_f32() * 5.0;
polylines.push(
- PlanarPolyline::new(Vec::with_capacity(TRAIL_LENGTH))
- .with_color(color.x, color.y, color.z)
+ Polyline2d::new(Vec::with_capacity(TRAIL_LENGTH))
+ .with_color(Color::new(color.x, color.y, color.z, 1.0))
.with_width(line_width),
);
}
@@ -193,8 +186,8 @@ fn init_polylines() -> Vec {
}
/// Compute gravitational acceleration on body i from all other bodies
-fn compute_acceleration(bodies: &[Body], i: usize) -> Vector2 {
- let mut acceleration = Vector2::zeros();
+fn compute_acceleration(bodies: &[Body], i: usize) -> Vec2 {
+ let mut acceleration = Vec2::ZERO;
let pos_i = bodies[i].position;
for (j, body_j) in bodies.iter().enumerate() {
@@ -203,7 +196,7 @@ fn compute_acceleration(bodies: &[Body], i: usize) -> Vector2 {
}
let offset = body_j.position - pos_i;
- let dist_sq = offset.norm_squared() + EPSILON * EPSILON;
+ let dist_sq = offset.length_squared() + EPSILON * EPSILON;
let dist = dist_sq.sqrt();
let force_mag = G * body_j.mass / dist_sq;
acceleration += offset / dist * force_mag;
@@ -217,8 +210,7 @@ fn physics_step(bodies: &mut [Body], dt: f32) {
let n = bodies.len();
// Store accelerations
- let accelerations: Vec> =
- (0..n).map(|i| compute_acceleration(bodies, i)).collect();
+ let accelerations: Vec = (0..n).map(|i| compute_acceleration(bodies, i)).collect();
// Update positions
for (i, body) in bodies.iter_mut().enumerate() {
@@ -226,8 +218,7 @@ fn physics_step(bodies: &mut [Body], dt: f32) {
}
// Compute new accelerations
- let new_accelerations: Vec> =
- (0..n).map(|i| compute_acceleration(bodies, i)).collect();
+ let new_accelerations: Vec = (0..n).map(|i| compute_acceleration(bodies, i)).collect();
// Update velocities
for (i, body) in bodies.iter_mut().enumerate() {
@@ -239,13 +230,10 @@ fn physics_step(bodies: &mut [Body], dt: f32) {
async fn main() {
let mut window = Window::new("Kiss3d: 2D N-Body Polyline Simulation").await;
- window.set_light(Light::StickToCamera);
- window.set_background_color(0.02, 0.02, 0.05);
+ window.set_background_color(Color::new(0.02, 0.02, 0.05, 1.0));
- // Create a Sidescroll camera looking at the center of the orbits (origin)
- // with a zoom level that shows the entire simulation
- let mut camera = Sidescroll::new();
- camera.look_at(Point2::origin(), 1.0); // Look at origin with zoom 1.0
+ let mut camera = PanZoomCamera2d::default();
+ let mut scene = SceneNode2d::empty();
// Initialize simulation centered at origin
let mut bodies = init_bodies();
@@ -254,7 +242,7 @@ async fn main() {
// Create polylines once with pre-allocated capacity
let mut polylines = init_polylines();
- while window.render_with_planar_camera(&mut camera).await {
+ while window.render_2d(&mut scene, &mut camera).await {
// Physics simulation with substeps
let sub_dt = DT / SUBSTEPS as f32;
for _ in 0..SUBSTEPS {
@@ -263,7 +251,7 @@ async fn main() {
// Update trails with current positions
for (i, body) in bodies.iter().enumerate() {
- trails[i].push(Point2::from(body.position));
+ trails[i].push(body.position);
}
// Draw trails as polylines (reusing pre-allocated polylines)
@@ -272,7 +260,7 @@ async fn main() {
trail.copy_to(&mut polylines[i].vertices);
if polylines[i].vertices.len() >= 2 {
- window.draw_planar_polyline(&polylines[i]);
+ window.draw_polyline_2d(&polylines[i]);
}
}
}
diff --git a/examples/post_processing.rs b/examples/post_processing.rs
index 5871a7708..d28339867 100644
--- a/examples/post_processing.rs
+++ b/examples/post_processing.rs
@@ -1,45 +1,44 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-extern crate rand;
-
-use kiss3d::light::Light;
#[cfg(not(target_arch = "wasm32"))]
use kiss3d::post_processing::SobelEdgeHighlight;
use kiss3d::post_processing::{Grayscales, Waves};
-use kiss3d::window::Window;
-use na::Translation3;
+use kiss3d::prelude::*;
use rand::random;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: post_processing").await;
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- let mut c = window.add_cube(1.0, 1.0, 1.0);
- let mut b = window.add_sphere(0.5);
- let mut p = window.add_cone(0.5, 1.0);
- let mut y = window.add_cylinder(0.5, 1.0);
- let mut a = window.add_capsule(0.5, 1.0);
-
- c.append_translation(&Translation3::new(2.0, 0.0, 0.0));
- b.append_translation(&Translation3::new(4.0, 0.0, 0.0));
- p.append_translation(&Translation3::new(-2.0, 0.0, 0.0));
- y.append_translation(&Translation3::new(-4.0, 0.0, 0.0));
- a.append_translation(&Translation3::new(0.0, 0.0, 0.0));
-
- c.set_color(random(), random(), random());
- b.set_color(random(), random(), random());
- p.set_color(random(), random(), random());
- y.set_color(random(), random(), random());
- a.set_color(random(), random(), random());
+ scene
+ .add_cube(1.0, 1.0, 1.0)
+ .translate(Vec3::new(2.0, 0.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ scene
+ .add_sphere(0.5)
+ .translate(Vec3::new(4.0, 0.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ scene
+ .add_cone(0.5, 1.0)
+ .translate(Vec3::new(-2.0, 0.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ scene
+ .add_cylinder(0.5, 1.0)
+ .translate(Vec3::new(-4.0, 0.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ scene
+ .add_capsule(0.5, 1.0)
+ .set_color(Color::new(random(), random(), random(), 1.0));
#[cfg(not(target_arch = "wasm32"))]
let mut sobel = SobelEdgeHighlight::new(4.0);
let mut waves = Waves::new();
let mut grays = Grayscales::new();
- window.set_background_color(1.0, 1.0, 1.0);
- window.set_light(Light::StickToCamera);
- window.set_framerate_limit(Some(60));
+ window.set_background_color(WHITE);
let mut time = 0usize;
let mut counter = 0usize;
@@ -53,11 +52,48 @@ async fn main() {
time = time + 1;
let _ = match counter {
- 0 => window.render().await,
- 1 => window.render_with_effect(&mut grays).await,
- 2 => window.render_with_effect(&mut waves).await,
+ 0 => {
+ window
+ .render(Some(&mut scene), None, Some(&mut camera), None, None, None)
+ .await
+ }
+ 1 => {
+ window
+ .render(
+ Some(&mut scene),
+ None,
+ Some(&mut camera),
+ None,
+ None,
+ Some(&mut grays),
+ )
+ .await
+ }
+ 2 => {
+ window
+ .render(
+ Some(&mut scene),
+ None,
+ Some(&mut camera),
+ None,
+ None,
+ Some(&mut waves),
+ )
+ .await
+ }
#[cfg(not(target_arch = "wasm32"))]
- 3 => window.render_with_effect(&mut sobel).await,
+ 3 => {
+ window
+ .render(
+ Some(&mut scene),
+ None,
+ Some(&mut camera),
+ None,
+ None,
+ Some(&mut sobel),
+ )
+ .await
+ }
_ => true,
};
}
diff --git a/examples/primitives.rs b/examples/primitives.rs
index a2680ebac..4bd4c8ac6 100644
--- a/examples/primitives.rs
+++ b/examples/primitives.rs
@@ -1,43 +1,43 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-extern crate rand;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Translation3, UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
use rand::random;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: primitives").await;
-
- let mut c = window.add_cube(1.0, 1.0, 1.0);
- let mut s = window.add_sphere(0.5);
- let mut p = window.add_cone(0.5, 1.0);
- let mut y = window.add_cylinder(0.5, 1.0);
- let mut a = window.add_capsule(0.5, 1.0);
-
- c.set_color(random(), random(), random());
- s.set_color(random(), random(), random());
- p.set_color(random(), random(), random());
- y.set_color(random(), random(), random());
- a.set_color(random(), random(), random());
-
- c.append_translation(&Translation3::new(2.0, 0.0, 0.0));
- s.append_translation(&Translation3::new(4.0, 0.0, 0.0));
- p.append_translation(&Translation3::new(-2.0, 0.0, 0.0));
- y.append_translation(&Translation3::new(-4.0, 0.0, 0.0));
- a.append_translation(&Translation3::new(0.0, 0.0, 0.0));
-
- window.set_light(Light::StickToCamera);
-
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
-
- while window.render().await {
- c.append_rotation_wrt_center(&rot);
- s.append_rotation_wrt_center(&rot);
- p.append_rotation_wrt_center(&rot);
- y.append_rotation_wrt_center(&rot);
- a.append_rotation_wrt_center(&rot);
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
+
+ let mut c = scene
+ .add_cube(1.0, 1.0, 1.0)
+ .set_color(Color::new(random(), random(), random(), 1.0))
+ .set_position(Vec3::new(2.0, 0.0, 0.0));
+ let mut s = scene
+ .add_sphere(0.5)
+ .set_color(Color::new(random(), random(), random(), 1.0))
+ .set_position(Vec3::new(4.0, 0.0, 0.0));
+ let mut p = scene
+ .add_cone(0.5, 1.0)
+ .set_color(Color::new(random(), random(), random(), 1.0))
+ .set_position(Vec3::new(-2.0, 0.0, 0.0));
+ let mut y = scene
+ .add_cylinder(0.5, 1.0)
+ .set_color(Color::new(random(), random(), random(), 1.0))
+ .set_position(Vec3::new(-4.0, 0.0, 0.0));
+ let mut a =
+ scene
+ .add_capsule(0.5, 1.0)
+ .set_color(Color::new(random(), random(), random(), 1.0));
+
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
+
+ while window.render_3d(&mut scene, &mut camera).await {
+ c.rotate(rot);
+ s.rotate(rot);
+ p.rotate(rot);
+ y.rotate(rot);
+ a.rotate(rot);
}
}
diff --git a/examples/primitives2d.rs b/examples/primitives2d.rs
index 6c7878ecc..a362409f1 100644
--- a/examples/primitives2d.rs
+++ b/examples/primitives2d.rs
@@ -1,28 +1,28 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::window::Window;
-use na::{Point3, Translation2, UnitComplex};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: rectangle").await;
- let mut rect = window.add_rectangle(50.0, 150.0);
- let mut circ = window.add_circle(50.0);
- circ.append_translation(&Translation2::new(200.0, 0.0));
+ let mut camera = PanZoomCamera2d::new(Vec2::ZERO, 2.0);
+ let mut scene = SceneNode2d::empty();
- rect.set_color(0.0, 1.0, 0.0);
- rect.set_lines_width(10.0, false);
- rect.set_lines_color(Some(Point3::new(1.0, 1.0, 1.0)));
- circ.set_color(0.0, 0.0, 1.0);
- circ.set_lines_width(5.0, false);
- circ.set_lines_color(Some(Point3::new(1.0, 0.0, 1.0)));
+ let mut rect = scene
+ .add_rectangle(50.0, 150.0)
+ .set_color(GREEN)
+ .set_lines_width(10.0, false)
+ .set_lines_color(Some(WHITE));
+ let mut circ = scene
+ .add_circle(50.0)
+ .translate(Vec2::new(200.0, 0.0))
+ .set_color(BLUE)
+ .set_lines_width(5.0, false)
+ .set_lines_color(Some(MAGENTA));
- let rot_rect = UnitComplex::new(0.014);
- let rot_circ = UnitComplex::new(-0.014);
+ let rot_rect = 0.014;
+ let rot_circ = -0.014;
- while window.render().await {
- rect.prepend_to_local_rotation(&rot_rect);
- circ.append_rotation(&rot_circ);
+ while window.render_2d(&mut scene, &mut camera).await {
+ rect.append_rotation(rot_rect);
+ circ.append_rotation(rot_circ);
}
}
diff --git a/examples/primitives_scale.rs b/examples/primitives_scale.rs
index f80857f65..41f122d4b 100644
--- a/examples/primitives_scale.rs
+++ b/examples/primitives_scale.rs
@@ -1,48 +1,48 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-extern crate rand;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Translation3, UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
use rand::random;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: primitives_scale").await;
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, 15.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
+ let mut primitives = scene.add_group();
// NOTE: scaling is not possible.
for i in 0usize..11 {
- let dim: f32 = random::() / 2.0;
+ let dim: f32 = (0.4 + random::()) / 2.0;
let dim2 = dim / 2.0;
let offset = i as f32 * 1.0 - 5.0;
- let mut cu = window.add_cube(dim2, dim2, dim2);
- let mut sp = window.add_sphere(dim2);
- let mut co = window.add_cone(dim2, dim);
- let mut cy = window.add_cylinder(dim2, dim);
- let mut ca = window.add_capsule(dim2, dim);
-
- cu.append_translation(&Translation3::new(offset, 1.0, 0.0));
- sp.append_translation(&Translation3::new(offset, -1.0, 0.0));
- co.append_translation(&Translation3::new(offset, 2.0, 0.0));
- cy.append_translation(&Translation3::new(offset, -2.0, 0.0));
- ca.append_translation(&Translation3::new(offset, 0.0, 0.0));
-
- cu.set_color(random(), random(), random());
- sp.set_color(random(), random(), random());
- co.set_color(random(), random(), random());
- cy.set_color(random(), random(), random());
- ca.set_color(random(), random(), random());
+ primitives
+ .add_cube(dim2, dim2, dim2)
+ .translate(Vec3::new(offset, 1.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ primitives
+ .add_sphere(dim2)
+ .translate(Vec3::new(offset, -1.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ primitives
+ .add_cone(dim2, dim)
+ .translate(Vec3::new(offset, 2.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ primitives
+ .add_cylinder(dim2, dim)
+ .translate(Vec3::new(offset, -2.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
+ primitives
+ .add_capsule(dim2, dim)
+ .translate(Vec3::new(offset, 0.0, 0.0))
+ .set_color(Color::new(random(), random(), random(), 1.0));
}
- window.set_light(Light::StickToCamera);
-
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
- while window.render().await {
- // XXX: applying this to each object individually became complicated…
- window.scene_mut().append_rotation_wrt_center(&rot);
+ while window.render_3d(&mut scene, &mut camera).await {
+ primitives.rotate(rot);
}
}
diff --git a/examples/procedural.rs b/examples/procedural.rs
index ef444e29a..a2c42449d 100644
--- a/examples/procedural.rs
+++ b/examples/procedural.rs
@@ -1,26 +1,26 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-extern crate rand;
-
-use kiss3d::light::Light;
+use kiss3d::prelude::*;
use kiss3d::procedural::path::StrokePattern;
use kiss3d::procedural::path::{ArrowheadCap, PolylinePath, PolylinePattern};
use kiss3d::procedural::RenderMesh;
-use kiss3d::window::Window;
-use na::{Point2, Point3, Translation3, Vector2, Vector3};
use parry3d::shape::TriMesh;
use std::path::Path;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: procedural").await;
+ let mut camera = OrbitCamera3d::new(Vec3::new(2.0, 1.0, 12.0), Vec3::new(2.0, 1.0, 2.0));
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
/*
* A cube.
*/
- let cube = kiss3d::procedural::cuboid(&Vector3::new(0.7f32, 0.2, 0.4));
- let mut c = window.add_render_mesh(cube, Vector3::from_element(1.0));
- c.append_translation(&Translation3::new(1.0, 0.0, 0.0));
+ let cube = kiss3d::procedural::cuboid(Vec3::new(0.7f32, 0.2, 0.4));
+ let mut c = scene
+ .add_render_mesh(cube, Vec3::splat(1.0))
+ .set_position(Vec3::new(1.0, 0.0, 0.0));
#[cfg(not(target_arch = "wasm32"))]
c.set_texture_from_file(Path::new("./examples/media/kitten.png"), "kitten");
@@ -28,7 +28,7 @@ async fn main() {
* A sphere.
*/
let sphere = kiss3d::procedural::sphere(0.4f32, 20, 20, true);
- let mut s = window.add_render_mesh(sphere, Vector3::from_element(1.0));
+ let mut s = scene.add_render_mesh(sphere, Vec3::splat(1.0));
#[cfg(not(target_arch = "wasm32"))]
s.set_texture_with_name("kitten");
@@ -36,94 +36,98 @@ async fn main() {
* A capsule.
*/
let capsule = kiss3d::procedural::capsule(0.4f32, 0.4f32, 20, 20);
- let mut c = window.add_render_mesh(capsule, Vector3::from_element(1.0));
- c.append_translation(&Translation3::new(-1.0, 0.0, 0.0));
- c.set_color(0.0, 0.0, 1.0);
+ scene
+ .add_render_mesh(capsule, Vec3::splat(1.0))
+ .set_position(Vec3::new(-1.0, 0.0, 0.0))
+ .set_color(BLUE);
// /*
// * Triangulation.
// */
// let to_triangulate = ncollide_transformation::triangulate(&[
- // Point3::new(5.0f32, 0.0, 0.0),
- // Point3::new(6.1, 0.0, 0.5),
- // Point3::new(7.4, 0.0, 0.5),
- // Point3::new(8.2, 0.0, 0.0),
- // Point3::new(5.1f32, 1.0, 0.0),
- // Point3::new(6.2, 1.5, 0.5),
- // Point3::new(7.2, 1.0, 0.5),
- // Point3::new(8.0, 1.3, 0.0),
- // Point3::new(5.3f32, 2.0, 0.0),
- // Point3::new(6.1, 2.2, 0.5),
- // Point3::new(7.3, 2.0, 0.5),
- // Point3::new(8.2, 2.4, 0.0),
- // Point3::new(5.2f32, 3.0, 0.0),
- // Point3::new(6.1, 2.9, 0.5),
- // Point3::new(7.4, 3.0, 0.5),
- // Point3::new(8.0, 3.1, 0.0),
+ // Vec3::new(5.0f32, 0.0, 0.0),
+ // Vec3::new(6.1, 0.0, 0.5),
+ // Vec3::new(7.4, 0.0, 0.5),
+ // Vec3::new(8.2, 0.0, 0.0),
+ // Vec3::new(5.1f32, 1.0, 0.0),
+ // Vec3::new(6.2, 1.5, 0.5),
+ // Vec3::new(7.2, 1.0, 0.5),
+ // Vec3::new(8.0, 1.3, 0.0),
+ // Vec3::new(5.3f32, 2.0, 0.0),
+ // Vec3::new(6.1, 2.2, 0.5),
+ // Vec3::new(7.3, 2.0, 0.5),
+ // Vec3::new(8.2, 2.4, 0.0),
+ // Vec3::new(5.2f32, 3.0, 0.0),
+ // Vec3::new(6.1, 2.9, 0.5),
+ // Vec3::new(7.4, 3.0, 0.5),
+ // Vec3::new(8.0, 3.1, 0.0),
// ]);
- // let mut t = window.add_trimesh(to_triangulate, Vector3::from_element(1.0));
+ // let mut t = scene.add_trimesh(to_triangulate, Vec3::splat(1.0));
// t.set_surface_rendering_activation(false);
// t.set_lines_width(2.0);
- // t.set_color(0.0, 1.0, 0.0);
+ // t.set_color(GREEN);
/*
* A (non-rational) bicubic Bézier surface.
*/
let control_points = [
- Point3::new(0.0f32, 0.0, 0.0),
- Point3::new(1.0, 0.0, 2.0),
- Point3::new(2.0, 0.0, 2.0),
- Point3::new(3.0, 0.0, 0.0),
- Point3::new(0.0f32, 1.0, 2.0),
- Point3::new(1.0, 1.0, 3.0),
- Point3::new(2.0, 1.0, 3.0),
- Point3::new(3.0, 1.0, 2.0),
- Point3::new(0.0f32, 2.0, 2.0),
- Point3::new(1.0, 2.0, 3.0),
- Point3::new(2.0, 2.0, 3.0),
- Point3::new(3.0, 2.0, 2.0),
- Point3::new(0.0f32, 3.0, 0.0),
- Point3::new(1.0, 3.0, 2.0),
- Point3::new(2.0, 3.0, 2.0),
- Point3::new(3.0, 3.0, 0.0),
+ Vec3::ZERO,
+ Vec3::new(1.0, 0.0, 2.0),
+ Vec3::new(2.0, 0.0, 2.0),
+ Vec3::new(3.0, 0.0, 0.0),
+ Vec3::new(0.0f32, 1.0, 2.0),
+ Vec3::new(1.0, 1.0, 3.0),
+ Vec3::new(2.0, 1.0, 3.0),
+ Vec3::new(3.0, 1.0, 2.0),
+ Vec3::new(0.0f32, 2.0, 2.0),
+ Vec3::new(1.0, 2.0, 3.0),
+ Vec3::new(2.0, 2.0, 3.0),
+ Vec3::new(3.0, 2.0, 2.0),
+ Vec3::new(0.0f32, 3.0, 0.0),
+ Vec3::new(1.0, 3.0, 2.0),
+ Vec3::new(2.0, 3.0, 2.0),
+ Vec3::new(3.0, 3.0, 0.0),
];
let bezier = kiss3d::procedural::bezier_surface(&control_points, 4, 4, 100, 100);
- let mut b = window.add_render_mesh(bezier, Vector3::from_element(1.0));
- b.append_translation(&Translation3::new(-1.5, -1.5, 0.0));
- b.enable_backface_culling(false);
+ scene
+ .add_render_mesh(bezier, Vec3::splat(1.0))
+ .set_position(Vec3::new(-1.5, -1.5, 0.0))
+ .enable_backface_culling(false);
// XXX: replace by an `add_mesh`.
- let mut control_polyhedra_gfx = window.add_quad_with_vertices(&control_points, 4, 4);
- control_polyhedra_gfx.append_translation(&Translation3::new(-1.5, -1.5, 0.0));
- control_polyhedra_gfx.set_color(0.0, 0.0, 1.0);
- control_polyhedra_gfx.set_surface_rendering_activation(false);
- control_polyhedra_gfx.set_lines_width(2.0, false);
-
- let mut control_points_gfx = window.add_mesh(
- control_polyhedra_gfx.data().get_object().mesh().clone(),
- Vector3::from_element(1.0),
- );
- control_points_gfx.append_translation(&Translation3::new(-1.5, -1.5, 0.0));
- control_points_gfx.set_color(1.0, 0.0, 0.0);
- control_points_gfx.set_surface_rendering_activation(false);
- control_points_gfx.set_points_size(10.0, false);
+ let control_polyhedra_gfx = scene
+ .add_quad_with_vertices(&control_points, 4, 4)
+ .set_position(Vec3::new(-1.5, -1.5, 0.0))
+ .set_color(BLUE)
+ .set_surface_rendering_activation(false)
+ .set_lines_width(2.0, false);
+
+ scene
+ .add_mesh(
+ control_polyhedra_gfx.data().get_object().mesh().clone(),
+ Vec3::splat(1.0),
+ )
+ .set_position(Vec3::new(-1.5, -1.5, 0.0))
+ .set_color(RED)
+ .set_surface_rendering_activation(false)
+ .set_points_size(10.0, false);
/*
* Path stroke.
*/
let control_points = [
- Point3::new(0.0f32, 1.0, 0.0),
- Point3::new(2.0f32, 4.0, 2.0),
- Point3::new(2.0f32, 1.0, 4.0),
- Point3::new(4.0f32, 4.0, 6.0),
- Point3::new(2.0f32, 1.0, 8.0),
- Point3::new(2.0f32, 4.0, 10.0),
- Point3::new(0.0f32, 1.0, 12.0),
- Point3::new(-2.0f32, 4.0, 10.0),
- Point3::new(-2.0f32, 1.0, 8.0),
- Point3::new(-4.0f32, 4.0, 6.0),
- Point3::new(-2.0f32, 1.0, 4.0),
- Point3::new(-2.0f32, 4.0, 2.0),
+ Vec3::new(0.0f32, 1.0, 0.0),
+ Vec3::new(2.0f32, 4.0, 2.0),
+ Vec3::new(2.0f32, 1.0, 4.0),
+ Vec3::new(4.0f32, 4.0, 6.0),
+ Vec3::new(2.0f32, 1.0, 8.0),
+ Vec3::new(2.0f32, 4.0, 10.0),
+ Vec3::new(0.0f32, 1.0, 12.0),
+ Vec3::new(-2.0f32, 4.0, 10.0),
+ Vec3::new(-2.0f32, 1.0, 8.0),
+ Vec3::new(-4.0f32, 4.0, 6.0),
+ Vec3::new(-2.0f32, 1.0, 4.0),
+ Vec3::new(-2.0f32, 4.0, 2.0),
];
let bezier = kiss3d::procedural::bezier_curve(&control_points, 100);
let mut path = PolylinePath::new(&bezier);
@@ -132,92 +136,88 @@ async fn main() {
let end_cap = ArrowheadCap::new(2.0f32, 2.0, 0.5);
let mut pattern = PolylinePattern::new(pattern.coords(), true, start_cap, end_cap);
let mesh = pattern.stroke(&mut path);
- let mut m = window.add_render_mesh(mesh, Vector3::new(0.5f32, 0.5, 0.5));
- m.append_translation(&Translation3::new(4.0, -1.0, 0.0));
- m.set_color(1.0, 1.0, 0.0);
+ scene
+ .add_render_mesh(mesh, Vec3::new(0.5f32, 0.5, 0.5))
+ .set_position(Vec3::new(4.0, -1.0, 0.0))
+ .set_color(Color::new(1.0, 1.0, 0.0, 1.0));
/*
* Convex hull of 100,000 random 3d points.
*/
let mut points = Vec::new();
for _ in 0usize..100000 {
- points.push(rand::random::>() * 2.0f32);
+ points.push(Vec3::new(
+ rand::random::() * 2.0,
+ rand::random::() * 2.0,
+ rand::random::() * 2.0,
+ ));
}
- let chull = parry3d::transformation::convex_hull(&points[..]);
- let mut mhull = window.add_trimesh(
- TriMesh::new(chull.0, chull.1).unwrap(),
- Vector3::from_element(1.0),
- );
- let mut mpts = window.add_render_mesh(
- RenderMesh::new(points, None, None, None),
- Vector3::from_element(1.0),
- );
- mhull.append_translation(&Translation3::new(0.0, 2.0, -1.0));
- mhull.set_color(0.0, 1.0, 0.0);
- mhull.set_lines_width(2.0, false);
- mhull.set_surface_rendering_activation(false);
- mhull.set_points_size(10.0, false);
- mpts.set_color(0.0, 0.0, 1.0);
- mpts.append_translation(&Translation3::new(0.0, 2.0, -1.0));
- mpts.set_points_size(2.0, false);
- mpts.set_surface_rendering_activation(false);
+ let chull = parry3d::transformation::convex_hull(&points);
+ scene
+ .add_trimesh(
+ TriMesh::new(chull.0, chull.1).unwrap(),
+ Vec3::splat(1.0),
+ false,
+ )
+ .set_position(Vec3::new(0.0, 2.0, -1.0))
+ .set_color(GREEN)
+ .set_lines_width(2.0, false)
+ .set_surface_rendering_activation(false)
+ .set_points_size(10.0, false);
+ scene
+ .add_render_mesh(RenderMesh::new(points, None, None, None), Vec3::splat(1.0))
+ .set_color(BLUE)
+ .set_position(Vec3::new(0.0, 2.0, -1.0))
+ .set_points_size(2.0, false)
+ .set_surface_rendering_activation(false);
/*
* Convex hull of 100,000 random 2d points.
*/
- let mut points = Vec::new();
- let origin = Point2::new(3.0f32, 2.0);
+ let mut points2d = Vec::new();
+ let origin = Vec2::new(3.0f32, 2.0);
for _ in 0usize..100000 {
- points.push(origin + rand::random::>() * 2.0f32);
+ points2d.push(origin + Vec2::new(rand::random::() * 2.0, rand::random::() * 2.0));
}
- let points = &points[..];
- let polyline = parry2d::transformation::convex_hull(points);
+ let polyline = parry2d::transformation::convex_hull(&points2d);
/*
*
* Rendering.
*
*/
- window.set_light(Light::StickToCamera);
-
- while window.render().await {
- draw_polyline(&mut window, &polyline, points);
+ while window.render_3d(&mut scene, &mut camera).await {
+ draw_polyline(&mut window, &polyline, &points2d);
}
}
-fn draw_polyline(window: &mut Window, polyline: &[Point2], points: &[Point2]) {
+fn draw_polyline(window: &mut Window, polyline: &[Vec2], points: &[Vec2]) {
for pt in polyline.windows(2) {
window.draw_line(
- &Point3::new(pt[0].x, pt[0].y, 0.0),
- &Point3::new(pt[1].x, pt[1].y, 0.0),
- &Point3::new(0.0, 1.0, 0.0),
- 1.0,
+ Vec3::new(pt[0].x, pt[0].y, 0.0),
+ Vec3::new(pt[1].x, pt[1].y, 0.0),
+ GREEN,
+ 10.0,
+ false,
);
}
let last = polyline.len() - 1;
window.draw_line(
- &Point3::new(polyline[0].x, polyline[0].y, 0.0),
- &Point3::new(polyline[last].x, polyline[last].y, 0.0),
- &Point3::new(0.0, 1.0, 0.0),
- 1.0,
+ Vec3::new(polyline[0].x, polyline[0].y, 0.0),
+ Vec3::new(polyline[last].x, polyline[last].y, 0.0),
+ GREEN,
+ 6.0,
+ false,
);
for pt in points.iter() {
- window.draw_point(
- &Point3::new(pt.x, pt.y, 0.0),
- &Point3::new(0.0, 0.0, 1.0),
- 4.0,
- );
+ window.draw_point(Vec3::new(pt.x, pt.y, 0.0), BLUE, 1.0);
}
for pt in polyline.iter() {
- window.draw_point(
- &Point3::new(pt.x, pt.y, 0.0),
- &Point3::new(1.0, 0.0, 0.0),
- 4.0,
- );
+ window.draw_point(Vec3::new(pt.x, pt.y, 0.0), RED, 8.0);
}
}
diff --git a/examples/quad.rs b/examples/quad.rs
index f9e3db0ac..a6127b1ab 100644
--- a/examples/quad.rs
+++ b/examples/quad.rs
@@ -1,24 +1,23 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-extern crate rand;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
+use kiss3d::prelude::*;
use rand::random;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: quad").await;
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- let mut c = window.add_quad(5.0, 4.0, 100, 100);
-
- c.set_color(random(), random(), random());
+ let mut c =
+ scene
+ .add_quad(5.0, 4.0, 100, 100)
+ .set_color(Color::new(random(), random(), random(), 1.0));
let mut time = 0.016f32;
- window.set_light(Light::StickToCamera);
-
- while window.render().await {
+ while window.render_3d(&mut scene, &mut camera).await {
c.modify_vertices(&mut |coords| {
for v in coords.iter_mut() {
v.z = time.sin()
diff --git a/examples/recording.rs b/examples/recording.rs
index f9a7ea96e..510162070 100644
--- a/examples/recording.rs
+++ b/examples/recording.rs
@@ -1,6 +1,3 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
/// This example demonstrates how to record a screencast of the 3D scene.
///
/// Requires the `recording` feature to be enabled:
@@ -10,16 +7,15 @@ extern crate nalgebra as na;
#[kiss3d::main]
#[cfg(feature = "recording")]
async fn main() {
- use kiss3d::light::Light;
- use kiss3d::window::Window;
- use na::{UnitQuaternion, Vector3};
+ use kiss3d::prelude::*;
let mut window = Window::new("Kiss3d: recording").await;
- let mut c = window.add_cube(0.2, 0.2, 0.2);
-
- c.set_color(1.0, 0.0, 0.0);
-
- window.set_light(Light::StickToCamera);
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
+ let mut c = scene.add_cube(0.2, 0.2, 0.2).set_color(RED);
// Option 1: Simple recording (every frame)
// window.begin_recording();
@@ -30,12 +26,12 @@ async fn main() {
println!("Recording started (every 2nd frame)...");
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.02);
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.02);
// Record 90 frames (3 seconds at 30fps, or 1.5 seconds with frame_skip=2)
#[allow(unused_variables)]
for frame in 0..90 {
- c.prepend_to_local_rotation(&rot);
+ c.rotate(&rot);
// Demonstrate pause/resume at frame 30-60
if frame == 30 {
@@ -47,7 +43,7 @@ async fn main() {
println!("Recording resumed at frame 60...");
}
- if !window.render().await {
+ if !window.render_3d(&mut scene, &mut camera).await {
break;
}
diff --git a/examples/rectangle.rs b/examples/rectangle.rs
index 04cb9b00c..2ed56b17e 100644
--- a/examples/rectangle.rs
+++ b/examples/rectangle.rs
@@ -1,22 +1,17 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::UnitComplex;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: rectangle").await;
- let mut c = window.add_rectangle(100.0, 150.0);
-
- c.set_color(1.0, 0.0, 0.0);
+ let mut camera = PanZoomCamera2d::new(Vec2::ZERO, 5.0);
+ let mut scene = SceneNode2d::empty();
+ let mut c = scene.add_rectangle(100.0, 150.0);
- window.set_light(Light::StickToCamera);
+ c.set_color(RED);
- let rot = UnitComplex::new(0.014);
+ let rot = 0.014;
- while window.render().await {
- c.prepend_to_local_rotation(&rot);
+ while window.render_2d(&mut scene, &mut camera).await {
+ c.append_rotation(rot);
}
}
diff --git a/examples/screenshot.rs b/examples/screenshot.rs
index 82d965bc6..236a97f00 100644
--- a/examples/screenshot.rs
+++ b/examples/screenshot.rs
@@ -1,32 +1,24 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
+use kiss3d::prelude::*;
use std::path::Path;
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{UnitQuaternion, Vector3};
-
// Based on cube example.
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: screenshot").await;
- let mut c = window.add_cube(0.2, 0.2, 0.2);
-
- c.set_color(1.0, 0.0, 0.0);
- c.prepend_to_local_rotation(&UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.785));
- c.prepend_to_local_rotation(&UnitQuaternion::from_axis_angle(
- &Vector3::x_axis(),
- -0.6f32,
- ));
-
- window.set_light(Light::StickToCamera);
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
+ scene
+ .add_cube(0.2, 0.2, 0.2)
+ .set_color(RED)
+ .rotate(Quat::from_axis_angle(Vec3::Y, 0.785))
+ .rotate(Quat::from_axis_angle(Vec3::X, -0.6f32));
- while window.render().await {
- let img = window.snap_image();
- let img_path = Path::new("screenshot.png");
- img.save(img_path).unwrap();
- println!("Screenshot saved to `screenshot.png`");
- break;
- }
+ window.render_3d(&mut scene, &mut camera).await; // Render one frame.
+ let img = window.snap_image();
+ let img_path = Path::new("screenshot.png");
+ img.save(img_path).unwrap();
+ println!("Screenshot saved to `screenshot.png`");
}
diff --git a/examples/stereo.rs b/examples/stereo.rs
index 4f815d242..44ba6c71f 100644
--- a/examples/stereo.rs
+++ b/examples/stereo.rs
@@ -1,33 +1,33 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::camera::FirstPersonStereo;
-use kiss3d::event::{Action, Key, WindowEvent};
-use kiss3d::light::Light;
use kiss3d::post_processing::OculusStereo;
-use kiss3d::window::Window;
-use na::Point3;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new_with_size("Kiss3d: stereo", 1280, 800).await;
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
+ let mut cube = scene.add_cube(1.0, 1.0, 1.0).set_color(RED);
- let mut c = window.add_cube(1.0, 1.0, 1.0);
-
- let eye = Point3::new(0.0f32, 0.0, 10.0);
- let at = Point3::new(0.0f32, 0.0, 0.0);
- let mut camera = FirstPersonStereo::new(eye, at, 0.3f32);
-
- c.set_color(1.0, 0.0, 0.0);
-
- window.set_light(Light::StickToCamera);
+ let eye = Vec3::new(1.0f32, 2.0, 10.0);
+ let at = Vec3::ZERO;
+ let mut camera = FirstPersonCamera3dStereo::new(eye, at, 0.3f32);
let mut oculus_stereo = OculusStereo::new();
while window
- .render_with_camera_and_effect(&mut camera, &mut oculus_stereo)
+ .render(
+ Some(&mut scene),
+ None,
+ Some(&mut camera),
+ None,
+ None,
+ Some(&mut oculus_stereo),
+ )
.await
{
+ cube.rotate(Quat::from_rotation_y(0.02));
for event in window.events().iter() {
match event.value {
WindowEvent::Key(Key::Numpad1, Action::Release, _) => {
diff --git a/examples/text.rs b/examples/text.rs
index 873fd0cbe..3d64eec78 100644
--- a/examples/text.rs
+++ b/examples/text.rs
@@ -1,32 +1,16 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::text::Font;
-use kiss3d::window::Window;
-use na::{Point2, Point3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: text").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
let font = Font::default();
- while window.render().await {
- window.draw_text(
- "Hello birds!",
- &Point2::origin(),
- 120.0,
- &font,
- &Point3::new(0.0, 1.0, 1.0),
- );
+ while window.render_3d(&mut scene, &mut camera).await {
+ window.draw_text("Hello birds!", Vec2::ZERO, 120.0, &font, CYAN);
let ascii = " !\"#$%&'`()*+,-_./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^abcdefghijklmnopqrstuvwxyz{|}~";
-
- window.draw_text(
- ascii,
- &Point2::new(0.0, 120.0),
- 60.0,
- &font,
- &Point3::new(1.0, 1.0, 0.0),
- );
+ window.draw_text(ascii, Vec2::new(0.0, 120.0), 60.0, &font, YELLOW);
}
}
diff --git a/examples/texturing.rs b/examples/texturing.rs
index 7bba632cf..5e217775e 100644
--- a/examples/texturing.rs
+++ b/examples/texturing.rs
@@ -1,31 +1,45 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{Translation2, UnitComplex, UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
use std::path::Path;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: texturing").await;
+ let mut camera_3d = OrbitCamera3d::default();
+ let mut scene_3d = SceneNode3d::empty();
+ let mut camera_2d = PanZoomCamera2d::default();
+ let mut scene_2d = SceneNode2d::empty();
- let mut c = window.add_cube(1.0, 1.0, 1.0);
- c.set_color(1.0, 0.0, 0.0);
- c.set_texture_from_file(Path::new("./examples/media/kitten.png"), "kitten");
+ scene_3d
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 2.0, -10.0));
- let mut r = window.add_rectangle(100.0, 100.0);
- r.append_translation(&Translation2::new(-100.0, -100.0));
- r.set_color(0.0, 0.0, 1.0);
- r.set_texture_from_memory(include_bytes!("./media/kitten.png"), "kitten_mem");
+ let mut c = scene_3d
+ .add_cube(1.0, 1.0, 1.0)
+ .set_color(RED)
+ .set_texture_from_file(Path::new("./examples/media/kitten.png"), "kitten");
- window.set_light(Light::StickToCamera);
+ let mut r = scene_2d
+ .add_rectangle(100.0, 100.0)
+ .set_position(Vec2::new(-100.0, -100.0))
+ .set_color(BLUE)
+ .set_texture_from_memory(include_bytes!("./media/kitten.png"), "kitten_mem");
- let rot3d = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
- let rot2d = UnitComplex::new(0.01);
+ let rot3d = Quat::from_axis_angle(Vec3::Y, 0.014);
+ let rot2d = 0.01;
- while window.render().await {
- c.append_rotation(&rot3d);
- r.prepend_to_local_rotation(&rot2d)
+ // Render 3D and 2D scenes alternately
+ while window
+ .render(
+ Some(&mut scene_3d),
+ Some(&mut scene_2d),
+ Some(&mut camera_3d),
+ Some(&mut camera_2d),
+ None,
+ None,
+ )
+ .await
+ {
+ c.append_rotation(rot3d);
+ r.append_rotation(rot2d);
}
}
diff --git a/examples/texturing_mipmaps.rs b/examples/texturing_mipmaps.rs
index e064f198a..fd15ce1ea 100644
--- a/examples/texturing_mipmaps.rs
+++ b/examples/texturing_mipmaps.rs
@@ -1,33 +1,33 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::window::Window;
-use kiss3d::{light::Light, resource::TextureManager};
-use na::Translation3;
+use kiss3d::prelude::*;
use std::path::Path;
use std::time::Instant;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: texturing-mipmaps").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, -10.0));
let tex_path = Path::new("./examples/media/checkerboard.png");
// Show two spheres that are scaled up and down, one without mipmaps and one
// without mipmaps.
TextureManager::get_global_manager(|tm| tm.set_generate_mipmaps(false));
- let mut q1 = window.add_sphere(1.0);
- q1.set_texture_from_file(tex_path, "no-mipmaps");
- q1.set_local_translation(Translation3::new(0.3, 0.0, 0.0));
+ let mut q1 = scene
+ .add_sphere(1.0)
+ .set_texture_from_file(tex_path, "no-mipmaps")
+ .translate(Vec3::new(0.3, 0.0, 0.0));
TextureManager::get_global_manager(|tm| tm.set_generate_mipmaps(true));
- let mut q2 = window.add_sphere(1.0);
- q2.set_texture_from_file(tex_path, "with-mipmaps");
- q2.set_local_translation(Translation3::new(-0.3, 0.0, 0.0));
-
- window.set_light(Light::StickToCamera);
+ let mut q2 = scene
+ .add_sphere(1.0)
+ .set_texture_from_file(tex_path, "with-mipmaps")
+ .translate(Vec3::new(-0.3, 0.0, 0.0));
let start = Instant::now();
- while window.render().await {
+ while window.render_3d(&mut scene, &mut camera).await {
let scale = 0.25 + 0.2 * (Instant::now() - start).as_secs_f32().cos();
for c in [&mut q1, &mut q2] {
c.set_local_scale(scale, scale, scale);
diff --git a/examples/ui.rs b/examples/ui.rs
index 08dd594c0..778288395 100644
--- a/examples/ui.rs
+++ b/examples/ui.rs
@@ -1,13 +1,3 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-#[cfg(feature = "egui")]
-use kiss3d::light::Light;
-#[cfg(feature = "egui")]
-use kiss3d::window::Window;
-#[cfg(feature = "egui")]
-use na::{UnitQuaternion, Vector3};
-
#[cfg(not(feature = "egui"))]
#[kiss3d::main]
async fn main() {
@@ -17,26 +7,37 @@ async fn main() {
#[cfg(feature = "egui")]
#[kiss3d::main]
async fn main() {
+ use kiss3d::prelude::*;
+
let mut window = Window::new("Kiss3d: egui UI").await;
- window.set_background_color(0.9, 0.9, 0.9);
+ let mut camera = OrbitCamera3d::new(Vec3::new(0.0, 0.5, 1.0), Vec3::ZERO);
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- let mut cube = window.add_cube(0.2, 0.2, 0.2);
- cube.set_color(1.0, 0.0, 0.0);
+ window.set_background_color(LIGHT_STEEL_BLUE);
- window.set_light(Light::StickToCamera);
+ let mut cube = scene.add_cube(0.2, 0.2, 0.2).set_color(RED);
// UI state
let mut rotation_speed = 0.014;
+ let mut opacity = 1.0;
let mut cube_color = [1.0, 0.0, 0.0];
// Render loop
- while window.render().await {
+ while window.render_3d(&mut scene, &mut camera).await {
// Rotate cube
- let rot_current = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), rotation_speed);
- cube.prepend_to_local_rotation(&rot_current);
+ let rot_current = Quat::from_axis_angle(Vec3::Y, rotation_speed);
+ cube.rotate(rot_current);
// Update cube color
- cube.set_color(cube_color[0], cube_color[1], cube_color[2]);
+ cube.set_color(Color::new(
+ cube_color[0],
+ cube_color[1],
+ cube_color[2],
+ opacity,
+ ));
// Draw UI
window.draw_ui(|ctx| {
@@ -49,6 +50,10 @@ async fn main() {
ui.separator();
+ // Opacity control
+ ui.label("Opacity:");
+ ui.add(egui::Slider::new(&mut opacity, 0.0..=1.0));
+
// Color picker
ui.label("Cube Color:");
diff --git a/examples/window.rs b/examples/window.rs
index e998a2924..93ea832d4 100644
--- a/examples/window.rs
+++ b/examples/window.rs
@@ -1,12 +1,12 @@
-extern crate kiss3d;
-
-use kiss3d::window::Window;
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: window").await;
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
- window.set_background_color(0.0, 0.0, 0.3);
+ window.set_background_color(LIGHT_BLUE);
- while window.render().await {}
+ while window.render_3d(&mut scene, &mut camera).await {}
}
diff --git a/examples/wireframe.rs b/examples/wireframe.rs
index a7ea2cbea..5cd3ea14f 100644
--- a/examples/wireframe.rs
+++ b/examples/wireframe.rs
@@ -1,26 +1,25 @@
-extern crate kiss3d;
-extern crate nalgebra as na;
-
-use kiss3d::light::Light;
-use kiss3d::window::Window;
-use na::{UnitQuaternion, Vector3};
+use kiss3d::prelude::*;
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: wireframe").await;
- let mut c = window.add_cube(1.0, 1.0, 1.0);
-
- c.set_color(1.0, 0.0, 0.0);
- c.set_points_color(Some([0.0, 1.0, 1.0].into()));
- c.set_points_size(30.0, false); // false = screen pixels
- c.set_lines_width(10.0, false); // false = screen pixels
- c.set_surface_rendering_activation(false);
+ let mut camera = OrbitCamera3d::default();
+ let mut scene = SceneNode3d::empty();
+ scene
+ .add_light(Light::point(100.0))
+ .set_position(Vec3::new(0.0, 10.0, 10.0));
- window.set_light(Light::StickToCamera);
+ let mut c = scene
+ .add_cube(1.0, 1.0, 1.0)
+ .set_color(RED)
+ .set_points_color(Some(CYAN))
+ .set_points_size(30.0, false) // false = screen pixels
+ .set_lines_width(10.0, false) // false = screen pixels
+ .set_surface_rendering_activation(false);
- let rot = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), 0.014);
+ let rot = Quat::from_axis_angle(Vec3::Y, 0.014);
- while window.render().await {
- c.prepend_to_local_rotation(&rot);
+ while window.render_3d(&mut scene, &mut camera).await {
+ c.rotate(rot);
}
}
diff --git a/run_all_examples.sh b/run_all_examples.sh
index f4a6ee90f..c5dbfdfda 100755
--- a/run_all_examples.sh
+++ b/run_all_examples.sh
@@ -12,10 +12,12 @@ EXAMPLES=(
primitives
primitives_scale
primitives2d
+ multi_light
wireframe
lines
- planar_lines
+ lines2d
points
+ points2d
text
group
add_remove
@@ -38,7 +40,7 @@ EXAMPLES=(
instancing3d
polylines
polyline_strip
- planar_polylines
+ polylines2d
screenshot
recording
window
@@ -52,7 +54,7 @@ echo ""
for example in "${EXAMPLES[@]}"; do
echo "=== Running: $example ==="
- cargo run --release --example "$example" --features egui
+ cargo run --release --example "$example" --features egui,parry
echo ""
done
diff --git a/src/builtin/default.frag b/src/builtin/default.frag
deleted file mode 100644
index b93e29287..000000000
--- a/src/builtin/default.frag
+++ /dev/null
@@ -1,37 +0,0 @@
-#version 100
-#ifdef GL_FRAGMENT_PRECISION_HIGH
- precision highp float;
-#else
- precision mediump float;
-#endif
-
-varying vec3 local_light_position;
-varying vec2 tex_coord_v;
-varying vec3 normalInterp;
-varying vec3 vertPos;
-varying vec4 vertColor;
-
-uniform vec3 color;
-uniform sampler2D tex;
-const vec3 specColor = vec3(0.4, 0.4, 0.4);
-
-void main() {
- vec3 normal = normalize(normalInterp);
- vec3 lightDir = normalize(local_light_position - vertPos);
-
- float lambertian = max(dot(lightDir, normal), 0.0);
- float specular = 0.0;
-
- if(lambertian > 0.0) {
- vec3 viewDir = normalize(-vertPos);
- vec3 halfDir = normalize(lightDir + viewDir);
- float specAngle = max(dot(halfDir, normal), 0.0);
- specular = pow(specAngle, 30.0);
- }
-
- vec4 base_color = vertColor * vec4(color, 1.0);
- vec4 tex_color = texture2D(tex, tex_coord_v);
- gl_FragColor = tex_color * vec4(base_color.xyz / 3.0 +
- lambertian * base_color.xyz / 3.0 +
- specular * specColor / 3.0, base_color.w);
-}
diff --git a/src/builtin/default.vert b/src/builtin/default.vert
deleted file mode 100644
index 8027009f6..000000000
--- a/src/builtin/default.vert
+++ /dev/null
@@ -1,31 +0,0 @@
-#version 100
-attribute vec3 position;
-attribute vec2 tex_coord;
-attribute vec3 normal;
-attribute vec3 inst_tra;
-attribute vec4 inst_color;
-attribute vec3 inst_def_0;
-attribute vec3 inst_def_1;
-attribute vec3 inst_def_2;
-
-uniform mat3 ntransform, scale;
-uniform mat4 proj, view, transform;
-uniform vec3 light_position;
-
-varying vec3 local_light_position;
-varying vec2 tex_coord_v;
-varying vec3 normalInterp;
-varying vec3 vertPos;
-varying vec4 vertColor;
-
-void main(){
- mat3 deformation = mat3(inst_def_0, inst_def_1, inst_def_2);
- vec4 pt = vec4(inst_tra, 0.0) + transform * vec4(deformation * scale * position, 1.0);
- gl_Position = proj * view * pt;
- vec4 vertPos4 = view * pt;
- vertPos = vec3(vertPos4) / vertPos4.w;
- normalInterp = mat3(view) * deformation * ntransform * normal;
- tex_coord_v = tex_coord;
- local_light_position = (view * vec4(light_position, 1.0)).xyz;
- vertColor = inst_color;
-}
diff --git a/src/builtin/default.wgsl b/src/builtin/default.wgsl
index 3169584fc..8206af146 100644
--- a/src/builtin/default.wgsl
+++ b/src/builtin/default.wgsl
@@ -1,35 +1,83 @@
// Default material shader for kiss3d
-// Implements Blinn-Phong lighting with texture support and instancing
+// Implements Cook-Torrance PBR with texture support, instancing, and multi-light
-// Bind group 0: Frame uniforms (view, projection, light)
+const PI: f32 = 3.14159265359;
+const MAX_LIGHTS: u32 = 8u;
+
+// Light type constants
+const LIGHT_TYPE_POINT: u32 = 0u;
+const LIGHT_TYPE_DIRECTIONAL: u32 = 1u;
+const LIGHT_TYPE_SPOT: u32 = 2u;
+
+// Single light data structure
+struct LightData {
+ position: vec3,
+ light_type: u32,
+ direction: vec3,
+ intensity: f32,
+ color: vec3,
+ inner_cone_cos: f32,
+ outer_cone_cos: f32,
+ attenuation_radius: f32,
+ _padding: vec2,
+}
+
+// Bind group 0: Frame uniforms (view, projection, lights)
struct FrameUniforms {
view: mat4x4,
proj: mat4x4,
- light_position: vec3,
- _padding: f32,
+ lights: array,
+ num_lights: u32,
+ ambient_intensity: f32,
+ _padding: vec2,
}
@group(0) @binding(0)
var frame: FrameUniforms;
-// Bind group 1: Object uniforms (transform, scale, color)
+// Bind group 1: Object uniforms (transform, scale, color, PBR properties)
struct ObjectUniforms {
transform: mat4x4,
ntransform: mat3x3,
scale: mat3x3,
- color: vec3,
- _padding: f32,
+ color: vec4,
+ metallic: f32,
+ roughness: f32,
+ _pad0: vec2,
+ emissive: vec4,
+ has_normal_map: f32,
+ has_metallic_roughness_map: f32,
+ has_ao_map: f32,
+ has_emissive_map: f32,
}
@group(1) @binding(0)
var object: ObjectUniforms;
-// Bind group 2: Texture and sampler
+// Bind group 2: Albedo texture and sampler
@group(2) @binding(0)
var t_diffuse: texture_2d;
@group(2) @binding(1)
var s_diffuse: sampler;
+// Bind group 3: PBR texture maps
+@group(3) @binding(0)
+var t_normal: texture_2d;
+@group(3) @binding(1)
+var s_normal: sampler;
+@group(3) @binding(2)
+var t_metallic_roughness: texture_2d;
+@group(3) @binding(3)
+var s_metallic_roughness: sampler;
+@group(3) @binding(4)
+var t_ao: texture_2d;
+@group(3) @binding(5)
+var s_ao: sampler;
+@group(3) @binding(6)
+var t_emissive: texture_2d;
+@group(3) @binding(7)
+var s_emissive: sampler;
+
// Vertex input
struct VertexInput {
@location(0) position: vec3,
@@ -50,12 +98,81 @@ struct InstanceInput {
struct VertexOutput {
@builtin(position) clip_position: vec4,
@location(0) tex_coord: vec2,
- @location(1) normal_interp: vec3,
- @location(2) vert_pos: vec3,
+ @location(1) world_normal: vec3,
+ @location(2) world_pos: vec3,
@location(3) vert_color: vec4,
- @location(4) local_light_position: vec3,
+ @location(4) view_pos: vec3,
}
+// === PBR BRDF Functions ===
+
+// Normal Distribution Function (GGX/Trowbridge-Reitz)
+fn distribution_ggx(N: vec3, H: vec3, roughness: f32) -> f32 {
+ let a = roughness * roughness;
+ let a2 = a * a;
+ let NdotH = max(dot(N, H), 0.0);
+ let NdotH2 = NdotH * NdotH;
+
+ var denom = (NdotH2 * (a2 - 1.0) + 1.0);
+ denom = PI * denom * denom;
+
+ return a2 / max(denom, 0.0001);
+}
+
+// Geometry function (Smith's Schlick-GGX)
+fn geometry_schlick_ggx(NdotV: f32, roughness: f32) -> f32 {
+ let r = (roughness + 1.0);
+ let k = (r * r) / 8.0; // Direct lighting
+
+ return NdotV / (NdotV * (1.0 - k) + k);
+}
+
+fn geometry_smith(N: vec3, V: vec3, L: vec3, roughness: f32) -> f32 {
+ let NdotV = max(dot(N, V), 0.0);
+ let NdotL = max(dot(N, L), 0.0);
+ let ggx2 = geometry_schlick_ggx(NdotV, roughness);
+ let ggx1 = geometry_schlick_ggx(NdotL, roughness);
+
+ return ggx1 * ggx2;
+}
+
+// Fresnel (Schlick approximation)
+fn fresnel_schlick(cos_theta: f32, F0: vec3) -> vec3 {
+ return F0 + (1.0 - F0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
+}
+
+// Attenuation functions
+fn calculate_point_attenuation(dist: f32, radius: f32) -> f32 {
+ // Smooth falloff that reaches zero at the attenuation radius
+ let normalized_dist = clamp(dist / radius, 0.0, 1.0);
+ let attenuation = 1.0 - normalized_dist * normalized_dist;
+ return attenuation * attenuation;
+}
+
+fn calculate_spot_attenuation(
+ L: vec3,
+ spot_direction: vec3,
+ dist: f32,
+ inner_cone_cos: f32,
+ outer_cone_cos: f32,
+ radius: f32
+) -> f32 {
+ // Angular attenuation
+ let cos_angle = dot(-L, spot_direction);
+ let angular_attenuation = clamp(
+ (cos_angle - outer_cone_cos) / max(inner_cone_cos - outer_cone_cos, 0.0001),
+ 0.0,
+ 1.0
+ );
+
+ // Distance attenuation
+ let dist_attenuation = calculate_point_attenuation(dist, radius);
+
+ return angular_attenuation * angular_attenuation * dist_attenuation;
+}
+
+// === Vertex Shader ===
+
@vertex
fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {
var out: VertexOutput;
@@ -74,64 +191,175 @@ fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {
let world_pos = vec4(instance.inst_tra, 0.0) + model_pos;
out.clip_position = frame.proj * frame.view * world_pos;
+ out.world_pos = world_pos.xyz;
- // View-space position for lighting
- let view_pos = frame.view * world_pos;
- out.vert_pos = view_pos.xyz / view_pos.w;
+ // Transform normal to world space
+ out.world_normal = normalize(deformation * object.ntransform * vertex.normal);
- // Transform normal to view space
- let view_mat3 = mat3x3(
- frame.view[0].xyz,
- frame.view[1].xyz,
- frame.view[2].xyz
- );
- out.normal_interp = view_mat3 * deformation * object.ntransform * vertex.normal;
+ // View-space position for lighting calculations
+ let view_pos = frame.view * world_pos;
+ out.view_pos = view_pos.xyz / view_pos.w;
out.tex_coord = vertex.tex_coord;
- out.local_light_position = (frame.view * vec4(frame.light_position, 1.0)).xyz;
out.vert_color = instance.inst_color;
return out;
}
-// Convert linear RGB to sRGB for display.
-// This is applied manually because we use a non-sRGB framebuffer for
-// consistent behavior across native and web platforms.
-fn linear_to_srgb(linear: vec3) -> vec3 {
- let cutoff = linear < vec3(0.0031308);
- let lower = linear * 12.92;
- let higher = pow(linear, vec3(1.0 / 2.4)) * 1.055 - vec3(0.055);
- return select(higher, lower, cutoff);
-}
+// === Fragment Shader ===
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
- let specular_color = vec3(0.4, 0.4, 0.4);
+ // Sample albedo texture and combine with vertex/object color
+ let albedo_tex = textureSample(t_diffuse, s_diffuse, in.tex_coord);
+ let base_color = in.vert_color * object.color;
+ let albedo = (albedo_tex * base_color).rgb;
- let normal = normalize(in.normal_interp);
- let light_dir = normalize(in.local_light_position - in.vert_pos);
+ // Get PBR parameters - either from textures or uniforms
+ var metallic = object.metallic;
+ var roughness = object.roughness;
- // Lambertian diffuse
- let lambertian = max(dot(light_dir, normal), 0.0);
-
- // Blinn-Phong specular
- var specular = 0.0;
- if lambertian > 0.0 {
- let view_dir = normalize(-in.vert_pos);
- let half_dir = normalize(light_dir + view_dir);
- let spec_angle = max(dot(half_dir, normal), 0.0);
- specular = pow(spec_angle, 30.0);
+ if object.has_metallic_roughness_map > 0.5 {
+ let mr = textureSample(t_metallic_roughness, s_metallic_roughness, in.tex_coord);
+ // glTF convention: B = metallic, G = roughness
+ metallic = mr.b;
+ roughness = mr.g;
}
- let base_color = in.vert_color * vec4(object.color, 1.0);
- let tex_color = textureSample(t_diffuse, s_diffuse, in.tex_coord);
+ // Clamp roughness to prevent artifacts
+ roughness = clamp(roughness, 0.04, 1.0);
+
+ // Get normal - either from normal map or geometry
+ var N = normalize(in.world_normal);
- let final_color = tex_color * vec4(
- base_color.xyz / 3.0 +
- lambertian * base_color.xyz / 3.0 +
- specular * specular_color / 3.0,
- base_color.w
+ if object.has_normal_map > 0.5 {
+ let normal_sample = textureSample(t_normal, s_normal, in.tex_coord).rgb;
+ let tangent_normal = normal_sample * 2.0 - 1.0;
+
+ // Generate tangent space (simple method based on normal)
+ var tangent: vec3;
+ let c1 = cross(N, vec3(0.0, 0.0, 1.0));
+ let c2 = cross(N, vec3(0.0, 1.0, 0.0));
+ if length(c1) > length(c2) {
+ tangent = normalize(c1);
+ } else {
+ tangent = normalize(c2);
+ }
+ let bitangent = normalize(cross(N, tangent));
+ let TBN = mat3x3(tangent, bitangent, N);
+ N = normalize(TBN * tangent_normal);
+ }
+
+ // Transform normal to view space for lighting
+ let view_mat3 = mat3x3(
+ frame.view[0].xyz,
+ frame.view[1].xyz,
+ frame.view[2].xyz
);
+ let N_view = normalize(view_mat3 * N);
+
+ // Sample ambient occlusion
+ var ao = 1.0;
+ if object.has_ao_map > 0.5 {
+ ao = textureSample(t_ao, s_ao, in.tex_coord).r;
+ }
+
+ // Sample emissive
+ var emissive = object.emissive.rgb;
+ if object.has_emissive_map > 0.5 {
+ let emissive_sample = textureSample(t_emissive, s_emissive, in.tex_coord).rgb;
+ emissive = emissive * emissive_sample;
+ }
+
+ // View vector (in view space)
+ let V = normalize(-in.view_pos);
+
+ // Calculate reflectance at normal incidence (F0)
+ // Dielectric: 0.04, Metal: albedo
+ let F0 = mix(vec3(0.04), albedo, metallic);
+
+ // Accumulate lighting from all lights
+ var Lo = vec3(0.0);
+
+ for (var i = 0u; i < frame.num_lights; i++) {
+ let light = frame.lights[i];
+
+ // Calculate light direction and attenuation based on light type
+ var L: vec3;
+ var attenuation: f32 = 1.0;
+ var light_intensity = light.intensity;
+
+ if light.light_type == LIGHT_TYPE_POINT {
+ // Point light: calculate direction from light position to fragment
+ let light_pos_view = (frame.view * vec4(light.position, 1.0)).xyz;
+ let light_vec = light_pos_view - in.view_pos;
+ let dist = length(light_vec);
+ L = normalize(light_vec);
+ attenuation = calculate_point_attenuation(dist, light.attenuation_radius);
+ } else if light.light_type == LIGHT_TYPE_DIRECTIONAL {
+ // Directional light: use light direction directly
+ let light_dir_view = normalize(view_mat3 * light.direction);
+ L = -light_dir_view; // Light direction points FROM light, we need TO light
+ } else {
+ // Spot light: calculate direction and angular attenuation
+ let light_pos_view = (frame.view * vec4(light.position, 1.0)).xyz;
+ let light_dir_view = normalize(view_mat3 * light.direction);
+ let light_vec = light_pos_view - in.view_pos;
+ let dist = length(light_vec);
+ L = normalize(light_vec);
+ attenuation = calculate_spot_attenuation(
+ L,
+ light_dir_view,
+ dist,
+ light.inner_cone_cos,
+ light.outer_cone_cos,
+ light.attenuation_radius
+ );
+ }
+
+ // Skip if no contribution
+ if attenuation <= 0.0 {
+ continue;
+ }
+
+ let H = normalize(V + L);
+
+ // Cook-Torrance BRDF
+ let NDF = distribution_ggx(N_view, H, roughness);
+ let G = geometry_smith(N_view, V, L, roughness);
+ let F = fresnel_schlick(max(dot(H, V), 0.0), F0);
+
+ // Specular contribution
+ let numerator = NDF * G * F;
+ let denominator = 4.0 * max(dot(N_view, V), 0.0) * max(dot(N_view, L), 0.0) + 0.0001;
+ let specular = numerator / denominator;
+
+ // Energy conservation: diffuse + specular = 1
+ let kS = F;
+ var kD = vec3(1.0) - kS;
+ kD *= 1.0 - metallic; // Metals have no diffuse
+
+ // Lighting calculation
+ let NdotL_raw = dot(N_view, L);
+ let NdotL = max(NdotL_raw, 0.0);
+
+ // Wrapped diffuse (half-Lambert) for softer shadows on back-facing triangles
+ let wrap = 0.2;
+ let NdotL_wrapped = (NdotL_raw * (1.0 - wrap) + wrap);
+ let diffuse_wrap = max(NdotL_wrapped, 0.0);
+
+ // Combine lighting with light color
+ let radiance = light.color * light_intensity * attenuation;
+ let diffuse_contrib = kD * albedo / PI * diffuse_wrap;
+ let specular_contrib = specular * NdotL;
+ Lo += (diffuse_contrib + specular_contrib) * radiance;
+ }
+
+ // Ambient lighting using configurable intensity from frame uniforms
+ let ambient = vec3(frame.ambient_intensity) * albedo * ao;
+
+ // Final color
+ var color = ambient + Lo + emissive;
- return vec4(linear_to_srgb(final_color.rgb), final_color.a);
+ return vec4(color, albedo_tex.a * base_color.a);
}
diff --git a/src/builtin/grayscales.wgsl b/src/builtin/grayscales.wgsl
index 9e34c0120..ed023d252 100644
--- a/src/builtin/grayscales.wgsl
+++ b/src/builtin/grayscales.wgsl
@@ -27,19 +27,11 @@ fn vs_main(vertex: VertexInput) -> VertexOutput {
return out;
}
-// Convert linear RGB to sRGB for display.
-fn linear_to_srgb(linear: vec3) -> vec3 {
- let cutoff = linear < vec3(0.0031308);
- let lower = linear * 12.92;
- let higher = pow(linear, vec3(1.0 / 2.4)) * 1.055 - vec3(0.055);
- return select(higher, lower, cutoff);
-}
-
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
let color = textureSample(t_fbo, s_fbo, in.tex_coord);
// Use standard luminance weights
let gray = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
let result = vec3(gray, gray, gray);
- return vec4(linear_to_srgb(result), color.a);
+ return vec4(result, color.a);
}
diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs
index 64cd93050..23c286272 100644
--- a/src/builtin/mod.rs
+++ b/src/builtin/mod.rs
@@ -4,10 +4,10 @@ pub use self::normals_material::{NormalsMaterial, NORMAL_FRAGMENT_SRC, NORMAL_VE
pub use self::object_material::{ObjectMaterial, OBJECT_FRAGMENT_SRC, OBJECT_VERTEX_SRC};
pub use self::uvs_material::{UvsMaterial, UVS_FRAGMENT_SRC, UVS_VERTEX_SRC};
-pub use self::planar_object_material::PlanarObjectMaterial;
+pub use self::object_material2d::ObjectMaterial2d;
mod normals_material;
mod object_material;
mod uvs_material;
-mod planar_object_material;
+mod object_material2d;
diff --git a/src/builtin/normals.wgsl b/src/builtin/normals.wgsl
index fb37d2ef4..75660eb33 100644
--- a/src/builtin/normals.wgsl
+++ b/src/builtin/normals.wgsl
@@ -42,17 +42,9 @@ fn vs_main(vertex: VertexInput) -> VertexOutput {
return out;
}
-// Convert linear RGB to sRGB for display.
-fn linear_to_srgb(linear: vec3) -> vec3 {
- let cutoff = linear < vec3(0.0031308);
- let lower = linear * 12.92;
- let higher = pow(linear, vec3(1.0 / 2.4)) * 1.055 - vec3(0.055);
- return select(higher, lower, cutoff);
-}
-
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
// Map normal from [-1, 1] to [0, 1] for visualization
let color = (in.ls_normal + vec3(1.0)) / 2.0;
- return vec4(linear_to_srgb(color), 1.0);
+ return vec4(color, 1.0);
}
diff --git a/src/builtin/normals_material.rs b/src/builtin/normals_material.rs
index e8635a8da..dc2dcb5da 100644
--- a/src/builtin/normals_material.rs
+++ b/src/builtin/normals_material.rs
@@ -1,12 +1,13 @@
-use crate::camera::Camera;
+use crate::camera::Camera3d;
use crate::context::Context;
-use crate::light::Light;
+use crate::light::LightCollection;
use crate::resource::vertex_index::VERTEX_INDEX_FORMAT;
-use crate::resource::{GpuData, GpuMesh, Material, RenderContext};
-use crate::scene::{InstancesBuffer, ObjectData};
+use crate::resource::{DynamicUniformBuffer, GpuData, GpuMesh3d, Material3d, RenderContext};
+use crate::scene::{InstancesBuffer3d, ObjectData3d};
use bytemuck::{Pod, Zeroable};
-use na::{Isometry3, Matrix3, Vector3};
+use glamx::{Mat3, Pose3, Vec3};
use std::any::Any;
+use std::cell::Cell;
/// Frame-level uniforms (view, projection).
#[repr(C)]
@@ -27,31 +28,14 @@ struct ObjectUniforms {
/// Per-object GPU data for NormalsMaterial.
pub struct NormalsMaterialGpuData {
- frame_uniform_buffer: wgpu::Buffer,
- object_uniform_buffer: wgpu::Buffer,
+ /// Offset into the shared dynamic object uniform buffer.
+ object_uniform_offset: Option,
}
impl NormalsMaterialGpuData {
pub fn new() -> Self {
- let ctxt = Context::get();
-
- let frame_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("normals_material_frame_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let object_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("normals_material_object_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
Self {
- frame_uniform_buffer,
- object_uniform_buffer,
+ object_uniform_offset: None,
}
}
}
@@ -73,13 +57,32 @@ impl GpuData for NormalsMaterialGpuData {
}
/// A material that draws normals of an object.
+///
+/// ## Performance Optimization
+///
+/// This material uses dynamic uniform buffers to batch uniform data writes:
+/// - Frame uniforms (view, projection) are written once per frame
+/// - Object uniforms are accumulated in a dynamic buffer and flushed once
pub struct NormalsMaterial {
/// Pipeline with backface culling enabled
pipeline_cull: wgpu::RenderPipeline,
/// Pipeline with backface culling disabled
pipeline_no_cull: wgpu::RenderPipeline,
- frame_bind_group_layout: wgpu::BindGroupLayout,
object_bind_group_layout: wgpu::BindGroupLayout,
+
+ // === Dynamic uniform buffer system ===
+ /// Shared frame uniform buffer
+ frame_uniform_buffer: wgpu::Buffer,
+ /// Shared frame bind group
+ frame_bind_group: wgpu::BindGroup,
+ /// Dynamic buffer for object uniforms
+ object_uniform_buffer: DynamicUniformBuffer,
+ /// Bind group for object uniforms (recreated when buffer grows)
+ object_bind_group: Option,
+ /// Frame counter for detecting new frames
+ frame_counter: Cell,
+ /// Last frame we processed
+ last_frame: Cell,
}
impl Default for NormalsMaterial {
@@ -109,6 +112,7 @@ impl NormalsMaterial {
}],
});
+ // Object bind group uses dynamic offset for batched uniforms
let object_bind_group_layout =
ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("normals_material_object_bind_group_layout"),
@@ -117,7 +121,7 @@ impl NormalsMaterial {
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: false,
+ has_dynamic_offset: true, // Enable dynamic offsets!
min_binding_size: None,
},
count: None,
@@ -211,118 +215,184 @@ impl NormalsMaterial {
create_pipeline(Some(wgpu::Face::Back), "normals_material_pipeline_cull");
let pipeline_no_cull = create_pipeline(None, "normals_material_pipeline_no_cull");
- NormalsMaterial {
- pipeline_cull,
- pipeline_no_cull,
- frame_bind_group_layout,
- object_bind_group_layout,
- }
- }
+ // === Create shared dynamic buffer resources ===
- fn create_frame_bind_group(&self, buffer: &wgpu::Buffer) -> wgpu::BindGroup {
- let ctxt = Context::get();
- ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("normals_material_frame_bind_group"),
- layout: &self.frame_bind_group_layout,
+ // Frame uniform buffer (written once per frame)
+ let frame_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("normals_shared_frame_uniform_buffer"),
+ size: std::mem::size_of::() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ // Create frame bind group
+ let frame_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("normals_shared_frame_bind_group"),
+ layout: &frame_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
- resource: buffer.as_entire_binding(),
+ resource: frame_uniform_buffer.as_entire_binding(),
}],
- })
- }
+ });
- fn create_object_bind_group(&self, buffer: &wgpu::Buffer) -> wgpu::BindGroup {
- let ctxt = Context::get();
- ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("normals_material_object_bind_group"),
- layout: &self.object_bind_group_layout,
+ // Dynamic buffer for object uniforms
+ let object_uniform_buffer =
+ DynamicUniformBuffer::::new("normals_dynamic_object_uniform_buffer");
+
+ // Create initial object bind group
+ let object_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("normals_dynamic_object_bind_group"),
+ layout: &object_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
- resource: buffer.as_entire_binding(),
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: object_uniform_buffer.buffer(),
+ offset: 0,
+ size: std::num::NonZeroU64::new(object_uniform_buffer.aligned_size()),
+ }),
}],
- })
+ });
+
+ NormalsMaterial {
+ pipeline_cull,
+ pipeline_no_cull,
+ object_bind_group_layout,
+ frame_uniform_buffer,
+ frame_bind_group,
+ object_uniform_buffer,
+ object_bind_group: Some(object_bind_group),
+ frame_counter: Cell::new(0),
+ last_frame: Cell::new(u64::MAX),
+ }
}
}
-impl Material for NormalsMaterial {
+impl Material3d for NormalsMaterial {
fn create_gpu_data(&self) -> Box {
Box::new(NormalsMaterialGpuData::new())
}
- fn render(
+ fn begin_frame(&mut self) {
+ self.frame_counter
+ .set(self.frame_counter.get().wrapping_add(1));
+ self.object_uniform_buffer.clear();
+ }
+
+ fn flush(&mut self) {
+ let ctxt = Context::get();
+
+ // Flush returns true if buffer was reallocated
+ let buffer_reallocated = self.object_uniform_buffer.flush();
+
+ // Recreate bind group if buffer was reallocated
+ if buffer_reallocated {
+ self.object_bind_group = Some(ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("normals_dynamic_object_bind_group"),
+ layout: &self.object_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: self.object_uniform_buffer.buffer(),
+ offset: 0,
+ size: std::num::NonZeroU64::new(self.object_uniform_buffer.aligned_size()),
+ }),
+ }],
+ }));
+ }
+ }
+
+ fn prepare(
&mut self,
pass: usize,
- transform: &Isometry3,
- scale: &Vector3,
- camera: &mut dyn Camera,
- _: &Light,
- data: &ObjectData,
- mesh: &mut GpuMesh,
- _instances: &mut InstancesBuffer,
+ transform: Pose3,
+ scale: Vec3,
+ camera: &mut dyn Camera3d,
+ _lights: &LightCollection,
+ _data: &ObjectData3d,
gpu_data: &mut dyn GpuData,
- context: &mut RenderContext,
+ _viewport_width: u32,
+ _viewport_height: u32,
) {
let ctxt = Context::get();
- if !data.surface_rendering_active() {
- return;
- }
-
// Downcast gpu_data to our specific type
let gpu_data = gpu_data
.as_any_mut()
.downcast_mut::()
.expect("NormalsMaterial requires NormalsMaterialGpuData");
- // Update frame uniforms
- let (view, proj) = camera.view_transform_pair(pass);
+ // Check if this is a new frame (first object being prepared)
+ let current_frame = self.frame_counter.get();
+ let is_new_frame = current_frame != self.last_frame.get();
- let frame_uniforms = FrameUniforms {
- view: view.to_homogeneous().into(),
- proj: proj.into(),
- };
- ctxt.write_buffer(
- &gpu_data.frame_uniform_buffer,
- 0,
- bytemuck::bytes_of(&frame_uniforms),
- );
+ if is_new_frame {
+ self.last_frame.set(current_frame);
- // Update object uniforms
- let formatted_transform = transform.to_homogeneous();
- let formatted_scale = Matrix3::from_diagonal(&Vector3::new(scale.x, scale.y, scale.z));
+ // Compute frame uniforms and write once per frame
+ let (view, proj) = camera.view_transform_pair(pass);
+ let frame_uniforms = FrameUniforms {
+ view: view.to_mat4().to_cols_array_2d(),
+ proj: proj.to_cols_array_2d(),
+ };
+
+ ctxt.write_buffer(
+ &self.frame_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&frame_uniforms),
+ );
+ }
+
+ // Compute object uniforms
+ let formatted_transform = transform.to_mat4();
+ let formatted_scale = Mat3::from_diagonal(scale);
// Pad mat3x3 to mat3x4 for proper alignment
+ let scale_cols = formatted_scale.to_cols_array_2d();
let scale_padded: [[f32; 4]; 3] = [
- [
- formatted_scale[(0, 0)],
- formatted_scale[(1, 0)],
- formatted_scale[(2, 0)],
- 0.0,
- ],
- [
- formatted_scale[(0, 1)],
- formatted_scale[(1, 1)],
- formatted_scale[(2, 1)],
- 0.0,
- ],
- [
- formatted_scale[(0, 2)],
- formatted_scale[(1, 2)],
- formatted_scale[(2, 2)],
- 0.0,
- ],
+ [scale_cols[0][0], scale_cols[0][1], scale_cols[0][2], 0.0],
+ [scale_cols[1][0], scale_cols[1][1], scale_cols[1][2], 0.0],
+ [scale_cols[2][0], scale_cols[2][1], scale_cols[2][2], 0.0],
];
let object_uniforms = ObjectUniforms {
- transform: formatted_transform.into(),
+ transform: formatted_transform.to_cols_array_2d(),
scale: scale_padded,
_padding: [0.0; 4],
};
- ctxt.write_buffer(
- &gpu_data.object_uniform_buffer,
- 0,
- bytemuck::bytes_of(&object_uniforms),
- );
+
+ // Push to dynamic buffer and store offset in gpu_data
+ let object_offset = self.object_uniform_buffer.push(&object_uniforms);
+ gpu_data.object_uniform_offset = Some(object_offset);
+ }
+
+ fn render(
+ &mut self,
+ _pass: usize,
+ _transform: Pose3,
+ _scale: Vec3,
+ _camera: &mut dyn Camera3d,
+ _lights: &LightCollection,
+ data: &ObjectData3d,
+ mesh: &mut GpuMesh3d,
+ _instances: &mut InstancesBuffer3d,
+ gpu_data: &mut dyn GpuData,
+ render_pass: &mut wgpu::RenderPass<'_>,
+ _context: &RenderContext,
+ ) {
+ if !data.surface_rendering_active() {
+ return;
+ }
+
+ // Downcast gpu_data to our specific type
+ let gpu_data = gpu_data
+ .as_any_mut()
+ .downcast_mut::()
+ .expect("NormalsMaterial requires NormalsMaterialGpuData");
+
+ // Get the pre-computed object uniform offset from prepare() phase
+ let object_offset = gpu_data
+ .object_uniform_offset
+ .expect("prepare() must be called before render()");
// Ensure mesh buffers are on GPU
mesh.coords().write().unwrap().load_to_gpu();
@@ -346,52 +416,24 @@ impl Material for NormalsMaterial {
None => return,
};
- // Create bind groups with per-object buffers
- let frame_bind_group = self.create_frame_bind_group(&gpu_data.frame_uniform_buffer);
- let object_bind_group = self.create_object_bind_group(&gpu_data.object_uniform_buffer);
-
- // Create render pass
- {
- let mut render_pass = context
- .encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("normals_material_render_pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: context.color_view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
- view: context.depth_view,
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- }),
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
- // Select pipeline based on backface culling setting
- let pipeline = if data.backface_culling_enabled() {
- &self.pipeline_cull
- } else {
- &self.pipeline_no_cull
- };
- render_pass.set_pipeline(pipeline);
- render_pass.set_bind_group(0, &frame_bind_group, &[]);
- render_pass.set_bind_group(1, &object_bind_group, &[]);
+ let object_bind_group = self.object_bind_group.as_ref().unwrap();
- render_pass.set_vertex_buffer(0, coords_buf.slice(..));
- render_pass.set_vertex_buffer(1, normals_buf.slice(..));
- render_pass.set_index_buffer(faces_buf.slice(..), VERTEX_INDEX_FORMAT);
+ // Select pipeline based on backface culling setting
+ let pipeline = if data.backface_culling_enabled() {
+ &self.pipeline_cull
+ } else {
+ &self.pipeline_no_cull
+ };
+ render_pass.set_pipeline(pipeline);
+ render_pass.set_bind_group(0, &self.frame_bind_group, &[]);
+ // Use dynamic offset for object uniforms!
+ render_pass.set_bind_group(1, object_bind_group, &[object_offset]);
- render_pass.draw_indexed(0..mesh.num_indices(), 0, 0..1);
- }
+ render_pass.set_vertex_buffer(0, coords_buf.slice(..));
+ render_pass.set_vertex_buffer(1, normals_buf.slice(..));
+ render_pass.set_index_buffer(faces_buf.slice(..), VERTEX_INDEX_FORMAT);
+
+ render_pass.draw_indexed(0..mesh.num_indices(), 0, 0..1);
}
}
diff --git a/src/builtin/planar.wgsl b/src/builtin/object2d.wgsl
similarity index 85%
rename from src/builtin/planar.wgsl
rename to src/builtin/object2d.wgsl
index b87016c88..a05dec4bf 100644
--- a/src/builtin/planar.wgsl
+++ b/src/builtin/object2d.wgsl
@@ -22,8 +22,7 @@ struct ObjectUniforms {
model_2: vec4,
scale_0: vec4,
scale_1: vec4,
- color: vec3,
- _padding: f32,
+ color: vec4,
}
@group(1) @binding(0)
@@ -104,17 +103,9 @@ fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {
return out;
}
-// Convert linear RGB to sRGB for display.
-fn linear_to_srgb(linear: vec3) -> vec3 {
- let cutoff = linear < vec3(0.0031308);
- let lower = linear * 12.92;
- let higher = pow(linear, vec3(1.0 / 2.4)) * 1.055 - vec3(0.055);
- return select(higher, lower, cutoff);
-}
-
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
let tex_color = textureSample(t_diffuse, s_diffuse, in.tex_coord);
- let final_color = tex_color * (vec4(object.color, 1.0) * in.vert_color);
- return vec4(linear_to_srgb(final_color.rgb), final_color.a);
+ let final_color = tex_color * (object.color * in.vert_color);
+ return final_color;
}
diff --git a/src/builtin/object_material.rs b/src/builtin/object_material.rs
index 40b7dd378..59733e06b 100644
--- a/src/builtin/object_material.rs
+++ b/src/builtin/object_material.rs
@@ -1,32 +1,76 @@
-use crate::camera::Camera;
+use crate::camera::Camera3d;
use crate::context::Context;
-use crate::light::Light;
+use crate::light::{LightCollection, LightType, MAX_LIGHTS};
use crate::resource::vertex_index::VERTEX_INDEX_FORMAT;
-use crate::resource::{GpuData, GpuMesh, Material, RenderContext, Texture};
-use crate::scene::{InstancesBuffer, ObjectData};
+use crate::resource::{
+ DynamicUniformBuffer, GpuData, GpuMesh3d, Material3d, RenderContext, Texture,
+};
+use crate::scene::{InstancesBuffer3d, ObjectData3d};
use bytemuck::{Pod, Zeroable};
-use na::{Isometry3, Matrix3, Point3, Vector3};
+use glamx::{Mat3, Pose3, Vec3};
use std::any::Any;
+use std::cell::Cell;
-/// Frame-level uniforms (view, projection, light).
+/// GPU representation of a single light.
+#[repr(C)]
+#[derive(Copy, Clone, Debug, Pod, Zeroable)]
+struct GpuLight {
+ position: [f32; 3],
+ light_type: u32, // 0=point, 1=directional, 2=spot
+ direction: [f32; 3],
+ intensity: f32,
+ color: [f32; 3],
+ inner_cone_cos: f32,
+ outer_cone_cos: f32,
+ attenuation_radius: f32,
+ _padding: [f32; 2],
+}
+
+impl Default for GpuLight {
+ fn default() -> Self {
+ Self {
+ position: [0.0; 3],
+ light_type: 0,
+ direction: [0.0, 0.0, -1.0],
+ intensity: 0.0,
+ color: [1.0, 1.0, 1.0],
+ inner_cone_cos: 1.0,
+ outer_cone_cos: 0.0,
+ attenuation_radius: 100.0,
+ _padding: [0.0; 2],
+ }
+ }
+}
+
+/// Frame-level uniforms (view, projection, lights).
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct FrameUniforms {
view: [[f32; 4]; 4],
proj: [[f32; 4]; 4],
- light_position: [f32; 3],
- _padding: f32,
+ lights: [GpuLight; MAX_LIGHTS],
+ num_lights: u32,
+ ambient_intensity: f32,
+ _padding: [f32; 2],
}
-/// Object-level uniforms (transform, scale, color).
+/// Object-level uniforms (transform, scale, color, PBR properties).
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct ObjectUniforms {
transform: [[f32; 4]; 4],
ntransform: [[f32; 4]; 3], // mat3x3 padded to mat3x4 for alignment
scale: [[f32; 4]; 3], // mat3x3 padded to mat3x4 for alignment
- color: [f32; 3],
- _padding: f32,
+ color: [f32; 4],
+ metallic: f32,
+ roughness: f32,
+ _pad0: [f32; 2],
+ emissive: [f32; 4],
+ // Texture presence flags (0.0 or 1.0 - WGSL doesn't support bool in uniforms)
+ has_normal_map: f32,
+ has_metallic_roughness_map: f32,
+ has_ao_map: f32,
+ has_emissive_map: f32,
}
/// View uniforms for wireframe rendering (includes viewport).
@@ -95,39 +139,42 @@ struct GpuVertex {
/// Per-object GPU data for ObjectMaterial.
///
-/// Each object in the scene has its own instance of this struct,
-/// containing uniform buffers specific to that object.
+/// This struct now only contains wireframe and points rendering data.
+/// The main uniform buffers and shared view uniforms are managed by ObjectMaterial.
pub struct ObjectMaterialGpuData {
- frame_uniform_buffer: wgpu::Buffer,
- object_uniform_buffer: wgpu::Buffer,
- // Cached bind groups (created lazily)
- frame_bind_group: Option,
- object_bind_group: Option,
// Cached texture bind group with pointer to detect texture changes
texture_bind_group: Option,
cached_texture_ptr: usize,
- // Wireframe rendering data
- wireframe_view_uniform_buffer: wgpu::Buffer,
+ /// Offset into the dynamic object uniform buffer, set during prepare() phase.
+ object_uniform_offset: Option,
+ // PBR texture bind group (normal, metallic-roughness, ao, emissive maps)
+ pbr_texture_bind_group: Option,
+ cached_normal_map_ptr: usize,
+ cached_metallic_roughness_map_ptr: usize,
+ cached_ao_map_ptr: usize,
+ cached_emissive_map_ptr: usize,
+ // Wireframe rendering data (model uniforms are per-object)
wireframe_model_uniform_buffer: wgpu::Buffer,
wireframe_edge_buffer: wgpu::Buffer,
wireframe_edge_capacity: usize,
- wireframe_view_bind_group: Option,
wireframe_model_bind_group: Option,
/// Cached wireframe edges in local coordinates (built lazily from mesh).
- wireframe_edges: Option, Point3)>>,
+ wireframe_edges: Option>,
/// Hash of mesh faces to detect when edges need rebuilding.
wireframe_edges_mesh_hash: u64,
- // Point rendering data
- points_view_uniform_buffer: wgpu::Buffer,
+ /// Cached wireframe model uniforms (written during prepare).
+ wireframe_model_uniforms: WireframeModelUniforms,
+ // Point rendering data (model uniforms are per-object)
points_model_uniform_buffer: wgpu::Buffer,
points_vertex_buffer: wgpu::Buffer,
points_vertex_capacity: usize,
- points_view_bind_group: Option,
points_model_bind_group: Option,
/// Cached vertices for point rendering (built lazily from mesh).
- points_vertices: Option>>,
+ points_vertices: Option>,
/// Hash of mesh coords to detect when vertices need rebuilding.
points_vertices_mesh_hash: u64,
+ /// Cached points model uniforms (written during prepare).
+ points_model_uniforms: PointsModelUniforms,
}
impl ObjectMaterialGpuData {
@@ -135,28 +182,7 @@ impl ObjectMaterialGpuData {
pub fn new() -> Self {
let ctxt = Context::get();
- let frame_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("object_material_frame_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let object_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("object_material_object_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- // Wireframe uniform buffers
- let wireframe_view_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("wireframe_view_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
+ // Wireframe model uniform buffer (per-object)
let wireframe_model_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("wireframe_model_uniform_buffer"),
size: std::mem::size_of::() as u64,
@@ -173,14 +199,7 @@ impl ObjectMaterialGpuData {
mapped_at_creation: false,
});
- // Point rendering uniform buffers (reuse same view uniform layout as wireframe)
- let points_view_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("points_view_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
+ // Point model uniform buffer (per-object)
let points_model_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("points_model_uniform_buffer"),
size: std::mem::size_of::() as u64,
@@ -198,28 +217,46 @@ impl ObjectMaterialGpuData {
});
Self {
- frame_uniform_buffer,
- object_uniform_buffer,
- frame_bind_group: None,
- object_bind_group: None,
texture_bind_group: None,
cached_texture_ptr: 0,
- wireframe_view_uniform_buffer,
+ object_uniform_offset: None,
+ // PBR texture caching
+ pbr_texture_bind_group: None,
+ cached_normal_map_ptr: 0,
+ cached_metallic_roughness_map_ptr: 0,
+ cached_ao_map_ptr: 0,
+ cached_emissive_map_ptr: 0,
+ // Wireframe rendering
wireframe_model_uniform_buffer,
wireframe_edge_buffer,
wireframe_edge_capacity,
- wireframe_view_bind_group: None,
wireframe_model_bind_group: None,
wireframe_edges: None,
wireframe_edges_mesh_hash: 0,
- points_view_uniform_buffer,
+ wireframe_model_uniforms: WireframeModelUniforms {
+ transform: [[0.0; 4]; 4],
+ scale: [0.0; 3],
+ num_edges: 0,
+ default_color: [0.0; 4],
+ default_width: 0.0,
+ use_perspective: 0,
+ _padding: [0.0; 2],
+ },
points_model_uniform_buffer,
points_vertex_buffer,
points_vertex_capacity,
- points_view_bind_group: None,
points_model_bind_group: None,
points_vertices: None,
points_vertices_mesh_hash: 0,
+ points_model_uniforms: PointsModelUniforms {
+ transform: [[0.0; 4]; 4],
+ scale: [0.0; 3],
+ num_vertices: 0,
+ default_color: [0.0; 4],
+ default_size: 0.0,
+ use_perspective: 0,
+ _padding: [0.0; 2],
+ },
}
}
@@ -276,25 +313,63 @@ impl GpuData for ObjectMaterialGpuData {
/// The default material used to draw objects.
///
-/// This struct holds shared resources (pipeline, bind group layouts) that
-/// are used by all objects. Per-object resources are stored in
-/// `ObjectMaterialGpuData` instances.
+/// This struct holds shared resources (pipeline, bind group layouts, dynamic buffers)
+/// that are used by all objects. Per-object resources for wireframe/points are stored
+/// in `ObjectMaterialGpuData` instances.
+///
+/// ## Performance Optimization
+///
+/// This material uses dynamic uniform buffers to batch uniform data writes:
+/// - Frame uniforms (view, projection, light) are written once per frame
+/// - Object uniforms are accumulated in a dynamic buffer and flushed once
+/// - Wireframe/points view uniforms (view, proj, viewport) are shared and written once per frame
+/// - This significantly reduces the number of `write_buffer` calls per frame
pub struct ObjectMaterial {
/// Pipeline with backface culling enabled
pipeline_cull: wgpu::RenderPipeline,
/// Pipeline with backface culling disabled
pipeline_no_cull: wgpu::RenderPipeline,
- frame_bind_group_layout: wgpu::BindGroupLayout,
object_bind_group_layout: wgpu::BindGroupLayout,
texture_bind_group_layout: wgpu::BindGroupLayout,
+ /// PBR texture bind group layout (normal, metallic-roughness, ao, emissive maps)
+ pbr_texture_bind_group_layout: wgpu::BindGroupLayout,
+ /// Default PBR textures for when user hasn't set any
+ default_normal_map: std::sync::Arc,
+ default_metallic_roughness_map: std::sync::Arc,
+ default_ao_map: std::sync::Arc,
+ default_emissive_map: std::sync::Arc,
// Wireframe rendering resources
wireframe_pipeline: wgpu::RenderPipeline,
- wireframe_view_bind_group_layout: wgpu::BindGroupLayout,
wireframe_model_bind_group_layout: wgpu::BindGroupLayout,
// Point rendering resources
points_pipeline: wgpu::RenderPipeline,
- points_view_bind_group_layout: wgpu::BindGroupLayout,
points_model_bind_group_layout: wgpu::BindGroupLayout,
+
+ // === Dynamic uniform buffer system ===
+ /// Shared frame uniform buffer (view, projection, light)
+ frame_uniform_buffer: wgpu::Buffer,
+ /// Shared bind group for frame uniforms
+ frame_bind_group: wgpu::BindGroup,
+ /// Dynamic buffer for object uniforms
+ object_uniform_buffer: DynamicUniformBuffer,
+ /// Bind group for object uniforms (recreated when buffer grows)
+ object_bind_group: Option,
+ /// Capacity when bind group was last created (to detect regrowth)
+ object_bind_group_capacity: u64,
+ /// Frame counter for detecting new frames
+ frame_counter: Cell,
+ /// Last frame we processed (to detect new frame)
+ last_frame: Cell,
+
+ // === Shared wireframe/points view uniforms ===
+ /// Shared wireframe view uniform buffer (view, proj, viewport - same for all objects)
+ wireframe_view_uniform_buffer: wgpu::Buffer,
+ /// Shared wireframe view bind group
+ wireframe_view_bind_group: wgpu::BindGroup,
+ /// Shared points view uniform buffer (view, proj, viewport - same for all objects)
+ points_view_uniform_buffer: wgpu::Buffer,
+ /// Shared points view bind group
+ points_view_bind_group: wgpu::BindGroup,
}
impl Default for ObjectMaterial {
@@ -324,6 +399,7 @@ impl ObjectMaterial {
}],
});
+ // Object bind group uses dynamic offset for batched uniforms
let object_bind_group_layout =
ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("object_material_object_bind_group_layout"),
@@ -332,7 +408,7 @@ impl ObjectMaterial {
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: false,
+ has_dynamic_offset: true, // Enable dynamic offsets!
min_binding_size: None,
},
count: None,
@@ -362,12 +438,96 @@ impl ObjectMaterial {
],
});
+ // PBR texture bind group layout (group 3): normal, metallic-roughness, ao, emissive maps
+ let pbr_texture_bind_group_layout =
+ ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("object_material_pbr_texture_bind_group_layout"),
+ entries: &[
+ // Normal map
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ // Metallic-roughness map
+ wgpu::BindGroupLayoutEntry {
+ binding: 2,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 3,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ // Ambient occlusion map
+ wgpu::BindGroupLayoutEntry {
+ binding: 4,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 5,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ // Emissive map
+ wgpu::BindGroupLayoutEntry {
+ binding: 6,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 7,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+
+ // Create default PBR textures
+ let default_normal_map = crate::resource::Texture::new_default_normal_map();
+ let default_metallic_roughness_map =
+ crate::resource::Texture::new_default_metallic_roughness_map();
+ let default_ao_map = crate::resource::Texture::new_default_ao_map();
+ let default_emissive_map = crate::resource::Texture::new_default_emissive_map();
+
let pipeline_layout = ctxt.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("object_material_pipeline_layout"),
bind_group_layouts: &[
&frame_bind_group_layout,
&object_bind_group_layout,
&texture_bind_group_layout,
+ &pbr_texture_bind_group_layout,
],
push_constant_ranges: &[],
});
@@ -641,7 +801,7 @@ impl ObjectMaterial {
// Load wireframe polyline shader
let wireframe_polyline_shader = ctxt.create_shader_module(
Some("wireframe_polyline_shader"),
- include_str!("wireframe_polyline.wgsl"),
+ include_str!("wireframe_polyline3d.wgsl"),
);
// Instance vertex buffer layouts for wireframe (matching InstancesBuffer)
@@ -815,7 +975,7 @@ impl ObjectMaterial {
// Load points shader
let points_shader = ctxt.create_shader_module(
Some("wireframe_points_shader"),
- include_str!("wireframe_points.wgsl"),
+ include_str!("wireframe_points3d.wgsl"),
);
// Instance vertex buffer layouts for points (similar to wireframe but with points_colors/sizes)
@@ -928,45 +1088,110 @@ impl ObjectMaterial {
cache: None,
});
+ // === Create shared dynamic buffer resources ===
+
+ // Frame uniform buffer (written once per frame)
+ let frame_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("shared_frame_uniform_buffer"),
+ size: std::mem::size_of::() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ // Create frame bind group
+ let frame_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("shared_frame_bind_group"),
+ layout: &frame_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: frame_uniform_buffer.as_entire_binding(),
+ }],
+ });
+
+ // Dynamic buffer for object uniforms
+ let object_uniform_buffer =
+ DynamicUniformBuffer::::new("dynamic_object_uniform_buffer");
+
+ // Create initial object bind group
+ let object_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("dynamic_object_bind_group"),
+ layout: &object_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: object_uniform_buffer.buffer(),
+ offset: 0,
+ size: std::num::NonZeroU64::new(object_uniform_buffer.aligned_size()),
+ }),
+ }],
+ });
+
+ // Get capacity before moving into struct
+ let object_bind_group_capacity = object_uniform_buffer.capacity();
+
+ // === Shared wireframe/points view uniform buffers ===
+ // These contain view, proj, and viewport which are the same for all objects in a frame
+
+ let wireframe_view_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("shared_wireframe_view_uniform_buffer"),
+ size: std::mem::size_of::() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let wireframe_view_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("shared_wireframe_view_bind_group"),
+ layout: &wireframe_view_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wireframe_view_uniform_buffer.as_entire_binding(),
+ }],
+ });
+
+ let points_view_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("shared_points_view_uniform_buffer"),
+ size: std::mem::size_of::() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let points_view_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("shared_points_view_bind_group"),
+ layout: &points_view_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: points_view_uniform_buffer.as_entire_binding(),
+ }],
+ });
+
ObjectMaterial {
pipeline_cull,
pipeline_no_cull,
- frame_bind_group_layout,
object_bind_group_layout,
texture_bind_group_layout,
+ pbr_texture_bind_group_layout,
+ default_normal_map,
+ default_metallic_roughness_map,
+ default_ao_map,
+ default_emissive_map,
wireframe_pipeline,
- wireframe_view_bind_group_layout,
wireframe_model_bind_group_layout,
points_pipeline,
- points_view_bind_group_layout,
points_model_bind_group_layout,
+ frame_uniform_buffer,
+ frame_bind_group,
+ object_uniform_buffer,
+ object_bind_group: Some(object_bind_group),
+ object_bind_group_capacity,
+ frame_counter: Cell::new(0),
+ last_frame: Cell::new(u64::MAX),
+ wireframe_view_uniform_buffer,
+ wireframe_view_bind_group,
+ points_view_uniform_buffer,
+ points_view_bind_group,
}
}
- fn create_frame_bind_group(&self, buffer: &wgpu::Buffer) -> wgpu::BindGroup {
- let ctxt = Context::get();
- ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("object_material_frame_bind_group"),
- layout: &self.frame_bind_group_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: buffer.as_entire_binding(),
- }],
- })
- }
-
- fn create_object_bind_group(&self, buffer: &wgpu::Buffer) -> wgpu::BindGroup {
- let ctxt = Context::get();
- ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("object_material_object_bind_group"),
- layout: &self.object_bind_group_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: buffer.as_entire_binding(),
- }],
- })
- }
-
fn create_texture_bind_group(&self, texture: &Texture) -> wgpu::BindGroup {
let ctxt = Context::get();
ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
@@ -985,15 +1210,55 @@ impl ObjectMaterial {
})
}
- fn create_wireframe_view_bind_group(&self, buffer: &wgpu::Buffer) -> wgpu::BindGroup {
+ fn create_pbr_texture_bind_group(
+ &self,
+ normal_map: &Texture,
+ metallic_roughness_map: &Texture,
+ ao_map: &Texture,
+ emissive_map: &Texture,
+ ) -> wgpu::BindGroup {
let ctxt = Context::get();
ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("wireframe_view_bind_group"),
- layout: &self.wireframe_view_bind_group_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: buffer.as_entire_binding(),
- }],
+ label: Some("object_material_pbr_texture_bind_group"),
+ layout: &self.pbr_texture_bind_group_layout,
+ entries: &[
+ // Normal map
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&normal_map.view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(&normal_map.sampler),
+ },
+ // Metallic-roughness map
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: wgpu::BindingResource::TextureView(&metallic_roughness_map.view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 3,
+ resource: wgpu::BindingResource::Sampler(&metallic_roughness_map.sampler),
+ },
+ // Ambient occlusion map
+ wgpu::BindGroupEntry {
+ binding: 4,
+ resource: wgpu::BindingResource::TextureView(&ao_map.view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 5,
+ resource: wgpu::BindingResource::Sampler(&ao_map.sampler),
+ },
+ // Emissive map
+ wgpu::BindGroupEntry {
+ binding: 6,
+ resource: wgpu::BindingResource::TextureView(&emissive_map.view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 7,
+ resource: wgpu::BindingResource::Sampler(&emissive_map.sampler),
+ },
+ ],
})
}
@@ -1024,18 +1289,6 @@ impl ObjectMaterial {
})
}
- fn create_points_view_bind_group(&self, buffer: &wgpu::Buffer) -> wgpu::BindGroup {
- let ctxt = Context::get();
- ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("points_view_bind_group"),
- layout: &self.points_view_bind_group_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: buffer.as_entire_binding(),
- }],
- })
- }
-
fn create_points_model_bind_group(
&self,
model_buffer: &wgpu::Buffer,
@@ -1062,123 +1315,349 @@ impl ObjectMaterial {
],
})
}
+
+ /// Signals the start of a new frame.
+ ///
+ /// This clears the dynamic object uniform buffer and resets the frame counter.
+ /// Should be called before rendering any objects for a new frame.
+ pub fn begin_frame(&mut self) {
+ self.frame_counter
+ .set(self.frame_counter.get().wrapping_add(1));
+ self.object_uniform_buffer.clear();
+ }
+
+ /// Flushes the accumulated object uniforms to the GPU.
+ ///
+ /// This performs a single `write_buffer` call with all accumulated object data.
+ /// Should be called after all objects have been processed for the frame.
+ pub fn flush(&mut self) {
+ let ctxt = Context::get();
+
+ self.object_uniform_buffer.flush();
+
+ // Recreate bind group if buffer grew
+ if self.object_uniform_buffer.capacity() != self.object_bind_group_capacity {
+ self.object_bind_group = Some(ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("dynamic_object_bind_group"),
+ layout: &self.object_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: self.object_uniform_buffer.buffer(),
+ offset: 0,
+ size: std::num::NonZeroU64::new(self.object_uniform_buffer.aligned_size()),
+ }),
+ }],
+ }));
+ self.object_bind_group_capacity = self.object_uniform_buffer.capacity();
+ }
+ }
}
-impl Material for ObjectMaterial {
+impl Material3d for ObjectMaterial {
fn create_gpu_data(&self) -> Box {
Box::new(ObjectMaterialGpuData::new())
}
- fn render(
+ fn begin_frame(&mut self) {
+ self.frame_counter
+ .set(self.frame_counter.get().wrapping_add(1));
+ self.object_uniform_buffer.clear();
+ }
+
+ fn prepare(
&mut self,
pass: usize,
- transform: &Isometry3,
- scale: &Vector3,
- camera: &mut dyn Camera,
- light: &Light,
- data: &ObjectData,
- mesh: &mut GpuMesh,
- instances: &mut InstancesBuffer,
+ transform: Pose3,
+ scale: Vec3,
+ camera: &mut dyn Camera3d,
+ lights: &LightCollection,
+ data: &ObjectData3d,
gpu_data: &mut dyn GpuData,
- context: &mut RenderContext,
+ viewport_width: u32,
+ viewport_height: u32,
) {
let ctxt = Context::get();
- let render_surface = data.surface_rendering_active();
- let render_wireframe = data.lines_width() > 0.0;
- let render_points = data.points_size() > 0.0;
-
- // Nothing to render
- if !render_surface && !render_wireframe && !render_points {
- return;
- }
-
// Downcast gpu_data to our specific type
let gpu_data = gpu_data
.as_any_mut()
.downcast_mut::()
.expect("ObjectMaterial requires ObjectMaterialGpuData");
- // Create frame uniforms and write to per-object buffer
- let (view, proj) = camera.view_transform_pair(pass);
- let light_pos = match light {
- Light::Absolute(p) => *p,
- Light::StickToCamera => camera.eye(),
- };
+ // Check if this is a new frame (first object being prepared)
+ let current_frame = self.frame_counter.get();
+ let is_new_frame = current_frame != self.last_frame.get();
- let frame_uniforms = FrameUniforms {
- view: view.to_homogeneous().into(),
- proj: proj.into(),
- light_position: light_pos.coords.into(),
- _padding: 0.0,
- };
+ if is_new_frame {
+ self.last_frame.set(current_frame);
- ctxt.write_buffer(
- &gpu_data.frame_uniform_buffer,
- 0,
- bytemuck::bytes_of(&frame_uniforms),
- );
+ // Write frame uniforms once per frame
+ let (view, proj) = camera.view_transform_pair(pass);
+
+ // Convert collected lights to GPU format
+ let mut gpu_lights: [GpuLight; MAX_LIGHTS] = [GpuLight::default(); MAX_LIGHTS];
+ for (i, collected_light) in lights.lights.iter().take(MAX_LIGHTS).enumerate() {
+ let (light_type, inner_cone_cos, outer_cone_cos, attenuation_radius) =
+ match &collected_light.light_type {
+ LightType::Point { attenuation_radius } => {
+ (0u32, 1.0, 0.0, *attenuation_radius)
+ }
+ LightType::Directional(_) => (1u32, 1.0, 0.0, 0.0),
+ LightType::Spot {
+ inner_cone_angle,
+ outer_cone_angle,
+ attenuation_radius,
+ } => (
+ 2u32,
+ inner_cone_angle.cos(),
+ outer_cone_angle.cos(),
+ *attenuation_radius,
+ ),
+ };
+
+ gpu_lights[i] = GpuLight {
+ position: collected_light.world_position.into(),
+ light_type,
+ direction: collected_light.world_direction.into(),
+ intensity: collected_light.intensity,
+ color: collected_light.color.into(),
+ inner_cone_cos,
+ outer_cone_cos,
+ attenuation_radius,
+ _padding: [0.0; 2],
+ };
+ }
- // Create object uniforms and write to per-object buffer
- let formatted_transform = transform.to_homogeneous();
- let ntransform = transform.rotation.to_rotation_matrix().into_inner();
- let formatted_scale = Matrix3::from_diagonal(&Vector3::new(scale.x, scale.y, scale.z));
+ let frame_uniforms = FrameUniforms {
+ view: view.to_mat4().to_cols_array_2d(),
+ proj: proj.to_cols_array_2d(),
+ lights: gpu_lights,
+ num_lights: lights.lights.len().min(MAX_LIGHTS) as u32,
+ ambient_intensity: lights.ambient,
+ _padding: [0.0; 2],
+ };
+
+ ctxt.write_buffer(
+ &self.frame_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&frame_uniforms),
+ );
+
+ // Write shared wireframe/points view uniforms once per frame
+ // These contain view, proj, viewport which are same for all objects
+ let wireframe_view_uniforms = WireframeViewUniforms {
+ view: view.to_mat4().to_cols_array_2d(),
+ proj: proj.to_cols_array_2d(),
+ viewport: [0.0, 0.0, viewport_width as f32, viewport_height as f32],
+ };
+
+ ctxt.write_buffer(
+ &self.wireframe_view_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&wireframe_view_uniforms),
+ );
+
+ // Points use the same view uniform layout
+ ctxt.write_buffer(
+ &self.points_view_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&wireframe_view_uniforms),
+ );
+ }
+
+ // Create object uniforms
+ let formatted_transform = transform.to_mat4();
+ let ntransform = Mat3::from_quat(transform.rotation);
+ let formatted_scale = Mat3::from_diagonal(scale);
// Pad mat3x3 to mat3x4 for proper alignment
+ let ntransform_cols = ntransform.to_cols_array_2d();
let ntransform_padded: [[f32; 4]; 3] = [
[
- ntransform[(0, 0)],
- ntransform[(1, 0)],
- ntransform[(2, 0)],
+ ntransform_cols[0][0],
+ ntransform_cols[0][1],
+ ntransform_cols[0][2],
0.0,
],
[
- ntransform[(0, 1)],
- ntransform[(1, 1)],
- ntransform[(2, 1)],
+ ntransform_cols[1][0],
+ ntransform_cols[1][1],
+ ntransform_cols[1][2],
0.0,
],
[
- ntransform[(0, 2)],
- ntransform[(1, 2)],
- ntransform[(2, 2)],
+ ntransform_cols[2][0],
+ ntransform_cols[2][1],
+ ntransform_cols[2][2],
0.0,
],
];
+ let scale_cols = formatted_scale.to_cols_array_2d();
let scale_padded: [[f32; 4]; 3] = [
- [
- formatted_scale[(0, 0)],
- formatted_scale[(1, 0)],
- formatted_scale[(2, 0)],
- 0.0,
- ],
- [
- formatted_scale[(0, 1)],
- formatted_scale[(1, 1)],
- formatted_scale[(2, 1)],
- 0.0,
- ],
- [
- formatted_scale[(0, 2)],
- formatted_scale[(1, 2)],
- formatted_scale[(2, 2)],
- 0.0,
- ],
+ [scale_cols[0][0], scale_cols[0][1], scale_cols[0][2], 0.0],
+ [scale_cols[1][0], scale_cols[1][1], scale_cols[1][2], 0.0],
+ [scale_cols[2][0], scale_cols[2][1], scale_cols[2][2], 0.0],
];
+ let color = data.color();
+ let emissive = data.emissive();
let object_uniforms = ObjectUniforms {
- transform: formatted_transform.into(),
+ transform: formatted_transform.to_cols_array_2d(),
ntransform: ntransform_padded,
scale: scale_padded,
- color: (*data.color()).into(),
- _padding: 0.0,
+ color: [color.r, color.g, color.b, color.a],
+ metallic: data.metallic(),
+ roughness: data.roughness(),
+ _pad0: [0.0; 2],
+ emissive: [emissive.r, emissive.g, emissive.b, emissive.a],
+ has_normal_map: if data.normal_map().is_some() {
+ 1.0
+ } else {
+ 0.0
+ },
+ has_metallic_roughness_map: if data.metallic_roughness_map().is_some() {
+ 1.0
+ } else {
+ 0.0
+ },
+ has_ao_map: if data.ao_map().is_some() { 1.0 } else { 0.0 },
+ has_emissive_map: if data.emissive_map().is_some() {
+ 1.0
+ } else {
+ 0.0
+ },
};
- ctxt.write_buffer(
- &gpu_data.object_uniform_buffer,
- 0,
- bytemuck::bytes_of(&object_uniforms),
- );
+ // Push to dynamic buffer and store offset in gpu_data
+ let object_offset = self.object_uniform_buffer.push(&object_uniforms);
+ gpu_data.object_uniform_offset = Some(object_offset);
+
+ // Prepare wireframe model uniforms if needed (view uniforms are shared)
+ let render_wireframe = data.lines_width() > 0.0;
+ if render_wireframe {
+ // Compute model uniforms (num_edges will be set in render when mesh is available)
+ let wireframe_color = data.lines_color().unwrap_or(data.color());
+ let cached_num_edges = gpu_data
+ .wireframe_edges
+ .as_ref()
+ .map(|e| e.len())
+ .unwrap_or(0) as u32;
+ gpu_data.wireframe_model_uniforms = WireframeModelUniforms {
+ transform: formatted_transform.to_cols_array_2d(),
+ scale: scale.into(),
+ num_edges: cached_num_edges,
+ default_color: [
+ wireframe_color.r,
+ wireframe_color.g,
+ wireframe_color.b,
+ wireframe_color.a,
+ ],
+ default_width: data.lines_width(),
+ use_perspective: if data.lines_use_perspective() { 1 } else { 0 },
+ _padding: [0.0; 2],
+ };
+
+ // Write model uniforms to GPU (view uniforms are shared and written once per frame)
+ ctxt.write_buffer(
+ &gpu_data.wireframe_model_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&gpu_data.wireframe_model_uniforms),
+ );
+ }
+
+ // Prepare points model uniforms if needed (view uniforms are shared)
+ let render_points = data.points_size() > 0.0;
+ if render_points {
+ // Compute model uniforms (num_vertices will be set in render when mesh is available)
+ let points_color = data.points_color().unwrap_or(data.color());
+ let cached_num_vertices = gpu_data
+ .points_vertices
+ .as_ref()
+ .map(|v| v.len())
+ .unwrap_or(0) as u32;
+ gpu_data.points_model_uniforms = PointsModelUniforms {
+ transform: formatted_transform.to_cols_array_2d(),
+ scale: scale.into(),
+ num_vertices: cached_num_vertices,
+ default_color: [
+ points_color.r,
+ points_color.g,
+ points_color.b,
+ points_color.a,
+ ],
+ default_size: data.points_size(),
+ use_perspective: if data.points_use_perspective() { 1 } else { 0 },
+ _padding: [0.0; 2],
+ };
+
+ // Write model uniforms to GPU (view uniforms are shared and written once per frame)
+ ctxt.write_buffer(
+ &gpu_data.points_model_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&gpu_data.points_model_uniforms),
+ );
+ }
+ }
+
+ fn flush(&mut self) {
+ let ctxt = Context::get();
+
+ self.object_uniform_buffer.flush();
+
+ // Recreate bind group if buffer grew
+ if self.object_uniform_buffer.capacity() != self.object_bind_group_capacity {
+ self.object_bind_group = Some(ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("dynamic_object_bind_group"),
+ layout: &self.object_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
+ buffer: self.object_uniform_buffer.buffer(),
+ offset: 0,
+ size: std::num::NonZeroU64::new(self.object_uniform_buffer.aligned_size()),
+ }),
+ }],
+ }));
+ self.object_bind_group_capacity = self.object_uniform_buffer.capacity();
+ }
+ }
+
+ fn render(
+ &mut self,
+ _pass: usize,
+ _transform: Pose3,
+ _scale: Vec3,
+ _camera: &mut dyn Camera3d,
+ _lights: &LightCollection,
+ data: &ObjectData3d,
+ mesh: &mut GpuMesh3d,
+ instances: &mut InstancesBuffer3d,
+ gpu_data: &mut dyn GpuData,
+ render_pass: &mut wgpu::RenderPass<'_>,
+ _context: &RenderContext,
+ ) {
+ let ctxt = Context::get();
+
+ let render_surface = data.surface_rendering_active();
+ let render_wireframe = data.lines_width() > 0.0;
+ let render_points = data.points_size() > 0.0;
+
+ // Nothing to render
+ if !render_surface && !render_wireframe && !render_points {
+ return;
+ }
+
+ // Downcast gpu_data to our specific type
+ let gpu_data = gpu_data
+ .as_any_mut()
+ .downcast_mut::()
+ .expect("ObjectMaterial requires ObjectMaterialGpuData");
+
+ // Get the pre-computed object uniform offset from prepare() phase
+ let object_offset = gpu_data
+ .object_uniform_offset
+ .expect("prepare() must be called before render()");
// Load instance data directly to GPU without conversion
let num_instances = instances.len();
@@ -1228,16 +1707,6 @@ impl Material for ObjectMaterial {
None => return,
};
- // Get or create cached bind groups
- if gpu_data.frame_bind_group.is_none() {
- gpu_data.frame_bind_group =
- Some(self.create_frame_bind_group(&gpu_data.frame_uniform_buffer));
- }
- if gpu_data.object_bind_group.is_none() {
- gpu_data.object_bind_group =
- Some(self.create_object_bind_group(&gpu_data.object_uniform_buffer));
- }
-
// Cache texture bind group, invalidate if texture changed
let texture_ptr = std::sync::Arc::as_ptr(data.texture()) as usize;
if gpu_data.texture_bind_group.is_none() || gpu_data.cached_texture_ptr != texture_ptr {
@@ -1245,34 +1714,43 @@ impl Material for ObjectMaterial {
gpu_data.cached_texture_ptr = texture_ptr;
}
+ // Cache PBR texture bind group, invalidate if any PBR texture changed
+ let normal_map = data.normal_map().unwrap_or(&self.default_normal_map);
+ let metallic_roughness_map = data
+ .metallic_roughness_map()
+ .unwrap_or(&self.default_metallic_roughness_map);
+ let ao_map = data.ao_map().unwrap_or(&self.default_ao_map);
+ let emissive_map = data.emissive_map().unwrap_or(&self.default_emissive_map);
+
+ let normal_ptr = std::sync::Arc::as_ptr(normal_map) as usize;
+ let mr_ptr = std::sync::Arc::as_ptr(metallic_roughness_map) as usize;
+ let ao_ptr = std::sync::Arc::as_ptr(ao_map) as usize;
+ let emissive_ptr = std::sync::Arc::as_ptr(emissive_map) as usize;
+
+ let pbr_textures_changed = gpu_data.pbr_texture_bind_group.is_none()
+ || gpu_data.cached_normal_map_ptr != normal_ptr
+ || gpu_data.cached_metallic_roughness_map_ptr != mr_ptr
+ || gpu_data.cached_ao_map_ptr != ao_ptr
+ || gpu_data.cached_emissive_map_ptr != emissive_ptr;
+
+ if pbr_textures_changed {
+ gpu_data.pbr_texture_bind_group = Some(self.create_pbr_texture_bind_group(
+ normal_map,
+ metallic_roughness_map,
+ ao_map,
+ emissive_map,
+ ));
+ gpu_data.cached_normal_map_ptr = normal_ptr;
+ gpu_data.cached_metallic_roughness_map_ptr = mr_ptr;
+ gpu_data.cached_ao_map_ptr = ao_ptr;
+ gpu_data.cached_emissive_map_ptr = emissive_ptr;
+ }
+
// Render surface (filled triangles)
if render_surface {
- let frame_bind_group = gpu_data.frame_bind_group.as_ref().unwrap();
- let object_bind_group = gpu_data.object_bind_group.as_ref().unwrap();
let texture_bind_group = gpu_data.texture_bind_group.as_ref().unwrap();
- let mut render_pass = context
- .encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("object_material_render_pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: context.color_view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
- view: context.depth_view,
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- }),
- timestamp_writes: None,
- occlusion_query_set: None,
- });
+ let pbr_texture_bind_group = gpu_data.pbr_texture_bind_group.as_ref().unwrap();
+ let object_bind_group = self.object_bind_group.as_ref().unwrap();
// Select pipeline based on backface culling setting
let pipeline = if data.backface_culling_enabled() {
@@ -1281,9 +1759,11 @@ impl Material for ObjectMaterial {
&self.pipeline_no_cull
};
render_pass.set_pipeline(pipeline);
- render_pass.set_bind_group(0, frame_bind_group, &[]);
- render_pass.set_bind_group(1, object_bind_group, &[]);
+ render_pass.set_bind_group(0, &self.frame_bind_group, &[]);
+ // Use dynamic offset for object uniforms!
+ render_pass.set_bind_group(1, object_bind_group, &[object_offset]);
render_pass.set_bind_group(2, texture_bind_group, &[]);
+ render_pass.set_bind_group(3, pbr_texture_bind_group, &[]);
// Set vertex buffers for mesh data
render_pass.set_vertex_buffer(0, coords_buf.slice(..));
@@ -1316,9 +1796,9 @@ impl Material for ObjectMaterial {
if let (Some(coords), Some(faces)) = (coords_guard.data(), faces_guard.data()) {
let mut edges = Vec::with_capacity(faces.len() * 3);
for face in faces.iter() {
- let idx_a = face.x as usize;
- let idx_b = face.y as usize;
- let idx_c = face.z as usize;
+ let idx_a = face[0] as usize;
+ let idx_b = face[1] as usize;
+ let idx_c = face[2] as usize;
if idx_a < coords.len() && idx_b < coords.len() && idx_c < coords.len() {
edges.push((coords[idx_a], coords[idx_b]));
@@ -1346,9 +1826,9 @@ impl Material for ObjectMaterial {
let gpu_e: Vec = edges
.iter()
.map(|(a, b)| GpuEdge {
- point_a: a.coords.into(),
+ point_a: (*a).into(),
_pad_a: 0.0,
- point_b: b.coords.into(),
+ point_b: (*b).into(),
_pad_b: 0.0,
})
.collect();
@@ -1370,7 +1850,7 @@ impl Material for ObjectMaterial {
None => return,
};
- // Ensure edge buffer capacity and upload edges
+ // Ensure edge buffer capacity and upload edges (geometry data)
gpu_data.ensure_edge_buffer_capacity(num_edges);
ctxt.write_buffer(
@@ -1379,47 +1859,17 @@ impl Material for ObjectMaterial {
bytemuck::cast_slice(&gpu_edges),
);
- // Update wireframe view uniforms
- let wireframe_view_uniforms = WireframeViewUniforms {
- view: view.to_homogeneous().into(),
- proj: proj.into(),
- viewport: [
- 0.0,
- 0.0,
- context.viewport_width as f32,
- context.viewport_height as f32,
- ],
- };
- ctxt.write_buffer(
- &gpu_data.wireframe_view_uniform_buffer,
- 0,
- bytemuck::bytes_of(&wireframe_view_uniforms),
- );
-
- // Update wireframe model uniforms
- let wireframe_color = data.lines_color().unwrap_or(data.color());
- let wireframe_model_uniforms = WireframeModelUniforms {
- transform: formatted_transform.into(),
- scale: (*scale).into(),
- num_edges: num_edges as u32,
- default_color: [wireframe_color.x, wireframe_color.y, wireframe_color.z, 1.0],
- default_width: data.lines_width(),
- use_perspective: if data.lines_use_perspective() { 1 } else { 0 },
- _padding: [0.0; 2],
- };
- ctxt.write_buffer(
- &gpu_data.wireframe_model_uniform_buffer,
- 0,
- bytemuck::bytes_of(&wireframe_model_uniforms),
- );
-
- // Get or create wireframe bind groups
- if gpu_data.wireframe_view_bind_group.is_none() {
- gpu_data.wireframe_view_bind_group =
- Some(self.create_wireframe_view_bind_group(
- &gpu_data.wireframe_view_uniform_buffer,
- ));
+ // Update num_edges in model uniforms if it changed from prepare()
+ if gpu_data.wireframe_model_uniforms.num_edges != num_edges as u32 {
+ gpu_data.wireframe_model_uniforms.num_edges = num_edges as u32;
+ ctxt.write_buffer(
+ &gpu_data.wireframe_model_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&gpu_data.wireframe_model_uniforms),
+ );
}
+
+ // Get or create wireframe model bind group (view bind group is shared)
if gpu_data.wireframe_model_bind_group.is_none() {
let edge_size = (num_edges * std::mem::size_of::()) as u64;
gpu_data.wireframe_model_bind_group =
@@ -1430,41 +1880,12 @@ impl Material for ObjectMaterial {
));
}
- let wireframe_view_bind_group =
- gpu_data.wireframe_view_bind_group.as_ref().unwrap();
let wireframe_model_bind_group =
gpu_data.wireframe_model_bind_group.as_ref().unwrap();
- // Begin wireframe render pass
- let mut render_pass =
- context
- .encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("wireframe_render_pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: context.color_view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: Some(
- wgpu::RenderPassDepthStencilAttachment {
- view: context.depth_view,
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- },
- ),
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
render_pass.set_pipeline(&self.wireframe_pipeline);
- render_pass.set_bind_group(0, wireframe_view_bind_group, &[]);
+ // Use shared view bind group (written once per frame)
+ render_pass.set_bind_group(0, &self.wireframe_view_bind_group, &[]);
render_pass.set_bind_group(1, wireframe_model_bind_group, &[]);
// Set instance vertex buffers (5 total: positions, colors, deformations, lines_colors, lines_widths)
@@ -1519,7 +1940,7 @@ impl Material for ObjectMaterial {
let gpu_v: Vec = vertices
.iter()
.map(|p| GpuVertex {
- position: p.coords.into(),
+ position: (*p).into(),
_pad: 0.0,
})
.collect();
@@ -1541,7 +1962,7 @@ impl Material for ObjectMaterial {
None => return,
};
- // Ensure vertex buffer capacity and upload vertices
+ // Ensure vertex buffer capacity and upload vertices (geometry data)
gpu_data.ensure_vertex_buffer_capacity(num_vertices);
ctxt.write_buffer(
@@ -1550,46 +1971,17 @@ impl Material for ObjectMaterial {
bytemuck::cast_slice(&gpu_vertices),
);
- // Update points view uniforms (same format as wireframe)
- let points_view_uniforms = WireframeViewUniforms {
- view: view.to_homogeneous().into(),
- proj: proj.into(),
- viewport: [
- 0.0,
- 0.0,
- context.viewport_width as f32,
- context.viewport_height as f32,
- ],
- };
- ctxt.write_buffer(
- &gpu_data.points_view_uniform_buffer,
- 0,
- bytemuck::bytes_of(&points_view_uniforms),
- );
-
- // Update points model uniforms
- let points_color = data.points_color().unwrap_or(data.color());
- let points_model_uniforms = PointsModelUniforms {
- transform: formatted_transform.into(),
- scale: (*scale).into(),
- num_vertices: num_vertices as u32,
- default_color: [points_color.x, points_color.y, points_color.z, 1.0],
- default_size: data.points_size(),
- use_perspective: if data.points_use_perspective() { 1 } else { 0 },
- _padding: [0.0; 2],
- };
- ctxt.write_buffer(
- &gpu_data.points_model_uniform_buffer,
- 0,
- bytemuck::bytes_of(&points_model_uniforms),
- );
-
- // Get or create points bind groups
- if gpu_data.points_view_bind_group.is_none() {
- gpu_data.points_view_bind_group = Some(
- self.create_points_view_bind_group(&gpu_data.points_view_uniform_buffer),
+ // Update num_vertices in model uniforms if it changed from prepare()
+ if gpu_data.points_model_uniforms.num_vertices != num_vertices as u32 {
+ gpu_data.points_model_uniforms.num_vertices = num_vertices as u32;
+ ctxt.write_buffer(
+ &gpu_data.points_model_uniform_buffer,
+ 0,
+ bytemuck::bytes_of(&gpu_data.points_model_uniforms),
);
}
+
+ // Get or create points model bind group (view bind group is shared)
if gpu_data.points_model_bind_group.is_none() {
let vertex_size = (num_vertices * std::mem::size_of::()) as u64;
gpu_data.points_model_bind_group = Some(self.create_points_model_bind_group(
@@ -1599,39 +1991,11 @@ impl Material for ObjectMaterial {
));
}
- let points_view_bind_group = gpu_data.points_view_bind_group.as_ref().unwrap();
let points_model_bind_group = gpu_data.points_model_bind_group.as_ref().unwrap();
- // Begin points render pass
- let mut render_pass =
- context
- .encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("points_render_pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: context.color_view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: Some(
- wgpu::RenderPassDepthStencilAttachment {
- view: context.depth_view,
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- },
- ),
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
render_pass.set_pipeline(&self.points_pipeline);
- render_pass.set_bind_group(0, points_view_bind_group, &[]);
+ // Use shared view bind group (written once per frame)
+ render_pass.set_bind_group(0, &self.points_view_bind_group, &[]);
render_pass.set_bind_group(1, points_model_bind_group, &[]);
// Set instance vertex buffers (5 total: positions, colors, deformations, points_colors, points_sizes)
diff --git a/src/builtin/planar_object_material.rs b/src/builtin/object_material2d.rs
similarity index 66%
rename from src/builtin/planar_object_material.rs
rename to src/builtin/object_material2d.rs
index 2036dca2c..1f51cd0a6 100644
--- a/src/builtin/planar_object_material.rs
+++ b/src/builtin/object_material2d.rs
@@ -1,11 +1,14 @@
+use crate::camera::Camera2d;
use crate::context::Context;
-use crate::planar_camera::PlanarCamera;
use crate::resource::vertex_index::VERTEX_INDEX_FORMAT;
-use crate::resource::{GpuData, PlanarMaterial, PlanarMesh, PlanarRenderContext, Texture};
-use crate::scene::{PlanarInstancesBuffer, PlanarObjectData};
+use crate::resource::{
+ DynamicUniformBuffer, GpuData, GpuMesh2d, Material2d, RenderContext2d, Texture,
+};
+use crate::scene::{InstancesBuffer2d, ObjectData2d};
use bytemuck::{Pod, Zeroable};
-use na::{Isometry2, Matrix2, Matrix3, Point2, Vector2};
+use glamx::{Mat2, Mat3, Pose2, Vec2};
use std::any::Any;
+use std::cell::Cell;
/// Frame-level uniforms (view, projection) for 2D rendering.
#[repr(C)]
@@ -24,8 +27,7 @@ struct ObjectUniforms {
model: [[f32; 4]; 3],
// mat2x2 stored as 2x vec4 for alignment
scale: [[f32; 4]; 2],
- color: [f32; 3],
- _padding: f32,
+ color: [f32; 4],
}
/// View uniforms for wireframe rendering (includes viewport).
@@ -83,15 +85,13 @@ struct GpuVertex2D {
position: [f32; 2],
}
-/// Per-object GPU data for PlanarObjectMaterial.
-pub struct PlanarObjectMaterialGpuData {
- frame_uniform_buffer: wgpu::Buffer,
- object_uniform_buffer: wgpu::Buffer,
+/// Per-object GPU data for ObjectMaterial2d.
+pub struct ObjectMaterial2dGpuData {
// Cached bind groups for surface rendering
- frame_bind_group: Option,
- object_bind_group: Option,
texture_bind_group: Option,
cached_texture_ptr: usize,
+ /// Offset into the dynamic object uniform buffer, set during prepare() phase.
+ object_uniform_offset: Option,
// Wireframe rendering data
wireframe_view_uniform_buffer: wgpu::Buffer,
wireframe_model_uniform_buffer: wgpu::Buffer,
@@ -100,9 +100,17 @@ pub struct PlanarObjectMaterialGpuData {
wireframe_view_bind_group: Option,
wireframe_model_bind_group: Option,
/// Cached wireframe edges in local coordinates (built lazily from mesh).
- wireframe_edges: Option, Point2)>>,
+ wireframe_edges: Option>,
/// Hash of mesh faces to detect when edges need rebuilding.
wireframe_edges_mesh_hash: u64,
+ /// Cached wireframe view uniforms.
+ wireframe_view_uniforms: WireframeViewUniforms,
+ /// Cached wireframe model uniforms.
+ wireframe_model_uniforms: WireframeModelUniforms,
+ /// Number of edges to render.
+ wireframe_num_edges: usize,
+ /// Whether wireframe uniforms are prepared.
+ wireframe_prepared: bool,
// Point rendering data
points_view_uniform_buffer: wgpu::Buffer,
points_model_uniform_buffer: wgpu::Buffer,
@@ -111,29 +119,23 @@ pub struct PlanarObjectMaterialGpuData {
points_view_bind_group: Option,
points_model_bind_group: Option,
/// Cached vertices for point rendering (built lazily from mesh).
- points_vertices: Option>>,
+ points_vertices: Option>,
/// Hash of mesh coords to detect when vertices need rebuilding.
points_vertices_mesh_hash: u64,
+ /// Cached points view uniforms.
+ points_view_uniforms: WireframeViewUniforms,
+ /// Cached points model uniforms.
+ points_model_uniforms: PointsModelUniforms,
+ /// Number of vertices to render as points.
+ points_num_vertices: usize,
+ /// Whether points uniforms are prepared.
+ points_prepared: bool,
}
-impl PlanarObjectMaterialGpuData {
+impl ObjectMaterial2dGpuData {
pub fn new() -> Self {
let ctxt = Context::get();
- let frame_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("planar_material_frame_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let object_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
- label: Some("planar_material_object_uniform_buffer"),
- size: std::mem::size_of::() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
// Wireframe buffers
let wireframe_view_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("planar_wireframe_view_uniform_buffer"),
@@ -181,12 +183,9 @@ impl PlanarObjectMaterialGpuData {
});
Self {
- frame_uniform_buffer,
- object_uniform_buffer,
- frame_bind_group: None,
- object_bind_group: None,
texture_bind_group: None,
cached_texture_ptr: 0,
+ object_uniform_offset: None,
wireframe_view_uniform_buffer,
wireframe_model_uniform_buffer,
wireframe_edge_buffer,
@@ -195,6 +194,22 @@ impl PlanarObjectMaterialGpuData {
wireframe_model_bind_group: None,
wireframe_edges: None,
wireframe_edges_mesh_hash: 0,
+ wireframe_view_uniforms: WireframeViewUniforms {
+ view: [[0.0; 4]; 3],
+ proj: [[0.0; 4]; 3],
+ viewport: [0.0; 4],
+ },
+ wireframe_model_uniforms: WireframeModelUniforms {
+ model: [[0.0; 4]; 3],
+ scale: [[0.0; 4]; 2],
+ num_edges: 0,
+ default_width: 0.0,
+ use_perspective: 0,
+ _padding1: 0.0,
+ default_color: [0.0; 4],
+ },
+ wireframe_num_edges: 0,
+ wireframe_prepared: false,
points_view_uniform_buffer,
points_model_uniform_buffer,
points_vertex_buffer,
@@ -203,6 +218,22 @@ impl PlanarObjectMaterialGpuData {
points_model_bind_group: None,
points_vertices: None,
points_vertices_mesh_hash: 0,
+ points_view_uniforms: WireframeViewUniforms {
+ view: [[0.0; 4]; 3],
+ proj: [[0.0; 4]; 3],
+ viewport: [0.0; 4],
+ },
+ points_model_uniforms: PointsModelUniforms {
+ model: [[0.0; 4]; 3],
+ scale: [[0.0; 4]; 2],
+ num_vertices: 0,
+ default_size: 0.0,
+ use_perspective: 0,
+ _padding1: 0.0,
+ default_color: [0.0; 4],
+ },
+ points_num_vertices: 0,
+ points_prepared: false,
}
}
@@ -241,13 +272,13 @@ impl PlanarObjectMaterialGpuData {
}
}
-impl Default for PlanarObjectMaterialGpuData {
+impl Default for ObjectMaterial2dGpuData {
fn default() -> Self {
Self::new()
}
}
-impl GpuData for PlanarObjectMaterialGpuData {
+impl GpuData for ObjectMaterial2dGpuData {
fn as_any(&self) -> &dyn Any {
self
}
@@ -258,9 +289,15 @@ impl GpuData for PlanarObjectMaterialGpuData {
}
/// The default material used to draw 2D objects.
-pub struct PlanarObjectMaterial {
+///
+/// ## Performance Optimization
+///
+/// This material uses dynamic uniform buffers to batch uniform data writes:
+/// - Frame uniforms (view, projection) are written once per frame
+/// - Object uniforms are accumulated in a dynamic buffer and flushed once
+/// - This significantly reduces the number of `write_buffer` calls per frame
+pub struct ObjectMaterial2d {
pipeline: wgpu::RenderPipeline,
- frame_bind_group_layout: wgpu::BindGroupLayout,
object_bind_group_layout: wgpu::BindGroupLayout,
texture_bind_group_layout: wgpu::BindGroupLayout,
// Wireframe pipeline and layouts
@@ -271,17 +308,31 @@ pub struct PlanarObjectMaterial {
points_pipeline: wgpu::RenderPipeline,
points_view_bind_group_layout: wgpu::BindGroupLayout,
points_model_bind_group_layout: wgpu::BindGroupLayout,
+
+ // === Dynamic uniform buffer system ===
+ /// Shared frame uniform buffer (view, projection)
+ frame_uniform_buffer: wgpu::Buffer,
+ /// Shared bind group for frame uniforms
+ frame_bind_group: wgpu::BindGroup,
+ /// Dynamic buffer for object uniforms
+ object_uniform_buffer: DynamicUniformBuffer,
+ /// Bind group for object uniforms (recreated when buffer grows)
+ object_bind_group: Option,
+ /// Frame counter for detecting new frames
+ frame_counter: Cell,
+ /// Last frame we processed (to detect new frame)
+ last_frame: Cell,
}
-impl Default for PlanarObjectMaterial {
+impl Default for ObjectMaterial2d {
fn default() -> Self {
Self::new()
}
}
-impl PlanarObjectMaterial {
- /// Creates a new `PlanarObjectMaterial`.
- pub fn new() -> PlanarObjectMaterial {
+impl ObjectMaterial2d {
+ /// Creates a new `ObjectMaterial2d`.
+ pub fn new() -> ObjectMaterial2d {
let ctxt = Context::get();
// Create bind group layouts
@@ -300,6 +351,7 @@ impl PlanarObjectMaterial {
}],
});
+ // Object bind group uses dynamic offset for batched uniforms
let object_bind_group_layout =
ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("planar_material_object_bind_group_layout"),
@@ -308,7 +360,7 @@ impl PlanarObjectMaterial {
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: false,
+ has_dynamic_offset: true, // Enable dynamic offsets!
min_binding_size: None,
},
count: None,
@@ -349,8 +401,10 @@ impl PlanarObjectMaterial {
});
// Load shader
- let shader =
- ctxt.create_shader_module(Some("planar_material_shader"), include_str!("planar.wgsl"));
+ let shader = ctxt.create_shader_module(
+ Some("planar_material_shader"),
+ include_str!("object2d.wgsl"),
+ );
// Vertex buffer layouts
// Note: We use separate buffers for instance data (positions, colors, deformations)
@@ -513,7 +567,7 @@ impl PlanarObjectMaterial {
// Load wireframe shader
let wireframe_shader = ctxt.create_shader_module(
Some("planar_wireframe_shader"),
- include_str!("wireframe_planar_polyline.wgsl"),
+ include_str!("wireframe_polyline2d.wgsl"),
);
// Wireframe instance vertex buffer layouts
@@ -674,7 +728,7 @@ impl PlanarObjectMaterial {
// Load points shader
let points_shader = ctxt.create_shader_module(
Some("planar_points_shader"),
- include_str!("wireframe_planar_points.wgsl"),
+ include_str!("wireframe_points2d.wgsl"),
);
// Points instance vertex buffer layouts (same as wireframe but with points_colors/sizes)
@@ -778,9 +832,46 @@ impl PlanarObjectMaterial {
cache: None,
});
- PlanarObjectMaterial {
+ // === Create shared dynamic buffer resources ===
+
+ // Frame uniform buffer (written once per frame)
+ let frame_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("planar_shared_frame_uniform_buffer"),
+ size: std::mem::size_of::() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ // Create frame bind group
+ let frame_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("planar_shared_frame_bind_group"),
+ layout: &frame_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: frame_uniform_buffer.as_entire_binding(),
+ }],
+ });
+
+ // Dynamic buffer for object uniforms
+ let object_uniform_buffer =
+ DynamicUniformBuffer::