three.js 绝对是前端 3D 开发的王者,可以在前端实现各种各样的 3D 特效,只有你想不到没有它做不到
基于 three.js,我们可以实现很多漂亮的效果
下面我们来尝试着使用 three.js 实现一个简单的星系效果
准备
首先安装 three.js
接着编写一个 js 文件并初始化一个基本的场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; document.body.appendChild(renderer.domElement); camera.position.z = 20; function animate() { requestAnimationFrame(animate); controls.update();
renderer.render(scene, camera); }
animate(); window.addEventListener("resize", () => { renderer.setSize(window.innerWidth, window.innerHeight); camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); });
|
引入这个 js 文件到 html 页面中,我们就可以看到一个全屏的黑色场景,这样我们的环境就初始化成功了
实现
接着我们要思考如何创建一个星系效果
首先我们知道星系是由很多个小粒子组成,这些粒子围绕着中心旋转,形成一个圆形的结构,粒子会组成一个个星臂
我们先来定义参数,包括星星的数量,大小,颜色,分支数量,半径
1 2 3 4 5 6 7 8 9 10
| let points = null; const params = { count: 10000, size: 0.6, color: "#2682C7", branch: 6, radius: 20, rotateScale: 0.2, endColor: "#CF2C72", };
|
然后我们需要给星星一个贴图,要不然形成的粒子会是一个个的正方形结构
图片预览
图片的地址可以替换成你自己的地址
1 2 3
| const particleTexture = new THREE.TextureLoader().load( "https://img.hoshinagi.top/blog/post/front/three/particle/5.png" );
|
接下来我们就可以开始编写生成星系的函数了,three.js 中的物体都是由几何形状和材质组成的,所以我们需要先创建几何形状,然后再创建材质,最后将物体添加到场景中
星星的几何形状是点状,所以我们只需要给一个点的位置就好了
那么点的位置是如何计算的呢?
首先我们得知道当前点在哪个分支,为了使各个分支平均,我们可以在遍历生成点的时候,使用当前遍历的索引模除分支的数量,就可以得到处于那个分支了
得到了点处于那个分支,接着再乘以分支对应的角度,就可以得到当前点相对于中心点的角度了
每个粒子根据其在总数中的索引 i 和分支数 params.branch 计算出它所在分支的角度。这里使用了模运算 % 来确保角度在 0 到 2π 之间循环。
1
| const branchAngle = (i % params.branch) * ((Math.PI * 2) / params.branch);
|
计算粒子与中心的距离,这里使用了 Math.pow(Math.random(), 3) 来使得距离的分布更偏向于中心,即粒子更密集于星系的中心区域。
1
| const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);
|
计算一个随机偏移,让粒子看起来更加自然,每个粒子在 X、Y 和 Z 方向上添加了随机偏移。这些偏移量是通过随机数生成的,并且使用了立方函数 Math.pow(…, 3) 来使得偏移量更偏向于零,即粒子更倾向于在其原始位置附近。
1 2 3 4 5 6
| const randomX = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5; const randomY = (Math.pow(Math.random() * 4 - 2, 3) * (params.radius - distance)) / 5; const randomZ = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
|
确定粒子的最终位置
1 2 3 4 5 6
| const current = i * 3; positions[current] = Math.cos(branchAngle + distance * params.rotateScale) * distance + randomX; positions[current + 1] = randomY * (1 - distance / params.radius); positions[current + 2] = Math.sin(branchAngle + distance * params.rotateScale) * distance + randomZ;
|
- X 坐标:使用余弦函数 Math.cos 计算,结合分支角度和距离,再加上 X 方向的随机偏移。
- Y 坐标:直接使用 Y 方向的随机偏移,并乘以一个因子 (1 - distance / params.radius) 使得距离中心越远的粒子在 Y 方向上的偏移越小。
- Z 坐标:使用正弦函数 Math.sin 计算,结合分支角度和距离,再加上 Z 方向的随机偏移。
总结上述代码,得到生成星系的函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| const genertateGalaxy = () => { const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(params.count * 3); const colors = new Float32Array(params.count * 3); const centerColor = new THREE.Color(params.color); const endColor = new THREE.Color(params.endColor); for (let i = 0; i < params.count; i++) { const branchAngle = (i % params.branch) * ((Math.PI * 2) / params.branch); const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);
const randomX = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5; const randomY = (Math.pow(Math.random() * 4 - 2, 3) * (params.radius - distance)) / 5; const randomZ = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
const current = i * 3; positions[current] = Math.cos(branchAngle + distance * params.rotateScale) * distance + randomX; positions[current + 1] = randomY * (1 - distance / params.radius); positions[current + 2] = Math.sin(branchAngle + distance * params.rotateScale) * distance + randomZ;
const mixColor = centerColor.clone(); mixColor.lerp(endColor, distance / params.radius); colors[current] = mixColor.r; colors[current + 1] = mixColor.g; colors[current + 2] = mixColor.b; } geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ size: params.size * (Math.random() * 0.5 + 0.5), depthWrite: false, blending: THREE.AdditiveBlending, map: particleTexture, alphaMap: particleTexture, transparent: true, vertexColors: true, }); points = new THREE.Points(geometry, material); scene.add(points); };
|
为了让星系旋转,我们需要修改渲染函数如下,让星系绕 Y 轴不断旋转
1 2 3 4 5 6 7 8 9
| function animate() { requestAnimationFrame(animate); controls.update(); points.rotation.y += 0.005; if (points.rotation.y > Math.PI * 2) { points.rotation.y = 0; } renderer.render(scene, camera); }
|
效果预览
最终代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; document.body.appendChild(renderer.domElement); camera.position.z = 20;
const params = { count: 10000, size: 0.6, color: "#2682C7", branch: 6, radius: 20, rotateScale: 0.2, endColor: "#CF2C72", };
let points = null;
const particleTexture = new THREE.TextureLoader().load( "https://img.hoshinagi.top/blog/post/front/three/particle/5.png" );
const genertateGalaxy = () => { const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(params.count * 3); const colors = new Float32Array(params.count * 3);
const centerColor = new THREE.Color(params.color); const endColor = new THREE.Color(params.endColor); for (let i = 0; i < params.count; i++) { const branchAngle = (i % params.branch) * ((Math.PI * 2) / params.branch); const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);
const randomX = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5; const randomY = (Math.pow(Math.random() * 4 - 2, 3) * (params.radius - distance)) / 5; const randomZ = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
const current = i * 3; positions[current] = Math.cos(branchAngle + distance * params.rotateScale) * distance + randomX; positions[current + 1] = randomY * (1 - distance / params.radius); positions[current + 2] = Math.sin(branchAngle + distance * params.rotateScale) * distance + randomZ;
const mixColor = centerColor.clone(); mixColor.lerp(endColor, distance / params.radius); colors[current] = mixColor.r; colors[current + 1] = mixColor.g; colors[current + 2] = mixColor.b; } geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ size: params.size * (Math.random() * 0.5 + 0.5), sizeAttenuation: true, depthWrite: false, blending: THREE.AdditiveBlending, map: particleTexture, alphaMap: particleTexture, transparent: true, vertexColors: true, }); points = new THREE.Points(geometry, material); scene.add(points); }; genertateGalaxy();
function animate() { requestAnimationFrame(animate); controls.update(); points.rotation.y += 0.005; if (points.rotation.y > Math.PI * 2) { points.rotation.y = 0; } renderer.render(scene, camera); }
animate(); window.addEventListener("resize", () => { renderer.setSize(window.innerWidth, window.innerHeight); camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); });
|