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

news2024/11/17 9:47:57

今天我们使用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.innerHeight
  camera.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() {
    // 遍历  创建CSS3DSprite
    for(let i = 0; i < particlesTotal; i++ ) {
      const object = new CSS3DSprite( image.cloneNode())
      object.position.x = Math.random() * 4000 - 2000
      object.position.y = Math.random() * 4000 - 2000
      object.position.z = Math.random() * 4000 - 2000
      scene.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数组中

// Plane
  const 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) * separationPlane
    const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200 
    positions.push(x-offsetX,y,z-offsetZ) //每个小球的坐标
  }

定义立方体

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

// Cube
  const amount = 8 //数量
  const separationCube = 150  //间隔
  const offset = ((amount - 1 ) * separationCube ) /2 //长度
  for(let i = 0; i < particlesTotal; i ++ ) {
    const x = (i % amount) * separationCube
    const 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数组中

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

定义圆形

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

// Sphere
  const 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() {
    // 遍历  创建CSS3DSprite
    for(let i = 0; i < particlesTotal; i++ ) {
      const object = new CSS3DSprite( image.cloneNode())
      object.position.x = Math.random() * 4000 - 2000
      object.position.y = Math.random() * 4000 - 2000
      object.position.z = Math.random() * 4000 - 2000
      scene.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.innerHeight
  camera.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() {
    // 遍历  创建CSS3DSprite
    for(let i = 0; i < particlesTotal; i++ ) {
      const object = new CSS3DSprite( image.cloneNode())
      object.position.x = Math.random() * 4000 - 2000
      object.position.y = Math.random() * 4000 - 2000
      object.position.z = Math.random() * 4000 - 2000
      scene.add(object)
      objects.push(object)
    }
    transition() 
  })
  

  // Plane
  const 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) * separationPlane
    const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200

    positions.push(x-offsetX,y,z-offsetZ) //每个小球的坐标
  }

  // Cube
  const amount = 8 //数量
  const separationCube = 150  //间隔
  const offset = ((amount - 1 ) * separationCube ) /2 //偏移量
  for(let i = 0; i < particlesTotal; i ++ ) {
    const x = (i % amount) * separationCube
    const y = Math.floor( ( i / amount ) % amount ) * separationCube;
		const z = Math.floor( i / ( amount * amount ) ) * separationCube;

		positions.push( x - offset, y - offset, z - offset );
  }

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

  // Sphere
  const 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.coloradmin.cn/o/496816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux——进程间通信(管道)

目录 进程通信的目的 管道 见见猪跑(举个例子) 文件描述符fd与管道的关系(深度理解管道) 什么是管道&#xff1f; 匿名管道 pipe函数概述 父子进程通信时与文件描述符的关系图(理解pipe函数的关键) pipe函数的使用 管道读写规则 管道的大小 自测 使用man 7 pipe查看 …

Unity Timeline使用

Unity Timeline使用 1.创建Timeline&#xff1a;打开面板Window->Sequencing->Timeline (1.1)选择一个要添加 Timeline 的物体&#xff0c;我创建一个物体就叫 Timeline(可以随意命名)&#xff0c;选择Timeline&#xff0c;然后在面板上显示 Create 按钮&#xff0c;如…

Arduino_STM32 之Arduino IDE开发配置

前言 由于选了物联网作为选修课&#xff0c;老师喜欢使用Arduino进行编程&#xff0c;但是也要教我们使用STM32。于是他就让我们使用Arduino IDE开发STM32&#xff08;用Keil 不好吗&#xff1f;&#xff1f;&#xff1f;&#xff09;。 第一章 软件下载 安装Arduino IDE&…

springboot请求响应

SpringBootWeb请求响应 前言 在上一次的课程中&#xff0c;我们开发了springbootweb的入门程序。 基于SpringBoot的方式开发一个web应用&#xff0c;浏览器发起请求 /hello 后 &#xff0c;给浏览器返回字符串 “Hello World ~”。 其实呢&#xff0c;是我们在浏览器发起请求…

环形链表 力扣

题目描述 题目要求 判断一个单链表是不是环形链表&#xff0c;是就返回true 不是就返回false 思路 要搞清楚环形链表长啥样环形链表有哪些特征 环形链表顾名思义就是在链表中有一个类似环形的结构&#xff0c; 它和普通单链表的区别就是 你用遍历普通单链表的法子遍历一个环…

k8s基础5——Pod常用命令、资源共享机制、重启策略和健康检查、环境变量、初始化容器、静态pod

文章目录 一、基本了解二、管理命令三、yaml文件参数大全四、创建pod的工作流程五、资源共享机制5.1 共享网络5.2 共享存储 六、生命周期重启策略健康检查七、环境变量八、Init Containe初始化容器九、静态Pod 一、基本了解 概念&#xff1a; Pod是一个逻辑抽象概念&#xff0c…

Vben Admin 自学记录 —— Table组件的基本使用及练习(持续更新中...)

Table 表格 对 antv 的 table 组件进行封装 table相关使用及概念 练习 —— 画一个简单的包含增删改查的表格静态页面&#xff08;不包含相关逻辑和处理&#xff09; 之前相关记录&#xff1a; Vben Admin 自学记录 —— 介绍及使用 1.在之前添加的新路由模块中添加一个表…

TCP/IP网络编程(三)

