Switching between attachmentNodes?

Hello monkeys :smiley:

Don’t know if this is really the right category or not for something like this… but it’s kind of a design related topic. Basically, you have a character who uses a sword as his choice of weapon to go beat things up. Swords are cool, right?

Anyway, when not hacking at monsters or the occasional passing shrub, that sword is going to be sheathed and looking cool as it bounces over one shoulder. What I want to do, is to draw and sheathe a sword. This means there will be two positions that it can be at, following one of two animation paths, out and sheathed.

I don’t know if this is the best idea, but I thought to use two attachment nodes. Well, cool I can attach it in the scene editor, but only to one node. Is there a way to transition between attachment nodes? Is this even the right approach?

here’s a derp picture explaining what I’m trying to accomplish

That’s definitely the right approach. I already used it in a game.
The idea is to make your sheath and unsheath animation, and know at what moment the hand is grabbing or ungrabbing the handle of the sword.
You can use a control on the model to switch the sword attachement node at this precise moment.

Oooh, so you can @Nehon. Thank you sir, I shall look into this :]

Nice sketch. I would never draw something like it, even if I try to do a final art :sweat_smile:

Fiiiiinally got a skeletal rig put together! Those durn yobs getting in the way of the important things in life :wink:

Time for some animating :]

2 Likes

Arrrg, got an animation related issue now :yum:

Both Ogre Exporter and Jmonkey yell at me for having too many verts parented to a single bone. Obviously this is at the sheath, where there are about 16 or so connected to the sheath bone

hehe … the sheath bone’s connected to the, hip bone, the hip bone’s connected to the, chest_IK …

This doesn’t however, affect any animations in the scene editor or while implemented in code… Anyone know if this is gonna cause problems later?

There’s a limitation where a vertex can only be influenced by 4 bones. If you have more than that it will pick the 4 highest ones, renormalize them, and discard the rest. In some cases you won’t notice the difference but in others there could be visual artifacts (such as parts of body going inside other parts), which is why there’s a warning.

Ah, I see, so it’s not the amount of verts per bone its the bones per vert. That’s good to know :smiley:

I wonder what the underlying math for vertex deformation around a bone looks like? So many rabbit holes to fall into, all of them infinitely deep :stuck_out_tongue_closed_eyes:

Its quite complicated… First something called the “offset transform” needs to be calculated which transforms the vertices from bind pose to the animation pose. These transforms are uploaded, per bone, onto the GPU. Then each vertex that the vertex shader processes gets transformed by 4 of those matrices, a matrix per each influencing bone. Due to limitations with how GPUs are programmed, its not allowed to have a variable number of bones per vertex, so it is hardcoded to be exactly 4.

1 Like

Math is amazing :yum: I wonder why put the cap at four though, the amount of entries in a quaternion matrix? … Does this mean that even if there’s only one influencing bone 3 empty matrices get offloaded to the GPU?

Arr I wish our school would take algebra beyond basic conics :frowning:

The cap is 4 because a vertex attribute can contain up to 4 elements. If you want to use more than that (like 6 or 8), you have to create another vertex attribute.

If there’s only one influencing bone, the shader will compute the other 3 matrices, but then will discard the result (since the weights for those bones will be zero).

1 Like

Thanks momoko for entertaining my questions, but before I derail the topic its update time! :smile:

Here’s the first half of the draw animation, where the hand reaches for the sword:

And the second half where the transition will occur:

( don’t know why thumbnail is broken :confused: )

Now all that’s left is to delete the sword verts and bone from blender, export the mesh again, and move the sword over to its own file. Than do some attachment nod(y) stuff to transition when the animation ends. That can till tomorrow though :sleepy:

If you don’t need the sword extra, you could just name it’s node, and get it by code.
Eg specify that the weapon is expected at the holster attachment on file load.

That’s pretty much what I’m going to be doing, switching the sword object’s location through different nodes during anim cycles :smiley:

Hey guys, got a quick math related question (as if there hasn’t been enough trouble with quaternions in the forums already :stuck_out_tongue_closed_eyes:)

In the scene editor, I’ve been playing around with the loc and rot parameters of objects on the attachment Nodes, so as to get the sword’s position correct. This is just so I know where to attach relative to each attNode.

Than I’ve coppied the location and rotation values from the properties file to use in setting translations in code. Yet, the results have been kind of funny.

Here’s the current code :

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package animation;

import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.SkeletonControl;
import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

/**
 *
 * @author Zachariah
 */
public class AttNodes extends SimpleApplication implements AnimEventListener {
    
    private AnimChannel channel;
    private AnimControl control;
    private SkeletonControl skeleton;
    
    Spatial character;
    Spatial sword;
    
    Node sheath;
    Node hand;
    
    public static void main(String[] args) {
        AttNodes app = new AttNodes();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        
        initSpatials();
        initControls();
        initNodes();
        initKeys();
        initLight();
        
        Material debug = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        debug.setColor("Color", ColorRGBA.Red);
        sword.setMaterial(debug);
        
        sheath.attachChild(sword);
        sword.setLocalTranslation(0.01842127f, 0.50035536f, -0.012447849f);
        sword.setLocalRotation(new Quaternion().fromAngles(90.855644f, -110.67349f, 0.2837259f).normalizeLocal());
        
        rootNode.attachChild(character);
    }
    
