调整植被生成为指定模式

This commit is contained in:
2026-03-26 11:06:07 +08:00
parent df6d78236d
commit 2061e97d5c
2 changed files with 248 additions and 93 deletions

View File

@@ -310,11 +310,40 @@ export class OceanScene {
async initVegetation() {
const vegSystem = new VegetationSystem(this.terrainGenerator, {
grassCount: 28000, // 草簇数量,越大地表覆盖越密
shrubCount: 12, // 灌木和低矮植物数量
treeCount: 9, // 树木数量
terrainSize: 1200, // 植被允许分布的地形范围
waterLevel: 1 // 植被生成时参考的水位,避免贴近海边
grassCount: 0, // 随机草簇数量,设为 0 时只使用 grassAreas
shrubCount: 0, // 随机灌木数量,设为 0 时只使用 shrubPlacements
lowPlantCount: 0, // 随机低矮植物数量,设为 0 时只使用 lowPlantPlacements
treeCount: 0, // 随机树木数量,设为 0 时只使用 treePlacements
terrainSize: 1200, // 随机植被允许分布的地形范围
waterLevel: 1, // 植被生成时参考的水位,避免贴近海边
treePlacements: [ // 手动指定树木坐标
{ x: -180, z: -120, rotation: 0.4, scale: 1.6 },
{ x: -120, z: -40, rotation: 1.2, scale: 1.35 },
// { x: -40, z: -150, rotation: 2.1, scale: 1.75 },
// { x: 70, z: -70, rotation: 2.8, scale: 1.45 },
// { x: 135, z: 15, rotation: 4.1, scale: 1.55 },
// { x: 30, z: 120, rotation: 5.2, scale: 1.7 }
],
shrubPlacements: [ // 手动指定灌木坐标
{ x: -210, z: -65, rotation: 0.3, scale: 1.05 },
{ x: -195, z: -75, rotation: 1.4, scale: 0.95 },
// { x: -20, z: -95, rotation: 2.2, scale: 1.1 },
// { x: 55, z: -5, rotation: 3.6, scale: 0.9 },
// { x: 150, z: -55, rotation: 4.5, scale: 1.15 },
// { x: 185, z: 75, rotation: 5.4, scale: 1.0 }
],
lowPlantPlacements: [ // 手动指定低矮植物坐标
{ x: -235, z: -20, rotation: 0.6, scale: 0.58 },
{ x: -205, z: 15, rotation: 1.8, scale: 0.52 },
// { x: -10, z: -20, rotation: 2.7, scale: 0.48 },
// { x: 82, z: -132, rotation: 3.4, scale: 0.62 },
// { x: 118, z: 58, rotation: 4.2, scale: 0.56 },
// { x: 225, z: 18, rotation: 5.1, scale: 0.6 }
],
grassAreas: [ // 手动指定草簇生成区域
{ centerX: -140, centerZ: -10, width: 220, depth: 170, count: 4200 },
{ centerX: 110, centerZ: 65, width: 210, depth: 160, count: 3800 }
]
});
this.vegetation = vegSystem.generate();

View File

@@ -6,11 +6,16 @@ export class VegetationSystem {
constructor(terrain, options = {}) {
this.terrain = terrain;
this.options = {
grassCount: options.grassCount || 18000,
shrubCount: options.shrubCount || 420,
treeCount: options.treeCount || 120,
terrainSize: options.terrainSize || 1000,
waterLevel: options.waterLevel || 0
grassCount: options.grassCount ?? 18000,
shrubCount: options.shrubCount ?? 420,
lowPlantCount: options.lowPlantCount ?? 260,
treeCount: options.treeCount ?? 120,
terrainSize: options.terrainSize ?? 1000,
waterLevel: options.waterLevel ?? 0,
treePlacements: options.treePlacements ?? [],
shrubPlacements: options.shrubPlacements ?? [],
lowPlantPlacements: options.lowPlantPlacements ?? [],
grassAreas: options.grassAreas ?? []
};
this.noise = new SimplexNoise(12345);
@@ -19,6 +24,7 @@ export class VegetationSystem {
this.plants = [];
this.animatedPlants = [];
this.treePositions = [];
this.occupiedPlantPositions = [];
}
generate() {
@@ -26,10 +32,12 @@ export class VegetationSystem {
this.plants = [];
this.animatedPlants = [];
this.treePositions = [];
this.occupiedPlantPositions = [];
this.generateGrass();
this.generateTrees();
this.generateShrubs();
this.generateLowPlants();
if (this.grass) {
this.group.add(this.grass);
@@ -41,14 +49,41 @@ export class VegetationSystem {
}
generateGrass() {
const placements = this.collectPlacements(this.options.grassCount, {
const placements = [];
if (this.options.grassAreas.length > 0) {
this.options.grassAreas.forEach((area, areaIndex) => {
const areaCount = area.count ?? 2400;
placements.push(...this.collectAreaPlacements(areaCount, {
centerX: area.centerX ?? 0,
centerZ: area.centerZ ?? 0,
width: area.width ?? 120,
depth: area.depth ?? 120,
minHeight: area.minHeight ?? this.options.waterLevel + 1.2,
maxHeight: area.maxHeight ?? this.options.waterLevel + 12,
maxSlope: area.maxSlope ?? 1.35,
densityScale: area.densityScale ?? 0.02,
densityThreshold: area.densityThreshold ?? -0.18,
jitterSeed: areaIndex * 13.17
}));
});
}
if (this.options.grassCount > 0) {
placements.push(...this.collectPlacements(this.options.grassCount, {
areaRatio: 0.78,
minHeight: this.options.waterLevel + 1.2,
maxHeight: this.options.waterLevel + 12,
maxSlope: 1.35,
densityScale: 0.02,
densityThreshold: -0.18
});
}));
}
if (placements.length === 0) {
this.grass = null;
return;
}
const grassGeometries = [
this.createGrassBladeGeometry(0),
@@ -96,38 +131,6 @@ export class VegetationSystem {
this.grass = grassGroup;
}
generateShrubs() {
const prototypes = [
this.createPlantPrototype('Bush 1', 2001, 0.85),
this.createPlantPrototype('Bush 2', 2002, 0.95),
this.createPlantPrototype('Bush 3', 2003, 1.05)
];
const placements = this.collectPlacements(this.options.shrubCount, {
areaRatio: 0.72,
minHeight: this.options.waterLevel + 1.5,
maxHeight: this.options.waterLevel + 13,
maxSlope: 1.1,
minSpacing: 5,
densityScale: 0.006,
densityThreshold: 0.05
});
placements.forEach((placement, index) => {
const prototype = prototypes[index % prototypes.length];
const shrub = prototype.clone(true);
shrub.position.set(placement.x, placement.y, placement.z);
shrub.rotation.y = placement.rotation;
shrub.scale.setScalar(THREE.MathUtils.lerp(0.75, 1.35, placement.scaleMix));
shrub.traverse((child) => {
child.castShadow = true;
child.receiveShadow = true;
});
this.plants.push(shrub);
this.animatedPlants.push(shrub);
});
}
generateTrees() {
const prototypes = [
this.createPlantPrototype('Pine Small', 3001, 1.05),
@@ -136,7 +139,11 @@ export class VegetationSystem {
this.createPlantPrototype('Pine Medium', 3004, 1.25)
];
const placements = this.collectPlacements(this.options.treeCount, {
const placements = [
...this.normalizeManualPlacements(this.options.treePlacements, {
defaultScaleRange: [1.15, 1.85]
}),
...this.collectPlacements(this.options.treeCount, {
areaRatio: 0.66,
minHeight: this.options.waterLevel + 2.5,
maxHeight: this.options.waterLevel + 18,
@@ -144,21 +151,116 @@ export class VegetationSystem {
minSpacing: 15,
densityScale: 0.0042,
densityThreshold: 0.14
});
})
];
placements.forEach((placement, index) => {
const prototype = prototypes[index % prototypes.length];
const tree = prototype.clone(true);
tree.position.set(placement.x, placement.y, placement.z);
tree.rotation.y = placement.rotation;
tree.scale.setScalar(THREE.MathUtils.lerp(1.1, 1.9, placement.scaleMix));
tree.traverse((child) => {
const tree = this.instantiatePlant(prototypes[index % prototypes.length], placement, 1.15, 1.9);
this.plants.push(tree);
this.animatedPlants.push(tree);
this.treePositions.push({ x: placement.x, z: placement.z });
this.occupiedPlantPositions.push({ x: placement.x, z: placement.z });
});
}
generateShrubs() {
this.generateBushLayer({
placements: this.options.shrubPlacements,
count: this.options.shrubCount,
placementScaleRange: [0.85, 1.35],
instantiateScaleRange: [0.75, 1.35],
config: {
areaRatio: 0.72,
minHeight: this.options.waterLevel + 1.5,
maxHeight: this.options.waterLevel + 13,
maxSlope: 1.1,
minSpacing: 6,
densityScale: 0.006,
densityThreshold: 0.05
},
seedBase: 2001,
tintJitterBase: 0.85
});
}
generateLowPlants() {
this.generateBushLayer({
placements: this.options.lowPlantPlacements,
count: this.options.lowPlantCount,
placementScaleRange: [0.42, 0.7],
instantiateScaleRange: [0.42, 0.72],
config: {
areaRatio: 0.74,
minHeight: this.options.waterLevel + 1.3,
maxHeight: this.options.waterLevel + 10,
maxSlope: 1.2,
minSpacing: 4,
densityScale: 0.008,
densityThreshold: -0.02
},
seedBase: 4001,
tintJitterBase: 0.7
});
}
generateBushLayer(layerOptions) {
const prototypes = [
this.createPlantPrototype('Bush 1', layerOptions.seedBase, layerOptions.tintJitterBase),
this.createPlantPrototype('Bush 2', layerOptions.seedBase + 1, layerOptions.tintJitterBase + 0.08),
this.createPlantPrototype('Bush 3', layerOptions.seedBase + 2, layerOptions.tintJitterBase + 0.02)
];
const placements = [
...this.normalizeManualPlacements(layerOptions.placements, {
defaultScaleRange: layerOptions.placementScaleRange
}),
...this.collectPlacements(layerOptions.count, layerOptions.config)
];
placements.forEach((placement, index) => {
const plant = this.instantiatePlant(
prototypes[index % prototypes.length],
placement,
layerOptions.instantiateScaleRange[0],
layerOptions.instantiateScaleRange[1]
);
this.plants.push(plant);
this.animatedPlants.push(plant);
this.occupiedPlantPositions.push({ x: placement.x, z: placement.z });
});
}
instantiatePlant(prototype, placement, minScale, maxScale) {
const plant = prototype.clone(true);
plant.position.set(placement.x, placement.y, placement.z);
plant.rotation.y = placement.rotation;
plant.scale.setScalar(placement.scale ?? THREE.MathUtils.lerp(minScale, maxScale, placement.scaleMix ?? Math.random()));
plant.traverse((child) => {
child.castShadow = true;
child.receiveShadow = true;
});
this.plants.push(tree);
this.animatedPlants.push(tree);
this.treePositions.push(new THREE.Vector2(placement.x, placement.z));
return plant;
}
normalizeManualPlacements(placements, config = {}) {
return placements.map((placement, index) => {
const y = placement.y ?? this.terrain.getHeightAt(placement.x, placement.z);
const randomScale = THREE.MathUtils.lerp(
config.defaultScaleRange?.[0] ?? 1,
config.defaultScaleRange?.[1] ?? 1,
((index * 37) % 100) / 100
);
return {
x: placement.x,
y,
z: placement.z,
rotation: placement.rotation ?? (index * Math.PI * 0.37) % (Math.PI * 2),
tilt: placement.tilt ?? 0,
scale: placement.scale ?? randomScale,
scaleMix: placement.scaleMix ?? (((index * 53) % 100) / 100),
colorMix: placement.colorMix ?? 0.6
};
});
}
@@ -226,27 +328,56 @@ export class VegetationSystem {
}
collectPlacements(targetCount, config) {
if (!targetCount) return [];
const placements = [];
const maxAttempts = targetCount * 14;
for (let i = 0; i < maxAttempts && placements.length < targetCount; i++) {
const x = (Math.random() - 0.5) * this.options.terrainSize * config.areaRatio;
const z = (Math.random() - 0.5) * this.options.terrainSize * config.areaRatio;
const y = this.terrain.getHeightAt(x, z);
if (y < config.minHeight || y > config.maxHeight) continue;
const slope = this.getSlopeAt(x, z);
if (slope > config.maxSlope) continue;
const densityNoise = this.noise.noise2D(x * config.densityScale, z * config.densityScale);
if (densityNoise < config.densityThreshold) continue;
const placement = this.buildPlacementFromPoint(x, z, config);
if (!placement) continue;
if (config.minSpacing && !this.isFarEnoughFromPlants(placements, x, z, config.minSpacing)) continue;
if (config.minSpacing && !this.isFarEnoughFromPlants(this.treePositions, x, z, config.minSpacing * 0.8)) continue;
if (config.minSpacing && !this.isFarEnoughFromPlants(this.occupiedPlantPositions, x, z, config.minSpacing * 0.7)) continue;
placements.push(placement);
}
return placements;
}
collectAreaPlacements(targetCount, config) {
const placements = [];
const maxAttempts = targetCount * 10;
for (let i = 0; i < maxAttempts && placements.length < targetCount; i++) {
const x = config.centerX + (Math.random() - 0.5) * config.width;
const z = config.centerZ + (Math.random() - 0.5) * config.depth;
const placement = this.buildPlacementFromPoint(x, z, config, config.jitterSeed);
if (placement) {
placements.push(placement);
}
}
return placements;
}
buildPlacementFromPoint(x, z, config, jitterSeed = 0) {
const y = this.terrain.getHeightAt(x, z);
if (y < config.minHeight || y > config.maxHeight) return null;
const slope = this.getSlopeAt(x, z);
if (slope > config.maxSlope) return null;
const densityNoise = this.noise.noise2D(x * config.densityScale + jitterSeed, z * config.densityScale - jitterSeed);
if (densityNoise < config.densityThreshold) return null;
const colorMix = THREE.MathUtils.clamp((densityNoise + 1) * 0.5, 0, 1);
placements.push({
return {
x,
y,
z,
@@ -255,10 +386,7 @@ export class VegetationSystem {
scale: THREE.MathUtils.lerp(0.75, 1.45, Math.random()),
scaleMix: Math.random(),
colorMix
});
}
return placements;
};
}
getSlopeAt(x, z) {
@@ -275,10 +403,8 @@ export class VegetationSystem {
const minSpacingSq = minSpacing * minSpacing;
for (const pos of positions) {
const px = pos.x;
const pz = pos.z ?? pos.y;
const dx = px - x;
const dz = pz - z;
const dx = pos.x - x;
const dz = pos.z - z;
if (dx * dx + dz * dz < minSpacingSq) {
return false;
}