继续跟着@老陈打码学习!!!支持!!!
效果图
链接:https://pan.baidu.com/s/1Ft8U2HTeqmpyAeesL31iUg
提取码:6666
使用到的 模型文件和资源等都为@老陈打码提供!!!!!!!!!!!
如何做出这样的一个效果呢?
1.构建项目文件
这里使用了vue3+vite的写法。代码是脚手架直接生成的,安装一个gsap和three就行了
{"name": "threejs-vue","version": "0.0.0","private": true,"scripts": {"dev": "vite","build": "vite build","preview": "vite preview"},"dependencies": {"gsap": "^3.12.2","three": "^0.158.0","vue": "^3.3.4"},"devDependencies": {"@vitejs/plugin-vue": "^4.4.0","vite": "^4.4.11"}
}
2.引入相关依赖
引入我们threejs项目相关的依赖,控制器啊,gltf加载器啊,还有解压工具,补间动画库等。
import { ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { Water } from "three/examples/jsm/objects/Water2";
import gsap from "gsap/gsap-core";
3.初始化场景
threejs的第一步,初始化一个场景,创建相机,引入控制器等。其中相机的位置等参数都是实现测好的,所以直接就写好了。
//初始化场景
const scene = new THREE.Scene();
//初始化相机
const camera = new THREE.PerspectiveCamera(75,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);//初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(-8, 2, 0);
//
controls.enableDamping = true;// 渲染函数
function render() {requestAnimationFrame(render);renderer.render(scene, camera);controls.update();
}render();
4.添加模型
1.初始化解压模型
我们要用的模型文件是一个压缩的文件,所以使用的时候,需要全局引用解压模型。
//初始化解压模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/public/draco/");
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
注意!!!! 这里面的("/public/draco/") draco后面必须要有这个/ 没有的话会报错。
2.加载模型
将我们的模型加载,但是这时候我们运行发现,仍然是一片漆黑,所以我们需要加入我们的光源。
//加载模型
gltfLoader.load("/public/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);
});
5.添加光源
//初始化光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 50, 0);
scene.add(light);
添加光源以后,一个基本的效果就出现了。
我们的效果图里面,房间里是有灯光的,所以我们在房间里放入一个点光源,位置已经提前测好了,并且,房子的门口部分应该是要有阴影的,所以我们加入阴影。
//允许阴影
renderer.shadowMap.enabled = true;
// 设置物理的光照效果
renderer.physicallyCorrectLights = true;// 添加点光源
const pointLight = new THREE.PointLight(0xffffff, 100);
pointLight.position.set(0.1, 2.4, 0);
pointLight.castShadow = true;
scene.add(pointLight);
6.添加天空水面
1.创建水面
水面我们直接使用的threejs提供的水面效果,然后给他的position专门设置了一下,出现了这样的效果。
//创建水面
const waterGeometry = new THREE.CircleGeometry(300, 32);
const water = new Water(waterGeometry, {textureWidth: 1024,textureHeight: 1024,color: 0xeeeeff,flowDirection: new THREE.Vector2(1, 1),scale: 100,
});
water.rotation.x = -Math.PI / 2;
water.position.y = -0.4;
scene.add(water);
2.添加天空纹理
//加载环境纹理(天空、水面)
let rgbeLoader = new RGBELoader();
rgbeLoader.load("/public/textures/sky.hdr", (textures) => {//由于天空是一个全景图,所以给他加一个球体的映射 让他包裹住整个场景textures.mapping = THREE.EquirectangularReflectionMapping;scene.background = textures;scene.environment = textures;
});
天空的感觉已经出现了,但是会发现这个天空给我们的感觉有点不一样,我们需要调节一下色调。
3.调节色调映射
// 设置色调映射
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// 设置色调映射输出
renderer.outputEncoding = THREE.sRGBEncoding;
// 设置色调映射曝光
renderer.toneMappingExposure = 0.75;
现在感觉就还好一些
7.小树灯光
在我们的效果图中,三棵小树旁边是有灯光在跳动的,所以我们开始做小树灯的效果。
1.创建点光源组,循环做创建
我们使用for循环 循环构建小球,并且各自设置他们的位置,实现小球的效果。
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.2, 32, 32);const sphereMaterial = new THREE.MeshStandardMaterial({color: 0xffffff,emissive: 0xffffff,emissiveIntensity: 1,});const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);pointLightArr.push(sphere);sphere.position.set(radius * Math.cos((i * 2 * Math.PI) / 3),Math.cos((i * 2 * Math.PI) / 3),radius * Math.sin((i * 2 * Math.PI) / 3));let pointLight = new THREE.PointLight(0xffffff, 1);sphere.add(pointLight);pointLightGroup.add(sphere);
}scene.add(pointLightGroup);
2.点光源组旋转效果实现
效果图中的点光源组是有旋转的效果的,我们可以使用补间函数,结合数学公式进行点光源小球旋转的实现。
// 使用补间函数 从0到2π 旋转
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 * 2 * Math.PI) / 3),Math.cos((index * 2 * Math.PI) / 3 + options.angle * 5),radius * Math.sin((index * 2 * Math.PI) / 3));});},
});
8.滑轮滚动切换场景
在效果图中,我们滚动滑轮的时候,场景就会随之切换,相机的视角不是固定的,如何做到呢?
1.定义补间动画移动相机函数
我们需要定义一个函数,用来移动相机的位置,方便我们真听到滚轮事件的时候触发。我们使用补间动画来完成。
// 使用补间动画移动相机
let timeLine1 = new gsap.timeline();
let timeLine2 = new gsap.timeline();// 定义相机移动函数
function translateCamera(position, target) {// 通过补间函数移动相机timeLine1.to(camera.position, {x: position.x,y: position.y,z: position.z,duration: 1,ease: "power2.inout",});// 聚焦到哪一点timeLine2.to(controls.target, {x: target.x,y: target.y,z: target.z,duration: 1,ease: "power2.inout",});
}
2.制作场景文字和相机位置
我们使用一个scenes数组来制作我们的几个不同的视角场景和文字。
// 鼠标滑轮切换效果
let scenes = [{text: "圣诞快乐",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(-3.23, 3, 4.06),new THREE.Vector3(-8, 2, 0));},},{text: "感谢在这么大的世界里遇到了你",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));},},{text: "愿与你探寻世界的每一个角落",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(10, 3, 0), new THREE.Vector3(5, 2, 0));},},{text: "愿将天上的星星送给你",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));//makeHeart();},},{text: "加油~",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(-20, 1.3, 6.6),new THREE.Vector3(5, 2, 0));},},
];
3.鼠标滚轮事件
我们需要一个index 来做场景的记录,方便我们知道我们移动到了哪个场景,为了后面的判断。并且我们还需要一个防抖函数,来控制切换场景。不能一直切换。做一个短暂的开关,来保证我们的视觉感受。
// 用来做场景的记录
const index = ref(0);// 防抖函数
let isAnimte = false;// 侦听鼠标滚轮事件
window.addEventListener("wheel",(e) => {if (isAnimte) return;isAnimte = true;// 判断滚轮的方向if (e.deltaY > 0) {index.value++;if (index.value > scenes.length - 1) {index.value = 0;// restoreHeart();}}scenes[index.value].callback();setTimeout(() => {isAnimte = false;}, 1000);},false
);
9.祝福文字显示
使用css样式进行文字的显示,用定位的方式将他固定在左边,使用transform进行高度的移动,来看到想看的内容。
<template><divclass="scenes"style="position: fixed;left: 0;top: 0;z-index: 10;pointer-events: none;transition: all 1s;":style="{ transform: `translate3d(0,${-index * 100}vh,0)` }"><div v-for="item in scenes" style="width: 100vw; height: 100vh"><h1 style="padding: 100px 50px; font-size: 50px; color: #fff">{{ item.text }}</h1></div></div>
</template>
10.满天星星
我们来做最后一个效果,满天星星,并且在文字切换到星星送给你的时候,汇聚成一个心形
1.创建星星群体
// 实例化创建漫天星星
let starsInstance = new THREE.InstancedMesh(new THREE.SphereGeometry(0.1, 32, 32),new THREE.MeshStandardMaterial({color: 0xffffff,emissive: 0xffffff,emissiveIntensity: 10,}),100
);
2.随机分布到天空中
// 随机分布到天空
let startArr = [];
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;startArr.push(new THREE.Vector3(x, y, z));// endArr.push(new THREE.Vector3(x, y, z));let matrix = new THREE.Matrix4();matrix.setPosition(x, y, z);// i第几个 matrix矩阵starsInstance.setMatrixAt(i, matrix);
}
scene.add(starsInstance);
3.使用贝塞尔曲线构建一个心形
至于这个代码为什么最后变成的是心形,涉及到canvas的内容,即将跟着@老陈打码学习。
// 创建爱心路径 贝塞尔曲线创建
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);
4.设置点的坐标
for (let i = 0; i < 100; i++) {// threejs的方法let point = heartShape.getPoint(i / 100);endArr.push(new THREE.Vector3(point.x, point.y, point.z));
}
5.构建星星函数
// 创建爱心动画
function makeHeart() {// 创建一个爱心let params = {time: 0,};gsap.to(params, {time: 1,duration: 1,onUpdate: () => {let time = params.time;for (let i = 0; i < 100; i++) {let x = startArr[i].x + (endArr[i].x - startArr[i].x) * time;let y = startArr[i].y + (endArr[i].y - startArr[i].y) * time;let z = startArr[i].z + (endArr[i].z - startArr[i].z) * time;let matrix = new THREE.Matrix4();matrix.setPosition(x, y, z);starsInstance.setMatrixAt(i, matrix);}starsInstance.instanceMatrix.needsUpdate = true;},});
}
我们会发现效果上面,图形很大,所以我们对其进行缩小处理
// 根据爱心路径获取点
// 设置中心
let center = new THREE.Vector3(0, 2, 10);
for (let i = 0; i < 100; i++) {let point = heartShape.getPoint(i / 100);endArr.push(new THREE.Vector3(point.x * 0.1 + center.x,point.y * 0.1 + center.y,center.z));
}
6.重置心形函数
我们也需要在过去这个场景之后,将这个原型还原为之前的样子。
function restoreHeart() {let params = {time: 0,};gsap.to(params, {time: 1,duration: 1,onUpdate: () => {let time = params.time;for (let i = 0; i < 100; i++) {let x = endArr[i].x + (startArr[i].x - endArr[i].x) * time;let y = endArr[i].y + (startArr[i].y - endArr[i].y) * time;let z = endArr[i].z + (startArr[i].z - endArr[i].z) * time;let matrix = new THREE.Matrix4();matrix.setPosition(x, y, z);starsInstance.setMatrixAt(i, matrix);}starsInstance.instanceMatrix.needsUpdate = true;},});
}
我们要在侦听函数中,当index == 0 的时候 调用这个
// 侦听鼠标滚轮事件
window.addEventListener("wheel",(e) => {if (isAnimte) return;isAnimte = true;// 判断滚轮的方向if (e.deltaY > 0) {index.value++;if (index.value > scenes.length - 1) {index.value = 0;restoreHeart();}}scenes[index.value].callback();setTimeout(() => {isAnimte = false;}, 1000);},false
);
我们的作品基本上就完成了。
全部代码
App.vue
<template><divclass="scenes"style="position: fixed;left: 0;top: 0;z-index: 10;pointer-events: none;transition: all 1s;":style="{ transform: `translate3d(0,${-index * 100}vh,0)` }"><div v-for="item in scenes" style="width: 100vw; height: 100vh"><h1 style="padding: 100px 50px; font-size: 50px; color: #fff">{{ item.text }}</h1></div> </div>
</template><script setup>
import { ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { Water } from "three/examples/jsm/objects/Water2";
import gsap from "gsap/gsap-core";//初始化场景
const scene = new THREE.Scene();
//初始化相机
const camera = new THREE.PerspectiveCamera(75,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.toneMapping = THREE.ACESFilmicToneMapping;
// 设置色调映射输出
renderer.outputEncoding = THREE.sRGBEncoding;
// 设置色调映射曝光
renderer.toneMappingExposure = 0.75;
//允许阴影
renderer.shadowMap.enabled = true;
// 设置物理的光照效果
renderer.physicallyCorrectLights = true;//初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(-8, 2, 0);
//
controls.enableDamping = true;//初始化解压模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/public/draco/");
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);//加载环境纹理(天空、水面)
let rgbeLoader = new RGBELoader();
rgbeLoader.load("/public/textures/sky.hdr", (textures) => {//由于天空是一个全景图,所以给他加一个球体的映射 让他包裹住整个场景textures.mapping = THREE.EquirectangularReflectionMapping;scene.background = textures;scene.environment = textures;
});//加载模型
gltfLoader.load("/public/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.CircleGeometry(300, 32);
const water = new Water(waterGeometry, {textureWidth: 1024,textureHeight: 1024,color: 0xeeeeff,flowDirection: new THREE.Vector2(1, 1),scale: 100,
});
water.rotation.x = -Math.PI / 2;
water.position.y = -0.4;
scene.add(water);//初始化光源
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.2, 32, 32);const sphereMaterial = new THREE.MeshStandardMaterial({color: 0xffffff,emissive: 0xffffff,emissiveIntensity: 1,});const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);pointLightArr.push(sphere);sphere.position.set(radius * Math.cos((i * 2 * Math.PI) / 3),Math.cos((i * 2 * Math.PI) / 3),radius * Math.sin((i * 2 * Math.PI) / 3));let pointLight = new THREE.PointLight(0xffffff, 1);sphere.add(pointLight);pointLightGroup.add(sphere);
}
// 使用补间函数 从0到2π 旋转
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 * 2 * Math.PI) / 3),Math.cos((index * 2 * Math.PI) / 3 + options.angle * 5),radius * Math.sin((index * 2 * Math.PI) / 3));});},
});scene.add(pointLightGroup);
// 渲染函数
function render() {requestAnimationFrame(render);renderer.render(scene, camera);controls.update();
}render();// 使用补间动画移动相机
let timeLine1 = new gsap.timeline();
let timeLine2 = new gsap.timeline();// 定义相机移动函数
function translateCamera(position, target) {// 通过补间函数移动相机timeLine1.to(camera.position, {x: position.x,y: position.y,z: position.z,duration: 1,ease: "power2.inout",});// 聚焦到哪一点timeLine2.to(controls.target, {x: target.x,y: target.y,z: target.z,duration: 1,ease: "power2.inout",});
}// 鼠标滑轮切换效果
let scenes = [{text: "圣诞快乐",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(-3.23, 3, 4.06),new THREE.Vector3(-8, 2, 0));},},{text: "感谢在这么大的世界里遇到了你",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));},},{text: "愿与你探寻世界的每一个角落",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(10, 3, 0), new THREE.Vector3(5, 2, 0));},},{text: "愿将天上的星星送给你",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));makeHeart();},},{text: "加油~",callback: () => {// 执行函数切换位置translateCamera(new THREE.Vector3(-20, 1.3, 6.6),new THREE.Vector3(5, 2, 0));},},
];// 用来做场景的记录
const index = ref(0);// 防抖函数
let isAnimte = false;// 侦听鼠标滚轮事件
window.addEventListener("wheel",(e) => {if (isAnimte) return;isAnimte = true;// 判断滚轮的方向if (e.deltaY > 0) {index.value++;if (index.value > scenes.length - 1) {index.value = 0;restoreHeart();}}scenes[index.value].callback();setTimeout(() => {isAnimte = false;}, 1000);},false
);// 实例化创建漫天星星
let starsInstance = new THREE.InstancedMesh(new THREE.SphereGeometry(0.1, 32, 32),new THREE.MeshStandardMaterial({color: 0xffffff,emissive: 0xffffff,emissiveIntensity: 10,}),100
);// 随机分布到天空
let startArr = [];
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;startArr.push(new THREE.Vector3(x, y, z));// endArr.push(new THREE.Vector3(x, y, z));let matrix = new THREE.Matrix4();matrix.setPosition(x, y, z);// i第几个 matrix矩阵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);// 根据爱心路径获取点
// 设置中心
let center = new THREE.Vector3(0, 2, 10);
for (let i = 0; i < 100; i++) {let point = heartShape.getPoint(i / 100);endArr.push(new THREE.Vector3(point.x * 0.1 + center.x,point.y * 0.1 + center.y,center.z));
}for (let i = 0; i < 100; i++) {// threejs的方法let point = heartShape.getPoint(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: 1,onUpdate: () => {let time = params.time;for (let i = 0; i < 100; i++) {let x = startArr[i].x + (endArr[i].x - startArr[i].x) * time;let y = startArr[i].y + (endArr[i].y - startArr[i].y) * time;let z = startArr[i].z + (endArr[i].z - startArr[i].z) * time;let matrix = new THREE.Matrix4();matrix.setPosition(x, y, z);starsInstance.setMatrixAt(i, matrix);}starsInstance.instanceMatrix.needsUpdate = true;},});
}function restoreHeart() {let params = {time: 0,};gsap.to(params, {time: 1,duration: 1,onUpdate: () => {let time = params.time;for (let i = 0; i < 100; i++) {let x = endArr[i].x + (startArr[i].x - endArr[i].x) * time;let y = endArr[i].y + (startArr[i].y - endArr[i].y) * time;let z = endArr[i].z + (startArr[i].z - endArr[i].z) * time;let matrix = new THREE.Matrix4();matrix.setPosition(x, y, z);starsInstance.setMatrixAt(i, matrix);}starsInstance.instanceMatrix.needsUpdate = true;},});
}
</script><style>
* {margin: 0;padding: 0;
}
canvas {width: 100vw;height: 100vh;position: fixed;left: 0;top: 0;
}
</style>