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.camera
        this.renderer
        this.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.camera
        this.renderer
        this.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) + 1
        parent.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 = i
        let y = i
        this.objects[i].position.setFromCylindricalCoords(900, theta, y)
    }   
}    

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

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

// 螺旋元素周期表
spiralTable() {
    for (let i = 0; i < this.objects.length; i++) {
        let theta = i * 0.175
        let y = -i * 8 + 450
        this.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.x
        this.objects[i].rotation.y = obj.rotation.y + Math.PI
        this.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 -720
        this.objects[i].position.y = Math.floor((i / 5)) % 5 * 400 - 750
        this.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.x
        obj1.rotation.y = obj.rotation.y
        obj1.rotation.z = obj.rotation.z
        obj1.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>

最终呈现的效果如下:

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

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

相关文章

OpenHarmony实战:瑞芯微RK3568移植案例

本文章是基于瑞芯微RK3568芯片的DAYU200开发板&#xff0c;进行标准系统相关功能的移植&#xff0c;主要包括产品配置添加&#xff0c;内核启动、升级&#xff0c;音频ADM化&#xff0c;Camera&#xff0c;TP&#xff0c;LCD&#xff0c;WIFI&#xff0c;BT&#xff0c;vibrato…

每日一题(力扣)---插入区间

官方网址&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 给你一个 无重叠的 &#xff0c;按照区间起始端点排序的区间列表 intervals&#xff0c;其中 intervals[i] [starti, endi] 表示第 i 个区间的开始和结束&#xff0c;并且 intervals按照 st…

【鸿蒙开发】@State装饰器:组件内状态

1. 概述 State装饰的变量&#xff0c;与声明式范式中的其他被装饰变量一样&#xff0c;是私有的&#xff0c;只能从组件内部访问&#xff0c;在声明时必须指定其类型和本地初始化。 2. 装饰器使用规则说明 State变量装饰器 说明 装饰器参数 无 同步类型 不与父组件中任何…

蚁群(ACO)算法简介

蚁群&#xff08;ACO&#xff09;算法简介 前言一、 ACO简介1. 起源2. 思想3. 基本概念3.1 并行3.2 禁忌表3.3 启发式信息 4. 流程 前言 生活中我们总能看到一群蚂蚁按照一条非常有规律的路线搬运食物回到巢穴&#xff0c;而且每只蚂蚁的路线都是近似相同且较优的&#xff0c;…

7-11 分段计算居民水费

题目链接&#xff1a;7-11 分段计算居民水费 一. 题目 1. 题目 2. 输入输出格式 3. 输入输出样例 4. 限制 二、代码 1. 代码实现 #include <stdio.h>static float money (unsigned int water) {if (water < 15) { // 不超过15吨时if (water) { // 不为0return wat…

教师必备工具?了解一下国产电路仿真软件的教学应用吧!

在当今信息化时代&#xff0c;科技的不断发展为教学提供了更多可能性。特别是在电子工程领域&#xff0c;教师们需要寻找更有效的方式来教授复杂的电路知识。在这个背景下&#xff0c;国产电路仿真软件的教学应用成为了备受关注的话题。本文将探讨教师使用电路仿真软件的必要性…

【MySQL学习】MySQL的慢查询日志和错误日志

꒰˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN …

SpringCloudAlibaba基础使用(2024最全、最新)

一、简介二、服务注册配置Nacos2.1 下载启动2.2 服务注册2.3 服务配置2.3.1 NameSpace-GroupID-DataId 三、熔断限流 Sentinel3.1 介绍3.2 下载安装3.3 如何使用3.3.1 流控规则流控模式流控效果 3.3.2 熔断规则慢调用比例异常比例异常数 3.3.3 SentinelResource3.3.4 热点规则3…

铁山靠之数学建模-基础篇

小黑子的数模基础篇 一、什么是数学建模1.1 数学模型分类1.2 备战准备什么1.3 组队学习路线1.4 赛前准备1.5 赛题选择1.5.1 赛题类型1.5.2 ABC赛题建议 1.6 学会查询1.6.1 百度搜索技巧1.6.2 查文献1.6.3 数据预处理 1.7 建模全过程 二、数模论文2.1 论文排版2.2 标题怎么写2.3…

UE4 避免布料模拟重置后抖动

问题&#xff1a;每次设置带布料模拟的布料新位置&#xff0c;就会发生突然的抖动 解决办法&#xff1a;给“布料混合权重”或“布料最大距离缩放”K帧&#xff0c;参考数值为0.2—1&#xff08;红框内的值都试过无法解决&#xff09;

Swift 异步序列 AsyncStream 新“玩法”以及内存泄漏、死循环那些事儿(上)

概览 异步序列&#xff08;Async Sequence&#xff09;是 Swift 5.5 新并发模型中的一员“悍将”&#xff0c;系统标准库中很多类都做了重构以支持异步序列。我们还可以用 AsyncStream 辅助结构非常方便的创建自己的异步序列。 这里我们就来一起聊聊 AsyncStream 结构&#xf…

领鸡蛋游戏养鸡游戏淘宝客源码广告联盟

用户中心 用户信息&#xff1a;显示用户名、头像、鸡蛋数量、足迹等基本信息。我的足迹&#xff1a;展示用户的饲料获取记录明细&#xff0c;包括来源、数量和时间。我的好友&#xff1a;展示邀请的好友列表&#xff0c;支持好友间互动&#xff0c;如串门、偷取/赠送饲料&#…

贪心算法|135.分发糖果

力扣题目链接 class Solution { public:int candy(vector<int>& ratings) {vector<int> candyVec(ratings.size(), 1);// 从前向后for (int i 1; i < ratings.size(); i) {if (ratings[i] > ratings[i - 1]) candyVec[i] candyVec[i - 1] 1;}// 从后…

【科东软件】鸿道Intewell-Lin_V2.2.0 软件版本发布

鸿道操作系统 Intewell-Lin_V2.2.0 软件版本发布 Intewell-Lin_V2.2.0 版本号&#xff1a;V2.2.0 版本或修改说明 增加功能&#xff1a; 1、增加T3板级支持 支持硬件列表

AI绘画工具的兴起与应用:热门AI绘画生成器推荐及使用指南

文章目录 一、AI绘画工具概述二、热门AI绘画生成器推荐2.1、DALL-E22.2、DeepDreamGenerator2.3、Artbreeder2.4、BigSleep2.5、NightCafe2.6、DeepAI2.7、触站AI2.8、美术加AI2.9、文心一格 三、如何使用AI绘画生成器3.1、选择AI绘画生成器3.2、描述画面内容3.3、选择绘画风格…

【Spring Cloud】服务容错中间件Sentinel入门

文章目录 什么是 SentinelSentinel 具有以下特征&#xff1a;Sentinel分为两个部分: 安装 Sentinel 控制台下载jar包&#xff0c;解压到文件夹启动控制台访问了解控制台的使用原理 微服务集成 Sentinel添加依赖增加配置测试用例编写启动程序 实现接口限流总结 欢迎来到阿Q社区 …

为什么 C/C++ 的库很喜欢缩写?

一、正如很多回答已经提到的&#xff0c;早期的有效标识符长度有限制&#xff0c;所以缩写用得比较多。也主要是在 C 里&#xff08;Unix 的传统&#xff09;。C 里的标识符用缩写的不多。如 C98&#xff08;毕竟比 C89 晚了 9 年么&#xff09;里我们就已经有了很多挺长的名字…

Java面试必问题29:MySQL篇(重点必问)

数据库的ACID特性 原子性&#xff08;Atomicity&#xff09;&#xff1a;事务中的操作要么全部成功&#xff0c;要么全部失败。事务是一个不可分割的单元&#xff0c;要么全部执行&#xff0c;要么全部回滚。如果事务中的任何操作失败&#xff0c;所有操作都将被回滚到事务开始…

​5种常用于LLM的令牌遮蔽技术介绍以及Pytorch的实现

本文将介绍大语言模型中使用的不同令牌遮蔽技术&#xff0c;并比较它们的优点&#xff0c;以及使用Pytorch实现以了解它们的底层工作原理。 令牌掩码Token Masking是一种广泛应用于语言模型分类变体和生成模型训练的策略。BERT语言模型首先使用&#xff0c;并被用于许多变体(R…

python|enumerate

enumerate可以用来列举可遍历的对象 # 假设我们有一个列表 fruits [apple, banana, cherry] # 使用enumerate()函数遍历列表 for index, fruit in enumerate(fruits): print(f"Index: {index}, Fruit: {fruit}")# 假设我们想要从1开始计数 for index, fru…