I’ve set my ArcRotateCamera’s targetScreenOffset to Vector2(0, -1) to position my third-person character. To prevent the camera from clipping through obstacles, I’m using raycasting.
The raycast is functioning correctly, detecting obstacles like walls, and adjusting the camera’s position. However, despite this adjustment, the camera still shows the wall itself due to the targetScreenOffset. You can see the issue in the video below.
Does anyone have suggestions on how to resolve this?
Here is the pseudo code;
// Get the player's position
const playerPos = capsule.position.clone();
// Get the direction opposite to the camera's forward direction
const direction = camera.getForwardRay().direction.negate();
// Create a ray from the player's position in the opposite direction
const ray = new BABYLON.Ray(playerPos, direction, api.camera.defaultRadius);
// Perform a multi-pick with the ray
const picks = scene.multiPickWithRay(ray);
// Sort the pick results by distance and filter out any mesh that starts with "Chr_"
const sortedPicks = picks
.sort((a, b) => a.distance - b.distance)
.filter(pick => !pick.pickedMesh.name.startsWith("Chr_"));
// Get the first valid pick
const pick = sortedPicks[0];
if (pick && pick.hit) {
// If a hit was detected, set the camera's position to the picked point
camera.position = pick.pickedPoint.clone();
} else {
// Otherwise, set the camera's radius to the default radius
camera.radius = api.camera.defaultRadius;
}
This should be because the start of the ray is not in the right place. The start position should not be “playerPos” but the actual camera target.
But as far as I know the camera’s getTarget() function doesn’t take targetScreenOffset into account.
const character = new PhyCharacter(scene)
character.height = 1
let offsetY = new TransformNode("offsetY", scene)
offsetY.parent = character
offsetY.position.y = 1
offsetY.billboardMode = 2
let offsetX = new TransformNode("offsetX", scene)
offsetX.parent = offsetY
offsetX.position.x = 0.5
let camera = createCamera(scene)
camera.targetHost = offsetX
camera.addBehavior(new PhyArcCameraCollision())
PhyArcCameraCollision.ts
import { Behavior } from "@babylonjs/core/Behaviors/behavior"
import { PhysicsRaycastResult } from "@babylonjs/core/Physics/physicsRaycastResult"
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera"
import { Scalar, TmpVectors, Vector3 } from "@babylonjs/core"
export class PhyArcCameraCollision implements Behavior<ArcRotateCamera> {
name: string = "BehaviorArcCameraCollision"
private _originalGetViewMatrix: any
target: ArcRotateCamera | undefined
collideWith: number = 0b0001
_targetRadius: number = 0
_currentRadius: number = 0
testSize: number = 1
smooth: boolean = false
smoothFactor: number = 20
init(): void {}
attach(target: ArcRotateCamera): void {
const scene = target.getScene()
if (!target.getScene().physicsEnabled) {
console.warn("Physics engine not enabled, phyArcCameraCollision is invalid")
}
this.target = target
this._originalGetViewMatrix = target._getViewMatrix.bind(target)
let result = new PhysicsRaycastResult()
target._getViewMatrix = () => {
let originRaidus = target.radius
let phyEngine = target.getScene().getPhysicsEngine()?.getPhysicsPlugin()!
if (!phyEngine) {
return this._originalGetViewMatrix()
}
let from = target.getTarget()
let dir = TmpVectors.Vector3[1]
target.getDirectionToRef(Vector3.LeftHandedBackwardReadOnly, dir)
let to = from.add(dir.scale(originRaidus + this.testSize))
phyEngine.raycast(from, to, result, { collideWith: this.collideWith })
if (result.hasHit) {
this._targetRadius = result.hitDistance - this.testSize
} else {
this._targetRadius = originRaidus
}
if (this.smooth) {
this._currentRadius = Scalar.Lerp(
this._currentRadius,
this._targetRadius,
(this.smoothFactor * scene.getEngine().getDeltaTime()) / 1000,
)
} else {
this._currentRadius = this._targetRadius
}
this._currentRadius = Math.max(0.001, this._currentRadius)
target.radius = Math.min(originRaidus, this._currentRadius)
let viewMatrix = this._originalGetViewMatrix(target)
target.radius = originRaidus
return viewMatrix
}
scene.onBeforeRenderObservable.add(() => {})
}
detach(): void {
if (this.target) {
this.target._getViewMatrix = this._originalGetViewMatrix
}
}
}
I finally found a solution after a few days. I created a small box and update its position every frame based on the camera’s right direction. The position of the box is set to the character’s position plus the camera’s right direction, and I set the camera’s target to that box (made it invisible, no targetScreenOffset applied).