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

@@ -92,7 +92,7 @@ export function ViewportCanvas({
viewportGridVisible,
selection,
activeSelectionId,
terrainBrushState,
terrainBrushState = null,
toolMode,
toolPreview,
transformSession,
@@ -297,7 +297,7 @@ export function ViewportCanvas({
? getWhiteboxSelectionFeedbackLabel(sceneDocument, selection)
: null;
const terrainBrushOverlayVisible =
toolMode === "select" && terrainBrushState !== null;
toolMode === "select" && terrainBrushState != null;
const resolvedViewportBackground =
editorSimulationScene !== null && editorSimulationClock !== null
? resolveRuntimeDayNightWorldState(
@@ -378,6 +378,9 @@ export function ViewportCanvas({
data-testid={`viewport-terrain-brush-preview-${panelId}`}
>
terrain · {terrainBrushState.tool}
{terrainBrushState.tool === "paint"
? ` · layer ${terrainBrushState.layerIndex + 1}`
: ""}
</div>
)}
{selectedWhiteboxLabel === null ? null : (

View File

@@ -142,7 +142,7 @@ export function ViewportPanel({
viewportGridVisible,
selection,
activeSelectionId,
terrainBrushState,
terrainBrushState = null,
toolMode,
toolPreview,
transformSession,

View File

@@ -35,6 +35,7 @@ import {
TorusGeometry,
SpotLight,
TextureLoader,
Texture,
Vector2,
Vector3,
WebGLRenderTarget,
@@ -205,6 +206,11 @@ import {
} from "../rendering/advanced-rendering";
import { createFogQualityMaterial } from "../rendering/fog-material";
import { updatePlanarReflectionCamera } from "../rendering/planar-reflection";
import {
createTerrainLayerBlendMaterial,
getTerrainLayerPreviewColor,
getTerrainLayerTexture
} from "../rendering/terrain-layer-material";
import {
applyWhiteboxBevelToMaterial,
shouldApplyWhiteboxBevel
@@ -348,10 +354,12 @@ const TERRAIN_SELECTED_COLOR = 0xe0c17f;
const TERRAIN_ACTIVE_COLOR = 0xf0d8a2;
const TERRAIN_ACTIVE_EMISSIVE = 0x5c4623;
const TERRAIN_SELECTED_EMISSIVE = 0x3f2d17;
const TERRAIN_HOVERED_EMISSIVE = 0x24311b;
const TERRAIN_BRUSH_PREVIEW_RAISE_COLOR = 0x8dd977;
const TERRAIN_BRUSH_PREVIEW_LOWER_COLOR = 0xe17b75;
const TERRAIN_BRUSH_PREVIEW_SMOOTH_COLOR = 0x7dbbf1;
const TERRAIN_BRUSH_PREVIEW_FLATTEN_COLOR = 0xf1d37d;
const TERRAIN_BRUSH_PREVIEW_PAINT_COLOR = 0x8eb9ff;
const TERRAIN_BRUSH_PREVIEW_OFFSET = 0.05;
const BOX_CREATE_PREVIEW_FILL = 0x89b6ff;
const BOX_CREATE_PREVIEW_EDGE = 0xf3be8f;
@@ -1016,10 +1024,22 @@ export class ViewportHost {
this.currentTerrainBrushState?.terrainId !== terrainBrushState?.terrainId;
const toolChanged =
this.currentTerrainBrushState?.tool !== terrainBrushState?.tool;
const layerChanged =
this.currentTerrainBrushState?.tool === "paint" &&
terrainBrushState?.tool === "paint"
? this.currentTerrainBrushState.layerIndex !==
terrainBrushState.layerIndex
: this.currentTerrainBrushState?.tool === "paint" ||
terrainBrushState?.tool === "paint";
this.currentTerrainBrushState = terrainBrushState;
if (terrainChanged || toolChanged || terrainBrushState === null) {
if (
terrainChanged ||
toolChanged ||
layerChanged ||
terrainBrushState === null
) {
this.cancelActiveTerrainBrushStroke(false);
}
@@ -4902,10 +4922,18 @@ export class ViewportHost {
this.applyShadowState();
}
private createTerrainMaterial(terrainId: string): Material {
const selected = isTerrainSelected(this.currentSelection, terrainId);
const hovered = isTerrainSelected(this.hoveredSelection, terrainId);
const active = selected && this.currentActiveSelectionId === terrainId;
private resolveTerrainLayerMaterial(materialId: string | null): MaterialDef | null {
if (materialId === null || this.currentDocument === null) {
return null;
}
return this.currentDocument.materials[materialId] ?? null;
}
private createTerrainMaterial(terrain: Terrain): Material {
const selected = isTerrainSelected(this.currentSelection, terrain.id);
const hovered = isTerrainSelected(this.hoveredSelection, terrain.id);
const active = selected && this.currentActiveSelectionId === terrain.id;
const color = active
? TERRAIN_ACTIVE_COLOR
: selected
@@ -4921,16 +4949,23 @@ export class ViewportHost {
});
}
return new MeshStandardMaterial({
color,
emissive: active
const layerTextures = terrain.layers.map((layer) =>
getTerrainLayerTexture(
this.resolveTerrainLayerMaterial(layer.materialId),
(material) => this.getOrCreateTextureSet(material).baseColor
)
) as [Texture, Texture, Texture, Texture];
return createTerrainLayerBlendMaterial({
layerTextures,
emissiveHex: active
? TERRAIN_ACTIVE_EMISSIVE
: selected
? TERRAIN_SELECTED_EMISSIVE
: 0x000000,
emissiveIntensity: active ? 0.26 : selected ? 0.18 : 0,
roughness: 0.98,
metalness: 0
: hovered
? TERRAIN_HOVERED_EMISSIVE
: 0x000000,
emissiveIntensity: active ? 0.26 : selected ? 0.18 : hovered ? 0.08 : 0
});
}
@@ -4952,7 +4987,7 @@ export class ViewportHost {
const derivedMesh = buildTerrainDerivedMeshData(displayedTerrain);
const mesh = new Mesh(
derivedMesh.geometry,
this.createTerrainMaterial(displayedTerrain.id)
this.createTerrainMaterial(displayedTerrain)
);
mesh.position.set(
@@ -6647,6 +6682,34 @@ export class ViewportHost {
});
}
for (const terrain of getTerrains(document.terrains)) {
if (!terrain.enabled || !terrain.visible) {
continue;
}
const derivedMesh = buildTerrainDerivedMeshData(terrain);
contactBounds.push({
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
}
}
});
}
for (const modelInstance of getModelInstances(document.modelInstances)) {
if (modelInstance.collision.mode === "none") {
continue;
@@ -6888,8 +6951,9 @@ export class ViewportHost {
continue;
}
const displayedTerrain = this.getDisplayedTerrainState(terrain.id) ?? terrain;
const previousMaterial = renderObjects.mesh.material;
renderObjects.mesh.material = this.createTerrainMaterial(terrain.id);
renderObjects.mesh.material = this.createTerrainMaterial(displayedTerrain);
previousMaterial.dispose();
}
}
@@ -6928,10 +6992,8 @@ export class ViewportHost {
);
}
private getTerrainBrushPreviewColor(
tool: ArmedTerrainBrushState["tool"]
): number {
switch (tool) {
private getTerrainBrushPreviewColor(brushState: ArmedTerrainBrushState): number {
switch (brushState.tool) {
case "raise":
return TERRAIN_BRUSH_PREVIEW_RAISE_COLOR;
case "lower":
@@ -6940,6 +7002,23 @@ export class ViewportHost {
return TERRAIN_BRUSH_PREVIEW_SMOOTH_COLOR;
case "flatten":
return TERRAIN_BRUSH_PREVIEW_FLATTEN_COLOR;
case "paint": {
const terrain =
this.getDisplayedTerrainState(brushState.terrainId) ??
this.currentDocument?.terrains[brushState.terrainId] ??
null;
if (terrain === null) {
return TERRAIN_BRUSH_PREVIEW_PAINT_COLOR;
}
const terrainLayer = terrain.layers[brushState.layerIndex] ?? null;
const terrainMaterial =
terrainLayer === null
? null
: this.resolveTerrainLayerMaterial(terrainLayer.materialId);
return getTerrainLayerPreviewColor(terrainMaterial);
}
}
}
@@ -6989,7 +7068,7 @@ export class ViewportHost {
previousGeometry.dispose();
const previewColor = this.getTerrainBrushPreviewColor(
this.currentTerrainBrushState.tool
this.currentTerrainBrushState
);
(this.terrainBrushPreviewLine.material as LineBasicMaterial).color.setHex(
previewColor
@@ -7109,7 +7188,8 @@ export class ViewportHost {
center: point,
settings: toolState,
tool: toolState.tool,
referenceHeight
referenceHeight,
layerIndex: toolState.tool === "paint" ? toolState.layerIndex : null
});
}