添加雪山场景,优化植被和地形系统
This commit is contained in:
@@ -31,9 +31,9 @@ function GameScene({ skyPreset }: { skyPreset: SkyPreset }) {
|
|||||||
// 使用 ref 跟踪上一次的版本号
|
// 使用 ref 跟踪上一次的版本号
|
||||||
const prevVersionRef = useRef(TERRAIN_VERSION);
|
const prevVersionRef = useRef(TERRAIN_VERSION);
|
||||||
|
|
||||||
// 初始化地图 - 默认生成山地场景
|
// 初始化地图 - 默认生成雪山场景
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateMap('small', undefined, 'mountain');
|
generateMap('small', undefined, 'snowy_mountain');
|
||||||
|
|
||||||
// 临时注释:角色生成和战斗初始化
|
// 临时注释:角色生成和战斗初始化
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
|
|||||||
@@ -9,6 +9,186 @@ import type { ThreeEvent } from '@react-three/fiber';
|
|||||||
const tempObject = new Object3D();
|
const tempObject = new Object3D();
|
||||||
const tempColor = new Color();
|
const tempColor = new Color();
|
||||||
|
|
||||||
|
// ============= 冰块 Shader =============
|
||||||
|
|
||||||
|
const iceVertexShader = /* glsl */ `
|
||||||
|
attribute vec3 color;
|
||||||
|
|
||||||
|
varying vec3 vColor;
|
||||||
|
varying vec3 vNormal;
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vColor = color;
|
||||||
|
vNormal = normalize(normalMatrix * normal);
|
||||||
|
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const iceFragmentShader = /* glsl */ `
|
||||||
|
uniform float uTime;
|
||||||
|
|
||||||
|
varying vec3 vColor;
|
||||||
|
varying vec3 vNormal;
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
|
||||||
|
// 简单的伪随机函数
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voronoi 用于生成自然裂纹
|
||||||
|
vec2 voronoi(vec2 x) {
|
||||||
|
vec2 p = floor(x);
|
||||||
|
vec2 f = fract(x);
|
||||||
|
|
||||||
|
float minDist = 1.0;
|
||||||
|
float secondMin = 1.0;
|
||||||
|
|
||||||
|
for(int j = -1; j <= 1; j++) {
|
||||||
|
for(int i = -1; i <= 1; i++) {
|
||||||
|
vec2 b = vec2(float(i), float(j));
|
||||||
|
vec2 r = b - f + random(p + b);
|
||||||
|
float d = dot(r, r);
|
||||||
|
|
||||||
|
if(d < minDist) {
|
||||||
|
secondMin = minDist;
|
||||||
|
minDist = d;
|
||||||
|
} else if(d < secondMin) {
|
||||||
|
secondMin = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec2(sqrt(minDist), sqrt(secondMin));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 finalColor = vColor;
|
||||||
|
float alpha = 0.88;
|
||||||
|
|
||||||
|
// 判断面的朝向
|
||||||
|
bool isTopFace = vNormal.y > 0.7;
|
||||||
|
bool isBottomFace = vNormal.y < -0.7;
|
||||||
|
bool isVerticalFace = !isTopFace && !isBottomFace;
|
||||||
|
|
||||||
|
// 冰块颜色定义 - 更明显的蓝色调
|
||||||
|
vec3 clearIce = vec3(0.65, 0.85, 0.95); // 清澈冰蓝(主色调,更蓝)
|
||||||
|
vec3 deepIce = vec3(0.4, 0.65, 0.85); // 深处冰蓝(更深的蓝)
|
||||||
|
vec3 frostWhite = vec3(0.85, 0.92, 0.98); // 霜白色(带蓝调)
|
||||||
|
vec3 bubbleWhite = vec3(0.9, 0.95, 1.0); // 气泡白(带蓝调)
|
||||||
|
vec3 specularWhite = vec3(0.95, 0.98, 1.0); // 镜面反射(带蓝调)
|
||||||
|
|
||||||
|
vec2 worldXZ = vWorldPosition.xz;
|
||||||
|
float worldY = vWorldPosition.y;
|
||||||
|
|
||||||
|
// 光源和视角
|
||||||
|
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
|
||||||
|
vec3 viewDir = normalize(cameraPosition - vWorldPosition);
|
||||||
|
|
||||||
|
// ============= 顶面(冰面)=============
|
||||||
|
if (isTopFace) {
|
||||||
|
// 基础颜色 - 清澈的冰蓝色
|
||||||
|
finalColor = clearIce;
|
||||||
|
|
||||||
|
// 1. 深度变化 - 非常柔和的渐变,模拟冰的厚度不均
|
||||||
|
float depthNoise = noise(worldXZ * 0.8);
|
||||||
|
finalColor = mix(finalColor, deepIce, depthNoise * 0.15);
|
||||||
|
|
||||||
|
// 2. 自然裂纹 - 使用 Voronoi 生成裂纹网络(加大20%)
|
||||||
|
vec2 vor = voronoi(worldXZ * 3.3);
|
||||||
|
float crackEdge = smoothstep(0.025, 0.075, vor.y - vor.x);
|
||||||
|
float cracks = (1.0 - crackEdge) * 0.09;
|
||||||
|
finalColor -= vec3(cracks);
|
||||||
|
|
||||||
|
// 3. 气泡效果 - 稀疏的小亮点,模拟冻在冰里的气泡
|
||||||
|
float bubbleNoise = random(floor(worldXZ * 20.0));
|
||||||
|
float bubble = step(0.92, bubbleNoise) * 0.3;
|
||||||
|
finalColor = mix(finalColor, bubbleWhite, bubble);
|
||||||
|
|
||||||
|
// 4. 表面霜花 - 边缘区域轻微的白霜
|
||||||
|
float frostNoise = noise(worldXZ * 12.0);
|
||||||
|
float frost = pow(frostNoise, 3.0) * 0.1;
|
||||||
|
finalColor = mix(finalColor, frostWhite, frost);
|
||||||
|
|
||||||
|
// 5. 镜面反射 - Blinn-Phong 高光
|
||||||
|
vec3 halfDir = normalize(lightDir + viewDir);
|
||||||
|
float specAngle = max(dot(vNormal, halfDir), 0.0);
|
||||||
|
float specular = pow(specAngle, 64.0) * 0.8; // 锐利的镜面高光
|
||||||
|
finalColor += specularWhite * specular;
|
||||||
|
|
||||||
|
// 6. 闪烁高光 - 稀疏的亮点
|
||||||
|
float sparkle = pow(random(worldXZ * 50.0 + uTime * 0.5), 10.0) * 0.4;
|
||||||
|
finalColor += vec3(sparkle);
|
||||||
|
|
||||||
|
// 透明度 - 更不透明
|
||||||
|
alpha = 0.88 + depthNoise * 0.08;
|
||||||
|
}
|
||||||
|
// ============= 垂直面(冰壁)=============
|
||||||
|
else if (isVerticalFace) {
|
||||||
|
// 基础颜色
|
||||||
|
finalColor = clearIce;
|
||||||
|
|
||||||
|
// 深度渐变
|
||||||
|
float wallDepth = noise(vec2(worldXZ.x + worldXZ.y, worldY) * 1.5);
|
||||||
|
finalColor = mix(finalColor, deepIce, wallDepth * 0.2);
|
||||||
|
|
||||||
|
// 垂直方向的裂纹(加大20%)
|
||||||
|
vec2 wallVor = voronoi(vec2(worldXZ.x + worldXZ.y, worldY * 2.0) * 2.5);
|
||||||
|
float wallCracks = (1.0 - smoothstep(0.025, 0.095, wallVor.y - wallVor.x)) * 0.07;
|
||||||
|
finalColor -= vec3(wallCracks);
|
||||||
|
|
||||||
|
// 气泡
|
||||||
|
float wallBubble = random(floor(vec2(worldXZ.x + worldXZ.y, worldY) * 15.0));
|
||||||
|
float bubble = step(0.94, wallBubble) * 0.25;
|
||||||
|
finalColor = mix(finalColor, bubbleWhite, bubble);
|
||||||
|
|
||||||
|
// 镜面反射
|
||||||
|
vec3 halfDir = normalize(lightDir + viewDir);
|
||||||
|
float specAngle = max(dot(vNormal, halfDir), 0.0);
|
||||||
|
float specular = pow(specAngle, 48.0) * 0.6;
|
||||||
|
finalColor += specularWhite * specular;
|
||||||
|
|
||||||
|
// 垂直面不透明度
|
||||||
|
alpha = 0.85 + wallDepth * 0.08;
|
||||||
|
}
|
||||||
|
// ============= 底面 =============
|
||||||
|
else {
|
||||||
|
finalColor = deepIce;
|
||||||
|
alpha = 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 柔和的环境光照
|
||||||
|
float diffuse = max(dot(vNormal, lightDir), 0.0) * 0.2 + 0.8;
|
||||||
|
finalColor *= diffuse;
|
||||||
|
|
||||||
|
// 菲涅尔效果 - 边缘反射更强
|
||||||
|
float fresnel = pow(1.0 - max(dot(vNormal, viewDir), 0.0), 3.0);
|
||||||
|
finalColor = mix(finalColor, frostWhite, fresnel * 0.35);
|
||||||
|
|
||||||
|
// 菲涅尔增加不透明度(边缘更实)
|
||||||
|
alpha = mix(alpha, 0.98, fresnel * 0.3);
|
||||||
|
|
||||||
|
gl_FragColor = vec4(finalColor, alpha);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// ============= 水体动画 Shader =============
|
// ============= 水体动画 Shader =============
|
||||||
|
|
||||||
const waterVertexShader = /* glsl */ `
|
const waterVertexShader = /* glsl */ `
|
||||||
@@ -509,6 +689,163 @@ const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onCli
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专门用于冰块的网格渲染器
|
||||||
|
* 使用 Face Culling 思想,去除相邻冰块之间的内部面
|
||||||
|
* 解决半透明材质叠加导致的颗粒感问题
|
||||||
|
*
|
||||||
|
* 视觉效果:
|
||||||
|
* - 冰晶纹理和裂纹
|
||||||
|
* - 微弱的高光闪烁
|
||||||
|
* - 菲涅尔边缘高光
|
||||||
|
*/
|
||||||
|
const IceMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onClick }) => {
|
||||||
|
const meshRef = useRef<any>(null);
|
||||||
|
|
||||||
|
// 创建冰块 Shader Material
|
||||||
|
const material = useMemo(() => {
|
||||||
|
return new ShaderMaterial({
|
||||||
|
vertexShader: iceVertexShader,
|
||||||
|
fragmentShader: iceFragmentShader,
|
||||||
|
uniforms: {
|
||||||
|
uTime: { value: 0 },
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 构建体素列表
|
||||||
|
interface IceVoxel {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
ix: number;
|
||||||
|
iy: number;
|
||||||
|
iz: number;
|
||||||
|
color: string;
|
||||||
|
heightScale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iceVoxels: IceVoxel[] = data.map(v => ({
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 建立查找表 (Set of "ix|iy|iz")
|
||||||
|
const lookup = new Set<string>();
|
||||||
|
iceVoxels.forEach(v => lookup.add(`${v.ix}|${v.iy}|${v.iz}`));
|
||||||
|
|
||||||
|
const positions: number[] = [];
|
||||||
|
const normals: number[] = [];
|
||||||
|
const colors: number[] = [];
|
||||||
|
|
||||||
|
const voxelSize = isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE;
|
||||||
|
const halfSize = voxelSize / 2;
|
||||||
|
|
||||||
|
// 6个方向的邻居偏移和法线
|
||||||
|
const dirs = [
|
||||||
|
{ name: 'right', off: [1, 0, 0], normal: [1, 0, 0] },
|
||||||
|
{ name: 'left', off: [-1, 0, 0], normal: [-1, 0, 0] },
|
||||||
|
{ name: 'top', off: [0, 1, 0], normal: [0, 1, 0] },
|
||||||
|
{ name: 'bottom', off: [0, -1, 0], normal: [0, -1, 0] },
|
||||||
|
{ name: 'front', off: [0, 0, 1], normal: [0, 0, 1] },
|
||||||
|
{ name: 'back', off: [0, 0, -1], normal: [0, 0, -1] },
|
||||||
|
];
|
||||||
|
|
||||||
|
iceVoxels.forEach(v => {
|
||||||
|
const { x, y, z, ix, iy, iz, color, heightScale = 1 } = v;
|
||||||
|
const col = new Color(color);
|
||||||
|
|
||||||
|
// 只有当 heightScale 为 1 时才能完美拼接
|
||||||
|
const isFullBlock = Math.abs(heightScale - 1) < 0.01;
|
||||||
|
|
||||||
|
dirs.forEach(dir => {
|
||||||
|
// 检查该方向是否有同类邻居
|
||||||
|
const neighborKey = `${ix + dir.off[0]}|${iy + dir.off[1]}|${iz + dir.off[2]}`;
|
||||||
|
const hasNeighbor = isFullBlock && lookup.has(neighborKey);
|
||||||
|
|
||||||
|
// 如果没有邻居,渲染该面
|
||||||
|
if (!hasNeighbor) {
|
||||||
|
const centerY = y + (heightScale - 1) * (voxelSize / 2);
|
||||||
|
const scaleY = voxelSize * heightScale;
|
||||||
|
const halfY = scaleY / 2;
|
||||||
|
|
||||||
|
let v1, v2, v3, v4;
|
||||||
|
const hs = halfSize;
|
||||||
|
const hy = halfY;
|
||||||
|
const [nx, ny, nz] = dir.normal;
|
||||||
|
|
||||||
|
if (nx === 1) { // Right
|
||||||
|
v1 = [hs, -hy, hs]; v2 = [hs, -hy, -hs]; v3 = [hs, hy, -hs]; v4 = [hs, hy, hs];
|
||||||
|
} else if (nx === -1) { // Left
|
||||||
|
v1 = [-hs, -hy, -hs]; v2 = [-hs, -hy, hs]; v3 = [-hs, hy, hs]; v4 = [-hs, hy, -hs];
|
||||||
|
} else if (ny === 1) { // Top
|
||||||
|
v1 = [-hs, hy, hs]; v2 = [ hs, hy, hs]; v3 = [ hs, hy, -hs]; v4 = [-hs, hy, -hs];
|
||||||
|
} else if (ny === -1) { // Bottom
|
||||||
|
v1 = [ hs, -hy, hs]; v2 = [-hs, -hy, hs]; v3 = [-hs, -hy, -hs]; v4 = [ hs, -hy, -hs];
|
||||||
|
} else if (nz === 1) { // Front (Z+)
|
||||||
|
v1 = [-hs, -hy, hs]; v2 = [ hs, -hy, hs]; v3 = [ hs, hy, hs]; v4 = [-hs, hy, hs];
|
||||||
|
} else if (nz === -1) { // Back (Z-)
|
||||||
|
v1 = [ hs, -hy, -hs]; v2 = [-hs, -hy, -hs]; v3 = [-hs, hy, -hs]; v4 = [ hs, hy, -hs];
|
||||||
|
} else {
|
||||||
|
v1=[0,0,0]; v2=[0,0,0]; v3=[0,0,0]; v4=[0,0,0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyPos = (vtx: number[]) => [vtx[0] + x, vtx[1] + centerY, vtx[2] + z];
|
||||||
|
const p1 = applyPos(v1);
|
||||||
|
const p2 = applyPos(v2);
|
||||||
|
const p3 = applyPos(v3);
|
||||||
|
const p4 = applyPos(v4);
|
||||||
|
|
||||||
|
// Push 2 triangles (CCW winding)
|
||||||
|
positions.push(...p1, ...p2, ...p3);
|
||||||
|
positions.push(...p1, ...p3, ...p4);
|
||||||
|
|
||||||
|
// Normals
|
||||||
|
for(let k=0; k<6; k++) normals.push(nx, ny, nz);
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
for(let k=0; k<6; k++) colors.push(col.r, col.g, col.b);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const bufGeom = new BufferGeometry();
|
||||||
|
bufGeom.setAttribute('position', new Float32BufferAttribute(positions, 3));
|
||||||
|
bufGeom.setAttribute('normal', new Float32BufferAttribute(normals, 3));
|
||||||
|
bufGeom.setAttribute('color', new Float32BufferAttribute(colors, 3));
|
||||||
|
|
||||||
|
return bufGeom;
|
||||||
|
}, [data, isHighRes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
ref={meshRef}
|
||||||
|
geometry={geometry}
|
||||||
|
material={material}
|
||||||
|
castShadow
|
||||||
|
receiveShadow
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const VoxelLayer: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onClick }) => {
|
const VoxelLayer: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onClick }) => {
|
||||||
const meshRef = useRef<InstancedMesh<any, any> | null>(null);
|
const meshRef = useRef<InstancedMesh<any, any> | null>(null);
|
||||||
const setHoveredTile = useUnitStore(state => state.setHoveredTile);
|
const setHoveredTile = useUnitStore(state => state.setHoveredTile);
|
||||||
@@ -574,10 +911,10 @@ const VoxelLayer: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onClick
|
|||||||
};
|
};
|
||||||
} else if (type === 'ice') {
|
} else if (type === 'ice') {
|
||||||
return {
|
return {
|
||||||
roughness: 0.1,
|
roughness: 0.95, // 提高粗糙度,降低反射强度
|
||||||
metalness: 0.1,
|
metalness: 0.01, // 降低金属感
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.8
|
opacity: 0.60
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -647,6 +984,21 @@ export const ChunkRenderer: React.FC<ChunkRendererProps> = ({ onVoxelClick }) =>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 特殊处理冰块类型:使用 Face Culling 网格来消除内部面,解决半透明颗粒感
|
||||||
|
// 包括:ice(冰湖)、icicle(冰锥)、ice_boulder(冰块)、packed_ice(压缩冰)
|
||||||
|
if (type === 'ice' || type === 'icicle' || type === 'ice_boulder' || type === 'packed_ice') {
|
||||||
|
return (
|
||||||
|
<IceMesh
|
||||||
|
key={key}
|
||||||
|
isHighRes={isHighRes}
|
||||||
|
type={type as VoxelType}
|
||||||
|
// @ts-ignore: voxels contain ix/iy/iz but type def in props might be strict
|
||||||
|
data={groupedVoxels[key]}
|
||||||
|
onClick={onVoxelClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VoxelLayer
|
<VoxelLayer
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { VoxelData } from './terrain';
|
import type { VoxelData } from './terrain';
|
||||||
import { rgbToHex } from './voxelStyles';
|
import { rgbToHex } from './voxelStyles';
|
||||||
|
import type { SnowyMountainContext } from './snowyMountainFeatures';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新的植被生成系统
|
* 新的植被生成系统
|
||||||
@@ -21,6 +22,8 @@ export interface VegetationGenerationContext {
|
|||||||
mountainStreamMap?: Map<string, { depth: number; waterHeight: number; flowDir?: number }>;
|
mountainStreamMap?: Map<string, { depth: number; waterHeight: number; flowDir?: number }>;
|
||||||
mountainRockContext?: { stoneHeight: number[][]; stoneDepth: number[][] };
|
mountainRockContext?: { stoneHeight: number[][]; stoneDepth: number[][] };
|
||||||
terrainHeightMap?: number[][]; // 逻辑层级的高度图 (mapSize x mapSize)
|
terrainHeightMap?: number[][]; // 逻辑层级的高度图 (mapSize x mapSize)
|
||||||
|
// 雪山场景专用数据
|
||||||
|
snowyMountainContext?: SnowyMountainContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
@@ -46,7 +49,7 @@ const C: Record<string, string> = {
|
|||||||
|
|
||||||
// Ice/Tundra (Balanced greens)
|
// Ice/Tundra (Balanced greens)
|
||||||
iceBase: '#E0FFFF', iceDeep: '#AEEEEE', iceSpike: '#F0FFFF',
|
iceBase: '#E0FFFF', iceDeep: '#AEEEEE', iceSpike: '#F0FFFF',
|
||||||
snow: '#FFFFFF',
|
snow: '#FFFFFF', packed_ice: '#A5D5E5',
|
||||||
lichenOrange: '#D98E32', lichenRed: '#A64B4B', lichenGreen: '#6B855E',
|
lichenOrange: '#D98E32', lichenRed: '#A64B4B', lichenGreen: '#6B855E',
|
||||||
frozenGreen: '#92C4B5', frozenStem: '#56757A',
|
frozenGreen: '#92C4B5', frozenStem: '#56757A',
|
||||||
crystalBlue: '#7FFFD4', crystalWhite: '#EFFFFF',
|
crystalBlue: '#7FFFD4', crystalWhite: '#EFFFFF',
|
||||||
@@ -2522,18 +2525,357 @@ const createCoralTree = (builder: VoxelBuilder, ox: number, oz: number) => {
|
|||||||
builder.add(ox, 0, oz, C.coralBlue);
|
builder.add(ox, 0, oz, C.coralBlue);
|
||||||
branch(ox, 0, oz, 8, 0, 0, 2);
|
branch(ox, 0, oz, 8, 0, 0, 2);
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 中型冰锥
|
||||||
|
* 高度: 8-14 体素
|
||||||
|
*/
|
||||||
const createIceSpike = (builder: VoxelBuilder, ox: number, oz: number) => {
|
const createIceSpike = (builder: VoxelBuilder, ox: number, oz: number) => {
|
||||||
let h = 4 + Math.random() * 4;
|
const h = 8 + Math.random() * 6; // 8-14 高度
|
||||||
|
const baseR = 2 + Math.random() * 1; // 2-3 底部半径
|
||||||
|
|
||||||
for (let y = 0; y < h; y++) {
|
for (let y = 0; y < h; y++) {
|
||||||
let r = (h - y) * 0.4;
|
const t = y / h;
|
||||||
for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) {
|
const r = baseR * (1 - t * 0.9);
|
||||||
for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) {
|
const rCeil = Math.ceil(r);
|
||||||
if (x * x + z * z <= r * r) builder.add(ox + x, y, oz + z, C.iceSpike);
|
|
||||||
|
for (let x = -rCeil; x <= rCeil; x++) {
|
||||||
|
for (let z = -rCeil; z <= rCeil; z++) {
|
||||||
|
const d2 = x * x + z * z;
|
||||||
|
if (d2 <= r * r) {
|
||||||
|
// 颜色渐变
|
||||||
|
let col: string;
|
||||||
|
if (t > 0.8) {
|
||||||
|
col = '#F0FFFF';
|
||||||
|
} else if (t > 0.5) {
|
||||||
|
col = C.iceSpike;
|
||||||
|
} else {
|
||||||
|
col = Math.random() > 0.5 ? C.iceDeep : C.packed_ice;
|
||||||
|
}
|
||||||
|
builder.add(ox + x, y, oz + z, col);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// --- SNOWY MOUNTAIN (积雪山地专用) ---
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积雪针叶树:在 DensePine 基础上添加积雪
|
||||||
|
* 保持原有树形,但在树冠顶部和边缘添加白雪
|
||||||
|
*/
|
||||||
|
const createSnowyPine = (builder: VoxelBuilder, ox: number, oz: number) => {
|
||||||
|
// 高度范围(比原版稍矮,适应雪山环境)
|
||||||
|
const h = 28 + Math.random() * 14;
|
||||||
|
|
||||||
|
// 雪覆盖率(40-70%,部分露出绿叶)
|
||||||
|
const snowCoverage = 0.4 + Math.random() * 0.3;
|
||||||
|
|
||||||
|
// 颜色配置
|
||||||
|
const pineColors = [C.pineDeepBoreal, C.pineMidBoreal, C.pineLightBoreal];
|
||||||
|
|
||||||
|
// 1. 树根
|
||||||
|
drawCylinder(builder, ox, -4, oz, 4, 2.0, C.woodOak);
|
||||||
|
|
||||||
|
// 2. 树干(贯穿整棵树)
|
||||||
|
const trunkH = h - 2;
|
||||||
|
const baseR = 1.4 + (h - 28) / 30.0;
|
||||||
|
|
||||||
|
for (let y = 0; y < trunkH; y++) {
|
||||||
|
const t = y / trunkH;
|
||||||
|
const r = baseR * (1 - t * 0.5);
|
||||||
|
for (let dx = -Math.ceil(r); dx <= Math.ceil(r); dx++) {
|
||||||
|
for (let dz = -Math.ceil(r); dz <= Math.ceil(r); dz++) {
|
||||||
|
if (dx * dx + dz * dz <= r * r) {
|
||||||
|
builder.add(ox + dx, y, oz + dz, C.woodOak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 分层锥形树冠(带积雪)
|
||||||
|
const numLayers = h > 38 ? 4 : 3;
|
||||||
|
const foliageStartH = h * 0.3;
|
||||||
|
const totalFoliageH = h - foliageStartH;
|
||||||
|
const layerHeight = (totalFoliageH / numLayers) * 1.2;
|
||||||
|
|
||||||
|
for (let i = 0; i < numLayers; i++) {
|
||||||
|
const t = i / (numLayers - 1 || 1);
|
||||||
|
const startY = foliageStartH + i * (totalFoliageH / numLayers * 0.85);
|
||||||
|
const maxR = 7.0 + (h - 28) / 6.0;
|
||||||
|
const bottomR = maxR * (1 - t * 0.6);
|
||||||
|
|
||||||
|
// 绘制树冠锥形,带积雪
|
||||||
|
for (let y = 0; y < layerHeight; y++) {
|
||||||
|
const layerT = y / layerHeight;
|
||||||
|
const r = bottomR * (1 - layerT * 0.8);
|
||||||
|
const rCeil = Math.ceil(r);
|
||||||
|
|
||||||
|
for (let x = -rCeil; x <= rCeil; x++) {
|
||||||
|
for (let z = -rCeil; z <= rCeil; z++) {
|
||||||
|
const d2 = x * x + z * z;
|
||||||
|
const noise = (Math.random() - 0.5) * 0.4 * r;
|
||||||
|
|
||||||
|
if (d2 <= (r + noise) * (r + noise)) {
|
||||||
|
const finalY = Math.round(startY + y);
|
||||||
|
|
||||||
|
// 决定颜色:顶部和边缘更容易有雪
|
||||||
|
const isTopOfLayer = y >= layerHeight - 2;
|
||||||
|
const isEdge = d2 > r * r * 0.5;
|
||||||
|
const snowChance = isTopOfLayer ? snowCoverage + 0.2 :
|
||||||
|
isEdge ? snowCoverage : snowCoverage * 0.3;
|
||||||
|
|
||||||
|
let col: string;
|
||||||
|
if (Math.random() < snowChance) {
|
||||||
|
// 积雪:白色或接近白色
|
||||||
|
col = Math.random() > 0.3 ? C.snow : '#F0F8FF';
|
||||||
|
} else {
|
||||||
|
// 绿叶:深浅不同的针叶绿
|
||||||
|
col = pineColors[Math.floor(Math.random() * pineColors.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(ox + x, finalY, oz + z, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 树尖(通常有雪)
|
||||||
|
builder.add(ox, h, oz, C.snow);
|
||||||
|
builder.add(ox, h + 1, oz, C.snow);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重雪针叶树:雪覆盖更厚的版本
|
||||||
|
*/
|
||||||
|
const createHeavySnowyPine = (builder: VoxelBuilder, ox: number, oz: number) => {
|
||||||
|
const h = 24 + Math.random() * 12;
|
||||||
|
const snowCoverage = 0.6 + Math.random() * 0.25; // 60-85% 雪覆盖
|
||||||
|
|
||||||
|
// 树根和树干
|
||||||
|
drawCylinder(builder, ox, -3, oz, 3, 1.8, C.woodDark);
|
||||||
|
const trunkH = h - 2;
|
||||||
|
const baseR = 1.2;
|
||||||
|
|
||||||
|
for (let y = 0; y < trunkH; y++) {
|
||||||
|
const t = y / trunkH;
|
||||||
|
const r = baseR * (1 - t * 0.4);
|
||||||
|
for (let dx = -Math.ceil(r); dx <= Math.ceil(r); dx++) {
|
||||||
|
for (let dz = -Math.ceil(r); dz <= Math.ceil(r); dz++) {
|
||||||
|
if (dx * dx + dz * dz <= r * r) {
|
||||||
|
builder.add(ox + dx, y, oz + dz, C.woodDark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树冠(更圆润,像被雪压弯)
|
||||||
|
const numLayers = 4;
|
||||||
|
const foliageStartH = h * 0.25;
|
||||||
|
const totalFoliageH = h - foliageStartH;
|
||||||
|
|
||||||
|
for (let i = 0; i < numLayers; i++) {
|
||||||
|
const t = i / (numLayers - 1 || 1);
|
||||||
|
const startY = foliageStartH + i * (totalFoliageH / numLayers * 0.9);
|
||||||
|
const maxR = 6.0 + (h - 24) / 5.0;
|
||||||
|
const bottomR = maxR * (1 - t * 0.5);
|
||||||
|
const layerHeight = (totalFoliageH / numLayers) * 1.1;
|
||||||
|
|
||||||
|
for (let y = 0; y < layerHeight; y++) {
|
||||||
|
const layerT = y / layerHeight;
|
||||||
|
const r = bottomR * (1 - layerT * 0.7);
|
||||||
|
const rCeil = Math.ceil(r);
|
||||||
|
|
||||||
|
for (let x = -rCeil; x <= rCeil; x++) {
|
||||||
|
for (let z = -rCeil; z <= rCeil; z++) {
|
||||||
|
const d2 = x * x + z * z;
|
||||||
|
const noise = (Math.random() - 0.5) * 0.3 * r;
|
||||||
|
|
||||||
|
if (d2 <= (r + noise) * (r + noise)) {
|
||||||
|
const finalY = Math.round(startY + y);
|
||||||
|
const isTop = y >= layerHeight - 1;
|
||||||
|
const isUpperHalf = y >= layerHeight / 2;
|
||||||
|
|
||||||
|
// 重雪版本:顶部几乎全白
|
||||||
|
const snowChance = isTop ? 0.95 : isUpperHalf ? snowCoverage + 0.15 : snowCoverage * 0.5;
|
||||||
|
|
||||||
|
let col: string;
|
||||||
|
if (Math.random() < snowChance) {
|
||||||
|
col = C.snow;
|
||||||
|
} else {
|
||||||
|
col = Math.random() > 0.5 ? C.pineDeepBoreal : C.pineMidBoreal;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(ox + x, finalY, oz + z, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树尖
|
||||||
|
builder.add(ox, h, oz, C.snow);
|
||||||
|
builder.add(ox, h + 1, oz, C.snow);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积雪灌木(中型,适配大型针叶树)
|
||||||
|
* 高度: 10-16 体素,半径: 6-10 体素
|
||||||
|
*/
|
||||||
|
const createSnowyShrub = (builder: VoxelBuilder, ox: number, oz: number) => {
|
||||||
|
const h = 10 + Math.random() * 6; // 10-16 高度
|
||||||
|
const r = 6 + Math.random() * 4; // 6-10 半径
|
||||||
|
const snowCoverage = 0.25 + Math.random() * 0.2; // 降低雪覆盖率:25%-45%
|
||||||
|
|
||||||
|
// 灌木颜色
|
||||||
|
const shrubColors = [C.pineDeepBoreal, C.pineMidBoreal, C.pineLightBoreal];
|
||||||
|
|
||||||
|
// 短粗树干(2-3 高)
|
||||||
|
const trunkH = 2 + Math.floor(Math.random() * 2);
|
||||||
|
for (let y = 0; y < trunkH; y++) {
|
||||||
|
const trunkR = 1 + (1 - y / trunkH) * 0.5;
|
||||||
|
for (let dx = -Math.ceil(trunkR); dx <= Math.ceil(trunkR); dx++) {
|
||||||
|
for (let dz = -Math.ceil(trunkR); dz <= Math.ceil(trunkR); dz++) {
|
||||||
|
if (dx * dx + dz * dz <= trunkR * trunkR) {
|
||||||
|
builder.add(ox + dx, y, oz + dz, C.woodDark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灌木主体(圆润的半球形,带积雪)
|
||||||
|
const foliageStartY = trunkH - 1;
|
||||||
|
const foliageH = h - foliageStartY;
|
||||||
|
|
||||||
|
for (let y = 0; y < foliageH; y++) {
|
||||||
|
// 使用椭球形状,底部较宽,顶部收窄
|
||||||
|
const t = y / foliageH;
|
||||||
|
const layerR = r * Math.sin((0.2 + t * 0.8) * Math.PI * 0.5) * (1 - t * 0.4);
|
||||||
|
const rCeil = Math.ceil(layerR);
|
||||||
|
|
||||||
|
for (let x = -rCeil; x <= rCeil; x++) {
|
||||||
|
for (let z = -rCeil; z <= rCeil; z++) {
|
||||||
|
const d2 = x * x + z * z;
|
||||||
|
const noise = (Math.random() - 0.5) * layerR * 0.4;
|
||||||
|
|
||||||
|
if (d2 <= (layerR + noise) * (layerR + noise)) {
|
||||||
|
const finalY = foliageStartY + y;
|
||||||
|
|
||||||
|
// 顶部和边缘积雪更多(减少雪量,让绿色更明显)
|
||||||
|
const isTop = y >= foliageH - 2;
|
||||||
|
const isEdge = d2 > layerR * layerR * 0.5;
|
||||||
|
const snowChance = isTop ? snowCoverage + 0.15 :
|
||||||
|
isEdge ? snowCoverage : snowCoverage * 0.3;
|
||||||
|
|
||||||
|
let col: string;
|
||||||
|
if (Math.random() < snowChance) {
|
||||||
|
col = Math.random() > 0.2 ? C.snow : '#F0F8FF';
|
||||||
|
} else {
|
||||||
|
col = shrubColors[Math.floor(Math.random() * shrubColors.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(ox + x, finalY, oz + z, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大型冰锥(用于雪山场景的冰锥特征)
|
||||||
|
* 高度: 16-28 体素,底部半径: 3-5 体素
|
||||||
|
*/
|
||||||
|
const createLargeIceSpike = (builder: VoxelBuilder, ox: number, oz: number) => {
|
||||||
|
const h = 16 + Math.random() * 12; // 16-28 高度
|
||||||
|
const baseR = 3 + Math.random() * 2; // 3-5 底部半径
|
||||||
|
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const t = y / h;
|
||||||
|
const r = baseR * (1 - t * 0.92); // 从底部到顶部逐渐变细
|
||||||
|
const rCeil = Math.ceil(r);
|
||||||
|
|
||||||
|
for (let x = -rCeil; x <= rCeil; x++) {
|
||||||
|
for (let z = -rCeil; z <= rCeil; z++) {
|
||||||
|
const d2 = x * x + z * z;
|
||||||
|
const noise = (Math.random() - 0.5) * 0.35 * r;
|
||||||
|
|
||||||
|
if (d2 <= (r + noise) * (r + noise)) {
|
||||||
|
// 冰的颜色:底部更深,顶部更透明
|
||||||
|
let col: string;
|
||||||
|
if (t > 0.85) {
|
||||||
|
col = '#F0FFFF'; // 顶部接近透明白
|
||||||
|
} else if (t > 0.6) {
|
||||||
|
col = C.iceSpike;
|
||||||
|
} else if (t > 0.3) {
|
||||||
|
col = Math.random() > 0.4 ? C.iceDeep : C.packed_ice;
|
||||||
|
} else {
|
||||||
|
// 底部更深的冰色
|
||||||
|
col = Math.random() > 0.5 ? C.packed_ice : C.iceDeep;
|
||||||
|
}
|
||||||
|
builder.add(ox + x, y, oz + z, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冰块/冰石(大型)
|
||||||
|
* 尺寸: 6-12 体素
|
||||||
|
*/
|
||||||
|
const createIceBoulder = (builder: VoxelBuilder, ox: number, oz: number) => {
|
||||||
|
const size = 6 + Math.random() * 6; // 6-12 尺寸
|
||||||
|
const stretchY = 0.5 + Math.random() * 0.4; // 稍微扁平
|
||||||
|
|
||||||
|
// 不规则的冰石形状
|
||||||
|
for (let x = -Math.ceil(size); x <= Math.ceil(size); x++) {
|
||||||
|
for (let y = -Math.ceil(size * stretchY); y <= Math.ceil(size * stretchY); y++) {
|
||||||
|
for (let z = -Math.ceil(size); z <= Math.ceil(size); z++) {
|
||||||
|
const d2 = (x * x) + (y / stretchY) * (y / stretchY) + (z * z);
|
||||||
|
const noise = (Math.random() - 0.5) * size * 0.5;
|
||||||
|
|
||||||
|
if (d2 <= (size + noise) * (size + noise) * 0.85) {
|
||||||
|
// 随机冰色变化
|
||||||
|
const edgeDist = Math.sqrt(d2) / size;
|
||||||
|
let col: string;
|
||||||
|
if (edgeDist > 0.75) {
|
||||||
|
// 边缘更亮
|
||||||
|
col = Math.random() > 0.4 ? C.iceSpike : '#E0FFFF';
|
||||||
|
} else if (edgeDist > 0.4) {
|
||||||
|
col = Math.random() > 0.5 ? C.iceDeep : C.packed_ice;
|
||||||
|
} else {
|
||||||
|
// 中心更深
|
||||||
|
col = Math.random() > 0.7 ? C.packed_ice : C.iceDeep;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalY = Math.max(0, y + Math.floor(size * stretchY * 0.6));
|
||||||
|
builder.add(ox + x, finalY, oz + z, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 雪山专用生成器列表
|
||||||
|
const SNOWY_MOUNTAIN_TREE_GENERATORS = [
|
||||||
|
createSnowyPine, createSnowyPine, createSnowyPine, // 积雪针叶树 x3
|
||||||
|
createHeavySnowyPine, createHeavySnowyPine, // 重雪针叶树 x2
|
||||||
|
createSpruceTallSnowy, // 积雪云杉 x1
|
||||||
|
];
|
||||||
|
|
||||||
|
const SNOWY_MOUNTAIN_SHRUB_GENERATORS = [
|
||||||
|
createSnowyShrub, createSnowyShrub, createSnowyShrub,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SNOWY_MOUNTAIN_ICE_GENERATORS = [
|
||||||
|
createLargeIceSpike, createLargeIceSpike,
|
||||||
|
createIceBoulder,
|
||||||
|
createIceSpike,
|
||||||
|
];
|
||||||
|
|
||||||
// --- BEACH (Tropical) ---
|
// --- BEACH (Tropical) ---
|
||||||
const createPalm = (builder: VoxelBuilder, ox: number, oz: number, height: number, leanType: 'straight' | 'slight') => {
|
const createPalm = (builder: VoxelBuilder, ox: number, oz: number, height: number, leanType: 'straight' | 'slight') => {
|
||||||
let leanX = 0, leanZ = 0;
|
let leanX = 0, leanZ = 0;
|
||||||
@@ -3446,7 +3788,6 @@ const MOUNTAIN_TREE_GENERATORS = [createLushOak, createProceduralBirch, createDe
|
|||||||
|
|
||||||
const FOREST_GENERATORS = [createLushOak, createProceduralBirch, createDensePine, createFatPoplar, createScotsPine];
|
const FOREST_GENERATORS = [createLushOak, createProceduralBirch, createDensePine, createFatPoplar, createScotsPine];
|
||||||
const SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom];
|
const SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom];
|
||||||
const TUNDRA_GENERATORS = [createSpruceTallSnowy, createSpruceMediumClear, createLarch];
|
|
||||||
const ICE_GENERATORS = [createCoralTree, createIceSpike];
|
const ICE_GENERATORS = [createCoralTree, createIceSpike];
|
||||||
const BEACH_GENERATORS = [createTwinPalms, createMangrove, createBanana, (b: any, x: any, z: any) => createPalm(b, x, z, 20, 'straight')];
|
const BEACH_GENERATORS = [createTwinPalms, createMangrove, createBanana, (b: any, x: any, z: any) => createPalm(b, x, z, 20, 'straight')];
|
||||||
|
|
||||||
@@ -3525,13 +3866,16 @@ export const generateVegetation = async (
|
|||||||
generators = SWAMP_GENERATORS;
|
generators = SWAMP_GENERATORS;
|
||||||
density = 0.2; // Very dense
|
density = 0.2; // Very dense
|
||||||
break;
|
break;
|
||||||
case 'tundra':
|
|
||||||
case 'snowy_mountain':
|
case 'snowy_mountain':
|
||||||
generators = [...TUNDRA_GENERATORS, ...ICE_GENERATORS];
|
// 雪山场景:积雪针叶树 + 灌木 + 冰锥/冰块
|
||||||
density = 0.1;
|
generators = [
|
||||||
|
...SNOWY_MOUNTAIN_TREE_GENERATORS,
|
||||||
|
...SNOWY_MOUNTAIN_SHRUB_GENERATORS,
|
||||||
|
...SNOWY_MOUNTAIN_ICE_GENERATORS
|
||||||
|
];
|
||||||
|
density = 0.12;
|
||||||
break;
|
break;
|
||||||
case 'beach':
|
case 'beach':
|
||||||
case 'riverside':
|
|
||||||
generators = BEACH_GENERATORS;
|
generators = BEACH_GENERATORS;
|
||||||
density = 0.12;
|
density = 0.12;
|
||||||
break;
|
break;
|
||||||
@@ -4758,6 +5102,417 @@ export const generateVegetation = async (
|
|||||||
|
|
||||||
console.log(`Mountain placed: forests=${forestClustersCreated}, trees=${treesPlaced}, shrubs=${shrubsPlaced}, groundCover=${groundCoverPlaced}, riverGrass=${riparianGrassPlaced}, riparianPlants=${riparianPlantsPlaced}, mushrooms=${mushroomsPlaced}, randomPlants=${randomPlantsPlaced}`);
|
console.log(`Mountain placed: forests=${forestClustersCreated}, trees=${treesPlaced}, shrubs=${shrubsPlaced}, groundCover=${groundCoverPlaced}, riverGrass=${riparianGrassPlaced}, riparianPlants=${riparianPlantsPlaced}, mushrooms=${mushroomsPlaced}, randomPlants=${randomPlantsPlaced}`);
|
||||||
|
|
||||||
|
} else if (sceneType === 'snowy_mountain') {
|
||||||
|
// ============================================================================
|
||||||
|
// 雪山场景:种群特征生成
|
||||||
|
// 1. 针叶林种群:多棵积雪针叶树聚集成林
|
||||||
|
// 2. 冰锥群:冰锥聚集在低洼或特定区域
|
||||||
|
// 3. 冰块群:散落的大型冰块
|
||||||
|
// 4. 边缘灌木:在森林边缘生成积雪灌木
|
||||||
|
// ============================================================================
|
||||||
|
console.log(`Snowy mountain vegetation: generating with forest clusters and ice formations`);
|
||||||
|
|
||||||
|
// 从 context 获取雪山专用数据
|
||||||
|
const { snowyMountainContext, terrainHeightMap } = context;
|
||||||
|
|
||||||
|
// 统计计数器
|
||||||
|
let forestClustersCreated = 0;
|
||||||
|
let treesPlaced = 0;
|
||||||
|
let shrubsPlaced = 0;
|
||||||
|
let iceSpikeGroupsCreated = 0;
|
||||||
|
let iceSpikesPlaced = 0;
|
||||||
|
let iceBoulderGroupsCreated = 0;
|
||||||
|
let iceBouldersPlaced = 0;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 辅助函数
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 检查位置是否有冰湖
|
||||||
|
const isInFrozenLake = (lx: number, lz: number): boolean => {
|
||||||
|
if (!snowyMountainContext) return false;
|
||||||
|
const mx = lx * 8 + 4;
|
||||||
|
const mz = lz * 8 + 4;
|
||||||
|
return snowyMountainContext.frozenLakeMap.has(`${mx}|${mz}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查位置是否有岩石/冰锥/冰块(从 snowyMountainContext)
|
||||||
|
const hasFeature = (lx: number, lz: number): boolean => {
|
||||||
|
if (!snowyMountainContext) return false;
|
||||||
|
// 检查岩石
|
||||||
|
const stoneH = snowyMountainContext.rockContext.stoneHeight[lx]?.[lz] ?? 0;
|
||||||
|
if (stoneH > 0) return true;
|
||||||
|
// 检查冰锥
|
||||||
|
for (const spike of snowyMountainContext.iceSpikePositions) {
|
||||||
|
if (spike.lx === lx && spike.lz === lz) return true;
|
||||||
|
}
|
||||||
|
// 检查冰块
|
||||||
|
for (const boulder of snowyMountainContext.iceBoulderPositions) {
|
||||||
|
if (boulder.lx === lx && boulder.lz === lz) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打乱数组
|
||||||
|
const shuffleArray = <T>(arr: T[]): T[] => {
|
||||||
|
const result = [...arr];
|
||||||
|
for (let i = result.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[result[i], result[j]] = [result[j], result[i]];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 收集所有可用位置
|
||||||
|
// ==========================================
|
||||||
|
interface SnowyTileInfo {
|
||||||
|
lx: number;
|
||||||
|
lz: number;
|
||||||
|
surfaceY: number;
|
||||||
|
microX: number;
|
||||||
|
microZ: number;
|
||||||
|
terrainY: number;
|
||||||
|
isFrozenLake: boolean;
|
||||||
|
hasFeature: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTiles: SnowyTileInfo[] = [];
|
||||||
|
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
const hKey = `${lx*8+4}|${lz*8+4}`;
|
||||||
|
const terrainY = heightMap.get(hKey);
|
||||||
|
if (terrainY === undefined) continue;
|
||||||
|
|
||||||
|
// 雪面有 ±2 微体素的高度变化,向下偏移 2 确保树根接触地面
|
||||||
|
// surfaceY 计算:(terrainY + 1) * 4 是基础高度,-2 补偿雪面高度变化
|
||||||
|
const surfaceY = (terrainY + 1) * 4 - 2;
|
||||||
|
const microX = lx * 32 + 16;
|
||||||
|
const microZ = lz * 32 + 16;
|
||||||
|
|
||||||
|
allTiles.push({
|
||||||
|
lx, lz, surfaceY, microX, microZ, terrainY,
|
||||||
|
isFrozenLake: isInFrozenLake(lx, lz),
|
||||||
|
hasFeature: hasFeature(lx, lz)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可用于植被生成的位置(排除冰湖和已有地物)
|
||||||
|
const availableTiles = allTiles.filter(t => !t.isFrozenLake && !t.hasFeature);
|
||||||
|
|
||||||
|
// 计算高度分布
|
||||||
|
const heights = availableTiles.map(p => p.terrainY);
|
||||||
|
const minHeight = Math.min(...heights);
|
||||||
|
const maxHeight = Math.max(...heights);
|
||||||
|
const heightRange = maxHeight - minHeight || 1;
|
||||||
|
|
||||||
|
// 低洼区域(用于冰锥群)
|
||||||
|
const lowAltitude = minHeight + heightRange * 0.4;
|
||||||
|
|
||||||
|
console.log(`Snowy terrain: ${availableTiles.length} available tiles, height range: ${minHeight}-${maxHeight}`);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. 针叶林种群生成
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 针叶树生成器(全部使用积雪版本)
|
||||||
|
const SNOWY_CONIFER_TREES = [
|
||||||
|
createSnowyPine, createSnowyPine, createSnowyPine,
|
||||||
|
createHeavySnowyPine, createHeavySnowyPine,
|
||||||
|
createSpruceTallSnowy,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算森林数量(根据地图大小)
|
||||||
|
// 16x16: 2-3 个,24x24: 3-4 个,32x32: 4-5 个
|
||||||
|
const targetForestCount = Math.max(2, Math.min(5, Math.floor(mapSize / 8) + 1));
|
||||||
|
|
||||||
|
// 最小森林间距
|
||||||
|
const minForestSpacing = Math.max(4, Math.floor(mapSize / 5));
|
||||||
|
|
||||||
|
// 森林聚集点
|
||||||
|
interface SnowyForestCluster {
|
||||||
|
centerX: number;
|
||||||
|
centerZ: number;
|
||||||
|
radius: number;
|
||||||
|
tiles: SnowyTileInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const forestClusters: SnowyForestCluster[] = [];
|
||||||
|
|
||||||
|
// 随机打乱 tiles,然后选择森林中心
|
||||||
|
const shuffledTiles = shuffleArray(availableTiles);
|
||||||
|
|
||||||
|
for (const tile of shuffledTiles) {
|
||||||
|
if (forestClusters.length >= targetForestCount) break;
|
||||||
|
|
||||||
|
// 检查与已有森林的距离
|
||||||
|
let tooClose = false;
|
||||||
|
for (const existing of forestClusters) {
|
||||||
|
const dx = tile.lx - existing.centerX;
|
||||||
|
const dz = tile.lz - existing.centerZ;
|
||||||
|
if (Math.sqrt(dx * dx + dz * dz) < minForestSpacing) {
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooClose) continue;
|
||||||
|
|
||||||
|
// 森林半径:2-3 tiles
|
||||||
|
const radius = 2 + Math.floor(Math.random() * 2);
|
||||||
|
|
||||||
|
forestClusters.push({
|
||||||
|
centerX: tile.lx,
|
||||||
|
centerZ: tile.lz,
|
||||||
|
radius,
|
||||||
|
tiles: []
|
||||||
|
});
|
||||||
|
forestClustersCreated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Created ${forestClustersCreated} snowy forest clusters`);
|
||||||
|
|
||||||
|
// 记录被森林占用的 tiles
|
||||||
|
const tilesUsedByForest = new Set<string>();
|
||||||
|
|
||||||
|
// 为每个森林收集 tiles 并生成树木
|
||||||
|
for (const forest of forestClusters) {
|
||||||
|
// 收集森林范围内的 tiles
|
||||||
|
for (const tile of availableTiles) {
|
||||||
|
const dx = tile.lx - forest.centerX;
|
||||||
|
const dz = tile.lz - forest.centerZ;
|
||||||
|
const dist = Math.sqrt(dx * dx + dz * dz);
|
||||||
|
|
||||||
|
if (dist <= forest.radius) {
|
||||||
|
forest.tiles.push(tile);
|
||||||
|
tilesUsedByForest.add(`${tile.lx}|${tile.lz}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在森林中生成树木
|
||||||
|
for (const tile of forest.tiles) {
|
||||||
|
const dx = tile.lx - forest.centerX;
|
||||||
|
const dz = tile.lz - forest.centerZ;
|
||||||
|
const distFromCenter = Math.sqrt(dx * dx + dz * dz);
|
||||||
|
const normalizedDist = forest.radius > 0 ? distFromCenter / forest.radius : 0;
|
||||||
|
|
||||||
|
// 边缘判定
|
||||||
|
const isEdge = normalizedDist > 0.6;
|
||||||
|
|
||||||
|
// 密度:中心 100%,边缘 80%
|
||||||
|
const density = isEdge ? 0.80 : 1.0;
|
||||||
|
|
||||||
|
if (Math.random() < density) {
|
||||||
|
// 选择树木生成器
|
||||||
|
const gen = SNOWY_CONIFER_TREES[Math.floor(Math.random() * SNOWY_CONIFER_TREES.length)];
|
||||||
|
|
||||||
|
// 随机偏移
|
||||||
|
const offsetX = (Math.random() - 0.5) * 16;
|
||||||
|
const offsetZ = (Math.random() - 0.5) * 16;
|
||||||
|
|
||||||
|
builder.setOffset(tile.microX + offsetX, tile.surfaceY, tile.microZ + offsetZ);
|
||||||
|
try {
|
||||||
|
gen(builder, 0, 0);
|
||||||
|
builder.commit();
|
||||||
|
treesPlaced++;
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 边缘生成灌木
|
||||||
|
if (isEdge && Math.random() < 0.6) {
|
||||||
|
const shrubOffsetX = (Math.random() - 0.5) * 24;
|
||||||
|
const shrubOffsetZ = (Math.random() - 0.5) * 24;
|
||||||
|
|
||||||
|
builder.setOffset(tile.microX + shrubOffsetX, tile.surfaceY, tile.microZ + shrubOffsetZ);
|
||||||
|
try {
|
||||||
|
createSnowyShrub(builder, 0, 0);
|
||||||
|
builder.commit();
|
||||||
|
shrubsPlaced++;
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. 冰锥群生成
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 冰锥群数量:2-3 个(根据地图大小)
|
||||||
|
const targetIceSpikeGroups = Math.max(2, Math.min(4, Math.floor(mapSize / 10) + 1));
|
||||||
|
|
||||||
|
// 优先选择低洼区域
|
||||||
|
const lowTiles = availableTiles.filter(t =>
|
||||||
|
t.terrainY <= lowAltitude &&
|
||||||
|
!tilesUsedByForest.has(`${t.lx}|${t.lz}`)
|
||||||
|
);
|
||||||
|
const shuffledLowTiles = shuffleArray(lowTiles.length > 0 ? lowTiles : availableTiles.filter(t => !tilesUsedByForest.has(`${t.lx}|${t.lz}`)));
|
||||||
|
|
||||||
|
// 最小冰锥群间距
|
||||||
|
const minIceSpikeSpacing = 4;
|
||||||
|
const iceSpikeGroupCenters: { lx: number; lz: number }[] = [];
|
||||||
|
|
||||||
|
for (const tile of shuffledLowTiles) {
|
||||||
|
if (iceSpikeGroupCenters.length >= targetIceSpikeGroups) break;
|
||||||
|
|
||||||
|
// 检查间距
|
||||||
|
let tooClose = false;
|
||||||
|
for (const existing of iceSpikeGroupCenters) {
|
||||||
|
const dx = tile.lx - existing.lx;
|
||||||
|
const dz = tile.lz - existing.lz;
|
||||||
|
if (Math.sqrt(dx * dx + dz * dz) < minIceSpikeSpacing) {
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 也检查与森林的距离
|
||||||
|
for (const forest of forestClusters) {
|
||||||
|
const dx = tile.lx - forest.centerX;
|
||||||
|
const dz = tile.lz - forest.centerZ;
|
||||||
|
if (Math.sqrt(dx * dx + dz * dz) < forest.radius + 2) {
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooClose) continue;
|
||||||
|
|
||||||
|
iceSpikeGroupCenters.push({ lx: tile.lx, lz: tile.lz });
|
||||||
|
iceSpikeGroupsCreated++;
|
||||||
|
|
||||||
|
// 在这个中心点周围生成 3-6 个冰锥
|
||||||
|
const spikeCount = 3 + Math.floor(Math.random() * 4);
|
||||||
|
|
||||||
|
for (let s = 0; s < spikeCount; s++) {
|
||||||
|
// 随机偏移(在 2 tiles 范围内)
|
||||||
|
const offsetLx = Math.floor((Math.random() - 0.5) * 4);
|
||||||
|
const offsetLz = Math.floor((Math.random() - 0.5) * 4);
|
||||||
|
const spikeLx = Math.max(0, Math.min(mapSize - 1, tile.lx + offsetLx));
|
||||||
|
const spikeLz = Math.max(0, Math.min(mapSize - 1, tile.lz + offsetLz));
|
||||||
|
|
||||||
|
// 获取该位置的地面高度
|
||||||
|
const hKey = `${spikeLx*8+4}|${spikeLz*8+4}`;
|
||||||
|
const terrainY = heightMap.get(hKey);
|
||||||
|
if (terrainY === undefined) continue;
|
||||||
|
const surfaceY = (terrainY + 1) * 4;
|
||||||
|
|
||||||
|
const microX = spikeLx * 32 + 16 + (Math.random() - 0.5) * 20;
|
||||||
|
const microZ = spikeLz * 32 + 16 + (Math.random() - 0.5) * 20;
|
||||||
|
|
||||||
|
// 随机选择大型或中型冰锥
|
||||||
|
const gen = Math.random() < 0.6 ? createLargeIceSpike : createIceSpike;
|
||||||
|
|
||||||
|
builder.setOffset(microX, surfaceY, microZ);
|
||||||
|
try {
|
||||||
|
gen(builder, 0, 0);
|
||||||
|
builder.commit();
|
||||||
|
iceSpikesPlaced++;
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Created ${iceSpikeGroupsCreated} ice spike groups with ${iceSpikesPlaced} spikes`);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. 冰块群生成
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 冰块群数量:1-3 个
|
||||||
|
const targetIceBoulderGroups = Math.max(1, Math.min(3, Math.floor(mapSize / 12) + 1));
|
||||||
|
|
||||||
|
// 冰块可以出现在任何空闲区域
|
||||||
|
const remainingTiles = availableTiles.filter(t =>
|
||||||
|
!tilesUsedByForest.has(`${t.lx}|${t.lz}`) &&
|
||||||
|
!iceSpikeGroupCenters.some(c => Math.sqrt((t.lx - c.lx)**2 + (t.lz - c.lz)**2) < 3)
|
||||||
|
);
|
||||||
|
const shuffledRemainingTiles = shuffleArray(remainingTiles);
|
||||||
|
|
||||||
|
const iceBoulderGroupCenters: { lx: number; lz: number }[] = [];
|
||||||
|
|
||||||
|
for (const tile of shuffledRemainingTiles) {
|
||||||
|
if (iceBoulderGroupCenters.length >= targetIceBoulderGroups) break;
|
||||||
|
|
||||||
|
// 检查间距
|
||||||
|
let tooClose = false;
|
||||||
|
for (const existing of iceBoulderGroupCenters) {
|
||||||
|
const dx = tile.lx - existing.lx;
|
||||||
|
const dz = tile.lz - existing.lz;
|
||||||
|
if (Math.sqrt(dx * dx + dz * dz) < 5) {
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooClose) continue;
|
||||||
|
|
||||||
|
iceBoulderGroupCenters.push({ lx: tile.lx, lz: tile.lz });
|
||||||
|
iceBoulderGroupsCreated++;
|
||||||
|
|
||||||
|
// 在这个中心点周围生成 2-4 个冰块
|
||||||
|
const boulderCount = 2 + Math.floor(Math.random() * 3);
|
||||||
|
|
||||||
|
for (let b = 0; b < boulderCount; b++) {
|
||||||
|
// 随机偏移
|
||||||
|
const offsetLx = Math.floor((Math.random() - 0.5) * 3);
|
||||||
|
const offsetLz = Math.floor((Math.random() - 0.5) * 3);
|
||||||
|
const boulderLx = Math.max(0, Math.min(mapSize - 1, tile.lx + offsetLx));
|
||||||
|
const boulderLz = Math.max(0, Math.min(mapSize - 1, tile.lz + offsetLz));
|
||||||
|
|
||||||
|
const hKey = `${boulderLx*8+4}|${boulderLz*8+4}`;
|
||||||
|
const terrainY = heightMap.get(hKey);
|
||||||
|
if (terrainY === undefined) continue;
|
||||||
|
const surfaceY = (terrainY + 1) * 4;
|
||||||
|
|
||||||
|
const microX = boulderLx * 32 + 16 + (Math.random() - 0.5) * 24;
|
||||||
|
const microZ = boulderLz * 32 + 16 + (Math.random() - 0.5) * 24;
|
||||||
|
|
||||||
|
builder.setOffset(microX, surfaceY, microZ);
|
||||||
|
try {
|
||||||
|
createIceBoulder(builder, 0, 0);
|
||||||
|
builder.commit();
|
||||||
|
iceBouldersPlaced++;
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Created ${iceBoulderGroupsCreated} ice boulder groups with ${iceBouldersPlaced} boulders`);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. 散落灌木(在空地上)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 在非森林、非冰锥群、非冰块群的区域散落一些灌木
|
||||||
|
const emptyTiles = availableTiles.filter(t =>
|
||||||
|
!tilesUsedByForest.has(`${t.lx}|${t.lz}`) &&
|
||||||
|
!iceSpikeGroupCenters.some(c => Math.sqrt((t.lx - c.lx)**2 + (t.lz - c.lz)**2) < 3) &&
|
||||||
|
!iceBoulderGroupCenters.some(c => Math.sqrt((t.lx - c.lx)**2 + (t.lz - c.lz)**2) < 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 15% 密度散落灌木
|
||||||
|
for (const tile of emptyTiles) {
|
||||||
|
if (Math.random() < 0.15) {
|
||||||
|
const offsetX = (Math.random() - 0.5) * 24;
|
||||||
|
const offsetZ = (Math.random() - 0.5) * 24;
|
||||||
|
|
||||||
|
builder.setOffset(tile.microX + offsetX, tile.surfaceY, tile.microZ + offsetZ);
|
||||||
|
try {
|
||||||
|
createSnowyShrub(builder, 0, 0);
|
||||||
|
builder.commit();
|
||||||
|
shrubsPlaced++;
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Snowy mountain placed: forests=${forestClustersCreated}, trees=${treesPlaced}, shrubs=${shrubsPlaced}, iceSpikeGroups=${iceSpikeGroupsCreated}, iceSpikes=${iceSpikesPlaced}, iceBoulderGroups=${iceBoulderGroupsCreated}, iceBoulders=${iceBouldersPlaced}`);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// ============= 其他场景:使用原有概率生成逻辑 =============
|
// ============= 其他场景:使用原有概率生成逻辑 =============
|
||||||
for (let lx = 0; lx < mapSize; lx++) {
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
@@ -4768,7 +5523,7 @@ export const generateVegetation = async (
|
|||||||
const streamInfo = desertContext?.streamDepthMap.get(`${centerMicroX}|${centerMicroZ}`);
|
const streamInfo = desertContext?.streamDepthMap.get(`${centerMicroX}|${centerMicroZ}`);
|
||||||
const isStream = (streamInfo?.depth ?? 0) > 0;
|
const isStream = (streamInfo?.depth ?? 0) > 0;
|
||||||
|
|
||||||
if (stoneHeight > 0 && sceneType !== 'tundra' && sceneType !== 'snowy_mountain') continue;
|
if (stoneHeight > 0 && sceneType !== 'snowy_mountain') continue;
|
||||||
if (isStream && sceneType !== 'swamp') continue;
|
if (isStream && sceneType !== 'swamp') continue;
|
||||||
|
|
||||||
const hKey = `${lx*8+4}|${lz*8+4}`;
|
const hKey = `${lx*8+4}|${lz*8+4}`;
|
||||||
|
|||||||
@@ -3,51 +3,13 @@
|
|||||||
* 包含各种对生成的地形进行后处理的算法
|
* 包含各种对生成的地形进行后处理的算法
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型导入 ====================
|
||||||
|
|
||||||
/**
|
// 从 voxelStyles 导入统一的 VoxelType 类型定义
|
||||||
* VoxelType 类型定义(从terrain.ts复制,保持一致)
|
import type { VoxelType } from './voxelStyles';
|
||||||
*/
|
|
||||||
export type VoxelType =
|
// 重新导出以供其他模块使用
|
||||||
| 'water'
|
export type { VoxelType };
|
||||||
| 'sand'
|
|
||||||
| 'grass'
|
|
||||||
| 'stone'
|
|
||||||
| 'snow'
|
|
||||||
| 'dirt'
|
|
||||||
| 'wood'
|
|
||||||
| 'leaves'
|
|
||||||
| 'dark_stone'
|
|
||||||
| 'bedrock'
|
|
||||||
| 'grass_dirt_blend'
|
|
||||||
| 'dark_grass'
|
|
||||||
| 'cactus'
|
|
||||||
| 'deep_dirt'
|
|
||||||
| 'dark_grass_ground'
|
|
||||||
| 'medium_dirt'
|
|
||||||
| 'flower'
|
|
||||||
| 'reed'
|
|
||||||
| 'lava'
|
|
||||||
| 'volcanic_rock'
|
|
||||||
| 'obsidian'
|
|
||||||
| 'ash'
|
|
||||||
| 'magma_stone'
|
|
||||||
| 'mud'
|
|
||||||
| 'murky_water'
|
|
||||||
| 'swamp_grass'
|
|
||||||
| 'moss'
|
|
||||||
| 'lily_pad'
|
|
||||||
| 'ice'
|
|
||||||
| 'packed_ice'
|
|
||||||
| 'frozen_stone'
|
|
||||||
| 'icicle'
|
|
||||||
| 'permafrost'
|
|
||||||
| 'gobi_base'
|
|
||||||
| 'gobi_lower'
|
|
||||||
| 'gobi_upper'
|
|
||||||
| 'gobi_top'
|
|
||||||
| 'gobi_peak'
|
|
||||||
| 'etched_sand';
|
|
||||||
|
|
||||||
// ==================== 工具函数 ====================
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
@@ -806,4 +768,73 @@ export function applyDirtLayerBlending(params: DirtLayerBlendingParams): VoxelTy
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============= 冻土分层渐变效果 =============
|
||||||
|
|
||||||
|
export interface FrozenDirtLayerBlendingParams {
|
||||||
|
currentType: VoxelType;
|
||||||
|
y: number;
|
||||||
|
ix: number;
|
||||||
|
iz: number;
|
||||||
|
MIN_WORLD_Y: number;
|
||||||
|
surfaceY: number; // 当前列的地表 Y 坐标
|
||||||
|
noiseInfluence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用冻土分层渐变效果
|
||||||
|
* 三层平滑渐变(frozen_dirt_deep -> frozen_dirt_medium -> frozen_dirt)
|
||||||
|
* 从底部到表层,类似泥土的分层逻辑
|
||||||
|
*/
|
||||||
|
export function applyFrozenDirtLayerBlending(params: FrozenDirtLayerBlendingParams): VoxelType {
|
||||||
|
const {
|
||||||
|
currentType,
|
||||||
|
y,
|
||||||
|
ix,
|
||||||
|
iz,
|
||||||
|
// MIN_WORLD_Y 不再使用,改用绝对 y 坐标分层
|
||||||
|
noiseInfluence,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
let type = currentType;
|
||||||
|
|
||||||
|
// 只处理冻土类型
|
||||||
|
if (type !== 'frozen_dirt' && type !== 'frozen_dirt_medium' && type !== 'frozen_dirt_deep') {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用绝对 y 坐标分层(消除侧面条纹)
|
||||||
|
// 扩大高度范围,让侧面过渡更明显、更平缓
|
||||||
|
// y >= 8:表层冻土(frozen_dirt,淡灰蓝)
|
||||||
|
// y >= 2 且 y < 8:中层冻土(frozen_dirt_medium,中等灰蓝)
|
||||||
|
// y < 2:深层冻土(frozen_dirt_deep,深灰蓝)
|
||||||
|
|
||||||
|
// 像素化边界效果:在层边界处添加随机渗透(增大范围)
|
||||||
|
const layerEdgeNoise = pseudoRandom(ix * 0.21 + iz * 0.29 + y * 0.11);
|
||||||
|
const edgeOffset = (layerEdgeNoise - 0.5) * 2.5; // ±1.25 的随机偏移,增强边界模糊
|
||||||
|
const adjustedY = y + edgeOffset;
|
||||||
|
|
||||||
|
if (adjustedY >= 8) {
|
||||||
|
type = 'frozen_dirt';
|
||||||
|
} else if (adjustedY >= 2) {
|
||||||
|
type = 'frozen_dirt_medium';
|
||||||
|
} else {
|
||||||
|
type = 'frozen_dirt_deep';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 额外的像素化边界渗透效果(在层边界处)
|
||||||
|
const penetrationNoise = pseudoRandom(ix * 0.13 + iz * 0.19 + y * 0.27 + noiseInfluence * 0.3);
|
||||||
|
if (penetrationNoise > 0.82) {
|
||||||
|
// 在边界处随机渗透相邻层的颜色
|
||||||
|
if (type === 'frozen_dirt' && y < 10) {
|
||||||
|
type = 'frozen_dirt_medium';
|
||||||
|
} else if (type === 'frozen_dirt_medium') {
|
||||||
|
if (y > 4) type = 'frozen_dirt';
|
||||||
|
else if (y < 4) type = 'frozen_dirt_deep';
|
||||||
|
} else if (type === 'frozen_dirt_deep' && y > 0) {
|
||||||
|
type = 'frozen_dirt_medium';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,17 @@ export interface SceneConfig {
|
|||||||
|
|
||||||
// 特殊地物配置
|
// 特殊地物配置
|
||||||
specialFeatures: Array<{
|
specialFeatures: Array<{
|
||||||
type: 'lava_pool' | 'ice_spike' | 'swamp_tree' | 'cactus' | 'volcanic_vent';
|
type: 'lava_pool' | 'ice_spike' | 'ice_boulder' | 'snow_rock' | 'swamp_tree' | 'cactus' | 'volcanic_vent' | 'frozen_lake';
|
||||||
probability: number;
|
probability: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
// 高度分层配置(用于雪山等场景)
|
||||||
|
heightLayers?: {
|
||||||
|
snowThreshold: number; // 雪覆盖的高度百分比阈值 (0-1),高于此为纯雪
|
||||||
|
transitionThreshold: number; // 过渡区开始的高度百分比阈值 (0-1)
|
||||||
|
rockThreshold: number; // 岩石区域的高度百分比阈值 (0-1),低于此为纯岩石
|
||||||
|
};
|
||||||
|
|
||||||
// 生物群系权重(用于多样化)
|
// 生物群系权重(用于多样化)
|
||||||
biomeWeights: {
|
biomeWeights: {
|
||||||
hot: number; // 炎热区域权重
|
hot: number; // 炎热区域权重
|
||||||
@@ -57,12 +64,10 @@ export type SceneType =
|
|||||||
| 'desert'
|
| 'desert'
|
||||||
| 'mountain'
|
| 'mountain'
|
||||||
| 'snowy_mountain'
|
| 'snowy_mountain'
|
||||||
| 'riverside'
|
|
||||||
| 'beach'
|
| 'beach'
|
||||||
| 'plains'
|
| 'plains'
|
||||||
| 'volcano'
|
| 'volcano'
|
||||||
| 'swamp'
|
| 'swamp';
|
||||||
| 'tundra';
|
|
||||||
|
|
||||||
// 场景配置集合
|
// 场景配置集合
|
||||||
export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
|
export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
|
||||||
@@ -121,47 +126,32 @@ export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
|
|||||||
heightBase: 5,
|
heightBase: 5,
|
||||||
heightRoughness: 0.05,
|
heightRoughness: 0.05,
|
||||||
heightAmplitude: 12,
|
heightAmplitude: 12,
|
||||||
waterLevel: -2,
|
waterLevel: 0, // 调整水位,用于冰湖生成
|
||||||
hasWater: true,
|
hasWater: true,
|
||||||
waterType: 'ice',
|
waterType: 'ice',
|
||||||
surfaceBlock: 'snow',
|
surfaceBlock: 'snow',
|
||||||
subSurfaceBlock: 'packed_ice',
|
subSurfaceBlock: 'frozen_dirt', // 次表层使用冻土(会自动分层)
|
||||||
deepBlock: 'frozen_stone',
|
deepBlock: 'frozen_dirt_deep', // 深层使用深层冻土
|
||||||
vegetationDensity: 0.08,
|
vegetationDensity: 0.12, // 增加植被密度
|
||||||
vegetationTypes: [
|
vegetationTypes: [
|
||||||
{ type: 'wood', probability: 0.6, minHeight: 0, maxHeight: 4 }
|
{ type: 'wood', probability: 0.7, minHeight: 0, maxHeight: 6 } // 针叶林,限制在中低海拔
|
||||||
],
|
],
|
||||||
specialFeatures: [
|
specialFeatures: [
|
||||||
{ type: 'ice_spike', probability: 0.04 }
|
{ type: 'ice_spike', probability: 0.05 }, // 冰锥
|
||||||
|
{ type: 'ice_boulder', probability: 0.04 }, // 冰块/冰石
|
||||||
|
{ type: 'snow_rock', probability: 0.06 }, // 裸露岩石
|
||||||
|
{ type: 'frozen_lake', probability: 0.02 } // 冰湖
|
||||||
],
|
],
|
||||||
biomeWeights: { hot: 0.0, cold: 1.0, wet: 0.4, dry: 0.6 }
|
biomeWeights: { hot: 0.0, cold: 1.0, wet: 0.4, dry: 0.6 },
|
||||||
|
// 高度分层配置:混合型雪山
|
||||||
|
heightLayers: {
|
||||||
|
snowThreshold: 0.75, // 75% 以上高度为纯雪覆盖(提高阈值,减少低处出现雪)
|
||||||
|
transitionThreshold: 0.3, // 30-75% 为岩石区(frozen_stone)
|
||||||
|
rockThreshold: 0.3 // 30% 以下为岩石区域
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 4. 河岸场景
|
// 4. 海滩场景
|
||||||
riverside: {
|
|
||||||
name: 'riverside',
|
|
||||||
displayName: 'Riverside',
|
|
||||||
heightScale: 0.6,
|
|
||||||
heightBase: 3,
|
|
||||||
heightRoughness: 0.025,
|
|
||||||
heightAmplitude: 4,
|
|
||||||
waterLevel: 1.5,
|
|
||||||
hasWater: true,
|
|
||||||
waterType: 'water',
|
|
||||||
surfaceBlock: 'grass',
|
|
||||||
subSurfaceBlock: 'dirt',
|
|
||||||
deepBlock: 'stone',
|
|
||||||
vegetationDensity: 0.35,
|
|
||||||
vegetationTypes: [
|
|
||||||
{ type: 'wood', probability: 0.5, minHeight: 0, maxHeight: 5 },
|
|
||||||
{ type: 'reed', probability: 0.4, minHeight: 0, maxHeight: 2 },
|
|
||||||
{ type: 'flower', probability: 0.1, minHeight: 0, maxHeight: 3 }
|
|
||||||
],
|
|
||||||
specialFeatures: [],
|
|
||||||
biomeWeights: { hot: 0.5, cold: 0.3, wet: 1.0, dry: 0.2 }
|
|
||||||
},
|
|
||||||
|
|
||||||
// 5. 海滩场景
|
|
||||||
beach: {
|
beach: {
|
||||||
name: 'beach',
|
name: 'beach',
|
||||||
displayName: 'Beach',
|
displayName: 'Beach',
|
||||||
@@ -183,7 +173,7 @@ export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
|
|||||||
biomeWeights: { hot: 0.8, cold: 0.2, wet: 0.7, dry: 0.6 }
|
biomeWeights: { hot: 0.8, cold: 0.2, wet: 0.7, dry: 0.6 }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 6. 平原场景
|
// 5. 平原场景
|
||||||
plains: {
|
plains: {
|
||||||
name: 'plains',
|
name: 'plains',
|
||||||
displayName: 'Plains',
|
displayName: 'Plains',
|
||||||
@@ -206,7 +196,7 @@ export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
|
|||||||
biomeWeights: { hot: 0.6, cold: 0.4, wet: 0.5, dry: 0.5 }
|
biomeWeights: { hot: 0.6, cold: 0.4, wet: 0.5, dry: 0.5 }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 7. 火山场景
|
// 6. 火山场景
|
||||||
volcano: {
|
volcano: {
|
||||||
name: 'volcano',
|
name: 'volcano',
|
||||||
displayName: 'Volcano',
|
displayName: 'Volcano',
|
||||||
@@ -229,7 +219,7 @@ export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
|
|||||||
biomeWeights: { hot: 1.0, cold: 0.0, wet: 0.1, dry: 1.0 }
|
biomeWeights: { hot: 1.0, cold: 0.0, wet: 0.1, dry: 1.0 }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 8. 沼泽场景
|
// 7. 沼泽场景
|
||||||
swamp: {
|
swamp: {
|
||||||
name: 'swamp',
|
name: 'swamp',
|
||||||
displayName: 'Swamp',
|
displayName: 'Swamp',
|
||||||
@@ -253,28 +243,6 @@ export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
|
|||||||
{ type: 'swamp_tree', probability: 0.12 }
|
{ type: 'swamp_tree', probability: 0.12 }
|
||||||
],
|
],
|
||||||
biomeWeights: { hot: 0.7, cold: 0.2, wet: 1.0, dry: 0.1 }
|
biomeWeights: { hot: 0.7, cold: 0.2, wet: 1.0, dry: 0.1 }
|
||||||
},
|
|
||||||
|
|
||||||
// 9. 冰原场景
|
|
||||||
tundra: {
|
|
||||||
name: 'tundra',
|
|
||||||
displayName: 'Tundra',
|
|
||||||
heightScale: 0.3,
|
|
||||||
heightBase: 2,
|
|
||||||
heightRoughness: 0.022,
|
|
||||||
heightAmplitude: 3,
|
|
||||||
waterLevel: -2,
|
|
||||||
hasWater: true,
|
|
||||||
waterType: 'ice',
|
|
||||||
surfaceBlock: 'snow',
|
|
||||||
subSurfaceBlock: 'permafrost',
|
|
||||||
deepBlock: 'frozen_stone',
|
|
||||||
vegetationDensity: 0.03,
|
|
||||||
vegetationTypes: [],
|
|
||||||
specialFeatures: [
|
|
||||||
{ type: 'ice_spike', probability: 0.06 }
|
|
||||||
],
|
|
||||||
biomeWeights: { hot: 0.0, cold: 1.0, wet: 0.3, dry: 0.7 }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,475 @@
|
|||||||
|
/**
|
||||||
|
* 雪山特征地形系统:生成冰锥、冰块、岩石和冰湖
|
||||||
|
* - 冰锥 (Ice Spikes):尖锐的冰柱,从地面向上
|
||||||
|
* - 冰块 (Ice Boulders):大块的冰石
|
||||||
|
* - 裸露岩石 (Snow Rocks):山脚区域的岩石
|
||||||
|
* - 冰湖 (Frozen Lakes):低洼区域的冰面
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
createEmptyRockField,
|
||||||
|
generateRockClusters,
|
||||||
|
type RockFieldContext,
|
||||||
|
} from './rockFeatures';
|
||||||
|
|
||||||
|
// ============= 类型定义 =============
|
||||||
|
|
||||||
|
export interface SnowyMountainContext {
|
||||||
|
// 冰锥位置和高度
|
||||||
|
iceSpikePositions: Array<{
|
||||||
|
lx: number; // 逻辑 X 坐标
|
||||||
|
lz: number; // 逻辑 Z 坐标
|
||||||
|
height: number; // 冰锥高度(逻辑层数)
|
||||||
|
size: 'small' | 'medium' | 'large';
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// 冰块位置和大小
|
||||||
|
iceBoulderPositions: Array<{
|
||||||
|
lx: number;
|
||||||
|
lz: number;
|
||||||
|
size: 'small' | 'medium' | 'large';
|
||||||
|
aboveGround: number; // 地上层数
|
||||||
|
belowGround: number; // 地下层数
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// 冰湖区域(使用 Map 存储每个微体素的冰面信息)
|
||||||
|
frozenLakeMap: Map<string, {
|
||||||
|
depth: number; // 冰面深度(向下挖掘)
|
||||||
|
iceThickness: number; // 冰层厚度
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// 复用 rockFeatures 的岩石上下文
|
||||||
|
rockContext: RockFieldContext;
|
||||||
|
|
||||||
|
// 高度分层信息
|
||||||
|
heightLayers: {
|
||||||
|
minHeight: number;
|
||||||
|
maxHeight: number;
|
||||||
|
snowThreshold: number; // 纯雪高度阈值
|
||||||
|
transitionThreshold: number; // 过渡区阈值
|
||||||
|
rockThreshold: number; // 岩石区阈值
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 尺寸配置 =============
|
||||||
|
|
||||||
|
interface FeatureSizeConfig {
|
||||||
|
small: { minClusters: number; maxClusters: number; heightRange: [number, number] };
|
||||||
|
medium: { minClusters: number; maxClusters: number; heightRange: [number, number] };
|
||||||
|
large: { minClusters: number; maxClusters: number; heightRange: [number, number] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据地图大小获取特征配置
|
||||||
|
const getFeatureConfig = (mapSize: number): FeatureSizeConfig => {
|
||||||
|
if (mapSize <= 16) {
|
||||||
|
return {
|
||||||
|
small: { minClusters: 2, maxClusters: 4, heightRange: [2, 3] },
|
||||||
|
medium: { minClusters: 1, maxClusters: 2, heightRange: [3, 5] },
|
||||||
|
large: { minClusters: 0, maxClusters: 1, heightRange: [5, 7] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (mapSize <= 24) {
|
||||||
|
return {
|
||||||
|
small: { minClusters: 3, maxClusters: 6, heightRange: [2, 4] },
|
||||||
|
medium: { minClusters: 2, maxClusters: 4, heightRange: [4, 6] },
|
||||||
|
large: { minClusters: 1, maxClusters: 2, heightRange: [6, 9] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
small: { minClusters: 4, maxClusters: 8, heightRange: [2, 4] },
|
||||||
|
medium: { minClusters: 3, maxClusters: 5, heightRange: [4, 7] },
|
||||||
|
large: { minClusters: 1, maxClusters: 3, heightRange: [7, 10] },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= 冰锥生成 =============
|
||||||
|
|
||||||
|
const generateIceSpikes = (
|
||||||
|
mapSize: number,
|
||||||
|
rand: () => number,
|
||||||
|
terrainHeightMap: number[][],
|
||||||
|
heightLayers: SnowyMountainContext['heightLayers'],
|
||||||
|
occupiedPositions: Set<string>
|
||||||
|
): SnowyMountainContext['iceSpikePositions'] => {
|
||||||
|
const config = getFeatureConfig(mapSize);
|
||||||
|
const spikes: SnowyMountainContext['iceSpikePositions'] = [];
|
||||||
|
|
||||||
|
// 生成各种尺寸的冰锥
|
||||||
|
for (const size of ['small', 'medium', 'large'] as const) {
|
||||||
|
const sizeConfig = config[size];
|
||||||
|
const targetCount = sizeConfig.minClusters +
|
||||||
|
Math.floor(rand() * (sizeConfig.maxClusters - sizeConfig.minClusters + 1));
|
||||||
|
|
||||||
|
let placed = 0;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = targetCount * 30;
|
||||||
|
|
||||||
|
while (placed < targetCount && attempts < maxAttempts) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
const lx = Math.floor(rand() * mapSize);
|
||||||
|
const lz = Math.floor(rand() * mapSize);
|
||||||
|
const posKey = `${lx}|${lz}`;
|
||||||
|
|
||||||
|
// 检查是否已被占用
|
||||||
|
if (occupiedPositions.has(posKey)) continue;
|
||||||
|
|
||||||
|
// 获取当前位置高度
|
||||||
|
const height = terrainHeightMap[lx]?.[lz] ?? 0;
|
||||||
|
const heightRatio = (height - heightLayers.minHeight) /
|
||||||
|
(heightLayers.maxHeight - heightLayers.minHeight);
|
||||||
|
|
||||||
|
// 冰锥主要生成在中高海拔区域(过渡区和雪区)
|
||||||
|
if (heightRatio < heightLayers.transitionThreshold) continue;
|
||||||
|
|
||||||
|
// 检查周围是否有其他冰锥(保持距离)
|
||||||
|
const separationRadius = size === 'large' ? 3 : size === 'medium' ? 2 : 1;
|
||||||
|
let tooClose = false;
|
||||||
|
for (let dx = -separationRadius; dx <= separationRadius; dx++) {
|
||||||
|
for (let dz = -separationRadius; dz <= separationRadius; dz++) {
|
||||||
|
if (occupiedPositions.has(`${lx + dx}|${lz + dz}`)) {
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooClose) break;
|
||||||
|
}
|
||||||
|
if (tooClose) continue;
|
||||||
|
|
||||||
|
// 生成冰锥
|
||||||
|
const spikeHeight = sizeConfig.heightRange[0] +
|
||||||
|
Math.floor(rand() * (sizeConfig.heightRange[1] - sizeConfig.heightRange[0] + 1));
|
||||||
|
|
||||||
|
spikes.push({ lx, lz, height: spikeHeight, size });
|
||||||
|
occupiedPositions.add(posKey);
|
||||||
|
|
||||||
|
// 大型冰锥占用周围位置
|
||||||
|
if (size === 'large' || size === 'medium') {
|
||||||
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
|
for (let dz = -1; dz <= 1; dz++) {
|
||||||
|
occupiedPositions.add(`${lx + dx}|${lz + dz}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spikes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= 冰块生成 =============
|
||||||
|
|
||||||
|
const generateIceBoulders = (
|
||||||
|
mapSize: number,
|
||||||
|
rand: () => number,
|
||||||
|
terrainHeightMap: number[][],
|
||||||
|
heightLayers: SnowyMountainContext['heightLayers'],
|
||||||
|
occupiedPositions: Set<string>
|
||||||
|
): SnowyMountainContext['iceBoulderPositions'] => {
|
||||||
|
const config = getFeatureConfig(mapSize);
|
||||||
|
const boulders: SnowyMountainContext['iceBoulderPositions'] = [];
|
||||||
|
|
||||||
|
for (const size of ['small', 'medium', 'large'] as const) {
|
||||||
|
const sizeConfig = config[size];
|
||||||
|
// 冰块数量比冰锥少一些
|
||||||
|
const targetCount = Math.max(1, Math.floor(
|
||||||
|
(sizeConfig.minClusters + Math.floor(rand() * (sizeConfig.maxClusters - sizeConfig.minClusters + 1))) * 0.6
|
||||||
|
));
|
||||||
|
|
||||||
|
let placed = 0;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = targetCount * 30;
|
||||||
|
|
||||||
|
while (placed < targetCount && attempts < maxAttempts) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
const lx = Math.floor(rand() * mapSize);
|
||||||
|
const lz = Math.floor(rand() * mapSize);
|
||||||
|
const posKey = `${lx}|${lz}`;
|
||||||
|
|
||||||
|
if (occupiedPositions.has(posKey)) continue;
|
||||||
|
|
||||||
|
const height = terrainHeightMap[lx]?.[lz] ?? 0;
|
||||||
|
const heightRatio = (height - heightLayers.minHeight) /
|
||||||
|
(heightLayers.maxHeight - heightLayers.minHeight);
|
||||||
|
|
||||||
|
// 冰块可以出现在任何高度,但更倾向于中高海拔
|
||||||
|
if (heightRatio < 0.2) continue;
|
||||||
|
|
||||||
|
const separationRadius = size === 'large' ? 3 : size === 'medium' ? 2 : 1;
|
||||||
|
let tooClose = false;
|
||||||
|
for (let dx = -separationRadius; dx <= separationRadius; dx++) {
|
||||||
|
for (let dz = -separationRadius; dz <= separationRadius; dz++) {
|
||||||
|
if (occupiedPositions.has(`${lx + dx}|${lz + dz}`)) {
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooClose) break;
|
||||||
|
}
|
||||||
|
if (tooClose) continue;
|
||||||
|
|
||||||
|
// 冰块地上/地下高度
|
||||||
|
const aboveGround = size === 'small' ? 1 + Math.floor(rand() * 2) :
|
||||||
|
size === 'medium' ? 2 + Math.floor(rand() * 2) :
|
||||||
|
2 + Math.floor(rand() * 3);
|
||||||
|
const belowGround = 1 + Math.floor(rand() * 2);
|
||||||
|
|
||||||
|
boulders.push({ lx, lz, size, aboveGround, belowGround });
|
||||||
|
occupiedPositions.add(posKey);
|
||||||
|
|
||||||
|
// 大型冰块占用周围位置
|
||||||
|
if (size === 'large' || size === 'medium') {
|
||||||
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
|
for (let dz = -1; dz <= 1; dz++) {
|
||||||
|
occupiedPositions.add(`${lx + dx}|${lz + dz}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return boulders;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= 冰湖生成 =============
|
||||||
|
|
||||||
|
const generateFrozenLakes = (
|
||||||
|
mapSize: number,
|
||||||
|
rand: () => number,
|
||||||
|
terrainHeightMap: number[][],
|
||||||
|
heightLayers: SnowyMountainContext['heightLayers'],
|
||||||
|
occupiedPositions: Set<string>,
|
||||||
|
MICRO_SCALE: number
|
||||||
|
): Map<string, { depth: number; iceThickness: number }> => {
|
||||||
|
const frozenLakeMap = new Map<string, { depth: number; iceThickness: number }>();
|
||||||
|
|
||||||
|
// 根据地图大小确定冰湖数量
|
||||||
|
const lakeCount = mapSize <= 16 ? 1 : mapSize <= 24 ? 2 : 3;
|
||||||
|
|
||||||
|
// 收集所有可能的冰湖位置,按高度排序(优先选择最低处)
|
||||||
|
const candidatePositions: Array<{ lx: number; lz: number; height: number }> = [];
|
||||||
|
|
||||||
|
for (let lx = 2; lx < mapSize - 2; lx++) {
|
||||||
|
for (let lz = 2; lz < mapSize - 2; lz++) {
|
||||||
|
const height = terrainHeightMap[lx]?.[lz] ?? 0;
|
||||||
|
|
||||||
|
// 收集所有未被占用的位置
|
||||||
|
if (!occupiedPositions.has(`${lx}|${lz}`)) {
|
||||||
|
candidatePositions.push({ lx, lz, height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按高度升序排序(最低的在前面,优先在低处生成)
|
||||||
|
candidatePositions.sort((a, b) => a.height - b.height);
|
||||||
|
|
||||||
|
console.log(`[FrozenLake] 候选位置数量: ${candidatePositions.length}, 目标冰湖数: ${lakeCount}`);
|
||||||
|
|
||||||
|
// 记录已生成的冰湖中心,确保冰湖之间有间距
|
||||||
|
const lakeCenters: Array<{ lx: number; lz: number }> = [];
|
||||||
|
const minLakeSpacing = 5; // 冰湖之间的最小间距
|
||||||
|
|
||||||
|
let lakesCreated = 0;
|
||||||
|
|
||||||
|
for (const candidate of candidatePositions) {
|
||||||
|
if (lakesCreated >= lakeCount) break;
|
||||||
|
|
||||||
|
const { lx: centerLx, lz: centerLz } = candidate;
|
||||||
|
|
||||||
|
// 检查与已有冰湖的距离
|
||||||
|
let tooClose = false;
|
||||||
|
for (const existing of lakeCenters) {
|
||||||
|
const dx = centerLx - existing.lx;
|
||||||
|
const dz = centerLz - existing.lz;
|
||||||
|
if (Math.sqrt(dx * dx + dz * dz) < minLakeSpacing) {
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooClose) continue;
|
||||||
|
|
||||||
|
// 检查是否与已有特征冲突(再次检查,因为可能在循环中被添加)
|
||||||
|
if (occupiedPositions.has(`${centerLx}|${centerLz}`)) continue;
|
||||||
|
|
||||||
|
// 生成不规则形状的冰湖
|
||||||
|
const lakeRadiusX = 2 + Math.floor(rand() * 2); // 2-3 格半径
|
||||||
|
const lakeRadiusZ = 2 + Math.floor(rand() * 2);
|
||||||
|
|
||||||
|
// 在微体素级别生成冰湖
|
||||||
|
for (let dx = -lakeRadiusX; dx <= lakeRadiusX; dx++) {
|
||||||
|
for (let dz = -lakeRadiusZ; dz <= lakeRadiusZ; dz++) {
|
||||||
|
const lx = centerLx + dx;
|
||||||
|
const lz = centerLz + dz;
|
||||||
|
|
||||||
|
if (lx < 0 || lx >= mapSize || lz < 0 || lz >= mapSize) continue;
|
||||||
|
|
||||||
|
// 椭圆形检查 + 噪声边缘
|
||||||
|
const normalizedDist = (dx * dx) / (lakeRadiusX * lakeRadiusX) +
|
||||||
|
(dz * dz) / (lakeRadiusZ * lakeRadiusZ);
|
||||||
|
const noise = (rand() - 0.5) * 0.3;
|
||||||
|
|
||||||
|
if (normalizedDist + noise > 1.0) continue;
|
||||||
|
|
||||||
|
// 标记该逻辑格子被冰湖占用
|
||||||
|
occupiedPositions.add(`${lx}|${lz}`);
|
||||||
|
|
||||||
|
// 生成微体素级别的冰面
|
||||||
|
for (let mx = 0; mx < MICRO_SCALE; mx++) {
|
||||||
|
for (let mz = 0; mz < MICRO_SCALE; mz++) {
|
||||||
|
const ix = lx * MICRO_SCALE + mx;
|
||||||
|
const iz = lz * MICRO_SCALE + mz;
|
||||||
|
|
||||||
|
// 冰面深度和厚度
|
||||||
|
const depth = 1 + Math.floor(rand() * 2); // 1-2 微体素深度
|
||||||
|
const iceThickness = 1 + Math.floor(rand() * 2); // 1-2 微体素厚度
|
||||||
|
|
||||||
|
frozenLakeMap.set(`${ix}|${iz}`, { depth, iceThickness });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lakeCenters.push({ lx: centerLx, lz: centerLz });
|
||||||
|
lakesCreated++;
|
||||||
|
console.log(`[FrozenLake] 生成冰湖 #${lakesCreated} 于 (${centerLx}, ${centerLz}), 高度: ${candidate.height}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frozenLakeMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= 主入口函数 =============
|
||||||
|
|
||||||
|
export const createSnowyMountainContext = (
|
||||||
|
mapSize: number,
|
||||||
|
rand: () => number,
|
||||||
|
terrainHeightMap: number[][],
|
||||||
|
MICRO_SCALE: number = 8,
|
||||||
|
heightLayersConfig?: {
|
||||||
|
snowThreshold: number;
|
||||||
|
transitionThreshold: number;
|
||||||
|
rockThreshold: number;
|
||||||
|
}
|
||||||
|
): SnowyMountainContext => {
|
||||||
|
// 计算地形高度范围
|
||||||
|
let minHeight = Infinity;
|
||||||
|
let maxHeight = -Infinity;
|
||||||
|
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
const h = terrainHeightMap[lx]?.[lz] ?? 0;
|
||||||
|
minHeight = Math.min(minHeight, h);
|
||||||
|
maxHeight = Math.max(maxHeight, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用配置或默认值
|
||||||
|
const heightLayers: SnowyMountainContext['heightLayers'] = {
|
||||||
|
minHeight,
|
||||||
|
maxHeight,
|
||||||
|
snowThreshold: heightLayersConfig?.snowThreshold ?? 0.6,
|
||||||
|
transitionThreshold: heightLayersConfig?.transitionThreshold ?? 0.3,
|
||||||
|
rockThreshold: heightLayersConfig?.rockThreshold ?? 0.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 追踪已占用的位置
|
||||||
|
const occupiedPositions = new Set<string>();
|
||||||
|
|
||||||
|
// 1. 生成岩石(使用现有的 rockFeatures 系统)
|
||||||
|
const rockContext = createEmptyRockField(mapSize);
|
||||||
|
generateRockClusters({
|
||||||
|
mapSize,
|
||||||
|
rand,
|
||||||
|
field: rockContext,
|
||||||
|
profileOverride: {
|
||||||
|
minClusters: mapSize <= 16 ? 2 : mapSize <= 24 ? 3 : 4,
|
||||||
|
maxClusters: mapSize <= 16 ? 4 : mapSize <= 24 ? 6 : 8,
|
||||||
|
maxCellsPerCluster: 3,
|
||||||
|
minAboveGround: 1,
|
||||||
|
maxAboveGround: 3,
|
||||||
|
minBelowGround: 1,
|
||||||
|
maxBelowGround: 2,
|
||||||
|
separationRadius: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记岩石位置为已占用
|
||||||
|
for (let lx = 0; lx < mapSize; lx++) {
|
||||||
|
for (let lz = 0; lz < mapSize; lz++) {
|
||||||
|
if (rockContext.stoneHeight[lx][lz] > 0) {
|
||||||
|
occupiedPositions.add(`${lx}|${lz}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 生成冰锥
|
||||||
|
const iceSpikePositions = generateIceSpikes(
|
||||||
|
mapSize, rand, terrainHeightMap, heightLayers, occupiedPositions
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 生成冰块
|
||||||
|
const iceBoulderPositions = generateIceBoulders(
|
||||||
|
mapSize, rand, terrainHeightMap, heightLayers, occupiedPositions
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. 生成冰湖
|
||||||
|
const frozenLakeMap = generateFrozenLakes(
|
||||||
|
mapSize, rand, terrainHeightMap, heightLayers, occupiedPositions, MICRO_SCALE
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[SnowyMountain] 生成完成: ${iceSpikePositions.length} 个冰锥, ${iceBoulderPositions.length} 个冰块, ${frozenLakeMap.size} 个冰面微体素`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
iceSpikePositions,
|
||||||
|
iceBoulderPositions,
|
||||||
|
frozenLakeMap,
|
||||||
|
rockContext,
|
||||||
|
heightLayers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= 高度分层查询函数 =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据高度返回该位置应该使用的地表类型
|
||||||
|
* @param heightRatio 高度比例 (0-1)
|
||||||
|
* @param heightLayers 高度分层配置
|
||||||
|
* @returns 'snow' | 'transition' | 'rock'
|
||||||
|
*/
|
||||||
|
export const getHeightLayerType = (
|
||||||
|
heightRatio: number,
|
||||||
|
heightLayers: SnowyMountainContext['heightLayers']
|
||||||
|
): 'snow' | 'transition' | 'rock' => {
|
||||||
|
if (heightRatio >= heightLayers.snowThreshold) {
|
||||||
|
return 'snow';
|
||||||
|
} else if (heightRatio >= heightLayers.transitionThreshold) {
|
||||||
|
return 'transition';
|
||||||
|
} else {
|
||||||
|
return 'rock';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算某个位置的高度比例
|
||||||
|
*/
|
||||||
|
export const getHeightRatio = (
|
||||||
|
logicHeight: number,
|
||||||
|
heightLayers: SnowyMountainContext['heightLayers']
|
||||||
|
): number => {
|
||||||
|
const range = heightLayers.maxHeight - heightLayers.minHeight;
|
||||||
|
if (range <= 0) return 0.5;
|
||||||
|
return (logicHeight - heightLayers.minHeight) / range;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 雪山场景专用的石块类型决策函数
|
||||||
|
* 与沙漠场景不同,雪山的石块统一使用 frozen_stone,保持纯净一致的外观
|
||||||
|
*/
|
||||||
|
export const getSnowyStoneLayerType = (): 'frozen_stone' => {
|
||||||
|
return 'frozen_stone';
|
||||||
|
};
|
||||||
|
|
||||||
@@ -30,6 +30,11 @@ import {
|
|||||||
import { createMountainRockContext } from './mountainFeatures';
|
import { createMountainRockContext } from './mountainFeatures';
|
||||||
import type { RockFieldContext } from './rockFeatures';
|
import type { RockFieldContext } from './rockFeatures';
|
||||||
import { generateMountainStream, type MountainStreamVoxel } from './waterSystem';
|
import { generateMountainStream, type MountainStreamVoxel } from './waterSystem';
|
||||||
|
// 雪山特征地形
|
||||||
|
import {
|
||||||
|
createSnowyMountainContext,
|
||||||
|
type SnowyMountainContext,
|
||||||
|
} from './snowyMountainFeatures';
|
||||||
|
|
||||||
// 植被生成 (已移除旧系统)
|
// 植被生成 (已移除旧系统)
|
||||||
// import {
|
// import {
|
||||||
@@ -336,6 +341,7 @@ export const generateTerrain = async (
|
|||||||
|
|
||||||
|
|
||||||
const isDesertScene = sceneConfig?.name === 'desert';
|
const isDesertScene = sceneConfig?.name === 'desert';
|
||||||
|
const isSnowyMountainScene = sceneConfig?.name === 'snowy_mountain';
|
||||||
|
|
||||||
const desertContext = isDesertScene
|
const desertContext = isDesertScene
|
||||||
? createDesertContext(mapSize, seededRandom)
|
? createDesertContext(mapSize, seededRandom)
|
||||||
@@ -349,6 +355,10 @@ export const generateTerrain = async (
|
|||||||
? desertContext
|
? desertContext
|
||||||
: mountainRockContext;
|
: mountainRockContext;
|
||||||
|
|
||||||
|
// 雪山场景上下文:需要先计算地形高度图,所以在下面的循环后创建
|
||||||
|
// 这里先声明变量,后面填充
|
||||||
|
let snowyMountainContext: SnowyMountainContext | null = null;
|
||||||
|
|
||||||
// 地形类型决策:如果有场景配置,使用场景配置;否则根据 seed 生成
|
// 地形类型决策:如果有场景配置,使用场景配置;否则根据 seed 生成
|
||||||
let terrainType: string;
|
let terrainType: string;
|
||||||
let heightBase: number;
|
let heightBase: number;
|
||||||
@@ -467,6 +477,25 @@ export const generateTerrain = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 生成雪山特征(如果是雪山场景)=====
|
||||||
|
if (isSnowyMountainScene && terrainHeightMap) {
|
||||||
|
reportProgress('basic', 3, '生成雪山特征(冰锥、冰块、冰湖)');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
// 获取场景配置中的高度分层信息
|
||||||
|
const heightLayersConfig = sceneConfig?.heightLayers;
|
||||||
|
|
||||||
|
snowyMountainContext = createSnowyMountainContext(
|
||||||
|
mapSize,
|
||||||
|
seededRandom,
|
||||||
|
terrainHeightMap,
|
||||||
|
MICRO_SCALE,
|
||||||
|
heightLayersConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[Terrain] 雪山特征生成完成: ${snowyMountainContext.iceSpikePositions.length} 个冰锥, ${snowyMountainContext.iceBoulderPositions.length} 个冰块, ${snowyMountainContext.frozenLakeMap.size} 个冰面微体素`);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 生成山地溪流(如果是山地场景)=====
|
// ===== 生成山地溪流(如果是山地场景)=====
|
||||||
let mountainStreamMap: Map<string, MountainStreamVoxel> | null = null;
|
let mountainStreamMap: Map<string, MountainStreamVoxel> | null = null;
|
||||||
if (sceneConfig?.name === 'mountain' && mountainRockContext && terrainHeightMap) {
|
if (sceneConfig?.name === 'mountain' && mountainRockContext && terrainHeightMap) {
|
||||||
@@ -660,13 +689,41 @@ export const generateTerrain = async (
|
|||||||
} else {
|
} else {
|
||||||
logicType = moisture > 0.3 ? 'swamp_grass' : 'mud';
|
logicType = moisture > 0.3 ? 'swamp_grass' : 'mud';
|
||||||
}
|
}
|
||||||
} else if (terrainType === 'tundra' || terrainType === 'snowy_mountain') {
|
} else if (terrainType === 'snowy_mountain') {
|
||||||
if (relativeHeight < 1) {
|
// 雪山场景:使用噪声软化边界,减少锯齿感
|
||||||
logicType = 'permafrost';
|
// 基础积雪阈值为 5,但使用噪声调整局部阈值
|
||||||
} else if (relativeHeight < 3) {
|
const snowThresholdBase = 5;
|
||||||
|
|
||||||
|
// 使用两层噪声叠加,创建更自然的边界
|
||||||
|
// 1. 中频噪声:创建大范围的边界波动
|
||||||
|
const midFreqNoise = noise2D(lx * 0.12 + 500, lz * 0.12 + 500);
|
||||||
|
// 2. 高频噪声:创建细节级别的边界参差
|
||||||
|
const highFreqNoise = noise2D(lx * 0.35 + 700, lz * 0.35 + 700) * 0.4;
|
||||||
|
|
||||||
|
// 组合噪声:边界波动范围约 ±2 层
|
||||||
|
const boundaryNoise = midFreqNoise * 1.5 + highFreqNoise;
|
||||||
|
const adjustedThreshold = snowThresholdBase + boundaryNoise;
|
||||||
|
|
||||||
|
// 额外的过渡区处理:在边界附近增加随机性
|
||||||
|
const transitionWidth = 1.0; // 过渡区宽度
|
||||||
|
const distFromThreshold = relativeHeight - adjustedThreshold;
|
||||||
|
|
||||||
|
if (distFromThreshold >= transitionWidth) {
|
||||||
|
// 明确在积雪区
|
||||||
|
logicType = 'snow';
|
||||||
|
} else if (distFromThreshold <= -transitionWidth) {
|
||||||
|
// 明确在岩石区
|
||||||
|
logicType = 'frozen_stone';
|
||||||
|
} else {
|
||||||
|
// 过渡区:根据距离阈值的比例 + 细节噪声决定
|
||||||
|
const transitionRatio = (distFromThreshold + transitionWidth) / (2 * transitionWidth);
|
||||||
|
const detailNoiseFactor = noise2D(lx * 0.8 + 900, lz * 0.8 + 900) * 0.3;
|
||||||
|
|
||||||
|
if (transitionRatio + detailNoiseFactor > 0.5) {
|
||||||
logicType = 'snow';
|
logicType = 'snow';
|
||||||
} else {
|
} else {
|
||||||
logicType = moisture > 0.4 ? 'packed_ice' : 'frozen_stone';
|
logicType = 'frozen_stone';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logicType = surfaceBlock;
|
logicType = surfaceBlock;
|
||||||
@@ -805,17 +862,34 @@ export const generateTerrain = async (
|
|||||||
const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0;
|
const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0;
|
||||||
const isInRiverArea = streamDepthMicro > 0;
|
const isInRiverArea = streamDepthMicro > 0;
|
||||||
|
|
||||||
|
// 获取冰湖信息(雪山场景)
|
||||||
|
const frozenLakeInfo = isSnowyMountainScene && snowyMountainContext
|
||||||
|
? snowyMountainContext.frozenLakeMap.get(`${ix}|${iz}`)
|
||||||
|
: undefined;
|
||||||
|
const isInFrozenLake = !!frozenLakeInfo;
|
||||||
|
|
||||||
// SMOOTHING LOGIC:
|
// SMOOTHING LOGIC:
|
||||||
// 只对非石块、非河流区域应用 smoothing,保持石块和河床稳固对齐
|
// 只对非石块、非河流、非冰湖区域应用 smoothing,保持这些区域稳固对齐
|
||||||
if (!hasRockColumn && !isInRiverArea) {
|
if (!hasRockColumn && !isInRiverArea && !isInFrozenLake) {
|
||||||
// Only apply significant noise to Stone/Snow
|
// Only apply significant noise to Stone/Snow
|
||||||
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
|
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
|
||||||
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
|
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
|
||||||
if (Math.abs(detailVal) > 0.85) {
|
if (Math.abs(detailVal) > 0.85) {
|
||||||
worldY += Math.sign(detailVal);
|
worldY += Math.sign(detailVal);
|
||||||
}
|
}
|
||||||
} else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') {
|
} else if (logicType === 'snow') {
|
||||||
|
// 积雪表面:使用更平滑的高度变化,减少锯齿感
|
||||||
|
// 只在噪声值较极端时才有轻微起伏(约 30% 的位置)
|
||||||
|
if (Math.abs(detailVal) > 0.65) {
|
||||||
|
worldY += Math.sign(detailVal);
|
||||||
|
}
|
||||||
|
} else if (logicType === 'stone' || logicType === 'volcanic_rock') {
|
||||||
worldY += Math.floor(detailVal * 2);
|
worldY += Math.floor(detailVal * 2);
|
||||||
|
} else if (logicType === 'frozen_stone' || logicType === 'frozen_dirt') {
|
||||||
|
// 雪山岩石区:轻微的表面变化,保持整体平整感
|
||||||
|
if (Math.abs(detailVal) > 0.75) {
|
||||||
|
worldY += Math.sign(detailVal);
|
||||||
|
}
|
||||||
} else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') {
|
} else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') {
|
||||||
worldY = Math.floor(1.3 * MICRO_SCALE);
|
worldY = Math.floor(1.3 * MICRO_SCALE);
|
||||||
} else if (logicType === 'ash' || logicType === 'mud') {
|
} else if (logicType === 'ash' || logicType === 'mud') {
|
||||||
@@ -825,6 +899,12 @@ export const generateTerrain = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 冰湖区域:使用统一的平整高度
|
||||||
|
if (isInFrozenLake) {
|
||||||
|
// 冰湖表面保持平整,使用该逻辑格子的基础高度
|
||||||
|
worldY = logicHeight * MICRO_SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有巨石,地表被石块地上部分抬高
|
// 如果有巨石,地表被石块地上部分抬高
|
||||||
if (stoneMicroHeight > 0) {
|
if (stoneMicroHeight > 0) {
|
||||||
worldY += stoneMicroHeight;
|
worldY += stoneMicroHeight;
|
||||||
@@ -919,6 +999,19 @@ export const generateTerrain = async (
|
|||||||
type = 'dirt';
|
type = 'dirt';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 【雪山场景】冰湖处理:将冰湖区域渲染为半透明冰面
|
||||||
|
else if (isInFrozenLake && frozenLakeInfo) {
|
||||||
|
// 冰湖结构:
|
||||||
|
// - 表面层(depth = 0~iceThickness): ice(半透明冰面)
|
||||||
|
// - 下方层: ice(半透明冰块,形成完整的冰湖深度)
|
||||||
|
const iceThickness = frozenLakeInfo.iceThickness;
|
||||||
|
const lakeDepth = frozenLakeInfo.depth;
|
||||||
|
|
||||||
|
if (depth < iceThickness + lakeDepth + 2) {
|
||||||
|
// 整个冰湖深度都使用半透明的 ice 类型
|
||||||
|
type = 'ice';
|
||||||
|
}
|
||||||
|
}
|
||||||
// 【简化】戈壁溪流:统一使用 etched_sand(蚀刻沙)
|
// 【简化】戈壁溪流:统一使用 etched_sand(蚀刻沙)
|
||||||
else if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
|
else if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
|
||||||
type = 'etched_sand';
|
type = 'etched_sand';
|
||||||
@@ -1517,6 +1610,8 @@ export const generateTerrain = async (
|
|||||||
mountainStreamMap: mountainStreamMap ?? undefined,
|
mountainStreamMap: mountainStreamMap ?? undefined,
|
||||||
mountainRockContext: mountainRockContext ?? undefined,
|
mountainRockContext: mountainRockContext ?? undefined,
|
||||||
terrainHeightMap: terrainHeightMap ?? undefined,
|
terrainHeightMap: terrainHeightMap ?? undefined,
|
||||||
|
// 雪山场景专用数据
|
||||||
|
snowyMountainContext: snowyMountainContext ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newPlantVoxels = await generateVegetation(vegContext);
|
const newPlantVoxels = await generateVegetation(vegContext);
|
||||||
|
|||||||
@@ -46,7 +46,15 @@ export type VoxelType =
|
|||||||
| 'gobi_upper'
|
| 'gobi_upper'
|
||||||
| 'gobi_top'
|
| 'gobi_top'
|
||||||
| 'gobi_peak'
|
| 'gobi_peak'
|
||||||
| 'etched_sand';
|
| 'etched_sand'
|
||||||
|
// 雪山专用类型
|
||||||
|
| 'snowy_leaves' // 积雪树叶
|
||||||
|
| 'ice_boulder' // 冰块/冰石
|
||||||
|
| 'snow_rock' // 雪地岩石(带雪的石头)
|
||||||
|
| 'frozen_dirt' // 冻土(表层)
|
||||||
|
| 'frozen_dirt_medium' // 冻土(中层)
|
||||||
|
| 'frozen_dirt_deep' // 冻土(深层)
|
||||||
|
| 'river_ice'; // 冻河冰块(半透明淡蓝色)
|
||||||
|
|
||||||
// ============= 辅助函数 =============
|
// ============= 辅助函数 =============
|
||||||
|
|
||||||
@@ -141,6 +149,14 @@ export const PALETTE: Record<VoxelType, number[]> = {
|
|||||||
gobi_top: [165, 85, 55],
|
gobi_top: [165, 85, 55],
|
||||||
gobi_peak: [150, 70, 45],
|
gobi_peak: [150, 70, 45],
|
||||||
etched_sand: [236, 212, 170],
|
etched_sand: [236, 212, 170],
|
||||||
|
// 雪山专用
|
||||||
|
snowy_leaves: [85, 130, 85], // 积雪树叶:绿色基底,稍浅
|
||||||
|
ice_boulder: [180, 220, 240], // 冰块:淡蓝色冰
|
||||||
|
snow_rock: [180, 185, 195], // 雪地岩石:灰白色
|
||||||
|
frozen_dirt: [160, 175, 190], // 冻土表层:淡灰蓝色(接近雪)
|
||||||
|
frozen_dirt_medium: [130, 145, 165], // 冻土中层:中等灰蓝色
|
||||||
|
frozen_dirt_deep: [100, 115, 135], // 冻土深层:深灰蓝色
|
||||||
|
river_ice: [210, 240, 255], // 冻河冰块:非常淡的淡蓝色(半透明感)
|
||||||
};
|
};
|
||||||
|
|
||||||
const GRASS_SWATCHES = [
|
const GRASS_SWATCHES = [
|
||||||
@@ -972,6 +988,166 @@ const varyColor = (type: VoxelType, noiseVal: number, depth: number = 0): string
|
|||||||
gOut += shift;
|
gOut += shift;
|
||||||
bOut += (shift + 2); // 保持一点点冷色调倾向
|
bOut += (shift + 2); // 保持一点点冷色调倾向
|
||||||
}
|
}
|
||||||
|
} else if (type === 'snowy_leaves') {
|
||||||
|
// 积雪树叶:绿色基底 + 白色积雪斑块
|
||||||
|
const normalizedNoise = (noiseVal + 1) * 0.5;
|
||||||
|
const snowNoise = pseudoRandom(normalizedNoise * 123.456 + depth * 0.1);
|
||||||
|
|
||||||
|
if (snowNoise > 0.6) {
|
||||||
|
// 积雪部分:白色或接近白色
|
||||||
|
const snowBright = 150 + snowNoise * 100;
|
||||||
|
rOut = snowBright;
|
||||||
|
gOut = snowBright;
|
||||||
|
bOut = snowBright + 5; // 略带蓝色调
|
||||||
|
} else if (snowNoise > 0.3) {
|
||||||
|
// 半积雪:绿白混合
|
||||||
|
const mixRatio = (snowNoise - 0.3) / 0.3;
|
||||||
|
rOut = lerp(rOut, 230, mixRatio * 0.5);
|
||||||
|
gOut = lerp(gOut, 235, mixRatio * 0.3);
|
||||||
|
bOut = lerp(bOut, 240, mixRatio * 0.4);
|
||||||
|
} else {
|
||||||
|
// 纯绿色树叶部分:保持绿色调
|
||||||
|
const leafShift = (normalizedNoise - 0.5) * 15;
|
||||||
|
rOut += leafShift * 0.3;
|
||||||
|
gOut += leafShift * 0.8;
|
||||||
|
bOut += leafShift * 0.3;
|
||||||
|
}
|
||||||
|
} else if (type === 'ice_boulder') {
|
||||||
|
// 冰块:淡蓝色冰,带有晶莹的高光和深色裂纹
|
||||||
|
const normalizedNoise = (noiseVal + 1) * 0.5;
|
||||||
|
const crackNoise = pseudoRandom(normalizedNoise * 89.123 + depth * 0.2);
|
||||||
|
|
||||||
|
if (crackNoise > 0.95) {
|
||||||
|
// 冰裂纹:深蓝色
|
||||||
|
rOut -= 60;
|
||||||
|
gOut -= 40;
|
||||||
|
bOut -= 20;
|
||||||
|
} else if (crackNoise > 0.85) {
|
||||||
|
// 高光:接近白色
|
||||||
|
const highlight = 30 + crackNoise * 20;
|
||||||
|
rOut += highlight;
|
||||||
|
gOut += highlight * 0.8;
|
||||||
|
bOut += highlight * 0.5;
|
||||||
|
} else {
|
||||||
|
// 正常冰面:微弱的颜色变化
|
||||||
|
const iceShift = (normalizedNoise - 0.5) * 20;
|
||||||
|
rOut += iceShift * 0.3;
|
||||||
|
gOut += iceShift * 0.5;
|
||||||
|
bOut += iceShift * 0.2;
|
||||||
|
}
|
||||||
|
} else if (type === 'snow_rock') {
|
||||||
|
// 雪地岩石:灰色岩石 + 白色积雪斑块
|
||||||
|
const normalizedNoise = (noiseVal + 1) * 0.5;
|
||||||
|
const snowPatchNoise = pseudoRandom(normalizedNoise * 67.891 + depth * 0.15);
|
||||||
|
|
||||||
|
if (snowPatchNoise > 0.7) {
|
||||||
|
// 积雪斑块:白色
|
||||||
|
const snowBright = 230 + snowPatchNoise * 25;
|
||||||
|
rOut = snowBright;
|
||||||
|
gOut = snowBright;
|
||||||
|
bOut = snowBright + 5;
|
||||||
|
} else {
|
||||||
|
// 岩石部分:灰色变化
|
||||||
|
const rockShift = (normalizedNoise - 0.5) * 15;
|
||||||
|
rOut += rockShift;
|
||||||
|
gOut += rockShift;
|
||||||
|
bOut += rockShift + 3; // 略带蓝色调
|
||||||
|
}
|
||||||
|
} else if (type === 'frozen_stone') {
|
||||||
|
// 冻石:完全纯色,没有任何噪点或变化
|
||||||
|
// 直接使用 PALETTE 中的基础颜色,不做任何修改
|
||||||
|
// rOut, gOut, bOut 已经在函数开头从 PALETTE 获取
|
||||||
|
} else if (type === 'frozen_dirt' || type === 'frozen_dirt_medium' || type === 'frozen_dirt_deep') {
|
||||||
|
// 冻土:类似泥土的丰富噪点效果,但保持冷色调
|
||||||
|
const normalizedNoise = (noiseVal + 1) * 0.5;
|
||||||
|
const iceNoise = pseudoRandom(normalizedNoise * 67.891 + 0.17);
|
||||||
|
|
||||||
|
// 层内渐变:根据 y 坐标(通过 depth 传入)调整基础颜色
|
||||||
|
// depth 是 y 坐标,frozen_dirt 层在 y >= 8,frozen_dirt_medium 在 y >= 2
|
||||||
|
// 靠近顶部的部分更亮,靠近底部的部分更深
|
||||||
|
let heightGradient = 0;
|
||||||
|
if (type === 'frozen_dirt') {
|
||||||
|
// frozen_dirt: y >= 8,在层内做渐变
|
||||||
|
// y = 8 时最深,y 越高越亮
|
||||||
|
const yInLayer = Math.max(0, depth - 8); // depth 是 y 值
|
||||||
|
heightGradient = Math.min(yInLayer * 2.5, 20); // 最多提亮 20
|
||||||
|
} else if (type === 'frozen_dirt_medium') {
|
||||||
|
// frozen_dirt_medium: y >= 2 且 y < 8
|
||||||
|
const yInLayer = Math.max(0, depth - 2);
|
||||||
|
heightGradient = Math.min(yInLayer * 1.8, 12); // 最多提亮 12
|
||||||
|
} else {
|
||||||
|
// frozen_dirt_deep: y < 2
|
||||||
|
const yInLayer = Math.max(0, depth + 6); // MIN_WORLD_Y = -6
|
||||||
|
heightGradient = Math.min(yInLayer * 1.2, 8); // 最多提亮 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用层内渐变(靠近顶部更亮)
|
||||||
|
rOut += heightGradient * 0.6;
|
||||||
|
gOut += heightGradient * 0.75;
|
||||||
|
bOut += heightGradient * 0.9;
|
||||||
|
|
||||||
|
// 冰晶/冻土颗粒:浅蓝色或白色像素(约 8% 概率,类似泥土的小石子)
|
||||||
|
if (iceNoise > 0.92) {
|
||||||
|
// 亮冰晶:偏白偏蓝
|
||||||
|
const iceBright = 15 + Math.random() * 12;
|
||||||
|
rOut += iceBright * 0.6;
|
||||||
|
gOut += iceBright * 0.8;
|
||||||
|
bOut += iceBright;
|
||||||
|
} else if (iceNoise > 0.88 && iceNoise <= 0.92) {
|
||||||
|
// 小冰粒:淡蓝色
|
||||||
|
const iceGrain = 8 + Math.random() * 8;
|
||||||
|
rOut += iceGrain * 0.4;
|
||||||
|
gOut += iceGrain * 0.6;
|
||||||
|
bOut += iceGrain * 0.9;
|
||||||
|
} else if (noiseVal > 0.85) {
|
||||||
|
// 深色冻土块:偏深灰蓝
|
||||||
|
const darkAmount = 8 + Math.random() * 10;
|
||||||
|
rOut -= darkAmount * 0.9;
|
||||||
|
gOut -= darkAmount * 0.85;
|
||||||
|
bOut -= darkAmount * 0.6; // 蓝色减少较少,保持冷调
|
||||||
|
} else if (noiseVal > 0.70 && noiseVal <= 0.78) {
|
||||||
|
// 中等深色块
|
||||||
|
const mediumDark = 4 + Math.random() * 5;
|
||||||
|
rOut -= mediumDark * 0.8;
|
||||||
|
gOut -= mediumDark * 0.8;
|
||||||
|
bOut -= mediumDark * 0.5;
|
||||||
|
} else if (noiseVal < 0.15) {
|
||||||
|
// 浅色冻土:偏亮
|
||||||
|
const lightAmount = 6 + Math.random() * 6;
|
||||||
|
rOut += lightAmount * 0.7;
|
||||||
|
gOut += lightAmount * 0.85;
|
||||||
|
bOut += lightAmount;
|
||||||
|
} else if (noiseVal >= 0.15 && noiseVal < 0.22) {
|
||||||
|
// 很浅的霜点:接近白色
|
||||||
|
const frostLight = 8 + Math.random() * 8;
|
||||||
|
rOut += frostLight * 0.8;
|
||||||
|
gOut += frostLight * 0.9;
|
||||||
|
bOut += frostLight;
|
||||||
|
} else {
|
||||||
|
// 正常微小噪点:保持冷色调变化
|
||||||
|
const shift = (noiseVal * 8) - 4;
|
||||||
|
rOut += shift * 0.7;
|
||||||
|
gOut += shift * 0.85;
|
||||||
|
bOut += shift;
|
||||||
|
}
|
||||||
|
} else if (type === 'river_ice') {
|
||||||
|
// 冻河冰块:非常淡的淡蓝色,保持纯净,偶尔有高光
|
||||||
|
const normalizedNoise = (noiseVal + 1) * 0.5;
|
||||||
|
const sparkle = pseudoRandom(normalizedNoise * 123.456 + depth * 0.05);
|
||||||
|
|
||||||
|
// 轻微的颜色变化(非常微弱)
|
||||||
|
const iceShift = (normalizedNoise - 0.5) * 6;
|
||||||
|
rOut += iceShift * 0.5;
|
||||||
|
gOut += iceShift * 0.7;
|
||||||
|
bOut += iceShift * 0.3;
|
||||||
|
|
||||||
|
// 偶尔的高光反射
|
||||||
|
if (sparkle > 0.92) {
|
||||||
|
const highlight = 10 + sparkle * 15;
|
||||||
|
rOut += highlight;
|
||||||
|
gOut += highlight * 0.9;
|
||||||
|
bOut += highlight * 0.6;
|
||||||
|
}
|
||||||
} else if (type === 'deep_dirt') {
|
} else if (type === 'deep_dirt') {
|
||||||
// 深层泥土:多层次材质(深色斑点 + 浅色颗粒 + 基础波动)
|
// 深层泥土:多层次材质(深色斑点 + 浅色颗粒 + 基础波动)
|
||||||
if (noiseVal > 0.85) {
|
if (noiseVal > 0.85) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const useMapStore = create<MapState>((set, get) => ({
|
|||||||
voxels: [],
|
voxels: [],
|
||||||
logicalHeightMap: {},
|
logicalHeightMap: {},
|
||||||
terrainVersion: 0,
|
terrainVersion: 0,
|
||||||
currentScene: 'mountain', // 默认场景:山地
|
currentScene: 'snowy_mountain', // 默认场景:雪山
|
||||||
isGenerating: false,
|
isGenerating: false,
|
||||||
generationProgress: null,
|
generationProgress: null,
|
||||||
// 戈壁风化参数默认值
|
// 戈壁风化参数默认值
|
||||||
|
|||||||
Reference in New Issue
Block a user