From c64bc2cae87f887823f696e56a0602cdb4f67447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Wed, 22 Oct 2025 15:41:53 +0200 Subject: [PATCH 1/6] feat: update to the latest version of rapier --- crates/nexus2d/Cargo.toml | 2 +- crates/nexus3d/Cargo.toml | 2 +- src/dynamics/body.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/nexus2d/Cargo.toml b/crates/nexus2d/Cargo.toml index 30d2932..cbfb8ee 100644 --- a/crates/nexus2d/Cargo.toml +++ b/crates/nexus2d/Cargo.toml @@ -29,7 +29,7 @@ encase = { workspace = true } bytemuck = { workspace = true } slang-hal = { workspace = true } stensor = { workspace = true } -rapier2d = "0.28" # TODO: should be behind a feature? +rapier2d = "0.30" # TODO: should be behind a feature? num-traits = "0.2" [dev-dependencies] diff --git a/crates/nexus3d/Cargo.toml b/crates/nexus3d/Cargo.toml index a784087..addc7f9 100644 --- a/crates/nexus3d/Cargo.toml +++ b/crates/nexus3d/Cargo.toml @@ -29,7 +29,7 @@ encase = { workspace = true } bytemuck = { workspace = true } slang-hal = { workspace = true } stensor = { workspace = true } -rapier3d = "0.28"# TODO: should be behind a feature? +rapier3d = "0.30"# TODO: should be behind a feature? num-traits = "0.2" [dev-dependencies] diff --git a/src/dynamics/body.rs b/src/dynamics/body.rs index dd7d7dc..a9530e0 100644 --- a/src/dynamics/body.rs +++ b/src/dynamics/body.rs @@ -50,7 +50,7 @@ impl From for GpuMassProperties { fn from(props: MassProperties) -> Self { GpuMassProperties { #[cfg(feature = "dim2")] - inv_inertia: props.inv_principal_inertia_sqrt * props.inv_principal_inertia_sqrt, + inv_inertia: props.inv_principal_inertia, #[cfg(feature = "dim3")] inv_inertia: props.reconstruct_inverse_inertia_matrix(), inv_mass: Vector::repeat(props.inv_mass), From e38e1814592fb8224dce475d5cb9de767c27ed14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 27 Oct 2025 16:01:30 +0100 Subject: [PATCH 2/6] feat: update to wgpu 27 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 75c6ede..f593633 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ slang-hal = "0.1" # TODO: make the wgpu dependency optional too (all backends should be optional but with # wgpu enabled by default). -wgpu = { version = "26", features = ["naga-ir"] } +wgpu = { version = "27", features = ["naga-ir"] } encase = "0.12" [workspace.lints] From eb88cd77dfcee026161d6590656bacf4f24b107b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 27 Oct 2025 18:18:29 +0100 Subject: [PATCH 3/6] chore: update to slang-hal 0.2 --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f593633..748212b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ nalgebra = { version = "0.34.1", features = ["convert-bytemuck", "encase"] } bytemuck = { version = "1", features = ["derive", "extern_crate_std"] } futures = "0.3" -stensor = "0.1" -slang-hal = "0.1" +stensor = "0.2" +slang-hal = "0.2" # TODO: make the wgpu dependency optional too (all backends should be optional but with # wgpu enabled by default). From c759d01f1061e84d495a9ead53b77aacba733a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 27 Oct 2025 18:18:35 +0100 Subject: [PATCH 4/6] Release v0.2.0 --- crates/nexus2d/Cargo.toml | 2 +- crates/nexus3d/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/nexus2d/Cargo.toml b/crates/nexus2d/Cargo.toml index cbfb8ee..fd74b74 100644 --- a/crates/nexus2d/Cargo.toml +++ b/crates/nexus2d/Cargo.toml @@ -4,7 +4,7 @@ authors = ["Sébastien Crozet "] description = "Cross-platform 2D rigid-body physics." homepage = "https://wgmath.rs" repository = "https://github.com/dimforge/wgmath" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/crates/nexus3d/Cargo.toml b/crates/nexus3d/Cargo.toml index addc7f9..5f4abf9 100644 --- a/crates/nexus3d/Cargo.toml +++ b/crates/nexus3d/Cargo.toml @@ -4,7 +4,7 @@ authors = ["Sébastien Crozet "] description = "Cross-platform 3D rigid-body physics." homepage = "https://wgmath.rs" repository = "https://github.com/dimforge/wgmath" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" From 687403df52cb5077042a9aac5e6c9a8238cf413e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 27 Oct 2025 18:20:28 +0100 Subject: [PATCH 5/6] feat: add CI --- .github/workflows/ci.yaml | 166 ++++++++++++++++++++++++++++ .github/workflows/download_slang.sh | 109 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100755 .github/workflows/download_slang.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..f377112 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,166 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: --deny warnings + RUSTDOCFLAGS: --deny warnings + SLANG_TAG: 2025.18.2 + +jobs: + # Check formatting. + format: + name: Format + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Run cargo fmt + run: cargo fmt --all -- --check + setup-slang: + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + outputs: + slang-dir: ${{ steps.setup.outputs.slang-dir }} # Pass SLANG_DIR to dependent jobs + slang-cache-key: ${{ steps.setup.outputs.slang-cache-key }} # Pass SLANG_DIR to dependent jobs + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Slang + id: cache-slang + uses: actions/cache/restore@v4 # Restore first + with: + path: | + ~/.cache/slang # Matches script's default OUTPUT_DIR + key: slang-v$SLANG_TAG-${{ runner.os }}-${{ runner.arch }} + + - name: Setup Slang + id: setup + run: | + echo "version=$SLANG_TAG" >> $GITHUB_OUTPUT # Output for cache key + SLANG_DIR=$(./.github/workflows/download_slang.sh --version $SLANG_TAG | grep '^SLANG_DIR=' | cut -d'=' -f2-) + echo "slang-dir=$SLANG_DIR" >> $GITHUB_OUTPUT # Output for dependents + echo "slang-cache-key=slang-v$SLANG_TAG-${{ runner.os }}-${{ runner.arch }}" >> $GITHUB_OUTPUT + echo "SLANG_DIR=$SLANG_DIR" >> $GITHUB_ENV # For this job if needed + + - name: Save Slang Cache + if: steps.cache-slang.outputs.cache-hit != 'true' # Only save on miss + uses: actions/cache/save@v4 + with: + path: ~/.cache/slang + key: ${{ steps.setup.outputs.slang-cache-key }} + # Run clippy lints. + clippy: + needs: setup-slang # Depends on setup-slang + name: Clippy + runs-on: ubuntu-latest + env: + SLANG_DIR: ${{ needs.setup-slang.outputs.slang-dir }} + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Install dependencies + run: sudo apt-get update; sudo apt-get install --no-install-recommends build-essential curl wget file libssl-dev + + - name: Retrieve Cache for Slang + uses: actions/cache/restore@v4 + with: + path: ~/.cache/slang + key: ${{ needs.setup-slang.outputs.slang-cache-key }} + + - name: Populate target directory from cache + uses: Leafwing-Studios/cargo-cache@v2 + with: + sweep-cache: true + + - name: Run clippy lints + run: SLANG_DIR=$SLANG_DIR cargo clippy --locked --workspace --all-targets -- --deny warnings + + # Check documentation. + doc: + needs: setup-slang # Depends on setup-slang + name: Docs + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + SLANG_DIR: ${{ needs.setup-slang.outputs.slang-dir }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install dependencies + run: sudo apt-get update; sudo apt-get install --no-install-recommends build-essential curl wget file libssl-dev + + - name: Retrieve Cache for Slang + uses: actions/cache/restore@v4 + with: + path: ~/.cache/slang + key: ${{ needs.setup-slang.outputs.slang-cache-key }} + + - name: Populate target directory from cache + uses: Leafwing-Studios/cargo-cache@v2 + with: + sweep-cache: true + + - name: Check documentation + run: SLANG_DIR=$SLANG_DIR cargo doc --locked --workspace --document-private-items --no-deps + # Testing. + test: + needs: setup-slang # Depends on setup-slang + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + SLANG_DIR: ${{ needs.setup-slang.outputs.slang-dir }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + build-essential curl wget file libssl-dev \ + libegl1-mesa-dev libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + + - name: Retrieve Cache for Slang + uses: actions/cache/restore@v4 + with: + path: ~/.cache/slang + key: ${{ needs.setup-slang.outputs.slang-cache-key }} + + - name: Populate target directory from cache + uses: Leafwing-Studios/cargo-cache@v2 + with: + sweep-cache: true + - name: Run Cargo Tests + run: | + SLANG_DIR=$SLANG_DIR LIBGL_ALWAYS_SOFTWARE=1 cargo test --verbose \ No newline at end of file diff --git a/.github/workflows/download_slang.sh b/.github/workflows/download_slang.sh new file mode 100755 index 0000000..0d1738f --- /dev/null +++ b/.github/workflows/download_slang.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Default values +OS="" +OUTPUT_DIR="$HOME/.cache/slang" +SLANG_VERSION="" +SLANG_TAG="" +ASSET_SUFFIX="" +SLANG_URL_BASE="https://github.com/shader-slang/slang/releases/download" + +# Help message +usage() { + echo "Usage: $0 [--os ] [--output-dir ] [--version ]" + echo " --os: Target OS (default: auto-detect from current platform)" + echo " --output-dir: Directory to extract Slang (default: ~/.cache/slang)" + echo " --version: Slang version (e.g., 2025.18.2, default: latest)" + echo "Example: $0 --os linux --output-dir /tmp/slang" +} + +# Parse arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --os) OS="$2"; shift ;; + --output-dir) export OUTPUT_DIR="$2"; shift ;; + --version) export SLANG_VERSION="$2"; shift ;; + *) usage ; exit 1 ;; + esac + shift +done + +# Detect OS if not specified +if [[ -z "$OS" ]]; then + case "$(uname -s)" in + Linux*) OS="linux" ;; + Darwin*) + if [[ "$(uname -m)" == "arm64" ]]; then + OS="macos-aarch64" + else + OS="macos" + fi + ;; + CYGWIN*|MINGW*|MSYS*) OS="windows" ;; + *) echo "Error: Unable to detect OS. Specify --os (linux, macos, macos-arm64, windows)"; exit 1 ;; + esac +fi + +# Determine asset suffix based on OS +case "$OS" in + linux) ASSET_SUFFIX="linux-x86_64.zip" ;; + macos) ASSET_SUFFIX="macos-x86_64.zip" ;; + macos-aarch64) ASSET_SUFFIX="macos-aarch64.zip" ;; + windows) ASSET_SUFFIX="windows-x86_64.zip" ;; + *) echo "Error: Unsupported OS: $OS"; exit 1 ;; +esac + +# Get Slang version if not specified +if [[ -z "$SLANG_VERSION" ]]; then + export SLANG_TAG=$(curl -s https://api.github.com/repos/shader-slang/slang/releases/latest | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + export SLANG_VERSION=$(echo "$SLANG_TAG" | sed 's/v//') # e.g., v2025.18.2 -> 2025.18.2 +else + export SLANG_TAG="v$SLANG_VERSION" +fi + +if [[ -z "$SLANG_VERSION" ]]; then + echo "Error: Could not determine Slang version" + exit 1 +fi + +# Set up paths +SLANG_DIR="$OUTPUT_DIR/slang-v$SLANG_VERSION-$OS" +ZIP_URL="$SLANG_URL_BASE/$SLANG_TAG/slang-$SLANG_VERSION-$ASSET_SUFFIX" +TEMP_ZIP="/tmp/slang-$SLANG_VERSION.zip" + +# Check if Slang is already extracted +if [[ -d "$SLANG_DIR" ]] && [[ -f "$SLANG_DIR/bin/slangc" || -f "$SLANG_DIR/bin/slangc.exe" ]]; then + echo "Using existing Slang at $SLANG_DIR" + echo "SLANG_DIR=$SLANG_DIR" + exit 0 +fi + +# Download Slang release +echo "Downloading Slang v$SLANG_VERSION for $OS from $ZIP_URL..." +mkdir -p "$OUTPUT_DIR" +curl -L -o "$TEMP_ZIP" "$ZIP_URL" || { echo "Error: Download failed for $ZIP_URL"; exit 1; } + +# Extract based on OS +echo "Extracting to $SLANG_DIR..." +if [[ "$OS" == "windows" ]]; then + # Windows: Assume 7z is available (or adjust for PowerShell/Expand-Archive) + 7z x "$TEMP_ZIP" -o"$SLANG_DIR" -y > /dev/null || { echo "Error: Extraction failed"; rm -f "$TEMP_ZIP"; exit 1; } +else + # Linux/macOS: Use unzip + unzip -q "$TEMP_ZIP" -d "$SLANG_DIR" || { echo "Error: Extraction failed"; rm -f "$TEMP_ZIP"; exit 1; } +fi + +# Clean up +rm -f "$TEMP_ZIP" + +# Verify extraction +if [[ ! -f "$SLANG_DIR/bin/slangc" && ! -f "$SLANG_DIR/bin/slangc.exe" ]]; then + echo "Error: Extraction incomplete, slangc not found in $SLANG_DIR/bin" + exit 1 +fi + +echo "Slang v$SLANG_VERSION extracted to $SLANG_DIR" +echo "SLANG_DIR=$SLANG_DIR" + +# For use in calling script +export SLANG_DIR \ No newline at end of file From de243dc803ee5e94950af26d04d6e9ac4955594e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 27 Oct 2025 18:26:19 +0100 Subject: [PATCH 6/6] feat: add docs --- src/dynamics/body.rs | 54 +++++++++++++++++ src/dynamics/integrate.rs | 2 +- src/dynamics/mod.rs | 6 +- src/lib.rs | 36 ++++++++++- src/shapes.rs | 124 +++++++++++++++++++++++++++++++++++++- 5 files changed, 216 insertions(+), 6 deletions(-) diff --git a/src/dynamics/body.rs b/src/dynamics/body.rs index a9530e0..809ec16 100644 --- a/src/dynamics/body.rs +++ b/src/dynamics/body.rs @@ -119,17 +119,35 @@ impl Default for BodyDesc { } } +/// Coupling mode between GPU and CPU physics simulations. +/// +/// Determines how rigid body state is synchronized between GPU and CPU representations. #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] pub enum BodyCoupling { + /// One-way coupling: CPU -> GPU only. + /// + /// The GPU reads body state from CPU but doesn't write back. The body is treated + /// as kinematic from the GPU's perspective (zero mass). OneWay, + /// Two-way coupling: CPU <-> GPU. + /// + /// The GPU both reads from and writes to the body state. The body is fully dynamic + /// with its mass properties applied on the GPU. #[default] TwoWays, } +/// Associates a Rapier rigid body with a collider for GPU simulation. +/// +/// Defines which Rapier rigid body and collider pair should be included in the +/// GPU simulation and how they should be coupled. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct BodyCouplingEntry { + /// Handle to the Rapier rigid body pub body: RigidBodyHandle, + /// Handle to the Rapier collider attached to this body pub collider: ColliderHandle, + /// Coupling mode (one-way or two-way synchronization) pub mode: BodyCoupling, } @@ -144,6 +162,26 @@ impl GpuBodySet { self.len } + /// Create a GPU body set from Rapier bodies and colliders. + /// + /// Converts Rapier rigid bodies and their associated colliders into GPU-compatible + /// representations. The coupling entries determine which body-collider pairs are + /// included and how they synchronize with the CPU simulation. + /// + /// # Arguments + /// * `backend` - The GPU backend to allocate buffers on + /// * `bodies` - The Rapier rigid body set + /// * `colliders` - The Rapier collider set + /// * `coupling` - List of body-collider pairs to include with their coupling modes + /// + /// # Returns + /// A new `GpuBodySet` containing GPU representations of the specified bodies + /// + /// # Errors + /// Returns an error if GPU buffer allocation fails + /// + /// # Panics + /// Panics if a collider has an unsupported shape type pub fn from_rapier( backend: &B, bodies: &RigidBodySet, @@ -276,18 +314,34 @@ impl GpuBodySet { &self.shapes } + /// GPU storage buffer containing world-space vertices for complex shapes. + /// + /// Contains vertices for polylines and trimeshes in world-space coordinates. + /// Updated when body poses change. pub fn shapes_vertex_buffers(&self) -> &GpuTensor, B> { &self.shapes_vertex_buffers } + /// GPU storage buffer containing local-space vertices for complex shapes. + /// + /// Contains vertices for polylines and trimeshes in body-local coordinates. + /// These are the original untransformed vertices. pub fn shapes_local_vertex_buffers(&self) -> &GpuTensor, B> { &self.shapes_local_vertex_buffers } + /// GPU storage buffer mapping each vertex to its collider ID. + /// + /// For each vertex in the vertex buffers, stores which collider (body index) it belongs to. + /// Used for collision detection and response. pub fn shapes_vertex_collider_id(&self) -> &GpuTensor { &self.shapes_vertex_collider_id } + /// CPU copy of shape data for all bodies. + /// + /// Returns a slice containing the [`GpuShape`] for each body in the set. + /// Primarily used for convenience in particle-based simulations. pub fn shapes_data(&self) -> &[GpuShape] { &self.shapes_data } diff --git a/src/dynamics/integrate.rs b/src/dynamics/integrate.rs index 69c9cd3..8530a22 100644 --- a/src/dynamics/integrate.rs +++ b/src/dynamics/integrate.rs @@ -5,9 +5,9 @@ use crate::dynamics::{GpuMassProperties, GpuVelocity}; use crate::math::GpuSim; use slang_hal::backend::Backend; use slang_hal::function::GpuFunction; -use stensor::tensor::GpuTensor; use slang_hal::Shader; use slang_hal::ShaderArgs; +use stensor::tensor::GpuTensor; #[derive(Shader)] #[shader(module = "nexus::dynamics::integrate")] diff --git a/src/dynamics/mod.rs b/src/dynamics/mod.rs index 959dda0..570d3fe 100644 --- a/src/dynamics/mod.rs +++ b/src/dynamics/mod.rs @@ -1,6 +1,10 @@ //! Rigid-body dynamics (forces, velocities, etc.) -pub use body::{BodyDesc, GpuBodySet, GpuForce, GpuMassProperties, GpuVelocity}; +pub use body::{ + BodyCoupling, BodyCouplingEntry, BodyDesc, GpuBodySet, GpuForce, GpuMassProperties, GpuVelocity, +}; +/// Rigid body definitions and GPU body set management. pub mod body; +/// Physics integration routines (position, velocity updates). pub mod integrate; diff --git a/src/lib.rs b/src/lib.rs index f37e10b..53ccf19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,68 @@ #![doc = include_str!("../README.md")] -// #![warn(missing_docs)] +#![warn(missing_docs)] extern crate nalgebra as na; +/// Re-export of the Rapier 2D physics engine. +/// +/// This is available when the `dim2` feature is enabled. #[cfg(feature = "dim2")] pub extern crate rapier2d as rapier; +/// Re-export of the Rapier 3D physics engine. +/// +/// This is available when the `dim3` feature is enabled. #[cfg(feature = "dim3")] pub extern crate rapier3d as rapier; use slang_hal::re_exports::include_dir; use slang_hal::re_exports::minislang::SlangCompiler; +/// GPU-accelerated rigid body dynamics simulation. +/// +/// This module provides structures and methods for managing physics bodies +/// on the GPU, including body state, integration, and coupling with colliders. pub mod dynamics; +/// GPU-compatible shape representations. +/// +/// This module defines shape types and utilities for converting Rapier/Parry shapes +/// to GPU-friendly formats with vertex buffers. pub mod shapes; +/// Mathematical types and utilities for physics simulation. +/// +/// Re-exports Rapier's math types and defines dimension-specific type aliases +/// for GPU simulation and angular inertia calculations. pub mod math { + /// Re-export all mathematical types from Rapier (vectors, matrices, etc.) pub use rapier::math::*; + /// GPU similarity transformation for 2D simulations (translation + rotation). #[cfg(feature = "dim2")] pub type GpuSim = stensor::geometry::GpuSim2; + /// GPU similarity transformation for 3D simulations (translation + rotation). #[cfg(feature = "dim3")] pub type GpuSim = stensor::geometry::GpuSim3; + /// Angular inertia type for 2D simulations (scalar). #[cfg(feature = "dim2")] pub type AngularInertia = N; + /// Angular inertia type for 3D simulations (3x3 matrix). #[cfg(feature = "dim3")] pub type AngularInertia = na::Matrix3; } +/// Directory containing the Slang shader source files. +/// +/// This includes all shader code needed for GPU-accelerated physics simulation. pub const SLANG_SRC_DIR: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../shaders"); + +/// Register all required shaders with a Slang compiler. +/// +/// This function registers both stensor shaders and Nexus-specific shader sources +/// needed for GPU physics simulation. +/// +/// # Arguments +/// * `compiler` - The Slang compiler instance to register shaders with pub fn register_shaders(compiler: &mut SlangCompiler) { stensor::register_shaders(compiler); compiler.add_dir(SLANG_SRC_DIR.clone()); diff --git a/src/shapes.rs b/src/shapes.rs index 0951aa1..40b1486 100644 --- a/src/shapes.rs +++ b/src/shapes.rs @@ -1,31 +1,64 @@ -//! A shape enum. +//! GPU-compatible shape representations. +//! +//! This module provides structures for representing collision shapes in a format +//! optimized for GPU computation. It includes conversion utilities from Rapier/Parry +//! shapes to GPU-friendly formats with vertex buffers. use na::{vector, Vector4}; use rapier::geometry::{Shape, ShapeType, TypedShape}; use crate::math::{Point, Vector}; +/// GPU shape type identifiers. +/// +/// These numeric values must match the type constants defined in `shape.wgsl`. +/// They are used to tag shape data on the GPU for runtime type identification. // NOTE: this must match the type values in shape.wgsl pub enum GpuShapeType { + /// Spherical/circular shape (2D: circle, 3D: sphere) Ball = 0, + /// Rectangular/box shape (2D: rectangle, 3D: cuboid) Cuboid = 1, + /// Capsule shape (line segment with rounded ends) Capsule = 2, + /// Conical shape (3D only) #[cfg(feature = "dim3")] Cone = 3, + /// Cylindrical shape (3D only) #[cfg(feature = "dim3")] Cylinder = 4, + /// Polyline shape (connected line segments) // TODO: not sure we want to keep the Polyline in the shape type. Polyline = 5, + /// Triangle mesh shape TriMesh = 6, } +/// Storage for shape vertex data. +/// +/// Accumulates vertices from complex shapes (polylines, trimeshes) during +/// conversion from Rapier/Parry shapes. Shapes reference ranges within this buffer. #[derive(Default, Clone, Debug)] pub struct ShapeBuffers { + /// Vertex positions for all complex shapes. + /// + /// Polyline and trimesh shapes store references to ranges within this buffer. pub vertices: Vec>, - // NOTE: a bit weird we don’t have any index buffer here but - // we don’t need it yet (slosh has its own indexing method). + // NOTE: a bit weird we don't have any index buffer here but + // we don't need it yet (slosh has its own indexing method). } +/// GPU-compatible shape representation. +/// +/// A compact, fixed-size representation of collision shapes suitable for GPU processing. +/// Shape data is encoded into two 4D vectors, with the shape type stored in the `w` +/// component of the first vector as a bit-cast integer. +/// +/// # Memory Layout +/// - `a.xyz`: Primary shape parameters (radius, half-extents, vertex range start, etc.) +/// - `a.w`: Shape type identifier (bit-cast from [`GpuShapeType`]) +/// - `b.xyz`: Secondary shape parameters (capsule endpoint, vertex range end, etc.) +/// - `b.w`: Additional parameter (e.g., capsule radius) #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] pub struct GpuShape { @@ -34,6 +67,16 @@ pub struct GpuShape { } impl GpuShape { + /// Create a ball/sphere shape. + /// + /// # Arguments + /// * `radius` - The radius of the ball/sphere + /// + /// # Examples + /// ```ignore + /// # use nexus::shapes::GpuShape; + /// let ball = GpuShape::ball(1.5); + /// ``` pub fn ball(radius: f32) -> Self { let tag = f32::from_bits(GpuShapeType::Ball as u32); Self { @@ -42,6 +85,17 @@ impl GpuShape { } } + /// Create a cuboid/rectangle shape. + /// + /// # Arguments + /// * `half_extents` - The half-extents (half-width, half-height, half-depth) of the cuboid + /// + /// # Examples + /// ```ignore + /// # use nexus::shapes::GpuShape; + /// # use nexus::math::vector; + /// let cuboid = GpuShape::cuboid(vector![1.0, 2.0, 3.0]); + /// ``` pub fn cuboid(half_extents: Vector) -> Self { let tag = f32::from_bits(GpuShapeType::Cuboid as u32); Self { @@ -53,6 +107,14 @@ impl GpuShape { } } + /// Create a capsule shape. + /// + /// A capsule is a line segment with rounded ends of the specified radius. + /// + /// # Arguments + /// * `a` - First endpoint of the capsule's central segment + /// * `b` - Second endpoint of the capsule's central segment + /// * `radius` - The radius of the capsule's rounded ends pub fn capsule(a: Point, b: Point, radius: f32) -> Self { let tag = f32::from_bits(GpuShapeType::Capsule as u32); #[cfg(feature = "dim2")] @@ -67,6 +129,12 @@ impl GpuShape { }; } + /// Create a polyline shape from a vertex range. + /// + /// The vertices must already exist in a [`ShapeBuffers`] instance. + /// + /// # Arguments + /// * `vertex_range` - `[start, end]` indices into the vertex buffer pub fn polyline(vertex_range: [u32; 2]) -> Self { let tag = f32::from_bits(GpuShapeType::Polyline as u32); let rng0 = f32::from_bits(vertex_range[0]); @@ -77,6 +145,12 @@ impl GpuShape { } } + /// Create a triangle mesh shape from a vertex range. + /// + /// The vertices must already exist in a [`ShapeBuffers`] instance. + /// + /// # Arguments + /// * `vertex_range` - `[start, end]` indices into the vertex buffer pub fn trimesh(vertex_range: [u32; 2]) -> Self { let tag = f32::from_bits(GpuShapeType::TriMesh as u32); let rng0 = f32::from_bits(vertex_range[0]); @@ -87,6 +161,11 @@ impl GpuShape { } } + /// Create a cone shape (3D only). + /// + /// # Arguments + /// * `half_height` - Half the height of the cone along its central axis + /// * `radius` - The radius of the cone's base #[cfg(feature = "dim3")] pub fn cone(half_height: f32, radius: f32) -> Self { let tag = f32::from_bits(GpuShapeType::Cone as u32); @@ -96,6 +175,11 @@ impl GpuShape { } } + /// Create a cylinder shape (3D only). + /// + /// # Arguments + /// * `half_height` - Half the height of the cylinder along its central axis + /// * `radius` - The radius of the cylinder #[cfg(feature = "dim3")] pub fn cylinder(half_height: f32, radius: f32) -> Self { let tag = f32::from_bits(GpuShapeType::Cylinder as u32); @@ -105,6 +189,17 @@ impl GpuShape { } } + /// Convert a Rapier/Parry shape to a GPU-compatible representation. + /// + /// For complex shapes (polylines, trimeshes, heightfields), vertex data is + /// appended to the provided `buffers` and the shape stores references to those vertices. + /// + /// # Arguments + /// * `shape` - The Rapier/Parry shape to convert + /// * `buffers` - Storage for vertex data of complex shapes + /// + /// # Returns + /// `Some(GpuShape)` if the shape type is supported, `None` otherwise pub fn from_parry(shape: &(impl Shape + ?Sized), buffers: &mut ShapeBuffers) -> Option { match shape.as_typed_shape() { TypedShape::Ball(shape) => Some(Self::ball(shape.radius)), @@ -159,6 +254,15 @@ impl GpuShape { } } + /// Get the shape type identifier. + /// + /// Extracts and decodes the shape type tag stored in the `w` component. + /// + /// # Returns + /// The Rapier [`ShapeType`] enum variant corresponding to this shape + /// + /// # Panics + /// Panics if the stored type tag is invalid pub fn shape_type(&self) -> ShapeType { let tag = self.a.w.to_bits(); @@ -176,11 +280,25 @@ impl GpuShape { } } + /// Get the vertex range for a polyline shape. + /// + /// # Returns + /// `[start, end]` indices into the shape vertex buffer + /// + /// # Panics + /// Panics if this shape is not a polyline pub fn polyline_rngs(&self) -> [u32; 2] { assert!(self.shape_type() == ShapeType::Polyline); [self.a.x.to_bits(), self.a.y.to_bits()] } + /// Get the vertex range for a triangle mesh shape. + /// + /// # Returns + /// `[start, end]` indices into the shape vertex buffer + /// + /// # Panics + /// Panics if this shape is not a triangle mesh pub fn trimesh_rngs(&self) -> [u32; 2] { assert!(self.shape_type() == ShapeType::TriMesh); [self.a.x.to_bits(), self.a.y.to_bits()]