Browse Source

Port to Blender 2.80

pull/1/head
SavMartin 1 year ago
committed by GitHub
parent
commit
8e579918f0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 10993 additions and 0 deletions
  1. +24
    -0
      LICENSE.txt
  2. +1475
    -0
      __init__.py
  3. BIN
      bake_anti_alias.png
  4. BIN
      bake_obj_cage.png
  5. BIN
      bake_obj_float.png
  6. BIN
      bake_obj_high.png
  7. BIN
      bake_obj_low.png
  8. +131
    -0
      op_align.py
  9. BIN
      op_align_bottom.png
  10. BIN
      op_align_left.png
  11. BIN
      op_align_right.png
  12. BIN
      op_align_top.png
  13. BIN
      op_bake.png
  14. +581
    -0
      op_bake.py
  15. BIN
      op_bake_explode.png
  16. +221
    -0
      op_bake_explode.py
  17. +179
    -0
      op_bake_organize_names.py
  18. +98
    -0
      op_color_assign.py
  19. +96
    -0
      op_color_clear.py
  20. BIN
      op_color_convert_texture.png
  21. +153
    -0
      op_color_convert_texture.py
  22. BIN
      op_color_convert_vertex_colors.png
  23. +90
    -0
      op_color_convert_vertex_colors.py
  24. BIN
      op_color_from_directions.png
  25. +206
    -0
      op_color_from_directions.py
  26. +102
    -0
      op_color_from_elements.py
  27. +53
    -0
      op_color_from_materials.py
  28. +42
    -0
      op_color_io_export.py
  29. +66
    -0
      op_color_io_import.py
  30. +69
    -0
      op_color_select.py
  31. +375
    -0
      op_edge_split_bevel.py
  32. +142
    -0
      op_island_align_edge.py
  33. +171
    -0
      op_island_align_sort.py
  34. +291
    -0
      op_island_align_world.py
  35. +783
    -0
      op_island_mirror.py
  36. +81
    -0
      op_island_rotate_90.py
  37. +272
    -0
      op_island_straighten_edge_loops.py
  38. +296
    -0
      op_meshtex_create.py
  39. +193
    -0
      op_meshtex_pattern.py
  40. +77
    -0
      op_meshtex_trim.py
  41. +70
    -0
      op_meshtex_trim_collapse.py
  42. +86
    -0
      op_meshtex_wrap.py
  43. +637
    -0
      op_rectify.py
  44. +133
    -0
      op_select_islands_flipped.py
  45. +130
    -0
      op_select_islands_identical.py
  46. +78
    -0
      op_select_islands_outline.py
  47. +140
    -0
      op_select_islands_overlap.py
  48. +67
    -0
      op_smoothing_uv_islands.py
  49. +250
    -0
      op_texel_checker_map.py
  50. +133
    -0
      op_texel_density_get.py
  51. +189
    -0
      op_texel_density_set.py
  52. +50
    -0
      op_texture_open.py
  53. +101
    -0
      op_texture_preview.py
  54. +64
    -0
      op_texture_reload_all.py
  55. +43
    -0
      op_texture_remove.py
  56. +109
    -0
      op_texture_save.py
  57. +84
    -0
      op_texture_select.py
  58. +95
    -0
      op_unwrap_edge_peel.py
  59. +76
    -0
      op_unwrap_faces_iron.py
  60. +67
    -0
      op_uv_channel_add.py
  61. +70
    -0
      op_uv_channel_swap.py
  62. +66
    -0
      op_uv_crop.py
  63. +117
    -0
      op_uv_fill.py
  64. +264
    -0
      op_uv_resize.py
  65. +44
    -0
      op_uv_size_get.py
  66. +17
    -0
      settings.py
  67. +605
    -0
      utilities_bake.py
  68. +227
    -0
      utilities_color.py
  69. +120
    -0
      utilities_meshtex.py
  70. +138
    -0
      utilities_texel.py
  71. +170
    -0
      utilities_ui.py
  72. +286
    -0
      utilities_uv.py

+ 24
- 0
LICENSE.txt View File

@ -0,0 +1,24 @@
# Blender TexTools,
# <TexTools, Blender addon for editing UVs and Texture maps.>
# Copyright (C) <2018> <renderhjs>
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Credits
#
# UVSquares:
# <Uv Squares, Blender addon for reshaping UV vertices to grid.>
# Copyright (C) <2014> <Reslav Hollos>
# https://github.com/JoseConseco/UvSquares/blob/master/uv_squares.py

+ 1475
- 0
__init__.py
File diff suppressed because it is too large
View File


BIN
bake_anti_alias.png View File

Before After
Width: 32  |  Height: 32  |  Size: 15 KiB

BIN
bake_obj_cage.png View File

Before After
Width: 30  |  Height: 32  |  Size: 17 KiB

BIN
bake_obj_float.png View File

Before After
Width: 32  |  Height: 32  |  Size: 17 KiB

BIN
bake_obj_high.png View File

Before After
Width: 32  |  Height: 32  |  Size: 17 KiB

BIN
bake_obj_low.png View File

Before After
Width: 32  |  Height: 32  |  Size: 16 KiB

+ 131
- 0
op_align.py View File