    public void initKeys() {
        inputManager.addMapping("Draw", new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addListener(actionListener, "Draw");
    }
    
    public void initControls() {
        control = character.getControl(AnimControl.class);
        channel = control.createChannel();
        skeleton = character.getControl(SkeletonControl.class);
    }
    
    public void initNodes() {
        hand = skeleton.getAttachmentsNode("Hand.L");
        sheath = skeleton.getAttachmentsNode("Sheath");
    }
    
    public void initSpatials() {
        character = (Node) assetManager.loadModel("Models/tv_head/TV_Head3/Cube.mesh.j3o");
        sword = (Node) assetManager.loadModel("Models/Sword/Cube.003.mesh.j3o");
    }
    
    public void initLight() {
        viewPort.setBackgroundColor(ColorRGBA.Gray);
        AmbientLight al = new AmbientLight();
        al.setColor(ColorRGBA.White);
        rootNode.addLight(al);
    }
    
    public ActionListener actionListener = new ActionListener(){
        public void onAction(String name, boolean isPressed, float tpf) {
            if(name.equalsIgnoreCase("Draw")) {
                channel.setAnim("Draw_Beginning");
            }
        }
    };

    public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
        if(animName.equals("Draw_Beginning")) {
            hand.attachChild(sword);
            // Set some more locational coordinates here to correctly locate it at the hand
            channel.setAnim("Draw_End");
        }
    }

    public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {

    }
    
}

And then this ends up being my render result :yum:

Looks like the rotation in the scene editor is shown in degrees where as fromAngles() takes radians.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package animation;

import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.LoopMode;
import com.jme3.animation.SkeletonControl;
import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

/**
 *
 * @author Zachariah
 */
public class AttNodes extends SimpleApplication implements AnimEventListener {
    
    private AnimChannel channel;
    private AnimControl control;
    private SkeletonControl skeleton;
    
    Spatial character;
    Spatial sword;
    
    Node sheath;
    Node hand;
    
    public static void main(String[] args) {
        AttNodes app = new AttNodes();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        
        initSpatials();
        initControls();
        initNodes();
        initKeys();
        initLight();
        
        Material debug = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        debug.setColor("Color", ColorRGBA.Red);
        sword.setMaterial(debug);
        
        sheath.attachChild(sword);
        sword.setLocalTranslation(0.01842127f, 0.50035536f, -0.012447849f);
        sword.setLocalRotation(new Quaternion().fromAngles((FastMath.DEG_TO_RAD * 90.855644f), (FastMath.DEG_TO_RAD * -110.67349f), (FastMath.DEG_TO_RAD * 0.2837259f)).normalizeLocal());
        
        rootNode.attachChild(character);
        
        channel.setAnim("Idle");
    }
    
    public void initKeys() {
        inputManager.addMapping("Draw", new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addListener(actionListener, "Draw");
    }
    
    public void initControls() {
        control = character.getControl(AnimControl.class);
        control.addListener(this);
        channel = control.createChannel();
        skeleton = character.getControl(SkeletonControl.class);
    }
    
    public void initNodes() {
        hand = skeleton.getAttachmentsNode("Hand.L");
        sheath = skeleton.getAttachmentsNode("Sheath");
    }
    
    public void initSpatials() {
        character = (Node) assetManager.loadModel("Models/tv_head/TV_Head3/Cube.mesh.j3o");
        sword = (Node) assetManager.loadModel("Models/Sword/Cube.003.mesh.j3o");
    }
    
    public void initLight() {
        viewPort.setBackgroundColor(ColorRGBA.Gray);
        AmbientLight al = new AmbientLight();
        al.setColor(ColorRGBA.White);
        rootNode.addLight(al);
    }
    
    public ActionListener actionListener = new ActionListener(){
        public void onAction(String name, boolean isPressed, float tpf) {
            if(name.equalsIgnoreCase("Draw")) {
                channel.setAnim("Draw_Beginning", 1.0f);
                channel.setLoopMode(LoopMode.DontLoop);
            }
        }
    };

    public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
        if(animName.equals("Draw_Beginning")) {
            hand.attachChild(sword);
            // Set some more locational coordinates here to correctly locate it at the hand
            channel.setAnim("Draw_End");
            channel.setLoopMode(LoopMode.DontLoop);
        }
    }

    public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {

    }
    
}

Quick fix, thanks so much @pspeed :smile: I would have never caught that :flushed:

There is a way to not bother doing that in code.
For one of my game I used an intermediary node attached to the attachement_node.
So I could move it and rotate it in the scene composer so that it has the correct position and orientation, and then in code just attach the weapon geometry to the intermediray node.
See this shot

In the scene explorer in the bottom left corner you can see the there is a rHNode that is my intermidary node.
This would avoid the magic numbers in your code, and you woudln’t have to handle different values for different models.

And to make some shameless advertising here is the result in Adventure available on google play

see how the character on the left sheath and unsheath his weapons.

@nehon yeah actually, that’s quite a bit cleaner :smiley:

I like the game, the combat kind of reminds me of chrono trigger :stuck_out_tongue_winking_eye:

By the way what theme are you using in your sdk, its pretty close to what I use in Blender :chimpanzee_smile:

:stuck_out_tongue_closed_eyes: @nehon really noob question, I haven’t tried playing around with scene files other than in the editor. How do you grab that intermediary node in code? (Currently I’ve been working only with spatials)

Edit: All right, after some searching through the forums, I’ve got it. I constructed the object as a node to call getChild(); Then let the method move down the node tree of the j3o file to find it for me. Is there any advantage or disadvantage over creating things in j3o or in code?