diff --git a/app/InteriorInstance.tsx b/app/InteriorInstance.tsx
index 6ded0182..7082ddaf 100644
--- a/app/InteriorInstance.tsx
+++ b/app/InteriorInstance.tsx
@@ -7,8 +7,8 @@ import {
getRotation,
getScale,
} from "@/src/mission";
-import { memo, Suspense, useEffect, useMemo } from "react";
-import { Material, Mesh, MeshBasicMaterial } from "three";
+import { memo, Suspense, useMemo } from "react";
+import { Mesh } from "three";
import { setupColor } from "@/src/textureUtils";
const FALLBACK_URL = `${BASE_URL}/black.png`;
@@ -21,23 +21,17 @@ function useInterior(interiorFile: string) {
return useGLTF(url);
}
-function InteriorTexture({ material }: { material: Material }) {
+function InteriorTexture({ materialName }: { materialName: string }) {
let url = FALLBACK_URL;
try {
- url = interiorTextureToUrl(material.name);
+ url = interiorTextureToUrl(materialName);
} catch (err) {
console.error(err);
}
const texture = useTexture(url, (texture) => setupColor(texture));
- useEffect(() => {
- const asBasicMaterial = material as MeshBasicMaterial;
- asBasicMaterial.map = texture;
- asBasicMaterial.needsUpdate = true;
- }, [material, texture]);
-
- return ;
+ return ;
}
function InteriorMesh({ node }: { node: Mesh }) {
@@ -53,10 +47,10 @@ function InteriorMesh({ node }: { node: Mesh }) {
>
{Array.isArray(node.material) ? (
node.material.map((mat, index) => (
-
+
))
) : (
-
+
)}
) : null}
diff --git a/app/Mission.tsx b/app/Mission.tsx
index 87652b9d..14b6d523 100644
--- a/app/Mission.tsx
+++ b/app/Mission.tsx
@@ -16,12 +16,5 @@ export function Mission({ name }: { name: string }) {
return null;
}
- return (
- <>
-
- {mission.objects.map((object, i) => renderObject(object, i))}
- >
- );
+ return <>{mission.objects.map((object, i) => renderObject(object, i))}>;
}
diff --git a/app/Sky.tsx b/app/Sky.tsx
index cb968178..9fc99f43 100644
--- a/app/Sky.tsx
+++ b/app/Sky.tsx
@@ -1,10 +1,10 @@
import { ConsoleObject, getProperty } from "@/src/mission";
import { useSettings } from "./SettingsProvider";
-import { Suspense, useMemo } from "react";
+import { Suspense, useMemo, useEffect, useRef } 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";
+import { Color, ShaderMaterial, BackSide } from "three";
const FALLBACK_URL = `${BASE_URL}/black.png`;
@@ -18,7 +18,15 @@ function useDetailMapList(name: string) {
});
}
-export function SkyBox({ materialList }: { materialList: string }) {
+export function SkyBox({
+ materialList,
+ fogColor,
+ fogDistance,
+}: {
+ materialList: string;
+ fogColor?: Color;
+ fogDistance?: number;
+}) {
const { data: detailMapList } = useDetailMapList(materialList);
const skyBoxFiles = useMemo(
@@ -45,7 +53,83 @@ export function SkyBox({ materialList }: { materialList: string }) {
const skyBox = useCubeTexture(skyBoxFiles, { path: "" });
- return ;
+ // Create a shader material for the skybox with fog
+ const materialRef = useRef(null!);
+
+ const hasFog = !!fogColor && !!fogDistance;
+
+ const shaderMaterial = useMemo(() => {
+ if (!hasFog) {
+ return null;
+ }
+
+ return new ShaderMaterial({
+ uniforms: {
+ skybox: { value: skyBox },
+ fogColor: { value: fogColor },
+ },
+ vertexShader: `
+ varying vec3 vDirection;
+
+ void main() {
+ // Use position directly as direction (no world transform needed)
+ vDirection = position;
+
+ // Transform position but ignore translation
+ vec4 pos = projectionMatrix * mat4(mat3(modelViewMatrix)) * vec4(position, 1.0);
+ gl_Position = pos.xyww; // Set depth to far plane
+ }
+ `,
+ fragmentShader: `
+ uniform samplerCube skybox;
+ uniform vec3 fogColor;
+
+ varying vec3 vDirection;
+
+ // Convert linear to sRGB
+ vec3 linearToSRGB(vec3 color) {
+ return pow(color, vec3(1.0 / 2.2));
+ }
+
+ void main() {
+ vec3 direction = normalize(vDirection);
+ direction.x = -direction.x;
+ vec4 skyColor = textureCube(skybox, direction);
+
+ // Calculate fog factor based on vertical direction
+ // direction.y: -1 = straight down, 0 = horizon, 1 = straight up
+ // 100% fog from bottom to horizon, then fade from horizon (0) to 0.4
+ float fogFactor = smoothstep(0.0, 0.4, direction.y);
+
+ // Mix in sRGB space to match Three.js fog rendering
+ vec3 finalColor = mix(fogColor, skyColor.rgb, fogFactor);
+ gl_FragColor = vec4(finalColor, 1.0);
+ }
+ `,
+ side: BackSide,
+ depthWrite: false,
+ });
+ }, [skyBox, fogColor, hasFog]);
+
+ // Update uniforms when fog parameters change
+ useEffect(() => {
+ if (materialRef.current && hasFog && shaderMaterial) {
+ materialRef.current.uniforms.skybox.value = skyBox;
+ materialRef.current.uniforms.fogColor.value = fogColor!;
+ }
+ }, [skyBox, fogColor, hasFog, shaderMaterial]);
+
+ // If fog is disabled, just use the skybox as background
+ if (!hasFog) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ );
}
export function Sky({ object }: { object: ConsoleObject }) {
@@ -69,12 +153,15 @@ export function Sky({ object }: { object: ConsoleObject }) {
// `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);
+ return [
+ new Color().setRGB(r, g, b),
+ new Color().setRGB(r, g, b).convertSRGBToLinear(),
+ ];
}
}, [object]);
const backgroundColor = fogColor ? (
-
+
) : null;
return (
@@ -83,14 +170,23 @@ export function Sky({ object }: { object: ConsoleObject }) {
// If there's a skybox, its textures will need to load. Render just the
// fog color as the background in the meantime.
-
+
) : (
// If there's no skybox, just render the fog color as the background.
backgroundColor
)}
{fogEnabled && fogDistance && fogColor ? (
-
+
) : null}
>
);
diff --git a/app/Sun.tsx b/app/Sun.tsx
new file mode 100644
index 00000000..b6e34cae
--- /dev/null
+++ b/app/Sun.tsx
@@ -0,0 +1,48 @@
+import { ConsoleObject, getProperty } from "@/src/mission";
+import { useMemo } from "react";
+import { Color } from "three";
+
+export function Sun({ object }: { object: ConsoleObject }) {
+ const direction = useMemo(() => {
+ const directionStr = getProperty(object, "direction")?.value ?? "0 0 -1";
+ const [x, y, z] = directionStr.split(" ").map((s) => parseFloat(s));
+ // Scale the direction vector to position the light far from the scene
+ const scale = 5000;
+ return [x * scale, y * scale, z * scale] as [number, number, number];
+ }, [object]);
+
+ const color = useMemo(() => {
+ const colorStr = getProperty(object, "color")?.value ?? "1 1 1 1";
+ const [r, g, b] = colorStr.split(" ").map((s) => parseFloat(s));
+ return [r, g, b] as [number, number, number];
+ }, [object]);
+
+ const ambient = useMemo(() => {
+ const ambientStr = getProperty(object, "ambient")?.value ?? "0.5 0.5 0.5 1";
+ const [r, g, b] = ambientStr.split(" ").map((s) => parseFloat(s));
+ return [r, g, b] as [number, number, number];
+ }, [object]);
+
+ return (
+ <>
+ {/* Directional light for the sun */}
+ {/* */}
+ {/* Ambient light component */}
+
+ >
+ );
+}
diff --git a/app/TerrainBlock.tsx b/app/TerrainBlock.tsx
index 4b047a94..122c3d5e 100644
--- a/app/TerrainBlock.tsx
+++ b/app/TerrainBlock.tsx
@@ -209,6 +209,8 @@ export function TerrainBlock({ object }: { object: ConsoleObject }) {
position={position}
scale={scale}
geometry={planeGeometry}
+ receiveShadow
+ castShadow
>
{terrain ? (
-