ThreeJS —— 机房Demo(二)

  • 目录结构
  • 模拟一条管线
    • 创建TubeGeometry几何体
    • 为管线添加动画
    • 自定义管线运动部分的长度
  • 创建一个平面
    • 创建PlaneBufferGeometry几何体
    • 创建一个自定义形状的平面
  • 创建分组Group
  • 重构代码

Three世界中的物体都是大部分都是由一个个几何体Geometry构成的,上一节我们在场景中加入了一些几何体模拟机器,并渲染出来了画面,这一节我们将加入一些新的几何体,来模拟线路等

目录结构

├── font // 字体文件
 |├──── font.ttf // 字体源文件
 |└──── font.json // 转换后的字体文件
├── img // 素材图片
 |├──── xx.png
 |├──── xxx.jpg
 |└──── …
├── js // 自己编写的js文件
 |├──── composer_fn.js // 后期处理
 |├──── create_fn.js // 创建各种几何
 |├──── init_fn.js // 初始化项目
 |└──── util_fn.js // 工具函数
├── lib // 需要引入的js文件
 |├──── three.js
 |├──── OrbitControls.js
 |├──── RenderPass.js
 |└──── …
├── model // 建模工具导出的模型
 |├──── computer.gltf
 |└──── …
└── index.html // 入口文件

模拟一条管线

机房中必不可少的当然是一条条类似管道的线路了,里面在不停的传输着数据源,那么这次我们将模拟出一条管线来

创建TubeGeometry几何体

首先我们利用TubeGeometry创建一条管道,第一个参数传入自定义好的路径

// create_fn.js
// 传入一组三维坐标点,例如:([-15, -5, 15], [-15, -5, -40], [40, -5, -40]),按照这组点形成一条路径,在此路径基础上创建管道
function createTube(...pointsArr) {const path = createPath(pointsArr); // createPath是我们编写的创建路径的函数,详细如下const geometry = new THREE.TubeGeometry(path, 64, 0.3); // 第一个参数为路径,必须为Curve类,第二个参数为分段值(可理解为细粒度),第三个参数为管道横截面半径// curve是基类,表示曲线,子类有lineCurve二维直线,lineCurve3三维直线// curvePath是一组curve构成的路径,可以算是curve的子类,curvePath的子类path二维路径,shape是path的子类,所以第一个参数可以传入curvePathconst material = new THREE.MeshBasicMaterial({ color: "#00ffff" });const mesh = new THREE.Mesh(geometry, material);return mesh;
}// 创建一条路径,可以是三维或二维路径,传入一组点,例如:[[-15, -5, 15], [-15, -5, -40], [40, -5, -40]]
function createPath(pointsArr) {pointsArr = pointsArr.map((point) => new THREE.Vector3(...point)); // 将参数数组转换成点数组的形式// 方法一:自定义三维路径 curvePathconst path = new THREE.CurvePath();for (let i = 0; i < pointsArr.length - 1; i++) {const lineCurve = new THREE.LineCurve3(pointsArr[i], pointsArr[i + 1]); // 每两个点之间形成一条三维直线path.curves.push(lineCurve); // curvePath有一个curves属性,里面存放组成该三维路径的各个子路径}// 方法二:利用CatmullRomCurve3创建三维路径,不过CatmullRomCurve3是平滑的三维样条曲线// const path = new THREE.CatmullRomCurve3(pointsArr);return path;
}

效果图:

到这里,管道已经创建完毕了,不过只有一条管道并不能很好的模拟除管线的效果,因为缺少了很重要的一个元素——“动画”

为管线添加动画

管道实现动画原理:

  1. 对管道进行贴图,图片由两种相近的颜色组成,较亮的颜色可以模拟正在传输的数据元
  2. 在animate动画中,动态的改变贴图的偏移量offset,产生运动效果

运动素材贴图 tube.jpg :

创建管线:

// create_fn.js
async function createTube(...pointsArr) {const path = createPath(pointsArr);const geometry = new THREE.TubeGeometry(path, 64, 0.3);// 模拟管线运动动画的贴图textureconst texture = new THREE.TextureLoader().load('../img/tube.jpg');texture.wrapS = THREE.RepeatWrapping; // 设置x方向能够重复,这样才可以设置texture的偏移量offsettexture.repeat.x = 1; // 设置x方向的重复数为1,也可设置为2,这样产生的动画效果代表管道内同时有两端数据元在传输const material = new THREE.MeshBasicMaterial({map: texture,transparent: true,});const mesh = new THREE.Mesh(geometry, material);return { texture, mesh };
}

