armory/blender/lib/pbrpainter.py
2016-06-30 13:22:05 +02:00

610 lines
20 KiB
Python

# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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 2
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "PBR Painter",
"author": "Lubos Lenco",
"version": (1, 0, 2),
"blender": (2, 77, 0),
"location": "View3D > Tools > PBR",
"description": "Multi-texture painting",
"category": "Paint",
}
import math
import os
import bpy
from bpy.props import *
# Set occlusion to white
# Set strength to 1, set texture size to 0.2
# Set environmnent texture
# Paint to separate slots with pbr brush disabled
# Do not forget to save all texture slots at exit
def material_update(self, context):
scene = context.scene
if scene.material_prop != '':
for i in range(0, 5):
update_texture_index(context, i)
# Set brush textures
scene.brushindex1_prop = -1
scene.brushindex2_prop = -1
scene.brushindex3_prop = -1
scene.brushindex4_prop = -1
scene.brushindex5_prop = -1
imageindices = [
scene.imageindex1_prop,
scene.imageindex2_prop,
scene.imageindex3_prop,
scene.imageindex4_prop,
scene.imageindex5_prop]
i = 0
for ii in imageindices:
if ii != -1:
brush_name = 'brush_' + str(i + 1)
texture = bpy.data.textures[brush_name]
texture.image = bpy.data.images[ii]
j = 0
for brush in bpy.data.textures:
if brush.name == brush_name:
if i == 0:
scene.brushindex1_prop = j
elif i == 1:
scene.brushindex2_prop = j
elif i == 2:
scene.brushindex3_prop = j
elif i == 3:
scene.brushindex4_prop = j
elif i == 4:
scene.brushindex5_prop = j
break
j += 1
i += 1
def find_node_by_link(node_group, to_node, inp):
for link in node_group.links:
if link.to_node == to_node and link.to_socket == inp:
return link.from_node
def get_image_name(node_group, node, inp_index):
if node.inputs[inp_index].is_linked:
image_node = find_node_by_link(
node_group, node, node.inputs[inp_index])
if image_node.type == 'TEX_IMAGE':
return image_node.image.name
def update_texture_index(context, input_index):
scene = context.scene
mat = bpy.data.materials[scene.material_prop]
for n in mat.node_tree.nodes:
if n.type == 'GROUP' and n.node_tree.name.split('.', 1)[0] == 'PBR':
node = n
break
image_name = get_image_name(mat.node_tree, node, input_index)
i = 0
image_index = -1
for image in bpy.data.images:
if image_name == image.name:
image_index = i
break
i += 1
if image_index == -1:
return
# Get PBR material node
obj = context.image_paint_object
mat = obj.active_material
for n in mat.node_tree.nodes:
if n.type == 'GROUP' and n.node_tree.name == 'PBR':
node = n
break
image_name = get_image_name(mat.node_tree, node, input_index)
# Save image index to correct slot
i = 0
for slot in mat.texture_paint_images:
slot_name = slot.name
if slot_name == image_name:
if i == 0:
scene.imageindex1_prop = image_index
elif i == 1:
scene.imageindex2_prop = image_index
elif i == 2:
scene.imageindex3_prop = image_index
elif i == 3:
scene.imageindex4_prop = image_index
elif i == 4:
scene.imageindex5_prop = image_index
break
i += 1
def get_override():
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
override = {
'window': window,
'screen': screen,
'area': area,
'region': region}
return override
def brushtoggle_update(self, context):
if context.scene['PPBrush'] == 1:
material_update(self, context)
override = get_override()
bpy.ops.pp.brush_operator(override, 'INVOKE_DEFAULT')
def init_scene_properties():
bpy.types.Scene.PPBrush = BoolProperty(
name="PBR Brush", description="Enable multi-texture painting",
update=brushtoggle_update)
bpy.types.Scene.material_prop = bpy.props.StringProperty(
name="Material", description="PBR Material",
update=material_update, default="")
bpy.types.Scene.imageindex1_prop = bpy.props.IntProperty(
name="Texture Index 1", description="Texture index",
default=-1)
bpy.types.Scene.imageindex2_prop = bpy.props.IntProperty(
name="Texture Index 2", description="Texture index",
default=-1)
bpy.types.Scene.imageindex3_prop = bpy.props.IntProperty(
name="Texture Index 3", description="Texture index",
default=-1)
bpy.types.Scene.imageindex4_prop = bpy.props.IntProperty(
name="Texture Index 4", description="Texture index",
default=-1)
bpy.types.Scene.imageindex5_prop = bpy.props.IntProperty(
name="Texture Index 5", description="Texture index",
default=-1)
bpy.types.Scene.brushindex1_prop = bpy.props.IntProperty(
name="Brush Index 1", description="Brush index",
default=-1)
bpy.types.Scene.brushindex2_prop = bpy.props.IntProperty(
name="Brush Index 2", description="Brush index",
default=-1)
bpy.types.Scene.brushindex3_prop = bpy.props.IntProperty(
name="Brush Index 3", description="Brush index",
default=-1)
bpy.types.Scene.brushindex4_prop = bpy.props.IntProperty(
name="Brush Index 4", description="Brush index",
default=-1)
bpy.types.Scene.brushindex5_prop = bpy.props.IntProperty(
name="Brush Index 5", description="Brush index",
default=-1)
bpy.types.Scene.PPTextureWidth = IntProperty(
name="Width", description="Texture Width",
default=1024)
bpy.types.Scene.PPTextureHeight = IntProperty(
name="Height", description="Texture Height",
default=1024)
def paint_strokes(strokes):
bpy.ops.paint.image_paint(stroke=strokes)
def paint(mouse_path):
ts = bpy.context.tool_settings
bradius = ts.unified_paint_settings.size * 2
bstrength = ts.image_paint.brush.strength
strokes = []
i = 0
for x, y in mouse_path:
strokes.append({
'name': '',
'location': (0, 0, 0),
'mouse': (x, y),
'size': bradius,
'pen_flip': False,
'is_start': False,
'pressure': bstrength,
'time': i})
i += 1
scene = bpy.context.scene
obj = bpy.context.image_paint_object
slot = obj.active_material.paint_active_slot
brush = bpy.context.tool_settings.image_paint.brush
i = scene.brushindex1_prop
if i != -1:
brush.texture = bpy.data.textures[i]
obj.active_material.paint_active_slot = 0
paint_strokes(strokes)
i = scene.brushindex2_prop
if i != -1:
brush.texture = bpy.data.textures[i]
obj.active_material.paint_active_slot = 1
paint_strokes(strokes)
i = scene.brushindex3_prop
if i != -1:
brush.texture = bpy.data.textures[i]
obj.active_material.paint_active_slot = 2
paint_strokes(strokes)
i = scene.brushindex4_prop
if i != -1:
brush.texture = bpy.data.textures[i]
obj.active_material.paint_active_slot = 3
paint_strokes(strokes)
i = scene.brushindex5_prop
if i != -1:
brush.texture = bpy.data.textures[i]
obj.active_material.paint_active_slot = 4
paint_strokes(strokes)
obj.active_material.paint_active_slot = slot
class ModalPPBrushOperator(bpy.types.Operator):
"""Draw a line with the mouse"""
bl_idname = "pp.brush_operator"
bl_label = "PBR Brush"
bl_options = {"INTERNAL"}
def modal(self, context, event):
if bpy.context.image_paint_object is None:
return {'PASS_THROUGH'}
if event.type == 'MOUSEMOVE':
if self.mouse_down:
# Make points more dense if necessary
if len(self.last_mouse_path) > 0:
lx = self.last_mouse_path[-1][0]
ly = self.last_mouse_path[-1][1]
dx = event.mouse_region_x - lx
dy = event.mouse_region_y - ly
dist = math.sqrt(dx * dx + dy * dy)
if dist > 16:
stepx = (dx / dist) * 16
stepy = (dy / dist) * 16
for i in range(1, int(dist / 16)):
self.mouse_path.append(
(int(lx + stepx * i), int(ly + stepy * i)))
self.mouse_path.append(
(event.mouse_region_x, event.mouse_region_y))
paint(self.mouse_path)
self.last_mouse_path = self.mouse_path
self.mouse_path = []
return {'RUNNING_MODAL'}
elif context.scene['PPBrush'] == 0:
return {'FINISHED'}
elif event.type == 'LEFTMOUSE':
mx = event.mouse_region_x
my = event.mouse_region_y
prefs = bpy.context.user_preferences
use_region_overlap = prefs.system.use_region_overlap
view_area = False
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
view_area = True
r = area.regions[4]
if use_region_overlap:
tools_r = area.regions[1]
if mx < tools_r.width + 30 or my < 0 or mx > (r.width - tools_r.width - 30) or my > r.height:
return {'PASS_THROUGH'}
else: # mx is negative when in tools panel
if mx < 30 or my < 0 or mx > r.width - 30 or my > r.height:
return {'PASS_THROUGH'}
if view_area is False:
return {'PASS_THROUGH'}
brush = bpy.context.tool_settings.image_paint.brush
if brush.image_tool != 'DRAW':
return {'PASS_THROUGH'}
if context.scene.material_prop == '':
return {'PASS_THROUGH'}
if event.value == 'PRESS':
self.mouse_path.append(
(event.mouse_region_x, event.mouse_region_y))
paint(self.mouse_path)
self.last_mouse_path = self.mouse_path
self.mouse_path = []
self.mouse_down = True
args = (self, context)
# Disable undo
prefs = bpy.context.user_preferences
self.use_global_undo = prefs.edit.use_global_undo
self.undo_steps = prefs.edit.undo_steps
prefs.edit.undo_steps = 0
prefs.edit.use_global_undo = False
elif event.value == 'RELEASE':
# Clear
self.last_mouse_path = []
self.mouse_path = []
self.mouse_down = False
# Restore undo
prefs = bpy.context.user_preferences
prefs.edit.undo_steps = self.undo_steps
prefs.edit.use_global_undo = self.use_global_undo
return {'RUNNING_MODAL'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
if context.area.type == 'VIEW_3D':
self.last_mouse_path = []
self.mouse_path = []
self.mouse_down = False
self.use_global_undo = 0
self.undo_steps = 0
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
return {'CANCELLED'}
def get_override_smart_project(obj):
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
override = {
'window': bpy.context.window,
'scene': bpy.context.scene,
'screen': bpy.context.screen,
'blend_data': bpy.context.blend_data,
'active_object': bpy.context.active_object,
'selected_editable_objects':
bpy.context.selected_editable_objects,
'area': area,
'region': region,
'edit_object': obj}
return override
class PPSetupButton(bpy.types.Operator):
bl_idname = "pp.setup"
bl_label = "Setup"
def execute(self, context):
# Switch to cycles
for scene in bpy.data.scenes:
scene.render.engine = 'CYCLES'
# Create textures
w = context.scene.PPTextureWidth
h = context.scene.PPTextureHeight
obj = context.image_paint_object
basecolor_image = bpy.data.images.new(
obj.name + "_basecolor", width=w, height=h)
basecolor_image.pack(as_png=True)
occlusion_image = bpy.data.images.new(
obj.name + "_occlusion", width=w, height=h)
occlusion_image.pack(as_png=True)
roughness_image = bpy.data.images.new(
obj.name + "_roughness", width=w, height=h)
roughness_image.pack(as_png=True)
metalness_image = bpy.data.images.new(
obj.name + "_metalness", width=w, height=h)
metalness_image.pack(as_png=True)
normal_image = bpy.data.images.new(
obj.name + "_normal", width=w, height=h)
normal_image.pack(as_png=True)
# Unwrap if needed
if len(obj.data.uv_layers) == 0:
override = get_override_smart_project(obj)
bpy.ops.uv.smart_project(override)
# Import pbr nodes
import_node_groups()
# Create material if needed
if obj.name+'_pbr' not in bpy.data.materials:
mat = bpy.data.materials.new(name=obj.name+'_pbr')
mat.use_nodes = True
else:
mat = bpy.data.materials[obj.name+'_pbr']
if len(obj.data.materials) > 0:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
# Setup material
mat = obj.active_material
nodes = mat.node_tree.nodes
links = mat.node_tree.links
for n in nodes:
nodes.remove(n)
out_node = nodes.new('ShaderNodeOutputMaterial')
out_node.location = 0, 0
group_node = nodes.new("ShaderNodeGroup")
group_node.location = -250, 0
group_node.node_tree = bpy.data.node_groups['PBR']
links.new(group_node.outputs[0], out_node.inputs[0])
links.new(group_node.outputs[1], out_node.inputs[2])
# Connect textures
basecolor_node = nodes.new('ShaderNodeTexImage')
basecolor_node.location = -1300, 0
basecolor_node.image = basecolor_image
links.new(basecolor_node.outputs[0], group_node.inputs[0])
occlusion_node = nodes.new('ShaderNodeTexImage')
occlusion_node.location = -1100, -200
occlusion_node.image = occlusion_image
links.new(occlusion_node.outputs[0], group_node.inputs[1])
roughness_node = nodes.new('ShaderNodeTexImage')
roughness_node.location = -900, -400
roughness_node.image = roughness_image
roughness_node.color_space = 'NONE'
links.new(roughness_node.outputs[0], group_node.inputs[2])
metalness_node = nodes.new('ShaderNodeTexImage')
metalness_node.location = -700, -600
metalness_node.image = metalness_image
metalness_node.color_space = 'NONE'
links.new(metalness_node.outputs[0], group_node.inputs[3])
normal_node = nodes.new('ShaderNodeTexImage')
normal_node.location = -500, -800
normal_node.image = normal_image
links.new(normal_node.outputs[0], group_node.inputs[4])
# Create brush texture
for i in range(1, 6):
brush = bpy.data.textures.new('brush_' + str(i), 'IMAGE')
brush.use_fake_user = True
return{'FINISHED'}
def import_node_groups():
if bpy.data.node_groups.get('PBR') is None:
data_path = \
os.path.dirname(os.path.abspath(__file__)) + '/data.blend'
with bpy.data.libraries.load(data_path, link=False) as \
(data_from, data_to):
data_to.node_groups = ['PBR']
class PPImportNodeGroupsButton(bpy.types.Operator):
bl_idname = "pp.import_node_groups"
bl_label = "Import Node Groups"
def execute(self, context):
import_node_groups()
return{'FINISHED'}
class PPSetEnvironmentButton(bpy.types.Operator):
bl_idname = "pp.set_environment"
bl_label = "Set Environment"
def execute(self, context):
world = bpy.context.scene.world
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
for n in nodes:
nodes.remove(n)
out_node = nodes.new('ShaderNodeOutputWorld')
out_node.location = 0, 0
background_node = nodes.new('ShaderNodeBackground')
background_node.location = -250, 0
links.new(background_node.outputs[0], out_node.inputs[0])
sky_node = nodes.new('ShaderNodeTexSky')
sky_node.location = -500, 0
sky_node.sky_type = 'PREETHAM'
sky_node.turbidity = 3.0
links.new(sky_node.outputs[0], background_node.inputs[0])
return{'FINISHED'}
class PPExportTexturesButton(bpy.types.Operator):
bl_idname = "pp.export_textures"
bl_label = "Export Textures"
def execute(self, context):
return{'FINISHED'}
class PPExportSketchFabButton(bpy.types.Operator):
bl_idname = "pp.export_webgl"
bl_label = "Export to SketchFab"
def execute(self, context):
return{'FINISHED'}
class VIEW3D_PT_tools_imagepaint_pbr(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_context = "imagepaint"
bl_label = "PBR Painter"
def draw(self, context):
layout = self.layout
scene = context.scene
layout.label('Textures')
row = layout.row()
row.prop(scene, 'PPTextureWidth')
row.prop(scene, 'PPTextureHeight')
layout.operator("pp.setup")
layout.prop(scene, 'PPBrush')
if 'PPBrush' in scene and scene['PPBrush']:
layout.prop_search(
scene, "material_prop", bpy.data,
"materials", "Material")
layout.label('Utils')
box = layout.box()
box.operator("pp.import_node_groups")
box.operator("pp.set_environment")
#layout.label('Export')
#layout.operator("pp.export_textures")
#layout.operator("pp.export_webgl")
def register():
bpy.utils.register_module(__name__)
init_scene_properties()
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()