Composables Physics 3D
Package: @gwenjs/physics3d
Les composables Physics 3D ajoutent la dynamique des corps rigides et les collisions aux acteurs dans l'espace tridimensionnel. Comme pour la 2D, ils sont appelés à l'intérieur de defineActor() et fonctionnent avec le graphe de scène. La physique 3D inclut des fonctionnalités avancées comme les raycasts, les shape casts et les colliders de mesh pour les environnements complexes.
Les bases
Déclarez la physique 3D à l'intérieur de defineActor() — une fois par type d'acteur :
import { defineActor, definePrefab } from '@gwenjs/core/actor'
import { onUpdate } from '@gwenjs/core/system'
import { onContact, useDynamicBody, useSphereCollider, useRaycast } from '@gwenjs/physics3d'
const BallPrefab = definePrefab([{ def: Position, defaults: { x: 0, y: 0, z: 0 } }])
export const BallActor = defineActor(BallPrefab, () => {
const body = useDynamicBody({ mass: 2, ccdEnabled: true })
useSphereCollider({ radius: 0.5 })
onContact((contact) => {
console.log('Vitesse d\'impact :', contact.relativeVelocity)
})
onUpdate(() => {
// Update ball logic each frame
})
})Le plugin fait automatiquement :
- Enregistre le corps avec la simulation physique 3D
- Synchronise les formes de collider et les transformations
- Envoie les événements de collision
- Gère les raycasts, les shape casts et les chevauchements par image
- Nettoie quand l'acteur disparaît
Corps
Chaque acteur a besoin d'exactement un composable de corps :
| Composable | Cas d'usage |
|---|---|
useDynamicBody(opts?) | Entièrement simulé : gravité, forces, collisions. Personnages, véhicules, objets interactifs. |
useKinematicBody(opts?) | Piloté manuellement : se déplace sur commande, ne répond pas aux forces. Ascenseurs, portes coulissantes, plateformes mobiles. |
useStaticBody() | Ne bouge jamais : terrain, murs, structures immobiles. |
// Caisse en chute 3D
const CrateActor = defineActor(CratePrefab, () => {
useDynamicBody({ mass: 10, linearDamping: 0.2 })
useBoxCollider({ w: 1, h: 1, d: 1 })
})
// Ascenseur se déplaçant sur une piste
const ElevatorActor = defineActor(ElevatorPrefab, () => {
const body = useKinematicBody()
useBoxCollider({ w: 4, h: 0.5, d: 4 })
let elapsed = 0
onUpdate((dt) => {
elapsed += dt
const y = Math.sin(elapsed) * 5
body.moveTo(0, y, 0)
})
})
// Terrain (peut utiliser des colliders de mesh pour l'efficacité)
const TerrainActor = defineActor(TerrainPrefab, () => {
useStaticBody()
useMeshCollider({ vertices: terrainVerts, indices: terrainIndices })
})Colliders
Ajoutez des formes de collision avec des composables collider. Un acteur peut avoir plusieurs colliders :
| Composable | Forme | Meilleur pour |
|---|---|---|
useBoxCollider(opts) | Boîte alignée sur les axes | Caisses, bâtiments, structures simples |
useSphereCollider(opts) | Sphère | Balles, explosions, objets ronds |
useCapsuleCollider(opts) | Capsule (cylindre arrondi) | Personnages, mouvement lisse |
useConvexCollider(opts) | Enveloppe convexe des sommets | Formes irrégulières (rochers, astéroïdes) |
useCompoundCollider(opts) | Plusieurs formes combinées | Objets complexes (robots, véhicules) |
useMeshCollider(opts) | Mesh triangulaire (concave) | Terrain, environnement (corps statiques uniquement) |
useHeightfieldCollider(opts) | Grille d'hauteur | Terrain à partir de heightmaps |
// Personnage avec capsule
const CharacterActor = defineActor(CharacterPrefab, () => {
useDynamicBody({ mass: 1 })
useCapsuleCollider({ radius: 0.4, length: 1.8 })
})
// Astéroïde avec enveloppe convexe
const AsteroidActor = defineActor(AsteroidPrefab, () => {
useDynamicBody({ mass: 5 })
useConvexCollider({
vertices: asteroidVertices,
offsetX: 0, offsetY: 0, offsetZ: 0
})
})
// Robot avec collider composé (tête + corps + jambes)
const RobotActor = defineActor(RobotPrefab, () => {
useDynamicBody({ mass: 50 })
useCompoundCollider({
shapes: [
{ type: 'box', w: 1, h: 2, d: 1, offsetY: 0.5 }, // corps
{ type: 'sphere', radius: 0.5, offsetY: 2 }, // tête
{ type: 'capsule', radius: 0.2, length: 1, offsetY: -0.5 } // jambes
]
})
})
// Terrain avec collider de mesh (BVH préchargé pour l'efficacité)
const TerrainActor = defineActor(TerrainPrefab, () => {
useStaticBody()
const mesh = useMeshCollider('./terrain.glb')
ready.then(() => console.log('Terrain collider loaded'))
})Options des colliders
Tous les colliders acceptent :
offsetX?: number,offsetY?: number,offsetZ?: number— Décalage de position localeisSensor?: boolean— Événements de chevauchement uniquement, pas de réponse physiquelayer?: number— Masque de couche d'appartenancemask?: number— Masque de filtre de collision
Collider de boîte :
w: number— Largeur (X)h: number— Hauteur (Y)d: number— Profondeur (Z)
Sphère et capsule :
radius: number— Rayon
Capsule :
length: number— Longueur de la section cylindrique
Collider convexe :
vertices: Float32Array— Positions des sommets [x, y, z, x, y, z, ...]
Collider de mesh (concave) :
vertices?: Float32Array— Positions des sommetsindices?: Uint32Array— Indices de triangles__bvhUrl?: string— URL du BVH pré-balisé (asynchrone, recommandé)
Collider heightfield :
heights: Float32Array— Valeurs d'hauteur dans une grillescale: Vec3— Échelle du heightfield en X, Y, Z
Événements
Événements de contact
onContact((contact) => {
console.log('Entités :', contact.entityA, contact.entityB)
console.log('Vitesse :', contact.relativeVelocity)
console.log('Point :', contact.contactX, contact.contactY, contact.contactZ)
console.log('Normale :', contact.normalX, contact.normalY, contact.normalZ)
})Objet contact :
entityA— ID de la première entité participanteentityB— ID de la deuxième entité participantecontactX,contactY,contactZ— Coordonnées du point de contact en espace mondenormalX,normalY,normalZ— Composantes de la normale de contact (vecteur unitaire de B vers A)relativeVelocity— Vitesse d'impact relative au point de contact (m/s)restitution— Coefficient de restitution effectif au point de contact
Événements des capteurs
const sensor = useBoxCollider({ w: 2, h: 2, d: 2, isSensor: true })
onSensorEnter(sensor.colliderId, (entityId) => {
console.log('Entity entered:', entityId)
})
onSensorExit(sensor.colliderId, (entityId) => {
console.log('Entity left:', entityId)
})Requêtes
Raycasts
Lancez des rayons pour la détection de coups (détection du sol, ligne de vue, etc.) :
const groundRay = useRaycast({
origin: () => ({ x: player.x, y: player.y + 0.1, z: player.z }),
direction: { x: 0, y: -1, z: 0 },
maxDist: 0.5,
layer: Layers.player,
mask: Layers.terrain
})
onUpdate(() => {
if (groundRay.hit) {
console.log('On ground, distance:', groundRay.distance)
console.log('Hit point:', groundRay.point)
}
// Appeler dispose() quand terminé (si le raycast était temporaire)
// groundRay.dispose()
})Options du raycast :
origin?: () => Vec3— Fonction d'origine (mise à jour à chaque image)direction: Vec3— Direction du rayon (normalisée)maxDist: number— Distance maximale à rechercherlayer?: number— Couche d'appartenancemask?: number— Masque de filtresolid?: boolean— Ignorer les capteurs (défaut: false)
Handle du raycast :
get hit(): boolean— Si le rayon a touché quelque choseget entity(): bigint | null— ID de l'entité touchéeget distance(): number— Distance au point d'impactget point(): Vec3— Point d'impact dans l'espace du mondeget normal(): Vec3— Normale de surface au point d'impactdispose(): void— Désenregistrer le slot du raycast
Shape Casts
Lancez une forme (boîte, sphère, capsule) pour vérifier les collisions le long d'un chemin :
const sweep = useShapeCast({
shape: { type: 'sphere', radius: 0.5 },
origin: { x: 0, y: 1, z: 0 },
direction: { x: 1, y: 0, z: 0 },
maxDist: 10,
mask: Layers.terrain | Layers.enemy
})
onUpdate(() => {
if (sweep.hit) {
console.log('Obstacle ahead at distance:', sweep.distance)
}
})Chevauchements
Vérifiez ce qui chevauche actuellement une forme :
const explosionZone = useOverlap({
shape: { type: 'sphere', radius: 5 },
position: explosionPos,
layer: Layers.projectile,
mask: Layers.enemy | Layers.player
})
onUpdate(() => {
explosionZone.entities.forEach((entityId) => {
console.log('Entity in blast radius:', entityId)
})
})Articulations
Connectez deux corps avec des contraintes physiques :
const anchorBody = useDynamicBody()
const swingBody = useDynamicBody()
useJoint({
bodyA: anchorBody.bodyId,
bodyB: swingBody.bodyId,
type: 'revolute', // revolute, spherical, prismatic, fixed, rope, etc.
anchorA: { x: 0, y: 1, z: 0 },
anchorB: { x: 0, y: -1, z: 0 },
limits: { min: -Math.PI / 2, max: Math.PI / 2 }
})Types d'articulations :
'fixed'— Connecter rigidement deux corps'revolute'— Articulation à charnière (rotation autour d'un axe)'spherical'— Articulation sphérique (rotation libre)'prismatic'— Articulation curseur (mouvement linéaire)'rope'— Contrainte de distance- etc.
Couches de collision
Utilisez les couches pour contrôler quels objets entrent en collision :
import { defineLayers } from '@gwenjs/physics3d'
export const Layers = defineLayers({
player: 1 << 0,
enemy: 1 << 1,
terrain: 1 << 2,
projectile: 1 << 3,
debris: 1 << 4,
})
// Le joueur entre en collision avec le terrain uniquement
const PlayerActor = defineActor(PlayerPrefab, () => {
useDynamicBody()
useCapsuleCollider({
radius: 0.4, length: 1.8,
layer: Layers.player,
mask: Layers.terrain
})
})
// Le projectile entre en collision avec tout sauf les autres projectiles
const ProjectileActor = defineActor(ProjectilePrefab, () => {
useDynamicBody()
useSphereCollider({
radius: 0.1,
layer: Layers.projectile,
mask: Layers.player | Layers.enemy | Layers.terrain | Layers.debris
})
})En pratique
Contrôleur de personnage 3D
Un exemple complet : personnage avec gravité, détection du sol via raycast, et saut.
import { defineActor, useComponent, useService } from '@gwenjs/core/actor'
import { onUpdate } from '@gwenjs/core/system'
import { useDynamicBody, useCapsuleCollider, useRaycast } from '@gwenjs/physics3d'
import { Position } from './components'
import { Layers } from './layers'
export const PlayerActor = defineActor(PlayerPrefab, () => {
// Lire la position de l'entité chaque frame (physics3d écrit dans ce composant)
const pos = useComponent<{ x: number; y: number; z: number }>(Position)
// input vient de votre plugin d'entrée (enregistré via engine.provide)
const input = useService('input')
const body = useDynamicBody({ mass: 1, gravityScale: 2, linearDamping: 0.1 })
useCapsuleCollider({
radius: 0.4,
height: 1.8,
offsetY: 0.9,
layer: Layers.player,
mask: Layers.terrain | Layers.enemy,
})
// Détection du sol : l'origine suit les pieds de l'entité chaque frame
const groundRay = useRaycast({
origin: () => ({ x: pos.x, y: pos.y - 0.9, z: pos.z }),
direction: { x: 0, y: -1, z: 0 },
maxDist: 0.1,
mask: Layers.terrain,
})
onUpdate((dt) => {
const grounded = groundRay.hit
const forward = input.axis('forward') ?? 0 // W/S
const right = input.axis('right') ?? 0 // A/D
const vel = body.velocity
body.setVelocity(right * 5, vel.y, forward * 10)
if (input.justPressed('Space') && grounded) {
body.applyImpulse(0, 10, 0)
}
})
})Colliders de mesh avec BVH
Pour un terrain complexe, utilisez un BVH pré-balisé pour l'efficacité :
// gwen.config.ts — activer le pré-baking BVH via la sous-clé vite du module
export default defineConfig({
modules: [
['@gwenjs/physics3d', { vite: { bvhPrebake: true } }],
],
})
// Dans l'acteur — le plugin Vite remplace le chemin par un handle BVH pré-compilé
const TerrainActor = defineActor(TerrainPrefab, () => {
useStaticBody()
useMeshCollider('./models/terrain.glb') // chemin remplacé par le plugin Vite
})Sous le capot
Options du corps
Corps dynamique :
mass?: number— Masse en kg (défaut: 1)gravityScale?: number— Multiplicateur de gravité (défaut: 1)linearDamping?: number— Amortissement de la vélocité (défaut: 0.1)angularDamping?: number— Amortissement de la rotation (défaut: 0.1)ccdEnabled?: boolean— Détection de collision continue (défaut: false)fixedRotation?: boolean— Verrouiller la rotation (défaut: false)initialPosition?: Vec3— Position initialeinitialRotation?: Quat— Rotation initiale (quaternion)initialLinearVelocity?: Vec3— Vélocité initialeinitialAngularVelocity?: Vec3— Vélocité angulaire initialequality?: 'fast' | 'medium' | 'high'— Qualité du solveur (défaut: 'medium')
Corps cinématique :
initialPosition?: Vec3initialRotation?: Quat
Application des forces
Les corps dynamiques ont plusieurs façons d'appliquer des forces :
const body = useDynamicBody()
// Définir la vélocité directement (m/s)
body.setVelocity(5, 0, 0)
// Appliquer une impulsion (N·s) — changement de vélocité instantané
body.applyImpulse(0, 10, 0)
// Appliquer une force (N) — continue
body.applyForce(0, 20, 0)
// Appliquer un couple (N·m) — force rotationnelle
body.applyTorque(1, 0, 0)
// Vélocité actuelle et vélocité angulaire
const vel = body.velocity
const angVel = body.angularVelocityPréchargement des colliders de mesh
Pour les grands colliders de mesh, préchargez le BVH pour éviter les saccades pendant le jeu :
import { preloadMeshCollider } from '@gwenjs/physics3d'
// Lors de l'initialisation de l'application :
const terrainBvh = await preloadMeshCollider('./models/terrain.glb')
// Plus tard, dans un acteur :
const TerrainActor = defineActor(TerrainPrefab, () => {
useStaticBody()
useMeshCollider(terrainBvh) // Pas d'attente asynchrone nécessaire
})Résumé de l'API
Composables
| Fonction | Retours | Objectif |
|---|---|---|
useStaticBody() | void | Corps statique (immobile). |
useDynamicBody(opts?) | DynamicBodyHandle3D | Corps entièrement simulé. |
useKinematicBody(opts?) | KinematicBodyHandle3D | Corps piloté manuellement. |
useBoxCollider(opts) | BoxColliderHandle3D | Collider en forme de boîte. |
useSphereCollider(opts) | SphereColliderHandle3D | Collider de sphère. |
useCapsuleCollider(opts) | CapsuleColliderHandle3D | Collider en capsule. |
useConvexCollider(opts) | ConvexColliderHandle3D | Collider enveloppe convexe. |
useCompoundCollider(opts) | CompoundColliderHandle3D | Plusieurs formes combinées. |
useMeshCollider(opts) | MeshColliderHandle3D | Collider de mesh triangulaire. |
useHeightfieldCollider(opts) | HeightfieldColliderHandle3D | Collider heightfield. |
useRaycast(opts) | UseRaycastHandle | Requête raycast par image. |
useShapeCast(opts) | UseShapeCastHandle | Balayage de forme par image. |
useOverlap(opts) | UseOverlapHandle | Vérification de chevauchement par image. |
useJoint(opts) | UseJointHandle | Articulation de contrainte physique. |
defineLayers(def) | Record<string, number> | Couches de collision nommées. |
Gestionnaires d'événements
| Fonction | Signature du callback | Objectif |
|---|---|---|
onContact(callback) | (contact: ContactEvent3D) => void | Événement de collision. |
onSensorEnter(sensorId, cb) | (entityId: bigint) => void | Entrée de capteur. |
onSensorExit(sensorId, cb) | (entityId: bigint) => void | Sortie de capteur. |
Méthodes du handle de corps
DynamicBodyHandle3D :
get velocity(): Vec3— Vélocité linéaire actuelle.get angularVelocity(): Vec3— Vélocité angulaire actuelle.setVelocity(vx, vy, vz): void— Définir la vélocité.applyForce(fx, fy, fz): void— Appliquer une force continue.applyImpulse(ix, iy, iz): void— Appliquer une impulsion instantanée.applyTorque(tx, ty, tz): void— Appliquer une force rotationnelle.enable(): void— Réactiver si désactivé.disable(): void— Supprimer de la simulation.get active(): boolean— Si le corps est actif.get bodyId(): number— Identifiant unique.
KinematicBodyHandle3D : Similaire, mais sans les méthodes force/impulsion/torque.
Types
Vec3—{ x: number, y: number, z: number }ContactEvent3D—{ entityA: bigint, entityB: bigint, contactX: number, contactY: number, contactZ: number, normalX: number, normalY: number, normalZ: number, relativeVelocity: number, restitution: number }- Diverses handles de collider avec
colliderId,isSensor,remove(), etc. UseRaycastHandle— Propriétés de hit etdispose()UseShapeCastHandle— Similaire au raycastUseOverlapHandle—entities: bigint[]UseJointHandle—remove(): void