From f7a6fb227036e9af02b0e82e824992d44dab08c2 Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:41:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9B=AA=E5=B1=B1=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=EF=BC=8C=E4=BC=98=E5=8C=96=E6=A4=8D=E8=A2=AB=E5=92=8C?= =?UTF-8?q?=E5=9C=B0=E5=BD=A2=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voxel-tactics-horizon/src/App.tsx | 4 +- .../features/Map/components/ChunkRenderer.tsx | 358 +++++++- .../src/features/Map/logic/newVegetation.ts | 779 +++++++++++++++++- .../src/features/Map/logic/postprocessing.ts | 119 ++- .../src/features/Map/logic/scenes.ts | 90 +- .../Map/logic/snowyMountainFeatures.ts | 475 +++++++++++ .../src/features/Map/logic/terrain.ts | 113 ++- .../src/features/Map/logic/voxelStyles.ts | 178 +++- .../src/features/Map/store.ts | 2 +- 9 files changed, 1985 insertions(+), 133 deletions(-) create mode 100644 voxel-tactics-horizon/src/features/Map/logic/snowyMountainFeatures.ts diff --git a/voxel-tactics-horizon/src/App.tsx b/voxel-tactics-horizon/src/App.tsx index 609bd49..74d306f 100644 --- a/voxel-tactics-horizon/src/App.tsx +++ b/voxel-tactics-horizon/src/App.tsx @@ -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(() => { diff --git a/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx b/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx index d03f21e..7ccdf60 100644 --- a/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx +++ b/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx @@ -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 = ({ data, isHighRes, type, onCli ); } +/** + * 专门用于冰块的网格渲染器 + * 使用 Face Culling 思想,去除相邻冰块之间的内部面 + * 解决半透明材质叠加导致的颗粒感问题 + * + * 视觉效果: + * - 冰晶纹理和裂纹 + * - 微弱的高光闪烁 + * - 菲涅尔边缘高光 + */ +const IceMesh: React.FC = ({ data, isHighRes, type, onClick }) => { + const meshRef = useRef(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(); + 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 ( + + ); +} + const VoxelLayer: React.FC = ({ data, isHighRes, type, onClick }) => { const meshRef = useRef | null>(null); const setHoveredTile = useUnitStore(state => state.setHoveredTile); @@ -574,10 +911,10 @@ const VoxelLayer: React.FC = ({ 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 { @@ -646,6 +983,21 @@ export const ChunkRenderer: React.FC = ({ onVoxelClick }) => /> ); } + + // 特殊处理冰块类型:使用 Face Culling 网格来消除内部面,解决半透明颗粒感 + // 包括:ice(冰湖)、icicle(冰锥)、ice_boulder(冰块)、packed_ice(压缩冰) + if (type === 'ice' || type === 'icicle' || type === 'ice_boulder' || type === 'packed_ice') { + return ( + + ); + } return ( ; mountainRockContext?: { stoneHeight: number[][]; stoneDepth: number[][] }; terrainHeightMap?: number[][]; // 逻辑层级的高度图 (mapSize x mapSize) + // 雪山场景专用数据 + snowyMountainContext?: SnowyMountainContext; } // ========================================= @@ -46,7 +49,7 @@ const C: Record = { // 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 = (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(); + + // 为每个森林收集 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}`; diff --git a/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts index 4ed1201..51d9970 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts @@ -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; +} diff --git a/voxel-tactics-horizon/src/features/Map/logic/scenes.ts b/voxel-tactics-horizon/src/features/Map/logic/scenes.ts index 7d7f8d9..4f56c2b 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/scenes.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/scenes.ts @@ -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 = { @@ -121,47 +126,32 @@ export const SCENE_CONFIGS: Record = { 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 = { 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 = { 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 = { 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 = { { 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 } } }; diff --git a/voxel-tactics-horizon/src/features/Map/logic/snowyMountainFeatures.ts b/voxel-tactics-horizon/src/features/Map/logic/snowyMountainFeatures.ts new file mode 100644 index 0000000..6a10795 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/snowyMountainFeatures.ts @@ -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; + + // 复用 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 +): 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 +): 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, + MICRO_SCALE: number +): Map => { + const frozenLakeMap = new Map(); + + // 根据地图大小确定冰湖数量 + 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(); + + // 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'; +}; + diff --git a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts index 9352046..92ac42b 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts @@ -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 | 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 { - logicType = moisture > 0.4 ? 'packed_ice' : 'frozen_stone'; + // 过渡区:根据距离阈值的比例 + 细节噪声决定 + 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 = '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; @@ -918,7 +998,20 @@ export const generateTerrain = async ( if (depth < streamDepthMicro + 5) { 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); diff --git a/voxel-tactics-horizon/src/features/Map/logic/voxelStyles.ts b/voxel-tactics-horizon/src/features/Map/logic/voxelStyles.ts index 612fde9..5833866 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/voxelStyles.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/voxelStyles.ts @@ -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 = { 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 >= 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') { // 深层泥土:多层次材质(深色斑点 + 浅色颗粒 + 基础波动) if (noiseVal > 0.85) { diff --git a/voxel-tactics-horizon/src/features/Map/store.ts b/voxel-tactics-horizon/src/features/Map/store.ts index 30cc226..3759bda 100644 --- a/voxel-tactics-horizon/src/features/Map/store.ts +++ b/voxel-tactics-horizon/src/features/Map/store.ts @@ -26,7 +26,7 @@ export const useMapStore = create((set, get) => ({ voxels: [], logicalHeightMap: {}, terrainVersion: 0, - currentScene: 'mountain', // 默认场景:山地 + currentScene: 'snowy_mountain', // 默认场景:雪山 isGenerating: false, generationProgress: null, // 戈壁风化参数默认值