背景
常規的後台管理系統登陸頁面可能就只是一個簡單的背景頁面,這不太好看,接下來讓我們來使用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. 動畫與渲染循環
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. 處理窗口大小變化
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 渲染,同時確保了在窗口大小變化時的適配,以及在組件卸載時正確清理資源,通過合理的生命週期管理和資源清理,確保了程序的穩定性和性能。