Preserve homestead context and garden-centred Blender design

This commit is contained in:
2026-09-10 20:52:43 +10:00
commit c1369d9ed7
321 changed files with 17843 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
"""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)