尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
TL;DR — Visualisation 3D en temps réel de mon appartement, construite depuis zéro (~300 commits). Démarré comme un prototype LEGO en fichier unique, évolué vers ES Modules + Three.js, puis migré vers React Three Fiber. Projet personnel et portfolio technique documenté commit par commit. Un outil que j'utilise vraiment au quotidien : planifier les positions des meubles, visualiser l'éclairage, gérer l'inventaire de mes affaires. Et en même temps, un terrain d'expérimentation technique documenté commit par commit.查看完整 README ↗
TL;DR — Visualisation 3D en temps réel de mon appartement, construite depuis zéro (~300 commits). Démarré comme un prototype LEGO en fichier unique, évolué vers ES Modules + Three.js, puis migré vers React Three Fiber. Projet personnel et portfolio technique documenté commit par commit.
Un outil que j'utilise vraiment au quotidien : planifier les positions des meubles, visualiser l'éclairage, gérer l'inventaire de mes affaires. Et en même temps, un terrain d'expérimentation technique documenté commit par commit.
| Couche | Technologie | Pourquoi |
|---|---|---|
| Rendu 3D | React Three Fiber + Three.js r170 | Composants déclaratifs, state React, interactivité propre |
| Build | Vite + TypeScript | TSX, HMR, typage |
| Modèles 3D | GLB + Draco | Compression ×3, chargement GLTF standard |
| (archive) | ES Modules natifs + importmap | Version vanilla conservée dans js/ |
room-3d/
├── index.html # Point d'entrée Vite
├── vite.config.ts # Config Vite, aliases TS, build
├── tsconfig.json # TypeScript strict + paths
├── src/
│ ├── main.tsx # Monte <Studio /> et configure Draco
│ ├── types.ts # Interfaces communes Item / SceneItemProps
│ ├── index.css # Reset global minimal
│ └── features/
│ ├── scene/
│ │ ├── Studio.tsx # Racine R3F : Canvas, lights, état UI global
│ │ ├── Building.tsx # Murs, sol, portes, miroirs
│ │ ├── Placements.tsx # Placement des objets dans l'appartement
│ │ ├── CameraController.tsx
│ │ ├── Minimap.tsx
│ │ ├── FloorPlan.tsx
│ │ ├── HoverMenu.tsx
│ │ ├── RenderStyleLayer.tsx
│ │ ├── items/ # Composants autonomes par objet
│ │ ├── floorData.ts # Données partagées plan/minimap
│ │ ├── wallData.ts
│ │ └── config.ts # Dimensions, layers, constantes scène
│ └── inventory/
│ ├── Inventory.tsx
│ ├── InventoryPreview.tsx
│ ├── inventoryData.ts # Objets, catégories, espaces de rangement
│ └── previewRegistry.tsx # id → composant de preview
├── media/ # Modèles GLB utilisés par la scène
└── public/
└── draco/ # Décodeurs Draco servis statiquement
Diagramme interactif généré par GitDiagram — cliquez les nœuds pour accéder au code source.
flowchart TD
subgraph group_experience["Expérience 3D"]
node_entry["Point d'entrée<br/>[main.tsx]"]
node_studio["Studio 3D<br/>[Studio.tsx]"]
end
subgraph group_room["Scène appartement"]
node_scene["Composition pièce<br/>[Building.tsx]"]
node_placements["Placement meubles<br/>[Placements.tsx]"]
node_itemmodels["Modèles meubles"]
node_floorplan["Plan 2D<br/>[FloorPlan.tsx]"]
node_lighting["Éclairage<br/>[SunLight.tsx]"]
end
subgraph group_interaction["Caméra et contrôles"]
node_camera["Contrôleur caméra"]
node_camstate["État caméra<br/>[cameraState.ts]"]
node_scenecontrols["État scène<br/>[useSceneStore.ts]"]
node_minimap["Minimap<br/>[Minimap.tsx]"]
end
subgraph group_characters["Personnages et agents"]
node_agentcontroller["Contrôleur agent"]
node_smartobjects["Objets intelligents"]
node_occupancy["Occupation slots"]
node_duosessions["Sessions duo"]
node_navigation["Graphe navigation<br/>[navigationGraph.ts]"]
node_animations["Résolution animations"]
node_animationdata["Registre animations"]
node_duoanimations["Animations duo<br/>[duoAnimations.ts]"]
end
subgraph group_inventory["Inventaire"]
node_inventory["Navigateur inventaire<br/>[Inventory.tsx]"]
node_inventorydata["Données inventaire<br/>[inventoryData.ts]"]
node_preview["Prévisualisation"]
end
node_user(("Planificateur"))
node_user -->|"ouvre l'app"| node_entry
node_entry -->|"monte"| node_studio
node_studio -->|"affiche la pièce"| node_scene
node_studio -->|"affiche les placements"| node_placements
node_placements -->|"place les modèles"| node_itemmodels
node_studio -->|"affiche le plan"| node_floorplan
node_studio -->|"affiche l'éclairage"| node_lighting
node_studio -->|"active la navigation"| node_camera
node_camera -->|"lit l'état"| node_scenecontrols
node_camera -->|"met à jour la caméra"| node_camstate
node_camera -->|"partage la position"| node_minimap
node_studio -->|"propose l'inventaire"| node_inventory
node_inventory -->|"parcourt les items"| node_inventorydata
node_inventory -->|"prévisualise"| node_preview
node_agentcontroller -->|"résout les clips"| node_animations
node_agentcontroller -->|"utilise les affordances"| node_smartobjects
node_agentcontroller -->|"réserve des slots"| node_occupancy
node_agentcontroller -->|"coordonne les duos"| node_duosessions
node_agentcontroller -->|"utilise les chemins"| node_navigation
node_animations -->|"résout les définitions"| node_animationdata
node_animations -->|"résout les clips duo"| node_duoanimations
node_duosessions -->|"vérifie les réservations"| node_occupancy
node_duosessions -->|"cherche les objets"| node_smartobjects
click node_entry "https://github.com/dinatih/room-3d/blob/master/src/main.tsx"
click node_studio "https://github.com/dinatih/room-3d/blob/master/src/features/scene/Studio.tsx"
click node_scene "https://github.com/dinatih/room-3d/blob/master/src/features/scene/Building.tsx"
click node_placements "https://github.com/dinatih/room-3d/blob/master/src/features/scene/Placements.tsx"
click node_itemmodels "https://github.com/dinatih/room-3d/tree/master/src/features/scene/items"
click node_floorplan "https://github.com/dinatih/room-3d/blob/master/src/features/scene/FloorPlan.tsx"
click node_lighting "https://github.com/dinatih/room-3d/blob/master/src/features/scene/SunLight.tsx"
click node_camera "https://github.com/dinatih/room-3d/blob/master/src/features/scene/CameraController.tsx"
click node_camstate "https://github.com/dinatih/room-3d/blob/master/src/features/scene/cameraState.ts"
click node_scenecontrols "https://github.com/dinatih/room-3d/blob/master/src/features/scene/store/useSceneStore.ts"
click node_minimap "https://github.com/dinatih/room-3d/blob/master/src/features/scene/Minimap.tsx"
click node_inventory "https://github.com/dinatih/room-3d/blob/master/src/features/inventory/Inventory.tsx"
click node_inventorydata "https://github.com/dinatih/room-3d/blob/master/src/features/inventory/inventoryData.ts"
click node_preview "https://github.com/dinatih/room-3d/blob/master/src/features/inventory/InventoryPreview.tsx"
click node_agentcontroller "https://github.com/dinatih/room-3d/blob/master/src/features/scene/ai/useAgentController.ts"
click node_smartobjects "https://github.com/dinatih/room-3d/blob/master/src/features/scene/ai/smartObjectRegistry.ts"
click node_occupancy "https://github.com/dinatih/room-3d/blob/master/src/features/scene/ai/occupancyManager.ts"
click node_duosessions "https://github.com/dinatih/room-3d/blob/master/src/features/scene/ai/duoSessionManager.ts"
click node_navigation "https://github.com/dinatih/room-3d/blob/master/src/features/scene/ai/navigationGraph.ts"
click node_animations "https://github.com/dinatih/room-3d/blob/master/src/features/scene/animations/animationResolver.ts"
click node_animationdata "https://github.com/dinatih/room-3d/blob/master/src/features/scene/animations/animationRegistry.ts"
click node_duoanimations "https://github.com/dinatih/room-3d/blob/master/src/features/scene/animations/duoAnimations.ts"
classDef toneBlue fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#172554
classDef toneAmber fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
classDef toneMint fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
classDef toneRose fill:#ffe4e6,stroke:#e11d48,stroke-width:1.5px,color:#881337
classDef toneIndigo fill:#e0e7ff,stroke:#4f46e5,stroke-width:1.5px,color:#312e81
class node_entry,node_studio toneBlue
class node_scene,node_placements,node_itemmodels,node_floorplan,node_lighting toneAmber
class node_camera,node_camstate,node_scenecontrols,node_minimap toneMint
class node_agentcontroller,node_smartobjects,node_occupancy,node_duosessions,node_navigation,node_animations,node_animationdata,node_duoanimations toneRose
class node_inventory,node_inventorydata,node_preview,node_user toneIndigo
npm install
npm run dev # http://localhost:5173
npm run build # build de production dans dist/
npm run visualize # lance repo-visualizer localement
Le vrai README. Pas une liste de features : le pourquoi de chaque virage.
Commit initial : un seul fichier lego-room.html de ~1 700 lignes. L'idée de départ était de représenter l'appartement avec des briques LEGO — chaque brique = un InstancedMesh, 1 unité = 1 stud (1,6 cm). Animation brique par brique, couches par matériau.
Pourquoi LEGO ? C'est visuellement clair pour planifier un espace — les briques donnent une échelle intuitive, comme du papier quadrillé en 3D.
Limite vite atteinte : ajouter une ouverture de porte impliquait de recalculer toute la géométrie brick par brick. Ajouter un mur diagonal (l'appartement en a un) devenait ingérable.
819f0c3 Refactor: split monolithic lego-room.html into 17 ES modules
Décision : découper le fichier unique en ES Modules natifs, sans bundler.
Pourquoi pas Vite/webpack tout de suite ? Zéro friction. Un importmap + un serveur HTTP local suffisent. Les modules se rechargent directement dans le navigateur. Pour un projet solo d'expérimentation, pas de build step = pas de blocage mental.
Résultat : js/structure/, js/furniture/, js/decor/, js/ui/ — chaque module exporte une fonction buildXxx(scene). Convention simple, facile à tenir sur la durée.
ad83213 Remove LEGO brick system: replace all walls with solid BoxGeometry panels
Pourquoi ? Après avoir ajouté une niche dans le mur, une porte-fenêtre, un couloir en diagonale — le système de briques était devenu une contrainte. Recalculer quelles briques enlever pour chaque ouverture était une source constante de bugs.
Remplacement : panneaux BoxGeometry solides via une fonction locale panel(). Plus flexibles, plus rapides à rendre. L'esthétique "LEGO" devient des studs décoratifs InstancedMesh par-dessus — séparation nette entre géométrie structurelle et apparence.
Ce que ça m'a appris : une abstraction qui semble bonne au départ peut devenir un obstacle quand le scope grandit. Mieux vaut la remplacer tôt que de l'accumuler.
Le projet accumule des objets GLB (meubles IKEA, vêtements, scooter). Deux problèmes émergent :
Problème 1 — Taille : les GLBs bruts totalisent ~54 Mo.
6a483c9 Draco compress 10 GLBs + shared DRACOLoader (54 MB → 16 MB)
Solution : compression Draco via gltf-pipeline. Ratio ×3 sur la géométrie.
Problème 2 — Draw calls : 500+ calls pour une scène normale.
0967a98 Merge GLB sub-meshes by material to reduce draw calls
Solution : mergeGlbByMaterial() dans utils/mergeUtils.js — fusionne toutes les géométries partageant le même matériau. Passe de ~547 à ~200 draw calls.
Ce que ça m'a appris : le panel devtools.js (FPS + draw calls en temps réel, intégré dans la scène) a été indispensable pour mesurer avant/après. Optimiser sans mesurer = improviser.
Canvas 2D superposé, avec suivi du personnage (position + orientation), mode plein écran avec mise à l'échelle dynamique, plan annoté.
La difficulté : toutes les dimensions du canvas (épaisseur des lignes, taille des icônes, polices) doivent scaler quand on passe en plein écran. Solution : facteur scale = canvas.width / SMALL_W, appliqué à chaque appel de rendu.
553baad Fix hover menu showing through opaque obstacles
Le menu contextuel apparaissait sur des objets cachés derrière un mur. Fix : raycast sur toute la scène (pas seulement les objets interactifs), s'arrêter au premier hit opaque.
const allHits = raycaster.intersectObjects(scene.children, true);
for (const hit of allHits) {
if (isTransparent(hit.object.material)) continue;
if (hit.object.userData.brickType === 'ceiling') continue; // vue du dessus
const action = resolveAction(hit.object);
if (action) showMenu(action);
break; // premier hit opaque = fin du rayon
}
2b48e7f Factorize floor plan data into floorData.js shared by minimap and floorplan
floorplan.js et minimap.js avaient chacun leurs coordonnées hardcodées pour les mêmes murs. Solution : ROOMS, WALL_LABELS, DIMENSIONS centralisés dans floorData.js. Une seule source de vérité, les deux modules importent et itèrent.
cafe5a6 Add 0.5cm gap between neighbors and main apartment to fix z-fighting
Quand deux faces sont exactement coplanaires, le GPU scintille. Solution : décaler les appartements voisins de 0,5 cm. Imperceptible visuellement, élimine le problème.
_groupWest.position.x = -ROOM_W - 30.5; // était -30
Le constat : la preview 3D de l'inventaire gère manuellement un WebGLRenderer, une boucle requestAnimationFrame, des contrôles orbitaux maison, des event listeners sur le canvas. C'est ~120 lignes de code impératif pour afficher un objet qui tourne.
Avec R3F :
// SceneContent.tsx — ce qui remplace les 120 lignes
export function SceneContent({ item, actionState }: Props) {
return (
<>
<ambientLight intensity={0.7} />
<directionalLight position={[200, 400, 300]} intensity={1.3} />
<Controller size={...} /> {/* OrbitControls + camera fit */}
<GlbModel path={item.glbPath} /> {/* Suspense, centrage, onSize */}
</>
);
}
Pourquoi pas R3F dès le début ?
Deux raisons honnêtes :
Le projet vanilla ES Modules fonctionne sans build step — pour itérer vite sur la géométrie et les positions, ne pas avoir à lancer npm run dev était un vrai confort.
R3F brille sur l'UI réactive — dès qu'un composant 3D doit synchroniser son état avec un composant HTML (bouton, liste, filtre), React devient naturel. La preview inventaire est exactement ce cas. Et une fois la preview migrée, il était logique de migrer toute la scène.
La démonstration concrète — le congélateur interactif :
// Freezer.tsx — traduction déclarative de la géométrie procédurale de decor.js
export function Freezer({ actionState, onSize }: SceneItemProps) {
const isOpen = actionState['freezer-toggle'] ?? false;
const doorRef = useRef<THREE.Group>(null!);
useFrame(() => {
const target = isOpen ? Math.PI / 2 : 0;
doorRef.current.rotation.y += (target - doorRef.current.rotation.y) * 0.12;
});
return (
<group>
{/* carcasse, étagères, pieds… */}
<group ref={doorRef} position={[FRZ_D/2, 0, -FRZ_W/2]}>
<mesh position={[0, FRZ_H/2, FRZ_W/2]}>
<boxGeometry args={[FRZ_T, FRZ_H-2, FRZ_W-FRZ_T]} />
<meshStandardMaterial color="#1a1a1a" />
</mesh>
</group>
</group>
);
}
decor.js (impératif) | Freezer.tsx (déclaratif) |
|---|---|
addP(FRZ_T, FRZ_H, FRZ_W, ...) | <Panel sx={FRZ_T} sy={FRZ_H} .../> |
freezerDoorGroup.rotation.y = π/2 | useFrame(() => doorRef.current.rotation.y += lerp...) |
registerHoverAction('freezer-toggle', ...) | <button onClick={() => toggle('freezer-toggle')}> |
Pattern registry : chaque objet interactif s'enregistre par item.id, sans toucher au reste de la scène :
// registry.ts — ~50 composants enregistrés
export const SCENE_REGISTRY: Record<string, ComponentType<SceneItemProps>> = {
'freezer': Freezer,
'kallax': Kallax,
'sneaker': Sneakers,
// …
};
Résultat : la scène complète est en R3F. La version vanilla (js/) est conservée comme archive.
| Feature | Détail |
|---|---|
| Navigation | Walk mode WASD, vue 2D dessus, perspective, POV, WebXR VR |
| Structure | Murs, niche, couloir diagonal, cuisine, SDB, WC |
| Meubles | ~50 items/ components (procéduraux + GLB IKEA) |
| Interactivité | Hover menu, toggles portes/tiroirs/lit, murs rouges |
| Inventaire | ~120 items, preview 3D interactive (registry), filtres, recherche |
| Minimap | Canvas 2D temps réel, plein écran, suivi personnage |
| Floorplan | Plan 2D coté |
| Couches visuelles | Structure / GLB / Mobilier + toggles murs rouges, grille |
| Voisins | Appartements est/ouest semi-transparents |
| Dev Tools | FPS graph, draw calls, stats mémoire, tailles GLB |
| Visualisation repo | repo-visualizer — évolution du code au fil des commits |
| Personnage | Walking man animé (Lara 2026) + SkeletonHelper toggle |
Ce projet a démarré parce que j'emménageais dans un nouvel appartement et que je voulais visualiser les configurations de meubles avant d'acheter. Les outils existants (IKEA Kreativ, Planner 5D) ne modélisent pas les contraintes spécifiques de mon appartement — la niche asymétrique, le mur diagonal, la cuisine en galley.
Construire le mien était plus rapide que de contourner les limitations des outils existants. Et ça m'a donné une bonne raison de creuser Three.js sérieusement.
Statut : en recherche d'emploi. Si vous êtes arrivé jusqu'ici et que ce genre d'approche vous parle, je suis disponible.
→ linkedin.com/in/david-herelle
~250 commits · 7 semaines · Three.js vanilla → React Three Fiber
Conservé pour montrer l'évolution du projet et de sa documentation.
Ce projet est la modélisation en 3D de mon studio actuel — mon studio de rêve.
Tout a commencé parce que je voulais recréer mon appartement en LEGO. Le site mecabricks.com n'était pas adapté à ce que je voulais faire, alors j'ai ouvert un éditeur de texte et Three.js.
Mais au fil des jours, j'ai réalisé que c'était bien plus que ça. Après +15 ans depuis mes débuts avec Three.js — à une époque où j'étais passionné par Flash/AS3 (paix à son âme 🕯️) en début de carrière — le web 3D avait énormément évolué. J'avais toujours rêvé de modéliser mon studio de rêve. Je l'avais fait en SketchUp, mais le logiciel est devenu payant pour qui veut programmer la construction en Ruby. Blender a toujours été trop complexe pour moi.
Aujourd'hui avec Three.js, je peux enfin réaliser ce rêve — sans dépendre d'une technologie qui risque de disparaître (Flash, SketchUp…). Open source, dans le navigateur, des modules ES natifs, aucun bundler.
Le projet a démarré une semaine avant le premier commit (17 fév. 2026). En moins d'un mois, les progrès sont énormes.
python3 -m http.server 8000
Outil : @gltf-transform/cli
npm install -g @gltf-transform/cli
npx @gltf-transform/cli draco input.glb output.glb
Résultats (54 MB → 16 MB) :
| Fichier | Avant | Après | Gain |
|---|---|---|---|
| man_black_business_suit | 24.0 MB | 3.9 MB | −84% |
| potted_palm | 5.0 MB | 2.9 MB | −42% |
| sunnersta_trolley_ikea | 3.4 MB | 0.3 MB | −91% |
| baseball_cap | 7.3 MB | 3.9 MB | −47% |
| ikea_lamp_ola | 1.6 MB | 0.09 MB | −94% |
| mackapar_ikea | 0.5 MB | 0.06 MB | −87% |
| xiaomi_electric_scooter_4 | 1.2 MB | 0.14 MB | −88% |
| red_backpack | 0.2 MB | 0.04 MB | −84% |
| ikea_DRONA_black | 0.02 MB | 0.008 MB | −61% |
| ikea_Altappen | 4.6 MB | 4.1 MB | −11% |
Note :
smorkull.glb,folding-chair-generic.glbetviggja.glbconservés non-compressés — la quantification Draco modifie leur bounding box et casse le calcul de scale dynamique (rawSize.y,rawSize.z).
Chargeur partagé (js/loaders.js) — un seul GLTFLoader + DRACOLoader
pour tous les modules :
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const draco = new DRACOLoader();
draco.setDecoderPath('https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/draco/gltf/');
draco.preload();
export const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(draco);
Outil : simplify + draco en pipeline
npx @gltf-transform/cli simplify --ratio 0.05 --error 0.001 input.glb /tmp/simp.glb
npx @gltf-transform/cli draco /tmp/simp.glb output.glb
Appliqué aux modèles dont la couleur est overridée en rouge plat (qualité de texture non critique) :
| Fichier | Ratio | Avant | Après | Triangles |
|---|---|---|---|---|
| man_black_business_suit | 0.05 | 4.0 MB | 3.0 MB | −95% |
| baseball_cap | 0.05 | 4.1 MB | 3.9 MB | −95% |
| ikea_lamp_ola | 0.30 | 0.16 MB | 0.09 MB | −70% |
| xiaomi_electric_scooter_4 | 0.30 | 0.17 MB | 0.14 MB | −70% |
| potted_palm | 0.30 | 2.95 MB | 2.89 MB | −70% |
Attention : ne pas appliquer
weldavantsimplifysur des GLBs déjà Draco — le décodage + recompression gonfle les fichiers (ex. suit : 4 MB → 20 MB après weld avant de re-encoder en Draco). Fairesimplifydirectement sur le fichier Draco, puisdracopour recompresser.
smorkull.glbrésiste à la simplification (coutures UV complexes — gain négligeable même avec ratio=0.2).
mergeGlbByMaterial)Problème : les GLBs exportés depuis SketchUp/Blender/CAD créent des centaines de primitives séparées → draw calls × N par modèle.
Solution (js/mergeUtils.js) : après le chargement, fusionner toutes les
primitives qui partagent le même matériau en un seul Mesh par matériau.
Résultat : draw calls 2 253 → 238 (×9), fluidité mobile incluse.
import { mergeGlbByMaterial } from './mergeUtils.js';
gltfLoader.load('media/model.glb', (gltf) => {
const root = gltf.scene;
// 1. Appliquer les overrides de matériau AVANT la fusion
root.traverse(c => { if (c.isMesh) c.material = myMat; });
// 2. Positionner/scaler le root AVANT la fusion
root.scale.setScalar(myScale);
root.position.set(x, y, z);
// 3. Fusionner
mergeGlbByMaterial(root);
scene.add(root);
});
Comportement interne :
material.uuidtangent etc. si absent d'une géométrie)
pour éviter les erreurs mergeGeometries()Line (arêtes CAD embarquées dans certains GLBs)Mesh fusionné par groupe de matériau en enfant du rootCompatible avec le clonage (root.clone(true)) — les clones héritent du
mesh fusionné. Utilisé par drona.js et casquettes.js.
Appliquer le merge APRÈS les overrides de matériau pour maximiser la fusion
(ex. : si tous les meshes ont redMat, résultat = 1 seul mesh au lieu de N).
Pour démarrer directement dans la scène après chargement :
http://localhost:5173/?skipIntro=1&target=studio&cameraPos=north-east.
skipIntro=1 supprime le compte à rebours et les animations d'introduction.
target=studio ouvre l'orbite ; follow=1 active le suivi et fpv=1 la première personne.
target choisit le sujet, projection=ortho la projection orthographique,
cameraPos l'orientation et cameraElevation=90 la vue du dessus.
target=charactersGrid ouvre la grille Lara ; target=inventoryObjectGrid&wigGrid=1 ouvre les perruques.
Sans ces paramètres, le lancement conserve l'introduction et la vue FPV par défaut.
randomSpawn=1 fait démarrer tous les PNJ présents sur des emplacements d'activité aléatoires disponibles, comme Ybot, au lieu de leurs emplacements prioritaires.
Exemple avec tous les personnages : http://localhost:5173/?characterCount=all&randomSpawn=1.
Le panneau 🛠 Dev Tools dans le side-panel affiche en temps réel :
media/*.glb, triées par tailleBouton ↺ Refresh pour recalculer les stats scène après chargement des GLBs.