尚未偵測到更新。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.