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

2024-04-09 19:12

本文主要是介绍Three.js--》实现2D转3D的元素周期表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

目录

项目搭建

平铺元素周期表

螺旋元素周期表 

网格元素周期表

球状元素周期表

加底部交互按钮


项目搭建

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

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

<template><div ref="canvasDom" id="canvasDom"></div>
</template><script setup>
import { reactive, onMounted } from 'vue'
import Base from "../components/scene.js"
let data = reactive({base3d: {},
})
onMounted(() => {data.base3d = new Base("#canvasDom")
})
</script><style scoped>
#canvasDom {width: 100%;height: 100%;
}
</style>

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

import * as THREE from 'three'class Base {constructor(selector) {this.container = document.querySelector(selector)this.scene      this.camerathis.rendererthis.init()this.animate()}init() {this.initScene() // 初始化场景this.initCamera() // 初始化相机this.initRenderer() // 初始化渲染器this.initControl() // 初始化控制器this.windowSizeChange() // 初始化窗口大小}
}
export default Base

初始化场景

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

初始化相机: 

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

初始化渲染器

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

初始化控制器

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

初始化窗口大小

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

设置渲染函数

render() { // 渲染函数this.renderer.render(this.scene, this.camera)
}
animate() { // 动画函数this.renderer.setAnimationLoop(this.render.bind(this))
}

完整代码如下:

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'class Base {constructor(selector) {this.container = document.querySelector(selector)this.scene      this.camerathis.rendererthis.init()this.animate()}init() {this.initScene() // 初始化场景this.initCamera() // 初始化相机this.initRenderer() // 初始化渲染器this.initControl() // 初始化控制器this.windowSizeChange() // 初始化窗口大小}initScene() { // 初始化场景this.scene = new THREE.Scene() // 创建场景}initCamera() {// 创建透视相机this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10);// 设置相机位置this.camera.position.set(0, 15, 20);// 将相机添加到场景中if (this.scene) {this.scene.add(this.camera);} else {console.error("Scene is not initialized!");}// 设置相机观察目标并更新相关矩阵this.camera.lookAt(new THREE.Vector3(0, 0, 0));this.camera.updateProjectionMatrix();this.camera.updateMatrixWorld();}     initRenderer() { // 初始化渲染器this.renderer = new THREE.WebGLRenderer({ antialias: true });// 设置渲染器尺寸this.renderer.setPixelRatio(window.devicePixelRatio) // 设置屏幕像素比this.renderer.setSize(window.innerWidth, window.innerHeight) // 渲染的尺寸大小this.renderer.toneMapping = THREE.ACESFilmicToneMapping // 色调映射this.renderer.toneMappingExposure = 2 // 曝光程度this.container.appendChild(this.renderer.domElement)}  initControl() { // 初始化控制器this.controls = new OrbitControls(this.camera, this.renderer.domElement)this.controls.enableDamping = true // 启用阻尼或指数衰减的轨道控制}windowSizeChange() { // 初始化窗口大小window.addEventListener("resize", () => {// 重置渲染器宽高比this.renderer.setSize(window.innerWidth, window.innerHeight);// 重置相机宽高比this.camera.aspect = window.innerWidth / window.innerHeight;// 更新相机投影矩阵this.camera.updateProjectionMatrix();});}render() { // 渲染函数this.renderer.render(this.scene, this.camera)}animate() { // 动画函数this.renderer.setAnimationLoop(this.render.bind(this))}
}
export default Base

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

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

平铺元素周期表

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

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

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

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

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

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

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

createElement() {for (let i = 0; i < element.length; i+=5) {// 创建父容器let parent = document.createElement('div')parent.style.backgroundColor = `rgba(0, 127, 127, ${Math.random() * 0.5 + 0.25})`parent.className = 'element-container'// 设置数字let number = document.createElement('div')number.className = 'element-number'number.textContent = (i / 5) + 1parent.appendChild(number)// 设置元素名称let symbol = document.createElement('div')symbol.className = 'element-symbol'symbol.textContent = element[i]parent.appendChild(symbol)// 详细信息let detail = document.createElement('div')detail.className = 'element-detail'detail.innerHTML = element[i + 1] + '<br>' + element[i + 2]parent.appendChild(detail)// 实例化CSS3D对象let element3D = new CSS3DObject(parent)this.objects.push(element3D)// 加载3D场景this.scene.add(element3D)}
}

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

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

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

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

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

最终呈现的效果如下:

螺旋元素周期表 

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

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

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

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

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

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

// 螺旋元素周期表
spiralTable() {for (let i = 0; i < this.objects.length; i++) {let theta = ilet y = ithis.objects[i].position.setFromCylindricalCoords(900, theta, y)}   
}    

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

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

// 螺旋元素周期表
spiralTable() {for (let i = 0; i < this.objects.length; i++) {let theta = i * 0.175let y = -i * 8 + 450this.objects[i].position.setFromCylindricalCoords(900, theta, y)let obj = new THREE.Object3D()obj.position.copy(this.objects[i].position)// 改变物体的旋转obj.lookAt(0, this.objects[i].position.y, 0)this.objects[i].rotation.x = obj.rotation.xthis.objects[i].rotation.y = obj.rotation.y + Math.PIthis.objects[i].rotation.z = obj.rotation.z}   
}  

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

网格元素周期表

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

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

最终呈现的效果如下:

球状元素周期表

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

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

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

// 球状元素周期表
ballTable() {for (let i = 0; i < this.objects.length; i++) {const phi = Math.acos( -1 + (2 * i) / this.objects.length); // 方向角const theta = Math.sqrt(this.objects.length * Math.PI) * phi; // 半径// 球坐标this.objects[i].position.setFromSphericalCoords(800, phi, theta)let obj = new THREE.Object3D()let obj1 = this.objects[i]obj.position.copy(obj1.position)obj.lookAt(0, 0, 0)obj1.rotation.x = obj.rotation.xobj1.rotation.y = obj.rotation.yobj1.rotation.z = obj.rotation.zobj1.rotateOnAxis(new THREE.Vector3(0, 1, 0), Math.PI)}
}

最终呈现的效果如下:

加底部交互按钮

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

<template><!-- 场景 --><div id="canvasDom"></div><!-- 按钮 --><div class="menu"><button v-for="(btn, index) in buttons" :key="index" @click="handleButtonClick(btn.key)" :class="{ active: data.activeBtn === btn.key }">{{ btn.text }}</button></div>
</template><script setup>
import { reactive, onMounted } from 'vue'
import Base from "../components/scene.js"let data = reactive({base3d: {},activeBtn: 'tile'
})const buttons = [{ key: 'tile', text: '平铺' },{ key: 'spiral', text: '螺旋' },{ key: 'grid', text: '网格' },{ key: 'ball', text: '球状' }
]const handleButtonClick = (btnKey) => {switch (btnKey) {case 'tile':data.base3d.handleTableStyle()break;case 'spiral':data.base3d.spiralTable()break;case 'grid':data.base3d.gridTable()break;case 'ball':data.base3d.ballTable()break;default:break;}data.activeBtn = btnKey
}onMounted(() => {data.base3d = new Base("#canvasDom")// 默认选中第一个按钮handleButtonClick('tile')
})
</script>

效果如下所示:

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

<style scoped lang="scss">
#canvasDom {width: 100%;height: 100%;
}
.menu {position: absolute;z-index: 1000;bottom: 20px;text-align: center;width: 100%;button {color: rgba(127, 255, 255, 0.75);background: transparent;outline: 1px solid rgba(127, 255, 255, 0.75);padding: 10px 30px;margin: 0 10px;cursor: pointer;&:hover {background-color: rgba(0, 255, 255, 0.5);}&.active {background-color: rgba(0, 255, 255, 0.6);}}
}
</style>

最终呈现的效果如下:

这篇关于Three.js--》实现2D转3D的元素周期表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/888987

相关文章

JS常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P