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.
237 lines
8.6 KiB
Python
237 lines
8.6 KiB
Python
import bpy
|
|
import time
|
|
import os
|
|
import subprocess
|
|
import base64
|
|
import struct
|
|
from . import psy_export
|
|
|
|
class PsychopathRender(bpy.types.RenderEngine):
|
|
bl_idname = 'PSYCHOPATH_RENDER'
|
|
bl_label = "Psychopath"
|
|
DELAY = 1.0
|
|
|
|
@staticmethod
|
|
def _locate_binary():
|
|
addon_prefs = bpy.context.user_preferences.addons[__package__].preferences
|
|
|
|
# Use the system preference if its set.
|
|
psy_binary = addon_prefs.filepath_psychopath
|
|
if psy_binary:
|
|
if os.path.exists(psy_binary):
|
|
return psy_binary
|
|
else:
|
|
print("User Preference to psychopath %r NOT FOUND, checking $PATH" % psy_binary)
|
|
|
|
# search the path all os's
|
|
psy_binary_default = "psychopath"
|
|
|
|
os_path_ls = os.getenv("PATH").split(':') + [""]
|
|
|
|
for dir_name in os_path_ls:
|
|
psy_binary = os.path.join(dir_name, psy_binary_default)
|
|
if os.path.exists(psy_binary):
|
|
return psy_binary
|
|
return ""
|
|
|
|
def _start_psychopath(self, scene, psy_filepath, use_stdin, crop):
|
|
psy_binary = PsychopathRender._locate_binary()
|
|
if not psy_binary:
|
|
print("Psychopath: could not execute psychopath, possibly Psychopath isn't installed")
|
|
return False
|
|
|
|
# Figure out command line options
|
|
args = []
|
|
if crop != None:
|
|
args += ["--crop", str(crop[0]), str(self.size_y - crop[3]), str(crop[2] - 1), str(self.size_y - crop[1] - 1)]
|
|
if use_stdin:
|
|
args += ["--spb", str(scene.psychopath.max_samples_per_bucket), "--blender_output", "--use_stdin"]
|
|
else:
|
|
args += ["--spb", str(scene.psychopath.max_samples_per_bucket), "--blender_output", "-i", psy_filepath]
|
|
|
|
# Start Rendering!
|
|
try:
|
|
self._process = subprocess.Popen([psy_binary] + args, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
except OSError:
|
|
# TODO, report api
|
|
print("Psychopath: could not execute '%s'" % psy_binary)
|
|
import traceback
|
|
traceback.print_exc()
|
|
print ("***-DONE-***")
|
|
return False
|
|
|
|
return True
|
|
|
|
def _draw_bucket(self, crop, bucket_info, pixels_encoded):
|
|
if crop != None:
|
|
x = bucket_info[0] - crop[0]
|
|
y = self.size_y - bucket_info[3] - crop[1]
|
|
else:
|
|
x = bucket_info[0]
|
|
y = self.size_y - bucket_info[3]
|
|
width = bucket_info[2] - bucket_info[0]
|
|
height = bucket_info[3] - bucket_info[1]
|
|
|
|
# Decode pixel data
|
|
pixels = [p for p in struct.iter_unpack("ffff", base64.b64decode(pixels_encoded))]
|
|
pixels_flipped = []
|
|
for i in range(height):
|
|
n = height - i - 1
|
|
pixels_flipped += pixels[n*width:(n+1)*width]
|
|
|
|
# Write pixel data to render image
|
|
result = self.begin_result(x, y, width, height)
|
|
lay = result.layers[0].passes["Combined"]
|
|
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, scene):
|
|
# has to be called to update the frame on exporting animations
|
|
scene.frame_set(scene.frame_current)
|
|
|
|
export_path = scene.psychopath.export_path.strip()
|
|
use_stdin = False
|
|
|
|
r = scene.render
|
|
# compute resolution
|
|
self.size_x = int(r.resolution_x * r.resolution_percentage / 100)
|
|
self.size_y = int(r.resolution_y * r.resolution_percentage / 100)
|
|
|
|
# Calculate border cropping, if any.
|
|
if scene.render.use_border == True:
|
|
minx = r.resolution_x * scene.render.border_min_x * (r.resolution_percentage / 100)
|
|
miny = r.resolution_y * scene.render.border_min_y * (r.resolution_percentage / 100)
|
|
maxx = r.resolution_x * scene.render.border_max_x * (r.resolution_percentage / 100)
|
|
maxy = r.resolution_y * scene.render.border_max_y * (r.resolution_percentage / 100)
|
|
crop = (int(minx), int(miny), int(maxx), int(maxy))
|
|
else:
|
|
crop = None
|
|
|
|
# Are we using an output file or standard in/out?
|
|
if export_path != "":
|
|
export_path += "_%d.psy" % scene.frame_current
|
|
else:
|
|
# We'll write directly to Psychopath's stdin
|
|
use_stdin = True
|
|
|
|
if use_stdin:
|
|
# Start rendering
|
|
if not self._start_psychopath(scene, export_path, use_stdin, crop):
|
|
self.update_stats("", "Psychopath: Not found")
|
|
return
|
|
|
|
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.
|
|
self._process.terminate()
|
|
return
|
|
self._process.stdin.write(bytes("__PSY_EOF__", "utf-8"))
|
|
self._process.stdin.flush()
|
|
else:
|
|
# 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():
|
|
# Render cancelled in the middle of exporting,
|
|
# so just return.
|
|
return
|
|
|
|
# Start rendering
|
|
self.update_stats("", "Psychopath: Rendering from %s" % export_path)
|
|
if not self._start_psychopath(scene, export_path, use_stdin, crop):
|
|
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.
|
|
try:
|
|
import fcntl
|
|
fd = self._process.stdout.fileno()
|
|
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
except:
|
|
print("NOTE: Can't make Psychopath's stdout non-blocking, so canceling renders may take a moment to respond.")
|
|
|
|
# Process output from rendering process
|
|
reached_first_bucket = False
|
|
output = b""
|
|
render_process_finished = False
|
|
all_output_consumed = False
|
|
while not (render_process_finished and all_output_consumed):
|
|
if self._process.poll() != None:
|
|
render_process_finished = True
|
|
|
|
# Check for render cancel
|
|
if self.test_break():
|
|
self._process.terminate()
|
|
break
|
|
|
|
# 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
|
|
if render_process_finished:
|
|
all_output_consumed = True
|
|
continue
|
|
output += tmp
|
|
outputs = output.split(b'DIV\n')
|
|
|
|
# Skip render process output until we hit the first bucket.
|
|
# (The stuff before it is just informational printouts.)
|
|
if not reached_first_bucket:
|
|
if len(outputs) > 1:
|
|
reached_first_bucket = True
|
|
outputs = outputs[1:]
|
|
else:
|
|
continue
|
|
|
|
self.update_stats("", "Psychopath: Rendering")
|
|
|
|
# Clear output buffer, since it's all in 'outputs' now.
|
|
output = b""
|
|
|
|
# Process buckets
|
|
for bucket in outputs:
|
|
if len(bucket) == 0:
|
|
continue
|
|
|
|
if bucket[-11:] == b'BUCKET_END\n':
|
|
# Parse bucket text
|
|
contents = bucket.split(b'\n')
|
|
percentage = contents[0]
|
|
bucket_info = [int(i) for i in contents[1].split(b' ')]
|
|
pixels = contents[2]
|
|
|
|
# Draw the bucket
|
|
self._draw_bucket(crop, bucket_info, pixels)
|
|
|
|
# Update render progress bar
|
|
try:
|
|
progress = float(percentage[:-1])
|
|
except ValueError:
|
|
pass
|
|
finally:
|
|
self.update_progress(progress/100)
|
|
else:
|
|
output += bucket
|
|
|
|
def register():
|
|
bpy.utils.register_class(PsychopathRender)
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(PsychopathRender)
|