Commit 7004bd20 by Christopher Gearhart

Now supports Blender Internal/Game render engines. Also increased stability with…

Now supports Blender Internal/Game render engines. Also increased stability with respect to quotations found in filepath.
parent d9359c9a
......@@ -5,6 +5,7 @@ Scripts and associated files for rendering from Blender files on CSE remote serv
## Server Farm Client Add-On:
* Features:
* Clean UI for sending frames to servers and viewing them within Blender
* Full support for Cycles Render Engine (support for Blender Internal/Game 'animation' renders only - current frame render jobs processed locally)
* Mid-render previews/status updates available with 'SHIFT + P'
* Abort render with 'ESC'
* NOTE: Files are auto-packed into the .blend file with each render process
......
......@@ -64,7 +64,7 @@ def register():
bpy.types.Scene.compress = BoolProperty(
name="Compress",
description="Send compressed Blender file to host server (slower local save, faster file transfer)",
default=False)
default=True)
bpy.types.Scene.frameRanges = StringProperty(
name="Frames",
......@@ -132,7 +132,7 @@ def register():
bpy.props.lastRemotePath = None
bpy.props.needsUpdating = True
bpy.props.nameAveragedImage = None
bpy.props.nameAveragedImage = ""
bpy.props.imExtension = False
bpy.props.nameImOutputFiles = ""
bpy.props.animExtension = False
......
......@@ -299,6 +299,13 @@ class sendFrame(Operator):
self.projectName = bpy.path.display_name_from_filepath(bpy.data.filepath).replace(" ", "_")
scn = context.scene
if scn.render.engine != "CYCLES":
self.report({"INFO"}, "Rendering on local machine (switch to cycles to render current frame on remote servers).")
context.area.type = "IMAGE_EDITOR"
bpy.ops.render.render(use_viewport=True)
context.area.spaces.active.image = bpy.data.images["Render Result"]
return{"FINISHED"}
# for testing purposes only (saves unsaved file as 'unsaved_file.blend')
if self.projectName == "":
self.projectName = "unsaved_file"
......@@ -587,7 +594,7 @@ class openRenderedImageInUI(Operator):
for area in context.screen.areas:
if area.type == "IMAGE_EDITOR":
area.spaces.active.image = bpy.data.images[bpy.props.nameAveragedImage]
elif bpy.props.nameAveragedImage and bpy.props.nameAveragedImage != "":
elif bpy.props.nameAveragedImage != "":
self.report({"ERROR"}, "Image could not be found: '{nameAveragedImage}'".format(nameAveragedImage=bpy.props.nameAveragedImage))
return{"CANCELLED"}
else:
......
......@@ -48,7 +48,7 @@ def getFrames(projectName, archiveFiles=False, frameRange=False):
f.close()
else:
includeDict = ""
archiveRsyncCommand = "rsync -qx --rsync-path='mkdir -p {dumpLocation}/backups/ && rsync' --remove-source-files {includeDict} --exclude='{nameOutputFiles}_????.???' {dumpLocation}/* {dumpLocation}/backups/;".format(includeDict=includeDict, dumpLocation=dumpLocation, nameOutputFiles=getNameOutputFiles(), imExtension=bpy.props.imExtension)
archiveRsyncCommand = "rsync -qx --rsync-path='mkdir -p {dumpLocation}/backups/ && rsync' --remove-source-files {includeDict} --exclude='{nameOutputFiles}_????.???' '{dumpLocation}/*' '{dumpLocation}/backups/';".format(includeDict=includeDict, dumpLocation=dumpLocation, nameOutputFiles=getNameOutputFiles(), imExtension=bpy.props.imExtension)
else:
archiveRsyncCommand = "mkdir -p {dumpLocation};".format(dumpLocation=dumpLocation)
......@@ -56,6 +56,7 @@ def getFrames(projectName, archiveFiles=False, frameRange=False):
fetchRsyncCommand = "rsync -x --progress --remove-source-files --exclude='*.blend' --exclude='*_average.???' -e 'ssh -T -oCompression=no -oStrictHostKeyChecking=no -x' '{login}:{remotePath}{projectName}/results/*' '{dumpLocation}/';".format(login=bpy.props.serverPrefs["login"], remotePath=bpy.props.serverPrefs["path"], projectName=projectName, dumpLocation=dumpLocation)
# run the above processes
print(archiveRsyncCommand + fetchRsyncCommand)
process = subprocess.Popen(archiveRsyncCommand + fetchRsyncCommand, stdout=subprocess.PIPE, shell=True)
return process
......@@ -260,8 +261,10 @@ def getRenderDumpFolder():
# if no user input, use default render location
else:
dumpLoc = os.path.join(bpy.path.abspath("//"), "render-dump")
# cleanse input
# TODO: throw error if dumpLoc contains backslash
# ensure it doesn't end in forwardslash
if dumpLoc.endswith("/"):
dumpLoc = dumpLoc[:-1]
# TODO: Throw error if backslash found in path
# check to make sure dumpLoc exists on local machine
if not os.path.exists(dumpLoc):
os.mkdir(dumpLoc)
......
......@@ -33,6 +33,11 @@ def jobIsValid(jobType, classObject):
if classObject.projectName == "":
jobValidityDict = {"valid":False, "errorType":"WARNING", "errorMessage":"RENDER FAILED: You have not saved your project file. Please save it before attempting to render."}
# verify there are no single or double quotes in project path
projectPath = bpy.path.abspath("//")
if "'" in projectPath or "\"" in projectPath:
jobValidityDict = {"valid":False, "errorType":"ERROR", "errorMessage":"RENDER FAILED: Found illegal quotation in project path ({projectPath}). Please change the file/directory name before attempting to render.".format(projectPath=projectPath)}
# verify that a camera exists in the scene
elif scn.camera is None:
jobValidityDict = {"valid":False, "errorType":"ERROR", "errorMessage":"RENDER FAILED: No camera in scene."}
......@@ -43,7 +48,7 @@ def jobIsValid(jobType, classObject):
jobValidityDict = {"valid":False, "errorType":"ERROR", "errorMessage":"RENDER FAILED: Output file format not supported. Supported formats: BMP, PNG, TARGA, JPEG, JPEG 2000, TIFF. (Animation only: IRIS, CINEON, HDR, DPX, OPEN_EXR, OPEN_EXR_MULTILAYER)"}
# verify that sampling is high enough to provide expected results
if jobType == "image":
if jobType == "image" and scn.render.engine == "CYCLES":
jobsPerFrame = scn.maxSamples // scn.samplesPerFrame
if jobsPerFrame > 100:
jobValidityDict = {"valid":False, "errorType":"ERROR", "errorMessage": "Max Samples / SamplesPerJob > 100. Try increasing samples per frame or lowering max samples."}
......
......@@ -40,46 +40,41 @@ class renderOnServersPanel(Panel):
layout = self.layout
scn = context.scene
if scn.render.engine != "CYCLES":
imRenderStatus = getRenderStatus("image")
animRenderStatus = getRenderStatus("animation")
# Available Servers Info
col = layout.column(align=True)
row = col.row(align=True)
availableServerString = "Available Servers: {available} / {total}".format(available=str(scn.availableServers),total=str(scn.availableServers + scn.offlineServers))
row.operator("scene.refresh_num_available_servers", text=availableServerString, icon="FILE_REFRESH")
# Render Buttons
row = col.row(align=True)
row.alignment = "EXPAND"
row.active = scn.availableServers > 0 or not scn.render.engine == "CYCLES"
row.operator("scene.render_frame_on_servers", text="Render", icon="RENDER_STILL")
row.operator("scene.render_animation_on_servers", text="Animation", icon="RENDER_ANIMATION")
col = layout.column(align=True)
row = col.row(align=True)
row.prop(scn, "serverGroups")
# Render Status Info
if imRenderStatus != "None":
col = layout.column(align=True)
row = col.row(align=True)
row.label("Please switch to Cycles")
else:
imRenderStatus = getRenderStatus("image")
animRenderStatus = getRenderStatus("animation")
# Available Servers Info
row.label("Render Status: {imRenderStatus}".format(imRenderStatus=imRenderStatus))
elif animRenderStatus != "None":
col = layout.column(align=True)
row = col.row(align=True)
availableServerString = "Available Servers: {available} / {total}".format(available=str(scn.availableServers),total=str(scn.availableServers + scn.offlineServers))
row.operator("scene.refresh_num_available_servers", text=availableServerString, icon="FILE_REFRESH")
row.label("Render Status: {animRenderStatus}".format(animRenderStatus=animRenderStatus))
# Render Buttons
row = col.row(align=True)
row.alignment = "EXPAND"
row.active = scn.availableServers > 0
row.operator("scene.render_frame_on_servers", text="Render", icon="RENDER_STILL")
row.operator("scene.render_animation_on_servers", text="Animation", icon="RENDER_ANIMATION")
col = layout.column(align=True)
row = col.row(align=True)
row.prop(scn, "serverGroups")
# Render Status Info
if imRenderStatus != "None":
col = layout.column(align=True)
row = col.row(align=True)
row.label("Render Status: {imRenderStatus}".format(imRenderStatus=imRenderStatus))
elif animRenderStatus != "None":
col = layout.column(align=True)
row = col.row(align=True)
row.label("Render Status: {animRenderStatus}".format(animRenderStatus=animRenderStatus))
# display buttons to view render(s)
row = layout.row(align=True)
if "image" in scn.renderType:
row.operator("scene.open_rendered_image", text="View Image", icon="FILE_IMAGE")
if "animation" in scn.renderType:
row.operator("scene.open_rendered_animation", text="View Animation", icon="FILE_MOVIE")
# display buttons to view render(s)
row = layout.row(align=True)
if "image" in scn.renderType:
row.operator("scene.open_rendered_image", text="View Image", icon="FILE_IMAGE")
if "animation" in scn.renderType:
row.operator("scene.open_rendered_animation", text="View Animation", icon="FILE_MOVIE")
class frameRangePanel(Panel):
bl_space_type = "VIEW_3D"
......@@ -94,16 +89,15 @@ class frameRangePanel(Panel):
layout = self.layout
scn = context.scene
if scn.render.engine == "CYCLES":
col = layout.column(align=True)
row = col.row(align=True)
row.prop(scn, "frameRanges")
col = layout.column(align=True)
col.active = bpy.path.display_name_from_filepath(bpy.data.filepath) != ""
row = col.row(align=True)
row.operator("scene.list_frames", text="List Missing Frames", icon="LONGDISPLAY")
row = col.row(align=True)
row.operator("scene.set_to_missing_frames", text="Set to Missing Frames", icon="FILE_PARENT")
col = layout.column(align=True)
row = col.row(align=True)
row.prop(scn, "frameRanges")
col = layout.column(align=True)
col.active = bpy.path.display_name_from_filepath(bpy.data.filepath) != ""
row = col.row(align=True)
row.operator("scene.list_frames", text="List Missing Frames", icon="LONGDISPLAY")
row = col.row(align=True)
row.operator("scene.set_to_missing_frames", text="Set to Missing Frames", icon="FILE_PARENT")
class serversPanel(Panel):
bl_space_type = "VIEW_3D"
......@@ -119,35 +113,35 @@ class serversPanel(Panel):
layout = self.layout
scn = context.scene
if scn.render.engine == "CYCLES":
col = layout.column(align=True)
row = col.row(align=True)
row.operator("scene.edit_servers_dict", text="Edit Remote Servers", icon="TEXT")
col = layout.column(align=True)
row = col.row(align=True)
row.operator("scene.edit_servers_dict", text="Edit Remote Servers", icon="TEXT")
col = layout.column(align=True)
row = col.row(align=True)
col = layout.column(align=True)
row = col.row(align=True)
box = row.box()
box.prop(scn, "showAdvanced")
if scn.showAdvanced:
col = box.column()
col.prop(scn, "nameOutputFiles")
col.prop(scn, "renderDumpLoc")
box = row.box()
box.prop(scn, "showAdvanced")
if scn.showAdvanced:
col = box.column()
col.prop(scn, "nameOutputFiles")
col.prop(scn, "renderDumpLoc")
layout.separator()
layout.separator()
col = box.column(align=True)
col.label(text="Performance:")
col.prop(scn, "maxServerLoad")
col.prop(scn, "timeout")
col = box.column(align=True)
col.label(text="Performance:")
col.prop(scn, "maxServerLoad")
col.prop(scn, "timeout")
if scn.render.engine == "CYCLES":
col.prop(scn, "samplesPerFrame")
col.prop(scn, "maxSamples")
layout.separator()
layout.separator()
row = col.row(align=True)
row.prop(scn, "killPython")
row.prop(scn, "compress")
# The following is probably unnecessary
# col = box.row(align=True)
# col.prop(scn, "tempLocalDir")
row = col.row(align=True)
row.prop(scn, "killPython")
row.prop(scn, "compress")
# The following is probably unnecessary
# col = box.row(align=True)
# col.prop(scn, "tempLocalDir")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment