"""Import the actual Blender room, light it, save the level, and capture UE's viewport.""" import unreal as u import json import math import time from pathlib import Path ROOT = Path(__file__).resolve().parents[3] DATA = json.loads((ROOT / 'meditation-room-manifest.json').read_text()) LIB = u.EditorAssetLibrary MAT = u.MaterialEditingLibrary TOOLS = u.AssetToolsHelpers.get_asset_tools() ACTORS = u.get_editor_subsystem(u.EditorActorSubsystem) LEVELS = u.get_editor_subsystem(u.LevelEditorSubsystem) u.EditorPythonScripting.set_keep_python_script_alive(True) def log(message): u.log('SAMFORD: ' + message) def prop(obj, key, value): obj.set_editor_property(key, value) def import_file(filename, destination, options=None): task = u.AssetImportTask() for key, value in dict(filename=str(filename), destination_path=destination, automated=True, replace_existing=True, save=True).items(): prop(task, key, value) if options: prop(task, 'options', options) TOOLS.import_asset_tasks([task]) return [LIB.load_asset(p) for p in task.imported_object_paths] def constant(material, value, output): node = MAT.create_material_expression(material, u.MaterialExpressionConstant) prop(node, 'r', value) MAT.connect_material_property(node, '', output) return node def make_material(name, record): path = '/Game/Meditation/Materials/' + name material = LIB.load_asset(path) if not material: material = TOOLS.create_asset(name, '/Game/Meditation/Materials', u.Material, u.MaterialFactoryNew()) MAT.delete_all_material_expressions(material) prop(material, 'two_sided', True) if record.get('base_color_texture'): texture = import_file(ROOT / record['base_color_texture'], '/Game/Meditation/Textures')[0] color = MAT.create_material_expression(material, u.MaterialExpressionTextureSample) prop(color, 'texture', texture) color_output = 'RGB' else: color = MAT.create_material_expression(material, u.MaterialExpressionConstant3Vector) rgba = record['base_color'] prop(color, 'constant', u.LinearColor(*rgba)) color_output = '' MAT.connect_material_property(color, color_output, u.MaterialProperty.MP_BASE_COLOR) if record.get('normal_texture'): texture = import_file(ROOT / record['normal_texture'], '/Game/Meditation/Textures')[0] prop(texture, 'compression_settings', u.TextureCompressionSettings.TC_NORMALMAP) prop(texture, 'srgb', False) prop(texture, 'flip_green_channel', True) LIB.save_loaded_asset(texture) normal = MAT.create_material_expression(material, u.MaterialExpressionTextureSample) prop(normal, 'texture', texture) prop(normal, 'sampler_type', u.MaterialSamplerType.SAMPLERTYPE_NORMAL) MAT.connect_material_property(normal, 'RGB', u.MaterialProperty.MP_NORMAL) constant(material, record['roughness'], u.MaterialProperty.MP_ROUGHNESS) constant(material, record['metallic'], u.MaterialProperty.MP_METALLIC) if record.get('transmission', 0) > 0: prop(material, 'blend_mode', u.BlendMode.BLEND_TRANSLUCENT) constant(material, .12, u.MaterialProperty.MP_OPACITY) if record.get('emission', 0) > 0: emission = MAT.create_material_expression(material, u.MaterialExpressionConstant3Vector) rgb = [v * record['emission'] for v in record['base_color'][:3]] prop(emission, 'constant', u.LinearColor(*rgb, 1)) MAT.connect_material_property(emission, '', u.MaterialProperty.MP_EMISSIVE_COLOR) MAT.recompile_material(material) LIB.save_loaded_asset(material) return material materials = {name: make_material(name, record) for name, record in DATA['materials'].items()} log('Materials ready') options = u.FbxImportUI() for key, value in dict(import_mesh=True, import_materials=False, import_textures=False, import_as_skeletal=False, import_animations=False, mesh_type_to_import=u.FBXImportType.FBXIT_STATIC_MESH).items(): prop(options, key, value) mesh_options = options.static_mesh_import_data for key, value in dict(combine_meshes=False, auto_generate_collision=False, generate_lightmap_u_vs=False, convert_scene=True, force_front_x_axis=False).items(): prop(mesh_options, key, value) import_file(ROOT / 'MeditationRoom.fbx', '/Game/Meditation/Meshes', options) meshes = [LIB.load_asset(p) for p in LIB.list_assets('/Game/Meditation/Meshes', recursive=True)] meshes = [m for m in meshes if isinstance(m, u.StaticMesh)] if len(meshes) != len(DATA['objects']): raise RuntimeError('Expected %d room meshes, found %d' % (len(DATA['objects']), len(meshes))) LEVELS.new_level('/Game/Meditation/MeditationRoom') bounds = {} for mesh in meshes: for i, slot in enumerate(mesh.static_materials): slot_name = str(slot.material_slot_name) match = next((name for name in materials if slot_name == name or slot_name.startswith(name)), None) if match: mesh.set_material(i, materials[match]) else: raise RuntimeError('Unmapped material slot: ' + slot_name) LIB.save_loaded_asset(mesh) actor = ACTORS.spawn_actor_from_class(u.StaticMeshActor, u.Vector(0, 0, 0)) actor.static_mesh_component.set_static_mesh(mesh) actor.set_actor_label(mesh.get_name()) b = mesh.get_bounds() bounds[mesh.get_name()] = [b.origin.x, b.origin.y, b.origin.z] # The FBX coordinate conversion is checked from an asymmetric landmark. bench = next(p for name, p in bounds.items() if 'Meditation_low_timber_bench' in name) if abs(bench[0] + 160) < 3 and abs(bench[1] - 10) < 3: def position(values): return u.Vector(values[0]*100, -values[1]*100, values[2]*100) elif abs(bench[0] - 10) < 3 and abs(bench[1] + 160) < 3: def position(values): return u.Vector(-values[1]*100, values[0]*100, values[2]*100) else: (ROOT / 'import-bounds.json').write_text(json.dumps(bounds, indent=2)) raise RuntimeError('Check FBX orientation: bench center ' + repr(bench)) def spawn(cls, label, xyz, rotation=None): actor = ACTORS.spawn_actor_from_class(cls, position(xyz), rotation or u.Rotator(0, 0, 0)) actor.set_actor_label(label) return actor camera_pos = position(DATA['camera_blender_metres']) target = position(DATA['target_blender_metres']) rotation = u.MathLibrary.find_look_at_rotation(camera_pos, target) camera = ACTORS.spawn_actor_from_class(u.CameraActor, camera_pos, rotation) camera.set_actor_label('Meditation room - morning view') camera.camera_component.set_field_of_view(math.degrees(2*math.atan(36/(2*DATA['camera_lens_mm'])))) prop(camera.camera_component, 'aspect_ratio', 16/9) sun = spawn(u.DirectionalLight, 'Gentle morning sunlight', (4, 6, 8), u.Rotator(-28, 35, 0)) sun.light_component.set_mobility(u.ComponentMobility.MOVABLE) sun.light_component.set_intensity(3500) sun.light_component.set_light_color(u.LinearColor(1, .78, .55, 1)) prop(sun.light_component, 'light_source_angle', 3) prop(sun.light_component, 'atmosphere_sun_light', True) spawn(u.SkyAtmosphere, 'Subtropical morning sky', (0, 0, 0)) sky = spawn(u.SkyLight, 'Soft ambient sky', (0, 0, 4)) sky.light_component.set_mobility(u.ComponentMobility.MOVABLE) prop(sky.light_component, 'real_time_capture', True) sky.light_component.set_intensity(1.5) fill_position = position((0, 3.7, 2.1)) fill_rot = u.MathLibrary.find_look_at_rotation(fill_position, position((0, -.5, 1))) fill = ACTORS.spawn_actor_from_class(u.RectLight, fill_position, fill_rot) fill.set_actor_label('Soft daylight through open viewing face') fill.light_component.set_mobility(u.ComponentMobility.MOVABLE) fill.light_component.set_intensity(3200) fill.light_component.set_light_color(u.LinearColor(1, .9, .75, 1)) prop(fill.light_component, 'source_width', 380) prop(fill.light_component, 'source_height', 220) prop(fill.light_component, 'attenuation_radius', 1200) for name, center in bounds.items(): if 'Small_candle_flame' in name: light = ACTORS.spawn_actor_from_class(u.PointLight, u.Vector(*center)) light.set_actor_label('Warm candle glow') light.light_component.set_mobility(u.ComponentMobility.MOVABLE) light.light_component.set_intensity(5) light.light_component.set_light_color(u.LinearColor(1, .45, .12, 1)) prop(light.light_component, 'attenuation_radius', 110) post = spawn(u.PostProcessVolume, 'Calm natural exposure', (0, 0, 0)) prop(post, 'unbound', True) settings = post.settings for key, value in dict(override_auto_exposure_min_brightness=True, override_auto_exposure_max_brightness=True, auto_exposure_min_brightness=5.5, auto_exposure_max_brightness=5.5, override_auto_exposure_bias=True, auto_exposure_bias=0, override_bloom_intensity=True, bloom_intensity=.2, override_vignette_intensity=True, vignette_intensity=.15).items(): prop(settings, key, value) prop(post, 'settings', settings) u.EditorLevelLibrary.set_level_viewport_camera_info(camera_pos, rotation) u.AutomationLibrary.set_editor_viewport_view_mode(u.ViewModeIndex.VMI_LIT) LEVELS.save_current_level() LIB.save_directory('/Game/Meditation', only_if_is_dirty=True, recursive=True) (ROOT / 'unreal-scene-checks.json').write_text(json.dumps({'meshes': len(meshes), 'materials': len(materials), 'bench_center_cm': bench, 'status': 'Unreal level saved; render pending'}, indent=2)) log('Level saved. Waiting for lighting and shaders before capture.') capture_state = {'started': time.monotonic(), 'task': None} def tick(delta): elapsed = time.monotonic() - capture_state['started'] if elapsed > 40 and capture_state['task'] is None: u.AutomationLibrary.finish_loading_before_screenshot() capture_state['task'] = u.AutomationLibrary.take_high_res_screenshot(2560, 1440, str(ROOT / 'meditation-room-unreal.png'), camera=camera, delay=5) elif capture_state['task'] and capture_state['task'].is_task_done(): u.unregister_slate_post_tick_callback(capture_handle) log('UNREAL_RENDER_READY ' + str(ROOT / 'meditation-room-unreal.png')) capture_handle = u.register_slate_post_tick_callback(tick)