feat: update map terrain and post effects
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "WebCity",
|
"name": "VoxelGame",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -5,6 +5,34 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>voxel-tactics-horizon</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { VoxelType } from './voxelStyles';
|
import type { VoxelType } from './voxelStyles';
|
||||||
|
import { generateRockClusters } from './rockFeatures';
|
||||||
|
|
||||||
// MICRO_SCALE 常量:每个逻辑格子包含的微型体素数量
|
// MICRO_SCALE 常量:每个逻辑格子包含的微型体素数量
|
||||||
// const MICRO_SCALE = 8; // Unused
|
// const MICRO_SCALE = 8; // Unused
|
||||||
@@ -22,6 +23,7 @@ export 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; isCenter: boolean }>; // Added isCenter
|
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 gobiVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
|
||||||
const gobiMinHeight = 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 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));
|
const stoneVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0));
|
||||||
|
|
||||||
layoutStrategicDesert(mapSize, gobiHeight, gobiVariant, gobiMinHeight, rand);
|
layoutStrategicDesert(mapSize, gobiHeight, gobiVariant, gobiMinHeight, rand);
|
||||||
|
|
||||||
// Adjusted density: Ensure presence on 24/32 maps, occasional on 16
|
// 生成巨石簇(跨越地表的石块)
|
||||||
// 32x32 (1024) / 200 = ~5 stones
|
generateRockClusters({
|
||||||
// 24x24 (576) / 200 = ~2-3 stones
|
mapSize,
|
||||||
// 16x16 (256) / 200 = ~1.2 -> handled specifically below
|
rand,
|
||||||
let targetStones = Math.floor((mapSize * mapSize) / 200);
|
field: { stoneHeight, stoneDepth, stoneVariant },
|
||||||
|
avoidMaps: [gobiHeight],
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamDepthMap = generateDesertStreamMap(mapSize, rand, gobiHeight, stoneHeight);
|
const streamDepthMap = generateDesertStreamMap(mapSize, rand, gobiHeight, stoneHeight);
|
||||||
return {
|
return {
|
||||||
@@ -568,6 +499,7 @@ export const createDesertContext = (
|
|||||||
gobiVariant,
|
gobiVariant,
|
||||||
gobiMinHeight,
|
gobiMinHeight,
|
||||||
stoneHeight,
|
stoneHeight,
|
||||||
|
stoneDepth,
|
||||||
stoneVariant,
|
stoneVariant,
|
||||||
streamDepthMap,
|
streamDepthMap,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 山地特征系统:生成跨越地表的巨石
|
||||||
|
* - 石块部分在地上(1~3层),部分在地下(1~3层)
|
||||||
|
* - 直接复用石块风化与颜色过渡
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -91,6 +91,7 @@ export interface GobiWeatheringResult {
|
|||||||
*/
|
*/
|
||||||
export interface GobiWeatheringPreprocessParams {
|
export interface GobiWeatheringPreprocessParams {
|
||||||
desertContext: DesertContext; // 沙漠上下文
|
desertContext: DesertContext; // 沙漠上下文
|
||||||
|
terrainHeightMap: number[][]; // 地形高度图(每个逻辑方块的基础高度)
|
||||||
mapSize: number; // 地图大小(逻辑方块数)
|
mapSize: number; // 地图大小(逻辑方块数)
|
||||||
MICRO_SCALE: number; // 微观缩放比例
|
MICRO_SCALE: number; // 微观缩放比例
|
||||||
gobiMaxIterations: number; // 戈壁最大风蚀层数,默认 3
|
gobiMaxIterations: number; // 戈壁最大风蚀层数,默认 3
|
||||||
@@ -111,6 +112,7 @@ export interface GobiWeatheringPreprocessParams {
|
|||||||
export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams): GobiWeatheringResult {
|
export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams): GobiWeatheringResult {
|
||||||
const {
|
const {
|
||||||
desertContext,
|
desertContext,
|
||||||
|
terrainHeightMap,
|
||||||
mapSize,
|
mapSize,
|
||||||
MICRO_SCALE,
|
MICRO_SCALE,
|
||||||
gobiMaxIterations,
|
gobiMaxIterations,
|
||||||
@@ -126,18 +128,25 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
|
|||||||
|
|
||||||
// 预估最大世界Y坐标(关键修复:基于真实的 worldY 计算)
|
// 预估最大世界Y坐标(关键修复:基于真实的 worldY 计算)
|
||||||
let maxWorldY = MIN_WORLD_Y;
|
let maxWorldY = MIN_WORLD_Y;
|
||||||
|
let minWorldY = MIN_WORLD_Y;
|
||||||
for (let lx = 0; lx < mapSize; lx++) {
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
for (let lz = 0; lz < mapSize; lz++) {
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE;
|
const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE;
|
||||||
const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE;
|
const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE;
|
||||||
const logicHeight = 2; // 沙漠场景的基础高度
|
const stoneMicroDepth = (desertContext.stoneDepth?.[lx]?.[lz] ?? 0) * MICRO_SCALE;
|
||||||
// 计算该位置的最大可能 worldY(表面高度)
|
const logicHeight = terrainHeightMap[lx][lz]; // 使用真实地形高度
|
||||||
const surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight;
|
|
||||||
|
// 计算该位置的最大和最小可能 worldY
|
||||||
|
const groundLevelY = logicHeight * MICRO_SCALE + gobiMicroHeight;
|
||||||
|
const surfaceY = groundLevelY + stoneMicroHeight; // 最高点(石块顶部)
|
||||||
|
const bottomY = groundLevelY - stoneMicroDepth; // 最低点(石块底部)
|
||||||
|
|
||||||
maxWorldY = Math.max(maxWorldY, surfaceY);
|
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);
|
const gridHeight = Math.ceil(maxWorldY - MIN_WORLD_Y + MICRO_SCALE * 2);
|
||||||
|
|
||||||
// 创建3D网格:包含戈壁和巨石体素
|
// 创建3D网格:包含戈壁和巨石体素
|
||||||
@@ -168,10 +177,8 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
|
|||||||
// 只处理有戈壁的区域
|
// 只处理有戈壁的区域
|
||||||
if (gobiMicroHeight === 0) continue;
|
if (gobiMicroHeight === 0) continue;
|
||||||
|
|
||||||
// 计算这个逻辑方块的基础高度(需要复制 terrain.ts 的逻辑)
|
// 使用预计算的地形高度(不再硬编码为2)
|
||||||
// 注意:这里假设基础地形高度是固定的(沙漠场景通常是平坦的)
|
const logicHeight = terrainHeightMap[lx][lz];
|
||||||
// 对于沙漠场景,logicHeight 通常是固定值
|
|
||||||
const logicHeight = 2; // 沙漠场景的基础高度(从 terrain.ts 看出)
|
|
||||||
|
|
||||||
// 遍历该逻辑方块对应的所有微观方块
|
// 遍历该逻辑方块对应的所有微观方块
|
||||||
for (let mx = 0; mx < MICRO_SCALE; mx++) {
|
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 lx = 0; lx < mapSize; lx++) {
|
||||||
for (let lz = 0; lz < mapSize; lz++) {
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE;
|
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;
|
||||||
|
|
||||||
// 计算基础高度
|
// 使用预计算的地形高度(不再硬编码为2)
|
||||||
const logicHeight = 2; // 沙漠场景的基础高度
|
const logicHeight = terrainHeightMap[lx][lz];
|
||||||
|
|
||||||
// 遍历该逻辑方块对应的所有微观方块
|
// 遍历该逻辑方块对应的所有微观方块
|
||||||
for (let mx = 0; mx < MICRO_SCALE; mx++) {
|
for (let mx = 0; mx < MICRO_SCALE; mx++) {
|
||||||
@@ -231,23 +240,26 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
|
|||||||
// 获取当前位置的戈壁高度(可能为0)
|
// 获取当前位置的戈壁高度(可能为0)
|
||||||
const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE;
|
const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE;
|
||||||
|
|
||||||
// 计算这个微观位置的 surfaceY
|
// 计算地表位置(groundLevelY)
|
||||||
let surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight;
|
const groundLevelY = logicHeight * MICRO_SCALE + gobiMicroHeight;
|
||||||
|
|
||||||
// 遍历高度,填充巨石体素
|
// 石块范围:从地下延伸到地上
|
||||||
for (let h = 0; h < stoneMicroHeight; h++) {
|
// 地上部分:groundLevelY 到 groundLevelY + stoneMicroHeight
|
||||||
// 计算这个体素的真实世界Y坐标
|
// 地下部分:groundLevelY - stoneMicroDepth 到 groundLevelY
|
||||||
// 修复:巨石位于最顶层 (surfaceY),向下延伸
|
const stoneTopY = groundLevelY + stoneMicroHeight;
|
||||||
// h 从 0 (底部) 到 stoneMicroHeight-1 (顶部)
|
const stoneBottomY = groundLevelY - stoneMicroDepth;
|
||||||
// worldY 范围应为 [surfaceY - stoneMicroHeight + 1, surfaceY]
|
|
||||||
const worldY = surfaceY - stoneMicroHeight + 1 + h;
|
// 填充整个石块范围(地下+地上)
|
||||||
|
let filledThisColumn = 0;
|
||||||
|
for (let worldY = stoneBottomY; worldY <= stoneTopY; worldY++) {
|
||||||
// 转换为网格坐标(相对于 MIN_WORLD_Y)
|
// 转换为网格坐标(相对于 MIN_WORLD_Y)
|
||||||
const gridY = worldY - MIN_WORLD_Y;
|
const gridY = worldY - MIN_WORLD_Y;
|
||||||
|
|
||||||
if (inBounds(ix, gridY, iz)) {
|
if (inBounds(ix, gridY, iz)) {
|
||||||
const idx = getIdx(ix, gridY, iz);
|
const idx = getIdx(ix, gridY, iz);
|
||||||
voxelGrid[idx] = 2; // 标记为巨石体素
|
voxelGrid[idx] = 2; // 标记为巨石体素
|
||||||
|
totalStoneVoxelsFilled++;
|
||||||
|
filledThisColumn++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,6 +453,7 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
|
|||||||
|
|
||||||
// 第四步:收集被移除的体素(包括戈壁和巨石)
|
// 第四步:收集被移除的体素(包括戈壁和巨石)
|
||||||
const removedVoxels = new Set<string>();
|
const removedVoxels = new Set<string>();
|
||||||
|
let remainingStoneVoxels = 0;
|
||||||
|
|
||||||
for (let ix = 0; ix < microMapSize; ix++) {
|
for (let ix = 0; ix < microMapSize; ix++) {
|
||||||
for (let gridY = 0; gridY < gridHeight; gridY++) {
|
for (let gridY = 0; gridY < gridHeight; gridY++) {
|
||||||
@@ -454,6 +467,11 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams)
|
|||||||
const worldY = MIN_WORLD_Y + gridY;
|
const worldY = MIN_WORLD_Y + gridY;
|
||||||
removedVoxels.add(`${ix}|${worldY}|${iz}`);
|
removedVoxels.add(`${ix}|${worldY}|${iz}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统计剩余的石块体素
|
||||||
|
if (currentGrid[idx] === 2) {
|
||||||
|
remainingStoneVoxels++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
198
voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts
Normal file
198
voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* 通用巨石簇生成工具
|
||||||
|
* - 根据地图尺寸推导默认分布策略
|
||||||
|
* - 生成跨越地表的石块(部分在地上,部分在地下)
|
||||||
|
* - stoneHeight: 地表以上的逻辑方块层数(1~3)
|
||||||
|
* - stoneDepth: 地表以下的逻辑方块层数(1~3)
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 地上部分:1~3层
|
||||||
|
const aboveGround = profile.minAboveGround +
|
||||||
|
Math.floor(rand() * (profile.maxAboveGround - profile.minAboveGround + 1));
|
||||||
|
|
||||||
|
// 地下部分:1~3层
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
|
|
||||||
// 戈壁特征地形
|
// 戈壁特征地形
|
||||||
import {
|
import {
|
||||||
|
type DesertContext,
|
||||||
createDesertContext,
|
createDesertContext,
|
||||||
getGobiLayerType,
|
getGobiLayerType,
|
||||||
getStoneLayerType,
|
getStoneLayerType,
|
||||||
@@ -26,6 +27,8 @@ import {
|
|||||||
// STONE_PLATFORM_STRUCTS,
|
// STONE_PLATFORM_STRUCTS,
|
||||||
// MESA_PLATFORM_STRUCTS,
|
// MESA_PLATFORM_STRUCTS,
|
||||||
} from './desertFeatures';
|
} from './desertFeatures';
|
||||||
|
import { createMountainRockContext } from './mountainFeatures';
|
||||||
|
import type { RockFieldContext } from './rockFeatures';
|
||||||
|
|
||||||
// 植被生成 (已移除旧系统)
|
// 植被生成 (已移除旧系统)
|
||||||
// import {
|
// import {
|
||||||
@@ -95,6 +98,7 @@ export interface VoxelData {
|
|||||||
iz: number;
|
iz: number;
|
||||||
heightScale: number;
|
heightScale: number;
|
||||||
isHighRes?: boolean; // 标记是否为高分辨率(16x16)体素
|
isHighRes?: boolean; // 标记是否为高分辨率(16x16)体素
|
||||||
|
isRock?: boolean; // 标记是否为石块体素(不可剔除)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MAP_SIZES = {
|
export const MAP_SIZES = {
|
||||||
@@ -115,6 +119,22 @@ export interface TerrainGenerationOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MagicaVoxel Vibrant Palette (More Saturation, Less Grey)
|
// 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 (
|
export const generateTerrain = async (
|
||||||
mapSize: number,
|
mapSize: number,
|
||||||
seed: string = 'default',
|
seed: string = 'default',
|
||||||
@@ -161,10 +181,19 @@ export const generateTerrain = async (
|
|||||||
// const forestClusterNoise = createNoise2D(() => Math.random());
|
// const forestClusterNoise = createNoise2D(() => Math.random());
|
||||||
|
|
||||||
|
|
||||||
const desertContext = sceneConfig?.name === 'desert'
|
const isDesertScene = sceneConfig?.name === 'desert';
|
||||||
|
|
||||||
|
const desertContext = isDesertScene
|
||||||
? createDesertContext(mapSize, seededRandom)
|
? createDesertContext(mapSize, seededRandom)
|
||||||
: null;
|
: 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 生成
|
// 地形类型决策:如果有场景配置,使用场景配置;否则根据 seed 生成
|
||||||
let terrainType: string;
|
let terrainType: string;
|
||||||
@@ -200,22 +229,113 @@ export const generateTerrain = async (
|
|||||||
// 开始生成:基础地形
|
// 开始生成:基础地形
|
||||||
reportProgress('basic', 0, '初始化地形生成');
|
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;
|
let rockWeatheringResult: GobiWeatheringResult | null = null;
|
||||||
if (isDesertScene && desertContext) {
|
if (rockContext) {
|
||||||
reportProgress('basic', 5, '预处理戈壁和巨石风化效果');
|
const contextForWeathering = desertContext ?? buildRockOnlyContext(mapSize, rockContext);
|
||||||
|
reportProgress(
|
||||||
|
'basic',
|
||||||
|
5,
|
||||||
|
isDesertScene ? '预处理戈壁和巨石风化效果' : '预处理巨石风化效果'
|
||||||
|
);
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
// 从options中获取参数,如果没有则使用默认值
|
// 从options中获取参数,如果没有则使用默认值
|
||||||
const gobiMaxIterations = options?.weatheringMaxIterations ?? 3;
|
const gobiMaxIterations = options?.weatheringMaxIterations ?? 3;
|
||||||
const gobiHeightBias = options?.weatheringHeightBias ?? 0.5;
|
const gobiHeightBias = options?.weatheringHeightBias ?? 0.5;
|
||||||
const stoneMaxIterations = options?.stoneWeatheringMaxIterations ?? 3;
|
// 山地场景:减少风化迭代次数和降低高度保护阈值,因为山地整体高度更高
|
||||||
const stoneHeightBias = options?.stoneWeatheringHeightBias ?? 0.5;
|
const stoneMaxIterations = options?.stoneWeatheringMaxIterations ?? (isDesertScene ? 3 : 2);
|
||||||
const stoneMinExposedFaces = options?.stoneWeatheringMinExposedFaces ?? 2;
|
const stoneHeightBias = options?.stoneWeatheringHeightBias ?? (isDesertScene ? 0.8 : 0.8);
|
||||||
|
const stoneMinExposedFaces = options?.stoneWeatheringMinExposedFaces ?? (isDesertScene ? 2 : 2);
|
||||||
|
|
||||||
gobiWeatheringResult = preprocessGobiWeathering({
|
rockWeatheringResult = preprocessGobiWeathering({
|
||||||
desertContext,
|
desertContext: contextForWeathering,
|
||||||
|
terrainHeightMap,
|
||||||
mapSize,
|
mapSize,
|
||||||
MICRO_SCALE,
|
MICRO_SCALE,
|
||||||
gobiMaxIterations,
|
gobiMaxIterations,
|
||||||
@@ -226,7 +346,11 @@ export const generateTerrain = async (
|
|||||||
MIN_WORLD_Y,
|
MIN_WORLD_Y,
|
||||||
});
|
});
|
||||||
|
|
||||||
reportProgress('basic', 10, `风化预处理完成,移除 ${gobiWeatheringResult.removedVoxels.size} 个体素`);
|
reportProgress(
|
||||||
|
'basic',
|
||||||
|
10,
|
||||||
|
`风化预处理完成,移除 ${rockWeatheringResult.removedVoxels.size} 个体素`
|
||||||
|
);
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
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 gobiLogicHeight = isDesertScene && desertContext ? desertContext.gobiHeight[lx][lz] : 0;
|
||||||
const gobiMicroHeight = gobiLogicHeight * MICRO_SCALE;
|
const gobiMicroHeight = gobiLogicHeight * MICRO_SCALE;
|
||||||
const gobiVariantIdx = isDesertScene && desertContext ? desertContext.gobiVariant[lx][lz] : 0;
|
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 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)
|
// Pre-fetch neighbor heights for Gobi smoothing (not used in current erosion algorithm)
|
||||||
// Kept for potential future use
|
// Kept for potential future use
|
||||||
@@ -457,27 +589,42 @@ export const generateTerrain = async (
|
|||||||
// Use lower frequency for broader, gentler slopes
|
// Use lower frequency for broader, gentler slopes
|
||||||
const detailVal = detailNoise(ix * 0.08, iz * 0.08);
|
const detailVal = detailNoise(ix * 0.08, iz * 0.08);
|
||||||
|
|
||||||
|
// 检测当前位置是否有石块(石块区域不应用 smoothing)
|
||||||
|
const hasRockColumn = stoneMicroHeight > 0 || stoneMicroDepth > 0;
|
||||||
|
|
||||||
|
// 计算地表高度(基础地形 + 戈壁)
|
||||||
let worldY = logicHeight * MICRO_SCALE;
|
let worldY = logicHeight * MICRO_SCALE;
|
||||||
if (isDesertScene) {
|
if (isDesertScene) {
|
||||||
worldY += stoneMicroHeight + gobiMicroHeight;
|
worldY += gobiMicroHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录原始地表位置(用于石块范围判断)
|
||||||
|
const groundLevelY = worldY;
|
||||||
|
|
||||||
// SMOOTHING LOGIC:
|
// SMOOTHING LOGIC:
|
||||||
// Only apply significant noise to Stone/Snow
|
// 只对非石块区域应用 smoothing,保持石块稳固
|
||||||
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
|
if (!hasRockColumn) {
|
||||||
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
|
// Only apply significant noise to Stone/Snow
|
||||||
if (Math.abs(detailVal) > 0.85) {
|
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
|
||||||
worldY += Math.sign(detailVal);
|
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
|
||||||
}
|
if (Math.abs(detailVal) > 0.85) {
|
||||||
} else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') {
|
worldY += Math.sign(detailVal);
|
||||||
worldY += Math.floor(detailVal * 2);
|
}
|
||||||
} else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') {
|
} else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') {
|
||||||
worldY = Math.floor(1.3 * MICRO_SCALE);
|
worldY += Math.floor(detailVal * 2);
|
||||||
} else if (logicType === 'ash' || logicType === 'mud') {
|
} else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') {
|
||||||
if (Math.abs(detailVal) > 0.75) {
|
worldY = Math.floor(1.3 * MICRO_SCALE);
|
||||||
worldY += Math.sign(detailVal);
|
} 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
|
// 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation
|
||||||
let sandMoundHeight = 0;
|
let sandMoundHeight = 0;
|
||||||
@@ -559,9 +706,16 @@ export const generateTerrain = async (
|
|||||||
let type: VoxelType = logicType;
|
let type: VoxelType = logicType;
|
||||||
const depth = surfaceY - y;
|
const depth = surfaceY - y;
|
||||||
|
|
||||||
// 沙漠场景:优先处理戈壁和巨石,确保它们保持纯色
|
// 判断当前体素是否在石块范围内
|
||||||
const isInGobiRange = gobiMicroHeight > 0 && depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight;
|
// 石块范围:从 groundLevelY - stoneMicroDepth 到 groundLevelY + stoneMicroHeight
|
||||||
const isInStoneRange = stoneMicroHeight > 0 && depth < 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 })) {
|
if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
|
||||||
@@ -575,21 +729,48 @@ export const generateTerrain = async (
|
|||||||
}
|
}
|
||||||
} else if (isInStoneRange) {
|
} else if (isInStoneRange) {
|
||||||
// 巨石风化检查:如果该体素被风化移除,跳过生成
|
// 巨石风化检查:如果该体素被风化移除,跳过生成
|
||||||
if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) {
|
if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) {
|
||||||
|
stoneVoxelsSkippedByWeathering++;
|
||||||
continue; // 跳过生成,风化效果
|
continue; // 跳过生成,风化效果
|
||||||
}
|
}
|
||||||
// 3. 确定基础颜色
|
stoneVoxelsRendered++;
|
||||||
type = getStoneLayerType(depth, stoneVariantIdx);
|
// 计算石块内部的深度(从石块顶部算起)
|
||||||
|
const stoneDepthFromTop = stoneTopY - y;
|
||||||
|
// 确定基础颜色
|
||||||
|
type = getStoneLayerType(stoneDepthFromTop, stoneVariantIdx);
|
||||||
|
|
||||||
// 4. 应用巨石颜色渗透效果
|
// 应用巨石颜色渗透效果
|
||||||
type = applyStoneColorBlending({
|
type = applyStoneColorBlending({
|
||||||
currentType: type,
|
currentType: type,
|
||||||
depth,
|
depth: stoneDepthFromTop,
|
||||||
stoneVariantIdx,
|
stoneVariantIdx,
|
||||||
ix,
|
ix,
|
||||||
iz,
|
iz,
|
||||||
y
|
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) {
|
} else if (isInGobiRange) {
|
||||||
const relativeDepth = depth - stoneMicroHeight;
|
const relativeDepth = depth - stoneMicroHeight;
|
||||||
const microDepthFromBottom = gobiMicroHeight - relativeDepth - 1;
|
const microDepthFromBottom = gobiMicroHeight - relativeDepth - 1;
|
||||||
@@ -604,7 +785,7 @@ export const generateTerrain = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 风化检查:如果该体素被风化移除,跳过生成
|
// 2. 风化检查:如果该体素被风化移除,跳过生成
|
||||||
if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) {
|
if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) {
|
||||||
continue; // 跳过生成,风化效果
|
continue; // 跳过生成,风化效果
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,6 +1011,9 @@ export const generateTerrain = async (
|
|||||||
const culledVoxels = voxels.filter((voxel) => {
|
const culledVoxels = voxels.filter((voxel) => {
|
||||||
// Always keep bedrock bottom layer to avoid gaps
|
// Always keep bedrock bottom layer to avoid gaps
|
||||||
if (voxel.iy <= MIN_WORLD_Y + 1) return true;
|
if (voxel.iy <= MIN_WORLD_Y + 1) return true;
|
||||||
|
|
||||||
|
// 保留石块体素(不可剔除)
|
||||||
|
if (voxel.isRock) return true;
|
||||||
|
|
||||||
for (const [dx, dy, dz] of neighborOffsets) {
|
for (const [dx, dy, dz] of neighborOffsets) {
|
||||||
let neighborKey = keyOf(voxel.ix + dx, voxel.iy + dy, voxel.iz + dz);
|
let neighborKey = keyOf(voxel.ix + dx, voxel.iy + dy, voxel.iz + dz);
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
esbuild: {
|
||||||
|
// 保留 React 的调试信息
|
||||||
|
keepNames: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user