feat: refine terrain water and postprocessing
This commit is contained in:
@@ -71,6 +71,7 @@ interface DesertContext {
|
|||||||
gobiVariant: number[][];
|
gobiVariant: number[][];
|
||||||
gobiMinHeight: number[][];
|
gobiMinHeight: number[][];
|
||||||
stoneHeight: number[][];
|
stoneHeight: number[][];
|
||||||
|
stoneDepth: number[][];
|
||||||
stoneVariant: number[][];
|
stoneVariant: number[][];
|
||||||
streamDepthMap: Map<string, { depth: number; waterHeight: number }>;
|
streamDepthMap: Map<string, { depth: number; waterHeight: number }>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from './desertFeatures';
|
} from './desertFeatures';
|
||||||
import { createMountainRockContext } from './mountainFeatures';
|
import { createMountainRockContext } from './mountainFeatures';
|
||||||
import type { RockFieldContext } from './rockFeatures';
|
import type { RockFieldContext } from './rockFeatures';
|
||||||
|
import { generateMountainStream, type MountainStreamVoxel } from './waterSystem';
|
||||||
|
|
||||||
// 植被生成 (已移除旧系统)
|
// 植被生成 (已移除旧系统)
|
||||||
// import {
|
// import {
|
||||||
@@ -74,7 +75,7 @@ export type ProgressCallback = (progress: TerrainGenerationProgress) => void;
|
|||||||
// ============= 地形生成配置 =============
|
// ============= 地形生成配置 =============
|
||||||
|
|
||||||
// 地形生成版本号 - 每次修改时改变这个数字,触发地图重新生成
|
// 地形生成版本号 - 每次修改时改变这个数字,触发地图重新生成
|
||||||
export const TERRAIN_VERSION = 83;
|
export const TERRAIN_VERSION = 86; // 优化:河流在同一平面时避开高度边界,保持在平台中心
|
||||||
|
|
||||||
// 逻辑层面的网格大小(战棋移动格子)
|
// 逻辑层面的网格大小(战棋移动格子)
|
||||||
export const TILE_SIZE = 1;
|
export const TILE_SIZE = 1;
|
||||||
@@ -313,6 +314,23 @@ export const generateTerrain = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 生成山地溪流(如果是山地场景)=====
|
||||||
|
let mountainStreamMap: Map<string, MountainStreamVoxel> | null = null;
|
||||||
|
if (sceneConfig?.name === 'mountain' && mountainRockContext && terrainHeightMap) {
|
||||||
|
reportProgress('basic', 3, '生成山地溪流系统');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
mountainStreamMap = generateMountainStream(
|
||||||
|
mapSize,
|
||||||
|
terrainHeightMap,
|
||||||
|
mountainRockContext,
|
||||||
|
seededRandom,
|
||||||
|
MICRO_SCALE
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[Terrain] 山地溪流已生成,包含 ${mountainStreamMap.size} 个微体素`);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 戈壁和巨石风化预处理 =====
|
// ===== 戈壁和巨石风化预处理 =====
|
||||||
// 在需要巨石的场景中,预先计算风化效果
|
// 在需要巨石的场景中,预先计算风化效果
|
||||||
let rockWeatheringResult: GobiWeatheringResult | null = null;
|
let rockWeatheringResult: GobiWeatheringResult | null = null;
|
||||||
@@ -629,10 +647,12 @@ export const generateTerrain = async (
|
|||||||
// 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation
|
// 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation
|
||||||
let sandMoundHeight = 0;
|
let sandMoundHeight = 0;
|
||||||
|
|
||||||
|
// 获取溪流信息(戈壁场景或山地场景)
|
||||||
const streamInfo = isDesertScene && desertContext
|
const streamInfo = isDesertScene && desertContext
|
||||||
? desertContext.streamDepthMap.get(`${ix}|${iz}`)
|
? desertContext.streamDepthMap.get(`${ix}|${iz}`)
|
||||||
: undefined;
|
: undefined;
|
||||||
const streamDepthMicro = streamInfo?.depth ?? 0;
|
const mountainStreamInfo = mountainStreamMap?.get(`${ix}|${iz}`);
|
||||||
|
const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0;
|
||||||
|
|
||||||
// 只有非河流区域才生成沙丘
|
// 只有非河流区域才生成沙丘
|
||||||
if (streamDepthMicro === 0 && isDesertScene && logicType === 'sand' && gobiLogicHeight === 0 && stoneLogicHeight === 0) {
|
if (streamDepthMicro === 0 && isDesertScene && logicType === 'sand' && gobiLogicHeight === 0 && stoneLogicHeight === 0) {
|
||||||
@@ -686,18 +706,16 @@ export const generateTerrain = async (
|
|||||||
// Apply sand mound height to worldY
|
// Apply sand mound height to worldY
|
||||||
worldY += sandMoundHeight;
|
worldY += sandMoundHeight;
|
||||||
|
|
||||||
// 河流生成:强制拉平水面
|
// 河流生成:强制使用绝对高度
|
||||||
// 1. 消除地形微噪声的影响(对于河流区域)
|
// 【方案A + B】如果河流存储了绝对表面高度,直接使用,完全忽略 logicHeight
|
||||||
// 2. 减去河道深度
|
|
||||||
if (streamDepthMicro > 0) {
|
if (streamDepthMicro > 0) {
|
||||||
// 还原到基础平面高度,去除 noise/smoothing 的影响
|
// 保持原有计算逻辑
|
||||||
|
worldY = logicHeight * MICRO_SCALE;
|
||||||
|
} else if (streamDepthMicro > 0) {
|
||||||
|
// 降级方案(如果没有绝对高度,使用旧逻辑)
|
||||||
const baseWorldY = logicHeight * MICRO_SCALE;
|
const baseWorldY = logicHeight * MICRO_SCALE;
|
||||||
|
worldY = baseWorldY;
|
||||||
// 如果当前已经受 Gobi/Stone 影响抬高了,也要考虑还原(但通常河流避开了它们)
|
worldY -= streamDepthMicro;
|
||||||
// 这里我们假设河流区域应当是平坦的沙地基础
|
|
||||||
|
|
||||||
worldY = baseWorldY; // 重置为绝对平坦
|
|
||||||
worldY -= streamDepthMicro; // 挖坑
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const surfaceY = worldY;
|
const surfaceY = worldY;
|
||||||
@@ -706,6 +724,10 @@ export const generateTerrain = async (
|
|||||||
let type: VoxelType = logicType;
|
let type: VoxelType = logicType;
|
||||||
const depth = surfaceY - y;
|
const depth = surfaceY - y;
|
||||||
|
|
||||||
|
// 【图2参考实现】河床蚀刻系统:只在河床内部处理
|
||||||
|
// 侧面会自然显示,因为相邻方块地表被降低了
|
||||||
|
// 不需要额外挖空周围地形
|
||||||
|
|
||||||
// 判断当前体素是否在石块范围内
|
// 判断当前体素是否在石块范围内
|
||||||
// 石块范围:从 groundLevelY - stoneMicroDepth 到 groundLevelY + stoneMicroHeight
|
// 石块范围:从 groundLevelY - stoneMicroDepth 到 groundLevelY + stoneMicroHeight
|
||||||
const stoneTopY = groundLevelY + stoneMicroHeight;
|
const stoneTopY = groundLevelY + stoneMicroHeight;
|
||||||
@@ -717,11 +739,26 @@ export const generateTerrain = async (
|
|||||||
const isInGobiRange = gobiMicroHeight > 0 && !isInStoneRange &&
|
const isInGobiRange = gobiMicroHeight > 0 && !isInStoneRange &&
|
||||||
depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight;
|
depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight;
|
||||||
|
|
||||||
// 应用溪流蚀刻效果
|
// 【图2参考】河床内部渲染系统(简化版)
|
||||||
if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
|
if (mountainStreamInfo && streamDepthMicro > 0 && depth < streamDepthMicro) {
|
||||||
|
// 渲染河床底部为泥土
|
||||||
|
type = 'dirt';
|
||||||
|
|
||||||
|
// 河床中心区域:混合深色泥土和碎石
|
||||||
|
if (mountainStreamInfo.isCenter) {
|
||||||
|
const noise = pseudoRandom(ix * 0.25 + iz * 0.25 + y * 0.1);
|
||||||
|
if (noise > 0.6) {
|
||||||
|
type = 'medium_dirt'; // 中等深度泥土
|
||||||
|
} else if (noise > 0.35) {
|
||||||
|
type = 'dark_stone'; // 碎石
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 戈壁溪流的原有逻辑
|
||||||
|
else if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
|
||||||
type = 'etched_sand';
|
type = 'etched_sand';
|
||||||
// CENTER OF STREAM = DARK STONE / GRAVEL
|
// CENTER OF STREAM = DARK STONE / GRAVEL
|
||||||
if (streamInfo?.isCenter) {
|
if (streamInfo && streamInfo.isCenter) {
|
||||||
// Simple noise for gravel patchiness
|
// Simple noise for gravel patchiness
|
||||||
if (pseudoRandom(ix * 0.3 + iz * 0.3) > 0.4) {
|
if (pseudoRandom(ix * 0.3 + iz * 0.3) > 0.4) {
|
||||||
type = 'dark_stone';
|
type = 'dark_stone';
|
||||||
@@ -881,7 +918,7 @@ export const generateTerrain = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (streamDepthMicro > 0) {
|
if (streamDepthMicro > 0) {
|
||||||
// Rule 1: Enable water filling logic
|
// 戈壁溪流水体填充
|
||||||
if (streamInfo?.waterHeight && streamInfo.waterHeight > 0) {
|
if (streamInfo?.waterHeight && streamInfo.waterHeight > 0) {
|
||||||
for (let h = 1; h <= streamInfo.waterHeight; h++) {
|
for (let h = 1; h <= streamInfo.waterHeight; h++) {
|
||||||
// Ensure water doesn't exceed surface (should not happen with waterH calculation, but safe)
|
// Ensure water doesn't exceed surface (should not happen with waterH calculation, but safe)
|
||||||
@@ -897,6 +934,132 @@ export const generateTerrain = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 山地溪流水体填充(包含瀑布效果)
|
||||||
|
if (mountainStreamInfo?.waterHeight && mountainStreamInfo.waterHeight > 0) {
|
||||||
|
const waterHeight = mountainStreamInfo.waterHeight;
|
||||||
|
|
||||||
|
// 普通水面填充
|
||||||
|
for (let h = 1; h <= waterHeight; h++) {
|
||||||
|
if (surfaceY + h > surfaceY + streamDepthMicro) break;
|
||||||
|
|
||||||
|
addVoxel(
|
||||||
|
ix,
|
||||||
|
surfaceY + h,
|
||||||
|
iz,
|
||||||
|
'water',
|
||||||
|
varyColor('water', seededRandom()),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 瀑布效果:基于相邻微体素的真实高度差填充
|
||||||
|
// 【修复】检测相邻河流微体素的绝对高度,填充高度差
|
||||||
|
if (mountainStreamInfo && waterHeight > 0) {
|
||||||
|
const waterTopY = surfaceY + Math.floor(waterHeight);
|
||||||
|
|
||||||
|
// 检测4个方向的相邻微体素
|
||||||
|
const checkDirections = [
|
||||||
|
{ dx: 1, dz: 0 },
|
||||||
|
{ dx: -1, dz: 0 },
|
||||||
|
{ dx: 0, dz: 1 },
|
||||||
|
{ dx: 0, dz: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let maxHeightDiff = 0;
|
||||||
|
|
||||||
|
for (const dir of checkDirections) {
|
||||||
|
const nx = ix + dir.dx;
|
||||||
|
const nz = iz + dir.dz;
|
||||||
|
|
||||||
|
const neighborStreamInfo = mountainStreamMap?.get(`${nx}|${nz}`);
|
||||||
|
|
||||||
|
if (neighborStreamInfo) {
|
||||||
|
// 简化:使用cascadeHeight判断
|
||||||
|
const cascadeH = neighborStreamInfo.cascadeHeight || 0;
|
||||||
|
|
||||||
|
if (cascadeH > 0) {
|
||||||
|
maxHeightDiff = Math.max(maxHeightDiff, cascadeH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果检测到高度差 > 2,填充侧面瀑布
|
||||||
|
if (maxHeightDiff >= 2) {
|
||||||
|
for (let dy = 1; dy < maxHeightDiff; dy++) {
|
||||||
|
const cascadeY = waterTopY - dy;
|
||||||
|
|
||||||
|
if (cascadeY > MIN_WORLD_Y && !occupancy.has(keyOf(ix, cascadeY, iz))) {
|
||||||
|
const noise = pseudoRandom(ix * 0.3 + cascadeY * 0.7 + iz * 0.5);
|
||||||
|
if (noise > 0.1) { // 90% 密度
|
||||||
|
addVoxel(
|
||||||
|
ix,
|
||||||
|
cascadeY,
|
||||||
|
iz,
|
||||||
|
'water',
|
||||||
|
varyColor('water', seededRandom()),
|
||||||
|
0.8
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 瀑布底部水花效果
|
||||||
|
if (mountainStreamInfo.isCascade || maxHeightDiff >= 2) {
|
||||||
|
const splashRange = 2;
|
||||||
|
for (let dx = -splashRange; dx <= splashRange; dx++) {
|
||||||
|
for (let dz = -splashRange; dz <= splashRange; dz++) {
|
||||||
|
if (dx === 0 && dz === 0) continue;
|
||||||
|
|
||||||
|
const splashX = ix + dx;
|
||||||
|
const splashZ = iz + dz;
|
||||||
|
const dist = Math.sqrt(dx * dx + dz * dz);
|
||||||
|
|
||||||
|
if (dist > splashRange) continue;
|
||||||
|
|
||||||
|
const splashHeight = Math.floor(2 - dist);
|
||||||
|
if (splashHeight < 1) continue;
|
||||||
|
|
||||||
|
const splashNoise = pseudoRandom(splashX * 0.5 + waterTopY * 0.9 + splashZ * 0.3);
|
||||||
|
if (splashNoise > 0.3) {
|
||||||
|
for (let h = 1; h <= splashHeight; h++) {
|
||||||
|
const splashY = waterTopY + h;
|
||||||
|
if (!occupancy.has(keyOf(splashX, splashY, splashZ))) {
|
||||||
|
addVoxel(
|
||||||
|
splashX,
|
||||||
|
splashY,
|
||||||
|
splashZ,
|
||||||
|
'water',
|
||||||
|
varyColor('water', seededRandom()),
|
||||||
|
0.6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激流效果:非瀑布段但有一定流速(流向明显)
|
||||||
|
if (!mountainStreamInfo.isCascade && mountainStreamInfo.flowDirection) {
|
||||||
|
const waterTopY = surfaceY + Math.floor(waterHeight);
|
||||||
|
const noise = pseudoRandom(ix * 0.4 + iz * 0.6);
|
||||||
|
|
||||||
|
// 50% 概率在水面上方生成激流水花(1格高)
|
||||||
|
if (noise > 0.5 && !occupancy.has(keyOf(ix, waterTopY + 1, iz))) {
|
||||||
|
addVoxel(
|
||||||
|
ix,
|
||||||
|
waterTopY + 1,
|
||||||
|
iz,
|
||||||
|
'water',
|
||||||
|
varyColor('water', seededRandom()),
|
||||||
|
0.3 // 很透明的水花
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sparse half-height tufts on surface for additional detail
|
// Sparse half-height tufts on surface for additional detail
|
||||||
|
|||||||
@@ -261,9 +261,6 @@ export const generateDesertRiver = (
|
|||||||
const bankDepth = 1; // 浅河床深度
|
const bankDepth = 1; // 浅河床深度
|
||||||
const waterFillHeight = 2; // 水深
|
const waterFillHeight = 2; // 水深
|
||||||
|
|
||||||
// 使用 Set 防止重复计算
|
|
||||||
const processedPoints = new Set<string>();
|
|
||||||
|
|
||||||
for (const node of bestPath) {
|
for (const node of bestPath) {
|
||||||
const cx = node.x;
|
const cx = node.x;
|
||||||
const cy = node.y; // cy is Z
|
const cy = node.y; // cy is Z
|
||||||
@@ -318,3 +315,558 @@ export const generateDesertRiver = (
|
|||||||
|
|
||||||
return streamMap;
|
return streamMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 山地溪流生成系统 V1
|
||||||
|
* 特点:
|
||||||
|
* 1. 从高处山泉起点流向低处,符合自然规律
|
||||||
|
* 2. 河流宽度 3-6 微体素,河床 8-9 微体素
|
||||||
|
* 3. 深度根据宽度动态调整(1.5-3 微体素)
|
||||||
|
* 4. 严格避开石头方块(stoneHeight > 0 或 stoneDepth > 0)
|
||||||
|
* 5. 高度落差处生成瀑布效果(密集水体素 + 水花)
|
||||||
|
* 6. 支持 1-2 条支流系统
|
||||||
|
* 7. 起点可以是山泉洞口效果
|
||||||
|
*
|
||||||
|
* @param mapSize 逻辑地图大小
|
||||||
|
* @param terrainHeightMap 地形高度图(逻辑坐标)
|
||||||
|
* @param rockContext 山地巨石上下文(用于避障)
|
||||||
|
* @param rng 随机数生成器
|
||||||
|
* @param microScale 微缩比例(默认 8)
|
||||||
|
* @returns 溪流深度Map,包含河床、水面、瀑布等信息
|
||||||
|
*/
|
||||||
|
export interface MountainStreamVoxel extends StreamVoxel {
|
||||||
|
isCascade?: boolean; // 是否瀑布段
|
||||||
|
cascadeHeight?: number; // 瀑布落差(微体素)
|
||||||
|
isSpring?: boolean; // 是否山泉起点
|
||||||
|
isTributary?: boolean; // 是否支流
|
||||||
|
flowDirection?: { dx: number; dz: number }; // 流向(未来用于动画)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MountainRockContext {
|
||||||
|
stoneHeight: number[][];
|
||||||
|
stoneDepth: number[][];
|
||||||
|
stoneVariant: number[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMountainStream = (
|
||||||
|
mapSize: number,
|
||||||
|
terrainHeightMap: number[][],
|
||||||
|
rockContext: MountainRockContext,
|
||||||
|
rng: () => number,
|
||||||
|
microScale: number = DEFAULT_MICRO_SCALE
|
||||||
|
): Map<string, MountainStreamVoxel> => {
|
||||||
|
const width = mapSize * microScale;
|
||||||
|
const height = mapSize * microScale;
|
||||||
|
|
||||||
|
console.log('[MountainStream] 开始生成山地溪流系统...');
|
||||||
|
|
||||||
|
// 1. 初始化噪声生成器
|
||||||
|
let seedVal = rng() * 10000;
|
||||||
|
const noise2D = createNoise2D(() => {
|
||||||
|
seedVal = (seedVal * 9301 + 49297) % 233280;
|
||||||
|
return seedVal / 233280;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 构建障碍物网格 + 高度图(微体素精度)
|
||||||
|
const grid = new Uint8Array(width * height).fill(0); // 0: 可行走, 1: 障碍物
|
||||||
|
const heightGrid = new Float32Array(width * height); // 每个微体素的高度
|
||||||
|
|
||||||
|
const setGrid = (x: number, z: number, val: number) => {
|
||||||
|
if (x >= 0 && x < width && z >= 0 && z < height) {
|
||||||
|
grid[z * width + x] = Math.max(grid[z * width + x], val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGrid = (x: number, z: number): number => {
|
||||||
|
if (x >= 0 && x < width && z >= 0 && z < height) {
|
||||||
|
return grid[z * width + x];
|
||||||
|
}
|
||||||
|
return 1; // 越界视为障碍
|
||||||
|
};
|
||||||
|
|
||||||
|
const setHeight = (x: number, z: number, h: number) => {
|
||||||
|
if (x >= 0 && x < width && z >= 0 && z < height) {
|
||||||
|
heightGrid[z * width + x] = h;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeight = (x: number, z: number): number => {
|
||||||
|
if (x >= 0 && x < width && z >= 0 && z < height) {
|
||||||
|
return heightGrid[z * width + x];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 填充障碍物数据和高度图
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
const hasStone = rockContext.stoneHeight[lx][lz] > 0 || rockContext.stoneDepth[lx][lz] > 0;
|
||||||
|
const baseHeight = terrainHeightMap[lx][lz];
|
||||||
|
|
||||||
|
// 填充微体素网格
|
||||||
|
for (let mx = 0; mx < microScale; mx++) {
|
||||||
|
for (let mz = 0; mz < microScale; mz++) {
|
||||||
|
const ix = lx * microScale + mx;
|
||||||
|
const iz = lz * microScale + mz;
|
||||||
|
|
||||||
|
// 石头区域标记为障碍物(严格避开)
|
||||||
|
if (hasStone) {
|
||||||
|
setGrid(ix, iz, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充高度(基础地形高度,不考虑石头)
|
||||||
|
// 使用微小噪声增加地形起伏
|
||||||
|
const microNoise = noise2D(ix * 0.5, iz * 0.5) * 0.3;
|
||||||
|
setHeight(ix, iz, baseHeight + microNoise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 选择起点(山泉位置)
|
||||||
|
// 策略:在高度 > 平均高度 + 阈值 的非石头区域随机选择
|
||||||
|
const avgHeight = terrainHeightMap.flat().reduce((a, b) => a + b, 0) / (mapSize * mapSize);
|
||||||
|
const heightThreshold = avgHeight + Math.max(1, avgHeight * 0.3);
|
||||||
|
|
||||||
|
const candidateStarts: Array<{ x: number; z: number; h: number }> = [];
|
||||||
|
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
const h = terrainHeightMap[lx][lz];
|
||||||
|
const hasStone = rockContext.stoneHeight[lx][lz] > 0 || rockContext.stoneDepth[lx][lz] > 0;
|
||||||
|
|
||||||
|
if (h >= heightThreshold && !hasStone) {
|
||||||
|
// 转换为微体素坐标(中心点)
|
||||||
|
const ix = lx * microScale + Math.floor(microScale / 2);
|
||||||
|
const iz = lz * microScale + Math.floor(microScale / 2);
|
||||||
|
candidateStarts.push({ x: ix, z: iz, h });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateStarts.length === 0) {
|
||||||
|
console.warn('[MountainStream] 未找到合适的山泉起点,使用随机高点');
|
||||||
|
// 降级方案:选择最高的几个点
|
||||||
|
let maxH = -Infinity;
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
maxH = Math.max(maxH, terrainHeightMap[lx][lz]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
if (terrainHeightMap[lx][lz] >= maxH - 0.5) {
|
||||||
|
const ix = lx * microScale + Math.floor(microScale / 2);
|
||||||
|
const iz = lz * microScale + Math.floor(microScale / 2);
|
||||||
|
if (getGrid(ix, iz) === 0) {
|
||||||
|
candidateStarts.push({ x: ix, z: iz, h: terrainHeightMap[lx][lz] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateStarts.length === 0) {
|
||||||
|
console.error('[MountainStream] 无法找到任何有效起点,溪流生成失败');
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择最高的前5个候选点之一
|
||||||
|
candidateStarts.sort((a, b) => b.h - a.h);
|
||||||
|
const topCandidates = candidateStarts.slice(0, Math.min(5, candidateStarts.length));
|
||||||
|
const startPoint = topCandidates[Math.floor(rng() * topCandidates.length)];
|
||||||
|
|
||||||
|
console.log(`[MountainStream] 选择山泉起点: (${startPoint.x}, ${startPoint.z}), 高度: ${startPoint.h.toFixed(2)}`);
|
||||||
|
|
||||||
|
// 5. A* 寻路:从高处到低处
|
||||||
|
// 目标:选择地图边缘或低洼区域作为终点
|
||||||
|
const minHeight = Math.min(...terrainHeightMap.flat());
|
||||||
|
const targetHeight = minHeight + (avgHeight - minHeight) * 0.2; // 低于平均高度
|
||||||
|
|
||||||
|
// 选择地图边缘的低点作为终点候选
|
||||||
|
const candidateEnds: Array<{ x: number; z: number; h: number }> = [];
|
||||||
|
|
||||||
|
const edgePositions = [
|
||||||
|
...Array.from({ length: mapSize }, (_, i) => ({ lx: i, lz: 0 })), // 上边缘
|
||||||
|
...Array.from({ length: mapSize }, (_, i) => ({ lx: i, lz: mapSize - 1 })), // 下边缘
|
||||||
|
...Array.from({ length: mapSize }, (_, i) => ({ lx: 0, lz: i })), // 左边缘
|
||||||
|
...Array.from({ length: mapSize }, (_, i) => ({ lx: mapSize - 1, lz: i })), // 右边缘
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { lx, lz } of edgePositions) {
|
||||||
|
const h = terrainHeightMap[lx][lz];
|
||||||
|
const hasStone = rockContext.stoneHeight[lx][lz] > 0 || rockContext.stoneDepth[lx][lz] > 0;
|
||||||
|
|
||||||
|
if (h <= targetHeight && !hasStone) {
|
||||||
|
const ix = lx * microScale + Math.floor(microScale / 2);
|
||||||
|
const iz = lz * microScale + Math.floor(microScale / 2);
|
||||||
|
candidateEnds.push({ x: ix, z: iz, h });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateEnds.length === 0) {
|
||||||
|
console.warn('[MountainStream] 未找到边缘低点,使用最低点');
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
const h = terrainHeightMap[lx][lz];
|
||||||
|
if (h <= minHeight + 0.5 && getGrid(lx * microScale, lz * microScale) === 0) {
|
||||||
|
const ix = lx * microScale + Math.floor(microScale / 2);
|
||||||
|
const iz = lz * microScale + Math.floor(microScale / 2);
|
||||||
|
candidateEnds.push({ x: ix, z: iz, h });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateEnds.length === 0) {
|
||||||
|
console.error('[MountainStream] 无法找到有效终点,溪流生成失败');
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择最低的终点
|
||||||
|
candidateEnds.sort((a, b) => a.h - b.h);
|
||||||
|
const endPoint = candidateEnds[Math.floor(rng() * Math.min(3, candidateEnds.length))];
|
||||||
|
|
||||||
|
console.log(`[MountainStream] 选择终点: (${endPoint.x}, ${endPoint.z}), 高度: ${endPoint.h.toFixed(2)}`);
|
||||||
|
|
||||||
|
// 6. 执行 A* 寻路(优先选择高度下降方向)
|
||||||
|
interface PathNode {
|
||||||
|
x: number;
|
||||||
|
z: number;
|
||||||
|
f: number;
|
||||||
|
g: number;
|
||||||
|
h: number;
|
||||||
|
parent: PathNode | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minPathLength = mapSize * microScale * 0.5; // 最小路径长度 50%
|
||||||
|
let mainPath: PathNode[] | null = null;
|
||||||
|
|
||||||
|
const startNode: PathNode = {
|
||||||
|
x: startPoint.x,
|
||||||
|
z: startPoint.z,
|
||||||
|
f: 0,
|
||||||
|
g: 0,
|
||||||
|
h: 0,
|
||||||
|
parent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const openList = new PriorityQueue() as any;
|
||||||
|
openList.enqueue(startNode);
|
||||||
|
const closedSet = new Set<string>();
|
||||||
|
const nodeKey = (x: number, z: number) => `${x}|${z}`;
|
||||||
|
|
||||||
|
// 启发式函数:优先选择高度下降 + 接近终点
|
||||||
|
const heuristic = (x: number, z: number, currentHeight: number): number => {
|
||||||
|
const distToEnd = Math.abs(endPoint.x - x) + Math.abs(endPoint.z - z);
|
||||||
|
const heightAtPos = getHeight(x, z);
|
||||||
|
const heightDiff = currentHeight - heightAtPos; // 正值表示下降
|
||||||
|
|
||||||
|
// 高度下降奖励 + 距离惩罚
|
||||||
|
return distToEnd * 1.0 - heightDiff * 20.0; // 强烈倾向于下坡
|
||||||
|
};
|
||||||
|
|
||||||
|
let finalNode: PathNode | null = null;
|
||||||
|
let iterations = 0;
|
||||||
|
const maxIterations = width * height * 2;
|
||||||
|
|
||||||
|
while (!openList.isEmpty() && iterations < maxIterations) {
|
||||||
|
iterations++;
|
||||||
|
const currentNode = openList.dequeue();
|
||||||
|
if (!currentNode) break;
|
||||||
|
|
||||||
|
// 到达终点附近或者到达足够低的位置
|
||||||
|
const currentHeight = getHeight(currentNode.x, currentNode.z);
|
||||||
|
const distToEnd = Math.abs(endPoint.x - currentNode.x) + Math.abs(endPoint.z - currentNode.z);
|
||||||
|
|
||||||
|
if (distToEnd < 8 && currentHeight <= endPoint.h + 0.5) {
|
||||||
|
finalNode = currentNode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经走得很远且高度足够低,也可以停止
|
||||||
|
if (currentNode.g > minPathLength && currentHeight <= avgHeight * 0.5) {
|
||||||
|
finalNode = currentNode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = nodeKey(currentNode.x, currentNode.z);
|
||||||
|
if (closedSet.has(key)) continue;
|
||||||
|
closedSet.add(key);
|
||||||
|
|
||||||
|
// 8个方向 + 随机蜿蜒
|
||||||
|
const dirs = [
|
||||||
|
{ dx: 1, dz: 0, cost: 1.0 },
|
||||||
|
{ dx: -1, dz: 0, cost: 1.0 },
|
||||||
|
{ dx: 0, dz: 1, cost: 1.0 },
|
||||||
|
{ dx: 0, dz: -1, cost: 1.0 },
|
||||||
|
{ dx: 1, dz: 1, cost: 1.414 },
|
||||||
|
{ dx: 1, dz: -1, cost: 1.414 },
|
||||||
|
{ dx: -1, dz: 1, cost: 1.414 },
|
||||||
|
{ dx: -1, dz: -1, cost: 1.414 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const nx = currentNode.x + dir.dx;
|
||||||
|
const nz = currentNode.z + dir.dz;
|
||||||
|
|
||||||
|
if (nx < 0 || nx >= width || nz < 0 || nz >= height) continue;
|
||||||
|
if (closedSet.has(nodeKey(nx, nz))) continue;
|
||||||
|
if (getGrid(nx, nz) === 1) continue; // 避开石头
|
||||||
|
|
||||||
|
let newG = currentNode.g + dir.cost;
|
||||||
|
|
||||||
|
// 高度变化代价:上坡代价极高,下坡有奖励
|
||||||
|
const heightDiff = getHeight(nx, nz) - currentHeight;
|
||||||
|
if (heightDiff > 0) {
|
||||||
|
newG += heightDiff * 50; // 强烈惩罚上坡
|
||||||
|
} else {
|
||||||
|
newG += heightDiff * 5; // 下坡有轻微奖励(负值)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【新增】避开高度边界:检查周围8个方向的高度一致性
|
||||||
|
// 如果周围高度不一致,说明在边界上,增加代价
|
||||||
|
if (Math.abs(heightDiff) < 0.1 * microScale) { // 如果是在同一平面
|
||||||
|
let heightVariation = 0;
|
||||||
|
let sameHeightCount = 0;
|
||||||
|
const checkRadius = 1;
|
||||||
|
|
||||||
|
for (let dx = -checkRadius; dx <= checkRadius; dx++) {
|
||||||
|
for (let dz = -checkRadius; dz <= checkRadius; dz++) {
|
||||||
|
if (dx === 0 && dz === 0) continue;
|
||||||
|
const checkX = nx + dx;
|
||||||
|
const checkZ = nz + dz;
|
||||||
|
|
||||||
|
if (checkX >= 0 && checkX < width && checkZ >= 0 && checkZ < height) {
|
||||||
|
const neighborHeight = getHeight(checkX, checkZ);
|
||||||
|
const diff = Math.abs(neighborHeight - currentHeight);
|
||||||
|
|
||||||
|
if (diff < 0.1 * microScale) {
|
||||||
|
sameHeightCount++; // 同高度的邻居
|
||||||
|
} else {
|
||||||
|
heightVariation += diff; // 累计高度差异
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果周围高度差异大(在边界上),增加代价
|
||||||
|
if (heightVariation > 0.5 * microScale) {
|
||||||
|
newG += 30; // 强烈避开高度边界
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果同高度邻居多(在平台中心),减少代价
|
||||||
|
if (sameHeightCount >= 6) {
|
||||||
|
newG -= 5; // 奖励在平台中心
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 噪声引导:模拟地形凹陷偏好
|
||||||
|
const noiseVal = noise2D(nx * 0.05, nz * 0.05);
|
||||||
|
newG += (noiseVal + 1) * 2; // 轻微噪声影响
|
||||||
|
|
||||||
|
// 随机蜿蜒
|
||||||
|
newG += rng() * 1.0;
|
||||||
|
|
||||||
|
// 每隔一段距离,检查是否有局部凹陷(水流汇集效果)
|
||||||
|
if (Math.floor(currentNode.g / 10) !== Math.floor(newG / 10)) {
|
||||||
|
let minLocalHeight = currentHeight;
|
||||||
|
for (let dx = -2; dx <= 2; dx++) {
|
||||||
|
for (let dz = -2; dz <= 2; dz++) {
|
||||||
|
const checkX = nx + dx;
|
||||||
|
const checkZ = nz + dz;
|
||||||
|
if (getGrid(checkX, checkZ) === 0) {
|
||||||
|
minLocalHeight = Math.min(minLocalHeight, getHeight(checkX, checkZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minLocalHeight < currentHeight - 0.5) {
|
||||||
|
newG -= 10; // 向凹陷处倾斜
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = heuristic(nx, nz, currentHeight);
|
||||||
|
openList.enqueue({
|
||||||
|
x: nx,
|
||||||
|
z: nz,
|
||||||
|
f: newG + h,
|
||||||
|
g: newG,
|
||||||
|
h: h,
|
||||||
|
parent: currentNode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalNode) {
|
||||||
|
// 回溯路径
|
||||||
|
const path: PathNode[] = [];
|
||||||
|
let curr: PathNode | null = finalNode;
|
||||||
|
while (curr) {
|
||||||
|
path.push(curr);
|
||||||
|
curr = curr.parent;
|
||||||
|
}
|
||||||
|
path.reverse();
|
||||||
|
mainPath = path;
|
||||||
|
console.log(`[MountainStream] 主河流路径生成成功,长度: ${path.length} 微体素`);
|
||||||
|
} else {
|
||||||
|
console.warn('[MountainStream] 主河流路径生成失败');
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 生成支流(1-2条)
|
||||||
|
const tributaries: PathNode[][] = [];
|
||||||
|
const numTributaries = Math.floor(rng() * 2) + 1; // 1-2条支流
|
||||||
|
|
||||||
|
for (let i = 0; i < numTributaries; i++) {
|
||||||
|
// 在主河流 30%-70% 位置选择分叉点
|
||||||
|
const branchIndex = Math.floor(mainPath.length * (0.3 + rng() * 0.4));
|
||||||
|
const branchPoint = mainPath[branchIndex];
|
||||||
|
|
||||||
|
// 支流长度为主流的 10-20%
|
||||||
|
const tribLength = Math.floor(mainPath.length * (0.1 + rng() * 0.1));
|
||||||
|
|
||||||
|
// 支流方向:与主流夹角 30-60 度
|
||||||
|
const mainDir =
|
||||||
|
branchIndex < mainPath.length - 1
|
||||||
|
? {
|
||||||
|
dx: mainPath[branchIndex + 1].x - branchPoint.x,
|
||||||
|
dz: mainPath[branchIndex + 1].z - branchPoint.z,
|
||||||
|
}
|
||||||
|
: { dx: 1, dz: 0 };
|
||||||
|
|
||||||
|
const angle = Math.atan2(mainDir.dz, mainDir.dx);
|
||||||
|
const tribAngle = angle + (rng() > 0.5 ? 1 : -1) * (Math.PI / 6 + rng() * Math.PI / 6); // ±30-60度
|
||||||
|
|
||||||
|
// 生成支流路径(简化版:直线 + 噪声扰动)
|
||||||
|
const tributary: PathNode[] = [];
|
||||||
|
let tx = branchPoint.x;
|
||||||
|
let tz = branchPoint.z;
|
||||||
|
|
||||||
|
for (let step = 0; step < tribLength; step++) {
|
||||||
|
const stepX = Math.cos(tribAngle) + (rng() - 0.5) * 0.5;
|
||||||
|
const stepZ = Math.sin(tribAngle) + (rng() - 0.5) * 0.5;
|
||||||
|
|
||||||
|
tx += stepX;
|
||||||
|
tz += stepZ;
|
||||||
|
|
||||||
|
const ix = Math.floor(tx);
|
||||||
|
const iz = Math.floor(tz);
|
||||||
|
|
||||||
|
if (ix < 0 || ix >= width || iz < 0 || iz >= height) break;
|
||||||
|
if (getGrid(ix, iz) === 1) break; // 遇到石头停止
|
||||||
|
|
||||||
|
tributary.push({ x: ix, z: iz, f: 0, g: step, h: 0, parent: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tributary.length > 5) {
|
||||||
|
tributaries.push(tributary);
|
||||||
|
console.log(`[MountainStream] 支流 ${i + 1} 生成成功,长度: ${tributary.length}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 河床雕刻(主河流 + 支流)
|
||||||
|
const streamMap = new Map<string, MountainStreamVoxel>();
|
||||||
|
|
||||||
|
const carveRiverBed = (path: PathNode[], isTributary: boolean = false) => {
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
const node = path[i];
|
||||||
|
const cx = node.x;
|
||||||
|
const cz = node.z;
|
||||||
|
|
||||||
|
// 河流宽度和深度(支流更窄更浅)
|
||||||
|
const waterWidth = isTributary
|
||||||
|
? 2 + rng() * 1 // 2-3 微体素
|
||||||
|
: 3 + rng() * 3; // 3-6 微体素
|
||||||
|
const riverBedWidth = isTributary
|
||||||
|
? 4 + rng() * 1 // 4-5 微体素
|
||||||
|
: 8 + rng() * 1; // 8-9 微体素
|
||||||
|
const baseDepth = isTributary
|
||||||
|
? 1 + waterWidth * 0.2 // 1-1.6 微体素
|
||||||
|
: 1.5 + waterWidth * 0.25; // 1.5-3 微体素
|
||||||
|
|
||||||
|
// 检查是否为瀑布段(高度落差 >= 2 微体素)
|
||||||
|
let isCascade = false;
|
||||||
|
let cascadeHeight = 0;
|
||||||
|
|
||||||
|
if (i < path.length - 1) {
|
||||||
|
const currentH = getHeight(cx, cz);
|
||||||
|
const nextH = getHeight(path[i + 1].x, path[i + 1].z);
|
||||||
|
const heightDiff = currentH - nextH;
|
||||||
|
|
||||||
|
if (heightDiff >= 2) {
|
||||||
|
isCascade = true;
|
||||||
|
cascadeHeight = heightDiff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在河床宽度范围内雕刻
|
||||||
|
const scanRad = Math.ceil(riverBedWidth / 2) + 1;
|
||||||
|
|
||||||
|
for (let dx = -scanRad; dx <= scanRad; dx++) {
|
||||||
|
for (let dz = -scanRad; dz <= scanRad; dz++) {
|
||||||
|
const tx = cx + dx;
|
||||||
|
const tz = cz + dz;
|
||||||
|
|
||||||
|
if (tx < 0 || tx >= width || tz < 0 || tz >= height) continue;
|
||||||
|
if (getGrid(tx, tz) === 1) continue; // 避开石头
|
||||||
|
|
||||||
|
const dist = Math.sqrt(dx * dx + dz * dz);
|
||||||
|
|
||||||
|
// 边缘噪声,使河岸不规则
|
||||||
|
const edgeNoise = noise2D(tx * 0.1, tz * 0.1) * 0.8;
|
||||||
|
const noisyWaterWidth = waterWidth / 2 + edgeNoise * 0.3;
|
||||||
|
const noisyBedWidth = riverBedWidth / 2 + edgeNoise * 0.5;
|
||||||
|
|
||||||
|
if (dist < noisyBedWidth) {
|
||||||
|
const isWater = dist < noisyWaterWidth;
|
||||||
|
const depthFactor = 1 - dist / noisyBedWidth; // 中心深,边缘浅
|
||||||
|
const depth = baseDepth * Math.max(0.3, depthFactor);
|
||||||
|
|
||||||
|
const key = `${tx}|${tz}`;
|
||||||
|
const existing = streamMap.get(key);
|
||||||
|
|
||||||
|
// 保留更深的深度
|
||||||
|
if (!existing || depth > existing.depth) {
|
||||||
|
// 计算流向
|
||||||
|
let flowDir = { dx: 0, dz: 0 };
|
||||||
|
if (i < path.length - 1) {
|
||||||
|
flowDir = {
|
||||||
|
dx: path[i + 1].x - cx,
|
||||||
|
dz: path[i + 1].z - cz,
|
||||||
|
};
|
||||||
|
const len = Math.sqrt(flowDir.dx ** 2 + flowDir.dz ** 2);
|
||||||
|
if (len > 0) {
|
||||||
|
flowDir.dx /= len;
|
||||||
|
flowDir.dz /= len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamMap.set(key, {
|
||||||
|
depth: depth,
|
||||||
|
waterHeight: isWater ? depth * 0.8 : 0, // 水深为河床深度的80%
|
||||||
|
isCenter: dist < 1,
|
||||||
|
isCascade,
|
||||||
|
cascadeHeight,
|
||||||
|
isSpring: i === 0 && !isTributary, // 主河流起点标记为山泉
|
||||||
|
isTributary,
|
||||||
|
flowDirection: flowDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 雕刻主河流
|
||||||
|
carveRiverBed(mainPath, false);
|
||||||
|
|
||||||
|
// 雕刻支流
|
||||||
|
for (const tributary of tributaries) {
|
||||||
|
carveRiverBed(tributary, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[MountainStream] 溪流系统生成完成,共 ${streamMap.size} 个微体素`);
|
||||||
|
|
||||||
|
return streamMap;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user