Source code for xiuminglib.blender.render

from os.path import dirname, join
from glob import glob
from shutil import move
from time import time

from .. import os as xm_os
from ..imprt import preset_import

from ..log import get_logger
logger = get_logger()


[docs]def set_cycles(w=None, h=None, n_samples=None, max_bounces=None, min_bounces=None, transp_bg=None, color_mode=None, color_depth=None): """Sets up Cycles as rendering engine. ``None`` means no change. Args: w (int, optional): Width of render in pixels. h (int, optional): Height of render in pixels. n_samples (int, optional): Number of samples. max_bounces (int, optional): Maximum number of light bounces. Setting max_bounces to 0 for direct lighting only. min_bounces (int, optional): Minimum number of light bounces. transp_bg (bool, optional): Whether world background is transparent. color_mode (str, optional): Color mode: ``'BW'``, ``'RGB'`` or ``'RGBA'``. color_depth (str, optional): Color depth: ``'8'`` or ``'16'``. """ bpy = preset_import('bpy', assert_success=True) scene = bpy.context.scene scene.render.engine = 'CYCLES' cycles = scene.cycles cycles.use_progressive_refine = True if n_samples is not None: cycles.samples = n_samples if max_bounces is not None: cycles.max_bounces = max_bounces if min_bounces is not None: cycles.min_bounces = min_bounces cycles.caustics_reflective = False cycles.caustics_refractive = False cycles.diffuse_bounces = 10 cycles.glossy_bounces = 4 cycles.transmission_bounces = 4 cycles.volume_bounces = 0 cycles.transparent_min_bounces = 8 cycles.transparent_max_bounces = 64 # Avoid grainy renderings (fireflies) world = bpy.data.worlds['World'] world.cycles.sample_as_light = True cycles.blur_glossy = 5 cycles.sample_clamp_indirect = 5 # Ensure there's no background light emission world.use_nodes = True try: world.node_tree.nodes.remove(world.node_tree.nodes['Background']) except KeyError: pass # If world background is transparent with premultiplied alpha if transp_bg is not None: render.film_transparent = transp_bg # # Use GPU # bpy.context.user_preferences.system.compute_device_type = 'CUDA' # bpy.context.user_preferences.system.compute_device = \ # 'CUDA_' + str(randint(0, 3)) # scene.cycles.device = 'GPU' scene.render.tile_x = 16 # 256 optimal for GPU scene.render.tile_y = 16 # 256 optimal for GPU if w is not None: scene.render.resolution_x = w if h is not None: scene.render.resolution_y = h scene.render.resolution_percentage = 100 scene.render.use_file_extension = True scene.render.image_settings.file_format = 'PNG' if color_mode is not None: scene.render.image_settings.color_mode = color_mode if color_depth is not None: scene.render.image_settings.color_depth = color_depth logger.info("Cycles set up as rendering engine")
[docs]def easyset(w=None, h=None, n_samples=None, ao=None, color_mode=None, file_format=None, color_depth=None, sampling_method=None, n_aa_samples=None): """Sets some of the scene attributes more easily. Args: w (int, optional): Width of render in pixels. h (int, optional): Height of render in pixels. n_samples (int, optional): Number of samples. ao (bool, optional): Ambient occlusion. color_mode (str, optional): Color mode of rendering: ``'BW'``, ``'RGB'``, or ``'RGBA'``. file_format (str, optional): File format of the render: ``'PNG'``, ``'OPEN_EXR'``, etc. color_depth (str, optional): Color depth of rendering: ``'8'`` or ``'16'`` for .png; ``'16'`` or ``'32'`` for .exr. sampling_method (str, optional): Method to sample light and materials: ``'PATH'`` or ``'BRANCHED_PATH'``. n_aa_samples (int, optional): Number of anti-aliasing samples (used with ``'BRANCHED_PATH'``). """ bpy = preset_import('bpy', assert_success=True) scene = bpy.context.scene scene.render.resolution_percentage = 100 if w is not None: scene.render.resolution_x = w if h is not None: scene.render.resolution_y = h # Number of samples if n_samples is not None and scene.render.engine == 'CYCLES': scene.cycles.samples = n_samples # Ambient occlusion if ao is not None: scene.world.light_settings.use_ambient_occlusion = ao # Color mode of rendering if color_mode is not None: scene.render.image_settings.color_mode = color_mode # File format of the render if file_format is not None: scene.render.image_settings.file_format = file_format # Color depth of rendering if color_depth is not None: scene.render.image_settings.color_depth = color_depth # Method to sample light and materials if sampling_method is not None: scene.cycles.progressive = sampling_method # Number of anti-aliasing samples if n_aa_samples is not None: scene.cycles.aa_samples = n_aa_samples
def _render_prepare(cam, obj_names): bpy = preset_import('bpy', assert_success=True) if cam is None: cams = [o for o in bpy.data.objects if o.type == 'CAMERA'] assert (len(cams) == 1), \ "With `cam` not provided, there must be exactly one camera" cam = cams[0] if isinstance(obj_names, str): obj_names = [obj_names] elif obj_names is None: obj_names = [o.name for o in bpy.data.objects if o.type == 'MESH'] # Should be a list of strings by now for x in obj_names: assert isinstance(x, str), \ ("Objects should be specified by their names (strings), not " "objects themselves") scene = bpy.context.scene # Set active camera scene.camera = cam # Hide objects to ignore for obj in bpy.data.objects: if obj.type == 'MESH': obj.hide_render = obj.name not in obj_names scene.use_nodes = True # Clear the current scene node tree to avoid unexpected renderings nodes = scene.node_tree.nodes for n in nodes: if n.name != "Render Layers": nodes.remove(n) outnode = nodes.new('CompositorNodeOutputFile') return cam.name, obj_names, scene, outnode def _render(scene, outnode, result_socket, outpath, exr=True, alpha=True): bpy = preset_import('bpy', assert_success=True) node_tree = scene.node_tree # Set output file format if exr: file_format = 'OPEN_EXR' color_depth = '32' ext = '.exr' else: file_format = 'PNG' color_depth = '16' ext = '.png' if alpha: color_mode = 'RGBA' else: color_mode = 'RGB' outnode.base_path = '/tmp/%s/' % time() # NOTE: The trailing slash is important # Connect result socket(s) to the output node if isinstance(result_socket, dict): assert exr, ".exr must be used for multi-layer results" file_format += '_MULTILAYER' assert 'composite' in result_socket.keys(), ( "Composite pass is always rendered anyways. Plus, we need this " "dummy connection for the multi-layer OpenEXR file to be saved " "to disk (strangely)") node_tree.links.new(result_socket['composite'], outnode.inputs['Image']) # Add input slots and connect for k, v in result_socket.items(): outnode.layer_slots.new(k) node_tree.links.new(v, outnode.inputs[k]) render_f = join(outnode.base_path, '????.exr') else: node_tree.links.new(result_socket, outnode.inputs['Image']) render_f = join(outnode.base_path, 'Image????' + ext) outnode.format.file_format = file_format outnode.format.color_depth = color_depth outnode.format.color_mode = color_mode scene.render.filepath = '/tmp/%s' % time() # composite (to discard) # Render bpy.ops.render.render(write_still=True) # Depending on the scene state, the render filename may be anything # matching the pattern fs = glob(render_f) assert len(fs) == 1, ( "There should be only one file matching:\n\t{p}\n" "but found {n}").format(p=render_f, n=len(fs)) render_f = fs[0] # Move from temporary directory to the desired location if not outpath.endswith(ext): outpath += ext move(render_f, outpath) return outpath
[docs]def render(outpath, cam=None, obj_names=None, alpha=True, text=None): """Renders current scene with cameras in scene. Args: outpath (str): Path to save the render to. Should end with either .exr or .png. cam (bpy_types.Object, optional): Camera through which scene is rendered. If ``None``, use the only camera in scene. obj_names (str or list(str), optional): Name(s) of object(s) of interest. If ``None``, all objects are of interest and will appear in the render. alpha (bool, optional): Whether to render the alpha channel. text (dict, optional): What text to be overlaid on image and how, following the format:: { 'contents': "Hello World!", 'bottom_left_corner': (50, 50), 'font_scale': 1, 'bgr': (255, 0, 0), 'thickness': 2, } Writes - A 32-bit .exr or 16-bit .png image. """ outdir = dirname(outpath) xm_os.makedirs(outdir) cam_name, obj_names, scene, outnode = _render_prepare(cam, obj_names) result_socket = scene.node_tree.nodes['Render Layers'].outputs['Image'] # Render exr = outpath.endswith('.exr') outpath = _render( scene, outnode, result_socket, outpath, exr=exr, alpha=alpha) # Optionally overlay text if text is not None: cv2 = preset_import('cv2') im = cv2.imread(outpath, cv2.IMREAD_UNCHANGED) cv2.putText( im, text['contents'], text['bottom_left_corner'], cv2.FONT_HERSHEY_SIMPLEX, text['font_scale'], text['bgr'], text['thickness']) cv2.imwrite(outpath, im) logger.info("%s rendered through '%s'", obj_names, cam_name) logger.warning( "Node trees and renderability of these objects have changed")
def _disable_cycles_mat_nodes_for_bi(): """Disables Cycles material's nodes for Blender Internal. Cycles use_nodes being True leads to 0 alpha in Blender Internal. """ bpy = preset_import('bpy', assert_success=True) if bpy.context.scene.render.engine == 'BLENDER_RENDER': for o in bpy.data.objects: mat = o.active_material if mat is not None and mat.use_nodes: mat.use_nodes = False
[docs]def render_depth(outprefix, cam=None, obj_names=None, ray_depth=False): r"""Renders raw depth map in .exr of the specified object(s) from the specified camera. The EXR data contain an aliased :math:`z` map and an anti-aliased alpha map. Args: outprefix (str): Where to save the .exr maps to, e.g., ``'~/depth'``. cam (bpy_types.Object, optional): Camera through which scene is rendered. If ``None``, there must be the just one camera in the scene. obj_names (str or list(str), optional): Name(s) of object(s) of interest. ``None`` means all objects. ray_depth (bool, optional): Whether to render ray or plane depth. Writes - A 32-bit .exr depth map w/o anti-aliasing, located at ``outprefix + '_z.exr'``. - A 32-bit .exr alpha map w/ anti-aliasing, located at ``outprefix + '_a.exr'``. Todo: Ray depth. """ cam_name, obj_names, scene, outnode = _render_prepare(cam, obj_names) if ray_depth: raise NotImplementedError("Ray depth") # Use Blender Render for anti-aliased results -- faster than Cycles, # which needs >1 samples to figure out object boundary scene.render.engine = 'BLENDER_RENDER' scene.render.alpha_mode = 'TRANSPARENT' _disable_cycles_mat_nodes_for_bi() node_tree = scene.node_tree nodes = node_tree.nodes # Render z pass, without anti-aliasing to avoid values interpolated # between real depth values (e.g., 1.5) and large background depth values # (e.g., 1e10) scene.render.use_antialiasing = False scene.use_nodes = True try: result_socket = nodes['Render Layers'].outputs['Z'] except KeyError: result_socket = nodes['Render Layers'].outputs['Depth'] outpath_z = _render(scene, outnode, result_socket, outprefix + '_z') # Render alpha pass, with anti-aliasing to get a soft mask for blending scene.render.use_antialiasing = True result_socket = nodes['Render Layers'].outputs['Alpha'] outpath_a = _render(scene, outnode, result_socket, outprefix + '_a') logger.info("Depth map of %s rendered through '%s' to", obj_names, cam_name) logger.info("\t1. z w/o anti-aliasing: %s", outpath_z) logger.info("\t2. alpha w/ anti-aliasing: %s", outpath_a) logger.warning("The scene node tree has changed")
[docs]def render_alpha(outpath, cam=None, obj_names=None, samples=1000): r"""Renders binary or soft mask of objects from the specified camera. Args: outpath (str): Path to save the render to. Should end with .png. cam (bpy_types.Object, optional): Camera through which scene is rendered. If ``None``, there must be just one camera in scene. obj_names (str or list(str), optional): Name(s) of object(s) of interest. ``None`` means all objects. samples (int, optional): Samples per pixel. :math:`1` gives a hard mask, and :math:`\gt 1` gives a soft (anti-aliased) mask. Writes - A 16-bit three-channel .png mask, where bright indicates foreground. """ cam_name, obj_names, scene, outnode = _render_prepare(cam, obj_names) scene.render.engine = 'CYCLES' film_transparent_old = scene.render.film_transparent scene.render.film_transparent = True # Anti-aliased edges are built up by averaging multiple samples samples_old = scene.cycles.samples scene.cycles.samples = samples # Set nodes for (binary) alpha pass rendering node_tree = scene.node_tree nodes = node_tree.nodes result_socket = nodes['Render Layers'].outputs['Alpha'] # Render outpath = _render(scene, outnode, result_socket, outpath, exr=False, alpha=False) # Restore scene.cycles.samples = samples_old scene.render.film_transparent = film_transparent_old logger.info( "Foreground alpha of %s rendered through '%s'", obj_names, cam_name) logger.warning( "Node trees and renderability of these objects have changed")
[docs]def render_normal(outpath, cam=None, obj_names=None, outpath_refball=None, world_coords=False): r"""Renders raw normal map in .exr of the specified object(s) from the specified camera. RGB at each pixel is the (almost unit) normal vector at that location. Args: outpath (str): The .exr path (so data are raw values, not integer values) we save the normal map to. cam (bpy_types.Object, optional): Camera through which scene is rendered. If ``None``, there must be only one camera in scene. obj_names (str or list(str), optional): Name(s) of object(s) of interest. ``None`` means all objects. outpath_refball (str, optional): The .exr path to save the reference ball's normals to. ``None`` means not rendering the reference ball. world_coords (bool, optional): Whether to render normals in the world or *negated* camera space. Warning: **TL;DR** If you want camera-space normal maps, you need to negate the normal map after loading it from the .exr file this function writes. Otherwise, those normals live in a space that has all three axes flipped w.r.t. the camera's local space. **The Details** If you want world-space normals, then easy; I'll just use Cycles, and the normals are automatically in the world space. If camera-space normals are what you want, I'll use Blender Internal (BI), but there's some complication taken care of under the hood. BI renders normals "in the camera space." I verified this by keeping my scene intact, but having my camera rotating a bit; indeed, the normal vectors of a cube went from "round values", such as :math:`(0, 0, 1)`, to "non-round values", such as :math:`(0.02, 0.03, 0.99)`. But what precisely is this "camera space" (denoted by :math:`S`)? Is it really just the camera's local space? How do we go from :math:`S` to the world coordinate system, and possibly to another space therefrom? Here's my exploration. I put a camera at the scene center, and had it pointing, head-on, to one face of a default cube (so the camera saw just that face -- no other faces). I rendered the normals with BI: the raw RGB value is :math:`(0, 0, −1)`. OK, so the normal vector pointing out of the screen to my eyes is :math:`S`'s :math:`−z`. Hence, :math:`S`'s :math:`+z` points into the screen. Then I rotated the cube by just a little, so my camera got to see a little bit of the side faces it couldn't see before. The normal vector pointing to the left is :math:`(1, 0, 0)`; so the :math:`+x` of :math:`S` points to the left, tangent to the screen. Similarly, I found out the :math:`+y` points downwards, also tangent to the screen. But wait, the three axes don't even form a right-handed system; they form a left-handed one! This is so strange. Oh, if we negate all the axes, then we get a right-handed system. Would this negated system be the camera's local space (as in an object's local coordinate system)? It is! After eyeballing the camera's local space axes, I found they are exactly the flipped axes of :math:`S`. Therefore, when camera-space normals are asked for, I first render them out using BI, and then have to flip the signs to give the camera-space normals, which you can then transform to other spaces correctly with transformation matrices. Writes - A 32-bit .exr normal map of the object(s) of interest. - Another 32-bit .exr normal map of the reference ball, if asked for. """ from .object import add_sphere, remove_objects from .camera import get_2d_bounding_box bpy = preset_import('bpy', assert_success=True) objs = bpy.data.objects cam_name, obj_names, scene, outnode = _render_prepare(cam, obj_names) cam = objs[cam_name] # # Make normals consistent # for obj_name in obj_names: # scene.objects.active = objs[obj_name] # bpy.ops.object.mode_set(mode='EDIT') # bpy.ops.mesh.select_all() # bpy.ops.mesh.normals_make_consistent() # bpy.ops.object.mode_set(mode='OBJECT') # Set up scene node tree node_tree = scene.node_tree nodes = node_tree.nodes render_layer = bpy.context.view_layer # currently active one render_layer.use_pass_normal = True set_alpha_node = nodes.new('CompositorNodeSetAlpha') node_tree.links.new( nodes['Render Layers'].outputs['Alpha'], set_alpha_node.inputs['Alpha']) node_tree.links.new( nodes['Render Layers'].outputs['Normal'], set_alpha_node.inputs['Image']) result_socket = set_alpha_node.outputs['Image'] # Select rendering engine based on whether camera or world space if world_coords: scene.render.engine = 'CYCLES' film_transparent_old = scene.render.film_transparent scene.render.film_transparent = True samples_old = scene.cycles.samples scene.cycles.samples = 16 # for anti-aliased edges else: # camera space scene.render.engine = 'BLENDER_RENDER' scene.render.alpha_mode = 'TRANSPARENT' _disable_cycles_mat_nodes_for_bi() def add_refball(): from .camera import get_camera_matrix from ..geometry.proj import from_homo Vector = preset_import('Vector') # Place the ball at any depth so long as its center projects to # image center z_c = 10 # any reasonable depth in the camera space cam_mat, cam_int, _ = get_camera_matrix(cam, keep_disparity=True) x, y = cam_int[0][2], cam_int[1][2] center_xy1d = Vector([x, y, 1, 1 / z_c]) # with disparity xyzw = cam_mat.inverted() @ center_xy1d # from pixel to world xyz = from_homo(xyzw) sphere = add_sphere(location=xyz) # Scale the ball so that it, when projected, fits into the frame # and occupies reasonably large portion of the image bbox = get_2d_bounding_box(sphere, cam) s = 0.8 / max((bbox[1, 0] - bbox[0, 0]) / (2 * x), (bbox[3, 1] - bbox[0, 1]) / (2 * y)) sphere.scale = (s, s, s) # Achieve smooth normals with low polycount for f in sphere.data.polygons: f.use_smooth = True return sphere def render_refball(refball, out): mesh_hide_render = {} # Hide everything but the ball for o in [x for x in objs if x.type == 'MESH']: mesh_hide_render[o.name] = o.hide_render # save old state o.hide_render = o.name != refball.name out = _render(scene, outnode, result_socket, out) # Restore hide_render for k, v in mesh_hide_render.items(): objs[k].hide_render = v remove_objects(refball.name) return out if outpath_refball is not None: # Add reference normal ball refball = add_refball() outpath_refball = render_refball(refball, outpath_refball) # Render outpath = _render(scene, outnode, result_socket, outpath) # Restore if world_coords: scene.render.film_transparent = film_transparent_old scene.cycles.samples = samples_old logger.info("Normal map of %s rendered through %s to %s", obj_names, cam_name, outpath) if outpath_refball is not None: logger.info("Renference ball rendered through the same camera to %s", outpath_refball) logger.warning("The scene node tree has changed")
[docs]def render_lighting_passes( outpath, cam=None, obj_names=None, n_samples=None, select=None): """Renders select Cycles' lighting passes of the specified object(s) from the specified camera. Data are in a single multi-layer .exr file. See the code below for what channels are rendered. Args: outpath (str): Where to save the lighting passes to. Should end with .exr. cam (bpy_types.Object, optional): Camera through which scene is rendered. If ``None``, there must be only one camera in scene. obj_names (str or list(str), optional): Name(s) of object(s) of interest. ``None`` means all objects. n_samples (int, optional): Number of samples per pixel. Useful when you want a value different than other renderings; ``None`` means using the current value. select (list(str), optional): Render only this list of passes. ``None`` means rendering all passes: ``diffuse_direct``, ``diffuse_indirect``, ``diffuse_color``, ``glossy_direct``, ``glossy_indirect``, and ``glossy_color``. Writes - A 32-bit .exr multi-layer image containing the lighting passes. """ bpy = preset_import('bpy', assert_success=True) all_passes = { # a set useful for intrinsic image decomposition 'diffuse_direct': 'DiffDir', 'diffuse_indirect': 'DiffInd', 'diffuse_color': 'DiffCol', 'glossy_direct': 'GlossDir', 'glossy_indirect': 'GlossDir', 'glossy_color': 'GlossCol'} if select is None: select_passes = all_passes elif isinstance(select, str): select_passes = {select: all_passes[select]} else: select_passes = {k: v for k, v in all_passes.items() if k in select} cam_name, obj_names, scene, outnode = _render_prepare(cam, obj_names) scene.render.engine = 'CYCLES' n_samples_old = scene.cycles.samples if n_samples is not None: scene.cycles.samples = n_samples film_transparent_old = scene.render.film_transparent scene.render.film_transparent = True # Enable all passes of interest render_layer = bpy.context.view_layer # currently active one node_tree = scene.node_tree nodes = node_tree.nodes result_sockets = {} for p, p_key in select_passes.items(): setattr(render_layer, 'use_pass_' + p, True) # Set alpha set_alpha_node = nodes.new('CompositorNodeSetAlpha') node_tree.links.new( nodes['Render Layers'].outputs['Alpha'], set_alpha_node.inputs['Alpha']) node_tree.links.new( nodes['Render Layers'].outputs[p_key], set_alpha_node.inputs['Image']) result_sockets[p] = set_alpha_node.outputs['Image'] # Render if len(result_sockets) == 1: # NOTE: To avoid multi-layer .exr so that one can just read the render # with OpenCV (multi-layer would require OpenEXR) result_sockets = list(result_sockets.values())[0] else: # Multi-layer .exr saving strangely requires this composite pass result_sockets['composite'] = nodes['Render Layers'].outputs['Image'] outpath = _render(scene, outnode, result_sockets, outpath) # Restore scene.cycles.samples = n_samples_old scene.render.film_transparent = film_transparent_old logger.info( "Select lighting passes of %s rendered through '%s' to %s", obj_names, cam_name, outpath) logger.warning("The scene node tree has changed")