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 import bpy
from math import degrees, pi, log from math import log
from mathutils import Vector, Matrix
from .assembly import Assembly from .assembly import Assembly
from .util import escape_name, mat2str, ExportCancelled from .util import escape_name, mat2str, ExportCancelled
from .world import World
class IndentedWriter: class IndentedWriter:
@ -103,113 +103,31 @@ class PsychoExporter:
self.w.unindent() self.w.unindent()
self.w.write("}\n") self.w.write("}\n")
####################### ###############################
# Camera section begin # Export world and object data
self.w.write("Camera {\n") world = None
self.w.indent() root_assembly = None
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
try: 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) 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): for i in range(self.time_samples):
time = self.fr + self.shutter_start + (self.shutter_diff*i) time = self.fr + self.shutter_start + (self.shutter_diff*i)
self.set_frame(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) 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) root_assembly.export(self.render_engine, self.w)
except ExportCancelled: finally:
root_assembly.cleanup() if world != None:
raise world.cleanup()
else: if root_assembly != None:
root_assembly.cleanup() root_assembly.cleanup()
# Scene end # Scene end
self.w.unindent() self.w.unindent()
self.w.write("}\n") 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 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()
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")
@ -155,6 +153,8 @@ class PsychopathRender(bpy.types.RenderEngine):
self.update_stats("", "Psychopath: Not found") self.update_stats("", "Psychopath: Not found")
return return
self.update_stats("", "Psychopath: Building")
# If we can, make the render process's stdout non-blocking. The # If we can, make the render process's stdout non-blocking. The
# benefit of this is that canceling the render won't block waiting # benefit of this is that canceling the render won't block waiting
# for the next piece of input. # 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")