UNPKG

universe-simulation

Version:

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

705 lines (594 loc) 20.7 kB
import * as THREE from 'three'; /** * Enhanced Nanite system with Web Worker parallelization * Features: * - Multi-threaded LOD selection * - Parallel frustum culling * - Background geometry streaming * - GPU-driven rendering with instancing */ // Cluster with serializable data for workers class ParallelGeometryCluster { constructor(geometry, level = 0, bounds = null) { this.geometry = geometry; this.level = level; this.bounds = bounds || this.calculateBounds(); this.error = 0; this.children = []; this.parent = null; this.id = Math.random().toString(36).substr(2, 9); this.triangleCount = geometry.index ? geometry.index.count / 3 : 0; } calculateBounds() { this.geometry.computeBoundingBox(); return this.geometry.boundingBox.clone(); } // Serialize for worker transfer serialize() { return { id: this.id, level: this.level, bounds: { min: { x: this.bounds.min.x, y: this.bounds.min.y, z: this.bounds.min.z }, max: { x: this.bounds.max.x, y: this.bounds.max.y, z: this.bounds.max.z } }, triangleCount: this.triangleCount, childIds: this.children.map(c => c.id) }; } } export class ParallelNaniteSystem { constructor(renderer, camera, workerCount = navigator.hardwareConcurrency || 4) { this.renderer = renderer; this.camera = camera; this.clusters = new Map(); this.visibleClusters = new Set(); this.errorThreshold = 2.0; // Worker pool this.workerCount = workerCount; this.workers = []; this.workerTasks = new Map(); this.taskId = 0; // Performance monitoring this.stats = { totalClusters: 0, visibleClusters: 0, trianglesRendered: 0, memoryUsedMB: 0, workerTime: 0, mainThreadTime: 0 }; // Instance rendering batches this.instanceBatches = new Map(); // Initialize workers this.initializeWorkers(); // GPU compute for visibility this.initializeGPUCompute(); } /** * Initialize Web Worker pool */ initializeWorkers() { try { // Check if Workers are supported if (typeof Worker === 'undefined') { console.warn('Web Workers not supported, falling back to single-threaded mode'); this.workerCount = 0; return; } for (let i = 0; i < this.workerCount; i++) { try { const worker = new Worker(new URL('./workers/naniteWorker.js', import.meta.url)); worker.onmessage = (e) => { this.handleWorkerMessage(i, e.data); }; worker.onerror = (error) => { console.error(`Worker ${i} error:`, error); // Remove failed worker this.workers = this.workers.filter(w => w !== worker); }; this.workers.push(worker); } catch (workerError) { console.error(`Failed to create worker ${i}:`, workerError); this.workerCount = i; break; } } // Initialized Nanite workers } catch (error) { console.error('Failed to initialize workers:', error); this.workerCount = 0; this.workers = []; } } /** * Initialize GPU compute shaders for visibility culling */ initializeGPUCompute() { // Check for WebGL2 compute shader support const gl = this.renderer.getContext(); this.supportsCompute = gl.getParameter(gl.VERSION).includes('WebGL 2.0'); if (this.supportsCompute) { // Create compute shader for parallel visibility testing this.visibilityComputeShader = `#version 300 es precision highp float; uniform mat4 viewProjectionMatrix; uniform vec2 screenSize; uniform float errorThreshold; // Input: cluster bounds and metadata layout(std430, binding = 0) buffer ClusterData { vec4 boundsMin[]; vec4 boundsMax[]; vec4 metadata[]; // x: triangleCount, y: level, z: parentId, w: selected } clusters; void main() { uint index = gl_GlobalInvocationID.x; // Frustum culling vec3 center = (clusters.boundsMin[index].xyz + clusters.boundsMax[index].xyz) * 0.5; vec4 clipPos = viewProjectionMatrix * vec4(center, 1.0); // Check if in frustum bool inFrustum = all(lessThan(abs(clipPos.xyz), vec3(clipPos.w))); // Calculate screen-space error float size = length(clusters.boundsMax[index].xyz - clusters.boundsMin[index].xyz); float distance = length(center - cameraPosition); float screenSize = (size / distance) * screenSize.y; float error = screenSize / sqrt(clusters.metadata[index].x); // Select if visible and error is acceptable clusters.metadata[index].w = (inFrustum && error >= errorThreshold) ? 1.0 : 0.0; } `; } } /** * Handle messages from workers */ handleWorkerMessage(workerId, data) { const { type, results, errors } = data; switch (type) { case 'LOD_SELECTION_COMPLETE': this.processLODResults(results); break; case 'FRUSTUM_CULL_COMPLETE': this.processCullResults(data.visible, data.culled); break; case 'ERROR_COMPUTATION_COMPLETE': this.processErrorResults(errors); break; case 'SIMPLIFICATION_COMPLETE': this.processSimplifiedGeometry(data.result); break; default: console.warn('Unknown worker message type:', type); } // Mark task complete const taskId = this.workerTasks.get(workerId); if (taskId) { this.workerTasks.delete(workerId); } } /** * Distribute work across workers */ async distributeWork(clusters, operation, additionalData = {}) { if (this.workers.length === 0) return; const batchSize = Math.ceil(clusters.length / this.workers.length); const promises = []; for (let i = 0; i < this.workers.length; i++) { const start = i * batchSize; const end = Math.min(start + batchSize, clusters.length); const batch = clusters.slice(start, end); if (batch.length > 0) { promises.push(this.sendToWorker(i, operation, { clusters: batch, ...additionalData })); } } return Promise.all(promises); } /** * Send task to specific worker */ sendToWorker(workerId, operation, data) { return new Promise((resolve) => { const taskId = this.taskId++; this.workerTasks.set(workerId, taskId); const worker = this.workers[workerId]; worker.postMessage({ type: operation, data: data, taskId: taskId }); // Store resolver for later this.workerTasks.set(taskId, resolve); }); } /** * Create LOD hierarchy with parallel processing */ async createLODHierarchy(mesh, levels = 5) { // Generate LODs in parallel const lodPromises = []; const baseGeometry = mesh.geometry; for (let i = 1; i < levels; i++) { const targetRatio = Math.pow(0.5, i); lodPromises.push(this.simplifyGeometryAsync(baseGeometry, targetRatio)); } const simplifiedGeometries = await Promise.all(lodPromises); const geometries = [baseGeometry, ...simplifiedGeometries]; // Build cluster hierarchy const rootCluster = this.buildClusterHierarchy(geometries); // Store in cluster map this.clusters.set(mesh.uuid, { root: rootCluster, mesh: mesh, material: mesh.material, instances: new Map() // For instanced rendering }); this.stats.totalClusters = this.clusters.size; // LOD hierarchy created in ${performance.now() - startTime}ms return rootCluster; } /** * Simplify geometry using Web Worker */ async simplifyGeometryAsync(geometry, targetRatio) { const positions = geometry.attributes.position.array; const indices = geometry.index ? geometry.index.array : null; if (!indices) return geometry; // Can't simplify non-indexed // Find available worker const workerId = this.findAvailableWorker(); return new Promise((resolve) => { const worker = this.workers[workerId]; const handleMessage = (e) => { if (e.data.type === 'SIMPLIFICATION_COMPLETE') { worker.removeEventListener('message', handleMessage); // Create new geometry from result const simplified = new THREE.BufferGeometry(); simplified.setAttribute('position', new THREE.BufferAttribute( new Float32Array(e.data.result.vertices), 3 )); simplified.setIndex(new THREE.BufferAttribute( new Uint32Array(e.data.result.indices), 1 )); simplified.computeVertexNormals(); simplified.computeBoundingBox(); resolve(simplified); } }; worker.addEventListener('message', handleMessage); // Send arrays to worker (copy, not transfer) worker.postMessage({ type: 'SIMPLIFY_GEOMETRY', data: { vertices: Array.from(positions), indices: Array.from(indices), targetRatio: targetRatio } }); }); } /** * Find available worker or least busy */ findAvailableWorker() { for (let i = 0; i < this.workers.length; i++) { if (!this.workerTasks.has(i)) { return i; } } // Return random if all busy return Math.floor(Math.random() * this.workers.length); } /** * Build hierarchical cluster structure */ buildClusterHierarchy(geometries) { const clusters = geometries.map((geom, level) => new ParallelGeometryCluster(geom, level) ); // Link parent-child relationships for (let i = 0; i < clusters.length - 1; i++) { clusters[i].children.push(clusters[i + 1]); clusters[i + 1].parent = clusters[i]; } return clusters[0]; } /** * Update visible clusters using parallel processing */ async update() { const startTime = performance.now(); this.visibleClusters.clear(); this.stats.trianglesRendered = 0; const screenWidth = this.renderer.domElement.width; const screenHeight = this.renderer.domElement.height; // Prepare cluster data for workers (exclude non-serializable data) const clusterData = []; this.clusters.forEach((data, meshId) => { clusterData.push({ meshId: meshId, cluster: data.root.serialize() // Don't include material - it has functions that can't be cloned }); }); // Parallel frustum culling and LOD selection const cameraData = { position: { x: this.camera.position.x, y: this.camera.position.y, z: this.camera.position.z }, fov: this.camera.fov, aspect: this.camera.aspect, near: this.camera.near, far: this.camera.far }; // Distribute work to workers if (this.workers.length > 0) { try { await this.distributeWork(clusterData, 'SELECT_LODS', { camera: cameraData, screenWidth: screenWidth, screenHeight: screenHeight, errorThreshold: this.errorThreshold }); } catch (error) { console.error('Worker distribution error:', error); // Fallback to single-threaded processing this.processClustersLocally(clusterData, cameraData, screenWidth, screenHeight); } } else { // No workers available, process locally this.processClustersLocally(clusterData, cameraData, screenWidth, screenHeight); } // Update stats this.stats.workerTime = performance.now() - startTime; this.stats.visibleClusters = this.visibleClusters.size; } /** * Process LOD selection results from workers */ processLODResults(results) { results.forEach(result => { const clusterData = this.clusters.get(result.meshId); if (clusterData) { this.visibleClusters.add({ cluster: this.findClusterById(clusterData.root, result.clusterId), material: clusterData.material, meshId: result.meshId }); this.stats.trianglesRendered += result.triangleCount; } }); } /** * Process simplified geometry from workers */ processSimplifiedGeometry(result) { // This method is called when geometry simplification is complete // The result is already processed in simplifyGeometryAsync // Geometry simplified } /** * Process clusters locally when workers are not available */ processClustersLocally(clusterData, cameraData, screenWidth, screenHeight) { clusterData.forEach(({ meshId, cluster }) => { // Simple distance-based LOD selection const center = { x: (cluster.bounds.min.x + cluster.bounds.max.x) / 2, y: (cluster.bounds.min.y + cluster.bounds.max.y) / 2, z: (cluster.bounds.min.z + cluster.bounds.max.z) / 2 }; const dx = cameraData.position.x - center.x; const dy = cameraData.position.y - center.y; const dz = cameraData.position.z - center.z; const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); // Calculate screen-space error const sizeX = cluster.bounds.max.x - cluster.bounds.min.x; const sizeY = cluster.bounds.max.y - cluster.bounds.min.y; const sizeZ = cluster.bounds.max.z - cluster.bounds.min.z; const size = Math.sqrt(sizeX * sizeX + sizeY * sizeY + sizeZ * sizeZ); const fov = cameraData.fov * Math.PI / 180; const screenSize = (size / distance) * screenHeight / (2 * Math.tan(fov / 2)); const error = screenSize / Math.max(1, Math.sqrt(cluster.triangleCount || 1000)); if (error >= this.errorThreshold) { const clusterData = this.clusters.get(meshId); if (clusterData) { this.visibleClusters.add({ cluster: clusterData.root, material: clusterData.material, meshId: meshId }); this.stats.trianglesRendered += cluster.triangleCount || 1000; } } }); } /** * Find cluster by ID in hierarchy */ findClusterById(root, id) { if (root.id === id) return root; for (const child of root.children) { const found = this.findClusterById(child, id); if (found) return found; } return null; } /** * Render visible clusters with instancing */ render(scene) { const startTime = performance.now(); // Group clusters by material for instanced rendering const materialGroups = new Map(); this.visibleClusters.forEach(({ cluster, material, meshId }) => { const key = material.uuid; if (!materialGroups.has(key)) { materialGroups.set(key, []); } materialGroups.get(key).push(cluster); }); // Clear previous frame const naniteGroup = scene.getObjectByName('naniteGroup') || new THREE.Group(); naniteGroup.name = 'naniteGroup'; while (naniteGroup.children.length > 0) { naniteGroup.remove(naniteGroup.children[0]); } // Render each material group with instancing materialGroups.forEach((clusters, materialId) => { // Find the material from the visible clusters let material = null; this.visibleClusters.forEach(({ cluster, material: mat }) => { if (clusters.includes(cluster)) { material = mat; } }); if (clusters.length === 1) { // Single instance - regular mesh const mesh = new THREE.Mesh(clusters[0].geometry, material); naniteGroup.add(mesh); } else { // Multiple instances - use instanced mesh const instancedMesh = this.createInstancedMesh(clusters, material); naniteGroup.add(instancedMesh); } }); if (!scene.getObjectByName('naniteGroup')) { scene.add(naniteGroup); } this.stats.mainThreadTime = performance.now() - startTime; } /** * Create instanced mesh for multiple clusters */ createInstancedMesh(clusters, material) { // Use first cluster's geometry as base const baseGeometry = clusters[0].geometry; const count = clusters.length; const instancedMesh = new THREE.InstancedMesh(baseGeometry, material, count); // Set transforms for each instance const matrix = new THREE.Matrix4(); clusters.forEach((cluster, i) => { // In real implementation, would use cluster transform matrix.identity(); instancedMesh.setMatrixAt(i, matrix); }); instancedMesh.instanceMatrix.needsUpdate = true; return instancedMesh; } /** * Get current statistics including parallel performance */ getStats() { return { ...this.stats, avgTrianglesPerCluster: this.stats.visibleClusters > 0 ? Math.floor(this.stats.trianglesRendered / this.stats.visibleClusters) : 0, workerEfficiency: this.stats.workerTime > 0 ? (this.stats.mainThreadTime / this.stats.workerTime * 100).toFixed(1) + '%' : 'N/A', activeWorkers: Array.from(this.workerTasks.keys()).length }; } /** * Clean up resources and terminate workers */ dispose() { // Terminate workers this.workers.forEach(worker => worker.terminate()); this.workers = []; this.workerTasks.clear(); // Clean up geometry this.clusters.forEach(clusterData => { let cluster = clusterData.root; while (cluster) { if (cluster.geometry) { cluster.geometry.dispose(); } cluster = cluster.children[0]; } }); this.clusters.clear(); this.visibleClusters.clear(); this.instanceBatches.clear(); } } /** * Optimized planet creation with parallel LOD generation */ export async function createParallelNanitePlanet(size, detail, material, naniteSystem) { const mesh = new THREE.Mesh( new THREE.SphereGeometry(size, detail, detail), material ); await naniteSystem.createLODHierarchy(mesh, 5); return mesh; } /** * Optimized galaxy with parallel point cloud processing */ export async function createParallelNaniteGalaxy(starCount, size, naniteSystem) { const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(starCount * 3); const colors = new Float32Array(starCount * 3); // Generate galaxy in parallel chunks const chunkSize = Math.ceil(starCount / naniteSystem.workerCount); const promises = []; for (let i = 0; i < naniteSystem.workerCount; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, starCount); promises.push(generateGalaxyChunk(start, end, size, positions, colors)); } await Promise.all(promises); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.computeBoundingBox(); const material = new THREE.PointsMaterial({ size: 2, vertexColors: true, blending: THREE.AdditiveBlending, transparent: true }); const mesh = new THREE.Points(geometry, material); await naniteSystem.createLODHierarchy(mesh, 4); return mesh; } /** * Generate galaxy chunk in parallel */ async function generateGalaxyChunk(start, end, size, positions, colors) { return new Promise((resolve) => { for (let i = start; i < end; i++) { const i3 = i * 3; // Spiral galaxy shape const angle = Math.random() * Math.PI * 2; const radius = Math.random() * size; const armAngle = angle + radius * 0.2; // Spiral const height = (Math.random() - 0.5) * size * 0.1; positions[i3] = Math.cos(armAngle) * radius; positions[i3 + 1] = height; positions[i3 + 2] = Math.sin(armAngle) * radius; // Star colors const temp = Math.random(); if (temp < 0.3) { // Hot blue stars colors[i3] = 0.8; colors[i3 + 1] = 0.9; colors[i3 + 2] = 1.0; } else if (temp < 0.6) { // Sun-like stars colors[i3] = 1.0; colors[i3 + 1] = 1.0; colors[i3 + 2] = 0.9; } else { // Cool red stars colors[i3] = 1.0; colors[i3 + 1] = 0.8; colors[i3 + 2] = 0.7; } } resolve(); }); }