Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3616 from simonebarbieri/feature/blender-review
Browse files Browse the repository at this point in the history
  • Loading branch information
antirotor authored Mar 17, 2023
2 parents a80d6c5 + 78cc078 commit b45a907
Show file tree
Hide file tree
Showing 11 changed files with 781 additions and 3 deletions.
5 changes: 5 additions & 0 deletions openpype/hosts/blender/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@
lsattrs,
read,
maintained_selection,
maintained_time,
get_selection,
# unique_name,
)

from .capture import capture


__all__ = [
"install",
Expand All @@ -56,9 +59,11 @@

# Utility functions
"maintained_selection",
"maintained_time",
"lsattr",
"lsattrs",
"read",
"get_selection",
"capture",
# "unique_name",
]
278 changes: 278 additions & 0 deletions openpype/hosts/blender/api/capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@

"""Blender Capture
Playblasting with independent viewport, camera and display options
"""
import contextlib
import bpy

from .lib import maintained_time
from .plugin import deselect_all, create_blender_context


def capture(
camera=None,
width=None,
height=None,
filename=None,
start_frame=None,
end_frame=None,
step_frame=None,
sound=None,
isolate=None,
maintain_aspect_ratio=True,
overwrite=False,
image_settings=None,
display_options=None
):
"""Playblast in an independent windows
Arguments:
camera (str, optional): Name of camera, defaults to "Camera"
width (int, optional): Width of output in pixels
height (int, optional): Height of output in pixels
filename (str, optional): Name of output file path. Defaults to current
render output path.
start_frame (int, optional): Defaults to current start frame.
end_frame (int, optional): Defaults to current end frame.
step_frame (int, optional): Defaults to 1.
sound (str, optional): Specify the sound node to be used during
playblast. When None (default) no sound will be used.
isolate (list): List of nodes to isolate upon capturing
maintain_aspect_ratio (bool, optional): Modify height in order to
maintain aspect ratio.
overwrite (bool, optional): Whether or not to overwrite if file
already exists. If disabled and file exists and error will be
raised.
image_settings (dict, optional): Supplied image settings for render,
using `ImageSettings`
display_options (dict, optional): Supplied display options for render
"""

scene = bpy.context.scene
camera = camera or "Camera"

# Ensure camera exists.
if camera not in scene.objects and camera != "AUTO":
raise RuntimeError("Camera does not exist: {0}".format(camera))

# Ensure resolution.
if width and height:
maintain_aspect_ratio = False
width = width or scene.render.resolution_x
height = height or scene.render.resolution_y
if maintain_aspect_ratio:
ratio = scene.render.resolution_x / scene.render.resolution_y
height = round(width / ratio)

# Get frame range.
if start_frame is None:
start_frame = scene.frame_start
if end_frame is None:
end_frame = scene.frame_end
if step_frame is None:
step_frame = 1
frame_range = (start_frame, end_frame, step_frame)

if filename is None:
filename = scene.render.filepath

render_options = {
"filepath": "{}.".format(filename.rstrip(".")),
"resolution_x": width,
"resolution_y": height,
"use_overwrite": overwrite,
}

with _independent_window() as window:

applied_view(window, camera, isolate, options=display_options)

with contextlib.ExitStack() as stack:
stack.enter_context(maintain_camera(window, camera))
stack.enter_context(applied_frame_range(window, *frame_range))
stack.enter_context(applied_render_options(window, render_options))
stack.enter_context(applied_image_settings(window, image_settings))
stack.enter_context(maintained_time())

bpy.ops.render.opengl(
animation=True,
render_keyed_only=False,
sequencer=False,
write_still=False,
view_context=True
)

return filename


ImageSettings = {
"file_format": "FFMPEG",
"color_mode": "RGB",
"ffmpeg": {
"format": "QUICKTIME",
"use_autosplit": False,
"codec": "H264",
"constant_rate_factor": "MEDIUM",
"gopsize": 18,
"use_max_b_frames": False,
},
}


def isolate_objects(window, objects):
"""Isolate selection"""
deselect_all()

for obj in objects:
obj.select_set(True)

context = create_blender_context(selected=objects, window=window)

bpy.ops.view3d.view_axis(context, type="FRONT")
bpy.ops.view3d.localview(context)

deselect_all()


def _apply_options(entity, options):
for option, value in options.items():
if isinstance(value, dict):
_apply_options(getattr(entity, option), value)
else:
setattr(entity, option, value)


def applied_view(window, camera, isolate=None, options=None):
"""Apply view options to window."""
area = window.screen.areas[0]
space = area.spaces[0]

area.ui_type = "VIEW_3D"

meshes = [obj for obj in window.scene.objects if obj.type == "MESH"]

if camera == "AUTO":
space.region_3d.view_perspective = "ORTHO"
isolate_objects(window, isolate or meshes)
else:
isolate_objects(window, isolate or meshes)
space.camera = window.scene.objects.get(camera)
space.region_3d.view_perspective = "CAMERA"

if isinstance(options, dict):
_apply_options(space, options)
else:
space.shading.type = "SOLID"
space.shading.color_type = "MATERIAL"
space.show_gizmo = False
space.overlay.show_overlays = False