@ -0,0 +1,131 @@
import bpy
import bmesh
import operator
from mathutils import Vector
from collections import defaultdict
from math import pi
from . import utilities_uv
class op(bpy.types.Operator):
bl_idname = "uv.textools_align"
bl_label = "Align"
bl_description = "Align vertices, edges or shells"
bl_options = {'REGISTER', 'UNDO'}
direction : bpy.props.StringProperty(name="Direction", default="top")
@classmethod
def poll(cls, context):
if not bpy.context.active_object:
return False
#Only in Edit mode
if bpy.context.active_object.mode != 'EDIT':
return False
#Only in UV editor mode
if bpy.context.area.type != 'IMAGE_EDITOR':
return False
#Requires UV map
if not bpy.context.object.data.uv_layers:
return False #self.report({'WARNING'}, "Object must have more than one UV map")
# Not in Synced mode
if bpy.context.scene.tool_settings.use_uv_select_sync:
return False
return True
def execute(self, context):
align(context, self.direction)
return {'FINISHED'}
def align(context, direction):
#Store selection
utilities_uv.selection_store()
if bpy.context.tool_settings.transform_pivot_point != 'CURSOR':
bpy.context.tool_settings.transform_pivot_point = 'CURSOR'
#B-Mesh
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data);
uv_layers = bm.loops.layers.uv.verify();
if len(obj.data.uv_layers) == 0:
print("There is no UV channel or UV data set")
return
# Collect BBox sizes
boundsAll = utilities_uv.getSelectionBBox()
mode = bpy.context.scene.tool_settings.uv_select_mode
if mode == 'FACE' or mode == 'ISLAND':
print("____ Align Islands")
#Collect UV islands
islands = utilities_uv.getSelectionIslands()
for island in islands:
bpy.ops.uv.select_all(action='DESELECT')
utilities_uv.set_selected_faces(island)
bounds = utilities_uv.getSelectionBBox()
# print("Island "+str(len(island))+"x faces, delta: "+str(delta.y))
if direction == "bottom":
delta = boundsAll['min'] - bounds['min']
bpy.ops.transform.translate(value=(0, delta.y, 0))
elif direction == "top":
delta = boundsAll['max'] - bounds['max']
bpy.ops.transform.translate(value=(0, delta.y, 0))
elif direction == "left":
delta = boundsAll['min'] - bounds['min']
bpy.ops.transform.translate(value=(delta.x, 0, 0))
elif direction == "right":
delta = boundsAll['max'] - bounds['max']
bpy.ops.transform.translate(value=(delta.x, 0, 0))
else:
print("Unkown direction: "+str(direction))
elif mode == 'EDGE' or mode == 'VERTEX':
print("____ Align Verts")
for f in bm.faces:
if f.select:
for l in f.loops:
luv = l[uv_layers]
if luv.select:
# print("Idx: "+str(luv.uv))
if direction == "top":
luv.uv[1] = boundsAll['max'].y
elif direction == "bottom":
luv.uv[1] = boundsAll['min'].y
elif direction == "left":
luv.uv[0] = boundsAll['min'].x
elif direction == "right":
luv.uv[0] = boundsAll['max'].x
bmesh.update_edit_mesh(obj.data)
#Restore selection
utilities_uv.selection_restore()
bpy.utils.register_class(op)

BIN
op_align_bottom.png View File

Before After
Width: 33  |  Height: 32  |  Size: 604 B

BIN
op_align_left.png View File

Before After
Width: 32  |  Height: 33  |  Size: 587 B

BIN
op_align_right.png View File

Before After
Width: 32  |  Height: 33  |  Size: 608 B

BIN
op_align_top.png View File

Before After
Width: 33  |  Height: 32  |  Size: 575 B

BIN
op_bake.png View File

Before After
Width: 32  |  Height: 32  |  Size: 16 KiB

+ 581
- 0
op_bake.py View File

