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

View File

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

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