Vue中使用Three.js实现动态海洋与天空背景
作者:mb683dbd30b8b2c
背景
常规的后台管理系统登陆页面可能就只是一个简单的背景页面,这不太好看,接下来让我们来使用three.js来实现一个动态的海洋和天空效果当作背景,这样的效果总会让人眼前一亮,如下图所示。
代码实现
接下来,让我们用trae来编写实现这个功能吧。
1. 组合式 API 初始化
import { onMounted, onBeforeUnmount } from "vue"; import * as THREE from "three"; import { Water } from "three/examples/jsm/objects/Water.js"; import { Sky } from "three/examples/jsm/objects/Sky.js";
Vue 组合式 API:使用 onMounted
和 onBeforeUnmount
来处理组件的生命周期。在组件挂载时初始化场景,卸载时清理资源。
Three.js 导入:导入 THREE
来处理 3D 渲染,Water
和 Sky
分别处理水面和天空的效果。
2. 初始化 Three.js 场景
let scene: THREE.Scene; let camera: THREE.PerspectiveCamera; let renderer: THREE.WebGLRenderer; let water: any; let sun: THREE.Vector3; let sky: any; let animationFrameId: number;
变量声明:在 useOcean
函数中声明了多个变量,用于保存 Three.js 的场景、相机、渲染器、以及水面和天空的实例。animationFrameId
用于控制动画帧的请求。
const initThree = () => { const container = document.getElementById(canvasId); if (!container) { console.warn(`Canvas element with id '${canvasId}' not found`); return; } scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 20000); camera.position.set(30, 30, 100); camera.lookAt(0, 0, 0); sun = new THREE.Vector3(); renderer = new THREE.WebGLRenderer({ canvas: container, antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.5; }
场景与相机初始化:创建了一个 Three.js 场景,并使用 PerspectiveCamera
创建相机,设置了相机的位置和朝向。
渲染器初始化:创建了一个 WebGLRenderer
,并设置了反走样(antialias
)和透明背景(alpha
)。同时设置了渲染器的大小和色调映射。
3. 创建水面效果
const waterGeometry = new THREE.PlaneGeometry(10000, 10000); water = new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: new THREE.TextureLoader().load( "https://threejs.org/examples/textures/waternormals.jpg", function (texture) { texture.wrapS = texture.wrapT = THREE.RepeatWrapping; } ), sunDirection: new THREE.Vector3(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: scene.fog !== undefined, }); water.rotation.x = -Math.PI / 2; scene.add(water);
水面几何体:使用 THREE.PlaneGeometry
创建了一个大的平面,作为海面基础。
水面着色器:使用 Water
对象并传入配置项,设置水面波动、光照、颜色等属性。
水面纹理:加载了一个水面法线贴图,并设置为重复模式。
4. 创建天空效果
sky = new Sky(); sky.scale.setScalar(10000); scene.add(sky); const skyUniforms = sky.material.uniforms; skyUniforms["turbidity"].value = 10; skyUniforms["rayleigh"].value = 2; skyUniforms["mieCoefficient"].value = 0.005; skyUniforms["mieDirectionalG"].value = 0.8; const parameters = { elevation: 2, azimuth: 180, };
天空对象:使用 Sky
对象创建了一个天空,并通过设置 scale
来放大天空的大小。
天空着色器的配置:调整了 turbidity
(浑浊度)、rayleigh
(瑞利散射)、mieCoefficient
(米散射系数)等参数来改变天空的效果。
5. 更新太阳位置与场景环境
const pmremGenerator = new THREE.PMREMGenerator(renderer); let renderTarget: THREE.WebGLRenderTarget; function updateSun() { const phi = THREE.MathUtils.degToRad(90 - parameters.elevation); const theta = THREE.MathUtils.degToRad(parameters.azimuth); sun.setFromSphericalCoords(1, phi, theta); sky.material.uniforms["sunPosition"].value.copy(sun); water.material.uniforms["sunDirection"].value.copy(sun).normalize(); if (renderTarget !== undefined) renderTarget.dispose(); renderTarget = pmremGenerator.fromScene(sky as any); scene.environment = renderTarget.texture; } updateSun();
太阳位置更新:通过 elevation
和 azimuth
参数计算太阳的位置,并将其应用于天空和水面材质的着色器中,使太阳的位置影响场景中的光照和水面反射。
6. 动画与渲染循环
水面动画:通过每帧更新水面着色器的 time
值,触发水面动画效果。
渲染循环:使用 requestAnimationFrame
实现每一帧的渲染。
7. 处理窗口大小变化
const handleResize = () => { if (camera && renderer) { try { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } catch (error) { console.error("Error during resize:", error); } } };
响应窗口变化:当窗口大小变化时,更新相机的 aspect
比例并重新调整渲染器的大小,确保渲染效果不变形。
8. 资源清理
const cleanup = () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = 0; } if (renderer) { renderer.dispose(); } if (scene) { while (scene.children.length > 0) { scene.remove(scene.children[0]); } } };
清理动画和资源:当组件卸载时,清除动画帧和渲染器,移除场景中的所有对象,防止内存泄漏。
9. 生命周期钩子
onMounted(() => { initThree(); window.addEventListener("resize", handleResize); }); onBeforeUnmount(() => { window.removeEventListener("resize", handleResize); cleanup(); });
生命周期钩子:在组件挂载时初始化 Three.js 场景,并在卸载时清理资源。
完整源码
完整源码如下:
import { onMounted, onBeforeUnmount } from "vue"; import * as THREE from "three"; // 导入海洋着色器 import { Water } from "three/examples/jsm/objects/Water.js"; import { Sky } from "three/examples/jsm/objects/Sky.js"; export function useOcean(canvasId: string) { // Three.js 相关变量 let scene: THREE.Scene; let camera: THREE.PerspectiveCamera; let renderer: THREE.WebGLRenderer; let water: any; let sun: THREE.Vector3; let sky: any; let animationFrameId: number; // 初始化Three.js场景 const initThree = () => { const container = document.getElementById(canvasId); if (!container) { console.warn(`Canvas element with id '${canvasId}' not found`); return; } // 创建场景 scene = new THREE.Scene(); // 创建相机 camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 20000 ); camera.position.set(30, 30, 100); camera.lookAt(0, 0, 0); // 创建太阳光源 sun = new THREE.Vector3(); // 创建渲染器 renderer = new THREE.WebGLRenderer({ canvas: container, antialias: true, alpha: true, }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.5; // 创建水面 const waterGeometry = new THREE.PlaneGeometry(10000, 10000); water = new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: new THREE.TextureLoader().load( "https://threejs.org/examples/textures/waternormals.jpg", function (texture) { texture.wrapS = texture.wrapT = THREE.RepeatWrapping; } ), sunDirection: new THREE.Vector3(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: scene.fog !== undefined, }); water.rotation.x = -Math.PI / 2; scene.add(water); // 创建天空 sky = new Sky(); sky.scale.setScalar(10000); scene.add(sky); const skyUniforms = sky.material.uniforms; skyUniforms["turbidity"].value = 10; skyUniforms["rayleigh"].value = 2; skyUniforms["mieCoefficient"].value = 0.005; skyUniforms["mieDirectionalG"].value = 0.8; const parameters = { elevation: 2, azimuth: 180, }; const pmremGenerator = new THREE.PMREMGenerator(renderer); let renderTarget: THREE.WebGLRenderTarget; function updateSun() { const phi = THREE.MathUtils.degToRad(90 - parameters.elevation); const theta = THREE.MathUtils.degToRad(parameters.azimuth); sun.setFromSphericalCoords(1, phi, theta); sky.material.uniforms["sunPosition"].value.copy(sun); water.material.uniforms["sunDirection"].value.copy(sun).normalize(); if (renderTarget !== undefined) renderTarget.dispose(); renderTarget = pmremGenerator.fromScene(sky as any); scene.environment = renderTarget.texture; } updateSun(); // 添加环境光 const ambient = new THREE.AmbientLight(0x555555); scene.add(ambient); animate(); }; // 动画循环 const animate = () => { if (!scene || !camera || !renderer || !water) { return; } // 更新水面动画 water.material.uniforms["time"].value += 1.0 / 60.0; renderer.render(scene, camera); animationFrameId = requestAnimationFrame(animate); }; // 处理窗口大小变化 const handleResize = () => { if (camera && renderer) { try { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } catch (error) { console.error("Error during resize:", error); } } }; // 清理资源 const cleanup = () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = 0; } if (renderer) { renderer.dispose(); } // 清理场景中的对象 if (scene) { while (scene.children.length > 0) { scene.remove(scene.children[0]); } } }; // 生命周期钩子 onMounted(() => { initThree(); window.addEventListener("resize", handleResize); }); onBeforeUnmount(() => { window.removeEventListener("resize", handleResize); cleanup(); }); return { // 如果需要暴露更多方法或属性,可以在这里添加 }; }
使用示例:
<canvas id="bg-canvas"></canvas>
useOcean('bg-canvas');
总结
以上我们就完成了一个动态的海洋和天空效果,它让我们的登陆页显得更加高大上档次,并且也展示了如何在 Vue 中集成复杂的 3D 渲染,同时确保了在窗口大小变化时的适配,以及在组件卸载时正确清理资源,通过合理的生命周期管理和资源清理,确保了程序的稳定性和性能。
到此这篇关于Vue中使用Three.js实现动态海洋与天空背景的文章就介绍到这了,更多相关Vue用Three.js实现动态背景内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!