PsychoBlend: further preformance improvements, and minor fixes.

All scene data collection is now done in a single sweep of frame
changing.  Previous commits were already working towards this, and
but now it's done.  Yay!

Over-all, switching to this approach gives huge speed boosts on
large scenes with animation, rigs, dependencies, etc.  For such
scenes, frame changing is very expensive.
This commit is contained in:
Nathan Vegdahl 2017-06-10 16:11:28 -07:00
parent 914a13f899
commit 13ee6066b8
3 changed files with 171 additions and 103 deletions

View File

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

View File

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

150
psychoblend/world.py Normal file
View File

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