当前位置: 首页 > news >正文

threeJS 实现开花的效果

一、效果

threeJs 开花效果

二、核心知识点

**

开花效果就是一个顶点状态到另一个顶点状态的变化过程: 就是拿到花瓣的顶点的位置,然后再把这些位置的变化连接起来,就会成为一个动画效果。

**

(1)Three.js 基础

场景(Scene)、相机(Camera)、渲染器(Renderer)、灯光(Light)、控制器(OrbitControls)等基本概念。
GLTF/GLB 模型加载
使用 GLTFLoader 加载 glb 模型,支持 DRACO 压缩(需配置 DRACOLoader)。
Morph Target(形变目标)动画
利用 morph target 实现模型顶点的平滑变形,适合做“开花”、“表情”等动画。
morph target 要求所有目标 mesh 的顶点数量和顺序完全一致。

主要用到的事threeJS中geometry的核心属性:

morphAttributesmorphTargetInfluences

简单来说:morphAttributes就是用来存储你的模型的顶点数据,morphTargetInfluences来实现你的状态从0到1的一个变化。
属性文档:
https://threejs.org/docs/?q=geo#api/zh/core/BufferGeometry
在这里插入图片描述
文档地址:https://threejs.org/docs/?q=mesh#api/zh/objects/Mesh
在这里插入图片描述
此外,为了方便还用到一个动画库,这个动画库也可以不用

(2) GSAP 动画库

用于平滑控制 morphTargetInfluences,实现自然的动画过渡。

GSAP(GreenSock Animation
Platform)是一个强大的JavaScript动画库,可让开发人员轻松地制作各种复杂的动画。以下是导入GSAP动画库的步骤:

  1. 首先,你需要安装GSAP。你可以通过npm来安装它。在你的终端中运行以下命令:npm install gsap。
  2. 安装完成后,你可以在你的JavaScript文件中导入GSAP。使用以下代码来导入它:import { gsap } from ‘gsap’;。
    安装和导入GSAP后,你就可以使用它的各种功能来创建动画了。例如,你可以使用gsap.to()方法来创建一个动画,将一个或多个对象从当前状态过渡到指定的目标状态。

三、实现步骤

(1)并行加载多个 GLB 模型

用 Promise.all 并行加载多个形变阶段的 glb 文件。
配置 DRACO 解码器,支持压缩模型。

(2) 收集 morph target 数据

以第一个模型为主模型,其余模型作为 morph target 数据源。
遍历主模型,找到需要做动画的 mesh(如 Stem、Petal),初始化 morphAttributes。
遍历其余模型,将同名 mesh 的顶点数据 push 到主模型的 morphAttributes.position 数组。

(3) 更新 morph target 并添加到场景

调用 updateMorphTargets(),让 Three.js 识别 morph target。
将主模型添加到场景。

(4) Three.js 场景与渲染

创建场景、相机、渲染器、灯光、坐标轴辅助、轨道控制器。
启动渲染循环。

(5)材质处理

针对特殊材质(如水、玻璃)替换为支持透明和物理属性的材质,保证渲染效果。

(6)开花动画实现

使用 GSAP 对一个对象的 value 属性做插值动画。
在 onUpdate 回调中,采用“波浪推进”方式,平滑控制每个 morphTargetInfluences,实现自然的开花动画。

四、动画优化技巧

  • 单对象插值推进:只用一个 gsap 动画对象,避免多重嵌套和性能浪费,动画更顺滑。
  • morph target 数量建议 4~8 个,兼容性和性能最佳。
  • 材质类型必须支持 morph target,如 MeshStandardMaterial、MeshPhysicalMaterial。

五、常见问题与排查

  • morph target 动画无效?检查顶点数量、顺序、材质类型、morphTargetInfluences 是否为有效数组。
  • 动画卡顿?避免多重 gsap 动画,推荐单对象插值推进。
  • morph target 数据结构报错?确保所有 BufferAttribute 的 itemSize、count 完全一致。

六、完整代码结构参考

gitee仓库地址

