Compare commits

..

18 commits

Author SHA1 Message Date
Michel Fedde
e4b2858000 Merge remote-tracking branch 'origin/master' 2024-02-02 20:51:59 +01:00
Michel Fedde
7073ad2ac2 fixes imports 2024-02-02 20:51:48 +01:00
Michel Fedde
561f9b9241
Update features.md 2024-02-02 10:36:55 +01:00
Michel Fedde
09ceff62f3 adds documentation 2024-02-02 10:32:47 +01:00
Michel Fedde
df765f020a release: 1.3.0 2024-02-01 14:48:56 +01:00
Michel Fedde
816b5e51cb adds apply and revert color feature to editor 2024-02-01 14:47:42 +01:00
Michel Fedde
7b86199ee5 adds find used ids feature to editor 2024-02-01 12:42:47 +01:00
Michel Fedde
f763479a64 adds select feature to editor 2024-02-01 12:08:11 +01:00
Michel Fedde
40a7e4b8ba adds id editor 2024-02-01 00:48:12 +01:00
Michel Fedde
28ab2072e5 fixes crash when baking without enough colors 2024-01-31 19:37:55 +01:00
Michel Fedde
c16e4c268b adds palette support 2024-01-31 19:23:55 +01:00
Michel Fedde
feb1887a7d
Merge pull request #3 from Neintonine/develop
Develop
2024-01-30 21:24:41 +01:00
Michel Fedde
730a39388f
Merge branch 'master' into develop 2024-01-30 21:24:35 +01:00
Michel Fedde
6fbf07b96c adds color source 2024-01-30 21:22:33 +01:00
Michel Fedde
c0ab4bb7ad refactored type init 2024-01-30 20:28:36 +01:00
Michel Fedde
060612d789 adds option for different selection modes 2024-01-30 20:19:58 +01:00
Michel Fedde
53366cc71d adds option to color attribute to override the attribute 2024-01-30 17:20:59 +01:00
Michel Fedde
9765e28786 adds object as source 2024-01-30 17:00:03 +01:00
47 changed files with 1235 additions and 254 deletions

View file

@ -1,13 +1,14 @@
# Blender - Bake ID Mask Plugin # Blender - ID Mask Tools
A plugin to quickly bake ID masks for meshes from/to different sources and targets. A set of tools to quickly create ID masks for meshes.
### Currently supported: ### What is this?
Sources: For usage in specific programs like Substance Painter, its useful to create a mask for specific parts for your mesh, so you can easily make out parts of the mesh, to f.E. apply a mterial.
- Material Index You can specify colors in color attributes that define "IDs" for specific parts of the mesh, which you can then use in different programs.
Targets: Currently this workflow requires you to assign them tediously by using a mixture of Edit Mode and Vertex Paint to paint the mesh correctly.
- Vertex Color
This plugin tries to fix that, by providing proper tools for it. The most important is probably the [ID Mask Editor](docu/features/id_mask_editor.md), that allows you to quickly add IDs to faces all within the Edit Mode.
## Installation ## Installation
1. Download the newest file from the releases tab. 1. Download the newest file from the releases tab.
@ -16,12 +17,4 @@ Targets:
4. After selecting it, just enable it and done. 4. After selecting it, just enable it and done.
## Usage ## Usage
You find the tool in the panel on the right of the "3D Viewport" under "Tool". Check out the [Features](docu/features.md) page to find more specific information about the usage of each feature.
![Panel Location](docu/images/panel.png)
How to use properly it, highly depends on the selected source.
### Usage - Material Index Source
Here the IDs are given based on the object and its material indices.
Lets say, you have 10 objects and each has 2 materials attached to it. Then the amount of IDs given is 20, since each object contains the two material indecies.

View file

@ -1 +1 @@
1.1.0 1.3.1+build.2

View file

@ -5,4 +5,3 @@ if __name__ == "__main__":
currentVersion = versioning.get_version() currentVersion = versioning.get_version()
nextVersion = currentVersion.bump_minor() nextVersion = currentVersion.bump_minor()
versioning.save_version(nextVersion) versioning.save_version(nextVersion)

3
docu/features.md Normal file
View file

@ -0,0 +1,3 @@
# Features
- [ID Mask Editor](features/id_mask_editor.md)
- [Bake ID Mask](features/bake_id_mask.md)

View file

@ -0,0 +1,52 @@
# Bake ID Mask
This tool allows you to quickly create a ID mask based on different sources
The tool is found in the Tool category in the Side Panel of the 3D View.
![Panel Location](../images/bake_id_mask.panel.png)
## The UI
![UI](../images/bake_id_mask.ui.png)
1. The Bake button. Click on it to bake, after you have setup your selection
2. Selection Mode. See "Parameters"
3. This panel gives you infos about how many ID you gonna bake and how many colors you have in your color source
4. The source specifies, what kind of dataset the system will take to calculate its IDs
5. This box allows the source to have parameters, you can change
6. The target specifies, where the ID get drawn to. Currently only color attributes are supported
7. as with the source, the target can have parameters, as well.
8. This allows you to change the color source, from where the system takes the colors.
## Parameters
### Selection Mode
The "Selection Mode" has 3 different options:
1. Only active element - The bake will only apply to the actively selected object. If no active object was found, nothing will happens.
2. Use Selection, separate - The bake will apply to all selected objects, but each object will be processed individually by the source.
Example: With Material Index, the ID will always correspond with the Material Index, ignoring what ever happens with the other objects.
3. Use Selection, combined - The bake will apply to all selected object and will be processed together by the source.
Example: With Material Index, it will count up every Material Index it finds.
### Sources
#### Material Index
This source will give away IDs based on the material index, a face uses.
This can be useful, when you use multiple materials in one mesh and want to export it with only one material and without loosing the info, which face had different materials.
The "Remove all source materials" option will, if checked, remove all material indecies from the mesh, allowing you to directly export it to your application.
#### Object ID
This source will only work properly in "Use Selection, combined" - Selection Mode. The source works by giving every selected Object an incrementing ID.
### Targets
#### Color Attribute / Vertex Color
This target will save the ID-color onto the color attribute specified.
By default, it will remove the old color attribute completely and replace it, if one already exists with the name. This can be prevent by unchecking "Override Color Attribute".
The ID-color will then just painted on top of the existing attribute, overriding existing colors, if there were any.
### Color Sources
#### Generated
This color source will generate a list of colors to use, which get increasingly less saturated and bright as further, you approach the limit.
#### Color Palette
This allows you to specify an own palette, previously created in Blender.
Due to the way palettes work in Blender, you can **not** create or edit the palette here. You can create one in one of the "Paint" modes, like Vertex Paint.

