[SOLVED] Shadow issues when using camera rotation to move models

I have some pretty cool first person movement code, which plays footsteps, has a model with animation etc, the issue is: it results in strange shadows to occur in the scene.


first person movement with playcanvas’ script result above.

result of my script.

If you notice: shadows are all wrong, not in places they should be. The post effects are the same for both cameras, in fact they are the same camera since I thought the camera could be the issue, or the model, so I removed the head model, same result. It has to be the code or a side effect of it. Please see my script to help me fix this issue if anyone has the time.
(script is as follows)

var FirstPersonMovement = pc.createScript('firstPersonMovement');

FirstPersonMovement.attributes.add('head', {
    type: 'entity',
    description: 'Assign the head_1 entity for rotation'
});

// Add an attribute to reference the sound entity
FirstPersonMovement.attributes.add('footSteps', {
    type: 'entity',
    description: 'The entity with the walking sound component (footsteps)'
});

FirstPersonMovement.attributes.add('bodyParts', {
    type: 'entity',
    array: true,
    description: 'Assign the body parts that should follow the cameras Y rotation'
});

FirstPersonMovement.attributes.add('power', {
    type: 'number',
    default: 2500,
    description: 'Adjusts the speed of player movement'
});

FirstPersonMovement.attributes.add('lookSpeed', {
    type: 'number',
    default: 0.25,
    description: 'Adjusts the sensitivity of looking'
});

FirstPersonMovement.attributes.add('jumpPower', {
    type: 'number',
    default: 350,
    description: 'Adjusts the jump force'
});

FirstPersonMovement.attributes.add('sprintMultiplier', {
    type: 'number',
    default: 1.5,
    description: 'Multiplier for sprint speed'
});

FirstPersonMovement.prototype.initialize = function () {
    this.force = new pc.Vec3();
    this.eulers = new pc.Vec3();
    this.canJump = false;

    var app = this.app;

    app.mouse.on("mousemove", this._onMouseMove, this);
    app.mouse.on("mousedown", function () { app.mouse.enablePointerLock(); }, this);

    this.entity.rigidbody.fixedRotation = true;

    this.animComponent = this.entity.anim;
    if (!this.animComponent) console.error("Missing 'anim' component");

    // Sound references
    this.footStepsSound = this.footSteps?.sound?.slot('footsteps') || null;
    this.runStepsSound = this.footSteps?.sound?.slot('runsteps') || null;

    // Movement states
    this.isMoving = false;
    this.isRunning = false;
    this.isJumping = false;

    // Collision events
    this.entity.collision.on("collisionstart", this.onCollisionStart, this);
    this.entity.collision.on("collisionend", this.onCollisionEnd, this);

    // Listen for jump input
    app.keyboard.on(pc.EVENT_KEYDOWN, this._onKeyDown, this);
};

FirstPersonMovement.prototype.update = function (dt) {
    var force = this.force;
    var app = this.app;

    var forward = this.head.forward;
    var right = this.head.right;
    var x = 0;
    var z = 0;

    // Movement input
    if (app.keyboard.isPressed(pc.KEY_D)) { x -= right.x; z -= right.z; }
    if (app.keyboard.isPressed(pc.KEY_A)) { x += right.x; z += right.z; }
    if (app.keyboard.isPressed(pc.KEY_S)) { x += forward.x; z += forward.z; }
    if (app.keyboard.isPressed(pc.KEY_W)) { x -= forward.x; z -= forward.z; }

    var isSprinting = app.keyboard.isPressed(pc.KEY_SHIFT) && (x !== 0 || z !== 0);
    var speedMultiplier = isSprinting ? this.sprintMultiplier : 1;

    // Apply movement force
    if (x !== 0 || z !== 0) {
        force.set(x, 0, z).normalize().scale(this.power * speedMultiplier);
        this.entity.rigidbody.applyForce(force);

        if (!this.isJumping) {
            this._setWalkState(!isSprinting);
            this._setRunState(isSprinting);
            this._playFootsteps(isSprinting);
        }
    } else {
        this._setWalkState(false);
        this._setRunState(false);
        this._stopFootsteps();
    }

    // Mouse look
    this.bodyParts.forEach(function (part) {
        part.setLocalEulerAngles(0, this.eulers.x, 0);
    }, this);
};

