我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛
效果预览
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)
30
、1
这两个参数分别为长边的分段数和短边的分段数。长方形实际上是由三角形组成的(见下图),先把长边的分段数搞多一点,后面有用。
然后把100块的花纹在这个平面上印出来。
自己一点点画花纹也不是不可以,但是第一我不会,第二太费劲,网上扒拉一个100块钱的正面图片(百度一搜就是了,这个合法的,别担心),往这个平面上一贴就完事了。
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块钱~
让100块有点弧度
非常平整的100块,如果是一张的话还勉强说的过去,如果都是平整的就有点假了。所以要让100块钱有点弧度。
在制作100块形状的时候,我选择了让长边分段数多一点,在这里就派上了用场。曲面实际上是一小段一小段平面连接而成的。
红圈中的每个点都有(x,y,z) 三个坐标。Three.js 在生成平面的时候,默认平面中心位置为(0,0,0),平面垂直于空间坐标系中的 xOz 平面。
所以100块平面各点的初始坐标为 (x,y,0),每个点坐标的 z 值都是 0,让100块完全就是要合理分配 z 坐标的值,使得 z 坐标在一条曲线上,那么对应的平面自然是曲面。
z 的坐标通过椭圆方程来计算。
通过设置不同的 a,b 值,100块的弯曲程度也会不同,最终的弯曲程度取决于 a/b 的值。
const curveRatio = Math.random()*2+2// 弯曲程度,测试弯曲程度在 2~4 比较好看。
const positionAttribute = geometry.getAttribute('position');// 获取平面上各点的坐标
positionAttribute
是一个对象,记录了形状坐标相关的信息,其中比较重要的属性一个是 count
,表示平面一共由多少个顶点组成,另一个是 array
。array
中的值每三个为一组,代表一个顶点的 [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)
写一个循环,生成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);
};
最终,一桩天降百元大钞的美事就诞生了~