feat: refine terrain water and postprocessing

This commit is contained in:
Rocky
2025-12-01 15:16:35 +08:00
parent 5744ad4dde
commit 297aa0eda3
3 changed files with 735 additions and 19 deletions

View File

@@ -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 }>;
} }

View File

@@ -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

View File

@@ -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;
};