FirstPersonMovement.prototype._resumeFootsteps = function () {
    var app = this.app;
    var forward = this.head.forward;
    var right = this.head.right;
    var x = 0;
    var z = 0;

    // Movement detection for all keys: WASD
    if (app.keyboard.isPressed(pc.KEY_D)) { x -= right.x; z -= right.z; }
    if (app.keyboard.isPressed(pc.KEY_A)) { x += right.x; z += right.z; }
    if (app.keyboard.isPressed(pc.KEY_S)) { x += forward.x; z += forward.z; }
    if (app.keyboard.isPressed(pc.KEY_W)) { x -= forward.x; z -= forward.z; }

    // Check for sprinting (Shift key held while moving)
    var isSprinting = app.keyboard.isPressed(pc.KEY_SHIFT) && (x !== 0 || z !== 0);
    
    // If the player is moving, play the correct footstep sounds
    if (x !== 0 || z !== 0) {
        this._playFootsteps(isSprinting);
    } else {
        this._stopFootsteps();
    }
};

FirstPersonMovement.prototype._playFootsteps = function (isRunning) {
    if (this.isJumping) {
        this._stopFootsteps();
        return;
    }

    // Play the appropriate sound based on whether the player is running or walking
    if (isRunning && this.runStepsSound && !this.runStepsSound.isPlaying) {
        this._stopFootsteps();
        this.runStepsSound.play();
    } else if (!isRunning && this.footStepsSound && !this.footStepsSound.isPlaying) {
        this._stopFootsteps();
        this.footStepsSound.play();
    }
};

// Stop all footsteps
FirstPersonMovement.prototype._stopFootsteps = function () {
    if (this.footStepsSound && this.footStepsSound.isPlaying) this.footStepsSound.stop();
    if (this.runStepsSound && this.runStepsSound.isPlaying) this.runStepsSound.stop();
};

// Handle mouse movement
FirstPersonMovement.prototype._onMouseMove = function (e) {
    if (pc.Mouse.isPointerLocked() || e.buttons[0]) {
        this.eulers.x -= this.lookSpeed * e.dx;
        this.eulers.y += this.lookSpeed * e.dy; // Fixed inverted movement
        this.eulers.y = pc.math.clamp(this.eulers.y, -90, 70); // Clamp vertical rotation
        this.head.setLocalEulerAngles(this.eulers.y, this.eulers.x, 0);
    }
};

// Jump input
FirstPersonMovement.prototype._onKeyDown = function (e) {
    if (e.key === pc.KEY_SPACE && this.canJump) {
        this.entity.rigidbody.applyImpulse(0, this.jumpPower, 0);
        this.canJump = false;
        this._setJumpState(true);
        this._stopFootsteps(); // Stop sounds immediately on jump
    }
};

// Jump input
FirstPersonMovement.prototype._onKeyDown = function (e) {
    if (e.key === pc.KEY_SPACE && this.canJump) {
        this.entity.rigidbody.applyImpulse(0, this.jumpPower, 0);
        this.canJump = false;
        this._setJumpState(true);
        this._stopFootsteps(); // Stop sounds immediately on jump
    }
};

FirstPersonMovement.prototype.onCollisionStart = function (result) {
    if (result.other && result.other.rigidbody) {
        this.canJump = true;

        // Set jump state to false when landing
        this._setJumpState(false);

        // Recheck movement inputs immediately
        this._checkMovementAfterLanding();

        // Resume footsteps if movement keys are held
        this._resumeFootsteps();
    }
};

// Re-enable footsteps when the player lands
FirstPersonMovement.prototype._checkMovementAfterLanding = function () {
    var app = this.app;
    var forward = this.head.forward;
    var right = this.head.right;
    var x = 0;
    var z = 0;

    // Movement detection
    if (app.keyboard.isPressed(pc.KEY_D)) { x -= right.x; z -= right.z; }
    if (app.keyboard.isPressed(pc.KEY_A)) { x += right.x; z += right.z; }
    if (app.keyboard.isPressed(pc.KEY_S)) { x += forward.x; z += forward.z; }
    if (app.keyboard.isPressed(pc.KEY_W)) { x -= forward.x; z -= forward.z; }

    // Check if the player is sprinting or walking
    var isSprinting = app.keyboard.isPressed(pc.KEY_SHIFT) && (x !== 0 || z !== 0);
    var isMoving = x !== 0 || z !== 0;

    // Update animation and sound state based on movement
    if (isMoving) {
        this._setWalkState(!isSprinting);
        this._setRunState(isSprinting);
        this._playFootsteps(isSprinting);
    } else {
        this._setWalkState(false);
        this._setRunState(false);
        this._stopFootsteps();
    }
};

