IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    手写一个动态海洋和天空效果的vue hooks

    夕水发表于 2025-05-08 16:12:12
    love 0

    背景

    常规的后台管理系统登陆页面可能就只是一个简单的背景页面,这不太好看,接下来让我们来使用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 渲染,同时确保了在窗口大小变化时的适配,以及在组件卸载时正确清理资源,通过合理的生命周期管理和资源清理,确保了程序的稳定性和性能。



沪ICP备19023445号-2号
友情链接