Meshroom/meshroom/nodes/blender/scripts/renderAnimatedCameraInBlender.py
luz paz f4dcf6557f Fix various typos in the source code
## Description
Fix various typos in the source code. This includes user facing code, documentation, and source comments. This PR has not been tested.
Closes #1605
2022-01-22 07:39:05 -05:00

391 lines
19 KiB
Python

import bpy
import bmesh
import os
import re
import mathutils
import sys # to get command line args
import argparse # to parse options for us and print a nice help message
from distutils.util import strtobool
def main():
argv = sys.argv
if "--" not in argv:
argv = [] # as if no args are passed
else:
argv = argv[argv.index("--") + 1:] # get all args after "--"
# When --help or no args are given, print this help
usage_text = (
"Run blender in background mode with this script:"
" blender --background --python " + __file__ + " -- [options]"
)
parser = argparse.ArgumentParser(description=usage_text)
parser.add_argument(
"--sfmCameraPath", metavar='FILE', required=True,
help="sfmData with the animated camera.",
)
parser.add_argument(
"--useBackground", type=strtobool, required=True,
help="Display the background image or not.",
)
parser.add_argument(
"--undistortedImages", metavar='FILE', required=False,
help="Save the generated file to the specified path",
)
parser.add_argument(
"--model", metavar='FILE', required=True,
help="Point Cloud or Mesh used in the rendering.",
)
# Point Cloud Arguments (when SFM Data is .abc)
parser.add_argument(
"--pointCloudDensity", type=float, required=False,
help="Number of point from the cloud rendered",
)
parser.add_argument(
"--particleSize", type=float, required=False,
help="Scale of particles used to show the point cloud",
)
parser.add_argument(
"--particleColor", type=str, required=False,
help="Color of particles used to show the point cloud (SFM Data is .abc)",
)
# Mesh Arguments (when SFM Data is .obj)
parser.add_argument(
"--edgeColor", type=str, required=False,
help="Color of the edges of the rendered object (SFM Data is .obj)",
)
# Output Arguments
parser.add_argument(
"--videoFormat", type=str, required=True,
help="Format of the video output",
)
parser.add_argument(
"--outputPath", metavar='FILE', required=True,
help="Render an image to the specified path",
)
args = parser.parse_args(argv)
if not argv:
parser.print_help()
return -1
if not args.undistortedImages and args.useBackground:
print("Error: --undistortedImages argument not given, aborting.")
parser.print_help()
return -1
# Clear Current Scene
try:
for objects in bpy.data.objects:
bpy.data.objects.remove(objects)
except RuntimeError:
print("Error while clearing current scene")
raise
# The Switcher is the setting for most of the colors (if you want to add some, do it here and in the arguments of the node)
# Keep in mind that we use the same switcher for both the Edge Color and the Particle Color settings.
# So if you add a color to one of them in the node, might has well add it to the other.
switcher={
'Grey':(0.2, 0.2, 0.2, 1),
'White':(1, 1, 1, 1),
'Red':(0.5, 0, 0, 1),
'Green':(0, 0.5, 0, 1),
'Magenta':(1.0, 0, 0.75, 1)
}
print("Import Undistorted Images")
undis_imgs = []
# Some of these variable will be very useful in the next steps keep them in mind
number_of_frame = 0
offset = 0
first_image_name = ""
try:
# In this part of the code we take the undistorted images and we process some info about them
# undis_imgs is the list of the images' names
# first_image_name says it all in the name
# The offset is important, it corresponds to the last part of the name of the first frame.
# In most case, it will be 0 but the sequence may start from a more advanced frame.
if args.useBackground:
files = os.listdir(args.undistortedImages)
for f in files :
if f.endswith(".exr") and not f.__contains__("UVMap"):
undis_imgs.append({"name":f})
number_of_frame = len(undis_imgs)
print("undis_imgs: " + str(undis_imgs))
first_image_name = undis_imgs[0]['name']
offset = int(re.findall(r'\d+', first_image_name)[-1]) - 1
except RuntimeError:
print("Error while importing the undistorted images.")
raise
print("Import Animated Camera")
try:
# In this part of the code we import the alembic in the cam_path to get the animated camera
# We use cam_location and cam_obj to store information about this camera
# We look for a camera (of type 'Persp') with the name 'anim' (not to confuse them with previously imported cams)
# Once the cam has been found we select the main camera of the scene.
# The rest of the code is setting up the display of the background image,
# As it is not a simple image but an image Sequence, we have to use the offset and the number of frames.
# We also have to make the scene render film transparent because we want to be able to display
# our background afterwards.
bpy.ops.wm.alembic_import(filepath=args.sfmCameraPath)
animated_cams = bpy.context.selected_editable_objects[:]
cam_location = mathutils.Vector((0, 0, 0))
cam_obj = None
for obj in animated_cams:
if obj.data and obj.data.type == 'PERSP' and "anim" in obj.data.name:
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
bpy.context.scene.camera = obj
cam_location = obj.location
cam_obj = obj
if args.useBackground :
bpy.ops.image.open(filepath=args.undistortedImages + "/" + first_image_name, directory=args.undistortedImages, files=undis_imgs, relative_path=True, show_multiview=False)
bpy.data.cameras[obj.data.name].background_images.new()
bpy.data.cameras[obj.data.name].show_background_images = True
bpy.data.cameras[obj.data.name].background_images[0].image = bpy.data.images[first_image_name]
bpy.data.cameras[obj.data.name].background_images[0].frame_method = 'CROP'
bpy.data.cameras[obj.data.name].background_images[0].image_user.frame_offset = offset
bpy.data.cameras[obj.data.name].background_images[0].image_user.frame_duration = number_of_frame
bpy.data.cameras[obj.data.name].background_images[0].image_user.frame_start = 1
bpy.context.scene.render.film_transparent = True
except RuntimeError:
print("Error while importing the alembic file (Animated Camera): " + args.sfmCameraPath)
raise
print("Create the particle plane")
try:
# We are using a particle system later in the code to display the Point Cloud.
# We need to setup a model for the particle, a plane that always face the camera.
# It is declared as a child of the camera in the parenting system, so when the camera moves, the plane moves accordingly.
# We use an 'Emission' material so it does not react to lights.
# To do that we have to use the shader 'node_tree' we clear all links between nodes, create the emission node
# and connect it to the 'Material Output' node.
plane = bpy.data.meshes.new('Plane')
objectsPlane = bpy.data.objects.new(name="Plane", object_data=plane)
bm = bmesh.new()
bmesh.ops.create_grid(bm, x_segments = 1, y_segments = 1, size = 1.0)
bm.to_mesh(plane)
bm.free()
if args.model.lower().endswith('.abc'):
objectsPlane.scale = mathutils.Vector((args.particleSize, args.particleSize, args.particleSize))
cam_location.y += -2.0
objectsPlane.location = cam_location
bpy.context.scene.collection.objects.link(objectsPlane)
bpy.data.objects['Plane'].parent = cam_obj
bpy.context.view_layer.objects.active = objectsPlane
col = bpy.data.materials.new('Color')
objectsPlane.active_material = col
objectsPlane.active_material.use_nodes = True
objectsPlane.active_material.node_tree.links.clear()
objectsPlane.active_material.node_tree.nodes.new(type='ShaderNodeEmission')
objectsPlane.active_material.node_tree.links.new(objectsPlane.active_material.node_tree.nodes['Emission'].outputs['Emission'], objectsPlane.active_material.node_tree.nodes['Material Output'].inputs['Surface'])
if args.model.lower().endswith('.abc'):
objectsPlane.active_material.node_tree.nodes['Emission'].inputs[0].default_value = switcher.get(args.particleColor, 'Invalid Color')
except RuntimeError:
print("Error: while setting up the particle model.")
raise
if args.model.lower().endswith('.abc'):
print("Import ABC Point Cloud")
# After importing the alembic, we look for a specific Point Cloud in the file.
# We make it the active object (important for the node_tree later).
# Then, we create a particle system on it. Render_type set to object and the said object is the plane.
# Emit_from 'vert' so the points of the point cloud are the one rendering the particle.
# The count is the number of particles repeated on the point cloud. We use the rate given as arguments
# to give a number.
# use_rotation and use_rotation_instance ensure that we use the same rotation than the model (which is needed to have the particle always facing the camera).
# Import Point Cloud
try:
bpy.ops.wm.alembic_import(filepath=args.model)
all_abc_info = bpy.context.selected_editable_objects[:]
for obj in all_abc_info:
if obj.name == 'mvgPointCloud.001': #May have a problem with such hard code
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.modifiers.new("ParticleSystem", "PARTICLE_SYSTEM")
particle_system = bpy.data.particles["ParticleSystem"]
particle_system.render_type = 'OBJECT'
particle_system.instance_object = bpy.data.objects["Plane"]
particle_system.emit_from = 'VERT'
if args.model.lower().endswith('.abc'):
particle_system.count = int(args.pointCloudDensity * len(obj.data.vertices.values()))
particle_system.frame_end = 1.0
particle_system.use_emit_random = False
particle_system.particle_size = 0.02
particle_system.physics_type = 'NO'
particle_system.use_rotations = True
particle_system.use_rotation_instance = True
particle_system.rotation_mode = 'GLOB_X'
except RuntimeError:
print("Error while importing the alembic file (Point Cloud): " + args.model)
raise
# For showing an outline of the object, we need to add two materials to the mesh:
# Center and Edge, we are using a method that consists in having a "bold" effect on the Edge Material so we can see it
# around the Center material. We use a Solidify Modifier on which we flip normals and reduce Thickness to below zero.
# The more the thickness get below zero, the more the edge will be largely revealed.
elif args.model.lower().endswith('.obj'):
print("Import OBJ")
bpy.ops.import_scene.obj(filepath=args.model)
center = bpy.data.materials.new('Center')
center.use_nodes = True
center.node_tree.links.clear()
center.node_tree.nodes.new(type='ShaderNodeEmission')
center.node_tree.links.new(center.node_tree.nodes['Emission'].outputs['Emission'], center.node_tree.nodes['Material Output'].inputs['Surface'])
center.node_tree.nodes['Emission'].inputs[0].default_value = (0,0,0,0)
if not args.useBackground and args.model.lower().endswith('.obj'):
center.node_tree.nodes['Emission'].inputs[0].default_value = (0.05, 0.05, 0.05, 1) #Same Color as the no background color in blender
edge = bpy.data.materials.new('Edge')
edge.use_nodes = True
edge.node_tree.links.clear()
edge.node_tree.nodes.new(type='ShaderNodeEmission')
edge.use_backface_culling = True
edge.node_tree.links.new(edge.node_tree.nodes['Emission'].outputs['Emission'], edge.node_tree.nodes['Material Output'].inputs['Surface'])
edge.node_tree.nodes['Emission'].inputs[0].default_value = switcher.get(args.edgeColor, 'Invalid Color')
bpy.data.meshes['mesh'].materials.clear()
bpy.data.meshes['mesh'].materials.append(bpy.data.materials['Center'])
bpy.data.meshes['mesh'].materials.append(bpy.data.materials['Edge'])
print(bpy.data.meshes['mesh'].materials.values())
bpy.data.objects['mesh'].modifiers.new('New', type='SOLIDIFY')
bpy.data.objects['mesh'].modifiers["New"].thickness = -0.01
bpy.data.objects['mesh'].modifiers["New"].use_rim = False
bpy.data.objects['mesh'].modifiers["New"].use_flip_normals = True
bpy.data.objects['mesh'].modifiers["New"].material_offset = 1
else:
raise ValueError("sfmData: unknown file format, only alembic (.abc) and object (.obj) are supported: " + args.model)
print("Create compositing graph")
# We use the compositing graph to add the background image.
# If the SFM Data is a Mesh, its extension is .obj so we have to build the graph accordingly. If the Background image setting was activated,
# we need to include it in our node tree through the "Image" and Scale node.
try:
bpy.context.scene.use_nodes = True
# Create all the nodes that we could need
bpy.context.scene.node_tree.nodes.new(type="CompositorNodeAlphaOver")
bpy.context.scene.node_tree.nodes.new(type="CompositorNodeScale")
bpy.context.scene.node_tree.nodes.new(type="CompositorNodeImage")
bpy.context.scene.node_tree.nodes.new(type="CompositorNodePremulKey")
bpy.context.scene.node_tree.nodes.new(type="CompositorNodeMixRGB")
bpy.data.scenes["Scene"].node_tree.nodes["Mix"].blend_type = 'LIGHTEN'
bpy.data.scenes["Scene"].node_tree.nodes["Image"].frame_duration = number_of_frame
bpy.data.scenes["Scene"].node_tree.nodes["Image"].frame_offset = offset
bpy.data.scenes["Scene"].node_tree.nodes["Scale"].space = 'RENDER_SIZE'
bpy.data.scenes["Scene"].node_tree.nodes["Scale"].frame_method = 'CROP'
# create links between nodes
if args.useBackground :
if args.model.lower().endswith('.obj'):
bpy.context.scene.node_tree.nodes["Image"].image = bpy.data.images[first_image_name]
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Mix'].outputs['Image'], bpy.context.scene.node_tree.nodes['Composite'].inputs['Image'])
# Two Inputs of AlphaOver are named "Image" so we use indexes instead
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Render Layers'].outputs['Image'], bpy.context.scene.node_tree.nodes['Alpha Convert'].inputs['Image'])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Alpha Convert'].outputs['Image'], bpy.context.scene.node_tree.nodes['Mix'].inputs[2])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Scale'].outputs['Image'], bpy.context.scene.node_tree.nodes['Mix'].inputs[1])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Image'].outputs['Image'], bpy.context.scene.node_tree.nodes['Scale'].inputs['Image'])
else:
bpy.context.scene.node_tree.nodes["Image"].image = bpy.data.images[first_image_name]
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Alpha Over'].outputs['Image'], bpy.context.scene.node_tree.nodes['Composite'].inputs['Image'])
# Two Inputs of AlphaOver are named "Image" so we use indexes instead
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Render Layers'].outputs['Image'], bpy.context.scene.node_tree.nodes['Alpha Over'].inputs[2])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Scale'].outputs['Image'], bpy.context.scene.node_tree.nodes['Alpha Over'].inputs[1])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Image'].outputs['Image'], bpy.context.scene.node_tree.nodes['Scale'].inputs['Image'])
else:
if args.model.lower().endswith('.obj'):
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Mix'].outputs['Image'], bpy.context.scene.node_tree.nodes['Composite'].inputs['Image'])
# Two Inputs of AlphaOver are named "Image" so we use indexes instead
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Render Layers'].outputs['Image'], bpy.context.scene.node_tree.nodes['Alpha Convert'].inputs['Image'])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Alpha Convert'].outputs['Image'], bpy.context.scene.node_tree.nodes['Mix'].inputs[2])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Scale'].outputs['Image'], bpy.context.scene.node_tree.nodes['Mix'].inputs[1])
bpy.context.scene.node_tree.links.new(bpy.context.scene.node_tree.nodes['Image'].outputs['Image'], bpy.context.scene.node_tree.nodes['Scale'].inputs['Image'])
except RuntimeError:
print("Error while creating the compositing graph.")
raise
try:
# Setup the render format and filepath
bpy.context.scene.render.image_settings.file_format = 'FFMPEG'
if args.videoFormat == 'mkv':
bpy.context.scene.render.ffmpeg.format = 'MKV'
elif args.videoFormat == 'avi':
bpy.context.scene.render.ffmpeg.format = 'AVI'
elif args.videoFormat == 'mov':
bpy.context.scene.render.ffmpeg.format = 'QUICKTIME'
else:
bpy.context.scene.render.ffmpeg.format = 'MPEG4'
bpy.context.scene.render.filepath = args.outputPath + '/render.' + args.videoFormat
print("Start Rendering")
# Render everything on to the filepath
bpy.ops.render.render(animation=True)
print("Rendering Done")
# Starts a player automatically to play the output
# bpy.ops.render.play_rendered_anim()
except RuntimeError:
print("Error while rendering the scene")
raise
return 0
if __name__ == "__main__":
err = 1
try:
err = main()
except Exception as e:
print("\n" + str(e))
sys.exit(err)
sys.exit(err)