[SOLVED] When rotating bones, rays behave as if the mesh hasn't rotated

Hello!

I’m having some problems with rays when I rotate bones (in what I believe is termed a custom animation), and am wondering if anyone can help me.

I put a bone (called head_target) in the middle of each character’s head, and made it so that NPCs use a ray from their eyes to the head_target bone to determine whether that character is in their line of sight. The ray either collides with the character’s head mesh first (which means there’s a clear line of sight), or collides with something else first (which means there isn’t a clear line of sight).

I then added some code to rotate the chest bone of characters when they aim their weapons. Rotating the chest bone causes all the bones above it, including the head_target bone, to move. I did this using setUserControl(true), and then setUserTransforms(Vector3f.ZERO, rotation, Vector3f.UNIT_XYZ) on the chest bone. Visually, this works fine.

However, I then found that when a character (PC or NPC) is aiming his weapon, the rays going to that character often don’t collide with anything at all. Some debugging reveals that the rays are behaving (colliding or not colliding) as though the mesh was where it was before the bone was rotated. The head_target bone is in the correct place, and the rays are going to that bone, but they don’t actually register any collisions. I’ve been trying to figure out what I’m doing wrong, but nothing I’ve tried has worked.

I’ve reproduced the problem by adding a few lines of code to the TestCustomAnim example class (which is what I used to understand how to rotate bones in the first place).

Rather than print out the whole class, I’ll just give the code that I add to the simpleUpdate method:

Ray ray = new Ray(new Vector3f(-10, 0.9f, 0.9f), new Vector3f(1, 0, 0));
CollisionResults results = new CollisionResults();
rootNode.collideWith(ray, results);
System.out.println(results.size() == 0 ? "No collisions" : results.getClosestCollision().getContactPoint());

I’m casting a ray at one corner of the box. As it rotates, that ray should alternately collide and not collide with the box. However, in practise, the ray always collides with the box.

I’ve run this on 3.0 and 3.1, and the same thing happens in each.

Thanks!

1 Like

If you want to check collision against animated meshes you have to turn off hardware skinning.
SkeletonControl.setHardwareSkinningPreferred(false) (or something like this I never remember the name of the method)

This will actually modify the position buffer on the CPU instead of on the GPU and you’ll be able to ray cast on the animated version.
Note that hardware skinning is more performant that software skinning.

That said… if at some point you want to have rag dolls for your npc, you may create a rigid body in the physics space for each bone… In that case I really advise to turn back hardware skinning on and to do the ray cast against the rigid bodies instead of the actual mesh.

2 Likes

isHardwareSkinningUsed() already returns false, so it looks like I’m already using software skinning, probably because I have a really low-end graphics card (GeForce GT 710). So, in my TestCustomAnim example, calling setHardwareSkinningPreferred(false) makes no difference - the ray still collides every time.

1 Like

mhhh I guess collision data is not updated when the mesh is transformed, I guess it’s for performence reason…
Well then that lets you with plan B, and create rigid bodies for each bone

1 Like

Or do what I did: use low poly hitboxes at the Attachment Nodes.

You could even just Add a box to the desired Bone and then just raycast

2 Likes

I’ve been meaning to ask this question since I’ve run into this problem and I’m not quite sure if my solution was a good one or not but it kind of works -

I made an array of bounding spheres and boxes for each model im trying to cast rays or collide with and a parallel array for their local translation, and depending on the current animation being run, I add or removes bounding volumes at their local translation so that it matches the animation at least a little better. I’m still unsure if using and updating a list of bounding volumes like this will eventually cause performance issues, I’m still learning as I go but I figured Id share my temporary solution in hopes I can find a better one as well

I’ve run into the same issue in my Maud project, where I need a solution that doesn’t involve altering the model. I’ll let you know what I come up with.

I’ve found two issues with collision detection on animated models. The first issue is that meshes cache collision data. This is easily worked around by invoking Mesh.clearCollisionData() on each mesh prior to invoking Spatial.collideWith(). The second issue is caching of model bounds. This is harder to work around because Spatial.setBoundRefresh() is a protected method. In Maud, I’ve replaced JME’s Spatial.java with a patched version that makes setBoundRefresh() a public method.

Edit: To get the correct spatial bounds, it’s also necessary to update the mesh’s bounds using Mesh.updateBound().

