Files
webeditor3d/src/rendering/terrain-layer-material.ts
Victor Giers 94dec56eb4 auto-git:
[add] src/rendering/terrain-layer-material.ts
 [add] tests/domain/terrains.test.ts
 [change] src/app/App.tsx
 [change] src/core/terrain-brush.ts
 [change] src/document/migrate-scene-document.ts
 [change] src/document/scene-document-validation.ts
 [change] src/document/scene-document.ts
 [change] src/document/terrains.ts
 [change] src/geometry/terrain-brush.ts
 [change] src/geometry/terrain-mesh.ts
 [change] src/runtime-three/rapier-collision-world.ts
 [change] src/runtime-three/runtime-host.ts
 [change] src/runtime-three/runtime-scene-build.ts
 [change] src/viewport-three/ViewportCanvas.tsx
 [change] src/viewport-three/ViewportPanel.tsx
 [change] src/viewport-three/viewport-host.ts
 [change] tests/domain/build-runtime-scene.test.ts
 [change] tests/domain/rapier-collision-world.test.ts
 [change] tests/domain/terrain.command.test.ts
 [change] tests/domain/water-material.test.ts
 [change] tests/geometry/terrain-brush.test.ts
 [change] tests/geometry/terrain-mesh.test.ts
 [change] tests/serialization/scene-document-json.test.ts
 [change] tests/unit/terrain-foundation.integration.test.tsx
 [change] tests/unit/viewport-canvas.test.tsx
2026-04-20 02:37:01 +02:00

145 lines
3.9 KiB
TypeScript

import {
ClampToEdgeWrapping,
Color,
DataTexture,
LinearFilter,
Material,
MeshBasicMaterial,
MeshStandardMaterial,
RGBAFormat,
SRGBColorSpace,
UnsignedByteType,
type Texture
} from "three";
import type { MaterialDef } from "../materials/starter-material-library";
interface TerrainLayerBlendMaterialOptions {
layerTextures: readonly [Texture, Texture, Texture, Texture];
emissiveHex?: number;
emissiveIntensity?: number;
wireframe?: boolean;
}
let fallbackTerrainLayerTexture: Texture | null = null;
function createFallbackTerrainLayerTexture(): Texture {
const texture = new DataTexture(
new Uint8Array([174, 167, 154, 255]),
1,
1,
RGBAFormat,
UnsignedByteType
);
texture.colorSpace = SRGBColorSpace;
texture.minFilter = LinearFilter;
texture.magFilter = LinearFilter;
texture.wrapS = ClampToEdgeWrapping;
texture.wrapT = ClampToEdgeWrapping;
texture.needsUpdate = true;
return texture;
}
export function getFallbackTerrainLayerTexture(): Texture {
if (fallbackTerrainLayerTexture === null) {
fallbackTerrainLayerTexture = createFallbackTerrainLayerTexture();
}
return fallbackTerrainLayerTexture;
}
export function getTerrainLayerTexture(
material: MaterialDef | null,
textureLookup: (material: MaterialDef) => Texture
): Texture {
return material === null
? getFallbackTerrainLayerTexture()
: textureLookup(material);
}
export function getTerrainLayerPreviewColor(material: MaterialDef | null): number {
return material === null
? new Color("#aea79a").getHex()
: new Color(material.swatchColorHex).getHex();
}
export function createTerrainLayerBlendMaterial(
options: TerrainLayerBlendMaterialOptions
): Material {
if (options.wireframe === true) {
return new MeshBasicMaterial({
color: 0xf2ece2,
wireframe: true
});
}
const material = new MeshStandardMaterial({
color: 0xffffff,
map: options.layerTextures[0],
emissive: options.emissiveHex ?? 0x000000,
emissiveIntensity: options.emissiveIntensity ?? 0,
roughness: 1,
metalness: 0
});
material.onBeforeCompile = (shader) => {
shader.uniforms.terrainLayerMap0 = { value: options.layerTextures[0] };
shader.uniforms.terrainLayerMap1 = { value: options.layerTextures[1] };
shader.uniforms.terrainLayerMap2 = { value: options.layerTextures[2] };
shader.uniforms.terrainLayerMap3 = { value: options.layerTextures[3] };
shader.vertexShader = shader.vertexShader
.replace(
"#include <common>",
`#include <common>
attribute vec4 terrainLayerWeights;
varying vec4 vTerrainLayerWeights;
varying vec2 vTerrainUv;`
)
.replace(
"#include <uv_vertex>",
`#include <uv_vertex>
vTerrainUv = uv;
vTerrainLayerWeights = terrainLayerWeights;`
);
shader.fragmentShader = shader.fragmentShader
.replace(
"#include <common>",
`#include <common>
varying vec4 vTerrainLayerWeights;
varying vec2 vTerrainUv;
uniform sampler2D terrainLayerMap0;
uniform sampler2D terrainLayerMap1;
uniform sampler2D terrainLayerMap2;
uniform sampler2D terrainLayerMap3;`
)
.replace(
"#include <map_fragment>",
`vec4 terrainWeights = max(vTerrainLayerWeights, 0.0);
float terrainWeightSum =
terrainWeights.x +
terrainWeights.y +
terrainWeights.z +
terrainWeights.w;
if (terrainWeightSum <= 0.0) {
terrainWeights = vec4(1.0, 0.0, 0.0, 0.0);
} else {
terrainWeights /= terrainWeightSum;
}
vec4 terrainLayerColor =
texture2D(terrainLayerMap0, vTerrainUv) * terrainWeights.x +
texture2D(terrainLayerMap1, vTerrainUv) * terrainWeights.y +
texture2D(terrainLayerMap2, vTerrainUv) * terrainWeights.z +
texture2D(terrainLayerMap3, vTerrainUv) * terrainWeights.w;
diffuseColor *= terrainLayerColor;`
);
};
material.customProgramCacheKey = () => "terrain-layer-blend-v1";
material.needsUpdate = true;
return material;
}