ECON / lib /common /blender_utils.py
Yuliang's picture
remove MeshLab dependency with Open3D
fb140f6
raw
history blame
12.4 kB
import bpy
import sys, os
from math import radians
import mathutils
import bmesh
print(sys.exec_prefix)
from tqdm import tqdm
import numpy as np
##################################################
# Globals
##################################################
views = 120
render = 'eevee'
cycles_gpu = False
quality_preview = False
samples_preview = 16
samples_final = 256
resolution_x = 512
resolution_y = 512
shadows = False
# diffuse_color = (57.0/255.0, 108.0/255.0, 189.0/255.0, 1.0)
# diffuse_color = (18/255., 139/255., 142/255.,1) #correct
# diffuse_color = (251/255., 60/255., 60/255.,1) #wrong
smooth = False
wireframe = False
line_thickness = 0.1
quads = False
object_transparent = False
mouth_transparent = False
compositor_background_image = False
compositor_image_scale = 1.0
compositor_alpha = 0.7
##################################################
# Helper functions
##################################################
def blender_print(*args, **kwargs):
print(*args, **kwargs, file=sys.stderr)
def using_app():
''' Returns if script is running through Blender application (GUI or background processing)'''
return (not sys.argv[0].endswith('.py'))
def setup_diffuse_transparent_material(target, color, object_transparent, backface_transparent):
''' Sets up diffuse/transparent material with backface culling in cycles'''
mat = target.active_material
if mat is None:
# Create material
mat = bpy.data.materials.new(name='Material')
target.data.materials.append(mat)
mat.use_nodes = True
nodes = mat.node_tree.nodes
for node in nodes:
nodes.remove(node)
node_geometry = nodes.new('ShaderNodeNewGeometry')
node_diffuse = nodes.new('ShaderNodeBsdfDiffuse')
node_diffuse.inputs[0].default_value = color
node_transparent = nodes.new('ShaderNodeBsdfTransparent')
node_transparent.inputs[0].default_value = (1.0, 1.0, 1.0, 1.0)
node_emission = nodes.new('ShaderNodeEmission')
node_emission.inputs[0].default_value = (0.0, 0.0, 0.0, 1.0)
node_mix = nodes.new(type='ShaderNodeMixShader')
if object_transparent:
node_mix.inputs[0].default_value = 1.0
else:
node_mix.inputs[0].default_value = 0.0
node_mix_mouth = nodes.new(type='ShaderNodeMixShader')
if object_transparent or backface_transparent:
node_mix_mouth.inputs[0].default_value = 1.0
else:
node_mix_mouth.inputs[0].default_value = 0.0
node_mix_backface = nodes.new(type='ShaderNodeMixShader')
node_output = nodes.new(type='ShaderNodeOutputMaterial')
links = mat.node_tree.links
links.new(node_geometry.outputs[6], node_mix_backface.inputs[0])
links.new(node_diffuse.outputs[0], node_mix.inputs[1])
links.new(node_transparent.outputs[0], node_mix.inputs[2])
links.new(node_mix.outputs[0], node_mix_backface.inputs[1])
links.new(node_emission.outputs[0], node_mix_mouth.inputs[1])
links.new(node_transparent.outputs[0], node_mix_mouth.inputs[2])
links.new(node_mix_mouth.outputs[0], node_mix_backface.inputs[2])
links.new(node_mix_backface.outputs[0], node_output.inputs[0])
return
##################################################
def setup_scene():
global render
global cycles_gpu
global quality_preview
global resolution_x
global resolution_y
global shadows
global wireframe
global line_thickness
global compositor_background_image
# Remove default cube
if 'Cube' in bpy.data.objects:
bpy.data.objects['Cube'].select_set(True)
bpy.ops.object.delete()
scene = bpy.data.scenes['Scene']
# Setup render engine
if render == 'cycles':
scene.render.engine = 'CYCLES'
else:
scene.render.engine = 'BLENDER_EEVEE'
scene.render.resolution_x = resolution_x
scene.render.resolution_y = resolution_y
scene.render.resolution_percentage = 100
scene.render.film_transparent = True
if quality_preview:
scene.cycles.samples = samples_preview
else:
scene.cycles.samples = samples_final
# Setup Cycles CUDA GPU acceleration if requested
if render == 'cycles':
if cycles_gpu:
print('Activating GPU acceleration')
bpy.context.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA'
if bpy.app.version[0] >= 3:
cuda_devices = bpy.context.preferences.addons[
'cycles'].preferences.get_devices_for_type(compute_device_type='CUDA')
else:
(cuda_devices, opencl_devices
) = bpy.context.preferences.addons['cycles'].preferences.get_devices()
if (len(cuda_devices) < 1):
print('ERROR: CUDA GPU acceleration not available')
sys.exit(1)
for cuda_device in cuda_devices:
if cuda_device.type == 'CUDA':
cuda_device.use = True
print('Using CUDA device: ' + str(cuda_device.name))
else:
cuda_device.use = False
print('Igoring CUDA device: ' + str(cuda_device.name))
scene.cycles.device = 'GPU'
if bpy.app.version[0] < 3:
scene.render.tile_x = 256
scene.render.tile_y = 256
else:
scene.cycles.device = 'CPU'
if bpy.app.version[0] < 3:
scene.render.tile_x = 64
scene.render.tile_y = 64
# Disable Blender 3 denoiser to properly measure Cycles render speed
if bpy.app.version[0] >= 3:
scene.cycles.use_denoising = False
# Setup camera
camera = bpy.data.objects['Camera']
camera.location = (0.0, -3, 1.8)
camera.rotation_euler = (radians(74), 0.0, 0)
bpy.data.cameras['Camera'].lens = 55
# Setup light
# Setup lights
light = bpy.data.objects['Light']
light.location = (-2, -3.0, 0.0)
light.rotation_euler = (radians(90.0), 0.0, 0.0)
bpy.data.lights['Light'].type = 'POINT'
bpy.data.lights['Light'].energy = 2
light.data.cycles.cast_shadow = False
if 'Sun' not in bpy.data.objects:
bpy.ops.object.light_add(type='SUN')
light_sun = bpy.context.active_object
light_sun.location = (0.0, -3, 0.0)
light_sun.rotation_euler = (radians(45.0), 0.0, radians(30))
bpy.data.lights['Sun'].energy = 2
light_sun.data.cycles.cast_shadow = shadows
else:
light_sun = bpy.data.objects['Sun']
if shadows:
# Setup shadow catcher
bpy.ops.mesh.primitive_plane_add()
plane = bpy.context.active_object
plane.scale = (5.0, 5.0, 1)
plane.cycles.is_shadow_catcher = True
# Exclude plane from diffuse cycles contribution to avoid bright pixel noise in body rendering
# plane.cycles_visibility.diffuse = False
if wireframe:
# Unmark freestyle edges
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.mark_freestyle_edge(clear=True)
bpy.ops.object.mode_set(mode='OBJECT')
# Setup freestyle mode for wireframe overlay rendering
if wireframe:
scene.render.use_freestyle = True
scene.render.line_thickness = line_thickness
bpy.context.view_layer.freestyle_settings.linesets[0].select_edge_mark = True
# Disable border edges so that we don't see contour of shadow catcher plane
bpy.context.view_layer.freestyle_settings.linesets[0].select_border = False
else:
scene.render.use_freestyle = False
if compositor_background_image:
# Setup compositing when using background image
setup_compositing()
else:
# Output transparent image when no background is used
scene.render.image_settings.color_mode = 'RGBA'
##################################################
def setup_compositing():
global compositor_image_scale
global compositor_alpha
# Node editor compositing setup
bpy.context.scene.use_nodes = True
tree = bpy.context.scene.node_tree
# Create input image node
image_node = tree.nodes.new(type='CompositorNodeImage')
scale_node = tree.nodes.new(type='CompositorNodeScale')
scale_node.inputs[1].default_value = compositor_image_scale
scale_node.inputs[2].default_value = compositor_image_scale
blend_node = tree.nodes.new(type='CompositorNodeAlphaOver')
blend_node.inputs[0].default_value = compositor_alpha
# Link nodes
links = tree.links
links.new(image_node.outputs[0], scale_node.inputs[0])
links.new(scale_node.outputs[0], blend_node.inputs[1])
links.new(tree.nodes['Render Layers'].outputs[0], blend_node.inputs[2])
links.new(blend_node.outputs[0], tree.nodes['Composite'].inputs[0])
def render_file(input_file, input_dir, output_file, output_dir, yaw, correct):
'''Render image of given model file'''
global smooth
global object_transparent
global mouth_transparent
global compositor_background_image
global quads
path = input_dir + input_file
# Import object into scene
bpy.ops.import_scene.obj(filepath=path)
object = bpy.context.selected_objects[0]
object.rotation_euler = (radians(90.0), 0.0, radians(yaw))
z_bottom = np.min(np.array([vert.co for vert in object.data.vertices])[:, 1])
# z_top = np.max(np.array([vert.co for vert in object.data.vertices])[:,1])
# blender_print(radians(90.0), z_bottom, z_top)
object.location -= mathutils.Vector((0.0, 0.0, z_bottom))
if quads:
bpy.context.view_layer.objects.active = object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.tris_convert_to_quads()
bpy.ops.object.mode_set(mode='OBJECT')
if smooth:
bpy.ops.object.shade_smooth()
# Mark freestyle edges
bpy.context.view_layer.objects.active = object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.mark_freestyle_edge(clear=False)
bpy.ops.object.mode_set(mode='OBJECT')
if correct:
diffuse_color = (18 / 255., 139 / 255., 142 / 255., 1) #correct
else:
diffuse_color = (251 / 255., 60 / 255., 60 / 255., 1) #wrong
setup_diffuse_transparent_material(object, diffuse_color, object_transparent, mouth_transparent)
if compositor_background_image:
# Set background image
image_path = input_dir + input_file.replace('.obj', '_original.png')
bpy.context.scene.node_tree.nodes['Image'].image = bpy.data.images.load(image_path)
# Render
bpy.context.scene.render.filepath = os.path.join(output_dir, output_file)
# Silence console output of bpy.ops.render.render by redirecting stdout to file
# Note: Does not actually write the output to file (Windows 7)
sys.stdout.flush()
old = os.dup(1)
os.close(1)
os.open('blender_render.log', os.O_WRONLY | os.O_CREAT)
# Render
bpy.ops.render.render(write_still=True)
# Remove temporary output redirection
# sys.stdout.flush()
# os.close(1)
# os.dup(old)
# os.close(old)
# Delete last selected object from scene
object.select_set(True)
bpy.ops.object.delete()
def process_file(input_file, input_dir, output_file, output_dir, correct=True):
global views
global quality_preview
if not input_file.endswith('.obj'):
print('ERROR: Invalid input: ' + input_file)
return
print('Processing: ' + input_file)
if output_file == '':
output_file = input_file[:-4]
if quality_preview:
output_file = output_file.replace('.png', '-preview.png')
angle = 360.0 / views
pbar = tqdm(range(0, views))
for view in pbar:
pbar.set_description(f"{os.path.basename(output_file)} | View:{str(view)}")
yaw = view * angle
output_file_view = f"{output_file}/{view:03d}.png"
if not os.path.exists(os.path.join(output_dir, output_file_view)):
render_file(input_file, input_dir, output_file_view, output_dir, yaw, correct)
cmd = "ffmpeg -loglevel quiet -r 30 -f lavfi -i color=c=white:s=512x512 -i " + os.path.join(output_dir, output_file, '%3d.png') + \
" -shortest -filter_complex \"[0:v][1:v]overlay=shortest=1,format=yuv420p[out]\" -map \"[out]\" -y " + output_dir+"/"+output_file+".mp4"
os.system(cmd)