fix skybox mirroring, tested with DMP2-Anabatic

This commit is contained in:
Brian Beck 2025-12-04 22:35:14 -08:00
parent 8b86c2f05d
commit 9121d0a81b

View file

@ -1,12 +1,11 @@
import { Suspense, useMemo, useEffect, useRef } from "react";
import { Suspense, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { useCubeTexture } from "@react-three/drei";
import { Color, ShaderMaterial, BackSide, Euler, ShaderChunk } from "three";
import { Color, ShaderMaterial, BackSide, ShaderChunk } from "three";
import type { TorqueObject } from "../torqueScript";
import { getFloat, getInt, getProperty } from "../mission";
import { useSettings } from "./SettingsProvider";
import { BASE_URL, loadDetailMapList, textureToUrl } from "../loaders";
import { useThree } from "@react-three/fiber";
import { CloudLayers } from "./CloudLayers";
const FALLBACK_TEXTURE_URL = `${BASE_URL}/black.png`;
@ -46,6 +45,21 @@ if (USE_QUADRATIC_FOG) {
installQuadraticFogShader();
}
/**
* Parse a Tribes 2 color string (space-separated RGB or RGBA values 0-1).
* Returns [sRGB Color, linear Color] or undefined if no color string.
*/
function parseColorString(
colorString: string | undefined,
): [Color, Color] | undefined {
if (!colorString) return undefined;
const [r, g, b] = colorString.split(" ").map((s) => parseFloat(s));
return [
new Color().setRGB(r, g, b),
new Color().setRGB(r, g, b).convertSRGBToLinear(),
];
}
/**
* Load a .dml file, used to list the textures for different faces of a skybox.
*/
@ -59,13 +73,9 @@ function useDetailMapList(name: string) {
export function SkyBox({
materialList,
fogColor,
fogNear,
fogFar,
}: {
materialList: string;
fogColor?: Color;
fogNear?: number;
fogFar?: number;
}) {
const { data: detailMapList } = useDetailMapList(materialList);
@ -93,21 +103,14 @@ export function SkyBox({
const skyBox = useCubeTexture(skyBoxFiles, { path: "" });
// Create a shader material for the skybox with fog
const materialRef = useRef<ShaderMaterial>(null!);
const hasFog = !!fogColor && fogNear != null && fogFar != null;
const shaderMaterial = useMemo(() => {
if (!hasFog) {
return null;
}
// Skybox fog blends toward horizon
// Always use a shader to apply the X-axis mirror transformation.
// Optionally blend fog toward the horizon.
return new ShaderMaterial({
uniforms: {
skybox: { value: skyBox },
fogColor: { value: fogColor },
fogColor: { value: fogColor ?? new Color(0, 0, 0) },
enableFog: { value: !!fogColor },
},
vertexShader: `
varying vec3 vDirection;
@ -121,53 +124,37 @@ export function SkyBox({
fragmentShader: `
uniform samplerCube skybox;
uniform vec3 fogColor;
uniform bool enableFog;
varying vec3 vDirection;
void main() {
vec3 direction = normalize(vDirection);
// Swap X and Z to match scene.backgroundRotation used in non-fog path
direction = vec3(direction.z, direction.y, direction.x);
// Swap X and Z, negate X to mirror across X axis
direction = vec3(direction.z, direction.y, -direction.x);
vec4 skyColor = textureCube(skybox, direction);
// Fog increases toward and below horizon
// direction.y: -1 = straight down, 0 = horizon, 1 = straight up
// Use smoothstep for gradual transition (matches Three.js linear fog feel)
float fogFactor = 1.0 - smoothstep(-0.1, 0.5, direction.y);
vec3 finalColor = mix(skyColor.rgb, fogColor, fogFactor);
gl_FragColor = vec4(finalColor, 1.0);
if (enableFog) {
// Fog increases toward and below horizon
// direction.y: -1 = straight down, 0 = horizon, 1 = straight up
// Use smoothstep for gradual transition (matches Three.js linear fog feel)
float fogFactor = 1.0 - smoothstep(-0.1, 0.5, direction.y);
vec3 finalColor = mix(skyColor.rgb, fogColor, fogFactor);
gl_FragColor = vec4(finalColor, 1.0);
} else {
gl_FragColor = skyColor;
}
}
`,
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]);
const { scene } = useThree();
// Rotate background to match the X/Z swap applied in the fog shader path
useEffect(() => {
scene.backgroundRotation = new Euler(0, Math.PI / 2, 0);
}, [scene]);
// If fog is disabled, just use the skybox as background
if (!hasFog) {
return <primitive attach="background" object={skyBox} />;
}
}, [skyBox, fogColor]);
return (
<mesh scale={5000} frustumCulled={false}>
<sphereGeometry args={[1, 60, 40]} />
<primitive ref={materialRef} object={shaderMaterial} attach="material" />
<primitive object={shaderMaterial} attach="material" />
</mesh>
);
}
@ -178,21 +165,10 @@ export function Sky({ object }: { object: TorqueObject }) {
// Skybox textures
const materialList = getProperty(object, "materialList");
const skySolidColor = useMemo(() => {
const colorString = getProperty(object, "SkySolidColor");
if (colorString) {
// `colorString` might specify an alpha value, but three.js doesn't
// support opacity on fog or scene backgrounds, so ignore it.
// Note: This is a space-separated string, so we split and parse each component.
const [r, g, b] = colorString
.split(" ")
.map((s: string) => parseFloat(s));
return [
new Color().setRGB(r, g, b),
new Color().setRGB(r, g, b).convertSRGBToLinear(),
];
}
}, [object]);
const skySolidColor = useMemo(
() => parseColorString(getProperty(object, "SkySolidColor")),
[object],
);
const useSkyTextures = getInt(object, "useSkyTextures") ?? 1;
@ -213,21 +189,10 @@ export function Sky({ object }: { object: TorqueObject }) {
? highVisibleDistance
: visibleDistanceBase;
const fogColor = useMemo(() => {
const colorString = getProperty(object, "fogColor");
if (colorString) {
// `colorString` might specify an alpha value, but three.js doesn't
// support opacity on fog or scene backgrounds, so ignore it.
// Note: This is a space-separated string, so we split and parse each component.
const [r, g, b] = colorString
.split(" ")
.map((s: string) => parseFloat(s));
return [
new Color().setRGB(r, g, b),
new Color().setRGB(r, g, b).convertSRGBToLinear(),
];
}
}, [object]);
const fogColor = useMemo(
() => parseColorString(getProperty(object, "fogColor")),
[object],
);
const skyColor = skySolidColor || fogColor;
@ -245,8 +210,6 @@ export function Sky({ object }: { object: TorqueObject }) {
<SkyBox
materialList={materialList}
fogColor={fogEnabled && hasFogParams ? fogColor?.[1] : undefined}
fogNear={fogEnabled && hasFogParams ? fogNear : undefined}
fogFar={fogEnabled && hasFogParams ? fogFar : undefined}
/>
</Suspense>
) : (