View file

@ -0,0 +1,29 @@
# ID Mask Editor
The "ID Mask Editor" allows you to quickly add IDs to faces in a mesh.
## Getting started
The editor is found in the Tool category in the Side Panel of the 3D View. The editor is only visible when you are in Edit Mode.
![Panel Location](../images/id_mask_editor.panel.png)
By default, its pretty empty. That is because you need to specify a color attribute, you want to paint your ID-mask to.
You can either select an already existing color attribute or you create a new one using the "Plus"-button. That will automatically create color attribute called "ID_MASK" and set it as ID-mask.
With the attribute selected, you will see a few more options.
## The UI
![Panel Options](../images/id_mask_editor.options_0.png)
1. The color source. That specifies how the initial colors are generated, when adding new IDs using the "Plus"-Button
2. This is the ID-list. It works similarly to the Vertex Groups-list. The only big difference is how the items works. You can change the color and name of an ID. Change the color will cause the two buttons to be enabled. They allow you to either "Revert" the change you made or to "Apply" it, which will change the ID-mask to reflect the change.
3. Clicking the "Paint"-button will paint the currently active ID (indicated by the blue background in the list) onto the currently selected **faces**.
4. Clicking the "Select"-button will select every face currently using the currently active ID (indicated by the blue background in the list)
5. Changing the color on a ID will not automatically apply it onto the attribute. This will allow you to apply/revert all those changes, with one click.
### Additional Features
Using the arrow next to the list allows you to find additional features:
- Find used colors: Using this feature will automatically detect colors found in the attribute, but not found in the ID list, and add them.
Very useful when dealing with a mesh created without this tool or if you accidentally removed an ID from the list.
## One additional thing
While not directly related to the ID-Mask, but still kinda, there is a new selection found under the "Select" menu.
This will allow you to quickly select faces, with the same ID, then the face you have currently active.
![Select by ID Mask](../images/id_mask_editor.select_by_id_mask.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

6
docu/planned_features.md Normal file
View file

@ -0,0 +1,6 @@
## Planned features
### ID Mask - Editor
- [X] A way to automatically get already used colors, f.E. you already did it by hand
- [X] Select by color
- [ ] Update after color change

View file

@ -1,5 +1,28 @@
import bpy
from . menu import id_mask_select_menu
from . menu.id_mask_editor_options import IDEDITOR_IDMaskEditorOptionsMenu
from . operators.create_id_mask import CreateIDMaskOperator
from . operators.id_editor_apply_color import IDEDITOR_ColorApplyOperator
from . operators.id_editor_create_id import IDEDITOR_CreateIDOperator
from . operators.id_editor_find_used_ids import IDEDITOR_FindUsedIDsOperator
from . operators.id_editor_paint import IDEDITOR_PaintIDMaskOperator
from . operators.id_editor_remove_id import IDEDITOR_RemoveIDOperator
from . operators.id_editor_revert_color import IDEDITOR_ColorResetOperator
from . operators.id_mask_select import IDEDITOR_SelectIDMaskOperator
from . panels.id_mask_editor_id_list import IDMaskEditorIDList
from . properties.id_mask_editor_value_properties import IDMaskEditorValueProperties
from . panels.bake_panel_options import BakeToIDOptionsPanel
from . operators.bake_to_id_map import BakeToIDMapOperator
from . panels.bake_panel import BakeToIDMapPanel
from . panels.bake_panel_info import BakeToIDInfoPanel
from . panels.id_mask_editor import IDMaskEditorPanel
from . properties.bake_properties import BakeToIDProperties
from . properties.id_mask_editor_properties import IDMaskEditorProperties
bl_info = { bl_info = {
"name": "Bake ID Mask", "name": "ID Mask - Tools",
"author": "iedSoftworks", "author": "iedSoftworks",
"description": "", "description": "",
# !VERSION # !VERSION
@ -7,36 +30,50 @@ bl_info = {
"category": "Object" "category": "Object"
} }
import bpy
from . panels.panel_options import BakeToIDOptionsPanel
from . panels.panel_advanced import BakeToIDAdvancedMenu
from . operators.bake_to_id_map import BakeToIDMapOperator
from . panels.panel import BakeToIDMapPanel
from . panels.panel_info import BakeToIDInfoPanel
from . properties.bake_to_id import BakeToIDProperties
classes = ( classes = (
BakeToIDMapOperator, BakeToIDMapOperator,
BakeToIDMapPanel, BakeToIDMapPanel,
BakeToIDInfoPanel, BakeToIDInfoPanel,
BakeToIDOptionsPanel, BakeToIDOptionsPanel,
BakeToIDAdvancedMenu,
BakeToIDProperties, BakeToIDProperties,
IDMaskEditorValueProperties,
IDMaskEditorProperties,
IDMaskEditorPanel,
CreateIDMaskOperator,
IDMaskEditorIDList,
IDEDITOR_CreateIDOperator,
IDEDITOR_RemoveIDOperator,
IDEDITOR_PaintIDMaskOperator,
IDEDITOR_SelectIDMaskOperator,
IDEDITOR_FindUsedIDsOperator,
IDEDITOR_IDMaskEditorOptionsMenu,
IDEDITOR_ColorResetOperator,
IDEDITOR_ColorApplyOperator,
) )
menu_additions = [
id_mask_select_menu
]
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
setattr(bpy.types.Scene, 'bake_to_id_props', bpy.props.PointerProperty(type=BakeToIDProperties)) for menu in menu_additions:
menu.register()
setattr(bpy.types.Scene, 'bake_to_id_props', bpy.props.PointerProperty(type=BakeToIDProperties))
setattr(bpy.types.Mesh, 'id_mask_editor_properties', bpy.props.PointerProperty(type=IDMaskEditorProperties))
def unregister(): def unregister():
for menu in reversed(menu_additions):
menu.unregister()
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
del bpy.types.Scene.bake_to_id_props del bpy.types.Scene.bake_to_id_props

View file

@ -0,0 +1,13 @@
import bpy
from .. operators.id_editor_find_used_ids import IDEDITOR_FindUsedIDsOperator
class IDEDITOR_IDMaskEditorOptionsMenu(bpy.types.Menu):
bl_idname = "VIEW3D_MT_idmask_editor_options"
bl_label = "Options"
def draw(self, context):
layout = self.layout
layout.operator(IDEDITOR_FindUsedIDsOperator.bl_idname)

View file

@ -0,0 +1,12 @@
import bpy
from .. operators.id_mask_select import IDEDITOR_SelectIDMaskOperator
def id_mask_select_menu_function(self, context):
self.layout.operator(IDEDITOR_SelectIDMaskOperator.bl_idname).isCalledFromEditor = False
def register():
bpy.types.VIEW3D_MT_select_edit_mesh.append(id_mask_select_menu_function)
def unregister():
bpy.types.VIEW3D_MT_select_edit_mesh.remove(id_mask_select_menu_function)

View file

@ -1,9 +1,8 @@
import colorsys
import math
import bpy import bpy
from .. types import (get_source, get_target) from .. types.colors import get_color
from .. types.sources import get_source
from .. types.targets import get_target
class BakeToIDMapOperator(bpy.types.Operator): class BakeToIDMapOperator(bpy.types.Operator):
@ -27,27 +26,45 @@ class BakeToIDMapOperator(bpy.types.Operator):
def paint_id_mask(self, context, props): def paint_id_mask(self, context, props):
source = get_source(props.source) source = get_source(props.source)
targets = source.get_targets(context) targets = self.get_targets(context, source, props)
if len(targets) < 1: if len(targets) < 1:
return return
totalTargets = len(targets) color = get_color(props.colors)
colors = [] colors = color.get_colors(props)
total_hues = props.adv_total_hues
total_satuations = props.adv_total_satuations
total_brightnesses = props.adv_total_brightnesses
satuations_break_point = math.pow(total_brightnesses, total_hues)
for i in range(totalTargets):
h = (i / total_hues) % 1
l = (math.ceil(i / total_hues) % total_brightnesses) / total_brightnesses
s = math.ceil(i / satuations_break_point) / total_satuations
colors.append(colorsys.hls_to_rgb(h, l, s))
target = get_target(props.target) target = get_target(props.target)
self.paint_targets(props, target, targets, colors)
if 'after_painting' in dir(source):
source.after_painting(context, props)
def get_targets(self, context, source, props):
if props.selection_mode == 'SINGLE':
return source.get_targets([context.active_object])
if props.selection_mode == 'MULTIPLE_COMBINED':
filtered_object = filter(lambda x: x.type == 'MESH', context.selected_objects)
return source.get_targets(list(filtered_object))
if props.selection_mode == 'MULTIPLE_SEPARATE':
result = []
for obj in context.selected_objects:
if obj.type != 'MESH':
continue
obj_targets = source.get_targets([obj])
result.append(obj_targets)
return result
raise ValueError('Invalid selection_mode')
def paint_targets(self, props, target, targets, colors):
if props.selection_mode == 'MULTIPLE_SEPARATE':
for targetList in targets:
target.paint_targets(props,targetList, colors)
return
target.paint_targets(props, targets, colors) target.paint_targets(props, targets, colors)
source.after_painting(context, props)

View file

@ -0,0 +1,37 @@
import bpy
class CreateIDMaskOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.create_id_mask_attribute"
bl_label = "Create ID Mask - Attribute"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
obj = context.active_object
if obj.type != 'MESH':
return False
if not obj.data:
return False
mesh = obj.data
return 'ID_MASK' not in mesh.color_attributes
def execute(self, context):
obj = context.active_object
if obj.type != 'MESH':
return {'FINISHED'}
if not obj.data:
return {'FINISHED'}
mesh = obj.data
if 'ID_MASK' in mesh.color_attributes:
return {'FINISHED'}
bpy.ops.geometry.color_attribute_add(name='ID_MASK', data_type='FLOAT_COLOR', domain='CORNER')
mesh.id_mask_editor_properties.target_attribute = 'ID_MASK'
return {'FINISHED'}

View file

@ -0,0 +1,84 @@
import bpy
class IDEDITOR_ColorApplyOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.apply_colors"
bl_label = "Apply changed ID-Mask colors"
bl_description = "Searches the current ID-mask for colors and adds them to the id-list"
bl_options = {'INTERNAL'}
triggeredByList: bpy.props.BoolProperty(default=False)
listId: bpy.props.IntProperty(default=0)
@classmethod
def poll(cls, context):
obj = context.active_object
if obj.type != 'MESH':
return False
if not obj.data:
return False
mesh = obj.data
properties = mesh.id_mask_editor_properties
if not properties.target_attribute:
return False
return True
def execute(self, context):
obj = context.active_object
old_mode = bpy.context.object.mode
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data
properties = mesh.id_mask_editor_properties
color_attribute = mesh.color_attributes.get(properties.target_attribute)
colors = []
if self.triggeredByList:
list_item = properties.possible_ids[self.listId]
if list_item.color_changed:
colors.append((list_item.original_color, list_item.color))
else:
for id in properties.possible_ids:
if not id.color_changed:
continue
colors.append((id.original_color, id.color))
for polygon in mesh.polygons:
polygon_color = self.get_color_from_polygon(color_attribute, polygon)
for (original_color, color) in colors:
if original_color[0] != polygon_color[0] or original_color[1] != polygon_color[1] or original_color[2] != polygon_color[2]:
continue
for idx in polygon.loop_indices:
color_attribute.data[idx].color = (color.r, color.g, color.b, 1.0)
break
if self.triggeredByList:
list_item = properties.possible_ids[self.listId]
list_item.color_changed = False
else:
for id in properties.possible_ids:
id.color_changed = False
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode=old_mode, toggle=False)
return {'FINISHED'}
def reset_color(self, value):
if not value.color_changed:
return
value.color = value.original_color
value.color_changed = False
def get_color_from_polygon(self, attribute, polygon):
color = attribute.data[polygon.loop_indices[0]].color
return (color[0], color[1], color[2], 1.0)

View file

@ -0,0 +1,32 @@
import bpy
from .. types.colors import get_color
class IDEDITOR_CreateIDOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.create_id_mask"
bl_label = "id_mask_editor.create_id_mask"
bl_options = {'INTERNAL'}
def execute(self, context):
obj = context.active_object
if obj.type != 'MESH':
return {'FINISHED'}
if not obj.data:
return {'FINISHED'}
mesh = obj.data
properties = mesh.id_mask_editor_properties
collection = properties.possible_ids
new_id = collection.add()
color = get_color(properties.colors)
colors = color.get_colors(properties)
colorCount = len(colors)
current_id_color = colors[properties.current_color_id % colorCount]
new_id.color = current_id_color
properties.current_color_id += 1
return {'FINISHED'}

View file

@ -0,0 +1,63 @@
import bpy
class IDEDITOR_FindUsedIDsOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.find_used_ids"
bl_label = "Find used colors"
bl_description = "Searches the current ID-mask for colors and adds them to the id-list"
@classmethod
def poll(cls, context):
obj = context.active_object
if obj.type != 'MESH':
return False
if not obj.data:
return False
mesh = obj.data
properties = mesh.id_mask_editor_properties
if not properties.target_attribute:
return False
return True
def execute(self, context):
obj = context.active_object
old_mode = bpy.context.object.mode
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data
properties = mesh.id_mask_editor_properties
color_attribute = mesh.color_attributes.get(properties.target_attribute)
colors = []
for color_data in color_attribute.data:
colors.append((color_data.color[0], color_data.color[1], color_data.color[2]))
i = 0
set_colors = set(colors)
for color in set_colors:
has_color = False
for color_id in properties.possible_ids:
has_color = (color_id.color.r == color[0] and
color_id.color.g == color[1] and
color_id.color.b == color[2])
if has_color:
break
if has_color:
continue
color_identifier = properties.possible_ids.add()
color_identifier.color = (color[0], color[1], color[2])
color_identifier.name = "Imported ID " + str(i + 1)
i += 1
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode=old_mode, toggle=False)
return {'FINISHED'}

