本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。
摘要
粒子 particle
和 精灵 sprite
是在三维开发中经常用到的网格模型,在 Three.js
中使用 THREE.Points
可以非常容易地创建很多细小的物体,可以用来模拟星辰、雨滴、雪花、烟雾、火焰和其他有趣的效果。本文将讨论关于 Three.js
中各种创建粒子的方式和以及如何优化粒子的样式和使用粒子,最终将结合本文所讲的粒子知识,制作一个充满趣味和创意的 3D
粒子页面——迷失太空。
通过阅读本文及配套对应代码,你将学到的内容包括:使用 THREE.Sprite
创建粒子集合、使用 THREE.Points
创建粒子集合、如何创建样式化的粒子、使用 dat.GUI
动态控制页面参数、使用 Canvas
样式化粒子、使用纹理贴图样式化粒子、从高级几何体创建粒子、给场景添加 Fog
和 FogExp2
雾化效果、使用 正余弦函数
给模型添加动画效果、鼠标移动动画等。
效果
本文代码中包含7个粒子效果示例,解开代码中相应的注释即可查看每种粒子效果。迷失太空是本文中最后综合应用粒子知识实现的一个创意页面,下图就是它的实现效果。整个页面类似科幻电影《地心引力》的海报,宇航员不慎迷失太空,逃逸向无边无际的宇宙深处 🌌
。在屏幕上使用鼠标移动,宇航员 👨🚀
和 星辰 ✨
都会根据鼠标的移动产生相反方向的位移动画。
打开以下链接中的任意一个,在线预览效果,大屏访问效果更佳。
👁🗨
在线预览地址1:dragonir.github.io/3d/#/gravit…👁🗨
在线预览地址2:3d-eosin.vercel.app/#/gravity
本专栏系列代码托管在 Github
仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
🔗
代码仓库地址:git@github.com:dragonir/threejs-ode…
《地心引力》海报
码上掘金
实现
本文中有7个关于 Three.js
粒子效果的示例,为了方便,全都写在一个文件中,下述步骤中资源引入和场景初始化都是通用的,其他步骤 0~6
都是一个个单独的示例。在代码中,只要解开每个示例的注释,即可查看对应粒子效果。
资源引入
本示例中,除了引入样式表、Three.js
、镜头轨道控制器 OrbitControls
、模型加载器 GLTFLoader
之外,还额外引入了 dat.GUI
,它是可以通过在页面上添加一个可视化控制器来修改代码参数的库,方便动态查看和调试页面在各种参数下的渲染效果,具体用法在本文后续内容中有详细的描述。
import './style.css';
import * as THREE from 'three';
import * as dat from 'dat.gui';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
场景初始化
正式开发粒子效果之前,需要经过初始化渲染器、场景、相机、缩放事件监听、页面重绘动画调用等必备步骤。关于它们的具体原理本文不再赘述,需要了解的可前往本专栏前两章查看《Three.js 进阶之旅:基础入门(上)》、《Three.js 进阶之旅:基础入门(下)》。
// 初始化渲染器
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({ canvas: canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 初始化场景
const scene = new THREE.Scene();
// 初始化相机
const camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 0.1, 1000)
camera.position.z = 120
camera.lookAt(new THREE.Vector3(0, 0, 0))
scene.add(camera);
// 镜头控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 页面缩放事件监听
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// 更新渲染
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
// 更新相机
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
});
// 页面重绘动画
const tick = () => {
controls && controls.update();
// 更新渲染器
renderer.render(scene, camera);
// 页面重绘时调用自身
window.requestAnimationFrame(tick);
}
tick();
〇 使用THREE.Sprite创建粒子
Three.js
提供多种方法创建粒子,首先我们使用 THREE.Sprite
来通过如下的方式创建一个 20 x 30
的粒子系统。通过 new THREE.Sprite()
构造方法来创建粒子,给它传入唯一的参数材质,此时可选的材质类型只能是 THREE.SpriteMaterial
或 THREE.SpriteCanvasMaterial
。创建材质时将它的 color
属性值设置成了随机色。由于THREE.Sprite
对象继承于 THREE.Object3D
,它的大多数属性和方法都可以直接使用。示例中使用了 position
方法对粒子进行定位设置。还可以使用 scale
属性进行缩放、使用 translate
属性进行位移设置等。
const createParticlesBySprite = () => {
for (let x = -15; x < 15; x++) {
for(let y = -10; y < 10; y++) {
let material = new THREE.SpriteMaterial({
color: Math.random() * 0xffffff
});
let sprite = new THREE.Sprite(material);
sprite.position.set(x * 4, y * 4, 0);
scene.add(sprite);
}
}
}
将创建的粒子系统添加到场景中,就能看到如下图所示的粒子方块点阵。它们是一个个的彩色方块构成的网格,如果使用鼠标或触控板在场景中移动,你会发现,无论从哪个角度观察,阵列中的彩色方块看起来都没有变化,每一个粒子都是永远面向摄像机的二维平面,如果在创建粒子的时候没有指定任何属性,那么它们将会被渲染成二维的白色小方块 □
。
知识点 💡
精灵材质 THREE.SpriteMaterial
THREE.SpriteMatrial
对象的一些可修改属性及其说明。
color
:粒子的颜色。map
:粒子所用的纹理,可以是一组sprite sheet
。sizeAttenuation
:如果该属性设置为false
,那么距离摄像机的远近不影响粒子的大小,默认值为true
。opacity
:该属性设置粒子的不透明度。默认值为1
,不透明。blending
:该属性指定渲染粒子时所用的融合模式。fog
:该属性决定粒子是否受场景中雾化效果影响。默认值为true
。
① 使用THREE.Points创建粒子
通过 THREE.Sprite
你可以非常容易地创建一组对象并在场景中移动它们。当你使用少量的对象时,这会很有效,但是如果需要创建大量的粒子,如果这时候还是使用 THREE.Sprite
的话,就会产生性能问题,因为每个对象需要分别由 Three.js
进行管理。
Three.js
提供了另一种方式来处理大量的粒子,就是使用 THREE.Points
,通过 Three.Points
,Three.js
不需要管理大量 THREE.Sprite
对象,而只需要管理 THREE.Points
实例。使用这种方法创建粒子系统时,首先要创建粒子的网格 THREE.BufferGeometry
,然后创建粒子的材质 THREE.PointsMaterial
。然后创建两个数组 veticsFloat32Array
和 veticsColors
,用来管理粒子系统中每个粒子的位置和颜色,通过 THREE.Float32BufferAttribute
将它们设置为网格属性。最后使用 THREE.Points
将创建的网格和材质变为粒子系统添加到场景中。
const createParticlesByPoints = () => {
const geom = new THREE.BufferGeometry();
const material = new THREE.PointsMaterial({
size: 3,
vertexColors: true,
color: 0xffffff
});
let veticsFloat32Array = []
let veticsColors = []
for (let x = -15; x < 15; x++) {
for (let y = -10; y < 10; y++) {
veticsFloat32Array.push(x * 4, y * 4, 0);
const randomColor = new THREE.Color(Math.random() * 0xffffff);
veticsColors.push(randomColor.r, randomColor.g, randomColor.b);
}
}
const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
const colors = new THREE.Float32BufferAttribute(veticsColors, 3);
geom.attributes.position = vertices;
geom.attributes.color = colors;
const particles = new THREE.Points(geom, material);
scene.add(particles);
}
实现效果如下图所示,使用 THREE.Points
可以得到和使用 THREE.Sprite
相同的结果。
知识点 💡
点材质 THREE.PointsMaterial
上述例子中使用 THREE.PointsMaterial
来样式化粒子,它是 THREE.Points
使用的默认材质,下面列举了 THREE.PointsMaterial
中所有可设置属性及其说明。
color
: 粒子系统中所有粒子的颜色。将vertexColors
属性设置为true
,并且通过颜色属性指定了几何体的颜色来覆盖该属性。默认值为0xFFFFFF
。map
: 通过这个属性可以在粒子材质,比如可以使用canvas
、贴图等。size
:该属性指定粒子的大小,默认值为1
。sizeAnnutation
: 如果该属性设置为false
,那么所有的粒子都将拥有相同的尺寸,无论它们距离相机有多远。如果设置为true
,粒子的大小取决于其距离摄像机的距离的远近,默认值为true
。vertexColors
:通常THREE.Points
中所有的粒子都拥有相同的颜色,如果该属性设置为THREE.VertexColors
,并且几何体的颜色数组也有值,那就会使用颜色数组中的值,默认值为THREE.NoColors
。opacity
:该属性与transparent
属性一起使用,用来设置粒子的不透明度。默认值为1
(完全不透明)。transparent
:如果该属性设置为true
,那么粒子在渲染时会根据opacity
属性的值来确定其透明度,默认值为false
。blending
:该属性指定渲染粒子时的融合模式。fog
:该属性决定粒子是否受场景中雾化效果影响,默认值为true
。
② 创建样式化的粒子
在上个例子的基础上,我们改造一下创建粒子的方法,通过给 THREE.PointsMaterial
动态传入参数的方式来修改粒子的样式。为了能够实时修改参数并同时能够在页面上查看到参数改变之后的效果,我们可以使用 dat.GUI
库来实现这一功能。首先,通过 new dat.GUI()
进行初始化,然后通过 .add()
及 .addColor()
等方法为它添加控制选项,并在控制选项发生改变时在 .onChange()
中调用我们预先写好的回调函数来更新粒子样式。回调函数 ctrls
也很简单,就是通过 scene.getObjectByName("particles")
找到场景中已经创建好的粒子将它删除,然后使用新的参数再次调用 createStyledParticlesByPoints
来创建新的粒子。
const createStyledParticlesByPoints = (ctrls) => {
const geom = new THREE.BufferGeometry();
const material = new THREE.PointsMaterial({
size: ctrls.size,
transparent: ctrls.transparent,
opacity: ctrls.opacity,
color: new THREE.Color(ctrls.color),
vertexColors: ctrls.vertexColors,
sizeAttenuation: ctrls.sizeAttenuation
});
let veticsFloat32Array = []
let veticsColors = []
for (let x = -15; x < 15; x++) {
for (let y = -10; y < 10; y++) {
veticsFloat32Array.push(x * 4, y * 4, 0)
const randomColor = new THREE.Color(Math.random() * ctrls.vertexColor)
veticsColors.push(randomColor.r, randomColor.g, randomColor.b)
}
}
const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3)
const colors = new THREE.Float32BufferAttribute(veticsColors, 3)
geom.attributes.position = vertices;
geom.attributes.color = colors;
const particles = new THREE.Points(geom, material);
particles.name = 'particles';
scene.add(particles)
}
// 创建属性控制器
const ctrls = new function () {
this.size = 5;
this.transparent = true;
this.opacity = 0.6;
this.vertexColors = true;
this.color = 0xffffff;
this.vertexColor = 0x00ff00;
this.sizeAttenuation = true;
this.rotate = true;
this.redraw = function () {
if (scene.getObjectByName("particles")) {
scene.remove(scene.getObjectByName("particles"));
}
createStyledParticlesByPoints({
size: ctrls.size,
transparent: ctrls.transparent,
opacity: ctrls.opacity,
vertexColors: ctrls.vertexColors,
sizeAttenuation: ctrls.sizeAttenuation,
color: ctrls.color,
vertexColor: ctrls.vertexColor
});
};
}
const gui = new dat.GUI();
gui.add(ctrls, 'size', 0, 10).onChange(ctrls.redraw);
gui.add(ctrls, 'transparent').onChange(ctrls.redraw);
gui.add(ctrls, 'opacity', 0, 1).onChange(ctrls.redraw);
gui.add(ctrls, 'vertexColors').onChange(ctrls.redraw);
gui.addColor(ctrls, 'color').onChange(ctrls.redraw);
gui.addColor(ctrls, 'vertexColor').onChange(ctrls.redraw);
gui.add(ctrls, 'sizeAttenuation').onChange(ctrls.redraw);
添加 dat.GUI
后,页面顶部就会出现一个对应参数可视化的控制器 🎮
,用鼠标在上面下拉或滑动修改参数,就能实时更新到页面中了。赶快动手试试,看看修改不同参数对粒子的影响有何不同吧 🤩
。
知识点 💡
dat.GUI
dat.GUI
是一个轻量级的 JavaScript
图形用户界面控制库,它可以轻松地即时操作变量和触发函数,通过设定好的控制器去快捷的修改设定的变量。下面是它的一些基本使用方法:
// 初始化
const gui = new dat.GUI({ name: 'name'});
// 初始化控件属性
const ctrls = {
name: 'acqui',
speed: 0.5,
color1: '#FF0000',
color2: [0, 128, 255],
color3: [0, 128, 255, 0.3],
color4: { h: 350, s: 0.9, v: 0.3 },
test: ',
test2: ',
cb: () => {},
gender:true
};
// gui.add(控件对象变量名,对象属性名,其它参数),控制字符类型或数字
gui.add(ctrls, 'name');
// 缩放区间[0,100],变化步长10
gui.add(ctrls, 'speed', 0, 100, 10);
// 创建一个下拉选择
gui.add(ctrls, 'test', { 低速: 0.005, 中速: 0.01, 高速: 0.1 }).name('转速');
gui.add(ctrls, 'test2', ['低速', '中速', '高速']).name('转速2');
// 创建按钮
gui.add(ctrls, 'cb').name('按钮');
gui.add(ctrls, 'gender').name('性别');
// 控制颜色属性
gui.addColor(ctrls, 'color1');
// 通过name可设置别名
gui.addColor(ctrls, 'color2').name('颜色2');
// 创建一个Folder
const folder = gui.addFolder('颜色组');
folder.addColor(ctrls, 'color3');
folder.addColor(ctrls, 'color4');
//可以通过onChange方法来监听改变的值,从而修改样式
gui.addColor(ctrls, 'color2').onChange(callback);
📌
dat.GUI不仅能用到Three.js开发中,在其他需要实时修改参数查看效果的场景下都能用哦。 官网地址:github.com/dataarts/da…。
③ 使用Canvas样式化粒子
THREE.js
提供了将 HTML5
画布 Canvas
转化为纹理的功能,利用这一特性,我们就可以创建个性化的 Canvas
来美化我们的粒子效果。在本文示例 createParticlesByCanvas
方法中,首先提供了一个 createCanvasTexture
的方法用来生成 Canvas
纹理,在该方法中,我们创建一个 Canvas
画布,然后在上面绘制一个彩色渐变圆环,最后使用 THREE.CanvasTexture
方法将其转化为可以在 Three.js
中渲染的纹理。然后使用该纹理,通过 map
属性将其传递给粒子材质 THREE.PointsMaterial
。这样粒子就具有了如下图所示的 Canvas
纹理效果了。
注意 🔺
,如果此时用鼠标旋转粒子时会发现粒子四周本该透明的地方是黑色的,而且前后粒子之间还存在穿透问题。此时需要给粒子材质设置 depthTest: true
、depthWrite: false
设置这两个属性,以解决粒子显示正常的透明效果以及前后叠加层级问题。
const createParticlesByCanvas = () => {
// 使用canvas创建纹理
const createCanvasTexture = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 300
canvas.height = 300
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(170, 120);
var grd = ctx.createLinearGradient(0, 0, 170, 0);
grd.addColorStop('0', 'black');
grd.addColorStop('0.3', 'magenta');
grd.addColorStop('0.5', 'blue');
grd.addColorStop('0.6', 'green');
grd.addColorStop('0.8', 'yellow');
grd.addColorStop(1, 'red');
ctx.strokeStyle = grd;
ctx.arc(120, 120, 50, 0, Math.PI * 2);
ctx.stroke();
const texture = new THREE.CanvasTexture(canvas)
texture.needsUpdate = true
return texture
}
// 创建粒子系统
const createParticles = (size, transparent, opacity, sizeAttenuation, color) => {
const geom = new THREE.BufferGeometry()
const material = new THREE.PointsMaterial({
size: size,
transparent: transparent,
opacity: opacity,
map: texture,
sizeAttenuation: sizeAttenuation,
color: color,
depthTest: true,
depthWrite: false
})
let veticsFloat32Array = []
const range = 500
for (let i = 0; i < 400; i++) {
const particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2)
veticsFloat32Array.push(particle.x, particle.y, particle.z)
}
const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3)
geom.attributes.position = vertices
const particles = new THREE.Points(geom, material)
scene.add(particles)
}
createParticles(40, true, 1, true, 0xffffff)
}
知识点 💡
Canvas纹理CanvasTexture
用于从 Canvas
元素中创建纹理贴图。
构造函数:
CanvasTexture(canvas: HTMLElement, mapping: Constant, wrapS: Constant, wrapT: Constant, magFilter: Constant, minFilter: Constant, format: Constant, type: Constant, anisotropy: Number )
canvas
:将会被用于加载纹理贴图的Canvas
元素。mapping
:纹理贴图将被如何应用到物体上。wrapS
:默认值是THREE.ClampToEdgeWrapping
。wrapT
:默认值是THREE.ClampToEdgeWrapping
。magFilter
:当一个纹素覆盖大于一个像素时贴图将如何采样,默认值为THREE.LinearFilter
。minFilter
:当一个纹素覆盖小于一个像素时贴图将如何采样,默认值为THREE.LinearMipmapLinearFilter
。format
:在纹理贴图中使用的格式。type
:默认值是THREE.UnsignedByteType
。anisotropy
:沿着轴,通过具有最高纹素密度的像素的样本数。 默认情况下,这个值为1
。设置一个较高的值将会产生比基本的mipmap
更清晰的效果,代价是需要使用更多纹理样本。
属性和方法:
.isCanvasTexture[Boolean]
:检查是否是CanvasTexture
类型纹理的只读属性。.needsUpdate[Boolean]
:是否需要更新,默认值为true
,以便使得Canvas
中的数据能够载入。- 其他属性和方法继承于
Texture
。
④ 使用纹理贴图样式化粒子
自然,Three.js
中粒子材质也可以直接使用 THREE.TextureLoader
加载图片作为纹理进行粒子样式个性化设置。createParticlesByTexture
方法和 createParticlesByCanvas
除了在给 THREE.PointsMaterial
设置 map
属性之处有区别之外,其他地方完全相同,因此下面代码只保留了关键内容,详情可查看源码。
const createParticlesByTexture = () => {
const createParticles = (size, transparent, opacity, sizeAttenuation, color) => {
// ...
const material = new THREE.PointsMaterial({
'size': size,
'transparent': transparent,
'opacity': opacity,
// 加载自定义图片作为粒子纹理
'map': new THREE.TextureLoader().load('/images/heart.png'),
'sizeAttenuation': sizeAttenuation,
'color': color,
depthTest: true,
depthWrite: false
})
// ...
}
}
本文示例中采用了一张霓虹心形爱心 💖
图片作为粒子纹理,效果如下图所示:
⑤ 从高级几何体创建粒子
THREE.Points
是基于几何体的顶点来渲染每个粒子的,利用这一特性我们就可以从高级几何体来创建几何体形状的粒子。下面示例中我们利用 THREE.SphereGeometry
来创建一个球形的粒子系统。为了营造出好看视觉效果效果,我们可以使用 Canvas
的渐变方法 createRadialGradient
创建出一种类似发光特效来作为粒子的纹理。
const createParticlesByGeometry = () => {
// 创建发光canvas纹理
const generateSprite = () => {
const canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
const context = canvas.getContext('2d');
const gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.2, 'rgba(0, 255, 0, 1)');
gradient.addColorStop(0.4, 'rgba(0, 120, 20, 1)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;
}
// 创建立方体
const sphereGeometry = new THREE.SphereGeometry(15, 32, 16);
// 创建粒子材质
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 3,
transparent: true,
blending: THREE.AdditiveBlending,
map: generateSprite(),
depthWrite: false
})
const particles = new THREE.Points(sphereGeometry, material)
scene.add(particles)
}
⑥ 迷失太空
最后,我们利用上述汇总介绍的的粒子系统知识,并结合本专栏前几期的内容进行实践,打造一个趣味的 3D
页面,在无边无际的宇宙,宇航员跌向无尽深渊。
创建宇航员 👨🚀
首先使用 GLTFLoader
模型加载器加载宇航员 👨🚀
模型到场景中,并调整好模型在场景中的初始大小和位置。
const loader = new GLTFLoader();
loader.load('/models/astronaut.glb', mesh => {
astronaut = mesh.scene;
astronaut.material = new THREE.MeshLambertMaterial();
astronaut.scale.set(.0005, .0005, .0005);
astronaut.position.z = -10;
scene.add(astronaut);
});
创建宇宙粒子 🌌
使用上述创建粒子系统的方法,创建 1000
个粒子作为宇宙星系,并为它们设置限定范围内随机的位置,星系粒子纹理采用了一张径向渐变的贴图,将其添加到场景中。
const geom = new THREE.BufferGeometry();
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 10,
alphaTest: .8,
map: new THREE.TextureLoader().load('/images/particle.png')
});
let veticsFloat32Array = []
let veticsColors = []
for (let p = 0; p < 1000; p++) {
veticsFloat32Array.push(
rand(20, 30) * Math.cos(p),
rand(20, 30) * Math.sin(p),
rand(-1500, 0)
);
const randomColor = new THREE.Color(Math.random() * 0xffffff);
veticsColors.push(randomColor.r, randomColor.g, randomColor.b);
}
const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
const colors = new THREE.Float32BufferAttribute(veticsColors, 3);
geom.attributes.position = vertices;
geom.attributes.color = colors;
const particleSystem = new THREE.Points(geom, material);
scene.add(particleSystem);
场景优化 ✨
根据场景内摄像机的位置以及宇宙星系粒子在Z轴方向的变化,添加合适参数的黑色雾化效果来营造出由近到远星系由亮到暗的变化效果,增强画面的真实性。接着,为了宇航员显示在场景中,根据实际参数增加一些光照效果。
// 雾化效果
scene.fog = new THREE.FogExp2(0x000000, 0.005);
// 设置光照
let light = new THREE.PointLight(0xFFFFFF, 0.5);
light.position.x = -50;
light.position.y = -50;
light.position.z = 75;
scene.add(light);
light = new THREE.PointLight(0xFFFFFF, 0.5);
light.position.x = 50;
light.position.y = 50;
light.position.z = 75;
scene.add(light);
light = new THREE.PointLight(0xFFFFFF, 0.3);
light.position.x = 25;
light.position.y = 50;
light.position.z = 200;
scene.add(light);
light = new THREE.AmbientLight(0xFFFFFF, 0.02);
scene.add(light);
知识点 💡
雾化效果Fog和FogExp2
为了增强场景的真实性,示例中使用了 FogExp2
雾化效果,那么 THREE.Fog
和 THREE.FogExp2
有什么不同呢?
- 雾Fog
- 定义:表示线性雾,雾的密度是随着距离线性增大的,即场景中物体雾化效果随着随距离线性变化。
- 构造函数:
Fog(color, near, far)
.color
:表示雾的颜色,场景中远处物体为黑色,场景中最近处距离物体是自身颜色,最远和最近之间的物体颜色是物体本身颜色和雾颜色的混合效果。.near
:表示应用雾化效果的最小距离,距离活动摄像机长度小于.near
的物体将不会被雾所影响。.far
: 表示应用雾化效果的最大距离,距离活动摄像机长度大于.far
的物体将不会被雾所影响。
- 指数雾FogExp2
- 定义:表示指数雾,即雾的密度随着距离指数而增大。
- 构造函数:
FogExp2(color, density)
.color
:表示雾的颜色。.density
:表示雾的密度的增速,默认值为0.00025
。
添加动画效果 🎦
整个场景的动画分为以下三个部分:
- 粒子系统动画由以下两部分构成:
- 粒子系统旋转动画:使用余弦函数
Math.cos(t)
改变粒子系统的position
来创建旋转运动轨迹,其中t
指定了旋转速度。 - 粒子系统由近到远动画:通过遍历更改构成粒子的向量数组中表示每个粒子的
Z轴
方向的值来更新粒子系统实现。
- 粒子系统旋转动画:使用余弦函数
- 宇航员模型动画:
- 宇航员旋转动画:通过修改宇航员在
x
、y
、z
三个坐标轴上的位置参数实现。 - 宇航员由近到远动画:使用正弦函数
Math.sin(t)
来生成宇航员远近循环动画。
- 宇航员旋转动画:通过修改宇航员在
- 渲染器和相机的页面重绘更新动画。
const updateParticles = () => {
// 粒子系统旋转动画
particleSystem.position.x = 0.2 * Math.cos(t);
particleSystem.position.y = 0.2 * Math.cos(t);
particleSystem.rotation.z += 0.015;
camera.lookAt(particleSystem.position);
// 粒子系统由近到远动画
for (let i = 0; i < veticsFloat32Array.length; i++) {
// 如果是Z轴值,则修改数值
if ((i + 1) % 3 === 0) {
const dist = veticsFloat32Array[i] - camera.position.z;
if (dist >= 0) veticsFloat32Array[i] = rand(-1000, -500);
veticsFloat32Array[i] += 2.5;
const _vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
geom.attributes.position = _vertices;
}
}
particleSystem.geometry.verticesNeedUpdate = true;
}
const updateMeshes = () => {
if (astronaut) {
// 宇航员由近到远动画
astronaut.position.z = 0.08 * Math.sin(t) + (camera.position.z - 0.2);
// 宇航员旋转动画
astronaut.rotation.x += 0.015;
astronaut.rotation.y += 0.015;
astronaut.rotation.z += 0.01;
}
}
// 场景和相机更新
const updateRenderer = () => {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}
const tick = () => {
updateParticles();
updateMeshes();
updateRenderer();
renderer.render(scene, camera);
requestAnimationFrame(tick);
t += 0.01;
}
其中 rand
方法用于生成两数之间的随机数。
const rand = (min, max) => min + Math.random() * (max - min);
鼠标移动交互 🖱
为了增强页面的可交互性,可以添加一些鼠标效果。在 迷失太空
这个示例中,当鼠标 🖱
在页面上进行移动时,场景中的宇航员 👨🚀
以及相机 📷
的位置会根据鼠标的相反方向发生部分偏移。
window.addEventListener('mousemove', e => {
const cx = window.innerWidth / 2;
const cy = window.innerHeight / 2;
const dx = -1 * ((cx - e.clientX) / cx);
const dy = -1 * ((cy - e.clientY) / cy);
camera.position.x = dx * 5;
camera.position.y = dy * 5;
astronaut.position.x = dx * 5;
astronaut.position.y = dy * 5;
});
CSS样式优化 💥
最后,使用 CSS
在页面上添加一些装饰性文案和图片,其中的 GRAVITY
字样使用了一种免费的字体,大家可以选择自己喜欢的样式风格进行页面修饰,提升页面的整体视觉效果。
到此,本文涉及 Three.js
粒子系统的全部内容就结束了,完整代码可访问以下地址查看。
总结
本文中主要包含的知识点包括:
- 使用
THREE.Sprite
创建粒子。 - 使用
THREE.Points
创建粒子。 - 创建样式化的粒子。
- 使用
dat.GUI
动态控制页面参数。 - 使用
Canvas
样式化粒子。 - 使用纹理贴图样式化粒子。
- 从高级几何体创建粒子。
- 给场景添加
Fog
和FogExp2
雾化效果。 - 使用正弦余弦函数添加模型动画、鼠标移动动画等。
想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 👍。
附录
- [1]. 🌴 Three.js 打造缤纷夏日3D梦中情岛
- [2]. 🔥 Three.js 实现炫酷的赛博朋克风格3D数字地球大屏
- [3]. 🐼 Three.js 实现2022冬奥主题3D趣味页面,含冰墩墩
- [4]. 🦊 Three.js 实现3D开放世界小游戏:阿狸的多元宇宙
- [5]. 🏆 掘金1000粉!使用Three.js实现一个创意纪念页面
...
- 【Three.js 进阶之旅】系列专栏访问 👈
- 更多往期【3D】专栏访问 👈
- 更多往期【前端】专栏访问 👈
参考
- [1]. 《Three.js 开发指南——基于WebGL和HTML5在网页上渲染3D图形和动画》
- [2]. https://threejs.org