【threejs】基本编程概念及海岛模型展示逻辑

采用three封装模式完成的海岛动画(点击这里查看)

直接上代码吧

<template>
  <div class="scene">
    <video id="videoContainer" style="position:absolute;top:0px;left:0px;z-index:100;visibility: hidden"></video>
    <div v-if="loadingProcess !== 100" class='loading'>
      <span class='progress'>{{loadingProcess}} %</span>
    </div>
    <div class="scene" id="viewer-container"></div>
    <div class="point point-0">
      <div class="label label-0">1</div>
      <div class="text">灯塔:矗立在海岸的岩石之上,白色的塔身以及红色的塔屋,在湛蓝色的天空和深蓝色大海的映衬下,显得如此醒目和美丽。</div>
    </div>
    <div class="point point-1">
      <div class="label label-1">2</div>
      <div class="text">小船:梦中又见那宁静的大海,我前进了,驶向远方,我知道我是船,只属于远方。这一天,我用奋斗作为白帆,要和明天一起飘扬,呼喊。</div>
    </div>
    <div class="point point-2">
      <div class="label label-2">3</div>
      <div class="text">沙滩:宇宙展开的一小角。不想说来这里是暗自疗伤,那过于矫情,只想对每一粒沙子,每一朵浪花问声你们好吗</div>
    </div>
    <div class="point point-3">
      <div class="label label-3">4</div>
      <div class="text">飞鸟:在苍茫的大海上,狂风卷集着乌云。在乌云和大海之间,海燕像黑色的闪电,在高傲地飞翔。</div>
    </div>
    <div class="point point-4">
      <div class="label label-4">5</div>
      <div class="text">礁石:寂寞又怎么样?礁石都不说话,但是水流过去之后,礁石留下。</div>
    </div>
    <div class="panel">
      <div class="main">
        <li class="tools-li" @click="resetScene">
          <p class="tools-name">场景重置</p>
        </li>
        <li class="tools-li" @click="inScene">
          <p class="tools-name">进入场景</p>
        </li>
      </div>
    </div>
  </div>

</template>

<script setup>
import { onBeforeUnmount, onMounted, nextTick, ref } from "vue"
import gsap from "gsap";
import modules from "./modules/index.js";
import Animations from './utils/animations';
import * as THREE from "three";
import { Water } from 'three/examples/jsm/objects/Water';
import { TWEEN } from 'three/examples/jsm/libs/tween.module.min.js'; // tween 动画效果渲染 效果同 gsap
import { Lensflare, LensflareElement } from 'three/examples/jsm/objects/Lensflare.js';
import vertexShader from './shaders/vertex.glsl?raw';
import fragmentShader from './shaders/fragment.glsl?raw';

let loadingProcess = ref(0) // loading加载数据 0 25 50 75 100
let sceneReady = false // 场景加载完毕标志,程序进行label展示,镜头拉进等效果

let viewer = null // 基础类,包含场景、相机、控制器等实例
let tiemen = null // 水面动画 函数
let allTiemen = null // 全局动画 函数

const sizes = { // 存储全局宽度 高度
  width: window.innerWidth,
  height: window.innerHeight
}

const lensflareTexture0 = 'images/lensflare0.png' // 太阳光贴图
const lensflareTexture1 = 'images/lensflare1.png' // 黑色描边贴图
const waterTexture = 'images/waternormals.jpg' // 水面基础图

const resetScene = () => { // 重置场景函数
  // Animations.animateCamera 利用tweenjs 完成的镜头切换动画工具函数,分别传入相机,控制器,相机最终位置,指向控制器位置,动作时间
  Animations.animateCamera(viewer.camera, viewer.controls, { x: 0, y: 600, z: 1600 }, { x: 0, y: 0, z: 0 }, 4000, () => {
    sceneReady = true
  });
}

const inScene = () => { // 进入场景函数
  // Animations.animateCamera 利用tweenjs 完成的镜头切换动画工具函数,分别传入相机,控制器,相机最终位置,指向控制器位置,动作时间
  Animations.animateCamera(viewer.camera, viewer.controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 4000, () => {
    sceneReady = true
  });
}

