|
|
|
|
公众号矩阵

用 Three.js 画个 3D 生日蛋糕送给他(她)

Mesh 比较常用,它是由一个个三角形构成的几何体,还可以在每个面上贴图。所以,参数有两个,几何体 Geometry 和材质 Material。

作者:神说要有光zxg 来源:神光的编程秘籍 |2021-11-23 22:50

作为整天和 UI 打交道的前端工程师,是否想在他(她)生日的时候用代码送上一份惊喜呢?

不妨用 Three.js 做个 3D 的蛋糕送给 ta,既浪漫又能展现你技术的魅力。

这篇文章我们就来学习下如何用 Three.js 画一个蛋糕。

代码地址:https://github.com/QuarkGluonPlasma/threejs-exercize

Three.js 相关基础

Three.js 是通过场景 Scene 来管理所有的物体的,加到 Scene 的物体还可以分个组:

  1. const scene = new THREE.Scene(); 
  2.  
  3. scene.add(xxx); 
  4.  
  5. const group = new THREE.Group(); 
  6.  
  7. group.add(yyy); 
  8.  
  9. group.add(zzz); 
  10.  
  11. scene.add(group); 

想要把 Scene 中的所有物体渲染出来,需要指定一个相机 camera,然后用 renderer 来渲染,如果有动画效果,要用 requestAnimationFrame 来一帧帧不断渲染。

  1. const renderer = new THREE.WebGLRenderer(); 
  2.  
  3. function render() { 
  4.  
  5. renderer.render(scene, camera); 
  6.  
  7. requestAnimationFrame(render); 
  8.  
  9.  
  10. render(); 

相机 camera 分为从一个点去看的透视相机 PerspectiveCamera,还有从一个面去投影的正交相机 OrthographicCamera。

透视相机的特点是近大远小,而正交的则不是,就是一个平行投影,大小不变。

三维世界还需要指定一个光源,不然是全黑的,光源种类很多,常用的有这些:

  • 点光源:从一个点发射光线,就像灯泡一样
  • 平行光:平行的光线
  • 环境光:均匀照射每个地方
  • 聚光灯:舞台聚光灯的光源

三维场景中的物体有很多种,比如永远面向相机的平面是 Sprite(我们做“漫天花雨”效果用的那个),还有由三角形构成的物体叫做 Mesh。

Mesh 比较常用,它是由一个个三角形构成的几何体,还可以在每个面上贴图。所以,参数有两个,几何体 Geometry 和材质 Material。

比如圆柱体就是一个 Mesh,创建它的时候要指定圆柱几何体 CylinderBufferGeometry 和每个面的材质 Material。

  1. const 圆柱几何体 = new THREE.CylinderBufferGeometry(上圆半径, 下圆半径, 高度, 侧面分段数量); 
  2.  
  3. const 侧面材质 = new THREE.MeshBasicMaterial({map: 纹理图片}); 
  4. const 上面材质 = new THREE.MeshBasicMaterial({color: 'red'}); 
  5. const 下面材质 = new THREE.MeshBasicMaterial({color: 'red'}); 
  6.  
  7. const 圆柱 = new THREE.Mesh(圆柱几何体, [侧面材质, 上面材质, 下面材质]); 

MeshBasicMaterial 是基础的材质,可以通过 color 来指定颜色,也可以通过 map 来指定纹理图片 texture。

各种 Mesh 中比较特殊是文字,它用的是 TextGeometry,文字需要从一个 xxx.typeface.json 中加载。

而这种 json 文件可以用字体文件 ttf 来转换得到。用ttf 转 typeface.json 的这个网站来转:

之后就可以显示文字了:

  1. const fontLoader = new THREE.FontLoader(); 
  2.  
  3. fontLoader.load('./font/xxx.typeface.json'function (font) { 
  4.     var textGeometry = new THREE.TextGeometry('文字', 参数); 
  5.     const textMaterial = [ 
  6.         new THREE.MeshBasicMaterial({color: '字体颜色'}), 
  7.         new THREE.MeshBasicMaterial({color: '侧面颜色'}), 
  8.     ]; 
  9.  
  10.     const text = new THREE.Mesh(textGeometry, textMaterial); 
  11. }); 

这些就是我们会用到的 Three.js 基础,简单做个小结:

  • Three.js 是通过 Scene 来管理各种物体的,物体还可以分下组。
  • 物体中常见的有 Mesh 和 Sprite 等,Sprite 是永远面向相机的一个平面,Mesh 是由三角形构成的三维物体。Mesh 要指定几何体Geometry 和材质 Material,常用的材质可以是颜色或者纹理贴图。其中文字 TextGeometry 比较特殊,需要一个 typeface.json 的文件,这个可以由 ttf 转换得到。
  • 场景中的物体准备好之后,还需要设置下光源 Light 和相机 Camera,相机主要有从点去看的透视相机和从一个平面去投影的正交相机,之后就可以通过渲染器 Renderer 渲染出来了,结合 requestAnimationFrame 来一帧帧的渲染。

基础学完之后,正式开始画蛋糕了。

画 3D 蛋糕

蛋糕其实就是由 4 个圆柱体加上文字构成的,每个圆柱体都设置了不同的位置,圆柱体的侧面和上下面都贴上不同的贴图,就是一个蛋糕。

我们先准备蛋糕的贴图:

使用纹理加载器 TextureLoader 去加载他们:

  1. const cakeTexture1 = new THREE.TextureLoader().load('img/cake1.png'); 
  2.  
  3. const cakeTexture2 = new THREE.TextureLoader().load('img/cake2.png'); 
  4.  
  5. const cakeTexture3 = new THREE.TextureLoader().load(`img/cake3.png`); 
  6.  
  7. const cakeTexture4 = new THREE.TextureLoader().load('img/cake4.png'); 

然后构成纹理贴图的材质:

  1. const cakeMaterail1 = new THREE.MeshBasicMaterial({map: cakeTexture1}); 
  2.  
  3. const cakeMaterail2 = new THREE.MeshBasicMaterial({map: cakeTexture2}); 
  4.  
  5. const cakeMaterail3 = new THREE.MeshBasicMaterial({map: cakeTexture3}); 
  6.  
  7. const cakeMaterail4 = new THREE.MeshBasicMaterial({map: cakeTexture4}); 

除了纹理贴图的材质外,再准备个颜色构成的材质:

  1. const pinkMaterial = new THREE.MeshBasicMaterial({color: 'pink'}); 

然后创建 4 个圆柱体的物体(Mesh),使用不同的贴图材质和颜色材质:

  1. const cakeGeometry1 = new THREE.CylinderBufferGeometry(100, 100, 70, 40); 
  2.  
  3. const cakePart1 = new THREE.Mesh(cakeGeometry1, [cakeMaterail1, pinkMaterial, pinkMaterial]); 

圆柱体的几何体 CylinderBufferGeometry 的参数分别是上面圆的半径,下面圆的半径,高度,侧面的分割次数。

上面圆半径保持一致,这样才是圆柱体。侧面分割次数设置为 40,这样比较圆滑。

之后还设置下位移,然后就可以加到蛋糕分组里了。

我们用同样的方式创建四个圆柱体,设置不同的大小和位置,贴不同的图:

  1. const cakeGeometry1 = new THREE.CylinderBufferGeometry(100, 100, 70, 40); 
  2. const cakePart1 = new THREE.Mesh(cakeGeometry1, [cakeMaterail1, pinkMaterial, pinkMaterial]); 
  3. cakePart1.translateY(45) 
  4.  
  5. const cakeGeometry2 = new THREE.CylinderBufferGeometry(120, 120, 70, 40); 
  6. const cakePart2 = new THREE.Mesh(cakeGeometry2,[cakeMaterail3, pinkMaterial, pinkMaterial]); 
  7. cakePart2.translateY(-25) 
  8.  
  9. const cakeGeometry3 = new THREE.CylinderBufferGeometry(140, 140, 60, 40); 
  10. const cakePart3 = new THREE.Mesh(cakeGeometry3, [cakeMaterail2, pinkMaterial, pinkMaterial]); 
  11. cakePart3.translateY(-90) 
  12.  
  13. const cakeGeometry4 = new THREE.CylinderBufferGeometry(160, 160, 10, 40); 
  14. const cakePart4 = new THREE.Mesh(cakeGeometry4, [cakeMaterail4, cakeMaterail4, cakeMaterail4]); 
  15. cakePart4.translateY(-120) 
  16.  
  17. cake.add(cakePart1) 
  18. cake.add(cakePart2) 
  19. cake.add(cakePart3) 
  20. cake.add(cakePart4) 

如果对坐标位置拿不准,可以在 Scene 中加上一个坐标的辅助工具 AxisHelper。参数是坐标轴长度。

  1. const axisHelper = new THREE.AxisHelper(2500); 
  2.  
  3. scene.add(axisHelper); 

然后是文字的部分,这个要先通过字体文件 ttf 转成 typeface.json 的文件,然后用 fontLoader 来加载,之后创建相应的 Mesh:

  1. fontLoader.load('./font/guang.typeface.json'function (font) { 
  2.     var textGeometry = new THREE.TextGeometry('光光', { 
  3.         font: font, 
  4.         size: 30, 
  5.         height: 5, 
  6.         bevelEnabled: true
  7.         bevelSize: 10, 
  8.     }); 
  9.     const textMaterial = ['white''red'].map(color => new THREE.MeshBasicMaterial({color})); 
  10.  
  11.     const text = new THREE.Mesh(textGeometry, textMaterial); 
  12.     text.translateY(90) 
  13.     text.translateX(-45) 
  14.  
  15.     cake.add(text);  
  16. }); 

TextGeometry 需要设置的参数有字体大小 size,厚度 height,以及边缘是否是曲面 bevelEnabled,和曲面的大小 bevelSize。

我们这里的效果是需要开启曲面的。

4 个圆柱体画完了,文字也画完了,那蛋糕就算是画完了,之后设置下光源、相机,就可以用 Renderer 渲染了。

光源使用环境光,因为要均匀的照射:

  1. const light = new THREE.AmbientLight(0xCCCCCC); 
  2.  
  3. scene.add(light); 

相机使用正交相机,因为不需要近大远小的透视效果:

  1. const width = window.innerWidth; 
  2. const height = window.innerHeight; 
  3. //窗口宽高比 
  4. const k = width / height; 
  5. //三维场景显示范围的高度 
  6. const s = 200; 
  7.  
  8. const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000); 
  9.  
  10. camera.position.set(0, 100, 500) 
  11. camera.lookAt(scene.position); 