TCP/IP网络编程读书笔记 第14章 多播与广播14.1 多播14.1.1 多播的数据传输方式及流量方面的优点14.1.2 路由&#xff08;Routing&#xff09;和 TTL&#xff08;Time to Live&#xff0c;生存时间&#xff09;&#xff0c;以及加入组的办法14.1.3 实现多播 Sender 和 Receiver…

使用mybatisX逆向生成数据表实体类(pojo,dao),mapper,service

先看使用mybatisX后生成的文件。 1.先在idea安装mybatisX插件&#xff0c;在file->setting->plugins&#xff0c;搜索mybatisX插件&#xff0c;重新启动idea即可。 2.在idea编辑器右侧点击Database&#xff0c;点击“”链接你的数据库类型&#xff0c;这里我选mysql。 输…

Vue核心 列表渲染 数据监视

1.13.列表渲染 1.13.1.基本列表 v-for指令 用于展示列表数据语法&#xff1a;&#xff0c;这里key可以是index&#xff0c;更好的是遍历对象的唯一标识可遍历&#xff1a;数组、对象、字符串&#xff08;用的少&#xff09;、指定次数&#xff08;用的少&#xff09; <!…

尚硅谷大数据技术NiFi教程-笔记01【NiFi(基本概念、安装、使用)】

视频地址&#xff1a;尚硅谷大数据NiFi教程&#xff08;从部署到开发&#xff09;_哔哩哔哩_bilibili 尚硅谷大数据技术NiFi教程-笔记01【NiFi&#xff08;基本概念、安装、使用&#xff09;】尚硅谷大数据技术NiFi教程-笔记02【NiFi&#xff08;使用案例&#xff0c;同步文件、…

Kafka 权威指南

Kafka 权威指南 这本书于 2021 年看完&#xff0c;2022 年又看了一遍&#xff0c;感觉书读百遍&#xff0c;其义自现。 这本书侧重于 Kafka 的理论知识&#xff0c;虽然书有点老&#xff0c;但是其中关于 Kafka 的基础知识的章节讲得确实不错&#xff0c;适合学习 Kafka 的新手…

深入篇【C++】类与对象:运算符重载详解 -上

深入篇【C】类与对象&#xff1a;运算符重载详解 -上 ⏰.运算符重载&#x1f553;Ⅰ.<运算符重载&#x1f550;Ⅱ.>运算符重载&#x1f552;Ⅲ.运算符重载&#x1f551;Ⅳ.运算符重载①.格式1.改进12.改进2 ②.默认成员函数1.功能2.不足 ⏰.运算符重载 内置类型(int /do…

二分法相关使用

文章目录 1. 在一个有序数组中,找某个数是否存在2. 在一个有序数组中,找大于等于某个数最左侧的位置3. 在一个有序数组中, 找小于等于某个数最右侧的位置4. 局部最大值问题 1. 在一个有序数组中,找某个数是否存在 在线OJ&#xff1a;704. 二分查找 有序数组下的二分思路如下:…

亚马逊:分布式计算宣言

文章目录 分布式计算宣言背景关键概念基于服务的模型基于工作流的模型和数据域应用概念跟踪状态变化对进行中的工作流程元素进行更改工作流程和 DC 客户订单处理 分布式计算宣言 创建时间&#xff1a; 1998 年 5 月 24 日 修订日期&#xff1a; 1998 年 7 月 10 日 背景 很…

uni-app nvue页面中使用video视频播放组件

我遇到的问题是&#xff0c;在nvue页面引用video组件&#xff0c;然后啥也没显示的&#xff0c;显示了无法控制播放&#xff0c;折腾了好久&#xff0c;在这里记录下来&#xff01;希望可以帮助到需要的人 我的代码是这样的&#xff08;src换成官方的举例&#xff09; <vi…

查看mysql数据库版本的方式(cmd命令和navicat)

目录 一、使用场景 二、查看方式 &#xff08;一&#xff09;cmd命令方式 &#xff08;二&#xff09;navicat16软件里面查看 &#xff08;三&#xff09;navicat试用版本查看的方式 一、使用场景 在有些时候需要调试系统的时候&#xff0c;就要看数据库的版本&#xff…

【致敬未来的攻城狮计划】— 连续打卡第二十三天:RA2E1的存储器基础知识

系列文章目录 1.连续打卡第一天&#xff1a;提前对CPK_RA2E1是瑞萨RA系列开发板的初体验&#xff0c;了解一下 2.开发环境的选择和调试&#xff08;从零开始&#xff0c;加油&#xff09; 3.欲速则不达&#xff0c;今天是对RA2E1 基础知识的补充学习。 4.e2 studio 使用教程 5.…

【MySQL】数据库基础操作一:建库与建表

目录 &#x1f31f;前言 &#x1f308;1、常见的关系型数据库 &#x1f31f;数据库的基本操作 &#x1f308;1、常用数据库的操作 &#x1f308;2、常用的数据类型 &#x1f308;3、表的基本操作 &#x1f345;创建表的一个小练习 &#x1f31f;前言 &#x1f…

基于Python的特征工程:数据预处理(一)

一、概述 特征工程是机器学习工作流程中不可或缺的一环&#xff0c;它将原始数据转化为模型可理解的形式。数据和特征的质量决定了机器学习的上限&#xff0c;而模型和算法则是逼近这个上限的手段。因此&#xff0c;特征工程的重要性不言而喻。其主要工作涉及特征的采集、预处…