Skip to content
This repository was archived by the owner on Mar 22, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
4b5abee
Fix mesh stretching
Sylsatra Mar 6, 2026
d6cdc05
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
63454ca
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
ba87c6e
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
fe1f187
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
0eccac9
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
fc07340
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
0d3c085
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
2ed2b59
Try to make hair and clothes physics better
Sylsatra Mar 7, 2026
9e73c9d
Try to improve animation and physics
Sylsatra Mar 8, 2026
de8ea4c
Improve Performance
Sylsatra Mar 8, 2026
7348499
Stablize
Sylsatra Mar 8, 2026
586fe76
Stablize
Sylsatra Mar 8, 2026
a619532
Stablize
Sylsatra Mar 8, 2026
e5c26a3
Stablize
Sylsatra Mar 8, 2026
670053c
Fix physics
Sylsatra Mar 9, 2026
686f260
Fix physics
Sylsatra Mar 10, 2026
c7d1279
Fix physics
Sylsatra Mar 10, 2026
b816695
Fix physics
Sylsatra Mar 10, 2026
eae02e0
Fix physics
Sylsatra Mar 10, 2026
a11ec5f
Fix physics
Sylsatra Mar 10, 2026
6328982
Fix physics
Sylsatra Mar 13, 2026
015db08
Fix physics
Sylsatra Mar 13, 2026
ef5f720
Fix physics
Sylsatra Mar 13, 2026
e126abf
Fix physics
Sylsatra Mar 13, 2026
e1b269c
Fix physics
Sylsatra Mar 13, 2026
1ba973a
Fix physics
Sylsatra Mar 13, 2026
0a72faf
Fix physics
Sylsatra Mar 13, 2026
e31d7d1
Fix physics
Sylsatra Mar 13, 2026
09e248d
Physics
Sylsatra Mar 14, 2026
4d41e3b
Physics
Sylsatra Mar 14, 2026
ebdbbe0
Physics
Sylsatra Mar 14, 2026
9106d92
Physics
Sylsatra Mar 14, 2026
2d7dba2
Revert "Physics"
Sylsatra Mar 27, 2026
2044f2b
Fix Koikatsu's ghost physics
Sylsatra Mar 27, 2026
e3a40bf
Follow Bone
Sylsatra Mar 27, 2026
2f90651
Follow Bone
Sylsatra Mar 27, 2026
7eb3503
Fix build
Sylsatra Mar 28, 2026
7e424ae
Fix anchoring
Sylsatra Mar 28, 2026
fa82084
Fix some keys in scene
Sylsatra Mar 28, 2026
dbe07de
Add some missing keys
Sylsatra Mar 28, 2026
7b60aba
Add some missing root
Sylsatra Mar 28, 2026
a636c5d
Add some missing fix
Sylsatra Mar 28, 2026
0a8ebd4
Add some missing root
Sylsatra Mar 28, 2026
71a463f
Remove some bone
Sylsatra Mar 28, 2026
5b88d4b
Resort
Sylsatra Mar 28, 2026
b58a7a6
fix
Sylsatra Mar 28, 2026
9051227
fix
Sylsatra Mar 28, 2026
3ee59d5
fix
Sylsatra Mar 28, 2026
33ad051
fix
Sylsatra Mar 28, 2026
812bc6c
fix
Sylsatra Mar 28, 2026
eb8d6b3
fix
Sylsatra Mar 28, 2026
6b5bc98
fix
Sylsatra Mar 28, 2026
122e108
fix
Sylsatra Mar 29, 2026
045f0b7
debug
Sylsatra Mar 29, 2026
c5cba30
fix
Sylsatra Mar 29, 2026
86f68f2
fix
Sylsatra Mar 29, 2026
ebf73af
fix
Sylsatra Mar 29, 2026
85a56b4
fix
Sylsatra Mar 29, 2026
82a8e0c
fix
Sylsatra Mar 29, 2026
e64424f
fix
Sylsatra Mar 30, 2026
05a8249
fix
Sylsatra Mar 30, 2026
cf7f763
fix
Sylsatra Mar 30, 2026
e63f9c7
fix
Sylsatra Mar 30, 2026
637c6ce
remove logs
Sylsatra Mar 30, 2026
0be9018
upload files
Sylsatra Jun 16, 2026
7cd16ba
fix: allow Koikatsu skirt physics
Sylsatra Jun 16, 2026
8585d66
fix: reset model physics on reuse
Sylsatra Jun 17, 2026
e045b83
fix: declare runtime minecraft client deps
Sylsatra Jun 17, 2026
2a2c91d
fix: isolate PMX bake from runtime physics
Sylsatra Jun 17, 2026
c3b69eb
fix: contain PMX physics NaNs
Sylsatra Jun 17, 2026
f523caa
chore: remove PMX physics diagnostic logs
Sylsatra Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions blazerod/model/model-base/PhysicalJoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ data class PhysicalJoint(
val rotationMax: Vector3fc,
val positionSpring: Vector3fc,
val rotationSpring: Vector3fc,
val softness: Float = 1.0f,
val biasFactor: Float = 0.3f,
val relaxationFactor: Float = 1.0f,
) {
enum class JointType {
SPRING_6DOF,
Expand Down
3 changes: 3 additions & 0 deletions blazerod/model/model-base/RigidBody.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ data class RigidBody(
val repulsion: Float,
val frictionForce: Float,
val physicsMode: PhysicsMode,
val ccdMotionThreshold: Float = 0f,
val ccdSweptSphereRadius: Float = 0f,
val collisionMargin: Float = 0.04f,
) {
enum class ShapeType {
SPHERE,
Expand Down
498 changes: 449 additions & 49 deletions blazerod/model/model-pmx/PmxLoader.kt

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions blazerod/render/api/physics/PhysicsEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,32 @@ import top.fifthlight.blazerod.api.resource.ModelInstance

object PhysicsEngine {
private val activeWorlds = mutableMapOf<ModelInstance, PhysicsWorld>()
private val providers = mutableMapOf<ModelInstance, PhysicsProvider>()

fun register(instance: ModelInstance, provider: PhysicsProvider) {
providers[instance] = provider
if (!activeWorlds.containsKey(instance)) {
activeWorlds[instance] = provider.createWorld(instance)
}
}

fun unregister(instance: ModelInstance) {
providers.remove(instance)
activeWorlds.remove(instance)?.dispose()
}

fun getWorld(instance: ModelInstance): PhysicsWorld? {
return activeWorlds[instance]
}

fun recreate(instance: ModelInstance): PhysicsWorld? {
val provider = providers[instance] ?: return null
activeWorlds.remove(instance)?.dispose()
val world = provider.createWorld(instance)
activeWorlds[instance] = world
return world
}

fun update(time: Float) {
val iterator = activeWorlds.iterator()
while (iterator.hasNext()) {
Expand Down
4 changes: 3 additions & 1 deletion blazerod/render/api/resource/ModelInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ModelInstance : RefCount {
var lodDistance: Float

fun clearTransform()
fun resetPhysics(time: Float)
fun setTransformMatrix(nodeIndex: Int, transformId: TransformId, matrix: Matrix4f)
fun setTransformMatrix(nodeIndex: Int, transformId: TransformId, updater: Consumer<NodeTransform.Matrix>)
fun setTransformMatrix(nodeIndex: Int, transformId: TransformId, updater: NodeTransform.Matrix.() -> Unit)
Expand All @@ -31,6 +32,7 @@ interface ModelInstance : RefCount {

fun debugRender(viewProjectionMatrix: Matrix4fc, bufferSource: MultiBufferSource, time: Float)
fun updateRenderData(time: Float)
fun updateRenderDataNoPhysics(time: Float)

fun createRenderTask(
modelMatrix: Matrix4fc,
Expand All @@ -42,4 +44,4 @@ interface ModelInstance : RefCount {
interface Factory {
fun of(scene: RenderScene): ModelInstance
}
}
}
42 changes: 24 additions & 18 deletions blazerod/render/main/animation/CrossfadeAnimationItemInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import kotlin.math.min
class CrossfadeAnimationItemPendingValues(
val sourcePending: AnimationItemPendingValues,
val targetPending: AnimationItemPendingValues,
var weight: Float
var weight: Float,
var rootWeight: Float
) : AnimationItemPendingValues

class CrossfadeAnimationState(
Expand Down Expand Up @@ -66,8 +67,9 @@ class CrossfadeAnimationItemInstance(
val targetPending = targetInstance.update(context, crossfadeState.targetState)

val weight = min(1f, max(0f, crossfadeState.elapsedTime / crossfadeState.duration))
val rootWeight = min(1f, max(0f, crossfadeState.elapsedTime / 0.05f))

return CrossfadeAnimationItemPendingValues(sourcePending, targetPending, weight)
return CrossfadeAnimationItemPendingValues(sourcePending, targetPending, weight, rootWeight)
}

@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
Expand All @@ -81,8 +83,8 @@ class CrossfadeAnimationItemInstance(
val modelImpl = instance as ModelInstanceImpl

val weight = crossfadeValues.weight
val rootWeight = crossfadeValues.rootWeight

// If the crossfade is complete, just apply the target animation directly
if (weight >= 1f) {
targetInstance.apply(modelImpl, crossfadeValues.targetPending)
return
Expand All @@ -91,7 +93,6 @@ class CrossfadeAnimationItemInstance(
val sourcePendingImpl = crossfadeValues.sourcePending as AnimationItemPendingValuesImpl
val targetPendingImpl = crossfadeValues.targetPending as AnimationItemPendingValuesImpl

// Fast path: if weight is 0, just apply source
if (weight <= 0f) {
sourceInstance.apply(modelImpl, crossfadeValues.sourcePending)
return
Expand Down Expand Up @@ -121,45 +122,46 @@ class CrossfadeAnimationItemInstance(

appliedNodes.add(key)

// Check if this is a Root node (no parent)
val isRoot = modelImpl.scene.nodes[nodeIdx].parent == null
val currentWeight = if (isRoot && tChannel is AnimationChannelItem.TranslationItem) rootWeight else weight

when (tChannel) {
is AnimationChannelItem.TranslationItem -> {
val tVec = tVal as Vector3f
if (sVal != null) {
val sVec = sVal as Vector3f
tVec.lerp(sVec, 1f - weight) // if weight=1, tVec unchanged. if weight=0, tVec=sVec.
tVec.lerp(sVec, 1f - currentWeight)
} else {
// interpolate from identity (0,0,0)
val sVec = Vector3f(0f, 0f, 0f)
tVec.lerp(sVec, 1f - weight)
tVec.lerp(sVec, 1f - currentWeight)
}
tChannel.applyUnsafe(modelImpl, tVec)
}
is AnimationChannelItem.RotationItem -> {
val tQuat = tVal as Quaternionf
if (sVal != null) {
val sQuat = sVal as Quaternionf
tQuat.slerp(sQuat, 1f - weight)
tQuat.slerp(sQuat, 1f - currentWeight)
} else {
// interpolate from identity
val sQuat = Quaternionf()
tQuat.slerp(sQuat, 1f - weight)
tQuat.slerp(sQuat, 1f - currentWeight)
}
tChannel.applyUnsafe(modelImpl, tQuat)
}
is AnimationChannelItem.ScaleItem -> {
val tVec = tVal as Vector3f
if (sVal != null) {
val sVec = sVal as Vector3f
tVec.lerp(sVec, 1f - weight)
tVec.lerp(sVec, 1f - currentWeight)
} else {
// interpolate from identity (1,1,1)
val sVec = Vector3f(1f, 1f, 1f)
tVec.lerp(sVec, 1f - weight)
tVec.lerp(sVec, 1f - currentWeight)
}
tChannel.applyUnsafe(modelImpl, tVec)
}
else -> {
if (weight >= 0.5f) {
if (currentWeight >= 0.5f) {
tChannel.applyUnsafe(modelImpl, tVal)
} else if (sVal != null) {
for (j in sourceChannels.indices) {
Expand All @@ -182,27 +184,31 @@ class CrossfadeAnimationItemInstance(

if (!appliedNodes.contains(key)) {
val sVal = sourcePendingImpl.pendingValues[i]

val isRoot = modelImpl.scene.nodes[nodeIdx].parent == null
val currentWeight = if (isRoot && sChannel is AnimationChannelItem.TranslationItem) rootWeight else weight

when (sChannel) {
is AnimationChannelItem.TranslationItem -> {
val sVec = sVal as Vector3f
val tVec = Vector3f(0f, 0f, 0f)
sVec.lerp(tVec, weight) // if weight=1, sVec=(0,0,0)
sVec.lerp(tVec, currentWeight)
sChannel.applyUnsafe(modelImpl, sVec)
}
is AnimationChannelItem.RotationItem -> {
val sQuat = sVal as Quaternionf
val tQuat = Quaternionf()
sQuat.slerp(tQuat, weight)
sQuat.slerp(tQuat, currentWeight)
sChannel.applyUnsafe(modelImpl, sQuat)
}
is AnimationChannelItem.ScaleItem -> {
val sVec = sVal as Vector3f
val tVec = Vector3f(1f, 1f, 1f)
sVec.lerp(tVec, weight)
sVec.lerp(tVec, currentWeight)
sChannel.applyUnsafe(modelImpl, sVec)
}
else -> {
if (weight < 0.5f) {
if (currentWeight < 0.5f) {
sChannel.applyUnsafe(modelImpl, sVal)
}
}
Expand Down
4 changes: 2 additions & 2 deletions blazerod/render/main/physics/PhysicsLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public native static void resetRigidBody(long physicsWorld, int rigidBodyIndex,
public native static void applyVelocityDamping(long physicsWorld, int rigidBodyIndex,
float linearAttenuation, float angularAttenuation);

public native static void setSolverIterations(long physicsWorld, int iterations);

public native static void destroyPhysicsWorld(long physicsWorld);

public static boolean isPhysicsAvailable() {
Expand Down Expand Up @@ -113,8 +115,6 @@ public static boolean load() {
return true;
} catch (Exception | LinkageError ex) {
logger.error("Failed to load bullet physics native library", ex);
System.err.println("CRITICAL JNI LOAD ERROR:");
ex.printStackTrace(System.err);
return false;
}
}
Expand Down
10 changes: 8 additions & 2 deletions blazerod/render/main/physics/PhysicsScene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ static void CopyVector3fField(Vector3f& vector, void* data) {
}

static std::vector<RigidBody> DeserializeRigidbodies(size_t size, void* data) {
const size_t rigidbody_size = 72;
const size_t rigidbody_size = 84;
if (size == 0) {
throw std::invalid_argument("Empty rigidbody data");
}
Expand Down Expand Up @@ -56,6 +56,9 @@ static std::vector<RigidBody> DeserializeRigidbodies(size_t size, void* data) {
CopyField(rigidbody.rotation_damping, rigidbody_data + 60);
CopyField(rigidbody.repulsion, rigidbody_data + 64);
CopyField(rigidbody.friction_force, rigidbody_data + 68);
CopyField(rigidbody.ccd_motion_threshold, rigidbody_data + 72);
CopyField(rigidbody.ccd_swept_sphere_radius, rigidbody_data + 76);
CopyField(rigidbody.collision_margin, rigidbody_data + 80);

rigidbodies.push_back(rigidbody);
}
Expand All @@ -64,7 +67,7 @@ static std::vector<RigidBody> DeserializeRigidbodies(size_t size, void* data) {
}

static std::vector<Joint> DeserializeJoints(size_t size, void* data) {
const size_t joint_size = 108;
const size_t joint_size = 120;
if (size % joint_size != 0) {
throw std::invalid_argument("Invalid joint size");
}
Expand All @@ -88,6 +91,9 @@ static std::vector<Joint> DeserializeJoints(size_t size, void* data) {
CopyVector3fField(joint.rotation_max, joint_data + 72);
CopyVector3fField(joint.position_spring, joint_data + 84);
CopyVector3fField(joint.rotation_spring, joint_data + 96);
CopyField(joint.softness, joint_data + 108);
CopyField(joint.bias_factor, joint_data + 112);
CopyField(joint.relaxation_factor, joint_data + 116);

joints.push_back(joint);
}
Expand Down
6 changes: 6 additions & 0 deletions blazerod/render/main/physics/PhysicsScene.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ struct RigidBody {
float repulsion;
float friction_force;
PhysicsMode physics_mode;
float ccd_motion_threshold;
float ccd_swept_sphere_radius;
float collision_margin;
};

struct Joint {
Expand All @@ -56,6 +59,9 @@ struct Joint {
Vector3f rotation_max;
Vector3f position_spring;
Vector3f rotation_spring;
float softness;
float bias_factor;
float relaxation_factor;
};

class PhysicsScene {
Expand Down
10 changes: 8 additions & 2 deletions blazerod/render/main/physics/PhysicsScene.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PhysicsScene(
.putFloat(offset + 4, vector.y())
.putFloat(offset + 8, vector.z())

val rigidBodyItemSize = 72
val rigidBodyItemSize = 84
val rigidBodiesBuffer = ByteBuffer.allocateDirect(rigidBodyItemSize * rigidBodies.size)
.order(ByteOrder.nativeOrder())
rigidBodies.forEachIndexed { index, rigidBody ->
Expand All @@ -45,9 +45,12 @@ class PhysicsScene(
rigidBodiesBuffer.putFloat(offset + 60, rigidBody.rotationDamping)
rigidBodiesBuffer.putFloat(offset + 64, rigidBody.repulsion)
rigidBodiesBuffer.putFloat(offset + 68, rigidBody.frictionForce)
rigidBodiesBuffer.putFloat(offset + 72, rigidBody.ccdMotionThreshold)
rigidBodiesBuffer.putFloat(offset + 76, rigidBody.ccdSweptSphereRadius)
rigidBodiesBuffer.putFloat(offset + 80, rigidBody.collisionMargin)
}

val jointItemSize = 108
val jointItemSize = 120
val jointsBuffer = ByteBuffer.allocateDirect(jointItemSize * joints.size)
.order(ByteOrder.nativeOrder())
joints.forEachIndexed { index, joint ->
Expand All @@ -63,6 +66,9 @@ class PhysicsScene(
jointsBuffer.putVector3f(offset + 72, joint.rotationMax)
jointsBuffer.putVector3f(offset + 84, joint.positionSpring)
jointsBuffer.putVector3f(offset + 96, joint.rotationSpring)
jointsBuffer.putFloat(offset + 108, joint.softness)
jointsBuffer.putFloat(offset + 112, joint.biasFactor)
jointsBuffer.putFloat(offset + 116, joint.relaxationFactor)
}

try {
Expand Down
Loading