201 lines
10 KiB
Python
201 lines
10 KiB
Python
"""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)
|