背景
常规的后台管理系统登陆页面可能就只是一个简单的背景页面,这不太好看,接下来让我们来使用three.js来实现一个动态的海洋和天空效果当作背景,这样的效果总会让人眼前一亮,如下图所示。

代码实现
接下来,让我们用trae来编写实现这个功能吧。
1. 组合式 API 初始化
ts
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 场景
ts
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
用于控制动画帧的请求。
ts
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. 创建水面效果
ts
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. 创建天空效果
ts
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. 更新太阳位置与场景环境
ts
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. 动画与渲染循环
ts
const animate = () => {
if (!scene || !camera || !renderer || !water) {
return;
}
water.material.uniforms["time"].value += 1.0 / 60.0;
renderer.render(scene, camera);
animationFrameId = requestAnimationFrame(animate);
};
- 水面动画 :通过每帧更新水面着色器的
time
值,触发水面动画效果。 - 渲染循环 :使用
requestAnimationFrame
实现每一帧的渲染。
7. 处理窗口大小变化
ts
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. 资源清理
ts
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. 生命周期钩子
ts
onMounted(() => {
initThree();
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
cleanup();
});
- 生命周期钩子:在组件挂载时初始化 Three.js 场景,并在卸载时清理资源。
完整源码
完整源码如下:
ts
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 {
// 如果需要暴露更多方法或属性,可以在这里添加
};
}
使用示例:
html
<canvas id="bg-canvas"></canvas>
ts
useOcean('bg-canvas');
总结
以上我们就完成了一个动态的海洋和天空效果,它让我们的登陆页显得更加高大上档次,并且也展示了如何在 Vue 中集成复杂的 3D 渲染,同时确保了在窗口大小变化时的适配,以及在组件卸载时正确清理资源,通过合理的生命周期管理和资源清理,确保了程序的稳定性和性能。