@ -0,0 +1,581 @@
import bpy
import os
import bmesh
from mathutils import Vector
from collections import defaultdict
from math import pi
from random import random
from . import utilities_ui
from . import settings
from . import utilities_bake as ub #Use shorthand ub = utitlites_bake
# Notes: https://docs.blender.org/manual/en/dev/render/blender_render/bake.html
modes={
'normal_tangent': ub.BakeMode('', type='NORMAL', color=(0.5, 0.5, 1, 1), use_project=True),
'normal_object': ub.BakeMode('', type='NORMAL', color=(0.5, 0.5, 1, 1), normal_space='OBJECT' ),
'cavity': ub.BakeMode('bake_cavity', type='EMIT', setVColor=ub.setup_vertex_color_dirty),
'paint_base': ub.BakeMode('bake_paint_base', type='EMIT'),
'dust': ub.BakeMode('bake_dust', type='EMIT', setVColor=ub.setup_vertex_color_dirty),
'id_element': ub.BakeMode('bake_vertex_color',type='EMIT', setVColor=ub.setup_vertex_color_id_element),
'id_material': ub.BakeMode('bake_vertex_color',type='EMIT', setVColor=ub.setup_vertex_color_id_material),
'selection': ub.BakeMode('bake_vertex_color',type='EMIT', color=(0, 0, 0, 1), setVColor=ub.setup_vertex_color_selection),
'diffuse': ub.BakeMode('', type='DIFFUSE'),
# 'displacment': ub.BakeMode('', type='DISPLACEMENT', use_project=True, color=(0, 0, 0, 1), engine='CYCLES'),
'ao': ub.BakeMode('', type='AO', params=["bake_samples"], engine='CYCLES'),
'ao_legacy': ub.BakeMode('', type='AO', params=["bake_samples"], engine='CYCLES'),
'position': ub.BakeMode('bake_position', type='EMIT'),
'curvature': ub.BakeMode('', type='NORMAL', use_project=True, params=["bake_curvature_size"], composite="curvature"),
'wireframe': ub.BakeMode('bake_wireframe', type='EMIT', color=(0, 0, 0, 1), params=["bake_wireframe_size"])
}
if hasattr(bpy.types,"ShaderNodeBevel"):
# Has newer bevel shader (2.7 nightly build series)
modes['bevel_mask'] = ub.BakeMode('bake_bevel_mask', type='EMIT', color=(0, 0, 0, 1), params=["bake_bevel_samples","bake_bevel_size"])
modes['normal_tangent_bevel'] = ub.BakeMode('bake_bevel_normal', type='NORMAL', color=(0.5, 0.5, 1, 1), params=["bake_bevel_samples","bake_bevel_size"])
modes['normal_object_bevel'] = ub.BakeMode('bake_bevel_normal', type='NORMAL', color=(0.5, 0.5, 1, 1), normal_space='OBJECT', params=["bake_bevel_samples","bake_bevel_size"])
class op(bpy.types.Operator):
bl_idname = "uv.textools_bake"
bl_label = "Bake"
bl_description = "Bake selected objects"
@classmethod
def poll(cls, context):
if len(settings.sets) == 0:
return False
return True
def execute(self, context):
bake_mode = utilities_ui.get_bake_mode()
if bake_mode not in modes:
self.report({'ERROR_INVALID_INPUT'}, "Uknown mode '{}' only available: '{}'".format(bake_mode, ", ".join(modes.keys() )) )
return
# Store Selection
selected_objects = [obj for obj in bpy.context.selected_objects]
active_object = bpy.context.view_layer.objects.active
ub.store_bake_settings()
# Render sets
bake(
self = self,
mode = bake_mode,
size = bpy.context.scene.texToolsSettings.size,
bake_single = bpy.context.scene.texToolsSettings.bake_force_single,
sampling_scale = int(bpy.context.scene.texToolsSettings.bake_sampling),
samples = bpy.context.scene.texToolsSettings.bake_samples,
ray_distance = bpy.context.scene.texToolsSettings.bake_ray_distance
)
# Restore selection
ub.restore_bake_settings()
bpy.ops.object.select_all(action='DESELECT')
for obj in selected_objects:
obj.select_set( state = True, view_layer = None)
if active_object:
bpy.context.view_layer.objects.active = active_object
return {'FINISHED'}
def bake(self, mode, size, bake_single, sampling_scale, samples, ray_distance):
print("Bake '{}'".format(mode))
bpy.context.scene.render.engine = modes[mode].engine #Switch render engine
# Disable edit mode
if bpy.context.view_layer.objects.active != None and bpy.context.object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
ub.store_materials_clear()
# Get the baking sets / pairs
sets = settings.sets
render_width = sampling_scale * size[0]
render_height = sampling_scale * size[1]
for s in range(0,len(sets)):
set = sets[s]
# Get image name
name_texture = "{}_{}".format(set.name, mode)
if bake_single:
name_texture = "{}_{}".format(sets[0].name, mode)# In Single mode bake into same texture
path = bpy.path.abspath("//{}.tga".format(name_texture))
# Requires 1+ low poly objects
if len(set.objects_low) == 0:
self.report({'ERROR_INVALID_INPUT'}, "No low poly object as part of the '{}' set".format(set.name) )
return
# Check for UV maps
for obj in set.objects_low:
if not obj.data.uv_layers or len(obj.data.uv_layers) == 0:
self.report({'ERROR_INVALID_INPUT'}, "No UV map available for '{}'".format(obj.name))
return
# Check for cage inconsistencies
if len(set.objects_cage) > 0 and (len(set.objects_low) != len(set.objects_cage)):
self.report({'ERROR_INVALID_INPUT'}, "{}x cage objects do not match {}x low poly objects for '{}'".format(len(set.objects_cage), len(set.objects_low), obj.name))
return
# Get Materials
material_loaded = get_material(mode)
material_empty = None
if "TT_bake_node" in bpy.data.materials:
material_empty = bpy.data.materials["TT_bake_node"]
else:
material_empty = bpy.data.materials.new(name="TT_bake_node")
# Assign Materials to Objects
if (len(set.objects_high) + len(set.objects_float)) == 0:
# Low poly bake: Assign material to lowpoly
for obj in set.objects_low:
assign_vertex_color(mode, obj)
assign_material(mode, obj, material_loaded, material_empty)
else:
# High to low poly: Low poly require empty material to bake into image
for obj in set.objects_low:
assign_material(mode, obj, None, material_empty)
# Assign material to highpoly
for obj in (set.objects_high+set.objects_float):
assign_vertex_color(mode, obj)
assign_material(mode, obj, material_loaded)
# Setup Image
is_clear = (not bake_single) or (bake_single and s==0)
image = setup_image(mode, name_texture, render_width, render_height, path, is_clear)
# Assign bake node to Material
setup_image_bake_node(set.objects_low[0], image)
print("Bake '{}' = {}".format(set.name, path))
# Hide all cage objects i nrender
for obj_cage in set.objects_cage:
obj_cage.hide_render = True
# Bake each low poly object in this set
for i in range(len(set.objects_low)):
obj_low = set.objects_low[i]
obj_cage = None if i >= len(set.objects_cage) else set.objects_cage[i]
# Disable hide render
obj_low.hide_render = False
bpy.ops.object.select_all(action='DESELECT')
obj_low.select_set( state = True, view_layer = None)
bpy.context.view_layer.objects.active = obj_low
if modes[mode].engine == 'BLENDER_EEVEE':
# Assign image to texture faces
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
area.spaces[0].image = image
# bpy.data.screens['UV Editing'].areas[1].spaces[0].image = image
bpy.ops.object.mode_set(mode='OBJECT')
for obj_high in (set.objects_high):
obj_high.select_set( state = True, view_layer = None)
cycles_bake(
mode,
bpy.context.scene.texToolsSettings.padding,
sampling_scale,
samples,
ray_distance,
len(set.objects_high) > 0,
obj_cage
)
# Bake Floaters seperate bake
if len(set.objects_float) > 0:
bpy.ops.object.select_all(action='DESELECT')
for obj_high in (set.objects_float):
obj_high.select_set( state = True, view_layer = None)
obj_low.select_set( state = True, view_layer = None)
cycles_bake(
mode,
0,
sampling_scale,
samples,
ray_distance,
len(set.objects_float) > 0,
obj_cage
)
# Set background image (CYCLES & BLENDER_EEVEE)
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
area.spaces[0].image = image
# Restore renderable for cage objects
for obj_cage in set.objects_cage:
obj_cage.hide_render = False
# Downsample image?
if not bake_single or (bake_single and s == len(sets)-1):
# When baking single, only downsample on last bake
if render_width != size[0] or render_height != size[1]:
image.scale(size[0],size[1])
# Apply composite nodes on final image result
if modes[mode].composite:
apply_composite(image, modes[mode].composite, bpy.context.scene.texToolsSettings.bake_curvature_size)
# image.save()
# Restore non node materials
ub.restore_materials()
def apply_composite(image, scene_name, size):
previous_scene = bpy.context.window.scene
# Get Scene with compositing nodes
scene = None
if scene_name in bpy.data.scenes:
scene = bpy.data.scenes[scene_name]
else:
path = os.path.join(os.path.dirname(__file__), "resources/compositing.blend")+"\\Scene\\"
bpy.ops.wm.append(filename=scene_name, directory=path, link=False, autoselect=False)
scene = bpy.data.scenes[scene_name]
if scene:
# Switch scene
bpy.context.window.scene = scene
#Setup composite nodes for Curvature
if "Image" in scene.node_tree.nodes:
scene.node_tree.nodes["Image"].image = image
if "Offset" in scene.node_tree.nodes:
scene.node_tree.nodes["Offset"].outputs[0].default_value = size
print("Assign offset: {}".format(scene.node_tree.nodes["Offset"].outputs[0].default_value))
# Render image
bpy.ops.render.render(use_viewport=False)
# Get last images of viewer node and render result
image_viewer_node = get_last_item("Viewer Node", bpy.data.images)
image_render_result = get_last_item("Render Result", bpy.data.images)
#Copy pixels
image.pixels = image_viewer_node.pixels[:]
image.update()
if image_viewer_node:
bpy.data.images.remove(image_viewer_node)
if image_render_result:
bpy.data.images.remove(image_render_result)
#Restore scene & remove other scene
bpy.context.window.scene = previous_scene
# Delete compositing scene
bpy.data.scenes.remove(scene)
def get_last_item(key_name, collection):
# bpy.data.images
# Get last image of a series, e.g. .001, .002, 003
keys = []
for item in collection:
if key_name in item.name:
keys.append(item.name)
print("Search for {}x : '{}'".format(len(keys), ",".join(keys) ) )
if len(keys) > 0:
return collection[keys[-1]]
return None
def setup_image(mode, name, width, height, path, is_clear):
image = None
print("Path "+path)
if name in bpy.data.images:
image = bpy.data.images[name]
if image.source == 'FILE':
# Clear image if it was deleted outside
if not os.path.isfile(image.filepath):
image.user_clear()
bpy.data.images.remove(image)
# bpy.data.images[name].update()
# if bpy.data.images[name].has_data == False:
# Previous image does not have data, remove first
# print("Image pointer exists but no data "+name)
# image = bpy.data.images[name]
# image.update()
# image.generated_height = height
# bpy.data.images.remove(bpy.data.images[name])
if name not in bpy.data.images:
# Create new image with 32 bit float
is_float_32 = bpy.context.preferences.addons["textools"].preferences.bake_32bit_float == '32'
image = bpy.data.images.new(name, width=width, height=height, float_buffer=is_float_32)
if "_normal_" in image.name:
image.colorspace_settings.name = 'Non-Color'
else:
image.colorspace_settings.name = 'sRGB'
else:
# Reuse existing Image
image = bpy.data.images[name]
# Reisze?
if image.size[0] != width or image.size[1] != height or image.generated_width != width or image.generated_height != height:
image.generated_width = width
image.generated_height = height
image.scale(width, height)
# Fill with plain color
if is_clear:
image.generated_color = modes[mode].color
image.generated_type = 'BLANK'
image.file_format = 'TARGA'
# TODO: Verify that the path exists
# image.filepath_raw = path
return image
def setup_image_bake_node(obj, image):
if len(obj.data.materials) <= 0:
print("ERROR, need spare material to setup active image texture to bake!!!")
else:
for slot in obj.material_slots:
if slot.material:
if(slot.material.use_nodes == False):
slot.material.use_nodes = True
# Assign bake node
tree = slot.material.node_tree
node = None
if "bake" in tree.nodes:
node = tree.nodes["bake"]
else:
node = tree.nodes.new("ShaderNodeTexImage")
node.name = "bake"
node.select = True
node.image = image
tree.nodes.active = node
def assign_vertex_color(mode, obj):
if modes[mode].setVColor:
modes[mode].setVColor(obj)
def assign_material(mode, obj, material_bake=None, material_empty=None):
ub.store_materials(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set( state = True, view_layer = None)
# Select All faces
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(bpy.context.active_object.data);
faces = [face for face in bm.faces if face.select]
bpy.ops.mesh.select_all(action='SELECT')
if material_bake:
# Setup properties of bake materials
if mode == 'wireframe':
if "Value" in material_bake.node_tree.nodes:
material_bake.node_tree.nodes["Value"].outputs[0].default_value = bpy.context.scene.texToolsSettings.bake_wireframe_size
if mode == 'bevel_mask':
if "Bevel" in material_bake.node_tree.nodes:
material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size
material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples
if mode == 'normal_tangent_bevel':
if "Bevel" in material_bake.node_tree.nodes:
material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size
material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples
if mode == 'normal_object_bevel':
if "Bevel" in material_bake.node_tree.nodes:
material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size
material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples
# Don't apply in diffuse mode
if mode != 'diffuse':
if material_bake:
# Override with material_bake
if len(obj.material_slots) == 0:
obj.data.materials.append(material_bake)
else:
obj.material_slots[0].material = material_bake
obj.active_material_index = 0
bpy.ops.object.material_slot_assign()
elif material_empty:
#Assign material_empty if no material available
if len(obj.material_slots) == 0:
obj.data.materials.append(material_empty)
else: # not obj.material_slots[0].material:
obj.material_slots[0].material = material_empty
obj.active_material_index = 0
bpy.ops.object.material_slot_assign()
# Restore Face selection
bpy.ops.mesh.select_all(action='DESELECT')
for face in faces:
face.select = True
bpy.ops.object.mode_set(mode='OBJECT')
def get_material(mode):
if modes[mode].material == "":
return None # No material setup requires
# Find or load material
name = modes[mode].material
path = os.path.join(os.path.dirname(__file__), "resources/materials.blend")+"\\Material\\"
if "bevel" in mode:
path = os.path.join(os.path.dirname(__file__), "resources/materials_2.80.blend")+"\\Material\\"
print("Get mat {}\n{}".format(mode, path))
if bpy.data.materials.get(name) is None:
print("Material not yet loaded: "+mode)
bpy.ops.wm.append(filename=name, directory=path, link=False, autoselect=False)
return bpy.data.materials.get(name)
def cycles_bake(mode, padding, sampling_scale, samples, ray_distance, is_multi, obj_cage):
# if modes[mode].engine == 'BLENDER_EEVEE':
# # Snippet: https://gist.github.com/AndrewRayCode/760c4634a77551827de41ed67585064b
# bpy.context.scene.render.bake_margin = padding
# # AO Settings
# bpy.context.scene.render.bake_type = modes[mode].type
# bpy.context.scene.render.use_bake_normalize = True
# if modes[mode].type == 'AO':
# bpy.context.scene.world.light_settings.use_ambient_occlusion = True
# bpy.context.scene.world.light_settings.gather_method = 'RAYTRACE'
# bpy.context.scene.world.light_settings.samples = samples
# bpy.context.scene.render.use_bake_selected_to_active = is_multi
# bpy.context.scene.render.bake_distance = ray_distance
# bpy.context.scene.render.use_bake_clear = False
# bpy.ops.object.bake_image()
if modes[mode].engine == 'CYCLES' or modes[mode].engine == 'BLENDER_EEVEE' :
if modes[mode].normal_space == 'OBJECT':
#See: https://twitter.com/Linko_3D/status/963066705584054272
bpy.context.scene.render.bake.normal_r = 'POS_X'
bpy.context.scene.render.bake.normal_g = 'POS_Z'
bpy.context.scene.render.bake.normal_b = 'NEG_Y'
elif modes[mode].normal_space == 'TANGENT':
bpy.context.scene.render.bake.normal_r = 'POS_X'
bpy.context.scene.render.bake.normal_b = 'POS_Z'
# Adjust Y swizzle from Addon preferences
swizzle_y = bpy.context.preferences.addons["textools"].preferences.swizzle_y_coordinate
if swizzle_y == 'Y-':
bpy.context.scene.render.bake.normal_g = 'NEG_Y'
elif swizzle_y == 'Y+':
bpy.context.scene.render.bake.normal_g = 'POS_Y'
# Set samples
bpy.context.scene.cycles.samples = samples
# Speed up samples for simple render modes
if modes[mode].type == 'EMIT' or modes[mode].type == 'DIFFUSE':
bpy.context.scene.cycles.samples = 1
# Pixel Padding
bpy.context.scene.render.bake.margin = padding * sampling_scale
# Disable Direct and Indirect for all 'DIFFUSE' bake types
if modes[mode].type == 'DIFFUSE':
bpy.context.scene.render.bake.use_pass_direct = False
bpy.context.scene.render.bake.use_pass_indirect = False
bpy.context.scene.render.bake.use_pass_color = True
if obj_cage is None:
# Bake with Cage
bpy.ops.object.bake(
type=modes[mode].type,
use_clear=False,
cage_extrusion=ray_distance,
use_selected_to_active=is_multi,
normal_space=modes[mode].normal_space
)
else:
# Bake without Cage
bpy.ops.object.bake(
type=modes[mode].type,
use_clear=False,
cage_extrusion=ray_distance,
use_selected_to_active=is_multi,
normal_space=modes[mode].normal_space,
#Use Cage and assign object
use_cage=True,
cage_object=obj_cage.name
)
bpy.utils.register_class(op)

BIN
op_bake_explode.png View File

Before After
Width: 32  |  Height: 32  |  Size: 16 KiB

+ 221
- 0
op_bake_explode.py View File

@ -0,0 +1,221 @@
import bpy
import bmesh
import operator
from mathutils import Vector
from collections import defaultdict
from math import pi
from . import settings
frame_range = 50
class op(bpy.types.Operator):
bl_idname = "uv.textools_bake_explode"
bl_label = "Explode"
bl_description = "Explode selected bake pairs with animation keyframes"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if len(settings.sets) <= 1:
return False
return True
def execute(self, context):
explode(self)
return {'FINISHED'}
def explode(self):
sets = settings.sets
set_bounds = {}
set_volume = {}
avg_side = 0
for set in sets:
set_bounds[set] = get_bbox_set(set)
set_volume[set] = set_bounds[set]['size'].x * set_bounds[set]['size'].y * set_bounds[set]['size'].z
avg_side+=set_bounds[set]['size'].x
avg_side+=set_bounds[set]['size'].y
avg_side+=set_bounds[set]['size'].z
avg_side/=(len(sets)*3)
sorted_set_volume = sorted(set_volume.items(), key=operator.itemgetter(1))
sorted_sets = [item[0] for item in sorted_set_volume]
sorted_sets.reverse()
# All combined bounding boxes
bbox_all = merge_bounds(list(set_bounds.values()))
bbox_max = set_bounds[ sorted_sets[0] ] # max_bbox(list(set_bounds.values()))
# Offset sets into their direction
dir_offset_last_bbox = {}
for i in range(0,6):
dir_offset_last_bbox[i] = bbox_max #bbox_all
bpy.context.scene.frame_start = 0
bpy.context.scene.frame_end = frame_range
bpy.context.scene.frame_current = 0
# Process each set
for set in sorted_sets:
if set_bounds[set] != bbox_max:
delta = set_bounds[set]['center'] - bbox_all['center']
offset_set(set, delta, avg_side*0.35, dir_offset_last_bbox )
def offset_set(set, delta, margin, dir_offset_last_bbox):
objects = set.objects_low + set.objects_high + set.objects_cage
# print("\nSet '{}' with {}x".format(set.name, len(objects) ))
# Which Direction?
delta_max = max(abs(delta.x), abs(delta.y), abs(delta.z))
direction = [0,0,0]
if delta_max > 0:
for i in range(0,3):
if abs(delta[i]) == delta_max:
direction[i] = delta[i]/abs(delta[i])
else:
direction[i] = 0
else:
# Default when not delta offset was measure move up
direction = [0,0,1]
delta = Vector((direction[0], direction[1], direction[2]))
# Get Key
key = get_delta_key(delta)
# Calculate Offset
bbox = get_bbox_set(set)
bbox_last = dir_offset_last_bbox[key]
offset = Vector((0,0,0))
if delta.x == 1:
offset = delta * ( bbox_last['max'].x - bbox['min'].x )
elif delta.x == -1:
offset = delta * -( bbox_last['min'].x - bbox['max'].x )
elif delta.y == 1:
offset = delta * ( bbox_last['max'].y - bbox['min'].y )
elif delta.y == -1:
offset = delta * -( bbox_last['min'].y - bbox['max'].y )
elif delta.z == 1:
offset = delta * ( bbox_last['max'].z - bbox['min'].z )
elif delta.z == -1:
offset = delta * -( bbox_last['min'].z - bbox['max'].z )
# Add margin
offset+= delta * margin
# Offset items
# https://blenderartists.org/forum/showthread.php?237761-Blender-2-6-Set-keyframes-using-Python-script
# http://blenderscripting.blogspot.com.au/2011/05/inspired-by-post-on-ba-it-just-so.html
# Set key A
bpy.context.scene.frame_current = 0
for obj in objects:
obj.keyframe_insert(data_path="location")
for obj in objects:
obj.location += offset
bpy.context.view_layer.update()
# Set key B
bpy.context.scene.frame_current = frame_range
for obj in objects:
obj.keyframe_insert(data_path="location")
# Update last bbox in direction
dir_offset_last_bbox[key] = get_bbox_set(set)
def get_delta_key(delta):
# print("Get key {} is: {}".format(delta, delta.y == -1 ))
if delta.x == -1:
return 0
elif delta.x == 1:
return 1
if delta.y == -1:
return 2
elif delta.y == 1:
return 3
if delta.z == -1:
return 4
elif delta.z == 1:
return 5
def merge_bounds(bounds):
box_min = bounds[0]['min'].copy()
box_max = bounds[0]['max'].copy()
for bbox in bounds:
# box_min.x = -8
box_min.x = min(box_min.x, bbox['min'].x)
box_min.y = min(box_min.y, bbox['min'].y)
box_min.z = min(box_min.z, bbox['min'].z)
box_max.x = max(box_max.x, bbox['max'].x)
box_max.y = max(box_max.y, bbox['max'].y)
box_max.z = max(box_max.z, bbox['max'].z)
return {
'min':box_min,
'max':box_max,
'size':(box_max-box_min),
'center':box_min+(box_max-box_min)/2
}
def get_bbox_set(set):
objects = set.objects_low + set.objects_high + set.objects_cage
bounds = []
for obj in objects:
bounds.append( get_bbox(obj) )
return merge_bounds(bounds)
def get_bbox(obj):
corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
# Get world space Min / Max
box_min = Vector((corners[0].x, corners[0].y, corners[0].z))
box_max = Vector((corners[0].x, corners[0].y, corners[0].z))
for corner in corners:
# box_min.x = -8
box_min.x = min(box_min.x, corner.x)
box_min.y = min(box_min.y, corner.y)
box_min.z = min(box_min.z, corner.z)
box_max.x = max(box_max.x, corner.x)
box_max.y = max(box_max.y, corner.y)
box_max.z = max(box_max.z, corner.z)
return {
'min':box_min,
'max':box_max,
'size':(box_max-box_min),
'center':box_min+(box_max-box_min)/2
}
bpy.utils.register_class(op)

+ 179
- 0
op_bake_organize_names.py View File

@ -0,0 +1,179 @@
import bpy
import bmesh
import operator
import math
from mathutils import Vector
from collections import defaultdict
from math import pi
from . import utilities_bake
class op(bpy.types.Operator):
bl_idname = "uv.textools_bake_organize_names"
bl_label = "Match Names"
bl_description = "Match high poly object names to low poly objects by their bounding boxes."
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
# Require 2 or more objects to sort
if len(bpy.context.selected_objects) <= 1:
return False
return True
def execute(self, context):
sort_objects(self)
return {'FINISHED'}
def sort_objects(self):
# Collect objects
objects = []
bounds = {}
for obj in bpy.context.selected_objects:
if obj.type == 'MESH':
objects.append(obj)
bounds[obj] = get_bbox(obj)
print("Objects {}x".format(len(objects)))
# Get smallest side of any bounding box
min_side = min(bounds[objects[0]]['size'].x, bounds[objects[0]]['size'].y, bounds[objects[0]]['size'].z)
avg_side = 0
for obj in bounds:
min_side = min(min_side, bounds[obj]['size'].x, bounds[obj]['size'].y, bounds[obj]['size'].z)
avg_side+=bounds[obj]['size'].x
avg_side+=bounds[obj]['size'].y
avg_side+=bounds[obj]['size'].z
avg_side/=(len(bounds)*3)
# Get all Low and high poly objects
objects_low = [obj for obj in objects if utilities_bake.get_object_type(obj)=='low']
objects_high = [obj for obj in objects if utilities_bake.get_object_type(obj)=='high']
if len(objects_low) == 0:
self.report({'ERROR_INVALID_INPUT'}, "There are no low poly objects selected")
return
elif len(objects_high) == 0:
self.report({'ERROR_INVALID_INPUT'}, "There are no high poly objects selected")
return
print("Low {}x, High {}x".format(len(objects_low),len(objects_high)))
pairs_low_high = {}
objects_left_high = objects_high.copy()
for obj_A in objects_low:
matches = {}
for obj_B in objects_left_high:
score = get_score(obj_A, obj_B)
p = score / avg_side
if p > 0 and p <= 0.65:
matches[obj_B] = p
else:
print("Not matched: {} ".format(p))
if(len(matches) > 0):
sorted_matches = sorted(matches.items(), key=operator.itemgetter(1))
for i in range(0, len(sorted_matches)):
A = obj_A
B = sorted_matches[i][0]
p = sorted_matches[i][1]
print("Check: {}% '{}' = '{}' ".format(int(p * 100.0), A.name, B.name ))
# Remove from list
objects_left_high.remove(sorted_matches[0][0])
pairs_low_high[obj_A] = sorted_matches[0][0]
print("")
# objects_unsorted = [obj for obj in objects if (obj not in pairs_low_high.values() and obj not in pairs_low_high.keys() )]
bpy.ops.object.select_all(action='DESELECT')
for obj_A in pairs_low_high:
obj_B = pairs_low_high[obj_A]
try:
obj_B.name = utilities_bake.get_bake_name(obj_A)+" high"
obj_A.select = True
obj_B.select = True
except:
print("Fallo")
print("Matched {}x".format(len(pairs_low_high)))
def get_score(A, B):
bbox_A = get_bbox(A)
bbox_B = get_bbox(B)
# Not colliding
if not is_colliding(bbox_A, bbox_B):
return -1.0
# Position
delta_pos = (bbox_B['center'] - bbox_A['center']).length
# Volume
volume_A = bbox_A['size'].x * bbox_A['size'].y * bbox_A['size'].z
volume_B = bbox_B['size'].x * bbox_B['size'].y * bbox_B['size'].z
delta_vol = (max(volume_A, volume_B) - min(volume_A, volume_B))/3.0
# Longest side
side_A_max = max(bbox_A['size'].x, bbox_A['size'].y, bbox_A['size'].z )
side_B_max = max(bbox_B['size'].x, bbox_B['size'].y, bbox_B['size'].z )
delta_size_max = abs(side_A_max - side_B_max)
return delta_pos + delta_vol + delta_size_max
def get_bbox(obj):
corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
# Get world space Min / Max
box_min = Vector((corners[0].x, corners[0].y, corners[0].z))
box_max = Vector((corners[0].x, corners[0].y, corners[0].z))
for corner in corners:
# box_min.x = -8
box_min.x = min(box_min.x, corner.x)
box_min.y = min(box_min.y, corner.y)
box_min.z = min(box_min.z, corner.z)
box_max.x = max(box_max.x, corner.x)
box_max.y = max(box_max.y, corner.y)
box_max.z = max(box_max.z, corner.z)
return {
'min':box_min,
'max':box_max,
'size':(box_max-box_min),
'center':box_min+(box_max-box_min)/2
}
def is_colliding(bbox_A, bbox_B):
def is_collide_1D(A_min, A_max, B_min, B_max):
# One line is inside the other
length_A = A_max-A_min
length_B = B_max-B_min
center_A = A_min + length_A/2
center_B = B_min + length_B/2
return abs(center_A - center_B) <= (length_A+length_B)/2
collide_x = is_collide_1D(bbox_A['min'].x, bbox_A['max'].x, bbox_B['min'].x, bbox_B['max'].x)
collide_y = is_collide_1D(bbox_A['min'].y, bbox_A['max'].y, bbox_B['min'].y, bbox_B['max'].y)
collide_z = is_collide_1D(bbox_A['min'].z, bbox_A['max'].z, bbox_B['min'].z, bbox_B['max'].z)
return collide_x and collide_y and collide_z
bpy.utils.register_class(op)

+ 98
- 0
op_color_assign.py View File

@ -0,0 +1,98 @@
import bpy
import bmesh
import operator
from mathutils import Vector
from collections import defaultdict
from math import pi
from . import utilities_color
class op(bpy.types.Operator):
bl_idname = "uv.textools_color_assign"
bl_label = "Assign Color"
bl_description = "Assign color to selected objects or faces in edit mode."
bl_options = {'REGISTER', 'UNDO'}
index : bpy.props.IntProperty(description="Color Index", default=0)
@classmethod
def poll(cls, context):
if not bpy.context.active_object:
return False
if bpy.context.active_object not in bpy.context.selected_objects:
return False
if bpy.context.active_object.type != 'MESH':
return False
#Only in UV editor mode
if bpy.context.area.type != 'IMAGE_EDITOR':
return False
return True
def execute(self, context):
assign_color(self, context, self.index)
return {'FINISHED'}
def assign_color(self, context, index):
selected_obj = bpy.context.selected_objects.copy()
previous_mode = 'OBJECT'
if len(selected_obj) == 1:
previous_mode = bpy.context.active_object.mode
for obj in selected_obj:
# Select object
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
obj.select_set( state = True, view_layer = None)
bpy.context.view_layer.objects.active = obj
# Enter Edit mode
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data);
faces = []
#Assign to all or just selected faces?
if previous_mode == 'EDIT':
faces = [face for face in bm.faces if face.select]
else:
faces = [face for face in bm.faces]
if previous_mode == 'OBJECT':
bpy.ops.mesh.select_all(action='SELECT')
# Verify material slots
for i in range(index+1):
if index >= len(obj.material_slots):
bpy.ops.object.material_slot_add()
utilities_color.assign_slot(obj, index)
# Assign to selection
obj.active_material_index = index
bpy.ops.object.material_slot_assign()
#Change View mode to MATERIAL
# for area in bpy.context.screen.areas:
# if area.type == 'VIEW_3D':
# for space in area.spaces:
# if space.type == 'VIEW_3D':
# space.shading.type = 'MATERIAL'
# restore mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for obj in selected_obj:
obj.select_set( state = True, view_layer = None)
bpy.ops.object.mode_set(mode=previous_mode)
bpy.utils.register_class(op)

