采用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函数,
- 对场景等配置进行初始化类的实例化时,会将场景、相机、光照、渲染器进行初始化,满足大多数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和content
fun: (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 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
});
}
};
- 实例化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);
})
- 添加场景中各个景点的点位置和点击事件
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)
另外,太阳+天空的渲染方式还有一种就是官方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;
源码放在这里了
更多详细代码请关注公众号: