terrain support for detailTexture

This commit is contained in:
Brian Beck 2025-12-06 12:17:24 -08:00
parent 0bcb2ff9f4
commit 035812724d
11 changed files with 105 additions and 9 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
2:I[39756,["/t2-mapper/_next/static/chunks/060f9a97930f3d04.js"],"default"]
3:I[37457,["/t2-mapper/_next/static/chunks/060f9a97930f3d04.js"],"default"]
4:I[47257,["/t2-mapper/_next/static/chunks/060f9a97930f3d04.js"],"ClientPageRoot"]
5:I[31713,["/t2-mapper/_next/static/chunks/98d29aec52be59c3.js","/t2-mapper/_next/static/chunks/32ef0c8650712240.js","/t2-mapper/_next/static/chunks/b70f08013a69708a.js","/t2-mapper/_next/static/chunks/aaf7d6869584978f.js"],"default"]
5:I[31713,["/t2-mapper/_next/static/chunks/98d29aec52be59c3.js","/t2-mapper/_next/static/chunks/32ef0c8650712240.js","/t2-mapper/_next/static/chunks/b70f08013a69708a.js","/t2-mapper/_next/static/chunks/186f602c78093cf5.js"],"default"]
8:I[97367,["/t2-mapper/_next/static/chunks/060f9a97930f3d04.js"],"OutletBoundary"]
a:I[11533,["/t2-mapper/_next/static/chunks/060f9a97930f3d04.js"],"AsyncMetadataOutlet"]
c:I[97367,["/t2-mapper/_next/static/chunks/060f9a97930f3d04.js"],"ViewportBoundary"]
@ -10,7 +10,7 @@ e:I[97367,["/t2-mapper/_next/static/chunks/060f9a97930f3d04.js"],"MetadataBounda
f:"$Sreact.suspense"
11:I[68027,[],"default"]
:HL["/t2-mapper/_next/static/chunks/15b04c9d2ba2c4cf.css","style"]
0:{"P":null,"b":"70hpG-x4f3KKfn_e3s67S","p":"/t2-mapper","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/15b04c9d2ba2c4cf.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","searchParams":{},"params":{},"promises":["$@6","$@7"]}],[["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/98d29aec52be59c3.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/32ef0c8650712240.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/b70f08013a69708a.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/aaf7d6869584978f.js","async":true,"nonce":"$undefined"}]],["$","$L8",null,{"children":["$L9",["$","$La",null,{"promise":"$@b"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Lc",null,{"children":"$Ld"}],null],["$","$Le",null,{"children":["$","div",null,{"hidden":true,"children":["$","$f",null,{"fallback":null,"children":"$L10"}]}]}]]}],false]],"m":"$undefined","G":["$11",[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/15b04c9d2ba2c4cf.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"s":false,"S":true}
0:{"P":null,"b":"t8mk0uRHwYhFYlZ_2J51e","p":"/t2-mapper","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/15b04c9d2ba2c4cf.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","searchParams":{},"params":{},"promises":["$@6","$@7"]}],[["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/98d29aec52be59c3.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/32ef0c8650712240.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/b70f08013a69708a.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/186f602c78093cf5.js","async":true,"nonce":"$undefined"}]],["$","$L8",null,{"children":["$L9",["$","$La",null,{"promise":"$@b"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Lc",null,{"children":"$Ld"}],null],["$","$Le",null,{"children":["$","div",null,{"hidden":true,"children":["$","$f",null,{"fallback":null,"children":"$L10"}]}]}]]}],false]],"m":"$undefined","G":["$11",[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/15b04c9d2ba2c4cf.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"s":false,"S":true}
6:{}
7:"$0:f:0:1:2:children:1:props:children:0:props:params"
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]

View file

@ -97,6 +97,7 @@ export const TerrainBlock = memo(function TerrainBlock({
}) {
const terrainFile = getProperty(object, "terrainFile");
const squareSize = getInt(object, "squareSize") ?? DEFAULT_SQUARE_SIZE;
const detailTexture = getProperty(object, "detailTexture");
const blockSize = squareSize * 256;
const visibleDistance = useVisibleDistance();
const camera = useThree((state) => state.camera);
@ -234,6 +235,7 @@ export const TerrainBlock = memo(function TerrainBlock({
displacementMap={sharedDisplacementMap}
visibilityMask={primaryVisibilityMask}
alphaTextures={sharedAlphaTextures}
detailTextureName={detailTexture}
/>
{/* Pooled tiles - stable keys, always mounted */}
{poolIndices.map((poolIndex) => {
@ -250,6 +252,7 @@ export const TerrainBlock = memo(function TerrainBlock({
displacementMap={sharedDisplacementMap}
visibilityMask={pooledVisibilityMask}
alphaTextures={sharedAlphaTextures}
detailTextureName={detailTexture}
visible={assignment !== null}
/>
);

View file

@ -1,7 +1,11 @@
import { memo, Suspense, useCallback, useMemo } from "react";
import { DataTexture, DoubleSide, FrontSide, type PlaneGeometry } from "three";
import { useTexture } from "@react-three/drei";
import { terrainTextureToUrl } from "../loaders";
import {
FALLBACK_TEXTURE_URL,
terrainTextureToUrl,
textureToUrl,
} from "../loaders";
import { setupColor } from "../textureUtils";
import { updateTerrainTextureShader } from "../terrainMaterial";
import { useDebug } from "./SettingsProvider";
@ -28,6 +32,7 @@ interface TerrainTileProps {
displacementMap: DataTexture;
visibilityMask: DataTexture;
alphaTextures: DataTexture[];
detailTextureName?: string;
visible?: boolean;
}
@ -36,11 +41,13 @@ function BlendedTerrainTextures({
visibilityMask,
textureNames,
alphaTextures,
detailTextureName,
}: {
displacementMap: DataTexture;
visibilityMask: DataTexture;
textureNames: string[];
alphaTextures: DataTexture[];
detailTextureName?: string;
}) {
const { debugMode } = useDebug();
@ -51,6 +58,18 @@ function BlendedTerrainTextures({
},
);
// Load detail texture if specified
const detailTextureUrl = detailTextureName
? textureToUrl(detailTextureName)
: null;
const detailTexture = useTexture(
detailTextureUrl ?? FALLBACK_TEXTURE_URL,
(tex) => {
setupColor(tex);
},
);
const onBeforeCompile = useCallback(
(shader) => {
updateTerrainTextureShader({
@ -60,14 +79,27 @@ function BlendedTerrainTextures({
visibilityMask,
tiling: TILING,
debugMode,
detailTexture: detailTextureUrl ? detailTexture : null,
});
},
[baseTextures, alphaTextures, visibilityMask, debugMode],
[
baseTextures,
alphaTextures,
visibilityMask,
debugMode,
detailTexture,
detailTextureUrl,
],
);
// Key must include factors that change shader code structure (not just uniforms)
// - debugMode: affects fragment shader branching
// - detailTextureUrl: affects vertex shader (adds varying) and fragment shader
const materialKey = `${debugMode ? "debug" : "normal"}-${detailTextureUrl ? "detail" : "nodetail"}`;
return (
<meshStandardMaterial
key={debugMode ? "debug" : "normal"}
key={materialKey}
displacementMap={displacementMap}
map={displacementMap}
displacementScale={2048}
@ -83,11 +115,13 @@ function TerrainMaterial({
visibilityMask,
textureNames,
alphaTextures,
detailTextureName,
}: {
displacementMap: DataTexture;
visibilityMask: DataTexture;
textureNames: string[];
alphaTextures: DataTexture[];
detailTextureName?: string;
}) {
return (
<Suspense
@ -105,6 +139,7 @@ function TerrainMaterial({
visibilityMask={visibilityMask}
textureNames={textureNames}
alphaTextures={alphaTextures}
detailTextureName={detailTextureName}
/>
</Suspense>
);
@ -120,6 +155,7 @@ export const TerrainTile = memo(function TerrainTile({
displacementMap,
visibilityMask,
alphaTextures,
detailTextureName,
visible = true,
}: TerrainTileProps) {
const position = useMemo(() => {
@ -146,6 +182,7 @@ export const TerrainTile = memo(function TerrainTile({
visibilityMask={visibilityMask}
textureNames={textureNames}
alphaTextures={alphaTextures}
detailTextureName={detailTextureName}
/>
</mesh>
);

View file

@ -3,6 +3,19 @@
* Handles multi-layer texture blending for Tribes 2 terrain rendering.
*/
// Detail texture tiling factor.
// Torque uses world-space generation: U = worldX * (62.0 / textureWidth)
// For 256px texture across 2048 world units, this gives ~496 repeats mathematically.
// However, this appears visually excessive. Using a moderate multiplier relative
// to base texture tiling (32x) - detail should be finer but not overwhelming.
const DETAIL_TILING = 64.0;
// Distance at which detail texture fully fades out (in world units)
// Torque: zeroDetailDistance = (squareSize * worldToScreenScale) / 64 - squareSize/2
// For squareSize=8 and typical worldToScreenScale (~800), this gives ~96 units.
// Using 150 for a slightly more gradual fade.
const DETAIL_FADE_DISTANCE = 150.0;
export function updateTerrainTextureShader({
shader,
baseTextures,
@ -10,6 +23,7 @@ export function updateTerrainTextureShader({
visibilityMask,
tiling,
debugMode = false,
detailTexture = null,
}: {
shader: any;
baseTextures: any[];
@ -17,6 +31,7 @@ export function updateTerrainTextureShader({
visibilityMask: any;
tiling: Record<number, number>;
debugMode?: boolean;
detailTexture?: any;
}) {
const layerCount = baseTextures.length;
@ -45,6 +60,25 @@ export function updateTerrainTextureShader({
// Add debug mode uniform
shader.uniforms.debugMode = { value: debugMode ? 1.0 : 0.0 };
// Add detail texture uniforms
if (detailTexture) {
shader.uniforms.detailTexture = { value: detailTexture };
shader.uniforms.detailTiling = { value: DETAIL_TILING };
shader.uniforms.detailFadeDistance = { value: DETAIL_FADE_DISTANCE };
// Add vertex shader code to pass world position to fragment shader
shader.vertexShader = shader.vertexShader.replace(
"#include <common>",
`#include <common>
varying vec3 vTerrainWorldPos;`,
);
shader.vertexShader = shader.vertexShader.replace(
"#include <worldpos_vertex>",
`#include <worldpos_vertex>
vTerrainWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`,
);
}
// Declare our uniforms at the top of the fragment shader
shader.fragmentShader =
`
@ -67,6 +101,10 @@ uniform float tiling4;
uniform float tiling5;
uniform float debugMode;
${visibilityMask ? "uniform sampler2D visibilityMask;" : ""}
${detailTexture ? `uniform sampler2D detailTexture;
uniform float detailTiling;
uniform float detailFadeDistance;
varying vec3 vTerrainWorldPos;` : ""}
// Wireframe edge detection for debug mode
float getWireframe(vec2 uv, float gridSize, float lineWidth) {
@ -143,6 +181,24 @@ float getWireframe(vec2 uv, float gridSize, float lineWidth) {
// Assign to diffuseColor before lighting
vec3 textureColor = ${layerCount > 1 ? "blended" : "c0"};
${
detailTexture
? `// Detail texture blending (Torque-style multiplicative blend)
// Sample detail texture at high frequency tiling
vec3 detailColor = texture2D(detailTexture, baseUv * detailTiling).rgb;
// Calculate distance-based fade factor using world positions
// Torque: distFactor = (zeroDetailDistance - distance) / zeroDetailDistance
float distToCamera = distance(vTerrainWorldPos, cameraPosition);
float detailFade = clamp(1.0 - distToCamera / detailFadeDistance, 0.0, 1.0);
// Torque blending: dst * lerp(1.0, detailTexel, fadeFactor)
// Detail textures are authored with bright values (~0.8 mean), not 0.5 gray
// Direct multiplication adds subtle darkening for surface detail
textureColor *= mix(vec3(1.0), detailColor, detailFade);`
: ""
}
// Debug mode wireframe handling
if (debugMode > 0.5) {
// 256 grid cells across the terrain (matches terrain resolution)