FirstPersonMovement.prototype.onCollisionEnd = function (result) {
    if (result.other && result.other.rigidbody) {
        this.canJump = false;
    }
};

// State management functions
FirstPersonMovement.prototype._setWalkState = function (isWalking) {
    if (this.animComponent) {
        this.animComponent.setBoolean('isWalk', isWalking);
        this.isMoving = isWalking;
    }
};

FirstPersonMovement.prototype._setRunState = function (isRunning) {
    if (this.animComponent) {
        this.animComponent.setBoolean('isRun', isRunning);
        this.isRunning = isRunning;
    }
};

FirstPersonMovement.prototype._setJumpState = function (isJumping) {
    if (this.animComponent) {
        this.animComponent.setBoolean('isJump', isJumping);
        this.isJumping = isJumping;

        // Stop all movement sounds when jumping
        if (isJumping) {
            this._stopFootsteps();
        }
    }
};

Are the camera heights different?

1 Like

What do you mean?

If you’re talking about Y position, yes one is different to match the head of the character, but that shouldn’t effect the shadow rendering?

It’s rotated x and z 180 (or Y -180, but due to euler angles it is x and z)


above, non glitched shadows

above camera 2 (glitched shadows)

Playcanvas’ FPC shadows:

My FPC shadows:

camera settings for the cameras:

Can I have a project link to take a look?

1 Like

It’s a testing area for all kinds of things so it will be alot of entities, but just know I tried it isolated upon the same result. Here is the project. (New scene that’s more optimized and isolated)

1 Like

Is it cool if I either fork it or you give me admin? Also, Im assuming this shadow isn’t supposed to be there:

1 Like

Yes definitely not, and sure you can do either, I think admin is the best option since I’m using upwards of 200 models.

1 Like

What is your username?

1 Like

just CODEKNIGHT999 but lowercase.

I’ll be afk now for dinner, but I did add you as admin.

1 Like

Thanks! Ill have to go soon for dinner as well, Ill ook at it some now but mainly tomorrow morning.

For the “Player” entity, the View camera is perspective, but “Camera” is orthographic, whereas for “character” both View and Camera are perspective.

1 Like

this is by design, the second camera on the player isn’t rendered, but when it is it will be rendered to a image element for a minimap. Therefore it is orthographic.

1 Like

It’s not an issue with the camera, it seems to be the code for setting the rotation and position of the head entity but for the life of me I can’t fix it.

I tried the following way:

FirstPersonMovement.prototype._onMouseMove = function (e) {
    if (pc.Mouse.isPointerLocked() || e.buttons[0]) {
        this.eulers.x -= this.lookSpeed * e.dx;
        this.eulers.y += this.lookSpeed * e.dy;

        this.eulers.y = pc.math.clamp(this.eulers.y, -90, 70);

        var yawQuat = new pc.Quat().setFromEulerAngles(0, this.eulers.x, 0); // Horizontal rotation
        var pitchQuat = new pc.Quat().setFromEulerAngles(this.eulers.y, 0, 0); // Vertical rotation

        var combinedQuat = new pc.Quat().mul2(yawQuat, pitchQuat);

        this.head.setRotation(combinedQuat);
    }
};

which reduced the excess amount of glitch bar shadows, but didn’t “un-glitch” them. I’m thinking world space, local space, rotations etc have a big part in this issue.

1 Like

I’ll try different combos tomorrow.
I know it’s possible to take screenshots, I wonder if I can have some code try all the different combinations of movement types and take videos to download. That will make things very easy. I’ll do some research and try tomorrow.

Is it possible you could label and comment out code for rotation, local space, and world space?

1 Like

Yeah I can if you need that, I’ll make a backup for it and try somethings tonight and see if I can’t find the issue.

1 Like

The issue isn’t angles, the rotation or code, the camera is re-parented to the players head.
(so you see through the players actual view)
Issue is: the player is too massive & is scaled down, therefore the camera, as a child takes the same scale, instead of it’s original scale, and thus leading to shadow artifacts.


It’s fixed. The cameras scale was about 100 units too small. I upscaled the camera to 100 and the issue is 100% resolved. I do appreciate your help as well, I’m glad it was something so simple.

1 Like

Me too! I’m glad you got it fixed.

1 Like

Thanks, I appreciate your help/time as well. :smile:

1 Like