Character Head Follow

I’m implementing a head follow/look at target for a few of my animated characters. I need to figure out of if there is a a callback for after an animation is applied to the skeleton or should I do that on something like a layer’s prerender?

Thanks again!

You can do it in the postUpdate part of any script - at that point the animation has affected the bones already, and you can modify them.

We have some test internal projects, and it’s not a very clean code to release as an example, but feel free to use it as a motivation, especially the end part that handles the look at part.

var Locomotion = pc.createScript('locomotion');

window.characterDirection = new pc.Vec3(1, 0, 0);
window.targetPosition = new pc.Vec3(2,0,1.5);
window.jumpCurve = new pc.Curve([
    0, 0,
]);
window.jumpCurve.type = pc.CURVE_SPLINE;
window.jumpTime = 0;
window.runSpeed = 4.0;

// initialize code called once per entity
Locomotion.prototype.initialize = function() {
    this.app.mouse.disableContextMenu();
    this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
};

Locomotion.prototype.onMouseDown = function (event) {
    if (event.button !== 0) return;
    // Set the character target position to a position on the plane that the user has clicked
    var cameraEntity = this.app.root.findByName('Camera');
    var near = cameraEntity.camera.screenToWorld(event.x, event.y, 0.1);
    var far = cameraEntity.camera.screenToWorld(event.x, event.y, 1000.0);
    var result = this.app.systems.rigidbody.raycastFirst(far, near);
    if (result) {
        window.targetPosition = new pc.Vec3(result.point.x, 0, result.point.z);
    }
};

// update code called every frame
Locomotion.prototype.postUpdate = function(dt) {
    if(this.app.keyboard.wasPressed(pc.KEY_SPACE)) {
        var isJumping = this.entity.anim.baseLayer.activeState === 'Jump';
        if (!isJumping) {
            window.jumpTime = 0;
            this.entity.anim.setTrigger('jump');
        }
        this.entity.anim.setTrigger('jump');
    }
    // Move the character along X & Z axis based on click target position & make character face click direction
    if (!this.entity.position.equals(window.targetPosition)) {
        var prevMoveSpeed = 0.0;
        var activeMoveSpeed = 0.0;
        if (this.entity.anim.baseLayer.previousState === 'Walk') {
            prevMoveSpeed = window.runSpeed;
        } else if (this.entity.anim.baseLayer.previousState === 'Idle') {
            prevMoveSpeed = 0.0;
        } else if (this.entity.anim.baseLayer.previousState === 'Jump') {
            prevMoveSpeed = 0.0;
        }
        if (this.entity.anim.baseLayer.activeState === 'Walk') {
            activeMoveSpeed = window.runSpeed;
        } else if (this.entity.anim.baseLayer.activeState === 'Idle') {
            activeMoveSpeed = 0.0;
        } else if (this.entity.anim.baseLayer.activeState === 'Jump') {
            activeMoveSpeed = 0.0;
        }
        var totalMoveSpeed = activeMoveSpeed;
        if (this.entity.anim.baseLayer.transitioning) {
            var progress = this.entity.anim.baseLayer.transitionProgress;
            totalMoveSpeed = (prevMoveSpeed * (1.0 - progress)) + (activeMoveSpeed * progress);
        }
        this.entity.anim.setInteger('speed', 1);
        var distance = window.targetPosition.clone().sub(this.entity.position);
        var direction = distance.clone().normalize();
        window.characterDirection = new pc.Vec3().sub(direction);
        var movement = direction.clone().scale(dt * totalMoveSpeed);
        if (movement.length() < distance.length()) {
            this.entity.setPosition(this.entity.position.add(movement));
        } else {
            this.entity.position = window.targetPosition;
        }
    } else {
        this.entity.anim.setInteger('speed', 0);
    }
    this.entity.lookAt(this.entity.position.clone().add(window.characterDirection));

    // Make character look at the block if facing it
    var block = pc.app.root.findByName('Block');
    var head = pc.app.root.findByName('C_head0001_bind_JNT');
    var headToBlockPlaneDistance = new pc.Vec3(this.entity.position.x, 0, this.entity.position.z).sub(new pc.Vec3(block.localPosition.x, 0, block.localPosition.z));
    var headToBlockPlaneDirection = headToBlockPlaneDistance.clone().normalize();
    var prevLocalRotation = head.localRotation.clone();
    var isFacingBlock = window.characterDirection.clone().dot(headToBlockPlaneDirection) > 0.5;
    var isNotNearBlock = headToBlockPlaneDistance.length() > 0.6;
    var isBlinking = this.entity.anim.findAnimationLayer('face').activeState === 'idle-blink';
    if (isFacingBlock && isNotNearBlock) {
        head.lookAt(head.position.clone().add(head.position.clone().sub(block.localPosition)));
        window.lookAtBlockRotation = head.localRotation.clone();
        if (!isBlinking) this.entity.anim.findAnimationLayer('face').play('cheerful');
    } else {
        window.lookAtBlockRotation = head.localRotation.clone();
        if (!isBlinking) this.entity.anim.findAnimationLayer('face').play('idle');
    }
    if (!window.interpDirection) {
        window.interpDirection = prevLocalRotation;
    }
    var a = window.interpDirection;
    var b = window.lookAtBlockRotation;
    var rotationDistance = Math.acos(2.0 * Math.pow(a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w, 2) - 1);
    if (!Number.isNaN(rotationDistance) && rotationDistance > 0.001) {
        var interp = (1.0 / rotationDistance) * dt * 4;
        window.interpDirection = new pc.Quat().slerp(a, b, interp > 1.0 ? 1.0 : interp);
    }
    head.setLocalRotation(window.interpDirection);
};

// swap method called for script hot-reloading
// inherit your script state here
// Locomotion.prototype.swap = function(old) { };

// to learn more about script anatomy, please read:
// http://developer.playcanvas.com/en/user-manual/scripting/

Thanks you so much! This is awesome :slight_smile: