Compare commits
7 Commits
a4383a7cec
...
6d178dc59e
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d178dc59e | |||
| b4a3545460 | |||
| 1f6d9659ed | |||
| a52d57ae6c | |||
| d0497ec42c | |||
| 4c42e11268 | |||
| f175ad6240 |
+1
-1
@@ -7,7 +7,7 @@ import tseslint from "typescript-eslint";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["dist", "POC-grass"]),
|
||||
globalIgnores(["dist"]),
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
extends: [
|
||||
|
||||
+21
-71
@@ -40201,7 +40201,7 @@
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneauxcentre",
|
||||
"name": "panneauclassique",
|
||||
"type": "Object3D",
|
||||
"role": "group",
|
||||
"position": [0, 0, 0],
|
||||
@@ -40209,40 +40209,30 @@
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneauxcentre",
|
||||
"name": "panneauclassique",
|
||||
"type": "Object3D",
|
||||
"position": [-35.2895, 3.8515, 40.1864],
|
||||
"rotation": [-3.0988, 0.9934, -3.1367],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneauxcentre",
|
||||
"name": "panneauclassique",
|
||||
"type": "Mesh",
|
||||
"position": [-35.2895, 3.8515, 40.1864],
|
||||
"rotation": [-3.0988, 0.9934, -3.1367],
|
||||
"scale": [1, 1, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "panneauxdomaine",
|
||||
"type": "Object3D",
|
||||
"role": "group",
|
||||
"position": [0, 0, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
},
|
||||
{
|
||||
"name": "panneauxdomaine",
|
||||
"name": "panneauclassique",
|
||||
"type": "Object3D",
|
||||
"position": [-2.4347, 3.7803, -56.1439],
|
||||
"rotation": [0.0474, 0.2148, -0.0027],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneauxdomaine",
|
||||
"name": "panneauclassique",
|
||||
"type": "Mesh",
|
||||
"position": [-2.4347, 3.7803, -56.1439],
|
||||
"rotation": [0.0474, 0.2148, -0.0027],
|
||||
@@ -40253,7 +40243,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "panneaudirresidences2",
|
||||
"name": "panneaufleche",
|
||||
"type": "Object3D",
|
||||
"role": "group",
|
||||
"position": [0, 0, 0],
|
||||
@@ -40261,118 +40251,78 @@
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneaudirresidences2",
|
||||
"name": "panneaufleche",
|
||||
"type": "Object3D",
|
||||
"position": [30.1743, 7.1944, -13.5781],
|
||||
"rotation": [0.0374, -1.297, -0.0099],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneaudirresidences2",
|
||||
"name": "panneaufleche",
|
||||
"type": "Mesh",
|
||||
"position": [30.1743, 7.1944, -13.5781],
|
||||
"rotation": [0.0374, -1.297, -0.0099],
|
||||
"scale": [1, 1, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "panneaudirdomaine",
|
||||
"type": "Object3D",
|
||||
"role": "group",
|
||||
"position": [0, 0, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
},
|
||||
{
|
||||
"name": "panneaudirdomaine",
|
||||
"name": "panneaufleche",
|
||||
"type": "Object3D",
|
||||
"position": [8.1032, 7.1918, -23.0004],
|
||||
"rotation": [0.0462, -0.2443, -0.0027],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneaudirdomaine",
|
||||
"name": "panneaufleche",
|
||||
"type": "Mesh",
|
||||
"position": [8.1032, 7.1918, -23.0004],
|
||||
"rotation": [0.0462, -0.2443, -0.0027],
|
||||
"scale": [1, 1, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "panneaudirresidences1",
|
||||
"type": "Object3D",
|
||||
"role": "group",
|
||||
"position": [0, 0, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
},
|
||||
{
|
||||
"name": "panneaudirresidences1",
|
||||
"name": "panneaufleche",
|
||||
"type": "Object3D",
|
||||
"position": [-10.1125, 7.2582, -11.4511],
|
||||
"rotation": [0.0478, 0.3436, -0.0028],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneaudirresidences1",
|
||||
"name": "panneaufleche",
|
||||
"type": "Mesh",
|
||||
"position": [-10.1125, 7.2582, -11.4511],
|
||||
"rotation": [0.0478, 0.3436, -0.0028],
|
||||
"scale": [1, 1, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "panneaudircentre",
|
||||
"type": "Object3D",
|
||||
"role": "group",
|
||||
"position": [0, 0, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
},
|
||||
{
|
||||
"name": "panneaudircentre",
|
||||
"name": "panneaufleche",
|
||||
"type": "Object3D",
|
||||
"position": [-10.5207, 7.0874, 19.3819],
|
||||
"rotation": [-3.1416, 1.5139, -3.0947],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneaudircentre",
|
||||
"name": "panneaufleche",
|
||||
"type": "Mesh",
|
||||
"position": [-10.5207, 7.0874, 19.3819],
|
||||
"rotation": [-3.1416, 1.5139, -3.0947],
|
||||
"scale": [1, 1, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "panneaudirfabrik",
|
||||
"type": "Object3D",
|
||||
"role": "group",
|
||||
"position": [0, 0, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
},
|
||||
{
|
||||
"name": "panneaudirfabrik",
|
||||
"name": "panneaufleche",
|
||||
"type": "Object3D",
|
||||
"position": [29.0899, 6.9811, 23.4988],
|
||||
"rotation": [3.1416, -0.8852, 3.1253],
|
||||
"scale": [1, 1, 1],
|
||||
"children": [
|
||||
{
|
||||
"name": "panneaudirfabrik",
|
||||
"name": "panneaufleche",
|
||||
"type": "Mesh",
|
||||
"position": [29.0899, 6.9811, 23.4988],
|
||||
"rotation": [3.1416, -0.8852, 3.1253],
|
||||
|
||||
@@ -10,6 +10,13 @@ const MESH_NAME_MAPPINGS = {
|
||||
eoliennes: "eolienne",
|
||||
immeuble_1: "immeuble1",
|
||||
buissons: "buisson",
|
||||
panneauxcentre: "panneauclassique",
|
||||
panneauxdomaine: "panneauclassique",
|
||||
panneaudircentre: "panneaufleche",
|
||||
panneaudirdomaine: "panneaufleche",
|
||||
panneaudirfabrik: "panneaufleche",
|
||||
panneaudirresidences1: "panneaufleche",
|
||||
panneaudirresidences2: "panneaufleche",
|
||||
panneauxquartier: "panneauaffichage",
|
||||
};
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useEffect, useMemo, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||
|
||||
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
|
||||
|
||||
interface TerrainModelProps {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||
|
||||
export const FOG_CONFIG = {
|
||||
enabled: true,
|
||||
color: "#eef3f5",
|
||||
color: "#dce8df",
|
||||
near: 38,
|
||||
far: 45,
|
||||
};
|
||||
|
||||
@@ -1,108 +1,59 @@
|
||||
import type { TerrainSurfaceColorConfig } from "@/types/world/terrainSurface";
|
||||
|
||||
export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||
export const TERRAIN_SURFACE_COLOR_TOLERANCE = 15;
|
||||
export const TERRAIN_WATER_HEIGHT = 0;
|
||||
export const TERRAIN_TILE_SIZE = 1;
|
||||
export const GRASS_BASE_COLOR = "#1a3a1a";
|
||||
|
||||
export const TERRAIN_COLORS = {
|
||||
grass1: {
|
||||
hex: "#84C66B",
|
||||
rgb: [132, 198, 107] as const,
|
||||
type: "grass" as const,
|
||||
kind: "grass",
|
||||
grassTipColor: "#84C66B",
|
||||
},
|
||||
grass2: {
|
||||
hex: "#67B058",
|
||||
rgb: [103, 176, 88] as const,
|
||||
type: "grass" as const,
|
||||
kind: "grass",
|
||||
grassTipColor: "#67B058",
|
||||
},
|
||||
grass3: {
|
||||
hex: "#A3CA5B",
|
||||
rgb: [163, 202, 91] as const,
|
||||
type: "grass" as const,
|
||||
kind: "grass",
|
||||
grassTipColor: "#A3CA5B",
|
||||
},
|
||||
potager: {
|
||||
hex: "#342420",
|
||||
rgb: [52, 36, 32] as const,
|
||||
type: "tile" as const,
|
||||
tileModel: "/models/potager/potager.gltf",
|
||||
tileSize: 1,
|
||||
kind: "garden",
|
||||
modelPath: "/models/potager/potager.gltf",
|
||||
tileSize: TERRAIN_TILE_SIZE,
|
||||
},
|
||||
terre: {
|
||||
hex: "#513E2C",
|
||||
rgb: [81, 62, 44] as const,
|
||||
type: "none" as const,
|
||||
kind: "dirt",
|
||||
},
|
||||
chemin: {
|
||||
hex: "#F5D896",
|
||||
rgb: [245, 216, 150] as const,
|
||||
type: "tile" as const,
|
||||
tileModel: "/models/chemins/model.gltf",
|
||||
tileSize: 1,
|
||||
kind: "path",
|
||||
modelPath: "/models/chemins/model.gltf",
|
||||
tileSize: TERRAIN_TILE_SIZE,
|
||||
},
|
||||
eau: {
|
||||
hex: "#91DAF5",
|
||||
rgb: [145, 218, 245] as const,
|
||||
type: "water" as const,
|
||||
kind: "water",
|
||||
},
|
||||
cailloux: {
|
||||
hex: "#B6D3DE",
|
||||
rgb: [182, 211, 222] as const,
|
||||
type: "none" as const,
|
||||
kind: "rock",
|
||||
},
|
||||
} as const;
|
||||
} satisfies Record<string, TerrainSurfaceColorConfig>;
|
||||
|
||||
export type TerrainColorKey = keyof typeof TERRAIN_COLORS;
|
||||
export type TerrainType = "grass" | "tile" | "water" | "none";
|
||||
|
||||
export const GRASS_BASE_COLOR = "#1a3a1a";
|
||||
|
||||
export const COLOR_TOLERANCE = 15;
|
||||
|
||||
export function colorMatchesTerrain(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
targetRgb: readonly [number, number, number],
|
||||
tolerance: number = COLOR_TOLERANCE,
|
||||
): boolean {
|
||||
return (
|
||||
Math.abs(r - targetRgb[0]) <= tolerance &&
|
||||
Math.abs(g - targetRgb[1]) <= tolerance &&
|
||||
Math.abs(b - targetRgb[2]) <= tolerance
|
||||
);
|
||||
}
|
||||
|
||||
export function getTerrainTypeFromColor(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
): TerrainColorKey | null {
|
||||
for (const [key, config] of Object.entries(TERRAIN_COLORS)) {
|
||||
if (colorMatchesTerrain(r, g, b, config.rgb)) {
|
||||
return key as TerrainColorKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isGrassZone(r: number, g: number, b: number): boolean {
|
||||
return (
|
||||
colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass1.rgb) ||
|
||||
colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass2.rgb) ||
|
||||
colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass3.rgb)
|
||||
);
|
||||
}
|
||||
|
||||
export function getGrassTipColor(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
): string | null {
|
||||
if (colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass1.rgb)) {
|
||||
return TERRAIN_COLORS.grass1.grassTipColor;
|
||||
}
|
||||
if (colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass2.rgb)) {
|
||||
return TERRAIN_COLORS.grass2.grassTipColor;
|
||||
}
|
||||
if (colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass3.rgb)) {
|
||||
return TERRAIN_COLORS.grass3.grassTipColor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||
import { Debug } from "@/utils/debug/Debug";
|
||||
import {
|
||||
MAP_PERFORMANCE_GROUP_NAMES,
|
||||
MAP_PERFORMANCE_MODEL_NAMES,
|
||||
@@ -14,7 +15,9 @@ function toLabel(value: string): string {
|
||||
}
|
||||
|
||||
export function useMapPerformanceDebug(): void {
|
||||
useDebugFolder("Performance / Map", (folder) => {
|
||||
useDebugFolder("Map", (folder) => {
|
||||
Debug.getInstance().addFogControl(folder);
|
||||
|
||||
const {
|
||||
groups,
|
||||
models,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
||||
import type {
|
||||
TerrainSurfaceBounds,
|
||||
TerrainSurfaceData,
|
||||
} from "@/types/world/terrainSurface";
|
||||
import { createTerrainSurfaceImageData } from "@/utils/world/terrainSurfaceSampler";
|
||||
|
||||
function findTerrainBaseColorTexture(
|
||||
scene: THREE.Object3D,
|
||||
): THREE.Texture | null {
|
||||
let texture: THREE.Texture | null = null;
|
||||
|
||||
scene.traverse((child) => {
|
||||
if (texture || !(child instanceof THREE.Mesh)) return;
|
||||
|
||||
const materials = Array.isArray(child.material)
|
||||
? child.material
|
||||
: [child.material];
|
||||
|
||||
for (const material of materials) {
|
||||
if (material instanceof THREE.MeshStandardMaterial && material.map) {
|
||||
texture = material.map;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
function createTerrainSurfaceBounds(
|
||||
scene: THREE.Object3D,
|
||||
): TerrainSurfaceBounds {
|
||||
scene.updateWorldMatrix(true, true);
|
||||
|
||||
const box = new THREE.Box3().setFromObject(scene);
|
||||
return {
|
||||
minX: box.min.x,
|
||||
maxX: box.max.x,
|
||||
minZ: box.min.z,
|
||||
maxZ: box.max.z,
|
||||
};
|
||||
}
|
||||
|
||||
export function useTerrainSurfaceData(): TerrainSurfaceData | null {
|
||||
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
||||
|
||||
return useMemo(() => {
|
||||
const texture = findTerrainBaseColorTexture(scene);
|
||||
if (!texture) return null;
|
||||
|
||||
const imageData = createTerrainSurfaceImageData(texture);
|
||||
if (!imageData) return null;
|
||||
|
||||
return {
|
||||
bounds: createTerrainSurfaceBounds(scene),
|
||||
imageData,
|
||||
};
|
||||
}, [scene]);
|
||||
}
|
||||
@@ -24,9 +24,6 @@ export function HandTrackingProvider({
|
||||
children: ReactNode;
|
||||
}): React.JSX.Element {
|
||||
const sceneMode = useSceneMode();
|
||||
const handTrackingSource = useDebugStore((debug) =>
|
||||
debug.getHandTrackingSource(),
|
||||
);
|
||||
const repairNeedsHands = useGameStore((state) => {
|
||||
switch (state.mainState) {
|
||||
case "bike":
|
||||
@@ -44,20 +41,34 @@ export function HandTrackingProvider({
|
||||
const enabled =
|
||||
repairNeedsHands ||
|
||||
(sceneMode === "physics" && (nearby || holding || handHolding));
|
||||
|
||||
if (!enabled) {
|
||||
return (
|
||||
<HandTrackingContext value={HAND_TRACKING_IDLE_SNAPSHOT}>
|
||||
{children}
|
||||
</HandTrackingContext>
|
||||
);
|
||||
}
|
||||
|
||||
return <ActiveHandTrackingProvider>{children}</ActiveHandTrackingProvider>;
|
||||
}
|
||||
|
||||
function ActiveHandTrackingProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): React.JSX.Element {
|
||||
const handTrackingSource = useDebugStore((debug) =>
|
||||
debug.getHandTrackingSource(),
|
||||
);
|
||||
const backendSnapshot = useRemoteHandTracking({
|
||||
enabled: enabled && handTrackingSource === "backend",
|
||||
enabled: handTrackingSource === "backend",
|
||||
});
|
||||
const browserSnapshot = useBrowserHandTracking({
|
||||
enabled: enabled && handTrackingSource === "browser",
|
||||
enabled: handTrackingSource === "browser",
|
||||
});
|
||||
const snapshot =
|
||||
handTrackingSource === "browser" ? browserSnapshot : backendSnapshot;
|
||||
|
||||
return (
|
||||
<HandTrackingContext
|
||||
value={enabled ? snapshot : HAND_TRACKING_IDLE_SNAPSHOT}
|
||||
>
|
||||
{children}
|
||||
</HandTrackingContext>
|
||||
);
|
||||
return <HandTrackingContext value={snapshot}>{children}</HandTrackingContext>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
export type TerrainSurfaceKind =
|
||||
| "grass"
|
||||
| "path"
|
||||
| "water"
|
||||
| "garden"
|
||||
| "dirt"
|
||||
| "rock";
|
||||
|
||||
export type TerrainSurfaceRgb = readonly [number, number, number];
|
||||
|
||||
export interface TerrainSurfaceUv {
|
||||
u: number;
|
||||
v: number;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceBounds {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minZ: number;
|
||||
maxZ: number;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceColorConfig {
|
||||
hex: string;
|
||||
rgb: TerrainSurfaceRgb;
|
||||
kind: TerrainSurfaceKind;
|
||||
grassTipColor?: string;
|
||||
modelPath?: string;
|
||||
tileSize?: number;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceSample {
|
||||
rgb: TerrainSurfaceRgb;
|
||||
key: string | null;
|
||||
config: TerrainSurfaceColorConfig | null;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceData {
|
||||
bounds: TerrainSurfaceBounds;
|
||||
imageData: ImageData;
|
||||
}
|
||||
+98
-13
@@ -1,6 +1,7 @@
|
||||
import GUI from "lil-gui";
|
||||
import type { CameraMode, SceneMode } from "@/types/debug/debug";
|
||||
import type { HandTrackingSource } from "@/types/handTracking/handTracking";
|
||||
import { FOG_CONFIG } from "@/data/world/fogConfig";
|
||||
import { EventEmitter } from "@/utils/core/EventEmitter";
|
||||
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
||||
|
||||
@@ -15,6 +16,14 @@ interface DebugEvents {
|
||||
change: void;
|
||||
}
|
||||
|
||||
const DEBUG_FOLDER_ORDER = [
|
||||
"Lighting",
|
||||
"Game",
|
||||
"Interaction",
|
||||
"Hand Tracking",
|
||||
"Map",
|
||||
] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
@@ -58,6 +67,7 @@ export class Debug {
|
||||
private readonly folderRefCounts = new Map<string, number>();
|
||||
private readonly controls: {
|
||||
cameraMode: CameraMode;
|
||||
fogEnabled: boolean;
|
||||
handTrackingSource: HandTrackingSource;
|
||||
showDebugOverlay: boolean;
|
||||
showHandTrackingSvg: boolean;
|
||||
@@ -80,7 +90,8 @@ export class Debug {
|
||||
|
||||
this.controls = {
|
||||
cameraMode: storedControls.cameraMode ?? "player",
|
||||
handTrackingSource: "backend",
|
||||
fogEnabled: FOG_CONFIG.enabled,
|
||||
handTrackingSource: "browser",
|
||||
showDebugOverlay: true,
|
||||
showHandTrackingSvg: false,
|
||||
showInteractionSpheres: false,
|
||||
@@ -88,14 +99,12 @@ export class Debug {
|
||||
sceneMode: storedControls.sceneMode ?? "game",
|
||||
};
|
||||
|
||||
this.gui = this.active ? new GUI({ title: "La-Fabrik Debug" }) : null;
|
||||
this.gui = this.active ? new GUI({ title: "La Fabrik" }) : null;
|
||||
|
||||
if (this.gui) {
|
||||
const folder = this.createFolder("Debug");
|
||||
this.gui.open();
|
||||
|
||||
if (!folder) return;
|
||||
|
||||
folder
|
||||
this.gui
|
||||
.add(this.controls, "cameraMode", { Player: "player", Debug: "debug" })
|
||||
.name("Camera Mode")
|
||||
.onChange((value: CameraMode) => {
|
||||
@@ -103,7 +112,7 @@ export class Debug {
|
||||
this.saveAndEmit();
|
||||
});
|
||||
|
||||
folder
|
||||
this.gui
|
||||
.add(this.controls, "sceneMode", { Game: "game", Physics: "physics" })
|
||||
.name("Scene")
|
||||
.onChange((value: SceneMode) => {
|
||||
@@ -111,7 +120,7 @@ export class Debug {
|
||||
this.saveAndEmit();
|
||||
});
|
||||
|
||||
folder
|
||||
this.gui
|
||||
.add(this.controls, "showPerf")
|
||||
.name("R3F Perf")
|
||||
.onChange((value: boolean) => {
|
||||
@@ -119,7 +128,7 @@ export class Debug {
|
||||
this.emit();
|
||||
});
|
||||
|
||||
folder
|
||||
this.gui
|
||||
.add(this.controls, "showDebugOverlay")
|
||||
.name("Debug Overlay")
|
||||
.onChange((value: boolean) => {
|
||||
@@ -127,6 +136,8 @@ export class Debug {
|
||||
this.emit();
|
||||
});
|
||||
|
||||
this.createOrderedFolders();
|
||||
|
||||
const handTrackingFolder = this.createFolder("Hand Tracking");
|
||||
|
||||
handTrackingFolder
|
||||
@@ -139,8 +150,8 @@ export class Debug {
|
||||
|
||||
handTrackingFolder
|
||||
?.add(this.controls, "handTrackingSource", {
|
||||
Backend: "backend",
|
||||
"Browser JS": "browser",
|
||||
Backend: "backend",
|
||||
})
|
||||
.name("Source")
|
||||
.onChange((value: HandTrackingSource) => {
|
||||
@@ -154,23 +165,47 @@ export class Debug {
|
||||
* Acquires a named GUI folder. Returns the folder on first acquisition and null
|
||||
* on subsequent acquisitions so callers only register controls once.
|
||||
*/
|
||||
createFolder(name: string): GUI | null {
|
||||
createFolder(name: string, options?: { open?: boolean }): GUI | null {
|
||||
if (!this.gui) return null;
|
||||
|
||||
const existing = this.folders.get(name);
|
||||
|
||||
if (existing) {
|
||||
this.folderRefCounts.set(name, (this.folderRefCounts.get(name) ?? 0) + 1);
|
||||
return null;
|
||||
const refCount = this.folderRefCounts.get(name) ?? 0;
|
||||
|
||||
if (refCount > 0) {
|
||||
this.folderRefCounts.set(name, refCount + 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.folderRefCounts.set(name, 1);
|
||||
return existing;
|
||||
}
|
||||
|
||||
const folder = this.gui.addFolder(name);
|
||||
this.folders.set(name, folder);
|
||||
this.folderRefCounts.set(name, 1);
|
||||
this.sortFolders();
|
||||
|
||||
if (options?.open) {
|
||||
folder.open();
|
||||
} else {
|
||||
folder.close();
|
||||
}
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
addFogControl(folder: GUI): void {
|
||||
folder
|
||||
.add(this.controls, "fogEnabled")
|
||||
.name("Fog")
|
||||
.onChange((value: boolean) => {
|
||||
this.controls.fogEnabled = value;
|
||||
this.emit();
|
||||
});
|
||||
}
|
||||
|
||||
destroyFolder(name: string): void {
|
||||
const folder = this.folders.get(name);
|
||||
const refCount = this.folderRefCounts.get(name);
|
||||
@@ -206,6 +241,10 @@ export class Debug {
|
||||
return this.controls.handTrackingSource;
|
||||
}
|
||||
|
||||
getFogEnabled(): boolean {
|
||||
return this.controls.fogEnabled;
|
||||
}
|
||||
|
||||
getShowInteractionSpheres(): boolean {
|
||||
return this.controls.showInteractionSpheres;
|
||||
}
|
||||
@@ -247,4 +286,50 @@ export class Debug {
|
||||
|
||||
this.emit();
|
||||
}
|
||||
|
||||
private createOrderedFolders(): void {
|
||||
for (const folderName of DEBUG_FOLDER_ORDER) {
|
||||
this.ensureFolder(folderName);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureFolder(name: string): GUI | null {
|
||||
if (!this.gui) return null;
|
||||
|
||||
const existing = this.folders.get(name);
|
||||
if (existing) return existing;
|
||||
|
||||
const folder = this.gui.addFolder(name);
|
||||
folder.close();
|
||||
this.folders.set(name, folder);
|
||||
this.folderRefCounts.set(name, 0);
|
||||
this.sortFolders();
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
private sortFolders(): void {
|
||||
if (!this.gui) return;
|
||||
|
||||
const rootElement = this.gui.domElement.querySelector(".children");
|
||||
if (!rootElement) return;
|
||||
|
||||
const orderedFolders = [...this.folders.entries()].sort(([a], [b]) => {
|
||||
const aIndex = DEBUG_FOLDER_ORDER.indexOf(
|
||||
a as (typeof DEBUG_FOLDER_ORDER)[number],
|
||||
);
|
||||
const bIndex = DEBUG_FOLDER_ORDER.indexOf(
|
||||
b as (typeof DEBUG_FOLDER_ORDER)[number],
|
||||
);
|
||||
const safeAIndex = aIndex === -1 ? DEBUG_FOLDER_ORDER.length : aIndex;
|
||||
const safeBIndex = bIndex === -1 ? DEBUG_FOLDER_ORDER.length : bIndex;
|
||||
|
||||
if (safeAIndex !== safeBIndex) return safeAIndex - safeBIndex;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
for (const [, folder] of orderedFolders) {
|
||||
rootElement.appendChild(folder.domElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
TERRAIN_COLORS,
|
||||
TERRAIN_SURFACE_COLOR_TOLERANCE,
|
||||
type TerrainColorKey,
|
||||
} from "@/data/world/terrainConfig";
|
||||
import type { TerrainSurfaceRgb } from "@/types/world/terrainSurface";
|
||||
|
||||
export function colorMatchesTerrainSurface(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
targetRgb: TerrainSurfaceRgb,
|
||||
tolerance: number = TERRAIN_SURFACE_COLOR_TOLERANCE,
|
||||
): boolean {
|
||||
return (
|
||||
Math.abs(r - targetRgb[0]) <= tolerance &&
|
||||
Math.abs(g - targetRgb[1]) <= tolerance &&
|
||||
Math.abs(b - targetRgb[2]) <= tolerance
|
||||
);
|
||||
}
|
||||
|
||||
export function getTerrainColorKeyFromRgb(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
): TerrainColorKey | null {
|
||||
for (const [key, config] of Object.entries(TERRAIN_COLORS)) {
|
||||
if (colorMatchesTerrainSurface(r, g, b, config.rgb)) {
|
||||
return key as TerrainColorKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isGrassTerrainColor(r: number, g: number, b: number): boolean {
|
||||
const key = getTerrainColorKeyFromRgb(r, g, b);
|
||||
return key !== null && TERRAIN_COLORS[key].kind === "grass";
|
||||
}
|
||||
|
||||
export function getGrassTipColorFromRgb(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
): string | null {
|
||||
const key = getTerrainColorKeyFromRgb(r, g, b);
|
||||
if (key === null) return null;
|
||||
|
||||
const terrainColor = TERRAIN_COLORS[key];
|
||||
return "grassTipColor" in terrainColor ? terrainColor.grassTipColor : null;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type * as THREE from "three";
|
||||
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||
import type {
|
||||
TerrainSurfaceBounds,
|
||||
TerrainSurfaceRgb,
|
||||
TerrainSurfaceSample,
|
||||
TerrainSurfaceUv,
|
||||
} from "@/types/world/terrainSurface";
|
||||
import { getTerrainColorKeyFromRgb } from "@/utils/world/terrainSurfaceColor";
|
||||
|
||||
type TerrainSurfaceImageSource =
|
||||
| HTMLImageElement
|
||||
| HTMLCanvasElement
|
||||
| ImageBitmap;
|
||||
|
||||
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
||||
|
||||
function clamp01(value: number): number {
|
||||
return Math.min(Math.max(value, 0), 1);
|
||||
}
|
||||
|
||||
function isTerrainSurfaceImageSource(
|
||||
value: unknown,
|
||||
): value is TerrainSurfaceImageSource {
|
||||
return (
|
||||
value instanceof HTMLImageElement ||
|
||||
value instanceof HTMLCanvasElement ||
|
||||
(typeof ImageBitmap !== "undefined" && value instanceof ImageBitmap)
|
||||
);
|
||||
}
|
||||
|
||||
export function createTerrainSurfaceImageData(
|
||||
texture: THREE.Texture,
|
||||
): ImageData | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
|
||||
const image = texture.image as unknown;
|
||||
if (!isTerrainSurfaceImageSource(image)) return null;
|
||||
|
||||
const cachedImageData = imageDataCache.get(image);
|
||||
if (cachedImageData) return cachedImageData;
|
||||
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return null;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.drawImage(image, 0, 0, width, height);
|
||||
|
||||
const imageData = context.getImageData(0, 0, width, height);
|
||||
imageDataCache.set(image, imageData);
|
||||
return imageData;
|
||||
}
|
||||
|
||||
export function sampleTerrainSurfaceAtUv(
|
||||
imageData: ImageData,
|
||||
uv: TerrainSurfaceUv,
|
||||
): TerrainSurfaceSample {
|
||||
const x = Math.round(clamp01(uv.u) * (imageData.width - 1));
|
||||
const y = Math.round((1 - clamp01(uv.v)) * (imageData.height - 1));
|
||||
const index = (y * imageData.width + x) * 4;
|
||||
|
||||
const rgb: TerrainSurfaceRgb = [
|
||||
imageData.data[index] ?? 0,
|
||||
imageData.data[index + 1] ?? 0,
|
||||
imageData.data[index + 2] ?? 0,
|
||||
];
|
||||
const key = getTerrainColorKeyFromRgb(rgb[0], rgb[1], rgb[2]);
|
||||
|
||||
return {
|
||||
rgb,
|
||||
key,
|
||||
config: key === null ? null : TERRAIN_COLORS[key],
|
||||
};
|
||||
}
|
||||
|
||||
export function terrainSurfaceUvFromXZ(
|
||||
x: number,
|
||||
z: number,
|
||||
bounds: TerrainSurfaceBounds,
|
||||
): TerrainSurfaceUv {
|
||||
const width = bounds.maxX - bounds.minX;
|
||||
const depth = bounds.maxZ - bounds.minZ;
|
||||
|
||||
return {
|
||||
u: width === 0 ? 0 : (x - bounds.minX) / width,
|
||||
v: depth === 0 ? 0 : (z - bounds.minZ) / depth,
|
||||
};
|
||||
}
|
||||
|
||||
export function sampleTerrainSurfaceAtXZ(
|
||||
imageData: ImageData,
|
||||
x: number,
|
||||
z: number,
|
||||
bounds: TerrainSurfaceBounds,
|
||||
): TerrainSurfaceSample {
|
||||
return sampleTerrainSurfaceAtUv(
|
||||
imageData,
|
||||
terrainSurfaceUvFromXZ(x, z, bounds),
|
||||
);
|
||||
}
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
useMapPerformanceStore,
|
||||
} from "@/managers/stores/useMapPerformanceStore";
|
||||
import { SkyModel } from "@/components/three/world/SkyModel";
|
||||
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
||||
|
||||
export function Environment(): React.JSX.Element {
|
||||
const cameraMode = useCameraMode();
|
||||
const sceneMode = useSceneMode();
|
||||
const fogEnabled = useDebugStore((debug) => debug.getFogEnabled());
|
||||
const groups = useMapPerformanceStore((state) => state.groups);
|
||||
const models = useMapPerformanceStore((state) => state.models);
|
||||
const showSky = isMapModelVisible("sky", { groups, models });
|
||||
@@ -30,7 +32,10 @@ export function Environment(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
{FOG_CONFIG.enabled && sceneMode === "game" && cameraMode === "player" ? (
|
||||
{FOG_CONFIG.enabled &&
|
||||
fogEnabled &&
|
||||
sceneMode === "game" &&
|
||||
cameraMode === "player" ? (
|
||||
<fog
|
||||
attach="fog"
|
||||
args={[FOG_CONFIG.color, FOG_CONFIG.near, FOG_CONFIG.far]}
|
||||
|
||||
+22
-9
@@ -111,10 +111,13 @@ export function GameMap({
|
||||
const settledMapNodesRef = useRef(new Set<number>());
|
||||
const groups = useMapPerformanceStore((state) => state.groups);
|
||||
const models = useMapPerformanceStore((state) => state.models);
|
||||
const [mapNodes, setMapNodes] = useState<LoadedMapNode[]>([]);
|
||||
const [renderMapNodes, setRenderMapNodes] = useState<LoadedMapNode[]>([]);
|
||||
const [collisionMapNodes, setCollisionMapNodes] = useState<LoadedMapNode[]>(
|
||||
[],
|
||||
);
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
const [settledMapNodeCount, setSettledMapNodeCount] = useState(0);
|
||||
const mapReady = mapLoaded && settledMapNodeCount >= mapNodes.length;
|
||||
const mapReady = mapLoaded && settledMapNodeCount >= renderMapNodes.length;
|
||||
|
||||
const handleMapNodeSettled = useCallback((index: number) => {
|
||||
if (settledMapNodesRef.current.has(index)) return;
|
||||
@@ -125,7 +128,8 @@ export function GameMap({
|
||||
|
||||
const showEmptyMap = useCallback(
|
||||
(currentStep: string) => {
|
||||
setMapNodes([]);
|
||||
setRenderMapNodes([]);
|
||||
setCollisionMapNodes([]);
|
||||
setMapLoaded(true);
|
||||
settledMapNodesRef.current.clear();
|
||||
setSettledMapNodeCount(0);
|
||||
@@ -174,6 +178,12 @@ export function GameMap({
|
||||
const modelUrl = sceneData.models.get(node.name);
|
||||
return { node, modelUrl: modelUrl ?? null };
|
||||
});
|
||||
const loadedCollisionNodes = sceneData.mapNodes
|
||||
.filter((node) => node.name === "terrain")
|
||||
.map((node) => {
|
||||
const modelUrl = sceneData.models.get(node.name);
|
||||
return { node, modelUrl: modelUrl ?? null };
|
||||
});
|
||||
const missingModelCount = loadedMapNodes.filter(
|
||||
(mapNode) => mapNode.modelUrl === null,
|
||||
).length;
|
||||
@@ -188,7 +198,8 @@ export function GameMap({
|
||||
);
|
||||
}
|
||||
|
||||
setMapNodes(loadedMapNodes);
|
||||
setRenderMapNodes(loadedMapNodes);
|
||||
setCollisionMapNodes(loadedCollisionNodes);
|
||||
setMapLoaded(true);
|
||||
settledMapNodesRef.current.clear();
|
||||
setSettledMapNodeCount(0);
|
||||
@@ -209,21 +220,23 @@ export function GameMap({
|
||||
}, [onLoadingStateChange, showEmptyMap]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mapNodes.length === 0) return;
|
||||
if (renderMapNodes.length === 0) return;
|
||||
|
||||
const renderProgress =
|
||||
mapNodes.length === 0 ? 1 : settledMapNodeCount / mapNodes.length;
|
||||
renderMapNodes.length === 0
|
||||
? 1
|
||||
: settledMapNodeCount / renderMapNodes.length;
|
||||
onLoadingStateChange?.({
|
||||
currentStep: "Chargement des modèles de la map",
|
||||
progress: 0.25 + renderProgress * 0.45,
|
||||
status: "loading",
|
||||
});
|
||||
}, [mapNodes.length, onLoadingStateChange, settledMapNodeCount]);
|
||||
}, [renderMapNodes.length, onLoadingStateChange, settledMapNodeCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<group>
|
||||
{mapNodes.map((mapNode, index) => (
|
||||
{renderMapNodes.map((mapNode, index) => (
|
||||
<ModelErrorBoundary
|
||||
key={index}
|
||||
fallback={<FallbackMapNode node={mapNode.node} />}
|
||||
@@ -251,7 +264,7 @@ export function GameMap({
|
||||
<GameMapCollision
|
||||
buildOctree={buildOctree}
|
||||
mapReady={mapReady}
|
||||
nodes={mapNodes}
|
||||
nodes={collisionMapNodes}
|
||||
onLoaded={onLoaded}
|
||||
onLoadingStateChange={onLoadingStateChange}
|
||||
onOctreeReady={onOctreeReady}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Suspense, useMemo, useRef, useState } from "react";
|
||||
import { Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { CHUNK_CONFIG } from "@/data/world/fogConfig";
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
@@ -106,16 +106,21 @@ export function VegetationSystem(): React.JSX.Element | null {
|
||||
}, [data, groups, models]);
|
||||
|
||||
const visibleChunks = streamingEnabled
|
||||
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
|
||||
? chunks.filter((chunk) => {
|
||||
if (activeChunkKeys.size > 0) {
|
||||
return activeChunkKeys.has(chunk.key);
|
||||
}
|
||||
|
||||
return (
|
||||
Math.hypot(
|
||||
chunk.centerX - camera.position.x,
|
||||
chunk.centerZ - camera.position.z,
|
||||
) <= CHUNK_CONFIG.loadRadius
|
||||
);
|
||||
})
|
||||
: chunks;
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!streamingEnabled) return;
|
||||
|
||||
const now = clock.elapsedTime * 1000;
|
||||
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
|
||||
lastUpdateRef.current = now;
|
||||
|
||||
const updateActiveChunks = useCallback(() => {
|
||||
const nextKeys = new Set<string>();
|
||||
const cameraX = camera.position.x;
|
||||
const cameraZ = camera.position.z;
|
||||
@@ -143,6 +148,16 @@ export function VegetationSystem(): React.JSX.Element | null {
|
||||
}
|
||||
|
||||
setActiveChunkKeys(nextKeys);
|
||||
}, [activeChunkKeys, camera, chunks]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!streamingEnabled) return;
|
||||
|
||||
const now = clock.elapsedTime * 1000;
|
||||
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
|
||||
lastUpdateRef.current = now;
|
||||
|
||||
updateActiveChunks();
|
||||
});
|
||||
|
||||
if (isLoading || !data) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const VEGETATION_TYPES = {
|
||||
sapin: {
|
||||
mapName: "sapin",
|
||||
modelPath: "/models/sapin/model.gltf",
|
||||
scaleMultiplier: 2,
|
||||
scaleMultiplier: 5,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user