From 2061e97d5c8fad48b0563417c5648f91b62d6bfa Mon Sep 17 00:00:00 2001 From: como Date: Thu, 26 Mar 2026 11:06:07 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=A4=8D=E8=A2=AB=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=B8=BA=E6=8C=87=E5=AE=9A=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/OceanScene.js | 39 +++++- src/VegetationSystem.js | 302 ++++++++++++++++++++++++++++------------ 2 files changed, 248 insertions(+), 93 deletions(-) diff --git a/src/OceanScene.js b/src/OceanScene.js index 4888d22..d1eed95 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -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(); diff --git a/src/VegetationSystem.js b/src/VegetationSystem.js index 7fe13be..ec1027f 100644 --- a/src/VegetationSystem.js +++ b/src/VegetationSystem.js @@ -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, { - areaRatio: 0.78, - minHeight: this.options.waterLevel + 1.2, - maxHeight: this.options.waterLevel + 12, - maxSlope: 1.35, - densityScale: 0.02, - densityThreshold: -0.18 - }); + 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,29 +139,128 @@ export class VegetationSystem { this.createPlantPrototype('Pine Medium', 3004, 1.25) ]; - const placements = this.collectPlacements(this.options.treeCount, { - areaRatio: 0.66, - minHeight: this.options.waterLevel + 2.5, - maxHeight: this.options.waterLevel + 18, - maxSlope: 0.95, - minSpacing: 15, - densityScale: 0.0042, - densityThreshold: 0.14 - }); + 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, + maxSlope: 0.95, + 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) => { - child.castShadow = true; - child.receiveShadow = true; - }); + const tree = this.instantiatePlant(prototypes[index % prototypes.length], placement, 1.15, 1.9); this.plants.push(tree); this.animatedPlants.push(tree); - this.treePositions.push(new THREE.Vector2(placement.x, placement.z)); + 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; + }); + 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,41 +328,67 @@ 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; - const colorMix = THREE.MathUtils.clamp((densityNoise + 1) * 0.5, 0, 1); - placements.push({ - x, - y, - z, - rotation: Math.random() * Math.PI * 2, - tilt: this.noise.noise2D(x * 0.03, z * 0.03) * 0.08, - scale: THREE.MathUtils.lerp(0.75, 1.45, Math.random()), - scaleMix: Math.random(), - colorMix - }); + 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); + return { + x, + y, + z, + rotation: Math.random() * Math.PI * 2, + tilt: this.noise.noise2D(x * 0.03, z * 0.03) * 0.08, + scale: THREE.MathUtils.lerp(0.75, 1.45, Math.random()), + scaleMix: Math.random(), + colorMix + }; + } + getSlopeAt(x, z) { const sample = 2.5; const left = this.terrain.getHeightAt(x - sample, 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; }