universe-simulation
Version:
A powerful JavaScript library for creating interactive universe simulations with three.js
705 lines (594 loc) • 20.7 kB
JavaScript
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();
});
}