+ 96
- 0
op_color_clear.py View File

@ -0,0 +1,96 @@
import bpy
import bmesh
import operator
from mathutils import Vector
from collections import defaultdict
from math import pi
from . import utilities_color
class op(bpy.types.Operator):
bl_idname = "uv.textools_color_clear"
bl_label = "Clear Colors"
bl_description = "Clears the Color IDs and materials on the selected model"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if not bpy.context.active_object:
return False
if bpy.context.active_object not in bpy.context.selected_objects:
return False
if bpy.context.active_object.type != 'MESH':
return False
#Only in UV editor mode
if bpy.context.area.type != 'IMAGE_EDITOR':
return False
return True
def execute(self, context):
clear_colors(self, context)
return {'FINISHED'}
def clear_colors(self, context):
obj = bpy.context.active_object
# Store previous mode
previous_mode = bpy.context.active_object.mode
if bpy.context.active_object.mode != 'EDIT':
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(bpy.context.active_object.data);
# Set all faces
for face in bm.faces:
face.material_index = 0
# Clear material slots
bpy.ops.object.mode_set(mode='OBJECT')
count = len(obj.material_slots)
for i in range(count):
bpy.ops.object.material_slot_remove()
# Delete materials if not used
for material in bpy.data.materials:
if utilities_color.material_prefix in material.name:
if material.users == 0:
material.user_clear()
bpy.data.materials.remove(material)
# Restore previous mode
bpy.ops.object.mode_set(mode=previous_mode)
for area in bpy.context.screen.areas:
print("area: {}".format(area.type))
if area.type == 'PROPERTIES':
for space in area.spaces:
if space.type == 'PROPERTIES':
# space.shading.type = 'MATERIAL'
space.context = 'MATERIAL'
# Show Material Tab
for area in bpy.context.screen.areas:
if area.type == 'PROPERTIES':
for space in area.spaces:
if space.type == 'PROPERTIES':
space.context = 'MATERIAL'
# Switch Solid shading
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'SOLID'
bpy.utils.register_class(op)

