当前位置:   article > 正文

Three.js--》实现2D转3D的元素周期表_three.js时间轴

three.js时间轴

今天简单实现一个three.js的小Demo,加强自己对three知识的掌握与学习,只有在项目中才能灵活将所学知识运用起来,话不多说直接开始。

目录

项目搭建

平铺元素周期表

螺旋元素周期表 

网格元素周期表

球状元素周期表

加底部交互按钮


项目搭建

本案例还是借助框架书写three项目,借用vite构建工具搭建vue项目,vite这个构建工具如果有不了解的朋友,可以参考我之前对其讲解的文章:vite脚手架的搭建与使用。搭建完成之后,用编辑器打开该项目,在终端执行 npm i 安装一下依赖,安装完成之后终端在安装 npm i three 即可。

因为我搭建的是vue3项目,为了便于代码的可读性,所以我将three.js代码单独抽离放在一个js文件当中,在views下的index.vue文件中使用该js文件,然后再将index.vue组件引入根组件。具体如下:

  1. <template>
  2. <div ref="canvasDom" id="canvasDom"></div>
  3. </template>
  4. <script setup>
  5. import { reactive, onMounted } from 'vue'
  6. import Base from "../components/scene.js"
  7. let data = reactive({
  8. base3d: {},
  9. })
  10. onMounted(() => {
  11. data.base3d = new Base("#canvasDom")
  12. })
  13. </script>
  14. <style scoped>
  15. #canvasDom {
  16. width: 100%;
  17. height: 100%;
  18. }
  19. </style>

接下来我们重点的three代码就不像之前的项目Demo一样直接写在vue组件中,例子 。这里我们直接将其放在一个js文件中,当然这里也是需要对three代码进行初始化代码处理,如下我们先定义一个基础的class类,将要使用的场景、相机、渲染器和渲染函数先定义起来:

  1. import * as THREE from 'three'
  2. class Base {
  3. constructor(selector) {
  4. this.container = document.querySelector(selector)
  5. this.scene
  6. this.camera
  7. this.renderer
  8. this.init()
  9. this.animate()
  10. }
  11. init() {
  12. this.initScene() // 初始化场景
  13. this.initCamera() // 初始化相机
  14. this.initRenderer() // 初始化渲染器
  15. this.initControl() // 初始化控制器
  16. this.windowSizeChange() // 初始化窗口大小
  17. }
  18. }
  19. export default Base

初始化场景

  1. initScene() { // 初始化场景
  2. this.scene = new THREE.Scene() // 创建场景
  3. }

初始化相机: 

  1. initCamera() {
  2. // 创建透视相机
  3. this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10);
  4. // 设置相机位置
  5. this.camera.position.set(0, 15, 20);
  6. // 将相机添加到场景中
  7. if (this.scene) {
  8. this.scene.add(this.camera);
  9. } else {
  10. console.error("Scene is not initialized!");
  11. }
  12. // 设置相机观察目标并更新相关矩阵
  13. this.camera.lookAt(new THREE.Vector3(0, 0, 0));
  14. this.camera.updateProjectionMatrix();
  15. this.camera.updateMatrixWorld();
  16. }

初始化渲染器

  1. initRenderer() { // 初始化渲染器
  2. this.renderer = new THREE.WebGLRenderer({ antialias: true });
  3. // 设置渲染器尺寸
  4. this.renderer.setPixelRatio(window.devicePixelRatio) // 设置屏幕像素比
  5. this.renderer.setSize(window.innerWidth, window.innerHeight) // 渲染的尺寸大小
  6. this.renderer.toneMapping = THREE.ACESFilmicToneMapping // 色调映射
  7. this.renderer.toneMappingExposure = 2 // 曝光程度
  8. this.container.appendChild(this.renderer.domElement)
  9. }

初始化控制器

  1. initControl() { // 初始化控制器
  2. this.controls = new OrbitControls(this.camera, this.renderer.domElement)
  3. this.controls.enableDamping = true // 启用阻尼或指数衰减的轨道控制
  4. }

初始化窗口大小

  1. windowSizeChange() { // 初始化窗口大小
  2. window.addEventListener("resize", () => {
  3. // 重置渲染器宽高比
  4. this.renderer.setSize(window.innerWidth, window.innerHeight);
  5. // 重置相机宽高比
  6. this.camera.aspect = window.innerWidth / window.innerHeight;
  7. // 更新相机投影矩阵
  8. this.camera.updateProjectionMatrix();
  9. });
  10. }

