Significant PsychoBlend improvements.

- Improved export time by quite a bit.
- Added more fine-grained status updates during export so it
  doesn't feel like it's hanging.
This commit is contained in:
Nathan Vegdahl 2017-06-09 23:57:18 -07:00
parent 9d92bd099d
commit 9025715335
4 changed files with 352 additions and 312 deletions

265
psychoblend/assembly.py Normal file
View File

@ -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")

View File

@ -3,6 +3,9 @@ import bpy
from math import degrees, pi, log from math import degrees, pi, log
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from .assembly import Assembly
from .util import escape_name, mat2str
class ExportCancelled(Exception): class ExportCancelled(Exception):
""" Indicates that the render was cancelled in the middle of exporting """ Indicates that the render was cancelled in the middle of exporting
the scene file. the scene file.
@ -10,66 +13,6 @@ class ExportCancelled(Exception):
pass 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: class IndentedWriter:
def __init__(self, file_handle): def __init__(self, file_handle):
self.f = file_handle self.f = file_handle
@ -91,8 +34,6 @@ class IndentedWriter:
self.f.write(bytes(text, "utf-8")) self.f.write(bytes(text, "utf-8"))
class PsychoExporter: class PsychoExporter:
def __init__(self, f, render_engine, scene): def __init__(self, f, render_engine, scene):
self.w = IndentedWriter(f) self.w = IndentedWriter(f)
@ -231,253 +172,23 @@ class PsychoExporter:
####################### #######################
# Export objects and materials # Export objects and materials
# TODO: handle materials from linked files (as used in group try:
# instances) properly. root_assembly = Assembly(self.render_engine, self.scene.objects, self.scene.layers)
self.w.write("Assembly {\n") for i in range(self.time_samples):
self.w.indent() time = self.fr + self.shutter_start + (self.shutter_diff*i)
self.export_materials(bpy.data.materials) self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))
self.export_objects(self.scene.objects, self.scene.layers) root_assembly.take_sample(self.render_engine, self.scene, time)
self.w.unindent() root_assembly.export(self.render_engine, self.w)
self.w.write("}\n") except ExportCancelled:
root_assembly.cleanup()
raise ExportCancelled()
else:
root_assembly.cleanup()
# Scene end # Scene end
self.w.unindent() self.w.unindent()
self.w.write("}\n") 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): def export_world_distant_disk_lamp(self, ob, group_prefix):
name = group_prefix + "__" + escape_name(ob.name) name = group_prefix + "__" + escape_name(ob.name)

View File

@ -120,16 +120,20 @@ class PsychopathRender(bpy.types.RenderEngine):
self.update_stats("", "Psychopath: Not found") self.update_stats("", "Psychopath: Not found")
return return
self.update_stats("", "Psychopath: Exporting data from Blender") self.update_stats("", "Psychopath: Collecting...")
# Export to Psychopath's stdin # Export to Psychopath's stdin
try:
if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy(): if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy():
# Render cancelled in the middle of exporting, # Render cancelled in the middle of exporting,
# so just return. # so just return.
return return
self._process.stdin.write(bytes("__PSY_EOF__", "utf-8")) self._process.stdin.write(bytes("__PSY_EOF__", "utf-8"))
self._process.stdin.flush() self._process.stdin.flush()
except:
self._process.terminate()
raise
self.update_stats("", "Psychopath: Rendering") self.update_stats("", "Psychopath: Building")
else: else:
# Export to file # Export to file
self.update_stats("", "Psychopath: Exporting data from Blender") self.update_stats("", "Psychopath: Exporting data from Blender")
@ -189,6 +193,8 @@ class PsychopathRender(bpy.types.RenderEngine):
else: else:
continue continue
self.update_stats("", "Psychopath: Rendering")
# Clear output buffer, since it's all in 'outputs' now. # Clear output buffer, since it's all in 'outputs' now.
output = b"" output = b""

58
psychoblend/util.py Normal file
View File

@ -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