1016 lines
37 KiB
TypeScript
1016 lines
37 KiB
TypeScript
import React, { useLayoutEffect, useMemo, useRef } from 'react';
|
||
import { InstancedMesh, Object3D, Color, BufferGeometry, Float32BufferAttribute, ShaderMaterial } from 'three';
|
||
import { useFrame } from '@react-three/fiber';
|
||
import { useMapStore } from '../store';
|
||
import { useUnitStore } from '../../Units/store';
|
||
import { VOXEL_SIZE, TILE_SIZE, TREE_VOXEL_SIZE, type VoxelType } from '../logic/terrain';
|
||
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 */ `
|
||
// 顶点属性
|
||
attribute vec3 color;
|
||
attribute float isCascade; // 是否是瀑布(垂直面):基于上下邻居判断
|
||
attribute vec2 flowDirection; // 流向 (dx, dz)
|
||
|
||
// 传递给片元着色器
|
||
varying vec3 vColor;
|
||
varying vec3 vNormal;
|
||
varying vec3 vWorldPosition;
|
||
varying float vIsCascade;
|
||
varying vec2 vFlowDirection;
|
||
|
||
void main() {
|
||
vColor = color;
|
||
vNormal = normalize(normalMatrix * normal);
|
||
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||
vIsCascade = isCascade;
|
||
vFlowDirection = flowDirection;
|
||
|
||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||
}
|
||
`;
|
||
|
||
const waterFragmentShader = /* glsl */ `
|
||
uniform float uTime;
|
||
uniform float uCascadeSpeed; // 瀑布流速
|
||
uniform float uRippleSpeed; // 涟漪速度
|
||
uniform vec3 uHighlightColor; // 高光颜色(瀑布白沫)
|
||
|
||
varying vec3 vColor;
|
||
varying vec3 vNormal;
|
||
varying vec3 vWorldPosition;
|
||
varying float vIsCascade;
|
||
varying vec2 vFlowDirection;
|
||
|
||
// 简单的伪随机函数
|
||
float random(vec2 st) {
|
||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||
}
|
||
|
||
// 噪声函数(用于涟漪和水花)
|
||
float noise(vec2 st) {
|
||
vec2 i = floor(st);
|
||
vec2 f = fract(st);
|
||
|
||
float a = random(i);
|
||
float b = random(i + vec2(1.0, 0.0));
|
||
float c = random(i + vec2(0.0, 1.0));
|
||
float d = random(i + vec2(1.0, 1.0));
|
||
|
||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||
|
||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||
}
|
||
|
||
void main() {
|
||
vec3 finalColor = vColor;
|
||
float alpha = 0.75;
|
||
|
||
// 判断面的朝向 - 使用更宽松的阈值
|
||
bool isTopFace = vNormal.y > 0.7; // 顶面(水平)
|
||
bool isBottomFace = vNormal.y < -0.7; // 底面
|
||
bool isVerticalFace = !isTopFace && !isBottomFace; // 所有非上下面都是垂直面
|
||
|
||
// ============= 瀑布效果(所有垂直面)=============
|
||
if (isVerticalFace) {
|
||
float worldY = vWorldPosition.y;
|
||
float worldX = vWorldPosition.x;
|
||
float worldZ = vWorldPosition.z;
|
||
|
||
// 瀑布强度
|
||
float cascadeStrength = vIsCascade > 0.5 ? 1.0 : 0.35;
|
||
float flowSpeed = uCascadeSpeed * 6.0;
|
||
|
||
// ====== 1. 主水流 - 不规则的垂直流动 ======
|
||
// 用噪声创建水流的"路径",让水流有粗细变化
|
||
float pathNoise = noise(vec2(worldX * 5.0 + worldZ * 4.0, worldY * 2.0));
|
||
float pathOffset = pathNoise * 0.8; // 水流横向摆动
|
||
|
||
// 主水流 - 不同位置有不同的速度
|
||
float localSpeed = flowSpeed * (0.8 + pathNoise * 0.4);
|
||
float mainFlow = sin(worldY * 30.0 + uTime * localSpeed + pathOffset * 15.0) * 0.5 + 0.5;
|
||
|
||
// 用噪声调制水流宽度(有些地方水多,有些地方水少)
|
||
float widthNoise = noise(vec2(worldX * 8.0 + worldZ * 6.0, worldY * 4.0 + uTime * 0.3));
|
||
mainFlow *= (0.5 + widthNoise * 0.5);
|
||
|
||
// ====== 2. 次级水流 - 更小更快的水珠 ======
|
||
float secondaryNoise = noise(vec2(worldX * 12.0 - worldZ * 8.0, worldY * 6.0));
|
||
float secondaryFlow = sin(worldY * 50.0 + uTime * flowSpeed * 1.3 + secondaryNoise * 10.0) * 0.5 + 0.5;
|
||
secondaryFlow *= secondaryNoise; // 用噪声掩码让它不连续
|
||
|
||
// ====== 3. 泡沫 - 随机分布的白色斑点 ======
|
||
float foamNoise1 = noise(vec2(worldX * 15.0 + worldZ * 12.0, worldY * 18.0 + uTime * flowSpeed * 0.5));
|
||
float foamNoise2 = noise(vec2(worldX * 20.0 - worldZ * 15.0, worldY * 25.0 + uTime * flowSpeed * 0.8));
|
||
float foam = smoothstep(0.55, 0.8, foamNoise1) * smoothstep(0.4, 0.7, foamNoise2);
|
||
|
||
// ====== 4. 飞溅水滴 - 稀疏的亮点 ======
|
||
float dropletNoise = noise(vec2(worldX * 30.0 + worldZ * 25.0, worldY * 35.0 + uTime * flowSpeed * 1.5));
|
||
float droplets = pow(dropletNoise, 6.0); // 非常稀疏
|
||
|
||
// ====== 5. 水帘的透明度变化 ======
|
||
float thicknessNoise = noise(vec2(worldX * 6.0 + worldZ * 5.0, worldY * 3.0 + uTime * 0.2));
|
||
|
||
// ====== 组合所有效果 ======
|
||
float waterFlow = mainFlow * 0.5 + secondaryFlow * 0.25 + foam * 0.2 + droplets * 0.05;
|
||
waterFlow *= cascadeStrength;
|
||
|
||
// ====== 颜色混合 - 亮蓝青色调 ======
|
||
vec3 deepWater = vec3(0.1, 0.45, 0.8); // 亮蓝 - 主体
|
||
vec3 midWater = vec3(0.18, 0.55, 0.88); // 更亮蓝 - 水流
|
||
vec3 lightWater = vec3(0.28, 0.65, 0.95); // 高亮蓝 - 高光
|
||
vec3 foamBlue = vec3(0.4, 0.75, 0.98); // 泡沫亮蓝
|
||
|
||
// 基础是亮蓝色
|
||
finalColor = deepWater;
|
||
// 水流区域变亮
|
||
finalColor = mix(finalColor, midWater, mainFlow * 0.6);
|
||
// 次级水流更亮
|
||
finalColor = mix(finalColor, lightWater, secondaryFlow * 0.4);
|
||
// 泡沫区域(亮蓝色调)
|
||
finalColor = mix(finalColor, foamBlue, foam * 0.35);
|
||
// 水滴闪烁(亮蓝色调)
|
||
finalColor = mix(finalColor, vec3(0.5, 0.85, 1.0), droplets * 0.25);
|
||
|
||
// ====== 透明度 - 水薄的地方更透明 ======
|
||
alpha = 0.6 + thicknessNoise * 0.15 + waterFlow * 0.2;
|
||
alpha = clamp(alpha, 0.55, 0.95);
|
||
}
|
||
// ============= 水平面(河流/湖泊)=============
|
||
else if (isTopFace) {
|
||
vec2 worldXZ = vWorldPosition.xz;
|
||
vec2 flowDir = vFlowDirection;
|
||
float flowStrength = length(flowDir); // 流向强度,用于区分河流和湖泊
|
||
|
||
// 颜色定义
|
||
vec3 deepWater = vec3(0.12, 0.48, 0.82);
|
||
vec3 midWater = vec3(0.2, 0.58, 0.9);
|
||
vec3 lightWater = vec3(0.3, 0.68, 0.96);
|
||
vec3 highlightColor = vec3(0.42, 0.78, 0.98);
|
||
|
||
float waterEffect = 0.0;
|
||
|
||
// ============= 河流(有流向)=============
|
||
if (flowStrength > 0.01) {
|
||
float flowSpeed = uCascadeSpeed * 3.5; // 比瀑布慢
|
||
|
||
vec2 normalizedFlow = normalize(flowDir);
|
||
vec2 perpFlow = vec2(-normalizedFlow.y, normalizedFlow.x);
|
||
|
||
// 沿流向和垂直方向的坐标
|
||
float flowCoord = dot(worldXZ, normalizedFlow);
|
||
float perpCoord = dot(worldXZ, perpFlow);
|
||
|
||
// ====== 1. 主水流 - 类似瀑布的流向动画 ======
|
||
float pathNoise = noise(vec2(perpCoord * 5.0, flowCoord * 2.0));
|
||
float pathOffset = pathNoise * 0.8;
|
||
|
||
float localSpeed = flowSpeed * (0.8 + pathNoise * 0.4);
|
||
float mainFlow = sin(flowCoord * 25.0 - uTime * localSpeed + pathOffset * 12.0) * 0.5 + 0.5;
|
||
|
||
// 用噪声调制水流宽度
|
||
float widthNoise = noise(vec2(perpCoord * 8.0, flowCoord * 4.0 - uTime * 0.3));
|
||
mainFlow *= (0.5 + widthNoise * 0.5);
|
||
|
||
// ====== 2. 次级水流 - 更小更快的波纹 ======
|
||
float secondaryNoise = noise(vec2(perpCoord * 12.0, flowCoord * 6.0));
|
||
float secondaryFlow = sin(flowCoord * 45.0 - uTime * flowSpeed * 1.3 + secondaryNoise * 10.0) * 0.5 + 0.5;
|
||
secondaryFlow *= secondaryNoise;
|
||
|
||
// ====== 3. 泡沫 - 沿流向移动的斑点 ======
|
||
float foamNoise1 = noise(vec2(perpCoord * 15.0, flowCoord * 18.0 - uTime * flowSpeed * 0.5));
|
||
float foamNoise2 = noise(vec2(perpCoord * 20.0, flowCoord * 25.0 - uTime * flowSpeed * 0.8));
|
||
float foam = smoothstep(0.55, 0.8, foamNoise1) * smoothstep(0.4, 0.7, foamNoise2);
|
||
|
||
// ====== 4. 水面闪烁 ======
|
||
float sparkleNoise = noise(vec2(perpCoord * 30.0, flowCoord * 35.0 - uTime * flowSpeed * 1.2));
|
||
float sparkle = pow(sparkleNoise, 6.0);
|
||
|
||
// ====== 5. 水深变化 ======
|
||
float depthNoise = noise(vec2(perpCoord * 6.0, flowCoord * 3.0 - uTime * 0.2));
|
||
|
||
// 组合效果
|
||
waterEffect = mainFlow * 0.5 + secondaryFlow * 0.25 + foam * 0.2 + sparkle * 0.05;
|
||
|
||
// 颜色混合
|
||
finalColor = deepWater;
|
||
finalColor = mix(finalColor, midWater, mainFlow * 0.6);
|
||
finalColor = mix(finalColor, lightWater, secondaryFlow * 0.4);
|
||
finalColor = mix(finalColor, highlightColor, foam * 0.35);
|
||
finalColor = mix(finalColor, vec3(0.52, 0.85, 1.0), sparkle * 0.25);
|
||
|
||
// 透明度
|
||
alpha = 0.6 + depthNoise * 0.15 + waterEffect * 0.2;
|
||
alpha = clamp(alpha, 0.55, 0.85);
|
||
}
|
||
// ============= 湖泊(无流向)- 静态涟漪 =============
|
||
else {
|
||
float rippleSpeed = uRippleSpeed * 2.0;
|
||
|
||
// ====== 1. 涟漪 - 缓慢扩散的同心圆 ======
|
||
float rippleOffset = noise(worldXZ * 0.3 + uTime * 0.05) * 5.0;
|
||
float ripple1 = sin(length(worldXZ * 3.0 + rippleOffset) - uTime * rippleSpeed * 0.3) * 0.5 + 0.5;
|
||
float ripple2 = sin(length((worldXZ + vec2(2.0, 1.5)) * 2.5) - uTime * rippleSpeed * 0.25) * 0.5 + 0.5;
|
||
float ripples = ripple1 * 0.5 + ripple2 * 0.5;
|
||
ripples *= noise(worldXZ * 1.5 + uTime * 0.08) * 0.4 + 0.6;
|
||
|
||
// ====== 2. 微波 - 小的表面扰动 ======
|
||
float microWave = noise(worldXZ * 8.0 + uTime * 0.15) * 0.5 + 0.5;
|
||
|
||
// ====== 3. 闪烁高光 - 稀疏的亮点 ======
|
||
float sparkleNoise = noise(worldXZ * 20.0 + vec2(uTime * 0.3, -uTime * 0.2));
|
||
float sparkle = pow(sparkleNoise, 5.0);
|
||
|
||
// 组合效果
|
||
waterEffect = ripples * 0.5 + microWave * 0.3 + sparkle * 0.2;
|
||
|
||
// 颜色混合 - 湖水更平静,颜色变化更小
|
||
finalColor = deepWater;
|
||
finalColor = mix(finalColor, midWater, ripples * 0.4);
|
||
finalColor = mix(finalColor, lightWater, microWave * 0.25);
|
||
finalColor = mix(finalColor, vec3(0.52, 0.85, 1.0), sparkle * 0.2);
|
||
|
||
// 透明度 - 湖水更稳定
|
||
float depthNoise = noise(worldXZ * 2.0 + uTime * 0.1);
|
||
alpha = 0.7 + depthNoise * 0.08 + waterEffect * 0.1;
|
||
alpha = clamp(alpha, 0.65, 0.85);
|
||
}
|
||
}
|
||
// ============= 底面 =============
|
||
else {
|
||
// 底面保持暗色
|
||
finalColor *= 0.8;
|
||
alpha = 0.7;
|
||
}
|
||
|
||
// 简单的环境光照(减弱以保留动画效果)
|
||
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
|
||
float diffuse = max(dot(vNormal, lightDir), 0.0) * 0.2 + 0.8;
|
||
finalColor *= diffuse;
|
||
|
||
gl_FragColor = vec4(finalColor, alpha);
|
||
}
|
||
`;
|
||
|
||
interface VoxelLayerProps {
|
||
// Update to include ix, iy, iz which are present in actual VoxelData
|
||
data: {
|
||
x: number;
|
||
y: number;
|
||
z: number;
|
||
color: string;
|
||
heightScale?: number;
|
||
isHighRes?: boolean;
|
||
ix: number;
|
||
iy: number;
|
||
iz: number;
|
||
isMergedWater?: boolean;
|
||
mergedPositions?: Array<{
|
||
ix: number;
|
||
iy: number;
|
||
iz: number;
|
||
flowDirection?: { dx: number; dz: number };
|
||
}>;
|
||
flowDirection?: { dx: number; dz: number }; // 单个水体素的流向
|
||
}[];
|
||
isHighRes: boolean;
|
||
type: VoxelType;
|
||
onClick?: (x: number, z: number) => void;
|
||
}
|
||
|
||
/**
|
||
* 专门用于水体的网格渲染器
|
||
* 使用 Greedy Meshing / Face Culling 思想,去除相邻水体之间的内部面
|
||
* 解决半透明材质叠加导致的颗粒感和反射问题
|
||
*
|
||
* 动态效果:
|
||
* - 瀑布(垂直面):白色光带从上往下流动
|
||
* - 河流(水平面):涟漪波动 + 流向光带
|
||
*/
|
||
const WaterFlowMesh: React.FC<VoxelLayerProps> = ({ data, isHighRes, type, onClick }) => {
|
||
const meshRef = useRef<any>(null);
|
||
const materialRef = useRef<ShaderMaterial>(null);
|
||
|
||
// 创建 Shader Material
|
||
const material = useMemo(() => {
|
||
return new ShaderMaterial({
|
||
vertexShader: waterVertexShader,
|
||
fragmentShader: waterFragmentShader,
|
||
uniforms: {
|
||
uTime: { value: 0 },
|
||
uCascadeSpeed: { value: 2.0 }, // 瀑布流速(调整为更自然的速度)
|
||
uRippleSpeed: { value: 2.0 }, // 涟漪速度
|
||
uHighlightColor: { value: new Color('#e0f0ff') }, // 高光颜色(淡蓝白色)
|
||
},
|
||
transparent: true,
|
||
depthWrite: false,
|
||
});
|
||
}, []);
|
||
|
||
// 每帧更新 time uniform - 实现动画
|
||
useFrame((state) => {
|
||
if (material) {
|
||
material.uniforms.uTime.value = state.clock.elapsedTime;
|
||
}
|
||
});
|
||
|
||
const geometry = useMemo(() => {
|
||
if (!data.length) return new BufferGeometry();
|
||
|
||
// 1. 展开所有合并的水体素,构建完整的体素列表
|
||
interface ExpandedVoxel {
|
||
x: number;
|
||
y: number;
|
||
z: number;
|
||
ix: number;
|
||
iy: number;
|
||
iz: number;
|
||
color: string;
|
||
heightScale: number;
|
||
flowDirection?: { dx: number; dz: number }; // 真实的水流方向
|
||
}
|
||
|
||
const expandedVoxels: ExpandedVoxel[] = [];
|
||
|
||
data.forEach(v => {
|
||
if (v.isMergedWater && v.mergedPositions && v.mergedPositions.length > 0) {
|
||
// 展开合并的水体素(保留每个位置的流向)
|
||
v.mergedPositions.forEach(pos => {
|
||
expandedVoxels.push({
|
||
x: pos.ix * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE),
|
||
y: pos.iy * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE),
|
||
z: pos.iz * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE),
|
||
ix: pos.ix,
|
||
iy: pos.iy,
|
||
iz: pos.iz,
|
||
color: v.color,
|
||
heightScale: v.heightScale || 1,
|
||
flowDirection: pos.flowDirection, // 使用该位置的真实流向
|
||
});
|
||
});
|
||
} else {
|
||
// 普通水体素(直接添加,使用体素自身的流向)
|
||
expandedVoxels.push({
|
||
x: v.x,
|
||
y: v.y,
|
||
z: v.z,
|
||
ix: v.ix,
|
||
iy: v.iy,
|
||
iz: v.iz,
|
||
color: v.color,
|
||
heightScale: v.heightScale || 1,
|
||
flowDirection: v.flowDirection, // 使用体素的流向
|
||
});
|
||
}
|
||
});
|
||
|
||
// 2. 建立查找表 (Set of "ix|iy|iz")
|
||
const lookup = new Set<string>();
|
||
expandedVoxels.forEach(v => lookup.add(`${v.ix}|${v.iy}|${v.iz}`));
|
||
|
||
const positions: number[] = [];
|
||
const normals: number[] = [];
|
||
const colors: number[] = [];
|
||
const isCascadeArr: number[] = []; // 瀑布标记
|
||
const flowDirectionArr: 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] },
|
||
];
|
||
|
||
expandedVoxels.forEach(v => {
|
||
const { x, y, z, ix, iy, iz, color, heightScale = 1, flowDirection } = v;
|
||
const col = new Color(color);
|
||
|
||
// 只有当 heightScale 为 1 时才能完美拼接
|
||
const isFullBlock = Math.abs(heightScale - 1) < 0.01;
|
||
|
||
// 检测是否是瀑布:垂直方向有邻居水体
|
||
// 如果上方或下方有水体素,说明这是瀑布的一部分
|
||
const hasAbove = lookup.has(`${ix}|${iy + 1}|${iz}`);
|
||
const hasBelow = lookup.has(`${ix}|${iy - 1}|${iz}`);
|
||
const isCascade = hasAbove || hasBelow;
|
||
|
||
// 【使用真实的流向数据】
|
||
let flowDx = 0;
|
||
let flowDz = 0;
|
||
|
||
if (flowDirection) {
|
||
// 直接使用河流系统计算的真实流向(湖泊为 0,0)
|
||
flowDx = flowDirection.dx;
|
||
flowDz = flowDirection.dz;
|
||
}
|
||
// 注意:不再使用回退逻辑,因为真实流向已经在 terrain.ts 中正确设置
|
||
// 湖泊的流向就是 (0,0),不需要回退
|
||
|
||
dirs.forEach(dir => {
|
||
// 检查该方向是否有同类邻居
|
||
const neighborKey = `${ix + dir.off[0]}|${iy + dir.off[1]}|${iz + dir.off[2]}`;
|
||
const hasNeighbor = isFullBlock && lookup.has(neighborKey);
|
||
|
||
// 如果没有邻居,渲染该面
|
||
if (!hasNeighbor) {
|
||
// Calculate actual center based on heightScale logic from VoxelLayer
|
||
const centerY = y + (heightScale - 1) * (voxelSize / 2);
|
||
const scaleY = voxelSize * heightScale;
|
||
const halfY = scaleY / 2;
|
||
|
||
// Vertices relative to center
|
||
let v1, v2, v3, v4;
|
||
|
||
const hs = halfSize;
|
||
const hy = halfY;
|
||
|
||
// Using dir.normal to determine face
|
||
const [nx, ny, nz] = dir.normal;
|
||
|
||
// Logic to generate quad vertices
|
||
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 {
|
||
// Should not happen
|
||
v1=[0,0,0]; v2=[0,0,0]; v3=[0,0,0]; v4=[0,0,0];
|
||
}
|
||
|
||
// Translate to world pos
|
||
const applyPos = (v: number[]) => [v[0] + x, v[1] + centerY, v[2] + z];
|
||
const p1 = applyPos(v1);
|
||
const p2 = applyPos(v2);
|
||
const p3 = applyPos(v3);
|
||
const p4 = applyPos(v4);
|
||
|
||
// Push 2 triangles (CCW winding)
|
||
// Tri 1: p1, p2, p3
|
||
positions.push(...p1, ...p2, ...p3);
|
||
// Tri 2: p1, p3, p4
|
||
positions.push(...p1, ...p3, ...p4);
|
||
|
||
// Normals (same for all 6 verts)
|
||
for(let k=0; k<6; k++) normals.push(nx, ny, nz);
|
||
|
||
// Colors (same for all 6 verts)
|
||
for(let k=0; k<6; k++) colors.push(col.r, col.g, col.b);
|
||
|
||
// 瀑布标记:垂直面 + 有上下邻居
|
||
const isVerticalFace = Math.abs(ny) < 0.3;
|
||
const cascadeValue = (isVerticalFace && isCascade) ? 1.0 : 0.0;
|
||
for(let k=0; k<6; k++) isCascadeArr.push(cascadeValue);
|
||
|
||
// 流向 (same for all 6 verts)
|
||
for(let k=0; k<6; k++) {
|
||
flowDirectionArr.push(flowDx, flowDz);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
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));
|
||
bufGeom.setAttribute('isCascade', new Float32BufferAttribute(isCascadeArr, 1));
|
||
bufGeom.setAttribute('flowDirection', new Float32BufferAttribute(flowDirectionArr, 2));
|
||
|
||
return bufGeom;
|
||
}, [data, isHighRes]);
|
||
|
||
return (
|
||
<mesh
|
||
ref={meshRef}
|
||
geometry={geometry}
|
||
material={material}
|
||
castShadow
|
||
receiveShadow
|
||
/>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 专门用于冰块的网格渲染器
|
||
* 使用 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);
|
||
|
||
useLayoutEffect(() => {
|
||
const mesh = meshRef.current;
|
||
if (!mesh || data.length === 0) return;
|
||
|
||
// 明确使用配置的尺寸
|
||
const voxelSize = isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE;
|
||
|
||
data.forEach((voxel, i) => {
|
||
const hScale = voxel.heightScale ?? 1;
|
||
const scaleY = voxelSize * hScale;
|
||
tempObject.position.set(
|
||
voxel.x,
|
||
voxel.y + (hScale - 1) * (voxelSize / 2),
|
||
voxel.z
|
||
);
|
||
tempObject.scale.set(voxelSize, scaleY, voxelSize);
|
||
tempObject.updateMatrix();
|
||
mesh.setMatrixAt(i, tempObject.matrix);
|
||
|
||
tempColor.set(voxel.color);
|
||
mesh.setColorAt(i, tempColor);
|
||
});
|
||
|
||
mesh.instanceMatrix.needsUpdate = true;
|
||
|
||
}, [data, isHighRes]);
|
||
|
||
const handlePointerMove = (e: ThreeEvent<PointerEvent>) => {
|
||
e.stopPropagation();
|
||
if (e.instanceId === undefined) return;
|
||
|
||
const voxel = data[e.instanceId];
|
||
if (voxel) {
|
||
const lx = Math.floor(voxel.x / TILE_SIZE);
|
||
const lz = Math.floor(voxel.z / TILE_SIZE);
|
||
setHoveredTile({ x: lx, z: lz });
|
||
}
|
||
};
|
||
|
||
const handleClick = (e: ThreeEvent<MouseEvent>) => {
|
||
e.stopPropagation();
|
||
if (e.instanceId === undefined || !onClick) return;
|
||
const voxel = data[e.instanceId];
|
||
if (voxel) {
|
||
const lx = Math.floor(voxel.x / TILE_SIZE);
|
||
const lz = Math.floor(voxel.z / TILE_SIZE);
|
||
onClick(lx, lz);
|
||
}
|
||
}
|
||
|
||
const materialProps = useMemo(() => {
|
||
// Water is now handled by WaterFlowMesh, but keeping this for fallback or other transparent blocks
|
||
if (type === 'water') {
|
||
return {
|
||
roughness: 0.1,
|
||
metalness: 0.2,
|
||
transparent: true,
|
||
opacity: 0.75
|
||
};
|
||
} else if (type === 'ice') {
|
||
return {
|
||
roughness: 0.95, // 提高粗糙度,降低反射强度
|
||
metalness: 0.01, // 降低金属感
|
||
transparent: true,
|
||
opacity: 0.60
|
||
};
|
||
}
|
||
return {
|
||
roughness: 0.9,
|
||
metalness: 0.2,
|
||
transparent: false,
|
||
opacity: 1.0
|
||
};
|
||
}, [type]);
|
||
|
||
return (
|
||
<instancedMesh
|
||
ref={meshRef}
|
||
args={[undefined, undefined, data.length]}
|
||
castShadow
|
||
receiveShadow
|
||
onPointerMove={handlePointerMove}
|
||
onClick={handleClick}
|
||
>
|
||
<boxGeometry args={[1, 1, 1]} />
|
||
<meshStandardMaterial
|
||
roughness={materialProps.roughness}
|
||
metalness={materialProps.metalness}
|
||
transparent={materialProps.transparent}
|
||
opacity={materialProps.opacity}
|
||
alphaTest={0}
|
||
depthWrite={!materialProps.transparent}
|
||
/>
|
||
</instancedMesh>
|
||
);
|
||
};
|
||
|
||
interface ChunkRendererProps {
|
||
onVoxelClick?: (x: number, z: number) => void;
|
||
}
|
||
|
||
export const ChunkRenderer: React.FC<ChunkRendererProps> = ({ onVoxelClick }) => {
|
||
const voxels = useMapStore((state) => state.voxels);
|
||
|
||
const groupedVoxels = useMemo(() => {
|
||
const groups: Record<string, typeof voxels> = {};
|
||
voxels.forEach((v) => {
|
||
const key = `${v.type}|${v.isHighRes ? 'true' : 'false'}`;
|
||
if (!groups[key]) groups[key] = [];
|
||
groups[key].push(v);
|
||
});
|
||
return groups;
|
||
}, [voxels]);
|
||
|
||
return (
|
||
<group>
|
||
{Object.keys(groupedVoxels).map((key) => {
|
||
const [type, isHighResStr] = key.split('|');
|
||
const isHighRes = isHighResStr === 'true';
|
||
|
||
// 特殊处理水体:使用 Face Culling 网格来消除内部面
|
||
if (type === 'water') {
|
||
return (
|
||
<WaterFlowMesh
|
||
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}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// 特殊处理冰块类型:使用 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}
|
||
isHighRes={isHighRes}
|
||
type={type as VoxelType}
|
||
// @ts-ignore
|
||
data={groupedVoxels[key]}
|
||
onClick={onVoxelClick}
|
||
/>
|
||
);
|
||
})}
|
||
</group>
|
||
);
|
||
};
|