mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 20:25:01 +00:00
Fix texture orientation. Add AO. Fix fog transition in sky
This commit is contained in:
parent
75750571de
commit
8ab6f63fee
|
|
@ -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 <primitive object={material} attach="material" />;
|
||||
return <meshStandardMaterial map={texture} side={2} />;
|
||||
}
|
||||
|
||||
function InteriorMesh({ node }: { node: Mesh }) {
|
||||
|
|
@ -53,10 +47,10 @@ function InteriorMesh({ node }: { node: Mesh }) {
|
|||
>
|
||||
{Array.isArray(node.material) ? (
|
||||
node.material.map((mat, index) => (
|
||||
<InteriorTexture key={index} material={mat} />
|
||||
<InteriorTexture key={index} materialName={mat.name} />
|
||||
))
|
||||
) : (
|
||||
<InteriorTexture material={node.material} />
|
||||
<InteriorTexture materialName={node.material.name} />
|
||||
)}
|
||||
</Suspense>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,5 @@ export function Mission({ name }: { name: string }) {
|
|||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<hemisphereLight
|
||||
args={["rgba(209, 237, 255, 1)", "rgba(186, 200, 181, 1)", 2]}
|
||||
/>
|
||||
{mission.objects.map((object, i) => renderObject(object, i))}
|
||||
</>
|
||||
);
|
||||
return <>{mission.objects.map((object, i) => renderObject(object, i))}</>;
|
||||
}
|
||||
|
|
|
|||
112
app/Sky.tsx
112
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 <primitive attach="background" object={skyBox} />;
|
||||
// Create a shader material for the skybox with fog
|
||||
const materialRef = useRef<ShaderMaterial>(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 <primitive attach="background" object={skyBox} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<mesh scale={5000}>
|
||||
<sphereGeometry args={[1, 60, 40]} />
|
||||
<primitive ref={materialRef} object={shaderMaterial} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
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 ? (
|
||||
<color attach="background" args={[fogColor]} />
|
||||
<color attach="background" args={[fogColor[0]]} />
|
||||
) : 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.
|
||||
<Suspense fallback={backgroundColor}>
|
||||
<SkyBox materialList={materialList} />
|
||||
<SkyBox
|
||||
materialList={materialList}
|
||||
fogColor={fogEnabled ? fogColor[1] : undefined}
|
||||
fogDistance={fogEnabled ? fogDistance : undefined}
|
||||
/>
|
||||
</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} />
|
||||
<fog
|
||||
attach="fog"
|
||||
color={fogColor[1]}
|
||||
near={fogDistance / 2}
|
||||
far={fogDistance * 2}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
48
app/Sun.tsx
Normal file
48
app/Sun.tsx
Normal file
|
|
@ -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 */}
|
||||
{/* <directionalLight
|
||||
position={[500, 500, 500]}
|
||||
target-position={direction}
|
||||
color={color}
|
||||
intensity={2}
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-camera-left={-2000}
|
||||
shadow-camera-right={2000}
|
||||
shadow-camera-top={2000}
|
||||
shadow-camera-bottom={-2000}
|
||||
shadow-camera-near={0.5}
|
||||
shadow-camera-far={5000}
|
||||
shadow-bias={-0.001}
|
||||
/> */}
|
||||
{/* Ambient light component */}
|
||||
<hemisphereLight args={[new Color(...color), new Color(...ambient), 2]} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -209,6 +209,8 @@ export function TerrainBlock({ object }: { object: ConsoleObject }) {
|
|||
position={position}
|
||||
scale={scale}
|
||||
geometry={planeGeometry}
|
||||
receiveShadow
|
||||
castShadow
|
||||
>
|
||||
{terrain ? (
|
||||
<TerrainMaterial
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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";
|
||||
|
||||
// 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.
|
||||
|
|
@ -20,7 +21,7 @@ export default function HomePage() {
|
|||
<SettingsProvider fogEnabled={fogEnabled}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<main>
|
||||
<Canvas>
|
||||
<Canvas shadows>
|
||||
<ObserverControls />
|
||||
<Mission key={missionName} name={missionName} />
|
||||
<PerspectiveCamera
|
||||
|
|
@ -28,6 +29,9 @@ export default function HomePage() {
|
|||
position={[-512, 256, -512]}
|
||||
fov={90}
|
||||
/>
|
||||
<EffectComposer>
|
||||
<N8AO intensity={3} aoRadius={3} quality="performance" />
|
||||
</EffectComposer>
|
||||
</Canvas>
|
||||
<InspectorControls
|
||||
missionName={missionName}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { WaterBlock } from "./WaterBlock";
|
|||
import { SimGroup } from "./SimGroup";
|
||||
import { InteriorInstance } from "./InteriorInstance";
|
||||
import { Sky } from "./Sky";
|
||||
import { Sun } from "./Sun";
|
||||
|
||||
const componentMap = {
|
||||
SimGroup,
|
||||
|
|
@ -11,6 +12,7 @@ const componentMap = {
|
|||
WaterBlock,
|
||||
InteriorInstance,
|
||||
Sky,
|
||||
Sun,
|
||||
};
|
||||
|
||||
export function renderObject(object: ConsoleObject, key: string | number) {
|
||||
|
|
|
|||
47
package-lock.json
generated
47
package-lock.json
generated
|
|
@ -11,6 +11,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",
|
||||
|
|
@ -1217,6 +1218,32 @@
|
|||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-three/postprocessing": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz",
|
||||
"integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"maath": "^0.6.0",
|
||||
"n8ao": "^1.9.4",
|
||||
"postprocessing": "^6.36.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": "^9.0.0",
|
||||
"react": "^19.0",
|
||||
"three": ">= 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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue