Create a 3D particle galaxy with swirling nebulas, dynamic lighting
HTML
预览
复制
<!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 with Swirling Nebulas</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
font-family: 'Arial', sans-serif;
color: white;
}
canvas {
display: block;
}
.info {
position: absolute;
top: 10px;
left: 10px;
z-index: 100;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
font-size: 12px;
}
.controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
font-size: 12px;
}
.controls div {
margin-bottom: 5px;
}
.controls label {
display: inline-block;
width: 120px;
}
</style>
</head>
<body>
<div class="info">
<h3>3D Particle Galaxy</h3>
<p>Mouse: Rotate view</p>
<p>Scroll: Zoom in/out</p>
</div>
<div class="controls">
<div>
<label for="particleCount">Particles: </label>
<input type="range" id="particleCount" min="5000" max="50000" value="20000" step="1000">
<span id="particleCountValue">20000</span>
</div>
<div>
<label for="rotationSpeed">Rotation: </label>
<input type="range" id="rotationSpeed" min="0" max="0.005" value="0.001" step="0.0001">
<span id="rotationSpeedValue">0.001</span>
</div>
<div>
<label for="nebulaOpacity">Nebula Opacity: </label>
<input type="range" id="nebulaOpacity" min="0" max="1" value="0.7" step="0.1">
<span id="nebulaOpacityValue">0.7</span>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>
<script>
// Main variables
let scene, camera, renderer, controls;
let galaxyParticles, nebulaClouds, dustParticles;
let galaxyGroup = new THREE.Group();
let clock = new THREE.Clock();
let rotationSpeed = 0.001;
let nebulaOpacity = 0.7;
// Galaxy parameters
const galaxyParams = {
count: 20000,
size: 0.02,
radius: 5,
branches: 5,
spin: 1,
randomness: 0.2,
randomnessPower: 3,
insideColor: '#ff6030',
outsideColor: '#1b3984',
nebulaCount: 20,
nebulaSize: 0.5,
dustCount: 10000,
dustSize: 0.01,
dustRadius: 8
};
// Initialize the scene
init();
animate();
// Setup GUI controls
setupGUI();
function init() {
// Create scene
scene = new THREE.Scene();
// Create camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 3, 10);
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
// Setup orbit controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Create galaxy
generateGalaxy();
// Add galaxy group to scene
scene.add(galaxyGroup);
// Add ambient light
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
// Add directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(0, 5, 5);
scene.add(directionalLight);
// Add point lights for dynamic lighting
const pointLight1 = new THREE.PointLight(0x1e90ff, 2, 20);
pointLight1.position.set(2, 3, 2);
scene.add(pointLight1);
const pointLight2 = new THREE.PointLight(0xff4500, 2, 20);
pointLight2.position.set(-2, -3, -2);
scene.add(pointLight2);
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Update particle count display
document.getElementById('particleCountValue').textContent = galaxyParams.count;
document.getElementById('rotationSpeedValue').textContent = rotationSpeed;
document.getElementById('nebulaOpacityValue').textContent = nebulaOpacity;
// Add event listeners for controls
document.getElementById('particleCount').addEventListener('input', (e) => {
galaxyParams.count = parseInt(e.target.value);
document.getElementById('particleCountValue').textContent = galaxyParams.count;
regenerateGalaxy();
});
document.getElementById('rotationSpeed').addEventListener('input', (e) => {
rotationSpeed = parseFloat(e.target.value);
document.getElementById('rotationSpeedValue').textContent = rotationSpeed;
});
document.getElementById('nebulaOpacity').addEventListener('input', (e) => {
nebulaOpacity = parseFloat(e.target.value);
document.getElementById('nebulaOpacityValue').textContent = nebulaOpacity;
if (nebulaClouds) {
nebulaClouds.material.opacity = nebulaOpacity;
}
});
}
function generateGalaxy() {
// Clear previous galaxy objects
galaxyGroup.clear();
// Generate galaxy particles
galaxyParticles = createGalaxyParticles();
galaxyGroup.add(galaxyParticles);
// Generate nebula clouds
nebulaClouds = createNebulaClouds();
galaxyGroup.add(nebulaClouds);
// Generate dust particles
dustParticles = createDustParticles();
galaxyGroup.add(dustParticles);
}
function createGalaxyParticles() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(galaxyParams.count * 3);
const colors = new Float32Array(galaxyParams.count * 3);
const scales = new Float32Array(galaxyParams.count);
const insideColor = new THREE.Color(galaxyParams.insideColor);
const outsideColor = new THREE.Color(galaxyParams.outsideColor);
for (let i = 0; i < galaxyParams.count; i++) {
const i3 = i * 3;
// Position
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;
// 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() * 2;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
// Material
const material = new THREE.PointsMaterial({
size: galaxyParams.size,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true
});
// Points
const particles = new THREE.Points(geometry, material);
return particles;
}
function createNebulaClouds() {
const nebulaGroup = new THREE.Group();
for (let i = 0; i < galaxyParams.nebulaCount; i++) {
const radius = Math.random() * (galaxyParams.radius + 2) + 1;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const x = radius * Math.sin(phi) * Math.cos(theta);
const y = radius * Math.sin(phi) * Math.sin(theta);
const z = radius * Math.cos(phi);
const color = new THREE.Color();
color.setHSL(Math.random(), 0.7, 0.5);
const nebulaGeometry = new THREE.SphereGeometry(
Math.random() * galaxyParams.nebulaSize + 0.5,
16,
16
);
const nebulaMaterial = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: nebulaOpacity,
blending: THREE.AdditiveBlending
});
const nebula = new THREE.Mesh(nebulaGeometry, nebulaMaterial);
nebula.position.set(x, y, z);
nebulaGroup.add(nebula);
}
return nebulaGroup;
}
function createDustParticles() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(galaxyParams.dustCount * 3);
const colors = new Float32Array(galaxyParams.dustCount * 3);
for (let i = 0; i < galaxyParams.dustCount; i++) {
const i3 = i * 3;
// Position
const radius = Math.random() * galaxyParams.dustRadius;
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] = radius * Math.sin(phi) * Math.sin(theta) * 0.2; // Flatten the dust
positions[i3 + 2] = radius * Math.cos(phi);
// Color
const brightness = Math.random() * 0.5 + 0.5;
colors[i3] = brightness * 0.8;
colors[i3 + 1] = brightness * 0.9;
colors[i3 + 2] = brightness;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// Material
const material = new THREE.PointsMaterial({
size: galaxyParams.dustSize,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
transparent: true,
opacity: 0.3
});
// Points
const dust = new THREE.Points(geometry, material);
return dust;
}
function regenerateGalaxy() {
generateGalaxy();
}
function setupGUI() {
const gui = new dat.GUI({ width: 300 });
gui.add(galaxyParams, 'count', 5000, 50000, 1000).name('Stars').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'size', 0.001, 0.1, 0.001).name('Star Size').onChange(() => {
galaxyParticles.material.size = galaxyParams.size;
});
gui.add(galaxyParams, 'radius', 2, 10, 0.1).name('Galaxy Radius').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'branches', 2, 10, 1).name('Branches').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'spin', -2, 2, 0.1).name('Spin').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'randomness', 0, 1, 0.01).name('Randomness').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'randomnessPower', 1, 10, 0.1).name('Random Power').onChange(() => {
regenerateGalaxy();
});
gui.addColor(galaxyParams, 'insideColor').name('Inside Color').onChange(() => {
regenerateGalaxy();
});
gui.addColor(galaxyParams, 'outsideColor').name('Outside Color').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'nebulaCount', 0, 50, 1).name('Nebula Count').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'nebulaSize', 0.1, 2, 0.1).name('Nebula Size').onChange(() => {
regenerateGalaxy();
});
gui.add(galaxyParams, 'dustCount', 0, 20000, 1000).name('Dust Particles').onChange(() => {
regenerateGalaxy();
});
}
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
// Rotate galaxy
galaxyGroup.rotation.y = elapsedTime * rotationSpeed;
// Animate nebula clouds
if (nebulaClouds) {
nebulaClouds.children.forEach((nebula, index) => {
nebula.rotation.x = elapsedTime * 0.05 * (1 + index * 0.01);
nebula.rotation.y = elapsedTime * 0.03 * (1 + index * 0.01);
// Subtle pulsing effect
const scale = 1 + Math.sin(elapsedTime * 0.5 + index) * 0.1;
nebula.scale.set(scale, scale, scale);
});
}
// Animate dust particles
if (dustParticles) {
dustParticles.rotation.y = elapsedTime * 0.0005;
}
controls.update();
renderer.render(scene, camera);
}
</script>
</body>
</html>