Here’s a utility method to clear all collision data and force a bound refresh on a specified subtree of the scene graph:

    public static void prepareForCollide(Spatial subtree) {
        if (subtree instanceof Geometry) {
            Geometry geometry = (Geometry) subtree;
            Mesh mesh = geometry.getMesh();
            mesh.clearCollisionData();
            mesh.updateBound();
            geometry.setBoundRefresh();

        } else if (subtree instanceof Node) {
            Node node = (Node) subtree;
            List<Spatial> children = node.getChildren();
            for (Spatial child : children) {
                prepareForCollide(child);
            }
        }
    }
1 Like

Geometry.updateModelBound() is what you are looking for.

Something like:

mySpatial.depthFirstTraversal(new SceneGraphVisitorAdapter() {
        public void visit( Geometry geom ) {
            geom.getMesh().clearCollisionData();
            geom.updateModelBound();
        }
    });

Edit: that will do what your method is doing only with less code.

1 Like

Thank you all for your taking the time to look into this. I’m already calling updateModelBound, but I didn’t realize that collision data is cached, and it would have taken me ages to figure that out, if ever.

It looks like (in 3.0 at least) calling Mesh.createCollisionData() solves my problem. 3.0 doesn’t have a clearCollisionData method, but calling createCollisionData achieves the same end. Also, if I call createCollisionData in my edited version of TestCustomAnim, the ray now alternately succeeds and fails to collide with the box as it rotates. So, I’m now adding the following lines to simpleUpdate:

geom.getMesh().createCollisionData();
Ray ray = new Ray(new Vector3f(-10, 0.9f, 0.9f), new Vector3f(1, 0, 0));
CollisionResults results = new CollisionResults();
rootNode.collideWith(ray, results);
System.out.println(results.size() == 0 ? "No collisions" : results.getClosestCollision().getContactPoint());

However, this only works in 3.0. In 3.1, it still doesn’t work, whether I call clearCollisionData or createCollisionData. That’s sort of OK for me for now, because I haven’t upgraded to 3.1 yet, but I would like to upgrade at some point.

1 Like

maybe we could just have a flag on the SkeletonControl to force bound and collision data recalculation on each frame, or a method to do it properly on demand…

Your sample code doesn’t show updating the geometry’s model bound. So collisions may fail because the bounding box is checked first.

Well, the code I posted should do it on demand. I would hesitate to include it by default because it’s such a bad practice for anything but a visual editor.

Yeah I wouldn’t do it by default. But yeah anyway you’re right.

That’s not required for this code to work in 3.0. If I add a call to geom.updateModelBound(), it still doesn’t work in 3.1.

geom.getMesh().clearCollisionData();
geom.updateModelBound();
Ray ray = new Ray(new Vector3f(-10, 0.9f, 0.9f), new Vector3f(1, 0, 0));
CollisionResults results = new CollisionResults();
rootNode.collideWith(ray, results);
System.out.println(results.size() == 0 ? "No collisions" : results.getClosestCollision().getContactPoint());

If you share a complete file, I’ll take a closer look at it.

That would be great if you could, thank you. As I say, it’s working in 3.0 now, but if I can get it working in 3.1, then I’ll be able to upgrade at some point.

The code I’m using is TestCustomAnim with some extra lines inserted into the simpleUpdate method. The whole thing is shown below. What’s supposed to happen is that as the box rotates, the ray alternates between colliding and not colliding. In 3.0, this works perfectly, but in 3.1, the ray continually collides with the box.

import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SkeletonControl;
import com.jme3.app.SimpleApplication;
import com.jme3.collision.CollisionResults;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.shape.Box;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;

public class TestCustomAnim extends SimpleApplication {

    private Bone bone;
    private Skeleton skeleton;
    private Quaternion rotation = new Quaternion();
    private Geometry geom;

    public static void main(String[] args) {
        TestCustomAnim app = new TestCustomAnim();
        app.start();
    }

