From 69ace90689b0907d8d53893318116f8a8a5f2a97 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Sat, 6 Aug 2022 21:40:13 -0700 Subject: [PATCH] Continue WIP update PsychoBlend for Blender 3.x. It exports and renders successfully... except there are no objects. Just a blank background. --- psychoblend/__init__.py | 2 +- psychoblend/assembly.py | 398 -------------------------------------- psychoblend/material.py | 77 ++++++++ psychoblend/objects.py | 227 ++++++++++++++++++++++ psychoblend/psy_export.py | 164 ++++++++++++---- psychoblend/render.py | 52 ++--- psychoblend/world.py | 117 ++++------- 7 files changed, 479 insertions(+), 558 deletions(-) delete mode 100644 psychoblend/assembly.py create mode 100644 psychoblend/material.py create mode 100644 psychoblend/objects.py diff --git a/psychoblend/__init__.py b/psychoblend/__init__.py index 5a8be22..908980f 100644 --- a/psychoblend/__init__.py +++ b/psychoblend/__init__.py @@ -1,6 +1,6 @@ bl_info = { "name": "PsychoBlend", - "version": (0, 1), + "version": (0, 1, 0), "author": "Nathan Vegdahl", "blender": (3, 1, 0), "description": "Psychopath renderer integration", diff --git a/psychoblend/assembly.py b/psychoblend/assembly.py deleted file mode 100644 index 73df05f..0000000 --- a/psychoblend/assembly.py +++ /dev/null @@ -1,398 +0,0 @@ -import bpy - -from .util import escape_name, mat2str, needs_def_mb, needs_xform_mb, ExportCancelled - -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.material_names = set() - 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: - # Check if render is cancelled - if render_engine.test_break(): - raise ExportCancelled() - mat.take_sample(render_engine, scene, time) - - for ob in self.objects: - # Check if render is cancelled - if render_engine.test_break(): - raise ExportCancelled() - ob.take_sample(render_engine, scene, time) - - for inst in self.instances: - # Check if render is cancelled - if render_engine.test_break(): - raise ExportCancelled() - 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 + "_") - has_faces = len(ob.data.polygons) > 0 - should_export_mesh = has_faces and (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)] - - # Get materials - for ms in ob.material_slots: - if ms != None: - if ms.material.name not in self.material_names: - self.material_names.add(ms.material.name) - self.materials += [Material(self.render_engine, ms.material)] - - 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.needs_mb = needs_def_mb(self.ob) - self.time_meshes = [] - - def take_sample(self, render_engine, scene, time): - if len(self.time_meshes) == 0 or self.needs_mb: - render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(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 and (if it's smooth shaded) normals - 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) - if self.time_meshes[0].polygons[0].use_smooth and self.ob.data.psychopath.is_subdivision_surface == False: - w.write("Normals [") - w.write(" ".join([("%f" % i) for vert in self.time_meshes[ti].vertices for i in vert.normal]), 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 '{}' at time {}".format(self.ob.name, time)) - - if self.ob.data.psychopath.color_type == 'Rec709': - self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] - elif self.ob.data.psychopath.color_type == 'Blackbody': - self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] - elif self.ob.data.psychopath.color_type == 'ColorTemperature': - self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, 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: - if col[0] == 'Rec709': - w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) - elif col[0] == 'Blackbody': - w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) - elif col[0] == 'ColorTemperature': - w.write("Color [color_temperature, %f %f]\n" % (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 '{}' at time {}".format(self.ob.name, time)) - - if self.ob.data.psychopath.color_type == 'Rec709': - self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] - elif self.ob.data.psychopath.color_type == 'Blackbody': - self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] - elif self.ob.data.psychopath.color_type == 'ColorTemperature': - self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] - - if self.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: - if col[0] == 'Rec709': - w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) - elif col[0] == 'Blackbody': - w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) - elif col[0] == 'ColorTemperature': - w.write("Color [color_temperature, %f %f]\n" % (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.needs_mb = needs_xform_mb(self.ob) - self.time_xforms = [] - - def take_sample(self, render_engine, time, translation_offset): - if len(self.time_xforms) == 0 or self.needs_mb: - render_engine.update_stats("", "Psychopath: Collecting '{}' xforms at time {}".format(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)) - for ms in self.ob.material_slots: - if ms != None: - w.write("SurfaceShaderBind [$%s]\n" % escape_name(ms.material.name)) - break - w.unindent() - w.write("}\n") - - -class Material: - def __init__(self, render_engine, material): - self.mat = material - - def take_sample(self, render_engine, time, translation_offset): - # TODO: motion blur of material settings - pass - - def export(self, render_engine, w): - render_engine.update_stats("", "Psychopath: Exporting %s" % self.mat.name) - - w.write("SurfaceShader $%s {\n" % escape_name(self.mat.name)) - w.indent() - if self.mat.psychopath.surface_shader_type == 'Emit': - w.write("Type [Emit]\n") - if self.mat.psychopath.color_type == 'Rec709': - col = self.mat.psychopath.color - w.write("Color [rec709, %f %f %f]\n" % ( - col[0], col[1], col[2], - )) - elif self.mat.psychopath.color_type == 'Blackbody': - w.write("Color [blackbody, %f %f]\n" % ( - self.mat.psychopath.color_blackbody_temp, - 1.0, - )) - elif self.mat.psychopath.color_type == 'ColorTemperature': - w.write("Color [color_temperature, %f %f]\n" % ( - self.mat.psychopath.color_blackbody_temp, - 1.0, - )) - elif self.mat.psychopath.surface_shader_type == 'Lambert': - w.write("Type [Lambert]\n") - if self.mat.psychopath.color_type == 'Rec709': - col = self.mat.psychopath.color - w.write("Color [rec709, %f %f %f]\n" % ( - col[0], col[1], col[2], - )) - elif self.mat.psychopath.color_type == 'Blackbody': - w.write("Color [blackbody, %f %f]\n" % ( - self.mat.psychopath.color_blackbody_temp, - 1.0, - )) - elif self.mat.psychopath.color_type == 'ColorTemperature': - w.write("Color [color_temperature, %f %f]\n" % ( - self.mat.psychopath.color_blackbody_temp, - 1.0, - )) - elif self.mat.psychopath.surface_shader_type == 'GGX': - w.write("Type [GGX]\n") - if self.mat.psychopath.color_type == 'Rec709': - col = self.mat.psychopath.color - w.write("Color [rec709, %f %f %f]\n" % ( - col[0], col[1], col[2], - )) - elif self.mat.psychopath.color_type == 'Blackbody': - w.write("Color [blackbody, %f %f]\n" % ( - self.mat.psychopath.color_blackbody_temp, - 1.0, - )) - elif self.mat.psychopath.color_type == 'ColorTemperature': - w.write("Color [color_temperature, %f %f]\n" % ( - self.mat.psychopath.color_blackbody_temp, - 1.0, - )) - w.write("Roughness [%f]\n" % self.mat.psychopath.roughness) - w.write("Fresnel [%f]\n" % self.mat.psychopath.fresnel) - else: - raise "Unsupported surface shader type '%s'" % self.mat.psychopath.surface_shader_type - w.unindent() - w.write("}\n") - - def cleanup(self): - pass diff --git a/psychoblend/material.py b/psychoblend/material.py new file mode 100644 index 0000000..450e60e --- /dev/null +++ b/psychoblend/material.py @@ -0,0 +1,77 @@ +import bpy + +from .util import escape_name, mat2str, needs_def_mb, needs_xform_mb, ExportCancelled + +class Material: + def __init__(self, render_engine, depsgraph, material): + self.mat = material + + def take_sample(self, render_engine, depsgraph, time): + # TODO: motion blur of material settings + pass + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.mat.name) + + w.write("SurfaceShader $%s {\n" % escape_name(self.mat.name)) + w.indent() + if self.mat.psychopath.surface_shader_type == 'Emit': + w.write("Type [Emit]\n") + if self.mat.psychopath.color_type == 'Rec709': + col = self.mat.psychopath.color + w.write("Color [rec709, %f %f %f]\n" % ( + col[0], col[1], col[2], + )) + elif self.mat.psychopath.color_type == 'Blackbody': + w.write("Color [blackbody, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) + elif self.mat.psychopath.color_type == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) + elif self.mat.psychopath.surface_shader_type == 'Lambert': + w.write("Type [Lambert]\n") + if self.mat.psychopath.color_type == 'Rec709': + col = self.mat.psychopath.color + w.write("Color [rec709, %f %f %f]\n" % ( + col[0], col[1], col[2], + )) + elif self.mat.psychopath.color_type == 'Blackbody': + w.write("Color [blackbody, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) + elif self.mat.psychopath.color_type == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) + elif self.mat.psychopath.surface_shader_type == 'GGX': + w.write("Type [GGX]\n") + if self.mat.psychopath.color_type == 'Rec709': + col = self.mat.psychopath.color + w.write("Color [rec709, %f %f %f]\n" % ( + col[0], col[1], col[2], + )) + elif self.mat.psychopath.color_type == 'Blackbody': + w.write("Color [blackbody, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) + elif self.mat.psychopath.color_type == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % ( + self.mat.psychopath.color_blackbody_temp, + 1.0, + )) + w.write("Roughness [%f]\n" % self.mat.psychopath.roughness) + w.write("Fresnel [%f]\n" % self.mat.psychopath.fresnel) + else: + raise "Unsupported surface shader type '%s'" % self.mat.psychopath.surface_shader_type + w.unindent() + w.write("}\n") + + def cleanup(self): + pass diff --git a/psychoblend/objects.py b/psychoblend/objects.py new file mode 100644 index 0000000..713cd6d --- /dev/null +++ b/psychoblend/objects.py @@ -0,0 +1,227 @@ +import bpy + +from .util import escape_name, mat2str, needs_def_mb, needs_xform_mb, ExportCancelled + +def make_object_data_cache(render_engine, depsgraph, ob, name): + if ob.type == 'MESH': + return Mesh(render_engine, depsgraph, ob, name) + elif ob.type == 'LIGHT': + if ob.data.type == 'POINT': + return SphereLamp(render_engine, depsgraph, ob, name) + elif ob.data.type == 'AREA': + return RectLamp(render_engine, depsgraph, ob, name) + elif ob.data.type == 'AREA': + return RectLamp(render_engine, depsgraph, ob, name) + +class Mesh: + """ Holds data for a mesh to be exported. + """ + def __init__(self, render_engine, depsgraph, ob, name): + self.name = name + self.is_subdiv = ob.data.psychopath.is_subdivision_surface + self.needs_mb = needs_def_mb(ob) + self.time_meshes = [] + + def take_sample(self, render_engine, depsgraph, ob, time): + if len(self.time_meshes) == 0 or self.needs_mb: + render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.name, time)) + self.time_meshes += [ob.to_mesh(depsgraph=depsgraph).copy()] + + 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.name) + + if self.is_subdiv == 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 and (if it's smooth shaded) normals + 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) + if self.time_meshes[0].polygons[0].use_smooth and self.is_subdiv == False: + w.write("Normals [") + w.write(" ".join([("%f" % i) for vert in self.time_meshes[ti].vertices for i in vert.normal]), 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, depsgraph, ob, name): + self.name = name + self.time_col = [] + self.time_rad = [] + + def take_sample(self, render_engine, depsgraph, ob, time): + render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(ob.name, time)) + + if ob.data.psychopath.color_type == 'Rec709': + self.time_col += [('Rec709', ob.data.color * ob.data.energy)] + elif ob.data.psychopath.color_type == 'Blackbody': + self.time_col += [('Blackbody', ob.data.psychopath.color_blackbody_temp, ob.data.energy)] + elif ob.data.psychopath.color_type == 'ColorTemperature': + self.time_col += [('ColorTemperature', ob.data.psychopath.color_blackbody_temp, ob.data.energy)] + + self.time_rad += [ob.data.shadow_soft_size] + + def cleanup(self): + pass + + def export(self, render_engine, w): + render_engine.update_stats("", "Psychopath: Exporting %s" % self.name) + + w.write("SphereLight $%s {\n" % self.name) + w.indent() + for col in self.time_col: + if col[0] == 'Rec709': + w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) + elif col[0] == 'Blackbody': + w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) + elif col[0] == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % (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, depsgraph, ob, name): + self.name = name + self.time_col = [] + self.time_dim = [] + + def take_sample(self, render_engine, depsgraph, ob, time): + render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.name, time)) + + if ob.data.psychopath.color_type == 'Rec709': + self.time_col += [('Rec709', ob.data.color * ob.data.energy)] + elif ob.data.psychopath.color_type == 'Blackbody': + self.time_col += [('Blackbody', ob.data.psychopath.color_blackbody_temp, ob.data.energy)] + elif ob.data.psychopath.color_type == 'ColorTemperature': + self.time_col += [('ColorTemperature', ob.data.psychopath.color_blackbody_temp, ob.data.energy)] + + if ob.data.shape == 'RECTANGLE': + self.time_dim += [(ob.data.size, ob.data.size_y)] + else: + self.time_dim += [(ob.data.size, 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: + if col[0] == 'Rec709': + w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) + elif col[0] == 'Blackbody': + w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) + elif col[0] == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) + for dim in self.time_dim: + w.write("Dimensions [%f %f]\n" % dim) + + w.unindent() + w.write("}\n") + + +class DistantDiskLamp: + def __init__(self, render_engine, depsgraph, ob, name): + self.name = name + self.time_col = [] + self.time_dir = [] + self.time_rad = [] + + def take_sample(self, render_engine, depsgraph, ob, time): + render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.name, time)) + self.time_dir += [tuple(ob.matrix_world.to_3x3() @ Vector((0, 0, -1)))] + + if ob.data.psychopath.color_type == 'Rec709': + self.time_col += [('Rec709', ob.data.color * ob.data.energy)] + elif ob.data.psychopath.color_type == 'Blackbody': + self.time_col += [('Blackbody', ob.data.psychopath.color_blackbody_temp, ob.data.energy)] + elif ob.data.psychopath.color_type == 'ColorTemperature': + self.time_col += [('ColorTemperature', ob.data.psychopath.color_blackbody_temp, ob.data.energy)] + + self.time_rad += [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: + if col[0] == 'Rec709': + w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) + elif col[0] == 'Blackbody': + w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) + elif col[0] == 'ColorTemperature': + w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) + for rad in self.time_rad: + w.write("Radius [%f]\n" % rad) + + w.unindent() + w.write("}\n") + + +# class Instance: +# def __init__(self, render_engine, depsgraph, ob, data_name): +# self.ob = ob +# self.data_name = data_name +# self.needs_mb = needs_xform_mb(self.ob) +# self.time_xforms = [] + +# def take_sample(self, render_engine, time, translation_offset): +# if len(self.time_xforms) == 0 or self.needs_mb: +# render_engine.update_stats("", "Psychopath: Collecting '{}' xforms at time {}".format(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)) +# for ms in self.ob.material_slots: +# if ms != None: +# w.write("SurfaceShaderBind [$%s]\n" % escape_name(ms.material.name)) +# break +# w.unindent() +# w.write("}\n") diff --git a/psychoblend/psy_export.py b/psychoblend/psy_export.py index 65a7cc7..cf9652b 100644 --- a/psychoblend/psy_export.py +++ b/psychoblend/psy_export.py @@ -2,9 +2,10 @@ import bpy from math import log -from .assembly import Assembly +from .objects import make_object_data_cache, Mesh, DistantDiskLamp from .util import escape_name, mat2str, ExportCancelled -from .world import World +from .world import World, Camera +from . import bl_info class IndentedWriter: @@ -29,25 +30,40 @@ class IndentedWriter: class PsychoExporter: - def __init__(self, f, render_engine, scene): + def __init__(self, f, render_engine, depsgraph): self.w = IndentedWriter(f) self.render_engine = render_engine - self.scene = scene + self.depsgraph = depsgraph + self.scene = depsgraph.scene + self.view_layer = depsgraph.view_layer - self.mesh_names = {} - self.group_names = {} + # 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)) - # Motion blur segments are rounded down to a power of two - if scene.psychopath.motion_blur_segments > 0: - self.time_samples = (2**int(log(scene.psychopath.motion_blur_segments, 2))) + 1 + # 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] + + # 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 = scene.psychopath.shutter_start - self.shutter_diff = (scene.psychopath.shutter_end - scene.psychopath.shutter_start) / max(1, (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 = scene.frame_current + self.fr = self.scene.frame_current def set_frame(self, frame, fraction): @@ -70,25 +86,31 @@ class PsychoExporter: def _export_psy(self): # Info - self.w.write("# Exported from Blender 2.7x\n") + 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 begin + #------------------------------------------------------ + # Output section. + self.w.write("Output {\n") self.w.indent() self.w.write('Path [""]\n') - # Output section end self.w.unindent() self.w.write("}\n") - ############################### - # RenderSettings section begin + #------------------------------------------------------ + # RenderSettings section. + self.w.write("RenderSettings {\n") self.w.indent() @@ -99,34 +121,94 @@ class PsychoExporter: self.w.write("DicingRate [%f]\n" % self.scene.psychopath.dicing_rate) self.w.write('Seed [%d]\n' % self.fr) - # RenderSettings section end self.w.unindent() self.w.write("}\n") - ############################### - # Export world and object data - world = None - root_assembly = None + #------------------------------------------------------ + # Collect world and object data. + 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) + # Check if render is cancelled + if self.render_engine.test_break(): + raise ExportCancelled() - # Export collected data - world.export(self.render_engine, self.w) - root_assembly.export(self.render_engine, self.w) - finally: - if world != None: - world.cleanup() - if root_assembly != None: - root_assembly.cleanup() + time = self.fr + self.shutter_start + (self.shutter_diff*i) + + # 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' + + # 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)), 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() + + 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() diff --git a/psychoblend/render.py b/psychoblend/render.py index 1dad4af..db37a9d 100644 --- a/psychoblend/render.py +++ b/psychoblend/render.py @@ -18,31 +18,16 @@ class PsychopathRender(bpy.types.RenderEngine): pass def update(self, data, depsgraph): - print("Psychopath scene update!") + pass def render(self, depsgraph): - print("Psychopath render!") - scene = depsgraph.scene - scale = scene.render.resolution_percentage / 100.0 - self.size_x = int(scene.render.resolution_x * scale) - self.size_y = int(scene.render.resolution_y * scale) - - # Fill the render result with a flat color. The framebuffer is - # defined as a list of pixels, each pixel itself being a list of - # R,G,B,A values. - if self.is_preview: - color = [0.1, 0.2, 0.1, 1.0] - else: - color = [0.2, 0.1, 0.1, 1.0] - - pixel_count = self.size_x * self.size_y - rect = [color] * pixel_count - - # Here we write the pixel values to the RenderResult - result = self.begin_result(0, 0, self.size_x, self.size_y) - layer = result.layers[0].passes["Combined"] - layer.rect = rect - self.end_result(result) + self._process = None + try: + self._render(depsgraph) + except: + if self._process != None: + self._process.terminate() + raise def view_update(self, context, depsgraph): pass @@ -54,7 +39,7 @@ class PsychopathRender(bpy.types.RenderEngine): @staticmethod def _locate_binary(): - addon_prefs = bpy.context.user_preferences.addons[__package__].preferences + addon_prefs = bpy.context.preferences.addons[__package__].preferences # Use the system preference if its set. psy_binary = addon_prefs.filepath_psychopath @@ -126,16 +111,9 @@ class PsychopathRender(bpy.types.RenderEngine): lay.rect = pixels_flipped self.end_result(result) - def render(self, scene): - self._process = None - try: - self._render(scene) - except: - if self._process != None: - self._process.terminate() - raise + def _render(self, depsgraph): + scene = depsgraph.scene - def _render(self, scene): # has to be called to update the frame on exporting animations scene.frame_set(scene.frame_current) @@ -171,8 +149,8 @@ class PsychopathRender(bpy.types.RenderEngine): return self.update_stats("", "Psychopath: Collecting...") - # Export to Psychopath's stdin - if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy(): + # Export to Psychopath's stdin. + if not psy_export.PsychoExporter(self._process.stdin, self, depsgraph).export_psy(): # Render cancelled in the middle of exporting, # so just return. self._process.terminate() @@ -183,7 +161,7 @@ class PsychopathRender(bpy.types.RenderEngine): # Export to file self.update_stats("", "Psychopath: Exporting data from Blender") with open(export_path, 'w+b') as f: - if not psy_export.PsychoExporter(f, self, scene).export_psy(): + if not psy_export.PsychoExporter(f, self, depsgraph).export_psy(): # Render cancelled in the middle of exporting, # so just return. return @@ -224,7 +202,7 @@ class PsychopathRender(bpy.types.RenderEngine): # Get render output from stdin tmp = self._process.stdout.read1(2**16) if len(tmp) == 0: - time.sleep(0.0001) # Don't spin on the CPU + time.sleep(0.001) # Don't spin on the CPU if render_process_finished: all_output_consumed = True continue diff --git a/psychoblend/world.py b/psychoblend/world.py index bcaa645..17c269b 100644 --- a/psychoblend/world.py +++ b/psychoblend/world.py @@ -6,47 +6,28 @@ from mathutils import Vector, Matrix from .util import escape_name, mat2str, ExportCancelled class World: - def __init__(self, render_engine, scene, visible_layers, aspect_ratio): + def __init__(self, render_engine, depsgraph): + scene = depsgraph.scene 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) + def take_sample(self, render_engine, depsgraph, time): + if render_engine.test_break(): + raise ExportCancelled() + self.background_shader.take_sample(render_engine, depsgraph, 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 cleanup(self): + pass 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): @@ -58,25 +39,27 @@ class Camera: self.focal_distances = [] self.xforms = [] - def take_sample(self, render_engine, scene, time): + def take_sample(self, render_engine, depsgraph, 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))] + self.fovs += [degrees(self.ob.data.angle_x)] - # Aperture radius - self.aperture_radii += [self.ob.data.psychopath.aperture_radius] + if self.ob.data.dof.use_dof: + # TODO + # # 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] + # 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.") else: - # TODO: implement DoF object tracking here - self.focal_distances += [0.0] - print("WARNING: DoF object tracking not yet implemented.") + self.aperture_radii += [0.0] + self.focal_distances += [1.0] # Transform mat = self.ob.matrix_world.copy() @@ -84,6 +67,9 @@ class Camera: matz[2][2] = -1 self.xforms += [(mat * matz).inverted()] + def cleanup(self): + pass + def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) w.write("Camera {\n") @@ -108,55 +94,24 @@ class Camera: class BackgroundShader: def __init__(self, render_engine, world): self.world = world + self.color = [] + + def take_sample(self, render_engine, depsgraph, time): if self.world != None: - self.color = (world.horizon_color[0], world.horizon_color[1], world.horizon_color[2]) + self.color += [( + self.world.psychopath.background_color[0], + self.world.psychopath.background_color[1], + self.world.psychopath.background_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 [rec709, %f %f %f]\n" % self.color) + for c in self.color: + w.write("Color [rec709, %f %f %f]\n" % c) 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)))] - - if self.ob.data.psychopath.color_type == 'Rec709': - self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] - elif self.ob.data.psychopath.color_type == 'Blackbody': - self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] - elif self.ob.data.psychopath.color_type == 'ColorTemperature': - self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, 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: - if col[0] == 'Rec709': - w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) - elif col[0] == 'Blackbody': - w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) - elif col[0] == 'ColorTemperature': - w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) - for rad in self.time_rad: - w.write("Radius [%f]\n" % rad) - - w.unindent() - w.write("}\n")