mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-04-29 16:25:49 +00:00
migrate to react-three-fiber
This commit is contained in:
parent
c20ca94953
commit
76e9f68e63
18 changed files with 1367 additions and 752 deletions
50
app/InspectorControls.tsx
Normal file
50
app/InspectorControls.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { getResourceList } from "@/src/manifest";
|
||||||
|
|
||||||
|
const excludeMissions = new Set([
|
||||||
|
"SkiFree",
|
||||||
|
"SkiFree_Daily",
|
||||||
|
"SkiFree_Randomizer",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const missions = getResourceList()
|
||||||
|
.map((resourcePath) => resourcePath.match(/^missions\/(.+)\.mis$/))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((match) => match[1])
|
||||||
|
.filter((name) => !excludeMissions.has(name));
|
||||||
|
|
||||||
|
export function InspectorControls({
|
||||||
|
missionName,
|
||||||
|
onChangeMission,
|
||||||
|
fogEnabled,
|
||||||
|
onChangeFogEnabled,
|
||||||
|
}: {
|
||||||
|
missionName: string;
|
||||||
|
onChangeMission: (name: string) => void;
|
||||||
|
fogEnabled: boolean;
|
||||||
|
onChangeFogEnabled: (enabled: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div id="controls">
|
||||||
|
<select
|
||||||
|
id="missionList"
|
||||||
|
value={missionName}
|
||||||
|
onChange={(event) => onChangeMission(event.target.value)}
|
||||||
|
>
|
||||||
|
{missions.map((missionName) => (
|
||||||
|
<option key={missionName}>{missionName}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="CheckboxField">
|
||||||
|
<input
|
||||||
|
id="fogInput"
|
||||||
|
type="checkbox"
|
||||||
|
checked={fogEnabled}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChangeFogEnabled(event.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label htmlFor="fogInput">Fog?</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { useGLTF, useTexture } from "@react-three/drei";
|
||||||
|
import { BASE_URL, interiorTextureToUrl, interiorToUrl } from "@/src/loaders";
|
||||||
|
import {
|
||||||
|
ConsoleObject,
|
||||||
|
getPosition,
|
||||||
|
getProperty,
|
||||||
|
getRotation,
|
||||||
|
getScale,
|
||||||
|
} from "@/src/mission";
|
||||||
|
import { Suspense, useMemo } from "react";
|
||||||
|
import { Material, Mesh } from "three";
|
||||||
|
import { setupColor } from "@/src/textureUtils";
|
||||||
|
|
||||||
|
const FALLBACK_URL = `${BASE_URL}/black.png`;
|
||||||
|
|
||||||
|
function useInterior(interiorFile: string) {
|
||||||
|
const url = interiorToUrl(interiorFile);
|
||||||
|
return useGLTF(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InteriorTexture({ material }: { material: Material }) {
|
||||||
|
let url = FALLBACK_URL;
|
||||||
|
try {
|
||||||
|
url = interiorTextureToUrl(material.name);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const texture = useTexture(url, (texture) => setupColor(texture));
|
||||||
|
|
||||||
|
return <meshStandardMaterial map={texture} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InteriorMesh({ node }: { node: Mesh }) {
|
||||||
|
return (
|
||||||
|
<mesh geometry={node.geometry} castShadow receiveShadow>
|
||||||
|
{node.material ? (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
// Allow the mesh to render while the texture is still loading;
|
||||||
|
// show a wireframe placeholder.
|
||||||
|
<meshStandardMaterial color="yellow" wireframe />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InteriorTexture material={node.material} />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InteriorModel({ interiorFile }: { interiorFile: string }) {
|
||||||
|
const { nodes } = useInterior(interiorFile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.entries(nodes)
|
||||||
|
// .filter(
|
||||||
|
// ([name, node]: [string, any]) => true
|
||||||
|
// // !node.material || !node.material.name.match(/\.\d+$/)
|
||||||
|
// )
|
||||||
|
.map(([name, node]: [string, any]) => (
|
||||||
|
<InteriorMesh key={name} node={node} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InteriorPlaceholder() {
|
||||||
|
return (
|
||||||
|
<mesh>
|
||||||
|
<boxGeometry args={[10, 10, 10]} />
|
||||||
|
<meshStandardMaterial color="orange" wireframe />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InteriorInstance({ object }: { object: ConsoleObject }) {
|
||||||
|
const interiorFile = getProperty(object, "interiorFile").value;
|
||||||
|
const [z, y, x] = useMemo(() => getPosition(object), [object]);
|
||||||
|
const [scaleX, scaleY, scaleZ] = useMemo(() => getScale(object), [object]);
|
||||||
|
const q = useMemo(() => getRotation(object, true), [object]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group quaternion={q} position={[x, y, z]} scale={[scaleX, scaleY, scaleZ]}>
|
||||||
|
<Suspense fallback={<InteriorPlaceholder />}>
|
||||||
|
<InteriorModel interiorFile={interiorFile} />
|
||||||
|
</Suspense>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
app/Mission.tsx
Normal file
31
app/Mission.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { loadMission } from "@/src/loaders";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { renderObject } from "./renderObject";
|
||||||
|
|
||||||
|
function useMission(name: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["mission", name],
|
||||||
|
queryFn: () => loadMission(name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LIGHT_ARGS = [
|
||||||
|
"rgba(209, 237, 255, 1)",
|
||||||
|
"rgba(186, 200, 181, 1)",
|
||||||
|
2,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function Mission({ name }: { name: string }) {
|
||||||
|
const { data: mission } = useMission(name);
|
||||||
|
|
||||||
|
if (!mission) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hemisphereLight args={DEFAULT_LIGHT_ARGS} />
|
||||||
|
{mission.objects.map((object, i) => renderObject(object, i))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
app/ObserverControls.tsx
Normal file
95
app/ObserverControls.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import {
|
||||||
|
KeyboardControls,
|
||||||
|
KeyboardControlsEntry,
|
||||||
|
Point,
|
||||||
|
PointerLockControls,
|
||||||
|
} from "@react-three/drei";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { useKeyboardControls } from "@react-three/drei";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
enum Controls {
|
||||||
|
forward = "forward",
|
||||||
|
backward = "backward",
|
||||||
|
left = "left",
|
||||||
|
right = "right",
|
||||||
|
up = "up",
|
||||||
|
down = "down",
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_SPEED = 100; // units per second
|
||||||
|
|
||||||
|
function CameraMovement() {
|
||||||
|
const [subscribe, getKeys] = useKeyboardControls<Controls>();
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
// Scratch vectors to avoid allocations each frame
|
||||||
|
const forwardVec = useRef(new THREE.Vector3());
|
||||||
|
const sideVec = useRef(new THREE.Vector3());
|
||||||
|
const moveVec = useRef(new THREE.Vector3());
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
const { forward, backward, left, right, up, down } = getKeys();
|
||||||
|
|
||||||
|
if (!forward && !backward && !left && !right && !up && !down) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const speed = BASE_SPEED;
|
||||||
|
|
||||||
|
// Forward/backward: take complete camera angle into account (including Y)
|
||||||
|
camera.getWorldDirection(forwardVec.current);
|
||||||
|
forwardVec.current.normalize();
|
||||||
|
|
||||||
|
// Left/right: move along XZ plane
|
||||||
|
sideVec.current.crossVectors(camera.up, forwardVec.current).normalize();
|
||||||
|
|
||||||
|
moveVec.current.set(0, 0, 0);
|
||||||
|
|
||||||
|
if (forward) {
|
||||||
|
moveVec.current.add(forwardVec.current);
|
||||||
|
}
|
||||||
|
if (backward) {
|
||||||
|
moveVec.current.sub(forwardVec.current);
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
moveVec.current.add(sideVec.current);
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
moveVec.current.sub(sideVec.current);
|
||||||
|
}
|
||||||
|
if (up) {
|
||||||
|
moveVec.current.y += 1;
|
||||||
|
}
|
||||||
|
if (down) {
|
||||||
|
moveVec.current.y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moveVec.current.lengthSq() > 0) {
|
||||||
|
moveVec.current.normalize().multiplyScalar(speed * delta);
|
||||||
|
camera.position.add(moveVec.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYBOARD_CONTROLS = [
|
||||||
|
{ name: Controls.forward, keys: ["KeyW"] },
|
||||||
|
{ name: Controls.backward, keys: ["KeyS"] },
|
||||||
|
{ name: Controls.left, keys: ["KeyA"] },
|
||||||
|
{ name: Controls.right, keys: ["KeyD"] },
|
||||||
|
{ name: Controls.up, keys: ["Space"] },
|
||||||
|
{ name: Controls.down, keys: ["ShiftLeft", "ShiftRight"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ObserverControls() {
|
||||||
|
return (
|
||||||
|
<KeyboardControls map={KEYBOARD_CONTROLS}>
|
||||||
|
<CameraMovement />
|
||||||
|
<PointerLockControls makeDefault />
|
||||||
|
</KeyboardControls>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
app/SettingsProvider.tsx
Normal file
23
app/SettingsProvider.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React, { useContext, useMemo } from "react";
|
||||||
|
|
||||||
|
const SettingsContext = React.createContext(null);
|
||||||
|
|
||||||
|
export function useSettings() {
|
||||||
|
return useContext(SettingsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsProvider({
|
||||||
|
children,
|
||||||
|
fogEnabled,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
fogEnabled: boolean;
|
||||||
|
}) {
|
||||||
|
const value = useMemo(() => ({ fogEnabled }), [fogEnabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
app/SimGroup.tsx
Normal file
6
app/SimGroup.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { ConsoleObject } from "@/src/mission";
|
||||||
|
import { renderObject } from "./renderObject";
|
||||||
|
|
||||||
|
export function SimGroup({ object }: { object: ConsoleObject }) {
|
||||||
|
return object.children.map((child, i) => renderObject(child, i));
|
||||||
|
}
|
||||||
94
app/Sky.tsx
94
app/Sky.tsx
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { ConsoleObject, getProperty } from "@/src/mission";
|
||||||
|
import { useSettings } from "./SettingsProvider";
|
||||||
|
import { Suspense, useMemo } from "react";
|
||||||
|
import { BASE_URL, getUrlForPath, loadDetailMapList } from "@/src/loaders";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useCubeTexture } from "@react-three/drei";
|
||||||
|
import { Color } from "three";
|
||||||
|
|
||||||
|
const FALLBACK_URL = `${BASE_URL}/black.png`;
|
||||||
|
|
||||||
|
function useDetailMapList(name: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["detailMapList", name],
|
||||||
|
queryFn: () => loadDetailMapList(name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkyBox({ materialList }: { materialList: string }) {
|
||||||
|
const { data: detailMapList } = useDetailMapList(materialList);
|
||||||
|
|
||||||
|
const skyBoxFiles = useMemo(
|
||||||
|
() =>
|
||||||
|
detailMapList
|
||||||
|
? [
|
||||||
|
getUrlForPath(detailMapList[1], FALLBACK_URL), // +x
|
||||||
|
getUrlForPath(detailMapList[3], FALLBACK_URL), // -x
|
||||||
|
getUrlForPath(detailMapList[4], FALLBACK_URL), // +y
|
||||||
|
getUrlForPath(detailMapList[5], FALLBACK_URL), // -y
|
||||||
|
getUrlForPath(detailMapList[0], FALLBACK_URL), // +z
|
||||||
|
getUrlForPath(detailMapList[2], FALLBACK_URL), // -z
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
FALLBACK_URL,
|
||||||
|
FALLBACK_URL,
|
||||||
|
FALLBACK_URL,
|
||||||
|
FALLBACK_URL,
|
||||||
|
FALLBACK_URL,
|
||||||
|
FALLBACK_URL,
|
||||||
|
],
|
||||||
|
[detailMapList]
|
||||||
|
);
|
||||||
|
|
||||||
|
const skyBox = useCubeTexture(skyBoxFiles, { path: "" });
|
||||||
|
|
||||||
|
return <primitive attach="background" object={skyBox} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sky({ object }: { object: ConsoleObject }) {
|
||||||
|
const { fogEnabled } = useSettings();
|
||||||
|
|
||||||
|
// Skybox textures.
|
||||||
|
const materialList = getProperty(object, "materialList")?.value;
|
||||||
|
|
||||||
|
// Fog parameters.
|
||||||
|
// TODO: There can be multiple fog volumes/layers. Render simple fog for now.
|
||||||
|
const fogDistance = useMemo(() => {
|
||||||
|
const distainceString = getProperty(object, "fogDistance")?.value;
|
||||||
|
if (distainceString) {
|
||||||
|
return parseFloat(distainceString);
|
||||||
|
}
|
||||||
|
}, [object]);
|
||||||
|
|
||||||
|
const fogColor = useMemo(() => {
|
||||||
|
const colorString = getProperty(object, "fogColor")?.value;
|
||||||
|
if (colorString) {
|
||||||
|
// `colorString` might specify an alpha value, but three.js doesn't
|
||||||
|
// support opacity on fog or scene backgrounds, so ignore it.
|
||||||
|
const [r, g, b] = colorString.split(" ").map((s) => parseFloat(s));
|
||||||
|
return new Color().setRGB(r, g, b);
|
||||||
|
}
|
||||||
|
}, [object]);
|
||||||
|
|
||||||
|
const backgroundColor = fogColor ? (
|
||||||
|
<color attach="background" args={[fogColor]} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{materialList ? (
|
||||||
|
// If there's a skybox, its textures will need to load. Render just the
|
||||||
|
// fog color as the background in the meantime.
|
||||||
|
<Suspense fallback={backgroundColor}>
|
||||||
|
<SkyBox materialList={materialList} />
|
||||||
|
</Suspense>
|
||||||
|
) : (
|
||||||
|
// If there's no skybox, just render the fog color as the background.
|
||||||
|
backgroundColor
|
||||||
|
)}
|
||||||
|
{fogEnabled && fogDistance && fogColor ? (
|
||||||
|
<fog attach="fog" color={fogColor} near={0} far={fogDistance * 2} />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
import { uint16ToFloat32 } from "@/src/arrayUtils";
|
||||||
|
import { loadTerrain, terrainTextureToUrl } from "@/src/loaders";
|
||||||
|
import {
|
||||||
|
ConsoleObject,
|
||||||
|
getPosition,
|
||||||
|
getProperty,
|
||||||
|
getRotation,
|
||||||
|
getScale,
|
||||||
|
} from "@/src/mission";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Suspense, useCallback, useMemo } from "react";
|
||||||
|
import { useTexture } from "@react-three/drei";
|
||||||
|
import {
|
||||||
|
DataTexture,
|
||||||
|
RedFormat,
|
||||||
|
FloatType,
|
||||||
|
NoColorSpace,
|
||||||
|
NearestFilter,
|
||||||
|
ClampToEdgeWrapping,
|
||||||
|
UnsignedByteType,
|
||||||
|
PlaneGeometry,
|
||||||
|
} from "three";
|
||||||
|
import {
|
||||||
|
setupColor,
|
||||||
|
setupMask,
|
||||||
|
updateTerrainTextureShader,
|
||||||
|
} from "@/src/textureUtils";
|
||||||
|
|
||||||
|
function useTerrain(terrainFile: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["terrain", terrainFile],
|
||||||
|
queryFn: () => loadTerrain(terrainFile),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlendedTerrainTextures({
|
||||||
|
displacementMap,
|
||||||
|
visibilityMask,
|
||||||
|
textureNames,
|
||||||
|
alphaMaps,
|
||||||
|
}: {
|
||||||
|
displacementMap: DataTexture;
|
||||||
|
visibilityMask: DataTexture;
|
||||||
|
textureNames: string[];
|
||||||
|
alphaMaps: Uint8Array[];
|
||||||
|
}) {
|
||||||
|
const baseTextures = useTexture(
|
||||||
|
textureNames.map((name) => terrainTextureToUrl(name)),
|
||||||
|
(textures) => {
|
||||||
|
textures.forEach((tex) => setupColor(tex));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const alphaTextures = useMemo(
|
||||||
|
() => alphaMaps.map((data) => setupMask(data)),
|
||||||
|
[alphaMaps]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBeforeCompile = useCallback(
|
||||||
|
(shader) => {
|
||||||
|
updateTerrainTextureShader({
|
||||||
|
shader,
|
||||||
|
baseTextures,
|
||||||
|
alphaTextures,
|
||||||
|
visibilityMask,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[baseTextures, alphaTextures, visibilityMask]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
displacementMap={displacementMap}
|
||||||
|
map={displacementMap}
|
||||||
|
displacementScale={2048}
|
||||||
|
depthWrite
|
||||||
|
onBeforeCompile={onBeforeCompile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TerrainMaterial({
|
||||||
|
heightMap,
|
||||||
|
textureNames,
|
||||||
|
alphaMaps,
|
||||||
|
emptySquares,
|
||||||
|
}: {
|
||||||
|
heightMap: Uint16Array;
|
||||||
|
emptySquares: number[];
|
||||||
|
textureNames: string[];
|
||||||
|
alphaMaps: Uint8Array[];
|
||||||
|
}) {
|
||||||
|
const displacementMap = useMemo(() => {
|
||||||
|
const f32HeightMap = uint16ToFloat32(heightMap);
|
||||||
|
const displacementMap = new DataTexture(
|
||||||
|
f32HeightMap,
|
||||||
|
256,
|
||||||
|
256,
|
||||||
|
RedFormat,
|
||||||
|
FloatType
|
||||||
|
);
|
||||||
|
displacementMap.colorSpace = NoColorSpace;
|
||||||
|
displacementMap.generateMipmaps = false;
|
||||||
|
displacementMap.needsUpdate = true;
|
||||||
|
return displacementMap;
|
||||||
|
}, [heightMap]);
|
||||||
|
|
||||||
|
const visibilityMask: DataTexture | null = useMemo(() => {
|
||||||
|
if (!emptySquares.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const terrainSize = 256;
|
||||||
|
|
||||||
|
// Create a mask texture (1 = visible, 0 = invisible)
|
||||||
|
const maskData = new Uint8Array(terrainSize * terrainSize);
|
||||||
|
maskData.fill(255); // Start with everything visible
|
||||||
|
|
||||||
|
for (const squareId of emptySquares) {
|
||||||
|
// The squareId encodes position and count:
|
||||||
|
// Bits 0-7: X position (starting position)
|
||||||
|
// Bits 8-15: Y position
|
||||||
|
// Bits 16+: Count (number of consecutive horizontal squares)
|
||||||
|
const x = squareId & 0xff;
|
||||||
|
const y = (squareId >> 8) & 0xff;
|
||||||
|
const count = squareId >> 16;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const px = x + i;
|
||||||
|
const py = y;
|
||||||
|
const index = py * terrainSize + px;
|
||||||
|
if (index >= 0 && index < maskData.length) {
|
||||||
|
maskData[index] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibilityMask = new DataTexture(
|
||||||
|
maskData,
|
||||||
|
terrainSize,
|
||||||
|
terrainSize,
|
||||||
|
RedFormat,
|
||||||
|
UnsignedByteType
|
||||||
|
);
|
||||||
|
visibilityMask.colorSpace = NoColorSpace;
|
||||||
|
visibilityMask.wrapS = visibilityMask.wrapT = ClampToEdgeWrapping;
|
||||||
|
visibilityMask.magFilter = NearestFilter;
|
||||||
|
visibilityMask.minFilter = NearestFilter;
|
||||||
|
visibilityMask.needsUpdate = true;
|
||||||
|
|
||||||
|
return visibilityMask;
|
||||||
|
}, [emptySquares]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
// Render a wireframe while the terrain textures load.
|
||||||
|
<meshStandardMaterial
|
||||||
|
color="rgb(0, 109, 56)"
|
||||||
|
displacementMap={displacementMap}
|
||||||
|
displacementScale={2048}
|
||||||
|
wireframe
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BlendedTerrainTextures
|
||||||
|
displacementMap={displacementMap}
|
||||||
|
visibilityMask={visibilityMask}
|
||||||
|
textureNames={textureNames}
|
||||||
|
alphaMaps={alphaMaps}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TerrainBlock({ object }: { object: ConsoleObject }) {
|
||||||
|
const terrainFile: string = getProperty(object, "terrainFile").value;
|
||||||
|
|
||||||
|
const emptySquares: number[] = useMemo(() => {
|
||||||
|
const emptySquaresString: string | undefined = getProperty(
|
||||||
|
object,
|
||||||
|
"emptySquares"
|
||||||
|
)?.value;
|
||||||
|
|
||||||
|
return emptySquaresString
|
||||||
|
? emptySquaresString.split(" ").map((s) => parseInt(s, 10))
|
||||||
|
: [];
|
||||||
|
}, [object]);
|
||||||
|
|
||||||
|
const position = useMemo(() => getPosition(object), [object]);
|
||||||
|
const scale = useMemo(() => getScale(object), [object]);
|
||||||
|
const q = useMemo(() => getRotation(object), [object]);
|
||||||
|
|
||||||
|
const planeGeometry = useMemo(() => {
|
||||||
|
const geometry = new PlaneGeometry(2048, 2048, 256, 256);
|
||||||
|
geometry.rotateX(-Math.PI / 2);
|
||||||
|
geometry.rotateY(-Math.PI / 2);
|
||||||
|
return geometry;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { data: terrain } = useTerrain(terrainFile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
quaternion={q}
|
||||||
|
position={position}
|
||||||
|
scale={scale}
|
||||||
|
geometry={planeGeometry}
|
||||||
|
>
|
||||||
|
{terrain ? (
|
||||||
|
<TerrainMaterial
|
||||||
|
heightMap={terrain.heightMap}
|
||||||
|
emptySquares={emptySquares}
|
||||||
|
textureNames={terrain.textureNames}
|
||||||
|
alphaMaps={terrain.alphaMaps}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ConsoleObject } from "@/src/mission";
|
||||||
|
|
||||||
|
export function WaterBlock({ object }: { object: ConsoleObject }) {
|
||||||
|
return (
|
||||||
|
<mesh>
|
||||||
|
<boxGeometry />
|
||||||
|
<meshStandardMaterial color="blue" transparent opacity={0.5} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
760
app/page.tsx
760
app/page.tsx
|
|
@ -1,738 +1,42 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import * as THREE from "three";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import {
|
import { Mission } from "./Mission";
|
||||||
getActualResourcePath,
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
getResourceList,
|
import { ObserverControls } from "./ObserverControls";
|
||||||
getSource,
|
import { InspectorControls } from "./InspectorControls";
|
||||||
} from "@/src/manifest";
|
import { SettingsProvider } from "./SettingsProvider";
|
||||||
import { parseTerrainBuffer } from "@/src/terrain";
|
import { PerspectiveCamera } from "@react-three/drei";
|
||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
|
||||||
import { getTerrainFile, iterObjects, parseMissionScript } from "@/src/mission";
|
|
||||||
|
|
||||||
const BASE_URL = "/t2-mapper";
|
// three.js has its own loaders for textures and models, but we need to load other
|
||||||
const RESOURCE_ROOT_URL = `${BASE_URL}/base/`;
|
// stuff too, e.g. missions, terrains, and more. This client is used for those.
|
||||||
|
const queryClient = new QueryClient();
|
||||||
function getUrlForPath(resourcePath: string, fallbackUrl?: string) {
|
|
||||||
resourcePath = getActualResourcePath(resourcePath);
|
|
||||||
let sourcePath: string;
|
|
||||||
try {
|
|
||||||
sourcePath = getSource(resourcePath);
|
|
||||||
} catch (err) {
|
|
||||||
if (fallbackUrl) {
|
|
||||||
return fallbackUrl;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!sourcePath) {
|
|
||||||
return `${RESOURCE_ROOT_URL}${resourcePath}`;
|
|
||||||
} else {
|
|
||||||
return `${RESOURCE_ROOT_URL}@vl2/${sourcePath}/${resourcePath}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function interiorToUrl(name: string) {
|
|
||||||
const difUrl = getUrlForPath(`interiors/${name}`);
|
|
||||||
return difUrl.replace(/\.dif$/i, ".gltf");
|
|
||||||
}
|
|
||||||
|
|
||||||
function terrainTextureToUrl(name: string) {
|
|
||||||
name = name.replace(/^terrain\./, "");
|
|
||||||
return getUrlForPath(`textures/terrain/${name}.png`, `${BASE_URL}/black.png`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function interiorTextureToUrl(name: string) {
|
|
||||||
name = name.replace(/\.\d+$/, "");
|
|
||||||
return getUrlForPath(`textures/${name}.png`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function textureToUrl(name: string) {
|
|
||||||
try {
|
|
||||||
return getUrlForPath(`textures/${name}.png`);
|
|
||||||
} catch (err) {
|
|
||||||
return `${BASE_URL}/black.png`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadDetailMapList(name: string) {
|
|
||||||
const url = getUrlForPath(`textures/${name}`);
|
|
||||||
const res = await fetch(url);
|
|
||||||
const text = await res.text();
|
|
||||||
return text
|
|
||||||
.split(/(?:\r\n|\n|\r)/)
|
|
||||||
.map((line) => `textures/${line.trim().replace(/\.png$/i, "")}.png`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function uint16ToFloat32(src: Uint16Array) {
|
|
||||||
const out = new Float32Array(src.length);
|
|
||||||
for (let i = 0; i < src.length; i++) {
|
|
||||||
out[i] = src[i] / 65535;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMission(name: string) {
|
|
||||||
const res = await fetch(getUrlForPath(`missions/${name}.mis`));
|
|
||||||
const missionScript = await res.text();
|
|
||||||
return parseMissionScript(missionScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTerrain(fileName: string) {
|
|
||||||
const res = await fetch(getUrlForPath(`terrains/${fileName}`));
|
|
||||||
const terrainBuffer = await res.arrayBuffer();
|
|
||||||
return parseTerrainBuffer(terrainBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeRendererToDisplaySize(renderer) {
|
|
||||||
const canvas = renderer.domElement;
|
|
||||||
const width = canvas.clientWidth;
|
|
||||||
const height = canvas.clientHeight;
|
|
||||||
const needResize = canvas.width !== width || canvas.height !== height;
|
|
||||||
if (needResize) {
|
|
||||||
renderer.setSize(width, height, false);
|
|
||||||
}
|
|
||||||
return needResize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const excludeMissions = new Set([
|
|
||||||
"SkiFree",
|
|
||||||
"SkiFree_Daily",
|
|
||||||
"SkiFree_Randomizer",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const missions = getResourceList()
|
|
||||||
.map((resourcePath) => resourcePath.match(/^missions\/(.+)\.mis$/))
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((match) => match[1])
|
|
||||||
.filter((name) => !excludeMissions.has(name));
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const [missionName, setMissionName] = useState("TWL2_WoodyMyrk");
|
const [missionName, setMissionName] = useState("TWL2_WoodyMyrk");
|
||||||
const [fogEnabled, setFogEnabled] = useState(true);
|
const [fogEnabled, setFogEnabled] = useState(false);
|
||||||
const threeContext = useRef<Record<string, any>>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const renderer = new THREE.WebGLRenderer({
|
|
||||||
canvas,
|
|
||||||
antialias: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const textureLoader = new THREE.TextureLoader();
|
|
||||||
const gltfLoader = new GLTFLoader();
|
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
|
||||||
const camera = new THREE.PerspectiveCamera(
|
|
||||||
75,
|
|
||||||
canvas.clientWidth / canvas.clientHeight,
|
|
||||||
0.1,
|
|
||||||
2000
|
|
||||||
);
|
|
||||||
|
|
||||||
function setupColor(tex, repeat = [1, 1]) {
|
|
||||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping; // Still need this for tiling to work
|
|
||||||
tex.colorSpace = THREE.SRGBColorSpace;
|
|
||||||
tex.repeat.set(...repeat);
|
|
||||||
tex.anisotropy = renderer.capabilities.getMaxAnisotropy?.() ?? 16;
|
|
||||||
tex.generateMipmaps = true;
|
|
||||||
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
||||||
tex.magFilter = THREE.LinearFilter;
|
|
||||||
return tex;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupMask(data) {
|
|
||||||
const tex = new THREE.DataTexture(
|
|
||||||
data,
|
|
||||||
256,
|
|
||||||
256,
|
|
||||||
THREE.RedFormat, // 1 channel
|
|
||||||
THREE.UnsignedByteType // 8-bit
|
|
||||||
);
|
|
||||||
|
|
||||||
// Masks should stay linear
|
|
||||||
tex.colorSpace = THREE.NoColorSpace;
|
|
||||||
|
|
||||||
// Set tiling / sampling. For NPOT sizes, disable mips or use power-of-two.
|
|
||||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
|
||||||
tex.generateMipmaps = false; // if width/height are not powers of two
|
|
||||||
tex.minFilter = THREE.LinearFilter; // avoid mips if generateMipmaps=false
|
|
||||||
tex.magFilter = THREE.LinearFilter;
|
|
||||||
|
|
||||||
tex.needsUpdate = true;
|
|
||||||
|
|
||||||
return tex;
|
|
||||||
}
|
|
||||||
|
|
||||||
const skyColor = "rgba(209, 237, 255, 1)";
|
|
||||||
const groundColor = "rgba(186, 200, 181, 1)";
|
|
||||||
const intensity = 2;
|
|
||||||
const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
|
|
||||||
scene.add(light);
|
|
||||||
|
|
||||||
// Free-look camera setup
|
|
||||||
camera.position.set(0, 100, 512);
|
|
||||||
|
|
||||||
const keys = {
|
|
||||||
w: false, a: false, s: false, d: false,
|
|
||||||
shift: false, space: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (e) => {
|
|
||||||
if (e.code === "KeyW") keys.w = true;
|
|
||||||
if (e.code === "KeyA") keys.a = true;
|
|
||||||
if (e.code === "KeyS") keys.s = true;
|
|
||||||
if (e.code === "KeyD") keys.d = true;
|
|
||||||
if (e.code === "ShiftLeft" || e.code === "ShiftRight") keys.shift = true;
|
|
||||||
if (e.code === "Space") keys.space = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyUp = (e) => {
|
|
||||||
if (e.code === "KeyW") keys.w = false;
|
|
||||||
if (e.code === "KeyA") keys.a = false;
|
|
||||||
if (e.code === "KeyS") keys.s = false;
|
|
||||||
if (e.code === "KeyD") keys.d = false;
|
|
||||||
if (e.code === "ShiftLeft" || e.code === "ShiftRight") keys.shift = false;
|
|
||||||
if (e.code === "Space") keys.space = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("keydown", onKeyDown);
|
|
||||||
document.addEventListener("keyup", onKeyUp);
|
|
||||||
|
|
||||||
// Mouse look controls
|
|
||||||
let isPointerLocked = false;
|
|
||||||
const euler = new THREE.Euler(0, 0, 0, 'YXZ');
|
|
||||||
const PI_2 = Math.PI / 2;
|
|
||||||
|
|
||||||
const onMouseMove = (e) => {
|
|
||||||
if (!isPointerLocked) return;
|
|
||||||
|
|
||||||
const movementX = e.movementX || 0;
|
|
||||||
const movementY = e.movementY || 0;
|
|
||||||
|
|
||||||
euler.setFromQuaternion(camera.quaternion);
|
|
||||||
euler.y -= movementX * 0.002;
|
|
||||||
euler.x -= movementY * 0.002;
|
|
||||||
euler.x = Math.max(-PI_2, Math.min(PI_2, euler.x));
|
|
||||||
camera.quaternion.setFromEuler(euler);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerLockChange = () => {
|
|
||||||
isPointerLocked = document.pointerLockElement === canvas;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCanvasClick = () => {
|
|
||||||
if (!isPointerLocked) {
|
|
||||||
canvas.requestPointerLock();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
canvas.addEventListener('click', onCanvasClick);
|
|
||||||
document.addEventListener('pointerlockchange', onPointerLockChange);
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
|
|
||||||
let moveSpeed = 2;
|
|
||||||
|
|
||||||
const onWheel = (e: WheelEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Adjust speed based on wheel direction
|
|
||||||
const delta = e.deltaY > 0 ? .75 : 1.25;
|
|
||||||
moveSpeed = Math.max(0.025, Math.min(4, moveSpeed * delta));
|
|
||||||
|
|
||||||
// Log the new speed for user feedback
|
|
||||||
console.log(`Movement speed: ${moveSpeed.toFixed(3)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
canvas.addEventListener('wheel', onWheel, { passive: false });
|
|
||||||
|
|
||||||
const animate = (t) => {
|
|
||||||
// Free-look movement
|
|
||||||
const forward = new THREE.Vector3();
|
|
||||||
camera.getWorldDirection(forward);
|
|
||||||
|
|
||||||
const right = new THREE.Vector3();
|
|
||||||
right.crossVectors(forward, camera.up).normalize();
|
|
||||||
|
|
||||||
let move = new THREE.Vector3();
|
|
||||||
if (keys.w) move.add(forward);
|
|
||||||
if (keys.s) move.sub(forward);
|
|
||||||
if (keys.a) move.sub(right);
|
|
||||||
if (keys.d) move.add(right);
|
|
||||||
if (keys.space) move.add(camera.up);
|
|
||||||
if (keys.shift) move.sub(camera.up);
|
|
||||||
|
|
||||||
if (move.lengthSq() > 0) {
|
|
||||||
move.normalize().multiplyScalar(moveSpeed);
|
|
||||||
camera.position.add(move);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resizeRendererToDisplaySize(renderer)) {
|
|
||||||
const canvas = renderer.domElement;
|
|
||||||
camera.aspect = canvas.clientWidth / canvas.clientHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
};
|
|
||||||
renderer.setAnimationLoop(animate);
|
|
||||||
|
|
||||||
threeContext.current = {
|
|
||||||
scene,
|
|
||||||
renderer,
|
|
||||||
camera,
|
|
||||||
setupColor,
|
|
||||||
setupMask,
|
|
||||||
textureLoader,
|
|
||||||
gltfLoader,
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", onKeyDown);
|
|
||||||
document.removeEventListener("keyup", onKeyUp);
|
|
||||||
document.removeEventListener('pointerlockchange', onPointerLockChange);
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
|
||||||
canvas.removeEventListener('click', onCanvasClick);
|
|
||||||
canvas.removeEventListener('wheel', onWheel);
|
|
||||||
renderer.setAnimationLoop(null);
|
|
||||||
renderer.dispose();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const {
|
|
||||||
scene,
|
|
||||||
camera,
|
|
||||||
setupColor,
|
|
||||||
setupMask,
|
|
||||||
textureLoader,
|
|
||||||
gltfLoader,
|
|
||||||
} = threeContext.current;
|
|
||||||
|
|
||||||
let cancel = false;
|
|
||||||
let root: THREE.Group;
|
|
||||||
|
|
||||||
async function loadMap() {
|
|
||||||
const mission = await loadMission(missionName);
|
|
||||||
const terrainFile = getTerrainFile(mission);
|
|
||||||
const terrain = await loadTerrain(terrainFile);
|
|
||||||
|
|
||||||
const layerCount = terrain.textureNames.length;
|
|
||||||
|
|
||||||
const baseTextures = terrain.textureNames.map((name) => {
|
|
||||||
return setupColor(textureLoader.load(terrainTextureToUrl(name)));
|
|
||||||
});
|
|
||||||
|
|
||||||
const alphaTextures = terrain.alphaMaps.map((data) => setupMask(data));
|
|
||||||
|
|
||||||
const planeSize = 2048;
|
|
||||||
const geom = new THREE.PlaneGeometry(planeSize, planeSize, 256, 256);
|
|
||||||
geom.rotateX(-Math.PI / 2);
|
|
||||||
geom.rotateY(-Math.PI / 2);
|
|
||||||
|
|
||||||
// Find TerrainBlock properties for empty squares
|
|
||||||
let emptySquares: number[] | null = null;
|
|
||||||
for (const obj of iterObjects(mission.objects)) {
|
|
||||||
if (obj.className === "TerrainBlock") {
|
|
||||||
const emptySquaresStr = obj.properties.find((p: any) => p.target.name === "emptySquares")?.value;
|
|
||||||
if (emptySquaresStr) {
|
|
||||||
emptySquares = emptySquaresStr.split(" ").map((s: string) => parseInt(s))
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const f32HeightMap = uint16ToFloat32(terrain.heightMap);
|
|
||||||
|
|
||||||
// Create a visibility mask for empty squares
|
|
||||||
let visibilityMask: THREE.DataTexture | null = null;
|
|
||||||
if (emptySquares) {
|
|
||||||
const terrainSize = 256;
|
|
||||||
|
|
||||||
// Create a mask texture (1 = visible, 0 = invisible)
|
|
||||||
const maskData = new Uint8Array(terrainSize * terrainSize);
|
|
||||||
maskData.fill(255); // Start with everything visible
|
|
||||||
|
|
||||||
for (const squareId of emptySquares) {
|
|
||||||
// The squareId encodes position and count:
|
|
||||||
// Bits 0-7: X position (starting position)
|
|
||||||
// Bits 8-15: Y position
|
|
||||||
// Bits 16+: Count (number of consecutive horizontal squares)
|
|
||||||
const x = (squareId & 0xFF);
|
|
||||||
const y = (squareId >> 8) & 0xFF;
|
|
||||||
const count = (squareId >> 16);
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const px = x + i;
|
|
||||||
const py = y;
|
|
||||||
const index = py * terrainSize + px;
|
|
||||||
if (index >= 0 && index < maskData.length) {
|
|
||||||
maskData[index] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visibilityMask = new THREE.DataTexture(
|
|
||||||
maskData,
|
|
||||||
terrainSize,
|
|
||||||
terrainSize,
|
|
||||||
THREE.RedFormat,
|
|
||||||
THREE.UnsignedByteType
|
|
||||||
);
|
|
||||||
visibilityMask.colorSpace = THREE.NoColorSpace;
|
|
||||||
visibilityMask.wrapS = visibilityMask.wrapT = THREE.ClampToEdgeWrapping;
|
|
||||||
visibilityMask.magFilter = THREE.NearestFilter;
|
|
||||||
visibilityMask.minFilter = THREE.NearestFilter;
|
|
||||||
visibilityMask.needsUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const heightMap = new THREE.DataTexture(
|
|
||||||
f32HeightMap,
|
|
||||||
256,
|
|
||||||
256,
|
|
||||||
THREE.RedFormat,
|
|
||||||
THREE.FloatType
|
|
||||||
);
|
|
||||||
heightMap.colorSpace = THREE.NoColorSpace;
|
|
||||||
heightMap.generateMipmaps = false;
|
|
||||||
heightMap.needsUpdate = true;
|
|
||||||
|
|
||||||
// Start with a standard material; assign map to trigger USE_MAP/vMapUv
|
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
|
||||||
// map: base0,
|
|
||||||
displacementMap: heightMap,
|
|
||||||
map: heightMap,
|
|
||||||
displacementScale: 2048,
|
|
||||||
depthWrite: true,
|
|
||||||
// displacementBias: -128,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Inject our 4-layer blend before lighting
|
|
||||||
mat.onBeforeCompile = (shader) => {
|
|
||||||
// uniforms for 4 albedo maps + 3 alpha masks
|
|
||||||
baseTextures.forEach((tex, i) => {
|
|
||||||
shader.uniforms[`albedo${i}`] = { value: tex };
|
|
||||||
});
|
|
||||||
alphaTextures.forEach((tex, i) => {
|
|
||||||
if (i > 0) {
|
|
||||||
shader.uniforms[`mask${i}`] = { value: tex };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add visibility mask uniform if we have empty squares
|
|
||||||
if (visibilityMask) {
|
|
||||||
shader.uniforms.visibilityMask = { value: visibilityMask };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add per-texture tiling uniforms
|
|
||||||
baseTextures.forEach((tex, i) => {
|
|
||||||
shader.uniforms[`tiling${i}`] = {
|
|
||||||
value: Math.min(
|
|
||||||
512,
|
|
||||||
{ 0: 16, 1: 16, 2: 32, 3: 32, 4: 32, 5: 32 }[i]
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Declare our uniforms at the top of the fragment shader
|
|
||||||
shader.fragmentShader =
|
|
||||||
`
|
|
||||||
uniform sampler2D albedo0;
|
|
||||||
uniform sampler2D albedo1;
|
|
||||||
uniform sampler2D albedo2;
|
|
||||||
uniform sampler2D albedo3;
|
|
||||||
uniform sampler2D albedo4;
|
|
||||||
uniform sampler2D albedo5;
|
|
||||||
uniform sampler2D mask1;
|
|
||||||
uniform sampler2D mask2;
|
|
||||||
uniform sampler2D mask3;
|
|
||||||
uniform sampler2D mask4;
|
|
||||||
uniform sampler2D mask5;
|
|
||||||
uniform float tiling0;
|
|
||||||
uniform float tiling1;
|
|
||||||
uniform float tiling2;
|
|
||||||
uniform float tiling3;
|
|
||||||
uniform float tiling4;
|
|
||||||
uniform float tiling5;
|
|
||||||
${visibilityMask ? 'uniform sampler2D visibilityMask;' : ''}
|
|
||||||
` + shader.fragmentShader;
|
|
||||||
|
|
||||||
if (visibilityMask) {
|
|
||||||
const clippingPlaceholder = '#include <clipping_planes_fragment>';
|
|
||||||
shader.fragmentShader = shader.fragmentShader.replace(
|
|
||||||
clippingPlaceholder,
|
|
||||||
`${clippingPlaceholder}
|
|
||||||
// Early discard for invisible areas (before fog/lighting)
|
|
||||||
float visibility = texture2D(visibilityMask, vMapUv).r;
|
|
||||||
if (visibility < 0.5) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the default map sampling block with our layered blend.
|
|
||||||
// We rely on vMapUv provided by USE_MAP.
|
|
||||||
shader.fragmentShader = shader.fragmentShader.replace(
|
|
||||||
"#include <map_fragment>",
|
|
||||||
`
|
|
||||||
// Sample base albedo layers (sRGB textures auto-decoded to linear)
|
|
||||||
vec2 baseUv = vMapUv;
|
|
||||||
vec3 c0 = texture2D(albedo0, baseUv * vec2(tiling0)).rgb;
|
|
||||||
${
|
|
||||||
layerCount > 1
|
|
||||||
? `vec3 c1 = texture2D(albedo1, baseUv * vec2(tiling1)).rgb;`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
layerCount > 2
|
|
||||||
? `vec3 c2 = texture2D(albedo2, baseUv * vec2(tiling2)).rgb;`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
layerCount > 3
|
|
||||||
? `vec3 c3 = texture2D(albedo3, baseUv * vec2(tiling3)).rgb;`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
layerCount > 4
|
|
||||||
? `vec3 c4 = texture2D(albedo4, baseUv * vec2(tiling4)).rgb;`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
layerCount > 5
|
|
||||||
? `vec3 c5 = texture2D(albedo5, baseUv * vec2(tiling5)).rgb;`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample linear masks (use R channel)
|
|
||||||
float a1 = texture2D(mask1, baseUv).r;
|
|
||||||
${layerCount > 1 ? `float a2 = texture2D(mask2, baseUv).r;` : ""}
|
|
||||||
${layerCount > 2 ? `float a3 = texture2D(mask3, baseUv).r;` : ""}
|
|
||||||
${layerCount > 3 ? `float a4 = texture2D(mask4, baseUv).r;` : ""}
|
|
||||||
${layerCount > 4 ? `float a5 = texture2D(mask5, baseUv).r;` : ""}
|
|
||||||
|
|
||||||
// Bottom-up compositing: each mask tells how much the higher layer replaces lower
|
|
||||||
${layerCount > 1 ? `vec3 blended = mix(c0, c1, clamp(a1, 0.0, 1.0));` : ""}
|
|
||||||
${layerCount > 2 ? `blended = mix(blended, c2, clamp(a2, 0.0, 1.0));` : ""}
|
|
||||||
${layerCount > 3 ? `blended = mix(blended, c3, clamp(a3, 0.0, 1.0));` : ""}
|
|
||||||
${layerCount > 4 ? `blended = mix(blended, c4, clamp(a4, 0.0, 1.0));` : ""}
|
|
||||||
${layerCount > 5 ? `blended = mix(blended, c5, clamp(a5, 0.0, 1.0));` : ""}
|
|
||||||
|
|
||||||
// Assign to diffuseColor before lighting
|
|
||||||
diffuseColor.rgb = ${layerCount > 1 ? "blended" : "c0"};
|
|
||||||
`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
root = new THREE.Group();
|
|
||||||
|
|
||||||
const terrainMesh = new THREE.Mesh(geom, mat);
|
|
||||||
root.add(terrainMesh);
|
|
||||||
|
|
||||||
for (const obj of iterObjects(mission.objects)) {
|
|
||||||
const getProperty = (name) => {
|
|
||||||
const property = obj.properties.find((p) => p.target.name === name);
|
|
||||||
// console.log({ name, property });
|
|
||||||
return property;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPosition = () => {
|
|
||||||
const position = getProperty("position")?.value ?? "0 0 0";
|
|
||||||
const [x, z, y] = position.split(" ").map((s) => parseFloat(s));
|
|
||||||
return [x, y, z];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getScale = () => {
|
|
||||||
const scale = getProperty("scale")?.value ?? "1 1 1";
|
|
||||||
const [scaleX, scaleZ, scaleY] = scale
|
|
||||||
.split(" ")
|
|
||||||
.map((s) => parseFloat(s));
|
|
||||||
return [scaleX, scaleY, scaleZ];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRotation = (isInterior = false) => {
|
|
||||||
const rotation = getProperty("rotation")?.value ?? "1 0 0 0";
|
|
||||||
const [ax, az, ay, angle] = rotation
|
|
||||||
.split(" ")
|
|
||||||
.map((s) => parseFloat(s));
|
|
||||||
|
|
||||||
if (isInterior) {
|
|
||||||
// For interiors: Apply coordinate system transformation
|
|
||||||
// 1. Convert rotation axis from source coords (ax, az, ay) to Three.js coords
|
|
||||||
// 2. Apply -90 Y rotation to align coordinate systems
|
|
||||||
const sourceRotation = new THREE.Quaternion().setFromAxisAngle(
|
|
||||||
new THREE.Vector3(az, ay, ax),
|
|
||||||
-angle * (Math.PI / 180)
|
|
||||||
);
|
|
||||||
const coordSystemFix = new THREE.Quaternion().setFromAxisAngle(
|
|
||||||
new THREE.Vector3(0, 1, 0),
|
|
||||||
Math.PI / 2
|
|
||||||
);
|
|
||||||
return coordSystemFix.multiply(sourceRotation);
|
|
||||||
} else {
|
|
||||||
// For other objects (terrain, etc)
|
|
||||||
return new THREE.Quaternion().setFromAxisAngle(
|
|
||||||
new THREE.Vector3(ax, ay, -az),
|
|
||||||
angle * (Math.PI / 180)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (obj.className) {
|
|
||||||
case "TerrainBlock": {
|
|
||||||
const [x, y, z] = getPosition();
|
|
||||||
camera.position.set(x - 512, y + 256, z - 512);
|
|
||||||
const [scaleX, scaleY, scaleZ] = getScale();
|
|
||||||
const q = getRotation();
|
|
||||||
terrainMesh.position.set(x, y, z);
|
|
||||||
terrainMesh.scale.set(scaleX, scaleY, scaleZ);
|
|
||||||
terrainMesh.quaternion.copy(q);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Sky": {
|
|
||||||
const materialList = getProperty("materialList")?.value;
|
|
||||||
if (materialList) {
|
|
||||||
const detailMapList = await loadDetailMapList(materialList);
|
|
||||||
const skyLoader = new THREE.CubeTextureLoader();
|
|
||||||
const fallbackUrl = `${BASE_URL}/black.png`;
|
|
||||||
const texture = skyLoader.load([
|
|
||||||
getUrlForPath(detailMapList[1], fallbackUrl), // +x
|
|
||||||
getUrlForPath(detailMapList[3], fallbackUrl), // -x
|
|
||||||
getUrlForPath(detailMapList[4], fallbackUrl), // +y
|
|
||||||
getUrlForPath(detailMapList[5], fallbackUrl), // -y
|
|
||||||
getUrlForPath(detailMapList[0], fallbackUrl), // +z
|
|
||||||
getUrlForPath(detailMapList[2], fallbackUrl), // -z
|
|
||||||
]);
|
|
||||||
scene.background = texture;
|
|
||||||
}
|
|
||||||
const fogDistance = getProperty("fogDistance")?.value;
|
|
||||||
const fogColor = getProperty("fogColor")?.value;
|
|
||||||
if (fogDistance && fogColor) {
|
|
||||||
const distance = parseFloat(fogDistance);
|
|
||||||
const [r, g, b] = fogColor.split(" ").map((s) => parseFloat(s));
|
|
||||||
const color = new THREE.Color().setRGB(r, g, b);
|
|
||||||
const fog = new THREE.Fog(color, 0, distance * 2);
|
|
||||||
if (fogEnabled) {
|
|
||||||
scene.fog = fog;
|
|
||||||
} else {
|
|
||||||
scene._fog = fog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "InteriorInstance": {
|
|
||||||
const [z, y, x] = getPosition();
|
|
||||||
const [scaleX, scaleY, scaleZ] = getScale();
|
|
||||||
const q = getRotation(true);
|
|
||||||
const interiorFile = getProperty("interiorFile").value;
|
|
||||||
gltfLoader.load(interiorToUrl(interiorFile), (gltf) => {
|
|
||||||
gltf.scene.traverse((o) => {
|
|
||||||
if (o.material?.name) {
|
|
||||||
const name = o.material.name;
|
|
||||||
try {
|
|
||||||
const tex = textureLoader.load(interiorTextureToUrl(name));
|
|
||||||
o.material.map = setupColor(tex);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
o.material.needsUpdate = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const interior = gltf.scene;
|
|
||||||
interior.position.set(x - 1024, y, z - 1024);
|
|
||||||
interior.scale.set(-scaleX, scaleY, -scaleZ);
|
|
||||||
interior.quaternion.copy(q);
|
|
||||||
root.add(interior);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "WaterBlock": {
|
|
||||||
const [z, y, x] = getPosition();
|
|
||||||
const [scaleZ, scaleY, scaleX] = getScale();
|
|
||||||
const q = getRotation(true);
|
|
||||||
|
|
||||||
const surfaceTexture =
|
|
||||||
getProperty("surfaceTexture")?.value ?? "liquidTiles/BlueWater";
|
|
||||||
|
|
||||||
const geometry = new THREE.BoxGeometry(scaleZ, scaleY, scaleX);
|
|
||||||
const material = new THREE.MeshStandardMaterial({
|
|
||||||
map: setupColor(
|
|
||||||
textureLoader.load(textureToUrl(surfaceTexture)),
|
|
||||||
[8, 8]
|
|
||||||
),
|
|
||||||
// transparent: true,
|
|
||||||
opacity: 0.8,
|
|
||||||
});
|
|
||||||
const water = new THREE.Mesh(geometry, material);
|
|
||||||
|
|
||||||
water.position.set(
|
|
||||||
x - 1024 + scaleX / 2,
|
|
||||||
y + scaleY / 2,
|
|
||||||
z - 1024 + scaleZ / 2
|
|
||||||
);
|
|
||||||
water.quaternion.copy(q);
|
|
||||||
|
|
||||||
root.add(water);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.add(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMap();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancel = true;
|
|
||||||
root.removeFromParent();
|
|
||||||
};
|
|
||||||
}, [missionName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { scene } = threeContext.current;
|
|
||||||
if (fogEnabled) {
|
|
||||||
scene.fog = scene._fog ?? null;
|
|
||||||
scene._fog = null;
|
|
||||||
scene.needsUpdate = true;
|
|
||||||
} else {
|
|
||||||
scene._fog = scene.fog;
|
|
||||||
scene.fog = null;
|
|
||||||
scene.needsUpdate = true;
|
|
||||||
}
|
|
||||||
}, [fogEnabled]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<SettingsProvider fogEnabled={fogEnabled}>
|
||||||
<canvas ref={canvasRef} id="canvas" />
|
<QueryClientProvider client={queryClient}>
|
||||||
<div id="controls">
|
<main>
|
||||||
<select
|
<Canvas>
|
||||||
id="missionList"
|
<ObserverControls />
|
||||||
value={missionName}
|
<Mission key={missionName} name={missionName} />
|
||||||
onChange={(e) => setMissionName(e.target.value)}
|
<PerspectiveCamera
|
||||||
>
|
makeDefault
|
||||||
{missions.map((missionName) => (
|
position={[-512, 256, -512]}
|
||||||
<option key={missionName}>{missionName}</option>
|
fov={90}
|
||||||
))}
|
/>
|
||||||
</select>
|
</Canvas>
|
||||||
<div className="CheckboxField">
|
<InspectorControls
|
||||||
<input
|
missionName={missionName}
|
||||||
id="fogInput"
|
onChangeMission={setMissionName}
|
||||||
type="checkbox"
|
fogEnabled={fogEnabled}
|
||||||
checked={fogEnabled}
|
onChangeFogEnabled={setFogEnabled}
|
||||||
onChange={(event) => {
|
|
||||||
setFogEnabled(event.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="fogInput">Fog?</label>
|
</main>
|
||||||
</div>
|
</QueryClientProvider>
|
||||||
</div>
|
</SettingsProvider>
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
app/renderObject.tsx
Normal file
19
app/renderObject.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ConsoleObject } from "@/src/mission";
|
||||||
|
import { TerrainBlock } from "./TerrainBlock";
|
||||||
|
import { WaterBlock } from "./WaterBlock";
|
||||||
|
import { SimGroup } from "./SimGroup";
|
||||||
|
import { InteriorInstance } from "./InteriorInstance";
|
||||||
|
import { Sky } from "./Sky";
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
SimGroup,
|
||||||
|
TerrainBlock,
|
||||||
|
WaterBlock,
|
||||||
|
InteriorInstance,
|
||||||
|
Sky,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderObject(object: ConsoleObject, key: string | number) {
|
||||||
|
const Component = componentMap[object.className];
|
||||||
|
return Component ? <Component key={key} object={object} /> : null;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
background: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
@ -10,8 +11,7 @@ html {
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas {
|
main {
|
||||||
display: block;
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
408
package-lock.json
generated
408
package-lock.json
generated
|
|
@ -9,7 +9,9 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-three/drei": "^10.7.6",
|
||||||
"@react-three/fiber": "^9.3.0",
|
"@react-three/fiber": "^9.3.0",
|
||||||
|
"@tanstack/react-query": "^5.90.8",
|
||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|
@ -40,7 +42,6 @@
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
|
|
@ -954,6 +955,24 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mediapipe/tasks-vision": {
|
||||||
|
"version": "0.10.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||||
|
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@monogrid/gainmap-js": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"promise-worker-transferable": "^1.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">= 0.159.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "15.5.2",
|
"version": "15.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz",
|
||||||
|
|
@ -1101,6 +1120,46 @@
|
||||||
"node": ">=20.8"
|
"node": ">=20.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-three/drei": {
|
||||||
|
"version": "10.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.6.tgz",
|
||||||
|
"integrity": "sha512-ZSFwRlRaa4zjtB7yHO6Q9xQGuyDCzE7whXBhum92JslcMRC3aouivp0rAzszcVymIoJx6PXmibyP+xr+zKdwLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.0",
|
||||||
|
"@mediapipe/tasks-vision": "0.10.17",
|
||||||
|
"@monogrid/gainmap-js": "^3.0.6",
|
||||||
|
"@use-gesture/react": "^10.3.1",
|
||||||
|
"camera-controls": "^3.1.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"detect-gpu": "^5.0.56",
|
||||||
|
"glsl-noise": "^0.0.0",
|
||||||
|
"hls.js": "^1.5.17",
|
||||||
|
"maath": "^0.10.8",
|
||||||
|
"meshline": "^3.3.1",
|
||||||
|
"stats-gl": "^2.2.8",
|
||||||
|
"stats.js": "^0.17.0",
|
||||||
|
"suspend-react": "^0.1.3",
|
||||||
|
"three-mesh-bvh": "^0.8.3",
|
||||||
|
"three-stdlib": "^2.35.6",
|
||||||
|
"troika-three-text": "^0.52.4",
|
||||||
|
"tunnel-rat": "^0.1.2",
|
||||||
|
"use-sync-external-store": "^1.4.0",
|
||||||
|
"utility-types": "^3.11.0",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-three/fiber": "^9.0.0",
|
||||||
|
"react": "^19",
|
||||||
|
"react-dom": "^19",
|
||||||
|
"three": ">=0.159"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-three/fiber": {
|
"node_modules/@react-three/fiber": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.3.0.tgz",
|
||||||
|
|
@ -1166,11 +1225,42 @@
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.90.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.8.tgz",
|
||||||
|
"integrity": "sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.90.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.8.tgz",
|
||||||
|
"integrity": "sha512-/3b9QGzkf4rE5/miL6tyhldQRlLXzMHcySOm/2Tm2OLEFE9P1ImkH0+OviDBSvyAvtAOJocar5xhd7vxdLi3aQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tweenjs/tween.js": {
|
"node_modules/@tweenjs/tween.js": {
|
||||||
"version": "23.1.3",
|
"version": "23.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
"dev": true,
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/draco3d": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
|
|
@ -1183,6 +1273,12 @@
|
||||||
"undici-types": "~7.10.0"
|
"undici-types": "~7.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/offscreencanvas": {
|
||||||
|
"version": "2019.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
||||||
|
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.12",
|
"version": "19.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
||||||
|
|
@ -1206,15 +1302,14 @@
|
||||||
"version": "0.17.4",
|
"version": "0.17.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/three": {
|
"node_modules/@types/three": {
|
||||||
"version": "0.180.0",
|
"version": "0.180.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
||||||
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
"@tweenjs/tween.js": "~23.1.3",
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
|
@ -1241,11 +1336,28 @@
|
||||||
"integrity": "sha512-GPe4AsfOSpqWd3xA/0gwoKod13ChcfV67trvxaW2krUbgb9gxQjnCx8zGshzMl8LSHZlNH5gQ8LNScsDuc7nGQ==",
|
"integrity": "sha512-GPe4AsfOSpqWd3xA/0gwoKod13ChcfV67trvxaW2krUbgb9gxQjnCx8zGshzMl8LSHZlNH5gQ8LNScsDuc7nGQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@use-gesture/core": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@use-gesture/react": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@use-gesture/core": "10.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@webgpu/types": {
|
"node_modules/@webgpu/types": {
|
||||||
"version": "0.1.64",
|
"version": "0.1.64",
|
||||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz",
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz",
|
||||||
"integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==",
|
"integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
|
|
@ -1294,6 +1406,15 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bidi-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bluebird": {
|
"node_modules/bluebird": {
|
||||||
"version": "3.7.2",
|
"version": "3.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||||
|
|
@ -1324,6 +1445,19 @@
|
||||||
"ieee754": "^1.2.1"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camera-controls": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.11.0",
|
||||||
|
"npm": ">=10.8.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.126.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001741",
|
"version": "1.0.30001741",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
|
||||||
|
|
@ -1411,11 +1545,28 @@
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "src/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
|
|
@ -1432,6 +1583,15 @@
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-gpu": {
|
||||||
|
"version": "5.0.70",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
|
||||||
|
"integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"webgl-constants": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
|
|
@ -1442,6 +1602,12 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/draco3d": {
|
||||||
|
"version": "1.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
|
||||||
|
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/duplexer2": {
|
"node_modules/duplexer2": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||||
|
|
@ -1511,7 +1677,6 @@
|
||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
|
|
@ -1597,12 +1762,24 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glsl-noise": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/hls.js": {
|
||||||
|
"version": "1.6.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz",
|
||||||
|
"integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
|
@ -1623,6 +1800,12 @@
|
||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
|
@ -1646,6 +1829,12 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
|
@ -1656,7 +1845,6 @@
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/its-fine": {
|
"node_modules/its-fine": {
|
||||||
|
|
@ -1708,6 +1896,15 @@
|
||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "11.2.1",
|
"version": "11.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
|
||||||
|
|
@ -1718,11 +1915,29 @@
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/maath": {
|
||||||
|
"version": "0.10.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
||||||
|
"integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/three": ">=0.134.0",
|
||||||
|
"three": ">=0.134.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/meshline": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.137"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/meshoptimizer": {
|
"node_modules/meshoptimizer": {
|
||||||
"version": "0.22.0",
|
"version": "0.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
|
||||||
"integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
|
"integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
|
|
@ -1838,7 +2053,6 @@
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
|
|
@ -1913,12 +2127,28 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/potpack": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/promise-worker-transferable": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"is-promise": "^2.1.0",
|
||||||
|
"lie": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.1.1",
|
"version": "19.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||||
|
|
@ -1993,6 +2223,15 @@
|
||||||
"util-deprecate": "~1.0.1"
|
"util-deprecate": "~1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-pkg-maps": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
|
@ -2095,7 +2334,6 @@
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shebang-regex": "^3.0.0"
|
"shebang-regex": "^3.0.0"
|
||||||
|
|
@ -2108,7 +2346,6 @@
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
|
|
@ -2156,6 +2393,32 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stats-gl": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/three": "*",
|
||||||
|
"three": "^0.170.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/three": "*",
|
||||||
|
"three": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stats-gl/node_modules/three": {
|
||||||
|
"version": "0.170.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
|
||||||
|
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/stats.js": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
|
@ -2308,6 +2571,68 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/three-mesh-bvh": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">= 0.159.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/three-stdlib": {
|
||||||
|
"version": "2.36.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
|
||||||
|
"integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/draco3d": "^1.4.0",
|
||||||
|
"@types/offscreencanvas": "^2019.6.4",
|
||||||
|
"@types/webxr": "^0.5.2",
|
||||||
|
"draco3d": "^1.4.1",
|
||||||
|
"fflate": "^0.6.9",
|
||||||
|
"potpack": "^1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.128.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/three-stdlib/node_modules/fflate": {
|
||||||
|
"version": "0.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||||
|
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/troika-three-text": {
|
||||||
|
"version": "0.52.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||||
|
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bidi-js": "^1.0.2",
|
||||||
|
"troika-three-utils": "^0.52.4",
|
||||||
|
"troika-worker-utils": "^0.52.0",
|
||||||
|
"webgl-sdf-generator": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.125.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/troika-three-utils": {
|
||||||
|
"version": "0.52.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
|
||||||
|
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.125.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/troika-worker-utils": {
|
||||||
|
"version": "0.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
|
||||||
|
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
|
@ -2334,6 +2659,43 @@
|
||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-rat": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"zustand": "^4.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tunnel-rat/node_modules/zustand": {
|
||||||
|
"version": "4.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||||
|
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.8",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||||
|
|
@ -2382,7 +2744,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2393,11 +2754,30 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utility-types": {
|
||||||
|
"version": "3.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
|
||||||
|
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webgl-constants": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
|
||||||
|
},
|
||||||
|
"node_modules/webgl-sdf-generator": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
"start": "next dev"
|
"start": "next dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-three/drei": "^10.7.6",
|
||||||
"@react-three/fiber": "^9.3.0",
|
"@react-three/fiber": "^9.3.0",
|
||||||
|
"@tanstack/react-query": "^5.90.8",
|
||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|
|
||||||
7
src/arrayUtils.ts
Normal file
7
src/arrayUtils.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function uint16ToFloat32(src: Uint16Array) {
|
||||||
|
const out = new Float32Array(src.length);
|
||||||
|
for (let i = 0; i < src.length; i++) {
|
||||||
|
out[i] = src[i] / 65535;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
69
src/loaders.ts
Normal file
69
src/loaders.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { getActualResourcePath, getSource } from "./manifest";
|
||||||
|
import { parseMissionScript } from "./mission";
|
||||||
|
import { parseTerrainBuffer } from "./terrain";
|
||||||
|
|
||||||
|
export const BASE_URL = "/t2-mapper";
|
||||||
|
export const RESOURCE_ROOT_URL = `${BASE_URL}/base/`;
|
||||||
|
|
||||||
|
export function getUrlForPath(resourcePath: string, fallbackUrl?: string) {
|
||||||
|
resourcePath = getActualResourcePath(resourcePath);
|
||||||
|
let sourcePath: string;
|
||||||
|
try {
|
||||||
|
sourcePath = getSource(resourcePath);
|
||||||
|
} catch (err) {
|
||||||
|
if (fallbackUrl) {
|
||||||
|
return fallbackUrl;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sourcePath) {
|
||||||
|
return `${RESOURCE_ROOT_URL}${resourcePath}`;
|
||||||
|
} else {
|
||||||
|
return `${RESOURCE_ROOT_URL}@vl2/${sourcePath}/${resourcePath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interiorToUrl(name: string) {
|
||||||
|
const difUrl = getUrlForPath(`interiors/${name}`);
|
||||||
|
return difUrl.replace(/\.dif$/i, ".gltf");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function terrainTextureToUrl(name: string) {
|
||||||
|
name = name.replace(/^terrain\./, "");
|
||||||
|
return getUrlForPath(`textures/terrain/${name}.png`, `${BASE_URL}/black.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interiorTextureToUrl(name: string) {
|
||||||
|
name = name.replace(/\.\d+$/, "");
|
||||||
|
return getUrlForPath(`textures/${name}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function textureToUrl(name: string) {
|
||||||
|
try {
|
||||||
|
return getUrlForPath(`textures/${name}.png`);
|
||||||
|
} catch (err) {
|
||||||
|
return `${BASE_URL}/black.png`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadDetailMapList(name: string) {
|
||||||
|
const url = getUrlForPath(`textures/${name}`);
|
||||||
|
const res = await fetch(url);
|
||||||
|
const text = await res.text();
|
||||||
|
return text
|
||||||
|
.split(/(?:\r\n|\n|\r)/)
|
||||||
|
.map((line) => `textures/${line.trim().replace(/\.png$/i, "")}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadMission(name: string) {
|
||||||
|
const res = await fetch(getUrlForPath(`missions/${name}.mis`));
|
||||||
|
const missionScript = await res.text();
|
||||||
|
return parseMissionScript(missionScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTerrain(fileName: string) {
|
||||||
|
const res = await fetch(getUrlForPath(`terrains/${fileName}`));
|
||||||
|
const terrainBuffer = await res.arrayBuffer();
|
||||||
|
return parseTerrainBuffer(terrainBuffer);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import parser from "@/generated/mission.cjs";
|
import parser from "@/generated/mission.cjs";
|
||||||
|
import { Quaternion, Vector3 } from "three";
|
||||||
|
|
||||||
const definitionComment = /^ (DisplayName|MissionTypes) = (.+)$/;
|
const definitionComment = /^ (DisplayName|MissionTypes) = (.+)$/;
|
||||||
const sectionBeginComment = /^--- ([A-Z ]+) BEGIN ---$/;
|
const sectionBeginComment = /^--- ([A-Z ]+) BEGIN ---$/;
|
||||||
|
|
@ -175,7 +176,8 @@ export function parseMissionScript(script) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mission = ReturnType<typeof parseMissionScript>;
|
export type Mission = ReturnType<typeof parseMissionScript>;
|
||||||
|
export type ConsoleObject = Mission["objects"][number];
|
||||||
|
|
||||||
export function* iterObjects(objectList) {
|
export function* iterObjects(objectList) {
|
||||||
for (const obj of objectList) {
|
for (const obj of objectList) {
|
||||||
|
|
@ -186,18 +188,62 @@ export function* iterObjects(objectList) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTerrainFile(mission: Mission) {
|
export function getTerrainBlock(mission: Mission): ConsoleObject {
|
||||||
let terrainBlock;
|
|
||||||
for (const obj of iterObjects(mission.objects)) {
|
for (const obj of iterObjects(mission.objects)) {
|
||||||
if (obj.className === "TerrainBlock") {
|
if (obj.className === "TerrainBlock") {
|
||||||
terrainBlock = obj;
|
return obj;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!terrainBlock) {
|
throw new Error("No TerrainBlock found!");
|
||||||
throw new Error("Error!");
|
}
|
||||||
}
|
|
||||||
|
export function getTerrainFile(mission: Mission) {
|
||||||
|
const terrainBlock = getTerrainBlock(mission);
|
||||||
return terrainBlock.properties.find(
|
return terrainBlock.properties.find(
|
||||||
(prop) => prop.target.name === "terrainFile"
|
(prop) => prop.target.name === "terrainFile"
|
||||||
).value;
|
).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProperty(obj: ConsoleObject, name: string) {
|
||||||
|
const property = obj.properties.find((p) => p.target.name === name);
|
||||||
|
// console.log({ name, property });
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPosition(obj: ConsoleObject): [number, number, number] {
|
||||||
|
const position = getProperty(obj, "position")?.value ?? "0 0 0";
|
||||||
|
const [x, z, y] = position.split(" ").map((s) => parseFloat(s));
|
||||||
|
return [x, y, z];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScale(obj: ConsoleObject): [number, number, number] {
|
||||||
|
const scale = getProperty(obj, "scale")?.value ?? "1 1 1";
|
||||||
|
const [scaleX, scaleZ, scaleY] = scale.split(" ").map((s) => parseFloat(s));
|
||||||
|
return [scaleX, scaleY, scaleZ];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRotation(obj: ConsoleObject, isInterior = false) {
|
||||||
|
const rotation = getProperty(obj, "rotation")?.value ?? "1 0 0 0";
|
||||||
|
const [ax, az, ay, angle] = rotation.split(" ").map((s) => parseFloat(s));
|
||||||
|
|
||||||
|
if (isInterior) {
|
||||||
|
// For interiors: Apply coordinate system transformation
|
||||||
|
// 1. Convert rotation axis from source coords (ax, az, ay) to Three.js coords
|
||||||
|
// 2. Apply -90 Y rotation to align coordinate systems
|
||||||
|
const sourceRotation = new Quaternion().setFromAxisAngle(
|
||||||
|
new Vector3(az, ay, ax),
|
||||||
|
-angle * (Math.PI / 180)
|
||||||
|
);
|
||||||
|
const coordSystemFix = new Quaternion().setFromAxisAngle(
|
||||||
|
new Vector3(0, 1, 0),
|
||||||
|
Math.PI / 2
|
||||||
|
);
|
||||||
|
return coordSystemFix.multiply(sourceRotation);
|
||||||
|
} else {
|
||||||
|
// For other objects (terrain, etc)
|
||||||
|
return new Quaternion().setFromAxisAngle(
|
||||||
|
new Vector3(ax, ay, -az),
|
||||||
|
angle * (Math.PI / 180)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
168
src/textureUtils.ts
Normal file
168
src/textureUtils.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import {
|
||||||
|
DataTexture,
|
||||||
|
LinearFilter,
|
||||||
|
LinearMipmapLinearFilter,
|
||||||
|
NoColorSpace,
|
||||||
|
RedFormat,
|
||||||
|
RepeatWrapping,
|
||||||
|
SRGBColorSpace,
|
||||||
|
UnsignedByteType,
|
||||||
|
} from "three";
|
||||||
|
|
||||||
|
export function setupColor(tex, repeat = [1, 1]) {
|
||||||
|
tex.wrapS = tex.wrapT = RepeatWrapping;
|
||||||
|
tex.colorSpace = SRGBColorSpace;
|
||||||
|
tex.repeat.set(...repeat);
|
||||||
|
tex.anisotropy = 16;
|
||||||
|
tex.generateMipmaps = true;
|
||||||
|
tex.minFilter = LinearMipmapLinearFilter;
|
||||||
|
tex.magFilter = LinearFilter;
|
||||||
|
|
||||||
|
tex.needsUpdate = true;
|
||||||
|
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupMask(data) {
|
||||||
|
const tex = new DataTexture(
|
||||||
|
data,
|
||||||
|
256,
|
||||||
|
256,
|
||||||
|
RedFormat, // 1 channel
|
||||||
|
UnsignedByteType // 8-bit
|
||||||
|
);
|
||||||
|
|
||||||
|
// Masks should stay linear
|
||||||
|
tex.colorSpace = NoColorSpace;
|
||||||
|
|
||||||
|
// Set tiling / sampling. For NPOT sizes, disable mips or use power-of-two.
|
||||||
|
tex.wrapS = tex.wrapT = RepeatWrapping;
|
||||||
|
tex.generateMipmaps = false; // if width/height are not powers of two
|
||||||
|
tex.minFilter = LinearFilter; // avoid mips if generateMipmaps=false
|
||||||
|
tex.magFilter = LinearFilter;
|
||||||
|
|
||||||
|
tex.needsUpdate = true;
|
||||||
|
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTerrainTextureShader({
|
||||||
|
shader,
|
||||||
|
baseTextures,
|
||||||
|
alphaTextures,
|
||||||
|
visibilityMask,
|
||||||
|
}) {
|
||||||
|
const layerCount = baseTextures.length;
|
||||||
|
|
||||||
|
baseTextures.forEach((tex, i) => {
|
||||||
|
shader.uniforms[`albedo${i}`] = { value: tex };
|
||||||
|
});
|
||||||
|
|
||||||
|
alphaTextures.forEach((tex, i) => {
|
||||||
|
if (i > 0) {
|
||||||
|
shader.uniforms[`mask${i}`] = { value: tex };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add visibility mask uniform if we have empty squares
|
||||||
|
if (visibilityMask) {
|
||||||
|
shader.uniforms.visibilityMask = { value: visibilityMask };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add per-texture tiling uniforms
|
||||||
|
baseTextures.forEach((tex, i) => {
|
||||||
|
shader.uniforms[`tiling${i}`] = {
|
||||||
|
value: Math.min(512, { 0: 16, 1: 16, 2: 32, 3: 32, 4: 32, 5: 32 }[i]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Declare our uniforms at the top of the fragment shader
|
||||||
|
shader.fragmentShader =
|
||||||
|
`
|
||||||
|
uniform sampler2D albedo0;
|
||||||
|
uniform sampler2D albedo1;
|
||||||
|
uniform sampler2D albedo2;
|
||||||
|
uniform sampler2D albedo3;
|
||||||
|
uniform sampler2D albedo4;
|
||||||
|
uniform sampler2D albedo5;
|
||||||
|
uniform sampler2D mask1;
|
||||||
|
uniform sampler2D mask2;
|
||||||
|
uniform sampler2D mask3;
|
||||||
|
uniform sampler2D mask4;
|
||||||
|
uniform sampler2D mask5;
|
||||||
|
uniform float tiling0;
|
||||||
|
uniform float tiling1;
|
||||||
|
uniform float tiling2;
|
||||||
|
uniform float tiling3;
|
||||||
|
uniform float tiling4;
|
||||||
|
uniform float tiling5;
|
||||||
|
${visibilityMask ? "uniform sampler2D visibilityMask;" : ""}
|
||||||
|
` + shader.fragmentShader;
|
||||||
|
|
||||||
|
if (visibilityMask) {
|
||||||
|
const clippingPlaceholder = "#include <clipping_planes_fragment>";
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
clippingPlaceholder,
|
||||||
|
`${clippingPlaceholder}
|
||||||
|
// Early discard for invisible areas (before fog/lighting)
|
||||||
|
float visibility = texture2D(visibilityMask, vMapUv).r;
|
||||||
|
if (visibility < 0.5) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the default map sampling block with our layered blend.
|
||||||
|
// We rely on vMapUv provided by USE_MAP.
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
"#include <map_fragment>",
|
||||||
|
`
|
||||||
|
// Sample base albedo layers (sRGB textures auto-decoded to linear)
|
||||||
|
vec2 baseUv = vMapUv;
|
||||||
|
vec3 c0 = texture2D(albedo0, baseUv * vec2(tiling0)).rgb;
|
||||||
|
${
|
||||||
|
layerCount > 1
|
||||||
|
? `vec3 c1 = texture2D(albedo1, baseUv * vec2(tiling1)).rgb;`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
layerCount > 2
|
||||||
|
? `vec3 c2 = texture2D(albedo2, baseUv * vec2(tiling2)).rgb;`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
layerCount > 3
|
||||||
|
? `vec3 c3 = texture2D(albedo3, baseUv * vec2(tiling3)).rgb;`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
layerCount > 4
|
||||||
|
? `vec3 c4 = texture2D(albedo4, baseUv * vec2(tiling4)).rgb;`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
layerCount > 5
|
||||||
|
? `vec3 c5 = texture2D(albedo5, baseUv * vec2(tiling5)).rgb;`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample linear masks (use R channel)
|
||||||
|
float a1 = texture2D(mask1, baseUv).r;
|
||||||
|
${layerCount > 1 ? `float a2 = texture2D(mask2, baseUv).r;` : ""}
|
||||||
|
${layerCount > 2 ? `float a3 = texture2D(mask3, baseUv).r;` : ""}
|
||||||
|
${layerCount > 3 ? `float a4 = texture2D(mask4, baseUv).r;` : ""}
|
||||||
|
${layerCount > 4 ? `float a5 = texture2D(mask5, baseUv).r;` : ""}
|
||||||
|
|
||||||
|
// Bottom-up compositing: each mask tells how much the higher layer replaces lower
|
||||||
|
${layerCount > 1 ? `vec3 blended = mix(c0, c1, clamp(a1, 0.0, 1.0));` : ""}
|
||||||
|
${layerCount > 2 ? `blended = mix(blended, c2, clamp(a2, 0.0, 1.0));` : ""}
|
||||||
|
${layerCount > 3 ? `blended = mix(blended, c3, clamp(a3, 0.0, 1.0));` : ""}
|
||||||
|
${layerCount > 4 ? `blended = mix(blended, c4, clamp(a4, 0.0, 1.0));` : ""}
|
||||||
|
${layerCount > 5 ? `blended = mix(blended, c5, clamp(a5, 0.0, 1.0));` : ""}
|
||||||
|
|
||||||
|
// Assign to diffuseColor before lighting
|
||||||
|
diffuseColor.rgb = ${layerCount > 1 ? "blended" : "c0"};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue