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
This commit is contained in:
2026-04-20 02:36:38 +02:00
parent 02f6d058a0
commit 94dec56eb4
25 changed files with 2199 additions and 70 deletions

View File

@@ -145,6 +145,9 @@ import {
import {
BOX_BRUSH_SCENE_DOCUMENT_VERSION,
ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION,
AUTHORED_TERRAIN_COLLISION_SCENE_DOCUMENT_VERSION,
AUTHORED_TERRAIN_FOUNDATION_SCENE_DOCUMENT_VERSION,
AUTHORED_TERRAIN_PAINT_SCENE_DOCUMENT_VERSION,
AUTHORED_OBJECT_STATE_SCENE_DOCUMENT_VERSION,
CONTROL_SURFACE_FOUNDATION_SCENE_DOCUMENT_VERSION,
DEFAULT_PROJECT_NAME,
@@ -1855,6 +1858,36 @@ function readTerrain(value: unknown, label: string): Terrain {
const heights = value.heights.map((heightValue, index) =>
expectFiniteNumber(heightValue, `${label}.heights.${index}`)
);
const layers =
value.layers === undefined
? undefined
: (() => {
if (!Array.isArray(value.layers) || value.layers.some((layer) => !isRecord(layer))) {
throw new Error(`${label}.layers must be an array of layer objects.`);
}
return value.layers.map((layerValue, layerIndex) => ({
materialId:
layerValue.materialId === undefined || layerValue.materialId === null
? null
: expectString(
layerValue.materialId,
`${label}.layers.${layerIndex}.materialId`
)
}));
})();
const paintWeights =
value.paintWeights === undefined
? undefined
: (() => {
if (!Array.isArray(value.paintWeights)) {
throw new Error(`${label}.paintWeights must be an array.`);
}
return value.paintWeights.map((paintWeight, index) =>
expectFiniteNumber(paintWeight, `${label}.paintWeights.${index}`)
);
})();
return createTerrain({
id: expectString(value.id, `${label}.id`),
@@ -1863,11 +1896,17 @@ function readTerrain(value: unknown, label: string): Terrain {
),
visible: expectBoolean(value.visible, `${label}.visible`),
enabled: expectBoolean(value.enabled, `${label}.enabled`),
collisionEnabled:
value.collisionEnabled === undefined
? undefined
: expectBoolean(value.collisionEnabled, `${label}.collisionEnabled`),
position: readVec3(value.position, `${label}.position`),
sampleCountX,
sampleCountZ,
cellSize,
heights
heights,
layers,
paintWeights
});
}
@@ -4841,6 +4880,9 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
source.version !== PROJECT_SEQUENCE_EFFECTS_SCENE_DOCUMENT_VERSION &&
source.version !== PROJECT_SEQUENCE_UNIFIED_VISIBILITY_SCENE_DOCUMENT_VERSION &&
source.version !== SCENE_TRANSITION_SEQUENCE_EFFECTS_SCENE_DOCUMENT_VERSION &&
source.version !== AUTHORED_TERRAIN_FOUNDATION_SCENE_DOCUMENT_VERSION &&
source.version !== AUTHORED_TERRAIN_PAINT_SCENE_DOCUMENT_VERSION &&
source.version !== AUTHORED_TERRAIN_COLLISION_SCENE_DOCUMENT_VERSION &&
source.version !== FOLLOW_ACTOR_PATH_SMOOTH_SCENE_DOCUMENT_VERSION
) {
throw new Error(

View File

@@ -66,7 +66,11 @@ import {
type ProjectTimeSettings
} from "./project-time-settings";
import { MIN_SCENE_PATH_POINT_COUNT, type ScenePath } from "./paths";
import { MIN_TERRAIN_SAMPLE_COUNT, type Terrain } from "./terrains";
import {
MIN_TERRAIN_SAMPLE_COUNT,
TERRAIN_LAYER_COUNT,
type Terrain
} from "./terrains";
import {
isAdvancedRenderingWaterReflectionMode,
isAdvancedRenderingShadowMapSize,
@@ -1652,6 +1656,7 @@ function validateScenePath(
function validateTerrain(
terrain: Terrain,
path: string,
document: SceneDocument,
diagnostics: SceneDiagnostic[]
) {
if (!isBoolean(terrain.visible)) {
@@ -1698,6 +1703,17 @@ function validateTerrain(
);
}
if (!isBoolean(terrain.collisionEnabled)) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-collision-enabled",
"Terrain collisionEnabled must remain a boolean.",
`${path}.collisionEnabled`
)
);
}
if (
!isPositiveInteger(terrain.sampleCountX) ||
terrain.sampleCountX < MIN_TERRAIN_SAMPLE_COUNT
@@ -1762,6 +1778,67 @@ function validateTerrain(
)
);
}
if (terrain.layers.length !== TERRAIN_LAYER_COUNT) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-layer-count",
`Terrain layers must contain exactly ${TERRAIN_LAYER_COUNT} authored layer slots.`,
`${path}.layers`
)
);
}
for (let index = 0; index < terrain.layers.length; index += 1) {
const layer = terrain.layers[index];
if (layer.materialId === null) {
continue;
}
if (document.materials[layer.materialId] === undefined) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-layer-material",
`Terrain layer material reference ${layer.materialId} does not exist in the document material registry.`,
`${path}.layers.${index}.materialId`
)
);
}
}
const expectedPaintWeightCount =
terrain.sampleCountX * terrain.sampleCountZ * (TERRAIN_LAYER_COUNT - 1);
if (terrain.paintWeights.length !== expectedPaintWeightCount) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-paint-weight-count",
`Terrain paint weights must contain exactly ${expectedPaintWeightCount} values.`,
`${path}.paintWeights`
)
);
}
for (let index = 0; index < terrain.paintWeights.length; index += 1) {
const paintWeight = terrain.paintWeights[index];
if (isFiniteNumber(paintWeight) && paintWeight >= 0 && paintWeight <= 1) {
continue;
}
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-paint-weight",
"Terrain paint weights must remain finite values between 0 and 1.",
`${path}.paintWeights.${index}`
)
);
}
}
function validateAuthoredEntityState(
@@ -5718,7 +5795,7 @@ export function validateSceneDocument(
}
registerAuthoredId(terrain.id, path, seenIds, diagnostics);
validateTerrain(terrain, path, diagnostics);
validateTerrain(terrain, path, document, diagnostics);
}
for (const [modelInstanceKey, modelInstance] of Object.entries(

View File

@@ -29,7 +29,9 @@ import {
} from "../sequencer/project-sequences";
import type { Terrain } from "./terrains";
export const SCENE_DOCUMENT_VERSION = 65 as const;
export const SCENE_DOCUMENT_VERSION = 67 as const;
export const AUTHORED_TERRAIN_COLLISION_SCENE_DOCUMENT_VERSION = 67 as const;
export const AUTHORED_TERRAIN_PAINT_SCENE_DOCUMENT_VERSION = 66 as const;
export const AUTHORED_TERRAIN_FOUNDATION_SCENE_DOCUMENT_VERSION = 65 as const;
export const FOLLOW_ACTOR_PATH_SMOOTH_SCENE_DOCUMENT_VERSION = 64 as const;
export const NPC_DIALOGUE_LINE_SPEAKER_REMOVED_SCENE_DOCUMENT_VERSION =

View File

@@ -1,26 +1,41 @@
import { createOpaqueId } from "../core/ids";
import type { Vec3 } from "../core/vector";
export interface TerrainLayer {
materialId: string | null;
}
export interface Terrain {
id: string;
kind: "terrain";
name?: string;
visible: boolean;
enabled: boolean;
collisionEnabled: boolean;
position: Vec3;
sampleCountX: number;
sampleCountZ: number;
cellSize: number;
heights: number[];
layers: TerrainLayer[];
paintWeights: number[];
}
export const DEFAULT_TERRAIN_VISIBLE = true;
export const DEFAULT_TERRAIN_ENABLED = true;
export const DEFAULT_TERRAIN_COLLISION_ENABLED = true;
export const MIN_TERRAIN_SAMPLE_COUNT = 2;
export const DEFAULT_TERRAIN_SAMPLE_COUNT_X = 9;
export const DEFAULT_TERRAIN_SAMPLE_COUNT_Z = 9;
export const DEFAULT_TERRAIN_CELL_SIZE = 1;
export const DEFAULT_TERRAIN_HEIGHT = 0;
export const TERRAIN_LAYER_COUNT = 4;
export const DEFAULT_TERRAIN_LAYER_MATERIAL_IDS = [
"patchy_grass_ground_250x250",
"patchy_weedy_dirt_ground_300x300",
"ground_sand_300x300",
"concrete_wall_cladding_250x250"
] as const;
function cloneVec3(vector: Vec3): Vec3 {
return {
@@ -72,6 +87,82 @@ export function normalizeTerrainCellSize(value: number): number {
return value;
}
function normalizeTerrainLayerMaterialId(
value: string | null | undefined,
label: string
): string | null {
if (value === null || value === undefined) {
return null;
}
if (typeof value !== "string") {
throw new Error(`${label} must be a string or null.`);
}
const normalizedValue = value.trim();
return normalizedValue.length === 0 ? null : normalizedValue;
}
function normalizeTerrainCollisionEnabled(value: boolean): boolean {
if (typeof value !== "boolean") {
throw new Error("Terrain collisionEnabled must be a boolean.");
}
return value;
}
export function getTerrainLayerLabel(layerIndex: number): string {
if (!Number.isInteger(layerIndex) || layerIndex < 0 || layerIndex >= TERRAIN_LAYER_COUNT) {
throw new Error(`Terrain layer index ${layerIndex} is out of range.`);
}
return layerIndex === 0 ? "Base Layer" : `Layer ${layerIndex + 1}`;
}
export function createDefaultTerrainLayers(): TerrainLayer[] {
return Array.from({ length: TERRAIN_LAYER_COUNT }, (_, layerIndex) => ({
materialId: DEFAULT_TERRAIN_LAYER_MATERIAL_IDS[layerIndex] ?? null
}));
}
export function cloneTerrainLayers(
layers: readonly TerrainLayer[]
): TerrainLayer[] {
return layers.map((layer, layerIndex) => ({
materialId: normalizeTerrainLayerMaterialId(
layer.materialId,
`Terrain layer ${layerIndex}`
)
}));
}
function normalizeTerrainLayers(
layers: readonly TerrainLayer[] | undefined
): TerrainLayer[] {
if (layers === undefined) {
return createDefaultTerrainLayers();
}
if (layers.length !== TERRAIN_LAYER_COUNT) {
throw new Error(
`Terrain layers must contain exactly ${TERRAIN_LAYER_COUNT} layer slots.`
);
}
return layers.map((layer, layerIndex) => {
if (typeof layer !== "object" || layer === null) {
throw new Error(`Terrain layer ${layerIndex} must be an object.`);
}
return {
materialId: normalizeTerrainLayerMaterialId(
layer.materialId,
`Terrain layer ${layerIndex}.materialId`
)
};
});
}
export function createFlatTerrainHeights(
sampleCountX: number,
sampleCountZ: number,
@@ -93,6 +184,26 @@ export function createFlatTerrainHeights(
return new Array(normalizedSampleCountX * normalizedSampleCountZ).fill(height);
}
export function createFlatTerrainPaintWeights(
sampleCountX: number,
sampleCountZ: number
): number[] {
const normalizedSampleCountX = normalizeTerrainSampleCount(
sampleCountX,
"Terrain sampleCountX"
);
const normalizedSampleCountZ = normalizeTerrainSampleCount(
sampleCountZ,
"Terrain sampleCountZ"
);
return new Array(
normalizedSampleCountX *
normalizedSampleCountZ *
(TERRAIN_LAYER_COUNT - 1)
).fill(0);
}
export function getTerrainSampleIndex(
terrain: Pick<Terrain, "sampleCountX" | "sampleCountZ">,
sampleX: number,
@@ -117,6 +228,14 @@ export function getTerrainSampleIndex(
return sampleZ * terrain.sampleCountX + sampleX;
}
export function getTerrainPaintWeightSampleOffset(
terrain: Pick<Terrain, "sampleCountX" | "sampleCountZ">,
sampleX: number,
sampleZ: number
): number {
return getTerrainSampleIndex(terrain, sampleX, sampleZ) * (TERRAIN_LAYER_COUNT - 1);
}
export function getTerrainHeightAtSample(
terrain: Terrain,
sampleX: number,
@@ -125,6 +244,90 @@ export function getTerrainHeightAtSample(
return terrain.heights[getTerrainSampleIndex(terrain, sampleX, sampleZ)] ?? 0;
}
function normalizeTerrainPaintWeights(
sampleCountX: number,
sampleCountZ: number,
paintWeights: readonly number[] | undefined
): number[] {
const expectedLength =
sampleCountX * sampleCountZ * (TERRAIN_LAYER_COUNT - 1);
const normalizedPaintWeights =
paintWeights === undefined
? createFlatTerrainPaintWeights(sampleCountX, sampleCountZ)
: [...paintWeights];
if (normalizedPaintWeights.length !== expectedLength) {
throw new Error(
`Terrain paint weights must contain exactly ${expectedLength} values.`
);
}
for (
let sampleIndex = 0;
sampleIndex < sampleCountX * sampleCountZ;
sampleIndex += 1
) {
const offset = sampleIndex * (TERRAIN_LAYER_COUNT - 1);
let weightSum = 0;
for (
let layerOffset = 0;
layerOffset < TERRAIN_LAYER_COUNT - 1;
layerOffset += 1
) {
const value = normalizedPaintWeights[offset + layerOffset];
if (!Number.isFinite(value)) {
throw new Error("Terrain paint weights must remain finite.");
}
const clampedValue = Math.min(1, Math.max(0, value));
normalizedPaintWeights[offset + layerOffset] = clampedValue;
weightSum += clampedValue;
}
if (weightSum <= 1) {
continue;
}
const scale = 1 / weightSum;
for (
let layerOffset = 0;
layerOffset < TERRAIN_LAYER_COUNT - 1;
layerOffset += 1
) {
normalizedPaintWeights[offset + layerOffset] *= scale;
}
}
return normalizedPaintWeights;
}
export function getTerrainSampleLayerWeights(
terrain: Pick<Terrain, "sampleCountX" | "sampleCountZ" | "paintWeights">,
sampleX: number,
sampleZ: number
): [number, number, number, number] {
const offset = getTerrainPaintWeightSampleOffset(terrain, sampleX, sampleZ);
const layer1 = terrain.paintWeights[offset] ?? 0;
const layer2 = terrain.paintWeights[offset + 1] ?? 0;
const layer3 = terrain.paintWeights[offset + 2] ?? 0;
const baseLayer = Math.max(0, 1 - (layer1 + layer2 + layer3));
const weightSum = baseLayer + layer1 + layer2 + layer3;
if (weightSum <= 0) {
return [1, 0, 0, 0];
}
return [
baseLayer / weightSum,
layer1 / weightSum,
layer2 / weightSum,
layer3 / weightSum
];
}
export function getTerrainWorldSamplePosition(
terrain: Terrain,
sampleX: number,
@@ -137,9 +340,21 @@ export function getTerrainWorldSamplePosition(
};
}
export function getTerrainFootprintWidth(
terrain: Pick<Terrain, "sampleCountX" | "cellSize">
): number {
return (terrain.sampleCountX - 1) * terrain.cellSize;
}
export function getTerrainFootprintDepth(
terrain: Pick<Terrain, "sampleCountZ" | "cellSize">
): number {
return (terrain.sampleCountZ - 1) * terrain.cellSize;
}
export function getTerrainBounds(terrain: Terrain): { min: Vec3; max: Vec3 } {
const width = (terrain.sampleCountX - 1) * terrain.cellSize;
const depth = (terrain.sampleCountZ - 1) * terrain.cellSize;
const width = getTerrainFootprintWidth(terrain);
const depth = getTerrainFootprintDepth(terrain);
let minHeight = Number.POSITIVE_INFINITY;
let maxHeight = Number.NEGATIVE_INFINITY;
@@ -167,6 +382,225 @@ export function getTerrainBounds(terrain: Terrain): { min: Vec3; max: Vec3 } {
};
}
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
function lerp(start: number, end: number, alpha: number): number {
return start + (end - start) * alpha;
}
function sampleTerrainHeightAtGridCoordinate(
terrain: Terrain,
sampleX: number,
sampleZ: number
): number {
const clampedSampleX = clamp(sampleX, 0, terrain.sampleCountX - 1);
const clampedSampleZ = clamp(sampleZ, 0, terrain.sampleCountZ - 1);
const minSampleX = Math.floor(clampedSampleX);
const maxSampleX = Math.min(terrain.sampleCountX - 1, minSampleX + 1);
const minSampleZ = Math.floor(clampedSampleZ);
const maxSampleZ = Math.min(terrain.sampleCountZ - 1, minSampleZ + 1);
const blendX = clampedSampleX - minSampleX;
const blendZ = clampedSampleZ - minSampleZ;
const height00 = getTerrainHeightAtSample(terrain, minSampleX, minSampleZ);
const height10 = getTerrainHeightAtSample(terrain, maxSampleX, minSampleZ);
const height01 = getTerrainHeightAtSample(terrain, minSampleX, maxSampleZ);
const height11 = getTerrainHeightAtSample(terrain, maxSampleX, maxSampleZ);
return lerp(
lerp(height00, height10, blendX),
lerp(height01, height11, blendX),
blendZ
);
}
function getStoredTerrainPaintWeightAtSample(
terrain: Terrain,
sampleX: number,
sampleZ: number,
layerOffset: number
): number {
const offset = getTerrainPaintWeightSampleOffset(terrain, sampleX, sampleZ);
return terrain.paintWeights[offset + layerOffset] ?? 0;
}
function sampleTerrainPaintWeightAtGridCoordinate(
terrain: Terrain,
sampleX: number,
sampleZ: number,
layerOffset: number
): number {
const clampedSampleX = clamp(sampleX, 0, terrain.sampleCountX - 1);
const clampedSampleZ = clamp(sampleZ, 0, terrain.sampleCountZ - 1);
const minSampleX = Math.floor(clampedSampleX);
const maxSampleX = Math.min(terrain.sampleCountX - 1, minSampleX + 1);
const minSampleZ = Math.floor(clampedSampleZ);
const maxSampleZ = Math.min(terrain.sampleCountZ - 1, minSampleZ + 1);
const blendX = clampedSampleX - minSampleX;
const blendZ = clampedSampleZ - minSampleZ;
const weight00 = getStoredTerrainPaintWeightAtSample(
terrain,
minSampleX,
minSampleZ,
layerOffset
);
const weight10 = getStoredTerrainPaintWeightAtSample(
terrain,
maxSampleX,
minSampleZ,
layerOffset
);
const weight01 = getStoredTerrainPaintWeightAtSample(
terrain,
minSampleX,
maxSampleZ,
layerOffset
);
const weight11 = getStoredTerrainPaintWeightAtSample(
terrain,
maxSampleX,
maxSampleZ,
layerOffset
);
return lerp(
lerp(weight00, weight10, blendX),
lerp(weight01, weight11, blendX),
blendZ
);
}
function createTerrainPositionFromCenter(
center: Vec3,
sampleCountX: number,
sampleCountZ: number,
cellSize: number
): Vec3 {
return {
x: center.x - ((sampleCountX - 1) * cellSize) * 0.5,
y: center.y,
z: center.z - ((sampleCountZ - 1) * cellSize) * 0.5
};
}
function getTerrainFootprintCenter(terrain: Terrain): Vec3 {
return {
x: terrain.position.x + getTerrainFootprintWidth(terrain) * 0.5,
y: terrain.position.y,
z: terrain.position.z + getTerrainFootprintDepth(terrain) * 0.5
};
}
function createResampledTerrainHeights(
terrain: Terrain,
sampleCountX: number,
sampleCountZ: number
): number[] {
const heights = new Array<number>(sampleCountX * sampleCountZ);
for (let sampleZ = 0; sampleZ < sampleCountZ; sampleZ += 1) {
const normalizedSampleZ =
sampleCountZ === 1 ? 0 : sampleZ / (sampleCountZ - 1);
const sourceSampleZ = normalizedSampleZ * (terrain.sampleCountZ - 1);
for (let sampleX = 0; sampleX < sampleCountX; sampleX += 1) {
const normalizedSampleX =
sampleCountX === 1 ? 0 : sampleX / (sampleCountX - 1);
const sourceSampleX = normalizedSampleX * (terrain.sampleCountX - 1);
heights[sampleZ * sampleCountX + sampleX] =
sampleTerrainHeightAtGridCoordinate(
terrain,
sourceSampleX,
sourceSampleZ
);
}
}
return heights;
}
function createResampledTerrainPaintWeights(
terrain: Terrain,
sampleCountX: number,
sampleCountZ: number
): number[] {
const paintWeights = new Array<number>(
sampleCountX * sampleCountZ * (TERRAIN_LAYER_COUNT - 1)
);
for (let sampleZ = 0; sampleZ < sampleCountZ; sampleZ += 1) {
const normalizedSampleZ =
sampleCountZ === 1 ? 0 : sampleZ / (sampleCountZ - 1);
const sourceSampleZ = normalizedSampleZ * (terrain.sampleCountZ - 1);
for (let sampleX = 0; sampleX < sampleCountX; sampleX += 1) {
const normalizedSampleX =
sampleCountX === 1 ? 0 : sampleX / (sampleCountX - 1);
const sourceSampleX = normalizedSampleX * (terrain.sampleCountX - 1);
const offset =
(sampleZ * sampleCountX + sampleX) * (TERRAIN_LAYER_COUNT - 1);
for (
let layerOffset = 0;
layerOffset < TERRAIN_LAYER_COUNT - 1;
layerOffset += 1
) {
paintWeights[offset + layerOffset] =
sampleTerrainPaintWeightAtGridCoordinate(
terrain,
sourceSampleX,
sourceSampleZ,
layerOffset
);
}
}
}
return paintWeights;
}
export function resizeTerrainGrid(
terrain: Terrain,
options: Pick<Terrain, "sampleCountX" | "sampleCountZ" | "cellSize"> & {
preserveCenter?: boolean;
}
): Terrain {
const sampleCountX = normalizeTerrainSampleCount(
options.sampleCountX,
"Terrain sampleCountX"
);
const sampleCountZ = normalizeTerrainSampleCount(
options.sampleCountZ,
"Terrain sampleCountZ"
);
const cellSize = normalizeTerrainCellSize(options.cellSize);
const preserveCenter = options.preserveCenter ?? true;
const nextPosition = preserveCenter
? createTerrainPositionFromCenter(
getTerrainFootprintCenter(terrain),
sampleCountX,
sampleCountZ,
cellSize
)
: cloneVec3(terrain.position);
return createTerrain({
...terrain,
position: nextPosition,
sampleCountX,
sampleCountZ,
cellSize,
heights: createResampledTerrainHeights(terrain, sampleCountX, sampleCountZ),
paintWeights: createResampledTerrainPaintWeights(
terrain,
sampleCountX,
sampleCountZ
)
});
}
function createDefaultTerrainPosition(
sampleCountX: number,
sampleCountZ: number,
@@ -187,11 +621,14 @@ export function createTerrain(
| "name"
| "visible"
| "enabled"
| "collisionEnabled"
| "position"
| "sampleCountX"
| "sampleCountZ"
| "cellSize"
| "heights"
| "layers"
| "paintWeights"
>
> = {}
): Terrain {
@@ -214,8 +651,17 @@ export function createTerrain(
overrides.heights !== undefined
? [...overrides.heights]
: createFlatTerrainHeights(sampleCountX, sampleCountZ);
const layers = normalizeTerrainLayers(overrides.layers);
const paintWeights = normalizeTerrainPaintWeights(
sampleCountX,
sampleCountZ,
overrides.paintWeights
);
const visible = overrides.visible ?? DEFAULT_TERRAIN_VISIBLE;
const enabled = overrides.enabled ?? DEFAULT_TERRAIN_ENABLED;
const collisionEnabled = normalizeTerrainCollisionEnabled(
overrides.collisionEnabled ?? DEFAULT_TERRAIN_COLLISION_ENABLED
);
assertFiniteVec3(position, "Terrain position");
@@ -243,11 +689,14 @@ export function createTerrain(
name: normalizeTerrainName(overrides.name),
visible,
enabled,
collisionEnabled,
position,
sampleCountX,
sampleCountZ,
cellSize,
heights
heights,
layers,
paintWeights
};
}
@@ -262,12 +711,21 @@ export function areTerrainsEqual(left: Terrain, right: Terrain): boolean {
left.name === right.name &&
left.visible === right.visible &&
left.enabled === right.enabled &&
left.collisionEnabled === right.collisionEnabled &&
areVec3Equal(left.position, right.position) &&
left.sampleCountX === right.sampleCountX &&
left.sampleCountZ === right.sampleCountZ &&
left.cellSize === right.cellSize &&
left.heights.length === right.heights.length &&
left.heights.every((height, index) => height === right.heights[index])
left.heights.every((height, index) => height === right.heights[index]) &&
left.layers.length === right.layers.length &&
left.layers.every(
(layer, index) => layer.materialId === right.layers[index]?.materialId
) &&
left.paintWeights.length === right.paintWeights.length &&
left.paintWeights.every(
(weight, index) => weight === right.paintWeights[index]
)
);
}