添加雪山场景,优化植被和地形系统

This commit is contained in:
Rocky
2025-12-03 20:41:46 +08:00
parent bd0b00c37e
commit f7a6fb2270
9 changed files with 1985 additions and 133 deletions

View File

@@ -31,9 +31,9 @@ function GameScene({ skyPreset }: { skyPreset: SkyPreset }) {
// 使用 ref 跟踪上一次的版本号
const prevVersionRef = useRef(TERRAIN_VERSION);
// 初始化地图 - 默认生成山场景
// 初始化地图 - 默认生成山场景
useEffect(() => {
generateMap('small', undefined, 'mountain');
generateMap('small', undefined, 'snowy_mountain');
// 临时注释:角色生成和战斗初始化
// setTimeout(() => {

View File

@@ -9,6 +9,186 @@ import type { ThreeEvent } from '@react-three/fiber';
const tempObject = new Object3D();
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 =============
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 meshRef = useRef<InstancedMesh<any, any> | null>(null);
const setHoveredTile = useUnitStore(state => state.setHoveredTile);
@@ -574,10 +911,10 @@ const VoxelLayer: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onClick
};
} else if (type === 'ice') {
return {
roughness: 0.1,
metalness: 0.1,
roughness: 0.95, // 提高粗糙度,降低反射强度
metalness: 0.01, // 降低金属感
transparent: true,
opacity: 0.8
opacity: 0.60
};
}
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 (
<VoxelLayer
key={key}

View File

@@ -1,5 +1,6 @@
import type { VoxelData } from './terrain';
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 }>;
mountainRockContext?: { stoneHeight: number[][]; stoneDepth: number[][] };
terrainHeightMap?: number[][]; // 逻辑层级的高度图 (mapSize x mapSize)
// 雪山场景专用数据
snowyMountainContext?: SnowyMountainContext;
}
// =========================================
@@ -46,7 +49,7 @@ const C: Record<string, string> = {
// Ice/Tundra (Balanced greens)
iceBase: '#E0FFFF', iceDeep: '#AEEEEE', iceSpike: '#F0FFFF',
snow: '#FFFFFF',
snow: '#FFFFFF', packed_ice: '#A5D5E5',
lichenOrange: '#D98E32', lichenRed: '#A64B4B', lichenGreen: '#6B855E',
frozenGreen: '#92C4B5', frozenStem: '#56757A',
crystalBlue: '#7FFFD4', crystalWhite: '#EFFFFF',
@@ -2522,18 +2525,357 @@ const createCoralTree = (builder: VoxelBuilder, ox: number, oz: number) => {
builder.add(ox, 0, oz, C.coralBlue);
branch(ox, 0, oz, 8, 0, 0, 2);
};
/**
* 中型冰锥
* 高度: 8-14 体素
*/
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++) {
let r = (h - y) * 0.4;
for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) {
for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) {
if (x * x + z * z <= r * r) builder.add(ox + x, y, oz + z, C.iceSpike);
const t = y / h;
const r = baseR * (1 - t * 0.9);
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;
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) ---
const createPalm = (builder: VoxelBuilder, ox: number, oz: number, height: number, leanType: 'straight' | 'slight') => {
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 SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom];
const TUNDRA_GENERATORS = [createSpruceTallSnowy, createSpruceMediumClear, createLarch];
const ICE_GENERATORS = [createCoralTree, createIceSpike];
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;
density = 0.2; // Very dense
break;
case 'tundra':
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;
case 'beach':
case 'riverside':
generators = BEACH_GENERATORS;
density = 0.12;
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}`);
} 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 {
// ============= 其他场景:使用原有概率生成逻辑 =============
for (let lx = 0; lx < mapSize; lx++) {
@@ -4768,7 +5523,7 @@ export const generateVegetation = async (
const streamInfo = desertContext?.streamDepthMap.get(`${centerMicroX}|${centerMicroZ}`);
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;
const hKey = `${lx*8+4}|${lz*8+4}`;

View File

@@ -3,51 +3,13 @@
* 包含各种对生成的地形进行后处理的算法
*/
// ==================== 类型定义 ====================
// ==================== 类型导入 ====================
/**
* VoxelType 类型定义从terrain.ts复制保持一致
*/
export type VoxelType =
| 'water'
| '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';
// 从 voxelStyles 导入统一的 VoxelType 类型定义
import type { VoxelType } from './voxelStyles';
// 重新导出以供其他模块使用
export type { VoxelType };
// ==================== 工具函数 ====================
@@ -806,4 +768,73 @@ export function applyDirtLayerBlending(params: DirtLayerBlendingParams): VoxelTy
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;
}

View File

@@ -32,10 +32,17 @@ export interface SceneConfig {
// 特殊地物配置
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;
}>;
// 高度分层配置(用于雪山等场景)
heightLayers?: {
snowThreshold: number; // 雪覆盖的高度百分比阈值 (0-1),高于此为纯雪
transitionThreshold: number; // 过渡区开始的高度百分比阈值 (0-1)
rockThreshold: number; // 岩石区域的高度百分比阈值 (0-1),低于此为纯岩石
};
// 生物群系权重(用于多样化)
biomeWeights: {
hot: number; // 炎热区域权重
@@ -57,12 +64,10 @@ export type SceneType =
| 'desert'
| 'mountain'
| 'snowy_mountain'
| 'riverside'
| 'beach'
| 'plains'
| 'volcano'
| 'swamp'
| 'tundra';
| 'swamp';
// 场景配置集合
export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
@@ -121,47 +126,32 @@ export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
heightBase: 5,
heightRoughness: 0.05,
heightAmplitude: 12,
waterLevel: -2,
waterLevel: 0, // 调整水位,用于冰湖生成
hasWater: true,
waterType: 'ice',
surfaceBlock: 'snow',
subSurfaceBlock: 'packed_ice',
deepBlock: 'frozen_stone',
vegetationDensity: 0.08,
subSurfaceBlock: 'frozen_dirt', // 次表层使用冻土(会自动分层)
deepBlock: 'frozen_dirt_deep', // 深层使用深层冻土
vegetationDensity: 0.12, // 增加植被密度
vegetationTypes: [
{ type: 'wood', probability: 0.6, minHeight: 0, maxHeight: 4 }
{ type: 'wood', probability: 0.7, minHeight: 0, maxHeight: 6 } // 针叶林,限制在中低海拔
],
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. 河岸场景
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. 海滩场景
// 4. 海滩场景
beach: {
name: '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 }
},
// 6. 平原场景
// 5. 平原场景
plains: {
name: '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 }
},
// 7. 火山场景
// 6. 火山场景
volcano: {
name: '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 }
},
// 8. 沼泽场景
// 7. 沼泽场景
swamp: {
name: 'swamp',
displayName: 'Swamp',
@@ -253,28 +243,6 @@ export const SCENE_CONFIGS: Record<SceneType, SceneConfig> = {
{ type: 'swamp_tree', probability: 0.12 }
],
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 }
}
};

View File

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

View File

@@ -30,6 +30,11 @@ import {
import { createMountainRockContext } from './mountainFeatures';
import type { RockFieldContext } from './rockFeatures';
import { generateMountainStream, type MountainStreamVoxel } from './waterSystem';
// 雪山特征地形
import {
createSnowyMountainContext,
type SnowyMountainContext,
} from './snowyMountainFeatures';
// 植被生成 (已移除旧系统)
// import {
@@ -336,6 +341,7 @@ export const generateTerrain = async (
const isDesertScene = sceneConfig?.name === 'desert';
const isSnowyMountainScene = sceneConfig?.name === 'snowy_mountain';
const desertContext = isDesertScene
? createDesertContext(mapSize, seededRandom)
@@ -349,6 +355,10 @@ export const generateTerrain = async (
? desertContext
: mountainRockContext;
// 雪山场景上下文:需要先计算地形高度图,所以在下面的循环后创建
// 这里先声明变量,后面填充
let snowyMountainContext: SnowyMountainContext | null = null;
// 地形类型决策:如果有场景配置,使用场景配置;否则根据 seed 生成
let terrainType: string;
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;
if (sceneConfig?.name === 'mountain' && mountainRockContext && terrainHeightMap) {
@@ -660,13 +689,41 @@ export const generateTerrain = async (
} else {
logicType = moisture > 0.3 ? 'swamp_grass' : 'mud';
}
} else if (terrainType === 'tundra' || terrainType === 'snowy_mountain') {
if (relativeHeight < 1) {
logicType = 'permafrost';
} else if (relativeHeight < 3) {
} else if (terrainType === 'snowy_mountain') {
// 雪山场景:使用噪声软化边界,减少锯齿感
// 基础积雪阈值为 5但使用噪声调整局部阈值
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';
} else {
logicType = moisture > 0.4 ? 'packed_ice' : 'frozen_stone';
logicType = 'frozen_stone';
}
}
} else {
logicType = surfaceBlock;
@@ -805,17 +862,34 @@ export const generateTerrain = async (
const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0;
const isInRiverArea = streamDepthMicro > 0;
// 获取冰湖信息(雪山场景)
const frozenLakeInfo = isSnowyMountainScene && snowyMountainContext
? snowyMountainContext.frozenLakeMap.get(`${ix}|${iz}`)
: undefined;
const isInFrozenLake = !!frozenLakeInfo;
// SMOOTHING LOGIC:
// 只对非石块、非河流区域应用 smoothing保持石块和河床稳固对齐
if (!hasRockColumn && !isInRiverArea) {
// 只对非石块、非河流、非冰湖区域应用 smoothing保持这些区域稳固对齐
if (!hasRockColumn && !isInRiverArea && !isInFrozenLake) {
// Only apply significant noise to Stone/Snow
// Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally)
if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') {
if (Math.abs(detailVal) > 0.85) {
worldY += Math.sign(detailVal);
}
} else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') {
} 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);
} 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') {
worldY = Math.floor(1.3 * MICRO_SCALE);
} else if (logicType === 'ash' || logicType === 'mud') {
@@ -825,6 +899,12 @@ export const generateTerrain = async (
}
}
// 冰湖区域:使用统一的平整高度
if (isInFrozenLake) {
// 冰湖表面保持平整,使用该逻辑格子的基础高度
worldY = logicHeight * MICRO_SCALE;
}
// 如果有巨石,地表被石块地上部分抬高
if (stoneMicroHeight > 0) {
worldY += stoneMicroHeight;
@@ -919,6 +999,19 @@ export const generateTerrain = async (
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蚀刻沙
else if (shouldApplyStreamEtching({ streamDepthMicro, depth })) {
type = 'etched_sand';
@@ -1517,6 +1610,8 @@ export const generateTerrain = async (
mountainStreamMap: mountainStreamMap ?? undefined,
mountainRockContext: mountainRockContext ?? undefined,
terrainHeightMap: terrainHeightMap ?? undefined,
// 雪山场景专用数据
snowyMountainContext: snowyMountainContext ?? undefined,
};
const newPlantVoxels = await generateVegetation(vegContext);

View File

@@ -46,7 +46,15 @@ export type VoxelType =
| 'gobi_upper'
| 'gobi_top'
| '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_peak: [150, 70, 45],
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 = [
@@ -972,6 +988,166 @@ const varyColor = (type: VoxelType, noiseVal: number, depth: number = 0): string
gOut += shift;
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 >= 8frozen_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') {
// 深层泥土:多层次材质(深色斑点 + 浅色颗粒 + 基础波动)
if (noiseVal > 0.85) {

View File

@@ -26,7 +26,7 @@ export const useMapStore = create<MapState>((set, get) => ({
voxels: [],
logicalHeightMap: {},
terrainVersion: 0,
currentScene: 'mountain', // 默认场景:山
currentScene: 'snowy_mountain', // 默认场景:
isGenerating: false,
generationProgress: null,
// 戈壁风化参数默认值