优化植物系统和地形生成,删除旧植物文件

This commit is contained in:
Rocky
2025-12-02 17:45:47 +08:00
parent 297aa0eda3
commit 8cf5fb7397
7 changed files with 3969 additions and 3108 deletions

View File

@@ -1,5 +1,6 @@
import React, { useLayoutEffect, useMemo, useRef } from 'react';
import { InstancedMesh, Object3D, Color, BufferGeometry, Float32BufferAttribute } from 'three';
import { InstancedMesh, Object3D, Color, BufferGeometry, Float32BufferAttribute, ShaderMaterial } from 'three';
import { useFrame } from '@react-three/fiber';
import { useMapStore } from '../store';
import { useUnitStore } from '../../Units/store';
import { VOXEL_SIZE, TILE_SIZE, TREE_VOXEL_SIZE, type VoxelType } from '../logic/terrain';
@@ -8,9 +9,275 @@ import type { ThreeEvent } from '@react-three/fiber';
const tempObject = new Object3D();
const tempColor = new Color();
// ============= 水体动画 Shader =============
const waterVertexShader = /* glsl */ `
// 顶点属性
attribute vec3 color;
attribute float isCascade; // 是否是瀑布(垂直面):基于上下邻居判断
attribute vec2 flowDirection; // 流向 (dx, dz)
// 传递给片元着色器
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
varying float vIsCascade;
varying vec2 vFlowDirection;
void main() {
vColor = color;
vNormal = normalize(normalMatrix * normal);
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
vIsCascade = isCascade;
vFlowDirection = flowDirection;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const waterFragmentShader = /* glsl */ `
uniform float uTime;
uniform float uCascadeSpeed; // 瀑布流速
uniform float uRippleSpeed; // 涟漪速度
uniform vec3 uHighlightColor; // 高光颜色(瀑布白沫)
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
varying float vIsCascade;
varying vec2 vFlowDirection;
// 简单的伪随机函数
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}
// 噪声函数(用于涟漪和水花)
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
void main() {
vec3 finalColor = vColor;
float alpha = 0.75;
// 判断面的朝向 - 使用更宽松的阈值
bool isTopFace = vNormal.y > 0.7; // 顶面(水平)
bool isBottomFace = vNormal.y < -0.7; // 底面
bool isVerticalFace = !isTopFace && !isBottomFace; // 所有非上下面都是垂直面
// ============= 瀑布效果(所有垂直面)=============
if (isVerticalFace) {
float worldY = vWorldPosition.y;
float worldX = vWorldPosition.x;
float worldZ = vWorldPosition.z;
// 瀑布强度
float cascadeStrength = vIsCascade > 0.5 ? 1.0 : 0.35;
float flowSpeed = uCascadeSpeed * 6.0;
// ====== 1. 主水流 - 不规则的垂直流动 ======
// 用噪声创建水流的"路径",让水流有粗细变化
float pathNoise = noise(vec2(worldX * 5.0 + worldZ * 4.0, worldY * 2.0));
float pathOffset = pathNoise * 0.8; // 水流横向摆动
// 主水流 - 不同位置有不同的速度
float localSpeed = flowSpeed * (0.8 + pathNoise * 0.4);
float mainFlow = sin(worldY * 30.0 + uTime * localSpeed + pathOffset * 15.0) * 0.5 + 0.5;
// 用噪声调制水流宽度(有些地方水多,有些地方水少)
float widthNoise = noise(vec2(worldX * 8.0 + worldZ * 6.0, worldY * 4.0 + uTime * 0.3));
mainFlow *= (0.5 + widthNoise * 0.5);
// ====== 2. 次级水流 - 更小更快的水珠 ======
float secondaryNoise = noise(vec2(worldX * 12.0 - worldZ * 8.0, worldY * 6.0));
float secondaryFlow = sin(worldY * 50.0 + uTime * flowSpeed * 1.3 + secondaryNoise * 10.0) * 0.5 + 0.5;
secondaryFlow *= secondaryNoise; // 用噪声掩码让它不连续
// ====== 3. 泡沫 - 随机分布的白色斑点 ======
float foamNoise1 = noise(vec2(worldX * 15.0 + worldZ * 12.0, worldY * 18.0 + uTime * flowSpeed * 0.5));
float foamNoise2 = noise(vec2(worldX * 20.0 - worldZ * 15.0, worldY * 25.0 + uTime * flowSpeed * 0.8));
float foam = smoothstep(0.55, 0.8, foamNoise1) * smoothstep(0.4, 0.7, foamNoise2);
// ====== 4. 飞溅水滴 - 稀疏的亮点 ======
float dropletNoise = noise(vec2(worldX * 30.0 + worldZ * 25.0, worldY * 35.0 + uTime * flowSpeed * 1.5));
float droplets = pow(dropletNoise, 6.0); // 非常稀疏
// ====== 5. 水帘的透明度变化 ======
float thicknessNoise = noise(vec2(worldX * 6.0 + worldZ * 5.0, worldY * 3.0 + uTime * 0.2));
// ====== 组合所有效果 ======
float waterFlow = mainFlow * 0.5 + secondaryFlow * 0.25 + foam * 0.2 + droplets * 0.05;
waterFlow *= cascadeStrength;
// ====== 颜色混合 - 亮蓝青色调 ======
vec3 deepWater = vec3(0.1, 0.45, 0.8); // 亮蓝 - 主体
vec3 midWater = vec3(0.18, 0.55, 0.88); // 更亮蓝 - 水流
vec3 lightWater = vec3(0.28, 0.65, 0.95); // 高亮蓝 - 高光
vec3 foamBlue = vec3(0.4, 0.75, 0.98); // 泡沫亮蓝
// 基础是亮蓝色
finalColor = deepWater;
// 水流区域变亮
finalColor = mix(finalColor, midWater, mainFlow * 0.6);
// 次级水流更亮
finalColor = mix(finalColor, lightWater, secondaryFlow * 0.4);
// 泡沫区域(亮蓝色调)
finalColor = mix(finalColor, foamBlue, foam * 0.35);
// 水滴闪烁(亮蓝色调)
finalColor = mix(finalColor, vec3(0.5, 0.85, 1.0), droplets * 0.25);
// ====== 透明度 - 水薄的地方更透明 ======
alpha = 0.6 + thicknessNoise * 0.15 + waterFlow * 0.2;
alpha = clamp(alpha, 0.55, 0.95);
}
// ============= 水平面(河流/湖泊)=============
else if (isTopFace) {
vec2 worldXZ = vWorldPosition.xz;
vec2 flowDir = vFlowDirection;
float flowStrength = length(flowDir); // 流向强度,用于区分河流和湖泊
// 颜色定义
vec3 deepWater = vec3(0.12, 0.48, 0.82);
vec3 midWater = vec3(0.2, 0.58, 0.9);
vec3 lightWater = vec3(0.3, 0.68, 0.96);
vec3 highlightColor = vec3(0.42, 0.78, 0.98);
float waterEffect = 0.0;
// ============= 河流(有流向)=============
if (flowStrength > 0.01) {
float flowSpeed = uCascadeSpeed * 3.5; // 比瀑布慢
vec2 normalizedFlow = normalize(flowDir);
vec2 perpFlow = vec2(-normalizedFlow.y, normalizedFlow.x);
// 沿流向和垂直方向的坐标
float flowCoord = dot(worldXZ, normalizedFlow);
float perpCoord = dot(worldXZ, perpFlow);
// ====== 1. 主水流 - 类似瀑布的流向动画 ======
float pathNoise = noise(vec2(perpCoord * 5.0, flowCoord * 2.0));
float pathOffset = pathNoise * 0.8;
float localSpeed = flowSpeed * (0.8 + pathNoise * 0.4);
float mainFlow = sin(flowCoord * 25.0 - uTime * localSpeed + pathOffset * 12.0) * 0.5 + 0.5;
// 用噪声调制水流宽度
float widthNoise = noise(vec2(perpCoord * 8.0, flowCoord * 4.0 - uTime * 0.3));
mainFlow *= (0.5 + widthNoise * 0.5);
// ====== 2. 次级水流 - 更小更快的波纹 ======
float secondaryNoise = noise(vec2(perpCoord * 12.0, flowCoord * 6.0));
float secondaryFlow = sin(flowCoord * 45.0 - uTime * flowSpeed * 1.3 + secondaryNoise * 10.0) * 0.5 + 0.5;
secondaryFlow *= secondaryNoise;
// ====== 3. 泡沫 - 沿流向移动的斑点 ======
float foamNoise1 = noise(vec2(perpCoord * 15.0, flowCoord * 18.0 - uTime * flowSpeed * 0.5));
float foamNoise2 = noise(vec2(perpCoord * 20.0, flowCoord * 25.0 - uTime * flowSpeed * 0.8));
float foam = smoothstep(0.55, 0.8, foamNoise1) * smoothstep(0.4, 0.7, foamNoise2);
// ====== 4. 水面闪烁 ======
float sparkleNoise = noise(vec2(perpCoord * 30.0, flowCoord * 35.0 - uTime * flowSpeed * 1.2));
float sparkle = pow(sparkleNoise, 6.0);
// ====== 5. 水深变化 ======
float depthNoise = noise(vec2(perpCoord * 6.0, flowCoord * 3.0 - uTime * 0.2));
// 组合效果
waterEffect = mainFlow * 0.5 + secondaryFlow * 0.25 + foam * 0.2 + sparkle * 0.05;
// 颜色混合
finalColor = deepWater;
finalColor = mix(finalColor, midWater, mainFlow * 0.6);
finalColor = mix(finalColor, lightWater, secondaryFlow * 0.4);
finalColor = mix(finalColor, highlightColor, foam * 0.35);
finalColor = mix(finalColor, vec3(0.52, 0.85, 1.0), sparkle * 0.25);
// 透明度
alpha = 0.6 + depthNoise * 0.15 + waterEffect * 0.2;
alpha = clamp(alpha, 0.55, 0.85);
}
// ============= 湖泊(无流向)- 静态涟漪 =============
else {
float rippleSpeed = uRippleSpeed * 2.0;
// ====== 1. 涟漪 - 缓慢扩散的同心圆 ======
float rippleOffset = noise(worldXZ * 0.3 + uTime * 0.05) * 5.0;
float ripple1 = sin(length(worldXZ * 3.0 + rippleOffset) - uTime * rippleSpeed * 0.3) * 0.5 + 0.5;
float ripple2 = sin(length((worldXZ + vec2(2.0, 1.5)) * 2.5) - uTime * rippleSpeed * 0.25) * 0.5 + 0.5;
float ripples = ripple1 * 0.5 + ripple2 * 0.5;
ripples *= noise(worldXZ * 1.5 + uTime * 0.08) * 0.4 + 0.6;
// ====== 2. 微波 - 小的表面扰动 ======
float microWave = noise(worldXZ * 8.0 + uTime * 0.15) * 0.5 + 0.5;
// ====== 3. 闪烁高光 - 稀疏的亮点 ======
float sparkleNoise = noise(worldXZ * 20.0 + vec2(uTime * 0.3, -uTime * 0.2));
float sparkle = pow(sparkleNoise, 5.0);
// 组合效果
waterEffect = ripples * 0.5 + microWave * 0.3 + sparkle * 0.2;
// 颜色混合 - 湖水更平静,颜色变化更小
finalColor = deepWater;
finalColor = mix(finalColor, midWater, ripples * 0.4);
finalColor = mix(finalColor, lightWater, microWave * 0.25);
finalColor = mix(finalColor, vec3(0.52, 0.85, 1.0), sparkle * 0.2);
// 透明度 - 湖水更稳定
float depthNoise = noise(worldXZ * 2.0 + uTime * 0.1);
alpha = 0.7 + depthNoise * 0.08 + waterEffect * 0.1;
alpha = clamp(alpha, 0.65, 0.85);
}
}
// ============= 底面 =============
else {
// 底面保持暗色
finalColor *= 0.8;
alpha = 0.7;
}
// 简单的环境光照(减弱以保留动画效果)
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
float diffuse = max(dot(vNormal, lightDir), 0.0) * 0.2 + 0.8;
finalColor *= diffuse;
gl_FragColor = vec4(finalColor, alpha);
}
`;
interface VoxelLayerProps {
// Update to include ix, iy, iz which are present in actual VoxelData
data: { x: number; y: number; z: number; color: string; heightScale?: number; isHighRes?: boolean; ix: number; iy: number; iz: number }[];
data: {
x: number;
y: number;
z: number;
color: string;
heightScale?: number;
isHighRes?: boolean;
ix: number;
iy: number;
iz: number;
isMergedWater?: boolean;
mergedPositions?: Array<{
ix: number;
iy: number;
iz: number;
flowDirection?: { dx: number; dz: number };
}>;
flowDirection?: { dx: number; dz: number }; // 单个水体素的流向
}[];
isHighRes: boolean;
type: VoxelType;
onClick?: (x: number, z: number) => void;
@@ -20,21 +287,97 @@ interface VoxelLayerProps {
* 专门用于水体的网格渲染器
* 使用 Greedy Meshing / Face Culling 思想,去除相邻水体之间的内部面
* 解决半透明材质叠加导致的颗粒感和反射问题
*
* 动态效果:
* - 瀑布(垂直面):白色光带从上往下流动
* - 河流(水平面):涟漪波动 + 流向光带
*/
const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onClick }) => {
const meshRef = useRef<any>(null);
const materialRef = useRef<ShaderMaterial>(null);
// 创建 Shader Material
const material = useMemo(() => {
return new ShaderMaterial({
vertexShader: waterVertexShader,
fragmentShader: waterFragmentShader,
uniforms: {
uTime: { value: 0 },
uCascadeSpeed: { value: 2.0 }, // 瀑布流速(调整为更自然的速度)
uRippleSpeed: { value: 2.0 }, // 涟漪速度
uHighlightColor: { value: new Color('#e0f0ff') }, // 高光颜色(淡蓝白色)
},
transparent: true,
depthWrite: false,
});
}, []);
// 每帧更新 time uniform - 实现动画
useFrame((state) => {
if (material) {
material.uniforms.uTime.value = state.clock.elapsedTime;
}
});
const geometry = useMemo(() => {
if (!data.length) return new BufferGeometry();
// 1. 建立查找表 (Set of "ix|iy|iz")
// 1. 展开所有合并的水体素,构建完整的体素列表
interface ExpandedVoxel {
x: number;
y: number;
z: number;
ix: number;
iy: number;
iz: number;
color: string;
heightScale: number;
flowDirection?: { dx: number; dz: number }; // 真实的水流方向
}
const expandedVoxels: ExpandedVoxel[] = [];
data.forEach(v => {
if (v.isMergedWater && v.mergedPositions && v.mergedPositions.length > 0) {
// 展开合并的水体素(保留每个位置的流向)
v.mergedPositions.forEach(pos => {
expandedVoxels.push({
x: pos.ix * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE),
y: pos.iy * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE),
z: pos.iz * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE),
ix: pos.ix,
iy: pos.iy,
iz: pos.iz,
color: v.color,
heightScale: v.heightScale || 1,
flowDirection: pos.flowDirection, // 使用该位置的真实流向
});
});
} else {
// 普通水体素(直接添加,使用体素自身的流向)
expandedVoxels.push({
x: v.x,
y: v.y,
z: v.z,
ix: v.ix,
iy: v.iy,
iz: v.iz,
color: v.color,
heightScale: v.heightScale || 1,
flowDirection: v.flowDirection, // 使用体素的流向
});
}
});
// 2. 建立查找表 (Set of "ix|iy|iz")
const lookup = new Set<string>();
data.forEach(v => lookup.add(`${v.ix}|${v.iy}|${v.iz}`));
expandedVoxels.forEach(v => lookup.add(`${v.ix}|${v.iy}|${v.iz}`));
const positions: number[] = [];
const normals: number[] = [];
const colors: number[] = [];
// const indices: number[] = []; // Not strictly needed if not sharing vertices
const isCascadeArr: number[] = []; // 瀑布标记
const flowDirectionArr: number[] = []; // 流向
const voxelSize = isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE;
const halfSize = voxelSize / 2;
@@ -49,13 +392,30 @@ const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onCli
{ name: 'back', off: [0, 0, -1], normal: [0, 0, -1] },
];
data.forEach(v => {
const { x, y, z, ix, iy, iz, color, heightScale = 1 } = v;
expandedVoxels.forEach(v => {
const { x, y, z, ix, iy, iz, color, heightScale = 1, flowDirection } = v;
const col = new Color(color);
// 只有当 heightScale 为 1 时才能完美拼接
// 如果有缩放,简单起见仍然按照满格判定邻居,或者忽略拼接优化(水通常是满格)
const isFullBlock = Math.abs(heightScale - 1) < 0.01;
// 检测是否是瀑布:垂直方向有邻居水体
// 如果上方或下方有水体素,说明这是瀑布的一部分
const hasAbove = lookup.has(`${ix}|${iy + 1}|${iz}`);
const hasBelow = lookup.has(`${ix}|${iy - 1}|${iz}`);
const isCascade = hasAbove || hasBelow;
// 【使用真实的流向数据】
let flowDx = 0;
let flowDz = 0;
if (flowDirection) {
// 直接使用河流系统计算的真实流向(湖泊为 0,0
flowDx = flowDirection.dx;
flowDz = flowDirection.dz;
}
// 注意:不再使用回退逻辑,因为真实流向已经在 terrain.ts 中正确设置
// 湖泊的流向就是 (0,0),不需要回退
dirs.forEach(dir => {
// 检查该方向是否有同类邻居
@@ -64,22 +424,11 @@ const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onCli
// 如果没有邻居,渲染该面
if (!hasNeighbor) {
// 生成面的4个顶点
// 中心点 (x, y, z) 是体素的中心吗?
// InstancedMesh logic: y + (hScale - 1) * (size/2). If hScale=1, y is center.
// VoxelData.y is world coordinate.
// Assuming voxel.y is the center Y coordinate of the block if scale is 1.
// Calculate actual center based on heightScale logic from VoxelLayer
const centerY = y + (heightScale - 1) * (voxelSize / 2);
const scaleY = voxelSize * heightScale;
const halfY = scaleY / 2;
// Base vertex offsets for a unit cube centered at 0,0,0
// Right: (+h, -h, +h), (+h, -h, -h), (+h, +h, -h), (+h, +h, +h)
// Need to be careful with coordinate system
// THREE.js: Y up. Right(x+), Left(x-), Top(y+), Bottom(y-), Front(z+), Back(z-)
// Vertices relative to center
let v1, v2, v3, v4;
@@ -125,6 +474,16 @@ const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onCli
// Colors (same for all 6 verts)
for(let k=0; k<6; k++) colors.push(col.r, col.g, col.b);
// 瀑布标记:垂直面 + 有上下邻居
const isVerticalFace = Math.abs(ny) < 0.3;
const cascadeValue = (isVerticalFace && isCascade) ? 1.0 : 0.0;
for(let k=0; k<6; k++) isCascadeArr.push(cascadeValue);
// 流向 (same for all 6 verts)
for(let k=0; k<6; k++) {
flowDirectionArr.push(flowDx, flowDz);
}
}
});
});
@@ -133,6 +492,8 @@ const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onCli
bufGeom.setAttribute('position', new Float32BufferAttribute(positions, 3));
bufGeom.setAttribute('normal', new Float32BufferAttribute(normals, 3));
bufGeom.setAttribute('color', new Float32BufferAttribute(colors, 3));
bufGeom.setAttribute('isCascade', new Float32BufferAttribute(isCascadeArr, 1));
bufGeom.setAttribute('flowDirection', new Float32BufferAttribute(flowDirectionArr, 2));
return bufGeom;
}, [data, isHighRes]);
@@ -141,19 +502,10 @@ const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onCli
<mesh
ref={meshRef}
geometry={geometry}
material={material}
castShadow
receiveShadow
>
<meshStandardMaterial
vertexColors
roughness={0.1}
metalness={0.2}
transparent={true}
opacity={0.75}
alphaTest={0}
depthWrite={false} // Water doesn't need depth write usually to avoid occlusion artifacts with itself
/>
</mesh>
/>
);
}

View File

@@ -25,7 +25,7 @@ export interface DesertContext {
stoneHeight: number[][];
stoneDepth: number[][];
stoneVariant: number[][];
streamDepthMap: Map<string, { depth: number; waterHeight: number; isCenter: boolean }>; // Added isCenter
streamDepthMap: Map<string, { depth: number; waterHeight: number; isCenter: boolean; flowDirection?: { dx: number; dz: number } }>;
}
// ============= 戈壁岩石结构生成 =============

File diff suppressed because it is too large Load Diff

View File

@@ -135,8 +135,6 @@ export const generateRockClusters = (params: RockClusterGenerationParams): numbe
);
const attemptLimit = maxAttempts ?? Math.max(25, targetClusters * 40);
console.log(`[RockGen] Target clusters: ${targetClusters}, Profile:`, profile);
let placed = 0;
let attempts = 0;
@@ -179,8 +177,6 @@ export const generateRockClusters = (params: RockClusterGenerationParams): numbe
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;
@@ -190,8 +186,6 @@ export const generateRockClusters = (params: RockClusterGenerationParams): numbe
placed++;
}
console.log(`[RockGen] Finished: ${placed}/${targetClusters} clusters placed after ${attempts} attempts`);
return placed;
};

View File

@@ -75,7 +75,7 @@ export type ProgressCallback = (progress: TerrainGenerationProgress) => void;
// ============= 地形生成配置 =============
// 地形生成版本号 - 每次修改时改变这个数字,触发地图重新生成
export const TERRAIN_VERSION = 86; // 优化:河流在同一平面时避开高度边界,保持在平台中心
export const TERRAIN_VERSION = 110; // 优化:河流转弯处圆角插值,更自然
// 逻辑层面的网格大小(战棋移动格子)
export const TILE_SIZE = 1;
@@ -100,6 +100,9 @@ export interface VoxelData {
heightScale: number;
isHighRes?: boolean; // 标记是否为高分辨率(16x16)体素
isRock?: boolean; // 标记是否为石块体素(不可剔除)
isMergedWater?: boolean; // 标记是否为合并的水体几何体
mergedPositions?: Array<{ ix: number; iy: number; iz: number; flowDirection?: { dx: number; dz: number } }>; // 合并的体素位置集合
flowDirection?: { dx: number; dz: number }; // 水流方向
}
export const MAP_SIZES = {
@@ -133,9 +136,159 @@ const buildRockOnlyContext = (
stoneHeight: rockField.stoneHeight,
stoneDepth: rockField.stoneDepth,
stoneVariant: rockField.stoneVariant,
streamDepthMap: new Map<string, { depth: number; waterHeight: number; isCenter: boolean }>(),
streamDepthMap: new Map<string, { depth: number; waterHeight: number; isCenter: boolean; flowDirection?: { dx: number; dz: number } }>(),
});
/**
* 水体素临时数据结构(用于合并前的收集)
*/
interface WaterVoxelTemp {
ix: number;
iy: number;
iz: number;
x: number;
y: number;
z: number;
color: string;
heightScale: number;
flowDirection?: { dx: number; dz: number }; // 水流方向
}
/**
* 使用 BFS 找到所有 3D 连通的水体素区域
* @param waterVoxels 所有水体素的位置集合
* @returns 连通区域数组,每个区域包含该区域的所有体素
*/
const findConnectedWaterRegions = (waterVoxels: WaterVoxelTemp[]): WaterVoxelTemp[][] => {
if (waterVoxels.length === 0) return [];
// 构建快速查找表
const voxelMap = new Map<string, WaterVoxelTemp>();
const keyOf = (ix: number, iy: number, iz: number) => `${ix}|${iy}|${iz}`;
waterVoxels.forEach(v => {
voxelMap.set(keyOf(v.ix, v.iy, v.iz), v);
});
const visited = new Set<string>();
const regions: WaterVoxelTemp[][] = [];
// 6个方向的邻居偏移3D连通性
const neighborOffsets = [
[1, 0, 0], [-1, 0, 0],
[0, 1, 0], [0, -1, 0],
[0, 0, 1], [0, 0, -1],
];
// BFS 遍历
const bfs = (startVoxel: WaterVoxelTemp): WaterVoxelTemp[] => {
const region: WaterVoxelTemp[] = [];
const queue: WaterVoxelTemp[] = [startVoxel];
const startKey = keyOf(startVoxel.ix, startVoxel.iy, startVoxel.iz);
visited.add(startKey);
while (queue.length > 0) {
const current = queue.shift()!;
region.push(current);
// 检查6个邻居
for (const [dx, dy, dz] of neighborOffsets) {
const neighborKey = keyOf(
current.ix + dx,
current.iy + dy,
current.iz + dz
);
if (!visited.has(neighborKey) && voxelMap.has(neighborKey)) {
visited.add(neighborKey);
queue.push(voxelMap.get(neighborKey)!);
}
}
}
return region;
};
// 对每个未访问的体素启动 BFS
for (const voxel of waterVoxels) {
const key = keyOf(voxel.ix, voxel.iy, voxel.iz);
if (!visited.has(key)) {
const region = bfs(voxel);
if (region.length > 0) {
regions.push(region);
}
}
}
return regions;
};
/**
* 将连通的水体素区域合并为单个 VoxelData 对象
* @param region 连通区域内的所有体素
* @returns 合并后的 VoxelData 对象
*/
const mergeWaterRegion = (region: WaterVoxelTemp[]): VoxelData => {
if (region.length === 0) {
throw new Error('Cannot merge empty water region');
}
// 如果只有一个体素,直接返回(不需要合并)
if (region.length === 1) {
const single = region[0];
return {
x: single.x,
y: single.y,
z: single.z,
type: 'water',
color: single.color,
ix: single.ix,
iy: single.iy,
iz: single.iz,
heightScale: single.heightScale,
isMergedWater: false, // 单个体素不需要标记为合并
};
}
// 计算区域的中心位置(用作代表位置)
const avgX = region.reduce((sum, v) => sum + v.x, 0) / region.length;
const avgY = region.reduce((sum, v) => sum + v.y, 0) / region.length;
const avgZ = region.reduce((sum, v) => sum + v.z, 0) / region.length;
// 选择最接近中心的体素作为代表体素
const representative = region.reduce((closest, v) => {
const distCurrent = Math.sqrt(
Math.pow(v.x - avgX, 2) +
Math.pow(v.y - avgY, 2) +
Math.pow(v.z - avgZ, 2)
);
const distClosest = Math.sqrt(
Math.pow(closest.x - avgX, 2) +
Math.pow(closest.y - avgY, 2) +
Math.pow(closest.z - avgZ, 2)
);
return distCurrent < distClosest ? v : closest;
}, region[0]);
// 使用代表体素的平均颜色(混合所有颜色)
const avgColor = representative.color; // 简化:使用代表体素的颜色
return {
x: representative.x,
y: representative.y,
z: representative.z,
type: 'water',
color: avgColor,
ix: representative.ix,
iy: representative.iy,
iz: representative.iz,
heightScale: representative.heightScale,
isMergedWater: true,
mergedPositions: region.map(v => ({ ix: v.ix, iy: v.iy, iz: v.iz, flowDirection: v.flowDirection })),
flowDirection: representative.flowDirection, // 代表体素的流向
};
};
export const generateTerrain = async (
mapSize: number,
seed: string = 'default',
@@ -327,8 +480,6 @@ export const generateTerrain = async (
seededRandom,
MICRO_SCALE
);
console.log(`[Terrain] 山地溪流已生成,包含 ${mountainStreamMap.size} 个微体素`);
}
// ===== 戈壁和巨石风化预处理 =====
@@ -372,7 +523,7 @@ export const generateTerrain = async (
await new Promise(resolve => setTimeout(resolve, 0));
}
const voxels: VoxelData[] = [];
let voxels: VoxelData[] = [];
const occupancy = new Set<string>();
const keyOf = (x: number, y: number, z: number) => `${x}|${y}|${z}`;
const topColumns = new Map<string, { index: number; baseType: VoxelType }>();
@@ -380,6 +531,9 @@ export const generateTerrain = async (
// 记录树木位置
const treePositions: Array<{ x: number, z: number }> = [];
// 临时收集所有水体素(用于后续合并)
const tempWaterVoxels: WaterVoxelTemp[] = [];
// 定义添加体素的函数类型
type AddVoxelFn = (
ix: number,
@@ -390,7 +544,31 @@ export const generateTerrain = async (
heightScale?: number
) => void;
// 【新增】记录需要移除的体素位置(用于侧面瀑布蚀刻)
const voxelsToRemove = new Set<string>();
// 当前正在处理的微体素的流向(用于水体素)
let currentFlowDirection: { dx: number; dz: number } | undefined = undefined;
const addVoxel: AddVoxelFn = (ix, iy, iz, type, color, heightScale = 1) => {
// 如果是水体素,收集到临时数组而不是立即添加
if (type === 'water') {
tempWaterVoxels.push({
ix,
iy,
iz,
x: ix * VOXEL_SIZE,
y: iy * VOXEL_SIZE,
z: iz * VOXEL_SIZE,
color,
heightScale,
flowDirection: currentFlowDirection, // 保存当前流向
});
occupancy.add(keyOf(ix, iy, iz));
return;
}
// 非水体素正常添加
const voxel: VoxelData = {
x: ix * VOXEL_SIZE,
y: iy * VOXEL_SIZE,
@@ -619,9 +797,17 @@ export const generateTerrain = async (
// 记录原始地表位置(用于石块范围判断)
const groundLevelY = worldY;
// 获取溪流信息(戈壁场景或山地场景),用于判断是否在河流区域
const streamInfo = isDesertScene && desertContext
? desertContext.streamDepthMap.get(`${ix}|${iz}`)
: undefined;
const mountainStreamInfo = mountainStreamMap?.get(`${ix}|${iz}`);
const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0;
const isInRiverArea = streamDepthMicro > 0;
// SMOOTHING LOGIC:
// 只对非石块区域应用 smoothing保持石块稳固
if (!hasRockColumn) {
// 只对非石块、非河流区域应用 smoothing保持石块和河床稳固对齐
if (!hasRockColumn && !isInRiverArea) {
// Only apply significant noise to Stone/Snow
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
@@ -647,13 +833,6 @@ export const generateTerrain = async (
// 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation
let sandMoundHeight = 0;
// 获取溪流信息(戈壁场景或山地场景)
const streamInfo = isDesertScene && desertContext
? desertContext.streamDepthMap.get(`${ix}|${iz}`)
: undefined;
const mountainStreamInfo = mountainStreamMap?.get(`${ix}|${iz}`);
const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0;
// 只有非河流区域才生成沙丘
if (streamDepthMicro === 0 && isDesertScene && logicType === 'sand' && gobiLogicHeight === 0 && stoneLogicHeight === 0) {
// Check if we are near a Gobi hill
@@ -706,16 +885,10 @@ export const generateTerrain = async (
// Apply sand mound height to worldY
worldY += sandMoundHeight;
// 河流生成:强制使用绝对高度
// 【方案A + B】如果河流存储了绝对表面高度直接使用完全忽略 logicHeight
// 【修复】河床蚀刻:从地表减去河床深度,形成凹陷
if (streamDepthMicro > 0) {
// 保持原有计算逻辑
worldY = logicHeight * MICRO_SCALE;
} else if (streamDepthMicro > 0) {
// 降级方案(如果没有绝对高度,使用旧逻辑)
const baseWorldY = logicHeight * MICRO_SCALE;
worldY = baseWorldY;
worldY -= streamDepthMicro;
// 河流区域worldY 是基础地形高度,减去蚀刻深度
worldY = logicHeight * MICRO_SCALE - streamDepthMicro;
}
const surfaceY = worldY;
@@ -739,31 +912,16 @@ export const generateTerrain = async (
const isInGobiRange = gobiMicroHeight > 0 && !isInStoneRange &&
depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight;
// 【图2参考】河床内部渲染系统(简化版)
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'; // 碎石
}
// 【简化】河床内部渲染:统一泥土材质
if (mountainStreamInfo && streamDepthMicro > 0) {
// 河床蚀刻深度内 + 下方填充层:统一使用泥土
if (depth < streamDepthMicro + 5) {
type = 'dirt';
}
}
// 戈壁溪流的原有逻辑
// 【简化】戈壁溪流:统一使用 etched_sand蚀刻沙
else if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
type = 'etched_sand';
// CENTER OF STREAM = DARK STONE / GRAVEL
if (streamInfo && streamInfo.isCenter) {
// Simple noise for gravel patchiness
if (pseudoRandom(ix * 0.3 + iz * 0.3) > 0.4) {
type = 'dark_stone';
}
}
} else if (isInStoneRange) {
// 巨石风化检查:如果该体素被风化移除,跳过生成
if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) {
@@ -917,9 +1075,13 @@ export const generateTerrain = async (
}
}
// 【重新启用】水体生成 - 河床调试已完成
if (streamDepthMicro > 0) {
// 戈壁溪流水体填充
if (streamInfo?.waterHeight && streamInfo.waterHeight > 0) {
// 设置当前流向(用于水体素)
currentFlowDirection = streamInfo.flowDirection;
for (let h = 1; h <= streamInfo.waterHeight; h++) {
// Ensure water doesn't exceed surface (should not happen with waterH calculation, but safe)
if (surfaceY + h > surfaceY + streamDepthMicro) break;
@@ -939,6 +1101,9 @@ export const generateTerrain = async (
if (mountainStreamInfo?.waterHeight && mountainStreamInfo.waterHeight > 0) {
const waterHeight = mountainStreamInfo.waterHeight;
// 设置当前流向(用于水体素)
currentFlowDirection = mountainStreamInfo.flowDirection;
// 普通水面填充
for (let h = 1; h <= waterHeight; h++) {
if (surfaceY + h > surfaceY + streamDepthMicro) break;
@@ -953,112 +1118,184 @@ export const generateTerrain = async (
);
}
// 瀑布效果:基于相邻微体素的真实高度差填充
// 【修复】检测相邻河流微体素的绝对高度,填充高度差
if (mountainStreamInfo && waterHeight > 0) {
const waterTopY = surfaceY + Math.floor(waterHeight);
// 【新增】侧面瀑布填充逻辑
// 检测相邻微体素,如果有高度落差,在侧面填充水体素
const checkDirections = [
{ dx: 1, dz: 0 },
{ dx: -1, dz: 0 },
{ dx: 0, dz: 1 },
{ dx: 0, dz: -1 },
];
for (const dir of checkDirections) {
const nx = ix + dir.dx;
const nz = iz + dir.dz;
// 检测4个方向的相邻微体素
const checkDirections = [
{ dx: 1, dz: 0 },
{ dx: -1, dz: 0 },
{ dx: 0, dz: 1 },
{ dx: 0, dz: -1 },
];
// 获取相邻微体素的河流信息
const neighborStreamInfo = mountainStreamMap?.get(`${nx}|${nz}`);
let maxHeightDiff = 0;
for (const dir of checkDirections) {
const nx = ix + dir.dx;
const nz = iz + dir.dz;
if (neighborStreamInfo && neighborStreamInfo.depth > 0) {
// 计算相邻微体素的逻辑坐标
const neighborLx = Math.floor(nx / MICRO_SCALE);
const neighborLz = Math.floor(nz / MICRO_SCALE);
const neighborStreamInfo = mountainStreamMap?.get(`${nx}|${nz}`);
if (neighborStreamInfo) {
// 简化使用cascadeHeight判断
const cascadeH = neighborStreamInfo.cascadeHeight || 0;
if (cascadeH > 0) {
maxHeightDiff = Math.max(maxHeightDiff, cascadeH);
}
// 边界检查
if (neighborLx < 0 || neighborLx >= mapSize || neighborLz < 0 || neighborLz >= mapSize) {
continue;
}
}
// 如果检测到高度差 > 2填充侧面瀑布
if (maxHeightDiff >= 2) {
for (let dy = 1; dy < maxHeightDiff; dy++) {
const cascadeY = waterTopY - dy;
// 获取相邻的逻辑高度
const neighborLogicHeight = terrainHeightMap[neighborLx][neighborLz];
// 计算相邻的河床表面高度
const neighborSurfaceY = neighborLogicHeight * MICRO_SCALE - neighborStreamInfo.depth;
// 如果相邻更低,说明有落差
if (neighborSurfaceY < surfaceY) {
const heightDiff = surfaceY - neighborSurfaceY;
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(
// 只处理明显的落差至少2微体素
if (heightDiff >= 2) {
// 在当前位置垂直填充水体素,覆盖侧面
// 从相邻表面+1 到当前表面
for (let y = Math.floor(neighborSurfaceY) + 1; y <= surfaceY; y++) {
if (y <= MIN_WORLD_Y) {
continue;
}
const posKey = keyOf(ix, y, iz);
// 【修改】如果位置已被占用(泥土),标记为需要移除(蚀刻掉)
if (occupancy.has(posKey)) {
occupancy.delete(posKey);
voxelsToRemove.add(posKey); // 标记为需要移除
}
// 【修复】移除噪声检查,填满所有侧面瀑布位置
// 添加水体素 (会重新添加到occupancy中)
addVoxel(
ix,
cascadeY,
y,
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
);
}
}
}
}
1
);
}
}
}
}
}
// 激流效果:非瀑布段但有一定流速(流向明显)
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 // 很透明的水花
);
}
}
// 【临时注释】瀑布效果 - 用于调试
/*
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);
}
}
}
// 垂直瀑布填充
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 // 很透明的水花
);
}
}
*/
}
}
@@ -1102,24 +1339,85 @@ export const generateTerrain = async (
reportProgress('postprocessing', 0, '开始地貌后处理');
await new Promise(resolve => setTimeout(resolve, 0));
// 【新增】移除被侧面瀑布蚀刻掉的泥土体素
if (voxelsToRemove.size > 0) {
console.log(`[侧面瀑布蚀刻] 移除 ${voxelsToRemove.size} 个被覆盖的泥土体素`);
const originalLength = voxels.length;
voxels = voxels.filter(v => {
const key = keyOf(v.ix, v.iy, v.iz);
return !voxelsToRemove.has(key);
});
console.log(`[侧面瀑布蚀刻] 实际移除 ${originalLength - voxels.length} 个体素`);
// 【修复】重建 topColumns 映射,因为 voxels 数组已改变,旧的索引失效
// 使用临时 Map 存储每个位置的最高点信息
const heightTracker = new Map<string, { maxY: number; index: number; baseType: VoxelType }>();
voxels.forEach((v, index) => {
const key = `${v.ix}|${v.iz}`;
const existing = heightTracker.get(key);
if (!existing || v.iy > existing.maxY) {
heightTracker.set(key, { maxY: v.iy, index, baseType: v.type });
}
});
// 重建 topColumns
topColumns.clear();
heightTracker.forEach((data, key) => {
topColumns.set(key, { index: data.index, baseType: data.baseType });
});
}
// ===== 水体合并处理:将连通的水体素合并为单一几何体 =====
reportProgress('postprocessing', 5, '开始合并水体区域');
await new Promise(resolve => setTimeout(resolve, 0));
console.log(`[水体合并] 收集到 ${tempWaterVoxels.length} 个水体素`);
if (tempWaterVoxels.length > 0) {
// 使用 BFS 找到所有连通区域
const waterRegions = findConnectedWaterRegions(tempWaterVoxels);
console.log(`[水体合并] 检测到 ${waterRegions.length} 个连通水体区域`);
// 统计信息
let totalOriginalVoxels = 0;
let totalMergedVoxels = 0;
// 合并每个区域并添加到 voxels 数组
waterRegions.forEach((region, index) => {
totalOriginalVoxels += region.length;
const mergedVoxel = mergeWaterRegion(region);
voxels.push(mergedVoxel);
totalMergedVoxels++;
if (index < 5 || region.length > 100) {
// 只打印前5个区域或大型区域的信息
console.log(`[水体合并] 区域 ${index + 1}: ${region.length} 个体素 -> 1 个合并体素`);
}
});
const reductionPercent = ((1 - totalMergedVoxels / totalOriginalVoxels) * 100).toFixed(1);
console.log(`[水体合并] 优化完成: ${totalOriginalVoxels} 个体素 -> ${totalMergedVoxels} 个合并体素 (减少 ${reductionPercent}%)`);
}
reportProgress('postprocessing', 10, '水体合并完成');
await new Promise(resolve => setTimeout(resolve, 0));
// ===== 水体颜色后处理:基于离陆地距离 =====
// 1. 构建逻辑地形类型映射(基于 topColumns 的 baseType
// 注意:合并后的水体素已经包含了所有子体素的颜色信息
// 颜色渲染将在 WaterFlowMesh 中基于 mergedPositions 进行
// 这里保留逻辑以兼容未合并的水体素(如果有的话)
const logicalTerrainMap = new Map<string, VoxelType>();
topColumns.forEach(({ baseType }, key) => {
logicalTerrainMap.set(key, baseType);
});
// 2. 为每个水体方块计算到最近陆地的距离
voxels.forEach(voxel => {
if (voxel.type === 'water') {
// 转换到逻辑坐标
if (voxel.type === 'water' && !voxel.isMergedWater) {
// 只处理未合并的水体素(正常情况下不应该有)
const logicX = Math.floor(voxel.ix / TILE_SIZE);
const logicZ = Math.floor(voxel.iz / TILE_SIZE);
// BFS 查找最近的陆地(使用逻辑坐标)
let minDistToLand = Infinity;
// 搜索半径(逻辑坐标)- 增加到 12 格
const searchRadius = 12;
for (let dx = -searchRadius; dx <= searchRadius; dx++) {
for (let dz = -searchRadius; dz <= searchRadius; dz++) {
@@ -1127,7 +1425,6 @@ export const generateTerrain = async (
const checkZ = logicZ + dz;
const checkType = logicalTerrainMap.get(`${checkX}|${checkZ}`);
// 如果是陆地(非水体)
if (checkType && checkType !== 'water') {
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist < minDistToLand) {
@@ -1137,13 +1434,8 @@ export const generateTerrain = async (
}
}
// 根据距离重新计算颜色
// 使用平滑的平方根函数:让浅色区域更长,深色只在远处出现
// sqrt(x) 函数增长缓慢,适合创建平滑渐变
const normalizedDist = Math.min(minDistToLand / 12, 1); // 归一化到 0-1
const effectiveDepth = Math.sqrt(normalizedDist) * 10; // 平方根映射到 0-10
// 降低颜色噪声,让渐变更平滑
const normalizedDist = Math.min(minDistToLand / 12, 1);
const effectiveDepth = Math.sqrt(normalizedDist) * 10;
voxel.color = varyColor('water', Math.random() * 0.1, effectiveDepth);
}
});
@@ -1177,6 +1469,9 @@ export const generateTerrain = async (
// 保留石块体素(不可剔除)
if (voxel.isRock) return true;
// 保留合并的水体素(已经过优化,不需要剔除)
if (voxel.isMergedWater) return true;
for (const [dx, dy, dz] of neighborOffsets) {
let neighborKey = keyOf(voxel.ix + dx, voxel.iy + dy, voxel.iz + dz);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff