diff --git a/Cargo.toml b/Cargo.toml index 3757bbc4..a446351f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,3 +92,7 @@ renderdoc = "0.11" # This is too slow in Debug [profile.dev.package.texpresso] opt-level = 3 + +[package.metadata.cargo_check_external_types] +#TODO: reconsider `egui`/`epaint` and `winit` here +allowed_external_types = ["egui::*", "epaint::*", "mint::*", "profiling::*", "serde::*", "winit::*"] diff --git a/blade-asset/Cargo.toml b/blade-asset/Cargo.toml index 822d7dc7..e0995589 100644 --- a/blade-asset/Cargo.toml +++ b/blade-asset/Cargo.toml @@ -15,3 +15,6 @@ bytemuck = { workspace = true } choir = { workspace = true } log = { workspace = true } profiling = { workspace = true } + +[package.metadata.cargo_check_external_types] +allowed_external_types = ["choir::*"] diff --git a/blade-asset/src/lib.rs b/blade-asset/src/lib.rs index 6e8a18db..c0df231e 100644 --- a/blade-asset/src/lib.rs +++ b/blade-asset/src/lib.rs @@ -234,7 +234,9 @@ enum InvalidDependency { enum CookReason { NoTarget, BadHeader, + #[allow(dead_code)] TooManyDependencies(usize), + #[allow(dead_code)] Dependency(usize, InvalidDependency), Outdated, WrongDataOffset, diff --git a/blade-egui/Cargo.toml b/blade-egui/Cargo.toml index 5be46426..b41e8896 100644 --- a/blade-egui/Cargo.toml +++ b/blade-egui/Cargo.toml @@ -15,3 +15,6 @@ blade-macros = { version = "0.2", path = "../blade-macros"} bytemuck = { workspace = true } egui = { workspace = true, features = ["bytemuck"] } profiling = { workspace = true } + +[package.metadata.cargo_check_external_types] +allowed_external_types = ["blade_graphics::*", "epaint::*"] diff --git a/blade-egui/src/lib.rs b/blade-egui/src/lib.rs index fac41a03..19075cd1 100644 --- a/blade-egui/src/lib.rs +++ b/blade-egui/src/lib.rs @@ -42,6 +42,7 @@ struct Locals { r_texture: blade_graphics::TextureView, } +#[derive(Debug, PartialEq)] pub struct ScreenDescriptor { pub physical_size: (u32, u32), pub scale_factor: f32, diff --git a/blade-graphics/Cargo.toml b/blade-graphics/Cargo.toml index 1a5b251a..74b1e3d7 100644 --- a/blade-graphics/Cargo.toml +++ b/blade-graphics/Cargo.toml @@ -46,3 +46,6 @@ libloading = { version = "0.8" } wasm-bindgen = "0.2.83" web-sys = { workspace = true, features = ["HtmlCanvasElement", "WebGl2RenderingContext"] } js-sys = "0.3.60" + +[package.metadata.cargo_check_external_types] +allowed_external_types = ["bitflags::*", "mint::*", "naga::*", "raw_window_handle::*"] diff --git a/blade-graphics/src/vulkan/mod.rs b/blade-graphics/src/vulkan/mod.rs index ae3bc92a..4929dc8c 100644 --- a/blade-graphics/src/vulkan/mod.rs +++ b/blade-graphics/src/vulkan/mod.rs @@ -134,13 +134,6 @@ impl Buffer { unsafe impl Send for Buffer {} unsafe impl Sync for Buffer {} -#[derive(Clone, Copy, Debug, PartialEq)] -struct BlockInfo { - bytes: u8, - width: u8, - height: u8, -} - #[derive(Clone, Copy, Debug, Hash, PartialEq)] pub struct Texture { raw: vk::Image, diff --git a/blade-render/Cargo.toml b/blade-render/Cargo.toml index 8baf564d..b84c1301 100644 --- a/blade-render/Cargo.toml +++ b/blade-render/Cargo.toml @@ -36,3 +36,6 @@ zune-jpeg = { version = "0.4", optional = true } zune-png = { version = "0.4", optional = true } zune-hdr = { version = "0.4", optional = true } zune-imageprocs = { version = "0.4", optional = true } + +[package.metadata.cargo_check_external_types] +allowed_external_types = ["bitflags::*", "blade_asset::*", "blade_graphics::*", "bytemuck::*", "choir::*", "epaint::*", "mint::*", "strum::*"] diff --git a/blade-render/src/render/debug.rs b/blade-render/src/render/debug.rs index 17827aa7..0f4b5f84 100644 --- a/blade-render/src/render/debug.rs +++ b/blade-render/src/render/debug.rs @@ -22,22 +22,6 @@ pub struct DebugLine { pub b: DebugPoint, } -#[repr(C)] -#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] -struct DebugLineParams { - target_offset: [f32; 2], - target_size: [f32; 2], - mip_level: f32, - unused: u32, -} - -#[derive(blade_macros::ShaderData)] -struct DebugLinedata { - input: blade_graphics::TextureView, - output: blade_graphics::Sampler, - params: DebugBlitParams, -} - #[derive(blade_macros::ShaderData)] struct DebugDrawData { camera: super::CameraParams, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 54aa4731..f8f6c330 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,7 @@ Changelog for Blade ## blade-0.2 (TBD) - high-level engine +- built-in physics via Rapier3D - support object motion - support clockwise mesh winding diff --git a/examples/vehicle/data/ground.bin b/examples/vehicle/data/ground.bin new file mode 100644 index 00000000..8c115d77 Binary files /dev/null and b/examples/vehicle/data/ground.bin differ diff --git a/examples/vehicle/data/ground.glb b/examples/vehicle/data/ground.glb deleted file mode 100644 index c9d3f391..00000000 Binary files a/examples/vehicle/data/ground.glb and /dev/null differ diff --git a/examples/vehicle/data/ground.gltf b/examples/vehicle/data/ground.gltf new file mode 100644 index 00000000..d830dd6e --- /dev/null +++ b/examples/vehicle/data/ground.gltf @@ -0,0 +1,480 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.0.44", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0, + 1, + 2, + 3, + 4 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Plane", + "scale":[ + 20, + 20, + 20 + ] + }, + { + "mesh":1, + "name":"Plane.001", + "rotation":[ + 0, + 0, + -0.7071068286895752, + 0.7071068286895752 + ], + "scale":[ + 1, + 1, + 20 + ], + "translation":[ + 20, + 1, + 0 + ] + }, + { + "mesh":2, + "name":"Plane.002", + "rotation":[ + 0, + 0, + -0.7071068286895752, + 0.7071068286895752 + ], + "scale":[ + 1, + 1, + 20 + ], + "translation":[ + -20, + 1, + 0 + ] + }, + { + "mesh":3, + "name":"Plane.003", + "rotation":[ + -0.5, + 0.5, + -0.5, + 0.5 + ], + "scale":[ + 1, + 1, + 20 + ], + "translation":[ + 0, + 1, + -20 + ] + }, + { + "mesh":4, + "name":"Plane.004", + "rotation":[ + -0.5, + 0.5, + -0.5, + 0.5 + ], + "scale":[ + 1, + 1, + 20 + ], + "translation":[ + 0, + 1, + 20 + ] + } + ], + "materials":[ + { + "doubleSided":true, + "name":"Material.001", + "pbrMetallicRoughness":{ + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0, + "roughnessFactor":0.5 + } + }, + { + "doubleSided":true, + "name":"Material.002", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.800000011920929, + 0.800000011920929, + 0.800000011920929, + 1 + ], + "metallicFactor":0, + "roughnessFactor":0.5 + } + } + ], + "meshes":[ + { + "name":"Plane", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + }, + { + "name":"Plane.001", + "primitives":[ + { + "attributes":{ + "POSITION":4, + "NORMAL":5, + "TEXCOORD_0":6 + }, + "indices":3, + "material":1 + } + ] + }, + { + "name":"Plane.002", + "primitives":[ + { + "attributes":{ + "POSITION":7, + "NORMAL":8, + "TEXCOORD_0":9 + }, + "indices":3, + "material":1 + } + ] + }, + { + "name":"Plane.003", + "primitives":[ + { + "attributes":{ + "POSITION":10, + "NORMAL":11, + "TEXCOORD_0":12 + }, + "indices":3, + "material":1 + } + ] + }, + { + "name":"Plane.004", + "primitives":[ + { + "attributes":{ + "POSITION":13, + "NORMAL":14, + "TEXCOORD_0":15 + }, + "indices":3, + "material":1 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"orange_light_grid", + "uri":"orange_light_grid.png" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":6, + "type":"SCALAR" + }, + { + "bufferView":4, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":5, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":6, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":7, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":8, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":9, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":10, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":11, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":12, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":13, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":14, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":15, + "componentType":5126, + "count":4, + "type":"VEC2" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":48, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":48, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":96, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":128, + "target":34963 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":140, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":188, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":236, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":268, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":316, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":364, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":396, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":444, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":492, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":524, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":572, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":620, + "target":34962 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":652, + "uri":"ground.bin" + } + ] +} diff --git a/examples/vehicle/data/level.ron b/examples/vehicle/data/level.ron index d6115618..12df5567 100644 --- a/examples/vehicle/data/level.ron +++ b/examples/vehicle/data/level.ron @@ -7,9 +7,8 @@ name: "ground", visuals: [ ( - model: "ground.glb", + model: "ground.gltf", pos: (0, 1, 0), - scale: 60, ), ], colliders: [ @@ -17,9 +16,45 @@ density: 1.0, friction: 1.0, shape: Cuboid( - half: (1000, 1.0, 1000), + half: (20, 1, 20), ), ), + ( + density: 1.0, + friction: 1.0, + shape: Cuboid( + half: (20, 1, 1), + ), + pos: (21, 2, 0), + rot: (0, 90, 0), + ), + ( + density: 1.0, + friction: 1.0, + shape: Cuboid( + half: (20, 1, 1), + ), + pos: (-21, 2, 0), + rot: (0, 90, 0), + ), + ( + density: 1.0, + friction: 1.0, + shape: Cuboid( + half: (20, 1, 1), + ), + pos: (0, 2, 21), + rot: (0, 0, 0), + ), + ( + density: 1.0, + friction: 1.0, + shape: Cuboid( + half: (20, 1, 1), + ), + pos: (0, 2, -21), + rot: (0, 0, 0), + ), ], ), ) diff --git a/examples/vehicle/data/orange_light_grid.png b/examples/vehicle/data/orange_light_grid.png new file mode 100755 index 00000000..adcd3bb5 Binary files /dev/null and b/examples/vehicle/data/orange_light_grid.png differ diff --git a/examples/vehicle/main.rs b/examples/vehicle/main.rs index c5a72778..060dd371 100644 --- a/examples/vehicle/main.rs +++ b/examples/vehicle/main.rs @@ -1,6 +1,6 @@ mod config; -use std::{f32::consts, fs, mem, path::PathBuf, time}; +use std::{f32::consts, fs, mem, ops, path::PathBuf, time}; #[derive(Clone)] struct Wheel { @@ -10,17 +10,11 @@ struct Wheel { steer_joint: Option, } -struct WheelAxle { - wheels: Vec, - steer: Option, -} - struct Vehicle { body_handle: blade::ObjectHandle, - drive_factor: f32, jump_impulse: f32, roll_impulse: f32, - wheel_axles: Vec, + wheels: Vec, } struct Game { @@ -28,7 +22,7 @@ struct Game { engine: blade::Engine, last_physics_update: time::Instant, last_camera_update: time::Instant, - last_camera_base_quat: nalgebra::UnitQuaternion, + last_camera_base_quat: glam::Quat, is_paused: bool, // windowing window: winit::window::Window, @@ -38,12 +32,47 @@ struct Game { _ground_handle: blade::ObjectHandle, vehicle: Vehicle, cam_config: config::Camera, - spawn_pos: nalgebra::Vector3, + spawn_pos: glam::Vec3, } -const SUSPENSION_AXIS: rapier3d::dynamics::JointAxis = rapier3d::dynamics::JointAxis::Y; -const STEERING_AXIS: rapier3d::dynamics::JointAxis = rapier3d::dynamics::JointAxis::AngY; -const SPIN_AXIS: rapier3d::dynamics::JointAxis = rapier3d::dynamics::JointAxis::AngX; +#[derive(Clone, Debug, PartialEq)] +struct Isometry { + position: glam::Vec3, + orientation: glam::Quat, +} +impl From for Isometry { + fn from(transform: blade::Transform) -> Self { + Self { + position: transform.position.into(), + orientation: transform.orientation.into(), + } + } +} +impl Isometry { + fn inverse(&self) -> Self { + let orientation = self.orientation.inverse(); + Self { + position: orientation * -self.position, + orientation, + } + } + + fn to_blade(&self) -> blade::Transform { + blade::Transform { + position: self.position.into(), + orientation: self.orientation.into(), + } + } +} +impl ops::Mul for Isometry { + type Output = Self; + fn mul(self, other: Self) -> Self { + Self { + position: self.orientation * other.position + self.position, + orientation: self.orientation * other.orientation, + } + } +} #[derive(Debug)] struct QuitEvent; @@ -92,8 +121,8 @@ impl Game { let ground_handle = engine.add_object( &lev_config.ground, - nalgebra::Isometry3::default(), - blade::BodyType::Fixed, + blade::Transform::default(), + blade::DynamicInput::Empty, ); let veh_config: config::Vehicle = ron::de::from_bytes( @@ -106,17 +135,19 @@ impl Game { colliders: vec![veh_config.body.collider], additional_mass: None, }; - let spawn_pos = nalgebra::Vector3::from(lev_config.spawn_pos); + let spawn_pos = glam::Vec3::from(lev_config.spawn_pos); let mut vehicle = Vehicle { body_handle: engine.add_object( &body_config, - nalgebra::Isometry3::new(spawn_pos, nalgebra::Vector3::zeros()), - blade::BodyType::Dynamic, + blade::Transform { + position: spawn_pos.into(), + ..Default::default() + }, + blade::DynamicInput::Full, ), - drive_factor: veh_config.drive_factor, jump_impulse: veh_config.jump_impulse, roll_impulse: veh_config.roll_impulse, - wheel_axles: Vec::new(), + wheels: Vec::new(), }; let wheel_config = blade::config::Object { name: "vehicle/wheel".to_string(), @@ -133,123 +164,159 @@ impl Game { //Note: in the vehicle coordinate system X=left, Y=up, Z=forward for ac in veh_config.axles { - let joint_kind = blade::JointKind::Soft; - let mut wheels = Vec::new(); - for wheel_x in ac.x_wheels { - let offset = nalgebra::Vector3::new(wheel_x, ac.y, ac.z); + let offset = glam::Vec3::new(wheel_x, ac.y, ac.z); let rotation = if wheel_x > 0.0 { - nalgebra::Vector3::y_axis().scale(consts::PI) + glam::Quat::from_rotation_y(consts::PI) } else { - nalgebra::Vector3::zeros() + glam::Quat::IDENTITY }; let wheel_handle = engine.add_object( &wheel_config, - nalgebra::Isometry3::new(spawn_pos + offset, rotation), - blade::BodyType::Dynamic, + blade::Transform { + position: (spawn_pos + offset).into(), + orientation: rotation.into(), + }, + blade::DynamicInput::Full, ); + let wheel_angular_freedoms = mint::Vector3 { + x: Some(blade::FreedomAxis { + limits: None, + motor: Some(blade::MotorDesc { + stiffness: 0.0, + damping: veh_config.drive_factor, + max_force: 1000.0, + }), + }), + y: None, + z: None, + }; - wheels.push(if ac.steering.limit > 0.0 || ac.suspension.limit > 0.0 { - let max_angle = ac.steering.limit.to_radians(); - let mut locked_axes = rapier3d::dynamics::JointAxesMask::LOCKED_FIXED_AXES; - if ac.steering.limit > 0.0 { - locked_axes ^= STEERING_AXIS.into(); - } - if ac.suspension.limit > 0.0 { - locked_axes ^= SUSPENSION_AXIS.into(); - } - - let suspender_handle = engine.add_object( - &suspender_config, - nalgebra::Isometry3::new(spawn_pos + offset, nalgebra::Vector3::zeros()), - blade::BodyType::Dynamic, - ); + vehicle + .wheels + .push(if ac.steering.limit > 0.0 || ac.suspension.limit > 0.0 { + let max_angle = ac.steering.limit.to_radians(); + let suspender_handle = engine.add_object( + &suspender_config, + blade::Transform { + position: (spawn_pos + offset).into(), + ..Default::default() + }, + blade::DynamicInput::Full, + ); - let suspension_joint = engine.add_joint( - vehicle.body_handle, - suspender_handle, - rapier3d::dynamics::GenericJointBuilder::new(locked_axes) - .contacts_enabled(false) - .local_anchor1(offset.into()) - .limits(SUSPENSION_AXIS, [0.0, ac.suspension.limit]) - .motor_position( - SUSPENSION_AXIS, - 0.0, - ac.suspension.stiffness, - ac.suspension.damping, - ) - .limits(STEERING_AXIS, [-max_angle, max_angle]) - .motor_position( - STEERING_AXIS, - 0.0, - ac.steering.stiffness, - ac.steering.damping, - ) - .build(), - joint_kind, - ); + let suspension_joint = engine.add_joint( + vehicle.body_handle, + suspender_handle, + blade::JointDesc { + parent_anchor: blade::Transform { + position: offset.into(), + ..Default::default() + }, + linear: mint::Vector3 { + x: None, + y: if ac.suspension.limit > 0.0 { + Some(blade::FreedomAxis { + limits: Some(0.0..ac.suspension.limit), + motor: Some(blade::MotorDesc { + stiffness: ac.suspension.stiffness, + damping: ac.suspension.damping, + max_force: 1000.0, + }), + }) + } else { + None + }, + z: None, + }, + angular: mint::Vector3 { + x: None, + y: if ac.steering.limit > 0.0 { + Some(blade::FreedomAxis { + limits: Some(-max_angle..max_angle), + motor: Some(blade::MotorDesc { + stiffness: ac.steering.stiffness, + damping: ac.steering.damping, + max_force: 1000.0, + }), + }) + } else { + None + }, + z: None, + }, + ..Default::default() + }, + ); - let wheel_joint = engine.add_joint( - suspender_handle, - wheel_handle, - rapier3d::dynamics::GenericJointBuilder::new( - rapier3d::dynamics::JointAxesMask::LOCKED_REVOLUTE_AXES, - ) - .contacts_enabled(false) - .local_frame2(nalgebra::Isometry3::rotation(rotation)) - .build(), - joint_kind, - ); + let wheel_joint = engine.add_joint( + suspender_handle, + wheel_handle, + blade::JointDesc { + child_anchor: blade::Transform { + orientation: rotation.into(), + ..Default::default() + }, + angular: wheel_angular_freedoms, + ..Default::default() + }, + ); - let _extra_joint = engine.add_joint( - vehicle.body_handle, - wheel_handle, - rapier3d::dynamics::GenericJoint { - contacts_enabled: false, - ..Default::default() - }, - joint_kind, - ); + let _extra_joint = engine.add_joint( + vehicle.body_handle, + wheel_handle, + blade::JointDesc { + linear: mint::Vector3 { + x: Some(Default::default()), + y: Some(Default::default()), + z: Some(Default::default()), + }, + angular: mint::Vector3 { + x: Some(Default::default()), + y: Some(Default::default()), + z: Some(Default::default()), + }, + ..Default::default() + }, + ); - Wheel { - object: wheel_handle, - spin_joint: wheel_joint, - suspender: Some(suspender_handle), - steer_joint: Some(suspension_joint), - } - } else { - let locked_axes = - rapier3d::dynamics::JointAxesMask::LOCKED_FIXED_AXES ^ SPIN_AXIS.into(); - - let wheel_joint = engine.add_joint( - vehicle.body_handle, - wheel_handle, - rapier3d::dynamics::GenericJointBuilder::new(locked_axes) - .contacts_enabled(false) - .local_anchor1(offset.into()) - .local_frame2(nalgebra::Isometry3::rotation(rotation)) - .build(), - joint_kind, - ); + Wheel { + object: wheel_handle, + spin_joint: wheel_joint, + suspender: Some(suspender_handle), + steer_joint: if ac.steering.limit > 0.0 { + Some(suspension_joint) + } else { + None + }, + } + } else { + let wheel_joint = engine.add_joint( + vehicle.body_handle, + wheel_handle, + blade::JointDesc { + parent_anchor: blade::Transform { + position: offset.into(), + ..Default::default() + }, + child_anchor: blade::Transform { + orientation: rotation.into(), + ..Default::default() + }, + angular: wheel_angular_freedoms, + ..Default::default() + }, + ); - Wheel { - object: wheel_handle, - spin_joint: wheel_joint, - suspender: None, - steer_joint: None, - } - }); + Wheel { + object: wheel_handle, + spin_joint: wheel_joint, + suspender: None, + steer_joint: None, + } + }); } - - vehicle.wheel_axles.push(WheelAxle { - wheels, - steer: if ac.steering.limit > 0.0 { - Some(ac.steering) - } else { - None - }, - }); } let egui_context = egui::Context::default(); @@ -276,70 +343,55 @@ impl Game { fn set_velocity(&mut self, velocity: f32) { self.engine.wake_up(self.vehicle.body_handle); self.update_time(); - for wax in self.vehicle.wheel_axles.iter() { - for wheel in wax.wheels.iter() { - self.engine[wheel.spin_joint].set_motor_velocity( - SPIN_AXIS, - velocity, - self.vehicle.drive_factor, - ); - } + for wheel in self.vehicle.wheels.iter() { + self.engine.set_joint_motor( + wheel.spin_joint, + blade::JointAxis::AngularX, + 0.0, + velocity, + ); } } fn set_steering(&mut self, angle_rad: f32) { self.update_time(); - for wax in self.vehicle.wheel_axles.iter() { - let steer = match wax.steer { - Some(ref steer) => steer, - None => continue, - }; - for wheel in wax.wheels.iter() { - if let Some(handle) = wheel.steer_joint { - self.engine[handle].set_motor_position( - STEERING_AXIS, - angle_rad, - steer.stiffness, - steer.damping, - ); - } + for wheel in self.vehicle.wheels.iter() { + if let Some(handle) = wheel.steer_joint { + self.engine + .set_joint_motor(handle, blade::JointAxis::AngularY, angle_rad, 0.0); } } } - fn teleport_object_rel( - &mut self, - handle: blade::ObjectHandle, - transform: &nalgebra::Isometry3, - ) { - let prev = self.engine.get_object_isometry_approx(handle); - let next = transform * prev; - self.engine.teleport_object(handle, next); + fn teleport_object_rel(&mut self, handle: blade::ObjectHandle, isometry: &Isometry) { + let prev_transform = self + .engine + .get_object_transform(handle, blade::Prediction::LastKnown); + let next = isometry.clone() * Isometry::from(prev_transform); + self.engine.teleport_object(handle, next.to_blade()); } - fn teleport(&mut self, position: nalgebra::Vector3) { - let old_isometry_inv = self + fn teleport(&mut self, position: glam::Vec3) { + let old_transform = self .engine - .get_object_isometry_approx(self.vehicle.body_handle) - .inverse(); - let new_isometry = nalgebra::Isometry3 { - rotation: Default::default(), - translation: position.into(), + .get_object_transform(self.vehicle.body_handle, blade::Prediction::LastKnown); + let old_isometry_inv = Isometry::from(old_transform).inverse(); + let new_transform = blade::Transform { + position: position.into(), + ..Default::default() }; self.engine - .teleport_object(self.vehicle.body_handle, new_isometry); - - let relative = new_isometry * old_isometry_inv; - let waxes = mem::take(&mut self.vehicle.wheel_axles); - for wax in waxes.iter() { - for wheel in wax.wheels.iter() { - if let Some(suspender) = wheel.suspender { - self.teleport_object_rel(suspender, &relative); - } - self.teleport_object_rel(wheel.object, &relative); + .teleport_object(self.vehicle.body_handle, new_transform.clone()); + + let relative = Isometry::from(new_transform) * old_isometry_inv; + let wheels = mem::take(&mut self.vehicle.wheels); + for wheel in wheels.iter() { + if let Some(suspender) = wheel.suspender { + self.teleport_object_rel(suspender, &relative); } + self.teleport_object_rel(wheel.object, &relative); } - self.vehicle.wheel_axles = waxes; + self.vehicle.wheels = wheels; } fn update_time(&mut self) { @@ -389,33 +441,38 @@ impl Game { self.set_steering(-1.0); } winit::keyboard::KeyCode::Comma => { - let forward = self - .engine - .get_object_isometry_approx(self.vehicle.body_handle) - .transform_vector(&nalgebra::Vector3::z_axis()); - self.engine.apply_torque_impulse( + let transform = self.engine.get_object_transform( + self.vehicle.body_handle, + blade::Prediction::LastKnown, + ); + let forward = glam::Quat::from(transform.orientation) * glam::Vec3::Z; + self.engine.apply_angular_impulse( self.vehicle.body_handle, - -self.vehicle.roll_impulse * forward, + (-self.vehicle.roll_impulse * forward).into(), ); } winit::keyboard::KeyCode::Period => { - let forward = self - .engine - .get_object_isometry_approx(self.vehicle.body_handle) - .transform_vector(&nalgebra::Vector3::z_axis()); - self.engine.apply_torque_impulse( + let transform = self.engine.get_object_transform( + self.vehicle.body_handle, + blade::Prediction::LastKnown, + ); + let forward = glam::Quat::from(transform.orientation) * glam::Vec3::Z; + self.engine.apply_angular_impulse( self.vehicle.body_handle, - self.vehicle.roll_impulse * forward, + (self.vehicle.roll_impulse * forward).into(), ); } winit::keyboard::KeyCode::Space => { - let mut up = self - .engine - .get_object_isometry_approx(self.vehicle.body_handle) - .transform_vector(&nalgebra::Vector3::y_axis()); + let transform = self.engine.get_object_transform( + self.vehicle.body_handle, + blade::Prediction::LastKnown, + ); + let mut up = glam::Quat::from(transform.orientation) * glam::Vec3::Y; up.y = up.y.abs(); - self.engine - .apply_impulse(self.vehicle.body_handle, self.vehicle.jump_impulse * up); + self.engine.apply_linear_impulse( + self.vehicle.body_handle, + (self.vehicle.jump_impulse * up).into(), + ); } _ => {} }, @@ -503,17 +560,13 @@ impl Game { }); ui.horizontal(|ui| { if ui.button("Recover").clicked() { - let pos = self - .engine - .get_object_isometry_approx(self.vehicle.body_handle) - .translation - .vector; - let bounds = self.engine.get_object_bounds(self.vehicle.body_handle); - self.teleport( - pos + bounds - .half_extents() - .component_mul(&nalgebra::Vector3::y_axis()), + let transform = self.engine.get_object_transform( + self.vehicle.body_handle, + blade::Prediction::LastKnown, ); + let pos = glam::Vec3::from(transform.position); + let bounds = self.engine.get_object_bounds(self.vehicle.body_handle); + self.teleport(pos + glam::Vec3::from(bounds.half) * glam::Vec3::Y); } if ui.button("Respawn").clicked() { self.teleport(self.spawn_pos); @@ -555,47 +608,44 @@ impl Game { .handle_platform_output(&self.window, egui_output.platform_output); let camera = { - let veh_isometry = self.engine.get_object_isometry(self.vehicle.body_handle); - // Projection of the rotation of the vehicle on the Y axis - let projection = veh_isometry - .rotation - .quaternion() - .imag() - .dot(&nalgebra::Vector3::y_axis()); - let base_quat_nonorm = nalgebra::Quaternion::from_parts( - veh_isometry.rotation.quaternion().w, - nalgebra::Vector3::y_axis().scale(projection), + let veh_transform = self.engine.get_object_transform( + self.vehicle.body_handle, + blade::Prediction::IntegrateVelocityAndForces, ); - let validity = base_quat_nonorm.norm(); - let base_quat = nalgebra::UnitQuaternion::new_normalize(base_quat_nonorm); + let veh_isometry = Isometry::from(veh_transform); + // Projection of the rotation of the vehicle on the Y axis + let projection = veh_isometry.orientation.xyz().dot(glam::Vec3::Y); + let base_quat_nonorm = + glam::Quat::from_xyzw(0.0, projection, 0.0, veh_isometry.orientation.w); + let validity = base_quat_nonorm.length(); + let base_quat = base_quat_nonorm / validity; let camera_dt = self.last_camera_update.elapsed().as_secs_f32(); self.last_physics_update = time::Instant::now(); let cc = &self.cam_config; let smooth_t = (-camera_dt * cc.speed * validity).exp(); - let smooth_quat = nalgebra::UnitQuaternion::new_normalize( - base_quat.lerp(&self.last_camera_base_quat, smooth_t), - ); - let base = - nalgebra::geometry::Isometry3::from_parts(veh_isometry.translation, smooth_quat); + let smooth_quat = base_quat.lerp(self.last_camera_base_quat, smooth_t); + let base = Isometry { + position: veh_isometry.position, + orientation: smooth_quat, + }; self.last_camera_base_quat = smooth_quat; - //TODO: `nalgebra::Point3::from(mint::Vector3)` doesn't exist? - let source = nalgebra::Vector3::from(cc.target) - + nalgebra::Vector3::new( - -cc.azimuth.sin() * cc.altitude.cos(), - cc.altitude.sin(), - -cc.azimuth.cos() * cc.altitude.cos(), - ) - .scale(cc.distance); - let local = nalgebra::geometry::Isometry3::look_at_rh( - &source.into(), - &nalgebra::Vector3::from(cc.target).into(), - &nalgebra::Vector3::y_axis(), - ); - blade::Camera { - isometry: base * local.inverse(), + let source = glam::Vec3::from(cc.target) + + cc.distance + * glam::Vec3::new( + -cc.azimuth.sin() * cc.altitude.cos(), + cc.altitude.sin(), + -cc.azimuth.cos() * cc.altitude.cos(), + ); + let local_affine = glam::Affine3A::look_at_rh(source, cc.target.into(), glam::Vec3::Y); + let local = Isometry { + position: local_affine.translation.into(), + orientation: glam::Quat::from_affine3(&local_affine), + }; + blade::FrameCamera { + transform: (base * local.inverse()).to_blade(), fov_y: cc.fov, } }; diff --git a/src/lib.rs b/src/lib.rs index e9217e08..8e39a7c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,18 +20,161 @@ use std::{ops, path::Path, sync::Arc}; pub mod config; mod trimesh; +const ZERO_V3: mint::Vector3 = mint::Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Transform { + pub position: mint::Vector3, + pub orientation: mint::Quaternion, +} +impl Default for Transform { + fn default() -> Self { + Self { + position: ZERO_V3, + orientation: mint::Quaternion { s: 1.0, v: ZERO_V3 }, + } + } +} +impl Transform { + fn from_isometry(isometry: nalgebra::Isometry3) -> Self { + Self { + position: isometry.translation.vector.into(), + orientation: isometry.rotation.into(), + } + } + fn into_isometry(self) -> nalgebra::Isometry3 { + nalgebra::Isometry3 { + translation: nalgebra::Translation { + vector: self.position.into(), + }, + rotation: nalgebra::Unit::new_unchecked(self.orientation.into()), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BoundingBox { + pub center: mint::Vector3, + pub half: mint::Vector3, +} + +/// Type of prediction to be made about the object transformation. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub enum Prediction { + /// Report the last known transform. It always makes sense, + /// but it could be out of date. + #[default] + LastKnown, + /// Integrate using velocity only. + IntegrateVelocity, + /// Integrate using velocity and forces affecting the object. + IntegrateVelocityAndForces, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub enum DynamicInput { + /// Object is not controllable, it's static. + Empty, + /// Object is controlled by user setting the position. + SetPosition, + /// Object is controlled by user setting the velocity. + SetVelocity, + /// Object is affected by all forces around. + #[default] + Full, +} +impl DynamicInput { + fn into_rapier(self) -> rapier3d::dynamics::RigidBodyType { + use rapier3d::dynamics::RigidBodyType as Rbt; + match self { + Self::Empty => Rbt::Fixed, + Self::SetPosition => Rbt::KinematicPositionBased, + Self::SetVelocity => Rbt::KinematicVelocityBased, + Self::Full => Rbt::Dynamic, + } + } +} + +#[repr(u8)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum JointKind { - Soft, - Hard, +pub enum JointAxis { + LinearX = 0, + LinearY = 1, + LinearZ = 2, + AngularX = 3, + AngularY = 4, + AngularZ = 5, +} +impl JointAxis { + fn into_rapier(self) -> rapier3d::dynamics::JointAxis { + use rapier3d::dynamics::JointAxis as Ja; + match self { + Self::LinearX => Ja::X, + Self::LinearY => Ja::Y, + Self::LinearZ => Ja::Z, + Self::AngularX => Ja::AngX, + Self::AngularY => Ja::AngY, + Self::AngularZ => Ja::AngZ, + } + } } + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum JointHandle { - Soft(rapier3d::dynamics::ImpulseJointHandle), - Hard(rapier3d::dynamics::MultibodyJointHandle), + Soft(#[doc(hidden)] rapier3d::dynamics::ImpulseJointHandle), + Hard(#[doc(hidden)] rapier3d::dynamics::MultibodyJointHandle), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct MotorDesc { + pub stiffness: f32, + pub damping: f32, + pub max_force: f32, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FreedomAxis { + pub limits: Option>, + pub motor: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct JointDesc { + pub parent_anchor: Transform, + pub child_anchor: Transform, + pub linear: mint::Vector3>, + pub angular: mint::Vector3>, + /// Allow the contacts to happen between A and B + pub allow_contacts: bool, + /// Hard joints guarantee the releation between + /// objects, while soft joints only try to get there. + pub is_hard: bool, +} +impl Default for JointDesc { + fn default() -> Self { + Self { + parent_anchor: Transform::default(), + child_anchor: Transform::default(), + linear: mint::Vector3 { + x: None, + y: None, + z: None, + }, + angular: mint::Vector3 { + x: None, + y: None, + z: None, + }, + allow_contacts: false, + is_hard: false, + } + } } -//TODO: hide Rapier3D as a private dependency -pub use rapier3d::dynamics::RigidBodyType as BodyType; const MAX_DEPTH: f32 = 1e9; @@ -155,6 +298,31 @@ impl Physics { } } +#[doc(hidden)] +impl ops::Index for Physics { + type Output = rapier3d::dynamics::GenericJoint; + fn index(&self, handle: JointHandle) -> &Self::Output { + match handle { + JointHandle::Soft(h) => &self.impulse_joints.get(h).unwrap().data, + JointHandle::Hard(h) => { + let (multibody, link_index) = self.multibody_joints.get(h).unwrap(); + &multibody.link(link_index).unwrap().joint.data + } + } + } +} +impl ops::IndexMut for Physics { + fn index_mut(&mut self, handle: JointHandle) -> &mut Self::Output { + match handle { + JointHandle::Soft(h) => &mut self.impulse_joints.get_mut(h).unwrap().data, + JointHandle::Hard(h) => { + let (multibody, link_index) = self.multibody_joints.get_mut(h).unwrap(); + &mut multibody.link_mut(link_index).unwrap().joint.data + } + } + } +} + struct Visual { model: blade_asset::Handle, similarity: nalgebra::geometry::Similarity3, @@ -168,8 +336,9 @@ struct Object { visuals: Vec, } -pub struct Camera { - pub isometry: nalgebra::Isometry3, +#[derive(Clone, Debug, PartialEq)] +pub struct FrameCamera { + pub transform: Transform, pub fov_y: f32, } @@ -203,30 +372,6 @@ pub struct Engine { time_ahead: f32, } -impl ops::Index for Engine { - type Output = rapier3d::dynamics::GenericJoint; - fn index(&self, handle: JointHandle) -> &Self::Output { - match handle { - JointHandle::Soft(h) => &self.physics.impulse_joints.get(h).unwrap().data, - JointHandle::Hard(h) => { - let (multibody, link_index) = self.physics.multibody_joints.get(h).unwrap(); - &multibody.link(link_index).unwrap().joint.data - } - } - } -} -impl ops::IndexMut for Engine { - fn index_mut(&mut self, handle: JointHandle) -> &mut Self::Output { - match handle { - JointHandle::Soft(h) => &mut self.physics.impulse_joints.get_mut(h).unwrap().data, - JointHandle::Hard(h) => { - let (multibody, link_index) = self.physics.multibody_joints.get_mut(h).unwrap(); - &mut multibody.link_mut(link_index).unwrap().joint.data - } - } - } -} - impl Engine { fn make_surface_config(physical_size: winit::dpi::PhysicalSize) -> gpu::SurfaceConfig { gpu::SurfaceConfig { @@ -361,7 +506,7 @@ impl Engine { #[profiling::function] pub fn render( &mut self, - camera: &Camera, + camera: &FrameCamera, gui_primitives: &[egui::ClippedPrimitive], gui_textures: &egui::TexturesDelta, physical_size: winit::dpi::PhysicalSize, @@ -446,8 +591,8 @@ impl Engine { self.renderer.prepare( command_encoder, &blade_render::Camera { - pos: camera.isometry.translation.vector.into(), - rot: camera.isometry.rotation.into(), + pos: camera.transform.position, + rot: camera.transform.orientation, fov_y: camera.fov_y, depth: MAX_DEPTH, }, @@ -690,8 +835,8 @@ impl Engine { pub fn add_object( &mut self, config: &config::Object, - isometry: nalgebra::Isometry3, - body_type: BodyType, + transform: Transform, + dynamic_input: DynamicInput, ) -> ObjectHandle { use rapier3d::{ dynamics::MassProperties, @@ -738,8 +883,8 @@ impl Engine { None => Default::default(), }; - let rigid_body = rapier3d::dynamics::RigidBodyBuilder::new(body_type) - .position(isometry) + let rigid_body = rapier3d::dynamics::RigidBodyBuilder::new(dynamic_input.into_rapier()) + .position(transform.into_isometry()) .additional_mass_properties(add_mass_properties) .build(); let rb_handle = self.physics.rigid_bodies.insert(rigid_body); @@ -831,71 +976,129 @@ impl Engine { pub fn add_joint( &mut self, - a: ObjectHandle, - b: ObjectHandle, - data: impl Into, - kind: JointKind, + parent: ObjectHandle, + child: ObjectHandle, + desc: JointDesc, ) -> JointHandle { - let body1 = self.objects[a.0].rigid_body; - let body2 = self.objects[b.0].rigid_body; - match kind { - JointKind::Soft => { - JointHandle::Soft(self.physics.impulse_joints.insert(body1, body2, data, true)) + let data = { + let mut locked_axes = rapier3d::dynamics::JointAxesMask::empty(); + let freedoms = [ + (JointAxis::LinearX, &desc.linear.x), + (JointAxis::LinearY, &desc.linear.y), + (JointAxis::LinearZ, &desc.linear.z), + (JointAxis::AngularX, &desc.angular.x), + (JointAxis::AngularY, &desc.angular.y), + (JointAxis::AngularZ, &desc.angular.z), + ]; + let mut joint_builder = + rapier3d::dynamics::GenericJointBuilder::new(Default::default()) + .local_frame1(desc.parent_anchor.into_isometry()) + .local_frame2(desc.child_anchor.into_isometry()) + .contacts_enabled(desc.allow_contacts); + for &(axis, ref maybe_freedom) in freedoms.iter() { + let rapier_axis = axis.into_rapier(); + match *maybe_freedom { + Some(freedom) => { + if let Some(ref limits) = freedom.limits { + joint_builder = + joint_builder.limits(rapier_axis, [limits.start, limits.end]); + } + if let Some(ref motor) = freedom.motor { + joint_builder = joint_builder + .motor_position(rapier_axis, 0.0, motor.stiffness, motor.damping) + .motor_max_force(rapier_axis, motor.max_force); + } + } + None => { + locked_axes |= rapier3d::dynamics::JointAxesMask::from(rapier_axis); + } + } } - JointKind::Hard => JointHandle::Hard( + joint_builder.locked_axes(locked_axes).0 + }; + + let body1 = self.objects[parent.0].rigid_body; + let body2 = self.objects[child.0].rigid_body; + if desc.is_hard { + JointHandle::Hard( self.physics .multibody_joints .insert(body1, body2, data, true) .unwrap(), - ), + ) + } else { + JointHandle::Soft(self.physics.impulse_joints.insert(body1, body2, data, true)) } } - pub fn get_object_isometry(&self, handle: ObjectHandle) -> nalgebra::Isometry3 { + /// Get the current object transform. + /// + /// Since the simulation is done at fixed key frames, the position specifically + /// at the current time needs to be predicted. + pub fn get_object_transform(&self, handle: ObjectHandle, prediction: Prediction) -> Transform { let object = &self.objects[handle.0]; let body = &self.physics.rigid_bodies[object.rigid_body]; - body.predict_position_using_velocity_and_forces(self.time_ahead) + let isometry = match prediction { + Prediction::LastKnown => *body.position(), + Prediction::IntegrateVelocity => unimplemented!(), + Prediction::IntegrateVelocityAndForces => { + body.predict_position_using_velocity_and_forces(self.time_ahead) + } + }; + Transform::from_isometry(isometry) } - pub fn get_object_bounds(&self, handle: ObjectHandle) -> rapier3d::geometry::Aabb { + pub fn get_object_bounds(&self, handle: ObjectHandle) -> BoundingBox { let object = &self.objects[handle.0]; let mut aabb = rapier3d::geometry::Aabb::new_invalid(); for &collider_handle in object.colliders.iter() { let collider = &self.physics.colliders[collider_handle]; - //TODO: use `merge` - https://github.com/dimforge/rapier/issues/574 - for point in collider.compute_aabb().vertices() { - aabb.take_point(point); - } + rapier3d::geometry::BoundingVolume::merge(&mut aabb, &collider.compute_aabb()); + } + BoundingBox { + //TODO: proper Point3 -> Mint conversion? + center: (aabb.center() - nalgebra::Point3::default()).into(), + half: aabb.half_extents().into(), } - aabb - } - - /// Returns the position of the object within the "time_step" of precision. - /// Faster than the main `get_object_isometry`. - pub fn get_object_isometry_approx(&self, handle: ObjectHandle) -> &nalgebra::Isometry3 { - let object = &self.objects[handle.0]; - let body = &self.physics.rigid_bodies[object.rigid_body]; - body.position() } - pub fn apply_impulse(&mut self, handle: ObjectHandle, impulse: nalgebra::Vector3) { + pub fn apply_linear_impulse(&mut self, handle: ObjectHandle, impulse: mint::Vector3) { let object = &self.objects[handle.0]; let body = &mut self.physics.rigid_bodies[object.rigid_body]; - body.apply_impulse(impulse, false) + body.apply_impulse(impulse.into(), false) } - pub fn apply_torque_impulse(&mut self, handle: ObjectHandle, impulse: nalgebra::Vector3) { + pub fn apply_angular_impulse(&mut self, handle: ObjectHandle, impulse: mint::Vector3) { let object = &self.objects[handle.0]; let body = &mut self.physics.rigid_bodies[object.rigid_body]; - body.apply_torque_impulse(impulse, false) + body.apply_torque_impulse(impulse.into(), false) } - pub fn teleport_object(&mut self, handle: ObjectHandle, isometry: nalgebra::Isometry3) { + pub fn teleport_object(&mut self, handle: ObjectHandle, transform: Transform) { let object = &self.objects[handle.0]; let body = &mut self.physics.rigid_bodies[object.rigid_body]; body.set_linvel(Default::default(), false); body.set_angvel(Default::default(), false); - body.set_position(isometry, true); + body.set_position(transform.into_isometry(), true); + } + + pub fn set_joint_motor( + &mut self, + handle: JointHandle, + axis: JointAxis, + target_pos: f32, + target_vel: f32, + ) { + let joint = &mut self.physics[handle]; + let rapier_axis = axis.into_rapier(); + match joint.motor(rapier_axis) { + Some(&rapier3d::dynamics::JointMotor { + damping, stiffness, .. + }) => { + joint.set_motor(rapier_axis, target_pos, target_vel, stiffness, damping); + } + None => panic!("Axis {:?} of {:?} is not motorized", axis, handle), + } } pub fn set_environment_map(&mut self, path: &str) {