Skip to content

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 :

ts
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 :

ComposableCas 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.
ts
// 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 :

ComposableFormeMeilleur pour
useBoxCollider(opts)Boîte alignée sur les axesCaisses, bâtiments, structures simples
useSphereCollider(opts)SphèreBalles, explosions, objets ronds
useCapsuleCollider(opts)Capsule (cylindre arrondi)Personnages, mouvement lisse
useConvexCollider(opts)Enveloppe convexe des sommetsFormes irrégulières (rochers, astéroïdes)
useCompoundCollider(opts)Plusieurs formes combinéesObjets complexes (robots, véhicules)
useMeshCollider(opts)Mesh triangulaire (concave)Terrain, environnement (corps statiques uniquement)
useHeightfieldCollider(opts)Grille d'hauteurTerrain à partir de heightmaps
ts
// 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 locale
  • isSensor?: boolean — Événements de chevauchement uniquement, pas de réponse physique
  • layer?: number — Masque de couche d'appartenance
  • mask?: 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 sommets
  • indices?: Uint32Array — Indices de triangles
  • __bvhUrl?: string — URL du BVH pré-balisé (asynchrone, recommandé)

Collider heightfield :

  • heights: Float32Array — Valeurs d'hauteur dans une grille
  • scale: Vec3 — Échelle du heightfield en X, Y, Z

Événements

Événements de contact

ts
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é participante
  • entityB — ID de la deuxième entité participante
  • contactX, contactY, contactZ — Coordonnées du point de contact en espace monde
  • normalX, 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

ts
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.) :

ts
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 à rechercher
  • layer?: number — Couche d'appartenance
  • mask?: number — Masque de filtre
  • solid?: boolean — Ignorer les capteurs (défaut: false)

Handle du raycast :

  • get hit(): boolean — Si le rayon a touché quelque chose
  • get entity(): bigint | null — ID de l'entité touchée
  • get distance(): number — Distance au point d'impact
  • get point(): Vec3 — Point d'impact dans l'espace du monde
  • get normal(): Vec3 — Normale de surface au point d'impact
  • dispose(): 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 :

ts
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 :

ts
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 :

ts
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 :

ts
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.

ts
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é :

ts
// 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 initiale
  • initialRotation?: Quat — Rotation initiale (quaternion)
  • initialLinearVelocity?: Vec3 — Vélocité initiale
  • initialAngularVelocity?: Vec3 — Vélocité angulaire initiale
  • quality?: 'fast' | 'medium' | 'high' — Qualité du solveur (défaut: 'medium')

Corps cinématique :

  • initialPosition?: Vec3
  • initialRotation?: Quat

Application des forces

Les corps dynamiques ont plusieurs façons d'appliquer des forces :

ts
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.angularVelocity

Préchargement des colliders de mesh

Pour les grands colliders de mesh, préchargez le BVH pour éviter les saccades pendant le jeu :

ts
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

FonctionRetoursObjectif
useStaticBody()voidCorps statique (immobile).
useDynamicBody(opts?)DynamicBodyHandle3DCorps entièrement simulé.
useKinematicBody(opts?)KinematicBodyHandle3DCorps piloté manuellement.
useBoxCollider(opts)BoxColliderHandle3DCollider en forme de boîte.
useSphereCollider(opts)SphereColliderHandle3DCollider de sphère.
useCapsuleCollider(opts)CapsuleColliderHandle3DCollider en capsule.
useConvexCollider(opts)ConvexColliderHandle3DCollider enveloppe convexe.
useCompoundCollider(opts)CompoundColliderHandle3DPlusieurs formes combinées.
useMeshCollider(opts)MeshColliderHandle3DCollider de mesh triangulaire.
useHeightfieldCollider(opts)HeightfieldColliderHandle3DCollider heightfield.
useRaycast(opts)UseRaycastHandleRequête raycast par image.
useShapeCast(opts)UseShapeCastHandleBalayage de forme par image.
useOverlap(opts)UseOverlapHandleVérification de chevauchement par image.
useJoint(opts)UseJointHandleArticulation de contrainte physique.
defineLayers(def)Record<string, number>Couches de collision nommées.

Gestionnaires d'événements

FonctionSignature du callbackObjectif
onContact(callback)(contact: ContactEvent3D) => voidÉvénement de collision.
onSensorEnter(sensorId, cb)(entityId: bigint) => voidEntrée de capteur.
onSensorExit(sensorId, cb)(entityId: bigint) => voidSortie 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 et dispose()
  • UseShapeCastHandle — Similaire au raycast
  • UseOverlapHandleentities: bigint[]
  • UseJointHandleremove(): void