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

@@ -10,6 +10,7 @@ import {
import { createBoxBrush } from "../../src/document/brushes";
import { createScenePath } from "../../src/document/paths";
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
import { createTerrain } from "../../src/document/terrains";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import {
DEFAULT_PLAYER_START_MOVE_SPEED,
@@ -1351,6 +1352,56 @@ describe("buildRuntimeSceneFromDocument", () => {
expect(runtimeScene.sceneBounds?.max.y).toBeGreaterThanOrEqual(2);
});
it("adds authored terrain heightfield colliders to the runtime scene build", () => {
const terrain = createTerrain({
id: "terrain-runtime-heightfield",
position: {
x: -4,
y: 0,
z: -4
},
sampleCountX: 3,
sampleCountZ: 3,
cellSize: 4,
heights: [0, 1, 0, 0, 2, 0, 0, 1, 0]
});
const runtimeScene = buildRuntimeSceneFromDocument({
...createEmptySceneDocument({ name: "Authored Terrain Collider Scene" }),
terrains: {
[terrain.id]: terrain
}
});
expect(runtimeScene.terrains[0]).toMatchObject({
id: terrain.id,
collisionEnabled: true
});
expect(runtimeScene.colliders[0]).toMatchObject({
source: "terrain",
terrainId: terrain.id,
kind: "heightfield",
rows: 3,
cols: 3,
minX: 0,
maxX: 8,
minZ: 0,
maxZ: 8
});
if (
runtimeScene.colliders[0] === undefined ||
runtimeScene.colliders[0].source !== "terrain"
) {
throw new Error("Expected the runtime collider to be an authored terrain heightfield.");
}
expect(Array.from(runtimeScene.colliders[0].heights)).toEqual(
terrain.heights
);
expect(runtimeScene.sceneBounds?.max.y).toBe(2);
});
it("adds static-simple imported-model colliders as compound box pieces", () => {
const wallGeometry = new PlaneGeometry(4, 4, 4, 4);
wallGeometry.rotateY(Math.PI * 0.5);

View File

@@ -4,6 +4,7 @@ import { BoxGeometry, PlaneGeometry } from "three";
import { createModelInstance } from "../../src/assets/model-instances";
import { createBoxBrush } from "../../src/document/brushes";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import { createTerrain } from "../../src/document/terrains";
import {
createNpcEntity,
createPlayerStartEntity
@@ -954,4 +955,93 @@ describe("RapierCollisionWorld", () => {
collisionWorld.dispose();
}
});
it("initializes and resolves first-person motion against authored terrain colliders", async () => {
const terrain = createTerrain({
id: "terrain-authored-collision",
position: {
x: -4,
y: 0,
z: -4
},
sampleCountX: 5,
sampleCountZ: 5,
cellSize: 2,
heights: [
0,
0.25,
0.5,
0.75,
1,
0.75,
1,
1.25,
1.5,
1.75,
1.5,
1.75,
2,
2.25,
2.5,
2.25,
2.5,
2.75,
3,
3.25,
3,
3.25,
3.5,
3.75,
4
]
});
const runtimeScene = buildRuntimeSceneFromDocument({
...createEmptySceneDocument({ name: "Authored Terrain Collision Scene" }),
terrains: {
[terrain.id]: terrain
}
});
const collisionWorld = await RapierCollisionWorld.create(
runtimeScene.colliders,
runtimeScene.playerCollider
);
try {
const highLanding = collisionWorld.resolveFirstPersonMotion(
{
x: 4,
y: 8,
z: 4
},
{
x: 0,
y: -10,
z: 0
},
runtimeScene.playerCollider
);
const lowLanding = collisionWorld.resolveFirstPersonMotion(
{
x: -2,
y: 8,
z: -2
},
{
x: 0,
y: -10,
z: 0
},
runtimeScene.playerCollider
);
expect(highLanding.grounded).toBe(true);
expect(highLanding.feetPosition.y).toBeGreaterThan(3.9);
expect(highLanding.feetPosition.y).toBeLessThan(4.1);
expect(lowLanding.grounded).toBe(true);
expect(lowLanding.feetPosition.y).toBeGreaterThan(1.0);
expect(lowLanding.feetPosition.y).toBeLessThan(1.2);
} finally {
collisionWorld.dispose();
}
});
});

View File

@@ -62,7 +62,36 @@ describe("terrain commands", () => {
sampleCountX: 3,
sampleCountZ: 3,
cellSize: 1.5,
heights: [0, 1, 0, 1, 3, 1, 0, 1, 0]
heights: [0, 1, 0, 1, 3, 1, 0, 1, 0],
paintWeights: [
0.2,
0,
0,
0.1,
0.3,
0,
0,
0.15,
0,
0.25,
0.25,
0,
0.05,
0.1,
0.1,
0,
0,
0,
0.2,
0,
0.2,
0,
0.1,
0,
0,
0,
0
]
});
store.executeCommand(

View File

@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import {
getTerrainPaintWeightSampleOffset,
resizeTerrainGrid,
createTerrain
} from "../../src/document/terrains";
describe("terrain grid resizing", () => {
it("keeps terrain centered while bilinearly resampling heights and paint weights", () => {
const terrain = createTerrain({
id: "terrain-resample-main",
position: {
x: 0,
y: 1,
z: 0
},
sampleCountX: 2,
sampleCountZ: 2,
cellSize: 1,
heights: [0, 2, 4, 6],
paintWeights: [
0,
0,
0,
0.6,
0,
0,
0,
0.5,
0,
0,
0,
0.25
]
});
const resizedTerrain = resizeTerrainGrid(terrain, {
sampleCountX: 3,
sampleCountZ: 3,
cellSize: 1
});
const centerOffset = getTerrainPaintWeightSampleOffset(resizedTerrain, 1, 1);
expect(resizedTerrain.position).toEqual({
x: -0.5,
y: 1,
z: -0.5
});
expect(resizedTerrain.sampleCountX).toBe(3);
expect(resizedTerrain.sampleCountZ).toBe(3);
expect(resizedTerrain.heights[4]).toBeCloseTo(3);
expect(resizedTerrain.paintWeights[centerOffset]).toBeCloseTo(0.15);
expect(resizedTerrain.paintWeights[centerOffset + 1]).toBeCloseTo(0.125);
expect(resizedTerrain.paintWeights[centerOffset + 2]).toBeCloseTo(0.0625);
expect(terrain.position).toEqual({
x: 0,
y: 1,
z: 0
});
expect(terrain.heights).toEqual([0, 2, 4, 6]);
});
});

View File

@@ -3,7 +3,9 @@ import { describe, expect, it } from "vitest";
import { MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT } from "../../src/document/brushes";
import { createBoxBrush } from "../../src/document/brushes";
import { createTerrain } from "../../src/document/terrains";
import { buildBoxBrushDerivedMeshData } from "../../src/geometry/box-brush-mesh";
import { buildTerrainDerivedMeshData } from "../../src/geometry/terrain-mesh";
import { collectWaterContactPatches, createWaterMaterial } from "../../src/rendering/water-material";
describe("water material helpers", () => {
@@ -497,6 +499,92 @@ describe("water material helpers", () => {
expect(patches[0]?.halfDepth ?? 0).toBeGreaterThan(0.05);
});
it("builds shoreline contact patches from authored terrain meshes", () => {
const terrain = createTerrain({
id: "terrain-waterline-main",
position: {
x: -4,
y: 0,
z: -4
},
sampleCountX: 5,
sampleCountZ: 5,
cellSize: 2,
heights: [
-1.2,
-0.8,
-0.4,
0,
0.4,
-1,
-0.6,
-0.2,
0.2,
0.6,
-0.8,
-0.4,
0,
0.4,
0.8,
-0.6,
-0.2,
0.2,
0.6,
1,
-0.4,
0,
0.4,
0.8,
1.2
]
});
const derivedMesh = buildTerrainDerivedMeshData(terrain);
const patches = collectWaterContactPatches(
{
center: {
x: 0,
y: 0,
z: 0
},
rotationDegrees: {
x: 0,
y: 0,
z: 0
},
size: {
x: 10,
y: 2,
z: 10
}
},
[
{
kind: "triangleMesh",
vertices: derivedMesh.positions,
indices: derivedMesh.indices,
mergeProfile: "aggressive",
transform: {
position: terrain.position,
rotationDegrees: {
x: 0,
y: 0,
z: 0
},
scale: {
x: 1,
y: 1,
z: 1
}
}
}
]
);
expect(patches.length).toBeGreaterThan(0);
expect(patches[0]?.shape).toBe("segment");
expect(patches[0]?.halfWidth ?? 0).toBeGreaterThan(1);
});
it("keeps foam patches for both long and short edges of a box intersecting the water surface", () => {
const intersectingBox = createBoxBrush({
center: {
@@ -780,4 +868,4 @@ describe("water material helpers", () => {
expect(material.uniforms["reflectionEnabled"]?.value).toBe(0.36);
}
});
});
});