学习Three.js--风车星系
前置核心说明
开发目标
基于Three.js的粒子系统+自定义着色器实现真实感M101风车星系效果,还原星系的核心结构与视觉特征,核心能力包括:
- 模拟星系分层结构:核心核球(Bulge)+ 螺旋旋臂(Spiral Arms),还原真实星系的形态;
- 采用对数螺旋算法生成旋臂,实现自然流畅的风车状螺旋结构,符合真实星系的旋臂规律;
- 粒子分层差异化配置:核球与旋臂使用不同的粒子分布、颜色、尺寸,模拟真实恒星的视觉差异;
- 借助
ShaderMaterial实现圆形抗锯齿粒子与加法混合光晕,提升星系的细腻度与真实感; - 实现微弱粒子脉动+星系整体自转,营造动态的宇宙星系氛围,支持轨道交互查看。

核心技术栈(关键知识点)
| 技术点 | 作用 |
|---|---|
THREE.BufferGeometry + 自定义attribute |
高效存储10万级粒子数据(顶点、颜色、尺寸),为每个粒子提供独立属性,支撑分层差异化效果 |
对数螺旋公式(r = a * e^(b*θ)) |
生成自然流畅的星系旋臂,实现风车状螺旋结构,是模拟螺旋星系的核心算法 |
| 粒子分层生成策略(核球+旋臂) | 不同区域采用不同的分布逻辑、视觉参数,还原星系的真实分层结构,提升场景真实感 |
THREE.ShaderMaterial(顶点/片元着色器) |
1. 实现圆形抗锯齿粒子,替代默认方形粒子;2. 传递粒子独立颜色/尺寸,实现差异化视觉;3. 实现微弱脉动动画,兼顾性能与效果 |
THREE.AdditiveBlending(加法混合) |
粒子颜色亮度叠加,营造星系的朦胧光晕感,提升旋臂与核球的层次感,模拟宇宙星尘的发光效果 |
| 星系双动画逻辑(粒子脉动+整体自转) | 微观粒子微弱脉动+宏观星系缓慢自转,营造动态且真实的宇宙氛围,避免场景静态生硬 |
THREE.OrbitControls(轨道控制器) |
支持拖拽旋转/滚轮缩放,全方位查看3D星系结构,便捷观察旋臂与核球的细节 |
核心开发流程
初始化场景/相机/渲染器/控制器
粒子数据分层生成(核球+旋臂)
构建BufferGeometry(绑定顶点+颜色+尺寸自定义attribute)
创建ShaderMaterial(配置uniforms+顶点/片元着色器,实现圆形粒子+光晕)
创建Points粒子对象并添加到场景
动画循环(更新时间uniform+粒子脉动+星系自转+控制器阻尼)
窗口适配+持续渲染
分步开发详解
步骤1:基础环境搭建(场景/相机/渲染器/控制器)
搭建Three.js 3D场景的基础框架,为星系提供展示载体,保证场景的流畅交互与高清渲染。
1.1 核心代码
javascript
// 导入Three.js核心库与轨道控制器
import * as THREE from 'https://esm.sh/three@0.174.0';
import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
// 1. 场景初始化(纯黑背景,最大化衬托星系的光晕效果,模拟宇宙真空环境)
const scene = new THREE.Scene();
// 2. 透视相机(适配3D星系场景,兼顾核球细节与旋臂整体查看)
const camera = new THREE.PerspectiveCamera(
60, // 视角(FOV):60°视野适中,无星系变形
innerWidth / innerHeight, // 宽高比:适配浏览器窗口
0.1, // 近裁切面:过滤过近无效对象,提升性能
2000 // 远裁切面:保证星系完整处于可见范围,支持远距离缩放查看
);
camera.position.set(0, 20, 80); // 高位侧视:既清晰查看旋臂的风车结构,又能观察核球细节
// 3. 渲染器(抗锯齿,提升星系粒子边缘细腻度,避免光晕锯齿感)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // 高清适配:Retina屏幕无模糊
document.body.appendChild(renderer.domElement);
// 4. 轨道控制器(支持拖拽旋转/滚轮缩放,便捷查看3D星系结构)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼:拖拽旋转有惯性,交互更顺滑自然
1.2 关键说明
- 相机位置 :
(0, 20, 80)采用「高位+稍远」视角,既可以完整捕捉风车星系的4条旋臂结构,又能清晰观察核心核球的细节,避免视角过近导致旋臂变形、光晕过曝。 - 渲染器
antialias: true:开启抗锯齿,配合后续片元着色器的smoothstep抗锯齿逻辑,让星系粒子的边缘和光晕更细腻,这对明亮的核球与旋臂尤为重要。 - 控制器阻尼:启用阻尼后,交互体验更贴近真实3D场景,适合长时间查看星系的自转与脉动效果,避免拖拽后瞬间停止的生硬感。
步骤2:粒子数据分层生成(核球+旋臂,星系核心形态实现)
这是风车星系的核心步骤,采用分层生成策略,分别生成核球与旋臂的粒子数据,通过不同的分布逻辑、颜色、尺寸,还原星系的真实结构,其中对数螺旋算法是旋臂实现的关键。
2.1 核心代码
javascript
// 初始化粒子数据数组(集中存储,后续绑定到BufferGeometry)
const pointsArr = []; // 粒子顶点坐标数组
const colors = []; // 粒子颜色数组(每个粒子独立颜色,实现分层视觉差异)
const sizes = []; // 粒子尺寸数组(每个粒子独立尺寸,提升星系细腻度)
// ---- 分层1:核球(Bulge)- 星系中心密集区域,模拟老年恒星集群 ----
const bulgeCount = 8000; // 核球粒子数(8000,保证密集度且不卡顿)
for (let i = 0; i < bulgeCount; i++) {
// 幂次采样(Math.pow(Math.random(), 3)):让粒子高度集中在中心,模拟真实核球的恒星分布
// 3次方让随机值更偏向0,粒子密集在中心区域,外层逐渐稀疏
const radius = Math.pow(Math.random(), 3) * 6;
// 随机方向生成点,再乘以半径,得到核球内的均匀分布粒子
const point = new THREE.Vector3().randomDirection().multiplyScalar(radius);
pointsArr.push(point);
// 核球颜色:黄白色(模拟老年恒星,亮度高、偏暖色调)
colors.push(1.0, 0.9, 0.7);
// 核球粒子尺寸:0.2~1.0,较小且密集,体现核球的紧凑感
sizes.push(Math.random() * 0.8 + 0.2);
}
// ---- 分层2:旋臂(Spiral Arms)- 风车状螺旋结构,模拟新生恒星/星云 ----
const armCount = 92000; // 旋臂粒子数(92000,保证旋臂的细腻度与流畅感)
const arms = 4; // 旋臂数量:4条,还原M101风车星系的经典形态
const armSpread = 0.4; // 旋臂松紧度(越小越紧,越大越松散)
const maxRadius = 60; // 旋臂最大半径,决定星系的整体大小
for (let i = 0; i < armCount; i++) {
// 随机分配旋臂,保证4条旋臂的粒子分布均匀
const armIndex = Math.floor(Math.random() * arms);
// 旋臂角度偏移:让4条旋臂均匀分布在360°范围内,无重叠
const angleOffset = (armIndex / arms) * Math.PI * 2;
// 核心算法:对数螺旋(r = a * e^(b*θ)),生成自然流畅的螺旋结构
const theta = Math.random() * Math.PI * 8; // 螺旋角度:8π对应4圈,保证旋臂的长度与流畅度
// 归一化计算螺旋半径,让旋臂刚好延伸到maxRadius,避免超出边界
const radius = Math.exp(theta * armSpread) * (maxRadius / Math.exp(Math.PI * 8 * armSpread));
// 径向扰动:让旋臂有"宽度",避免旋臂过于纤细、生硬,模拟真实旋臂的厚度
const radialJitter = (Math.random() - 0.5) * 3;
const finalRadius = Math.min(radius + radialJitter, maxRadius); // 限制最大半径,避免粒子超出星系范围
// 角度扰动:让旋臂的粒子分布更自然,避免螺旋线过于规整,提升真实感
const angle = theta + angleOffset + (Math.random() - 0.5) * 0.3;
// 旋臂高度:-1~1,形成薄盘结构,模拟真实星系的旋臂平面特性
const height = (Math.random() - 0.5) * 2;
// 从极坐标转换为直角坐标,生成旋臂粒子的坐标
const x = Math.cos(angle) * finalRadius;
const z = Math.sin(angle) * finalRadius;
pointsArr.push(new THREE.Vector3(x, height, z));
// 旋臂颜色:蓝白色(模拟新生恒星/星云,偏冷色调,与核球形成视觉差异)
colors.push(0.7, 0.85, 1.0);
// 旋臂粒子尺寸:0.3~1.5,比核球稍大,体现旋臂的视觉层次感
sizes.push(Math.random() * 1.2 + 0.3);
}
2.2 关键技术点解析
-
粒子分层生成策略(核球vs旋臂):
- 核球 :采用
randomDirection()+幂次采样,粒子高度集中在中心,颜色为黄白色(老年恒星),尺寸较小且密集,体现核球的紧凑、温暖、明亮的特性; - 旋臂:采用对数螺旋算法,粒子沿螺旋线分布,颜色为蓝白色(新生恒星/星云),尺寸稍大且有厚度,体现旋臂的舒展、冷艳、有层次的特性;
- 分层设计的核心是还原真实星系的结构差异,通过视觉参数(颜色、尺寸)与分布逻辑的不同,让星系更具真实感。
- 核球 :采用
-
对数螺旋算法(旋臂实现的核心):
- 公式:
r = a * e^(b*θ),其中r为螺旋半径,θ为螺旋角度,b为螺旋松紧度(对应代码中的armSpread),a为比例系数; - 代码中通过
Math.exp(theta * armSpread)计算螺旋半径,再通过归一化处理让旋臂刚好延伸到maxRadius,避免超出边界; armSpread越小,螺旋越紧凑,旋臂越"卷";越大,螺旋越松散,旋臂越"舒展",这是调整旋臂形态的核心参数。
- 公式:
-
扰动优化(旋臂真实感提升):
- 径向扰动 :
radialJitter让粒子在半径方向有轻微偏移,形成旋臂的"宽度",避免旋臂成为一条纤细的线; - 角度扰动:让粒子在螺旋角度上有轻微偏移,避免旋臂的螺旋线过于规整,模拟真实星系旋臂的不规则性;
- 高度限制 :旋臂粒子的Y轴高度限制在
-1~1,形成薄盘结构,符合真实星系旋臂的平面特性,避免旋臂呈现立体球状。
- 径向扰动 :
步骤3:构建BufferGeometry(绑定粒子数据,支撑着色器渲染)
将步骤2生成的粒子数据(顶点、颜色、尺寸)绑定到BufferGeometry,并通过自定义attribute向着色器传递粒子的独立颜色与尺寸,为后续着色器渲染提供数据支撑。
3.1 核心代码
javascript
// 1. 构建BufferGeometry,绑定粒子顶点坐标(星系粒子的基础位置数据)
const geometry = new THREE.BufferGeometry().setFromPoints(pointsArr);
// 2. 添加自定义attribute:color(粒子颜色,每个粒子3个分量:R/G/B)
// 第二个参数「3」表示每个顶点的分量数为3,与colors数组的每3个元素对应一个粒子
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
// 3. 添加自定义attribute:size(粒子尺寸,每个粒子1个值)
// 第二个参数「1」表示每个顶点的分量数为1,与sizes数组的每个元素一一对应
geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));
3.2 关键技术点解析
BufferGeometry的高效性 :直接操作二进制数组存储数据,渲染时减少CPU与GPU之间的数据传输开销,适合10万级粒子场景(本次总粒子数10万),性能远优于已被废弃的普通Geometry。- 自定义
attribute的核心作用 :向着色器传递「每个粒子的独立数据」,本次传递了color(颜色)与size(尺寸),让着色器能够为每个粒子渲染不同的颜色与尺寸,实现星系的分层视觉差异。 Float32BufferAttribute:最常用的BufferAttribute类型,存储32位浮点型数据,兼顾精度与性能,适合传递粒子颜色、尺寸等数据,着色器中需声明同名attribute变量才能访问对应数据。
步骤4:创建自定义ShaderMaterial(星系视觉效果的核心)
通过ShaderMaterial自定义顶点着色器与片元着色器,实现圆形抗锯齿粒子、粒子独立颜色/尺寸渲染、微弱脉动动画,同时通过加法混合营造星系的朦胧光晕,提升真实感。
4.1 核心代码
javascript
// 构建ShaderMaterial,配置全局uniforms和着色器,实现星系的核心视觉效果
const material = new THREE.ShaderMaterial({
// 1. 全局uniforms(向着色器传递全局统一数据,此处为时间,驱动脉动动画)
uniforms: {
uTime: { value: 0 } // 全局时间:驱动所有粒子的脉动动画同步更新
},
// 2. 顶点着色器(处理粒子位置、颜色、尺寸,实现微弱脉动与透视变换)
vertexShader: `
uniform float uTime;
attribute vec3 color; // 粒子颜色(自定义attribute,每个粒子独立)
attribute float size; // 粒子尺寸(自定义attribute,每个粒子独立)
varying vec3 vColor; // 传递给片元着色器的颜色(varying变量,实现平滑插值)
void main() {
vec3 pos = position;
// 微弱脉动动画:基于粒子距离与时间的球面扰动,提升星系的动态感
// sin函数实现周期性脉动,0.1为脉动幅度,避免过于剧烈影响星系形态
float pulse = sin(uTime * 0.5 + length(pos)) * 0.1;
pos += normalize(pos) * pulse; // 沿粒子径向脉动,保持星系整体形态不变
// 透视变换:将3D粒子坐标转换为2D屏幕坐标,3D渲染必备
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); // 模型视图矩阵:局部坐标→相机视角坐标
gl_Position = projectionMatrix * mvPosition; // 投影矩阵:相机视角坐标→屏幕裁剪坐标
// 粒子尺寸计算:透视缩放(近大远小),实现真实的3D视觉效果
gl_PointSize = size * (80.0 / -mvPosition.z); // 80.0为缩放系数,控制粒子的整体大小
gl_PointSize = min(gl_PointSize, 100.0); // 限制最大尺寸,避免远处粒子过大导致光晕过曝
// 传递粒子颜色到片元着色器,实现每个粒子的独立颜色渲染
vColor = color;
}
`,
// 3. 片元着色器(处理粒子像素颜色、形状,实现圆形抗锯齿与柔和光晕)
fragmentShader: `
varying vec3 vColor; // 从顶点着色器传递过来的粒子颜色
void main() {
// 步骤1:绘制圆形粒子(基于点坐标的UV计算,替代默认方形粒子)
vec2 uv = gl_PointCoord - 0.5; // 将点坐标从(0,0)~(1,1)转换为(-0.5,-0.5)~(0.5,0.5)
float d = length(uv); // 计算当前像素到粒子中心的距离
// 步骤2:圆形裁剪(丢弃超出圆心0.5范围的像素,形成圆形粒子)
if (d > 0.5) discard; // discard:丢弃当前像素,不渲染,实现圆形轮廓
// 步骤3:抗锯齿边缘(smoothstep实现渐隐,避免圆形边缘锯齿,提升光晕质感)
float alpha = 1.0 - smoothstep(0.45, 0.5, d); // 从0.45到0.5,alpha从1渐变到0,边缘渐隐
// 步骤4:设置最终像素颜色(粒子独立颜色+渐变Alpha,实现柔和光晕)
gl_FragColor = vec4(vColor, alpha);
}
`,
// 4. 材质附加配置(提升星系视觉效果,核心是加法混合与透明设置)
transparent: true, // 启用透明:支持粒子边缘渐隐,实现柔和光晕效果
blending: THREE.AdditiveBlending, // 加法混合:粒子颜色亮度叠加,呈现朦胧光晕,提升星系层次感
depthWrite: false // 关闭深度写入:允许粒子叠加,避免旋臂与核球互相遮挡,光晕更连贯
});
// 5. 创建Points粒子对象,添加到场景(将几何体与材质结合,形成最终的M101风车星系)
const points = new THREE.Points(geometry, material);
scene.add(points);
4.2 关键技术点解析
-
顶点着色器核心逻辑:
- 微弱脉动动画 :基于
sin(uTime * 0.5 + length(pos))实现周期性脉动,length(pos)让不同位置的粒子脉动相位不同,避免同步脉动的生硬感,0.1的脉动幅度保证星系整体形态不变,仅提升动态感; - 透视缩放 :
gl_PointSize = size * (80.0 / -mvPosition.z)实现「近大远小」的真实透视效果,让星系更具3D立体感,min函数限制最大尺寸,避免光晕过曝; - 颜色传递 :通过
varying vec3 vColor将粒子的独立颜色传递到片元着色器,Three.js会自动进行平滑插值,保证颜色过渡自然,无明显断层。
- 微弱脉动动画 :基于
-
片元着色器核心逻辑:
- 圆形粒子绘制 :通过
gl_PointCoord转换UV坐标,计算像素到粒子中心的距离,丢弃超出圆心的像素,形成圆形粒子,替代默认的方形粒子,提升星系的细腻度; - 抗锯齿边缘 :
smoothstep(0.45, 0.5, d)实现边缘渐隐,避免圆形粒子的锯齿感,同时为粒子营造柔和的光晕,这对星系的旋臂与核球尤为重要; - 独立颜色渲染 :直接使用从顶点着色器传递的
vColor,实现每个粒子的差异化颜色,还原星系核球与旋臂的视觉差异。
- 圆形粒子绘制 :通过
-
材质配置优化:
AdditiveBlending(加法混合):粒子颜色亮度叠加,越密集的地方越亮,形成自然的朦胧光晕,模拟宇宙星尘的发光效果,提升星系的真实感与层次感;depthWrite: false:关闭深度写入,允许旋臂与核球的粒子互相叠加,避免粒子之间的遮挡,保证光晕的连贯性,同时提升渲染性能;transparent: true:启用透明,支持粒子边缘的渐隐效果,配合加法混合,让星系的光晕更柔和、更自然。
步骤5:动画循环(驱动星系动态效果,实现流畅渲染)
每帧更新全局时间uTime,驱动粒子微弱脉动,同时实现星系整体自转,更新控制器阻尼,保证星系的动态效果流畅、自然,提升视觉体验。
5.1 核心代码
javascript
const clock = new THREE.Clock(); // 时钟:用于获取累计运行时间,不受帧率影响,避免动画累积误差
function animate() {
requestAnimationFrame(animate); // 绑定浏览器刷新率(通常60帧/秒),实现流畅无卡顿的动画
// 1. 获取累计运行时间,驱动着色器脉动动画
const t = clock.getElapsedTime();
material.uniforms.uTime.value = t;
// 2. 星系整体自转:绕Y轴缓慢旋转,营造真实的宇宙星系氛围
// 0.02为自转速度,缓慢旋转便于观察星系的旋臂结构
points.rotation.y = t * 0.02;
// 3. 更新轨道控制器阻尼(必须在动画循环中调用,保证阻尼效果生效)
controls.update();
// 4. 渲染场景(将场景和相机的3D信息渲染为2D画布,呈现最终的M101风车星系)
renderer.render(scene, camera);
}
// 启动动画循环,开始运行星系的脉动、自转与渲染
animate();
5.2 关键说明
clock.getElapsedTime():获取从时钟启动到当前的累计运行时间(单位:秒),相比getDelta()更适合驱动全局循环动画,避免动画因帧率波动出现累积误差,保证星系的脉动与自转效果在不同设备上一致。- 双动画逻辑(微观脉动+宏观自转) :
- 微观:粒子的微弱脉动,提升星系的动态感,避免场景过于静态;
- 宏观:星系绕Y轴缓慢自转,还原真实星系的自转特性,营造宇宙星系的氛围;
- 双动画逻辑的核心是「动静结合」,既保证星系的整体形态不变,又提升视觉体验的丰富性。
- 自转速度优化 :
0.02的自转速度较为缓慢,便于用户观察星系的旋臂与核球细节,若想提升动态感,可适当增大该值(如0.05)。
步骤6:窗口适配(响应式调整,适配不同屏幕尺寸)
保证M101风车星系在不同屏幕尺寸下都能全屏显示,且不会出现拉伸变形,适配桌面端、移动端等不同设备。
6.1 核心代码
javascript
window.addEventListener('resize', () => {
// 1. 更新相机宽高比(适配新的窗口尺寸,避免场景拉伸)
camera.aspect = window.innerWidth / window.innerHeight;
// 2. 更新相机投影矩阵(必须调用,否则宽高比修改不生效,场景会出现拉伸变形)
camera.updateProjectionMatrix();
// 3. 更新渲染器尺寸(适配新的窗口尺寸,保证星系全屏显示)
renderer.setSize(window.innerWidth, window.innerHeight);
});
6.2 关键说明
- 窗口大小变化时,同步更新相机宽高比和渲染器尺寸,保证星系在不同屏幕尺寸下都能全屏显示,且透视效果正常,不会出现拉伸变形。
camera.updateProjectionMatrix():相机参数(如宽高比)修改后,必须调用该方法更新投影矩阵,否则宽高比的修改不会生效,场景会出现明显的拉伸变形,影响星系的视觉效果。
核心技术深度解析
1. 对数螺旋算法(旋臂实现的核心)
对数螺旋是自然界中螺旋星系的普遍形态,其核心公式为:$r = a \cdot e^{b \cdot \theta}$
- 各参数含义:
$r$:螺旋线上某点到原点的半径(对应代码中的radius);$\theta$:螺旋线上某点的极角(对应代码中的theta);$b$:螺旋松紧度(对应代码中的armSpread),$b$越小,螺旋越紧凑,旋臂越"卷";$b$越大,螺旋越松散,旋臂越"舒展";$a$:比例系数,用于调整螺旋的整体大小(代码中通过归一化处理实现,让旋臂刚好延伸到maxRadius)。- 代码落地技巧:通过
Math.exp(theta * armSpread)计算螺旋半径,再通过maxRadius / Math.exp(Math.PI * 8 * armSpread)进行归一化,保证旋臂的长度与边界可控,避免超出星系范围。
2. 粒子分层生成策略(核球vs旋臂)
| 对比项 | 核球(Bulge) | 旋臂(Spiral Arms) |
|---|---|---|
| 分布逻辑 | randomDirection()+幂次采样,高度集中在中心 |
对数螺旋算法+径向/角度扰动,沿螺旋线分布 |
| 粒子数量 | 8000(密集紧凑) | 92000(舒展细腻) |
| 颜色 | 黄白色(1.0, 0.9, 0.7),模拟老年恒星 | 蓝白色(0.7, 0.85, 1.0),模拟新生恒星/星云 |
| 尺寸 | 0.2~1.0,较小且均匀 | 0.3~1.5,稍大且有差异 |
| 空间形态 | 球形(3D分布) | 薄盘形(2D平面分布,Y轴高度限制在-1~1) |
3. ShaderMaterial的性能优势
本次星系总粒子数为10万,采用ShaderMaterial能够实现流畅渲染,其核心优势在于:
- GPU并行处理:着色器逻辑运行在GPU上,具备强大的并行处理能力,可轻松应对10万级甚至百万级粒子,性能远超普通JS驱动的粒子系统;
- 减少数据传输 :通过
BufferGeometry将粒子数据一次性传递到GPU,后续动画仅需更新少量全局uniform(如uTime),减少CPU与GPU之间的数据传输开销; - 灵活的视觉定制:通过自定义着色器,可实现圆形粒子、抗锯齿、光晕等复杂视觉效果,且无需额外的几何体开销,提升渲染效率。
核心参数速查表(快速调整星系效果)
| 参数名 | 当前取值 | 作用 | 修改建议 |
|---|---|---|---|
bulgeCount |
8000 | 核球粒子数,决定核球的密集程度 | 改为5000:核球更稀疏;改为10000:核球更密集,亮度更高 |
armCount |
92000 | 旋臂粒子数,决定旋臂的细腻程度 | 改为50000:旋臂更稀疏,低配设备更流畅;改为150000:旋臂更细腻,光晕更丰富 |
arms |
4 | 旋臂数量,决定星系的风车形态 | 改为2:双旋臂星系(如银河系);改为6:六旋臂星系,更具视觉冲击力 |
armSpread |
0.4 | 旋臂松紧度,决定螺旋的紧凑程度 | 改为0.3:旋臂更紧凑,更"卷";改为0.5:旋臂更松散,更"舒展" |
maxRadius |
60 | 旋臂最大半径,决定星系的整体大小 | 改为40:星系更小更紧凑;改为80:星系更大更舒展,支持远距离缩放查看 |
| 核球颜色 | (1.0, 0.9, 0.7) | 核球视觉色调,模拟老年恒星 | 改为(1.0, 0.8, 0.5):更暖的金黄色,核球更明亮;改为(1.0, 1.0, 0.9):更浅的黄白色,核球更柔和 |
| 旋臂颜色 | (0.7, 0.85, 1.0) | 旋臂视觉色调,模拟新生恒星/星云 | 改为(0.6, 0.8, 1.0):更冷的蓝色,旋臂更鲜明;改为(0.8, 0.9, 1.0):更浅的蓝白色,旋臂更柔和 |
自转速度 t * 0.02 |
0.02 | 星系整体自转速度,决定动态氛围 | 改为0.01:自转更缓慢,便于观察细节;改为0.05:自转更快,动态感更强 |
完整优化代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>M101 风车星系 - Three.js</title>
<style>body { margin: 0; overflow: hidden; background: #000; }</style>
</head>
<body>
<script type="module">
// 导入Three.js核心库与轨道控制器
import * as THREE from 'https://esm.sh/three@0.174.0';
import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
// ========== 1. 基础环境初始化(场景/相机/渲染器/控制器) ==========
const scene = new THREE.Scene();
// 透视相机:高位侧视,清晰查看风车星系的旋臂结构与核球细节
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 2000);
camera.position.set(0, 20, 80);
// 渲染器:抗锯齿,提升星系粒子边缘与光晕的细腻度,适配高清屏幕
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// 轨道控制器:启用阻尼,实现顺滑的3D交互体验
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// ========== 2. 粒子数据分层生成(核球+旋臂,还原星系真实结构) ==========
const pointsArr = []; // 粒子顶点坐标数组
const colors = []; // 粒子颜色数组:分层差异化,实现核球与旋臂的视觉差异
const sizes = []; // 粒子尺寸数组:分层差异化,提升星系细腻度
// ---- 分层1:核球(Bulge)- 中心密集区域,模拟老年恒星集群 ----
const bulgeCount = 8000;
for (let i = 0; i < bulgeCount; i++) {
// 幂次采样:Math.pow(Math.random(), 3)让粒子高度集中在中心,模拟真实核球分布
const radius = Math.pow(Math.random(), 3) * 6;
const point = new THREE.Vector3().randomDirection().multiplyScalar(radius);
pointsArr.push(point);
// 核球颜色:黄白色(老年恒星,偏暖色调,亮度高)
colors.push(1.0, 0.9, 0.7);
// 核球尺寸:0.2~1.0,较小且密集,体现核球的紧凑感
sizes.push(Math.random() * 0.8 + 0.2);
}
// ---- 分层2:旋臂(Spiral Arms)- 风车状螺旋结构,模拟新生恒星/星云 ----
const armCount = 92000;
const arms = 4; // 旋臂数量:4条,还原M101风车星系的经典形态
const armSpread = 0.4; // 旋臂松紧度:越小越紧,越大越松散
const maxRadius = 60; // 旋臂最大半径,决定星系整体大小
for (let i = 0; i < armCount; i++) {
// 随机分配旋臂,保证4条旋臂均匀分布
const armIndex = Math.floor(Math.random() * arms);
const angleOffset = (armIndex / arms) * Math.PI * 2;
// 核心算法:对数螺旋(r = a * e^(b*θ)),生成自然流畅的螺旋结构
const theta = Math.random() * Math.PI * 8; // 螺旋角度:8π对应4圈,保证旋臂长度与流畅度
const radius = Math.exp(theta * armSpread) * (maxRadius / Math.exp(Math.PI * 8 * armSpread));
// 径向扰动:让旋臂有宽度,避免过于纤细、生硬
const radialJitter = (Math.random() - 0.5) * 3;
const finalRadius = Math.min(radius + radialJitter, maxRadius);
// 角度扰动:让旋臂粒子分布更自然,避免螺旋线过于规整
const angle = theta + angleOffset + (Math.random() - 0.5) * 0.3;
// 薄盘结构:Y轴高度限制在-1~1,符合真实星系旋臂的平面特性
const height = (Math.random() - 0.5) * 2;
// 极坐标→直角坐标,生成旋臂粒子坐标
const x = Math.cos(angle) * finalRadius;
const z = Math.sin(angle) * finalRadius;
pointsArr.push(new THREE.Vector3(x, height, z));
// 旋臂颜色:蓝白色(新生恒星/星云,偏冷色调,与核球形成视觉差异)
colors.push(0.7, 0.85, 1.0);
// 旋臂尺寸:0.3~1.5,比核球稍大,体现旋臂的视觉层次感
sizes.push(Math.random() * 1.2 + 0.3);
}
// ========== 3. 构建BufferGeometry(绑定粒子数据,支撑着色器渲染) ==========
const geometry = new THREE.BufferGeometry().setFromPoints(pointsArr);
// 添加自定义attribute:color(粒子独立颜色,3个分量)
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
// 添加自定义attribute:size(粒子独立尺寸,1个分量)
geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));
// ========== 4. 创建ShaderMaterial(核心:圆形粒子+光晕+脉动动画) ==========
const material = new THREE.ShaderMaterial({
// 全局uniforms:传递时间,驱动粒子微弱脉动动画
uniforms: {
uTime: { value: 0 }
},
// 顶点着色器:处理粒子位置、颜色、尺寸,实现微弱脉动与透视变换
vertexShader: `
uniform float uTime;
attribute vec3 color;
attribute float size;
varying vec3 vColor;
void main() {
vec3 pos = position;
// 微弱脉动动画:周期性球面扰动,提升星系动态感,保持整体形态不变
float pulse = sin(uTime * 0.5 + length(pos)) * 0.1;
pos += normalize(pos) * pulse;
// 透视变换:3D坐标→2D屏幕坐标,保证星系正确显示
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
// 透视缩放:近大远小,实现真实3D视觉效果,限制最大尺寸避免光晕过曝
gl_PointSize = size * (80.0 / -mvPosition.z);
gl_PointSize = min(gl_PointSize, 100.0);
// 传递粒子颜色到片元着色器,实现分层差异化视觉
vColor = color;
}
`,
// 片元着色器:处理粒子形状、抗锯齿、光晕,实现圆形柔和粒子
fragmentShader: `
varying vec3 vColor;
void main() {
// 圆形粒子绘制:转换UV坐标,计算到粒子中心的距离
vec2 uv = gl_PointCoord - 0.5;
float d = length(uv);
// 圆形裁剪:丢弃超出圆心的像素,形成圆形轮廓
if (d > 0.5) discard;
// 抗锯齿边缘:smoothstep实现渐隐,提升光晕质感,避免锯齿感
float alpha = 1.0 - smoothstep(0.45, 0.5, d);
// 最终像素颜色:粒子独立颜色+渐变Alpha,实现柔和光晕效果
gl_FragColor = vec4(vColor, alpha);
}
`,
// 材质配置:提升星系视觉效果,实现朦胧光晕与流畅叠加
transparent: true, // 启用透明,支持边缘渐隐
blending: THREE.AdditiveBlending, // 加法混合,颜色亮度叠加,营造光晕
depthWrite: false // 关闭深度写入,避免粒子互相遮挡,光晕更连贯
});
// 创建Points粒子对象,添加到场景,形成最终的M101风车星系
const points = new THREE.Points(geometry, material);
scene.add(points);
// ========== 5. 动画循环(驱动脉动+自转,实现流畅动态星系) ==========
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
// 更新全局时间,驱动粒子微弱脉动动画
const t = clock.getElapsedTime();
material.uniforms.uTime.value = t;
// 星系整体自转:绕Y轴缓慢旋转,营造真实宇宙氛围
points.rotation.y = t * 0.02;
// 更新轨道控制器阻尼,保证顺滑交互
controls.update();
// 渲染场景,呈现最终的M101风车星系效果
renderer.render(scene, camera);
}
// 启动动画循环,开始运行星系的脉动、自转与渲染
animate();
// ========== 6. 窗口适配(响应式调整,适配不同屏幕尺寸) ==========
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
总结与扩展建议
核心总结
- 形态核心:对数螺旋算法是实现螺旋星系旋臂的关键,配合径向/角度扰动,可生成自然流畅的风车状结构,还原真实星系的形态特征。
- 视觉核心:粒子分层生成策略(核球+旋臂)+ 差异化视觉参数(颜色、尺寸),是提升星系真实感的核心,能够还原真实星系的结构与视觉差异。
- 性能核心 :
ShaderMaterial+BufferGeometry的组合,能够高效处理10万级粒子,借助GPU并行处理,实现流畅渲染,同时支持复杂的视觉定制(圆形粒子、光晕、脉动)。 - 动态核心:微观粒子微弱脉动+宏观星系整体自转的双动画逻辑,动静结合,既保证星系的整体形态不变,又提升视觉体验的丰富性,营造真实的宇宙氛围。
扩展建议
- 星系效果增强 :
- 添加暗物质晕:在星系外围生成一层稀疏、暗淡的粒子,模拟暗物质晕,提升星系的真实感;
- 添加星云效果:通过
Texture纹理结合片元着色器,在旋臂之间添加朦胧的星云,丰富星系的视觉层次; - 动态颜色变化:通过
uniform传递颜色参数,让核球与旋臂的颜色随时间缓慢变化,提升场景的动态感。
- 功能扩展 :
- 交互增强:绑定鼠标位置,让星系跟随鼠标旋转、缩放,提升交互体验;
- 参数控制面板:提供可视化面板,允许用户调整旋臂数量、松紧度、自转速度等参数,实时预览效果;
- 相机自动漫游:实现相机的自动路径漫游,让用户无需手动操作即可全方位查看星系的细节。
- 性能优化 :
- 使用
InstancedBufferGeometry替代BufferGeometry,进一步减少DrawCall,支持更多粒子(百万级); - 开启渲染器的
powerPreference: "high-performance",优先使用高性能GPU,提升渲染效率; - 剔除不可见粒子:通过视锥体裁剪,剔除屏幕外的粒子,减少GPU渲染开销。
- 使用
- 场景扩展 :
- 添加背景星空:通过
THREE.TextureLoader加载星空纹理,作为场景背景,营造更真实的宇宙环境; - 添加其他天体:在场景中添加恒星、行星等天体,丰富宇宙场景的内容;
- 实现多星系并存:创建多个不同形态的星系,形成星系集群,提升场景的壮观感。
- 添加背景星空:通过