// Enhanced Natural Language Scene Parser // Converts user descriptions into comprehensive structured scene data export class SceneParser { constructor() { this.keywords = { environments: { forest: ['forest', 'woods', 'jungle', 'trees', 'woodland'], beach: ['beach', 'shore', 'ocean', 'seaside', 'coast'], desert: ['desert', 'sand', 'dunes', 'arid'], city: ['city', 'urban', 'street', 'building', 'metropolis'], lab: ['lab', 'laboratory', 'sci-fi', 'futuristic', 'science'], room: ['room', 'indoor', 'interior', 'chamber'], space: ['space', 'planet', 'alien', 'cosmic', 'astronomical'], underwater: ['underwater', 'ocean floor', 'sea', 'aquatic'], magical: ['magical', 'enchanted', 'mystical', 'fantasy'] }, lighting: { day: ['day', 'daylight', 'sunny', 'bright'], night: ['night', 'dark', 'moonlight', 'stars', 'nocturnal'], sunset: ['sunset', 'dusk', 'evening', 'golden hour', 'twilight'], sunrise: ['sunrise', 'dawn', 'morning', 'aurora'], indoor: ['indoor', 'artificial', 'fluorescent', 'lamp'], blue: ['blue light', 'blue ambient', 'cyan', 'azure'], warm: ['warm', 'orange', 'amber', 'firelight'] }, objects: { tree: ['tree', 'palm', 'oak', 'pine', 'cedar'], mushroom: ['mushroom', 'fungus', 'toadstool', 'glowing mushroom'], fire: ['fire', 'campfire', 'flame', 'bonfire', 'torch'], rock: ['rock', 'stone', 'boulder', 'pebble'], water: ['water', 'ocean', 'sea', 'lake', 'river', 'pond'], building: ['building', 'house', 'structure', 'tower'], vehicle: ['car', 'vehicle', 'truck', 'bike', 'automobile'], firefly: ['firefly', 'fireflies', 'lightning bug', 'glowing bug'], wizard: ['wizard', 'mage', 'sorcerer', 'magician'], character: ['character', 'person', 'human', 'avatar', 'npc'] }, materials: { metal: ['metal', 'metallic', 'steel', 'iron', 'bronze'], wood: ['wood', 'wooden', 'timber', 'oak', 'pine'], glass: ['glass', 'transparent', 'clear', 'crystal'], fabric: ['fabric', 'cloth', 'textile', 'fabric'], stone: ['stone', 'rock', 'concrete', 'marble', 'granite'], glowing: ['glowing', 'emissive', 'luminous', 'radiant', 'shining'], magical: ['magical', 'enchanted', 'mystical', 'ethereal'] }, effects: { fog: ['fog', 'mist', 'haze', 'vapor', 'cloud'], rain: ['rain', 'rainy', 'storm', 'precipitation'], snow: ['snow', 'snowy', 'blizzard', 'flurry'], bloom: ['bloom', 'glow', 'bright', 'halo'], particles: ['particles', 'sparkles', 'dust', 'debris'] }, audio: { ambient: ['ambient', 'background', 'music', 'soundtrack', 'atmosphere'], spatial: ['spatial', 'positional', '3d sound', 'surround'], nature: ['nature', 'birds', 'wind', 'water', 'forest sounds'], magical: ['magical', 'mystical', 'enchanted', 'fantasy music'] }, animations: { walk: ['walk', 'walking', 'stroll', 'pace'], run: ['run', 'running', 'sprint'], sit: ['sit', 'sitting', 'rest'], stand: ['stand', 'standing', 'idle'], fly: ['fly', 'flying', 'hover', 'float'], glow: ['glow', 'pulse', 'flicker', 'animate'] } }; } parse(description) { const lowerDesc = description.toLowerCase(); const scene = { sceneName: this.extractSceneName(description), environment: this.detectEnvironment(lowerDesc), lighting: this.detectLighting(lowerDesc), objects: this.extractObjects(lowerDesc, description), materials: this.detectMaterials(lowerDesc), effects: this.detectEffects(lowerDesc), avatar: this.extractAvatar(lowerDesc, description), audio: this.extractAudio(lowerDesc, description), camera: this.detectCamera(lowerDesc) }; // Fill defaults this.fillDefaults(scene); return scene; } extractSceneName(description) { // Try to extract a name from quotes or first few words const match = description.match(/"([^"]+)"/); if (match) return match[1]; const words = description.split(/\s+/).slice(0, 3); return words.join('_').replace(/[^a-z0-9_]/gi, '') || 'generated_scene'; } detectEnvironment(desc) { for (const [env, keywords] of Object.entries(this.keywords.environments)) { if (keywords.some(kw => desc.includes(kw))) { return env; } } return 'room'; // default } detectLighting(desc) { const lighting = { type: 'day', hdri: null, sun_energy: 3.0, sun_angle: 0.5, ambient_strength: 0.3, color_temperature: 5500, color: [1, 1, 1], lights: [] }; if (this.keywords.lighting.night.some(kw => desc.includes(kw))) { lighting.type = 'night'; lighting.sun_energy = 0.5; lighting.ambient_strength = 0.1; lighting.color_temperature = 2000; lighting.hdri = 'night_sky'; lighting.color = [0.2, 0.2, 0.4]; } else if (this.keywords.lighting.sunset.some(kw => desc.includes(kw))) { lighting.type = 'sunset'; lighting.sun_energy = 2.0; lighting.color_temperature = 3000; lighting.hdri = 'sunset_sky'; lighting.color = [1, 0.6, 0.3]; } else if (this.keywords.lighting.sunrise.some(kw => desc.includes(kw))) { lighting.type = 'sunrise'; lighting.sun_energy = 2.5; lighting.color_temperature = 4000; lighting.hdri = 'sunrise_sky'; lighting.color = [1, 0.8, 0.5]; } // Detect blue ambient light if (this.keywords.lighting.blue.some(kw => desc.includes(kw))) { lighting.color = [0.3, 0.5, 1.0]; lighting.ambient_strength = 0.4; lighting.lights.push({ type: 'AMBIENT', color: [0.2, 0.3, 0.8], energy: 0.5 }); } return lighting; } extractObjects(desc, fullDesc) { const objects = []; let objIndex = 0; // Count objects - improved matching const counts = {}; for (const [type, keywords] of Object.entries(this.keywords.objects)) { const matches = keywords.filter(kw => desc.includes(kw)); if (matches.length > 0) { // Try to find number before the keyword const countMatch = fullDesc.match(new RegExp(`(\\d+)\\s+${matches[0]}`, 'i')); const count = countMatch ? parseInt(countMatch[1]) : (type === 'firefly' ? 10 : type === 'mushroom' ? 5 : 1); counts[type] = count; } } // Create objects for (const [type, count] of Object.entries(counts)) { for (let i = 0; i < count; i++) { objects.push(this.createObject(type, objIndex++, objects.length)); } } // Extract custom objects from description const customMatches = fullDesc.match(/(\d+)\s+(\w+)/gi); if (customMatches) { customMatches.forEach(match => { const parts = match.match(/(\d+)\s+(\w+)/i); if (parts && !this.keywords.objects[parts[2].toLowerCase()]) { const count = parseInt(parts[1]); for (let i = 0; i < count; i++) { objects.push(this.createObject('cube', objIndex++, objects.length, parts[2])); } } }); } return objects; } createObject(type, index, position, name = null) { const base = { type: 'cube', name: name || `${type}_${index}`, location: [ (position % 5 - 2) * 2 + (Math.random() - 0.5) * 0.5, Math.floor(position / 5) * 2 + (Math.random() - 0.5) * 0.5, 0.5 ], rotation: [0, 0, 0], scale: [1, 1, 1], color: [0.8, 0.8, 0.8, 1], metallic: 0.0, roughness: 0.5 }; // Type-specific defaults switch (type) { case 'tree': base.type = 'cylinder'; base.scale = [0.3, 0.3, 3]; base.location[2] = 1.5; base.color = [0.2, 0.6, 0.2, 1]; base.roughness = 0.8; break; case 'mushroom': base.type = 'sphere'; base.scale = [0.3, 0.3, 0.4]; base.location[2] = 0.2; base.color = [0.8, 0.2, 0.8, 1]; base.emission = [0.5, 0.1, 0.5, 1]; base.emissionStrength = 2.0; base.roughness = 0.3; break; case 'fire': base.type = 'cone'; base.scale = [0.5, 0.5, 1]; base.location[2] = 0.5; base.color = [1, 0.3, 0, 1]; base.emission = [1, 0.5, 0, 1]; base.emissionStrength = 3.0; base.roughness = 0.2; break; case 'firefly': base.type = 'sphere'; base.scale = [0.05, 0.05, 0.05]; base.location[2] = 1.5 + Math.random() * 0.5; base.color = [1, 1, 0.5, 1]; base.emission = [1, 1, 0.3, 1]; base.emissionStrength = 5.0; base.roughness = 0.1; base.animation = { type: 'float', speed: 1.0 }; break; case 'rock': base.type = 'sphere'; base.scale = [0.8, 0.8, 0.6]; base.color = [0.4, 0.4, 0.4, 1]; base.roughness = 0.9; break; case 'water': base.type = 'plane'; base.scale = [10, 10, 1]; base.location[2] = 0; base.color = [0.2, 0.5, 0.8, 0.7]; base.metallic = 0.1; base.roughness = 0.1; break; } return base; } detectMaterials(desc) { const materials = {}; for (const [mat, keywords] of Object.entries(this.keywords.materials)) { if (keywords.some(kw => desc.includes(kw))) { materials[mat] = true; } } return materials; } detectEffects(desc) { const effects = {}; for (const [effect, keywords] of Object.entries(this.keywords.effects)) { if (keywords.some(kw => desc.includes(kw))) { effects[effect] = true; } } return effects; } extractAvatar(desc, fullDesc) { const avatar = { present: desc.includes('avatar') || desc.includes('character') || desc.includes('person') || desc.includes('wizard') || desc.includes('mage') || desc.includes('sorcerer'), action: 'stand', position: [0, 0, 0], name: 'Avatar', scale: 1.0 }; if (avatar.present) { // Detect wizard/mage if (desc.includes('wizard') || desc.includes('mage') || desc.includes('sorcerer')) { avatar.name = 'Wizard'; avatar.scale = 1.0; } // Detect actions for (const [action, keywords] of Object.entries(this.keywords.animations)) { if (keywords.some(kw => desc.includes(kw))) { avatar.action = action; break; } } // Position based on action if (avatar.action === 'sit') { avatar.position[2] = 0.5; } else if (avatar.action === 'walk') { avatar.position = [0, 0, 0]; avatar.animation = { type: 'walk', speed: 1.0 }; } } return avatar; } extractAudio(desc, fullDesc) { const audio = { ambient: [], spatial: [] }; // Detect ambient sounds if (desc.includes('wave') || desc.includes('ocean')) { audio.ambient.push({ type: 'ocean_waves', volume: 0.5, loop: true }); } if (desc.includes('wind') || desc.includes('breeze')) { audio.ambient.push({ type: 'wind', volume: 0.3, loop: true }); } if (desc.includes('music') || desc.includes('soundtrack') || desc.includes('background')) { const isMagical = desc.includes('magical') || desc.includes('mystical') || desc.includes('enchanted'); audio.ambient.push({ type: isMagical ? 'magical_ambient' : 'ambient_music', volume: 0.4, loop: true }); } if (this.keywords.audio.magical.some(kw => desc.includes(kw))) { audio.ambient.push({ type: 'magical_ambient', volume: 0.5, loop: true }); } // Detect spatial sounds if (desc.includes('fire') || desc.includes('campfire')) { audio.spatial.push({ type: 'fire_crackle', position: [0, 0, 0], volume: 0.4, loop: true }); } return audio; } detectCamera(desc) { const camera = { position: [4, -4, 2.2], rotation: [1.05, 0, 0.78], fov: 50 }; if (desc.includes('close') || desc.includes('close-up')) { camera.position = [2, -2, 1.5]; } else if (desc.includes('far') || desc.includes('wide')) { camera.position = [8, -8, 4]; camera.fov = 70; } return camera; } fillDefaults(scene) { // Ensure lighting exists if (!scene.lighting.lights) { scene.lighting.lights = []; } // Add environment-specific objects if (scene.environment === 'forest' && scene.objects.filter(o => o.name.includes('tree')).length === 0) { for (let i = 0; i < 5; i++) { scene.objects.push(this.createObject('tree', i, scene.objects.length)); } } if (scene.environment === 'beach' && scene.objects.filter(o => o.name.includes('water')).length === 0) { scene.objects.push(this.createObject('water', 0, scene.objects.length)); } // Add magical forest elements if (scene.environment === 'magical' || scene.environment === 'forest') { const hasMushrooms = scene.objects.some(o => o.name.includes('mushroom')); const hasFireflies = scene.objects.some(o => o.name.includes('firefly')); if (!hasMushrooms && scene.effects && !scene.effects.fog) { // Add some mushrooms if not present for (let i = 0; i < 3; i++) { scene.objects.push(this.createObject('mushroom', i, scene.objects.length)); } } } // Add ground plane scene.ground = { type: 'plane', size: 20, texture: scene.environment === 'beach' ? 'sand' : scene.environment === 'forest' || scene.environment === 'magical' ? 'grass' : 'concrete', color: scene.environment === 'beach' ? [0.9, 0.8, 0.6, 1] : scene.environment === 'forest' || scene.environment === 'magical' ? [0.2, 0.4, 0.2, 1] : [0.5, 0.5, 0.5, 1], roughness: 0.8, metallic: 0.0 }; // Add default sun if not specified if (scene.lighting.type === 'day' && scene.lighting.lights.filter(l => l.type === 'SUN').length === 0) { scene.lighting.lights.push({ type: 'SUN', location: [10, -10, 10], energy: scene.lighting.sun_energy, angle: scene.lighting.sun_angle, color: scene.lighting.color }); } // Add night lighting if needed if (scene.lighting.type === 'night' && scene.lighting.lights.length === 0) { scene.lighting.lights.push({ type: 'POINT', location: [0, 0, 3], energy: 50, color: [0.3, 0.3, 0.5] }); } } }