remove unnecessary assets, add setting persistence

This commit is contained in:
Brian Beck 2025-11-13 23:41:10 -08:00
parent 6ae7a19332
commit fc4146c824
70 changed files with 180 additions and 18618 deletions

View file

@ -1,4 +1,5 @@
import { getResourceList } from "@/src/manifest";
import { useSettings } from "./SettingsProvider";
const excludeMissions = new Set([
"SkiFree",
@ -15,14 +16,19 @@ const missions = getResourceList()
export function InspectorControls({
missionName,
onChangeMission,
fogEnabled,
onChangeFogEnabled,
}: {
missionName: string;
onChangeMission: (name: string) => void;
fogEnabled: boolean;
onChangeFogEnabled: (enabled: boolean) => void;
}) {
const {
fogEnabled,
setFogEnabled,
speedMultiplier,
setSpeedMultiplier,
fov,
setFov,
} = useSettings();
return (
<div
id="controls"
@ -44,11 +50,38 @@ export function InspectorControls({
type="checkbox"
checked={fogEnabled}
onChange={(event) => {
onChangeFogEnabled(event.target.checked);
setFogEnabled(event.target.checked);
}}
/>
<label htmlFor="fogInput">Fog?</label>
</div>
<div className="Field">
<label htmlFor="fovInput">FOV</label>
<input
id="speedInput"
type="range"
min={75}
max={120}
step={5}
value={fov}
onChange={(event) => setFov(parseInt(event.target.value))}
/>
<output htmlFor="speedInput">{fov}</output>
</div>
<div className="Field">
<label htmlFor="speedInput">Speed</label>
<input
id="speedInput"
type="range"
min={0.1}
max={5}
step={0.05}
value={speedMultiplier}
onChange={(event) =>
setSpeedMultiplier(parseFloat(event.target.value))
}
/>
</div>
</div>
);
}

10
app/ObserverCamera.tsx Normal file
View file

@ -0,0 +1,10 @@
import { PerspectiveCamera } from "@react-three/drei";
import { useSettings } from "./SettingsProvider";
export function ObserverCamera() {
const { fov } = useSettings();
return (
<PerspectiveCamera makeDefault position={[-512, 256, -512]} fov={fov} />
);
}

View file

@ -1,10 +1,11 @@
import { KeyboardControls } from "@react-three/drei";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { useKeyboardControls } from "@react-three/drei";
import * as THREE from "three";
import { PointerLockControls } from "three-stdlib";
import { useSettings } from "./SettingsProvider";
import { Vector3 } from "three";
enum Controls {
forward = "forward",
@ -16,17 +17,19 @@ enum Controls {
}
const BASE_SPEED = 100; // units per second
const MIN_SPEED_ADJUSTMENT = 0.05;
const MAX_SPEED_ADJUSTMENT = 1;
function CameraMovement() {
const { speedMultiplier, setSpeedMultiplier } = useSettings();
const [subscribe, getKeys] = useKeyboardControls<Controls>();
const { camera, gl } = useThree();
const [speedMultiplier, setSpeedMultiplier] = useState(1);
const controlsRef = useRef<PointerLockControls | null>(null);
// 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());
const forwardVec = useRef(new Vector3());
const sideVec = useRef(new Vector3());
const moveVec = useRef(new Vector3());
// Setup pointer lock controls
useEffect(() => {
@ -53,10 +56,20 @@ function CameraMovement() {
const handleWheel = (e: WheelEvent) => {
e.preventDefault();
// Adjust speed based on wheel direction
const delta = e.deltaY > 0 ? 0.75 : 1.25;
const direction = e.deltaY > 0 ? 1 : -1;
setSpeedMultiplier((prev) => Math.max(0.05, Math.min(5, prev * delta)));
const delta =
// Helps normalize sensitivity; trackpad scrolling will have many small
// updates while mouse wheels have fewer updates but large deltas.
Math.max(
MIN_SPEED_ADJUSTMENT,
Math.min(MAX_SPEED_ADJUSTMENT, Math.abs(e.deltaY * 0.01))
) * direction;
setSpeedMultiplier((prev) => {
const newSpeed = Math.round((prev + delta) * 20) / 20;
return Math.max(0.1, Math.min(5, newSpeed));
});
};
const canvas = gl.domElement;

View file

@ -1,19 +1,67 @@
import React, { useContext, useMemo } from "react";
import React, { useContext, useEffect, useMemo, useState } from "react";
import { PerspectiveCamera } from "three";
const SettingsContext = React.createContext(null);
type PersistedSettings = {
fogEnabled?: boolean;
speedMultiplier?: number;
fov?: number;
};
export function useSettings() {
return useContext(SettingsContext);
}
export function SettingsProvider({
children,
fogEnabled,
}: {
children: React.ReactNode;
fogEnabled: boolean;
}) {
const value = useMemo(() => ({ fogEnabled }), [fogEnabled]);
export function SettingsProvider({ children }: { children: React.ReactNode }) {
const [fogEnabled, setFogEnabled] = useState(true);
const [speedMultiplier, setSpeedMultiplier] = useState(1);
const [fov, setFov] = useState(90);
const value = useMemo(
() => ({
fogEnabled,
setFogEnabled,
speedMultiplier,
setSpeedMultiplier,
fov,
setFov,
}),
[fogEnabled, speedMultiplier, fov]
);
// Read persisted settings from localStoarge.
useEffect(() => {
let savedSettings: PersistedSettings = {};
try {
savedSettings = JSON.parse(localStorage.getItem("settings"));
} catch (err) {
// Ignore.
}
if (savedSettings.fogEnabled != null) {
setFogEnabled(savedSettings.fogEnabled);
}
if (savedSettings.speedMultiplier != null) {
setSpeedMultiplier(savedSettings.speedMultiplier);
}
if (savedSettings.fov != null) {
setFov(savedSettings.fov);
}
}, []);
// Persist settings to localStoarge.
useEffect(() => {
const settingsToSave: PersistedSettings = {
fogEnabled,
speedMultiplier,
fov,
};
try {
localStorage.setItem("settings", JSON.stringify(settingsToSave));
} catch (err) {
// Probably forbidden by browser settings.
}
}, [fogEnabled, speedMultiplier, fov]);
return (
<SettingsContext.Provider value={value}>

View file

@ -59,6 +59,18 @@ function BlendedTerrainTextures({
[alphaMaps]
);
const tiling = useMemo(
() => ({
0: 32,
1: 32,
2: 32,
3: 32,
4: 32,
5: 32,
}),
[]
);
const onBeforeCompile = useCallback(
(shader) => {
updateTerrainTextureShader({
@ -66,13 +78,16 @@ function BlendedTerrainTextures({
baseTextures,
alphaTextures,
visibilityMask,
tiling,
});
},
[baseTextures, alphaTextures, visibilityMask]
[baseTextures, alphaTextures, visibilityMask, tiling]
);
return (
<meshStandardMaterial
// For testing tiling values; forces recompile.
key={JSON.stringify(tiling)}
displacementMap={displacementMap}
map={displacementMap}
displacementScale={2048}

View file

@ -6,9 +6,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ObserverControls } from "./ObserverControls";
import { InspectorControls } from "./InspectorControls";
import { SettingsProvider } from "./SettingsProvider";
import { PerspectiveCamera } from "@react-three/drei";
import { EffectComposer, N8AO } from "@react-three/postprocessing";
import { useSearchParams, useRouter } from "next/navigation";
import { ObserverCamera } from "./ObserverCamera";
// three.js has its own loaders for textures and models, but we need to load other
// stuff too, e.g. missions, terrains, and more. This client is used for those.
@ -22,30 +22,22 @@ export default function HomePage() {
const [missionName, setMissionName] = useState(
searchParams.get("mission") || "TWL2_WoodyMyrk"
);
const [fogEnabled, setFogEnabled] = useState(
searchParams.get("fog") === "true"
);
// Update query params when state changes
useEffect(() => {
const params = new URLSearchParams();
params.set("mission", missionName);
params.set("fog", String(fogEnabled));
router.replace(`?${params.toString()}`, { scroll: false });
}, [missionName, fogEnabled, router]);
}, [missionName, router]);
return (
<SettingsProvider fogEnabled={fogEnabled}>
<QueryClientProvider client={queryClient}>
<main>
<QueryClientProvider client={queryClient}>
<main>
<SettingsProvider>
<Canvas shadows>
<ObserverControls />
<Mission key={missionName} name={missionName} />
<PerspectiveCamera
makeDefault
position={[-512, 256, -512]}
fov={90}
/>
<ObserverCamera />
<EffectComposer>
<N8AO intensity={3} aoRadius={3} quality="performance" />
</EffectComposer>
@ -53,11 +45,9 @@ export default function HomePage() {
<InspectorControls
missionName={missionName}
onChangeMission={setMissionName}
fogEnabled={fogEnabled}
onChangeFogEnabled={setFogEnabled}
/>
</main>
</QueryClientProvider>
</SettingsProvider>
</SettingsProvider>
</main>
</QueryClientProvider>
);
}

View file

@ -19,7 +19,7 @@ main {
#controls {
display: flex;
align-items: center;
gap: 10px;
gap: 20px;
position: fixed;
top: 0;
left: 0;
@ -35,3 +35,13 @@ main {
align-items: center;
gap: 6px;
}
.Field {
display: flex;
align-items: center;
gap: 6px;
}
#speedInput {
max-width: 80px;
}