设置渲染函数

  1. render() { // 渲染函数
  2. this.renderer.render(this.scene, this.camera)
  3. }
  4. animate() { // 动画函数
  5. this.renderer.setAnimationLoop(this.render.bind(this))
  6. }

完整代码如下:

  1. import * as THREE from 'three'
  2. import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
  3. class Base {
  4. constructor(selector) {
  5. this.container = document.querySelector(selector)
  6. this.scene
  7. this.camera
  8. this.renderer
  9. this.init()
  10. this.animate()
  11. }
  12. init() {
  13. this.initScene() // 初始化场景
  14. this.initCamera() // 初始化相机
  15. this.initRenderer() // 初始化渲染器
  16. this.initControl() // 初始化控制器
  17. this.windowSizeChange() // 初始化窗口大小
  18. }
  19. initScene() { // 初始化场景
  20. this.scene = new THREE.Scene() // 创建场景
  21. }
  22. initCamera() {
  23. // 创建透视相机
  24. this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10);
  25. // 设置相机位置
  26. this.camera.position.set(0, 15, 20);
  27. // 将相机添加到场景中
  28. if (this.scene) {
  29. this.scene.add(this.camera);
  30. } else {
  31. console.error("Scene is not initialized!");
  32. }
  33. // 设置相机观察目标并更新相关矩阵
  34. this.camera.lookAt(new THREE.Vector3(0, 0, 0));
  35. this.camera.updateProjectionMatrix();
  36. this.camera.updateMatrixWorld();
  37. }
  38. initRenderer() { // 初始化渲染器
  39. this.renderer = new THREE.WebGLRenderer({ antialias: true });
  40. // 设置渲染器尺寸
  41. this.renderer.setPixelRatio(window.devicePixelRatio) // 设置屏幕像素比
  42. this.renderer.setSize(window.innerWidth, window.innerHeight) // 渲染的尺寸大小
  43. this.renderer.toneMapping = THREE.ACESFilmicToneMapping // 色调映射
  44. this.renderer.toneMappingExposure = 2 // 曝光程度
  45. this.container.appendChild(this.renderer.domElement)
  46. }
  47. initControl() { // 初始化控制器
  48. this.controls = new OrbitControls(this.camera, this.renderer.domElement)
  49. this.controls.enableDamping = true // 启用阻尼或指数衰减的轨道控制
  50. }
  51. windowSizeChange() { // 初始化窗口大小
  52. window.addEventListener("resize", () => {
  53. // 重置渲染器宽高比
  54. this.renderer.setSize(window.innerWidth, window.innerHeight);
  55. // 重置相机宽高比
  56. this.camera.aspect = window.innerWidth / window.innerHeight;
  57. // 更新相机投影矩阵
  58. this.camera.updateProjectionMatrix();
  59. });
  60. }
  61. render() { // 渲染函数
  62. this.renderer.render(this.scene, this.camera)
  63. }
  64. animate() { // 动画函数
  65. this.renderer.setAnimationLoop(this.render.bind(this))
  66. }
  67. }
  68. export default Base

写完之后,最后页面呈现一个黑色的背景说明我们的场景加载成功了:

ok,写完基础代码之后,接下来开始具体的Demo实操。 

平铺元素周期表

本次项目元素周期表并不是使用我们常用的WebGLRenderer渲染器,而是CSS3DRenderer渲染器,两者区别如下,代码中是可以同时存在这两个渲染器的,它们各自负责不同类型的渲染任务。

WebGLRenderer:用于渲染基于 WebGL 的 3D 场景

CSS3DRenderer:用于渲染基于 CSS 的 3D 对象。这种情况通常用于在 Web 页面中同时显示 3D 对象和其他 HTML 元素,例如在 3D 场景中嵌入文字、按钮等。

因为本次项目单纯就使用基于CSS的3D对象,所以我们要对之前的代码进行修改,切换渲染器:

  1. createCSS3DRenderer() { // 创建CSS3D渲染器
  2. this.renderer3D = new CSS3DRenderer();
  3. this.renderer3D.setSize(window.innerWidth, window.innerHeight);
  4. this.renderer3D.domElement.style.backgroundColor = 'black';
  5. this.container.appendChild(this.renderer3D.domElement);
  6. }

接下来将元素周期表的相关数据进行如下的总结,将元素周期表的数据和位置抽离成js文件:

