tirbofish/dropbear · commit
6c47dd80d79e6b1a2a6195e569aed18c12008120
feature: physics shape cast. fix: implement the shadowJar for creating farJars to package jarvis, run github actions Unverified
@@ -25,7 +25,7 @@ jobs: run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build fatJar + run: ./gradlew build shadowJar - name: Upload JAR artifact uses: actions/upload-artifact@v4 @@ -41,8 +41,8 @@ jobs: - name: give gradle permissions if: matrix.os != 'windows-latest' run: chmod +x gradlew - - name: build fatjar - run: ./gradlew fatJar + - name: build shadowJar + run: ./gradlew shadowJar - name: install libudev-dev (Linux) if: matrix.os == 'ubuntu-latest' run: sudo apt-get update && sudo apt-get install -y libudev-dev @@ -13,8 +13,6 @@ <option name="backtrace" value="FULL" /> <option name="isRedirectInput" value="false" /> <option name="redirectInputPath" value="" /> - <method v="2"> - <option name="RunConfigurationTask" enabled="true" run_configuration_name="fatJar" run_configuration_type="GradleRunConfiguration" /> - </method> + <method v="2" /> </configuration> </component> @@ -1,5 +1,5 @@ <component name="ProjectRunConfigurationManager"> - <configuration default="false" name="fatJar" type="GradleRunConfiguration" factoryName="Gradle"> + <configuration default="false" name="shadowJar" type="GradleRunConfiguration" factoryName="Gradle"> <ExternalSystemSettings> <option name="executionName" /> <option name="externalProjectPath" value="$PROJECT_DIR$" /> @@ -10,7 +10,7 @@ </option> <option name="taskNames"> <list> - <option value="fatJar" /> + <option value="shadowJar" /> </list> </option> <option name="vmOptions" /> @@ -78,7 +78,7 @@ git clone git@github.com:tirbofish/dropbear cd dropbear # eucalyptus-editor requires dropbear-1.0-SNAPSHOT-all.jar to be built first -./gradlew fatJar +./gradlew shadowJar # this will build all the projects in the workspace cargo build ``` @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.kotlinxSerialization) `maven-publish` id("org.jetbrains.dokka") version "2.1.0" + id("com.gradleup.shadow") version "9.2.2" } group = "com.dropbear" @@ -126,6 +127,12 @@ kotlin { } } + java { + sourceSets.getByName("jvmMain") { + java.srcDirs("scripting/jvmMain/java") + } + } + targets.all { compilations.all { compileTaskProvider.configure { @@ -207,21 +214,4 @@ publishing { } } } -} - -tasks.register<Jar>("fatJar") { - archiveClassifier.set("all") - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - - from(kotlin.jvm().compilations["main"].output) - - configurations.named("jvmRuntimeClasspath").get().forEach { file -> - if (file.name.endsWith(".jar")) { - from(zipTree(file)) - } else { - from(file) - } - } - - manifest {} } @@ -334,7 +334,7 @@ pub mod jni { use crate::physics::nalgebra; use crate::physics::PhysicsState; use crate::scripting::jni::utils::{FromJObject, ToJObject}; - use crate::types::{ColliderFFI, IndexNative, RayHit, Vector3}; + use crate::types::{ColliderFFI, IndexNative, RayHit, ShapeCastHitFFI, Vector3}; use hecs::Entity; use jni::objects::{JClass, JObject}; use jni::sys::{jboolean, jdouble, jlong, jobject}; @@ -342,6 +342,9 @@ pub mod jni { use rapier3d::parry::query::DefaultQueryDispatcher; use rapier3d::pipeline::QueryFilter; use rapier3d::prelude::{point, vector, Ray}; + use rapier3d::parry::query::ShapeCastOptions; + use rapier3d::math::{Pose3, Vec3}; + use crate::physics::collider::ColliderShape; #[unsafe(no_mangle)] pub extern "system" fn Java_com_dropbear_physics_PhysicsNative_getGravity( @@ -475,6 +478,161 @@ pub mod jni { } } + fn collider_ffi_from_handle(physics: &PhysicsState, handle: rapier3d::prelude::ColliderHandle) -> ColliderFFI { + let (idx, generation) = handle.into_raw_parts(); + + let mut found_label = None; + for (label, colliders) in physics.colliders_entity_map.iter() { + for (_, c) in colliders { + if c.0 == handle.0 { + found_label = Some(label); + break; + } + } + if found_label.is_some() { + break; + } + } + + let entity_id = if let Some(label) = found_label { + physics + .entity_label_map + .iter() + .find(|(_, l)| *l == label) + .map(|(e, _)| e.to_bits().get()) + .unwrap_or(Entity::DANGLING.to_bits().get()) + } else { + Entity::DANGLING.to_bits().get() + }; + + ColliderFFI { + index: IndexNative { index: idx, generation }, + entity_id, + id: idx, + } + } + + fn shared_shape_from_collider_shape(shape: &ColliderShape) -> rapier3d::geometry::SharedShape { + match shape { + ColliderShape::Box { half_extents } => { + rapier3d::geometry::SharedShape::cuboid(half_extents[0], half_extents[1], half_extents[2]) + } + ColliderShape::Sphere { radius } => rapier3d::geometry::SharedShape::ball(*radius), + ColliderShape::Capsule { half_height, radius } => { + rapier3d::geometry::SharedShape::capsule_y(*half_height, *radius) + } + ColliderShape::Cylinder { half_height, radius } => { + rapier3d::geometry::SharedShape::cylinder(*half_height, *radius) + } + ColliderShape::Cone { half_height, radius } => { + rapier3d::geometry::SharedShape::cone(*half_height, *radius) + } + } + } + + #[unsafe(no_mangle)] + pub extern "system" fn Java_com_dropbear_physics_PhysicsNative_shapeCast( + mut env: JNIEnv, + _: JClass, + physics_handle: jlong, + origin: JObject, + direction: JObject, + shape: JObject, + time_of_impact: jdouble, + solid: jboolean, + ) -> jobject { + let physics = crate::convert_ptr!(mut physics_handle => PhysicsState); + + let qp = physics.broad_phase.as_query_pipeline( + &DefaultQueryDispatcher, + &physics.bodies, + &physics.colliders, + QueryFilter::new(), + ); + + let origin = match Vector3::from_jobject(&mut env, &origin) { + Ok(v) => v, + Err(e) => { + let _ = env.throw_new( + "java/lang/RuntimeException", + format!("Unable to convert origin to Vector3: {e}"), + ); + return std::ptr::null_mut(); + } + }; + + let direction = match Vector3::from_jobject(&mut env, &direction) { + Ok(v) => v, + Err(e) => { + let _ = env.throw_new( + "java/lang/RuntimeException", + format!("Unable to convert direction to Vector3: {e}"), + ); + return std::ptr::null_mut(); + } + }; + + let shape = match ColliderShape::from_jobject(&mut env, &shape) { + Ok(v) => v, + Err(e) => { + let _ = env.throw_new( + "java/lang/RuntimeException", + format!("Unable to convert shape to ColliderShape: {e}"), + ); + return std::ptr::null_mut(); + } + }; + + let dir_len = ((direction.x * direction.x) + (direction.y * direction.y) + (direction.z * direction.z)).sqrt(); + if dir_len <= f64::EPSILON { + return std::ptr::null_mut(); + } + + let dir_unit = Vector3 { + x: direction.x / dir_len, + y: direction.y / dir_len, + z: direction.z / dir_len, + }; + + let cast_shape = shared_shape_from_collider_shape(&shape); + let iso: Pose3 = nalgebra::Isometry3::translation(origin.x as f32, origin.y as f32, origin.z as f32).into(); + let vel: Vec3 = vector![dir_unit.x as f32, dir_unit.y as f32, dir_unit.z as f32].into(); + + let options = ShapeCastOptions { + max_time_of_impact: time_of_impact as f32, + target_distance: 0.0, + stop_at_penetration: solid != 0, + compute_impact_geometry_on_penetration: true, + }; + + let Some((hit_handle, toi)) = qp.cast_shape(&iso, vel, cast_shape.as_ref(), options) else { + return std::ptr::null_mut(); + }; + + let collider = collider_ffi_from_handle(&physics, hit_handle); + + let hit = ShapeCastHitFFI { + collider, + distance: toi.time_of_impact as f64, + witness1: Vector3::from([toi.witness1.x, toi.witness1.y, toi.witness1.z]), + witness2: Vector3::from([toi.witness2.x, toi.witness2.y, toi.witness2.z]), + normal1: Vector3::from([toi.normal1.x, toi.normal1.y, toi.normal1.z]), + normal2: Vector3::from([toi.normal2.x, toi.normal2.y, toi.normal2.z]), + status: toi.status, + }; + + match hit.to_jobject(&mut env) { + Ok(v) => v.into_raw(), + Err(e) => { + let _ = env.throw_new( + "java/lang/RuntimeException", + format!("Unable to create ShapeCastHit object: {e}"), + ); + std::ptr::null_mut() + } + } + } + #[unsafe(no_mangle)] pub extern "system" fn Java_com_dropbear_physics_PhysicsNative_isOverlapping( mut env: JNIEnv, @@ -723,15 +723,15 @@ pub async fn build_jvm( let gradle_cmd = get_gradle_command(project_root); - let _ = status_sender.send(BuildStatus::Building(format!("Running: {} fatJar", gradle_cmd))); + let _ = status_sender.send(BuildStatus::Building(format!("Running: {} shadowJar", gradle_cmd))); let mut child = Command::new(&gradle_cmd) .current_dir(project_root) - .args(["--console=plain", "fatJar"]) + .args(["--console=plain", "shadowJar"]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() - .context(format!("Failed to spawn `{} fatJar`", gradle_cmd))?; + .context(format!("Failed to spawn `{} shadowJar`", gradle_cmd))?; let stdout = child.stdout.take().expect("Stdout was piped"); let stderr = child.stderr.take().expect("Stderr was piped"); @@ -510,6 +510,59 @@ impl ToJObject for RayHit { #[repr(C)] #[derive(Clone, Copy)] +pub struct ShapeCastHitFFI { + pub collider: ColliderFFI, + pub distance: f64, + pub witness1: Vector3, + pub witness2: Vector3, + pub normal1: Vector3, + pub normal2: Vector3, + pub status: rapier3d::parry::query::ShapeCastStatus, +} + +impl ToJObject for ShapeCastHitFFI { + fn to_jobject<'a>(&self, env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> { + use jni::sys::jdouble; + + let collider = self.collider.to_jobject(env)?; + let witness1 = self.witness1.to_jobject(env)?; + let witness2 = self.witness2.to_jobject(env)?; + let normal1 = self.normal1.to_jobject(env)?; + let normal2 = self.normal2.to_jobject(env)?; + let status = self.status.to_jobject(env)?; + + let distance = self.distance as jdouble; + + let class = env.find_class("com/dropbear/physics/ShapeCastHit").map_err(|e| { + eprintln!("[JNI Error] Failed to find ShapeCastHit class: {:?}", e); + DropbearNativeError::JNIClassNotFound + })?; + + let object = env + .new_object( + class, + "(Lcom/dropbear/physics/Collider;DLcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Vector3d;Lcom/dropbear/physics/ShapeCastStatus;)V", + &[ + JValue::Object(&collider), + JValue::Double(distance), + JValue::Object(&witness1), + JValue::Object(&witness2), + JValue::Object(&normal1), + JValue::Object(&normal2), + JValue::Object(&status), + ], + ) + .map_err(|e| { + eprintln!("[JNI Error] Failed to create ShapeCastHit object: {:?}", e); + DropbearNativeError::JNIFailedToCreateObject + })?; + + Ok(object) + } +} + +#[repr(C)] +#[derive(Clone, Copy)] /// Class: `com.dropbear.physics.CollisionEventType` pub enum CollisionEventType { Started, @@ -35,4 +35,12 @@ fun normalizeRadians(radians: Double): Double { normalized += 2 * PI } return normalized +} + + +/** + * Compute the linear interpolation between two points x and y with a factor of t. + */ +fun lerp(x: Double, y: Double, t: Double): Double { + return x*(1-t)+y*t } @@ -57,6 +57,30 @@ class Physics { fun touching(entity1: EntityRef, entity2: EntityRef): Boolean { return isTouching(entity1, entity2) } + + /** + * Casts a [shape] from the [origin] in a specific [direction]. Returns a nullable [ShapeCastHit] object. + * + * @param origin The origin of the cast. + * @param direction The direction of the cast. Prefer unit vectors. + * @param shape The shape to cast. + * @param maxDistance The maximum distance before quitting operation. Set to `null` if no maxDistance. + * @param solid If true, detects hits even if the cast starts inside a shape. + * If false, the cast "passes through" from the inside until it exits. + */ + fun shapeCast( + origin: Vector3d, + direction: Vector3d, + shape: ColliderShape, + maxDistance: Double?, + solid: Boolean, + ): ShapeCastHit? { + return if (maxDistance != null) { + shapeCast(origin, direction, shape, toi = maxDistance, solid) + } else { + shapeCast(origin, direction, shape, toi = Double.MAX_VALUE, solid) + } + } } } @@ -66,4 +90,6 @@ internal expect fun setGravity(gravity: Vector3d) internal expect fun raycast(origin: Vector3d, direction: Vector3d, toi: Double, solid: Boolean): RayHit? internal expect fun isOverlapping(collider1: Collider, collider2: Collider): Boolean internal expect fun isTriggering(collider1: Collider, collider2: Collider): Boolean -internal expect fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean +internal expect fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean + +internal expect fun shapeCast(origin: Vector3d, direction: Vector3d, shape: ColliderShape, toi: Double, solid: Boolean): ShapeCastHit? @@ -0,0 +1,28 @@ +package com.dropbear.physics + +import com.dropbear.math.Vector3d + +/** + * Defines a hit from a shape-cast (sweeping a volume through space). + * + * @param collider The first collider that is hit. + * @param distance The distance travelled along the cast direction at the time of impact. + * @param witness1 Contact point on the world collider. + * @param witness2 Contact point on the casted shape. + * @param normal1 Normal pointing from the world collider toward the casted shape. + * @param normal2 Normal pointing from the casted shape toward the world collider. + * @param status Status of the cast result. + */ +class ShapeCastHit( + val collider: Collider, + val distance: Double, + val witness1: Vector3d, + val witness2: Vector3d, + val normal1: Vector3d, + val normal2: Vector3d, + val status: ShapeCastStatus, +) { + override fun toString(): String { + return "ShapeCastHit(collider=$collider, distance=$distance, status=$status)" + } +} @@ -2,7 +2,6 @@ package com.dropbear.physics; import com.dropbear.EucalyptusCoreLoader; import com.dropbear.math.Vector3d; -import org.jetbrains.annotations.Nullable; // fuck, this got a long ass name public class KinematicCharacterControllerNative { @@ -12,6 +12,7 @@ public class PhysicsNative { public static native void setGravity(long physicsHandle, Vector3d gravity); public static native RayHit raycast(long physicsHandle, Vector3d origin, Vector3d direction, double toi, boolean solid); + public static native ShapeCastHit shapeCast(long physicsHandle, Vector3d origin, Vector3d direction, ColliderShape shape, double toi, boolean solid); public static native boolean isOverlapping(long physicsHandle, Collider collider1, Collider collider2); public static native boolean isTriggering(long physicsHandle, Collider collider1, Collider collider2); public static native boolean isTouching(long physicsHandle, long entity1, long entity2); @@ -31,4 +31,14 @@ internal actual fun isTriggering(collider1: Collider, collider2: Collider): Bool internal actual fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean { return PhysicsNative.isTouching(DropbearEngine.native.physicsEngineHandle, entity1.id.raw, entity2.id.raw) +} + +internal actual fun shapeCast( + origin: Vector3d, + direction: Vector3d, + shape: ColliderShape, + toi: Double, + solid: Boolean +): ShapeCastHit? { + return PhysicsNative.shapeCast(DropbearEngine.native.physicsEngineHandle, origin, direction, shape, toi, solid) } @@ -29,4 +29,14 @@ internal actual fun isTriggering(collider1: Collider, collider2: Collider): Bool internal actual fun isTouching(entity1: EntityRef, entity2: EntityRef): Boolean { TODO("Not implemented yet") +} + +internal actual fun shapeCast( + origin: Vector3d, + direction: Vector3d, + shape: ColliderShape, + toi: Double, + solid: Boolean +): ShapeCastHit? { + TODO("Not yet implemented") }