Three.js案例
Three.js 行星光晕粒子特效

Three.js 行星光晕粒子特效

本篇文章中,使用 Three.js 构建一个具有行星、光晕和粒子特效的 3D 场景。

我们模拟了一个小型“太空”环境,有以下几个主要元素:

  1. 行星:使用球体几何体和基础纹理、凹凸贴图、高光贴图来增强细节。
  2. 光晕(Shader):通过自定义着色器模拟行星周围的光晕。
  3. 粒子系统:通过大量随机分布的四面体几何来模拟星空背景。
  4. 光源:通过环境光、半球光和定向光来让场景更有层次感。

代码解析

1. 初始化渲染器(Renderer)

import * as THREE from "three";
 
const renderer = new THREE.WebGLRenderer({
  antialias: true, // 启用抗锯齿,使边缘更平滑
  alpha: true, // 启用透明背景
});
renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.autoClear = false;
renderer.setClearColor(0x000000, 0.0);
document.body.appendChild(renderer.domElement);
  • WebGLRenderer:为我们提供了 3D 绘制的能力。
  • antialias:抗锯齿可以让图形边缘更加平滑。
  • alpha:开启透明背景,方便实现一些特效叠加。
  • renderer.setPixelRatio(...):根据设备像素比设置更精细的渲染,适配视网膜屏等高分辨率设备。
  • renderer.autoClear = false:在每帧绘制之前不自动清除画面,方便后期叠加某些特效。
  • setClearColor(0x000000, 0.0):将背景色设置为透明的黑色。

2. 创建场景(Scene)

const scene = new THREE.Scene();
  • Scene:场景是 Three.js 中所有 3D 对象、光照等的容器。

3. 设置相机(Camera)

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 400;
scene.add(camera);
  • PerspectiveCamera:透视相机,模拟人眼观察物体的视觉。
    • 第一个参数是视场角度(FOV,field of view),这里是 75°。
    • 第二个参数是宽高比,用于修正因屏幕宽高比不同造成的图像变形。
    • 第三个参数和第四个参数分别是近、远裁剪面,控制相机可见范围。
  • camera.position.z = 400:将相机在 Z 轴上往后移动,保证能看到场景中的物体。

4. 创建主要 3D 容器对象

const circle = new THREE.Object3D();
const particle = new THREE.Object3D();
const halo = new THREE.Object3D();
const luminor = new THREE.Object3D();
const lights = [];
 
scene.add(circle);
scene.add(particle);
scene.add(halo);
scene.add(luminor);
  • 这里定义了四个 Object3D 容器:
    • circle 用于存放行星;
    • particle 用于存放粒子系统;
    • halo 用于存放光晕;
    • luminor 用于存放光源;
    • 最后再把它们都加入到场景中,方便后续整体变换、管理。

5. 定义几何体(Geometry)

const geometry = new THREE.TetrahedronGeometry(1, 1); // 粒子使用的四面体
const geo_planet = new THREE.SphereGeometry(10, 64, 32); // 行星使用的球体
const geom3 = new THREE.SphereGeometry(16, 32, 16); // 光晕使用的球体
  • TetrahedronGeometry:四面体几何,用于粒子星空的散点效果。
  • SphereGeometry:球体几何,用于行星和光晕的基础模型。

6. 粒子材质(Material)

const material = new THREE.MeshPhongMaterial({
  color: 0x111111,
  shading: THREE.FlatShading,
});
  • 使用 MeshPhongMaterial 实现基础的光照模型。
  • color: 0x111111:粒子呈暗灰色。
  • shading: THREE.FlatShading:平面着色,更简约的视觉效果。

7. 创建粒子系统

for (let i = 0; i < 500; i++) {
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
  mesh.position.multiplyScalar(200 + Math.random() * 500);
  mesh.rotation.set(Math.random() * 2, Math.random() * 2, Math.random() * 2);
  particle.add(mesh);
}

通过循环生成 500 个带有四面体几何的粒子,然后随机设置位置与旋转:

  • set(...):随机生成位置向量;
  • normalize():将该向量规范化,分布在球面上;
  • multiplyScalar(...):再通过一个随机数控制它们与场景中心的距离,范围在 200 ~ 700;
  • 随机设置旋转以打破呆板,让星空更自然;
  • 将生成的粒子加入粒子容器 particle

8. 行星材质(MeshPhongMaterial)

const mat = new THREE.MeshPhongMaterial({
  color: 0x000000,
  emissive: 0x000000,
  map: new THREE.TextureLoader().load(
    "https://upload.wikimedia.org/wikipedia/commons/2/2c/Generic_Celestia_asteroid_texture.jpg"
  ),
  bumpMap: new THREE.TextureLoader().load(
    "https://upload.wikimedia.org/wikipedia/commons/2/2c/Generic_Celestia_asteroid_texture.jpg"
  ),
  bumpScale: 0.025,
  specularMap: new THREE.TextureLoader().load(
    "https://upload.wikimedia.org/wikipedia/commons/2/2c/Generic_Celestia_asteroid_texture.jpg"
  ),
  specular: new THREE.Color("grey"),
  shininess: 3,
});
  • MeshPhongMaterial 提供了漫反射和高光反射,适合模拟天体的质感。
    • map:基础纹理贴图,用于给表面上色。
    • bumpMapbumpScale:凹凸贴图及其强度,使表面出现微小起伏,增加细节真实性。
    • specularMap:高光贴图,用于定义光照在表面产生的高光效果。
    • specularshininess:高光的颜色和强度。

