diff --git a/cg/blender/texturing/bake_submitter.py b/cg/blender/texturing/bake_submitter.py index 214c0ae..526b7ac 100644 --- a/cg/blender/texturing/bake_submitter.py +++ b/cg/blender/texturing/bake_submitter.py @@ -280,10 +280,12 @@ def bw_submit(lowpoly_obj_names, resolution=4096, tree_name='robossembler', area node_y_pos -= 4000 bpy.ops.bake_wrangler.bake_pass(tree=tree_name, node=node_batch.name, sock=-1) + logger.info('Baking first pass is finished!') # double pass tree.nodes['Mesh Settings']['ray_dist'] = 0.01 bpy.ops.bake_wrangler.bake_pass(tree=tree_name, node=node_batch_double.name, sock=-1) + logger.info('Baking double pass is finished!') - return True + return textures_path diff --git a/cg/blender/texturing/composing.py b/cg/blender/texturing/composing.py new file mode 100644 index 0000000..f758372 --- /dev/null +++ b/cg/blender/texturing/composing.py @@ -0,0 +1,237 @@ +# coding: utf-8 +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Basic image processing for asset pipeline. +''' +__version__ = '0.1' + +import logging +import os +import bpy + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def compose_baked_textures(textures_path, resolution=4096): + ''' Mix, edit, and inpaint baked textures to production condition. ''' + + bake_path = os.path.join(textures_path, 'bake') + bake_path_double = os.path.join(textures_path, 'bake_double') + + baked_images = sorted(os.listdir(bake_path)) + + bpy.context.scene.render.resolution_x = resolution + bpy.context.scene.render.resolution_y = resolution + bpy.context.scene.render.resolution_percentage = 100 + bpy.context.scene.frame_start = 1 + bpy.context.scene.frame_end = 1 + bpy.context.scene.render.filepath = textures_path + bpy.context.scene.render.image_settings.file_format = 'PNG' + bpy.context.scene.render.image_settings.color_mode = 'RGB' + bpy.context.scene.render.image_settings.compression = 100 + + bpy.context.scene.display_settings.display_device = 'sRGB' + bpy.context.scene.view_settings.view_transform = 'Standard' + bpy.context.scene.view_settings.look = 'None' + bpy.context.scene.view_settings.exposure = 0 + bpy.context.scene.view_settings.gamma = 1 + bpy.context.scene.view_settings.use_curve_mapping = False + + bpy.context.scene.render.use_compositing = True + bpy.context.scene.render.dither_intensity = 2 + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + # remove all default nodes + for node in tree.nodes: + tree.nodes.remove(node) + + node_y_pos = 0 + # render for eatch uv packs + uv_images = [image for image in baked_images if 'UV' in image] + for uv_image in uv_images: + uv_obj = bpy.data.images.load(os.path.join(bake_path, uv_image)) + uv_node = tree.nodes.new(type='CompositorNodeImage') + uv_node.image = bpy.data.images[uv_obj.name] + uv_node.location = 0, (node_y_pos + 500) + key_node = tree.nodes.new(type="CompositorNodeLumaMatte") + key_node.location = 250, (node_y_pos + 500) + key_node.limit_max = 0.01 + tree.links.new(uv_node.outputs['Image'], key_node.inputs['Image']) + de_node = tree.nodes.new(type="CompositorNodeDilateErode") + de_node.location = 500, (node_y_pos + 500) + de_node.distance = -2 + tree.links.new(key_node.outputs['Matte'], de_node.inputs['Mask']) + + slot_name = uv_image.split('_UV')[0] + for image in baked_images: + if '_UV' in image and image.startswith(slot_name): + continue + if '_AO' in image: + image_obj = bpy.data.images.load(os.path.join(bake_path, image)) + image_node = tree.nodes.new(type='CompositorNodeImage') + image_node.location = -500, (node_y_pos + 200) + image_node.image = bpy.data.images[image_obj.name] + + image_obj_double = bpy.data.images.load(os.path.join(bake_path_double, image)) + image_node_double = tree.nodes.new(type='CompositorNodeImage') + image_node_double.location = -500, (node_y_pos - 200) + image_node_double.image = bpy.data.images[image_obj_double.name] + + lighten_node = tree.nodes.new(type="CompositorNodeMixRGB") + lighten_node.location = 0, node_y_pos + lighten_node.blend_type = 'LIGHTEN' + + tree.links.new(image_node.outputs['Image'], lighten_node.inputs[1]) + tree.links.new(image_node_double.outputs['Image'], lighten_node.inputs[2]) + + aa_node = tree.nodes.new(type="CompositorNodeAntiAliasing") + aa_node.location = 500, node_y_pos + aa_node.threshold = 0 + tree.links.new(lighten_node.outputs['Image'], aa_node.inputs['Image']) + + elif '_N' in image: + image_obj = bpy.data.images.load(os.path.join(bake_path, image)) + image_node = tree.nodes.new(type='CompositorNodeImage') + image_node.location = -1000, (node_y_pos + 200) + image_node.image = bpy.data.images[image_obj.name] + + image_obj_double = bpy.data.images.load(os.path.join(bake_path_double, image)) + image_node_double = tree.nodes.new(type='CompositorNodeImage') + image_node_double.location = -1000, (node_y_pos - 200) + image_node_double.image = bpy.data.images[image_obj_double.name] + + channel_node = tree.nodes.new(type="CompositorNodeSeparateColor") + channel_node.location = -500, (node_y_pos + 200) + + tree.links.new(image_node.outputs['Image'], channel_node.inputs['Image']) + + mix_node = tree.nodes.new(type="CompositorNodeMixRGB") + mix_node.location = 0, node_y_pos + mix_node.blend_type = 'MIX' + + tree.links.new(channel_node.outputs['Blue'], mix_node.inputs[0]) + tree.links.new(image_node_double.outputs['Image'], mix_node.inputs[1]) + tree.links.new(image_node.outputs['Image'], mix_node.inputs[2]) + + aa_node = tree.nodes.new(type="CompositorNodeAntiAliasing") + aa_node.location = 500, node_y_pos + aa_node.threshold = 0 + tree.links.new(mix_node.outputs['Image'], aa_node.inputs['Image']) + + elif '_R' in image: + image_obj = bpy.data.images.load(os.path.join(bake_path, image)) + image_node = tree.nodes.new(type='CompositorNodeImage') + image_node.location = -600, (node_y_pos + 100) + image_node.image = bpy.data.images[image_obj.name] + + greater_node = tree.nodes.new(type="CompositorNodeMath") + greater_node.location = -300, (node_y_pos + 200) + greater_node.operation = 'GREATER_THAN' + greater_node.inputs[1].default_value = 0.01 + + tree.links.new(image_node.outputs['Image'], greater_node.inputs['Value']) + + mix_node = tree.nodes.new(type="CompositorNodeMixRGB") + mix_node.location = 0, node_y_pos + mix_node.blend_type = 'MIX' + mix_node.inputs[1].default_value = (1, 1, 1, 1) + + tree.links.new(greater_node.outputs['Value'], mix_node.inputs[0]) + tree.links.new(image_node.outputs['Image'], mix_node.inputs[2]) + + aa_node = tree.nodes.new(type="CompositorNodeAntiAliasing") + aa_node.location = 500, node_y_pos + aa_node.threshold = 0 + tree.links.new(mix_node.outputs['Image'], aa_node.inputs['Image']) + + else: + image_obj = bpy.data.images.load(os.path.join(bake_path, image)) + image_node = tree.nodes.new(type='CompositorNodeImage') + image_node.location = 0, node_y_pos + image_node.image = bpy.data.images[image_obj.name] + + aa_node = tree.nodes.new(type="CompositorNodeAntiAliasing") + aa_node.location = 500, node_y_pos + aa_node.threshold = 0 + tree.links.new(image_node.outputs['Image'], aa_node.inputs['Image']) + + sa_node = tree.nodes.new(type="CompositorNodeSetAlpha") + sa_node.location = 1000, node_y_pos + sa_node.mode = 'REPLACE_ALPHA' + tree.links.new(aa_node.outputs['Image'], sa_node.inputs['Image']) + tree.links.new(de_node.outputs['Mask'], sa_node.inputs['Alpha']) + + sa_node = tree.nodes.new(type="CompositorNodeSetAlpha") + sa_node.location = 1000, node_y_pos + sa_node.mode = 'REPLACE_ALPHA' + tree.links.new(aa_node.outputs['Image'], sa_node.inputs['Image']) + tree.links.new(de_node.outputs['Mask'], sa_node.inputs['Alpha']) + + ip_node = tree.nodes.new(type="CompositorNodeInpaint") + ip_node.location = 1500, node_y_pos + ip_node.distance = 512 + tree.links.new(sa_node.outputs['Image'], ip_node.inputs['Image']) + + sc_node = tree.nodes.new(type="CompositorNodeScale") + sc_node.location = 2000, node_y_pos + sc_node.space = 'RENDER_SIZE' + tree.links.new(ip_node.outputs['Image'], sc_node.inputs['Image']) + + if '_D' in image or '_N' in image: + out_node = tree.nodes.new(type="CompositorNodeOutputFile") + out_node.location = 3000, node_y_pos + out_node.file_slots[0].path = 'T_{}{}'.format( + slot_name, + image.split(slot_name)[1].split('.')[0].lower()) + tree.links.new(sc_node.outputs['Image'], out_node.inputs['Image']) + + else: + gamma_node = tree.nodes.new(type="CompositorNodeGamma") + gamma_node.location = 2500, node_y_pos + gamma_node.inputs[1].default_value = 2.2 + + tree.links.new(sc_node.outputs['Image'], gamma_node.inputs['Image']) + + out_node = tree.nodes.new(type="CompositorNodeOutputFile") + out_node.location = 3000, node_y_pos + out_node.file_slots[0].path = 'T_{}{}'.format( + slot_name, + image.split(slot_name)[1].split('.')[0].lower()) + tree.links.new(gamma_node.outputs['Image'], out_node.inputs['Image']) + + node_y_pos -= 500 + + # render + bpy.ops.render.render(use_viewport=True) + # remove all nodes after rendering + for node in tree.nodes: + tree.nodes.remove(node) + + # cleanup filenames + composed_textures = [ + texture + for texture in os.listdir(textures_path) + if '000' in texture and texture.endswith('.png') + ] + for texture in composed_textures: + os.rename( + os.path.join(textures_path, texture), + os.path.join(textures_path, texture.split('000')[0] + '.png') + ) + + logger.info('Composing %s textures is finished!', len(composed_textures)) + return textures_path diff --git a/cg/pipeline/cg_pipeline.py b/cg/pipeline/cg_pipeline.py index cd481aa..d9407f3 100644 --- a/cg/pipeline/cg_pipeline.py +++ b/cg/pipeline/cg_pipeline.py @@ -23,6 +23,7 @@ from blender.processing.midpoly_setup import hightpoly_collections_to_midpoly from blender.processing.lowpoly_setup import parts_to_shells from blender.processing.uv_setup import uv_unwrap from blender.texturing.bake_submitter import bw_submit +from blender.texturing.composing import compose_baked_textures from blender.export.dae import export_dae from blender.export.stl import export_stl import bpy @@ -129,7 +130,8 @@ def cg_pipeline(**kwargs): # bake textures bpy.ops.wm.open_mainfile(filepath=blend_path) - bw_submit(lowpoly_obj_names) + textures_path = bw_submit(lowpoly_obj_names) + compose_baked_textures(textures_path) # export object meshes and urdf to_urdf = collections.defaultdict(list)