Threejs进阶之十三:CSS3DRenderer与Tween.js实现粒子小球按规律变化

news/2024/5/5 6:16:08/文章来源:https://blog.csdn.net/w137160164/article/details/130534981

今天我们使用CSS3DRenderer+Tween.js实现Threejs官方示例中的粒子小球按规律变化的效果,先看下最终实现的效果
在这里插入图片描述
先来分析下,这个页面的动画效果是由512个小球组合起来的四种不同变化,分别是曲面、立方体、随机和圆球四种变化;下面我们来实现下这个效果

初始化页面

老套路,要实现上面的效果之前,我们需要先将Threejs的基础场景搭建起来,这个是老生常谈的事情了,不在赘述,不知道怎么创建的小伙伴请参考我前面的博客文章基于vite+vue3+threejs构建三维场景这里直接上代码

<template><div id="scene"></div>
</template>
<script setup>
import * as THREE from 'three' 
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { onMounted } from 'vue';  
let camera,scene,renderer
let controls 
onMounted(()=>{init()
})
function init() {initScene()initCamera()initMesh()initCss3DRenderer()initControls()animate()window.addEventListener('resize',onWindowResize)
}
function initScene() {scene = new THREE.Scene() scene.background = new THREE.Color(0x808080)
}
function initCamera() {camera = new THREE.PerspectiveCamera(75,window.innerWidth / window.innerHeight,1,5000)camera.position.set(600,400,1500)camera.lookAt(0,0,0)
} 
function initControls() {controls = new OrbitControls(camera,renderer.domElement)
}
function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeightcamera.updateProjectionMatrix()renderer.setSize(window.innerWidth,window.innerHeight)
}
function animate() {requestAnimationFrame(animate) renderer.render(scene,camera)
} 
</script>
<style lang='scss' scoped>
</style>

创建小球

上面的小球其实是一张png格式的图片,为了保证我们旋转相机时图片始终朝向屏幕,我们考虑将其转换为精灵图,使用CSS3DSprite可以将其作为参数传递进去,使其变为精灵图;另外,我们需要512个这样的精灵图,所以,我们定义一个变量,使其值为512,然后用for循环遍历,设置其位置随机变化,并添加到屏幕上;代码如下

引入CSS3DRenderer和CSS3DSprite

