2026-04-20 02:36:38 +02:00
|
|
|
import {
|
|
|
|
|
ClampToEdgeWrapping,
|
|
|
|
|
Color,
|
|
|
|
|
DataTexture,
|
|
|
|
|
LinearFilter,
|
|
|
|
|
Material,
|
|
|
|
|
MeshBasicMaterial,
|
|
|
|
|
MeshStandardMaterial,
|
|
|
|
|
RGBAFormat,
|
|
|
|
|
SRGBColorSpace,
|
|
|
|
|
UnsignedByteType,
|
|
|
|
|
type Texture
|
|
|
|
|
} from "three";
|
|
|
|
|
|
|
|
|
|
import type { MaterialDef } from "../materials/starter-material-library";
|
2026-05-13 00:45:22 +02:00
|
|
|
import { TERRAIN_SHADER_LAYER_COUNT } from "../document/terrains";
|
2026-04-20 02:36:38 +02:00
|
|
|
|
|
|
|
|
interface TerrainLayerBlendMaterialOptions {
|
2026-05-13 00:45:22 +02:00
|
|
|
layerTextures: readonly Texture[];
|
2026-04-20 02:36:38 +02:00
|
|
|
emissiveHex?: number;
|
|
|
|
|
emissiveIntensity?: number;
|
2026-05-02 04:17:52 +02:00
|
|
|
foliageMaskPreviewColorHex?: number;
|
|
|
|
|
foliageMaskPreviewOpacity?: number;
|
2026-04-20 02:36:38 +02:00
|
|
|
wireframe?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 23:16:45 +02:00
|
|
|
interface TerrainLayerColorBlendMaterialOptions {
|
2026-05-13 00:45:22 +02:00
|
|
|
layerColors: readonly number[];
|
2026-04-29 23:16:45 +02:00
|
|
|
emissiveHex?: number;
|
|
|
|
|
emissiveIntensity?: number;
|
2026-05-02 04:17:52 +02:00
|
|
|
foliageMaskPreviewColorHex?: number;
|
|
|
|
|
foliageMaskPreviewOpacity?: number;
|
2026-04-29 23:16:45 +02:00
|
|
|
wireframe?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 02:36:38 +02:00
|
|
|
let fallbackTerrainLayerTexture: Texture | null = null;
|
2026-05-18 16:28:59 +02:00
|
|
|
const solidColorTerrainLayerTextures = new Map<string, Texture>();
|
2026-04-20 02:36:38 +02:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 16:28:59 +02:00
|
|
|
function getSolidColorTerrainLayerTexture(colorHex: string): Texture {
|
|
|
|
|
const cachedTexture = solidColorTerrainLayerTextures.get(colorHex);
|
|
|
|
|
|
|
|
|
|
if (cachedTexture !== undefined) {
|
|
|
|
|
return cachedTexture;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const color = new Color(colorHex);
|
|
|
|
|
const texture = new DataTexture(
|
|
|
|
|
new Uint8Array([
|
|
|
|
|
Math.round(color.r * 255),
|
|
|
|
|
Math.round(color.g * 255),
|
|
|
|
|
Math.round(color.b * 255),
|
|
|
|
|
255
|
|
|
|
|
]),
|
|
|
|
|
1,
|
|
|
|
|
1,
|
|
|
|
|
RGBAFormat,
|
|
|
|
|
UnsignedByteType
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
texture.colorSpace = SRGBColorSpace;
|
|
|
|
|
texture.minFilter = LinearFilter;
|
|
|
|
|
texture.magFilter = LinearFilter;
|
|
|
|
|
texture.wrapS = ClampToEdgeWrapping;
|
|
|
|
|
texture.wrapT = ClampToEdgeWrapping;
|
|
|
|
|
texture.needsUpdate = true;
|
|
|
|
|
solidColorTerrainLayerTextures.set(colorHex, texture);
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 02:36:38 +02:00
|
|
|
export function getTerrainLayerTexture(
|
|
|
|
|
material: MaterialDef | null,
|
2026-05-18 16:06:35 +02:00
|
|
|
textureLookup: (material: MaterialDef) => Texture | null
|
2026-04-20 02:36:38 +02:00
|
|
|
): Texture {
|
2026-05-18 16:28:59 +02:00
|
|
|
if (material === null) {
|
|
|
|
|
return getFallbackTerrainLayerTexture();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return textureLookup(material) ?? getSolidColorTerrainLayerTexture(material.swatchColorHex);
|
2026-04-20 02:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-18 16:20:41 +02:00
|
|
|
export function getTerrainLayerPreviewColor(
|
|
|
|
|
material: MaterialDef | null
|
|
|
|
|
): number {
|
2026-04-20 02:36:38 +02:00
|
|
|
return material === null
|
|
|
|
|
? new Color("#aea79a").getHex()
|
|
|
|
|
: new Color(material.swatchColorHex).getHex();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:45:22 +02:00
|
|
|
function createPaddedTerrainLayerTextures(
|
|
|
|
|
textures: readonly Texture[]
|
|
|
|
|
): Texture[] {
|
|
|
|
|
const fallbackTexture = getFallbackTerrainLayerTexture();
|
|
|
|
|
|
|
|
|
|
return Array.from(
|
|
|
|
|
{ length: TERRAIN_SHADER_LAYER_COUNT },
|
|
|
|
|
(_, index) => textures[index] ?? fallbackTexture
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 16:20:41 +02:00
|
|
|
function createPaddedTerrainLayerColors(
|
|
|
|
|
layerColors: readonly number[]
|
|
|
|
|
): Color[] {
|
2026-05-13 00:45:22 +02:00
|
|
|
const fallbackColor = getTerrainLayerPreviewColor(null);
|
|
|
|
|
|
|
|
|
|
return Array.from(
|
|
|
|
|
{ length: TERRAIN_SHADER_LAYER_COUNT },
|
|
|
|
|
(_, index) => new Color(layerColors[index] ?? fallbackColor)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 02:36:38 +02:00
|
|
|
export function createTerrainLayerBlendMaterial(
|
|
|
|
|
options: TerrainLayerBlendMaterialOptions
|
|
|
|
|
): Material {
|
2026-05-13 00:45:46 +02:00
|
|
|
const layerTextures = createPaddedTerrainLayerTextures(options.layerTextures);
|
|
|
|
|
|
2026-04-20 02:36:38 +02:00
|
|
|
if (options.wireframe === true) {
|
|
|
|
|
return new MeshBasicMaterial({
|
|
|
|
|
color: 0xf2ece2,
|
|
|
|
|
wireframe: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const material = new MeshStandardMaterial({
|
|
|
|
|
color: 0xffffff,
|
2026-05-13 00:45:46 +02:00
|
|
|
map: layerTextures[0],
|
2026-04-20 02:36:38 +02:00
|
|
|
emissive: options.emissiveHex ?? 0x000000,
|
|
|
|
|
emissiveIntensity: options.emissiveIntensity ?? 0,
|
|
|
|
|
roughness: 1,
|
|
|
|
|
metalness: 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
material.onBeforeCompile = (shader) => {
|
2026-05-13 00:45:46 +02:00
|
|
|
shader.uniforms.terrainLayerMap0 = { value: layerTextures[0] };
|
|
|
|
|
shader.uniforms.terrainLayerMap1 = { value: layerTextures[1] };
|
|
|
|
|
shader.uniforms.terrainLayerMap2 = { value: layerTextures[2] };
|
|
|
|
|
shader.uniforms.terrainLayerMap3 = { value: layerTextures[3] };
|
|
|
|
|
shader.uniforms.terrainLayerMap4 = { value: layerTextures[4] };
|
|
|
|
|
shader.uniforms.terrainLayerMap5 = { value: layerTextures[5] };
|
|
|
|
|
shader.uniforms.terrainLayerMap6 = { value: layerTextures[6] };
|
|
|
|
|
shader.uniforms.terrainLayerMap7 = { value: layerTextures[7] };
|
2026-05-02 04:17:52 +02:00
|
|
|
shader.uniforms.terrainFoliageMaskPreviewColor = {
|
|
|
|
|
value: new Color(options.foliageMaskPreviewColorHex ?? 0x65d36e)
|
|
|
|
|
};
|
|
|
|
|
shader.uniforms.terrainFoliageMaskPreviewOpacity = {
|
|
|
|
|
value: options.foliageMaskPreviewOpacity ?? 0
|
|
|
|
|
};
|
2026-04-20 02:36:38 +02:00
|
|
|
|
|
|
|
|
shader.vertexShader = shader.vertexShader
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <common>",
|
|
|
|
|
`#include <common>
|
|
|
|
|
attribute vec4 terrainLayerWeights;
|
2026-05-13 00:45:46 +02:00
|
|
|
attribute vec4 terrainLayerWeights0;
|
|
|
|
|
attribute vec4 terrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
attribute float terrainFoliageMask;
|
2026-05-13 00:45:46 +02:00
|
|
|
varying vec4 vTerrainLayerWeights0;
|
|
|
|
|
varying vec4 vTerrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
varying float vTerrainFoliageMask;
|
2026-04-20 02:36:38 +02:00
|
|
|
varying vec2 vTerrainUv;`
|
|
|
|
|
)
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <uv_vertex>",
|
|
|
|
|
`#include <uv_vertex>
|
|
|
|
|
vTerrainUv = uv;
|
2026-05-13 00:45:46 +02:00
|
|
|
vTerrainLayerWeights0 = terrainLayerWeights0;
|
|
|
|
|
vTerrainLayerWeights1 = terrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
vTerrainFoliageMask = terrainFoliageMask;`
|
2026-04-20 02:36:38 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
shader.fragmentShader = shader.fragmentShader
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <common>",
|
|
|
|
|
`#include <common>
|
2026-05-13 00:45:46 +02:00
|
|
|
varying vec4 vTerrainLayerWeights0;
|
|
|
|
|
varying vec4 vTerrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
varying float vTerrainFoliageMask;
|
2026-04-20 02:36:38 +02:00
|
|
|
varying vec2 vTerrainUv;
|
|
|
|
|
uniform sampler2D terrainLayerMap0;
|
|
|
|
|
uniform sampler2D terrainLayerMap1;
|
|
|
|
|
uniform sampler2D terrainLayerMap2;
|
2026-05-02 04:17:52 +02:00
|
|
|
uniform sampler2D terrainLayerMap3;
|
2026-05-13 00:45:46 +02:00
|
|
|
uniform sampler2D terrainLayerMap4;
|
|
|
|
|
uniform sampler2D terrainLayerMap5;
|
|
|
|
|
uniform sampler2D terrainLayerMap6;
|
|
|
|
|
uniform sampler2D terrainLayerMap7;
|
2026-05-02 04:17:52 +02:00
|
|
|
uniform vec3 terrainFoliageMaskPreviewColor;
|
|
|
|
|
uniform float terrainFoliageMaskPreviewOpacity;`
|
2026-04-20 02:36:38 +02:00
|
|
|
)
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <map_fragment>",
|
2026-05-13 00:45:46 +02:00
|
|
|
`vec4 terrainWeights0 = max(vTerrainLayerWeights0, 0.0);
|
|
|
|
|
vec4 terrainWeights1 = max(vTerrainLayerWeights1, 0.0);
|
2026-04-20 02:36:38 +02:00
|
|
|
float terrainWeightSum =
|
2026-05-13 00:45:46 +02:00
|
|
|
terrainWeights0.x +
|
|
|
|
|
terrainWeights0.y +
|
|
|
|
|
terrainWeights0.z +
|
|
|
|
|
terrainWeights0.w +
|
|
|
|
|
terrainWeights1.x +
|
|
|
|
|
terrainWeights1.y +
|
|
|
|
|
terrainWeights1.z +
|
|
|
|
|
terrainWeights1.w;
|
2026-04-20 02:36:38 +02:00
|
|
|
|
|
|
|
|
if (terrainWeightSum <= 0.0) {
|
2026-05-13 00:45:46 +02:00
|
|
|
terrainWeights0 = vec4(1.0, 0.0, 0.0, 0.0);
|
|
|
|
|
terrainWeights1 = vec4(0.0);
|
2026-04-20 02:36:38 +02:00
|
|
|
} else {
|
2026-05-13 00:45:46 +02:00
|
|
|
terrainWeights0 /= terrainWeightSum;
|
|
|
|
|
terrainWeights1 /= terrainWeightSum;
|
2026-04-20 02:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vec4 terrainLayerColor =
|
2026-05-13 00:45:46 +02:00
|
|
|
texture2D(terrainLayerMap0, vTerrainUv) * terrainWeights0.x +
|
|
|
|
|
texture2D(terrainLayerMap1, vTerrainUv) * terrainWeights0.y +
|
|
|
|
|
texture2D(terrainLayerMap2, vTerrainUv) * terrainWeights0.z +
|
|
|
|
|
texture2D(terrainLayerMap3, vTerrainUv) * terrainWeights0.w +
|
|
|
|
|
texture2D(terrainLayerMap4, vTerrainUv) * terrainWeights1.x +
|
|
|
|
|
texture2D(terrainLayerMap5, vTerrainUv) * terrainWeights1.y +
|
|
|
|
|
texture2D(terrainLayerMap6, vTerrainUv) * terrainWeights1.z +
|
|
|
|
|
texture2D(terrainLayerMap7, vTerrainUv) * terrainWeights1.w;
|
2026-05-02 04:17:52 +02:00
|
|
|
diffuseColor *= terrainLayerColor;
|
|
|
|
|
float foliageMaskPreviewBlend = clamp(
|
|
|
|
|
vTerrainFoliageMask * terrainFoliageMaskPreviewOpacity,
|
|
|
|
|
0.0,
|
|
|
|
|
terrainFoliageMaskPreviewOpacity
|
|
|
|
|
);
|
|
|
|
|
diffuseColor.rgb = mix(
|
|
|
|
|
diffuseColor.rgb,
|
|
|
|
|
terrainFoliageMaskPreviewColor,
|
|
|
|
|
foliageMaskPreviewBlend
|
|
|
|
|
);`
|
2026-04-20 02:36:38 +02:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-13 00:45:46 +02:00
|
|
|
material.customProgramCacheKey = () => "terrain-layer-blend-v3";
|
2026-04-20 02:36:38 +02:00
|
|
|
material.needsUpdate = true;
|
|
|
|
|
return material;
|
|
|
|
|
}
|
2026-04-29 23:16:45 +02:00
|
|
|
|
|
|
|
|
export function createTerrainLayerColorBlendMaterial(
|
|
|
|
|
options: TerrainLayerColorBlendMaterialOptions
|
|
|
|
|
): Material {
|
2026-05-13 00:46:09 +02:00
|
|
|
const layerColors = createPaddedTerrainLayerColors(options.layerColors);
|
|
|
|
|
|
2026-04-29 23:16:45 +02:00
|
|
|
if (options.wireframe === true) {
|
|
|
|
|
return new MeshBasicMaterial({
|
|
|
|
|
color: 0xf2ece2,
|
|
|
|
|
wireframe: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const material = new MeshStandardMaterial({
|
|
|
|
|
color: 0xffffff,
|
|
|
|
|
emissive: options.emissiveHex ?? 0x000000,
|
|
|
|
|
emissiveIntensity: options.emissiveIntensity ?? 0,
|
|
|
|
|
roughness: 1,
|
|
|
|
|
metalness: 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
material.onBeforeCompile = (shader) => {
|
|
|
|
|
shader.uniforms.terrainLayerColor0 = { value: layerColors[0] };
|
|
|
|
|
shader.uniforms.terrainLayerColor1 = { value: layerColors[1] };
|
|
|
|
|
shader.uniforms.terrainLayerColor2 = { value: layerColors[2] };
|
|
|
|
|
shader.uniforms.terrainLayerColor3 = { value: layerColors[3] };
|
2026-05-13 00:46:09 +02:00
|
|
|
shader.uniforms.terrainLayerColor4 = { value: layerColors[4] };
|
|
|
|
|
shader.uniforms.terrainLayerColor5 = { value: layerColors[5] };
|
|
|
|
|
shader.uniforms.terrainLayerColor6 = { value: layerColors[6] };
|
|
|
|
|
shader.uniforms.terrainLayerColor7 = { value: layerColors[7] };
|
2026-05-02 04:17:52 +02:00
|
|
|
shader.uniforms.terrainFoliageMaskPreviewColor = {
|
|
|
|
|
value: new Color(options.foliageMaskPreviewColorHex ?? 0x65d36e)
|
|
|
|
|
};
|
|
|
|
|
shader.uniforms.terrainFoliageMaskPreviewOpacity = {
|
|
|
|
|
value: options.foliageMaskPreviewOpacity ?? 0
|
|
|
|
|
};
|
2026-04-29 23:16:45 +02:00
|
|
|
|
|
|
|
|
shader.vertexShader = shader.vertexShader
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <common>",
|
|
|
|
|
`#include <common>
|
|
|
|
|
attribute vec4 terrainLayerWeights;
|
2026-05-13 00:46:09 +02:00
|
|
|
attribute vec4 terrainLayerWeights0;
|
|
|
|
|
attribute vec4 terrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
attribute float terrainFoliageMask;
|
2026-05-13 00:46:09 +02:00
|
|
|
varying vec4 vTerrainLayerWeights0;
|
|
|
|
|
varying vec4 vTerrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
varying float vTerrainFoliageMask;`
|
2026-04-29 23:16:45 +02:00
|
|
|
)
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <uv_vertex>",
|
|
|
|
|
`#include <uv_vertex>
|
2026-05-13 00:46:09 +02:00
|
|
|
vTerrainLayerWeights0 = terrainLayerWeights0;
|
|
|
|
|
vTerrainLayerWeights1 = terrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
vTerrainFoliageMask = terrainFoliageMask;`
|
2026-04-29 23:16:45 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
shader.fragmentShader = shader.fragmentShader
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <common>",
|
|
|
|
|
`#include <common>
|
2026-05-13 00:46:09 +02:00
|
|
|
varying vec4 vTerrainLayerWeights0;
|
|
|
|
|
varying vec4 vTerrainLayerWeights1;
|
2026-05-02 04:17:52 +02:00
|
|
|
varying float vTerrainFoliageMask;
|
2026-04-29 23:16:45 +02:00
|
|
|
uniform vec3 terrainLayerColor0;
|
|
|
|
|
uniform vec3 terrainLayerColor1;
|
|
|
|
|
uniform vec3 terrainLayerColor2;
|
2026-05-02 04:17:52 +02:00
|
|
|
uniform vec3 terrainLayerColor3;
|
2026-05-13 00:46:09 +02:00
|
|
|
uniform vec3 terrainLayerColor4;
|
|
|
|
|
uniform vec3 terrainLayerColor5;
|
|
|
|
|
uniform vec3 terrainLayerColor6;
|
|
|
|
|
uniform vec3 terrainLayerColor7;
|
2026-05-02 04:17:52 +02:00
|
|
|
uniform vec3 terrainFoliageMaskPreviewColor;
|
|
|
|
|
uniform float terrainFoliageMaskPreviewOpacity;`
|
2026-04-29 23:16:45 +02:00
|
|
|
)
|
|
|
|
|
.replace(
|
|
|
|
|
"#include <map_fragment>",
|
2026-05-13 00:46:09 +02:00
|
|
|
`vec4 terrainWeights0 = max(vTerrainLayerWeights0, 0.0);
|
|
|
|
|
vec4 terrainWeights1 = max(vTerrainLayerWeights1, 0.0);
|
2026-04-29 23:16:45 +02:00
|
|
|
float terrainWeightSum =
|
2026-05-13 00:46:09 +02:00
|
|
|
terrainWeights0.x +
|
|
|
|
|
terrainWeights0.y +
|
|
|
|
|
terrainWeights0.z +
|
|
|
|
|
terrainWeights0.w +
|
|
|
|
|
terrainWeights1.x +
|
|
|
|
|
terrainWeights1.y +
|
|
|
|
|
terrainWeights1.z +
|
|
|
|
|
terrainWeights1.w;
|
2026-04-29 23:16:45 +02:00
|
|
|
|
|
|
|
|
if (terrainWeightSum <= 0.0) {
|
2026-05-13 00:46:09 +02:00
|
|
|
terrainWeights0 = vec4(1.0, 0.0, 0.0, 0.0);
|
|
|
|
|
terrainWeights1 = vec4(0.0);
|
2026-04-29 23:16:45 +02:00
|
|
|
} else {
|
2026-05-13 00:46:09 +02:00
|
|
|
terrainWeights0 /= terrainWeightSum;
|
|
|
|
|
terrainWeights1 /= terrainWeightSum;
|
2026-04-29 23:16:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vec3 terrainLayerColor =
|
2026-05-13 00:46:09 +02:00
|
|
|
terrainLayerColor0 * terrainWeights0.x +
|
|
|
|
|
terrainLayerColor1 * terrainWeights0.y +
|
|
|
|
|
terrainLayerColor2 * terrainWeights0.z +
|
|
|
|
|
terrainLayerColor3 * terrainWeights0.w +
|
|
|
|
|
terrainLayerColor4 * terrainWeights1.x +
|
|
|
|
|
terrainLayerColor5 * terrainWeights1.y +
|
|
|
|
|
terrainLayerColor6 * terrainWeights1.z +
|
|
|
|
|
terrainLayerColor7 * terrainWeights1.w;
|
2026-05-02 04:17:52 +02:00
|
|
|
diffuseColor.rgb *= terrainLayerColor;
|
|
|
|
|
float foliageMaskPreviewBlend = clamp(
|
|
|
|
|
vTerrainFoliageMask * terrainFoliageMaskPreviewOpacity,
|
|
|
|
|
0.0,
|
|
|
|
|
terrainFoliageMaskPreviewOpacity
|
|
|
|
|
);
|
|
|
|
|
diffuseColor.rgb = mix(
|
|
|
|
|
diffuseColor.rgb,
|
|
|
|
|
terrainFoliageMaskPreviewColor,
|
|
|
|
|
foliageMaskPreviewBlend
|
|
|
|
|
);`
|
2026-04-29 23:16:45 +02:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-13 00:46:09 +02:00
|
|
|
material.customProgramCacheKey = () => "terrain-layer-color-blend-v3";
|
2026-04-29 23:16:45 +02:00
|
|
|
material.needsUpdate = true;
|
|
|
|
|
return material;
|
|
|
|
|
}
|