手把手用 three.js从0-1带你实现圣诞🎄动画

2,839 阅读8分钟

前言

大家好我是Fly哥,今天是圣诞节, 首先祝大家圣诞节快乐。今天分享的是从0-1实现一个3D圣诞动画圣诞动画-封面

这篇有配套的视频链接讲解 和 源码。我会在文章末尾给出,感兴趣的同学自取哈!!! 但是视频的内容过于长了,有的同学还是喜欢看文章。 我就讲解下!!

正题

本篇文章大概花费你10分钟,读完本篇文章你可以学到下面几点,可以挑自己感兴趣的点进行阅读

  1. three.js 中加载圣诞树模型
  2. 实现自定义曲线路径动画
  3. three.js 中如何 实现粒子动画
  4. three.js 音频导入 和📷 动画

我们先看下圣诞动画实现的效果:

圣诞动画

圣诞动画

基本场景的搭建

首先圣诞树需要一个东西去承载, 也就是所谓的地面, 地面的其实使用的 「three.planeGeometry」, 这个api是用也是很简单定义平面的宽度 和长度, 所以你只需要的你的场景足够大, 也就说你平面的宽度和长度 足够大,然后你就可以看着制作出一个平面。 代码如下:

  const plane = new THREE.PlaneGeometry(10001000)
  const material = new THREE.MeshPhongMaterial({
      map: new THREE.TextureLoader().load('./edge.jpg'),
  })

是的就这么简单的一行代码,然后配上材质, 我们看下效果:

ground

ground

这里有同学问了这下面这两个问题??

  1. 飞哥 为啥看不到材质哇
  2. 为啥这个地面是紫色的哇

我们明明设置了材质,为啥看不到材质呢????

我给大家看下材质的图片 :

material

material

其实贴图的大小就这么大,然后你要贴在这么大的平面上肯定啥也看不到哇, 就好比如下这个场景:

show

show

紫色的就表示地面, 灰色的就是地板,这就现在的情况, 这种情况怎么解决呢, 有同学说选一个足够大的图片,可以是可以,但是你看着不怪嘛, 另一个比较聪明的同学,可以不可以有浏览器中图片 「repeat」 属性呢?? 是的材质当然是支持的

其实你创建的texture 可以用来操作横向纹理 和纵向 纹理的铺法

分别对应下面这两个属性

.wrapS

这个值定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应于**「U」**。 默认值是THREE.ClampToEdgeWrapping,即纹理边缘将被推到外部边缘的纹素。 其它的两个选项分别是THREE.RepeatWrapping和THREE.MirroredRepeatWrapping。 请参阅texture constants来了解详细信息。

.wrapT : number

这个值定义了纹理贴图在垂直方向上将如何包裹,在UV映射中对应于**「V」**。 可以使用与 .wrapS : number相同的选项。

「请注意:纹理中图像的平铺,仅有当图像大小(以像素为单位)为2的幂(2、4、8、16、32、64、128、256、512、1024、2048、……)时才起作用。 宽度、高度无需相等,但每个维度的长度必须都是2的幂。 这是WebGL中的限制,不是由three.js所限制的」

我们设置下横向平铺 和纵向平铺 为重复是不是就可以实现重叠了,

代码如下:

 if (material.map) {
      material.map.wrapS = THREE.RepeatWrapping
      material.map.wrapT = THREE.RepeatWrapping
      material.map.repeat.x = 30
  }

然后实现上面的有很多瓷砖的效果 , 我们来解释下第二个问题 贴图是白色的 , 为什么地板 看着是紫色的, 这是为啥呢 ,

答案就是 这个材质 THREE.MeshPhongMaterial 他其实是受光照影响的,

场景中其实有两个光源:

  1. 一个环境光
  2. 一个圣诞树下的点光源

没有环境光

没有环境光

没有环境光,贴图的颜色也是很黑暗,所以在three.js 中环境光 是十分重要的。

雪花粒子动画