import { CSS3DRenderer, CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer';

定义变量并遍历生成小球

1、定义变量:
定义小球总量用于遍历;定义objects 数组用于存储创建的每个小球对象;定义positions 数组用于存储每次变化时的每个小球的位置
2、创建img标签:
使用document.createElement('img')创建image标签,并使用image.src = '../../public/textures/sprite.png'加载图片
3、监听image的load事件
监听image的load事件,并在其回调函数中使用for循环创建CSS3DSprite对象,同时给每个创建的对象指定x,y,z坐标位置,位置在-2000到2000之间随机分布,将其添加到scene和objects中

const particlesTotal = 512 // 小球数量
const positions = [] //位置坐标数组
const objects = [] //物体数组
let current = 0
function initMesh() {// 创建image标签const image = document.createElement('img')image.src = '../../public/textures/sprite.png'// image监听load事件image.addEventListener('load',function() {// 遍历  创建CSS3DSpritefor(let i = 0; i < particlesTotal; i++ ) {const object = new CSS3DSprite( image.cloneNode())object.position.x = Math.random() * 4000 - 2000object.position.y = Math.random() * 4000 - 2000object.position.z = Math.random() * 4000 - 2000scene.add(object)objects.push(object)} }) 
} 

这里在创建CSS3DSprite是使用了HTML DOM cloneNode(deep) 方法
cloneNode(deep) 方法 拷贝所有属性和值。
deep参数是可选值,该方法将复制并返回调用它的节点的副本。如果传递给它的参数是 true,它还将递归复制当前节点的所有子孙节点。否则,它只复制当前节点。

定义曲面

观察上面曲面的变化,我们发现其是在xoz平面上沿x轴波浪起伏变化的,我们可以考虑使用正弦函数,使其达到起伏变化的效果;
1、定义小球
小球总量是512个,我们设置x轴每行16个,z轴每行32个,小球间隔150
2、计算x轴总长和z轴总长
通过上小球每行的总数和小球间隔,计算出x轴总长和z轴总长
3、循环遍历每个小球,计算每个小球的位置坐标
通过for循环遍历每个小球,计算出每个小球的x,y,z坐标,并将其存储在positions数组中

// Planeconst amountX = 16  //x 轴上的数量const amountZ = 32 // z 轴上的数量const separationPlane = 150 //间隔const offsetX = ((amountX - 1 ) * separationPlane) / 2 //x轴总长const offsetZ = ((amountZ - 1 ) * separationPlane) / 2 //z轴总长for(let i = 0; i < particlesTotal; i++) {const x = (i % amountX) * separationPlane const z = Math.floor(i / amountX) * separationPlaneconst y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200 positions.push(x-offsetX,y,z-offsetZ) //每个小球的坐标}

定义立方体

定义立方体的方法和上面类似,这里不再赘述,直接上代码

// Cubeconst amount = 8 //数量const separationCube = 150  //间隔const offset = ((amount - 1 ) * separationCube ) /2 //长度for(let i = 0; i < particlesTotal; i ++ ) {const x = (i % amount) * separationCubeconst y = Math.floor( ( i / amount ) % amount ) * separationCube;const z = Math.floor( i / ( amount * amount ) ) * separationCube; positions.push( x - offset, y - offset, z - offset );}

定义随机变化位置

定义每个小球随机变化的位置,只需要调用Math.random()函数就可以了,将x,y,z的随机位置存入positions数组中

 // Randomfor ( let i = 0; i < particlesTotal; i ++ ) { positions.push(Math.random() * 4000 - 2000,Math.random() * 4000 - 2000,Math.random() * 4000 - 2000); }

定义圆形

定义圆形,我们先定义一个半径,然后遍历每个小球,定义其在圆上的位置,这里我们用到了极坐标的知识,不了解的执行百度

// Sphereconst radius = 750 //半径for ( let i = 0; i < particlesTotal; i ++ ) { const phi = Math.acos( - 1 + ( 2 * i ) / particlesTotal );const theta = Math.sqrt( particlesTotal * Math.PI ) * phi;positions.push(radius * Math.cos( theta ) * Math.sin( phi ),radius * Math.sin( theta ) * Math.sin( phi ),radius * Math.cos( phi )); }

定义变化函数

上面我们定义好了各个变化的坐标,接着我们就可以Tween函数来指定动画了
上面我们将每种变化的位置坐标都放在了positions数组中,里面对应每一个球的x,y,z的坐标,通过在for循环中使用Tween.to()方法达到动画效果

function transition() {const offset = current * particlesTotal * 3;// 要切换到每种类型变化位置的偏移量const duration = 2000;//动画时长for(let i = 0, j = offset; i < particlesTotal; i++, j+=3){const object = objects[ i ]new TWEEN.Tween(object.position)//每个小球的位置变化.to({x:positions[ j ],y:positions[ j + 1 ],z:positions[ j + 2 ],},Math.random()*duration + duration).easing(TWEEN.Easing.Exponential.InOut).start()}//定时切换 这里使用tween的to方法传递一个空的对象,定义事件来完成定时,相当于一个定时器new TWEEN.Tween( this ).to( {}, duration * 3 ).onComplete( transition ).start();current = ( current + 1 ) % 4;
}

调用transition()方法

在图像加载监听器的回调函数中调用transition(),达到动画效果

image.addEventListener('load',function() {// 遍历  创建CSS3DSpritefor(let i = 0; i < particlesTotal; i++ ) {const object = new CSS3DSprite( image.cloneNode())object.position.x = Math.random() * 4000 - 2000object.position.y = Math.random() * 4000 - 2000object.position.z = Math.random() * 4000 - 2000scene.add(object)objects.push(object)}transition() })

至此,我们就实现了上面的动画效果
核心代码如下

<template><div id="scene"></div>
</template>
<script setup>
import * as THREE from 'three'
import * as  TWEEN   from '@tweenjs/tween.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { onMounted } from 'vue';
import { CSS3DRenderer, CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer';let camera,scene,renderer
let controls
const particlesTotal = 512 // 小球数量
const positions = [] //位置坐标数组
const objects = [] //物体数组
let current = 0
onMounted(()=>{init()
})
function init() {initScene()initCamera()initMesh()initCss3DRenderer()initControls()animate()window.addEventListener('resize',onWindowResize)
}
function initScene() {scene = new THREE.Scene() scene.background = new THREE.Color(0x808080)
}
function initCamera() {camera = new THREE.PerspectiveCamera(75,window.innerWidth / window.innerHeight,1,5000)camera.position.set(600,400,1500)camera.lookAt(0,0,0)
}
function initCss3DRenderer() {renderer = new CSS3DRenderer()renderer.setSize(window.innerWidth,window.innerHeight)document.querySelector('#scene').appendChild(renderer.domElement)
}
function initControls() {controls = new OrbitControls(camera,renderer.domElement)
}
function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeightcamera.updateProjectionMatrix()renderer.setSize(window.innerWidth,window.innerHeight)
}
function animate() {requestAnimationFrame(animate)TWEEN.update();renderer.render(scene,camera)
}
function initMesh() {// 创建image标签const image = document.createElement('img')image.src = '../../public/textures/sprite.png'// image监听load事件image.addEventListener('load',function() {// 遍历  创建CSS3DSpritefor(let i = 0; i < particlesTotal; i++ ) {const object = new CSS3DSprite( image.cloneNode())object.position.x = Math.random() * 4000 - 2000object.position.y = Math.random() * 4000 - 2000object.position.z = Math.random() * 4000 - 2000scene.add(object)objects.push(object)}transition() })// Planeconst amountX = 16  //x 轴上的数量const amountZ = 32 // z 轴上的数量const separationPlane = 150 //间隔const offsetX = ((amountX - 1 ) * separationPlane) / 2 //x轴总长const offsetZ = ((amountZ - 1 ) * separationPlane) / 2 //z轴总长for(let i = 0; i < particlesTotal; i++) {const x = (i % amountX) * separationPlane const z = Math.floor(i / amountX) * separationPlaneconst y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200positions.push(x-offsetX,y,z-offsetZ) //每个小球的坐标}// Cubeconst amount = 8 //数量const separationCube = 150  //间隔const offset = ((amount - 1 ) * separationCube ) /2 //偏移量for(let i = 0; i < particlesTotal; i ++ ) {const x = (i % amount) * separationCubeconst y = Math.floor( ( i / amount ) % amount ) * separationCube;const z = Math.floor( i / ( amount * amount ) ) * separationCube;positions.push( x - offset, y - offset, z - offset );}// Randomfor ( let i = 0; i < particlesTotal; i ++ ) { positions.push(Math.random() * 4000 - 2000,Math.random() * 4000 - 2000,Math.random() * 4000 - 2000); }// Sphereconst radius = 750 //半径for ( let i = 0; i < particlesTotal; i ++ ) { const phi = Math.acos( - 1 + ( 2 * i ) / particlesTotal );const theta = Math.sqrt( particlesTotal * Math.PI ) * phi;positions.push(radius * Math.cos( theta ) * Math.sin( phi ),radius * Math.sin( theta ) * Math.sin( phi ),radius * Math.cos( phi )); }
} 
function transition() {const offset = current * particlesTotal * 3;// 要切换到每种类型变化位置的偏移量const duration = 2000;//动画时长for(let i = 0, j = offset; i < particlesTotal; i++, j+=3){const object = objects[ i ]new TWEEN.Tween(object.position)//每个小球的位置变化.to({x:positions[ j ],y:positions[ j + 1 ],z:positions[ j + 2 ],},Math.random()*duration + duration).easing(TWEEN.Easing.Exponential.InOut).start()}//定时切换 这里使用tween的to方法传递一个空的对象,定义事件来完成定时,相当于一个定时器new TWEEN.Tween( this ).to( {}, duration * 3 ).onComplete( transition ).start();current = ( current + 1 ) % 4;
}
</script>
<style lang='scss' scoped>
</style>

今天就到这里吧,喜欢的小伙伴点赞关注收藏哦!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_296908.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UDP的报文结构

UDP 报文结构 基本上所有的教科书上都是这样画的图, 但实际上 UDP 报文结构不是这样的, 这样显示应该是容易排版. 正确应该如下图 : 端口号 : 每个端口号在 UDP 报文里占两个字节, 取值范围就是: 0 ~ 65535 源 ip 和源端口描述了数据从哪里来, 目的 ip 和目的端口描述了数据去哪…

文本的清洗和标准化:如何处理混乱的数据?

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

操作系统考试复习—第三章 优先级倒置 死锁问题

当前OS广泛采用优先级调度算法和抢占方式&#xff0c;然而在系统中存在着影响进程运行的资源从而可能产生"优先级倒置"现象 具体解释为&#xff1a;在原本的调度算法设计中&#xff0c;高优先级进程可以抢占低优先级的CPU资源&#xff0c;先执行高优先级任务。但是存…

x265码控分析

D和R的关系 高分辨率量化 均匀量化&#xff1a;量化区间 ‘ Δ k y k − y k − 1 ‘ \Delta_ky_k-y_{k-1} ‘Δk​yk​−yk−1​‘&#xff0c;近似为常数&#xff1b;p(x)为信源概率密度函数&#xff0c;且 ‘ Δ k ‘ \Delta_k ‘Δk​‘的大小相对于p(x)的变化率充分小&…

电力NLP:指令票规范识别

文章目录 任务目的想法讲解数据集介绍1电气主语2操作任务判断数据集3操作内容判断数据集4错误词数据集 解法讲解程序、数据集下载链接 任务目的 识别调度指令票&#xff08;或者其它操作票&#xff09;是否规范。 想法讲解 按石第2014—16号定值单投入石双西线161开关6区保护…

突发!ChatGPT王炸级更新!支持GPT-4联网 Code Interpreter!

4月30日&#xff0c;OpenAI官方悄悄发布了联网版GPT-3.5。虽然名字变了&#xff0c;但使用体验却是换汤不换药&#xff0c;还是那套。 然而&#xff0c;万万没想到的是&#xff0c;刚过去没几天&#xff0c;昨天5月4日&#xff0c;鱼哥发现自己的Plus账号竟然多了一些能力&…

树莓派硬件介绍及配件选择

目录 树莓派Datasheet下载地址&#xff1a; Raspberry 4B 外观图&#xff1a; 技术规格书&#xff1a; 性能介绍&#xff1a; 树莓派配件选用 电源的选用&#xff1a; 树莓派外壳选用&#xff1a; 内存卡/U盘选用 树莓派Datasheet下载地址&#xff1a; Raspberry Pi …

C++11多线程:std::thread创建线程和std::async创建异步任务的区别,std::async创建异步任务后没有被推迟执行。

系列文章目录 文章目录 系列文章目录前言一、thread和async的区别1.1 新线程和异步任务1.2 std::async和std::thread最明显的不同&#xff0c;就是async有时候并不创建新线程。1.3 std::async和std::thread的区别1.4 std::async不确定性问题的解决 二、使用方法2.1 std::async创…

JVM学习随笔02——虚拟机内存区组成与内存溢出异常

一、Java虚拟机内存区组成图 1、程序计数器&#xff1a; 每个线程独占一个计数器&#xff0c;用来指示该线程下一条要执行的指令的地址。这一部分不会导致内存异常。PS&#xff1a;如果一个线程进入的是一般的Java方法&#xff0c;计数器指示的是下一条指令地址&#xff1b;如果…

浅谈“孔乙己”的长衫

书中的孔乙己 孔乙已是鲁迅笔下人物&#xff0c;穷困流倒还穿着象征读书人的长衫&#xff0c;迁腐、麻木。最近&#xff0c;大家自我调佩是“当代孔乙己”&#xff0c;学历成为思想负担&#xff0c;找工作时高不成低不就。 当代的“孔乙己” 如今社会&#xff0c;从小学开始每…

C# 学习abstract

abstract 顾名思义&#xff1a;抽象 从微软官方文档来看&#xff1a;abstract 修饰符指示被修改内容的实现已丢失或不完整。 abstract 修饰符可用于类、方法、属性、索引和事件。 在类声明中使用 abstract 修饰符来指示某个类仅用作其他类的基类&#xff0c;而不用于自行进行…

L4公司进军辅助驾驶,放话无图也能跑遍中国

作者 | Amy 编辑 | 德新 高阶智能驾驶走向规模量产&#xff0c;高精地图成为关键的门槛之一。今年&#xff0c;多家车企和智驾公司都喊出「不依赖高精地图&#xff0c;快速大规模落地」的口号。 华为、小鹏、元戎以及毫末等&#xff0c;可能是最快在国内量产 无高精图智…

服务器的基本概念与初始Ajax

1. 客户端与服务器 1.1 上网的目的 刷微博、看新闻、听歌、看电影。。。 本质目的&#xff1a;通过互联网的形式来获取和消费资源 1.2 服务器 上网过程中&#xff0c;负责存放和对外提供资源的电脑&#xff0c;叫做服务器。 1.3 客户端 上网过程中&#xff0c;负责获取…

nginx(七十三)nginx与Location响应头细节探讨

一 nginx与Location响应头细节探讨 ① 重定向和Location回顾 多种重定向跳转方式的差异 nginx之absolute_redirect、server_name_in_redirect、port_in_redirect 共同控制Location响应头 ② STS响应头导致307重定向 "第一次访问 http://www.baidu.com" 观察…

个人博客系统调试详细过程

系统功能的详细说明和源代码见以下链接:https://blog.csdn.net/shooter7/article/details/121180333相关的源码数据库文件、软件安装包可以联系博主koukou(壹壹23七2五六98) 调试过程如下&#xff1a; 文章目录 调试过程如下&#xff1a;一、数据库安装二、sql数据文件的导入三…

yolov5图像识别voc转yolo代码解析

https://github.com/ultralytics/JSON2YOLO https://blog.csdn.net/qq_51831335/article/details/127237772 目标检测数据集标签转换COCO2VOC、YOLO2VOC、JSON2YOLO <annotation><folder>VOC2007</folder><filename>000001.jpg</filename><s…

【2023/05/06】EDSAC

Hello&#xff01;大家好&#xff0c;我是霜淮子&#xff0c;2023倒计时第1天。 Share 去成为你本该成为的人&#xff0c;任何时候都不会太晚。 &#xff08;出处&#xff1a;乔治艾略特&#xff09; Day1 EDSAC&#xff1a;存储程序式计算机的开山之作。 part1 EDSAC&…

Linux进程状态及优先级

本文已收录至《Linux知识与编程》专栏&#xff01; 作者&#xff1a;ARMCSKGT 演示环境&#xff1a;CentOS 7 进程状态及优先级 前言正文进程状态就绪运行状态R阻塞睡眠状态 S休眠状态D挂起 暂停状态T前台与后台进程待追踪暂停状态t 死亡状态 X僵尸状态 Z 孤儿进程进程优先级查…

《Java虚拟机学习》 java代码的运行过程

1. Java文件转换 当我们保存java文件后&#xff0c;首先由编译器编译成class文件&#xff0c;然后通过Java虚拟机将class文件转换成字节码文件 2.Java虚拟机是怎么运行Java文件 首先将java文件加载到java虚拟机中&#xff0c;然后由虚拟机将类元信息存储在 虚拟机的方法区中。…

华为OD机试 - 各位相加(Java)

一、题目描述 给定一个非负整数 num&#xff0c;反复将各个位上的数字相加&#xff0c;直到结果为一位数。 二、思路与算法 各位相加&#xff0c;使用递归&#xff0c;出口是结果的长度等于1。 三、Java算法源码 public static int addDigits(int num) {recursion(num);re…