仅用 200行代码,轻松给大家发10万新年红包(不是)~

1,232 阅读6分钟

我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

效果预览

Three.js 基础(相机、渲染器之类的,可以不看)

导入 Three.js

Three.js 是一个 3D 渲染库,通过对 WebGL 接口进行封装,使得用户不了解 WebGL 也可以进行 3D 绘图。

import * as THREE from 'three';

创建一个场景

场景是一个摆放物体的场所。通过 Three.js 可以很方便的创建场景。

const scene =  new THREE.Scene();

放一个相机

场景相当于一个无穷的空间,可以随意摆放任何物体。我们通过相机来取景,把这个景放到网页上进行展示。所以要在场景中放一个照相机。 Three.js 提供了

  • CubeCamera
  • OrthographicCamera
  • PerspectiveCamera

其中用的比较多的是 PerspectiveCamera ,它可以实现近大远小的效果,符合现实情况。

const camera =  new THREE.PerspectiveCamera(
    45,//fov
    window.innerWitdh/window.innerHeight,//aspect
    1,//near
    1000 //far
);

四个参数分别为:

  • fov: 视野垂直角度,单位是角度。
  • aspect: 视野宽长比。
  • near: 离摄像机最近拍摄距离(再近就拍不到了)。
  • far: 离摄像机最远拍摄距离(再远也拍不到了)。

通过这四个参数固定了一个锥体,在这个锥体中的物体可以被看见。

来源看水印 图中灰色部分中是摄像机的可见空间。

如果要看灰色部分外面的东西怎么办呢?一个办法是把摄像机换个位置。那么摄像机的可见空间自然跟着动了。

//x,y,z 三维空间中的坐标
camera.position.set(x,y,z)

另一个方法是调整摄像机的角度

camera.lookAt(new THREE.Vector3(x,y,z))

开始渲染

放好东西,取好景,还要通过渲染器将景渲染出来,不然浏览器中啥也看不到的。

先配置一个渲染器。