<template><div id="floor_canves"></div>
</template><script setup>
import * as THREE from "three";
import { onMounted, nextTick } from "vue";
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 gsap from "gsap";// 并行加载GLB模型的Promise封装
function loadGLB(url, loader) {return new Promise((resolve, reject) => {loader.load(url, gltf => resolve(gltf), undefined, reject);});
}onMounted(() => {nextTick(() => {init();});
});async function init() {// 获取DOM容器const container = document.getElementById("floor_canves");// 创建场景const scene = new THREE.Scene();scene.background = new THREE.Color(0xffffff);// 相机const camera = new THREE.PerspectiveCamera(50,container.clientWidth / container.clientHeight,0.1,2000);camera.position.set(0.55, 43.23, 44.27);// GLTF加载器和DRACO解码器// 由于模型被压缩了,所以需要解码,这个根据自己的模型情况而定// 使用DRACO解码器需要引入DRACOLoader,并且将四个文件放到你项目的 public/libs/draco/ 目录下(或其它你喜欢的目录)。/*** 如果你用 npm 安装了 three,可以在本地找到:* 里面的:draco_decoder.jsdraco_decoder.wasmdraco_wasm_wrapper.jsdraco_decoder.wasm.js这四个文件在 three.js 的 examples/jsm/libs/draco 目录下。下载后放到你项目的 public 目录下,路径和 setDecoderPath 保持一致即可。* */const loader = new GLTFLoader();const dracoLoader = new DRACOLoader();dracoLoader.setDecoderPath('/libs/draco/');loader.setDRACOLoader(dracoLoader);// 所有模型路径const urls = ["../assets/json/f4.glb","../assets/json/f3.glb","../assets/json/f2.glb","../assets/json/f1.glb",].map(url => new URL(url, import.meta.url).href);// 并行加载所有模型const gltfs = await Promise.all(urls.map(url => loadGLB(url, loader)));// 取 fl5 作为主模型const model = gltfs[0].scene;model.position.set(0, 0, 20);// 调整位置:模型设计的时候的缺陷的弥补// x轴转180度model.rotation.x = THREE.MathUtils.degToRad(180);model.rotation.z = THREE.MathUtils.degToRad(-5);let stem5, flower5;// 遍历主模型,找到Stem(茎)和Petal(花)model.traverse(child => {if (child.material && child.material.name === 'Water') {// 修改水的材质child.material = new THREE.MeshPhysicalMaterial({color: 0x00ffff,depthWrite: false,depthTest: false,transparent: true,opacity: 0.5,});}if (child.material && child.material.name === 'Glass Simple') {// 修改玻璃材质child.material = new THREE.MeshPhysicalMaterial({color: 0xffffff,metalness: 0.1,roughness: 0.1,transparent: true,opacity: 0.5,});}// 拿到 茎 的初始数据if (child.material && child.material.name === 'Stem') {stem5 = child;// 创建一个morphAttributes不存在的属性用于存储未来要变化的顶点数据stem5.geometry.morphAttributes.position = [];}// 拿到 花 的初始数据if (child.material && child.material.name === 'Petal') {flower5 = child;flower5.geometry.morphAttributes.position = [];}});// 收集其余模型的morph targetfor (let i = 1; i < gltfs.length; i++) {gltfs[i].scene.traverse(child => {// 获取Stem和Petal的顶点数据if (child.material && child.material.name === 'Stem') {stem5.geometry.morphAttributes.position.push(child.geometry.attributes.position);}if (child.material && child.material.name === 'Petal') {flower5.geometry.morphAttributes.position.push(child.geometry.attributes.position);}});}// 这段代码是为了查看模型是否正确:// 第一:判断顶点数是否一致,必须是一致的,才能实现动画// 第二:查看每一个模型的顶点的位置是否是一样的,每一组顶点位置必须不一致,才能实现动画,一致的话可能是同一个模型,那就是模型的问题//     for (let i = 0; i < gltfs.length; i++) {//         gltfs[i].scene.traverse(child => {//             if (child.material && child.material.name === 'Stem') {//                 console.log(`Stem ${i} 顶点数:`, child.geometry.attributes.position.count);//             }//             if (child.material && child.material.name === 'Petal') {//                 console.log(`Petal ${i} 顶点数:`, child.geometry.attributes.position.count);//             }//             if (child.material && child.material.name === 'Petal') {//                 // 打印前10个顶点坐标//                 let arr = child.geometry.attributes.position.array;//                 console.log(`Stem ${i} 前10顶点:`, arr.slice(0, 30));//             }//         });// }// 更新morph targetstem5.updateMorphTargets();flower5.updateMorphTargets();// 添加主模型到场景scene.add(model);// 渲染器let renderer = new THREE.WebGLRenderer({ antialias: true });renderer.setSize(container.clientWidth, container.clientHeight);container.appendChild(renderer.domElement);// 灯光let amlight = new THREE.AmbientLight(0xffffff, 1);scene.add(amlight);let dirLight = new THREE.DirectionalLight(0xffffff, 1.2);dirLight.position.set(50, 100, 50);scene.add(dirLight);// 坐标轴辅助const axesHelper = new THREE.AxesHelper(100);// scene.add(axesHelper);// 轨道控制器let controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;controls.dampingFactor = 0.25;// 渲染循环function animate() {requestAnimationFrame(animate);controls.update();renderer.render(scene, camera);// console.log(camera.position,"---查看相机位置");}animate();// console.log(flower5.geometry.morphAttributes.position, "模型加载完成,开始动画");// 开花动画--这段代码为未优化的代码,嵌套太深,后面又优化的代码,这段代码就是方便理解而保留的// let morphObj = { value: 0, value1: 0, value2: 0, value3: 0,};// gsap.to(morphObj, {//     value: 1,//     duration: 4,//     onUpdate: () => {//         stem5.morphTargetInfluences[0] = morphObj.value;//         flower5.morphTargetInfluences[0] = morphObj.value;//     },//     // 完成第一次动画以后,执行第二个动画//     onComplete: () => {//         gsap.to(morphObj, {//             value1: 1,//             duration: 4,                //             onUpdate: () => {//                 stem5.morphTargetInfluences[1] = morphObj.value1;//                 flower5.morphTargetInfluences[1] = morphObj.value1;//             },//             //第三个动画//             onComplete: () => {//                 gsap.to(morphObj, {//                     value2: 1,//                     duration: 4,//                     onUpdate: () => {//                         stem5.morphTargetInfluences[2] = morphObj.value2;//                         flower5.morphTargetInfluences[2] = morphObj.value2;//                     },//                 })//             }//         });//     }// });// 优化开花动画:单对象插值推进const morphCount = stem5.morphTargetInfluences.length;let morphObj = { value: 0 };gsap.to(morphObj, {value: morphCount,duration: morphCount * 4, // 总时长可调ease: "power1.inOut",onUpdate: () => {for (let i = 0; i < morphCount; i++) {// 让每个 influence 像波浪一样推进let t = morphObj.value - i;stem5.morphTargetInfluences[i] = THREE.MathUtils.clamp(t, 0, 1);flower5.morphTargetInfluences[i] = THREE.MathUtils.clamp(t, 0, 1);}}});}
</script><style scoped>
#floor_canves {width: 100vw;height: 100vh;background: #fff;overflow: hidden;
}
</style>

