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

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

# 正题

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

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

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

圣诞动画

# 基本场景的搭建

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

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

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

ground

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

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

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

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

material

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

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 x = Math.random() * 2000 - 1000
      const y = Math.random() * 2000 - 1000
      const z = 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({
      size: 20,
      map: texture,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      transparent: true,
  })
  material.color.setHSL(0.9, 0.05, 0.5)

  const particles = new THREE.Points(geo, material)

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

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

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

场景默认大小是20 ,我们改成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 (opens new window)算法, 从一系列的点创建一条平滑的三维样条曲线。然后three.js 封装好了插值方法,可以将这条曲线拆分成很多个点对吧, 线有了, 这时候我们去场景中加载文字立方体,不清楚的同学 看下 这篇文章

浅谈3d文字

图片

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

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

路径弯曲

其实在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.4, 0.4, 0.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, -5, 0)
        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 与你一同前行!!!

年度

上一次更新时间: 2021/12/26 下午6:37:38