然后接下来在scene.js文件中引入元素周期表.js获取相关数据,进行如下函数创建元素周期表:

  1. createElement() {
  2. for (let i = 0; i < element.length; i+=5) {
  3. // 创建父容器
  4. let parent = document.createElement('div')
  5. parent.style.backgroundColor = `rgba(0, 127, 127, ${Math.random() * 0.5 + 0.25})`
  6. parent.className = 'element-container'
  7. // 设置数字
  8. let number = document.createElement('div')
  9. number.className = 'element-number'
  10. number.textContent = (i / 5) + 1
  11. parent.appendChild(number)
  12. // 设置元素名称
  13. let symbol = document.createElement('div')
  14. symbol.className = 'element-symbol'
  15. symbol.textContent = element[i]
  16. parent.appendChild(symbol)
  17. // 详细信息
  18. let detail = document.createElement('div')
  19. detail.className = 'element-detail'
  20. detail.innerHTML = element[i + 1] + '<br>' + element[i + 2]
  21. parent.appendChild(detail)
  22. // 实例化CSS3D对象
  23. let element3D = new CSS3DObject(parent)
  24. this.objects.push(element3D)
  25. // 加载3D场景
  26. this.scene.add(element3D)
  27. }
  28. }

然后我们在App根组件中删除scoped设置全局css样式,给上面创建的div类名设置样式:

接下来我们开始处理元素周期表的位置样式,将element获取的位置数据进行放大,然后通过页面进行细微的调整:

  1. // 处理元素周期表样式
  2. handleTableStyle() {
  3. for (let i = 0; i < element.length; i+=5) {
  4. // 将第i+3个元素的值赋给objects数组中第i/5个对象的position.x属性
  5. this.objects[i / 5].position.x = element[i + 3] * 140 - 1350
  6. // 将第i+4个元素的值赋给objects数组中第i/5个对象的position.y属性
  7. this.objects[i / 5].position.y = -element[i + 4] * 180 + 1000
  8. }
  9. }

然后根据情况设置相机位置进行细微的调整,使得整个场景处于正中央即可:

  1. // 设置相机位置
  2. this.camera.position.set(0, 15, 2800);

最终呈现的效果如下:

螺旋元素周期表 

根据上面实现的基础上,接下来我们实现将元素周期表的位置进行一个螺旋状的展示,在three中提供了一个3D的函数,这个函数通常用于设置一个三维向量的坐标,其中柱面坐标系由一个半径、一个角度和一个高度组成。这种坐标系通常用于描述圆柱体表面上的点的位置,如下:

具体来说,setFromCylindricalCoords 函数接受柱面坐标系的三个参数:

1)radius:柱面坐标系中的半径。

2)theta:柱面坐标系中的角度,以弧度表示。

3)y:柱面坐标系中的高度。

当需要根据柱面坐标系来定位或者旋转一个对象时,可以使用这个函数来方便地设置该对象的位置或者方向,接下来通过如下代码进行简单的测试一下:

  1. // 螺旋元素周期表
  2. spiralTable() {
  3. for (let i = 0; i < this.objects.length; i++) {
  4. let theta = i
  5. let y = i
  6. this.objects[i].position.setFromCylindricalCoords(900, theta, y)
  7. }
  8. }

呈现的效果如下所示,可见是一圈圆,但我们想实现螺旋式的效果应该这么做,这里需要调整:

接下来对上面螺旋周期表函数进行一些参数的调整,然后设置一些rotation参数:

  1. // 螺旋元素周期表
  2. spiralTable() {
  3. for (let i = 0; i < this.objects.length; i++) {
  4. let theta = i * 0.175
  5. let y = -i * 8 + 450
  6. this.objects[i].position.setFromCylindricalCoords(900, theta, y)
  7. let obj = new THREE.Object3D()
  8. obj.position.copy(this.objects[i].position)
  9. // 改变物体的旋转
  10. obj.lookAt(0, this.objects[i].position.y, 0)
  11. this.objects[i].rotation.x = obj.rotation.x
  12. this.objects[i].rotation.y = obj.rotation.y + Math.PI
  13. this.objects[i].rotation.z = obj.rotation.z
  14. }
  15. }

最终呈现的效果如下,大体效果还是不错的:

网格元素周期表