// 初始化three场景
const init = () => {
  viewer = new modules.Viewer('viewer-container') //初始化场景
  // 初始化模型上方的label存储空间
  // let labels = new modules.Labels(viewer)
  // 添加3种天空盒子的效果中的一种 白天 黑夜 黄昏
  viewer._initSkybox(0)
  // 调整相机位置(相机位置在初始化的时候设置过一次,这里对其进行调整)
  viewer.camera.position.set(0, 600, 1600)
  // 限制controls的上下角度范围 (OrbitControls的范围)
  viewer.controls.maxPolarAngle = Math.PI / 2.1;

  // 增加灯光(初始化viewer的时候,对灯光也做了初始,这里进行灯光调整)
  let { lights } = viewer

  // 环境光会均匀的照亮场景中的所有物体。 环境光不能用来投射阴影,因为它没有方向。
  let ambientLight = lights.addAmbientLight() 
  ambientLight.setOption({color: 0xffffff, intensity: 0.8}) // 调用灯光内置方法,设置新的属性
  
  // 平行光是沿着特定方向发射的光。这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果。 太阳足够远,因此我们可以认为太阳的位置是无限远,所以我们认为从太阳发出的光线也都是平行的。
  lights.addDirectionalLight([-1, 1.75, 1], { // 增加直射灯光方法 
    color: 'rgb(255,234,229)',
    // intensity: 3, // intensity属性是用来设置聚光灯的强度,默认值是1,如果设置成0那什么也看不到,该值越大,点光源看起来越亮
    // castShadow: true, // castShadow属性是用来控制光源是否产生阴影,取值为true或false
  })
  
  // 从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光。
  const pointLight = lights.addPointLight([0, 45, -2000], { // 增加直射灯光方法 
    color: 'rgb(253,153,253)'
  })

  // 模拟太阳光效果
  const textureLoader = new THREE.TextureLoader(); // 加载texture的一个类。 内部使用ImageLoader来加载文件。
  const textureFlare0 = textureLoader.load(lensflareTexture0); // 加载太阳光 贴图
  const textureFlare1 = textureLoader.load(lensflareTexture1); // 加载黑色贴图
  // 镜头光晕
  const lensflare = new Lensflare(); // 创建一个模拟追踪着灯光的镜头光晕。 Lensflare can only be used when setting the alpha context parameter of WebGLRenderer to true.
  lensflare.addElement(new LensflareElement( textureFlare0, 600, 0, pointLight.color));
  // LensflareElement( texture : Texture, size : Float, distance : Float, color : Color )
  // texture - 用于光晕的THREE.Texture(贴图)
  // size - (可选)光晕尺寸(单位为像素)
  // distance - (可选)和光源的距离值在0到1之间(值为0时在光源的位置)
  // color - (可选)光晕的(Color)颜色
  lensflare.addElement(new LensflareElement( textureFlare1, 60, .6));
  lensflare.addElement(new LensflareElement( textureFlare1, 70, .7));
  lensflare.addElement(new LensflareElement( textureFlare1, 120, .9));
  lensflare.addElement(new LensflareElement( textureFlare1, 70, 1));
  pointLight.add(lensflare);

  // 海
  const waterGeometry = new THREE.PlaneGeometry(10000, 10000); // 一个用于生成平面几何体的类。
  const water = new Water(waterGeometry, { // 官方模板
    textureWidth: 512,
    textureHeight: 512,
    waterNormals: new THREE.TextureLoader().load(waterTexture,  texture => {
      texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    }),
    sunDirection: new THREE.Vector3(),
    sunColor: 0xffffff,
    waterColor: 0x0072ff,
    distortionScale: 4,
    fog: viewer.scene.fog !== undefined
  });
  water.rotation.x = - Math.PI / 2;
  viewer.scene.add(water)

  tiemen = { // 水面移动动画绘制fun和content
    fun: (water) => {
      water.material.uniforms[ 'time' ].value += 1.0 / 60.0; // 参考threejs example 进行设置
    }, // 水面移动方法汇总
    content: water
  }
  viewer.addAnimate(tiemen) // 设置水面波动 动画执行

  // 彩虹(目前未展示)
  const material = new THREE.ShaderMaterial({
    side: THREE.DoubleSide,
    transparent: true,
    uniforms: {},
    vertexShader: vertexShader,
    fragmentShader: fragmentShader
  });
  const geometry = new THREE.TorusGeometry(200, 10, 50, 100);
  const torus = new THREE.Mesh(geometry, material);
  torus.opacity = .1;
  torus.position.set(0, -50, -400);
  viewer.scene.add(torus);

  // 官方模板给定的太阳渲染方式

  // 天空 需配合太阳进行渲染
  // const sky = new Sky();
  // sky.scale.setScalar( 450000 );
  // viewer.scene.add( sky );
  // const skyUniforms = sky.material.uniforms;
  // skyUniforms['turbidity'].value = 20;
  // skyUniforms['rayleigh'].value = 2;
  // skyUniforms['mieCoefficient'].value = 0.005;
  // skyUniforms['mieDirectionalG'].value = 0.8;

  // // 太阳
  // const sun = new THREE.Vector3();
  // const pmremGenerator = new THREE.PMREMGenerator(viewer.renderer);
  // const phi = THREE.MathUtils.degToRad(88);
  // const theta = THREE.MathUtils.degToRad(180);
  // sun.setFromSphericalCoords( 1, phi, theta );
  // sky.material.uniforms['sunPosition'].value.copy( sun );
  // water.material.uniforms['sunDirection'].value.copy(sun).normalize();
  // viewer.scene.environment = pmremGenerator.fromScene(sky).texture;

  // LoadingManager是three.js中的加载管理器,用于监控和管理加载资源的过程。
  // 通过使用LoadingManager,我们可以在应用程序中方便地加载各种类型的数据,例如模型、纹理、声音等
  const manager = new THREE.LoadingManager();
  // 模型加载时,处理过程的函数
  manager.onProgress = async(url, loaded, total) => {
    const rate = Math.floor(loaded / total * 100) // 计算当前模型加载比例 0 25 50 75 100
    loadingProcess.value = rate // 设置模型加载比例
    if (rate === 100) { // 如果模型加载完,则进行镜头拉进
      // Animations.animateCamera 利用tweenjs 完成的镜头切换动画工具函数,分别传入相机,控制器,相机最终位置,指向控制器位置,动作时间
      Animations.animateCamera(viewer.camera, viewer.controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 4000, () => {
        sceneReady = true
      });
    }
  };

  // 实例化ModelLoder,用于加载模型
  // 将模型加载时要用到 的回调函数 传入loader的创建过程中
  let modeloader = new modules.ModelLoder(viewer, manager) // 这样利用modeloader进行加载的模型都会计入模型加载时机中

  // 小岛加载 利用modeloader,就会触发manager.onProgress中的方法
  modeloader.loadModelToScene('models/island.glb', _model => {
    // modeloader.loadModelToScene('models/island.glb', _model => { 跟下面,原 GLTFLoader 产生的mesh和loadModelToScene产生的_model 其实是一回事
    // const loader = new GLTFLoader(manager);
    // loader.load(islandModel, mesh => {
    _model.openCastShadow() // 开启模型阴影 数组中移除阴影
    _model.object.traverse(child => {
      if (child.isMesh) {
        child.material.metalness = .4;
        child.material.roughness = .6;
      }
    })
    _model.object.position.set(0, -2, 0);
    _model.object.scale.set(33, 33, 33);
  })

  // 鸟加载
  modeloader.loadModelToScene('models/flamingo.glb', _model => {
    _model.openCastShadow() // 开启模型阴影 数组中移除阴影
    _model.startAnima(0, 1.2) // 开启模型自带的第1个动画,延时1.2秒执行一次 code/src/components/three/modules/DsModel/index.js
    const mesh = _model.object.children[0];
    mesh.scale.set(.35, .35, .35);
    mesh.position.set(-100, 80, -300);
    mesh.rotation.y = - 1;
    mesh.castShadow = true;
    _model.cloneModel([150, 80, -500]).startAnima(0, 1.8) // 开启模型自带的第1个动画,延时1.8秒执行一次 code/src/components/three/modules/DsModel/index.js

    // _model.startAnima(0, 1.2) 同下方的动画效果
    // const mixer = new THREE.AnimationMixer(mesh);
    // mixer.clipAction(gltf.animations[0]).setDuration(1.2).play(); // 开启模型自带的第1个动画,延时1.2秒执行一次
    // this.mixers.push(mixer);

    // _model.cloneModel([150, 80, -500]).startAnima() 同下方的动画效果
    // const mixer2 = new THREE.AnimationMixer(bird2);
    // mixer2.clipAction(gltf.animations[0]).setDuration(1.8).play(); // 开启模型自带的第1个动画,延时1.8秒执行一次
    // this.mixers.push(mixer2);
  })

  const raycaster = new THREE.Raycaster()
  // 小岛上各个景点的点位置,和dom元素
  const points = [
    {
      position: new THREE.Vector3(10, 46, 0),
      element: document.querySelector('.point-0')
    },
    {
      position: new THREE.Vector3(-10, 8, 24),
      element: document.querySelector('.point-1')
    },
    {
      position: new THREE.Vector3(30, 10, 70),
      element: document.querySelector('.point-2')
    },
    {
      position: new THREE.Vector3(-100, 50, -300),
      element: document.querySelector('.point-3')
    },
    {
      position: new THREE.Vector3(-120, 50, -100),
      element: document.querySelector('.point-4')
    }
  ];

  // 给每一个景点增加click事件,点击后移动到对应位置
  document.querySelectorAll('.point').forEach(item => {
    item.addEventListener('click', event => {
      let className = event.target.classList[event.target.classList.length - 1];
      switch(className) {
        case 'label-0':
          Animations.animateCamera(viewer.camera, viewer.controls, { x: -15, y: 80, z: 60 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
        case 'label-1':
          Animations.animateCamera(viewer.camera, viewer.controls, { x: -20, y: 10, z: 60 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
        case 'label-2':
          Animations.animateCamera(viewer.camera, viewer.controls, { x: 30, y: 10, z: 100 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
        default:
          Animations.animateCamera(viewer.camera, viewer.controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
      }
    }, false);
  });

  const { camera, scene } = viewer
  allTiemen = {
    fun: (water) => {
      TWEEN && TWEEN.update(); // 镜头拉进等效果 Animations.animateCamera
      // 镜头上下浮动效果
      const timer = Date.now() * 0.0005;
      camera && (camera.position.y += Math.sin(timer) * .05);
      if (sceneReady) {
        // 遍历每个点
        for (const point of points) {
          // 获取2D屏幕位置
          const screenPosition = point.position.clone();
          screenPosition.project(camera);
          raycaster.setFromCamera(screenPosition, camera);
          const intersects = raycaster.intersectObjects(scene.children, true);
          if (intersects.length === 0) {
            // 未找到相交点,显示
            point.element.classList.add('visible');
          } else {
            // 找到相交点
            // 获取相交点的距离和点的距离
            const intersectionDistance = intersects[0].distance;
            const pointDistance = point.position.distanceTo(camera.position);
            // 相交点距离比点距离近,隐藏;相交点距离比点距离远,显示
            intersectionDistance < pointDistance ? point.element.classList.remove('visible') :  point.element.classList.add('visible');
          }
          const translateX = screenPosition.x * sizes.width * 0.5;
          const translateY = - screenPosition.y * sizes.height * 0.5;
          point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
        }
      }
    },
    content: water
  }
  viewer.addAnimate(allTiemen)
}

onBeforeUnmount(()=>{
  window.removeEventListener('resize', () => {
    viewer._undateDom()
  })
})

onMounted(()=>{
  init()
  // 监听页面大小变动,自适应页面, 第一次直接触发执行
  window.addEventListener('resize', () => {
    viewer._undateDom()
  })
  // 初次页面变动执行不成功,主动延迟执行一次
  nextTick(()=>{
    viewer._undateDom()
  })
})  
</script>

<style lang="scss">
//定义全局颜色
$color: #123ca8;
.scene {
  height: 100vh;
  width: 100%;
  overflow: hidden;

  .loading {
    position: fixed;
    height: 100%;
    width: 100%;
    z-index: 99;
    background: rgba(46, 66, 77, .8);
    filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, .25));
    backdrop-filter: blur(10px);
    display: flex;
    justify-content: space-around;
    align-items: center;
    .progress {
      font-size: 3.6rem;
      color: #FFFFFF;
      text-shadow: 0 1px 0 hsl(174,5%,80%),
                  0 2px 0 hsl(174,5%,75%),
                  0 3px 0 hsl(174,5%,70%),
                  0 4px 0 hsl(174,5%,66%),
                  0 5px 0 hsl(174,5%,64%),
                  0 6px 0 hsl(174,5%,62%),
                  0 7px 0 hsl(174,5%,61%),
                  0 8px 0 hsl(174,5%,60%),
                  0 0 5px rgba(0,0,0,.05),
                0 1px 3px rgba(0,0,0,.2),
                0 3px 5px rgba(0,0,0,.2),
                0 5px 10px rgba(0,0,0,.2),
              0 10px 10px rgba(0,0,0,.2),
              0 20px 20px rgba(0,0,0,.3);
    }
  }

  .point {
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 10;
    .label {
      position: absolute;
      top: -16px;
      left: -16px;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: #00000077;
      border: 1px solid #ffffff77;
      color: #ffffff;
      font-family: Helvetica, Arial, sans-serif;
      text-align: center;
      line-height: 8px;
      font-weight: 100;
      font-size: 14px;
      cursor: help;
      transform: scale(0, 0);
      transition: transform 0.3s;
    }
    .text {
      position: absolute;
      top: 30px;
      left: -120px;
      width: 200px;
      padding: 20px;
      border-radius: 4px;
      background: rgba(0, 0, 0, .6);
      border: 1px solid #ffffff77;
      color: #ffffff;
      line-height: 1.3em;
      font-family: Helvetica, Arial, sans-serif;
      font-weight: 100;
      font-size: 14px;
      opacity: 0;
      transition: opacity 0.3s;
      pointer-events: none;
      text-align: justify;
      text-align-last: left;
    }
    &:hover .text{
      opacity: 1;
    }
    &.visible .label{
      transform: scale(1, 1);
    }
  }
  .label {
    padding: 20px;
    background: $color;
    color: aliceblue;
    border-radius: 5px;
    cursor: pointer;
  }

  .panel {
    margin: 0 auto;
    padding: 0;
    box-sizing: border-box;
    bottom: 10px;
    position: absolute;
    opacity: 0.8;
    width: 100%;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    .main {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      border-radius: 4px;
      opacity: 0.96;
      border: 1px solid #14171c;
      background: linear-gradient(0deg, #1e202a 0%, #0d1013 100%);
      box-shadow: 0px 2px 21px 0px rgba(33, 34, 39, 0.55);

      li {
        padding: 5px 10px;
        box-sizing: border-box;
        list-style: none;
        cursor: pointer;
        border: 1px solid #313642;
        border-radius: 2px;
        float: left;
        margin: 5px;
        position: relative;
        width: 70px;

        p {
          list-style: none;
          cursor: pointer;
          margin: 0;
          padding: 0;
          box-sizing: border-box;
          height: 20px;
          text-align: center;
          font-size: 12px;
          font-weight: 400;
          color: #fbfbfb;
          display: block;
        }
      }
    }
  }
}
</style>

这里只把主文件进行讲解,涉及到的插件和方法就不细讲了
源码放在这里了

一个theejs的场景无外乎场景scene、相机camera、光照light、渲染器render
当整个组件进入mounted的时候调用init函数,

  1. 对场景等配置进行初始化类的实例化时,会将场景、相机、光照、渲染器进行初始化,满足大多数three功能的需要。
viewer = new modules.Viewer('viewer-container') 
  1. 添加天空盒子、调整相机位置、控制器位置
// 添加3种天空盒子的效果中的一种 白天 黑夜 黄昏
 viewer._initSkybox(0)
 // 调整相机位置(相机位置在初始化的时候设置过一次,这里对其进行调整)
 viewer.camera.position.set(0, 600, 1600)
 // 限制controls的上下角度范围 (OrbitControls的范围)
 viewer.controls.maxPolarAngle = Math.PI / 2.1;
  1. 调整光照位置、增加光照和太阳光模拟效果
// 增加灯光(初始化viewer的时候,对灯光也做了初始,这里进行灯光调整)
 let { lights } = viewer

 // 环境光会均匀的照亮场景中的所有物体。 环境光不能用来投射阴影,因为它没有方向。
 let ambientLight = lights.addAmbientLight() 
 ambientLight.setOption({color: 0xffffff, intensity: 0.8}) // 调用灯光内置方法,设置新的属性
 
 // 平行光是沿着特定方向发射的光。这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果。 太阳足够远,因此我们可以认为太阳的位置是无限远,所以我们认为从太阳发出的光线也都是平行的。
 lights.addDirectionalLight([-1, 1.75, 1], { // 增加直射灯光方法 
   color: 'rgb(255,234,229)',
   // intensity: 3, // intensity属性是用来设置聚光灯的强度,默认值是1,如果设置成0那什么也看不到,该值越大,点光源看起来越亮
   // castShadow: true, // castShadow属性是用来控制光源是否产生阴影,取值为true或false
 })
 
 // 从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光。
 const pointLight = lights.addPointLight([0, 45, -2000], { // 增加直射灯光方法 
   color: 'rgb(253,153,253)'
 })

 // 模拟太阳光效果
 const textureLoader = new THREE.TextureLoader(); // 加载texture的一个类。 内部使用ImageLoader来加载文件。
 const textureFlare0 = textureLoader.load(lensflareTexture0); // 加载太阳光 贴图
 const textureFlare1 = textureLoader.load(lensflareTexture1); // 加载黑色贴图
 // 镜头光晕
 const lensflare = new Lensflare(); // 创建一个模拟追踪着灯光的镜头光晕。 Lensflare can only be used when setting the alpha context parameter of WebGLRenderer to true.
 lensflare.addElement(new LensflareElement( textureFlare0, 600, 0, pointLight.color));
 // LensflareElement( texture : Texture, size : Float, distance : Float, color : Color )
 // texture - 用于光晕的THREE.Texture(贴图)
 // size - (可选)光晕尺寸(单位为像素)
 // distance - (可选)和光源的距离值在0到1之间(值为0时在光源的位置)
 // color - (可选)光晕的(Color)颜色
 lensflare.addElement(new LensflareElement( textureFlare1, 60, .6));
 lensflare.addElement(new LensflareElement( textureFlare1, 70, .7));
 lensflare.addElement(new LensflareElement( textureFlare1, 120, .9));
 lensflare.addElement(new LensflareElement( textureFlare1, 70, 1));
 pointLight.add(lensflare);
  1. 添加大海效果,并增加大海波浪动画(参考官网demo)
 // 海
 const waterGeometry = new THREE.PlaneGeometry(10000, 10000); // 一个用于生成平面几何体的类。
 const water = new Water(waterGeometry, { // 官方模板
   textureWidth: 512,
   textureHeight: 512,
   waterNormals: new THREE.TextureLoader().load(waterTexture,  texture => {
     texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
   }),
   sunDirection: new THREE.Vector3(),
   sunColor: 0xffffff,
   waterColor: 0x0072ff,
   distortionScale: 4,
   fog: viewer.scene.fog !== undefined
 });
 water.rotation.x = - Math.PI / 2;
 viewer.scene.add(water)
 
 tiemen = { // 水面移动动画绘制fun和content
   fun: (water) => {
     water.material.uniforms[ 'time' ].value += 1.0 / 60.0; // 参考threejs example 进行设置
   }, // 水面移动方法汇总
   content: water
 }
 viewer.addAnimate(tiemen) // 设置水面波动 动画执行
  1. 模型加载loading过程函数构建(用于loading效果的展示)
 // LoadingManager是three.js中的加载管理器,用于监控和管理加载资源的过程。
// 通过使用LoadingManager,我们可以在应用程序中方便地加载各种类型的数据,例如模型、纹理、声音等
const manager = new THREE.LoadingManager();
// 模型加载时,处理过程的函数
manager.onProgress = async(url, loaded, total) => {
  const rate = Math.floor(loaded / total * 100) // 计算当前模型加载比例 0 25 50 75 100
  loadingProcess.value = rate // 设置模型加载比例
  if (rate === 100) { // 如果模型加载完,则进行镜头拉进
    // Animations.animateCamera 利用tweenjs 完成的镜头切换动画工具函数,分别传入相机,控制器,相机最终位置,指向控制器位置,动作时间
    Animations.animateCamera(viewer.camera, viewer.controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 4000, () => {
      sceneReady = true
    });
  }
};
  1. 实例化Modeloader,用于加载模型,会触发步骤5中的loading过程函数
// 实例化ModelLoder,用于加载模型
 // 将模型加载时要用到 的回调函数 传入loader的创建过程中
 let modeloader = new modules.ModelLoder(viewer, manager) // 这样利用modeloader进行加载的模型都会计入模型加载时机中

 // 小岛加载 利用modeloader,就会触发manager.onProgress中的方法
 modeloader.loadModelToScene('models/island.glb', _model => {
   // modeloader.loadModelToScene('models/island.glb', _model => { 跟下面,原 GLTFLoader 产生的mesh和loadModelToScene产生的_model 其实是一回事
   // const loader = new GLTFLoader(manager);
   // loader.load(islandModel, mesh => {
   _model.openCastShadow() // 开启模型阴影 数组中移除阴影
   _model.object.traverse(child => {
     if (child.isMesh) {
       child.material.metalness = .4;
       child.material.roughness = .6;
     }
   })
   _model.object.position.set(0, -2, 0);
   _model.object.scale.set(33, 33, 33);
 })

 // 鸟加载
 modeloader.loadModelToScene('models/flamingo.glb', _model => {
   _model.openCastShadow() // 开启模型阴影 数组中移除阴影
   _model.startAnima(0, 1.2) // 开启模型自带的第1个动画,延时1.2秒执行一次 code/src/components/three/modules/DsModel/index.js
   const mesh = _model.object.children[0];
   mesh.scale.set(.35, .35, .35);
   mesh.position.set(-100, 80, -300);
   mesh.rotation.y = - 1;
   mesh.castShadow = true;
   _model.cloneModel([150, 80, -500]).startAnima(0, 1.8) // 开启模型自带的第1个动画,延时1.8秒执行一次 code/src/components/three/modules/DsModel/index.js

   // _model.startAnima(0, 1.2) 同下方的动画效果
   // const mixer = new THREE.AnimationMixer(mesh);
   // mixer.clipAction(gltf.animations[0]).setDuration(1.2).play(); // 开启模型自带的第1个动画,延时1.2秒执行一次
   // this.mixers.push(mixer);

   // _model.cloneModel([150, 80, -500]).startAnima() 同下方的动画效果
   // const mixer2 = new THREE.AnimationMixer(bird2);
   // mixer2.clipAction(gltf.animations[0]).setDuration(1.8).play(); // 开启模型自带的第1个动画,延时1.8秒执行一次
   // this.mixers.push(mixer2);
 })
  1. 添加场景中各个景点的点位置和点击事件
const raycaster = new THREE.Raycaster()
  // 小岛上各个景点的点位置,和dom元素
  const points = [
    {
      position: new THREE.Vector3(10, 46, 0),
      element: document.querySelector('.point-0')
    },
    {
      position: new THREE.Vector3(-10, 8, 24),
      element: document.querySelector('.point-1')
    },
    {
      position: new THREE.Vector3(30, 10, 70),
      element: document.querySelector('.point-2')
    },
    {
      position: new THREE.Vector3(-100, 50, -300),
      element: document.querySelector('.point-3')
    },
    {
      position: new THREE.Vector3(-120, 50, -100),
      element: document.querySelector('.point-4')
    }
  ];

  // 给每一个景点增加click事件,点击后移动到对应位置
  document.querySelectorAll('.point').forEach(item => {
    item.addEventListener('click', event => {
      let className = event.target.classList[event.target.classList.length - 1];
      switch(className) {
        case 'label-0':
          Animations.animateCamera(viewer.camera, viewer.controls, { x: -15, y: 80, z: 60 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
        case 'label-1':
          Animations.animateCamera(viewer.camera, viewer.controls, { x: -20, y: 10, z: 60 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
        case 'label-2':
          Animations.animateCamera(viewer.camera, viewer.controls, { x: 30, y: 10, z: 100 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
        default:
          Animations.animateCamera(viewer.camera, viewer.controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
          break;
      }
    }, false);
  });
  1. 全局动画效果逻辑处理
const { camera, scene } = viewer
  allTiemen = {
    fun: (water) => {
      TWEEN && TWEEN.update(); // 镜头拉进等效果 Animations.animateCamera
      // 镜头上下浮动效果
      const timer = Date.now() * 0.0005;
      camera && (camera.position.y += Math.sin(timer) * .05);
      if (sceneReady) {
        // 遍历每个点
        for (const point of points) {
          // 获取2D屏幕位置
          const screenPosition = point.position.clone();
          screenPosition.project(camera);
          raycaster.setFromCamera(screenPosition, camera);
          const intersects = raycaster.intersectObjects(scene.children, true);
          if (intersects.length === 0) {
            // 未找到相交点,显示
            point.element.classList.add('visible');
          } else {
            // 找到相交点
            // 获取相交点的距离和点的距离
            const intersectionDistance = intersects[0].distance;
            const pointDistance = point.position.distanceTo(camera.position);
            // 相交点距离比点距离近,隐藏;相交点距离比点距离远,显示
            intersectionDistance < pointDistance ? point.element.classList.remove('visible') :  point.element.classList.add('visible');
          }
          const translateX = screenPosition.x * sizes.width * 0.5;
          const translateY = - screenPosition.y * sizes.height * 0.5;
          point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
        }
      }
    },
    content: water
  }
  viewer.addAnimate(allTiemen)

另外,太阳+天空的渲染方式还有一种就是官方demo给出的

  // 天空 需配合太阳进行渲染
  const sky = new Sky();
  sky.scale.setScalar( 450000 );
  viewer.scene.add( sky );
  const skyUniforms = sky.material.uniforms;
  skyUniforms['turbidity'].value = 20;
  skyUniforms['rayleigh'].value = 2;
  skyUniforms['mieCoefficient'].value = 0.005;
  skyUniforms['mieDirectionalG'].value = 0.8;

  // 太阳
  const sun = new THREE.Vector3();
  const pmremGenerator = new THREE.PMREMGenerator(viewer.renderer);
  const phi = THREE.MathUtils.degToRad(88);
  const theta = THREE.MathUtils.degToRad(180);
  sun.setFromSphericalCoords( 1, phi, theta );
  sky.material.uniforms['sunPosition'].value.copy( sun );
  water.material.uniforms['sunDirection'].value.copy(sun).normalize();
  viewer.scene.environment = pmremGenerator.fromScene(sky).texture;

源码放在这里了

更多详细代码请关注公众号:
【threejs】基本编程概念及海岛模型展示逻辑_第1张图片

你可能感兴趣的:(threejs,前端)