正交相机的参数分别是左右上下远近的三维视野范围,我们指定高度为 200,然后根据窗口的宽高比算出宽度。远近可以设置一个比较大的范围。

之后就可以用 Renderer 来渲染了。把渲染出的 canvas 的 dom 挂载到 body 上。

  1. const renderer = new THREE.WebGLRenderer(); 
  2.  
  3. renderer.setSize(width, height); 
  4. //设置背景颜色 
  5. renderer.setClearColor(0xFFFFFF, 1); 
  6. document.body.appendChild(renderer.domElement); 
  7.  
  8. function render() {         
  9.     renderer.render(scene, camera); 
  10.  
  11.     cake.rotation.y += 0.005; 
  12.  
  13.     requestAnimationFrame(render) 
  14. render() 

在每帧 render 之前,还做了个围绕 y 轴的自动旋转。

还要支持手动的旋转,这个直接使用 Three.js 的轨道控制器 OrbitControls 就行。

  1. const controls = new THREE.OrbitControls(camera); 

参数是相机,因为这种视野的改变就是通过改变相机位置和朝向来实现的。

创建了 Scene 中的蛋糕的每一部分,设置好了光源、相机,用渲染器做了一帧帧的渲染,并且添加了用鼠标来改变视角的轨道控制器之后,就完成了 3D 蛋糕的制作。

我们来看下效果:

代码地址:https://github.com/QuarkGluonPlasma/threejs-exercize

全部代码:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <title>生日蛋糕</title> 
  6.     <style> 
  7.         body { 
  8.             margin: 0; 
  9.             overflow: hidden; 
  10.         } 
  11.     </style> 
  12.     <script src="./js/three.js"></script> 
  13.     <script src="./js/OrbitControls.js"></script> 
  14. </head> 
  15. <body> 
  16. <script> 
  17.     const width = window.innerWidth; 
  18.     const height = window.innerHeight; 
  19.     //窗口宽高比 
  20.     const k = width / height; 
  21.     //三维场景显示范围的宽度 
  22.     const s = 200; 
  23.  
  24.     const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000); 
  25.  
  26.     const fontLoader = new THREE.FontLoader(); 
  27.  
  28.     const scene = new THREE.Scene(); 
  29.  
  30.     const cake = new THREE.Group(); 
  31.  
  32.     const renderer = new THREE.WebGLRenderer(); 
  33.  
  34.  
  35.     function create() { 
  36.         renderer.setSize(width, height); 
  37.         //设置背景颜色 
  38.         renderer.setClearColor(0xFFFFFF, 1); 
  39.         document.body.appendChild(renderer.domElement); 
  40.  
  41.         camera.position.set(0, 100, 500) 
  42.         camera.lookAt(scene.position); 
  43.  
  44.         const light = new THREE.AmbientLight(0xCCCCCC); 
  45.         scene.add(light); 
  46.  
  47.         const axisHelper = new THREE.AxisHelper(2500); 
  48.         scene.add(axisHelper); 
  49.  
  50.         const cakeTexture1 = new THREE.TextureLoader().load('img/cake1.png'); 
  51.         const cakeTexture2 = new THREE.TextureLoader().load('img/cake2.png'); 
  52.         const cakeTexture3 = new THREE.TextureLoader().load(`img/cake3.png`); 
  53.         const cakeTexture4 = new THREE.TextureLoader().load('img/cake4.png'); 
  54.  
  55.         const cakeMaterail1 = new THREE.MeshBasicMaterial({map: cakeTexture1}); 
  56.         const cakeMaterail2 = new THREE.MeshBasicMaterial({map: cakeTexture2}); 
  57.         const cakeMaterail3 = new THREE.MeshBasicMaterial({map: cakeTexture3}); 
  58.         const cakeMaterail4 = new THREE.MeshBasicMaterial({map: cakeTexture4});  
  59.  
  60.         const pinkMaterial = new THREE.MeshBasicMaterial({color: 'pink'}); 
  61.  
  62.         const cakeGeometry1 = new THREE.CylinderBufferGeometry(100, 100, 70, 40); 
  63.         const cakePart1 = new THREE.Mesh(cakeGeometry1, [cakeMaterail1, pinkMaterial, pinkMaterial]); 
  64.         cakePart1.translateY(45) 
  65.   
  66.         const cakeGeometry2 = new THREE.CylinderBufferGeometry(120, 120, 70, 40); 
  67.         const cakePart2 = new THREE.Mesh(cakeGeometry2,[cakeMaterail3, pinkMaterial, pinkMaterial]); 
  68.         cakePart2.translateY(-25) 
  69.  
  70.         const cakeGeometry3 = new THREE.CylinderBufferGeometry(140, 140, 60, 40); 
  71.         const cakePart3 = new THREE.Mesh(cakeGeometry3, [cakeMaterail2, pinkMaterial, pinkMaterial]); 
  72.         cakePart3.translateY(-90) 
  73.  
  74.         const cakeGeometry4 = new THREE.CylinderBufferGeometry(160, 160, 10, 40); 
  75.         const cakePart4 = new THREE.Mesh(cakeGeometry4, [cakeMaterail4, cakeMaterail4, cakeMaterail4]); 
  76.         cakePart4.translateY(-120) 
  77.  
  78.         cake.add(cakePart1) 
  79.         cake.add(cakePart2) 
  80.         cake.add(cakePart3) 
  81.         cake.add(cakePart4) 
  82.  
  83.         fontLoader.load('./font/guang.typeface.json'function (font) { 
  84.             var textGeometry = new THREE.TextGeometry('光光', { 
  85.                 font: font, 
  86.                 size: 30, 
  87.                 height: 5, 
  88.                 bevelEnabled: true
  89.                 bevelSize: 10, 
  90.             }); 
  91.             const textMaterial = ['white''red'].map(color => new THREE.MeshBasicMaterial({color})); 
  92.  
  93.             const text = new THREE.Mesh(textGeometry, textMaterial); 
  94.             text.translateY(90) 
  95.             text.translateX(-45) 
  96.             cake.add(text);  
  97.         }); 
  98.  
  99.         scene.add(cake); 
  100.     } 
  101.  
  102.  
  103.     function render() {         
  104.         renderer.render(scene, camera); 
  105.  
  106.         cake.rotation.y += 0.005; 
  107.  
  108.         requestAnimationFrame(render) 
  109.     } 
  110.  
  111.     create() 
  112.     render() 
  113.  
  114.     const controls = new THREE.OrbitControls(camera); 
  115. </script> 
  116. </body> 
  117. </html> 