View file

@ -0,0 +1,36 @@
import bpy
class IDEDITOR_PaintIDMaskOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.paint_id_mask"
bl_label = "Paint ID Mask"
def execute(self, context):
obj = context.active_object
if obj.type != 'MESH':
return {'FINISHED'}
if not obj.data:
return {'FINISHED'}
old_mode = bpy.context.object.mode
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data
properties = mesh.id_mask_editor_properties
collection = properties.possible_ids
color = collection[properties.active_id].color
color_attribute = mesh.color_attributes.get(properties.target_attribute)
for polygon in mesh.polygons:
if not polygon.select:
continue
for idx in polygon.loop_indices:
color_attribute.data[idx].color = (color.r, color.g, color.b, 1.0)
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode=old_mode, toggle=False)
return {'FINISHED'}

View file

@ -0,0 +1,37 @@
import bpy
class IDEDITOR_RemoveIDOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.remove_id_mask"
bl_label = "id_mask_editor.remove_id_mask"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
obj = context.active_object
if obj.type != 'MESH':
return False
if not obj.data:
return False
mesh = obj.data
properties = mesh.id_mask_editor_properties
collection = properties.possible_ids
return 0 <= properties.active_id < len(collection)
def execute(self, context):
obj = context.active_object
if obj.type != 'MESH':
return {'FINISHED'}
if not obj.data:
return {'FINISHED'}
mesh = obj.data
properties = mesh.id_mask_editor_properties
collection = properties.possible_ids
collection.remove(properties.active_id)
return {'FINISHED'}

