Skip to content

File tree

1 file changed

+185
-48
lines changed

1 file changed

+185
-48
lines changed

packages/dev/core/src/Physics/v2/characterController.ts

Lines changed: 185 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Vector3, Quaternion, Matrix, TmpVectors } from "../../Maths/math.vector";
22
import type { Scene } from "../../scene";
33
import type { DeepImmutableObject } from "../../types";
4-
import type { PhysicsBody } from "./physicsBody";
4+
import { PhysicsBody } from "./physicsBody";
55
import { PhysicsShapeCapsule, type PhysicsShape } from "./physicsShape";
66
import { PhysicsMotionType } from "./IPhysicsEnginePlugin";
77
import type { HavokPlugin } from "./Plugins/havokPlugin";
88
import { BuildArray } from "core/Misc/arrayTools";
9+
import { TransformNode } from "../../Meshes/transformNode";
10+
import { Observable } from "../../Misc/observable";
911

1012
/**
1113
* Shape properties for the character controller
@@ -25,6 +27,29 @@ export interface CharacterShapeOptions {
2527
*/
2628
capsuleRadius?: number;
2729
}
30+
31+
/**
32+
* Collision event data for the character controller
33+
*/
34+
export interface ICharacterControllerCollisionEvent {
35+
/**
36+
* The collider physics body
37+
*/
38+
collider: PhysicsBody;
39+
/**
40+
*
41+
*/
42+
colliderIndex: number;
43+
/**
44+
* Separation force applied to the collider
45+
*/
46+
impulse: Vector3;
47+
/**
48+
* Position where the impulse is applied
49+
*/
50+
impulsePosition: Vector3;
51+
}
52+
2853
/**
2954
* State of the character on the surface
3055
*/
@@ -192,14 +217,16 @@ export class PhysicsCharacterController {
192217
private _velocity: Vector3;
193218
private _lastVelocity: Vector3;
194219
private _shape: PhysicsShape;
220+
private _body: PhysicsBody;
221+
private _transformNode: TransformNode;
195222
private _ownShape: boolean;
196223
private _manifold: IContact[] = [];
197224
private _lastDisplacement: Vector3;
198225
private _contactAngleSensitivity = 10.0;
199226
private _lastInvDeltaTime: number;
200227
private _scene: Scene;
201228
private _tmpMatrix = new Matrix();
202-
private _tmpVecs: Vector3[] = BuildArray(31, Vector3.Zero);
229+
private _tmpVecs: Vector3[] = BuildArray(32, Vector3.Zero);
203230

204231
/**
205232
* minimum distance to make contact
@@ -267,9 +294,19 @@ export class PhysicsCharacterController {
267294
* default 0
268295
*/
269296
public characterMass = 0;
297+
298+
/**
299+
* Observable for trigger entered and trigger exited events
300+
*/
301+
public onTriggerCollisionObservable = new Observable<ICharacterControllerCollisionEvent>();
302+
270303
private _startCollector;
271304
private _castCollector;
272305

306+
// If the difference between the cast displacement and the simplex solver output position is less than this
307+
// value (per component), do not do a second cast to check if it's possible to reach the output position.
308+
private _displacementEps = 1e-4;
309+
273310
/**
274311
* instanciate a new characterController
275312
* @param position Initial position
@@ -286,6 +323,12 @@ export class PhysicsCharacterController {
286323
this._tmpVecs[1].set(0, -h * 0.5 + r, 0);
287324
this._ownShape = !characterShapeOptions.shape;
288325
this._shape = characterShapeOptions.shape ?? new PhysicsShapeCapsule(this._tmpVecs[0], this._tmpVecs[1], r, scene);
326+
this._transformNode = new TransformNode("CCTransformNode", scene);
327+
this._transformNode.position.copyFrom(this._position);
328+
this._body = new PhysicsBody(this._transformNode, PhysicsMotionType.ANIMATED, false, scene);
329+
this._body.setMassProperties({ inertia: Vector3.ZeroReadOnly });
330+
this._body.shape = this._shape;
331+
this._body.disablePreStep = false;
289332
this._lastInvDeltaTime = 1.0 / 60.0;
290333
this._lastDisplacement = Vector3.Zero();
291334
this._scene = scene;
@@ -297,6 +340,22 @@ export class PhysicsCharacterController {
297340
this._castCollector = hknp.HP_QueryCollector_Create(16)[1];
298341
}
299342

343+
/**
344+
* Dispose the character controller
345+
*/
346+
public dispose() {
347+
if (this._ownShape) {
348+
this._shape.dispose();
349+
}
350+
this._body.dispose();
351+
this._transformNode.dispose();
352+
353+
const hk = this._scene.getPhysicsEngine()!.getPhysicsPlugin() as HavokPlugin;
354+
const hknp = hk._hknp;
355+
hknp.HP_QueryCollector_Release(this._startCollector);
356+
hknp.HP_QueryCollector_Release(this._castCollector);
357+
}
358+
300359
/**
301360
* Get shape used for collision
302361
*/
@@ -308,6 +367,7 @@ export class PhysicsCharacterController {
308367
* Set shape used for collision
309368
*/
310369
public set shape(value: PhysicsShape) {
370+
this._body.shape = this._shape;
311371
if (this._ownShape) {
312372
this._shape.dispose();
313373
}
@@ -329,6 +389,7 @@ export class PhysicsCharacterController {
329389
*/
330390
public setPosition(position: Vector3) {
331391
this._position.copyFrom(position);
392+
this._transformNode.position.copyFrom(this._position);
332393
}
333394

334395
/**
@@ -496,7 +557,10 @@ export class PhysicsCharacterController {
496557
return numHitBodies;
497558
}
498559

499-
protected _createSurfaceConstraint(contact: IContact, timeTravelled: number): ISurfaceConstraintInfo {
560+
// Store previous positions per body for velocity calculation
561+
protected _bodyPositionTracking = new Map();
562+
563+
protected _createSurfaceConstraint(dt: number, contact: IContact, timeTravelled: number): ISurfaceConstraintInfo {
500564
const constraint = {
501565
//let distance = contact.distance - this.keepDistance;
502566
planeNormal: contact.normal.clone(),
@@ -536,7 +600,49 @@ export class PhysicsCharacterController {
536600
if (motionType == PhysicsMotionType.STATIC) {
537601
constraint.priority = 2;
538602
} else if (motionType == PhysicsMotionType.ANIMATED) {
539-
constraint.priority = 1;
603+
const bodyTransformNode = contact.bodyB.body.transformNode;
604+
const bodyId = bodyTransformNode.uniqueId;
605+
606+
// Retrieve tracking data
607+
let tracking = this._bodyPositionTracking.get(bodyId);
608+
609+
const currentFrameWorldMatrix = contact.bodyB.body.transformNode.getWorldMatrix();
610+
const frameId = this._scene.getFrameId();
611+
612+
if (!tracking) {
613+
// Initialize tracking
614+
tracking = {
615+
prevWorldMatrix: currentFrameWorldMatrix.clone(),
616+
frameId: frameId,
617+
};
618+
this._bodyPositionTracking.set(bodyId, tracking);
619+
} else {
620+
// Only calculate velocity if this contact existed in the previous frame
621+
// This avoids huge delta spikes when first making contact or after gaps
622+
if (tracking.frameId + 1 === frameId) {
623+
const previousFrameWorldMatrix = tracking.prevWorldMatrix;
624+
625+
const currentFrameWorldMatrixInverse = TmpVectors.Matrix[1];
626+
627+
currentFrameWorldMatrix.invertToRef(currentFrameWorldMatrixInverse);
628+
629+
const characterPosition = this.getPosition();
630+
// compute characterPosition in body local space at previous frame
631+
const characterLocalPosition = this._tmpVecs[21];
632+
Vector3.TransformCoordinatesToRef(characterPosition, currentFrameWorldMatrixInverse, characterLocalPosition);
633+
634+
const characterWorldPosition = this._tmpVecs[22];
635+
Vector3.TransformCoordinatesToRef(characterLocalPosition, previousFrameWorldMatrix, characterWorldPosition);
636+
const playerDeltaPosition = this._tmpVecs[23];
637+
characterPosition.subtractToRef(characterWorldPosition, playerDeltaPosition);
638+
639+
constraint.velocity.copyFrom(playerDeltaPosition);
640+
constraint.velocity.scaleInPlace(1 / dt);
641+
constraint.priority = 1;
642+
}
643+
tracking.prevWorldMatrix.copyFrom(currentFrameWorldMatrix);
644+
tracking.frameId = frameId;
645+
}
540646
}
541647

542648
return constraint;
@@ -585,7 +691,7 @@ export class PhysicsCharacterController {
585691
protected _createConstraintsFromManifold(dt: number, timeTravelled: number): ISurfaceConstraintInfo[] {
586692
const constraints = [];
587693
for (let i = 0; i < this._manifold.length; i++) {
588-
const surfaceConstraint = this._createSurfaceConstraint(this._manifold[i], timeTravelled);
694+
const surfaceConstraint = this._createSurfaceConstraint(dt, this._manifold[i], timeTravelled);
589695
constraints.push(surfaceConstraint);
590696
this._addMaxSlopePlane(this.maxSlopeCosine, this.up, i, constraints, this._manifold[i].allowedPenetration);
591697
this._resolveConstraintPenetration(surfaceConstraint, this.penetrationRecoverySpeed);
@@ -1257,7 +1363,7 @@ export class PhysicsCharacterController {
12571363
orientation,
12581364
this.keepDistance + this.keepContactTolerance, // max distance
12591365
false, // should hit triggers
1260-
[BigInt(0)], // body to ignore //<todo allow for a proxy body!
1366+
[this._body._pluginData.hpBodyId[0]],
12611367
];
12621368
hknp.HP_World_ShapeProximityWithCollector(hk.world, startCollector, query);
12631369
}
@@ -1269,7 +1375,7 @@ export class PhysicsCharacterController {
12691375
startNative,
12701376
[endPos.x, endPos.y, endPos.z],
12711377
false, // should hit triggers
1272-
[BigInt(0)], // body to ignore //<todo allow for proxy body
1378+
[this._body._pluginData.hpBodyId[0]],
12731379
];
12741380
hknp.HP_World_ShapeCastWithCollector(hk.world, castCollector, query);
12751381
}
@@ -1356,6 +1462,14 @@ export class PhysicsCharacterController {
13561462

13571463
//<todo Fire callback to allow user to change impulse + use the info / play sounds
13581464

1465+
const triggerCollisionInfo: ICharacterControllerCollisionEvent = {
1466+
collider: bodyB.body,
1467+
colliderIndex: bodyB.index,
1468+
impulse: outputObjectImpulse,
1469+
impulsePosition: outputImpulsePosition,
1470+
};
1471+
this.onTriggerCollisionObservable.notifyObservers(triggerCollisionInfo);
1472+
13591473
bodyB.body.applyImpulse(outputObjectImpulse, outputImpulsePosition, bodyB.index);
13601474
}
13611475
}
@@ -1388,49 +1502,12 @@ export class PhysicsCharacterController {
13881502
return 1 / body.body.getMassProperties(body.index).mass!;
13891503
}
13901504

1391-
/**
1392-
* Update internal state. Must be called once per frame
1393-
* @param deltaTime frame delta time in seconds. When using scene.deltaTime divide by 1000.0
1394-
* @param surfaceInfo surface information returned by checkSupport
1395-
* @param gravity gravity applied to the character. Can be different that world gravity
1396-
*/
1397-
public integrate(deltaTime: number, surfaceInfo: CharacterSurfaceInfo, gravity: Vector3) {
1505+
protected _integrateManifolds(deltaTime: number, gravity: Vector3): void {
13981506
const hk = this._scene.getPhysicsEngine()!.getPhysicsPlugin() as HavokPlugin;
1399-
1400-
const invDeltaTime = 1 / deltaTime;
1401-
let remainingTime = deltaTime;
1402-
let newVelocity = Vector3.Zero();
1403-
1404-
// If the difference between the cast displacement and the simplex solver output position is less than this
1405-
// value (per component), do not do a second cast to check if it's possible to reach the output position.
1406-
const displacementEps = 1e-4;
14071507
const epsSqrd = 1e-8;
14081508

1409-
// Choose the first cast direction. If velocity hasn't changed from the previous integrate, guess that the
1410-
// displacement will be the same as last integrate, scaled by relative step length. Otherwise, guess based
1411-
// on current velocity.
1412-
{
1413-
const tolerance = displacementEps * invDeltaTime;
1414-
if (this._velocity.equalsWithEpsilon(this._lastVelocity, tolerance)) {
1415-
this._lastDisplacement.scaleInPlace(remainingTime * this._lastInvDeltaTime);
1416-
} else {
1417-
const displacementVelocity = this._velocity;
1418-
if (surfaceInfo.supportedState == CharacterSupportedState.SUPPORTED) {
1419-
const relativeVelocity = this._tmpVecs[28];
1420-
this._velocity.subtractToRef(surfaceInfo.averageSurfaceVelocity, relativeVelocity);
1421-
const normalDotVelocity = surfaceInfo.averageSurfaceNormal.dot(relativeVelocity);
1422-
if (normalDotVelocity < 0) {
1423-
relativeVelocity.subtractInPlace(surfaceInfo.averageSurfaceNormal.scale(normalDotVelocity));
1424-
displacementVelocity.copyFrom(relativeVelocity);
1425-
displacementVelocity.addInPlace(surfaceInfo.averageSurfaceVelocity);
1426-
}
1427-
}
1428-
this._lastDisplacement.copyFrom(displacementVelocity);
1429-
this._lastDisplacement.scaleInPlace(remainingTime);
1430-
}
1431-
this._lastVelocity.copyFrom(this._velocity);
1432-
this._lastInvDeltaTime = invDeltaTime;
1433-
}
1509+
let newVelocity = Vector3.Zero();
1510+
let remainingTime = deltaTime;
14341511

14351512
// Make sure that contact with bodies that have been removed since the call to checkSupport() are removed from the
14361513
// manifold
@@ -1458,7 +1535,7 @@ export class PhysicsCharacterController {
14581535
// If castCollector had hits on different bodies (so we're not sure if some non-closest body could be in our way) OR
14591536
// the simplex has given an output direction different from the cast guess
14601537
// we re-cast to check we can move there. There is no need to get the start points again.
1461-
if (updateResult != 0 || (newDisplacement.lengthSquared() > epsSqrd && !this._lastDisplacement.equalsWithEpsilon(newDisplacement, displacementEps))) {
1538+
if (updateResult != 0 || (newDisplacement.lengthSquared() > epsSqrd && !this._lastDisplacement.equalsWithEpsilon(newDisplacement, this._displacementEps))) {
14621539
this._castWithCollectors(this._position, this._position.add(newDisplacement), this._castCollector, this._startCollector);
14631540
const hknp = hk._hknp;
14641541
const numCastHits = hknp.HP_QueryCollector_GetNumHits(this._castCollector)[1];
@@ -1500,6 +1577,66 @@ export class PhysicsCharacterController {
15001577
}
15011578

15021579
this._velocity.copyFrom(newVelocity);
1580+
this._transformNode.position.copyFrom(this._position);
1581+
}
1582+
1583+
/**
1584+
* Move the character with collisions
1585+
* @param displacement defines the requested displacement vector
1586+
*/
1587+
public moveWithCollisions(displacement: Vector3): void {
1588+
if (this._scene.deltaTime == undefined) {
1589+
return;
1590+
}
1591+
const deltaTime = this._scene.deltaTime / 1000.0;
1592+
const invDeltaTime = 1 / deltaTime;
1593+
1594+
displacement.scaleToRef(1 / deltaTime, this._velocity);
1595+
this._lastDisplacement.copyFrom(displacement);
1596+
1597+
this._lastVelocity.copyFrom(this._velocity);
1598+
this._lastInvDeltaTime = invDeltaTime;
1599+
1600+
this._integrateManifolds(deltaTime, Vector3.ZeroReadOnly);
1601+
}
1602+
1603+
/**
1604+
* Update internal state. Must be called once per frame
1605+
* @param deltaTime frame delta time in seconds. When using scene.deltaTime divide by 1000.0
1606+
* @param surfaceInfo surface information returned by checkSupport
1607+
* @param gravity gravity applied to the character. Can be different that world gravity
1608+
*/
1609+
public integrate(deltaTime: number, surfaceInfo: CharacterSurfaceInfo, gravity: Vector3) {
1610+
const invDeltaTime = 1 / deltaTime;
1611+
const remainingTime = deltaTime;
1612+
1613+
// Choose the first cast direction. If velocity hasn't changed from the previous integrate, guess that the
1614+
// displacement will be the same as last integrate, scaled by relative step length. Otherwise, guess based
1615+
// on current velocity.
1616+
{
1617+
const tolerance = this._displacementEps * invDeltaTime;
1618+
if (this._velocity.equalsWithEpsilon(this._lastVelocity, tolerance)) {
1619+
this._lastDisplacement.scaleInPlace(remainingTime * this._lastInvDeltaTime);
1620+
} else {
1621+
const displacementVelocity = this._velocity;
1622+
if (surfaceInfo.supportedState == CharacterSupportedState.SUPPORTED) {
1623+
const relativeVelocity = this._tmpVecs[28];
1624+
this._velocity.subtractToRef(surfaceInfo.averageSurfaceVelocity, relativeVelocity);
1625+
const normalDotVelocity = surfaceInfo.averageSurfaceNormal.dot(relativeVelocity);
1626+
if (normalDotVelocity < 0) {
1627+
relativeVelocity.subtractInPlace(surfaceInfo.averageSurfaceNormal.scale(normalDotVelocity));
1628+
displacementVelocity.copyFrom(relativeVelocity);
1629+
displacementVelocity.addInPlace(surfaceInfo.averageSurfaceVelocity);
1630+
}
1631+
}
1632+
this._lastDisplacement.copyFrom(displacementVelocity);
1633+
this._lastDisplacement.scaleInPlace(remainingTime);
1634+
}
1635+
this._lastVelocity.copyFrom(this._velocity);
1636+
this._lastInvDeltaTime = invDeltaTime;
1637+
}
1638+
1639+
this._integrateManifolds(deltaTime, gravity);
15031640
}
15041641

15051642
/**

0 commit comments

Comments
 (0)