feat: update map terrain and post effects

This commit is contained in:
Rocky
2025-11-29 22:22:47 +08:00
parent d7ab602fdc
commit 5744ad4dde
8 changed files with 572 additions and 142 deletions

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{
"name": "WebCity",
"name": "VoxelGame",
"lockfileVersion": 3,
"requires": true,
"packages": {

View File

@@ -5,6 +5,34 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>voxel-tactics-horizon</title>
<!-- 在任何脚本运行前拦截 DevTools 错误 -->
<script>
// 拦截 React DevTools 的 semver 错误React 19.2 与 DevTools 的兼容性问题)
(function() {
const originalError = window.Error;
window.Error = function(message) {
if (typeof message === 'string' && message.includes('not valid semver')) {
// 创建一个静默的错误对象
const err = new originalError('');
err.stack = '';
return err;
}
return new originalError(message);
};
// 保留原型链
window.Error.prototype = originalError.prototype;
// 拦截全局错误事件
window.addEventListener('error', function(event) {
if (event.message && event.message.includes('not valid semver')) {
event.stopImmediatePropagation();
event.preventDefault();
return false;
}
}, true); // 使用捕获阶段,优先级最高
})();
</script>
</head>
<body>
<div id="root"></div>

View File

@@ -7,6 +7,7 @@
*/
import type { VoxelType } from './voxelStyles';
import { generateRockClusters } from './rockFeatures';
// MICRO_SCALE 常量:每个逻辑格子包含的微型体素数量
// const MICRO_SCALE = 8; // Unused
@@ -22,6 +23,7 @@ export interface DesertContext {
gobiVariant: number[][];
gobiMinHeight: number[][]; // 记录戈壁起始高度(用于拱门效果)
stoneHeight: number[][];
stoneDepth: number[][];
stoneVariant: number[][];
streamDepthMap: Map<string, { depth: number; waterHeight: number; isCenter: boolean }>; // Added isCenter
}
@@ -478,89 +480,18 @@ export const createDesertContext = (
const gobiVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
const gobiMinHeight = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
const stoneHeight = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
const stoneDepth = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
const stoneVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
layoutStrategicDesert(mapSize, gobiHeight, gobiVariant, gobiMinHeight, rand);
// Adjusted density: Ensure presence on 24/32 maps, occasional on 16
// 32x32 (1024) / 200 = ~5 stones
// 24x24 (576) / 200 = ~2-3 stones
// 16x16 (256) / 200 = ~1.2 -> handled specifically below
let targetStones = Math.floor((mapSize * mapSize) / 200);
if (mapSize <= 16) {
// For 16x16: 30% chance to spawn 1 stone, otherwise 0
targetStones = rand() > 0.7 ? 1 : 0;
}
let placedStones = 0;
let attempts = 0;
// Give plenty of attempts because the strict avoidance check will reject many positions
const maxAttempts = Math.max(20, targetStones * 50);
while (placedStones < targetStones && attempts < maxAttempts) {
attempts++;
const startX = Math.floor(rand() * mapSize);
const startZ = Math.floor(rand() * mapSize);
// Constraint: Max 3 blocks (1-3)
const targetSize = 1 + Math.floor(rand() * 3);
// Algorithm: Random Walker
const rockCells: Array<{x: number, z: number}> = [{x: startX, z: startZ}];
let safety = 0;
while (rockCells.length < targetSize && safety++ < 10) {
const seed = rockCells[Math.floor(rand() * rockCells.length)];
const dir = Math.floor(rand() * 4);
const dx = dir === 0 ? 1 : dir === 1 ? -1 : 0;
const dz = dir === 2 ? 1 : dir === 3 ? -1 : 0;
const nx = seed.x + dx;
const nz = seed.z + dz;
if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) {
if (!rockCells.some(c => c.x === nx && c.z === nz)) {
rockCells.push({x: nx, z: nz});
}
}
}
let isValid = true;
const checkDist = 2;
for (const cell of rockCells) {
for (let dx = -checkDist; dx <= checkDist; dx++) {
for (let dz = -checkDist; dz <= checkDist; dz++) {
const cx = cell.x + dx;
const cz = cell.z + dz;
if (cx >= 0 && cx < mapSize && cz >= 0 && cz < mapSize) {
if (gobiHeight[cx][cz] > 0) {
isValid = false;
break;
}
if (stoneHeight[cx][cz] > 0) {
isValid = false;
break;
}
}
}
if (!isValid) break;
}
if (!isValid) break;
}
if (isValid) {
const h = (targetSize > 1 && rand() > 0.85) ? 2 : 1;
const variant = Math.floor(rand() * 2);
for (const cell of rockCells) {
stoneHeight[cell.x][cell.z] = h;
stoneVariant[cell.x][cell.z] = variant;
}
placedStones++;
}
}
// 生成巨石簇(跨越地表的石块)
generateRockClusters({
mapSize,
rand,
field: { stoneHeight, stoneDepth, stoneVariant },
avoidMaps: [gobiHeight],
});
const streamDepthMap = generateDesertStreamMap(mapSize, rand, gobiHeight, stoneHeight);
return {
@@ -568,6 +499,7 @@ export const createDesertContext = (
gobiVariant,
gobiMinHeight,
stoneHeight,
stoneDepth,
stoneVariant,
streamDepthMap,
};

View File

@@ -0,0 +1,66 @@
/**
* 山地特征系统:生成跨越地表的巨石
* - 石块部分在地上13层部分在地下13层
* - 直接复用石块风化与颜色过渡
*/
import {
createEmptyRockField,
generateRockClusters,
type RockFieldContext,
} from './rockFeatures';
export type MountainRockContext = RockFieldContext;
const getMountainProfileOverride = (mapSize: number) => {
if (mapSize <= 16) {
return {
minClusters: 1,
maxClusters: 3,
maxCellsPerCluster: 2,
minAboveGround: 1,
maxAboveGround: 2,
minBelowGround: 1,
maxBelowGround: 2,
separationRadius: 2,
};
}
if (mapSize <= 24) {
return {
minClusters: 2,
maxClusters: 4,
maxCellsPerCluster: 3,
minAboveGround: 1,
maxAboveGround: 3,
minBelowGround: 1,
maxBelowGround: 2,
separationRadius: 3,
};
}
return {
minClusters: 3,
maxClusters: 5,
maxCellsPerCluster: 4,
minAboveGround: 2,
maxAboveGround: 3,
minBelowGround: 1,
maxBelowGround: 3,
separationRadius: 3,
};
};
export const createMountainRockContext = (
mapSize: number,
rand: () => number
): MountainRockContext => {
const field = createEmptyRockField(mapSize);
generateRockClusters({
mapSize,
rand,
field,
profileOverride: getMountainProfileOverride(mapSize),
});
return field;
};

View File

@@ -91,6 +91,7 @@ export interface GobiWeatheringResult {
*/
export interface GobiWeatheringPreprocessParams {
desertContext: DesertContext; // 沙漠上下文
terrainHeightMap: number[][]; // 地形高度图(每个逻辑方块的基础高度)
mapSize: number; // 地图大小(逻辑方块数)
MICRO_SCALE: number; // 微观缩放比例
gobiMaxIterations: number; // 戈壁最大风蚀层数,默认 3
@@ -111,6 +112,7 @@ export interface GobiWeatheringPreprocessParams {
export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams): GobiWeatheringResult {
const {
desertContext,
terrainHeightMap,
mapSize,
MICRO_SCALE,
gobiMaxIterations,
@@ -126,18 +128,25 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
// 预估最大世界Y坐标关键修复基于真实的 worldY 计算)
let maxWorldY = MIN_WORLD_Y;
let minWorldY = MIN_WORLD_Y;
for (let lx = 0; lx < mapSize; lx++) {
for (let lz = 0; lz < mapSize; lz++) {
const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE;
const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE;
const logicHeight = 2; // 沙漠场景的基础高度
// 计算该位置的最大可能 worldY表面高度
const surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight;
const stoneMicroDepth = (desertContext.stoneDepth?.[lx]?.[lz] ?? 0) * MICRO_SCALE;
const logicHeight = terrainHeightMap[lx][lz]; // 使用真实地形高度
// 计算该位置的最大和最小可能 worldY
const groundLevelY = logicHeight * MICRO_SCALE + gobiMicroHeight;
const surfaceY = groundLevelY + stoneMicroHeight; // 最高点(石块顶部)
const bottomY = groundLevelY - stoneMicroDepth; // 最低点(石块底部)
maxWorldY = Math.max(maxWorldY, surfaceY);
minWorldY = Math.min(minWorldY, bottomY);
}
}
// 基于 maxWorldY 计算网格高度(相对于 MIN_WORLD_Y
// 基于 maxWorldY 和 minWorldY 计算网格高度
const gridHeight = Math.ceil(maxWorldY - MIN_WORLD_Y + MICRO_SCALE * 2);
// 创建3D网格包含戈壁和巨石体素
@@ -168,10 +177,8 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
// 只处理有戈壁的区域
if (gobiMicroHeight === 0) continue;
// 计算这个逻辑方块的基础高度(需要复制 terrain.ts 的逻辑
// 注意:这里假设基础地形高度是固定的(沙漠场景通常是平坦的)
// 对于沙漠场景logicHeight 通常是固定值
const logicHeight = 2; // 沙漠场景的基础高度(从 terrain.ts 看出)
// 使用预计算的地形高度不再硬编码为2
const logicHeight = terrainHeightMap[lx][lz];
// 遍历该逻辑方块对应的所有微观方块
for (let mx = 0; mx < MICRO_SCALE; mx++) {
@@ -212,15 +219,17 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
}
// 第一步(续):填充巨石体素到网格中
let totalStoneVoxelsFilled = 0;
for (let lx = 0; lx < mapSize; lx++) {
for (let lz = 0; lz < mapSize; lz++) {
const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE;
const stoneMicroDepth = (desertContext.stoneDepth?.[lx]?.[lz] ?? 0) * MICRO_SCALE;
// 只处理有巨石的区域
if (stoneMicroHeight === 0) continue;
if (stoneMicroHeight === 0 && stoneMicroDepth === 0) continue;
// 计算基础高度
const logicHeight = 2; // 沙漠场景的基础高度
// 使用预计算的地形高度不再硬编码为2
const logicHeight = terrainHeightMap[lx][lz];
// 遍历该逻辑方块对应的所有微观方块
for (let mx = 0; mx < MICRO_SCALE; mx++) {
@@ -231,23 +240,26 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
// 获取当前位置的戈壁高度可能为0
const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE;
// 计算这个微观位置的 surfaceY
let surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight;
// 计算地表位置groundLevelY
const groundLevelY = logicHeight * MICRO_SCALE + gobiMicroHeight;
// 遍历高度,填充巨石体素
for (let h = 0; h < stoneMicroHeight; h++) {
// 计算这个体素的真实世界Y坐标
// 修复:巨石位于最顶层 (surfaceY),向下延伸
// h 从 0 (底部) 到 stoneMicroHeight-1 (顶部)
// worldY 范围应为 [surfaceY - stoneMicroHeight + 1, surfaceY]
const worldY = surfaceY - stoneMicroHeight + 1 + h;
// 石块范围:从地下延伸到地上
// 地上部分groundLevelY 到 groundLevelY + stoneMicroHeight
// 地下部分groundLevelY - stoneMicroDepth 到 groundLevelY
const stoneTopY = groundLevelY + stoneMicroHeight;
const stoneBottomY = groundLevelY - stoneMicroDepth;
// 填充整个石块范围(地下+地上)
let filledThisColumn = 0;
for (let worldY = stoneBottomY; worldY <= stoneTopY; worldY++) {
// 转换为网格坐标(相对于 MIN_WORLD_Y
const gridY = worldY - MIN_WORLD_Y;
if (inBounds(ix, gridY, iz)) {
const idx = getIdx(ix, gridY, iz);
voxelGrid[idx] = 2; // 标记为巨石体素
totalStoneVoxelsFilled++;
filledThisColumn++;
}
}
}
@@ -441,6 +453,7 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
// 第四步:收集被移除的体素(包括戈壁和巨石)
const removedVoxels = new Set<string>();
let remainingStoneVoxels = 0;
for (let ix = 0; ix < microMapSize; ix++) {
for (let gridY = 0; gridY < gridHeight; gridY++) {
@@ -454,6 +467,11 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
const worldY = MIN_WORLD_Y + gridY;
removedVoxels.add(`${ix}|${worldY}|${iz}`);
}
// 统计剩余的石块体素
if (currentGrid[idx] === 2) {
remainingStoneVoxels++;
}
}
}
}

View File

@@ -0,0 +1,198 @@
/**
* 通用巨石簇生成工具
* - 根据地图尺寸推导默认分布策略
* - 生成跨越地表的石块(部分在地上,部分在地下)
* - stoneHeight: 地表以上的逻辑方块层数13
* - stoneDepth: 地表以下的逻辑方块层数13
*/
export interface RockFieldContext {
stoneHeight: number[][]; // 露出地面的方块层数
stoneDepth: number[][]; // 埋在地下的方块层数
stoneVariant: number[][];
}
interface RockClusterProfile {
minClusters: number;
maxClusters: number;
maxCellsPerCluster: number;
minAboveGround: number; // 地上最少层数
maxAboveGround: number; // 地上最多层数
minBelowGround: number; // 地下最少层数
maxBelowGround: number; // 地下最多层数
separationRadius: number;
}
export interface RockClusterGenerationParams {
mapSize: number;
rand: () => number;
field: RockFieldContext;
avoidMaps?: Array<number[][]>;
profileOverride?: Partial<RockClusterProfile>;
maxAttempts?: number;
}
const createZeroMatrix = (size: number): number[][] =>
Array.from({ length: size }, () => Array(size).fill(0));
export const createEmptyRockField = (mapSize: number): RockFieldContext => ({
stoneHeight: createZeroMatrix(mapSize),
stoneDepth: createZeroMatrix(mapSize),
stoneVariant: createZeroMatrix(mapSize),
});
const deriveProfile = (mapSize: number): RockClusterProfile => {
if (mapSize <= 16) {
return {
minClusters: 1,
maxClusters: 2,
maxCellsPerCluster: 2,
minAboveGround: 1,
maxAboveGround: 2,
minBelowGround: 1,
maxBelowGround: 2,
separationRadius: 2,
};
}
if (mapSize <= 24) {
return {
minClusters: 2,
maxClusters: 4,
maxCellsPerCluster: 3,
minAboveGround: 1,
maxAboveGround: 3,
minBelowGround: 1,
maxBelowGround: 2,
separationRadius: 3,
};
}
return {
minClusters: 3,
maxClusters: 5,
maxCellsPerCluster: 4,
minAboveGround: 1,
maxAboveGround: 3,
minBelowGround: 1,
maxBelowGround: 3,
separationRadius: 3,
};
};
const isBlockedByMap = (maps: Array<number[][]>, x: number, z: number): boolean =>
maps.some(map => (map[x]?.[z] ?? 0) > 0);
const hasNearbyStone = (
field: RockFieldContext,
x: number,
z: number,
radius: number,
mapSize: number
): boolean => {
for (let dx = -radius; dx <= radius; dx++) {
for (let dz = -radius; dz <= radius; dz++) {
const nx = x + dx;
const nz = z + dz;
if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) {
if ((field.stoneHeight[nx]?.[nz] ?? 0) > 0) {
return true;
}
}
}
}
return false;
};
const buildCluster = (
startX: number,
startZ: number,
maxCells: number,
mapSize: number,
rand: () => number
) => {
const cells: Array<{ x: number; z: number }> = [{ x: startX, z: startZ }];
let safety = 0;
while (cells.length < maxCells && safety++ < 10) {
const seed = cells[Math.floor(rand() * cells.length)];
const dir = Math.floor(rand() * 4);
const dx = dir === 0 ? 1 : dir === 1 ? -1 : 0;
const dz = dir === 2 ? 1 : dir === 3 ? -1 : 0;
const nx = seed.x + dx;
const nz = seed.z + dz;
if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) {
if (!cells.some(c => c.x === nx && c.z === nz)) {
cells.push({ x: nx, z: nz });
}
}
}
return cells;
};
export const generateRockClusters = (params: RockClusterGenerationParams): number => {
const { mapSize, rand, field, avoidMaps = [], profileOverride, maxAttempts } = params;
const profile: RockClusterProfile = { ...deriveProfile(mapSize), ...profileOverride };
const targetClusters = profile.minClusters + Math.floor(
rand() * (profile.maxClusters - profile.minClusters + 1)
);
const attemptLimit = maxAttempts ?? Math.max(25, targetClusters * 40);
console.log(`[RockGen] Target clusters: ${targetClusters}, Profile:`, profile);
let placed = 0;
let attempts = 0;
while (placed < targetClusters && attempts < attemptLimit) {
attempts++;
const startX = Math.floor(rand() * mapSize);
const startZ = Math.floor(rand() * mapSize);
if (isBlockedByMap(avoidMaps, startX, startZ)) continue;
if (field.stoneHeight[startX][startZ] > 0) continue;
const clusterCells = buildCluster(
startX,
startZ,
1 + Math.floor(rand() * profile.maxCellsPerCluster),
mapSize,
rand
);
let valid = true;
for (const cell of clusterCells) {
if (
hasNearbyStone(field, cell.x, cell.z, profile.separationRadius, mapSize) ||
isBlockedByMap(avoidMaps, cell.x, cell.z)
) {
valid = false;
break;
}
}
if (!valid) continue;
// 地上部分13层
const aboveGround = profile.minAboveGround +
Math.floor(rand() * (profile.maxAboveGround - profile.minAboveGround + 1));
// 地下部分13层
const belowGround = profile.minBelowGround +
Math.floor(rand() * (profile.maxBelowGround - profile.minBelowGround + 1));
const variant = Math.floor(rand() * 2);
console.log(`[RockGen] Placed cluster ${placed + 1} at (${startX},${startZ}): cells=${clusterCells.length}, above=${aboveGround}, below=${belowGround}`);
for (const cell of clusterCells) {
field.stoneHeight[cell.x][cell.z] = aboveGround;
field.stoneDepth[cell.x][cell.z] = belowGround;
field.stoneVariant[cell.x][cell.z] = variant;
}
placed++;
}
console.log(`[RockGen] Finished: ${placed}/${targetClusters} clusters placed after ${attempts} attempts`);
return placed;
};

View File

@@ -19,6 +19,7 @@ import {
// 戈壁特征地形
import {
type DesertContext,
createDesertContext,
getGobiLayerType,
getStoneLayerType,
@@ -26,6 +27,8 @@ import {
// STONE_PLATFORM_STRUCTS,
// MESA_PLATFORM_STRUCTS,
} from './desertFeatures';
import { createMountainRockContext } from './mountainFeatures';
import type { RockFieldContext } from './rockFeatures';
// 植被生成 (已移除旧系统)
// import {
@@ -95,6 +98,7 @@ export interface VoxelData {
iz: number;
heightScale: number;
isHighRes?: boolean; // 标记是否为高分辨率(16x16)体素
isRock?: boolean; // 标记是否为石块体素(不可剔除)
}
export const MAP_SIZES = {
@@ -115,6 +119,22 @@ export interface TerrainGenerationOptions {
}
// MagicaVoxel Vibrant Palette (More Saturation, Less Grey)
const createZeroMatrix = (size: number): number[][] =>
Array.from({ length: size }, () => Array(size).fill(0));
const buildRockOnlyContext = (
mapSize: number,
rockField: RockFieldContext
): DesertContext => ({
gobiHeight: createZeroMatrix(mapSize),
gobiVariant: createZeroMatrix(mapSize),
gobiMinHeight: createZeroMatrix(mapSize),
stoneHeight: rockField.stoneHeight,
stoneDepth: rockField.stoneDepth,
stoneVariant: rockField.stoneVariant,
streamDepthMap: new Map<string, { depth: number; waterHeight: number; isCenter: boolean }>(),
});
export const generateTerrain = async (
mapSize: number,
seed: string = 'default',
@@ -161,10 +181,19 @@ export const generateTerrain = async (
// const forestClusterNoise = createNoise2D(() => Math.random());
const desertContext = sceneConfig?.name === 'desert'
const isDesertScene = sceneConfig?.name === 'desert';
const desertContext = isDesertScene
? createDesertContext(mapSize, seededRandom)
: null;
const isDesertScene = sceneConfig?.name === 'desert';
const mountainRockContext = sceneConfig?.name === 'mountain'
? createMountainRockContext(mapSize, seededRandom)
: null;
// 获取石块上下文:沙漠用完整的 desertContext山地用 mountainRockContext
const rockContext: RockFieldContext | null = isDesertScene
? desertContext
: mountainRockContext;
// 地形类型决策:如果有场景配置,使用场景配置;否则根据 seed 生成
let terrainType: string;
@@ -200,22 +229,113 @@ export const generateTerrain = async (
// 开始生成:基础地形
reportProgress('basic', 0, '初始化地形生成');
// ===== 预计算地形高度图(用于风化预处理)=====
const terrainHeightMap: number[][] = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
if (rockContext) {
reportProgress('basic', 2, '计算地形高度图');
for (let lx = 0; lx < mapSize; lx++) {
for (let lz = 0; lz < mapSize; lz++) {
let n, logicHeight;
// 使用场景配置生成地形
if (sceneConfig) {
if (terrainType === 'desert') {
logicHeight = Math.max(1, Math.floor(heightBase));
} else {
const scale = heightRoughness;
n = noise2D(lx * scale, lz * scale);
n += 0.5 * noise2D(lx * scale * 2, lz * scale * 2);
n /= 1.5;
logicHeight = Math.floor((n + 1) * sceneConfig.heightAmplitude * 0.5 + heightBase);
if (logicHeight < 0) logicHeight = 0;
}
} else {
// 旧逻辑(向后兼容)
if (terrainType === 'mountain') {
const scale = 0.06;
n = noise2D(lx * scale, lz * scale);
n += 0.7 * noise2D(lx * scale * 2, lz * scale * 2);
n /= 1.7;
logicHeight = Math.floor((n + 1) * 5);
if (logicHeight < 2) logicHeight = 2;
} else if (terrainType === 'desert') {
const scale = 0.1;
n = noise2D(lx * scale + 300, lz * scale + 300);
n += 0.3 * noise2D(lx * scale * 3, lz * scale * 3);
n /= 1.3;
logicHeight = Math.floor((n + 1) * 2.5);
if (logicHeight < 2) logicHeight = 2;
} else if (terrainType === 'canyon') {
const scale = 0.08;
n = noise2D(lx * scale + 600, lz * scale + 600);
n += 0.6 * noise2D(lx * scale * 2, lz * scale * 2);
n /= 1.6;
const distToRiver = Math.abs(lx - mapSize / 2);
const riverWidth = mapSize * 0.08;
if (distToRiver < riverWidth) {
const depthFactor = 1 - (distToRiver / riverWidth);
logicHeight = Math.max(0, Math.floor(2 - depthFactor * 2));
} else {
const sideEffect = Math.sin(lz * 0.15) * 0.3;
logicHeight = Math.floor((n + sideEffect + 1) * 4.5);
}
} else {
const scale = 0.08;
n = noise2D(lx * scale + 100, lz * scale + 100);
n += 0.5 * noise2D(lx * scale * 2 + 200, lz * scale * 2 + 200);
n /= 1.5;
logicHeight = Math.floor((n + 1) * 3.2);
if (logicHeight < 1) logicHeight = 1;
const lakeCenterX = mapSize * 0.35;
const lakeCenterZ = mapSize * 0.4;
const distToLake = Math.sqrt(
Math.pow(lx - lakeCenterX, 2) + Math.pow(lz - lakeCenterZ, 2)
);
const lakeRadius = mapSize * 0.12;
if (distToLake < lakeRadius) {
const depthFactor = 1 - (distToLake / lakeRadius);
logicHeight = Math.max(0, Math.floor(1 - depthFactor * 0.5));
}
}
}
terrainHeightMap[lx][lz] = logicHeight;
}
}
}
// ===== 戈壁和巨石风化预处理 =====
// 在沙漠场景中,预先计算风化效果
let gobiWeatheringResult: GobiWeatheringResult | null = null;
if (isDesertScene && desertContext) {
reportProgress('basic', 5, '预处理戈壁和巨石风化效果');
// 在需要巨石的场景中,预先计算风化效果
let rockWeatheringResult: GobiWeatheringResult | null = null;
if (rockContext) {
const contextForWeathering = desertContext ?? buildRockOnlyContext(mapSize, rockContext);
reportProgress(
'basic',
5,
isDesertScene ? '预处理戈壁和巨石风化效果' : '预处理巨石风化效果'
);
await new Promise(resolve => setTimeout(resolve, 0));
// 从options中获取参数如果没有则使用默认值
const gobiMaxIterations = options?.weatheringMaxIterations ?? 3;
const gobiHeightBias = options?.weatheringHeightBias ?? 0.5;
const stoneMaxIterations = options?.stoneWeatheringMaxIterations ?? 3;
const stoneHeightBias = options?.stoneWeatheringHeightBias ?? 0.5;
const stoneMinExposedFaces = options?.stoneWeatheringMinExposedFaces ?? 2;
// 从options中获取参数如果没有则使用默认值
const gobiMaxIterations = options?.weatheringMaxIterations ?? 3;
const gobiHeightBias = options?.weatheringHeightBias ?? 0.5;
// 山地场景:减少风化迭代次数和降低高度保护阈值,因为山地整体高度更高
const stoneMaxIterations = options?.stoneWeatheringMaxIterations ?? (isDesertScene ? 3 : 2);
const stoneHeightBias = options?.stoneWeatheringHeightBias ?? (isDesertScene ? 0.8 : 0.8);
const stoneMinExposedFaces = options?.stoneWeatheringMinExposedFaces ?? (isDesertScene ? 2 : 2);
gobiWeatheringResult = preprocessGobiWeathering({
desertContext,
rockWeatheringResult = preprocessGobiWeathering({
desertContext: contextForWeathering,
terrainHeightMap,
mapSize,
MICRO_SCALE,
gobiMaxIterations,
@@ -226,7 +346,11 @@ export const generateTerrain = async (
MIN_WORLD_Y,
});
reportProgress('basic', 10, `风化预处理完成,移除 ${gobiWeatheringResult.removedVoxels.size} 个体素`);
reportProgress(
'basic',
10,
`风化预处理完成,移除 ${rockWeatheringResult.removedVoxels.size} 个体素`
);
await new Promise(resolve => setTimeout(resolve, 0));
}
@@ -281,9 +405,17 @@ export const generateTerrain = async (
const gobiLogicHeight = isDesertScene && desertContext ? desertContext.gobiHeight[lx][lz] : 0;
const gobiMicroHeight = gobiLogicHeight * MICRO_SCALE;
const gobiVariantIdx = isDesertScene && desertContext ? desertContext.gobiVariant[lx][lz] : 0;
const stoneLogicHeight = isDesertScene && desertContext ? desertContext.stoneHeight[lx][lz] : 0;
// 巨石信息:地上部分和地下部分
const stoneLogicHeight = rockContext ? rockContext.stoneHeight[lx][lz] : 0;
const stoneLogicDepth = rockContext ? rockContext.stoneDepth[lx][lz] : 0;
const stoneMicroHeight = stoneLogicHeight * MICRO_SCALE;
const stoneVariantIdx = isDesertScene && desertContext ? desertContext.stoneVariant[lx][lz] : 0;
const stoneMicroDepth = stoneLogicDepth * MICRO_SCALE;
const stoneVariantIdx = rockContext ? rockContext.stoneVariant[lx][lz] : 0;
// 调试计数器:统计石块体素渲染情况(生产环境可移除)
let stoneVoxelsRendered = 0;
let stoneVoxelsSkippedByWeathering = 0;
// Pre-fetch neighbor heights for Gobi smoothing (not used in current erosion algorithm)
// Kept for potential future use
@@ -457,28 +589,43 @@ export const generateTerrain = async (
// Use lower frequency for broader, gentler slopes
const detailVal = detailNoise(ix * 0.08, iz * 0.08);
// 检测当前位置是否有石块(石块区域不应用 smoothing
const hasRockColumn = stoneMicroHeight > 0 || stoneMicroDepth > 0;
// 计算地表高度(基础地形 + 戈壁)
let worldY = logicHeight * MICRO_SCALE;
if (isDesertScene) {
worldY += stoneMicroHeight + gobiMicroHeight;
worldY += gobiMicroHeight;
}
// 记录原始地表位置(用于石块范围判断)
const groundLevelY = worldY;
// SMOOTHING LOGIC:
// Only apply significant noise to Stone/Snow
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
if (Math.abs(detailVal) > 0.85) {
worldY += Math.sign(detailVal);
}
} else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') {
worldY += Math.floor(detailVal * 2);
} else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') {
worldY = Math.floor(1.3 * MICRO_SCALE);
} else if (logicType === 'ash' || logicType === 'mud') {
if (Math.abs(detailVal) > 0.75) {
worldY += Math.sign(detailVal);
// 只对非石块区域应用 smoothing保持石块稳固
if (!hasRockColumn) {
// Only apply significant noise to Stone/Snow
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
if (Math.abs(detailVal) > 0.85) {
worldY += Math.sign(detailVal);
}
} else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') {
worldY += Math.floor(detailVal * 2);
} else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') {
worldY = Math.floor(1.3 * MICRO_SCALE);
} else if (logicType === 'ash' || logicType === 'mud') {
if (Math.abs(detailVal) > 0.75) {
worldY += Math.sign(detailVal);
}
}
}
// 如果有巨石,地表被石块地上部分抬高
if (stoneMicroHeight > 0) {
worldY += stoneMicroHeight;
}
// 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation
let sandMoundHeight = 0;
@@ -559,9 +706,16 @@ export const generateTerrain = async (
let type: VoxelType = logicType;
const depth = surfaceY - y;
// 沙漠场景:优先处理戈壁和巨石,确保它们保持纯色
const isInGobiRange = gobiMicroHeight > 0 && depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight;
const isInStoneRange = stoneMicroHeight > 0 && depth < stoneMicroHeight;
// 判断当前体素是否在石块范围内
// 石块范围:从 groundLevelY - stoneMicroDepth 到 groundLevelY + stoneMicroHeight
const stoneTopY = groundLevelY + stoneMicroHeight;
const stoneBottomY = groundLevelY - stoneMicroDepth;
const isInStoneRange = (stoneMicroHeight > 0 || stoneMicroDepth > 0) &&
y > stoneBottomY && y <= stoneTopY;
// 戈壁场景:判断是否在戈壁范围
const isInGobiRange = gobiMicroHeight > 0 && !isInStoneRange &&
depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight;
// 应用溪流蚀刻效果
if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
@@ -575,21 +729,48 @@ export const generateTerrain = async (
}
} else if (isInStoneRange) {
// 巨石风化检查:如果该体素被风化移除,跳过生成
if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) {
if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) {
stoneVoxelsSkippedByWeathering++;
continue; // 跳过生成,风化效果
}
// 3. 确定基础颜色
type = getStoneLayerType(depth, stoneVariantIdx);
stoneVoxelsRendered++;
// 计算石块内部的深度(从石块顶部算起)
const stoneDepthFromTop = stoneTopY - y;
// 确定基础颜色
type = getStoneLayerType(stoneDepthFromTop, stoneVariantIdx);
// 4. 应用巨石颜色渗透效果
// 应用巨石颜色渗透效果
type = applyStoneColorBlending({
currentType: type,
depth,
depth: stoneDepthFromTop,
stoneVariantIdx,
ix,
iz,
y
});
// 标记为石块体素(不可剔除)
const color = varyColor(type, detailVal + y * 0.02, 0);
const rockVoxel: VoxelData = {
x: ix * VOXEL_SIZE,
y: y * VOXEL_SIZE,
z: iz * VOXEL_SIZE,
type,
color,
ix,
iy: y,
iz,
heightScale: 1,
isRock: true
};
voxels.push(rockVoxel);
occupancy.add(keyOf(ix, y, iz));
if (y === surfaceY) {
topColumns.set(`${ix}|${iz}`, { index: voxels.length - 1, baseType: 'stone' });
}
continue; // 跳过后续的 addVoxel 调用
} else if (isInGobiRange) {
const relativeDepth = depth - stoneMicroHeight;
const microDepthFromBottom = gobiMicroHeight - relativeDepth - 1;
@@ -604,7 +785,7 @@ export const generateTerrain = async (
}
// 2. 风化检查:如果该体素被风化移除,跳过生成
if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) {
if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) {
continue; // 跳过生成,风化效果
}
@@ -831,6 +1012,9 @@ export const generateTerrain = async (
// Always keep bedrock bottom layer to avoid gaps
if (voxel.iy <= MIN_WORLD_Y + 1) return true;
// 保留石块体素(不可剔除)
if (voxel.isRock) return true;
for (const [dx, dy, dz] of neighborOffsets) {
let neighborKey = keyOf(voxel.ix + dx, voxel.iy + dy, voxel.iz + dz);
// 如果是树木体素检查树木专用的key空间

View File

@@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
esbuild: {
// 保留 React 的调试信息
keepNames: true,
},
})