【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和contentfun: (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 100loadingProcess.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.jsconst 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 } = viewerallTiemen = {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函数,
- 对场景等配置进行初始化类的实例化时,会将场景、相机、光照、渲染器进行初始化,满足大多数three功能的需要。
viewer = new modules.Viewer('viewer-container')
- 添加天空盒子、调整相机位置、控制器位置
// 添加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);
- 添加大海效果,并增加大海波浪动画(参考官网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和contentfun: (water) => {water.material.uniforms[ 'time' ].value += 1.0 / 60.0; // 参考threejs example 进行设置}, // 水面移动方法汇总content: water}viewer.addAnimate(tiemen) // 设置水面波动 动画执行
- 模型加载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 100loadingProcess.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});}
};
- 实例化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.jsconst 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 } = viewerallTiemen = {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】基本编程概念及海岛模型展示逻辑
采用three封装模式完成的海岛动画(点击这里查看) 直接上代码吧 <template><div class"scene"><video id"videoContainer" style"position:absolute;top:0px;left:0px;z-index:100;visibility: hidden"&g…...
python小技巧:创建单链表及删除元素
目前只有单链表(无法查找上一个元素),后面再更新循环链表和双链表。 class SingleLinkedList:def createList(self, raw_list):if len(raw_list) 0:head ListNode()else:head ListNode(raw_list[0])cur headfor i in range(1, len(raw_l…...

ADuM1250 ADuM1251 模块 I2C IIC总线2500V电磁隔离 接口保护
功能说明: 1,2500V电磁隔离,2通道双向I2C; 2,支持电压在3到5.5V,最大时钟频率可达1000KHz; 3,将该隔离模块接入总线,可以保护主MCU引脚,降低I2C总线上的干…...

C# 把多个dll合成一个dll
Nuget 下载ILMerge两个工程 dog为测试工程 TestIlmerge为准备合并的类库 如下图所示, 由于我们引用下面4个库 正常生成后,会有TestIlmerge.dll和下面的这4个dll 只生成TestIlmerge.dll 打开工程文件 在最下方加入以下两段 <Target Name"ILMerge…...
scipy.sparse.coo_matrix.sum()关于axis的用法
以下面的矩阵为例 [1,2,0] [0,3,0] [0,0,0]示例代码 from scipy.sparse import coo_matrix# 创建一个稀疏矩阵 data [1, 2, 3] row [0, 0, 1] col [0, 1, 1] sparse_matrix coo_matrix((data, (row, col)), shape(3,3))# 计算稀疏矩阵中每行非零元素的总和 sum_of_column…...
C++类与对象(下)
文章目录 1.非类型模板2.模板特化2.1.类模板特化2.1.1.全特化2.1.2.偏特化 2.2.函数模板特化 3.函数模板声明定义分离 之前我们学习的模板能达到泛型的原因是:使用了“泛型的类型”,但是如果经过后面的“造轮子”(后面会尝试实现一下 STL的一…...
SpringBoot——》引入Redis
推荐链接: 总结——》【Java】 总结——》【Mysql】 总结——》【Redis】 总结——》【Kafka】 总结——》【Spring】 总结——》【SpringBoot】 总结——》【MyBatis、MyBatis-Plus】 总结——》【Linux】 总结——》【MongoD…...
C# newtonsoft序列化将long类型转化为字符串
/// <summary> /// 转化为json的时候long类型转为string /// </summary> public class LongJsonConverter: JsonConverter {public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer){try{return r…...

黑马点评-02使用Redis代替session,Redis + token机制实现
Redis代替session session共享问题 每个Tomcat中都有一份属于自己的session,所以多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时可能会导致数据丢失 用户第一次访问1号tomcat并把自己的信息存放session域中, 如果第二次访问到了2号tomcat就无法获取到在1号…...

arm 点灯实验代码以及现象
.text .global _start _start: 1.设置GPIOE寄存器的时钟使能 RCC_MP_AHB4ENSETR[4]->1 0x50000a28 LDR R0,0x50000A28 LDR R1,[R0] ORR R1,R1,#(0x1<<4) 第4位置1 STR R1,[R0] 1.设置GPIOF寄存器的时钟使能 RCC_MP_AHB4ENSETR[4]->1 0x50000a28 LDR R…...

选择适合普通公司的项目管理软件
不管是打工人还是学生党都适合使用Zoho Projects项目管理软件。利用项目概览功能,将整体项目尽收眼底,作为项目管理者,项目日程、进度都可见,Zoho Projects项目管理APP助推项目每一环节的进展,更便于管理者设计项目的下…...
E (1081) : DS堆栈--逆序输出(STL栈使用)
Description C中已经自带堆栈对象stack,无需编写堆栈操作的具体实现代码。 本题目主要帮助大家熟悉stack对象的使用,然后实现字符串的逆序输出 输入一个字符串,按字符按输入顺序压入堆栈,然后根据堆栈后进先出的特点࿰…...
访问者模式 行为型设计模式之九
1.定义 在不改变数据结构的前提下,增加作用于一组对象元素的新功能。 2.动机 访问者模式适用于数据结构相对稳定的系统它把数据结构和作用于数据结构之上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式的目的是要把处理从数据结构…...
JVM垃圾回收之JVM GC算法探究
JVM垃圾回收之JVM GC算法探究 在Java虚拟机(JVM)中,垃圾回收(Garbage Collection,GC)是自动管理内存的重要机制,它负责回收程序中不再使用的对象所占用的内存。GC算法是垃圾回收的核心…...

Django 前端模板显示换行符、日期格式
linebreaksbr 显示换行符 <td>{{ data.sku_list|default:"无"|linebreaksbr }}</td> date:"Y年m月d日 H:i" 设置日期格式 <td>{{ data.submit_time|date:"Y年m月d日 H:i" }}</td> 其他语法 forloop 获取循环的索引 …...

Aurora中的策略模式和模板模式
Aurora中的策略模式和模板模式 在aurora中为了方便以后的扩展使用了策略模式和模板模式实现图片上传和搜索功能,能够在配置类中设置使用Oss或者minio上传图片,es或者mysql文章搜索。后续有新的上传方式或者搜索方式只需要编写对应的实现类即可ÿ…...

Ubuntu 22.04 安装系统 手动分区 针对只有一块硬盘 lvm 单独分出/home
自动安装的信息 参考自动安装时产生的分区信息 rootyeqiang-MS-7B23:~# fdisk /dev/sdb -l Disk /dev/sdb:894.25 GiB,960197124096 字节,1875385008 个扇区 Disk model: INTEL SSDSC2KB96 单元:扇区 / 1 * 512 512 字节 扇区大…...

Android系统定制之监听USB键盘来判断是否弹出软键盘
一.项目背景 在设备上弹出软键盘,会将一大部分UI遮挡起来,造成很多图标无法看到和点击,使用起来不方便,因此通过插入usb键盘输入代替软键盘,但是点击输入框默认会弹出软键盘,因此想要插入USB键盘时,默认关闭软键盘,拔出键盘时再弹出,方便用户使用 二.设计思路 2.1…...

LeakyReLU激活函数
nn.LeakyReLU 是PyTorch中的Leaky Rectified Linear Unit(ReLU)激活函数的实现。Leaky ReLU是一种修正线性单元,它在非负数部分保持线性,而在负数部分引入一个小的斜率(通常是一个小的正数),以防…...

Qt单一应用实例判断
原本项目中使用QSharedMemory的方法来判断当前是否已存在运行的实例,但在MacOS上,当程序异常崩溃后,QSharedMemory没有被正常销毁,导致应用程序无法再次被打开。 对此,Qt assistant中有相关说明: 摘抄 qt-s…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...

Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...

【Linux系统】Linux环境变量:系统配置的隐形指挥官
。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量:setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...

5. TypeScript 类型缩小
在 TypeScript 中,类型缩小(Narrowing)是指根据特定条件将变量的类型细化为更具体的过程。它帮助开发者编写更精确、更准确的代码,确保变量在运行时只以符合其类型的方式进行处理。 一、instanceof 缩小类型 TypeScript 中的 in…...

ubuntu2404 gpu 没接显示器,如何保证远程显示的分辨率
1. 使用 xserver-xorg-video-dummy 创建虚拟显示器 如果系统在无物理显示器连接时无法识别显示输出,可以使用 xserver-xorg-video-dummy 驱动程序创建虚拟显示器。以下是设置步骤: 安装虚拟显示器驱动程序: sudo apt install xserver-xorg-v…...