文章转载自:

http://MhAMorGn.cnLmp.cn
http://E7NgQ0Fy.cnLmp.cn
http://SfYSyVHi.cnLmp.cn
http://cqieX5JV.cnLmp.cn
http://MKic1QHt.cnLmp.cn
http://bExKIxwX.cnLmp.cn
http://2M1YErSM.cnLmp.cn
http://6vlZCm7Y.cnLmp.cn
http://kSuUL0Tu.cnLmp.cn
http://PbpYcpJP.cnLmp.cn
http://P1HIxdNF.cnLmp.cn
http://ORi70KT4.cnLmp.cn
http://yiES6yAv.cnLmp.cn
http://OqpT5zMF.cnLmp.cn
http://rVmci5Xa.cnLmp.cn
http://PeeKt0W9.cnLmp.cn
http://U44rFvCT.cnLmp.cn
http://UiE4AFS1.cnLmp.cn
http://Z96SxKVd.cnLmp.cn
http://H5ZPmdtO.cnLmp.cn
http://obtGAjBK.cnLmp.cn
http://ttbNMV75.cnLmp.cn
http://BKzWfxYi.cnLmp.cn
http://89r5C5dS.cnLmp.cn
http://vfyE8rO6.cnLmp.cn
http://r0wcM44F.cnLmp.cn
http://huQgF50R.cnLmp.cn
http://3elDdQG1.cnLmp.cn
http://NpZipXQv.cnLmp.cn
http://hSxcelKQ.cnLmp.cn
http://www.dtcms.com/a/367997.html

相关文章:

  • 【数字孪生核心技术】数字孪生有哪些核心技术?
  • Leetcode—2749. 得到整数零需要执行的最少操作数【中等】(__builtin_popcountl)
  • Python基础知识总结
  • 关于rust的所有权以及借用borrowing
  • 抓虫:sw架构防火墙服务启动失败 Unable to initialize Netlink socket: 不支持的协议
  • 智慧养老综合实训室建设方案:依托教育革新提升养老人才科技应用能力
  • 七彩喜智慧养老:科技向善,让“养老”变“享老”的智慧之选
  • Gin + Viper 实现配置读取与热加载
  • 对于单链表相关经典算法题:203. 移除链表元素的解析
  • OpenLayers常用控件 -- 章节五:鹰眼地图控件教程
  • Swift 协议扩展与泛型:构建灵活、可维护的代码的艺术
  • python代码Bug排查
  • Xilinx系列FPGA实现DP1.4视频收发,支持4K60帧分辨率,提供2套工程源码和技术支持
  • HTML文本格式化标签
  • OpenCV C++ 进阶:图像直方图与几何变换全解析
  • Java全栈学习笔记30
  • PiscCode轨迹跟踪Mediapipe + OpenCV进阶:速度估算
  • Java 学习笔记(进阶篇2)
  • OpenCV C++ 核心:Mat 与像素操作全解析
  • 实践指南:利用衡石AI Data Agent实现自然语言驱动的指标开发与归因
  • 23种设计模式——代理模式(Proxy Pattern)详解
  • 前端安全防护深度实践:从XSS到供应链攻击的全面防御
  • Bug排查日记:从崩溃到修复的实战记录
  • Xsens解码人形机器人训练的语言
  • 保姆级 i18n 使用攻略,绝对不踩坑(帮你踩完了)
  • Linux 文件系统及磁盘相关知识总结
  • 服务器为啥离不开传感器?一文看懂数据中心“隐形守护者”的关键角色
  • 【FastDDS】概述 Library Overview
  • 秋招还在手动筛简历?AI简历筛选3步实现效率跃升
  • 改 TDengine 数据库的时间写入限制