psychopath/psychoblend/psy_export.py

271 lines
10 KiB
Python

import bpy
from math import log
from .material import Material
from .objects import make_object_data_cache, Mesh, DistantDiskLamp
from .util import escape_name, mat2str, ExportCancelled
from .world import World, Camera
from . import bl_info
class IndentedWriter:
def __init__(self, file_handle):
self.f = file_handle
self.indent_level = 0
self.indent_size = 4
def indent(self):
self.indent_level += self.indent_size
def unindent(self):
self.indent_level -= self.indent_size
if self.indent_level < 0:
self.indent_level = 0
def write(self, text, do_indent=True):
if do_indent:
self.f.write(bytes(' '*self.indent_level + text, "utf-8"))
else:
self.f.write(bytes(text, "utf-8"))
class PsychoExporter:
def __init__(self, f, render_engine, depsgraph):
self.w = IndentedWriter(f)
self.render_engine = render_engine
self.depsgraph = depsgraph
self.scene = depsgraph.scene
self.view_layer = depsgraph.view_layer
# For camera data.
res_x = int(self.scene.render.resolution_x * (self.scene.render.resolution_percentage / 100))
res_y = int(self.scene.render.resolution_y * (self.scene.render.resolution_percentage / 100))
self.camera = Camera(render_engine, depsgraph.scene.camera, float(res_x) / float(res_y))
# For world data.
self.world = World(render_engine, depsgraph)
# For all objects except sun lamps.
self.object_data = {} # name -> cached_data
self.instances = {} # instance_id -> [object_data_name, transform_list]
# For all sun lamps.
self.sun_lamp_data = {} # name -> cached_data
self.sun_lamp_instances = {} # instance_id -> [sun_lamp_data_name, transform_list]
# For all materials.
self.materials = {} # name -> cached_data
# Motion blur segments are rounded down to a power of two.
if self.scene.psychopath.motion_blur_segments > 0:
self.time_samples = (2**int(log(self.scene.psychopath.motion_blur_segments, 2))) + 1
else:
self.time_samples = 1
# pre-calculate useful values for exporting motion blur.
self.shutter_start = self.scene.psychopath.shutter_start
self.shutter_diff = (self.scene.psychopath.shutter_end - self.scene.psychopath.shutter_start) / max(1, (self.time_samples-1))
self.fr = self.scene.frame_current
def set_frame(self, frame, fraction):
if fraction >= 0:
self.scene.frame_set(frame, fraction)
else:
self.scene.frame_set(frame-1, 1.0+fraction)
def export_psy(self):
try:
self._export_psy()
except ExportCancelled:
# Cleanup
self.scene.frame_set(self.fr)
return False
else:
# Cleanup
self.scene.frame_set(self.fr)
return True
def _export_psy(self):
# Info
self.w.write("# Exported from Blender {} with PsychoBlend {}.{}.{}\n".format(
bpy.app.version_string,
bl_info["version"][0],
bl_info["version"][1],
bl_info["version"][2],
))
# Scene begin
self.w.write("\n\nScene $%s_fr%d {\n" % (escape_name(self.scene.name), self.fr))
self.w.indent()
#------------------------------------------------------
# Output section.
self.w.write("Output {\n")
self.w.indent()
self.w.write('Path [""]\n')
self.w.unindent()
self.w.write("}\n")
#------------------------------------------------------
# RenderSettings section.
self.w.write("RenderSettings {\n")
self.w.indent()
res_x = int(self.scene.render.resolution_x * (self.scene.render.resolution_percentage / 100))
res_y = int(self.scene.render.resolution_y * (self.scene.render.resolution_percentage / 100))
self.w.write('Resolution [%d %d]\n' % (res_x, res_y))
self.w.write("SamplesPerPixel [%d]\n" % self.scene.psychopath.spp)
self.w.write("DicingRate [%f]\n" % self.scene.psychopath.dicing_rate)
self.w.write('Seed [%d]\n' % self.fr)
self.w.unindent()
self.w.write("}\n")
#------------------------------------------------------
# Collect materials.
# TODO: handle situations where there are more than one
# material with the same name. This can happen through
# library linking.
for inst in self.depsgraph.object_instances:
ob = inst.object
if ob.type in ['MESH']:
for ms in ob.material_slots:
if ms.material != None:
if ms.material.name not in self.materials:
self.materials[ms.material.name] = Material(self.render_engine, self.depsgraph, ms.material)
#------------------------------------------------------
# Collect world and object data.
try:
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
subframe = self.shutter_start + (self.shutter_diff*i)
time = self.fr + subframe
self.depsgraph.scene.frame_set(self.fr, subframe=subframe)
self.depsgraph.update()
# Collect camera and world data.
self.camera.take_sample(self.render_engine, self.depsgraph, time)
self.world.take_sample(self.render_engine, self.depsgraph, time)
# Collect renderable objects.
collected_objs = set() # Names of the objects whose data has already been collected.
for inst in self.depsgraph.object_instances:
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
if inst.object.type not in ['MESH', 'LIGHT']:
continue
# We use this a couple of times, so make a shorthand.
is_sun_lamp = inst.object.type == 'LIGHT' and inst.object.data.type == 'SUN'
# TODO: handle situations where there are more than one
# object with the same name. This can happen through
# library linking.
# Get a unique id for the instance. This is surprisingly
# tricky, because the instance's "persistent_id" property
# isn't globally unique, as I would have expected from
# the documentation.
id = None
if inst.is_instance:
id = (
hash((inst.object.name, inst.parent.name)),
# Has to be turned into a tuple, otherwise it doesn't
# work as part of the ID for some reason.
tuple(inst.persistent_id),
)
else:
id = inst.object.name
# Save the instance transforms.
if is_sun_lamp:
if id not in self.sun_lamp_instances:
self.sun_lamp_instances[id] = [inst.object.name, [inst.matrix_world.copy()]]
else:
self.sun_lamp_instances[id][1] += [inst.matrix_world.copy()]
else:
if id not in self.instances:
self.instances[id] = [inst.object.name, [inst.matrix_world.copy()]]
else:
self.instances[id][1] += [inst.matrix_world.copy()]
# Save the object data if it hasn't already been saved.
if inst.object.name not in collected_objs:
collected_objs.add(inst.object.name)
if is_sun_lamp:
if inst.object.name not in self.sun_lamp_data:
self.sun_lamp_data[inst.object.name] = DistantDiskLamp(self.render_engine, self.depsgraph, inst.object, inst.object.name)
self.sun_lamp_data[inst.object.name].take_sample(self.render_engine, self.depsgraph, inst.object, time)
else:
if inst.object.name not in self.object_data:
self.object_data[inst.object.name] = make_object_data_cache(self.render_engine, self.depsgraph, inst.object, inst.object.name)
self.object_data[inst.object.name].take_sample(self.render_engine, self.depsgraph, inst.object, time)
#------------------------------------------------------
# Export world and object data.
self.camera.export(self.render_engine, self.w)
self.world.export(self.render_engine, self.w)
self.w.write("Assembly {\n")
self.w.indent()
# Export materials.
for name in self.materials:
self.materials[name].export(self.render_engine, self.w)
# Export objects.
for name in self.object_data:
self.object_data[name].export(self.render_engine, self.w)
# Export instances.
for id in self.instances:
[obj_name, xforms] = self.instances[id]
self.render_engine.update_stats("", "Psychopath: Exporting %s instance" % obj_name)
prefix = str(hex(hash(id)))
name = "inst_{}__{}".format(prefix, escape_name(obj_name))
self.w.write("Instance {\n")
self.w.indent()
self.w.write("Data [${}]\n".format(escape_name(obj_name)))
for mat in xforms:
self.w.write("Transform [{}]\n".format(mat2str(mat)))
self.w.unindent()
self.w.write("}\n")
self.w.unindent()
self.w.write("}\n")
finally:
#------------------------------------------------------
# Cleanup collected data.
self.camera.cleanup()
self.world.cleanup()
for data in self.sun_lamp_data:
self.sun_lamp_data[data].cleanup()
for data in self.object_data:
self.object_data[data].cleanup()
# Scene end
self.w.unindent()
self.w.write("}\n")