View file

@ -0,0 +1,49 @@
import bpy
class IDEDITOR_ColorResetOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.reset_colors"
bl_label = "Resets ID-Mask colors"
bl_description = "Resets the colors to the previous values"
bl_options = {'INTERNAL'}
triggeredByList: bpy.props.BoolProperty(default=False)
listId: bpy.props.IntProperty(default=0)
@classmethod
def poll(cls, context):
obj = context.active_object
if obj.type != 'MESH':
return False
if not obj.data:
return False
mesh = obj.data
properties = mesh.id_mask_editor_properties
if not properties.target_attribute:
return False
return True
def execute(self, context):
obj = context.active_object
mesh = obj.data
properties = mesh.id_mask_editor_properties
if self.triggeredByList:
self.reset_color(properties.possible_ids[self.listId])
return {'FINISHED'}
for prop in properties.possible_ids:
self.reset_color(prop)
return {'FINISHED'}
def reset_color(self, value):
if not value.color_changed:
return
value.color = value.original_color
value.color_changed = False

View file

@ -0,0 +1,70 @@
import bpy
class IDEDITOR_SelectIDMaskOperator(bpy.types.Operator):
bl_idname = "id_mask_editor.select_id_mask"
bl_label = "Select by ID Mask"
bl_description = "Selects the faces of the active object, based on current ID mask"
isCalledFromEditor: bpy.props.BoolProperty(default=False)
@classmethod
def poll(cls, context):
obj = context.active_object
if obj.type != 'MESH':
return False
if not obj.data:
return False
mesh = obj.data
properties = mesh.id_mask_editor_properties
if not properties.target_attribute:
return False
return True
def execute(self, context):
obj = context.active_object
old_mode = bpy.context.object.mode
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data
properties = mesh.id_mask_editor_properties
color_attribute = mesh.color_attributes.get(properties.target_attribute)
test_color = self.get_test_color(properties, mesh, color_attribute)
if not test_color:
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode=old_mode, toggle=False)
return {'FINISHED'}
for polygon in mesh.polygons:
polygon_color = self.get_color_from_polygon(color_attribute, polygon)
if test_color[0] != polygon_color[0] or test_color[1] != polygon_color[1] or test_color[2] != polygon_color[2]:
continue
polygon.select = True
if old_mode != "OBJECT":
bpy.ops.object.mode_set(mode=old_mode, toggle=False)
return {'FINISHED'}
def get_test_color(self, properties, mesh, color_attribute):
if self.isCalledFromEditor:
color = properties.possible_ids[properties.active_id].color
return (color.r, color.g, color.b, 1.0)
if mesh.polygons.active:
return self.get_color_from_polygon(color_attribute, mesh.polygons[mesh.polygons.active])
return None
def get_color_from_polygon(self, attribute, polygon):
color = attribute.data[polygon.loop_indices[0]].color
return (color[0], color[1], color[2], 1.0)

