auto-git:
[change] src/commands/apply-terrain-brush-patch-command.ts [change] src/core/terrain-brush.ts [change] src/document/migrate-scene-document.ts [change] src/document/scene-document.ts [change] src/geometry/terrain-brush.ts [change] src/geometry/terrain-mesh.ts
This commit is contained in:
@@ -51,7 +51,8 @@ export function isTerrainBrushPatchEmpty(patch: TerrainBrushPatch): boolean {
|
|||||||
return (
|
return (
|
||||||
patch.heightSamples.length === 0 &&
|
patch.heightSamples.length === 0 &&
|
||||||
patch.paintWeights.length === 0 &&
|
patch.paintWeights.length === 0 &&
|
||||||
patch.foliageMaskValues.length === 0
|
patch.foliageMaskValues.length === 0 &&
|
||||||
|
patch.foliageBlockerMaskValues.length === 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +90,10 @@ export function createApplyTerrainBrushPatchCommand(
|
|||||||
paintWeights: options.patch.paintWeights.map((entry) => ({ ...entry })),
|
paintWeights: options.patch.paintWeights.map((entry) => ({ ...entry })),
|
||||||
foliageMaskValues: options.patch.foliageMaskValues.map((entry) => ({
|
foliageMaskValues: options.patch.foliageMaskValues.map((entry) => ({
|
||||||
...entry
|
...entry
|
||||||
}))
|
})),
|
||||||
|
foliageBlockerMaskValues: options.patch.foliageBlockerMaskValues.map(
|
||||||
|
(entry) => ({ ...entry })
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let previousSelection: EditorSelection | null = null;
|
let previousSelection: EditorSelection | null = null;
|
||||||
let previousToolMode: ToolMode | null = null;
|
let previousToolMode: ToolMode | null = null;
|
||||||
@@ -167,6 +171,21 @@ export function createApplyTerrainBrushPatchCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const entry of patch.foliageBlockerMaskValues) {
|
||||||
|
assertValidPatchEntry(
|
||||||
|
entry,
|
||||||
|
terrain.foliageBlockerMask.values.length,
|
||||||
|
"Terrain foliage blocker mask"
|
||||||
|
);
|
||||||
|
terrain.foliageBlockerMask.values[entry.index] =
|
||||||
|
direction === "forward" ? entry.after : entry.before;
|
||||||
|
renderDirtyBounds = mergeTerrainSampleIndexIntoBounds(
|
||||||
|
renderDirtyBounds,
|
||||||
|
terrain,
|
||||||
|
entry.index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
markTerrainRenderSamplesDirty(terrain, renderDirtyBounds);
|
markTerrainRenderSamplesDirty(terrain, renderDirtyBounds);
|
||||||
|
|
||||||
context.setDocument({
|
context.setDocument({
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ export type TerrainBrushTool =
|
|||||||
| "flatten"
|
| "flatten"
|
||||||
| "paint"
|
| "paint"
|
||||||
| "foliagePaint"
|
| "foliagePaint"
|
||||||
| "foliageErase";
|
| "foliageErase"
|
||||||
|
| "foliageBlockerPaint"
|
||||||
|
| "foliageBlockerErase";
|
||||||
|
|
||||||
export interface TerrainBrushSettings {
|
export interface TerrainBrushSettings {
|
||||||
radius: number;
|
radius: number;
|
||||||
@@ -17,7 +19,10 @@ export interface TerrainBrushSettings {
|
|||||||
|
|
||||||
export interface ArmedTerrainSculptBrushState extends TerrainBrushSettings {
|
export interface ArmedTerrainSculptBrushState extends TerrainBrushSettings {
|
||||||
terrainId: string;
|
terrainId: string;
|
||||||
tool: Exclude<TerrainBrushTool, "paint" | "foliagePaint" | "foliageErase">;
|
tool: Exclude<
|
||||||
|
TerrainBrushTool,
|
||||||
|
"paint" | "foliagePaint" | "foliageErase" | "foliageBlockerPaint" | "foliageBlockerErase"
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArmedTerrainPaintBrushState extends TerrainBrushSettings {
|
export interface ArmedTerrainPaintBrushState extends TerrainBrushSettings {
|
||||||
@@ -33,10 +38,17 @@ export interface ArmedTerrainFoliagePaintBrushState
|
|||||||
foliageLayerId: string;
|
foliageLayerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArmedTerrainFoliageBlockerBrushState
|
||||||
|
extends TerrainBrushSettings {
|
||||||
|
terrainId: string;
|
||||||
|
tool: "foliageBlockerPaint" | "foliageBlockerErase";
|
||||||
|
}
|
||||||
|
|
||||||
export type ArmedTerrainBrushState =
|
export type ArmedTerrainBrushState =
|
||||||
| ArmedTerrainSculptBrushState
|
| ArmedTerrainSculptBrushState
|
||||||
| ArmedTerrainPaintBrushState
|
| ArmedTerrainPaintBrushState
|
||||||
| ArmedTerrainFoliagePaintBrushState;
|
| ArmedTerrainFoliagePaintBrushState
|
||||||
|
| ArmedTerrainFoliageBlockerBrushState;
|
||||||
|
|
||||||
export interface TerrainSampleValuePatch {
|
export interface TerrainSampleValuePatch {
|
||||||
index: number;
|
index: number;
|
||||||
@@ -53,6 +65,7 @@ export interface TerrainBrushPatch {
|
|||||||
heightSamples: TerrainSampleValuePatch[];
|
heightSamples: TerrainSampleValuePatch[];
|
||||||
paintWeights: TerrainSampleValuePatch[];
|
paintWeights: TerrainSampleValuePatch[];
|
||||||
foliageMaskValues: TerrainFoliageMaskValuePatch[];
|
foliageMaskValues: TerrainFoliageMaskValuePatch[];
|
||||||
|
foliageBlockerMaskValues: TerrainSampleValuePatch[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerrainBrushStrokeCommit {
|
export interface TerrainBrushStrokeCommit {
|
||||||
@@ -143,6 +156,10 @@ export function getTerrainBrushToolLabel(tool: TerrainBrushTool): string {
|
|||||||
return "Paint Foliage";
|
return "Paint Foliage";
|
||||||
case "foliageErase":
|
case "foliageErase":
|
||||||
return "Erase Foliage";
|
return "Erase Foliage";
|
||||||
|
case "foliageBlockerPaint":
|
||||||
|
return "Paint Foliage Blocker";
|
||||||
|
case "foliageBlockerErase":
|
||||||
|
return "Clear Foliage Blocker";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2320,6 +2320,38 @@ function readTerrain(value: unknown, label: string): Terrain {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
const foliageBlockerMask =
|
||||||
|
value.foliageBlockerMask === undefined
|
||||||
|
? undefined
|
||||||
|
: (() => {
|
||||||
|
if (!isRecord(value.foliageBlockerMask)) {
|
||||||
|
throw new Error(`${label}.foliageBlockerMask must be an object.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(value.foliageBlockerMask.values)) {
|
||||||
|
throw new Error(
|
||||||
|
`${label}.foliageBlockerMask.values must be an array.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
resolutionX: expectFiniteNumber(
|
||||||
|
value.foliageBlockerMask.resolutionX,
|
||||||
|
`${label}.foliageBlockerMask.resolutionX`
|
||||||
|
),
|
||||||
|
resolutionZ: expectFiniteNumber(
|
||||||
|
value.foliageBlockerMask.resolutionZ,
|
||||||
|
`${label}.foliageBlockerMask.resolutionZ`
|
||||||
|
),
|
||||||
|
values: value.foliageBlockerMask.values.map(
|
||||||
|
(maskSample, sampleIndex) =>
|
||||||
|
expectFiniteNumber(
|
||||||
|
maskSample,
|
||||||
|
`${label}.foliageBlockerMask.values.${sampleIndex}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
return createTerrain({
|
return createTerrain({
|
||||||
id: expectString(value.id, `${label}.id`),
|
id: expectString(value.id, `${label}.id`),
|
||||||
@@ -2339,7 +2371,8 @@ function readTerrain(value: unknown, label: string): Terrain {
|
|||||||
heights,
|
heights,
|
||||||
layers,
|
layers,
|
||||||
paintWeights,
|
paintWeights,
|
||||||
foliageMasks
|
foliageMasks,
|
||||||
|
foliageBlockerMask
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ import {
|
|||||||
type FoliagePrototypeRegistry
|
type FoliagePrototypeRegistry
|
||||||
} from "../foliage/foliage";
|
} from "../foliage/foliage";
|
||||||
|
|
||||||
export const SCENE_DOCUMENT_VERSION = 94 as const;
|
export const SCENE_DOCUMENT_VERSION = 95 as const;
|
||||||
|
export const FOLIAGE_BLOCKER_MASKS_SCENE_DOCUMENT_VERSION = 95 as const;
|
||||||
export const FOLIAGE_QUALITY_SCENE_DOCUMENT_VERSION = 94 as const;
|
export const FOLIAGE_QUALITY_SCENE_DOCUMENT_VERSION = 94 as const;
|
||||||
export const FOLIAGE_MASKS_SCENE_DOCUMENT_VERSION = 93 as const;
|
export const FOLIAGE_MASKS_SCENE_DOCUMENT_VERSION = 93 as const;
|
||||||
export const FOLIAGE_FOUNDATION_SCENE_DOCUMENT_VERSION = 92 as const;
|
export const FOLIAGE_FOUNDATION_SCENE_DOCUMENT_VERSION = 92 as const;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
} from "../core/terrain-brush";
|
} from "../core/terrain-brush";
|
||||||
import {
|
import {
|
||||||
createTerrain,
|
createTerrain,
|
||||||
|
getTerrainFoliageBlockerMaskValueAtSample,
|
||||||
getOrCreateTerrainFoliageMask,
|
getOrCreateTerrainFoliageMask,
|
||||||
getTerrainHeightAtSample,
|
getTerrainHeightAtSample,
|
||||||
getTerrainFoliageMask,
|
getTerrainFoliageMask,
|
||||||
@@ -36,6 +37,7 @@ export interface TerrainBrushStampMutationResult {
|
|||||||
heightSampleIndices: number[];
|
heightSampleIndices: number[];
|
||||||
paintWeightIndices: number[];
|
paintWeightIndices: number[];
|
||||||
foliageMaskValueIndices: TerrainFoliageMaskValueIndex[];
|
foliageMaskValueIndices: TerrainFoliageMaskValueIndex[];
|
||||||
|
foliageBlockerMaskValueIndices: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerrainFoliageMaskValueIndex {
|
export interface TerrainFoliageMaskValueIndex {
|
||||||
@@ -337,7 +339,8 @@ export function applyTerrainBrushStampInPlace(options: {
|
|||||||
dirtyBounds: null,
|
dirtyBounds: null,
|
||||||
heightSampleIndices: [],
|
heightSampleIndices: [],
|
||||||
paintWeightIndices: [],
|
paintWeightIndices: [],
|
||||||
foliageMaskValueIndices: []
|
foliageMaskValueIndices: [],
|
||||||
|
foliageBlockerMaskValueIndices: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +359,7 @@ export function applyTerrainBrushStampInPlace(options: {
|
|||||||
const heightSampleIndices: number[] = [];
|
const heightSampleIndices: number[] = [];
|
||||||
const paintWeightIndices: number[] = [];
|
const paintWeightIndices: number[] = [];
|
||||||
const foliageMaskValueIndices: TerrainFoliageMaskValueIndex[] = [];
|
const foliageMaskValueIndices: TerrainFoliageMaskValueIndex[] = [];
|
||||||
|
const foliageBlockerMaskValueIndices: number[] = [];
|
||||||
|
|
||||||
const markDirty = (sampleX: number, sampleZ: number) => {
|
const markDirty = (sampleX: number, sampleZ: number) => {
|
||||||
if (dirtyBounds === null) {
|
if (dirtyBounds === null) {
|
||||||
@@ -494,6 +498,29 @@ export function applyTerrainBrushStampInPlace(options: {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
case "foliageBlockerPaint":
|
||||||
|
case "foliageBlockerErase": {
|
||||||
|
const maskIndex = getTerrainFoliageMaskSampleIndex(
|
||||||
|
terrain.foliageBlockerMask,
|
||||||
|
sampleX,
|
||||||
|
sampleZ
|
||||||
|
);
|
||||||
|
const currentMaskValue =
|
||||||
|
terrain.foliageBlockerMask.values[maskIndex] ?? 0;
|
||||||
|
const targetMaskValue = tool === "foliageBlockerPaint" ? 1 : 0;
|
||||||
|
const nextMaskValue = lerp(
|
||||||
|
currentMaskValue,
|
||||||
|
targetMaskValue,
|
||||||
|
clamp01(smoothingStrength * weight)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextMaskValue !== currentMaskValue) {
|
||||||
|
terrain.foliageBlockerMask.values[maskIndex] = nextMaskValue;
|
||||||
|
foliageBlockerMaskValueIndices.push(maskIndex);
|
||||||
|
markDirty(sampleX, sampleZ);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextHeight !== currentHeight) {
|
if (nextHeight !== currentHeight) {
|
||||||
@@ -509,7 +536,8 @@ export function applyTerrainBrushStampInPlace(options: {
|
|||||||
dirtyBounds,
|
dirtyBounds,
|
||||||
heightSampleIndices,
|
heightSampleIndices,
|
||||||
paintWeightIndices,
|
paintWeightIndices,
|
||||||
foliageMaskValueIndices
|
foliageMaskValueIndices,
|
||||||
|
foliageBlockerMaskValueIndices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +547,7 @@ export function createTerrainBrushPatchFromTerrains(options: {
|
|||||||
heightSampleIndices: Iterable<number>;
|
heightSampleIndices: Iterable<number>;
|
||||||
paintWeightIndices: Iterable<number>;
|
paintWeightIndices: Iterable<number>;
|
||||||
foliageMaskValueIndices?: Iterable<TerrainFoliageMaskValueIndex>;
|
foliageMaskValueIndices?: Iterable<TerrainFoliageMaskValueIndex>;
|
||||||
|
foliageBlockerMaskValueIndices?: Iterable<number>;
|
||||||
}): TerrainBrushPatch {
|
}): TerrainBrushPatch {
|
||||||
const { before, after } = options;
|
const { before, after } = options;
|
||||||
|
|
||||||
@@ -530,7 +559,9 @@ export function createTerrainBrushPatchFromTerrains(options: {
|
|||||||
before.sampleCountX !== after.sampleCountX ||
|
before.sampleCountX !== after.sampleCountX ||
|
||||||
before.sampleCountZ !== after.sampleCountZ ||
|
before.sampleCountZ !== after.sampleCountZ ||
|
||||||
before.heights.length !== after.heights.length ||
|
before.heights.length !== after.heights.length ||
|
||||||
before.paintWeights.length !== after.paintWeights.length
|
before.paintWeights.length !== after.paintWeights.length ||
|
||||||
|
before.foliageBlockerMask.values.length !==
|
||||||
|
after.foliageBlockerMask.values.length
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Terrain brush patches require matching terrain sample dimensions."
|
"Terrain brush patches require matching terrain sample dimensions."
|
||||||
@@ -540,6 +571,8 @@ export function createTerrainBrushPatchFromTerrains(options: {
|
|||||||
const heightSamples: TerrainBrushPatch["heightSamples"] = [];
|
const heightSamples: TerrainBrushPatch["heightSamples"] = [];
|
||||||
const paintWeights: TerrainBrushPatch["paintWeights"] = [];
|
const paintWeights: TerrainBrushPatch["paintWeights"] = [];
|
||||||
const foliageMaskValues: TerrainBrushPatch["foliageMaskValues"] = [];
|
const foliageMaskValues: TerrainBrushPatch["foliageMaskValues"] = [];
|
||||||
|
const foliageBlockerMaskValues: TerrainBrushPatch["foliageBlockerMaskValues"] =
|
||||||
|
[];
|
||||||
const normalizeIndices = (
|
const normalizeIndices = (
|
||||||
indices: Iterable<number>,
|
indices: Iterable<number>,
|
||||||
length: number,
|
length: number,
|
||||||
@@ -641,11 +674,41 @@ export function createTerrainBrushPatchFromTerrains(options: {
|
|||||||
left.layerId.localeCompare(right.layerId) || left.index - right.index
|
left.layerId.localeCompare(right.layerId) || left.index - right.index
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (const maskIndex of normalizeIndices(
|
||||||
|
options.foliageBlockerMaskValueIndices ?? [],
|
||||||
|
before.foliageBlockerMask.values.length,
|
||||||
|
"Terrain foliage blocker mask"
|
||||||
|
)) {
|
||||||
|
const beforeValue =
|
||||||
|
getTerrainFoliageBlockerMaskValueAtSample(
|
||||||
|
before.foliageBlockerMask,
|
||||||
|
maskIndex % before.foliageBlockerMask.resolutionX,
|
||||||
|
Math.floor(maskIndex / before.foliageBlockerMask.resolutionX)
|
||||||
|
) ?? 0;
|
||||||
|
const afterValue =
|
||||||
|
getTerrainFoliageBlockerMaskValueAtSample(
|
||||||
|
after.foliageBlockerMask,
|
||||||
|
maskIndex % after.foliageBlockerMask.resolutionX,
|
||||||
|
Math.floor(maskIndex / after.foliageBlockerMask.resolutionX)
|
||||||
|
) ?? 0;
|
||||||
|
|
||||||
|
if (beforeValue === afterValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foliageBlockerMaskValues.push({
|
||||||
|
index: maskIndex,
|
||||||
|
before: beforeValue,
|
||||||
|
after: afterValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
terrainId: before.id,
|
terrainId: before.id,
|
||||||
heightSamples,
|
heightSamples,
|
||||||
paintWeights,
|
paintWeights,
|
||||||
foliageMaskValues
|
foliageMaskValues,
|
||||||
|
foliageBlockerMaskValues
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BufferAttribute, BufferGeometry } from "three";
|
|||||||
import type { Vec3 } from "../core/vector";
|
import type { Vec3 } from "../core/vector";
|
||||||
import {
|
import {
|
||||||
getTerrainFoliageMask,
|
getTerrainFoliageMask,
|
||||||
|
getTerrainFoliageBlockerMaskValueAtSample,
|
||||||
getTerrainFoliageMaskValueAtSample,
|
getTerrainFoliageMaskValueAtSample,
|
||||||
getTerrainHeightAtSample,
|
getTerrainHeightAtSample,
|
||||||
getTerrainSampleLayerWeights,
|
getTerrainSampleLayerWeights,
|
||||||
@@ -47,6 +48,7 @@ const TERRAIN_LOD_HYSTERESIS_RATIO = 0.16;
|
|||||||
|
|
||||||
interface TerrainMeshBuildOptions {
|
interface TerrainMeshBuildOptions {
|
||||||
foliageMaskLayerId?: string | null;
|
foliageMaskLayerId?: string | null;
|
||||||
|
foliageBlockerMask?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerrainLodLevelMeshData {
|
export interface TerrainLodLevelMeshData {
|
||||||
@@ -225,13 +227,19 @@ export function buildTerrainDerivedMeshData(
|
|||||||
}
|
}
|
||||||
layerWeightOffset += TERRAIN_LAYER_COUNT;
|
layerWeightOffset += TERRAIN_LAYER_COUNT;
|
||||||
foliageMaskWeights[foliageMaskWeightOffset] =
|
foliageMaskWeights[foliageMaskWeightOffset] =
|
||||||
foliageMask === null
|
options.foliageBlockerMask === true
|
||||||
? 0
|
? getTerrainFoliageBlockerMaskValueAtSample(
|
||||||
: getTerrainFoliageMaskValueAtSample(
|
terrain.foliageBlockerMask,
|
||||||
foliageMask,
|
|
||||||
sampleX,
|
sampleX,
|
||||||
sampleZ
|
sampleZ
|
||||||
);
|
)
|
||||||
|
: foliageMask === null
|
||||||
|
? 0
|
||||||
|
: getTerrainFoliageMaskValueAtSample(
|
||||||
|
foliageMask,
|
||||||
|
sampleX,
|
||||||
|
sampleZ
|
||||||
|
);
|
||||||
foliageMaskWeightOffset += 1;
|
foliageMaskWeightOffset += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,9 +407,15 @@ function pushTerrainLodVertex(
|
|||||||
? null
|
? null
|
||||||
: getTerrainFoliageMask(terrain, options.foliageMaskLayerId);
|
: getTerrainFoliageMask(terrain, options.foliageMaskLayerId);
|
||||||
foliageMaskWeights.push(
|
foliageMaskWeights.push(
|
||||||
foliageMask === null
|
options.foliageBlockerMask === true
|
||||||
? 0
|
? getTerrainFoliageBlockerMaskValueAtSample(
|
||||||
: getTerrainFoliageMaskValueAtSample(foliageMask, sampleX, sampleZ)
|
terrain.foliageBlockerMask,
|
||||||
|
sampleX,
|
||||||
|
sampleZ
|
||||||
|
)
|
||||||
|
: foliageMask === null
|
||||||
|
? 0
|
||||||
|
: getTerrainFoliageMaskValueAtSample(foliageMask, sampleX, sampleZ)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user