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) | |