31
src/panels/bake_panel.py Normal file
View file

@ -0,0 +1,31 @@
import bpy
class BakeToIDMapPanel(bpy.types.Panel):
bl_idname = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS"
bl_label = "Bake ID Mask"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
props = context.scene.bake_to_id_props
operator_row = layout.row()
operator_row.operator("object.bake_to_id_map", text="Bake")
operator_row.enabled = self.check_if_props_valid(context, props)
layout.prop(props, "selection_mode")
def check_if_props_valid(self, context, props):
if (props.selection_mode == "SINGLE" and context.active_object is None):
return False
if (props.selection_mode != "SINGLE" and len(context.selected_objects) < 1):
return False
return True

View file

@ -0,0 +1,49 @@
import bpy
from .. types.colors import get_color
from .. types.sources import get_source
class BakeToIDInfoPanel(bpy.types.Panel):
bl_idname = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS_INFO"
bl_parent_id = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS"
bl_label = "Infos"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
def draw(self, context):
layout = self.layout
props = context.scene.bake_to_id_props
source = get_source(props.source)
if props.selection_mode != 'SINGLE':
layout.label(text="Selected Object-Count: " + str(len(context.selected_objects)))
if props.selection_mode == 'SINGLE':
layout.label(text="ID-Total: " + str(source.estimate_ids([context.active_object])))
if props.selection_mode == 'MULTIPLE_SEPARATE':
total = 0
count = 0
for obj in context.selected_objects:
if (obj.type != 'MESH'):
continue
total += source.estimate_ids([obj])
count += 1
layout.label(text="Estimated ID-Total: " + str(total))
try:
layout.label(text="Estimated ID-Average: " + str(total / count))
except ZeroDivisionError:
layout.label(text="Estimated ID-Average: 0")
if props.selection_mode == 'MULTIPLE_COMBINED':
layout.label(text="ID-Total: " + str(source.estimate_ids(context.selected_objects)))
color = get_color(props.colors)
layout.label(text="Colors available: " + str(color.get_count(props)))

View file

@ -0,0 +1,56 @@
import textwrap
import bpy
from .. types.colors import get_color
from .. types.sources import get_source
from .. types.targets import get_target
class BakeToIDOptionsPanel(bpy.types.Panel):
bl_idname = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS_OPTIONS"
bl_parent_id = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS"
bl_label = "Options"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
props = context.scene.bake_to_id_props
layout.prop(props, "source")
source = get_source(props.source)
self.draw_options(context, layout, props, source)
layout.separator()
layout.prop(props, "target")
target = get_target(props.target)
self.draw_options(context, layout, props, target)
layout.separator()
layout.prop(props, "colors")
color = get_color(props.colors)
self.draw_options(context, layout, props, color)
def draw_options(self, context, layout, props, element):
has_render_ui = 'render_ui' in dir(element)
has_connected_properties = 'connected_properties' in dir(element) and len(element.connected_properties) > 0
if not has_render_ui and not has_connected_properties:
return
object_box = layout.box()
if has_render_ui:
element.render_ui(context, object_box, props)
return
for setting in element.connected_properties:
object_box.prop(props, setting)

View file

@ -0,0 +1,117 @@
import bpy
from .. menu.id_mask_editor_options import IDEDITOR_IDMaskEditorOptionsMenu
from .. operators.create_id_mask import CreateIDMaskOperator
from .. operators.id_editor_apply_color import IDEDITOR_ColorApplyOperator
from .. operators.id_editor_create_id import IDEDITOR_CreateIDOperator
from .. operators.id_editor_paint import IDEDITOR_PaintIDMaskOperator
from .. operators.id_editor_remove_id import IDEDITOR_RemoveIDOperator
from .. operators.id_editor_revert_color import IDEDITOR_ColorResetOperator
from .. operators.id_mask_select import IDEDITOR_SelectIDMaskOperator
from .. types.colors import get_color
class IDMaskEditorPanel(bpy.types.Panel):
bl_idname = "ID_MASK_EDITOR_PT_Panel"
bl_label = "ID Mask Editor"
bl_category = "Tool"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@classmethod
def poll(cls, context):
if not context.object:
return False
if not context.object.type == 'MESH':
return False
return context.object.mode == "EDIT"
def draw(self, context):
layout = self.layout
layout.use_property_decorate = True
mesh = context.object.data
properties = mesh.id_mask_editor_properties
target_attribute_row = layout.row(align=True)
has_attribute = properties.target_attribute
target_attribute_row.prop_search(
properties,
'target_attribute',
mesh,
'color_attributes',
icon='GROUP_VCOL'
)
target_attribute_row.operator(CreateIDMaskOperator.bl_idname, icon='ADD', text="")
if not has_attribute:
return
layout.prop(properties, "colors")
color = get_color(properties.colors)
self.draw_options(context, layout, properties, color)
layout.separator()
row = layout.row()
col = row.column()
col.template_list(
'IDMaskEditorIDList',
'IDMaskEditorIDList',
properties,
'possible_ids',
properties,
'active_id',
rows=3
)
button_row = col.row()
button_row.operator(IDEDITOR_PaintIDMaskOperator.bl_idname, text='Paint', icon='VPAINT_HLT')
button_row.operator(IDEDITOR_SelectIDMaskOperator.bl_idname, text='Select', icon='SELECT_SET').isCalledFromEditor = True
color_button_row = button_row.row(align=True)
has_color_changed = False
for id in properties.possible_ids:
if not id.color_changed:
continue
has_color_changed = True
break
color_button_row.enabled = has_color_changed
reset_op = color_button_row.operator(IDEDITOR_ColorResetOperator.bl_idname, icon='LOOP_BACK', text='Reset Colors')
reset_op.triggeredByList = False
apply_op = color_button_row.operator(IDEDITOR_ColorApplyOperator.bl_idname, icon='CHECKMARK', text='Apply Colors')
apply_op.triggeredByList = False
col = row.column(align=True)
col.operator(IDEDITOR_CreateIDOperator.bl_idname, icon='ADD', text="")
col.operator(IDEDITOR_RemoveIDOperator.bl_idname, icon='REMOVE', text="")
col.separator()
col.menu(IDEDITOR_IDMaskEditorOptionsMenu.bl_idname, icon='DOWNARROW_HLT', text="")
def draw_options(self, context, layout, props, element):
has_render_ui = 'render_ui' in dir(element)
has_connected_properties = 'connected_properties' in dir(element) and len(element.connected_properties) > 0
if not has_render_ui and not has_connected_properties:
return
object_box = layout.box()
if has_render_ui:
element.render_ui(context, object_box, props)
return
for setting in element.connected_properties:
object_box.prop(props, setting)

View file

@ -0,0 +1,30 @@
import bpy
from .. operators.id_editor_apply_color import IDEDITOR_ColorApplyOperator
from .. operators.id_editor_revert_color import IDEDITOR_ColorResetOperator
class IDMaskEditorIDList(bpy.types.UIList):
def draw_item(self, _context, layout, data, attribute, _icon, _active_data, _active_propname, _index):
layout.alignment = 'EXPAND'
split = layout.split(factor=0.15)
split.alignment = 'LEFT'
split.prop(attribute, 'color', text='')
row = split.row()
col = row.column()
col.alignment = 'RIGHT'
col.emboss = 'NONE'
col.prop(attribute, "name", text="")
col.emboss = 'NORMAL'
row1 = row.row(align=True)
row1.enabled = attribute.color_changed
reset_op = row1.operator(IDEDITOR_ColorResetOperator.bl_idname, icon='LOOP_BACK', text='')
reset_op.triggeredByList = True
reset_op.listId = _index
apply_op = row1.operator(IDEDITOR_ColorApplyOperator.bl_idname, icon='CHECKMARK', text='')
apply_op.triggeredByList = True
apply_op.listId = _index

View file

@ -1,16 +0,0 @@
import bpy
class BakeToIDMapPanel(bpy.types.Panel):
bl_idname = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS"
bl_label = "Bake to ID Map"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
self.layout.operator("object.bake_to_id_map", text="Bake")

View file

@ -1,24 +0,0 @@
import math
import bpy
class BakeToIDAdvancedMenu(bpy.types.Panel):
bl_idname = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS_ADVANCED"
bl_parent_id = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS"
bl_label = "Advanced"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = context.scene.bake_to_id_props
layout.label(text="Colors")
layout.prop(props, "adv_total_hues")
layout.prop(props, "adv_total_satuations")
layout.prop(props, "adv_total_brightnesses")
layout.label(text="Max ID-count: " + str(
math.pow(math.pow(props.adv_total_hues, props.adv_total_satuations), props.adv_total_brightnesses)))

View file

@ -1,23 +0,0 @@
import bpy
from .. operators.bake_to_id_map import BakeToIDMapOperator
from ..types import get_source
class BakeToIDInfoPanel(bpy.types.Panel):
bl_idname = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS_INFO"
bl_parent_id = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS"
bl_label = "Infos"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
def draw(self, context):
layout = self.layout
props = context.scene.bake_to_id_props
source = get_source(props.source)
layout.label(text="Selected Object-Count: " + str(len(context.selected_objects)))
layout.label(text="Estimated ID-Count: " + str(source.estimate_ids(context, props)))

View file

@ -1,33 +0,0 @@
import textwrap
import bpy
from src.types import get_source, get_target
class BakeToIDOptionsPanel(bpy.types.Panel):
bl_idname = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS_OPTIONS"
bl_parent_id = "PANEL.BAKE_TO_ID_MAP_PT_SETTINGS"
bl_label = "Options"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
def draw(self, context):
layout = self.layout
props = context.scene.bake_to_id_props
layout.prop(props, "source")
source = get_source(props.source)
source_settings_box = layout.box()
for setting in source.connected_properties:
source_settings_box.prop(props, setting)
layout.separator()
layout.prop(props, "target")
target = get_target(props.target)
target_settings_box = layout.box()
for setting in target.connected_properties:
target_settings_box.prop(props, setting)

View file