@contextlib.contextmanager
def applied_frame_range(window, start, end, step):
"""Context manager for setting frame range."""
# Store current frame range
current_frame_start = window.scene.frame_start
current_frame_end = window.scene.frame_end
current_frame_step = window.scene.frame_step
# Apply frame range
window.scene.frame_start = start
window.scene.frame_end = end
window.scene.frame_step = step
try:
yield
finally:
# Restore frame range
window.scene.frame_start = current_frame_start
window.scene.frame_end = current_frame_end
window.scene.frame_step = current_frame_step


@contextlib.contextmanager
def applied_render_options(window, options):
"""Context manager for setting render options."""
render = window.scene.render

# Store current settings
original = {}
for opt in options.copy():
try:
original[opt] = getattr(render, opt)
except ValueError:
options.pop(opt)

# Apply settings
_apply_options(render, options)

try:
yield
finally:
# Restore previous settings
_apply_options(render, original)


@contextlib.contextmanager
def applied_image_settings(window, options):
"""Context manager to override image settings."""

options = options or ImageSettings.copy()
ffmpeg = options.pop("ffmpeg", {})
render = window.scene.render

# Store current image settings
original = {}
for opt in options.copy():
try:
original[opt] = getattr(render.image_settings, opt)
except ValueError:
options.pop(opt)

# Store current ffmpeg settings
original_ffmpeg = {}
for opt in ffmpeg.copy():
try:
original_ffmpeg[opt] = getattr(render.ffmpeg, opt)
except ValueError:
ffmpeg.pop(opt)

# Apply image settings
for opt, value in options.items():
setattr(render.image_settings, opt, value)

# Apply ffmpeg settings
for opt, value in ffmpeg.items():
setattr(render.ffmpeg, opt, value)

try:
yield
finally:
# Restore previous settings
for opt, value in original.items():
setattr(render.image_settings, opt, value)
for opt, value in original_ffmpeg.items():
setattr(render.ffmpeg, opt, value)


@contextlib.contextmanager
def maintain_camera(window, camera):
"""Context manager to override camera."""
current_camera = window.scene.camera
if camera in window.scene.objects:
window.scene.camera = window.scene.objects.get(camera)
try:
yield
finally:
window.scene.camera = current_camera


@contextlib.contextmanager
def _independent_window():
"""Create capture-window context."""
context = create_blender_context()
current_windows = set(bpy.context.window_manager.windows)
bpy.ops.wm.window_new(context)
window = list(set(bpy.context.window_manager.windows) - current_windows)[0]
context["window"] = window
try:
yield window
finally:
bpy.ops.wm.window_close(context)
10 changes: 10 additions & 0 deletions openpype/hosts/blender/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,13 @@ def maintained_selection():
# This could happen if the active node was deleted during the
# context.
log.exception("Failed to set active object.")


@contextlib.contextmanager
def maintained_time():
"""Maintain current frame during context."""
current_time = bpy.context.scene.frame_current
try:
yield
finally:
bpy.context.scene.frame_current = current_time
7 changes: 5 additions & 2 deletions openpype/hosts/blender/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def prepare_data(data, container_name=None):


def create_blender_context(active: Optional[bpy.types.Object] = None,
selected: Optional[bpy.types.Object] = None,):
selected: Optional[bpy.types.Object] = None,
window: Optional[bpy.types.Window] = None):
"""Create a new Blender context. If an object is passed as
parameter, it is set as selected and active.
"""
Expand All @@ -72,7 +73,9 @@ def create_blender_context(active: Optional[bpy.types.Object] = None,

override_context = bpy.context.copy()

for win in bpy.context.window_manager.windows:
windows = [window] if window else bpy.context.window_manager.windows

for win in windows:
for area in win.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
Expand Down
47 changes: 47 additions & 0 deletions openpype/hosts/blender/plugins/create/create_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Create review."""

import bpy

from openpype.pipeline import legacy_io
from openpype.hosts.blender.api import plugin, lib, ops
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES


class CreateReview(plugin.Creator):
"""Single baked camera"""

name = "reviewDefault"
label = "Review"
family = "review"
icon = "video-camera"

def process(self):
""" Run the creator on Blender main thread"""
mti = ops.MainThreadItem(self._process)
ops.execute_in_main_thread(mti)

def _process(self):
# Get Instance Container or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)

# Create instance object
asset = self.data["asset"]
subset = self.data["subset"]
name = plugin.asset_name(asset, subset)
asset_group = bpy.data.collections.new(name=name)
instances.children.link(asset_group)
self.data['task'] = legacy_io.Session.get('AVALON_TASK')
lib.imprint(asset_group, self.data)

if (self.options or {}).get("useSelection"):
selected = lib.get_selection()
for obj in selected:
asset_group.objects.link(obj)
elif (self.options or {}).get("asset_group"):
obj = (self.options or {}).get("asset_group")
asset_group.objects.link(obj)

return asset_group
Loading

0 comments on commit b45a907

Please sign in to comment.