926 lines
24 KiB
TypeScript
926 lines
24 KiB
TypeScript
import { BufferAttribute, BufferGeometry } from "three";
|
|
|
|
import type { Vec3 } from "../core/vector";
|
|
import {
|
|
getTerrainFoliageMask,
|
|
getTerrainFoliageBlockerMaskValueAtSample,
|
|
getTerrainFoliageMaskValueAtSample,
|
|
getTerrainHeightAtSample,
|
|
getTerrainSampleLayerWeights,
|
|
TERRAIN_SHADER_LAYER_COUNT,
|
|
type Terrain
|
|
} from "../document/terrains";
|
|
|
|
export type TerrainCellDiagonal = "forward" | "backward";
|
|
|
|
export interface TerrainCellTriangulation {
|
|
cellX: number;
|
|
cellZ: number;
|
|
diagonal: TerrainCellDiagonal;
|
|
}
|
|
|
|
export interface DerivedTerrainMeshData {
|
|
geometry: BufferGeometry;
|
|
positions: Float32Array;
|
|
normals: Float32Array;
|
|
uvs: Float32Array;
|
|
layerWeights: Float32Array;
|
|
foliageMaskWeights: Float32Array;
|
|
indices: Uint32Array;
|
|
cellTriangulation: TerrainCellTriangulation[];
|
|
localBounds: {
|
|
min: Vec3;
|
|
max: Vec3;
|
|
};
|
|
}
|
|
|
|
export const TERRAIN_LOD_CHUNK_SIZE_CELLS = 64;
|
|
export const TERRAIN_LOD_STRIDES = [1, 2, 4, 8, 16] as const;
|
|
export const TERRAIN_LOD_DEBUG_COLORS = [
|
|
0xff4d4d,
|
|
0xffa53d,
|
|
0xffe66d,
|
|
0x4ee06f,
|
|
0x4ba3ff
|
|
] as const;
|
|
const TERRAIN_LOD_DISTANCE_MULTIPLIERS = [0.75, 1.5, 3, 6] as const;
|
|
const TERRAIN_LOD_HYSTERESIS_RATIO = 0.16;
|
|
|
|
interface TerrainMeshBuildOptions {
|
|
foliageMaskLayerId?: string | null;
|
|
foliageBlockerMask?: boolean;
|
|
}
|
|
|
|
export interface TerrainLodLevelMeshData {
|
|
level: number;
|
|
stride: number;
|
|
geometry: BufferGeometry;
|
|
positions: Float32Array;
|
|
normals: Float32Array;
|
|
uvs: Float32Array;
|
|
layerWeights: Float32Array;
|
|
foliageMaskWeights: Float32Array;
|
|
indices: Uint32Array;
|
|
skirtVertexCount: number;
|
|
}
|
|
|
|
export interface TerrainLodChunkMeshData {
|
|
chunkX: number;
|
|
chunkZ: number;
|
|
startSampleX: number;
|
|
startSampleZ: number;
|
|
endSampleX: number;
|
|
endSampleZ: number;
|
|
cellCountX: number;
|
|
cellCountZ: number;
|
|
levels: TerrainLodLevelMeshData[];
|
|
localBounds: {
|
|
min: Vec3;
|
|
max: Vec3;
|
|
};
|
|
localCenter: Vec3;
|
|
diagonal: number;
|
|
}
|
|
|
|
export interface DerivedTerrainLodMeshData {
|
|
chunkSizeCells: number;
|
|
chunks: TerrainLodChunkMeshData[];
|
|
localBounds: {
|
|
min: Vec3;
|
|
max: Vec3;
|
|
};
|
|
}
|
|
|
|
function setTerrainLayerWeightAttributes(
|
|
geometry: BufferGeometry,
|
|
layerWeights: Float32Array
|
|
) {
|
|
const vertexCount = layerWeights.length / TERRAIN_SHADER_LAYER_COUNT;
|
|
const firstWeights = new Float32Array(vertexCount * 4);
|
|
const secondWeights = new Float32Array(vertexCount * 4);
|
|
|
|
for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex += 1) {
|
|
const sourceOffset = vertexIndex * TERRAIN_SHADER_LAYER_COUNT;
|
|
const targetOffset = vertexIndex * 4;
|
|
|
|
firstWeights[targetOffset] = layerWeights[sourceOffset] ?? 0;
|
|
firstWeights[targetOffset + 1] = layerWeights[sourceOffset + 1] ?? 0;
|
|
firstWeights[targetOffset + 2] = layerWeights[sourceOffset + 2] ?? 0;
|
|
firstWeights[targetOffset + 3] = layerWeights[sourceOffset + 3] ?? 0;
|
|
secondWeights[targetOffset] = layerWeights[sourceOffset + 4] ?? 0;
|
|
secondWeights[targetOffset + 1] = layerWeights[sourceOffset + 5] ?? 0;
|
|
secondWeights[targetOffset + 2] = layerWeights[sourceOffset + 6] ?? 0;
|
|
secondWeights[targetOffset + 3] = layerWeights[sourceOffset + 7] ?? 0;
|
|
}
|
|
|
|
geometry.setAttribute(
|
|
"terrainLayerWeights0",
|
|
new BufferAttribute(firstWeights, 4)
|
|
);
|
|
geometry.setAttribute(
|
|
"terrainLayerWeights1",
|
|
new BufferAttribute(secondWeights, 4)
|
|
);
|
|
geometry.setAttribute(
|
|
"terrainLayerWeights",
|
|
new BufferAttribute(firstWeights, 4)
|
|
);
|
|
}
|
|
|
|
function pushPaddedTerrainLayerWeights(
|
|
target: number[],
|
|
sampleLayerWeights: readonly number[]
|
|
) {
|
|
for (
|
|
let layerIndex = 0;
|
|
layerIndex < TERRAIN_SHADER_LAYER_COUNT;
|
|
layerIndex += 1
|
|
) {
|
|
target.push(sampleLayerWeights[layerIndex] ?? 0);
|
|
}
|
|
}
|
|
|
|
function createEmptyLocalBounds(): { min: Vec3; max: Vec3 } {
|
|
return {
|
|
min: {
|
|
x: Number.POSITIVE_INFINITY,
|
|
y: Number.POSITIVE_INFINITY,
|
|
z: Number.POSITIVE_INFINITY
|
|
},
|
|
max: {
|
|
x: Number.NEGATIVE_INFINITY,
|
|
y: Number.NEGATIVE_INFINITY,
|
|
z: Number.NEGATIVE_INFINITY
|
|
}
|
|
};
|
|
}
|
|
|
|
function includePointInBounds(bounds: { min: Vec3; max: Vec3 }, point: Vec3) {
|
|
bounds.min.x = Math.min(bounds.min.x, point.x);
|
|
bounds.min.y = Math.min(bounds.min.y, point.y);
|
|
bounds.min.z = Math.min(bounds.min.z, point.z);
|
|
bounds.max.x = Math.max(bounds.max.x, point.x);
|
|
bounds.max.y = Math.max(bounds.max.y, point.y);
|
|
bounds.max.z = Math.max(bounds.max.z, point.z);
|
|
}
|
|
|
|
function cloneBounds(bounds: { min: Vec3; max: Vec3 }): {
|
|
min: Vec3;
|
|
max: Vec3;
|
|
} {
|
|
return {
|
|
min: {
|
|
x: bounds.min.x,
|
|
y: bounds.min.y,
|
|
z: bounds.min.z
|
|
},
|
|
max: {
|
|
x: bounds.max.x,
|
|
y: bounds.max.y,
|
|
z: bounds.max.z
|
|
}
|
|
};
|
|
}
|
|
|
|
function chooseCellDiagonal(
|
|
topLeft: number,
|
|
topRight: number,
|
|
bottomLeft: number,
|
|
bottomRight: number
|
|
): TerrainCellDiagonal {
|
|
return Math.abs(topLeft - bottomRight) <= Math.abs(topRight - bottomLeft)
|
|
? "forward"
|
|
: "backward";
|
|
}
|
|
|
|
function pushCellIndices(
|
|
indices: number[],
|
|
cellTriangulation: TerrainCellTriangulation[],
|
|
cellX: number,
|
|
cellZ: number,
|
|
diagonal: TerrainCellDiagonal,
|
|
topLeft: number,
|
|
topRight: number,
|
|
bottomLeft: number,
|
|
bottomRight: number
|
|
) {
|
|
cellTriangulation.push({
|
|
cellX,
|
|
cellZ,
|
|
diagonal
|
|
});
|
|
|
|
if (diagonal === "forward") {
|
|
indices.push(topLeft, bottomLeft, bottomRight);
|
|
indices.push(topLeft, bottomRight, topRight);
|
|
return;
|
|
}
|
|
|
|
indices.push(topLeft, bottomLeft, topRight);
|
|
indices.push(topRight, bottomLeft, bottomRight);
|
|
}
|
|
|
|
export function buildTerrainDerivedMeshData(
|
|
terrain: Terrain,
|
|
options: TerrainMeshBuildOptions = {}
|
|
): DerivedTerrainMeshData {
|
|
const vertexCount = terrain.sampleCountX * terrain.sampleCountZ;
|
|
const positions = new Float32Array(vertexCount * 3);
|
|
const uvs = new Float32Array(vertexCount * 2);
|
|
const layerWeights = new Float32Array(vertexCount * TERRAIN_LAYER_COUNT);
|
|
const foliageMaskWeights = new Float32Array(vertexCount);
|
|
const foliageMask =
|
|
options.foliageMaskLayerId === undefined ||
|
|
options.foliageMaskLayerId === null
|
|
? null
|
|
: getTerrainFoliageMask(terrain, options.foliageMaskLayerId);
|
|
const localBounds = createEmptyLocalBounds();
|
|
let vertexOffset = 0;
|
|
let uvOffset = 0;
|
|
let layerWeightOffset = 0;
|
|
let foliageMaskWeightOffset = 0;
|
|
|
|
for (let sampleZ = 0; sampleZ < terrain.sampleCountZ; sampleZ += 1) {
|
|
for (let sampleX = 0; sampleX < terrain.sampleCountX; sampleX += 1) {
|
|
const localX = sampleX * terrain.cellSize;
|
|
const localY = getTerrainHeightAtSample(terrain, sampleX, sampleZ);
|
|
const sampleLayerWeights = getTerrainSampleLayerWeights(
|
|
terrain,
|
|
sampleX,
|
|
sampleZ
|
|
);
|
|
const localZ = sampleZ * terrain.cellSize;
|
|
positions[vertexOffset] = localX;
|
|
positions[vertexOffset + 1] = localY;
|
|
positions[vertexOffset + 2] = localZ;
|
|
vertexOffset += 3;
|
|
|
|
localBounds.min.x = Math.min(localBounds.min.x, localX);
|
|
localBounds.min.y = Math.min(localBounds.min.y, localY);
|
|
localBounds.min.z = Math.min(localBounds.min.z, localZ);
|
|
localBounds.max.x = Math.max(localBounds.max.x, localX);
|
|
localBounds.max.y = Math.max(localBounds.max.y, localY);
|
|
localBounds.max.z = Math.max(localBounds.max.z, localZ);
|
|
|
|
uvs[uvOffset] = terrain.position.x + localX;
|
|
uvs[uvOffset + 1] = terrain.position.z + localZ;
|
|
uvOffset += 2;
|
|
|
|
for (
|
|
let layerIndex = 0;
|
|
layerIndex < TERRAIN_LAYER_COUNT;
|
|
layerIndex += 1
|
|
) {
|
|
layerWeights[layerWeightOffset + layerIndex] =
|
|
sampleLayerWeights[layerIndex];
|
|
}
|
|
layerWeightOffset += TERRAIN_LAYER_COUNT;
|
|
foliageMaskWeights[foliageMaskWeightOffset] =
|
|
options.foliageBlockerMask === true
|
|
? getTerrainFoliageBlockerMaskValueAtSample(
|
|
terrain.foliageBlockerMask,
|
|
sampleX,
|
|
sampleZ
|
|
)
|
|
: foliageMask === null
|
|
? 0
|
|
: getTerrainFoliageMaskValueAtSample(
|
|
foliageMask,
|
|
sampleX,
|
|
sampleZ
|
|
);
|
|
foliageMaskWeightOffset += 1;
|
|
}
|
|
}
|
|
|
|
const indexValues: number[] = [];
|
|
const cellTriangulation: TerrainCellTriangulation[] = [];
|
|
|
|
for (let cellZ = 0; cellZ < terrain.sampleCountZ - 1; cellZ += 1) {
|
|
for (let cellX = 0; cellX < terrain.sampleCountX - 1; cellX += 1) {
|
|
const topLeft = cellZ * terrain.sampleCountX + cellX;
|
|
const topRight = topLeft + 1;
|
|
const bottomLeft = (cellZ + 1) * terrain.sampleCountX + cellX;
|
|
const bottomRight = bottomLeft + 1;
|
|
const diagonal = chooseCellDiagonal(
|
|
getTerrainHeightAtSample(terrain, cellX, cellZ),
|
|
getTerrainHeightAtSample(terrain, cellX + 1, cellZ),
|
|
getTerrainHeightAtSample(terrain, cellX, cellZ + 1),
|
|
getTerrainHeightAtSample(terrain, cellX + 1, cellZ + 1)
|
|
);
|
|
|
|
pushCellIndices(
|
|
indexValues,
|
|
cellTriangulation,
|
|
cellX,
|
|
cellZ,
|
|
diagonal,
|
|
topLeft,
|
|
topRight,
|
|
bottomLeft,
|
|
bottomRight
|
|
);
|
|
}
|
|
}
|
|
|
|
const indices = new Uint32Array(indexValues);
|
|
const geometry = new BufferGeometry();
|
|
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
geometry.setAttribute("uv", new BufferAttribute(uvs, 2));
|
|
geometry.setAttribute(
|
|
"terrainLayerWeights",
|
|
new BufferAttribute(layerWeights, TERRAIN_LAYER_COUNT)
|
|
);
|
|
geometry.setAttribute(
|
|
"terrainFoliageMask",
|
|
new BufferAttribute(foliageMaskWeights, 1)
|
|
);
|
|
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
geometry.computeVertexNormals();
|
|
|
|
const normalAttribute = geometry.getAttribute("normal");
|
|
const normals = new Float32Array(normalAttribute.array.length);
|
|
normals.set(normalAttribute.array as ArrayLike<number>);
|
|
|
|
return {
|
|
geometry,
|
|
positions,
|
|
normals,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
indices,
|
|
cellTriangulation,
|
|
localBounds
|
|
};
|
|
}
|
|
|
|
function createLodSampleCoordinates(
|
|
startSample: number,
|
|
endSample: number,
|
|
stride: number
|
|
): number[] {
|
|
if (startSample === endSample) {
|
|
return [startSample];
|
|
}
|
|
|
|
const coordinates: number[] = [];
|
|
|
|
for (
|
|
let sample = startSample;
|
|
sample <= endSample;
|
|
sample += Math.max(1, stride)
|
|
) {
|
|
coordinates.push(sample);
|
|
}
|
|
|
|
if (coordinates[coordinates.length - 1] !== endSample) {
|
|
coordinates.push(endSample);
|
|
}
|
|
|
|
return coordinates;
|
|
}
|
|
|
|
function getUsefulTerrainLodStrides(cellCountX: number, cellCountZ: number) {
|
|
const strides: number[] = [];
|
|
let previousSignature = "";
|
|
|
|
for (const stride of TERRAIN_LOD_STRIDES) {
|
|
const xCount = createLodSampleCoordinates(0, cellCountX, stride).length;
|
|
const zCount = createLodSampleCoordinates(0, cellCountZ, stride).length;
|
|
const signature = `${xCount}:${zCount}`;
|
|
|
|
if (signature === previousSignature) {
|
|
continue;
|
|
}
|
|
|
|
strides.push(stride);
|
|
previousSignature = signature;
|
|
}
|
|
|
|
return strides;
|
|
}
|
|
|
|
function buildTerrainChunkSourceBounds(
|
|
terrain: Terrain,
|
|
startSampleX: number,
|
|
startSampleZ: number,
|
|
endSampleX: number,
|
|
endSampleZ: number
|
|
) {
|
|
const localBounds = createEmptyLocalBounds();
|
|
|
|
for (let sampleZ = startSampleZ; sampleZ <= endSampleZ; sampleZ += 1) {
|
|
for (let sampleX = startSampleX; sampleX <= endSampleX; sampleX += 1) {
|
|
includePointInBounds(localBounds, {
|
|
x: sampleX * terrain.cellSize,
|
|
y: getTerrainHeightAtSample(terrain, sampleX, sampleZ),
|
|
z: sampleZ * terrain.cellSize
|
|
});
|
|
}
|
|
}
|
|
|
|
return localBounds;
|
|
}
|
|
|
|
function pushTerrainLodVertex(
|
|
terrain: Terrain,
|
|
sampleX: number,
|
|
sampleZ: number,
|
|
yOffset: number,
|
|
positions: number[],
|
|
uvs: number[],
|
|
layerWeights: number[],
|
|
foliageMaskWeights: number[],
|
|
options: TerrainMeshBuildOptions
|
|
) {
|
|
const localX = sampleX * terrain.cellSize;
|
|
const localY = getTerrainHeightAtSample(terrain, sampleX, sampleZ) + yOffset;
|
|
const localZ = sampleZ * terrain.cellSize;
|
|
const sampleLayerWeights = getTerrainSampleLayerWeights(
|
|
terrain,
|
|
sampleX,
|
|
sampleZ
|
|
);
|
|
|
|
positions.push(localX, localY, localZ);
|
|
uvs.push(terrain.position.x + localX, terrain.position.z + localZ);
|
|
|
|
for (const weight of sampleLayerWeights) {
|
|
layerWeights.push(weight);
|
|
}
|
|
|
|
const foliageMask =
|
|
options.foliageMaskLayerId === undefined ||
|
|
options.foliageMaskLayerId === null
|
|
? null
|
|
: getTerrainFoliageMask(terrain, options.foliageMaskLayerId);
|
|
foliageMaskWeights.push(
|
|
options.foliageBlockerMask === true
|
|
? getTerrainFoliageBlockerMaskValueAtSample(
|
|
terrain.foliageBlockerMask,
|
|
sampleX,
|
|
sampleZ
|
|
)
|
|
: foliageMask === null
|
|
? 0
|
|
: getTerrainFoliageMaskValueAtSample(foliageMask, sampleX, sampleZ)
|
|
);
|
|
}
|
|
|
|
function pushTerrainLodSkirtSegment(
|
|
terrain: Terrain,
|
|
startSampleX: number,
|
|
startSampleZ: number,
|
|
endSampleX: number,
|
|
endSampleZ: number,
|
|
skirtDepth: number,
|
|
positions: number[],
|
|
uvs: number[],
|
|
layerWeights: number[],
|
|
foliageMaskWeights: number[],
|
|
indices: number[],
|
|
options: TerrainMeshBuildOptions
|
|
) {
|
|
const topStart = positions.length / 3;
|
|
pushTerrainLodVertex(
|
|
terrain,
|
|
startSampleX,
|
|
startSampleZ,
|
|
0,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
options
|
|
);
|
|
const topEnd = positions.length / 3;
|
|
pushTerrainLodVertex(
|
|
terrain,
|
|
endSampleX,
|
|
endSampleZ,
|
|
0,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
options
|
|
);
|
|
const bottomStart = positions.length / 3;
|
|
pushTerrainLodVertex(
|
|
terrain,
|
|
startSampleX,
|
|
startSampleZ,
|
|
-skirtDepth,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
options
|
|
);
|
|
const bottomEnd = positions.length / 3;
|
|
pushTerrainLodVertex(
|
|
terrain,
|
|
endSampleX,
|
|
endSampleZ,
|
|
-skirtDepth,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
options
|
|
);
|
|
|
|
indices.push(topStart, bottomStart, bottomEnd);
|
|
indices.push(topStart, bottomEnd, topEnd);
|
|
}
|
|
|
|
function buildTerrainLodLevelMeshData(
|
|
terrain: Terrain,
|
|
startSampleX: number,
|
|
startSampleZ: number,
|
|
endSampleX: number,
|
|
endSampleZ: number,
|
|
level: number,
|
|
stride: number,
|
|
options: TerrainMeshBuildOptions
|
|
): TerrainLodLevelMeshData {
|
|
const sampleXs = createLodSampleCoordinates(
|
|
startSampleX,
|
|
endSampleX,
|
|
stride
|
|
);
|
|
const sampleZs = createLodSampleCoordinates(
|
|
startSampleZ,
|
|
endSampleZ,
|
|
stride
|
|
);
|
|
const positions: number[] = [];
|
|
const uvs: number[] = [];
|
|
const layerWeights: number[] = [];
|
|
const foliageMaskWeights: number[] = [];
|
|
const indices: number[] = [];
|
|
|
|
for (const sampleZ of sampleZs) {
|
|
for (const sampleX of sampleXs) {
|
|
pushTerrainLodVertex(
|
|
terrain,
|
|
sampleX,
|
|
sampleZ,
|
|
0,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
options
|
|
);
|
|
}
|
|
}
|
|
|
|
const sampleCountX = sampleXs.length;
|
|
|
|
for (let zIndex = 0; zIndex < sampleZs.length - 1; zIndex += 1) {
|
|
for (let xIndex = 0; xIndex < sampleXs.length - 1; xIndex += 1) {
|
|
const topLeft = zIndex * sampleCountX + xIndex;
|
|
const topRight = topLeft + 1;
|
|
const bottomLeft = (zIndex + 1) * sampleCountX + xIndex;
|
|
const bottomRight = bottomLeft + 1;
|
|
const sampleX = sampleXs[xIndex]!;
|
|
const nextSampleX = sampleXs[xIndex + 1]!;
|
|
const sampleZ = sampleZs[zIndex]!;
|
|
const nextSampleZ = sampleZs[zIndex + 1]!;
|
|
const diagonal = chooseCellDiagonal(
|
|
getTerrainHeightAtSample(terrain, sampleX, sampleZ),
|
|
getTerrainHeightAtSample(terrain, nextSampleX, sampleZ),
|
|
getTerrainHeightAtSample(terrain, sampleX, nextSampleZ),
|
|
getTerrainHeightAtSample(terrain, nextSampleX, nextSampleZ)
|
|
);
|
|
|
|
if (diagonal === "forward") {
|
|
indices.push(topLeft, bottomLeft, bottomRight);
|
|
indices.push(topLeft, bottomRight, topRight);
|
|
} else {
|
|
indices.push(topLeft, bottomLeft, topRight);
|
|
indices.push(topRight, bottomLeft, bottomRight);
|
|
}
|
|
}
|
|
}
|
|
|
|
const skirtStartVertexCount = positions.length / 3;
|
|
const skirtDepth = Math.max(terrain.cellSize * stride * 1.5, 0.5);
|
|
|
|
for (let xIndex = 0; xIndex < sampleXs.length - 1; xIndex += 1) {
|
|
pushTerrainLodSkirtSegment(
|
|
terrain,
|
|
sampleXs[xIndex]!,
|
|
startSampleZ,
|
|
sampleXs[xIndex + 1]!,
|
|
startSampleZ,
|
|
skirtDepth,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
indices,
|
|
options
|
|
);
|
|
pushTerrainLodSkirtSegment(
|
|
terrain,
|
|
sampleXs[xIndex + 1]!,
|
|
endSampleZ,
|
|
sampleXs[xIndex]!,
|
|
endSampleZ,
|
|
skirtDepth,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
indices,
|
|
options
|
|
);
|
|
}
|
|
|
|
for (let zIndex = 0; zIndex < sampleZs.length - 1; zIndex += 1) {
|
|
pushTerrainLodSkirtSegment(
|
|
terrain,
|
|
startSampleX,
|
|
sampleZs[zIndex + 1]!,
|
|
startSampleX,
|
|
sampleZs[zIndex]!,
|
|
skirtDepth,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
indices,
|
|
options
|
|
);
|
|
pushTerrainLodSkirtSegment(
|
|
terrain,
|
|
endSampleX,
|
|
sampleZs[zIndex]!,
|
|
endSampleX,
|
|
sampleZs[zIndex + 1]!,
|
|
skirtDepth,
|
|
positions,
|
|
uvs,
|
|
layerWeights,
|
|
foliageMaskWeights,
|
|
indices,
|
|
options
|
|
);
|
|
}
|
|
|
|
const typedPositions = new Float32Array(positions);
|
|
const typedUvs = new Float32Array(uvs);
|
|
const typedLayerWeights = new Float32Array(layerWeights);
|
|
const typedFoliageMaskWeights = new Float32Array(foliageMaskWeights);
|
|
const typedIndices = new Uint32Array(indices);
|
|
const geometry = new BufferGeometry();
|
|
|
|
geometry.setAttribute("position", new BufferAttribute(typedPositions, 3));
|
|
geometry.setAttribute("uv", new BufferAttribute(typedUvs, 2));
|
|
geometry.setAttribute(
|
|
"terrainLayerWeights",
|
|
new BufferAttribute(typedLayerWeights, TERRAIN_LAYER_COUNT)
|
|
);
|
|
geometry.setAttribute(
|
|
"terrainFoliageMask",
|
|
new BufferAttribute(typedFoliageMaskWeights, 1)
|
|
);
|
|
geometry.setIndex(new BufferAttribute(typedIndices, 1));
|
|
geometry.computeVertexNormals();
|
|
geometry.computeBoundingBox();
|
|
geometry.computeBoundingSphere();
|
|
|
|
const normalAttribute = geometry.getAttribute("normal");
|
|
const normals = new Float32Array(normalAttribute.array.length);
|
|
normals.set(normalAttribute.array as ArrayLike<number>);
|
|
|
|
return {
|
|
level,
|
|
stride,
|
|
geometry,
|
|
positions: typedPositions,
|
|
normals,
|
|
uvs: typedUvs,
|
|
layerWeights: typedLayerWeights,
|
|
foliageMaskWeights: typedFoliageMaskWeights,
|
|
indices: typedIndices,
|
|
skirtVertexCount: typedPositions.length / 3 - skirtStartVertexCount
|
|
};
|
|
}
|
|
|
|
export function buildTerrainLodMeshData(
|
|
terrain: Terrain,
|
|
options: TerrainMeshBuildOptions = {},
|
|
chunkSizeCells = TERRAIN_LOD_CHUNK_SIZE_CELLS
|
|
): DerivedTerrainLodMeshData {
|
|
const chunks: TerrainLodChunkMeshData[] = [];
|
|
const localBounds = createEmptyLocalBounds();
|
|
const maxCellX = terrain.sampleCountX - 1;
|
|
const maxCellZ = terrain.sampleCountZ - 1;
|
|
|
|
for (
|
|
let startSampleZ = 0, chunkZ = 0;
|
|
startSampleZ < maxCellZ;
|
|
startSampleZ += chunkSizeCells, chunkZ += 1
|
|
) {
|
|
for (
|
|
let startSampleX = 0, chunkX = 0;
|
|
startSampleX < maxCellX;
|
|
startSampleX += chunkSizeCells, chunkX += 1
|
|
) {
|
|
const chunk = buildTerrainLodChunkMeshData(
|
|
terrain,
|
|
startSampleX,
|
|
startSampleZ,
|
|
chunkSizeCells,
|
|
options
|
|
);
|
|
|
|
if (chunk === null) {
|
|
continue;
|
|
}
|
|
|
|
includePointInBounds(localBounds, chunk.localBounds.min);
|
|
includePointInBounds(localBounds, chunk.localBounds.max);
|
|
|
|
chunks.push({
|
|
...chunk,
|
|
chunkX,
|
|
chunkZ
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
chunkSizeCells,
|
|
chunks,
|
|
localBounds: cloneBounds(localBounds)
|
|
};
|
|
}
|
|
|
|
export function buildTerrainLodChunkMeshData(
|
|
terrain: Terrain,
|
|
startSampleX: number,
|
|
startSampleZ: number,
|
|
chunkSizeCells = TERRAIN_LOD_CHUNK_SIZE_CELLS,
|
|
options: TerrainMeshBuildOptions = {}
|
|
): TerrainLodChunkMeshData | null {
|
|
const maxCellX = terrain.sampleCountX - 1;
|
|
const maxCellZ = terrain.sampleCountZ - 1;
|
|
|
|
if (
|
|
startSampleX < 0 ||
|
|
startSampleZ < 0 ||
|
|
startSampleX >= maxCellX ||
|
|
startSampleZ >= maxCellZ
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const endSampleX = Math.min(startSampleX + chunkSizeCells, maxCellX);
|
|
const endSampleZ = Math.min(startSampleZ + chunkSizeCells, maxCellZ);
|
|
const cellCountX = endSampleX - startSampleX;
|
|
const cellCountZ = endSampleZ - startSampleZ;
|
|
const chunkBounds = buildTerrainChunkSourceBounds(
|
|
terrain,
|
|
startSampleX,
|
|
startSampleZ,
|
|
endSampleX,
|
|
endSampleZ
|
|
);
|
|
const strides = getUsefulTerrainLodStrides(cellCountX, cellCountZ);
|
|
const levels = strides.map((stride, level) =>
|
|
buildTerrainLodLevelMeshData(
|
|
terrain,
|
|
startSampleX,
|
|
startSampleZ,
|
|
endSampleX,
|
|
endSampleZ,
|
|
level,
|
|
stride,
|
|
options
|
|
)
|
|
);
|
|
const localCenter = {
|
|
x: (chunkBounds.min.x + chunkBounds.max.x) * 0.5,
|
|
y: (chunkBounds.min.y + chunkBounds.max.y) * 0.5,
|
|
z: (chunkBounds.min.z + chunkBounds.max.z) * 0.5
|
|
};
|
|
const diagonal = Math.hypot(
|
|
chunkBounds.max.x - chunkBounds.min.x,
|
|
chunkBounds.max.y - chunkBounds.min.y,
|
|
chunkBounds.max.z - chunkBounds.min.z
|
|
);
|
|
|
|
return {
|
|
chunkX: Math.floor(startSampleX / chunkSizeCells),
|
|
chunkZ: Math.floor(startSampleZ / chunkSizeCells),
|
|
startSampleX,
|
|
startSampleZ,
|
|
endSampleX,
|
|
endSampleZ,
|
|
cellCountX,
|
|
cellCountZ,
|
|
levels,
|
|
localBounds: cloneBounds(chunkBounds),
|
|
localCenter,
|
|
diagonal
|
|
};
|
|
}
|
|
|
|
export function resolveTerrainLodLevelIndex(options: {
|
|
levelCount: number;
|
|
chunkDiagonal: number;
|
|
cameraPosition: Vec3;
|
|
chunkWorldCenter: Vec3;
|
|
perspective: boolean;
|
|
}): number {
|
|
if (options.levelCount <= 1) {
|
|
return 0;
|
|
}
|
|
|
|
if (!options.perspective) {
|
|
return Math.min(2, options.levelCount - 1);
|
|
}
|
|
|
|
const distance = Math.hypot(
|
|
options.cameraPosition.x - options.chunkWorldCenter.x,
|
|
options.cameraPosition.y - options.chunkWorldCenter.y,
|
|
options.cameraPosition.z - options.chunkWorldCenter.z
|
|
);
|
|
const baseDistance = Math.max(options.chunkDiagonal, 1);
|
|
|
|
for (
|
|
let thresholdIndex = 0;
|
|
thresholdIndex < TERRAIN_LOD_DISTANCE_MULTIPLIERS.length;
|
|
thresholdIndex += 1
|
|
) {
|
|
if (
|
|
distance <
|
|
baseDistance * TERRAIN_LOD_DISTANCE_MULTIPLIERS[thresholdIndex]!
|
|
) {
|
|
return Math.min(thresholdIndex, options.levelCount - 1);
|
|
}
|
|
}
|
|
|
|
return options.levelCount - 1;
|
|
}
|
|
|
|
export function resolveTerrainLodLevelIndexWithHysteresis(options: {
|
|
levelCount: number;
|
|
activeLevelIndex: number;
|
|
chunkDiagonal: number;
|
|
cameraPosition: Vec3;
|
|
chunkWorldCenter: Vec3;
|
|
perspective: boolean;
|
|
}): number {
|
|
if (options.levelCount <= 1 || !options.perspective) {
|
|
return resolveTerrainLodLevelIndex(options);
|
|
}
|
|
|
|
const activeLevelIndex = Math.min(
|
|
Math.max(0, Math.trunc(options.activeLevelIndex)),
|
|
options.levelCount - 1
|
|
);
|
|
const distance = Math.hypot(
|
|
options.cameraPosition.x - options.chunkWorldCenter.x,
|
|
options.cameraPosition.y - options.chunkWorldCenter.y,
|
|
options.cameraPosition.z - options.chunkWorldCenter.z
|
|
);
|
|
const baseDistance = Math.max(options.chunkDiagonal, 1);
|
|
const normalizedDistance = distance / baseDistance;
|
|
const lowerBoundary =
|
|
activeLevelIndex <= 0
|
|
? Number.NEGATIVE_INFINITY
|
|
: TERRAIN_LOD_DISTANCE_MULTIPLIERS[activeLevelIndex - 1] ??
|
|
TERRAIN_LOD_DISTANCE_MULTIPLIERS[
|
|
TERRAIN_LOD_DISTANCE_MULTIPLIERS.length - 1
|
|
]!;
|
|
const upperBoundary =
|
|
activeLevelIndex >= options.levelCount - 1
|
|
? Number.POSITIVE_INFINITY
|
|
: TERRAIN_LOD_DISTANCE_MULTIPLIERS[activeLevelIndex] ??
|
|
TERRAIN_LOD_DISTANCE_MULTIPLIERS[
|
|
TERRAIN_LOD_DISTANCE_MULTIPLIERS.length - 1
|
|
]!;
|
|
|
|
if (
|
|
normalizedDistance >
|
|
upperBoundary * (1 + TERRAIN_LOD_HYSTERESIS_RATIO)
|
|
) {
|
|
return Math.min(activeLevelIndex + 1, options.levelCount - 1);
|
|
}
|
|
|
|
if (
|
|
normalizedDistance <
|
|
lowerBoundary * (1 - TERRAIN_LOD_HYSTERESIS_RATIO)
|
|
) {
|
|
return Math.max(activeLevelIndex - 1, 0);
|
|
}
|
|
|
|
return activeLevelIndex;
|
|
}
|