const renderer =  new THREE.WebGLRenderer({
    antialias: true // 抗锯齿,这个不设置为 true 物体边缘不平滑,比较难看。
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth,window.innerHeight)

把渲染器渲染出来的东西插入到网页中。

const container =  document.getElementById('app')
container.appendChild(renderer.domElement)

先造一张百元大钞

three.js 中的物体分形状和材质两个部分。形状就是东西的是圆的还是方的,是高的还是矮的这种。材质就是东西是什么颜色,反不反光,上面有什么花纹之类的。

100块钱的形状是一个长 15.6cm,宽7.6cm 的平面。先造一个长宽比跟100块一样的平面,把形状搞出来再说。

const geometry =  new THREE.PlaneGeometry(15.6,7.6,30,1)

301 这两个参数分别为长边的分段数和短边的分段数。长方形实际上是由三角形组成的(见下图),先把长边的分段数搞多一点,后面有用。

image.png

然后把100块的花纹在这个平面上印出来。

自己一点点画花纹也不是不可以,但是第一我不会,第二太费劲,网上扒拉一个100块钱的正面图片(百度一搜就是了,这个合法的,别担心),往这个平面上一贴就完事了。

front.png


    const loader = new THREE.TextureLoader();
    const frontMap =  loader.load(imgPath);
    const frontMaterial = new THREE.MeshLambertMaterial({
      color: 0xffffff,
      side: THREE.FrontSide,
      wireframe: false,
      map: frontMap
    });

这里选用的是 LambertMaterial 材质。这种材质会受环境光的影响,如果场景中的光线比较暗,比如完全没有亮光,那么这个材质的物体是看不见的。场景中亮一点,这个材质做的物体也会看的更清晰一些(亮),同时这个材质不会产生高光。所以比较适合用来做贴近现实的,粗糙的物体。

材质最终的颜色是由材质本身的颜色、它的贴图(map 属性)以及场景中的灯光共同决定的。这里设置材质本身的颜色是 0xffffff,那么最终的颜色就是贴图的颜色和灯光来控制。

所以先在场景中加个灯。

const ambientLight = new THREE.AmbientLight(0xffffff);

AmbientLight 是环境灯光,这个光在整个场景中的各个点都是一个颜色。简单点我把它设置为 0xffffff。那么最终材质的颜色就是贴图的颜色。

将形状和材质粘一起,形成最终的物体。

const frontMesh =  new THREE.Mesh(geometry, frontMaterial)

现在这张100块钱的正面就做好了。

反面也是网上扒一张100块的反面图案。 然后跟正面一样,唯独材质的 side 属性需要修改成 THREE.BackSide

最后,把正面和反面粘一块。

const group  = new THREE.Group()
group.add(frontMesh);
group.add(backMesh);

当当~ 新鲜的100块钱~

image.png

让100块有点弧度

非常平整的100块,如果是一张的话还勉强说的过去,如果都是平整的就有点假了。所以要让100块钱有点弧度。

在制作100块形状的时候,我选择了让长边分段数多一点,在这里就派上了用场。曲面实际上是一小段一小段平面连接而成的。

image.png

红圈中的每个点都有(x,y,z) 三个坐标。Three.js 在生成平面的时候,默认平面中心位置为(0,0,0),平面垂直于空间坐标系中的 xOz 平面。

所以100块平面各点的初始坐标为 (x,y,0),每个点坐标的 z 值都是 0,让100块完全就是要合理分配 z 坐标的值,使得 z 坐标在一条曲线上,那么对应的平面自然是曲面。

z 的坐标通过椭圆方程来计算。

image.png

通过设置不同的 a,b 值,100块的弯曲程度也会不同,最终的弯曲程度取决于 a/b 的值。

const curveRatio = Math.random()*2+2// 弯曲程度,测试弯曲程度在 2~4 比较好看。
const positionAttribute =  geometry.getAttribute('position');// 获取平面上各点的坐标

positionAttribute 是一个对象,记录了形状坐标相关的信息,其中比较重要的属性一个是 count,表示平面一共由多少个顶点组成,另一个是 arrayarray 中的值每三个为一组,代表一个顶点的 [x,y,z] 坐标。

const maxiumX = Math.max.apply(
    null,
    positionAttribute.array.filter((_,index)=>index % 3 === 0)
);

获取平面最大的 x ,最大的 x 对应着椭圆方程中 a 的值。

for(let i = 0, l = positionAttribute.count; i<l ;i++){
    const x =  positionAttribute.getX(i);// getX 方法是 positionAttribute 内置方法,可以获取第 i 个顶点的 x 坐标。
    const z = -Math.sqrt(maxiumX ** 2 - x ** 2) / (maxiumX / curveRatio) ** 2; // 只取椭圆的下半部分。
    z = positionAttribute.setZ(i,z)// setZ 方法是 positionAttribue 内置方法,可以设置第 i 个顶点的 z 坐标。
}

更新完物体中顶点的坐标后,设置 positionAttribute.needsUpdate = true

这样一张弯曲的百元大钞就做好了,记得把它放到场景中。

scene.add(mesh)

image.png

写一个循环,生成1000张这样的钞票。同时通过 mesh.position.set(x,y,z)函数,将每张钞票摆放到不同的位置。

我是将所有钞票设置成了一组对象:

const geometryGroup = new THREE.Group()
for(let i = 0;i<=1000;i++){
    //省略生成钞票的代码
    mesh.position.set(
      Math.random() * 500 - 250,
      Math.random() * 100 - 50,
      Math.random() * 500 - 250
     );
    geomertyGroup.add(mesh);
}

同时使用 simplex-noise 噪声,让钞票的位置看起来更随机一点。

const makeFlow = (geometeryGroup) => {
  geometeryGroup.traverse((object) => {
    const y = object.position.y;
    const z = object.position.z;
    const noise = nosie2d(y, z);
    object.position.z = noise * object.position.z;
    object.position.y = noise * object.position.y;
  });
};
makeFlow(geometeryGroup)

让钞票下落与旋转

为了让效果更逼真一点,所以每张钞票的下落速度和旋转速度都应该是随机的,因此在生成钞票的代码中,增加初始化每张钞票下落速度和旋转速度的部分。

const geometryGroup = new THREE.Group()
for(let i = 0;i<=1000;i++){
    //省略生成钞票的代码
    mesh.rotationSpeed = Math.random() * degToRad(10) + degToRad(-5);
    mesh.fallSpeed = Math.random() * 2 + 0.5;
    mesh.position.set(
      Math.random() * 500 - 250,
      Math.random() * 100 - 50,
      Math.random() * 500 - 250
     );
    geomertyGroup.add(mesh);
}

然后在动画部分,根据下落速度和旋转速度不断的去更新钞票位置和旋转角度。同时,当钞票下落到地面时,停止旋转和下落。

const animate = () => {
  requestAnimationFrame(animate);
  geometeryGroup.traverse((object) => {
    if (object.name == 'rmb') {
      if (object.position.y + rmbGroup.position.y <= -100) {//到达地面,停止旋转和下落。
        object.fallSpeed.y = 0;
        object.rotationSpeed.x = 0;
      }
      object.position.y -= object.fallSpeed.y;
      object.rotation.x += object.rotationSpeed.x;
    }
  });
  renderer.render(scene, camera);
};

最终,一桩天降百元大钞的美事就诞生了~