<canvas ref="bjsCanvas" class="render-canvas" />
加载中...
<button @click="toggleInspector">调试面板</button>
<button @click="resetCamera">重置视角</button>
<button @click="openLightPanel">灯光控制</button>
<button @click="openAnimationPanel">动画控制</button>
<button @click="openLightAnimationPanel">灯光动画</button>
<button @click="openVolumeLightPanel">体积光控制</button>
<button @click="toggleVolumeLights">
{{ volumeLightsEnabled ? '体积光:开' : '体积光:关' }}
</button>
<LightControlPanel ref="lightPanel" />
<VolumeLightControlPanel ref="volumeLightPanel" :volumeLights="volumeLightsArray" />
<AnimationControlPanel ref="animationPanel" />
<LightAnimationPanel ref="lightAnimationPanel" />
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import {
Engine, Scene, ArcRotateCamera, Vector3, HemisphericLight,
SceneLoader, Color3, MeshBuilder, HavokPlugin, SpotLight,
Texture, VolumetricLightScatteringPostProcess,
ActionManager, ExecuteCodeAction, StandardMaterial, WebGPUEngine,ExponentialHeightFog
} from '@babylonjs/core';
import '@babylonjs/loaders';
import '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManager';
import { useSceneStore } from '../stores/sceneStore';
import LightControlPanel from '../components/ControlPanel.vue';
import HavokPhysics from "@babylonjs/havok";
import AnimationControlPanel from '../components/AnimationControlPanel.vue';
import { useAnimationStore } from '../stores/animationStore';
import LightAnimationPanel from '../components/LightAnimationPanel.vue';
import VolumeLightControlPanel from '../components/VolumeLightControlPanel.vue';
const bjsCanvas = ref(null);
const isLoading = ref(true);
const lightPanel = ref(null);
const lightAnimationPanel = ref(null);
const animationPanel = ref(null);
const volumeLightPanel = ref(null);
const sceneStore = useSceneStore();
const animationStore = useAnimationStore();
let noiseTexture = null;
// 体积光状态管理
const volumeLightsEnabled = ref(true);
const volumeLights = ref([]); // 存储所有体积光对象
const volumeLightsArray = computed(() => volumeLights.value);
const createOptimizedEngine = async () => {
try {
if (navigator.gpu) {
const webgpuEngine = new WebGPUEngine(bjsCanvas.value);
await webgpuEngine.initAsync();
webgpuEngine.setHardwareScalingLevel(1.0);
// WebGPU兼容:禁用多采样
// webgpuEngine.setMultiview(false);
return webgpuEngine;
}
} catch (error) {
console.error("WebGPU回退到WebGL:", error);
const engine = new Engine(bjsCanvas.value, true);
engine.setHardwareScalingLevel(0.8);
return engine;
}
};
const openVolumeLightPanel = () => {
if (volumeLightPanel.value) {
volumeLightPanel.value.open();
}
};
const openLightAnimationPanel = () => {
if (lightAnimationPanel.value) {
lightAnimationPanel.value.open();
}
};
const openAnimationPanel = () => {
if (animationPanel.value) {
animationPanel.value.open();
}
};
const loadHavokPhysics = async () => {
try {
const wasmUrl = "/HavokPhysics.wasm";
const response = await fetch(wasmUrl);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const buffer = await response.arrayBuffer();
const HavokModule = await import("@babylonjs/havok");
return HavokModule.HavokPhysics ?
HavokModule.HavokPhysics({ wasmBinary: buffer }) :
HavokModule.default({ wasmBinary: buffer });
} catch (error) {
console.error("Havok加载失败:", error);
try {
const HavokModule = await import("@babylonjs/havok");
return HavokModule.HavokPhysics ?
HavokModule.HavokPhysics({ wasmWorker: false }) :
HavokModule.default({ wasmWorker: false });
} catch (fallbackError) {
console.error("Havok兼容模式失败:", fallbackError);
return null;
}
}
};
/**
* 专业级体积光创建函数(WebGPU兼容版)
* 此函数为指定灯光创建高质量体积光效果,支持聚光灯和点光源
* 体积光效果会根据灯光属性自动调整,提供真实的光线散射效果
*
* @param {Light} light - 要创建体积光的灯光对象,可以是SpotLight或PointLight
* @returns {VolumetricLightScatteringPostProcess|null} 成功创建的体积光对象,失败时返回null
*/
const createVolumeLightForLight = (light) => {
try {
// 获取灯光的基础属性用于体积光配置
const lightIntensity = light.intensity || 1;
// 使用白色偏蓝的浅大气颜色 - 模拟自然光效
const lightColor = new Color3(0.85, 0.9, 1.0);
const lightRange = light.range || 10;
// 根据灯光类型和属性动态调整体积光参数
let resolutionRatio, samples, noiseScale;
// 聚光灯体积光配置
if (light.getClassName().includes('Spot')) {
resolutionRatio = 1.0; // 最高分辨率,确保最佳质量
samples = 1; // WebGPU兼容:强制设置为1,避免多采样问题
noiseScale = 3.0; // 噪点缩放比例,影响体积光的纹理细节
}
// 点光源体积光配置
else if (light.getClassName().includes('Point')) {
resolutionRatio = 1.0; // 最高分辨率
samples = 1; // WebGPU兼容:强制设置为1
noiseScale = 2.5; // 点光源使用稍小的噪点缩放
}
// 其他灯光类型配置
else {
resolutionRatio = 1.0; // 最高分辨率
samples = 1; // WebGPU兼容:强制设置为1
noiseScale = 2.0; // 默认噪点缩放
}
// 创建体积光散射后处理效果实例
const volumeLight = new VolumetricLightScatteringPostProcess(
`volLight_${light.name}`, // 体积光名称
resolutionRatio, // 分辨率比例
camera, // 场景相机
null, // 自定义网格(可选)
samples, // 采样数量
Texture.BILINEAR_SAMPLINGMODE, // 纹理采样模式
engine // 渲染引擎
);
// 检查是否已创建共享噪点纹理
if (!noiseTexture) {
// 创建噪点纹理 - 使用高质量噪点贴图增强体积光效果
noiseTexture = new Texture("/textures/noise.png", scene);
noiseTexture.uScale = 2.0; // U方向纹理缩放
noiseTexture.vScale = 2.0; // V方向纹理缩放
}
// 应用噪点纹理和颜色到体积光材质
volumeLight.mesh.material.diffuseTexture = noiseTexture;
volumeLight.mesh.material.diffuseColor = lightColor;
volumeLight.mesh.material.alpha = 0.8; // 透明度设置,确保效果明显可见
// 根据灯光类型配置不同的散射效果参数
if (light.getClassName().includes('Spot')) {
const angle = light.angle || Math.PI / 3; // 聚光灯角度
const exponent = light.exponent || 1; // 聚光灯指数
// 聚光灯散射参数 - 优化以产生明显的体积光效果
volumeLight.scatterSettings = {
density: 1.2, // 散射密度 - 控制光束的浓度和可见度
decay: 0.9, // 衰减速度 - 控制光束随距离的减弱程度
weight: 0.8, // 散射权重 - 控制光束的整体强度
exposure: 1.0 // 曝光强度 - 控制光束的亮度
};
} else if (light.getClassName().includes('Point')) {
// 点光源散射参数
volumeLight.scatterSettings = {
density: 1.0, // 散射密度
decay: 0.8, // 衰减速度
weight: 0.7, // 散射权重
exposure: 0.9 // 曝光强度
};
} else {
// 默认散射参数
volumeLight.scatterSettings = {
density: 0.8, // 散射密度
decay: 0.7, // 衰减速度
weight: 0.6, // 散射权重
exposure: 0.8 // 曝光强度
};
}
// 设置网格位置和方向
volumeLight.mesh.parent = light;
// 对于聚光灯,调整网格方向使其向下照射
if (light.getClassName().includes('Spot')) {
volumeLight.mesh.rotation.x = Math.PI / 2; // 旋转网格使其向下照射
volumeLight.mesh.position.y = -0.5; // 稍微向下偏移,确保光效与灯光方向一致
}
// 设置网格缩放以匹配灯光范围
if (light.range) {
const scale = light.range / 8; // 缩放因子,确保体积光范围与灯光范围匹配
volumeLight.mesh.scaling.set(scale, scale, scale);
}
// 添加完整的自定义控制属性
volumeLight.customProperties = {
// 基础参数
resolutionRatio: resolutionRatio, // 分辨率比例
samples: samples, // 采样数量
noiseScale: noiseScale, // 噪点缩放
// 原始属性
originalIntensity: lightIntensity, // 原始灯光强度
originalColor: lightColor.clone(), // 原始灯光颜色
originalRange: lightRange, // 原始灯光范围
// 可调节的参数范围
minResolution: 0.5, // 最小分辨率比例
maxResolution: 1.5, // 最大分辨率比例
minSamples: 1, // 最小采样数
maxSamples: 1, // 最大采样数
minNoiseScale: 0.5, // 最小噪点缩放
maxNoiseScale: 5.0, // 最大噪点缩放
minDensity: 0.1, // 最小散射密度
maxDensity: 3.0, // 最大散射密度
minDecay: 0.1, // 最小衰减速度
maxDecay: 2.0, // 最大衰减速度
minWeight: 0.1, // 最小散射权重
maxWeight: 2.0, // 最大散射权重
minExposure: 0.1, // 最小曝光强度
maxExposure: 3.0, // 最大曝光强度
minAlpha: 0.1, // 最小透明度
maxAlpha: 1.0, // 最大透明度
// 完整的控制方法
setResolution: function (value) {
this.resolutionRatio = Math.max(this.minResolution, Math.min(this.maxResolution, value));
},
setSamples: function (value) {
this.samples = 1; // WebGPU兼容:采样数固定为1
},
setNoiseScale: function (value) {
this.noiseScale = Math.max(this.minNoiseScale, Math.min(this.maxNoiseScale, value));
if (noiseTexture) {
noiseTexture.uScale = this.noiseScale;
noiseTexture.vScale = this.noiseScale;
}
},
setDensity: function (value) {
this.scatterSettings.density = Math.max(this.minDensity, Math.min(this.maxDensity, value));
},
setDecay: function (value) {
this.scatterSettings.decay = Math.max(this.minDecay, Math.min(this.maxDecay, value));
},
setWeight: function (value) {
this.scatterSettings.weight = Math.max(this.minWeight, Math.min(this.maxWeight, value));
},
setExposure: function (value) {
this.scatterSettings.exposure = Math.max(this.minExposure, Math.min(this.maxExposure, value));
},
setColor: function (color) {
this.mesh.material.diffuseColor = color;
},
setAlpha: function (value) {
this.mesh.material.alpha = Math.max(this.minAlpha, Math.min(this.maxAlpha, value));
},
setIntensity: function (value) {
const alpha = Math.min(1, value / this.originalIntensity);
this.mesh.material.alpha = Math.max(this.minAlpha, Math.min(this.maxAlpha, alpha));
},
reset: function () {
this.scatterSettings.density = 1.2;
this.scatterSettings.decay = 0.9;
this.scatterSettings.weight = 0.8;
this.scatterSettings.exposure = 1.0;
this.mesh.material.diffuseColor = this.originalColor.clone();
this.mesh.material.alpha = 0.8;
if (noiseTexture) {
noiseTexture.uScale = this.noiseScale;
noiseTexture.vScale = this.noiseScale;
}
}
};
// 绑定所有控制方法
volumeLight.setResolution = volumeLight.customProperties.setResolution.bind(volumeLight);
volumeLight.setSamples = volumeLight.customProperties.setSamples.bind(volumeLight);
volumeLight.setNoiseScale = volumeLight.customProperties.setNoiseScale.bind(volumeLight);
volumeLight.setDensity = volumeLight.customProperties.setDensity.bind(volumeLight);
volumeLight.setDecay = volumeLight.customProperties.setDecay.bind(volumeLight);
volumeLight.setWeight = volumeLight.customProperties.setWeight.bind(volumeLight);
volumeLight.setExposure = volumeLight.customProperties.setExposure.bind(volumeLight);
volumeLight.setColor = volumeLight.customProperties.setColor.bind(volumeLight);
volumeLight.setAlpha = volumeLight.customProperties.setAlpha.bind(volumeLight);
volumeLight.setIntensity = volumeLight.customProperties.setIntensity.bind(volumeLight);
volumeLight.reset = volumeLight.customProperties.reset.bind(volumeLight);
return volumeLight;
} catch (error) {
console.error(`创建体积光失败: ${error.message}`);
return null;
}
};
// 切换体积光状态
const toggleVolumeLights = () => {
volumeLightsEnabled.value = !volumeLightsEnabled.value;
console.log(`体积光状态已切换为: ${volumeLightsEnabled.value ? '开启' : '关闭'}`);
// 切换所有体积光的可见性
volumeLights.value.forEach((vol, index) => {
if (vol) {
// 使用更高效的方式切换体积光状态
if (vol.mesh && vol.mesh.material) {
// 通过修改材质透明度而不是完全禁用,性能更好
vol.mesh.material.alpha = volumeLightsEnabled.value ? 1.0 : 0.01;
}
vol.enabled = volumeLightsEnabled.value;
}
});
};
// 对于重复的网格,使用实例化渲染
const enableInstancing = () => {
const meshMap = new Map();
scene.meshes.forEach(mesh => {
if (mesh.name && mesh.name.includes("重复元素")) {
if (!meshMap.has(mesh.name)) {
meshMap.set(mesh.name, []);
}
meshMap.get(mesh.name).push(mesh);
}
});
// 为重复网格创建实例
meshMap.forEach((meshes, name) => {
if (meshes.length > 5) {
const sourceMesh = meshes[0];
for (let i = 1; i < meshes.length; i++) {
const instance = sourceMesh.createInstance(`${name}_instance_${i}`);
instance.position = meshes[i].position.clone();
instance.rotation = meshes[i].rotation.clone();
meshes[i].dispose(); // 移除原始网格
}
}
});
};
// 添加细节层次系统
const setupLOD = () => {
scene.meshes.forEach(mesh => {
if (mesh.geometry && mesh.geometry.getTotalVertices() > 1000) {
// 创建低细节版本
const lodMesh = mesh.clone(`${mesh.name}_lod`);
lodMesh.position = mesh.position.clone();
lodMesh.setEnabled(false);
// 根据距离启用/禁用LOD
scene.registerBeforeRender(() => {
if (!camera) return;
const distance = Vector3.Distance(mesh.position, camera.position);
if (distance > 50) {
mesh.setEnabled(false);
lodMesh.setEnabled(true);
} else {
mesh.setEnabled(true);
lodMesh.setEnabled(false);
}
});
}
});
};
// 添加性能监控函数
const setupPerformanceMonitoring = () => {
let monitoringInterval = null;
const startMonitoring = () => {
monitoringInterval = setInterval(() => {
if (engine) {
const fps = engine.getFps().toFixed(1);
console.log(`FPS: ${fps}, 体积光数量: ${volumeLights.value.length}`);
// 如果FPS过低,自动调整质量
if (fps < 20 && volumeLights.value.length > 0) {
console.warn("FPS过低,自动降低体积光质量");
volumeLights.value.forEach(vol => {
if (vol && vol.scatterSettings) {
vol.scatterSettings.density *= 0.7;
vol.scatterSettings.exposure *= 0.7;
}
});
}
}
}, 3000); // 每3秒检查一次
};
const stopMonitoring = () => {
if (monitoringInterval) {
clearInterval(monitoringInterval);
monitoringInterval = null;
}
};
return { startMonitoring, stopMonitoring };
};
let engine, scene, camera;
// 在initScene函数中添加优化
const initScene = async () => {
console.log("开始初始化场景...");
// 使用优化后的引擎创建方法
engine = await createOptimizedEngine();
if (!engine) {
console.error("引擎初始化失败");
return;
}
scene = new Scene(engine);
sceneStore.setScene(scene);
//雾气
// 添加指数高度雾
try {
const fog = new ExponentialHeightFog("fog", scene);
fog.density = 0.01;
fog.fogHeight = 10.0;
fog.fogHeightFallOff = 0.2;
fog.color = new Color3(0.85, 0.9, 1.0); // 淡蓝色
console.log("指数高度雾创建成功");
} catch (error) {
console.error("创建指数高度雾失败:", error);
// 回退到简单的指数雾
scene.fogMode = Scene.FOGMODE_EXP2;
scene.fogDensity = 0.01;
scene.fogColor = new Color3(0.85, 0.9, 1.0);
}
// 设置性能监控
const performanceMonitor = setupPerformanceMonitoring();
performanceMonitor.startMonitoring();
console.log("创建相机...");
camera = new ArcRotateCamera(
"camera",
Math.PI / 2,
Math.PI / 4,
10,
Vector3.Zero(),
scene
);
camera.attachControl(bjsCanvas.value, true);
camera.lowerRadiusLimit = 3;
camera.upperRadiusLimit = 20;
console.log("创建环境光...");
const ambientLight = new HemisphericLight(
"ambient",
new Vector3(0, 1, 0),
scene
);
ambientLight.intensity = 0.5;
// 加载glTF模型
try {
isLoading.value = true;
console.log("开始加载glTF模型...");
const result = await SceneLoader.ImportMeshAsync(
"",
"/",
"转盘3.gltf",
scene
);
// 模型加载后优化
optimizeScene();
// 延迟创建体积光,确保场景稳定
setTimeout(() => {
console.log("收集场景灯光...");
sceneStore.collectLights();
createVolumeLightsForLights();
}, 500);
} catch (error) {
console.error("模型加载失败:", error);
} finally {
isLoading.value = false;
}
// 优化渲染循环
setupRenderLoop();
window.addEventListener("resize", () => engine.resize());
};
// 场景优化函数
const optimizeScene = () => {
// 材质优化
scene.meshes.forEach(mesh => {
if (mesh.material) {
mesh.material.backFaceCulling = true;
mesh.material.freeze();
}
});
// 实例化优化
enableInstancing();
// LOD系统
setupLOD();
};
// 设置渲染循环
const setupRenderLoop = () => {
let lastFrameTime = performance.now();
const targetFPS = 60;
const frameInterval = 1000 / targetFPS;
let frameCount = 0;
let lastFpsUpdate = performance.now();
engine.runRenderLoop(() => {
const currentTime = performance.now();
const elapsed = currentTime - lastFrameTime;
// 帧率控制
if (elapsed < frameInterval) return;
lastFrameTime = currentTime - (elapsed % frameInterval);
// 帧率统计
frameCount++;
if (currentTime - lastFpsUpdate >= 1000) {
const fps = frameCount;
frameCount = 0;
lastFpsUpdate = currentTime;
// 只在FPS变化较大时输出日志,避免控制台 spam
if (fps < 30) {
console.log(`当前FPS: ${fps}`);
}
}
// 使用场景主动渲染
if (scene.activeCamera) {
scene.render();
}
});
};
// ✅ 优化5:资源销毁逻辑
onUnmounted(() => {
console.log("组件卸载");
// 体积光特殊处理
volumeLights.value.forEach(vol => {
if (vol?.mesh) {
vol.mesh.parent = null;
vol.mesh.material?.dispose();
}
vol.dispose();
});
volumeLights.value = [];
// 共享纹理销毁
if (noiseTexture) {
noiseTexture.dispose();
noiseTexture = null;
}
// 场景资源清理
if (scene) {
scene.dispose();
}
// 引擎清理
if (engine) {
engine.dispose();
}
sceneStore.setScene(null);
});
// 修改后的 createVolumeLightsForLights 函数
const createVolumeLightsForLights = () => {
console.log("开始创建体积光...");
// 清空之前的体积光
volumeLights.value.forEach(vol => {
if (vol) {
console.log(`销毁体积光: ${vol.name}`);
vol.dispose();
}
});
volumeLights.value = [];
// ✅ 修正过滤条件:只对聚光灯和点光源创建体积光
const supportedLights = Object.values(sceneStore.lights)
.filter(light => {
if (!light) {
console.warn("发现无效灯光对象");
return false;
}
// ✅ 修正:检查是否为聚光灯 (Spot) 或点光源 (Point),排除环境光
const isSupportedLight = (light.type === 'Spot' || light.type === 'Point') &&
light.name !== 'ambient';
const hasNode = !!light.node;
if (!hasNode) {
console.warn(`灯光 ${light.name || '未命名'} 缺少节点属性`, light);
}
return isSupportedLight && hasNode;
});
console.log(`发现 ${supportedLights.length} 个支持的灯光`);
// ✅ 限制体积光数量,避免创建过多
const maxVolumeLights = 3;
let createdCount = 0;
supportedLights.forEach(light => {
if (createdCount >= maxVolumeLights) {
console.log(`已达到最大体积光数量限制 (${maxVolumeLights}),跳过创建`);
return;
}
try {
console.log(`为灯光 ${light.name} 创建体积光...`);
if (!light.node) {
console.error(`灯光 ${light.name} 的节点为空`);
return;
}
const volLight = createVolumeLightForLight(light.node);
if (volLight) {
volumeLights.value.push(volLight);
volLight.enabled = volumeLightsEnabled.value;
console.log(`体积光 ${volLight.name} 创建成功`);
createdCount++;
}
} catch (error) {
console.error(`创建体积光失败: ${error.message}`, error);
}
});
console.log(`创建体积光数量: ${volumeLights.value.length}`);
};
const highlightMesh = (targetMesh) => {
const currentScene = sceneStore.scene;
if (!currentScene || typeof currentScene.getMeshes !== 'function') return;
currentScene.getMeshes().forEach(mesh => {
if (mesh.material && mesh.metadata?.originalColor) {
mesh.material.diffuseColor = mesh.metadata.originalColor;
}
});
if (targetMesh.material) {
targetMesh.material.diffuseColor = new Color3(1, 0.5, 0);
}
};
const toggleInspector = async () => {
try {
const inspector = await import('@babylonjs/inspector')
if (scene.debugLayer.isVisible()) {
scene.debugLayer.hide();
} else {
scene.debugLayer.show();
}
} catch (error) {
console.error('调试面板加载失败:', error);
}
};
const resetCamera = () => {
if (!camera) return;
camera.setPosition(new Vector3(0, 5, -10));
if (camera.setTarget) camera.setTarget(Vector3.Zero());
camera.radius = 10;
};
const openLightPanel = () => {
if (lightPanel.value) {
lightPanel.value.open();
}
};
onMounted(() => {
console.log("组件挂载");
initScene();
});
onUnmounted(() => {
console.log("组件卸载");
if (scene) {
scene.onPointerDown = null;
// 清理体积光资源
volumeLights.value.forEach(volumeLight => {
if (volumeLight) {
console.log(`销毁体积光: ${volumeLight.name}`);
volumeLight.dispose();
}
});
}
if (engine) {
console.log("销毁引擎");
engine.dispose();
}
sceneStore.setScene(null);
});
</script>
<style scoped>
.scene-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.render-canvas {
width: 100%;
height: 100%;
touch-action: none;
outline: none;
}
.loading {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
z-index: 10;
}
.controls {
position: absolute;
bottom: 30px;
right: 20px;
display: flex;
gap: 10px;
z-index: 10;
flex-wrap: wrap;
}
.controls button {
padding: 8px 15px;
background: rgba(40, 40, 40, 0.8);
color: white;
border: 1px solid #555;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
font-size: 12px;
}
.controls button:hover {
background: rgba(60, 60, 60, 0.9);
}
/* 体积光按钮样式 */
.controls button:last-child {
background: rgba(40, 80, 40, 0.8);
}
</style>
我现在的体积光为什么不可见