9. 光晕材质(ShaderMaterial)

在这个示例中,我们使用了自定义着色器(Vertex + Fragment)。在你的项目里,可以把它们放在单独的文件 vertexShader.glslfragmentShader.glsl。下面是示例中的 GLSL 代码:

vertexShader.glsl

varying vec3 vNormal;
 
void main() {
  vNormal = normalize(normalMatrix * normal);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

fragmentShader.glsl

varying vec3 vNormal;
 
void main(){
  float intensity = pow(.7 - dot(vNormal, vec3(0., 0., .5)), 4.);
  gl_FragColor = vec4(.89, .82, .69, 1.) * intensity; // 自定义光晕色调
}
 
  • 在顶点着色器 vertexShader.glsl 中,我们将法线进行归一化计算,并传给片段着色器。
  • 在片段着色器 fragmentShader.glsl 中,通过与某个方向向量(此例中是 vec3(0., 0., .5)) 的点乘来控制光晕强度,再使用 pow() 提升光晕的“扩散感”,最终输出半透明的发光效果。

接着,用 ShaderMaterial 把它们应用到我们的光晕几何上:

const mat3 = new THREE.ShaderMaterial({
  uniforms: {},
  vertexShader: vertexShader,
  fragmentShader: fragmentShader,
  side: THREE.BackSide,
  blending: THREE.AdditiveBlending,
  transparent: true,
  opacity: 1.5,
  depthWrite: false,
});
  • side: THREE.BackSide:渲染背面,用于做包围在里面的光效。
  • blending: THREE.AdditiveBlending:加法混合,让发光区域和背景色叠加时会变得更亮。
  • depthWrite: false:避免因为深度缓冲造成自己遮挡自己。

10. 创建行星与光晕

// 行星
const planet = new THREE.Mesh(geo_planet, mat);
planet.scale.x = planet.scale.y = planet.scale.z = 15;
circle.add(planet);
 
// 光晕
const ball = new THREE.Mesh(geom3, mat3);
ball.scale.x = ball.scale.y = ball.scale.z = 16;
halo.add(ball);
 
const ball2 = new THREE.Mesh(geom3, mat3);
ball2.scale.x = ball2.scale.y = ball2.scale.z = 12;
ball2.position.set(25, 5, 1);
halo.add(ball2);
  • 将球体网格对象与材质绑定后,放大缩小以获得合适的尺寸,并把它们加到对应的容器内。
  • ball2 作为一个次级光晕,加上了相对于第一个光晕的偏移位置(25, 5, 1),在视觉上会形成微妙的浮动、层叠感。

11. 添加光源

const ambientLight = new THREE.AmbientLight(0x111111);
scene.add(ambientLight);
 
const hemiLight = new THREE.HemisphereLight(0x777777, 0x222222, 15);
hemiLight.position.set(-1, -1, 2);
luminor.add(hemiLight);
 
lights[1] = new THREE.DirectionalLight(0x999999, 4);
lights[1].position.set(-1, 0, 0.5);
 
lights[2] = new THREE.DirectionalLight(0x999999, 4);
lights[2].position.set(1, 0, 0.5);
 
scene.add(lights[1]);
scene.add(lights[2]);
  • 环境光(AmbientLight):为场景提供基础的漫射光照,让物体不会完全在阴影中。
  • 半球光(HemisphereLight):模拟来自天空和地面的光照,颜色可以分别设置天空和地面部分。
  • 定向光(DirectionalLight):像太阳光一样平行照射,让行星表面出现更逼真的明暗对比。

12. 窗口大小自适应(onWindowResize)

window.addEventListener("resize", onWindowResize, false);
 
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
  • 当浏览器窗口大小改变时,重新计算相机的宽高比并更新投影矩阵,同时调整渲染器大小,保持视觉上的正确显示。

13. 动画循环(animate)

function animate() {
  requestAnimationFrame(animate);
 
  particle.rotation.y -= 0.004;
  circle.rotation.x -= 0.001;
  circle.rotation.y -= 0.001;
  halo.rotation.z -= 0.005;
  luminor.rotation.z -= 0.005;
 
  renderer.clear();
  renderer.render(scene, camera);
}
animate();
  • 粒子系统的旋转:通过 particle.rotation.y 实现星空的慢速旋转。
  • 行星的旋转:让行星在 xy 方向同时缓慢旋转,模拟自转效果。
  • 光晕和光源容器的旋转:让光晕也进行缓慢旋转以获得流动感。
  • renderer.render(scene, camera):使用给定的相机渲染出场景。

代码

https://github.com/calmound/threejs-demo/tree/main/huiguang (opens in a new tab)

参考

https://codepen.io/tr13ze/pen/QpNbNd (opens in a new tab)