Rotate camera to look in same direction as an entity

For a virtual gallery I want to display hanging art as planes and have the player click to teleport in front of them and face them no matter which angle they’re facing.

I’ve put sub-entity teleporter points at a fixed distance and relative rotation from each artwork to work out the teleport position. I’d prefer to figure this out in math but for now that part works fine. The next part is getting the camera to look at the target entity. I can’t just add that to the teleport because the camera overrides the rotation.

What would be the correct way to map the teleporter entity’s global rotation to the camera?

I’m using this script for mouse drag camera movement, and setting the ex and ey from other scripts to trigger the new coordinates.

var LookCamera = pc.createScript('lookCamera');

// Script attributes to control the sensitivity of the camera look with touch and mouse
LookCamera.attributes.add("mouseLookSensitivity", {type: "number", default: 0, title: "Mouse Look Sensitivity"});
LookCamera.attributes.add("touchLookSensitivity", {type: "number", default: 0, title: "Touch Look Sensitivity"});

// 'Snappiness' factor (how fast does the camera reach the target rotation and distance)
LookCamera.attributes.add("snappinessFactor", {type: "number", default: 0.1, title: "Snappiness Factor", description: "Lower is faster"});

LookCamera.prototype.initialize = function () {
    //console.log("init");
    // Cache some temp variables for use later
    this._tempQuat1 = new pc.Quat();
    this._tempQuat2 = new pc.Quat();
    this._tempVec3_1 = new pc.Vec3();
    
    // Calculate the camera euler angle rotation around x and y axes
    // This allows us to place the camera at a particular rotation to begin with in the scene
    var quat = this.entity.getLocalRotation();
    this.ey = this.getYaw(quat) * pc.math.RAD_TO_DEG;
    this.ex = this.getPitch(quat, this.ey) * pc.math.RAD_TO_DEG;
    
    // The target rotation for the camera to rotate to
    this.targetEx = this.ex;
    this.targetEy = this.ey;
            
    // Workaround for mouse movement as the first move event can give very large
    // difference moved in screen position 
    this.moved = false;

    // Disabling the context menu stops the browser displaying a menu when
    // you right-click the page
    this.app.mouse.disableContextMenu();

    // Store the position of the touch so we can calculate the distance moved
    this.lastTouchPosition = new pc.Vec2();

    this.addEventCallbacks();
};


LookCamera.prototype.addEventCallbacks = function() {
    if (this.app.mouse) {
        this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
    }
    
    if (this.app.touch) {
        this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStart, this);
        this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
    }
};


LookCamera.prototype.postUpdate = function (dt) {
    // Update the camera's orientation to rotate smoothly towards the target rotation
    // By using lerp in this way, the rotation will go slower as it gets closer to
    // the target rotation
    var lerp = 1;
    if (this.snappinessFactor > 0) {
        lerp = dt / this.snappinessFactor;
    }
    
    this.ex = pc.math.lerp(this.ex, this.targetEx, lerp);
    this.ey = pc.math.lerp(this.ey, this.targetEy, lerp);
        
    this.entity.setLocalEulerAngles(this.ex, this.ey, 0);
    //this.entity.rigidbody.teleport(this.entity.getLocalPosition(), this.ex,0,this.ey);
};


LookCamera.prototype.moveCamera = function(dx, dy, sensitivity) {
    // Update the current Euler angles, clamp the pitch.
    if (!this.moved) {
        // first move event can be very large
        this.moved = true;
        return;
    }
        
    this.targetEx -= dy * sensitivity;
    this.targetEx = pc.math.clamp(this.targetEx, -90, 90);
    this.targetEy -= dx * sensitivity;
};


LookCamera.prototype.onMouseMove = function (event) {
    // Only update the camera target rotation only if the left mouse
    // button is pressed down
    if (this.app.mouse.isPressed(pc.MOUSEBUTTON_LEFT)) {
        if (event.dx !== 0 || event.dy !== 0)
        {
            this.moveCamera(event.dx, event.dy, this.mouseLookSensitivity);
            this.entity.script.niceraycast.lookmoved = true;
        }
    }
};


LookCamera.prototype.onTouchStart = function(event) {
    // We only care about the first touch. As the user touches the screen, 
    // we stored the current touch position
    var touch = event.touches[0];
    this.lastTouchPosition.set(touch.x, touch.y);
};


LookCamera.prototype.onTouchMove = function(event) {
    // We only care about the first touch. Work out the difference moved since the last event
    // and use that to update the camera target position 
    var touch = event.touches[0];
    
    if (Math.abs(touch.x - this.lastTouchPosition.x) > 1 || Math.abs(touch.y - this.lastTouchPosition.y) > 1)
        this.entity.script.niceraycast.lookmoved = true;
    
    this.moveCamera(-(touch.x - this.lastTouchPosition.x), -(touch.y - this.lastTouchPosition.y), this.touchLookSensitivity);
    this.lastTouchPosition.set(touch.x, touch.y);    
};


LookCamera.prototype.getYaw = function () {    
    var forward = this.entity.forward.clone();
    return Math.atan2(-forward.x, -forward.z);    
};


LookCamera.prototype.getPitch = function(quaternion, yaw) {
    var quatWithoutYaw = this._tempQuat1;
    var yawOffset = this._tempQuat2;
    
    yawOffset.setFromEulerAngles(0, -yaw, 0);
    quatWithoutYaw.mul2(yawOffset, quaternion);
    
    var transformedForward = this._tempVec3_1;
    
    quatWithoutYaw.transformVector(pc.Vec3.FORWARD, transformedForward);
    
    return Math.atan2(transformedForward.y, -transformedForward.z) ;      
};

If you look at the getYaw and getPitch functions, you should be able to reuse that logic to recalculate the new yaw and pitch angles that you want the camera to face.

Pitch should just be zero as I want it facing straight ahead.

From what I can see the getYaw is doing what I’ve already tried. I think this was originally taken from something I used for setting rotation directly from network input but doesn’t like working with the forwards taken from other entities.

It kind of works for perpendicular angles but anything else is all over the place.

            var tp = hitEntity.findByName("TeleportPoint");
            this.playerEntity.rigidbody.teleport(tp.getPosition());
            var fwd = tp.forward;
            var camy = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
            this.entity.script.lookCamera.targetEx = 0;
            this.entity.script.lookCamera.targetEy = camy;

TeleportPoints are children of the matching hitEntitys set to distance of 4 and 180 degrees. I’ve also tried using the hitEntity forward and reversing it.

I do something like this for the OrbitCamera https://playcanvas.com/editor/code/438243?tabs=5665660

See function resetAndLookAtPoint

I tried to adapt that but there are still some weird quirks at certain angles.

I’ve copied the project and cut it down to focus on this problem. There are a bunch of planes at different angles. Multiples of 90 degrees work fine. Some of the arbitrary angles work but others are anywhere up to 90 degrees out.

https://playcanvas.com/project/709779/overview/teleport-to-and-look-at

The project you linked to is private.

Oops. Fixed.

It’s mostly a scene setup issue. The Artwork entities are rotated so that forward vector is pointing towards the sky (world up).

I’ve fixed the scene here so that the forward vector is in the same direction as what you would want the player to face.

https://playcanvas.com/editor/scene/969678

Ohh right. Now it makes sense. I figured I must have been misunderstanding something very fundamental there.

This also solves the relative position problem that necessitated the use of those TeleportPoints:

this.playerEntity.rigidbody.teleport(x-hitEntity.forward.x * 4,y,z-hitEntity.forward.z * 4);