diff --git a/psychoblend/psy_export.py b/psychoblend/psy_export.py index c6a9007..65a7cc7 100644 --- a/psychoblend/psy_export.py +++ b/psychoblend/psy_export.py @@ -1,10 +1,10 @@ import bpy -from math import degrees, pi, log -from mathutils import Vector, Matrix +from math import log from .assembly import Assembly from .util import escape_name, mat2str, ExportCancelled +from .world import World class IndentedWriter: @@ -103,113 +103,31 @@ class PsychoExporter: self.w.unindent() self.w.write("}\n") - ####################### - # Camera section begin - self.w.write("Camera {\n") - self.w.indent() - - cam = self.scene.camera - - if cam.data.dof_object == None: - dof_distance = cam.data.dof_distance - else: - # TODO: implement DoF object tracking here - dof_distance = 0.0 - print("WARNING: DoF object tracking not yet implemented.") - - matz = Matrix() - matz[2][2] = -1 - for i in range(self.time_samples): - # Check if render is cancelled - if self.render_engine.test_break(): - raise ExportCancelled() - - if res_x >= res_y: - self.w.write("Fov [%f]\n" % degrees(cam.data.angle)) - else: - self.w.write("Fov [%f]\n" % (degrees(cam.data.angle) * res_x / res_y)) - self.w.write("FocalDistance [%f]\n" % dof_distance) - self.w.write("ApertureRadius [%f]\n" % (cam.data.psychopath.aperture_radius)) - if self.time_samples > 1: - self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) - mat = cam.matrix_world.copy() - mat = mat * matz - self.w.write("Transform [%s]\n" % mat2str(mat)) - - # Camera section end - self.w.unindent() - self.w.write("}\n") - - ####################### - # World section begin - self.w.write("World {\n") - self.w.indent() - - world = self.scene.world - - if world != None: - self.w.write("BackgroundShader {\n") - self.w.indent(); - self.w.write("Type [Color]\n") - self.w.write("Color [%f %f %f]\n" % (world.horizon_color[0], world.horizon_color[1], world.horizon_color[2])) - self.w.unindent() - self.w.write("}\n") - - # Infinite light sources - for ob in self.scene.objects: - if ob.type == 'LAMP' and ob.data.type == 'SUN': - self.export_world_distant_disk_lamp(ob, "") - - # World section end - self.w.unindent() - self.w.write("}\n") - - ####################### - # Export objects and materials + ############################### + # Export world and object data + world = None + root_assembly = None try: + # Prep for data collection + world = World(self.render_engine, self.scene, self.scene.layers, float(res_x) / float(res_y)) root_assembly = Assembly(self.render_engine, self.scene.objects, self.scene.layers) + + # Collect data for each time sample 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)) + world.take_sample(self.render_engine, self.scene, time) root_assembly.take_sample(self.render_engine, self.scene, time) + + # Export collected data + world.export(self.render_engine, self.w) root_assembly.export(self.render_engine, self.w) - except ExportCancelled: - root_assembly.cleanup() - raise - else: - root_assembly.cleanup() + finally: + if world != None: + world.cleanup() + if root_assembly != None: + root_assembly.cleanup() # Scene end self.w.unindent() self.w.write("}\n") - - def export_world_distant_disk_lamp(self, ob, group_prefix): - name = group_prefix + "__" + escape_name(ob.name) - - # Collect data over time - time_dir = [] - 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_dir += [tuple(ob.matrix_world.to_3x3() * Vector((0, 0, -1)))] - time_col += [ob.data.color * ob.data.energy] - time_rad += [ob.data.shadow_soft_size] - - # Write out sphere light - self.w.write("DistantDiskLight $%s {\n" % name) - self.w.indent() - for direc in time_dir: - self.w.write("Direction [%f %f %f]\n" % (direc[0], direc[1], direc[2])) - 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 diff --git a/psychoblend/render.py b/psychoblend/render.py index 57bef54..6350f7c 100644 --- a/psychoblend/render.py +++ b/psychoblend/render.py @@ -138,8 +138,6 @@ class PsychopathRender(bpy.types.RenderEngine): return self._process.stdin.write(bytes("__PSY_EOF__", "utf-8")) self._process.stdin.flush() - - self.update_stats("", "Psychopath: Building") else: # Export to file self.update_stats("", "Psychopath: Exporting data from Blender") @@ -155,6 +153,8 @@ class PsychopathRender(bpy.types.RenderEngine): self.update_stats("", "Psychopath: Not found") return + self.update_stats("", "Psychopath: Building") + # If we can, make the render process's stdout non-blocking. The # benefit of this is that canceling the render won't block waiting # for the next piece of input. diff --git a/psychoblend/world.py b/psychoblend/world.py new file mode 100644 index 0000000..f4503aa --- /dev/null +++ b/psychoblend/world.py @@ -0,0 +1,150 @@ +import bpy + +from math import degrees, tan, atan +from mathutils import Vector, Matrix + +from .util import escape_name, mat2str, ExportCancelled + +class World: + def __init__(self, render_engine, scene, visible_layers, aspect_ratio): + self.background_shader = BackgroundShader(render_engine, scene.world) + self.camera = Camera(render_engine, scene.camera, aspect_ratio) + self.lights = [] + + # Collect infinite-extent light sources. + # TODO: also get sun lamps inside group instances. + for ob in scene.objects: + if ob.type == 'LAMP' and ob.data.type == 'SUN': + name = escape_name(ob.name) + self.lights += [DistantDiskLamp(ob, name)] + + def take_sample(self, render_engine, scene, time): + self.camera.take_sample(render_engine, scene, time) + + for light in self.lights: + # Check if render is cancelled + if render_engine.test_break(): + raise ExportCancelled() + light.take_sample(render_engine, scene, time) + + def export(self, render_engine, w): + self.camera.export(render_engine, w) + + w.write("World {\n") + w.indent() + + self.background_shader.export(render_engine, w) + + for light in self.lights: + light.export(render_engine, w) + + w.unindent() + w.write("}\n") + + def cleanup(self): + # For future use. This is run by the calling code when finished, + # even if export did not succeed. + pass + +#================================================================ + +class Camera: + def __init__(self, render_engine, ob, aspect_ratio): + self.ob = ob + self.aspect_ratio = aspect_ratio + + self.fovs = [] + self.aperture_radii = [] + self.focal_distances = [] + self.xforms = [] + + def take_sample(self, render_engine, scene, time): + render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.ob.name, time)) + + # Fov + if self.aspect_ratio >= 1.0: + self.fovs += [degrees(self.ob.data.angle)] + else: + self.fovs += [degrees(2.0 * atan(tan(self.ob.data.angle * 0.5) * self.aspect_ratio))] + + # Aperture radius + self.aperture_radii += [self.ob.data.psychopath.aperture_radius] + + # Dof distance + if self.ob.data.dof_object == None: + self.focal_distances += [self.ob.data.dof_distance] + else: + # TODO: implement DoF object tracking here + self.focal_distances += [0.0] + print("WARNING: DoF object tracking not yet implemented.") + + # Transform + mat = self.ob.matrix_world.copy() + matz = Matrix() + matz[2][2] = -1 + self.xforms += [mat * matz] + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) + w.write("Camera {\n") + w.indent() + + for fov in self.fovs: + w.write("Fov [%f]\n" % fov) + + for rad in self.aperture_radii: + w.write("ApertureRadius [%f]\n" % rad) + + for dist in self.focal_distances: + w.write("FocalDistance [%f]\n" % dist) + + for mat in self.xforms: + w.write("Transform [%s]\n" % mat2str(mat)) + + w.unindent() + w.write("}\n") + + +class BackgroundShader: + def __init__(self, render_engine, world): + self.world = world + if self.world != None: + self.color = (world.horizon_color[0], world.horizon_color[1], world.horizon_color[2]) + + def export(self, render_engine, w): + if self.world != None: + w.write("BackgroundShader {\n") + w.indent(); + w.write("Type [Color]\n") + w.write("Color [%f %f %f]\n" % self.color) + w.unindent() + w.write("}\n") + + +class DistantDiskLamp: + def __init__(self, ob, name): + self.ob = ob + self.name = name + self.time_col = [] + self.time_dir = [] + self.time_rad = [] + + def take_sample(self, render_engine, scene, time): + render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.ob.name, time)) + self.time_dir += [tuple(self.ob.matrix_world.to_3x3() * Vector((0, 0, -1)))] + self.time_col += [self.ob.data.color * self.ob.data.energy] + self.time_rad += [self.ob.data.shadow_soft_size] + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) + w.write("DistantDiskLight $%s {\n" % self.name) + w.indent() + for direc in self.time_dir: + w.write("Direction [%f %f %f]\n" % (direc[0], direc[1], direc[2])) + 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")