@ -0,0 +1,62 @@
from bpy.types import (PropertyGroup, Palette)
from bpy.props import (EnumProperty, BoolProperty, IntProperty, StringProperty, PointerProperty)
from .. types.colors import get_color_enum
from .. types.sources import get_source_enum
from .. types.targets import get_targets_enum
class BakeToIDProperties(PropertyGroup):
selection_mode: EnumProperty(
items=[
("SINGLE", "Only active element", "Only use the currently selected element", 0),
("MULTIPLE_SEPARATE", "Use Selection, separate", "It performs the transfer for every selected element, but each get there own ids", 1),
("MULTIPLE_COMBINED", "Use Selection, combined", "It performs the transfer for every selected element, combining the ids", 2)
],
name="Selection Mode",
description="Specifies how the 3D View-Selection is gonna be used.",
default="MULTIPLE_SEPARATE"
)
source: EnumProperty(
items=get_source_enum(),
name="Source",
description="From where should the IDs be taken",
default="MATERIAL_INDEX"
)
target: EnumProperty(
items=get_targets_enum(),
name="Target",
description="To where should the IDs should be baked to",
default=get_targets_enum()[0][0]
)
colors: EnumProperty(
items=get_color_enum(),
name="Color Source",
description="From where to take the colors",
default=get_color_enum()[0][0]
)
source_materials_remove_all: BoolProperty(
name="Remove all source materials",
default=False,
description="Removes every material except the first one."
)
target_vertex_color_attribute_name: StringProperty(
name="Color Attribute",
default="ID_MASK",
)
target_vertex_color_override_attribute: BoolProperty(
name="Override Color Attribute",
default=True,
description="If set true, the attribute will be deleted and recreated, if it already exists. If set false, the data will just be overwritten."
)
colors_color_palette_palette: PointerProperty(
type=Palette,
name='Color Palette',
description="The Color Palette used for colors"
)

View file

@ -1,52 +0,0 @@
from bpy.types import (PropertyGroup)
from bpy.props import (EnumProperty, BoolProperty, IntProperty, StringProperty)
from src.types import get_source_enum, get_targets_enum
class BakeToIDProperties(PropertyGroup):
source: EnumProperty(
items=get_source_enum(),
name="Source",
description="From where should the IDs be taken",
default = "MATERIAL_INDEX"
)
target: EnumProperty(
items=get_targets_enum(),
name="Target",
description="To where should the IDs should be baked to",
default=get_targets_enum()[0][0]
)
source_materials_remove_all : BoolProperty(
name="Remove all source materials",
default=False,
description="Removes every material except the first one."
)
target_vertex_color_attribute_name: StringProperty(
name="Color Attribute",
default="ID_MASK",
)
adv_total_hues: IntProperty(
name="Total Hues",
default=10,
min=1,
soft_max=360,
)
adv_total_satuations: IntProperty(
name="Total Satuations",
default=10,
min=1,
soft_max=100,
)
adv_total_brightnesses: IntProperty(
name="Total Brightnesses",
default=10,
min=1,
soft_max=100,
)

View file

@ -0,0 +1,58 @@
import bpy
from bpy.types import (PropertyGroup, Palette, FloatColorAttribute)
from bpy.props import (EnumProperty, BoolProperty, IntProperty, StringProperty, PointerProperty, CollectionProperty)
from .. properties.id_mask_editor_value_properties import IDMaskEditorValueProperties
from .. types.colors import get_color_enum
def on_target_attribute_set(self, value):
return None
def on_target_attribute_get(self):
if bpy.context.active_object is None:
return None
if bpy.context.active_object.data is None:
return None
if bpy.context.active_object.type != 'MESH':
return None
color_attributes = bpy.context.active_object.data.color_attributes[0]
return color_attributes
class IDMaskEditorProperties(PropertyGroup):
colors: EnumProperty(
items=get_color_enum(),
name="Color Source",
description="From where to take the colors",
default=get_color_enum()[0][0]
)
colors_color_palette_palette: PointerProperty(
type=Palette,
name='Color Palette',
description="The Color Palette used for colors"
)
target_attribute: StringProperty(
name='',
description="The attribute to write the ID Mask to"
)
possible_ids: CollectionProperty(
type=IDMaskEditorValueProperties,
name='Entries',
)
active_id: IntProperty(
name="",
description="",
default=0
)
current_color_id: IntProperty(
default=0
)

View file

@ -0,0 +1,47 @@
from bpy.types import (PropertyGroup)
from bpy.props import (StringProperty,BoolProperty, FloatVectorProperty)
def get_color(self):
return self['color']
def set_color(self, value):
if 'color' not in self:
self['color'] = value
return
prev_color = self['color']
if not self.color_changed:
self.color_changed = True
self.original_color = prev_color
self['color'] = value
class IDMaskEditorValueProperties(PropertyGroup):
name: StringProperty(
name="Name",
description="ID-Name",
default='ID'
)
color_changed: BoolProperty(
default=False
)
color: FloatVectorProperty(
name="Color",
subtype='COLOR',
default=[1.0, 1.0, 1.0],
min=0,
max=1,
get=get_color,
set=set_color
)
original_color: FloatVectorProperty(
name="Original Color",
subtype='COLOR',
default=[1.0, 1.0, 1.0],
min=0,
max=1
)

View file

@ -1,43 +0,0 @@
from . sources import material_index as source_mat_index
from . targets import vertex_colors as target_vertex_colors
_sources = [
source_mat_index
]
_targets = [
target_vertex_colors
]
def get_source(id):
for source in _sources:
if source.source_id == id:
return source
raise Exception("Source not found: " + id)
def get_target(id):
for target in _targets:
if target.target_id == id:
return target
raise Exception("Target not found: " + id)
def get_source_enum():
enumList = []
i = 0
for source in _sources:
enumList.append((source.source_id, source.name, source.description, i))
i += 1
return enumList
def get_targets_enum():
enumList = []
i = 0
for target in _targets:
enumList.append((target.target_id, target.name, target.description, i))
i += 1
return enumList

View file

