UNPKG

universe-simulation

Version:

A powerful JavaScript library for creating interactive universe simulations with three.js

289 lines (248 loc) 10.7 kB
import * as THREE from 'three'; import { NEBULA_COLORS } from './data/astronomicalColors'; /** * Creates beautiful JWST-inspired nebulae with subtle, artistic rendering */ export function createJWSTNebula(nebula, mobile = false) { const nebulaGroup = new THREE.Group(); nebulaGroup.name = `nebula-${nebula.name}`; // Use real nebula colors based on type const nebulaColors = NEBULA_COLORS[nebula.type] || NEBULA_COLORS['emission']; const baseSize = (nebula.size || 100) * 50; // Scale up for visibility // JWST color palette adjustments for more realistic appearance const jwstColorAdjustments = { emission: { saturation: 0.7, brightness: 0.8 }, planetary: { saturation: 0.9, brightness: 0.9 }, reflection: { saturation: 0.4, brightness: 0.7 }, supernova: { saturation: 0.8, brightness: 0.85 }, dark: { saturation: 0.3, brightness: 0.2 }, 'star-forming': { saturation: 1.0, brightness: 0.9 } }; const colorAdjust = jwstColorAdjustments[nebula.type] || { saturation: 0.7, brightness: 0.7 }; // Create main nebula cloud with volumetric layers const createVolumetricCloud = () => { const cloudGroup = new THREE.Group(); const layerCount = mobile ? 3 : 6; for (let layer = 0; layer < layerCount; layer++) { const layerScale = 1 + layer * 0.15; const particleCount = mobile ? 200 : 500; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(particleCount * 3); const colors = new Float32Array(particleCount * 3); const sizes = new Float32Array(particleCount); const opacities = new Float32Array(particleCount); // Generate particle distribution based on nebula type for (let i = 0; i < particleCount; i++) { let x, y, z; if (nebula.type === 'planetary') { // Ring/shell structure for planetary nebulae const theta = Math.random() * Math.PI * 2; const phi = Math.acos(Math.random() * 2 - 1); const r = baseSize * layerScale * (0.7 + Math.random() * 0.3); x = r * Math.sin(phi) * Math.cos(theta); y = r * Math.sin(phi) * Math.sin(theta); z = r * Math.cos(phi); } else if (nebula.type === 'supernova') { // Expanding shell structure const theta = Math.random() * Math.PI * 2; const phi = Math.random() * Math.PI; const r = baseSize * layerScale * (0.8 + Math.random() * 0.2); x = r * Math.sin(phi) * Math.cos(theta); y = r * Math.sin(phi) * Math.sin(theta) * 0.5; // Flattened z = r * Math.cos(phi); } else { // Default cloud structure with pillars const angle = Math.random() * Math.PI * 2; const radius = Math.random() * baseSize * layerScale; const height = (Math.random() - 0.5) * baseSize * 0.6; // Create pillar-like structures (30% chance) if (Math.random() < 0.3) { x = Math.cos(angle) * radius * 0.3; y = height * 2.5; // Tall pillars z = Math.sin(angle) * radius * 0.3; } else { // Irregular cloud distribution x = Math.cos(angle) * radius * (0.5 + Math.random() * 0.5); y = height + Math.sin(radius * 0.005) * baseSize * 0.2; z = Math.sin(angle) * radius * (0.5 + Math.random() * 0.5); } // Add turbulence const turbulence = baseSize * 0.1; x += (Math.random() - 0.5) * turbulence; y += (Math.random() - 0.5) * turbulence; z += (Math.random() - 0.5) * turbulence; } positions[i * 3] = x; positions[i * 3 + 1] = y; positions[i * 3 + 2] = z; // Color mixing based on position const colorIndex = Math.floor(Math.random() * nebulaColors.length); const color = nebulaColors[colorIndex]; const distanceFromCenter = Math.sqrt(x * x + y * y + z * z) / (baseSize * layerScale); // Apply JWST-style color adjustments colors[i * 3] = color.r * colorAdjust.brightness * (1 - distanceFromCenter * 0.3); colors[i * 3 + 1] = color.g * colorAdjust.brightness * (1 - distanceFromCenter * 0.2); colors[i * 3 + 2] = color.b * colorAdjust.brightness * (1 - distanceFromCenter * 0.1); // Size and opacity based on layer and distance sizes[i] = (20 + Math.random() * 40) * (1 + layer * 0.2); opacities[i] = 0.02 + Math.random() * 0.03; // Very subtle opacity } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); geometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1)); // JWST-inspired shader const material = new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, cameraPos: { value: new THREE.Vector3() }, nebulaPos: { value: nebulaGroup.position } }, vertexShader: ` attribute float size; attribute float opacity; varying vec3 vColor; varying float vOpacity; varying float vDepth; void main() { vColor = color; vOpacity = opacity; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); vDepth = -mvPosition.z; gl_PointSize = size * (2000.0 / vDepth); gl_PointSize = clamp(gl_PointSize, 2.0, 80.0); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` varying vec3 vColor; varying float vOpacity; varying float vDepth; uniform float time; void main() { vec2 coord = gl_PointCoord - vec2(0.5); float distance = length(coord); // Soft gaussian cloud float strength = exp(-distance * distance * 4.0); // Add subtle noise for texture float noise = sin(gl_PointCoord.x * 20.0 + time * 0.1) * cos(gl_PointCoord.y * 20.0 - time * 0.15) * 0.05; // JWST-style bright core with color fringing vec3 coreColor = vColor * (1.0 + strength * 1.5); vec3 fringeColor = vec3( coreColor.r * (1.0 + strength * 0.2), coreColor.g * (1.0 + strength * 0.3), coreColor.b * (1.0 + strength * 0.5) ); // Distance fade for atmospheric perspective float distanceFade = 1.0 / (1.0 + vDepth * 0.00001); vec3 finalColor = mix(fringeColor, coreColor, distance) * distanceFade; float finalAlpha = vOpacity * strength * (1.0 + noise); gl_FragColor = vec4(finalColor, finalAlpha); } `, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, vertexColors: true }); const particles = new THREE.Points(geometry, material); particles.rotation.y = layer * 0.2; particles.userData = { layer }; cloudGroup.add(particles); } return cloudGroup; }; // Add main nebula cloud nebulaGroup.add(createVolumetricCloud()); // Add bright stars with JWST diffraction spikes const starCount = mobile ? 3 : 8; for (let i = 0; i < starCount; i++) { const starGroup = new THREE.Group(); // Star core const starGeometry = new THREE.SphereGeometry(3, 16, 16); const starMaterial = new THREE.MeshBasicMaterial({ color: new THREE.Color(1.5, 1.5, 1.8), emissive: new THREE.Color(1, 1, 1), emissiveIntensity: 2 }); const star = new THREE.Mesh(starGeometry, starMaterial); starGroup.add(star); // Star glow const glowGeometry = new THREE.SphereGeometry(15, 16, 16); const glowMaterial = new THREE.ShaderMaterial({ uniforms: { viewVector: { value: new THREE.Vector3() } }, vertexShader: ` varying vec3 vNormal; varying vec3 vPositionNormal; void main() { vNormal = normalize(normalMatrix * normal); vPositionNormal = normalize((modelViewMatrix * vec4(position, 1.0)).xyz); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` varying vec3 vNormal; varying vec3 vPositionNormal; void main() { float intensity = pow(0.7 - dot(vNormal, vPositionNormal), 2.0); vec3 glow = vec3(1.0, 0.95, 0.8) * intensity; gl_FragColor = vec4(glow, intensity * 0.4); } `, side: THREE.BackSide, blending: THREE.AdditiveBlending, transparent: true, depthWrite: false }); const glow = new THREE.Mesh(glowGeometry, glowMaterial); starGroup.add(glow); // JWST-style diffraction spikes (6-pointed) const spikeLength = 60; const spikeGeometry = new THREE.BufferGeometry(); const spikePositions = []; for (let j = 0; j < 6; j++) { const angle = (j / 6) * Math.PI * 2; spikePositions.push(0, 0, 0); spikePositions.push( Math.cos(angle) * spikeLength, Math.sin(angle) * spikeLength, 0 ); } spikeGeometry.setAttribute('position', new THREE.Float32BufferAttribute(spikePositions, 3)); const spikeMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.3, blending: THREE.AdditiveBlending, depthWrite: false }); const spikes = new THREE.LineSegments(spikeGeometry, spikeMaterial); starGroup.add(spikes); // Position star within nebula starGroup.position.set( (Math.random() - 0.5) * baseSize * 1.5, (Math.random() - 0.5) * baseSize * 0.8, (Math.random() - 0.5) * baseSize * 1.5 ); nebulaGroup.add(starGroup); } // Position nebula in space const distance = nebula.distance * 5000; // Convert kpc to simulation units nebulaGroup.position.set( Math.cos(nebula.ra * Math.PI / 180) * distance, Math.sin(nebula.dec * Math.PI / 180) * distance * 0.5, // Compress vertical Math.sin(nebula.ra * Math.PI / 180) * distance ); // Store metadata nebulaGroup.userData = { name: nebula.name, type: nebula.type, distance: nebula.distance, originalPosition: nebulaGroup.position.clone() }; return nebulaGroup; }