给管线添加动画:

<!DOCTYPE html>
<html><head>...</head><body><div id="canvas-frame"></div><!-- 引入的一些JS --><script src="lib/three.js"></script><script src="..."></script> <script>// ...// 新添加的代码const { texture, mesh } = await createTube([-15, -5, 15], [-15, -5, -40], [40, -5, -40]); // 获取到管线mesh,以及管线贴图texturescene.add(mesh); // 将管线添加到场景中function animate(time) {// ...texture.offset.x -= 0.022; // 每次让贴图的x偏移量减少0.022,以产生动画效果renderer.render(scene, camera);requestAnimationFrame(animate);}animate();</script></body>
</html>

自定义管线运动部分的长度

上面虽然实现了管线的动画效果,但是由于贴图素材中,运动部分和不运动部分的比例是固定的,导致如果管线很长的话,运动部分所占的长度也会相应变长,影响美观(例如:运动部分和不运动部分原本比例1:4,如果是10m长的管道,那么运动部分占了2m,但如果是100m长的管道,则运动部分就占了20m,显然过长),所以需要自定义运动部分所占的比例

实现原理:

  1. 首先准备两张长宽相同的素材图片,一张用作运动部分,一张用作不运动部分
  2. 根据传入的比例,在canvas中将两张图片按照比例合并成一张图片
  3. 将合成后的图片作为管线的贴图

首先我们提前准备好一个函数 mergeImage,用来将两张图片按比例合并成为一张

