前期准备
使用vue3+vite+three.js+gsap 开发
npm install three gsap
代码
<script setup>
// 导入three.js
import * as THREE from 'three';
// 导入轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// 加载模型
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// 解压缩库
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'
// 引入官方水效果
import { Water } from 'three/examples/jsm/objects/Water2.js';
// 导入补间动画库
import gsap from 'gsap';
import { ref, onMounted } from 'vue';
// 创建场景
const scene = new THREE.Scene();
// 初始化相机
const camera = new THREE.PerspectiveCamera(
45, // 视角
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近平面
1000 // 远平面
);
camera.position.set(-3.23,2.98,4.06);
camera.updateProjectionMatrix();
// 初始化渲染器
const renderer = new THREE.WebGLRenderer(
//设置抗锯齿
{ antialias: true }
);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 设置色调映射
renderer.outputEncoding = THREE.sRGBEncoding
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 0.5
renderer.shadowMap.enabled = true
// 调节亮度
renderer.toneMappingExposure = 1
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 初始化loader
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
// 初始化gltfLoader
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
// 加载环境纹理
let rgbeLoader = new RGBELoader()
rgbeLoader.load('./textures/sky.hdr',(texture)=>{
texture.mapping = THREE.EquiretangularReflectiionMapping;
scene.background = texture
scene.environment = texture
})
gltfLoader.load("./model/scene.glb",(gltf)=>{
const model = gltf.scene;
model.traverse((child)=>{
if(child.name = "Plane"){
child.visible = false
}
if(child.isMesh){
child.castShadow = true
child.receiveShadow = true
}
})
scene.add(model)
})
// 添加水效果
const waterGeometry = new THREE.CircleGemoetry(300,32)
const water = new Water(
waterGeometry,
{
// 数值越大效果越明显
textureWidth: 2048,
textureHeight: 2048,
color:0xeeeeff,
flowDirection: new THREE.Vector2(1,1),
scale:2,
}
)
// 设置水的旋转,否则导致房子浸泡在水中
water.rotation.x = -Math.PI / 2
// 设置水的位置
water.position.y = -1
scene.add(water)
// 加载模型
gltfLoader.load(
'models/scene.gltf',
(gltf) => {
scene.add(gltf.scene);
// 设置模型位置
gltf.scene.position.set(0,0,0);
// 设置模型大小
gltf.scene.scale.set(0.01,0.01,0.01);
// 设置模型旋转
gltf.scene.rotation.set(0,0,0);
},
(xhr) => {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
(error) => {
console.log('error!', error);
}
);
// 添加平行光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 50, 0);
scene.add(light);
// 添加点光源
const pointLight = new THREE.PointLight(0xffffff, 100);
pointLight.position.set(0.1,2.4, 0);
pointLight.castShadow = true;
scene.add(pointLight);
// 创建点光源组
const pointLightGroup = new THREE.Group();
pointLightGroup.position.set(-8,2.5,-1.5);
// 球绕圆的半径
let radius = 3;
let pointLightArr = [];
for(let i = 0; i < 3; i++){
// 创建点光源
const sphereGeometry = new THREE.SphereGeometry(0.1, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 10,
roughness: 0.5,
metalness: 0.5
})
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(radius * Math.cos(i * 120 * Math.PI / 180), Math.cos((i * 120 * Math.PI) / 180), radius * Math.sin(i * 120 * Math.PI / 180));
// 添加到点光源组
pointLightGroup.add(sphere);
const pointLight = new THREE.PointLight(0xffffff, 1);
sphere.add(pointLight);
// 将点光源添加到点光源组
pointLightGroup.add(sphere);
}
scene.add(pointLightGroup);
function render(){
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
}
// 使用补间动画,从0到2pi,使灯泡旋转
let options = {
angle:0
}
gsap.to(options,{
angle:Math.PI * 2,
duration:10,
repeat:-1,
ease:"linear",
onUpdate:()=>{
pointLightGroup.rotation.y = options.angle
pointLightArr.forEach((item,index)=>{
item.position.set(radius * Math.cos((index * 120) * Math.PI / 180), Math.cos(((index * 120) * Math.PI) / 180), radius * Math.sin((index * 120) * Math.PI / 180));
})
}
})
render();
// 使用补间动画,移动相机
let timeLine1 = gsap.timeline();
let timeline2 = gsap.timeline();
// 定义相机移动函数
function translateCamera(position,target){
timeLine1.to(camera.position,{
x:position.x,
y:position.y,
z:position.z,
duration:2,
ease:"power2.inOut"
})
timeline2.to(camera.position,{
x:target.x,
y:target.y,
z:target.z,
duration:2,
ease:"power2.inOut"
})
}
let words = [
{
text:"Attendre et espérer !",
callback:()=>{
new THREE.Vector3(-3.23,2.98,4.06),
new THREE.Vector3(-8, 2, 0)
},
},
{
text:"Attendre et espérer !!",
callback:()=>{
new THREE.Vector3(7,0,23),
new THREE.Vector3(0,0,0)
},
},
{
text:"Attendre et espérer !!!",
callback:()=>{
new THREE.Vector3(10,3,0),
new THREE.Vector3(5,2,0)
},
},
{
text:"Attendre et espérer !!!!",
callback:()=>{
new THREE.Vector3(7,0,23),
new THREE.Vector3(0,0,0)
},
},
{
text:"Attendre et espérer !!!!!",
callback:()=>{
new THREE.Vector3(-20,1.3,6.6),
new THREE.Vector3(5,2,0)
// 调用画爱心函数
makeHeart()
},
},
]
let index = ref(0)
let isAnimate = false
// 监听鼠标滚轮事件
window.addEventListener('wheel',(e)=>{
// 操作节流
if(isAnimate){
return
}
if(e.deltaY > 0){
index.value++
if(index.value > words.length - 1){
index.value = 0
}
}
words[index.value].callback()
setTimeout(() => {
isAnimate = false
}, 1000);
})
// 实例化星星
let starsInstance = new THREE.InstanceMesh(
new THREE.SphereGeometry(0.1,32,32),
new THREE.MeshStandardMaterial({
color:0xffffff,
emissive:0xffffff,
emissiveIntensity:10,
roughness:0.5,
metalness:0.5
}),
100
);
// 随机分布星星
let starsArr = []
let endArr = []
for(let i = 0; i < 100; i++){
let x = Math.random() * 100 - 50
let y = Math.random() * 100 - 50
let z = Math.random() * 100 - 50
starsArr.push(new THREE.Vector3(x,y,z))
// 矩阵修改位移
let matrix = new THREE.Matrix4()
matrix.setPosition(x,y,z)
starsInstance.setMatrixAt(i,matrix)
}
scene.add(starsInstance)
// 使用贝塞尔曲线,实现星星移动
let heartShape = new THREE.Shape()
heartShape.moveTo(25,25)
heartShape.bezierCurveTo(25,25,20,0,0,0)
heartShape.bezierCurveTo(-30,0,-30,35,-30,35)
heartShape.bezierCurveTo(-30,55,-10,77,25,95)
heartShape.bezierCurveTo(60,77,80,55,80,35)
heartShape.bezierCurveTo(80,35,80,0,50,0)
heartShape.bezierCurveTo(35,0,25,25,25,25)
//根据路径获取点
for(let i = 0; i < 100; i++){
let point = heartShape.getPointAt(i / 100)
endArr.push(new THREE.Vector3(point.x,point.y,point.z))
}
// 创建动画
function makeHeart(){
let params = {
time:0
}
gsap.to(params,{
time:1,
duration:10,
ease:"linear",
onUpdate:()=>{
for(let i = 0; i < 100; i++){
let x = THREE.MathUtils.lerp(starsArr[i].x,endArr[i].x,params.time)
let y = THREE.MathUtils.lerp(starsArr[i].y,endArr[i].y,params.time)
let z = THREE.MathUtils.lerp(starsArr[i].z,endArr[i].z,params.time)
let matrix = new THREE.Matrix4()
matrix.setPosition(x,y,z)
starsInstance.setMatrixAt(i,matrix)
}
starsInstance.instanceMatrix.needsUpdate = true
}
})
}
</script>
<template>
<div class="words" style="position:fixed;left: 0;top:0;z-index:10;pointer-events: none;transition: all 1s;" :style="{
transform:`translate(${index.value * -100}vw,0)`
}">
<div v-for="item in words" style="width: 100vw;height: 100vh;">
<h1 style="padding: 100px 50px;font-size:50px;color:#fff">{{item.text}}</h1>
</div>
</div>
</template>
<style scoped>
*{
margin:0;
padding:0;
}
canvas{
display: block;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
</style>