diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72419f62..e065aebe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: - uses: bazel-contrib/setup-bazel@0.14.0 with: bazelisk-cache: true - disk-cache: ${{ github.workflow }} + disk-cache: true repository-cache: true - name: build run: | @@ -39,20 +39,24 @@ jobs: --verbose_failures \ //blazerod/model:test \ //blazerod/render:test + - name: collect artifacts + run: | + mkdir -p artifacts + BAZEL_BIN=$(bazel info bazel-bin --config opt) + cp "$BAZEL_BIN/mod/mod_fabric.jar" artifacts/ + cp "$BAZEL_BIN/mod/mod_neoforge.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/blazerod_fabric.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/blazerod_neoforge.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/model/model-base/model-base.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/model/model-formats/model-formats.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/model/model-gltf/model-gltf.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/model/model-pmd/model-pmd.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/model/model-pmx/model-pmx.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/model/model-vmd/model-vmd.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/model/model-assimp/model-assimp-merged.jar" artifacts/ + cp "$BAZEL_BIN/blazerod/example/ball_block/ball_block.jar" artifacts/ - name: capture build artifacts uses: actions/upload-artifact@v4 with: name: artifacts-bazel - path: | - bazel-bin/mod/mod_fabric.jar - bazel-bin/mod/mod_neoforge.jar - bazel-bin/blazerod/blazerod_fabric.jar - bazel-bin/blazerod/blazerod_neoforge.jar - bazel-bin/blazerod/model/model-base/model-base.jar - bazel-bin/blazerod/model/model-formats/model-formats.jar - bazel-bin/blazerod/model/model-gltf/model-gltf.jar - bazel-bin/blazerod/model/model-pmd/model-pmd.jar - bazel-bin/blazerod/model/model-pmx/model-pmx.jar - bazel-bin/blazerod/model/model-vmd/model-vmd.jar - bazel-bin/blazerod/model/model-assimp/model-assimp-merged.jar - bazel-bin/blazerod/example/ball_block/ball_block.jar + path: artifacts/ diff --git a/.gitignore b/.gitignore index fcc31bab..a6fa5b6b 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ compile_commands.json .bazelbsp bazel-* external + +#KAIMyEntity-C +KAIMyEntity-C/ diff --git a/KAIMyEntity-C b/KAIMyEntity-C new file mode 160000 index 00000000..ef7d0922 --- /dev/null +++ b/KAIMyEntity-C @@ -0,0 +1 @@ +Subproject commit ef7d0922e22ba2b000bb8d703abc83651c10421e diff --git a/blazerod/model/model-pmx/PmxLoader.kt b/blazerod/model/model-pmx/PmxLoader.kt index 317e2de1..5c0dea49 100644 --- a/blazerod/model/model-pmx/PmxLoader.kt +++ b/blazerod/model/model-pmx/PmxLoader.kt @@ -1022,7 +1022,10 @@ class PmxLoader : ModelFileLoader { shape = loadShapeType(buffer.get()), shapeSize = loadVector3f(buffer).mul(MMD_SCALE), shapePosition = loadVector3f(buffer).transformPosition(), - shapeRotation = loadVector3f(buffer).also { it.y *= -1 }, + shapeRotation = loadVector3f(buffer).also { + it.y *= -1 + it.z *= -1 + }, mass = buffer.getFloat(), moveAttenuation = buffer.getFloat(), rotationDamping = buffer.getFloat(), @@ -1060,7 +1063,10 @@ class PmxLoader : ModelFileLoader { val rigidBodyIndexA = loadRigidBodyIndex(buffer) val rigidBodyIndexB = loadRigidBodyIndex(buffer) val position = loadVector3f(buffer).transformPosition() - val rotation = loadVector3f(buffer).also { it.y *= -1 } + val rotation = loadVector3f(buffer).also { + it.y *= -1 + it.z *= -1 + } val positionMinimumOrig = loadVector3f(buffer).transformPosition() val positionMaximumOrig = loadVector3f(buffer).transformPosition() @@ -1069,10 +1075,18 @@ class PmxLoader : ModelFileLoader { val rotationMinimumOrig = loadVector3f(buffer) val rotationMaximumOrig = loadVector3f(buffer) - val rotationMinimum = Vector3f(-rotationMaximumOrig.x, rotationMinimumOrig.y, rotationMinimumOrig.z) - val rotationMaximum = Vector3f(-rotationMinimumOrig.x, rotationMaximumOrig.y, rotationMaximumOrig.z) + val rotationMinimum = Vector3f( + rotationMinimumOrig.x, + -rotationMaximumOrig.y, + -rotationMaximumOrig.z, + ) + val rotationMaximum = Vector3f( + rotationMaximumOrig.x, + -rotationMinimumOrig.y, + -rotationMinimumOrig.z, + ) val positionSpring = loadVector3f(buffer) - val rotationSpring = loadVector3f(buffer).also { it.x *= -1 } + val rotationSpring = loadVector3f(buffer) PmxJoint( nameLocal = nameLocal, @@ -1166,15 +1180,56 @@ class PmxLoader : ModelFileLoader { ) ) } + boneToRigidBodyMap[index]?.forEach { index -> add( NodeComponent.RigidBodyComponent( rigidBodyId = RigidBodyId(modelId, index), rigidBody = rigidBodies[index].let { rigidBody -> + val enableNameBasedOverrides = false + val basePhysicsMode = when (rigidBody.physicsMode) { + PmxRigidBody.PhysicsMode.FOLLOW_BONE -> RigidBody.PhysicsMode.FOLLOW_BONE + PmxRigidBody.PhysicsMode.PHYSICS -> RigidBody.PhysicsMode.PHYSICS + PmxRigidBody.PhysicsMode.PHYSICS_PLUS_BONE -> RigidBody.PhysicsMode.PHYSICS_PLUS_BONE + } + + val nameLocal = rigidBody.nameLocal + val adjustedPhysicsMode = if (enableNameBasedOverrides) { + when { + nameLocal.startsWith("Skirt_D_") -> + RigidBody.PhysicsMode.FOLLOW_BONE + nameLocal.startsWith("Ribbon_Braid_") -> basePhysicsMode + nameLocal.startsWith("Ribbon_") || + nameLocal.startsWith("Pocket Watch_") || + nameLocal.startsWith("Strap_") -> + RigidBody.PhysicsMode.FOLLOW_BONE + nameLocal.startsWith("Skirt_") && + basePhysicsMode == RigidBody.PhysicsMode.PHYSICS -> + RigidBody.PhysicsMode.PHYSICS_PLUS_BONE + else -> basePhysicsMode + } + } else { + basePhysicsMode + } + + val baseGroup = 1 shl rigidBody.groupId + val collisionMask = rigidBody.nonCollisionGroup and 0xFFFF + val defaultMask = collisionMask + println( + "PHYSDBG RB_GROUP " + + "idx=$index " + + "name=${rigidBody.nameLocal} " + + "groupId=${rigidBody.groupId} " + + "nonColl=${rigidBody.nonCollisionGroup} " + + "baseGroup=$baseGroup " + + "defaultMask=$defaultMask " + + "finalMask=$collisionMask", + ) + RigidBody( name = rigidBody.nameLocal.takeIf(String::isNotBlank), - collisionGroup = rigidBody.groupId, - collisionMask = rigidBody.nonCollisionGroup, + collisionGroup = baseGroup, + collisionMask = collisionMask, shape = when (rigidBody.shape) { PmxRigidBody.ShapeType.SPHERE -> RigidBody.ShapeType.SPHERE PmxRigidBody.ShapeType.BOX -> RigidBody.ShapeType.BOX @@ -1188,11 +1243,7 @@ class PmxLoader : ModelFileLoader { rotationDamping = rigidBody.rotationDamping, repulsion = rigidBody.repulsion, frictionForce = rigidBody.frictionForce, - physicsMode = when (rigidBody.physicsMode) { - PmxRigidBody.PhysicsMode.FOLLOW_BONE -> RigidBody.PhysicsMode.FOLLOW_BONE - PmxRigidBody.PhysicsMode.PHYSICS -> RigidBody.PhysicsMode.PHYSICS - PmxRigidBody.PhysicsMode.PHYSICS_PLUS_BONE -> RigidBody.PhysicsMode.PHYSICS_PLUS_BONE - }, + physicsMode = adjustedPhysicsMode, ) }, ) diff --git a/blazerod/render/api/animation/AnimationItem.kt b/blazerod/render/api/animation/AnimationItem.kt index 6c0069da..0073ada3 100644 --- a/blazerod/render/api/animation/AnimationItem.kt +++ b/blazerod/render/api/animation/AnimationItem.kt @@ -31,4 +31,12 @@ interface AnimationItemInstance { interface Factory { fun of(animation: AnimationItem): AnimationItemInstance } +} + +interface MaskableAnimationItemInstance { + fun applyMasked( + instance: ModelInstance, + pendingValues: AnimationItemPendingValues, + allowedNodeIndices: BooleanArray, + ) } \ No newline at end of file diff --git a/blazerod/render/api/resource/ModelInstance.kt b/blazerod/render/api/resource/ModelInstance.kt index 09be283b..4c7c83f6 100644 --- a/blazerod/render/api/resource/ModelInstance.kt +++ b/blazerod/render/api/resource/ModelInstance.kt @@ -13,6 +13,8 @@ import java.util.function.Consumer interface ModelInstance : RefCount { val scene: RenderScene + fun copyNodeWorldTransform(nodeIndex: Int, dest: Matrix4f) + fun clearTransform() fun setTransformMatrix(nodeIndex: Int, transformId: TransformId, matrix: Matrix4f) fun setTransformMatrix(nodeIndex: Int, transformId: TransformId, updater: Consumer) diff --git a/blazerod/render/api/resource/RenderScene.kt b/blazerod/render/api/resource/RenderScene.kt index 729b9368..95804e8d 100644 --- a/blazerod/render/api/resource/RenderScene.kt +++ b/blazerod/render/api/resource/RenderScene.kt @@ -4,6 +4,7 @@ import top.fifthlight.blazerod.api.refcount.RefCount import top.fifthlight.blazerod.model.Camera import top.fifthlight.blazerod.model.HumanoidTag import top.fifthlight.blazerod.model.NodeId +import top.fifthlight.blazerod.model.NodeTransformView interface RenderScene : RefCount { val rootNode: RenderNode @@ -12,6 +13,8 @@ interface RenderScene : RefCount { val expressionGroups: List val cameras: List + val renderTransform: NodeTransformView? + val ikTargetData: List val nodeIdMap: Map val nodeNameMap: Map diff --git a/blazerod/render/main/animation/AnimationChannelItem.kt b/blazerod/render/main/animation/AnimationChannelItem.kt index 9094f144..b1e2835e 100644 --- a/blazerod/render/main/animation/AnimationChannelItem.kt +++ b/blazerod/render/main/animation/AnimationChannelItem.kt @@ -15,6 +15,13 @@ import top.fifthlight.blazerod.runtime.resource.CameraTransformImpl sealed class AnimationChannelItem( val channel: AnimationChannel, ) { + open val targetNodeIndex: Int? = null + + @Suppress("UNCHECKED_CAST") + fun applyUnsafe(instance: ModelInstanceImpl, pendingValue: Any) { + (this as AnimationChannelItem).apply(instance, pendingValue) + } + abstract fun createPendingValue(): P // run on client thread @@ -28,6 +35,8 @@ sealed class AnimationChannelItem( private val transformId: TransformId, channel: AnimationChannel, ) : AnimationChannelItem(channel) { + override val targetNodeIndex: Int = index + init { require(channel.type == AnimationChannel.Type.Translation) { "Unmatched animation channel: want translation, but got ${channel.type}" } } @@ -50,6 +59,8 @@ sealed class AnimationChannelItem( private val transformId: TransformId, channel: AnimationChannel, ) : AnimationChannelItem(channel) { + override val targetNodeIndex: Int = index + init { require(channel.type == AnimationChannel.Type.Scale) { "Unmatched animation channel: want scale, but got ${channel.type}" } } @@ -72,6 +83,8 @@ sealed class AnimationChannelItem( private val transformId: TransformId, channel: AnimationChannel, ) : AnimationChannelItem(channel) { + override val targetNodeIndex: Int = index + init { require(channel.type == AnimationChannel.Type.Rotation) { "Unmatched animation channel: want rotation, but got ${channel.type}" } } @@ -95,6 +108,8 @@ sealed class AnimationChannelItem( private val transformId: TransformId, channel: AnimationChannel, ) : AnimationChannelItem(channel) { + override val targetNodeIndex: Int = index + init { require(channel.type == AnimationChannel.Type.BedrockTranslation) { "Unmatched animation channel: want translation, but got ${channel.type}" } } @@ -117,6 +132,8 @@ sealed class AnimationChannelItem( private val transformId: TransformId, channel: AnimationChannel, ) : AnimationChannelItem(channel) { + override val targetNodeIndex: Int = index + init { require(channel.type == AnimationChannel.Type.BedrockScale) { "Unmatched animation channel: want scale, but got ${channel.type}" } } @@ -139,6 +156,8 @@ sealed class AnimationChannelItem( private val transformId: TransformId, channel: AnimationChannel, ) : AnimationChannelItem(channel) { + override val targetNodeIndex: Int = index + init { require(channel.type == AnimationChannel.Type.BedrockRotation) { "Unmatched animation channel: want rotation, but got ${channel.type}" } } diff --git a/blazerod/render/main/animation/AnimationItemImpl.kt b/blazerod/render/main/animation/AnimationItemImpl.kt index c12c9ffe..8347b723 100644 --- a/blazerod/render/main/animation/AnimationItemImpl.kt +++ b/blazerod/render/main/animation/AnimationItemImpl.kt @@ -3,6 +3,7 @@ package top.fifthlight.blazerod.animation import top.fifthlight.blazerod.api.animation.AnimationItem import top.fifthlight.blazerod.api.animation.AnimationItemInstance import top.fifthlight.blazerod.api.animation.AnimationItemPendingValues +import top.fifthlight.blazerod.api.animation.MaskableAnimationItemInstance import top.fifthlight.blazerod.api.resource.ModelInstance import top.fifthlight.blazerod.api.resource.RenderScene import top.fifthlight.blazerod.model.animation.Animation @@ -40,7 +41,7 @@ class AnimationItemPendingValuesImpl(animationItem: AnimationItemImpl) : Animati } @ActualImpl(AnimationItemInstance::class) -class AnimationItemInstanceImpl(val animationItem: AnimationItemImpl) : AnimationItemInstance { +class AnimationItemInstanceImpl(val animationItem: AnimationItemImpl) : AnimationItemInstance, MaskableAnimationItemInstance { @ActualConstructor("of") constructor(animationItem: AnimationItem) : this(animationItem as AnimationItemImpl) @@ -76,10 +77,42 @@ class AnimationItemInstanceImpl(val animationItem: AnimationItemImpl) : Animatio pendingValues.pendingValues[index].let { pendingValue -> channel.applyUnsafe(instance, pendingValue) } - if (!pendingValues.applied) { - pendingValues.applied = true - pendingStack.addLast(pendingValues) + } + if (!pendingValues.applied) { + pendingValues.applied = true + pendingStack.addLast(pendingValues) + } + } + + override fun applyMasked( + instance: ModelInstance, + pendingValues: AnimationItemPendingValues, + allowedNodeIndices: BooleanArray, + ) { + val instance = instance as ModelInstanceImpl + val pendingValues = pendingValues as AnimationItemPendingValuesImpl + + animationItem.channels.forEachIndexed { index, channel -> + val targetNodeIndex = channel.targetNodeIndex + if (targetNodeIndex == null || targetNodeIndex !in allowedNodeIndices.indices || !allowedNodeIndices[targetNodeIndex]) { + return@forEachIndexed } + + pendingValues.pendingValues[index].let { pendingValue -> + channel.applyUnsafe(instance, pendingValue) + } + } + if (!pendingValues.applied) { + pendingValues.applied = true + pendingStack.addLast(pendingValues) + } + } + + fun recyclePendingValues(pendingValues: AnimationItemPendingValues) { + val pendingValues = pendingValues as? AnimationItemPendingValuesImpl ?: return + if (!pendingValues.applied) { + pendingValues.applied = true + pendingStack.addLast(pendingValues) } } } diff --git a/blazerod/render/main/physics/PhysicsLibrary.java b/blazerod/render/main/physics/PhysicsLibrary.java index 36a8a3e1..37deca51 100644 --- a/blazerod/render/main/physics/PhysicsLibrary.java +++ b/blazerod/render/main/physics/PhysicsLibrary.java @@ -31,6 +31,10 @@ private PhysicsLibrary() { public native static void stepPhysicsWorld(long physicsWorld, float deltaTime, int maxSubSteps, float fixedTimeStep); + public native static void resetRigidBody(long physicsWorld, int rigidBodyIndex, + float px, float py, float pz, + float qx, float qy, float qz, float qw); + public native static void destroyPhysicsWorld(long physicsWorld); public static boolean isPhysicsAvailable() { diff --git a/blazerod/render/main/physics/PhysicsScene.kt b/blazerod/render/main/physics/PhysicsScene.kt index 6468b765..6681c471 100644 --- a/blazerod/render/main/physics/PhysicsScene.kt +++ b/blazerod/render/main/physics/PhysicsScene.kt @@ -2,6 +2,7 @@ package top.fifthlight.blazerod.physics import org.joml.Vector3fc import top.fifthlight.blazerod.model.RigidBody +import top.fifthlight.blazerod.model.util.MMD_SCALE import top.fifthlight.blazerod.runtime.resource.RenderPhysicsJoint import java.lang.ref.Reference import java.nio.ByteBuffer diff --git a/blazerod/render/main/physics/PhysicsWorld.cpp b/blazerod/render/main/physics/PhysicsWorld.cpp index 4a561429..89fb78ae 100644 --- a/blazerod/render/main/physics/PhysicsWorld.cpp +++ b/blazerod/render/main/physics/PhysicsWorld.cpp @@ -1,5 +1,7 @@ #include "PhysicsWorld.h" +#include +#include #include #include @@ -30,15 +32,43 @@ PhysicsMotionState::PhysicsMotionState(btTransform& initial_transform, const Vec transform.setIdentity(); btMatrix3x3 rotation_matrix; - rotation_matrix.setEulerZYX(rotation.x, rotation.y, rotation.z); - btVector3 pos(position.x, position.y, position.z); - btTransform rigidbody_transform; - rigidbody_transform.setIdentity(); - rigidbody_transform.setOrigin(pos); - rigidbody_transform.setBasis(rotation_matrix); - - from_node_to_world.mult(rigidbody_transform, initial_transform.inverse()); + // PmxLoader already applies the necessary axis flips to the Euler angles. + float rx_val = rotation.x; + float ry_val = rotation.y; + float rz_val = rotation.z; + + btMatrix3x3 rot_x(1, 0, 0, + 0, cos(rx_val), -sin(rx_val), + 0, sin(rx_val), cos(rx_val)); + btMatrix3x3 rot_y(cos(ry_val), 0, sin(ry_val), + 0, 1, 0, + -sin(ry_val), 0, cos(ry_val)); + btMatrix3x3 rot_z(cos(rz_val), -sin(rz_val), 0, + sin(rz_val), cos(rz_val), 0, + 0, 0, 1); + + rotation_matrix = rot_y * rot_x * rot_z; + + // Build the rigid-body transform in model/world space from the PMX + // shape position and rotation. + btTransform bone_transform = initial_transform; + btTransform rb_transform; + rb_transform.setIdentity(); + rb_transform.setBasis(rotation_matrix); + rb_transform.setOrigin(btVector3(position.x, position.y, position.z)); + + // Local offset from the bone to the rigid body, matching Saba's + // MMDPhysics logic: offset = inverse(boneGlobal) * rbMat. + btTransform bone_inverse = bone_transform.inverse(); + btTransform local_offset = bone_inverse * rb_transform; + + from_node_to_world = local_offset; from_world_to_node = from_node_to_world.inverse(); + + // Initialize the rigid body so that its center-of-mass starts exactly + // at the PMX shape_position in world space (no extra bone rotation + // applied on top). + this->transform = rb_transform; } class FollowBoneObjectMotionState : public PhysicsMotionState { @@ -48,8 +78,16 @@ class FollowBoneObjectMotionState : public PhysicsMotionState { void GetFromWorld(const PhysicsWorld* world, size_t rigidbody_index) override { btTransform node_transform; - node_transform.setFromOpenGLMatrix(&world->GetTransformBuffer()[rigidbody_index * 16]); - this->transform.mult(node_transform, this->from_node_to_world); + float* buffer = &world->GetTransformBuffer()[rigidbody_index * 7]; + // Input: MMD (LH) -> Bullet (RH) + // Flip Z position. + node_transform.setOrigin(btVector3(buffer[0], buffer[1], buffer[2])); + // Flip X and Y rotation (preserve Z rotation direction relative to flipped axis). + // MMD Quaternion (x, y, z, w) -> Bullet Quaternion (-x, -y, z, w) + node_transform.setRotation(btQuaternion(buffer[3], buffer[4], buffer[5], buffer[6])); + + btTransform node_rh = node_transform; + this->transform.mult(node_rh, this->from_node_to_world); } void setWorldTransform(const btTransform& world_transform) override {} @@ -64,15 +102,36 @@ class PhysicsObjectMotionState : public PhysicsMotionState { void GetFromWorld(const PhysicsWorld* world, size_t rigidbody_index) override { btTransform node_transform; - node_transform.setFromOpenGLMatrix(&world->GetTransformBuffer()[rigidbody_index * 16]); - this->transform.mult(node_transform, this->from_node_to_world); + float* buffer = &world->GetTransformBuffer()[rigidbody_index * 7]; + // Input: MMD (LH) -> Bullet (RH) + node_transform.setOrigin(btVector3(buffer[0], buffer[1], buffer[2])); + node_transform.setRotation(btQuaternion(buffer[3], buffer[4], buffer[5], buffer[6])); + + btTransform node_rh = node_transform; + this->transform.mult(node_rh, this->from_node_to_world); } void SetToWorld(PhysicsWorld* world, size_t rigidbody_index) override { this->isDirty = false; - btTransform node_transform; - node_transform.mult(this->transform, this->from_world_to_node); - node_transform.getOpenGLMatrix(&world->GetTransformBuffer()[rigidbody_index * 16]); + btTransform node_transform_rh; + node_transform_rh.mult(this->transform, this->from_world_to_node); + + btTransform node_transform = node_transform_rh; + + float* buffer = &world->GetTransformBuffer()[rigidbody_index * 7]; + btVector3 pos = node_transform.getOrigin(); + btQuaternion rot = node_transform.getRotation(); + + // Output: Bullet (RH) -> MMD (LH) + // Flip Z back. + buffer[0] = pos.x(); + buffer[1] = pos.y(); + buffer[2] = pos.z(); + // Flip X and Y back. + buffer[3] = rot.x(); + buffer[4] = rot.y(); + buffer[5] = rot.z(); + buffer[6] = rot.w(); } }; @@ -86,18 +145,41 @@ class PhysicsPlusBoneObjectMotionState : public PhysicsMotionState { void GetFromWorld(const PhysicsWorld* world, size_t rigidbody_index) override { btTransform node_transform; - node_transform.setFromOpenGLMatrix(&world->GetTransformBuffer()[rigidbody_index * 16]); - this->transform.mult(node_transform, this->from_node_to_world); - this->origin = transform.getOrigin(); + float* buffer = &world->GetTransformBuffer()[rigidbody_index * 7]; + // Input: MMD (LH) -> Bullet (RH) + node_transform.setOrigin(btVector3(buffer[0], buffer[1], buffer[2])); + node_transform.setRotation(btQuaternion(buffer[3], buffer[4], buffer[5], buffer[6])); + + btTransform node_rh = node_transform; + this->transform.mult(node_rh, this->from_node_to_world); + // For PHYSICS_PLUS_BONE, keep translation locked to the bone (node) position + // and only let Bullet drive the rotation. Use the node's origin here so that + // we don't propagate the rigidbody's center-of-mass offset back into the + // engine space, which was causing hair bodies to jump far away. + this->origin = node_rh.getOrigin(); } void SetToWorld(PhysicsWorld* world, size_t rigidbody_index) override { this->isDirty = false; btTransform world_transform = this->transform; world_transform.setOrigin(this->origin); - btTransform node_transform; - node_transform.mult(world_transform, this->from_world_to_node); - node_transform.getOpenGLMatrix(&world->GetTransformBuffer()[rigidbody_index * 16]); + btTransform node_transform_rh; + node_transform_rh.mult(world_transform, this->from_world_to_node); + + btTransform node_transform = node_transform_rh; + + float* buffer = &world->GetTransformBuffer()[rigidbody_index * 7]; + btVector3 pos = node_transform.getOrigin(); + btQuaternion rot = node_transform.getRotation(); + + // Output: Bullet (RH) -> MMD (LH) + buffer[0] = pos.x(); + buffer[1] = pos.y(); + buffer[2] = pos.z(); + buffer[3] = rot.x(); + buffer[4] = rot.y(); + buffer[5] = rot.z(); + buffer[6] = rot.w(); } }; @@ -108,7 +190,12 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c this->solver = std::make_unique(); this->world = std::make_unique(this->dispatcher.get(), this->broadphase.get(), this->solver.get(), this->collision_config.get()); - this->world->setGravity(btVector3(0, -9.81, 0)); + this->world->setGravity(btVector3(0, -98.0f, 0)); + + btContactSolverInfo& solver_info = this->world->getSolverInfo(); + solver_info.m_numIterations = 20; + solver_info.m_splitImpulse = 1; + solver_info.m_splitImpulsePenetrationThreshold = -0.04f; this->ground_shape = std::make_unique(btVector3(0, 1, 0), 0.0f); btTransform ground_transform; @@ -127,8 +214,27 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c if (initial_transform_count != rigidbodies.size() * 16) { throw std::invalid_argument("Initial transform count must match rigidbody count"); } - transform_buffer = std::make_unique(initial_transform_count); - memcpy(transform_buffer.get(), initial_transform, initial_transform_count * sizeof(float)); + + size_t num_rigidbodies = rigidbodies.size(); + transform_buffer = std::make_unique(num_rigidbodies * 7); + + // Convert initial matrix transforms to pos+rot format + for (size_t i = 0; i < num_rigidbodies; i++) { + btTransform transform; + transform.setFromOpenGLMatrix(&initial_transform[i * 16]); + + btVector3 pos = transform.getOrigin(); + btQuaternion rot = transform.getRotation(); + + transform_buffer[i * 7 + 0] = pos.x(); + transform_buffer[i * 7 + 1] = pos.y(); + transform_buffer[i * 7 + 2] = pos.z(); + transform_buffer[i * 7 + 3] = rot.x(); + transform_buffer[i * 7 + 4] = rot.y(); + transform_buffer[i * 7 + 5] = rot.z(); + transform_buffer[i * 7 + 6] = rot.w(); + } + this->rigidbodies.reserve(rigidbodies.size()); size_t rigidbody_count = 0; @@ -191,7 +297,7 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c } } - btRigidBody::btRigidBodyConstructionInfo rigidbody_info(rigidbody_item.mass, motion_state.get(), shape.get(), + btRigidBody::btRigidBodyConstructionInfo rigidbody_info(mass, motion_state.get(), shape.get(), local_inertia); rigidbody_info.m_linearDamping = rigidbody_item.move_attenuation; rigidbody_info.m_angularDamping = rigidbody_item.rotation_damping; @@ -200,8 +306,11 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c rigidbody_info.m_additionalDamping = true; auto rigidbody = std::make_unique(rigidbody_info); - this->world->addRigidBody(rigidbody.get()); - rigidbody->setActivationState(DISABLE_DEACTIVATION); + rigidbody->setSleepingThresholds(0.01f, 0.0017453293f); + this->world->addRigidBody(rigidbody.get(), rigidbody_item.collision_group, rigidbody_item.collision_mask); + if (rigidbody_item.physics_mode != PhysicsMode::PHYSICS) { + rigidbody->setActivationState(DISABLE_DEACTIVATION); + } if (rigidbody_item.physics_mode == PhysicsMode::FOLLOW_BONE) { rigidbody->setCollisionFlags(rigidbody->getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT); } @@ -209,6 +318,7 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c rigidbody_data.shape = std::move(shape); rigidbody_data.motion_state = std::move(motion_state); rigidbody_data.rigidbody = std::move(rigidbody); + rigidbody_data.physics_mode = rigidbody_item.physics_mode; this->rigidbodies.push_back(std::move(rigidbody_data)); } @@ -219,7 +329,11 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c for (const Joint& joint_item : joints) { size_t joint_index = joint_count++; btMatrix3x3 rotation_matrix; - rotation_matrix.setEulerZYX(joint_item.rotation.x, joint_item.rotation.y, joint_item.rotation.z); + // Match Saba's MMDPhysics PMX joint path: use preprocessed joint rotation directly. + rotation_matrix.setEulerZYX( + joint_item.rotation.x, + joint_item.rotation.y, + joint_item.rotation.z); btTransform transform; transform.setIdentity(); @@ -237,12 +351,13 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c } const auto& rigidbody_b = this->rigidbodies[joint_item.rigidbody_b_index]; - btTransform inverse_a; - btTransform inverse_b; - inverse_a.setFromOpenGLMatrix(initial_transform + rigidbody_a_index * 16); - inverse_b.setFromOpenGLMatrix(initial_transform + rigidbody_b_index * 16); - inverse_a = inverse_a.inverse() * transform; - inverse_b = inverse_b.inverse() * transform; + const btTransform& body_a_transform = this->rigidbodies[rigidbody_a_index].rigidbody->getWorldTransform(); + + // body_b_transform.setFromOpenGLMatrix(initial_transform + rigidbody_b_index * 16); + const btTransform& body_b_transform = this->rigidbodies[rigidbody_b_index].rigidbody->getWorldTransform(); + + btTransform inverse_a = body_a_transform.inverse() * transform; + btTransform inverse_b = body_b_transform.inverse() * transform; auto constraint = std::make_unique( *rigidbody_a.rigidbody, *rigidbody_b.rigidbody, inverse_a, inverse_b, true); @@ -251,6 +366,7 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c constraint->setLinearUpperLimit( btVector3(joint_item.position_max.x, joint_item.position_max.y, joint_item.position_max.z)); + // Apply angular limits directly from the preprocessed min/max values. constraint->setAngularLowerLimit( btVector3(joint_item.rotation_min.x, joint_item.rotation_min.y, joint_item.rotation_min.z)); constraint->setAngularUpperLimit( @@ -266,7 +382,7 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c } if (joint_item.position_spring.z != 0.0f) { constraint->enableSpring(2, true); - constraint->setStiffness(2, -joint_item.position_spring.z); + constraint->setStiffness(2, joint_item.position_spring.z); } if (joint_item.rotation_spring.x != 0.0f) { constraint->enableSpring(3, true); @@ -281,7 +397,7 @@ PhysicsWorld::PhysicsWorld(const PhysicsScene& scene, size_t initial_transform_c constraint->setStiffness(5, joint_item.rotation_spring.z); } - this->world->addConstraint(constraint.get(), true); + this->world->addConstraint(constraint.get(), false); this->joints.push_back(std::move(constraint)); } } @@ -296,18 +412,61 @@ PhysicsWorld::~PhysicsWorld() { } } +void PhysicsWorld::ResetRigidBody(size_t rigidbody_index, float px, float py, float pz, + float qx, float qy, float qz, float qw) { + if (rigidbody_index >= this->rigidbodies.size()) { + throw std::out_of_range("Invalid rigidbody index"); + } + + auto& rigidbody_data = this->rigidbodies[rigidbody_index]; + + btTransform node_transform; + node_transform.setIdentity(); + node_transform.setOrigin(btVector3(px, py, pz)); + node_transform.setRotation(btQuaternion(qx, qy, qz, qw)); + + btTransform world_transform; + world_transform.mult(node_transform, rigidbody_data.motion_state->GetFromNodeToWorld()); + + rigidbody_data.motion_state->SetWorldTransformDirect(world_transform); + rigidbody_data.rigidbody->setWorldTransform(world_transform); + rigidbody_data.rigidbody->setInterpolationWorldTransform(world_transform); + rigidbody_data.rigidbody->setLinearVelocity(btVector3(0, 0, 0)); + rigidbody_data.rigidbody->setAngularVelocity(btVector3(0, 0, 0)); + rigidbody_data.rigidbody->clearForces(); + + float* buffer = this->transform_buffer.get(); + float* dst = &buffer[rigidbody_index * 7]; + dst[0] = px; + dst[1] = py; + dst[2] = pz; + dst[3] = qx; + dst[4] = qy; + dst[5] = qz; + dst[6] = qw; +} + void PhysicsWorld::Step(float delta_time, int max_sub_steps, float fixed_time_step) { size_t rigidbody_index = 0; for (auto& rigidbody : this->rigidbodies) { - rigidbody.motion_state->GetFromWorld(this, rigidbody_index++); + if (rigidbody.physics_mode == PhysicsMode::FOLLOW_BONE || rigidbody.physics_mode == PhysicsMode::PHYSICS_PLUS_BONE) { + rigidbody.motion_state->GetFromWorld(this, rigidbody_index); + btTransform world_transform; + rigidbody.motion_state->getWorldTransform(world_transform); + rigidbody.rigidbody->setWorldTransform(world_transform); + rigidbody.rigidbody->setInterpolationWorldTransform(world_transform); + rigidbody.rigidbody->activate(true); + this->world->updateSingleAabb(rigidbody.rigidbody.get()); + } + rigidbody_index++; } - // this->world->stepSimulation(delta_time, max_sub_steps, fixed_time_step); + this->world->stepSimulation(delta_time, max_sub_steps, fixed_time_step); rigidbody_index = 0; for (auto& rigidbody : this->rigidbodies) { - if (!rigidbody.motion_state->IsDirty()) { - continue; + if (rigidbody.motion_state->IsDirty()) { + rigidbody.motion_state->SetToWorld(this, rigidbody_index); } - rigidbody.motion_state->SetToWorld(this, rigidbody_index++); + rigidbody_index++; } } diff --git a/blazerod/render/main/physics/PhysicsWorld.h b/blazerod/render/main/physics/PhysicsWorld.h index 1abbe448..8c4d73e5 100644 --- a/blazerod/render/main/physics/PhysicsWorld.h +++ b/blazerod/render/main/physics/PhysicsWorld.h @@ -27,6 +27,12 @@ class PhysicsMotionState : public btMotionState { void setWorldTransform(const btTransform& world_transform) override; bool IsDirty() const { return isDirty; } + const btTransform& GetFromNodeToWorld() const { return from_node_to_world; } + void SetWorldTransformDirect(const btTransform& world_transform) { + transform = world_transform; + isDirty = true; + } + virtual void GetFromWorld(const PhysicsWorld* world, size_t rigidbody_index) = 0; virtual void SetToWorld(PhysicsWorld* world, size_t rigidbody_index) = 0; }; @@ -35,6 +41,7 @@ struct RigidBodyData { std::unique_ptr shape; std::unique_ptr motion_state; std::unique_ptr rigidbody; + PhysicsMode physics_mode; }; class PhysicsWorld { @@ -60,7 +67,9 @@ class PhysicsWorld { ~PhysicsWorld(); float* GetTransformBuffer() const { return transform_buffer.get(); } - size_t GetTransformBufferSize() { return rigidbodies.size() * 16 * sizeof(float); } + size_t GetTransformBufferSize() { return rigidbodies.size() * 7 * sizeof(float); } + void ResetRigidBody(size_t rigidbody_index, float px, float py, float pz, + float qx, float qy, float qz, float qw); void Step(float delta_time, int max_sub_steps, float fixed_time_step); }; } // namespace blazerod::physics diff --git a/blazerod/render/main/physics/PhysicsWorld.kt b/blazerod/render/main/physics/PhysicsWorld.kt index 15889465..3c6baa79 100644 --- a/blazerod/render/main/physics/PhysicsWorld.kt +++ b/blazerod/render/main/physics/PhysicsWorld.kt @@ -1,10 +1,13 @@ package top.fifthlight.blazerod.physics import org.joml.Matrix4f +import org.joml.Quaternionf +import org.joml.Vector3f import java.lang.AutoCloseable import java.lang.ref.Reference import java.nio.ByteBuffer import java.nio.ByteOrder +import java.nio.FloatBuffer import java.util.* class PhysicsWorld( @@ -15,6 +18,7 @@ class PhysicsWorld( private var closed = false internal val rigidBodyCount = scene.rigidBodyCount private val transformBuffer: ByteBuffer + private val transformValues: FloatBuffer init { if (!PhysicsLibrary.isPhysicsAvailable()) { @@ -25,10 +29,11 @@ class PhysicsWorld( } finally { Reference.reachabilityFence(initialTransform) } - transformBuffer = ByteBuffer.allocateDirect(initialTransform.capacity()).order(ByteOrder.nativeOrder())//PhysicsLibrary.getTransformBuffer(pointer).order(ByteOrder.nativeOrder()) - transformBuffer.put(initialTransform) - transformBuffer.clear() - initialTransform.clear() + transformBuffer = PhysicsLibrary.getTransformBuffer(pointer).order(ByteOrder.nativeOrder()) + transformValues = transformBuffer.asFloatBuffer() + // transformBuffer.put(initialTransform) // Buffer formats are different now + // transformBuffer.clear() + // initialTransform.clear() } private inline fun requireNotClosed(crossinline block: () -> T): T { @@ -38,15 +43,61 @@ class PhysicsWorld( fun getTransform(rigidBodyIndex: Int, dst: Matrix4f): Matrix4f = requireNotClosed { Objects.checkIndex(rigidBodyIndex, rigidBodyCount) - dst.apply { - set(rigidBodyIndex * 64, transformBuffer) - } + val offset = rigidBodyIndex * 28 // 7 floats * 4 bytes + val px = transformBuffer.getFloat(offset + 0) + val py = transformBuffer.getFloat(offset + 4) + val pz = transformBuffer.getFloat(offset + 8) + val qx = transformBuffer.getFloat(offset + 12) + val qy = transformBuffer.getFloat(offset + 16) + val qz = transformBuffer.getFloat(offset + 20) + val qw = transformBuffer.getFloat(offset + 24) + dst.translationRotate(px, py, pz, qx, qy, qz, qw) } fun setTransform(rigidBodyIndex: Int, transform: Matrix4f) { Objects.checkIndex(rigidBodyIndex, rigidBodyCount) requireNotClosed { - transform.get(rigidBodyIndex * 64, transformBuffer) + val offset = rigidBodyIndex * 28 // 7 floats * 4 bytes + val pos = Vector3f() + transform.getTranslation(pos) + val rot = Quaternionf() + transform.getUnnormalizedRotation(rot) + + transformBuffer.putFloat(offset + 0, pos.x) + transformBuffer.putFloat(offset + 4, pos.y) + transformBuffer.putFloat(offset + 8, pos.z) + transformBuffer.putFloat(offset + 12, rot.x) + transformBuffer.putFloat(offset + 16, rot.y) + transformBuffer.putFloat(offset + 20, rot.z) + transformBuffer.putFloat(offset + 24, rot.w) + } + } + + fun resetRigidBody(rigidBodyIndex: Int, position: Vector3f, rotation: Quaternionf) { + Objects.checkIndex(rigidBodyIndex, rigidBodyCount) + requireNotClosed { + PhysicsLibrary.resetRigidBody( + pointer, + rigidBodyIndex, + position.x, position.y, position.z, + rotation.x, rotation.y, rotation.z, rotation.w, + ) + } + } + + fun pullTransforms(dst: FloatArray) { + requireNotClosed { + require(dst.size >= rigidBodyCount * 7) + transformValues.clear() + transformValues.get(dst, 0, rigidBodyCount * 7) + } + } + + fun pushTransforms(src: FloatArray) { + requireNotClosed { + require(src.size >= rigidBodyCount * 7) + transformValues.clear() + transformValues.put(src, 0, rigidBodyCount * 7) } } diff --git a/blazerod/render/main/physics/binding.cpp b/blazerod/render/main/physics/binding.cpp index 57b00afe..5f99ce1b 100644 --- a/blazerod/render/main/physics/binding.cpp +++ b/blazerod/render/main/physics/binding.cpp @@ -115,6 +115,19 @@ JNIEXPORT void JNICALL Java_top_fifthlight_blazerod_physics_PhysicsLibrary_stepP physics_world_ptr->Step(delta_time, max_sub_steps, fixed_time_step); } +/* + * Class: top_fifthlight_blazerod_physics_PhysicsLibrary + * Method: resetRigidBody + * Signature: (JIFFFFFFFF)V + */ +JNIEXPORT void JNICALL Java_top_fifthlight_blazerod_physics_PhysicsLibrary_resetRigidBody( + JNIEnv* env, jclass clazz, jlong physics_world, jint rigidbody_index, + jfloat px, jfloat py, jfloat pz, + jfloat qx, jfloat qy, jfloat qz, jfloat qw) { + auto physics_world_ptr = reinterpret_cast(physics_world); + physics_world_ptr->ResetRigidBody(static_cast(rigidbody_index), px, py, pz, qx, qy, qz, qw); +} + /* * Class: top_fifthlight_blazerod_physics_PhysicsLibrary * Method: destroyPhysicsWorld diff --git a/blazerod/render/main/runtime/ModelInstanceImpl.kt b/blazerod/render/main/runtime/ModelInstanceImpl.kt index 6133525d..8fbcf4e9 100644 --- a/blazerod/render/main/runtime/ModelInstanceImpl.kt +++ b/blazerod/render/main/runtime/ModelInstanceImpl.kt @@ -3,6 +3,8 @@ package top.fifthlight.blazerod.runtime import net.minecraft.client.render.VertexConsumerProvider import org.joml.Matrix4f import org.joml.Matrix4fc +import org.joml.Quaternionf +import org.joml.Vector3f import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil import top.fifthlight.blazerod.api.refcount.AbstractRefCount @@ -40,6 +42,10 @@ class ModelInstanceImpl( override val typeId: String get() = "model_instance" + override fun copyNodeWorldTransform(nodeIndex: Int, dest: Matrix4f) { + dest.set(modelData.worldTransforms[nodeIndex]) + } + val modelData = ModelData(scene) internal val physicsData = if (PhysicsInterface.isPhysicsAvailable && scene.physicsScene != null) { PhysicsData(scene, modelData, scene.physicsScene) @@ -50,6 +56,11 @@ class ModelInstanceImpl( init { scene.increaseReferenceCount() scene.attachToInstance(this) + // Ensure transforms are calculated (Bind Pose) before initializing physics + // Otherwise rigid bodies will be initialized with Identity transforms, leading to incorrect offsets + for (i in scene.nodes.indices) { + updateNodeTransform(i) + } physicsData?.initialize() } @@ -59,9 +70,13 @@ class ModelInstanceImpl( private val physicsScene: PhysicsScene, ) : AutoCloseable { var lastPhysicsTime: Float = -1f + var debugStepCount: Int = 0 + var explosionLogCount: Int = 0 private var _world: PhysicsWorld? = null val world: PhysicsWorld get() = _world ?: error("PhysicsWorld is not initialized") + lateinit var transformArray: FloatArray + private set fun initialize() { if (_world != null) { @@ -74,6 +89,45 @@ class ModelInstanceImpl( nodeWorldTransform.get(component.rigidBodyIndex * 64, initialTransform) } _world = PhysicsWorld(physicsScene, initialTransform) + transformArray = FloatArray(scene.rigidBodyComponents.size * 7) + // Initial pull to populate array + _world!!.pullTransforms(transformArray) + + // Debug: log initial bone vs rigidbody transforms for the first few bodies + val world = _world!! + val maxLogged = minOf(scene.rigidBodyComponents.size, 16) + for (i in 0 until maxLogged) { + val (nodeIndex, component) = scene.rigidBodyComponents[i] + val node = scene.nodes[nodeIndex] + + val boneWorld = modelData.worldTransforms[nodeIndex] + val bonePos = Vector3f() + val boneRot = Quaternionf() + boneWorld.getTranslation(bonePos) + boneWorld.getUnnormalizedRotation(boneRot) + + val bodyMatrix = Matrix4f() + world.getTransform(component.rigidBodyIndex, bodyMatrix) + val bodyPos = Vector3f() + val bodyRot = Quaternionf() + bodyMatrix.getTranslation(bodyPos) + bodyMatrix.getUnnormalizedRotation(bodyRot) + + val rb = component.rigidBodyData + println( + "PHYSDBG RB_INIT " + + "idx=${component.rigidBodyIndex} " + + "nodeIndex=$nodeIndex " + + "nodeName=${node.nodeName} " + + "mode=${rb.physicsMode} " + + "shapePos=(${rb.shapePosition.x()},${rb.shapePosition.y()},${rb.shapePosition.z()}) " + + "shapeRot=(${rb.shapeRotation.x()},${rb.shapeRotation.y()},${rb.shapeRotation.z()}) " + + "bonePos=(${bonePos.x},${bonePos.y},${bonePos.z}) " + + "boneRot=(${boneRot.x},${boneRot.y},${boneRot.z},${boneRot.w}) " + + "bodyPos=(${bodyPos.x},${bodyPos.y},${bodyPos.z}) " + + "bodyRot=(${bodyRot.x},${bodyRot.y},${bodyRot.z},${bodyRot.w})" + ) + } } } @@ -92,6 +146,7 @@ class ModelInstanceImpl( val transformDirty = Array(scene.nodes.size) { true } val worldTransforms = Array(scene.nodes.size) { Matrix4f() } + val worldTransformsNoPhysics = Array(scene.nodes.size) { Matrix4f() } val localMatricesBuffer = run { val buffer = LocalMatricesBuffer(scene.primitiveComponents.size) @@ -256,6 +311,25 @@ class ModelInstanceImpl( } } + internal fun updateNodeTransformNoPhysics(node: RenderNodeImpl) { + val nodeIndex = node.nodeIndex + val localBase = modelData.transformMaps[nodeIndex].getSum(TransformId.EXTERNAL_PARENT_DEFORM) + val parent = node.parent + val dst = modelData.worldTransformsNoPhysics[nodeIndex] + if (parent != null) { + dst.set(modelData.worldTransformsNoPhysics[parent.nodeIndex]).mul(localBase) + } else { + dst.set(localBase) + } + for (child in node.children) { + updateNodeTransformNoPhysics(child) + } + } + + internal fun updateWorldTransformsNoPhysics() { + updateNodeTransformNoPhysics(scene.rootNode) + } + override fun createRenderTask( modelMatrix: Matrix4fc, light: Int, diff --git a/blazerod/render/main/runtime/RenderSceneImpl.kt b/blazerod/render/main/runtime/RenderSceneImpl.kt index 009e9339..f7634071 100644 --- a/blazerod/render/main/runtime/RenderSceneImpl.kt +++ b/blazerod/render/main/runtime/RenderSceneImpl.kt @@ -2,7 +2,10 @@ package top.fifthlight.blazerod.runtime import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap import net.minecraft.client.render.VertexConsumerProvider +import org.joml.Matrix4f import org.joml.Matrix4fc +import org.joml.Quaternionf +import org.joml.Vector3f import top.fifthlight.blazerod.api.refcount.AbstractRefCount import top.fifthlight.blazerod.api.resource.RenderExpression import top.fifthlight.blazerod.api.resource.RenderExpressionGroup @@ -32,12 +35,14 @@ class RenderSceneImpl( override val expressionGroups: List, override val cameras: List, val physicsJoints: List, - val renderTransform: NodeTransform?, + override val renderTransform: NodeTransform?, ) : AbstractRefCount(), RenderScene { companion object { - private const val PHYSICS_MAX_SUB_STEP_COUNT = 1 + private const val PHYSICS_MAX_SUB_STEP_COUNT = 10 private const val PHYSICS_FPS = 120f private const val PHYSICS_TIME_STEP = 1f / PHYSICS_FPS + private const val PHYSICS_ENABLE_CLAMP = false + private const val PHYSICS_DEBUG_LOG = false } override val typeId: String @@ -148,6 +153,21 @@ class RenderSceneImpl( instance.physicsData?.let { data -> if (data.lastPhysicsTime < 0) { data.lastPhysicsTime = time + + instance.updateWorldTransformsNoPhysics() + executePhase(instance, UpdatePhase.PhysicsUpdatePre) + data.world.pushTransforms(data.transformArray) + + val initPos = Vector3f() + val initRot = Quaternionf() + for ((nodeIndex, component) in rigidBodyComponents) { + val nodeWorld = instance.modelData.worldTransforms[nodeIndex] + nodeWorld.getTranslation(initPos) + nodeWorld.getUnnormalizedRotation(initRot) + data.world.resetRigidBody(component.rigidBodyIndex, initPos, initRot) + } + data.world.pullTransforms(data.transformArray) + return@let } val timeStep = time - data.lastPhysicsTime @@ -155,14 +175,162 @@ class RenderSceneImpl( return@let } + val maxTimeStep = PHYSICS_MAX_SUB_STEP_COUNT * PHYSICS_TIME_STEP + val clampedTimeStep = minOf(timeStep, maxTimeStep) + data.lastPhysicsTime = time + instance.updateWorldTransformsNoPhysics() executePhase(instance, UpdatePhase.PhysicsUpdatePre) - measureTime { - data.world.step(timeStep, PHYSICS_MAX_SUB_STEP_COUNT, PHYSICS_TIME_STEP) - }.let { - println("Physics step time: $it, timeStep: $timeStep") + data.world.pushTransforms(data.transformArray) + if (PHYSICS_DEBUG_LOG) { + measureTime { + data.world.step(clampedTimeStep, PHYSICS_MAX_SUB_STEP_COUNT, PHYSICS_TIME_STEP) + }.let { + println("Physics step time: $it, timeStep: $clampedTimeStep") + } + } else { + data.world.step(clampedTimeStep, PHYSICS_MAX_SUB_STEP_COUNT, PHYSICS_TIME_STEP) } + data.world.pullTransforms(data.transformArray) + + if (PHYSICS_ENABLE_CLAMP) { + val array = data.transformArray + val basePos = Vector3f() + val baseRot = Quaternionf() + val clampMatrix = Matrix4f() + val maxDistSq = 4.0f + val maxRadiusSq = 100.0f + + for (i in 0 until rigidBodyComponents.size) { + val (nodeIndex, component) = rigidBodyComponents[i] + val offset = component.rigidBodyIndex * 7 + + val px = array[offset + 0] + val py = array[offset + 1] + val pz = array[offset + 2] + + val baseWorld = instance.modelData.worldTransformsNoPhysics[nodeIndex] + baseWorld.getTranslation(basePos) + baseWorld.getUnnormalizedRotation(baseRot) + + val dx = px - basePos.x + val dy = py - basePos.y + val dz = pz - basePos.z + val distSq = dx * dx + dy * dy + dz * dz + val bodyRadiusSq = px * px + py * py + pz * pz + + if (distSq > maxDistSq || bodyRadiusSq > maxRadiusSq) { + array[offset + 0] = basePos.x + array[offset + 1] = basePos.y + array[offset + 2] = basePos.z + array[offset + 3] = baseRot.x + array[offset + 4] = baseRot.y + array[offset + 5] = baseRot.z + array[offset + 6] = baseRot.w + + clampMatrix.translationRotate(basePos, baseRot) + data.world.resetRigidBody(component.rigidBodyIndex, basePos, baseRot) + } + } + } + + if (data.explosionLogCount < 64) { + val array = data.transformArray + val bonePos = Vector3f() + for (i in 0 until rigidBodyComponents.size) { + val (nodeIndex, component) = rigidBodyComponents[i] + val offset = component.rigidBodyIndex * 7 + val px = array[offset + 0] + val py = array[offset + 1] + val pz = array[offset + 2] + + val boneWorld = instance.modelData.worldTransforms[nodeIndex] + boneWorld.getTranslation(bonePos) + + val dx = px - bonePos.x + val dy = py - bonePos.y + val dz = pz - bonePos.z + val distSq = dx * dx + dy * dy + dz * dz + val bodyRadiusSq = px * px + py * py + pz * pz + + if (distSq > 9.0f || bodyRadiusSq > 400.0f) { + val node = nodes[nodeIndex] + val mode = component.rigidBodyData.physicsMode + println( + "PHYSERR RB_EXPLODE " + + "step=${data.debugStepCount} " + + "idx=${component.rigidBodyIndex} " + + "nodeIndex=$nodeIndex " + + "nodeName=${node.nodeName} " + + "mode=$mode " + + "bodyPos=($px,$py,$pz) " + + "bonePos=(${bonePos.x},${bonePos.y},${bonePos.z}) " + + "distSq=$distSq " + + "bodyRadiusSq=$bodyRadiusSq" + ) + data.explosionLogCount++ + if (data.explosionLogCount >= 64) { + break + } + } + } + } + + if (PHYSICS_DEBUG_LOG && data.debugStepCount < 10) { + val maxLogged = minOf(rigidBodyComponents.size, 160) + for (i in 0 until maxLogged) { + val (nodeIndex, component) = rigidBodyComponents[i] + val node = nodes[nodeIndex] + + val boneWorld = instance.modelData.worldTransforms[nodeIndex] + val bonePos = Vector3f() + val boneRot = Quaternionf() + boneWorld.getTranslation(bonePos) + boneWorld.getUnnormalizedRotation(boneRot) + + val offset = component.rigidBodyIndex * 7 + val array = data.transformArray + val px = array[offset + 0] + val py = array[offset + 1] + val pz = array[offset + 2] + val qx = array[offset + 3] + val qy = array[offset + 4] + val qz = array[offset + 5] + val qw = array[offset + 6] + + println( + "PHYSDBG RB_STEP " + + "step=${data.debugStepCount} " + + "idx=${component.rigidBodyIndex} " + + "nodeIndex=$nodeIndex " + + "nodeName=${node.nodeName} " + + "mode=${component.rigidBodyData.physicsMode} " + + "bonePos=(${bonePos.x},${bonePos.y},${bonePos.z}) " + + "boneRot=(${boneRot.x},${boneRot.y},${boneRot.z},${boneRot.w}) " + + "bodyPos=($px,$py,$pz) " + + "bodyRot=($qx,$qy,$qz,$qw)" + ) + + if (node.nodeName != null && node.nodeName.startsWith("Skirt_")) { + val shapePos = component.rigidBodyData.shapePosition + val shapeRot = component.rigidBodyData.shapeRotation + println( + "PHYSDBG RB_SKIRT " + + "step=${data.debugStepCount} " + + "idx=${component.rigidBodyIndex} " + + "nodeIndex=$nodeIndex " + + "nodeName=${node.nodeName} " + + "shapePos=(${shapePos.x()},${shapePos.y()},${shapePos.z()}) " + + "shapeRot=(${shapeRot.x()},${shapeRot.y()},${shapeRot.z()}) " + + "bonePos=(${bonePos.x},${bonePos.y},${bonePos.z}) " + + "bodyPos=($px,$py,$pz)" + ) + } + } + } + data.debugStepCount++ + executePhase(instance, UpdatePhase.PhysicsUpdatePost) executePhase(instance, UpdatePhase.GlobalTransformPropagation) } diff --git a/blazerod/render/main/runtime/load/ModelPreprocessor.kt b/blazerod/render/main/runtime/load/ModelPreprocessor.kt index 37d6dd15..1e3bc916 100644 --- a/blazerod/render/main/runtime/load/ModelPreprocessor.kt +++ b/blazerod/render/main/runtime/load/ModelPreprocessor.kt @@ -4,6 +4,7 @@ import com.mojang.blaze3d.textures.TextureFormat import com.mojang.blaze3d.vertex.VertexFormat import com.mojang.blaze3d.vertex.VertexFormatElement import kotlinx.coroutines.* +import org.joml.Vector3f import top.fifthlight.blazerod.api.resource.RenderExpression import top.fifthlight.blazerod.api.resource.RenderExpressionGroup import top.fifthlight.blazerod.extension.NativeImageExt @@ -632,22 +633,215 @@ class ModelPreprocessor private constructor( return Pair(expressions, expressionGroups) } - private fun loadPhysicalJoints(modelPhysicalJoints: List) = modelPhysicalJoints.mapNotNull { - RenderPhysicsJoint( - name = it.name, - type = it.type, - rigidBodyAIndex = rigidBodyIdToIndexMap[it.rigidBodyA] ?: return@mapNotNull null, - rigidBodyBIndex = rigidBodyIdToIndexMap[it.rigidBodyB] ?: return@mapNotNull null, - position = it.position, - rotation = it.rotation, - positionMin = it.positionMin, - positionMax = it.positionMax, - rotationMin = it.rotationMin, - rotationMax = it.rotationMax, - positionSpring = it.positionSpring, - rotationSpring = it.rotationSpring, - ) - } + private fun loadPhysicalJoints(modelPhysicalJoints: List) = + modelPhysicalJoints.mapIndexedNotNull { index, joint -> + + val name = joint.name + + val rigidBodyAIndex = rigidBodyIdToIndexMap[joint.rigidBodyA] ?: return@mapIndexedNotNull null + val rigidBodyBIndex = rigidBodyIdToIndexMap[joint.rigidBodyB] ?: return@mapIndexedNotNull null + + var position = joint.position + var rotation = joint.rotation + var positionMin = joint.positionMin + var positionMax = joint.positionMax + var rotationMin = joint.rotationMin + var rotationMax = joint.rotationMax + var positionSpring = joint.positionSpring + var rotationSpring = joint.rotationSpring + + val enableNameBasedPhysicsTuning = false + + if (enableNameBasedPhysicsTuning && name != null && (name.startsWith("Skirt_") || name.startsWith("スカート"))) { + val isAsciiSkirt = name.startsWith("Skirt_") + val ringChar = if (isAsciiSkirt) { + name.substringAfter("Skirt_").substringBefore('_').singleOrNull() + } else { + null + } + val segmentIndex = if (isAsciiSkirt) { + val parts = name.substringAfter("Skirt_").split('_') + parts.getOrNull(1)?.toIntOrNull() + } else { + null + } + val isOuterAsciiSkirt = ringChar != null && ringChar in 'B'..'Z' + val isDeepSegment = segmentIndex != null && segmentIndex >= 4 + + if (isAsciiSkirt && ringChar != null && ringChar in 'A'..'D') { + rotation = Vector3f(0f, 0f, 0f) + } + + val angleScale = when (ringChar) { + 'B' -> 5f + in 'C'..'Z' -> 3f + else -> 8f + } + rotationMin = Vector3f(rotationMin).mul(angleScale) + rotationMax = Vector3f(rotationMax).mul(angleScale) + + if (isOuterAsciiSkirt && ringChar != null && (ringChar == 'B' || ringChar == 'C')) { + + val (baseMaxBend, baseMaxYaw) = when (ringChar) { + 'B' -> 1.2f to 0.8f + else -> 1.0f to 0.6f + } + val bendDepthScale = when { + segmentIndex == null || segmentIndex <= 2 -> 1.0f + segmentIndex == 3 -> 0.7f + else -> 0.45f + } + + val yawDepthScale = when { + segmentIndex == null || segmentIndex <= 2 -> 1.0f + segmentIndex == 3 -> 0.6f + else -> 0.4f + } + val maxBend = baseMaxBend * bendDepthScale + val maxYaw = baseMaxYaw * yawDepthScale + rotationMin = Vector3f(-maxBend, -maxYaw, -maxBend) + rotationMax = Vector3f(maxBend, maxYaw, maxBend) + } else if (isOuterAsciiSkirt && ringChar == 'D') { + val baseMaxBend = 0.8f + val baseMaxYaw = 0.5f + val bendDepthScale = when { + segmentIndex == null || segmentIndex <= 2 -> 1.0f + segmentIndex == 3 -> 0.8f + else -> 0.6f + } + val yawDepthScale = when { + segmentIndex == null || segmentIndex <= 2 -> 1.0f + segmentIndex == 3 -> 0.7f + else -> 0.5f + } + val maxBend = baseMaxBend * bendDepthScale + val maxYaw = baseMaxYaw * yawDepthScale + rotationMin = Vector3f(-maxBend, -maxYaw, -maxBend) + rotationMax = Vector3f(maxBend, maxYaw, maxBend) + } else if (isOuterAsciiSkirt) { + val (baseMaxBend, baseMaxYaw) = 1.0f to 0.6f + val (maxBend, maxYaw) = if (isDeepSegment) { + baseMaxBend * 0.6f to baseMaxYaw * 0.3f + } else { + baseMaxBend to baseMaxYaw + } + rotationMin = rotationMin.set( + rotationMin.x().coerceIn(-maxBend, maxBend), + rotationMin.y().coerceIn(-maxYaw, maxYaw), + rotationMin.z().coerceIn(-maxBend, maxBend), + ) + rotationMax = rotationMax.set( + rotationMax.x().coerceIn(-maxBend, maxBend), + rotationMax.y().coerceIn(-maxYaw, maxYaw), + rotationMax.z().coerceIn(-maxBend, maxBend), + ) + } + + if (isAsciiSkirt && ringChar != null && (ringChar == 'B' || ringChar == 'C')) { + val smallYaw = 0.1f + rotationMin = Vector3f(rotationMin.x(), -smallYaw, rotationMin.z()) + rotationMax = Vector3f(rotationMax.x(), smallYaw, rotationMax.z()) + } + + val allowLinearSlack = + isAsciiSkirt && ringChar != null && + (ringChar == 'B' || ringChar == 'C' || ringChar == 'D') && + segmentIndex != null && segmentIndex >= 3 + + if (allowLinearSlack) { + val slack = 0.04f + positionMin = Vector3f(-slack, -slack, -slack) + positionMax = Vector3f(slack, slack, slack) + } else { + positionMin = Vector3f(0f, 0f, 0f) + positionMax = Vector3f(0f, 0f, 0f) + } + + val skirtSpring = 4.0f + val baseSpringScale = when (ringChar) { + 'B' -> 0.75f + 'D' -> 0.75f + in 'C'..'Z' -> 0.5f + else -> 1.0f + } + val springScale = if (isDeepSegment) baseSpringScale * 0.5f else baseSpringScale + val effectiveSpring = skirtSpring * springScale + + if (isAsciiSkirt && ringChar != null && (ringChar == 'B' || ringChar == 'C' || ringChar == 'D')) { + if (ringChar == 'B' || ringChar == 'C') { + positionSpring = Vector3f(0f, 0f, 0f) + } else { + val linearSpring = (skirtSpring * 0.5f) * springScale + if (linearSpring > 0f) { + // Apply linear spring only on the vertical axis so we pull the skirt + // panels toward the correct height without introducing a sideways bias. + positionSpring = Vector3f(0f, linearSpring, 0f) + } + } + } + + rotationSpring = if (isAsciiSkirt && ringChar != null && (ringChar == 'B' || ringChar == 'C')) { + val base = when (ringChar) { + 'B' -> 5.0f + 'C' -> 4.0f + else -> effectiveSpring + } + val depthScaleForSpring = when { + segmentIndex == null || segmentIndex <= 2 -> 1.0f + segmentIndex == 3 -> 0.8f + else -> 0.6f + } + val spring = base * depthScaleForSpring * 0.25f + Vector3f(spring, spring * 0.35f, spring) + } else if (name.startsWith("スカート横_")) { + Vector3f(effectiveSpring, effectiveSpring, effectiveSpring) + } else { + Vector3f( + if (rotationSpring.x() == 0f) effectiveSpring else rotationSpring.x() * springScale, + if (rotationSpring.y() == 0f) effectiveSpring else rotationSpring.y() * springScale, + if (rotationSpring.z() == 0f) effectiveSpring else rotationSpring.z() * springScale, + ) + } + } + + if (enableNameBasedPhysicsTuning && name != null && (name.startsWith("Hair_") || name.startsWith("Braid_") || name.startsWith("Ribbon_"))) { + val hairSpringScale = 0.25f + rotationSpring = Vector3f(rotationSpring).mul(hairSpringScale) + } + + if (index < 128) { + println( + "PHYSDBG JOINT_KT " + + "idx=$index " + + "name=$name " + + "A=$rigidBodyAIndex " + + "B=$rigidBodyBIndex " + + "pos=(${position.x()},${position.y()},${position.z()}) " + + "rot=(${rotation.x()},${rotation.y()},${rotation.z()}) " + + "posMin=(${positionMin.x()},${positionMin.y()},${positionMin.z()}) " + + "posMax=(${positionMax.x()},${positionMax.y()},${positionMax.z()}) " + + "rotMin=(${rotationMin.x()},${rotationMin.y()},${rotationMin.z()}) " + + "rotMax=(${rotationMax.x()},${rotationMax.y()},${rotationMax.z()}) " + + "posSpring=(${positionSpring.x()},${positionSpring.y()},${positionSpring.z()}) " + + "rotSpring=(${rotationSpring.x()},${rotationSpring.y()},${rotationSpring.z()})" + ) + } + + RenderPhysicsJoint( + name = joint.name, + type = joint.type, + rigidBodyAIndex = rigidBodyAIndex, + rigidBodyBIndex = rigidBodyBIndex, + position = position, + rotation = rotation, + positionMin = positionMin, + positionMax = positionMax, + rotationMin = rotationMin, + rotationMax = rotationMax, + positionSpring = positionSpring, + rotationSpring = rotationSpring, + ) + } private fun loadScene(scene: Scene, expressions: List): PreProcessModelLoadInfo { val rootNode = NodeLoadInfo( diff --git a/blazerod/render/main/runtime/node/RenderNodeImpl.kt b/blazerod/render/main/runtime/node/RenderNodeImpl.kt index 1b621ac7..5c97e2ff 100644 --- a/blazerod/render/main/runtime/node/RenderNodeImpl.kt +++ b/blazerod/render/main/runtime/node/RenderNodeImpl.kt @@ -105,6 +105,8 @@ fun ModelInstanceImpl.getTransformMap(node: RenderNodeImpl) = modelData.transfor fun ModelInstanceImpl.getWorldTransform(node: RenderNodeImpl) = modelData.worldTransforms[node.nodeIndex] fun ModelInstanceImpl.getTransformMap(nodeIndex: Int) = modelData.transformMaps[nodeIndex] fun ModelInstanceImpl.getWorldTransform(nodeIndex: Int) = modelData.worldTransforms[nodeIndex] +fun ModelInstanceImpl.getWorldTransformNoPhysics(node: RenderNodeImpl) = modelData.worldTransformsNoPhysics[node.nodeIndex] +fun ModelInstanceImpl.getWorldTransformNoPhysics(nodeIndex: Int) = modelData.worldTransformsNoPhysics[nodeIndex] private fun ModelInstanceImpl.isNodeTransformDirty(node: RenderNodeImpl) = modelData.transformDirty[node.nodeIndex] fun ModelInstanceImpl.markNodeTransformDirty(node: RenderNodeImpl) { if (!modelData.transformDirty[node.nodeIndex]) { diff --git a/blazerod/render/main/runtime/node/component/RigidBodyComponent.kt b/blazerod/render/main/runtime/node/component/RigidBodyComponent.kt index 36026b4c..f94766bf 100644 --- a/blazerod/render/main/runtime/node/component/RigidBodyComponent.kt +++ b/blazerod/render/main/runtime/node/component/RigidBodyComponent.kt @@ -2,13 +2,16 @@ package top.fifthlight.blazerod.runtime.node.component import net.minecraft.util.Colors import org.joml.Matrix4f +import org.joml.Quaternionf import org.joml.Vector3f import top.fifthlight.blazerod.model.RigidBody import top.fifthlight.blazerod.model.TransformId import top.fifthlight.blazerod.runtime.ModelInstanceImpl import top.fifthlight.blazerod.runtime.node.RenderNodeImpl import top.fifthlight.blazerod.runtime.node.UpdatePhase +import top.fifthlight.blazerod.runtime.node.getTransformMap import top.fifthlight.blazerod.runtime.node.getWorldTransform +import top.fifthlight.blazerod.runtime.node.getWorldTransformNoPhysics class RigidBodyComponent( val rigidBodyIndex: Int, @@ -30,6 +33,12 @@ class RigidBodyComponent( private val physicsMatrix = Matrix4f() private val inverseNodeWorldMatrix = Matrix4f() + private val baseWorldMatrix = Matrix4f() + private val parentWorldMatrix = Matrix4f() + + private val tempPos = Vector3f() + private val tempRot = Quaternionf() + override fun update( phase: UpdatePhase, node: RenderNodeImpl, @@ -40,8 +49,19 @@ class RigidBodyComponent( is UpdatePhase.PhysicsUpdatePre -> { when (rigidBodyData.physicsMode) { RigidBody.PhysicsMode.FOLLOW_BONE, RigidBody.PhysicsMode.PHYSICS_PLUS_BONE -> { - val nodeTransformMatrix = instance.getWorldTransform(node) - physicsData.world.setTransform(rigidBodyIndex, nodeTransformMatrix) + val nodeTransformMatrix = instance.getWorldTransformNoPhysics(node) + nodeTransformMatrix.getTranslation(tempPos) + nodeTransformMatrix.getUnnormalizedRotation(tempRot) + + val offset = rigidBodyIndex * 7 + val array = physicsData.transformArray + array[offset + 0] = tempPos.x + array[offset + 1] = tempPos.y + array[offset + 2] = tempPos.z + array[offset + 3] = tempRot.x + array[offset + 4] = tempRot.y + array[offset + 5] = tempRot.z + array[offset + 6] = tempRot.w } RigidBody.PhysicsMode.PHYSICS -> { @@ -53,11 +73,52 @@ class RigidBodyComponent( is UpdatePhase.PhysicsUpdatePost -> { when (rigidBodyData.physicsMode) { RigidBody.PhysicsMode.PHYSICS, RigidBody.PhysicsMode.PHYSICS_PLUS_BONE -> { - val physicsMatrix = physicsData.world.getTransform(rigidBodyIndex, physicsMatrix) - val inverseNodeWorldMatrix = instance.getWorldTransform(node).invert(inverseNodeWorldMatrix) - val deltaTransformMatrix = physicsMatrix.mul(inverseNodeWorldMatrix) + val offset = rigidBodyIndex * 7 + + val array = physicsData.transformArray + + val px = array[offset + 0] + val py = array[offset + 1] + val pz = array[offset + 2] + val qx = array[offset + 3] + val qy = array[offset + 4] + val qz = array[offset + 5] + val qw = array[offset + 6] + physicsMatrix.translationRotate(px, py, pz, qx, qy, qz, qw) + + val localBase = instance.getTransformMap(node).getSum(TransformId.EXTERNAL_PARENT_DEFORM) + baseWorldMatrix.set(localBase) + val parent = node.parent + if (parent != null) { + val parentRigidBody = parent + .getComponentsOfType(RenderNodeComponent.Type.RigidBody) + .firstOrNull() + if (parentRigidBody != null) { + val parentOffset = parentRigidBody.rigidBodyIndex * 7 + parentWorldMatrix.translationRotate( + physicsData.transformArray[parentOffset + 0], + physicsData.transformArray[parentOffset + 1], + physicsData.transformArray[parentOffset + 2], + physicsData.transformArray[parentOffset + 3], + physicsData.transformArray[parentOffset + 4], + physicsData.transformArray[parentOffset + 5], + physicsData.transformArray[parentOffset + 6], + ) + } else { + parentWorldMatrix.set(instance.getWorldTransform(parent)) + } + parentWorldMatrix.mul(baseWorldMatrix, baseWorldMatrix) + } + if (rigidBodyData.physicsMode == RigidBody.PhysicsMode.PHYSICS_PLUS_BONE) { + baseWorldMatrix.getTranslation(tempPos) + physicsMatrix.setTranslation(tempPos) + } + + baseWorldMatrix.invert(inverseNodeWorldMatrix) + inverseNodeWorldMatrix.mul(physicsMatrix) + instance.setTransformMatrix(node.nodeIndex, TransformId.PHYSICS) { - matrix.mul(deltaTransformMatrix) + this.matrix.set(inverseNodeWorldMatrix) } } diff --git a/mod/src/client/java/top/fifthlight/armorstand/mixin/HeldItemFeatureRendererMixin.java b/mod/src/client/java/top/fifthlight/armorstand/mixin/HeldItemFeatureRendererMixin.java new file mode 100644 index 00000000..5c2eb74f --- /dev/null +++ b/mod/src/client/java/top/fifthlight/armorstand/mixin/HeldItemFeatureRendererMixin.java @@ -0,0 +1,45 @@ +package top.fifthlight.armorstand.mixin; + +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.HeldItemFeatureRenderer; +import net.minecraft.client.render.entity.state.ArmedEntityRenderState; +import net.minecraft.client.render.entity.state.PlayerEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import top.fifthlight.armorstand.extension.internal.PlayerEntityRenderStateExtInternal; +import top.fifthlight.armorstand.state.ModelInstanceManager; + +@Mixin(HeldItemFeatureRenderer.class) +public class HeldItemFeatureRendererMixin { + @Inject( + method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/ArmedEntityRenderState;FF)V", + at = @At("HEAD"), + cancellable = true + ) + private void armorstand$cancelVanillaHeldItemRender( + MatrixStack matrices, + VertexConsumerProvider vertexConsumers, + int light, + ArmedEntityRenderState state, + float limbAngle, + float limbDistance, + CallbackInfo ci + ) { + if (!(state instanceof PlayerEntityRenderState)) { + return; + } + + var uuid = ((PlayerEntityRenderStateExtInternal) state).armorstand$getUuid(); + if (uuid == null) { + return; + } + + var entry = ModelInstanceManager.INSTANCE.get(uuid, System.nanoTime(), false); + if (entry instanceof ModelInstanceManager.ModelInstanceItem.Model) { + ci.cancel(); + } + } +} diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/PlayerRenderer.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/PlayerRenderer.kt index 618733c3..91bd374c 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/PlayerRenderer.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/PlayerRenderer.kt @@ -7,21 +7,40 @@ import net.minecraft.client.MinecraftClient import net.minecraft.client.network.AbstractClientPlayerEntity import net.minecraft.client.render.VertexConsumerProvider import net.minecraft.client.render.entity.state.PlayerEntityRenderState +import net.minecraft.client.render.item.ItemRenderState +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemDisplayContext +import net.minecraft.item.ItemStack import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.EntityPose +import net.minecraft.util.Arm +import org.joml.Matrix3f import org.joml.Matrix4f +import org.joml.Quaternionf +import org.joml.Vector3f import top.fifthlight.armorstand.config.ConfigHolder import top.fifthlight.armorstand.state.ModelInstanceManager import top.fifthlight.armorstand.util.RendererManager import top.fifthlight.blazerod.api.render.ScheduledRenderer import top.fifthlight.blazerod.api.resource.CameraTransform +import top.fifthlight.blazerod.api.resource.ModelInstance +import top.fifthlight.blazerod.model.HumanoidTag import top.fifthlight.blazerod.model.Camera import java.lang.ref.WeakReference import java.util.* object PlayerRenderer { private const val NANOSECONDS_PER_SECOND = 1_000_000_000L + private val startNanoTime = System.nanoTime() private var renderingWorld = false + private val handWorldMatrix = Matrix4f() + private val handWorldNoScaleMatrix = Matrix4f() + private val handWorldPos = Vector3f() + private val handWorldRot = Quaternionf() + private val itemLocalMatrix = Matrix4f() + private val itemNormalMatrix = Matrix3f() + private var prevModelItem = WeakReference(null) val selectedCameraIndex = MutableStateFlow(null) private val _totalCameras = MutableStateFlow?>(listOf()) @@ -61,6 +80,89 @@ object PlayerRenderer { private val matrix = Matrix4f() + private fun renderHeldItem( + instance: ModelInstance, + itemState: ItemRenderState, + player: LivingEntity?, + itemStack: ItemStack?, + displayContext: ItemDisplayContext, + tag: HumanoidTag, + matrixStack: MatrixStack, + consumers: VertexConsumerProvider, + light: Int, + overlay: Int, + ) { + if ((itemStack == null || itemStack.isEmpty) && itemState.isEmpty) { + return + } + + val config = ConfigHolder.config.value + val node = instance.scene.humanoidTagMap[tag] ?: return + instance.copyNodeWorldTransform(node.nodeIndex, handWorldMatrix) + handWorldMatrix.getTranslation(handWorldPos) + handWorldMatrix.getUnnormalizedRotation(handWorldRot) + handWorldRot.normalize() + handWorldNoScaleMatrix.translationRotate( + handWorldPos.x, + handWorldPos.y, + handWorldPos.z, + handWorldRot.x, + handWorldRot.y, + handWorldRot.z, + handWorldRot.w, + ) + + itemLocalMatrix.identity() + itemLocalMatrix.scale(config.modelScale) + instance.scene.renderTransform?.applyOnMatrix(itemLocalMatrix) + itemLocalMatrix.mul(handWorldNoScaleMatrix) + + itemLocalMatrix.rotateX(Math.toRadians(config.heldItemRotX.toDouble()).toFloat()) + itemLocalMatrix.rotateY(Math.toRadians(config.heldItemRotY.toDouble()).toFloat()) + itemLocalMatrix.rotateZ(Math.toRadians(config.heldItemRotZ.toDouble()).toFloat()) + val handSign = if (tag == HumanoidTag.LEFT_HAND) -1f else 1f + itemLocalMatrix.translate( + handSign * config.heldItemOffsetX, + config.heldItemOffsetY, + config.heldItemOffsetZ, + ) + itemLocalMatrix.getTranslation(handWorldPos) + itemLocalMatrix.getUnnormalizedRotation(handWorldRot) + handWorldRot.normalize() + itemLocalMatrix.translationRotate( + handWorldPos.x, + handWorldPos.y, + handWorldPos.z, + handWorldRot.x, + handWorldRot.y, + handWorldRot.z, + handWorldRot.w, + ) + + matrixStack.push() + matrixStack.multiplyPositionMatrix(itemLocalMatrix) + matrixStack.scale(config.heldItemScale, config.heldItemScale, config.heldItemScale) + matrixStack.peek().normalMatrix.set( + itemNormalMatrix.set(matrixStack.peek().positionMatrix).invert().transpose() + ) + if (player != null && itemStack != null && !itemStack.isEmpty) { + MinecraftClient.getInstance().itemRenderer.renderItem( + player, + itemStack, + displayContext, + matrixStack, + consumers, + player.world, + light, + overlay, + 0, + ) + } else { + itemState.render(matrixStack, consumers, light, overlay) + } + matrixStack.pop() + } + @JvmStatic fun updatePlayer( player: AbstractClientPlayerEntity, @@ -93,7 +195,13 @@ object PlayerRenderer { val controller = entry.controller val instance = entry.instance - val time = System.nanoTime().toFloat() / NANOSECONDS_PER_SECOND.toFloat() + val player = MinecraftClient.getInstance().world?.getPlayerByUuid(uuid) + val mainHandStack = player?.mainHandStack + val offHandStack = player?.offHandStack + val rightHandStack: ItemStack? = if (vanillaState.mainArm == Arm.RIGHT) mainHandStack else offHandStack + val leftHandStack: ItemStack? = if (vanillaState.mainArm == Arm.RIGHT) offHandStack else mainHandStack + + val time = (System.nanoTime() - startNanoTime).toFloat() / NANOSECONDS_PER_SECOND.toFloat() controller.apply(uuid, instance, vanillaState) instance.updateRenderData(time) @@ -101,6 +209,10 @@ object PlayerRenderer { matrixStack.pop() matrixStack.push() + if (vanillaState.pose == EntityPose.CROUCHING) { + matrixStack.translate(0.0, 0.125, 0.0) + } + if (ArmorStandClient.instance.debugBone) { instance.debugRender(matrixStack.peek().positionMatrix, consumers, time) } else { @@ -122,6 +234,31 @@ object PlayerRenderer { ) task.release() } + + renderHeldItem( + instance = instance, + itemState = vanillaState.rightHandItemState, + player = player, + itemStack = rightHandStack, + displayContext = ItemDisplayContext.THIRD_PERSON_RIGHT_HAND, + tag = HumanoidTag.RIGHT_HAND, + matrixStack = matrixStack, + consumers = consumers, + light = light, + overlay = overlay, + ) + renderHeldItem( + instance = instance, + itemState = vanillaState.leftHandItemState, + player = player, + itemStack = leftHandStack, + displayContext = ItemDisplayContext.THIRD_PERSON_LEFT_HAND, + tag = HumanoidTag.LEFT_HAND, + matrixStack = matrixStack, + consumers = consumers, + light = light, + overlay = overlay, + ) } matrixStack.pop() diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt index c356aa23..bd9a7ca2 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt @@ -16,6 +16,7 @@ import top.fifthlight.blazerod.api.render.RendererTypeHolderFactory import java.nio.file.InvalidPathException import kotlin.io.path.Path import kotlin.io.path.createDirectories +import kotlin.io.path.exists import kotlin.io.path.inputStream import kotlin.io.path.outputStream @@ -27,6 +28,13 @@ data class GlobalConfig( val hidePlayerShadow: Boolean = false, val hidePlayerArmor: Boolean = false, val modelScale: Float = 1f, + val heldItemScale: Float = 0.9f, + val heldItemOffsetX: Float = 0f, + val heldItemOffsetY: Float = 0f, + val heldItemOffsetZ: Float = 0f, + val heldItemRotX: Float = -90f, + val heldItemRotY: Float = 180f, + val heldItemRotZ: Float = 0f, val thirdPersonDistanceScale: Float = 1f, val renderer: RendererKey = RendererKey.VERTEX_SHADER_TRANSFORM, val vmcUdpPort: Int = 9000, @@ -75,11 +83,20 @@ object ConfigHolder { private val _config = MutableStateFlow(GlobalConfig()) val config = _config.asStateFlow() + private val json = Json { + encodeDefaults = true + prettyPrint = true + ignoreUnknownKeys = true + } + @OptIn(ExperimentalSerializationApi::class) fun read() { - runCatching { - _config.value = configFile.inputStream().use { Json.decodeFromStream(it) } + if (configFile.exists()) { + runCatching { + _config.value = configFile.inputStream().use { json.decodeFromStream(it) } + } } + save(_config.value) } fun update(editor: GlobalConfig.() -> GlobalConfig) { @@ -89,6 +106,6 @@ object ConfigHolder { @OptIn(ExperimentalSerializationApi::class) private fun save(config: GlobalConfig) { configFile.parent.createDirectories() - configFile.outputStream().use { Json.encodeToStream(config, it) } + configFile.outputStream().use { json.encodeToStream(config, it) } } } \ No newline at end of file diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt index 3ed62a24..5034cbb8 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt @@ -5,6 +5,8 @@ import net.minecraft.client.render.entity.state.PlayerEntityRenderState import net.minecraft.entity.EntityPose import net.minecraft.entity.EntityType import net.minecraft.registry.tag.EntityTypeTags +import net.minecraft.registry.Registries +import net.minecraft.util.Hand import net.minecraft.util.Arm import net.minecraft.util.math.Direction import net.minecraft.util.math.MathHelper @@ -13,6 +15,8 @@ import top.fifthlight.armorstand.util.toRadian import top.fifthlight.armorstand.vmc.VmcMarionetteManager import top.fifthlight.blazerod.api.animation.AnimationContextsFactory import top.fifthlight.blazerod.api.animation.AnimationItemInstance +import top.fifthlight.blazerod.api.animation.AnimationItemPendingValues +import top.fifthlight.blazerod.api.animation.MaskableAnimationItemInstance import top.fifthlight.blazerod.api.resource.ModelInstance import top.fifthlight.blazerod.api.resource.RenderExpression import top.fifthlight.blazerod.api.resource.RenderExpressionGroup @@ -226,6 +230,12 @@ sealed interface ModelController { abstract fun getItem(set: FullAnimationSet): AnimationItemInstance open val loop: Boolean = true + data class ItemActive( + private val item: AnimationItemInstance, + ) : PlayState() { + override fun getItem(set: FullAnimationSet) = item + } + data object Idle : PlayState() { override fun getItem(set: FullAnimationSet) = set.idle } @@ -309,11 +319,36 @@ sealed interface ModelController { } } + private data class ActionSelection( + val item: AnimationItemInstance, + val arm: Arm, + ) + + private class LayeredPendingValues( + val baseItem: AnimationItemInstance, + val basePendingValues: AnimationItemPendingValues, + val actionItem: AnimationItemInstance?, + val actionPendingValues: AnimationItemPendingValues?, + val actionArm: Arm?, + ) : AnimationItemPendingValues + private var playState: PlayState = PlayState.Idle - override var animationState: AnimationState = playState.getItem(animationSet).createState(context) - private var item: AnimationItemInstance? = null + private var item: AnimationItemInstance = playState.getItem(animationSet) + override var animationState: AnimationState = item.createState(context) + + private var actionItem: AnimationItemInstance? = null + private var actionAnimationState: AnimationState? = null + private var actionArm: Arm? = null + private var lastUsingItem: Boolean = false + private var lastHandSwinging: Boolean = false + private var reset = false + private var upperBodyMaskNodeCount: Int = -1 + private var upperBodyMaskScene: RenderScene? = null + private var leftUpperBodyMask: BooleanArray = BooleanArray(0) + private var rightUpperBodyMask: BooleanArray = BooleanArray(0) + companion object { private val horseEntityTypes = listOf( EntityType.HORSE, @@ -325,11 +360,172 @@ sealed interface ModelController { ) } - private fun getState( + private fun Arm.toHandSide() = when (this) { + Arm.LEFT -> AnimationSet.ItemActiveKey.HandSide.LEFT + Arm.RIGHT -> AnimationSet.ItemActiveKey.HandSide.RIGHT + } + + private fun getItemActiveAnimation( + itemId: net.minecraft.util.Identifier, + arm: Arm, + actionType: AnimationSet.ItemActiveKey.ActionType, + ): AnimationItemInstance? { + val key = AnimationSet.ItemActiveKey( + itemName = itemId, + hand = arm.toHandSide(), + actionType = actionType, + ) + return animationSet.itemActive[key] + } + + private fun getSwingingHand(mainArm: Arm, arm: Arm): Hand = if (arm == mainArm) { + Hand.MAIN_HAND + } else { + Hand.OFF_HAND + } + + private fun getActionSelection( + player: AbstractClientPlayerEntity, + renderState: PlayerEntityRenderState, + ): ActionSelection? { + if (player.isDead) { + return null + } + + if (player.isUsingItem) { + val usedArm = when (player.activeHand) { + Hand.MAIN_HAND -> renderState.mainArm + Hand.OFF_HAND -> if (renderState.mainArm == Arm.RIGHT) Arm.LEFT else Arm.RIGHT + else -> renderState.mainArm + } + val stack = player.getStackInHand(player.activeHand) + val itemId = Registries.ITEM.getId(stack.item) + val itemActive = getItemActiveAnimation( + itemId = itemId, + arm = usedArm, + actionType = AnimationSet.ItemActiveKey.ActionType.USING, + ) ?: return null + return ActionSelection(item = itemActive, arm = usedArm) + } + + if (renderState.handSwinging) { + return when (renderState.preferredArm) { + Arm.LEFT -> { + val stack = player.getStackInHand(getSwingingHand(renderState.mainArm, Arm.LEFT)) + val itemId = Registries.ITEM.getId(stack.item) + val itemActive = getItemActiveAnimation( + itemId = itemId, + arm = Arm.LEFT, + actionType = AnimationSet.ItemActiveKey.ActionType.SWINGING, + ) + ActionSelection(item = itemActive ?: animationSet.swingLeft, arm = Arm.LEFT) + } + + Arm.RIGHT -> { + val stack = player.getStackInHand(getSwingingHand(renderState.mainArm, Arm.RIGHT)) + val itemId = Registries.ITEM.getId(stack.item) + val itemActive = getItemActiveAnimation( + itemId = itemId, + arm = Arm.RIGHT, + actionType = AnimationSet.ItemActiveKey.ActionType.SWINGING, + ) + ActionSelection(item = itemActive ?: animationSet.swingRight, arm = Arm.RIGHT) + } + } + } + + return null + } + + private fun ensureUpperBodyMasks(scene: RenderScene) { + val nodeCount = scene.nodes.size + if (upperBodyMaskScene === scene && upperBodyMaskNodeCount == nodeCount) { + return + } + upperBodyMaskScene = scene + upperBodyMaskNodeCount = nodeCount + + fun BooleanArray.enable(tag: HumanoidTag) { + scene.humanoidTagMap[tag]?.let { node -> + val idx = node.nodeIndex + if (idx in indices) { + this[idx] = true + } + } + } + + fun BooleanArray.enableAll(tags: Array) { + for (tag in tags) { + enable(tag) + } + } + + val torsoTags = arrayOf( + HumanoidTag.SPINE, + HumanoidTag.CHEST, + HumanoidTag.UPPER_CHEST, + HumanoidTag.NECK, + HumanoidTag.HEAD, + ) + + val leftArmTags = arrayOf( + HumanoidTag.LEFT_SHOULDER, + HumanoidTag.LEFT_UPPER_ARM, + HumanoidTag.LEFT_LOWER_ARM, + HumanoidTag.LEFT_HAND, + HumanoidTag.LEFT_THUMB_METACARPAL, + HumanoidTag.LEFT_THUMB_PROXIMAL, + HumanoidTag.LEFT_THUMB_DISTAL, + HumanoidTag.LEFT_INDEX_PROXIMAL, + HumanoidTag.LEFT_INDEX_INTERMEDIATE, + HumanoidTag.LEFT_INDEX_DISTAL, + HumanoidTag.LEFT_MIDDLE_PROXIMAL, + HumanoidTag.LEFT_MIDDLE_INTERMEDIATE, + HumanoidTag.LEFT_MIDDLE_DISTAL, + HumanoidTag.LEFT_RING_PROXIMAL, + HumanoidTag.LEFT_RING_INTERMEDIATE, + HumanoidTag.LEFT_RING_DISTAL, + HumanoidTag.LEFT_LITTLE_PROXIMAL, + HumanoidTag.LEFT_LITTLE_INTERMEDIATE, + HumanoidTag.LEFT_LITTLE_DISTAL, + ) + + val rightArmTags = arrayOf( + HumanoidTag.RIGHT_SHOULDER, + HumanoidTag.RIGHT_UPPER_ARM, + HumanoidTag.RIGHT_LOWER_ARM, + HumanoidTag.RIGHT_HAND, + HumanoidTag.RIGHT_THUMB_METACARPAL, + HumanoidTag.RIGHT_THUMB_PROXIMAL, + HumanoidTag.RIGHT_THUMB_DISTAL, + HumanoidTag.RIGHT_INDEX_PROXIMAL, + HumanoidTag.RIGHT_INDEX_INTERMEDIATE, + HumanoidTag.RIGHT_INDEX_DISTAL, + HumanoidTag.RIGHT_MIDDLE_PROXIMAL, + HumanoidTag.RIGHT_MIDDLE_INTERMEDIATE, + HumanoidTag.RIGHT_MIDDLE_DISTAL, + HumanoidTag.RIGHT_RING_PROXIMAL, + HumanoidTag.RIGHT_RING_INTERMEDIATE, + HumanoidTag.RIGHT_RING_DISTAL, + HumanoidTag.RIGHT_LITTLE_PROXIMAL, + HumanoidTag.RIGHT_LITTLE_INTERMEDIATE, + HumanoidTag.RIGHT_LITTLE_DISTAL, + ) + + leftUpperBodyMask = BooleanArray(nodeCount) + rightUpperBodyMask = BooleanArray(nodeCount) + leftUpperBodyMask.enableAll(torsoTags) + rightUpperBodyMask.enableAll(torsoTags) + leftUpperBodyMask.enableAll(leftArmTags) + rightUpperBodyMask.enableAll(rightArmTags) + } + + private fun getBaseState( player: AbstractClientPlayerEntity, renderState: PlayerEntityRenderState, ): PlayState { val vehicleType = player.vehicle?.type + return when { player.isDead -> PlayState.Dying @@ -363,12 +559,6 @@ sealed interface ModelController { player.isSprinting -> PlayState.Sprinting player.movement.horizontalLength() > .05 -> PlayState.Walking - - renderState.handSwinging -> when (renderState.preferredArm) { - Arm.LEFT -> PlayState.LeftArmSwinging - Arm.RIGHT -> PlayState.RightArmSwinging - } - else -> PlayState.Idle } } @@ -378,29 +568,106 @@ sealed interface ModelController { player: AbstractClientPlayerEntity, renderState: PlayerEntityRenderState, ): Unit = AnimationContextsFactory.create().player(player).let { context -> - val newState = getState(player, renderState) + val newState = getBaseState(player, renderState) if (newState != playState) { - this.playState = newState + playState = newState } val newItem = newState.getItem(animationSet) if (newItem != item) { - animationState = newItem.createState(context) item = newItem + animationState = newItem.createState(context) reset = true } animationState.updateTime(context) - renderState.animationPendingValues = item?.update(context, animationState) + val basePending = item.update(context, animationState) + + val actionSelection = getActionSelection(player, renderState) + if (actionSelection == null) { + actionItem = null + actionAnimationState = null + actionArm = null + lastUsingItem = player.isUsingItem + lastHandSwinging = renderState.handSwinging + renderState.animationPendingValues = LayeredPendingValues( + baseItem = item, + basePendingValues = basePending, + actionItem = null, + actionPendingValues = null, + actionArm = null, + ) + return@let + } + + val needRestartByEdge = (player.isUsingItem && !lastUsingItem) || (renderState.handSwinging && !lastHandSwinging) + + if (needRestartByEdge || actionSelection.item != actionItem || actionSelection.arm != actionArm) { + actionItem = actionSelection.item + actionAnimationState = actionSelection.item.createState(context) + actionArm = actionSelection.arm + } + + val actionItem = actionItem + val actionAnimationState = actionAnimationState + val actionArm = actionArm + if (actionItem == null || actionAnimationState == null || actionArm == null) { + lastUsingItem = player.isUsingItem + lastHandSwinging = renderState.handSwinging + renderState.animationPendingValues = LayeredPendingValues( + baseItem = item, + basePendingValues = basePending, + actionItem = null, + actionPendingValues = null, + actionArm = null, + ) + return@let + } + + actionAnimationState.updateTime(context) + val actionPending = actionItem.update(context, actionAnimationState) + renderState.animationPendingValues = LayeredPendingValues( + baseItem = item, + basePendingValues = basePending, + actionItem = actionItem, + actionPendingValues = actionPending, + actionArm = actionArm, + ) + + lastUsingItem = player.isUsingItem + lastHandSwinging = renderState.handSwinging } override fun apply(uuid: UUID, instance: ModelInstance, renderState: PlayerEntityRenderState) { - val item = item ?: return if (reset) { instance.clearTransform() reset = false } - renderState.animationPendingValues?.let { - item.apply(instance, it) - renderState.animationPendingValues = null + val pending = renderState.animationPendingValues + when (pending) { + is LayeredPendingValues -> { + pending.baseItem.apply(instance, pending.basePendingValues) + if (pending.actionItem != null && pending.actionPendingValues != null && pending.actionArm != null) { + ensureUpperBodyMasks(instance.scene) + val mask = if (pending.actionArm == Arm.LEFT) { + leftUpperBodyMask + } else { + rightUpperBodyMask + } + val maskable = pending.actionItem as? MaskableAnimationItemInstance + if (maskable != null) { + maskable.applyMasked(instance, pending.actionPendingValues, mask) + } else { + pending.actionItem.apply(instance, pending.actionPendingValues) + } + } + renderState.animationPendingValues = null + } + + null -> Unit + + else -> { + item.apply(instance, pending) + renderState.animationPendingValues = null + } } val sleepingDirection = renderState.sleepingDirection diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/ConfigViewModel.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/ConfigViewModel.kt index bd9f1630..aa24f519 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/ConfigViewModel.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/ConfigViewModel.kt @@ -88,6 +88,13 @@ class ConfigViewModel(scope: CoroutineScope) : ViewModel(scope) { hidePlayerShadow = config.hidePlayerShadow, hidePlayerArmor = config.hidePlayerArmor, modelScale = config.modelScale, + heldItemScale = config.heldItemScale, + heldItemOffsetX = config.heldItemOffsetX, + heldItemOffsetY = config.heldItemOffsetY, + heldItemOffsetZ = config.heldItemOffsetZ, + heldItemRotX = config.heldItemRotX, + heldItemRotY = config.heldItemRotY, + heldItemRotZ = config.heldItemRotZ, thirdPersonDistanceScale = config.thirdPersonDistanceScale, ) } @@ -179,6 +186,48 @@ class ConfigViewModel(scope: CoroutineScope) : ViewModel(scope) { } } + fun updateHeldItemScale(heldItemScale: Float) { + ConfigHolder.update { + copy(heldItemScale = heldItemScale) + } + } + + fun updateHeldItemOffsetX(heldItemOffsetX: Float) { + ConfigHolder.update { + copy(heldItemOffsetX = heldItemOffsetX) + } + } + + fun updateHeldItemOffsetY(heldItemOffsetY: Float) { + ConfigHolder.update { + copy(heldItemOffsetY = heldItemOffsetY) + } + } + + fun updateHeldItemOffsetZ(heldItemOffsetZ: Float) { + ConfigHolder.update { + copy(heldItemOffsetZ = heldItemOffsetZ) + } + } + + fun updateHeldItemRotX(heldItemRotX: Float) { + ConfigHolder.update { + copy(heldItemRotX = heldItemRotX) + } + } + + fun updateHeldItemRotY(heldItemRotY: Float) { + ConfigHolder.update { + copy(heldItemRotY = heldItemRotY) + } + } + + fun updateHeldItemRotZ(heldItemRotZ: Float) { + ConfigHolder.update { + copy(heldItemRotZ = heldItemRotZ) + } + } + fun updateThirdPersonDistanceScale(thirdPersonDistanceScale: Float) { ConfigHolder.update { copy(thirdPersonDistanceScale = thirdPersonDistanceScale) diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt index 0fd801fe..914fe7dc 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt @@ -81,6 +81,40 @@ class ConfigScreen(parent: Screen? = null) : ArmorStandScreen Text.translatable("armorstand.config.held_item_scale", text) }, + min = 0.0, + max = 2.0, + value = viewModel.uiState.map { it.heldItemScale.toDouble() }, + onValueChanged = { userTriggered, value -> + viewModel.updateHeldItemScale(value.toFloat()) + }, + ) + } + + private val heldItemOffsetXSlider by lazy { + slider( + textFactory = { slider, text -> Text.translatable("armorstand.config.held_item_offset_x", text) }, + min = -1.0, + max = 1.0, + decimalPlaces = 3, + value = viewModel.uiState.map { it.heldItemOffsetX.toDouble() }, + onValueChanged = { userTriggered, value -> + viewModel.updateHeldItemOffsetX(value.toFloat()) + }, + ) + } + + private val heldItemOffsetYSlider by lazy { + slider( + textFactory = { slider, text -> Text.translatable("armorstand.config.held_item_offset_y", text) }, + min = -1.0, + max = 1.0, + decimalPlaces = 3, + value = viewModel.uiState.map { it.heldItemOffsetY.toDouble() }, + onValueChanged = { userTriggered, value -> + viewModel.updateHeldItemOffsetY(value.toFloat()) + }, + ) + } + + private val heldItemOffsetZSlider by lazy { + slider( + textFactory = { slider, text -> Text.translatable("armorstand.config.held_item_offset_z", text) }, + min = -1.0, + max = 1.0, + decimalPlaces = 3, + value = viewModel.uiState.map { it.heldItemOffsetZ.toDouble() }, + onValueChanged = { userTriggered, value -> + viewModel.updateHeldItemOffsetZ(value.toFloat()) + }, + ) + } + + private val heldItemRotXSlider by lazy { + slider( + textFactory = { slider, text -> Text.translatable("armorstand.config.held_item_rot_x", text) }, + min = -180.0, + max = 180.0, + decimalPlaces = 1, + value = viewModel.uiState.map { it.heldItemRotX.toDouble() }, + onValueChanged = { userTriggered, value -> + viewModel.updateHeldItemRotX(value.toFloat()) + }, + ) + } + + private val heldItemRotYSlider by lazy { + slider( + textFactory = { slider, text -> Text.translatable("armorstand.config.held_item_rot_y", text) }, + min = -180.0, + max = 180.0, + decimalPlaces = 1, + value = viewModel.uiState.map { it.heldItemRotY.toDouble() }, + onValueChanged = { userTriggered, value -> + viewModel.updateHeldItemRotY(value.toFloat()) + }, + ) + } + + private val heldItemRotZSlider by lazy { + slider( + textFactory = { slider, text -> Text.translatable("armorstand.config.held_item_rot_z", text) }, + min = -180.0, + max = 180.0, + decimalPlaces = 1, + value = viewModel.uiState.map { it.heldItemRotZ.toDouble() }, + onValueChanged = { userTriggered, value -> + viewModel.updateHeldItemRotZ(value.toFloat()) + }, + ) + } + private val thirdPersonDistanceScaleSlider = slider( textFactory = { slider, text -> Text.translatable("armorstand.config.third_person_distance_scale", text) }, min = 0.05, @@ -341,7 +465,7 @@ class ConfigScreen(parent: Screen? = null) : ArmorStandScreen