Spaces:
Runtime error
Runtime error
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) | |