diff --git a/psychoblend/assembly.py b/psychoblend/assembly.py new file mode 100644 index 0000000..ce90d7a --- /dev/null +++ b/psychoblend/assembly.py @@ -0,0 +1,265 @@ +import bpy + +from .util import escape_name, mat2str, needs_def_mb, needs_xform_mb + +class Assembly: + def __init__(self, render_engine, objects, visible_layers, group_prefix="", translation_offset=(0,0,0)): + self.name = group_prefix + self.translation_offset = translation_offset + self.render_engine = render_engine + + self.materials = [] + self.objects = [] + self.instances = [] + + self.mesh_names = set() + self.assembly_names = set() + + # Collect all the objects, materials, instances, etc. + for ob in objects: + # Check if render is cancelled + if render_engine.test_break(): + raise ExportCancelled() + + # Check if the object is visible for rendering + vis_layer = False + for i in range(len(ob.layers)): + vis_layer = vis_layer or (ob.layers[i] and visible_layers[i]) + if ob.hide_render or not vis_layer: + continue + + # Store object data + name = None + + if ob.type == 'EMPTY': + if ob.dupli_type == 'GROUP': + name = group_prefix + "__" + escape_name(ob.dupli_group.name) + if name not in self.assembly_names: + self.assembly_names.add(name) + self.objects += [Assembly(self.render_engine, ob.dupli_group.objects, ob.dupli_group.layers, name, ob.dupli_group.dupli_offset*-1)] + elif ob.type == 'MESH': + name = self.get_mesh(ob, group_prefix) + elif ob.type == 'LAMP' and ob.data.type == 'POINT': + name = self.get_sphere_lamp(ob, group_prefix) + elif ob.type == 'LAMP' and ob.data.type == 'AREA': + name = self.get_rect_lamp(ob, group_prefix) + + # Store instance + if name != None: + self.instances += [Instance(render_engine, ob, name)] + + def export(self, render_engine, w): + if self.name == "": + w.write("Assembly {\n") + else: + w.write("Assembly $%s {\n" % self.name) + w.indent() + + for mat in self.materials: + # Check if render is cancelled + if render_engine.test_break(): + raise ExportCancelled() + mat.export(render_engine, w) + + for ob in self.objects: + # Check if render is cancelled + if render_engine.test_break(): + raise ExportCancelled() + ob.export(render_engine, w) + + for inst in self.instances: + # Check if render is cancelled + if render_engine.test_break(): + raise ExportCancelled() + inst.export(render_engine, w) + + w.unindent() + w.write("}\n") + + #---------------- + + def take_sample(self, render_engine, scene, time): + for mat in self.materials: + mat.take_sample(render_engine, scene, time) + for ob in self.objects: + ob.take_sample(render_engine, scene, time) + for inst in self.instances: + inst.take_sample(render_engine, time, self.translation_offset) + + def cleanup(self): + for mat in self.materials: + mat.cleanup() + for ob in self.objects: + ob.cleanup() + + def get_mesh(self, ob, group_prefix): + # Figure out if we need to export or not and figure out what name to + # export with. + has_modifiers = len(ob.modifiers) > 0 + deform_mb = needs_def_mb(ob) + if has_modifiers or deform_mb: + mesh_name = group_prefix + escape_name("__" + ob.name + "__" + ob.data.name + "_") + else: + mesh_name = group_prefix + escape_name("__" + ob.data.name + "_") + should_export_mesh = mesh_name not in self.mesh_names + + # Get mesh + if should_export_mesh: + self.mesh_names.add(mesh_name) + self.objects += [Mesh(self.render_engine, ob, mesh_name)] + return mesh_name + else: + return None + + def get_sphere_lamp(self, ob, group_prefix): + name = group_prefix + "__" + escape_name(ob.name) + self.objects += [SphereLamp(self.render_engine, ob, name)] + return name + + def get_rect_lamp(self, ob, group_prefix): + name = group_prefix + "__" + escape_name(ob.name) + self.objects += [RectLamp(self.render_engine, ob, name)] + return name + + +#========================================================================= + + +class Mesh: + """ Holds data for a mesh to be exported. + """ + def __init__(self, render_engine, ob, name): + self.ob = ob + self.name = name + self.time_meshes = [] + + def take_sample(self, render_engine, scene, time): + render_engine.update_stats("", "Psychopath: Collecting '%s' at time %f" % (self.ob.name, time)) + self.time_meshes += [self.ob.to_mesh(scene, True, 'RENDER')] + + def cleanup(self): + for mesh in self.time_meshes: + bpy.data.meshes.remove(mesh) + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) + + if self.ob.data.psychopath.is_subdivision_surface == False: + # Exporting normal mesh + w.write("MeshSurface $%s {\n" % self.name) + w.indent() + else: + # Exporting subdivision surface cage + w.write("SubdivisionSurface $%s {\n" % self.name) + w.indent() + + # Write vertices + for ti in range(len(self.time_meshes)): + w.write("Vertices [") + w.write(" ".join([("%f" % i) for vert in self.time_meshes[ti].vertices for i in vert.co]), False) + w.write("]\n", False) + + # Write face vertex counts + w.write("FaceVertCounts [") + w.write(" ".join([("%d" % len(p.vertices)) for p in self.time_meshes[0].polygons]), False) + w.write("]\n", False) + + # Write face vertex indices + w.write("FaceVertIndices [") + w.write(" ".join([("%d"%v) for p in self.time_meshes[0].polygons for v in p.vertices]), False) + w.write("]\n", False) + + # MeshSurface/SubdivisionSurface section end + w.unindent() + w.write("}\n") + + +class SphereLamp: + """ Holds data for a sphere light to be exported. + """ + def __init__(self, render_engine, ob, name): + self.ob = ob + self.name = name + self.time_col = [] + self.time_rad = [] + + def take_sample(self, render_engine, scene, time): + render_engine.update_stats("", "Psychopath: Collecting '%s' at time %f" % (self.ob.name, time)) + self.time_col += [self.ob.data.color * self.ob.data.energy] + self.time_rad += [self.ob.data.shadow_soft_size] + + def cleanup(self): + pass + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) + + w.write("SphereLight $%s {\n" % self.name) + w.indent() + for col in self.time_col: + w.write("Color [%f %f %f]\n" % (col[0], col[1], col[2])) + for rad in self.time_rad: + w.write("Radius [%f]\n" % rad) + + w.unindent() + w.write("}\n") + + +class RectLamp: + """ Holds data for a rectangular light to be exported. + """ + def __init__(self, render_engine, ob, name): + self.ob = ob + self.name = name + self.time_col = [] + self.time_dim = [] + + def take_sample(self, render_engine, scene, time): + render_engine.update_stats("", "Psychopath: Collecting '%s' at time %f" % (self.ob.name, time)) + self.time_col += [self.ob.data.color * self.ob.data.energy] + if ob.data.shape == 'RECTANGLE': + self.time_dim += [(self.ob.data.size, self.ob.data.size_y)] + else: + self.time_dim += [(self.ob.data.size, self.ob.data.size)] + + def cleanup(self): + pass + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) + + w.write("RectangleLight $%s {\n" % self.name) + w.indent() + for col in self.time_col: + w.write("Color [%f %f %f]\n" % (col[0], col[1], col[2])) + for dim in self.time_dim: + w.write("Dimensions [%f %f]\n" % dim) + + w.unindent() + w.write("}\n") + + +class Instance: + def __init__(self, render_engine, ob, data_name): + self.ob = ob + self.data_name = data_name + self.time_xforms = [] + + def take_sample(self, render_engine, time, translation_offset): + render_engine.update_stats("", "Psychopath: Collecting '%s' at time %f" % (self.ob.name, time)) + mat = self.ob.matrix_world.copy() + mat[0][3] += translation_offset[0] + mat[1][3] += translation_offset[1] + mat[2][3] += translation_offset[2] + self.time_xforms += [mat] + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) + + w.write("Instance {\n") + w.indent() + w.write("Data [$%s]\n" % self.data_name) + for mat in self.time_xforms: + w.write("Transform [%s]\n" % mat2str(mat.inverted())) + w.unindent() + w.write("}\n") diff --git a/psychoblend/psy_export.py b/psychoblend/psy_export.py index 7457b5a..6480894 100644 --- a/psychoblend/psy_export.py +++ b/psychoblend/psy_export.py @@ -3,6 +3,9 @@ import bpy from math import degrees, pi, log from mathutils import Vector, Matrix +from .assembly import Assembly +from .util import escape_name, mat2str + class ExportCancelled(Exception): """ Indicates that the render was cancelled in the middle of exporting the scene file. @@ -10,66 +13,6 @@ class ExportCancelled(Exception): pass -def mat2str(m): - """ Converts a matrix into a single-line string of values. - """ - s = "" - for j in range(4): - for i in range(4): - s += (" %f" % m[i][j]) - return s[1:] - - -def needs_def_mb(ob): - """ Determines if the given object needs to be exported with - deformation motion blur or not. - """ - for mod in ob.modifiers: - if mod.type == 'SUBSURF': - pass - elif mod.type == 'MIRROR': - if mod.mirror_object == None: - pass - else: - return True - else: - return True - - if ob.type == 'MESH': - if ob.data.shape_keys == None: - pass - else: - return True - - return False - -def escape_name(name): - name = name.replace("\\", "\\\\") - name = name.replace(" ", "\\ ") - name = name.replace("$", "\\$") - name = name.replace("[", "\\[") - name = name.replace("]", "\\]") - name = name.replace("{", "\\{") - name = name.replace("}", "\\}") - return name - - -def needs_xform_mb(ob): - """ Determines if the given object needs to be exported with - transformation motion blur or not. - """ - if ob.animation_data != None: - return True - - if len(ob.constraints) > 0: - return True - - if ob.parent != None: - return needs_xform_mb(ob.parent) - - return False - - class IndentedWriter: def __init__(self, file_handle): self.f = file_handle @@ -91,8 +34,6 @@ class IndentedWriter: self.f.write(bytes(text, "utf-8")) - - class PsychoExporter: def __init__(self, f, render_engine, scene): self.w = IndentedWriter(f) @@ -231,253 +172,23 @@ class PsychoExporter: ####################### # Export objects and materials - # TODO: handle materials from linked files (as used in group - # instances) properly. - self.w.write("Assembly {\n") - self.w.indent() - self.export_materials(bpy.data.materials) - self.export_objects(self.scene.objects, self.scene.layers) - self.w.unindent() - self.w.write("}\n") + try: + root_assembly = Assembly(self.render_engine, self.scene.objects, self.scene.layers) + for i in range(self.time_samples): + time = self.fr + self.shutter_start + (self.shutter_diff*i) + self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) + root_assembly.take_sample(self.render_engine, self.scene, time) + root_assembly.export(self.render_engine, self.w) + except ExportCancelled: + root_assembly.cleanup() + raise ExportCancelled() + else: + root_assembly.cleanup() # Scene end self.w.unindent() self.w.write("}\n") - - - def export_materials(self, materials): - for m in materials: - self.w.write("SurfaceShader $%s {\n" % escape_name(m.name)) - self.w.indent() - self.w.write("Type [%s]\n" % m.psychopath.surface_shader_type) - self.w.write("Color [%f %f %f]\n" % (m.psychopath.color[0], m.psychopath.color[1], m.psychopath.color[2])) - if m.psychopath.surface_shader_type == 'GTR': - self.w.write("Roughness [%f]\n" % m.psychopath.roughness) - self.w.write("TailShape [%f]\n" % m.psychopath.tail_shape) - self.w.write("Fresnel [%f]\n" % m.psychopath.fresnel) - self.w.unindent() - self.w.write("}\n") - - - def export_objects(self, objects, visible_layers, group_prefix="", translation_offset=(0,0,0)): - for ob in objects: - # Check if render is cancelled - if self.render_engine.test_break(): - raise ExportCancelled() - - # Check if the object is visible for rendering - vis_layer = False - for i in range(len(ob.layers)): - vis_layer = vis_layer or (ob.layers[i] and visible_layers[i]) - if ob.hide_render or not vis_layer: - continue - - name = None - - # Write object data - if ob.type == 'EMPTY': - if ob.dupli_type == 'GROUP': - name = group_prefix + "__" + escape_name(ob.dupli_group.name) - if name not in self.group_names: - self.group_names[name] = True - self.w.write("Assembly $%s {\n" % name) - self.w.indent() - self.export_objects(ob.dupli_group.objects, ob.dupli_group.layers, name, ob.dupli_group.dupli_offset*-1) - self.w.unindent() - self.w.write("}\n") - elif ob.type == 'MESH': - name = self.export_mesh_object(ob, group_prefix) - elif ob.type == 'SURFACE': - name = self.export_surface_object(ob, group_prefix) - elif ob.type == 'LAMP' and ob.data.type == 'POINT': - name = self.export_sphere_lamp(ob, group_prefix) - elif ob.type == 'LAMP' and ob.data.type == 'AREA': - name = self.export_area_lamp(ob, group_prefix) - - # Write object instance, with transforms - if name != None: - time_mats = [] - - if needs_xform_mb(ob): - for i in range(self.time_samples): - # Check if render is cancelled - if self.render_engine.test_break(): - raise ExportCancelled() - self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) - mat = ob.matrix_world.copy() - mat[0][3] += translation_offset[0] - mat[1][3] += translation_offset[1] - mat[2][3] += translation_offset[2] - time_mats += [mat] - else: - mat = ob.matrix_world.copy() - mat[0][3] += translation_offset[0] - mat[1][3] += translation_offset[1] - mat[2][3] += translation_offset[2] - time_mats += [mat] - - self.w.write("Instance {\n") - self.w.indent() - self.w.write("Data [$%s]\n" % name) - if len(ob.material_slots) > 0 and ob.material_slots[0].material != None: - self.w.write("SurfaceShaderBind [$%s]\n" % escape_name(ob.material_slots[0].material.name)) - for i in range(len(time_mats)): - mat = time_mats[i].inverted() - self.w.write("Transform [%s]\n" % mat2str(mat)) - self.w.unindent() - self.w.write("}\n") - - - def export_mesh_object(self, ob, group_prefix): - # Determine if and how to export the mesh data - has_modifiers = len(ob.modifiers) > 0 - deform_mb = needs_def_mb(ob) - if has_modifiers or deform_mb: - mesh_name = group_prefix + escape_name("__" + ob.name + "__" + ob.data.name + "_") - else: - mesh_name = group_prefix + escape_name("__" + ob.data.name + "_") - export_mesh = (mesh_name not in self.mesh_names) or has_modifiers or deform_mb - - # Collect time samples - time_meshes = [] - if deform_mb: - for i in range(self.time_samples): - # Check if render is cancelled - if self.render_engine.test_break(): - raise ExportCancelled() - self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) - if export_mesh and (deform_mb or i == 0): - time_meshes += [ob.to_mesh(self.scene, True, 'RENDER')] - elif export_mesh: - time_meshes += [ob.to_mesh(self.scene, True, 'RENDER')] - - # Export mesh data if necessary - if export_mesh: - if ob.data.psychopath.is_subdivision_surface == False: - # Exporting normal mesh - self.mesh_names[mesh_name] = True - self.w.write("MeshSurface $%s {\n" % mesh_name) - self.w.indent() - elif ob.data.psychopath.is_subdivision_surface == True: - # Exporting subdivision surface cage - self.mesh_names[mesh_name] = True - self.w.write("SubdivisionSurface $%s {\n" % mesh_name) - self.w.indent() - - # Write vertices - for ti in range(len(time_meshes)): - self.w.write("Vertices [") - self.w.write(" ".join([("%f" % i) for vert in time_meshes[ti].vertices for i in vert.co]), False) - self.w.write("]\n", False) - - # Write face vertex counts - self.w.write("FaceVertCounts [") - self.w.write(" ".join([("%d" % len(p.vertices)) for p in time_meshes[0].polygons]), False) - self.w.write("]\n", False) - - # Write face vertex indices - self.w.write("FaceVertIndices [") - self.w.write(" ".join([("%d"%v) for p in time_meshes[0].polygons for v in p.vertices]), False) - self.w.write("]\n", False) - - # MeshSurface/SubdivisionSurface section end - self.w.unindent() - self.w.write("}\n") - - for mesh in time_meshes: - bpy.data.meshes.remove(mesh) - - return mesh_name - - - def export_surface_object(self, ob, group_prefix): - name = group_prefix + "__" + escape_name(ob.name) - - # Collect time samples - time_surfaces = [] - for i in range(self.time_samples): - # Check if render is cancelled - if self.render_engine.test_break(): - raise ExportCancelled() - self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) - time_surfaces += [ob.data.copy()] - - # Write patch - self.w.write("BicubicPatch $" + name + " {\n") - self.w.indent() - for i in range(self.time_samples): - verts = time_surfaces[i].splines[0].points - vstr = "" - for v in verts: - vstr += ("%f %f %f " % (v.co[0], v.co[1], v.co[2])) - self.w.write("Vertices [%s]\n" % vstr[:-1]) - for s in time_surfaces: - bpy.data.curves.remove(s) - self.w.unindent() - self.w.write("}\n") - - return name - - - def export_sphere_lamp(self, ob, group_prefix): - name = group_prefix + "__" + escape_name(ob.name) - - # Collect data over time - time_col = [] - time_rad = [] - for i in range(self.time_samples): - # Check if render is cancelled - if self.render_engine.test_break(): - raise ExportCancelled() - self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) - time_col += [ob.data.color * ob.data.energy] - time_rad += [ob.data.shadow_soft_size] - - # Write out sphere light - self.w.write("SphereLight $%s {\n" % name) - self.w.indent() - for col in time_col: - self.w.write("Color [%f %f %f]\n" % (col[0], col[1], col[2])) - for rad in time_rad: - self.w.write("Radius [%f]\n" % rad) - - self.w.unindent() - self.w.write("}\n") - - return name - - def export_area_lamp(self, ob, group_prefix): - name = group_prefix + "__" + escape_name(ob.name) - - # Collect data over time - time_col = [] - time_dim = [] - for i in range(self.time_samples): - # Check if render is cancelled - if self.render_engine.test_break(): - raise ExportCancelled() - self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) - time_col += [ob.data.color * ob.data.energy] - if ob.data.shape == 'RECTANGLE': - time_dim += [(ob.data.size, ob.data.size_y)] - else: - time_dim += [(ob.data.size, ob.data.size)] - - - # Write out sphere light - self.w.write("RectangleLight $%s {\n" % name) - self.w.indent() - for col in time_col: - self.w.write("Color [%f %f %f]\n" % (col[0], col[1], col[2])) - for dim in time_dim: - self.w.write("Dimensions [%f %f]\n" % dim) - - self.w.unindent() - self.w.write("}\n") - - return name - def export_world_distant_disk_lamp(self, ob, group_prefix): name = group_prefix + "__" + escape_name(ob.name) diff --git a/psychoblend/render.py b/psychoblend/render.py index f4b39e1..acb4b32 100644 --- a/psychoblend/render.py +++ b/psychoblend/render.py @@ -120,16 +120,20 @@ class PsychopathRender(bpy.types.RenderEngine): self.update_stats("", "Psychopath: Not found") return - self.update_stats("", "Psychopath: Exporting data from Blender") + self.update_stats("", "Psychopath: Collecting...") # Export to Psychopath's stdin - if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy(): - # Render cancelled in the middle of exporting, - # so just return. - return - self._process.stdin.write(bytes("__PSY_EOF__", "utf-8")) - self._process.stdin.flush() + try: + if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy(): + # Render cancelled in the middle of exporting, + # so just return. + return + self._process.stdin.write(bytes("__PSY_EOF__", "utf-8")) + self._process.stdin.flush() + except: + self._process.terminate() + raise - self.update_stats("", "Psychopath: Rendering") + self.update_stats("", "Psychopath: Building") else: # Export to file self.update_stats("", "Psychopath: Exporting data from Blender") @@ -189,6 +193,8 @@ class PsychopathRender(bpy.types.RenderEngine): else: continue + self.update_stats("", "Psychopath: Rendering") + # Clear output buffer, since it's all in 'outputs' now. output = b"" diff --git a/psychoblend/util.py b/psychoblend/util.py new file mode 100644 index 0000000..bd1721f --- /dev/null +++ b/psychoblend/util.py @@ -0,0 +1,58 @@ +def mat2str(m): + """ Converts a matrix into a single-line string of values. + """ + s = "" + for j in range(4): + for i in range(4): + s += (" %f" % m[i][j]) + return s[1:] + + +def needs_def_mb(ob): + """ Determines if the given object needs to be exported with + deformation motion blur or not. + """ + for mod in ob.modifiers: + if mod.type == 'SUBSURF': + pass + elif mod.type == 'MIRROR': + if mod.mirror_object == None: + pass + else: + return True + else: + return True + + if ob.type == 'MESH': + if ob.data.shape_keys == None: + pass + else: + return True + + return False + +def escape_name(name): + name = name.replace("\\", "\\\\") + name = name.replace(" ", "\\ ") + name = name.replace("$", "\\$") + name = name.replace("[", "\\[") + name = name.replace("]", "\\]") + name = name.replace("{", "\\{") + name = name.replace("}", "\\}") + return name + + +def needs_xform_mb(ob): + """ Determines if the given object needs to be exported with + transformation motion blur or not. + """ + if ob.animation_data != None: + return True + + if len(ob.constraints) > 0: + return True + + if ob.parent != None: + return needs_xform_mb(ob.parent) + + return False \ No newline at end of file