总结

本文我们用 Three.js 来实现了 3D 蛋糕的效果。

首先我们学习了下 Three.js 的基础:通过 Scene 来管理物体,物体可以分组,物体包括 Mesh、Sprite 等,Mesh 是三角形构成的 3D 物体,要分别指定几何体 Geometry 和材质 Material。材质可以是纹理(Texture)贴图、也可以是颜色。其中文字的 Mesh 需要做 ttf 到 typeface.json 的转换,加载这个 json 才能显示文字。

物体创建完了之后,还要设置相机、灯光等,然后通过渲染器来一帧帧的渲染。

调试的时候还可以添加 AxisHelper 坐标系辅助工具来辅助开发。

然后我们实现了 3D 蛋糕:

通过 4 个圆柱体 + 文字来画的,圆柱体用了不同的纹理贴图材质,设置了不同的位置,然后组成蛋糕的 group。

设置了环境光,使用了正交相机,还启用了轨道控制器 OrbitControls,来实现鼠标拖拽改变相机位置,进而改变视野角度的效果。

下个他(她)的生日,不妨试试用 Three.js 画个蛋糕送给他(她),或许会有不一样的收获哦。

【编辑推荐】

  1. 鸿蒙官方战略合作共建——HarmonyOS技术社区
  2. 前端vue.js全家桶vuejs教程vuecli3+elementui前后端分离项目实战
  3. 你不知道的 Node.js util
  4. 使用Node.JS在云服务器搭建WEB
  5. 通过漫天花雨来入门 Three.js
  6. Node.js 对比 Python:优点、缺点和用例
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

带你轻松入门 RabbitMQ

带你轻松入门 RabbitMQ

轻松入门RabbitMQ
共4章 | loong576

57人订阅学习

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

14人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

42人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微