// util_fn.js
function mergeImage(imgSrc1, imgSrc2, a, b) {return new Promise((res, rej) => {const canvas = document.createElement("canvas"); // 创建canvasconst ctx = canvas.getContext("2d");const img1 = new Image();img1.src = imgSrc1;img1.onload = function () {const img2 = new Image();img2.src = imgSrc2;img2.onload = function () {// 等两张图片都加载完毕后canvas.width = img1.width * a + img2.width * b; // 按两张图片的比例设置画布的大小canvas.height = img1.height;ctx.rect(0, 0, canvas.width, canvas.height);ctx.fillStyle = "#fff";ctx.fill();let width = 0;// 绘制img1for (let i = 0; i < a; i++) {ctx.drawImage(img1, width, 0, img1.width, img1.height);width += img1.width;}// 绘制img2for (let i = 0; i < b; i++) {ctx.drawImage(img2, width, 0, img2.width, img2.height);width += img2.width;}// 合并const base64 = canvas.toDataURL("image/png"); // "image/png" 这里注意一下res(base64); // 将得到的图片的base64传出去};};});
}

生成管道

// create_fn.js
async function createTube(...pointsArr) {const path = createPath(pointsArr);const geometry = new THREE.TubeGeometry(path, 64, 0.3);// 模拟管线运动动画,将两个素材图按比例合并,然后生成贴图texture,这里比例为1:7const base64 = await mergeImage("../img/2.png", "../img/1.png", 1, 7);const texture = createTexture(base64, { repeat: { x: 1 } }); // textureLoader支持加载Data URI,这里的createTexture为上一节我们封装的函数const material = new THREE.MeshBasicMaterial({map: texture,transparent: true,});const mesh = new THREE.Mesh(geometry, material);return { texture, mesh };
}

到这一步,整条管线就算制作完毕了

创建一个平面

可能细心的朋友已经发现了,前面很多效果图中的几何体都是在一个平面上的,那么这里我们将学习创建一个平面

创建PlaneBufferGeometry几何体

// create_fn.js
function createFace(width, height, arc, conf) {const geometry = new THREE.PlaneBufferGeometry(50, 40, 64); // 创建一个平面几何体,前两个参数设置长和宽,第三个参数设置细粒度const material = new THREE.MeshBasicMaterial({color: "rgb(159, 161, 162)",side: THREE.DoubleSide, // 保证两面都渲染,这样从正反两面看这个平面都是存在的transparent: true,opacity: 1 // 透明度});const mesh = new THREE.Mesh(geometry, material);initConfig(mesh, conf);return mesh;
}

效果图:

通过调整平面和物体的位置、旋转,可以实现物体恰好放在平面上的感觉。不过这里有个美中不足的地方就是,平面是一个矩形,如果我想实现弧角怎么办呢?

创建一个自定义形状的平面

原理:利用ShapeBufferGeometry实现自定义形状的平面

这里我们先提前准备一个函数 createArcRect,用来创建出一个弧角矩形的形状

// create_fn.js
// 利用Three中的Shape创建一个带弧角的矩形的形状,三个参数分别代表长、宽、弧度
function createArcRect(width, height, arc) {const shape = new THREE.Shape();const w = width - arc;const h = height - arc;// 下面是一系列的计算shape.moveTo(w, height);shape.arc(0, -1 * arc, arc, Math.PI / 2, 0, true);shape.lineTo(width, arc);shape.arc(-1 * arc, 0, arc, 0, (3 * Math.PI) / 2, true);shape.lineTo(arc, 0);shape.arc(0, arc, arc, (3 * Math.PI) / 2, Math.PI, true);shape.lineTo(0, h);shape.arc(arc, 0, arc, Math.PI, Math.PI / 2, true);shape.lineTo(w, height);return shape;
}

利用创建弧角矩形的函数,通过 ShapeBufferGeometry 来创建出我们需要的弧角矩形平面

// create_fn.js
// 创建一个弧角矩形平面,前两个参数代表长宽,arc代表弧角角度
function createFace(width, height, arc, conf) {const shape = createArcRect(width, height, arc);const geometry = new THREE.ShapeBufferGeometry(shape, 64); // 传入我们刚刚定义的形状shapeconst material = new THREE.MeshBasicMaterial({color: "rgb(159, 161, 162)",side: THREE.DoubleSide,transparent: true,opacity: 1,});const mesh = new THREE.Mesh(geometry, material);initConfig(mesh, conf);return mesh;
}

最终效果:

创建分组Group

既然我们建立了一个平面,以及将物体放置在了平面上,那么这一块就代表一个区域,通常一个项目中会有多个区域,那么这个时候我们就需要Group分组了,这样逻辑清晰,在处理交互的时候也会更加明了,就像模块拆分一样

// create_fn.js
// 传入的参数为一个个的几何体
function createGroup(...arr) {const group = new THREE.Group();arr.forEach((item) => group.add(item)); // 通过group.add向该分组中添加几何体scene.add(group); // 将分组加入到场景中return group;
}

在 index.html 中使用

<!DOCTYPE html>
<html><head>...</head><body><div id="canvas-frame"></div><!-- 引入的一些JS --><script src="lib/three.js"></script><script src="..."></script><script>// 上节的代码const { scene, camera, renderer } = initThree("#canvas-frame");// ...// 新添加的代码const earth = createEarth({ position: { x: -15, y: -1 } });const machine = createMachine("./img/move.png", { rotation: { x: Math.PI / 2 } });const face = createFace(50, 60, 4);const { texture: tubeTexture, mesh: tube } = await createTube([-15, -5, 0], [15, -5, 0]);group = createGroup(earth, machine, face, tube); // 直接将几何体一个个传进去group.position.x = -60; // 修改group的位置</script></body>
</html>

重构代码

效果图:

这里我将整个场景划分为左右两个区域,所以设置两个分组,代码如下:

<!DOCTYPE html>
<html><head><meta charset="UTF-8" /><title>Three-Demo</title><style type="text/css">body {margin: 0;}#canvas-frame {border: none;/* background-color: #eeeeee; */}</style></head><body><div id="canvas-frame"></div><script src="lib/three.js"></script><!-- 控制器 --><script src="lib/OrbitControls.js"></script><!-- fps --><script src="lib/stats.min.js"></script><!-- 加载模型 --><script src="lib/DRACOLoader.js"></script><script src="lib/GLTFLoader.js"></script><!-- 引入自己封装好的函数 --><script src="js/util_fn.js"></script><script src="js/init_fn.js"></script><script src="js/create_fn.js"></script><script>// 初始化const { scene, camera, renderer } = initThree("#canvas-frame");const lights = initLight();const controls = initControls();const stats = initStats();// 构建场景// 合并图片需要等图片加载,这里有回调地狱,所以用 async 解决(async function () {// 为了使每一组的命名不冲突,将每组的代码用块级作用域隔开// scene场景的第一组内容let group1, group1Animate;{const earth1 = createEarth({ position: { x: -15, y: -1 } });const earth2 = createEarth({ position: { x: 15, y: -1 } });const machine1 = createMachine("./img/move.png", {rotation: { x: Math.PI / 2 },position: { x: 15, y: -1, z: 15 },});const machine2 = createMachine("./img/move.png", {rotation: { x: Math.PI / 2 },position: { x: -15, y: -1, z: 15 },});const machine3 = createMachine("./img/move.png", {position: { x: 15, z: -20, y: -5 },});const machine4 = createMachine("./img/move.png", {position: { x: -15, z: -20, y: -5 },});const face = createFace(50, 60, 4, {rotation: { x: Math.PI / 2 },position: { x: -25, y: -6.1, z: -30 },});const { texture: tubeTexture1, mesh: tube1 } = await createTube([-15, -5, 0],[15, -5, 0]);const { texture: tubeTexture2, mesh: tube2 } = await createTube([-15, -5, 15],[15, -5, 15]);const { texture: tubeTexture3, mesh: tube3 } = await createTube([-15, -5, -20],[15, -5, -20]);const { texture: tubeTexture4, mesh: tube4 } = await createTube([-15, -5, 15],[-15, -5, -40],[40, -5, -40],[40, -5, -10],[60, -5, -10]);const { texture: tubeTexture5, mesh: tube5 } = await createTube([15, -5, 15],[15, -5, -35],[30, -5, -35],[30, -5, 10],[60, -5, 10]);group1 = createGroup(machine1,machine2,earth1,earth2,machine3,machine4,face,tube1,tube2,tube3,tube4,tube5);group1.position.x = -60;// 每一组的运动函数group1Animate = function () {tubeTexture1.offset.x -= 0.022;tubeTexture2.offset.x -= 0.02;tubeTexture3.offset.x -= 0.019;tubeTexture4.offset.x -= 0.022;tubeTexture5.offset.x -= 0.02;};}// scene场景的第二组内容let group2, group2Animate;{const earth = createEarth({ position: { z: -10, y: -1.1 } });const machine1 = createMachine("./img/move.png", {position: { y: -5, z: 10 },});const machine2 = createMachine("./img/electronics.png", {position: { x: 26, y: -5 },});const machine3 = createMachine("./img/electronics.png", {position: { x: 45, y: -5 },});const computer1 = await createImportModel("./model/com/computer.gltf",{scale: { x: 150, y: 150, z: 150 },position: { x: 71, y: -6, z: 15 },});const computer2 = await createImportModel("./model/com/computer.gltf",{scale: { x: 150, y: 150, z: 150 },position: { x: 71, y: -6, z: -5 },});const { texture: tubeTexture1, mesh: tube1 } = await createTube([0, -5, 10],[15, -5, 0],[56, -5, 0],[71, -5, 10]);const { texture: tubeTexture2, mesh: tube2 } = await createTube([0, -5, -10],[15, -5, 0],[56, -5, 0],[71, -5, -10]);const face = createFace(100, 56, 2, {rotation: { x: Math.PI / 2 },position: { x: -13, y: -6.1, z: -28 },});group2 = createGroup(earth,machine1,machine2,machine3,tube1,tube2,computer1,computer2,face);// 第二组的运动函数group2Animate = function () {tubeTexture1.offset.x -= 0.022;tubeTexture2.offset.x -= 0.02;};}// animateanimate();function animate(time) {// 载入两个分组的动画函数// 管道运动,路线循环流动效果group1Animate();group2Animate();// fps监控stats.update();renderer.render(scene, camera);requestAnimationFrame(animate);}})();</script></body>
</html>

ThreeJS —— 机房Demo(二)相关推荐

  1. java爬虫之基于httpclient的简单Demo(二)

    转载自 java爬虫之基于httpclient的简单Demo(二) 延续demo1的 java爬虫的2种爬取方式(HTTP||Socket)简单Demo(一),demo2出炉啦,大家想学爬虫都可以从这 ...

  2. pixi 平铺精灵 demo (二)

    pixi 平铺精灵 demo (二) 引言 上篇主要讲述了 pixi 平铺精灵的创建与使用及视差滚动,这篇主要讲述的是基于上篇 demo 的优化与新增的功能. 目录 新增功能 1.1 分数&& ...

  3. php框架 三维图展示,HTML5开发实例-3D全景(ThreeJs全景Demo) 详解(图)

    前言 在现在市面上很多全景H5的环境下,要实现全景的方式有很多,可以用css3直接构建也可以用基于threeJs的库来实现,还有很多别的制作全景的软件使用 本教学适用于未开发过3D全景的工程狮 如果觉 ...

  4. Json工具Demo(二)

    上篇文章Json工具Demo(一) 说了一些Json和Jackson框架的基本知识,以及Demo中需要用到的基本类.这篇文章主要写下json和java对象之间相互转换的工具类JacksonUtil.j ...

  5. 【游戏程序设计】三维游戏示例-战术竞技游戏Demo(二)

    突然相遇: 然后死掉. 源代码以及实现方法: 首先定义一个Character类为角色的基类,然后英雄魔兽(战士)类Warcraft与托尼(法师)类Timy继承于它.分别实现对应的方法. 角色类有许多的 ...

  6. NDK JNI方式读写Android系统的demo(二)

    NDK & JNI(方式读写Android系统的Demo) 大家都知道Android系统是一种基于Linux的自由及开放源码的操作系统,所以读写GPIO也可以直接用Linux那一套export ...

  7. Goalng小demo二:客户信息关系系统

    客户信息关系系统 项目需求分析 模拟实现基于文本界面的<客户信息管理软件>. 该软件能够实现对客户对象的插入.修改和删除(用切片实现),并能够打印客户明细表 项目的界面设计 主菜单界面 添 ...

  8. ALSA驱动中snd_soc_write()原理分析及demo(二十二)

    其实底层是调用remap_write()者i2c_transter()来写i2c设备寄存器,自查. 1.Audio Codec查结构体 kernel/include/sound/soc.h /* So ...

  9. threejs官方demo:clipping.html源码学习

    前言 gihub源码 效果演示 开始 初始化 function init() {// 初始化camera = new THREE.PerspectiveCamera(36,window.innerWi ...

  10. vue+echarts 地图携带参数下钻【demo二】

    相关必要文件已上传个人GIT代码库,自行下载 https://gitee.com/lemon-design/EchartsMapDeepDown.git 下钻数据为假数据 中国–>辽宁–> ...

最新文章

  1. Python基础20-面向对象:静态、组合、继承与派生、多态、封装、反射、内置attr方法等
  2. 巧用linux云服务器下的的/dev/shm/,避开磁盘IO不给力!
  3. 下列不是c语言浮点常量的是,C语言二级题库.doc
  4. linux系统下使用xampp 丢失mysql root密码【xampp的初始密码为空】
  5. matlab 集成学习方法,将simulink集成到matlab脚本中
  6. linux源码包编译安装与rpm安装方法介绍
  7. 【牛客 - 315F】美丽的项链(线性dp,递推,我为人人)
  8. java调用webservice_Oracle触发器调用webservice说明
  9. C语言编程猜谜语,简单的一字谜语合集
  10. php开源混合模式吗,CSS3混合模式使用详解
  11. 开源中国社区开源项目排行榜
  12. keil5软件安装开发环境搭建教程(mdk,c51通用)
  13. 程序员长见识需要关注的牛人
  14. html怎么写分享代码,怎么写一个文件分享网页?百度一键分享按钮HTML代码
  15. (PAT)BasiclLevel_c++ #1003 我要通过!(演绎法)
  16. 使用Wiredtiger恢复MongoDB中collection-x-xxx.wt文件数据
  17. linux6.5退出文档,Centos6.5及Linux基础命令和基础知识点笔记!6.25更新
  18. 前缀表达式、中缀表达式、后缀表达式的区别
  19. python下载链接下载文件_python从下载链接下载文件到本地
  20. 神经网络模型通用性分析,神经网络模型可解释性

热门文章

  1. wnmp支持php文件,Windows下配置nginx+php(wnmp)
  2. k均值的损失函数_常用的损失函数
  3. “dying ReLU“问题
  4. latex利用bibmap生成双语对照的文献表
  5. 【一日一logo_day_36】fai
  6. 怎么申请企业邮箱?手机怎么申请免费邮箱?
  7. widows计算器恢复
  8. MATLAB TIFF转Shape、TIFF和Shape的读写
  9. 基于GoLang的MMO游戏服务器(四)
  10. 最简单的可拖拽窗口教程