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

news2024/11/25 0:33:37

采用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;

源码放在这里了

更多详细代码请关注公众号:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1071244.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

37.普利姆(Prim)算法

从一个问题开始 “要想富&#xff0c;先修路”&#xff0c;郝乡长最近为了德胜乡修路的事情愁白了头。 得胜乡有A、B、C、D、E、F、G七个村子&#xff0c;现在需要修路把7个村庄连通&#xff0c;但是又想要耗费的公路建材最少&#xff08;修建公路的总里程最短&#xff09;&…

强烈推荐这5款功能强大的小软件

​ 今日的栽种&#xff0c;明日的果实&#xff0c;今天继续分享五个功能强大的小软件。 1.文本编辑——IDM UltraEdit ​ IDM UltraEdit是一款功能强大的文本编辑器&#xff0c;它支持多种编程语言和文件格式&#xff0c;可以处理大型文件&#xff0c;进行代码折叠&#xff0…

IDEA插件版本升级和兼容新版本idea

1.关于IDEA插件的版本设置问题 打开jetbrains插件市场&#xff0c;随意打开一个插件详情页面的Versions菜单&#xff0c;我们可以看见一个插件包不同时期发布的不同版本&#xff08;Versions&#xff09;&#xff0c;并且每个版本包含了可兼容IDEA或PyCharm的版本范围&#xf…

文件智能管理将文件统一保存在某个指定文件夹中

日常工作中经常会整理文件到指定的文件夹&#xff0c;少的时候用鼠标拖拖&#xff0c;多了就很麻烦了&#xff0c;手动操作很容易出现漏洞&#xff0c;会漏个某文件没有移动进去或出现重复移动同一个文件等&#xff0c;移动文件这种工作很枯燥可以交给文件批量改名高手软件&…

Excel宏管理库存清单

1. 开启宏: - 打开 Excel - 选择 “文件” > “选项” > “自定义功能区” > “开发工具” &#xff0c;将其添加到功能区。 - 返回Excel界面&#xff0c;点击 “开发工具” 选项卡。 2.准备你的库存清单&#xff1a; - 在一个新的工作表中创建你的库存清单。…

【QT入门1】

目录 1.创建工程时基类的选择 2.第一个QT程序 3.创建一个按钮 4.对象树简单理解 5.信号和槽 5.1自定义信号槽 5.2信号连接信号 5.3信号函数和槽函数的注意事项 5.4配合lambda表达式 1.创建工程时基类的选择 在创建工程时会被要求选择一个基类&#xff1a; 这里有三个…

【Java】语法特性篇

语法特性篇 Java对象的比较 1. 对象比较的问题 Java中引用类型的变量不能直接按照 > 或者 < 方式进行比较。那为什么可以比较&#xff1f; 因为&#xff1a;对于用户实现自定义类型&#xff0c;都默认继承自Object类&#xff0c;而Object类中提供了equal方法&#xf…

Kafka实战案例

kafka系统的生成&#xff0c;自顶向下 1. kafaka发送消息 1.1 是最初始外部调用kafaka的地方1.6 是最初调用kafaka的函数。中间是对kafaka的构建 1.1 向Kafka发送一条发布视频的message 在videoHandler的发布视频逻辑中&#xff0c;向Kafka发送一条发布视频的mq&#xff0c…

Ubuntu 22.04 安装系统 手动分区 针对只有一块硬盘 lvm 单独分出/home

自动安装的信息 参考自动安装时产生的分区信息 rootyeqiang-MS-7B23:~# fdisk /dev/sdb -l Disk /dev/sdb&#xff1a;894.25 GiB&#xff0c;960197124096 字节&#xff0c;1875385008 个扇区 Disk model: INTEL SSDSC2KB96 单元&#xff1a;扇区 / 1 * 512 512 字节 扇区大…

基于Springboot实现论坛管理系统项目演示【项目源码+论文说明】分享