对于网格处理的函数也很简单,如下该函数的主要逻辑是遍历 this.objects 数组,并为每个元素(即每个物体)计算其在三维空间中的新位置。每个物体在 x、y 和 z 轴上的位置都基于其索引 i 来计算,以达到这种排列效果:

  1. // 网格元素周期表
  2. gridTable() {
  3. for (let i = 0; i < this.objects.length; i++) {
  4. this.objects[i].position.x = (i % 5) * 400 -720
  5. this.objects[i].position.y = Math.floor((i / 5)) % 5 * 400 - 750
  6. this.objects[i].position.z = -Math.floor((i / 25)) * 400
  7. }
  8. }

最终呈现的效果如下:

球状元素周期表

在写球状元素周期表之前,我们先了解一下球概念,如下:

在threejs官网上,也有关于球状相关的api方法,如下:

在一个三维场景中,根据球状元素周期表的规则来排列和旋转一系列的物体。这里根据一定的数学规则(这里使用了反余弦函数和平方根函数)来调整 this.objects 数组中每个物体的位置和旋转模拟一种特殊的排列或动画效果:

  1. // 球状元素周期表
  2. ballTable() {
  3. for (let i = 0; i < this.objects.length; i++) {
  4. const phi = Math.acos( -1 + (2 * i) / this.objects.length); // 方向角
  5. const theta = Math.sqrt(this.objects.length * Math.PI) * phi; // 半径
  6. // 球坐标
  7. this.objects[i].position.setFromSphericalCoords(800, phi, theta)
  8. let obj = new THREE.Object3D()
  9. let obj1 = this.objects[i]
  10. obj.position.copy(obj1.position)
  11. obj.lookAt(0, 0, 0)
  12. obj1.rotation.x = obj.rotation.x
  13. obj1.rotation.y = obj.rotation.y
  14. obj1.rotation.z = obj.rotation.z
  15. obj1.rotateOnAxis(new THREE.Vector3(0, 1, 0), Math.PI)
  16. }
  17. }

最终呈现的效果如下:

加底部交互按钮

接下来我们实现点击底部的按钮进行不同的场景切换,如下:

  1. <template>
  2. <!-- 场景 -->
  3. <div id="canvasDom"></div>
  4. <!-- 按钮 -->
  5. <div class="menu">
  6. <button v-for="(btn, index) in buttons" :key="index" @click="handleButtonClick(btn.key)" :class="{ active: data.activeBtn === btn.key }">{{ btn.text }}</button>
  7. </div>
  8. </template>
  9. <script setup>
  10. import { reactive, onMounted } from 'vue'
  11. import Base from "../components/scene.js"
  12. let data = reactive({
  13. base3d: {},
  14. activeBtn: 'tile'
  15. })
  16. const buttons = [
  17. { key: 'tile', text: '平铺' },
  18. { key: 'spiral', text: '螺旋' },
  19. { key: 'grid', text: '网格' },
  20. { key: 'ball', text: '球状' }
  21. ]
  22. const handleButtonClick = (btnKey) => {
  23. switch (btnKey) {
  24. case 'tile':
  25. data.base3d.handleTableStyle()
  26. break;
  27. case 'spiral':
  28. data.base3d.spiralTable()
  29. break;
  30. case 'grid':
  31. data.base3d.gridTable()
  32. break;
  33. case 'ball':
  34. data.base3d.ballTable()
  35. break;
  36. default:
  37. break;
  38. }
  39. data.activeBtn = btnKey
  40. }
  41. onMounted(() => {
  42. data.base3d = new Base("#canvasDom")
  43. // 默认选中第一个按钮
  44. handleButtonClick('tile')
  45. })
  46. </script>

效果如下所示:

接下来我们设置其点击后样式:

  1. <style scoped lang="scss">
  2. #canvasDom {
  3. width: 100%;
  4. height: 100%;
  5. }
  6. .menu {
  7. position: absolute;
  8. z-index: 1000;
  9. bottom: 20px;
  10. text-align: center;
  11. width: 100%;
  12. button {
  13. color: rgba(127, 255, 255, 0.75);
  14. background: transparent;
  15. outline: 1px solid rgba(127, 255, 255, 0.75);
  16. padding: 10px 30px;
  17. margin: 0 10px;
  18. cursor: pointer;
  19. &:hover {
  20. background-color: rgba(0, 255, 255, 0.5);
  21. }
  22. &.active {
  23. background-color: rgba(0, 255, 255, 0.6);
  24. }
  25. }
  26. }
  27. </style>

最终呈现的效果如下:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/459592
推荐阅读
相关标签
  

闽ICP备14008679号