1 效果预览
通过参数调节可实现不同形态的银河效果,支持实时交互和全屏查看
2 核心实现步骤
2.1 初始化Three.js基础环境
// 初始化场景
const scene = new THREE.Scene();
// 创建透视相机
const camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000);
camera.position.set(0, 0, 5);
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
canvas: webglCanvas.value,
alpha: true,
antialias: true
});
2.2 创建银河粒子系统
几何体生成逻辑:
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
for (let i = 0; i < parameters.count; i++) {
// 极坐标计算粒子位置
const radius = Math.random() * galaxyRadius;
const spinAngle = radius * spinFactor;
const branchAngle = (i % branches) / branches * Math.PI * 2;
// 添加随机扰动
positions[i*3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
positions[i*3+1] = randomY;
positions[i*3+2] = Math.sin(branchAngle + spinAngle) * radius + randomZ;
// 颜色插值
colors[i*3] = insideColor.r + (outsideColor.r - insideColor.r) * (radius / maxRadius);
}
材质配置:
const material = new THREE.PointsMaterial({
size: particleSize,
map: particleTexture, // 使用粒子贴图
blending: THREE.AdditiveBlending,
depthWrite: false,
vertexColors: true
});
2.3 参数控制面板实现
const gui = new dat.GUI();
gui.add(parameters, 'branches', 2, 20).name('旋臂数量').onChange(updateGalaxy);
gui.addColor(parameters, 'insideColor').name('核心颜色').onChange(updateGalaxy);
gui.add(parameters, 'spin', -5, 5).name('旋转强度').onChange(updateGalaxy);
3 关键参数详解
参数名 | 类型 | 范围 | 说明 |
---|---|---|---|
count | number | 1000-10000 | 粒子总数 |
branches | number | 2-20 | 银河旋臂数量 |
spin | number | -5~5 | 旋转强度(正负值方向不同) |
randomness | number | 0-2 | 粒子位置随机扰动强度 |
randomPower | number | 1-10 | 1-10 扰动分布曲线 |
4 性能优化技巧
4.1 对象复用
更新参数时先销毁旧几何体
if (oldPoints) {
geometry.dispose();
material.dispose();
scene.remove(oldPoints);
}
4.2 渲染优化
- 开启抗锯齿:
antialias: true
- 使用
AdditiveBlending
混合模式 - 关闭深度写入:
depthWrite: false
4.3 内存管理
- 组件卸载时释放资源
- 使用
dispose()
方法清理Three.js对象
5 完整代码
<!-- eslint-disable no-undef -->
<template>
<div class="galaxy">
<canvas ref="webglCanvas" class="webgl"></canvas>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import * as dat from "dat.gui"; // 导入dat.gui
export default defineComponent({
name: "GalaxyView",
setup() {
const webglCanvas = ref<HTMLCanvasElement | null>(null);
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let controls: OrbitControls;
const parameters: any = {};
let gui: dat.GUI | null = null;
const initThree = () => {
if (!webglCanvas.value) return;
/**
* gui
*/
gui = new dat.GUI();
/**
* 场景
*/
scene = new THREE.Scene();
/**
* 银河
*/
// 创建银河
parameters.count = 3000;
parameters.size = 0.01;
parameters.insideColor = 0xff6030;
parameters.outsideColor = 0x00ff00;
parameters.radius = 5;
parameters.branches = 3; //分支
parameters.spin = 1; //旋转
parameters.randomness = 0.5; //随机度
parameters.randomPower = 3; //随机度的幂
let geometry: THREE.BufferGeometry<THREE.NormalBufferAttributes> | null =
null;
let material: THREE.PointsMaterial | null = null;
let Points: THREE.Object3D<THREE.Object3DEventMap> | null = null;
const generateGalaxy = () => {
/**
* 销毁旧的
*/
if (Points !== null && geometry !== null && material !== null) {
//scene.remove(Points);
geometry.dispose();
material.dispose();
scene.remove(Points);
}
/**
* 几何体
*/
geometry = new THREE.BufferGeometry();
const positions = new Float32Array(parameters.count * 3);
const colors = new Float32Array(parameters.count * 3);
const insideColor = new THREE.Color(parameters.insideColor);
const outsideColor = new THREE.Color(parameters.outsideColor);
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3;
/**
* 银河的位置
*/
const radius = Math.random() * parameters.radius;
const spinAngle = radius * parameters.spin;
//获得每个分支的角度
const branchAngle =
((i % parameters.branches) / parameters.branches) * Math.PI * 2;
//随机度
const randomX =
Math.pow(Math.random(), parameters.randomPower) *
(Math.random() < 0.5 ? 1 : -1);
const randomY =
Math.pow(Math.random(), parameters.randomPower) *
(Math.random() < 0.5 ? 1 : -1);
const randomZ =
Math.pow(Math.random(), parameters.randomPower) *
(Math.random() < 0.5 ? 1 : -1);
positions[i3 + 0] =
Math.cos(branchAngle + spinAngle) * radius + randomX;
positions[i3 + 1] = 0 + randomY;
positions[i3 + 2] =
Math.sin(branchAngle + spinAngle) * radius + randomZ;
/**
* 银河颜色
*/
colors[i3 + 0] =
outsideColor.r +
(insideColor.r - outsideColor.r) * (1 - radius / parameters.radius);
colors[i3 + 1] =
outsideColor.g +
(insideColor.g - outsideColor.g) * (1 - radius / parameters.radius);
colors[i3 + 2] =
outsideColor.b +
(insideColor.b - outsideColor.b) * (1 - radius / parameters.radius);
}
geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
/**
* 材质
*/
material = new THREE.PointsMaterial({
size: parameters.size,
sizeAttenuation: true, //是否衰减
depthWrite: false, //是否渲染深度
blending: THREE.AdditiveBlending, //混合模式
vertexColors: true, //是否使用顶点颜色,
});
/**
* 粒子
*/
Points = new THREE.Points(geometry, material);
Points.position.set(0, 0, 0);
scene.add(Points);
};
generateGalaxy();
gui
.add(parameters, "count", 1000, 10000, 100)
.name("粒子数量")
.onFinishChange(generateGalaxy);
gui
.add(parameters, "size", 0.001, 0.1, 0.001)
.name("粒子大小")
.onFinishChange(generateGalaxy);
gui
.addColor(parameters, "insideColor")
.name("内部颜色")
.onFinishChange(generateGalaxy);
gui
.addColor(parameters, "outsideColor")
.name("外部颜色")
.onFinishChange(generateGalaxy);
gui
.add(parameters, "radius", 0.01, 20, 0.01)
.name("银河半径")
.onFinishChange(generateGalaxy);
gui
.add(parameters, "branches", 2, 20, 1)
.name("银河分支数量")
.onFinishChange(generateGalaxy);
gui
.add(parameters, "spin", -5, 5, 0.001)
.name("银河旋转")
.onChange(generateGalaxy);
gui
.add(parameters, "randomness", 0, 2, 0.001)
.name("随机度")
.onFinishChange(generateGalaxy);
gui
.add(parameters, "randomPower", 1, 10, 0.001)
.name("随机度的幂")
.onFinishChange(generateGalaxy);
/**
* 纹理
*/
const textureLoader = new THREE.TextureLoader();
const particleTexture1 = textureLoader.load("/particle1.png");
/**
* 相机
*/
// 创建相机,调整宽高比
const width = window.innerWidth;
const height = window.innerHeight;
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.set(0, 0, 5);
// 渲染器,使用 canvas 元素
renderer = new THREE.WebGLRenderer({
canvas: webglCanvas.value,
alpha: true,
antialias: true,
});
// 设置渲染器大小
renderer.setSize(width, height);
//设置像素比
renderer.setPixelRatio(window.devicePixelRatio);
// 添加控制面板
controls = new OrbitControls(camera, webglCanvas.value); //webglCanvas.value与render.docutement一样
controls.enablePan = false; //禁止拖动
controls.enableDamping = true; //阻尼效果
// 动画效果
const animate = () => {
requestAnimationFrame(animate);
//更新粒子位置
controls.update();
renderer.render(scene, camera);
};
animate();
};
const onWindowResize = () => {
if (camera && renderer) {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
}
};
//双击事件
const ondblclick = () => {
if (!document.fullscreenElement) {
//有的浏览器不支持fullscreenElement,还还会有其他的,可以加判断,这就不加了。。
webglCanvas.value?.requestFullscreen();
} else {
document.exitFullscreen();
}
};
onMounted(() => {
initThree();
window.addEventListener("resize", onWindowResize);
window.addEventListener("dblclick", ondblclick);
});
onUnmounted(() => {
if (renderer) {
renderer.dispose();
}
if (gui) {
gui.destroy(); // 销毁 dat.gui
gui = null;
}
window.removeEventListener("resize", onWindowResize);
});
return {
webglCanvas,
};
},
});
</script>
<style scoped>
/* 这个地方的居中思想挺重要的,可以让元素在页面中居中显示。 */
.about {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
margin: 0;
}
.webgl {
background-color: rgb(1, 1, 1);
width: 100%;
height: 100%;
display: block;
position: fixed;
left: 0;
}
</style>