其实无论是什么粒子,你看完这一种实现 其他的粒子你可以随便拿捏。首先还是分析,粒子是啥也就是现在场景中的雪花, 其实一个三角形有 3个顶点 ,对应的矩形 就是 4个顶点, 如果一个3d 图形,有1000个甚至 有很多个顶点组成。 然后我们在对每个顶点去做贴图,其实就可以生成一个有许多顶点的图形 的立方体呢 ,这就是 3d实现粒子的原理了 很简单。

所以雪花粒子,就是搭配顶点材质, 其实就是这样的一个材质 「PointsMaterial」,和上面的高光材质一样使用

我们对每个粒子的位置去做随机性,是不是就可以制造出所谓的效果了。直接上代码:

  const geo new THREE.BufferGeometry()
  // 设置1000个顶点
  const vertices = []
  for (let i = 0; i < 10000; i++) {
      const = Math.random() * 2000 - 1000
      const = Math.random() * 2000 - 1000
      const = Math.random() * 2000 - 1000
      vertices.push(x, y, z)
  }
  geo.setAttribute('position'new THREE.Float32BufferAttribute(vertices, 3))
  // 加载雪花贴图
  const texture new THREE.TextureLoader().load(
      './snow.png',
  )
  const material new THREE.PointsMaterial({
      size20,
      map: texture,
      blending: THREE.AdditiveBlending,
      depthTestfalse,
      transparenttrue,
  })
  material.color.setHSL(0.90.050.5)

  const particles new THREE.Points(geo, material)

