From 8ab6f63feee5a60859a800bc13bad52a521bc138 Mon Sep 17 00:00:00 2001 From: bmathews Date: Fri, 14 Nov 2025 03:10:26 -0800 Subject: [PATCH] Fix texture orientation. Add AO. Fix fog transition in sky --- app/InteriorInstance.tsx | 20 +++---- app/Mission.tsx | 9 +--- app/Sky.tsx | 112 ++++++++++++++++++++++++++++++++++++--- app/Sun.tsx | 48 +++++++++++++++++ app/TerrainBlock.tsx | 2 + app/page.tsx | 6 ++- app/renderObject.tsx | 2 + package-lock.json | 47 ++++++++++++++++ package.json | 1 + src/textureUtils.ts | 1 + 10 files changed, 218 insertions(+), 30 deletions(-) create mode 100644 app/Sun.tsx 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 ? (
- + + + + = 0.156.0" + } + }, + "node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1967,6 +1994,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/n8ao": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz", + "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2128,6 +2165,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postprocessing": { + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.0.tgz", + "integrity": "sha512-tisx8XN/PWTL3uXz2mt8bjlMS1wiOUSCK3ixi4zjwUCFmP8XW8hNhXwrxwd2zf2VmCyCQ3GUaLm7GLnkkBbDsQ==", + "license": "Zlib", + "peer": true, + "peerDependencies": { + "three": ">= 0.157.0 < 0.182.0" + } + }, "node_modules/potpack": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", diff --git a/package.json b/package.json index dc5c7fa8..be1fc685 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.3.0", + "@react-three/postprocessing": "^3.0.4", "@tanstack/react-query": "^5.90.8", "next": "^15.5.2", "react": "^19.1.1", diff --git a/src/textureUtils.ts b/src/textureUtils.ts index a91b10f4..bc2a712f 100644 --- a/src/textureUtils.ts +++ b/src/textureUtils.ts @@ -13,6 +13,7 @@ export function setupColor(tex, repeat = [1, 1]) { tex.wrapS = tex.wrapT = RepeatWrapping; tex.colorSpace = SRGBColorSpace; tex.repeat.set(...repeat); + tex.flipY = false; // DDS/DIF textures are already flipped tex.anisotropy = 16; tex.generateMipmaps = true; tex.minFilter = LinearMipmapLinearFilter;