Système solaire
Trois planètes (rocheuse + métallique + gazeuse) en orbite autour d'une étoile centrale, partageant une seule PointLight. La lib n'a pas de mécanique orbitale — la position monde des corps est entièrement caller-driven.
Pattern d'orbite simple
// Source de lumière unique : la même PointLight illumine la scène (cœur
// molten, anneaux) ET pilote la direction soleil → planète dans le shader
// de chaque corps via l'option `sunLight`.
const sun = new THREE.PointLight(0xfff1cc, 4.5, 0, 0)
sun.position.set(0, 0, 0)
scene.add(sun)
const planets = [
{ body: useBody(rockyConfig, DEFAULT_TILE_SIZE, { sunLight: sun }), orbitRadius: 4.5, orbitSpeed: 0.30, phase: 0 },
{ body: useBody(metalConfig, DEFAULT_TILE_SIZE, { sunLight: sun }), orbitRadius: 7.0, orbitSpeed: 0.18, phase: 1.5 },
{ body: useBody(gasConfig, DEFAULT_TILE_SIZE, { sunLight: sun }), orbitRadius: 11, orbitSpeed: 0.10, phase: 3.2 },
]
planets.forEach(p => scene.add(p.body.group))
// par frame :
const angle = phase + elapsed * orbitSpeed
p.body.group.position.set(
Math.cos(angle) * orbitRadius,
0,
Math.sin(angle) * orbitRadius,
)
p.body.tick(dt)C'est minimaliste : pas d'ellipse, pas d'inclinaison orbitale, pas de précession. Pour un simulateur sérieux, branchez votre solveur (Kepler, n-body) et écrivez position chaque frame.
Comment l'étoile éclaire les planètes
Le matériau des corps est un THREE.ShaderMaterial custom — il n'écoute pas les lumières de scène standard (pas de calcul MeshStandardMaterial). À la place, chaque corps maintient son propre uniform uLightDir.
Pour pondre cette direction sans dupliquer la source de vérité, useBody accepte une option sunLight: THREE.PointLight | THREE.DirectionalLight. À chaque body.tick(dt), la lib lit sunLight.getWorldPosition(), calcule le vecteur normalisé planète → soleil, et met à jour le shader (corps + atmosphère + anneaux). Une seule lumière scène pilote toute la scène — pas de Vector3 séparé à synchroniser.
L'étoile, elle, n'a pas besoin d'être éclairée : son shader est émissif (auto-lumineux). On peut donc placer la PointLight au centre de la sphère étoile sans risque — le shader émissif ignore la scène lights, et les corps qui l'écoutent (via sunLight) lisent juste sa position.
Avec <Body> et pose
La version Vue/TresJS utilise pose.position pour piloter la position depuis l'extérieur :
<Body :body="planetBody" :pose="{ position }" />Quand pose est passé, <Body> désactive son animation interne de position et applique verbatim votre vecteur. Adapté pour : tick serveur, replay, scrubbing UI.
Performance
Trois corps animés ≈ 3 × body.tick(dt) par frame. Pour 50+ corps :
- baissez
tileSize(ex.0.15) sur les corps lointains, - mettez les corps statiques en pause (
paused: true) — pas d'appel àtick, - passez les corps non-focus en mode
'shader'(body.view.set('shader')) — pas de mesh hex, pas de BVH (cf. Mode jouable), - regroupez les corps identiques en
THREE.InstancedMesh(cf. Performance).
Raycasting multi-corps
Pour détecter quel corps l'utilisateur survole dans une scène avec plusieurs planètes, la lib expose raycastBodies — un raycast filtré qui élimine automatiquement les hits derrière le corps focus :
import { raycastBodies, findBodyIndex } from '@cedric-pouilleux/stellexjs/core'
const raycaster = new THREE.Raycaster()
raycaster.setFromCamera(pointer, camera)
const bodies = planets.map(p => ({ group: p.body.group, config: p.body.config }))
const hit = raycastBodies(raycaster, bodies, { focusedIndex })
if (hit) {
console.log(`Survol du corps #${hit.bodyIndex}`)
}Filtres appliqués :
- Distance > rayon du body → écarté (évite les false hits sur les bords du mesh).
- Body focus → écarté en candidat et utilisé comme occluder sphère — un hit sur un satellite caché derrière le focus est ignoré.
findBodyIndex(obj, bodies) remonte le scene graph pour identifier le body owner d'un objet arbitraire — utile quand vous gérez votre propre raycast et voulez juste résoudre l'index.