BIN
op_color_convert_texture.png View File

Before After
Width: 32  |  Height: 32  |  Size: 15 KiB

+ 153
- 0
op_color_convert_texture.py View File

@ -0,0 +1,153 @@
import bpy
import bmesh
import operator
import math
from mathutils import Vector
from collections import defaultdict
from . import utilities_color
from . import utilities_bake
from . import utilities_ui
material_prefix = "TT_atlas_"
gamma = 2.2
class op(bpy.types.Operator):
bl_idname = "uv.textools_color_convert_to_texture"
bl_label = "Pack Texture"
bl_description = "Pack ID Colors into single texture and UVs"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if not bpy.context.active_object:
return False
if bpy.context.active_object not in bpy.context.selected_objects:
return False
if len(bpy.context.selected_objects) != 1:
return False
if bpy.context.active_object.type != 'MESH':
return False
#Only in UV editor mode
if bpy.context.area.type != 'IMAGE_EDITOR':
return False
return True
def execute(self, context):
pack_texture(self, context)
return {'FINISHED'}
def pack_texture(self, context):
obj = bpy.context.active_object
name = material_prefix+obj.name
if obj.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Determine size
size_pixel = 8
size_square = math.ceil(math.sqrt( context.scene.texToolsSettings.color_ID_count ))
size_image = size_square * size_pixel
size_image_pow = int(math.pow(2, math.ceil(math.log(size_image, 2))))
# Maximize pixel size
size_pixel = math.floor(size_image_pow/size_square)
print("{0} colors = {1} x {1} = ({2}pix) {3} x {3} | {4} x {4}".format(
context.scene.texToolsSettings.color_ID_count,
size_square,
size_pixel,
size_image,
size_image_pow
))
# Create image
image = bpy.data.images.new(name, width=size_image_pow, height=size_image_pow)
pixels = [None] * size_image_pow * size_image_pow
# Black pixels
for x in range(size_image_pow):
for y in range(size_image_pow):
pixels[(y * size_image_pow) + x] = [0, 0, 0, 1]
# Pixels
for c in range(context.scene.texToolsSettings.color_ID_count):
x = c % size_square
y = math.floor(c/size_square)
color = utilities_color.get_color(c).copy()
for i in range(3):
color[i] = pow(color[i] , 1.0/gamma)
for sx in range(size_pixel):
for sy in range(size_pixel):
_x = x*size_pixel + sx
_y = y*size_pixel + sy
pixels[(_y * size_image_pow) + _x] = [color[0], color[1], color[2], 1]
# flatten list & assign pixels
pixels = [chan for px in pixels for chan in px]
image.pixels = pixels
# Set background image
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
area.spaces[0].image = image
# Edit mesh
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
bpy.ops.mesh.select_all(action='SELECT')
# bpy.ops.uv.smart_project(angle_limit=1)
bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.0078)
bm = bmesh.from_edit_mesh(bpy.context.active_object.data)
uv_layers = bm.loops.layers.uv.verify();
for face in bm.faces:
index = face.material_index
# Get UV coordinates for index
x = index%size_square
y = math.floor(index/size_square)
x*= (size_pixel / size_image_pow)
y*= (size_pixel / size_image_pow)
x+= size_pixel/size_image_pow/2
y+= size_pixel/size_image_pow/2
for loop in face.loops:
loop[uv_layers].uv = (x, y)
# Remove Slots & add one
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.uv.textools_color_clear()
bpy.ops.object.material_slot_add()
#Create material with image
obj.material_slots[0].material = utilities_bake.get_image_material(image)
#Display UVs
bpy.ops.object.mode_set(mode='EDIT')
# Switch textured shading
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="Packed texture with {} color IDs".format( context.scene.texToolsSettings.color_ID_count ))
bpy.utils.register_class(op)

BIN
op_color_convert_vertex_colors.png View File

Before After
Width: 32  |  Height: 32  |  Size: 15 KiB

+ 90
- 0
op_color_convert_vertex_colors.py View File

@ -0,0 +1,90 @@
import bpy
import bmesh
import operator
import math
from mathutils import Vector
from collections import defaultdict
from . import utilities_color
from . import utilities_bake
from . import utilities_ui
gamma = 2.2
class op(bpy.types.Operator):
bl_idname = "uv.textools_color_convert_to_vertex_colors"
bl_label = "Pack Texture"
bl_description = "Pack ID Colors into single texture and UVs"