Skip to main content
Glama
lighting.ts56.3 kB
// Lighting tools for Unreal Engine import { UnrealBridge } from '../unreal-bridge.js'; import { parseStandardResult } from '../utils/python-output.js'; import { escapePythonString } from '../utils/python.js'; export class LightingTools { constructor(private bridge: UnrealBridge) {} private ensurePythonSpawnSucceeded(label: string, result: any) { let logs = ''; if (Array.isArray(result?.LogOutput)) { logs = result.LogOutput.map((l: any) => String(l.Output || '')).join(''); } else if (typeof result === 'string') { logs = result; } // If Python reported a traceback or explicit failure, propagate as error if (/Traceback|Error:|Failed to spawn/i.test(logs)) { throw new Error(`Unreal reported error spawning '${label}': ${logs}`); } // If script executed (ReturnValue true) and no error patterns, treat as success const executed = result?.ReturnValue === true || result?.ReturnValue === 'true'; if (executed) return; // Fallback: if no ReturnValue but success-like logs exist, accept if (/spawned/i.test(logs)) return; // Otherwise, uncertain throw new Error(`Uncertain spawn result for '${label}'. Engine logs:\n${logs}`); } private normalizeName(value: unknown, fallback?: string): string { if (typeof value === 'string') { const trimmed = value.trim(); if (trimmed.length > 0) { return trimmed; } } if (typeof fallback === 'string') { const trimmedFallback = fallback.trim(); if (trimmedFallback.length > 0) { return trimmedFallback; } } throw new Error('Invalid name: must be a non-empty string'); } // Create directional light async createDirectionalLight(params: { name: string; intensity?: number; color?: [number, number, number]; rotation?: [number, number, number]; castShadows?: boolean; temperature?: number; }) { const name = this.normalizeName(params.name); const escapedName = escapePythonString(name); // Validate numeric parameters if (params.intensity !== undefined) { if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) { throw new Error(`Invalid intensity value: ${params.intensity}`); } if (params.intensity < 0) { throw new Error('Invalid intensity: must be non-negative'); } } if (params.temperature !== undefined) { if (typeof params.temperature !== 'number' || !isFinite(params.temperature)) { throw new Error(`Invalid temperature value: ${params.temperature}`); } } // Validate arrays if (params.color !== undefined) { if (!Array.isArray(params.color) || params.color.length !== 3) { throw new Error('Invalid color: must be an array [r,g,b]'); } for (const c of params.color) { if (typeof c !== 'number' || !isFinite(c)) { throw new Error('Invalid color component: must be finite numbers'); } } } if (params.rotation !== undefined) { if (!Array.isArray(params.rotation) || params.rotation.length !== 3) { throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]'); } for (const r of params.rotation) { if (typeof r !== 'number' || !isFinite(r)) { throw new Error('Invalid rotation component: must be finite numbers'); } } } const rot = params.rotation || [0, 0, 0]; // Build property setters const propSetters: string[] = []; if (params.intensity !== undefined) { propSetters.push(` light_component.set_intensity(${params.intensity})`); } if (params.color) { propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`); } if (params.castShadows !== undefined) { propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`); } if (params.temperature !== undefined) { propSetters.push(` light_component.set_temperature(${params.temperature})`); } const propertiesCode = propSetters.length > 0 ? propSetters.join('\n') : ' pass # No additional properties'; const pythonScript = ` import unreal # Get editor subsystem editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) # Spawn the directional light directional_light_class = unreal.DirectionalLight spawn_location = unreal.Vector(0, 0, 500) spawn_rotation = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]}) # Spawn the actor spawned_light = editor_actor_subsystem.spawn_actor_from_class( directional_light_class, spawn_location, spawn_rotation ) if spawned_light: # Set the label/name spawned_light.set_actor_label("${escapedName}") # Get the light component light_component = spawned_light.get_component_by_class(unreal.DirectionalLightComponent) if light_component: ${propertiesCode} print("Directional light '${escapedName}' spawned") else: print("Failed to spawn directional light '${escapedName}'") `; // Execute the Python script via bridge (UE 5.6-compatible) const result = await this.bridge.executePython(pythonScript); this.ensurePythonSpawnSucceeded(name, result); return { success: true, message: `Directional light '${name}' spawned` }; } // Create point light async createPointLight(params: { name: string; location?: [number, number, number]; intensity?: number; radius?: number; color?: [number, number, number]; falloffExponent?: number; castShadows?: boolean; }) { const name = this.normalizeName(params.name); const escapedName = escapePythonString(name); // Validate location array if (params.location !== undefined) { if (!Array.isArray(params.location) || params.location.length !== 3) { throw new Error('Invalid location: must be an array [x,y,z]'); } for (const l of params.location) { if (typeof l !== 'number' || !isFinite(l)) { throw new Error('Invalid location component: must be finite numbers'); } } } // Default location if not provided const location = params.location || [0, 0, 0]; // Validate numeric parameters if (params.intensity !== undefined) { if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) { throw new Error(`Invalid intensity value: ${params.intensity}`); } if (params.intensity < 0) { throw new Error('Invalid intensity: must be non-negative'); } } if (params.radius !== undefined) { if (typeof params.radius !== 'number' || !isFinite(params.radius)) { throw new Error(`Invalid radius value: ${params.radius}`); } if (params.radius < 0) { throw new Error('Invalid radius: must be non-negative'); } } if (params.falloffExponent !== undefined) { if (typeof params.falloffExponent !== 'number' || !isFinite(params.falloffExponent)) { throw new Error(`Invalid falloffExponent value: ${params.falloffExponent}`); } } // Validate color array if (params.color !== undefined) { if (!Array.isArray(params.color) || params.color.length !== 3) { throw new Error('Invalid color: must be an array [r,g,b]'); } for (const c of params.color) { if (typeof c !== 'number' || !isFinite(c)) { throw new Error('Invalid color component: must be finite numbers'); } } } // Build property setters const propSetters: string[] = []; if (params.intensity !== undefined) { propSetters.push(` light_component.set_intensity(${params.intensity})`); } if (params.radius !== undefined) { propSetters.push(` light_component.set_attenuation_radius(${params.radius})`); } if (params.color) { propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`); } if (params.castShadows !== undefined) { propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`); } if (params.falloffExponent !== undefined) { propSetters.push(` light_component.set_light_falloff_exponent(${params.falloffExponent})`); } const propertiesCode = propSetters.length > 0 ? propSetters.join('\n') : ' pass # No additional properties'; const pythonScript = ` import unreal # Get editor subsystem editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) # Spawn the point light point_light_class = unreal.PointLight spawn_location = unreal.Vector(${location[0]}, ${location[1]}, ${location[2]}) spawn_rotation = unreal.Rotator(0, 0, 0) # Spawn the actor spawned_light = editor_actor_subsystem.spawn_actor_from_class( point_light_class, spawn_location, spawn_rotation ) if spawned_light: # Set the label/name spawned_light.set_actor_label("${escapedName}") # Get the light component light_component = spawned_light.get_component_by_class(unreal.PointLightComponent) if light_component: ${propertiesCode} print(f"Point light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}") else: print("Failed to spawn point light '${escapedName}'") `; // Execute the Python script via bridge (UE 5.6-compatible) const result = await this.bridge.executePython(pythonScript); this.ensurePythonSpawnSucceeded(name, result); return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` }; } // Create spot light async createSpotLight(params: { name: string; location: [number, number, number]; rotation: [number, number, number]; intensity?: number; innerCone?: number; outerCone?: number; radius?: number; color?: [number, number, number]; castShadows?: boolean; }) { const name = this.normalizeName(params.name); const escapedName = escapePythonString(name); // Validate required location and rotation arrays if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) { throw new Error('Invalid location: must be an array [x,y,z]'); } for (const l of params.location) { if (typeof l !== 'number' || !isFinite(l)) { throw new Error('Invalid location component: must be finite numbers'); } } if (!params.rotation || !Array.isArray(params.rotation) || params.rotation.length !== 3) { throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]'); } for (const r of params.rotation) { if (typeof r !== 'number' || !isFinite(r)) { throw new Error('Invalid rotation component: must be finite numbers'); } } // Validate optional numeric parameters if (params.intensity !== undefined) { if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) { throw new Error(`Invalid intensity value: ${params.intensity}`); } if (params.intensity < 0) { throw new Error('Invalid intensity: must be non-negative'); } } if (params.innerCone !== undefined) { if (typeof params.innerCone !== 'number' || !isFinite(params.innerCone)) { throw new Error(`Invalid innerCone value: ${params.innerCone}`); } if (params.innerCone < 0 || params.innerCone > 180) { throw new Error('Invalid innerCone: must be between 0 and 180 degrees'); } } if (params.outerCone !== undefined) { if (typeof params.outerCone !== 'number' || !isFinite(params.outerCone)) { throw new Error(`Invalid outerCone value: ${params.outerCone}`); } if (params.outerCone < 0 || params.outerCone > 180) { throw new Error('Invalid outerCone: must be between 0 and 180 degrees'); } } if (params.radius !== undefined) { if (typeof params.radius !== 'number' || !isFinite(params.radius)) { throw new Error(`Invalid radius value: ${params.radius}`); } if (params.radius < 0) { throw new Error('Invalid radius: must be non-negative'); } } // Validate color array if (params.color !== undefined) { if (!Array.isArray(params.color) || params.color.length !== 3) { throw new Error('Invalid color: must be an array [r,g,b]'); } for (const c of params.color) { if (typeof c !== 'number' || !isFinite(c)) { throw new Error('Invalid color component: must be finite numbers'); } } } // Build property setters const propSetters: string[] = []; if (params.intensity !== undefined) { propSetters.push(` light_component.set_intensity(${params.intensity})`); } if (params.innerCone !== undefined) { propSetters.push(` light_component.set_inner_cone_angle(${params.innerCone})`); } if (params.outerCone !== undefined) { propSetters.push(` light_component.set_outer_cone_angle(${params.outerCone})`); } if (params.radius !== undefined) { propSetters.push(` light_component.set_attenuation_radius(${params.radius})`); } if (params.color) { propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`); } if (params.castShadows !== undefined) { propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`); } const propertiesCode = propSetters.length > 0 ? propSetters.join('\n') : ' pass # No additional properties'; const pythonScript = ` import unreal # Get editor subsystem editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) # Spawn the spot light spot_light_class = unreal.SpotLight spawn_location = unreal.Vector(${params.location[0]}, ${params.location[1]}, ${params.location[2]}) spawn_rotation = unreal.Rotator(${params.rotation[0]}, ${params.rotation[1]}, ${params.rotation[2]}) # Spawn the actor spawned_light = editor_actor_subsystem.spawn_actor_from_class( spot_light_class, spawn_location, spawn_rotation ) if spawned_light: # Set the label/name spawned_light.set_actor_label("${escapedName}") # Get the light component light_component = spawned_light.get_component_by_class(unreal.SpotLightComponent) if light_component: ${propertiesCode} print(f"Spot light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}") else: print("Failed to spawn spot light '${escapedName}'") `; // Execute the Python script via bridge (UE 5.6-compatible) const result = await this.bridge.executePython(pythonScript); this.ensurePythonSpawnSucceeded(name, result); return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` }; } // Create rect light async createRectLight(params: { name: string; location: [number, number, number]; rotation: [number, number, number]; width?: number; height?: number; intensity?: number; color?: [number, number, number]; }) { const name = this.normalizeName(params.name); const escapedName = escapePythonString(name); // Validate required location and rotation arrays if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) { throw new Error('Invalid location: must be an array [x,y,z]'); } for (const l of params.location) { if (typeof l !== 'number' || !isFinite(l)) { throw new Error('Invalid location component: must be finite numbers'); } } if (!params.rotation || !Array.isArray(params.rotation) || params.rotation.length !== 3) { throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]'); } for (const r of params.rotation) { if (typeof r !== 'number' || !isFinite(r)) { throw new Error('Invalid rotation component: must be finite numbers'); } } // Validate optional numeric parameters if (params.width !== undefined) { if (typeof params.width !== 'number' || !isFinite(params.width)) { throw new Error(`Invalid width value: ${params.width}`); } if (params.width <= 0) { throw new Error('Invalid width: must be positive'); } } if (params.height !== undefined) { if (typeof params.height !== 'number' || !isFinite(params.height)) { throw new Error(`Invalid height value: ${params.height}`); } if (params.height <= 0) { throw new Error('Invalid height: must be positive'); } } if (params.intensity !== undefined) { if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) { throw new Error(`Invalid intensity value: ${params.intensity}`); } if (params.intensity < 0) { throw new Error('Invalid intensity: must be non-negative'); } } // Validate color array if (params.color !== undefined) { if (!Array.isArray(params.color) || params.color.length !== 3) { throw new Error('Invalid color: must be an array [r,g,b]'); } for (const c of params.color) { if (typeof c !== 'number' || !isFinite(c)) { throw new Error('Invalid color component: must be finite numbers'); } } } // Build property setters const propSetters: string[] = []; if (params.intensity !== undefined) { propSetters.push(` light_component.set_intensity(${params.intensity})`); } if (params.color) { propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`); } if (params.width !== undefined) { propSetters.push(` light_component.set_source_width(${params.width})`); } if (params.height !== undefined) { propSetters.push(` light_component.set_source_height(${params.height})`); } const propertiesCode = propSetters.length > 0 ? propSetters.join('\n') : ' pass # No additional properties'; const pythonScript = ` import unreal # Get editor subsystem editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) # Spawn the rect light rect_light_class = unreal.RectLight spawn_location = unreal.Vector(${params.location[0]}, ${params.location[1]}, ${params.location[2]}) spawn_rotation = unreal.Rotator(${params.rotation[0]}, ${params.rotation[1]}, ${params.rotation[2]}) # Spawn the actor spawned_light = editor_actor_subsystem.spawn_actor_from_class( rect_light_class, spawn_location, spawn_rotation ) if spawned_light: # Set the label/name spawned_light.set_actor_label("${escapedName}") # Get the light component light_component = spawned_light.get_component_by_class(unreal.RectLightComponent) if light_component: ${propertiesCode} print(f"Rect light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}") else: print("Failed to spawn rect light '${escapedName}'") `; // Execute the Python script via bridge (UE 5.6-compatible) const result = await this.bridge.executePython(pythonScript); this.ensurePythonSpawnSucceeded(name, result); return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` }; } // Create sky light async createSkyLight(params: { name: string; sourceType?: 'CapturedScene' | 'SpecifiedCubemap'; cubemapPath?: string; intensity?: number; recapture?: boolean; }) { const name = this.normalizeName(params.name); const escapedName = escapePythonString(name); const sourceTypeRaw = typeof params.sourceType === 'string' ? params.sourceType.trim() : undefined; const normalizedSourceType = sourceTypeRaw ? sourceTypeRaw.toLowerCase() === 'specifiedcubemap' ? 'SpecifiedCubemap' : sourceTypeRaw.toLowerCase() === 'capturedscene' ? 'CapturedScene' : undefined : undefined; const cubemapPath = typeof params.cubemapPath === 'string' ? params.cubemapPath.trim() : undefined; if (normalizedSourceType === 'SpecifiedCubemap' && (!cubemapPath || cubemapPath.length === 0)) { const message = 'cubemapPath is required when sourceType is SpecifiedCubemap'; return { success: false, error: message, message }; } const escapedCubemapPath = cubemapPath ? escapePythonString(cubemapPath) : ''; const python = ` import unreal import json result = { "success": False, "message": "", "error": "", "warnings": [] } def add_warning(text): if text: result["warnings"].append(str(text)) def finish(): if result["success"]: if not result["message"]: result["message"] = "Sky light ensured" result.pop("error", None) else: if not result["error"]: result["error"] = result["message"] or "Failed to ensure sky light" if not result["message"]: result["message"] = result["error"] if not result["warnings"]: result.pop("warnings", None) print('RESULT:' + json.dumps(result)) try: actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) if not actor_sub: result["error"] = "EditorActorSubsystem unavailable" finish() raise SystemExit(0) spawn_location = unreal.Vector(0.0, 0.0, 500.0) spawn_rotation = unreal.Rotator(0.0, 0.0, 0.0) actor = None try: for candidate in actor_sub.get_all_level_actors(): try: if candidate.get_class().get_name() == 'SkyLight': actor = candidate break except Exception: continue except Exception: pass if actor is None: actor = actor_sub.spawn_actor_from_class(unreal.SkyLight, spawn_location, spawn_rotation) if not actor: result["error"] = "Failed to spawn SkyLight actor" finish() raise SystemExit(0) try: actor.set_actor_label("${escapedName}") except Exception: pass comp = actor.get_component_by_class(unreal.SkyLightComponent) if not comp: result["error"] = "SkyLight component missing" finish() raise SystemExit(0) ${params.intensity !== undefined ? ` try: comp.set_intensity(${params.intensity}) except Exception: try: comp.set_editor_property('intensity', ${params.intensity}) except Exception: add_warning('Unable to set intensity property') ` : ''} source_type = ${normalizedSourceType ? `'${normalizedSourceType}'` : 'None'} if source_type: try: comp.set_editor_property('source_type', getattr(unreal.SkyLightSourceType, source_type)) except Exception: try: comp.source_type = getattr(unreal.SkyLightSourceType, source_type) except Exception: add_warning(f"Unable to set source type {source_type}") if source_type == 'SpecifiedCubemap': path = "${escapedCubemapPath}" if not path: result["error"] = "cubemapPath is required when sourceType is SpecifiedCubemap" finish() raise SystemExit(0) try: exists = unreal.EditorAssetLibrary.does_asset_exist(path) except Exception: exists = False if not exists: result["error"] = f"Cubemap asset not found: {path}" finish() raise SystemExit(0) try: cube = unreal.EditorAssetLibrary.load_asset(path) except Exception as load_err: result["error"] = f"Failed to load cubemap asset: {load_err}" finish() raise SystemExit(0) if not cube: result["error"] = f"Cubemap asset could not be loaded: {path}" finish() raise SystemExit(0) try: if hasattr(comp, 'set_cubemap'): comp.set_cubemap(cube) else: comp.set_editor_property('cubemap', cube) except Exception as assign_err: result["error"] = f"Failed to assign cubemap: {assign_err}" finish() raise SystemExit(0) if ${params.recapture ? 'True' : 'False'}: try: comp.recapture_sky() except Exception as recapture_err: add_warning(f"Recapture failed: {recapture_err}") result["success"] = True result["message"] = "Sky light ensured" finish() except SystemExit: pass except Exception as run_err: result["error"] = str(run_err) finish() `.trim(); const resp = await this.bridge.executePython(python); const parsed = parseStandardResult(resp).data; if (parsed) { if (parsed.success) { return { success: true, message: parsed.message ?? 'Sky light ensured', warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined }; } return { success: false, error: parsed.error ?? parsed.message ?? 'Failed to ensure sky light', warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined }; } return { success: true, message: 'Sky light ensured' }; } // Remove duplicate SkyLights and keep only one (named target label) async ensureSingleSkyLight(params?: { name?: string; recapture?: boolean }) { const fallbackName = 'MCP_Test_Sky'; const name = this.normalizeName(params?.name, fallbackName); const escapedName = escapePythonString(name); const recapture = !!params?.recapture; const py = `\nimport unreal, json\nactor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\nactors = actor_sub.get_all_level_actors() if actor_sub else []\nskies = []\nfor a in actors:\n try:\n if a.get_class().get_name() == 'SkyLight':\n skies.append(a)\n except Exception: pass\nkeep = None\n# Prefer one with matching label; otherwise keep the first\nfor a in skies:\n try:\n label = a.get_actor_label()\n if label == "${escapedName}":\n keep = a\n break\n except Exception: pass\nif keep is None and len(skies) > 0:\n keep = skies[0]\n# Rename the kept one if needed\nif keep is not None:\n try: keep.set_actor_label("${escapedName}")\n except Exception: pass\n# Destroy all others using the correct non-deprecated API\nremoved = 0\nfor a in skies:\n if keep is not None and a == keep:\n continue\n try:\n # Use EditorActorSubsystem.destroy_actor instead of deprecated EditorLevelLibrary\n actor_sub.destroy_actor(a)\n removed += 1\n except Exception: pass\n# Optionally recapture\nif keep is not None and ${recapture ? 'True' : 'False'}:\n try:\n comp = keep.get_component_by_class(unreal.SkyLightComponent)\n if comp: comp.recapture_sky()\n except Exception: pass\nprint('RESULT:' + json.dumps({'success': True, 'removed': removed, 'kept': True if keep else False}))\n`.trim(); const resp = await this.bridge.executePython(py); let out = ''; if (resp?.LogOutput && Array.isArray((resp as any).LogOutput)) { out = (resp as any).LogOutput.map((l: any) => l.Output || '').join(''); } else if (typeof resp === 'string') { out = resp; } else { out = JSON.stringify(resp); } const m = out.match(/RESULT:({.*})/); if (m) { try { const parsed = JSON.parse(m[1]); if (parsed.success) { return { success: true, removed: parsed.removed, message: `Ensured single SkyLight (removed ${parsed.removed})` }; } } catch {} } return { success: true, message: 'Ensured single SkyLight' }; } // Setup global illumination async setupGlobalIllumination(params: { method: 'Lightmass' | 'LumenGI' | 'ScreenSpace' | 'None'; quality?: 'Low' | 'Medium' | 'High' | 'Epic'; indirectLightingIntensity?: number; bounces?: number; }) { const commands = []; switch (params.method) { case 'Lightmass': commands.push('r.DynamicGlobalIlluminationMethod 0'); break; case 'LumenGI': commands.push('r.DynamicGlobalIlluminationMethod 1'); break; case 'ScreenSpace': commands.push('r.DynamicGlobalIlluminationMethod 2'); break; case 'None': commands.push('r.DynamicGlobalIlluminationMethod 3'); break; } if (params.quality) { const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Epic': 3 }; commands.push(`r.Lumen.Quality ${qualityMap[params.quality]}`); } if (params.indirectLightingIntensity !== undefined) { commands.push(`r.IndirectLightingIntensity ${params.indirectLightingIntensity}`); } if (params.bounces !== undefined) { commands.push(`r.Lumen.MaxReflectionBounces ${params.bounces}`); } for (const cmd of commands) { await this.bridge.executeConsoleCommand(cmd); } return { success: true, message: 'Global illumination configured' }; } // Configure shadows async configureShadows(params: { shadowQuality?: 'Low' | 'Medium' | 'High' | 'Epic'; cascadedShadows?: boolean; shadowDistance?: number; contactShadows?: boolean; rayTracedShadows?: boolean; }) { const commands = []; if (params.shadowQuality) { const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Epic': 3 }; commands.push(`r.ShadowQuality ${qualityMap[params.shadowQuality]}`); } if (params.cascadedShadows !== undefined) { commands.push(`r.Shadow.CSM.MaxCascades ${params.cascadedShadows ? 4 : 1}`); } if (params.shadowDistance !== undefined) { commands.push(`r.Shadow.DistanceScale ${params.shadowDistance}`); } if (params.contactShadows !== undefined) { commands.push(`r.ContactShadows ${params.contactShadows ? 1 : 0}`); } if (params.rayTracedShadows !== undefined) { commands.push(`r.RayTracing.Shadows ${params.rayTracedShadows ? 1 : 0}`); } for (const cmd of commands) { await this.bridge.executeConsoleCommand(cmd); } return { success: true, message: 'Shadow settings configured' }; } // Build lighting (Python-based) async buildLighting(params: { quality?: 'Preview' | 'Medium' | 'High' | 'Production'; buildOnlySelected?: boolean; // ignored in Python path buildReflectionCaptures?: boolean; }) { const q = params.quality || 'High'; const qualityMap: Record<string, string> = { 'Preview': 'QUALITY_PREVIEW', 'Medium': 'QUALITY_MEDIUM', 'High': 'QUALITY_HIGH', 'Production': 'QUALITY_PRODUCTION' }; const qualityEnum = qualityMap[q] || 'QUALITY_HIGH'; // First try to ensure precomputed lighting is allowed and force-no-precomputed is disabled, then save changes const disablePrecomputedPy = ` import unreal, json messages = [] # Precheck: verify project supports static lighting (Support Static Lighting) try: rs = unreal.get_default_object(unreal.RendererSettings) support_static = False try: support_static = bool(rs.get_editor_property('bSupportStaticLighting')) except Exception: try: support_static = bool(rs.get_editor_property('support_static_lighting')) except Exception: support_static = False if not support_static: print('RESULT:' + json.dumps({ 'success': False, 'status': 'staticDisabled', 'error': 'Project has Support Static Lighting disabled (r.AllowStaticLighting=0). Enable Project Settings -> Rendering -> Support Static Lighting and restart the editor.' })) raise SystemExit(0) else: messages.append('Support Static Lighting is enabled') except Exception as e: messages.append(f'Precheck failed: {e}') # Ensure runtime CVar does not force disable precomputed lighting try: unreal.SystemLibrary.execute_console_command(None, 'r.ForceNoPrecomputedLighting 0') messages.append('Set r.ForceNoPrecomputedLighting 0') except Exception as e: messages.append(f'r.ForceNoPrecomputedLighting failed: {e}') # Temporarily disable source control prompts to avoid checkout dialogs during automated saves try: prefs = unreal.SourceControlPreferences() try: prefs.set_enable_source_control(False) except Exception: try: prefs.enable_source_control = False except Exception: pass messages.append('Disabled Source Control for this session') except Exception as e: messages.append(f'SourceControlPreferences modify failed: {e}') try: ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) world = ues.get_editor_world() if ues else None if world: world_settings = world.get_world_settings() if world_settings: # Mark for modification try: world_settings.modify() except Exception: pass # Try all known variants of the property name for prop in ['force_no_precomputed_lighting', 'bForceNoPrecomputedLighting']: try: world_settings.set_editor_property(prop, False) messages.append(f"Set WorldSettings.{prop}=False") except Exception as e: messages.append(f"Failed setting {prop}: {e}") # Also update the Class Default Object (CDO) to help persistence in some versions try: ws_class = world_settings.get_class() ws_cdo = unreal.get_default_object(ws_class) if ws_cdo: try: ws_cdo.set_editor_property('bForceNoPrecomputedLighting', False) messages.append('Set CDO bForceNoPrecomputedLighting=False') except Exception: pass try: ws_cdo.set_editor_property('force_no_precomputed_lighting', False) messages.append('Set CDO force_no_precomputed_lighting=False') except Exception: pass except Exception as e: messages.append(f'CDO update failed: {e}') # Apply and save level to persist change try: if hasattr(world_settings, 'post_edit_change'): world_settings.post_edit_change() except Exception: pass # Save current level/package try: wp = world.get_path_name() pkg_path = wp.split('.')[0] if '.' in wp else wp unreal.EditorAssetLibrary.save_asset(pkg_path) messages.append(f'Saved world asset: {pkg_path}') except Exception as e: messages.append(f'Failed to save world asset: {e}') # Secondary save method try: if les: les.save_current_level() messages.append('LevelEditorSubsystem.save_current_level called') except Exception as e: messages.append(f'save_current_level failed: {e}') # Verify final value(s) try: force_val = None bforce_val = None try: force_val = bool(world_settings.get_editor_property('force_no_precomputed_lighting')) except Exception: pass try: bforce_val = bool(world_settings.get_editor_property('bForceNoPrecomputedLighting')) except Exception: pass messages.append(f'Verify WorldSettings.force_no_precomputed_lighting={force_val}') messages.append(f'Verify WorldSettings.bForceNoPrecomputedLighting={bforce_val}') except Exception as e: messages.append(f'Verify failed: {e}') except Exception as e: messages.append(f'World modification failed: {e}') print('RESULT:' + json.dumps({'success': True, 'messages': messages, 'flags': { 'force_no_precomputed_lighting': force_val if 'force_val' in locals() else None, 'bForceNoPrecomputedLighting': bforce_val if 'bforce_val' in locals() else None }})) `.trim(); // Execute the disable script first and parse messages for diagnostics const preResp = await this.bridge.executePython(disablePrecomputedPy); try { const preOut = typeof preResp === 'string' ? preResp : JSON.stringify(preResp); const pm = preOut.match(/RESULT:({.*})/); if (pm) { try { const preJson = JSON.parse(pm[1]); if (preJson && preJson.success === false && preJson.status === 'staticDisabled') { return { success: false, error: preJson.error } as any; } if (preJson && preJson.flags) { const f = preJson.flags as any; if (f.bForceNoPrecomputedLighting === true || f.force_no_precomputed_lighting === true) { return { success: false, error: 'WorldSettings.bForceNoPrecomputedLighting is true. Unreal will skip static lighting builds. Please uncheck "Force No Precomputed Lighting" in this level\'s World Settings (or enable Support Static Lighting in Project Settings) and retry. If using source control, check out the map asset first.' } as any; } } } catch {} } } catch {} // Small delay to ensure settings are applied await new Promise(resolve => setTimeout(resolve, 150)); // Now execute the lighting build const py = ` import unreal import json try: les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) if les: # Build light maps with specified quality and reflection captures option les.build_light_maps(unreal.LightingBuildQuality.${qualityEnum}, ${params.buildReflectionCaptures !== false ? 'True' : 'False'}) print('RESULT:' + json.dumps({'success': True, 'message': 'Lighting build started via LevelEditorSubsystem'})) else: # Fallback: Try using console command if subsystem not available try: unreal.SystemLibrary.execute_console_command(None, 'BuildLighting Quality=${q}') ${params.buildReflectionCaptures ? "unreal.SystemLibrary.execute_console_command(None, 'BuildReflectionCaptures')" : ''} print('RESULT:' + json.dumps({'success': True, 'message': 'Lighting build started via console command (fallback)'})) except Exception as e2: print('RESULT:' + json.dumps({'success': False, 'error': f'Build failed: {str(e2)}'})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); const resp = await this.bridge.executePython(py); const out = typeof resp === 'string' ? resp : JSON.stringify(resp); const m = out.match(/RESULT:({.*})/); if (m) { try { const parsed = JSON.parse(m[1]); return parsed.success ? { success: true, message: parsed.message } : { success: false, error: parsed.error }; } catch {} } return { success: true, message: 'Lighting build started' }; } // Create a new level with proper lighting settings as workaround async createLightingEnabledLevel(params?: { levelName?: string; copyActors?: boolean; useTemplate?: boolean; }) { const levelName = params?.levelName || 'LightingEnabledLevel'; const py = ` import unreal import json def create_lighting_enabled_level(): """Create a new level with lighting enabled""" try: les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) editor_asset = unreal.EditorAssetLibrary if not les or not ues: return {'success': False, 'error': 'Required subsystems not available'} # Store current actors if we need to copy them actors_to_copy = [] if ${params?.copyActors ? 'True' : 'False'}: current_world = ues.get_editor_world() if current_world: all_actors = actor_sub.get_all_level_actors() # Filter out unnecessary actors - only copy static meshes and important gameplay actors for actor in all_actors: if actor: class_name = actor.get_class().get_name() # Only copy specific actor types if class_name in ['StaticMeshActor', 'SkeletalMeshActor', 'Blueprint', 'Actor']: try: actor_data = { 'class': actor.get_class(), 'location': actor.get_actor_location(), 'rotation': actor.get_actor_rotation(), 'scale': actor.get_actor_scale3d(), 'label': actor.get_actor_label() } # Check if actor has a static mesh component mesh_comp = actor.get_component_by_class(unreal.StaticMeshComponent) if mesh_comp: mesh = mesh_comp.get_editor_property('static_mesh') if mesh: actor_data['mesh'] = mesh actors_to_copy.append(actor_data) except: pass print(f'Stored {len(actors_to_copy)} actors to copy') # Create new level with proper template or blank level_name_str = "${levelName}" level_path = f'/Game/Maps/{level_name_str}' # Try different approaches to create a level with lighting enabled level_created = False # Method 1: Try using the Default template (not Blank) try: # The Default template should have lighting enabled template_path = '/Engine/Maps/Templates/Template_Default' if editor_asset.does_asset_exist(template_path): les.new_level_from_template(level_path, template_path) print(f'Created level from Default template: {level_path}') level_created = True except: pass # Method 2: Try TimeOfDay template if not level_created: try: template_path = '/Engine/Maps/Templates/TimeOfDay' if editor_asset.does_asset_exist(template_path): les.new_level_from_template(level_path, template_path) print(f'Created level from TimeOfDay template: {level_path}') level_created = True except: pass # Method 3: Create blank and manually configure if not level_created: les.new_level(level_path, False) print(f'Created new blank level: {level_path}') level_created = True # CRITICAL: Force disable ForceNoPrecomputedLighting using all possible methods new_world = ues.get_editor_world() if new_world: new_ws = new_world.get_world_settings() if new_ws: # Method 1: Direct property modification for prop in ['force_no_precomputed_lighting', 'bForceNoPrecomputedLighting', 'ForceNoPrecomputedLighting', 'bforce_no_precomputed_lighting']: try: new_ws.set_editor_property(prop, False) except: pass # Method 2: Modify via reflection try: # Access the property through the class default object ws_class = new_ws.get_class() ws_cdo = unreal.get_default_object(ws_class) if ws_cdo: ws_cdo.set_editor_property('force_no_precomputed_lighting', False) ws_cdo.set_editor_property('bForceNoPrecomputedLighting', False) except: pass # Method 3: Override with Lightmass settings try: # Create proper Lightmass settings lightmass_settings = unreal.LightmassWorldInfoSettings() lightmass_settings.static_lighting_level_scale = 1.0 lightmass_settings.num_indirect_lighting_bounces = 3 lightmass_settings.use_ambient_occlusion = True lightmass_settings.generate_ambient_occlusion_material_mask = False new_ws.set_editor_property('lightmass_settings', lightmass_settings) except: pass # Method 4: Force save and reload to apply changes try: # Mark the world settings as dirty new_ws.modify() # Save immediately les.save_current_level() # Force update new_world.force_update_level_bounds() except: pass # Verify the setting try: val = new_ws.get_editor_property('force_no_precomputed_lighting') print(f'New level force_no_precomputed_lighting: {val}') if val: print('WARNING: ForceNoPrecomputedLighting is persistent - project setting override detected') print('WORKAROUND: Will use dynamic lighting only') except: pass # Copy actors if requested if actors_to_copy and actor_sub: print('Copying actors to new level...') copied = 0 for actor_data in actors_to_copy: try: # Spawn a static mesh actor if we have mesh data if 'mesh' in actor_data: # Create a proper static mesh actor spawned = actor_sub.spawn_actor_from_class( unreal.StaticMeshActor, actor_data['location'], actor_data['rotation'] ) if spawned: spawned.set_actor_scale3d(actor_data['scale']) spawned.set_actor_label(actor_data['label']) # Set the static mesh mesh_comp = spawned.get_component_by_class(unreal.StaticMeshComponent) if mesh_comp: mesh_comp.set_static_mesh(actor_data['mesh']) copied += 1 else: # Spawn regular actor spawned = actor_sub.spawn_actor_from_class( actor_data['class'], actor_data['location'], actor_data['rotation'] ) if spawned: spawned.set_actor_scale3d(actor_data['scale']) spawned.set_actor_label(actor_data['label']) copied += 1 except Exception as e: pass # Silently skip failed copies print(f'Successfully copied {copied} actors') # Add essential lighting actors if not using template if not use_template: # Add a directional light for sun light = actor_sub.spawn_actor_from_class( unreal.DirectionalLight, unreal.Vector(0, 0, 500), unreal.Rotator(-45, 45, 0) ) if light: light.set_actor_label('Sun_Light') light_comp = light.get_component_by_class(unreal.DirectionalLightComponent) if light_comp: light_comp.set_intensity(3.14159) # Pi lux for realistic sun light_comp.set_light_color(unreal.LinearColor(1, 0.95, 0.8, 1)) print('Added directional light') # Add sky light for ambient sky = actor_sub.spawn_actor_from_class( unreal.SkyLight, unreal.Vector(0, 0, 300), unreal.Rotator(0, 0, 0) ) if sky: sky.set_actor_label('Sky_Light') sky_comp = sky.get_component_by_class(unreal.SkyLightComponent) if sky_comp: sky_comp.set_intensity(1.0) print('Added sky light') # Add sky atmosphere for realistic sky atmosphere = actor_sub.spawn_actor_from_class( unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), unreal.Rotator(0, 0, 0) ) if atmosphere: atmosphere.set_actor_label('Sky_Atmosphere') print('Added sky atmosphere') # Save the new level les.save_current_level() print('New level saved') return { 'success': True, 'message': f'Created new level "{level_name_str}" with lighting enabled', 'path': level_path } except Exception as e: return {'success': False, 'error': str(e)} result = create_lighting_enabled_level() print('RESULT:' + json.dumps(result)) `.trim(); const resp = await this.bridge.executePython(py); const out = typeof resp === 'string' ? resp : JSON.stringify(resp); const m = out.match(/RESULT:({.*})/); if (m) { try { const parsed = JSON.parse(m[1]); return parsed; } catch {} } return { success: true, message: 'New level creation attempted' }; } // Create lightmass importance volume via Python async createLightmassVolume(params: { name: string; location: [number, number, number]; size: [number, number, number]; }) { const name = this.normalizeName(params.name); const escapedName = escapePythonString(name); const [lx, ly, lz] = params.location; const [sx, sy, sz] = params.size; const py = ` import unreal editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) loc = unreal.Vector(${lx}, ${ly}, ${lz}) rot = unreal.Rotator(0,0,0) actor = editor_actor_subsystem.spawn_actor_from_class(unreal.LightmassImportanceVolume, loc, rot) if actor: try: actor.set_actor_label("${escapedName}") except Exception: pass # Best-effort: set actor scale to approximate size try: actor.set_actor_scale3d(unreal.Vector(max(${sx}/100.0, 0.1), max(${sy}/100.0, 0.1), max(${sz}/100.0, 0.1))) except Exception: pass print("RESULT:{'success': True}") else: print("RESULT:{'success': False, 'error': 'Failed to spawn LightmassImportanceVolume'}") `.trim(); const resp = await this.bridge.executePython(py); const out = typeof resp === 'string' ? resp : JSON.stringify(resp); const m = out.match(/RESULT:({.*})/); if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: `LightmassImportanceVolume '${name}' created` } : { success: false, error: parsed.error }; } catch {} } return { success: true, message: 'LightmassImportanceVolume creation attempted' }; } // Set exposure async setExposure(params: { method: 'Manual' | 'Auto'; compensationValue?: number; minBrightness?: number; maxBrightness?: number; }) { const commands = []; commands.push(`r.EyeAdaptation.ExposureMethod ${params.method === 'Manual' ? 0 : 1}`); if (params.compensationValue !== undefined) { commands.push(`r.EyeAdaptation.ExposureCompensation ${params.compensationValue}`); } if (params.minBrightness !== undefined) { commands.push(`r.EyeAdaptation.MinBrightness ${params.minBrightness}`); } if (params.maxBrightness !== undefined) { commands.push(`r.EyeAdaptation.MaxBrightness ${params.maxBrightness}`); } for (const cmd of commands) { await this.bridge.executeConsoleCommand(cmd); } return { success: true, message: 'Exposure settings updated' }; } // Set ambient occlusion async setAmbientOcclusion(params: { enabled: boolean; intensity?: number; radius?: number; quality?: 'Low' | 'Medium' | 'High'; }) { const commands = []; commands.push(`r.AmbientOcclusion.Enabled ${params.enabled ? 1 : 0}`); if (params.intensity !== undefined) { commands.push(`r.AmbientOcclusion.Intensity ${params.intensity}`); } if (params.radius !== undefined) { commands.push(`r.AmbientOcclusion.Radius ${params.radius}`); } if (params.quality) { const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2 }; commands.push(`r.AmbientOcclusion.Quality ${qualityMap[params.quality]}`); } for (const cmd of commands) { await this.bridge.executeConsoleCommand(cmd); } return { success: true, message: 'Ambient occlusion configured' }; } // Setup volumetric fog (prefer Python to adjust fog actor/component) async setupVolumetricFog(params: { enabled: boolean; density?: number; scatteringIntensity?: number; fogHeight?: number; // interpreted as Z location shift for ExponentialHeightFog actor }) { // Enable/disable global volumetric fog via CVar await this.bridge.executeConsoleCommand(`r.VolumetricFog ${params.enabled ? 1 : 0}`); const py = `\nimport unreal\ntry:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n actors = actor_sub.get_all_level_actors() if actor_sub else []\n fog = None\n for a in actors:\n try:\n if a.get_class().get_name() == 'ExponentialHeightFog':\n fog = a\n break\n except Exception: pass\n if fog:\n comp = fog.get_component_by_class(unreal.ExponentialHeightFogComponent)\n if comp:\n ${params.density !== undefined ? `\n try: comp.set_fog_density(${params.density})\n except Exception: comp.set_editor_property('fog_density', ${params.density})\n ` : ''} ${params.scatteringIntensity !== undefined ? `\n try: comp.set_fog_max_opacity(${Math.min(Math.max(params.scatteringIntensity,0),1)})\n except Exception: pass\n ` : ''} ${params.fogHeight !== undefined ? `\n try:\n L = fog.get_actor_location()\n fog.set_actor_location(unreal.Vector(L.x, L.y, ${params.fogHeight}))\n except Exception: pass\n ` : ''} print("RESULT:{'success': True}")\nexcept Exception as e:\n print("RESULT:{'success': False, 'error': '%s'}" % str(e))\n`.trim(); const resp = await this.bridge.executePython(py); const out = typeof resp === 'string' ? resp : JSON.stringify(resp); const m = out.match(/RESULT:({.*})/); if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: 'Volumetric fog configured' } : { success: false, error: parsed.error }; } catch {} } return { success: true, message: 'Volumetric fog configured' }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server