Drawing a curved arrow - deforming mesh along curve - what am I doing wrong?

I’m trying to draw stylized arrows along a bezeir curve. I understand drawLine is only meant for debugging, so I had to think of another way to draw my curve. (I’d prefer not to draw individual objects as line segments, but that might be a decent fallback if I can’t get this to work…)

Here’s a test project that shows my struggle:

https://playcanvas.com/project/977132/overview/arrows

I can’t embed images in my post, they are referenced inline below and here’s an album with all my screenshots in the order I mention them in this post:

The idea I came up with was to use a rigged mesh. I’ve got a nice 3D arrow mesh, and I rigged it in Blender.

See image 1, RgFw5Mg

I exported it as an FBX and imported into PC.

In PC, I can verify the rig is working by rotating the child bones:

See image 2, B9y3DDw

…so far, so good.

Here’s my scene without my mesh, with just the debug line drawn:

See image 3, byOnKZ0

Here, I’m looping through the points on my line and just setting the next bone position at each point:

    for(var i = 0; i < lut.length - 1; i++){
        var current = new pc.Vec3(lut[i].x, lut[i].y, lut[i].z);
        var next = new pc.Vec3(lut[i+1].x, lut[i+1].y, lut[i+1].z);
        // Draw debug curve
        this.app.drawLine(current, next, pc.Color.RED);
    
        // Attempt to set bone position 
        currentBone.setPosition(current);
        //...snip

See image 4, U1BeGp5

See image 5, X0CsY3p

This almost doesn’t look bad, but I want to rotate the bones so they flow better:

    for(var i = 0; i < lut.length - 1; i++){
        var current = new pc.Vec3(lut[i].x, lut[i].y, lut[i].z);
        var next = new pc.Vec3(lut[i+1].x, lut[i+1].y, lut[i+1].z);
        // Draw debug curve
        this.app.drawLine(current, next, pc.Color.RED);
    
        // Attempt to set bone position AND ROTATION
        currentBone.setPosition(current);
        currentBone.lookAt(next);

See image 6, YnPxbjL
See image 7, kZvd45H

The first and last bones always points up, no matter what I do. I can set the rotation of that last bone to anything, it has no effect whatsoever. That last bone just won’t rotate!

I’ve tried rigging and re-rigging in Blender, and exported the rig about a thousand times with different settings each time. No matter what I do, the “lookAt” on the first bone and the last bone always points upward.

I feel like there’s a trick, maybe something to do with localRotation, but I’m not smart enough to calculate the proper rotation values for each bone.

Any ideas? What else can I try? Is there a different way I should approach this problem? I just want to have some stylized, curvy arrows drawn from code.

Thanks so much! I’m a huge fan of PlayCanvas since I started using it a few weeks ago, I’ve loved every step of the way until now.

1 Like

I should mention, the end goal here is to draw a “flat” arrow, since I’m going to be pointing the camera straight down.

This means I could happily draw the arrow in 2D, if there’s some way that’s easier than rendering a mesh.

I was tempted by the HTML canvas’s “curveTo” functions, but they only work in a "2d"canvas context. I think since playcanvas operates in a “webgl” canvas context, so I can’t use those 2d drawing primitives. Please correct me if I’m wrong there.

The issue is the bones alignment is down the local UP axis (green arrow)

So lookAt won’t work by itself as it aligns the entity’s local forward axis (negative blue arrow) to the position you tell it to look at.

You have to do another local rotation on top so that the local UP axis of the bone is pointing in the direction of the next point.

    for(var i = 0; i < lut.length - 1; i++){
        
        var current = new pc.Vec3(lut[i].x, lut[i].y, lut[i].z);
        var next = new pc.Vec3(lut[i+1].x, lut[i+1].y, lut[i+1].z);
        // Draw debug curve
        this.app.drawLine(current, next, pc.Color.RED);
    
        // Attempt to set bone position AND ROTATION
        currentBone.setPosition(current);
        currentBone.lookAt(next);

        // The bones direction are aligned down their local UP axis 
        // so we do another local rotation to make sure they are a aligned
        // properly

        currentBone.rotateLocal(-90, 0, 0);
        
        if(!currentBone.children || currentBone.children.length < 1){
            console.log('Too many segments, not enough bones');
            break;
        }
        currentBone = currentBone.children[0];
    }

Modified fork: https://playcanvas.com/project/977175

3 Likes

Amazing. You’re a savior! This absolutely works.

I do remember having to rotate the skeleton to match the mesh - it must have been sculpted in a rotated position.

Did I break a best practice here somewhere? Maybe in Blender? Is there a definitive guide somewhere on how to position things in blender so that the coordinate systems mismatch doesn’t bite us?

I’m consistently amazed by you and your team’s dedication on these forums, and with the quality of the Playcanvas engine as well. It’s generally been a joy to work with, and when I hit these problems, the ability to share the project with just a URL and have an expert like yourself go in and correct my mistakes - just an amazing workflow. Well done.

2 Likes

Drawing the individual meshes as segments would probably be a simpler solution here. If you enable dynamic batching for those meshes, internally we’d create a skinned mesh and drive positions of individual segments using bones effectively. It’d be a single draw call as well. This is demonstrated here for example: PlayCanvas Examples

1 Like

Not sure tbh. The bones may have local Y as default going in the direction of the bone. I don’t think this is a coordinate mismatch issue

Following up a few weeks later: I figured out how to fix this at the source when exporting the FBX from Blender.

I had double- and triple-checked that I had “Applied” all my transforms and rotations in Blender, for my bones and my meshes (anyone reading this - go apply your transforms!). That wasn’t it.

The next thing I did was to select the armature in Blender and change the “Tracking Axis” to “-Z” and the “Up Axis” to “Y”. This matches the PlayCanvas system, “-Z Forward”, “Y Up”.

However, that alone wasn’t enough to fix my issue.

They key was that when I exported my FBX, I found these “Armature” settings in the export window. The solution for me was to set “Primary Bone Axis” to “-Z”, and “Secondary Bone Axis” to “X” (also, notice this is another place where you can set the Forward and Up axes to match PlayCanvas - I’m not sure if it’s necessary to do it in both places, but it works for me).

This combination of fixes gets me a rigged mesh that behaves as expected with regards to calling “lookAt()” on the bones. No more need for the localRotation hack.

I don’t know if this is an issue others have hit, but it seems like something that could go into the docs or examples - I imagine exporting rigged meshes from Blender is a common workflow. But either way, the answer is here on the forums now, hopefully I save someone a headache.

2 Likes

Hmm, it feels more like it’s more Blender specific for this particular use case. I’ve not seen this topic about bone orientation come up before.

The forums are part of the documentation search so thank you very much for posting the solution here!