基于Springboot实现论坛管理系统演示 摘要 在社会快速发展的影响下&#xff0c;论坛管理系统继续发展&#xff0c;使论坛管理系统的管理和运营比过去十年更加信息化。依照这一现实为基础&#xff0c;设计一个快捷而又方便的网上论坛管理系统是一项十分重要并且有价值的事情。对…

排序(order by)

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: select */列名 from 表名 order by 列名1 asc/desc, 列名2 asc/desc; 说明&#xff1a; 排序的目的&#xff1a;改变查询结果的返回顺序…

学习笔记(css穿透、vue-cookie、拦截器、vuex、导航守卫、token/Cookie、正则校验)

目录 一、记录 1、CSS穿透 2、输入框是否提示输入 3、插槽 #slot 4、v-deep深入改掉属性值 二、vue-cookie 1、官方文档 2、使用 三、拦截器 1、请求拦截器 2、响应拦截器 四、vuex对信息存取改 五、路由导航守卫 1、登录思路 2、设置白名单 六、Token与Cookie…

vue3 集成 tailwindcss

tailwindcss 介绍 Tailwind CSS 是一个流行的前端框架&#xff0c;用于构建现代、响应式的网页和 Web 应用程序。它的设计理念是提供一组可复用的简单、低级别的 CSS 类&#xff0c;这些类可以直接应用到 HTML 元素上&#xff0c;从而加速开发过程并提高样式一致性。 主要特点…

【数据结构与算法】二叉树的实现以及二叉排序数的实现

目录 通过数组实现二叉树 通过链表实现二叉树 排序二叉树的实现 通过数组实现二叉树 该实现方式只能用于完全二叉树&#xff0c;因为如果是普通二叉数的话&#xff0c;数组中会出现空隙&#xff0c;会导致空间的利用率会降低。 实现思路&#xff1a; 因为假设一个父节点的…

原码反码补码移码的介绍和计算

1.原码 原码的定义&#xff1a;十进制数据的二进制表示形式就是原码。 &#xff08;1&#xff09;原码的最左边那位是符号位&#xff0c;其他位为数据位&#xff0c;符号位是0则为正数&#xff0c;符号位是1则为负数。 &#xff08;2&#xff09;一个byte有8bit&#xff0c;最…

Node-RED系列教程-25node-red获取天气

安装节点:node-red-contrib-weather 节点图标如下: 使用说明:node-red-contrib-weather (node) - Node-RED 流程图中填写经度和纬度即可。 演示: json内容: {

jmeter 请求发送加密参数

最近在做http加密接口&#xff0c;请求头的uid参数及body的请求json参数都经过加密再发送请求&#xff0c;加密方式为&#xff1a;ase256。所以&#xff0c;jmeter发送请求前也需要对uid及json参数进行加密。我这里是让开发写了个加密、解密的jar&#xff0c;jmeter直接调用这个…

CRM系统如何自动分配线索

分配线索是销售部门很重要的一项工作&#xff0c;大量的线索中潜藏着许多企业未来的忠实客户。如果将大把的线索通过手工的方式分配给多个销售人员是一件棘手的事&#xff0c;就要借助CRM系统自动分配线索。 你的企业是否也面临这些难题&#xff1a; 1.渠道多线索多&#xff…

点击、拖拉拽开发可视化大屏,网友直呼不可思议

可视化大屏既足够炫酷&#xff0c;又能快速整合多业务系统数据&#xff0c;可视化分析数据&#xff0c;是一种可运用于博览中心、会议中心、监控中心、企业大屏看板等场景的常用数据可视化分析形式。但可视化大屏虽然好用&#xff0c;在开发制作上却难倒了不少人&#xff0c;直…

汇编实现点灯实验

.text .global _start _start: 设置GPIOF寄存器的时钟使能LDR R0,0X50000A28LDR R1,[R0]ORR R1,R1,#(0x1<<5)STR R1,[R0]设置GPIOE寄存器的时钟使能LDR R0,0X50000A28LDR R1,[R0] 从r0为起始地址的4字节数据取出放在R1ORR R1,R1,#(0x1<<4) 第4位设置为1STR R1,[…