这里要注意的事,不是生成mesh ,而是使用 THREE.Points, 或者说这两个有什么不同的呢 ??? 底层的webgl 渲染方式不一样xdm

  1. 「points一个用于显示点的类。 由WebGLRenderer渲染的点使用 gl.POINTS。」
  2. 「mesh 表示基于以三角形为polygon mesh(多边形网格 他是以面去渲染的」

因为顶点的大小是 固定的,如果想看粒子的大小,你可以改材质的size 就可以实现了。

场景默认大小是20 ,我们改成50 的你可以看下效果 :

50的大小

50的大小

有点丑呵呵哈哈哈!!!!!

至于动画 就很简单的, 一句话概括 ,整体动, 我们只需要对这个particles整体 去做动画就好了,自然所有粒子就开始动了。

 if (this.particle) {
      this.particle.rotation.y = time
  }

3d路径环绕动画

我们一步一步拆解, 首先你得有3d 路径 对吧, 这里的使用的Three.js 的api

使用一个点的列表然后你就可以得到一个 闭合的3D曲线 然后 在将这个3d 曲线分成 n个点, 最后将这n个点, 生成一个geometry,

其实一条线, 然后three.js 去创建闭合的曲线。

 this.curve = new THREE.CatmullRomCurve3(
      this.curveHandles.map((handle) => handle.position),
      true,
      'centripetal',
  )
  const points = this.curve.getPoints(50)
  const line = new THREE.LineLoop(
      new THREE.BufferGeometry().setFromPoints(points),
      new THREE.LineBasicMaterial({

          visible: false,
          // linecap: 'round', //ignored by WebGLRenderer
          // linejoin: 'round'
      }),
  )

THREE.CatmullRomCurve3: 简单介绍下使用Catmull-Rom算法, 从一系列的点创建一条平滑的三维样条曲线。然后three.js 封装好了插值方法,可以将这条曲线拆分成很多个点对吧, 线有了, 这时候我们去场景中加载文字立方体,不清楚的同学 看下 这篇文章

「浅谈3d文字」

图片

图片

默认文字立方体是这样的,如果让他的朝向 和曲线的方向一致呢,其实这里用到了three.js 另一个工具类, flow 其实你只要传给mesh 和路线给他,然后设置他就自动完成路径匹配了,代码如下:

  this.flow = new InstancedFlow(1, undefined, geometry, material);
  this.flow.updateCurve(0this.curve)
  this.scene.add(this.flow.object3D)
  this.flow.setCurve(00);

路径弯曲

路径弯曲

其实在3d 中有种很难的 就是吸附这种其实特别像圆柱面的侧面 ,你仔细看下。 接下里有了方法,然后怎么去运动呢 对吧 , 这个也不用担心,这个工具类已经帮我们封装好了。

if (this.flow) {
    this.flow.moveAlongCurve(0.002)
 }

沿着自定义曲线每秒移动多少,然后到终点自动回头。 这个方法是要 requestAnimation里面哦。

模型导入和音乐加载

模型我也是找了很多网站, 最后还是free3d.com 这个里面找到了很多免费的模型,其实模型的导入无非就是three.js 有对应的loader 去加载,然后你根文件的类型去找到对应的loader, 我这里的圣诞树的话,其实obj loader , mtl loader这个是加载材质信息的, 因为圣诞树上材质,

 const obj = new OBJLoader() //obj加载器
const mtl = new MTLLoader() //材质文件加载器
mtl.load('./tree/12150_Christmas_Tree_V2_L2.mtl'(materials) => {
    // 返回一个包含材质的对象MaterialCreator
    //obj的模型会和MaterialCreator包含的材质对应起来
    obj.setMaterials(materials)
    obj.load('./tree/12150_Christmas_Tree_V2_L2.obj'(tree) => {
        //tree.receiveShadow = true;
        tree.castShadow = true
        this.tree = tree
        this.tree.scale.set(0.40.40.4//放大obj组对象

        // 先绕Y轴旋转 在绕 x 轴旋转
        const matrix = new THREE.Matrix4().makeRotationY(Math.PI / 2).makeRotationX(-Math.PI / 2)
        this.tree.rotation.setFromRotationMatrix(matrix)
        this.tree.position.set(0, -50)
        this.scene.add(this.tree//返回的组对象插入场景中
    })
})

如果你在模型导入的时候时常找不到模型, 你可以先将模型先放大,后面在做位置微调就好了。

音乐的加载three.js 中有对应的音乐加载器,然后将音乐绑在相机上,会有声音变化的效果,代码如下

 addMusic() {
      const listener = new THREE.AudioListener();
      this.camera.add(listener);
      // 创建一个非位置音频对象  用来控制播放
      const audio = new THREE.Audio(listener);
      // 创建一个音频加载器对象
      const audioLoader = new THREE.AudioLoader();
      // 加载音频文件,返回一个音频缓冲区对象作为回调函数参数
      audioLoader.load('./music.mp3'function (AudioBuffer) {
          // console.log(AudioBuffer)
          // 音频缓冲区对象关联到音频对象audio
          audio.setBuffer(AudioBuffer);
          audio.setLoop(false); //是否循环
          audio.setVolume(1); //音量
          // 播放缓冲区中的音频数据
          audio.pause()
          audio.play(); //play播放、stop停止、pause暂停
      });
  }

最后我在讲下这运镜的动画是怎么实现的

相机动画

看镜头不断从远到近,不断地反复轮回,其实核心就一个字 改变相机的位置,但是你如果一直加 整个场景的东西 就会看不见了,

所以现在的需求固定相机在某个位置,然后到某一个位置后就返回。 如图:

相机

相机

其实也就是在A点和B 点 相机来回摆动,形成了错觉, 整个场景在动的感觉,其实这里用到了正余弦函数: 我们看下两个的图像

图像

图像

为啥这两个 这么符合 无论 是哪一个函数 他们的取值范围永远是 【-1,1】 而且同时也有单调性, 不像线性渐变辣么突兀,所以对相机运用这个动画非常适合,大家以后如果有这种场景,可以使用这个公式

 y = default + K * cos / sin(time)

default默认是 一开始大小, K 其实就是 最终的位置 减去默认的大小 就可以了,这样就不用担心了 哈哈哈哈!!

代码如下:

  this.camera.position.x = Math.sin(time * 0.0005) * 150
  this.camera.position.z = 100 + Math.cos(time * 0.0005) * 150
  this.camera.position.y = 30 + Math.cos(time * 0.0005) * 30

本篇文章大概想表达的应该都讲结束了

最后

很感谢你能看到最后,文章所有源码 和配套视频讲解, b站搜索 「程序员Fly哥」, 以后会持续更新视频,源码公众号回复 「圣诞」二字就好了

最后fly 哥最近在冲击,掘金年度作者, 有时间的同学可以帮我投个票, 「点击下方阅读原文」就好了,感谢, 2021 与你一同前行!!!

年度