@ -0,0 +1,25 @@
from . import generated, color_palette
_colors = [
generated,
color_palette
]
def get_color(id):
for color in _colors:
if color.color_id == id:
return color
raise Exception("Source not found: " + id)
def get_color_enum():
enum_list = []
i = 0
for color in _colors:
enum_list.append((color.color_id, color.name, color.description, i))
i += 1
return enum_list

View file

@ -0,0 +1,25 @@
color_id = 'COLOR_PALETTE'
name = 'Palette'
description = "The color palette is specified by the user"
def get_colors(props):
if not props.colors_color_palette_palette:
return []
return list(map(lambda x: x.color, props.colors_color_palette_palette.colors))
def get_count(props):
if not props.colors_color_palette_palette:
return 0
return len(props.colors_color_palette_palette.colors)
def render_ui(context, layout, props):
layout.template_ID(props, "colors_color_palette_palette")
if props.colors_color_palette_palette:
row = layout.column()
row.enabled = False
row.template_palette(props, "colors_color_palette_palette", color=True)

View file

@ -0,0 +1,31 @@
import colorsys
color_id = 'GENERATED'
name = 'Generated'
description = 'The colors get generated on-the-fly'
MAX_BRIGHTNESS_STEPS = 8
MAX_SATURATION_STEPS = 8
MAX_HUES_STEPS = 8
def get_colors(props):
return gen_colors()
def get_count(props):
return len(gen_colors())
def gen_colors():
colors = []
for brightnessStep in range(0, MAX_BRIGHTNESS_STEPS):
v = 1 - (brightnessStep / MAX_BRIGHTNESS_STEPS)
for satuationStep in range(0, MAX_SATURATION_STEPS):
s = 1 - (satuationStep / MAX_SATURATION_STEPS)
for hueStep in range(0, MAX_HUES_STEPS):
h = hueStep / MAX_HUES_STEPS
color = colorsys.hsv_to_rgb(h, s, v)
colors.append(color)
return colors

View file

@ -0,0 +1,25 @@
from . import material_index
from . import object
_sources = [
material_index,
object
]
def get_source(id):
for source in _sources:
if source.source_id == id:
return source
raise Exception("Source not found: " + id)
def get_source_enum():
enum_list = []
i = 0
for source in _sources:
enum_list.append((source.source_id, source.name, source.description, i))
i += 1
return enum_list

View file

@ -6,9 +6,9 @@ connected_properties = [
'source_materials_remove_all' 'source_materials_remove_all'
] ]
def get_targets(context): def get_targets(objects):
targets = [] targets = []
for obj in context.selected_objects: for obj in objects:
if not obj.material_slots: continue if not obj.material_slots: continue
mesh = obj.data mesh = obj.data
@ -33,9 +33,9 @@ def after_painting(context, props):
for obj in context.selected_objects: for obj in context.selected_objects:
obj.data.materials.clear() obj.data.materials.clear()
def estimate_ids(context, props): def estimate_ids(objects):
count = 0 count = 0
for obj in context.selected_objects: for obj in objects:
count += len(obj.material_slots) count += len(obj.material_slots)
return count return count

View file

@ -0,0 +1,16 @@
source_id = 'OBJECT'
name = 'Object ID'
description = 'Uses the object id as basis for the ID mask.'
connected_properties = []
def get_targets(objects):
targets = []
for obj in objects:
mesh = obj.data
targets.append((mesh, mesh.polygons))
return targets
def estimate_ids(objects):
return len(objects)

View file

@ -0,0 +1,23 @@
from . import vertex_colors
_targets = [
vertex_colors
]
def get_target(id):
for target in _targets:
if target.target_id == id:
return target
raise Exception("Target not found: " + id)
def get_targets_enum():
enum_list = []
i = 0
for target in _targets:
enum_list.append((target.target_id, target.name, target.description, i))
i += 1
return enum_list

View file

@ -3,12 +3,14 @@ name = 'Color Attribute / Vertex Color'
description = 'Bakes the ID onto a color attribute (previously known as Vertex Color)' description = 'Bakes the ID onto a color attribute (previously known as Vertex Color)'
connected_properties = [ connected_properties = [
'target_vertex_color_attribute_name' 'target_vertex_color_attribute_name',
'target_vertex_color_override_attribute'
] ]
def paint_targets(props, targets, colors): def paint_targets(props, targets, colors):
sorted_targets = {} sorted_targets = {}
colors_amount = len(colors)
for i in range(len(targets)): for i in range(len(targets)):
target = targets[i] target = targets[i]
obj = target[0] obj = target[0]
@ -17,16 +19,22 @@ def paint_targets(props, targets, colors):
if obj not in sorted_targets: if obj not in sorted_targets:
sorted_targets[obj] = [] sorted_targets[obj] = []
sorted_targets[obj].append((indecies, colors[i])) sorted_targets[obj].append((indecies, colors[i % colors_amount]))
layer_name = props.target_vertex_color_attribute_name layer_name = props.target_vertex_color_attribute_name
for mesh in sorted_targets: for mesh in sorted_targets:
if layer_name in mesh.attributes: color_attribute = get_color_attribute(props, mesh.attributes, layer_name)
mesh.attributes.remove(mesh.attributes[layer_name])
color_attribute = mesh.attributes.new(name=layer_name, type='FLOAT_COLOR', domain='CORNER')
for (indecies, color) in sorted_targets[mesh]: for (indecies, color) in sorted_targets[mesh]:
for polygon in indecies: for polygon in indecies:
for idx in polygon.loop_indices: for idx in polygon.loop_indices:
color_attribute.data[idx].color = (color[0], color[1], color[2], 1.0) color_attribute.data[idx].color = (color[0], color[1], color[2], 1.0)
def get_color_attribute(props, attributes, layer_name):
if layer_name in attributes:
if not props.target_vertex_color_override_attribute:
return attributes[layer_name]
attributes.remove(attributes[layer_name])
return attributes.new(name=layer_name, type='FLOAT_COLOR', domain='CORNER')