    @Override
    public void simpleInitApp() {

        AmbientLight al = new AmbientLight();
        rootNode.addLight(al);

        DirectionalLight dl = new DirectionalLight();
        dl.setDirection(Vector3f.UNIT_XYZ.negate());
        rootNode.addLight(dl);

        Box box = new Box(1, 1, 1);

        VertexBuffer weightsHW = new VertexBuffer(Type.HWBoneWeight);
        VertexBuffer indicesHW = new VertexBuffer(Type.HWBoneIndex);
        indicesHW.setUsage(Usage.CpuOnly);
        weightsHW.setUsage(Usage.CpuOnly);
        box.setBuffer(weightsHW);
        box.setBuffer(indicesHW);
        
        // Setup bone weight buffer
        FloatBuffer weights = FloatBuffer.allocate( box.getVertexCount() * 4 );
        VertexBuffer weightsBuf = new VertexBuffer(Type.BoneWeight);
        weightsBuf.setupData(Usage.CpuOnly, 4, Format.Float, weights);
        box.setBuffer(weightsBuf);

        // Setup bone index buffer
        ByteBuffer indices = ByteBuffer.allocate( box.getVertexCount() * 4 );
        VertexBuffer indicesBuf = new VertexBuffer(Type.BoneIndex);
        indicesBuf.setupData(Usage.CpuOnly, 4, Format.UnsignedByte, indices);
        box.setBuffer(indicesBuf);

        // Create bind pose buffers
        box.generateBindPose(true);

        // Create skeleton
        bone = new Bone("root");
        bone.setBindTransforms(Vector3f.ZERO, Quaternion.IDENTITY, Vector3f.UNIT_XYZ);
        bone.setUserControl(true);
        skeleton = new Skeleton(new Bone[]{ bone });

        // Assign all verticies to bone 0 with weight 1
        for (int i = 0; i < box.getVertexCount() * 4; i += 4){
            // assign vertex to bone index 0
            indices.array()[i+0] = 0;
            indices.array()[i+1] = 0;
            indices.array()[i+2] = 0;
            indices.array()[i+3] = 0;

            // set weight to 1 only for first entry
            weights.array()[i+0] = 1;
            weights.array()[i+1] = 0;
            weights.array()[i+2] = 0;
            weights.array()[i+3] = 0;
        }

        // Maximum number of weights per bone is 1
        box.setMaxNumWeights(1);

        // Create model
        geom = new Geometry("box", box);
        geom.setMaterial(assetManager.loadMaterial("Textures/Terrain/BrickWall/BrickWall.j3m"));
        Node model = new Node("model");
        model.attachChild(geom);

        // Create skeleton control
        SkeletonControl skeletonControl = new SkeletonControl(skeleton);
        model.addControl(skeletonControl);

        rootNode.attachChild(model);
    }

    @Override
    public void simpleUpdate(float tpf){
        // Rotate around X axis
        Quaternion rotate = new Quaternion();
        rotate.fromAngleAxis(tpf, Vector3f.UNIT_X);

        // Combine rotation with previous
        rotation.multLocal(rotate);

        // Set new rotation into bone
        bone.setUserTransforms(Vector3f.ZERO, rotation, Vector3f.UNIT_XYZ);

        // After changing skeleton transforms, must update world data
        skeleton.updateWorldVectors();
        
        geom.getMesh().clearCollisionData();
        geom.updateModelBound();
        Ray ray = new Ray(new Vector3f(-10, 0.9f, 0.9f), new Vector3f(1, 0, 0));
        CollisionResults results = new CollisionResults();
        rootNode.collideWith(ray, results);
        System.out.println(results.size() == 0 ? "No collisions" : results.getClosestCollision().getContactPoint());
    }

}
1 Like

Your code worked fine after I turned off hardware skinning like so:

        SkeletonControl skeletonControl = new SkeletonControl(skeleton);
        skeletonControl.setHardwareSkinningPreferred(false); // added code
        model.addControl(skeletonControl);

In 3.0, software skinning was the default, but not in 3.1. That is one of the known gotchas in the 3.0->3.1 transition. For other porting tips, see the forum topic:

1 Like

Ah, OK, that explains a few things then. And yes, turning off hardware skinning makes it work for me too in 3.1. Thank you! :smile:

1 Like

Unfortunately, I’ve discovered a new problem with this. If the camera isn’t looking at the mesh, then the rays continue to behave as if the mesh is in its original position. I can recreate this with my TestCustomAnim example by calling cam.setRotation(new Quaternion().fromAxes(Vector3f.ZERO, Vector3f.ZERO, Vector3f.UNIT_Z)) to point the camera away from the box. Now the rays continually collide with the mesh. Only if you turn the camera around to look at the box do they start to alternately collide and not collide.

The reason is obvious, of course. Animations don’t happen if the camera isn’t looking at the bounding volume. So, unless anyone has any better ideas, I think I’ll stop casting rays at animated meshes altogether, and try some of the alternative approaches people have mentioned.

1 Like