import bpy from math import log from .material import Material from .objects import make_object_data_cache, Mesh, DistantDiskLamp from .util import escape_name, mat2str, ExportCancelled from .world import World, Camera from . import bl_info class IndentedWriter: def __init__(self, file_handle): self.f = file_handle self.indent_level = 0 self.indent_size = 4 def indent(self): self.indent_level += self.indent_size def unindent(self): self.indent_level -= self.indent_size if self.indent_level < 0: self.indent_level = 0 def write(self, text, do_indent=True): if do_indent: self.f.write(bytes(' '*self.indent_level + text, "utf-8")) else: self.f.write(bytes(text, "utf-8")) class PsychoExporter: def __init__(self, f, render_engine, depsgraph): self.w = IndentedWriter(f) self.render_engine = render_engine self.depsgraph = depsgraph self.scene = depsgraph.scene self.view_layer = depsgraph.view_layer # For camera data. res_x = int(self.scene.render.resolution_x * (self.scene.render.resolution_percentage / 100)) res_y = int(self.scene.render.resolution_y * (self.scene.render.resolution_percentage / 100)) self.camera = Camera(render_engine, depsgraph.scene.camera, float(res_x) / float(res_y)) # For world data. self.world = World(render_engine, depsgraph) # For all objects except sun lamps. self.object_data = {} # name -> cached_data self.instances = {} # instance_id -> [object_data_name, transform_list] # For all sun lamps. self.sun_lamp_data = {} # name -> cached_data self.sun_lamp_instances = {} # instance_id -> [sun_lamp_data_name, transform_list] # For all materials. self.materials = {} # name -> cached_data # Motion blur segments are rounded down to a power of two. if self.scene.psychopath.motion_blur_segments > 0: self.time_samples = (2**int(log(self.scene.psychopath.motion_blur_segments, 2))) + 1 else: self.time_samples = 1 # pre-calculate useful values for exporting motion blur. self.shutter_start = self.scene.psychopath.shutter_start self.shutter_diff = (self.scene.psychopath.shutter_end - self.scene.psychopath.shutter_start) / max(1, (self.time_samples-1)) self.fr = self.scene.frame_current def set_frame(self, frame, fraction): if fraction >= 0: self.scene.frame_set(frame, fraction) else: self.scene.frame_set(frame-1, 1.0+fraction) def export_psy(self): try: self._export_psy() except ExportCancelled: # Cleanup self.scene.frame_set(self.fr) return False else: # Cleanup self.scene.frame_set(self.fr) return True def _export_psy(self): # Info self.w.write("# Exported from Blender {} with PsychoBlend {}.{}.{}\n".format( bpy.app.version_string, bl_info["version"][0], bl_info["version"][1], bl_info["version"][2], )) # Scene begin self.w.write("\n\nScene $%s_fr%d {\n" % (escape_name(self.scene.name), self.fr)) self.w.indent() #------------------------------------------------------ # Output section. self.w.write("Output {\n") self.w.indent() self.w.write('Path [""]\n') self.w.unindent() self.w.write("}\n") #------------------------------------------------------ # RenderSettings section. self.w.write("RenderSettings {\n") self.w.indent() res_x = int(self.scene.render.resolution_x * (self.scene.render.resolution_percentage / 100)) res_y = int(self.scene.render.resolution_y * (self.scene.render.resolution_percentage / 100)) self.w.write('Resolution [%d %d]\n' % (res_x, res_y)) self.w.write("SamplesPerPixel [%d]\n" % self.scene.psychopath.spp) self.w.write("DicingRate [%f]\n" % self.scene.psychopath.dicing_rate) self.w.write('Seed [%d]\n' % self.fr) self.w.unindent() self.w.write("}\n") #------------------------------------------------------ # Collect materials. # TODO: handle situations where there are more than one # material with the same name. This can happen through # library linking. for inst in self.depsgraph.object_instances: ob = inst.object if ob.type in ['MESH']: for ms in ob.material_slots: if ms.material != None: if ms.material.name not in self.materials: self.materials[ms.material.name] = Material(self.render_engine, self.depsgraph, ms.material) #------------------------------------------------------ # Collect world and object data. try: for i in range(self.time_samples): # Check if render is cancelled if self.render_engine.test_break(): raise ExportCancelled() subframe = self.shutter_start + (self.shutter_diff*i) time = self.fr + subframe self.depsgraph.scene.frame_set(self.fr, subframe=subframe) self.depsgraph.update() # Collect camera and world data. self.camera.take_sample(self.render_engine, self.depsgraph, time) self.world.take_sample(self.render_engine, self.depsgraph, time) # Collect renderable objects. collected_objs = set() # Names of the objects whose data has already been collected. for inst in self.depsgraph.object_instances: # Check if render is cancelled if self.render_engine.test_break(): raise ExportCancelled() if inst.object.type not in ['MESH', 'LIGHT']: continue # We use this a couple of times, so make a shorthand. is_sun_lamp = inst.object.type == 'LIGHT' and inst.object.data.type == 'SUN' # TODO: handle situations where there are more than one # object with the same name. This can happen through # library linking. # Get a unique id for the instance. This is surprisingly # tricky, because the instance's "persistent_id" property # isn't globally unique, as I would have expected from # the documentation. id = None if inst.is_instance: id = ( hash((inst.object.name, inst.parent.name)), # Has to be turned into a tuple, otherwise it doesn't # work as part of the ID for some reason. tuple(inst.persistent_id), ) else: id = inst.object.name # Save the instance transforms. if is_sun_lamp: if id not in self.sun_lamp_instances: self.sun_lamp_instances[id] = [inst.object.name, [inst.matrix_world.copy()]] else: self.sun_lamp_instances[id][1] += [inst.matrix_world.copy()] else: if id not in self.instances: self.instances[id] = [inst.object.name, [inst.matrix_world.copy()]] else: self.instances[id][1] += [inst.matrix_world.copy()] # Save the object data if it hasn't already been saved. if inst.object.name not in collected_objs: collected_objs.add(inst.object.name) if is_sun_lamp: if inst.object.name not in self.sun_lamp_data: self.sun_lamp_data[inst.object.name] = DistantDiskLamp(self.render_engine, self.depsgraph, inst.object, inst.object.name) self.sun_lamp_data[inst.object.name].take_sample(self.render_engine, self.depsgraph, inst.object, time) else: if inst.object.name not in self.object_data: self.object_data[inst.object.name] = make_object_data_cache(self.render_engine, self.depsgraph, inst.object, inst.object.name) self.object_data[inst.object.name].take_sample(self.render_engine, self.depsgraph, inst.object, time) #------------------------------------------------------ # Export world and object data. self.camera.export(self.render_engine, self.w) self.world.export(self.render_engine, self.w) self.w.write("Assembly {\n") self.w.indent() # Export materials. for name in self.materials: self.materials[name].export(self.render_engine, self.w) # Export objects. for name in self.object_data: self.object_data[name].export(self.render_engine, self.w) # Export instances. for id in self.instances: [obj_name, xforms] = self.instances[id] self.render_engine.update_stats("", "Psychopath: Exporting %s instance" % obj_name) prefix = str(hex(hash(id))) name = "inst_{}__{}".format(prefix, escape_name(obj_name)) self.w.write("Instance {\n") self.w.indent() self.w.write("Data [${}]\n".format(escape_name(obj_name))) for mat in xforms: self.w.write("Transform [{}]\n".format(mat2str(mat))) self.w.unindent() self.w.write("}\n") self.w.unindent() self.w.write("}\n") finally: #------------------------------------------------------ # Cleanup collected data. self.camera.cleanup() self.world.cleanup() for data in self.sun_lamp_data: self.sun_lamp_data[data].cleanup() for data in self.object_data: self.object_data[data].cleanup() # Scene end self.w.unindent() self.w.write("}\n")