import bpy,os,re,json,bmesh from mathutils import Vector,Matrix OUT=os.path.abspath(os.path.join(os.path.dirname(__file__),'../samford-meditation-unreal'));os.makedirs(OUT+'/Textures',exist_ok=True) source=bpy.context.scene detail=bpy.data.collections.get('DETAIL - full geometry for renders') if detail:detail.hide_viewport=False source.frame_set(2220);bpy.context.view_layer.update() origin=Vector((-11,12,1.12)) objects=list(bpy.data.collections['23 NOW - Quiet meditation room'].objects) objects += [o for o in bpy.data.collections['20 Spacious site connections'].objects if o.name.startswith('Meditation room')] objects=[o for o in objects if o.type=='MESH' and not o.name.startswith(('Meditation covered walk','Meditation walk'))] deps=bpy.context.evaluated_depsgraph_get() new=bpy.data.scenes.new('Meditation room - Unreal transfer');new.unit_settings.system='METRIC';new.unit_settings.scale_length=1 manifest={'source':'Samford solarpunk homestead','origin_blender_metres':list(origin),'room_metres':[4.5,4.8],'objects':[],'materials':{},'camera_blender_metres':[1.7,2.0,1.88],'target_blender_metres':[-.5,-.5,.43],'camera_lens_mm':25,'render_status':'Prepared for Unreal; no Unreal render has been produced by this exporter.'} new_objects=[];used=[] for i,o in enumerate(objects): ev=o.evaluated_get(deps);me=bpy.data.meshes.new_from_object(ev,preserve_all_data_layers=True,depsgraph=deps) me.transform(Matrix.Translation(-origin)@o.matrix_world) ob=bpy.data.objects.new('SM_%03d_'%i+re.sub('[^a-zA-Z0-9_]','_',o.name),me);new.collection.objects.link(ob);new_objects.append(ob) for mat in me.materials: if mat and mat not in used:used.append(mat) manifest['objects'].append({'name':ob.name,'source':o.name}) bpy.context.window.scene=new # Unwrap the small room assets; source procedural shaders remain in the original file. for o in new_objects: bpy.ops.object.select_all(action='DESELECT');o.select_set(True);bpy.context.view_layer.objects.active=o bpy.ops.object.mode_set(mode='EDIT');bpy.ops.mesh.select_all(action='SELECT');bpy.ops.uv.smart_project(island_margin=.025);bpy.ops.object.mode_set(mode='OBJECT') new.render.engine='CYCLES';new.cycles.device='CPU';new.cycles.samples=1;new.render.bake.margin=4 bpy.ops.object.select_all(action='DESELECT');bpy.ops.mesh.primitive_plane_add(size=2);tile=bpy.context.object;tile.name='Temporary material baking tile' replacement={} for i,m in enumerate(used): key='M_%02d_'%i+re.sub('[^a-zA-Z0-9_]','_',m.name) p=next((n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED'),None) if m.use_nodes else None base=list(m.diffuse_color);rough=float(p.inputs['Roughness'].default_value) if p else .7;metal=float(p.inputs['Metallic'].default_value) if p else 0 record={'source':m.name,'base_color':base,'roughness':rough,'metallic':metal,'emission':0,'transmission':0} if p: record['emission']=float(p.inputs['Emission Strength'].default_value) record['transmission']=float(p.inputs['Transmission Weight'].default_value) exported=bpy.data.materials.new(key);exported.use_nodes=True;ep=exported.node_tree.nodes.get('Principled BSDF');ep.inputs['Base Color'].default_value=base;ep.inputs['Roughness'].default_value=rough;ep.inputs['Metallic'].default_value=metal if p and p.inputs['Base Color'].is_linked: bake=m.copy();tile.data.materials.clear();tile.data.materials.append(bake);nodes=bake.node_tree.nodes;links=bake.node_tree.links;bp=next(n for n in nodes if n.type=='BSDF_PRINCIPLED');output=next(n for n in nodes if n.type=='OUTPUT_MATERIAL') im=bpy.data.images.new(key+'_BaseColor',width=512,height=512);im.filepath_raw=OUT+'/Textures/'+key+'_BaseColor.png';im.file_format='PNG' target=nodes.new('ShaderNodeTexImage');target.image=im;nodes.active=target emission=nodes.new('ShaderNodeEmission');links.new(bp.inputs['Base Color'].links[0].from_socket,emission.inputs['Color']);links.new(emission.outputs[0],output.inputs['Surface']) bpy.ops.object.select_all(action='DESELECT');tile.select_set(True);bpy.context.view_layer.objects.active=tile;bpy.ops.object.bake(type='EMIT');im.save() tex=exported.node_tree.nodes.new('ShaderNodeTexImage');tex.image=im;exported.node_tree.links.new(tex.outputs['Color'],ep.inputs['Base Color']);record['base_color_texture']='Textures/'+key+'_BaseColor.png' if bp.inputs['Normal'].is_linked: normal_image=bpy.data.images.new(key+'_Normal',width=512,height=512) normal_image.colorspace_settings.name='Non-Color';normal_image.filepath_raw=OUT+'/Textures/'+key+'_Normal.png';normal_image.file_format='PNG' target.image=normal_image;nodes.active=target;links.new(bp.outputs['BSDF'],output.inputs['Surface']) bpy.ops.object.bake(type='NORMAL');normal_image.save() normal_tex=exported.node_tree.nodes.new('ShaderNodeTexImage');normal_tex.image=normal_image normal_node=exported.node_tree.nodes.new('ShaderNodeNormalMap');exported.node_tree.links.new(normal_tex.outputs['Color'],normal_node.inputs['Color']);exported.node_tree.links.new(normal_node.outputs['Normal'],ep.inputs['Normal']) record['normal_texture']='Textures/'+key+'_Normal.png' record['normal_convention']='OpenGL +Y' bpy.data.materials.remove(bake) replacement[m]=exported;manifest['materials'][key]=record print('MATERIAL_READY '+key,flush=True) for o in new_objects: for slot in o.material_slots: if slot.material in replacement:slot.material=replacement[slot.material] bpy.data.objects.remove(tile,do_unlink=True) # This deliberately keeps the viewing face open for the simple room render. bpy.ops.object.select_all(action='DESELECT') for o in new_objects:o.select_set(True) bpy.context.view_layer.objects.active=new_objects[0] bpy.ops.export_scene.fbx(filepath=OUT+'/MeditationRoom.fbx',use_selection=True,object_types={'MESH'},use_mesh_modifiers=False,add_leaf_bones=False,bake_anim=False,axis_forward='-Y',axis_up='Z',path_mode='RELATIVE') bpy.ops.export_scene.gltf(filepath=OUT+'/MeditationRoom.glb',export_format='GLB',use_selection=True,export_animations=False) json.dump(manifest,open(OUT+'/meditation-room-manifest.json','w'),indent=2) bpy.ops.wm.save_as_mainfile(filepath=OUT+'/meditation-room-transfer.blend') print('MEDITATION_EXPORT_READY '+str(len(new_objects))+' meshes, '+str(len(used))+' materials',flush=True)