Create a 3D particle galaxy with swirling nebulas, dynamic lighting
HTML
Preview
Copy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Particle Galaxy</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
}
.info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: Arial, sans-serif;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
pointer-events: none;
}
</style>
</head>
<body>
<div class="info">Interactive 3D Galaxy - Drag to rotate, scroll to zoom</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>
<script>
// Initialize the scene, camera, and renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Set camera position
camera.position.z = 100;
// Add orbit controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.rotateSpeed = 0.5;
// Create galaxy parameters
const galaxyParams = {
count: 200000,
size: 0.01,
radius: 100,
branches: 5,
spin: 1,
randomness: 0.2,
randomnessPower: 3,
insideColor: '#ff6030',
outsideColor: '#1b3984',
nebulaCount: 5000,
nebulaSize: 0.15
};
// Create galaxy particles
let galaxyGeometry = null;
let galaxyMaterial = null;
let galaxyPoints = null;
// Create nebula particles
let nebulaGeometry = null;
let nebulaMaterial = null;
let nebulaPoints = null;
// Create star particles (background)
let starsGeometry = null;
let starsMaterial = null;
let starsPoints = null;
// Generate galaxy
function generateGalaxy() {
// Dispose of old galaxy if it exists
if (galaxyPoints !== null) {
galaxyGeometry.dispose();
galaxyMaterial.dispose();
scene.remove(galaxyPoints);
}
// Create geometry
galaxyGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(galaxyParams.count * 3);
const colors = new Float32Array(galaxyParams.count * 3);
const scales = new Float32Array(galaxyParams.count);
const randomness = new Float32Array(galaxyParams.count * 3);
const insideColor = new THREE.Color(galaxyParams.insideColor);
const outsideColor = new THREE.Color(galaxyParams.outsideColor);
for (let i = 0; i < galaxyParams.count; i++) {
// Position
const i3 = i * 3;
const radius = Math.random() * galaxyParams.radius;
const spinAngle = radius * galaxyParams.spin;
const branchAngle = (i % galaxyParams.branches) / galaxyParams.branches * Math.PI * 2;
const randomX = Math.pow(Math.random(), galaxyParams.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * galaxyParams.randomness * radius;
const randomY = Math.pow(Math.random(), galaxyParams.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * galaxyParams.randomness * radius;
const randomZ = Math.pow(Math.random(), galaxyParams.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * galaxyParams.randomness * radius;
positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
positions[i3 + 1] = randomY;
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ;
randomness[i3] = randomX;
randomness[i3 + 1] = randomY;
randomness[i3 + 2] = randomZ;
// Color
const mixedColor = insideColor.clone();
mixedColor.lerp(outsideColor, radius / galaxyParams.radius);
colors[i3] = mixedColor.r;
colors[i3 + 1] = mixedColor.g;
colors[i3 + 2] = mixedColor.b;
// Scale
scales[i] = Math.random() * 3;
}
galaxyGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
galaxyGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
galaxyGeometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
galaxyGeometry.setAttribute('aRandomness', new THREE.BufferAttribute(randomness, 3));
// Material
galaxyMaterial = new THREE.PointsMaterial({
size: galaxyParams.size,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true
});
// Points
galaxyPoints = new THREE.Points(galaxyGeometry, galaxyMaterial);
scene.add(galaxyPoints);
// Create nebula
createNebula();
// Create background stars
createStars();
}
// Create nebula cloud
function createNebula() {
if (nebulaPoints !== null) {
nebulaGeometry.dispose();
nebulaMaterial.dispose();
scene.remove(nebulaPoints);
}
nebulaGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(galaxyParams.nebulaCount * 3);
const colors = new Float32Array(galaxyParams.nebulaCount * 3);
const sizes = new Float32Array(galaxyParams.nebulaCount);
const colorPalette = [
new THREE.Color(0xff0040), // red
new THREE.Color(0x4000ff), // blue
new THREE.Color(0xff8000), // orange
new THREE.Color(0x8000ff), // purple
new THREE.Color(0x00ff80) // cyan
];
for (let i = 0; i < galaxyParams.nebulaCount; i++) {
const i3 = i * 3;
// Position in a more cloud-like distribution
const radius = Math.random() * galaxyParams.radius * 1.2;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = (Math.random() - 0.5) * galaxyParams.radius * 0.3;
positions[i3 + 2] = radius * Math.sin(phi) * Math.sin(theta);
// Color
const color = colorPalette[Math.floor(Math.random() * colorPalette.length)];
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
// Size
sizes[i] = galaxyParams.nebulaSize * (Math.random() * 0.5 + 0.5);
}
nebulaGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
nebulaGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
nebulaGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
nebulaMaterial = new THREE.PointsMaterial({
size: galaxyParams.nebulaSize,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
transparent: true,
opacity: 0.5
});
nebulaPoints = new THREE.Points(nebulaGeometry, nebulaMaterial);
scene.add(nebulaPoints);
}
// Create background stars
function createStars() {
if (starsPoints !== null) {
starsGeometry.dispose();
starsMaterial.dispose();
scene.remove(starsPoints);
}
starsGeometry = new THREE.BufferGeometry();
const count = 5000;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 2000;
}
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
starsMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.5,
sizeAttenuation: true
});
starsPoints = new THREE.Points(starsGeometry, starsMaterial);
scene.add(starsPoints);
}
// Add central light source
const centralLight = new THREE.PointLight(0xffffff, 2, 300);
scene.add(centralLight);
// Add ambient light for visibility
const ambientLight = new THREE.AmbientLight(0x333333);
scene.add(ambientLight);
// Generate the galaxy
generateGalaxy();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Animation loop
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
// Rotate galaxy slowly
if (galaxyPoints) {
galaxyPoints.rotation.y = elapsedTime * 0.05;
}
// Rotate nebula
if (nebulaPoints) {
nebulaPoints.rotation.y = elapsedTime * 0.03;
nebulaPoints.rotation.x = Math.sin(elapsedTime * 0.02) * 0.1;
}
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>