+1
-1
@@ -10,7 +10,7 @@ import numpy.typing as npt
|
|||||||
|
|
||||||
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type
|
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type
|
||||||
from mathutils import Vector, Matrix
|
from mathutils import Vector, Matrix
|
||||||
from bpy.types import (Context, Object, Modifier, EditBone, Operator,
|
from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material,
|
||||||
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
|
VertexGroup, ShapeKey, Bone, Mesh, Armature, PropertyGroup)
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from bpy.props import PointerProperty, IntProperty, StringProperty
|
from bpy.props import PointerProperty, IntProperty, StringProperty
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ def update_logging_state(self: Any, context: Context) -> None:
|
|||||||
from .addon_preferences import save_preference
|
from .addon_preferences import save_preference
|
||||||
enabled = self.enable_logging
|
enabled = self.enable_logging
|
||||||
save_preference("enable_logging", enabled)
|
save_preference("enable_logging", enabled)
|
||||||
configure_logging(enabled)
|
configure_logging(enabled)
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# thank you https://stackoverflow.com/a/71432759
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from bpy.types import Image, Material
|
||||||
|
|
||||||
|
|
||||||
|
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
class Rectangle_Obj:
|
||||||
|
x: int = 0
|
||||||
|
y: int = 0
|
||||||
|
w: int = 0
|
||||||
|
h: int = 0
|
||||||
|
down: Rectangle_Obj = None
|
||||||
|
used: bool = False
|
||||||
|
right: Rectangle_Obj = None
|
||||||
|
|
||||||
|
def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
self.down = down
|
||||||
|
self.used = used
|
||||||
|
self.right = right
|
||||||
|
|
||||||
|
def split(self, w, h) -> Rectangle_Obj:
|
||||||
|
self.used = True
|
||||||
|
self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h)
|
||||||
|
self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def find(self, w, h) -> Optional[Rectangle_Obj]:
|
||||||
|
if self.used:
|
||||||
|
return self.right.find(w, h) or self.down.find(w, h)
|
||||||
|
elif (w <= self.w) and (h <= self.h):
|
||||||
|
return self
|
||||||
|
return None
|
||||||
|
|
||||||
|
class MaterialImageList:
|
||||||
|
albedo: Image
|
||||||
|
normal: Image
|
||||||
|
emission: Image
|
||||||
|
ambient_occlusion: Image
|
||||||
|
height: Image
|
||||||
|
roughness: Image
|
||||||
|
fit: Rectangle_Obj
|
||||||
|
material: Material
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
x: int = 0
|
||||||
|
y: int = 0
|
||||||
|
w: int = 0
|
||||||
|
h: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BinPacker(object):
|
||||||
|
root: Rectangle_Obj
|
||||||
|
bin: list[MaterialImageList] = []
|
||||||
|
def __init__(self, structure: list[MaterialImageList]):
|
||||||
|
self.root = None
|
||||||
|
self.bin = structure
|
||||||
|
|
||||||
|
def fit(self):
|
||||||
|
structure = self.bin
|
||||||
|
structure_len = len(self.bin)
|
||||||
|
w: int = 0
|
||||||
|
h: int = 0
|
||||||
|
if structure_len > 0:
|
||||||
|
w = structure[0].w
|
||||||
|
h = structure[0].h
|
||||||
|
self.root = Rectangle_Obj(x=0, y=0, w=w, h=h)
|
||||||
|
for img in structure:
|
||||||
|
w = img.w
|
||||||
|
h = img.h
|
||||||
|
node = self.root.find(w, h)
|
||||||
|
if node:
|
||||||
|
img.fit = node.split(w, h)
|
||||||
|
else:
|
||||||
|
img.fit = self.grow_node(w, h)
|
||||||
|
return structure
|
||||||
|
|
||||||
|
def grow_node(self, w, h) -> Optional[Rectangle_Obj]:
|
||||||
|
can_grow_right = (h <= self.root.h)
|
||||||
|
can_grow_down = (w <= self.root.w)
|
||||||
|
|
||||||
|
should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w))
|
||||||
|
should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h))
|
||||||
|
|
||||||
|
if should_grow_right:
|
||||||
|
return self.grow_right(w, h)
|
||||||
|
elif should_grow_down:
|
||||||
|
return self.grow_down(w, h)
|
||||||
|
elif can_grow_right:
|
||||||
|
return self.grow_right(w, h)
|
||||||
|
elif can_grow_down:
|
||||||
|
return self.grow_down(w, h)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def grow_right(self, w, h) -> Optional[Rectangle_Obj]:
|
||||||
|
self.root = Rectangle_Obj(
|
||||||
|
used=True,
|
||||||
|
x=0,
|
||||||
|
y=0,
|
||||||
|
w=self.root.w + w,
|
||||||
|
h=self.root.h,
|
||||||
|
down=self.root,
|
||||||
|
right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h))
|
||||||
|
node = self.root.find(w, h)
|
||||||
|
if node:
|
||||||
|
return node.split(w, h)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def grow_down(self, w, h) -> Optional[Rectangle_Obj]:
|
||||||
|
self.root = Rectangle_Obj(
|
||||||
|
used=True,
|
||||||
|
x=0,
|
||||||
|
y=0,
|
||||||
|
w=self.root.w,
|
||||||
|
h=self.root.h + h,
|
||||||
|
down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h),
|
||||||
|
right=self.root
|
||||||
|
)
|
||||||
|
node = self.root.find(w, h)
|
||||||
|
if node:
|
||||||
|
return node.split(w, h)
|
||||||
|
return None
|
||||||
+102
-5
@@ -14,7 +14,7 @@ from .logging_setup import logger
|
|||||||
from .translations import t, get_languages_list, update_language
|
from .translations import t, get_languages_list, update_language
|
||||||
from .addon_preferences import get_preference, save_preference
|
from .addon_preferences import get_preference, save_preference
|
||||||
from .updater import get_version_list
|
from .updater import get_version_list
|
||||||
from .common import get_armature_list, get_active_armature, get_all_meshes
|
from .common import get_armature_list, get_active_armature, get_all_meshes, SceneMatClass
|
||||||
from ..functions.visemes import VisemePreview
|
from ..functions.visemes import VisemePreview
|
||||||
from ..functions.eye_tracking import set_rotation
|
from ..functions.eye_tracking import set_rotation
|
||||||
|
|
||||||
@@ -396,6 +396,12 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=0
|
default=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
merge_twist_bones: BoolProperty(
|
||||||
|
name=t("Tools.merge_twist_bones"),
|
||||||
|
description=t("Tools.merge_twist_bones_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
list_only_mode: BoolProperty(
|
list_only_mode: BoolProperty(
|
||||||
name=t("Tools.list_only_mode"),
|
name=t("Tools.list_only_mode"),
|
||||||
description=t("Tools.list_only_mode_desc"),
|
description=t("Tools.list_only_mode_desc"),
|
||||||
@@ -408,12 +414,103 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
merge_twist_bones: BoolProperty(
|
material_search_filter: StringProperty(
|
||||||
name=t("Tools.merge_twist_bones"),
|
name=t("TextureAtlas.search_materials"),
|
||||||
description=t("Tools.merge_twist_bones_desc"),
|
description=t("TextureAtlas.search_materials_desc"),
|
||||||
default=True
|
default=""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
|
||||||
|
if self.use_nodes:
|
||||||
|
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
|
||||||
|
(i.image.name if i.image else "node with no image..."),
|
||||||
|
(i.image.name if i.image else i.name),index+1)
|
||||||
|
for index,i in enumerate(self.node_tree.nodes)
|
||||||
|
if i.bl_idname == "ShaderNodeTexImage"]
|
||||||
|
if not len(Object.Enum):
|
||||||
|
Object.Enum = [(t("TextureAtlas.error.label"),
|
||||||
|
t("TextureAtlas.no_images_error.desc"),
|
||||||
|
t("TextureAtlas.error.label"), 0)]
|
||||||
|
else:
|
||||||
|
Object.Enum = [(t("TextureAtlas.error.label"),
|
||||||
|
t("TextureAtlas.no_nodes_error.desc"),
|
||||||
|
t("TextureAtlas.error.label"), 0)]
|
||||||
|
Object.Enum.append((t("TextureAtlas.none.label"),
|
||||||
|
t("TextureAtlas.none.label"),
|
||||||
|
t("TextureAtlas.none.label"), 0))
|
||||||
|
return Object.Enum
|
||||||
|
|
||||||
|
Material.texture_atlas_albedo = EnumProperty(
|
||||||
|
name=t("TextureAtlas.albedo"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_normal = EnumProperty(
|
||||||
|
name=t("TextureAtlas.normal"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_emission = EnumProperty(
|
||||||
|
name=t("TextureAtlas.emission"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_ambient_occlusion = EnumProperty(
|
||||||
|
name=t("TextureAtlas.ambient_occlusion"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_height = EnumProperty(
|
||||||
|
name=t("TextureAtlas.height"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.texture_atlas_roughness = EnumProperty(
|
||||||
|
name=t("TextureAtlas.roughness"),
|
||||||
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()),
|
||||||
|
default=0,
|
||||||
|
items=get_texture_node_list
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.include_in_atlas = BoolProperty(
|
||||||
|
name=t("TextureAtlas.include_in_atlas"),
|
||||||
|
description=t("TextureAtlas.include_in_atlas_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
Material.material_expanded = BoolProperty(
|
||||||
|
name=t("TextureAtlas.material_expanded"),
|
||||||
|
description=t("TextureAtlas.material_expanded_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
texture_atlas_Has_Mat_List_Shown: BoolProperty(
|
||||||
|
name=t("TextureAtlas.list_shown"),
|
||||||
|
description=t("TextureAtlas.list_shown_desc"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
texture_atlas_material_index: IntProperty(
|
||||||
|
default=-1,
|
||||||
|
get=lambda self: -1,
|
||||||
|
set=lambda self, context: None
|
||||||
|
)
|
||||||
|
|
||||||
|
materials: CollectionProperty(
|
||||||
|
type=SceneMatClass
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import numpy
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
from typing import List, Optional
|
||||||
|
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeNormalMap
|
||||||
|
from ..core.common import SceneMatClass, MaterialListBool, ProgressTracker
|
||||||
|
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
|
||||||
|
from ..core.translations import t
|
||||||
|
from ..core.logging_setup import logger
|
||||||
|
|
||||||
|
class MaterialImageList:
|
||||||
|
def __init__(self):
|
||||||
|
self.albedo: Image = None
|
||||||
|
self.normal: Image = None
|
||||||
|
self.emission: Image = None
|
||||||
|
self.ambient_occlusion: Image = None
|
||||||
|
self.height: Image = None
|
||||||
|
self.roughness: Image = None
|
||||||
|
self.material: Material = None
|
||||||
|
self.parent_mesh: Object = None
|
||||||
|
self.w: int = 0
|
||||||
|
self.h: int = 0
|
||||||
|
self.fit = None
|
||||||
|
|
||||||
|
def scale_images_to_largest(images: List[Image]) -> tuple[int, int]:
|
||||||
|
x: int = 0
|
||||||
|
y: int = 0
|
||||||
|
|
||||||
|
valid_images = [img for img in images if img and img.has_data]
|
||||||
|
|
||||||
|
if not valid_images:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
for image in valid_images:
|
||||||
|
x = max(x, image.size[0])
|
||||||
|
y = max(y, image.size[1])
|
||||||
|
|
||||||
|
for image in valid_images:
|
||||||
|
image.scale(width=int(x), height=int(y))
|
||||||
|
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> List[Image]:
|
||||||
|
return [
|
||||||
|
classitem.albedo,
|
||||||
|
classitem.normal,
|
||||||
|
classitem.emission,
|
||||||
|
classitem.ambient_occlusion,
|
||||||
|
classitem.height,
|
||||||
|
classitem.roughness
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
|
||||||
|
material_image_list: list[MaterialImageList] = []
|
||||||
|
|
||||||
|
with ProgressTracker(context, len(context.scene.objects), "Processing Materials") as progress:
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'MESH':
|
||||||
|
for mat_slot in obj.material_slots:
|
||||||
|
# Only process materials that are selected for atlas
|
||||||
|
if mat_slot.material and mat_slot.material.include_in_atlas is True:
|
||||||
|
new_mat_image_item = MaterialImageList()
|
||||||
|
try:
|
||||||
|
new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo]
|
||||||
|
except Exception:
|
||||||
|
name = mat_slot.material.name + "_albedo_replacement"
|
||||||
|
if name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
||||||
|
new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
|
new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
|
||||||
|
try:
|
||||||
|
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
|
||||||
|
except Exception:
|
||||||
|
name = mat_slot.material.name + "_normal_replacement"
|
||||||
|
if name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
||||||
|
new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
|
new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
|
||||||
|
try:
|
||||||
|
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
|
||||||
|
except Exception:
|
||||||
|
name = mat_slot.material.name + "_emission_replacement"
|
||||||
|
if name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
||||||
|
new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
|
new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
|
||||||
|
try:
|
||||||
|
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
|
||||||
|
except Exception:
|
||||||
|
name = mat_slot.material.name + "_ambient_occlusion_replacement"
|
||||||
|
if name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
||||||
|
new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
|
new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
|
||||||
|
try:
|
||||||
|
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
|
||||||
|
except Exception:
|
||||||
|
name = mat_slot.material.name + "_height_replacement"
|
||||||
|
if name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
||||||
|
new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
|
new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
|
||||||
|
try:
|
||||||
|
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
|
||||||
|
except Exception:
|
||||||
|
name = mat_slot.material.name + "_roughness_replacement"
|
||||||
|
if name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
|
||||||
|
new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
|
||||||
|
new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
|
||||||
|
|
||||||
|
new_mat_image_item.material = mat_slot.material
|
||||||
|
new_mat_image_item.parent_mesh = obj
|
||||||
|
material_image_list.append(new_mat_image_item)
|
||||||
|
|
||||||
|
progress.step(f"Processed {obj.name}")
|
||||||
|
|
||||||
|
return material_image_list
|
||||||
|
|
||||||
|
def prep_images_in_scene(context: Context) -> List[MaterialImageList]:
|
||||||
|
preped_images = get_material_images_from_scene(context)
|
||||||
|
|
||||||
|
with ProgressTracker(context, len(preped_images), "Preparing Images") as progress:
|
||||||
|
for MaterialImageClass in preped_images:
|
||||||
|
ImageList = MaterialImageList_to_Image_list(MaterialImageClass)
|
||||||
|
MaterialImageClass.w, MaterialImageClass.h = scale_images_to_largest(ImageList)
|
||||||
|
progress.step(f"Scaled images for {MaterialImageClass.material.name}")
|
||||||
|
|
||||||
|
return preped_images
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_AtlasMaterials(Operator):
|
||||||
|
bl_idname = "avatar_toolkit.atlas_materials"
|
||||||
|
bl_label = t("TextureAtlas.atlas_materials")
|
||||||
|
bl_description = t("TextureAtlas.atlas_materials_desc")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
return context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set:
|
||||||
|
try:
|
||||||
|
selected_materials = [m for m in prep_images_in_scene(context)
|
||||||
|
if m.material and m.material.include_in_atlas]
|
||||||
|
|
||||||
|
if not selected_materials:
|
||||||
|
self.report({'WARNING'}, t("TextureAtlas.no_materials_selected"))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
logger.info("Starting material atlas creation")
|
||||||
|
|
||||||
|
packer = BinPacker(selected_materials)
|
||||||
|
mat_images = packer.fit()
|
||||||
|
|
||||||
|
size = [
|
||||||
|
max([matimg.fit.w + matimg.albedo.size[0] for matimg in mat_images]),
|
||||||
|
max([matimg.fit.h + matimg.albedo.size[1] for matimg in mat_images])
|
||||||
|
]
|
||||||
|
|
||||||
|
atlased_mat = MaterialImageList()
|
||||||
|
|
||||||
|
# UV Remapping
|
||||||
|
with ProgressTracker(context, len(bpy.data.objects), "Remapping UVs") as progress:
|
||||||
|
for mat in mat_images:
|
||||||
|
x, y = int(mat.fit.x), int(mat.fit.y)
|
||||||
|
w, h = int(mat.albedo.size[0]), int(mat.albedo.size[1])
|
||||||
|
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.type == 'MESH':
|
||||||
|
mesh = obj.data
|
||||||
|
for layer in mesh.polygons:
|
||||||
|
if (obj.material_slots[layer.material_index].material and
|
||||||
|
obj.material_slots[layer.material_index].material == mat.material):
|
||||||
|
for loop_idx in layer.loop_indices:
|
||||||
|
for layer_loops in mesh.uv_layers:
|
||||||
|
uv_item = layer_loops.uv[loop_idx]
|
||||||
|
uv_item.vector.x = (uv_item.vector.x*(w/size[0]))+(x/size[0])
|
||||||
|
uv_item.vector.y = (uv_item.vector.y*(h/size[1]))+(y/size[1])
|
||||||
|
progress.step(f"Processed UVs for {obj.name}")
|
||||||
|
|
||||||
|
# Create atlas textures
|
||||||
|
texture_types = ["albedo", "normal", "emission", "ambient_occlusion", "height", "roughness"]
|
||||||
|
|
||||||
|
with ProgressTracker(context, len(texture_types), "Creating Atlas Textures") as progress:
|
||||||
|
for type_name in texture_types:
|
||||||
|
new_image_name = f"Atlas_{type_name}_{context.scene.name}_{Path(bpy.data.filepath).stem}"
|
||||||
|
logger.debug(f"Processing {type_name} atlas image")
|
||||||
|
|
||||||
|
if new_image_name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(bpy.data.images[new_image_name])
|
||||||
|
|
||||||
|
canvas = bpy.data.images.new(name=new_image_name, width=int(size[0]),
|
||||||
|
height=int(size[1]), alpha=True)
|
||||||
|
c_w = canvas.size[0]
|
||||||
|
canvas_pixels = list(canvas.pixels[:])
|
||||||
|
|
||||||
|
for mat in mat_images:
|
||||||
|
x, y = int(mat.fit.x), int(mat.fit.y)
|
||||||
|
w, h = int(mat.albedo.size[0]), int(mat.albedo.size[1])
|
||||||
|
image_var = getattr(mat, type_name)
|
||||||
|
image_pixels = list(image_var.pixels[:])
|
||||||
|
|
||||||
|
for k in range(h):
|
||||||
|
for i in range(w):
|
||||||
|
for channel in range(4):
|
||||||
|
canvas_pixels[int((((k+y)*c_w)+(i+x))*4)+channel] = \
|
||||||
|
image_pixels[int(((k*w)+i)*4)+channel]
|
||||||
|
|
||||||
|
canvas.pixels[:] = canvas_pixels[:]
|
||||||
|
canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath),
|
||||||
|
new_image_name+".png"))
|
||||||
|
setattr(atlased_mat, type_name, canvas)
|
||||||
|
progress.step(f"Created {type_name} atlas")
|
||||||
|
|
||||||
|
# Create material nodes
|
||||||
|
atlased_mat.material = bpy.data.materials.new(
|
||||||
|
name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}")
|
||||||
|
atlased_mat.material.use_nodes = True
|
||||||
|
atlased_mat.material.node_tree.nodes.clear()
|
||||||
|
|
||||||
|
principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
|
||||||
|
principled_node.location.x = 7.29706335067749
|
||||||
|
principled_node.location.y = 298.918212890625
|
||||||
|
|
||||||
|
output_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
|
||||||
|
output_node.location.x = 297.29705810546875
|
||||||
|
output_node.location.y = 298.918212890625
|
||||||
|
|
||||||
|
albedo_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||||
|
albedo_node.location.x = -588.6177978515625
|
||||||
|
albedo_node.location.y = 414.1948547363281
|
||||||
|
albedo_node.image = atlased_mat.albedo
|
||||||
|
|
||||||
|
emission_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||||
|
emission_node.location.x = -588.6177978515625
|
||||||
|
emission_node.location.y = -173.9259033203125
|
||||||
|
emission_node.image = atlased_mat.emission
|
||||||
|
|
||||||
|
normal_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||||
|
normal_node.location.x = -941.4189453125
|
||||||
|
normal_node.location.y = -20.8391780853271
|
||||||
|
normal_node.image = atlased_mat.normal
|
||||||
|
|
||||||
|
normal_map_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeNormalMap")
|
||||||
|
normal_map_node.location.x = -545.550537109375
|
||||||
|
normal_map_node.location.y = -0.7543716430664062
|
||||||
|
|
||||||
|
roughness_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||||
|
roughness_node.location.x = -592.1703491210938
|
||||||
|
roughness_node.location.y = 206.74075317382812
|
||||||
|
roughness_node.image = atlased_mat.roughness
|
||||||
|
|
||||||
|
ambient_occlusion_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||||
|
ambient_occlusion_node.location.x = -906.4371337890625
|
||||||
|
ambient_occlusion_node.location.y = -389.9602355957031
|
||||||
|
ambient_occlusion_node.image = atlased_mat.ambient_occlusion
|
||||||
|
|
||||||
|
height_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||||
|
height_node.location.x = -1222.383056640625
|
||||||
|
height_node.location.y = -375.48406982421875
|
||||||
|
height_node.image = atlased_mat.height
|
||||||
|
|
||||||
|
atlased_mat.material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
|
||||||
|
atlased_mat.material.node_tree.links.new(principled_node.inputs["Metallic"], roughness_node.outputs["Alpha"])
|
||||||
|
atlased_mat.material.node_tree.links.new(principled_node.inputs["Roughness"], roughness_node.outputs["Color"])
|
||||||
|
atlased_mat.material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
|
||||||
|
atlased_mat.material.node_tree.links.new(principled_node.inputs["Normal"], normal_map_node.outputs["Normal"])
|
||||||
|
atlased_mat.material.node_tree.links.new(principled_node.inputs["Emission Color"], emission_node.outputs["Color"])
|
||||||
|
atlased_mat.material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
|
||||||
|
atlased_mat.material.node_tree.links.new(normal_map_node.inputs["Color"], normal_node.outputs["Color"])
|
||||||
|
|
||||||
|
# Update materials
|
||||||
|
with ProgressTracker(context, len(context.scene.objects), "Updating Materials") as progress:
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'MESH':
|
||||||
|
mesh = obj.data
|
||||||
|
for i, mat_slot in enumerate(obj.material_slots):
|
||||||
|
if mat_slot.material and mat_slot.material.include_in_atlas:
|
||||||
|
mesh.materials[i] = atlased_mat.material
|
||||||
|
progress.step(f"Updated materials for {obj.name}")
|
||||||
|
|
||||||
|
logger.info("Material atlas creation completed successfully")
|
||||||
|
self.report({'INFO'}, t("TextureAtlas.atlas_completed"))
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating material atlas: {str(e)}", exc_info=True)
|
||||||
|
self.report({'ERROR'}, t("TextureAtlas.atlas_error"))
|
||||||
|
raise e
|
||||||
@@ -403,6 +403,26 @@
|
|||||||
"MergeArmature.cleanup_shape_keys": "Clean Shape Keys",
|
"MergeArmature.cleanup_shape_keys": "Clean Shape Keys",
|
||||||
"MergeArmature.cleanup_shape_keys_desc": "Remove unused shape keys",
|
"MergeArmature.cleanup_shape_keys_desc": "Remove unused shape keys",
|
||||||
|
|
||||||
|
"TextureAtlas.atlas_completed": "Texture atlas creation completed",
|
||||||
|
"TextureAtlas.atlas_error": "An error occurred during texture atlas creation",
|
||||||
|
"TextureAtlas.atlas_materials": "Atlas Materials",
|
||||||
|
"TextureAtlas.atlas_materials_desc": "Atlas materials to optimize the model",
|
||||||
|
"TextureAtlas.label": "Texture Atlasing",
|
||||||
|
"TextureAtlas.loaded_list": "Loaded Texture Atlas Material List",
|
||||||
|
"TextureAtlas.material_list_label": "Texture Atlas Material List Material",
|
||||||
|
"TextureAtlas.reload_list": "Reload Texture Atlas Material List",
|
||||||
|
"TextureAtlas.error.label": "ERROR",
|
||||||
|
"TextureAtlas.none.label": "None",
|
||||||
|
"TextureAtlas.no_nodes_error.desc": "THIS MATERIAL DOES NOT USE NODES!",
|
||||||
|
"TextureAtlas.no_images_error.desc": "THIS MATERIAL HAS NO IMAGES!",
|
||||||
|
"TextureAtlas.texture_use_atlas.desc": "The texture that will be used for the {name} map atlas",
|
||||||
|
"TextureAtlas.albedo": "Albedo",
|
||||||
|
"TextureAtlas.normal": "Normal",
|
||||||
|
"TextureAtlas.emission": "Emission",
|
||||||
|
"TextureAtlas.ambient_occlusion": "Ambient Occlusion",
|
||||||
|
"TextureAtlas.height": "Height",
|
||||||
|
"TextureAtlas.roughness": "Roughness",
|
||||||
|
|
||||||
"Settings.label": "Settings",
|
"Settings.label": "Settings",
|
||||||
"Settings.language": "Language",
|
"Settings.language": "Language",
|
||||||
"Settings.language_desc": "Select interface language",
|
"Settings.language_desc": "Select interface language",
|
||||||
|
|||||||
@@ -397,6 +397,26 @@
|
|||||||
"MergeArmature.cleanup_shape_keys": "シェイプキーをクリーン",
|
"MergeArmature.cleanup_shape_keys": "シェイプキーをクリーン",
|
||||||
"MergeArmature.cleanup_shape_keys_desc": "未使用のシェイプキーを削除",
|
"MergeArmature.cleanup_shape_keys_desc": "未使用のシェイプキーを削除",
|
||||||
|
|
||||||
|
"TextureAtlas.atlas_completed": "テクスチャアトラスの作成が完了しました",
|
||||||
|
"TextureAtlas.atlas_error": "テクスチャアトラスの作成中にエラーが発生しました",
|
||||||
|
"TextureAtlas.atlas_materials": "マテリアルをアトラス化",
|
||||||
|
"TextureAtlas.atlas_materials_desc": "モデルを最適化するためにマテリアルをアトラス化",
|
||||||
|
"TextureAtlas.label": "テクスチャアトラス化",
|
||||||
|
"TextureAtlas.loaded_list": "テクスチャアトラスマテリアルリストを読み込み済み",
|
||||||
|
"TextureAtlas.material_list_label": "テクスチャアトラスマテリアルリスト",
|
||||||
|
"TextureAtlas.reload_list": "テクスチャアトラスマテリアルリストを再読み込み",
|
||||||
|
"TextureAtlas.error.label": "エラー",
|
||||||
|
"TextureAtlas.none.label": "なし",
|
||||||
|
"TextureAtlas.no_nodes_error.desc": "このマテリアルはノードを使用していません!",
|
||||||
|
"TextureAtlas.no_images_error.desc": "このマテリアルには画像がありません!",
|
||||||
|
"TextureAtlas.texture_use_atlas.desc": "{name}マップアトラスに使用されるテクスチャ",
|
||||||
|
"TextureAtlas.albedo": "アルベド",
|
||||||
|
"TextureAtlas.normal": "法線",
|
||||||
|
"TextureAtlas.emission": "発光",
|
||||||
|
"TextureAtlas.ambient_occlusion": "アンビエントオクルージョン",
|
||||||
|
"TextureAtlas.height": "高さ",
|
||||||
|
"TextureAtlas.roughness": "ラフネス",
|
||||||
|
|
||||||
"Settings.label": "設定",
|
"Settings.label": "設定",
|
||||||
"Settings.language": "言語",
|
"Settings.language": "言語",
|
||||||
"Settings.language_desc": "インターフェース言語を選択",
|
"Settings.language_desc": "インターフェース言語を選択",
|
||||||
|
|||||||
@@ -396,6 +396,26 @@
|
|||||||
"MergeArmature.remove_zero_weights_desc": "가중치가 없는 버텍스 그룹 제거",
|
"MergeArmature.remove_zero_weights_desc": "가중치가 없는 버텍스 그룹 제거",
|
||||||
"MergeArmature.cleanup_shape_keys": "쉐이프 키 정리",
|
"MergeArmature.cleanup_shape_keys": "쉐이프 키 정리",
|
||||||
"MergeArmature.cleanup_shape_keys_desc": "미사용 쉐이프 키 제거",
|
"MergeArmature.cleanup_shape_keys_desc": "미사용 쉐이프 키 제거",
|
||||||
|
|
||||||
|
"TextureAtlas.atlas_completed": "텍스처 아틀라스 생성이 완료되었습니다",
|
||||||
|
"TextureAtlas.atlas_error": "텍스처 아틀라스 생성 중 오류가 발생했습니다",
|
||||||
|
"TextureAtlas.atlas_materials": "재질 아틀라스화",
|
||||||
|
"TextureAtlas.atlas_materials_desc": "모델을 최적화하기 위해 재질을 아틀라스화",
|
||||||
|
"TextureAtlas.label": "텍스처 아틀라스화",
|
||||||
|
"TextureAtlas.loaded_list": "텍스처 아틀라스 재질 목록 로드됨",
|
||||||
|
"TextureAtlas.material_list_label": "텍스처 아틀라스 재질 목록",
|
||||||
|
"TextureAtlas.reload_list": "텍스처 아틀라스 재질 목록 새로고침",
|
||||||
|
"TextureAtlas.error.label": "오류",
|
||||||
|
"TextureAtlas.none.label": "없음",
|
||||||
|
"TextureAtlas.no_nodes_error.desc": "이 재질은 노드를 사용하지 않습니다!",
|
||||||
|
"TextureAtlas.no_images_error.desc": "이 재질에는 이미지가 없습니다!",
|
||||||
|
"TextureAtlas.texture_use_atlas.desc": "{name} 맵 아틀라스에 사용될 텍스처",
|
||||||
|
"TextureAtlas.albedo": "알베도",
|
||||||
|
"TextureAtlas.normal": "노말",
|
||||||
|
"TextureAtlas.emission": "이미션",
|
||||||
|
"TextureAtlas.ambient_occlusion": "앰비언트 오클루전",
|
||||||
|
"TextureAtlas.height": "높이",
|
||||||
|
"TextureAtlas.roughness": "거칠기",
|
||||||
|
|
||||||
"Settings.label": "설정",
|
"Settings.label": "설정",
|
||||||
"Settings.language": "언어",
|
"Settings.language": "언어",
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
from bpy.types import UIList, Panel, UILayout, Object, Context, Material, Operator
|
||||||
|
import bpy
|
||||||
|
from math import sqrt
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from ..core.common import SceneMatClass, MaterialListBool, get_active_armature
|
||||||
|
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
|
||||||
|
from ..core.translations import t
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_SelectAllMaterials(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.select_all_materials'
|
||||||
|
bl_label = "Select All"
|
||||||
|
bl_description = "Select all materials for atlas"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for item in context.scene.avatar_toolkit.materials:
|
||||||
|
item.mat.include_in_atlas = True
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_SelectNoneMaterials(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.select_none_materials'
|
||||||
|
bl_label = "Select None"
|
||||||
|
bl_description = "Deselect all materials"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for item in context.scene.avatar_toolkit.materials:
|
||||||
|
item.mat.include_in_atlas = False
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_ExpandAllMaterials(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.expand_all_materials'
|
||||||
|
bl_label = "Expand All"
|
||||||
|
bl_description = "Expand all material settings"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for item in context.scene.avatar_toolkit.materials:
|
||||||
|
item.mat.material_expanded = True
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_CollapseAllMaterials(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.collapse_all_materials'
|
||||||
|
bl_label = "Collapse All"
|
||||||
|
bl_description = "Collapse all material settings"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for item in context.scene.avatar_toolkit.materials:
|
||||||
|
item.mat.material_expanded = False
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
|
||||||
|
bl_idname = 'avatar_toolkit.expand_section_materials'
|
||||||
|
bl_label = ""
|
||||||
|
bl_description = ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: Context) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> set:
|
||||||
|
if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
||||||
|
context.scene.avatar_toolkit.materials.clear()
|
||||||
|
newlist: list[Material] = []
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if len(obj.material_slots) > 0:
|
||||||
|
for mat_slot in obj.material_slots:
|
||||||
|
if mat_slot.material:
|
||||||
|
if mat_slot.material not in newlist:
|
||||||
|
newlist.append(mat_slot.material)
|
||||||
|
newitem: SceneMatClass = context.scene.avatar_toolkit.materials.add()
|
||||||
|
newitem.mat = mat_slot.material
|
||||||
|
MaterialListBool.old_list[context.scene.name] = newlist
|
||||||
|
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = True
|
||||||
|
else:
|
||||||
|
context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
|
||||||
|
bl_label = t("TextureAtlas.material_list_label")
|
||||||
|
bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list_mat"
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
row = layout.row(align=True)
|
||||||
|
|
||||||
|
row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT')
|
||||||
|
row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT')
|
||||||
|
row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN')
|
||||||
|
row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT')
|
||||||
|
row.prop(context.scene.avatar_toolkit, "material_search_filter", text="", icon='VIEWZOOM')
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
row = box.row()
|
||||||
|
row.label(text=f"Estimated Atlas Size: {self.calculate_atlas_size(context)}px")
|
||||||
|
|
||||||
|
def draw_item(self, context: Context, layout: UILayout, data: Object, item: SceneMatClass, icon, active_data, active_propname, index):
|
||||||
|
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
||||||
|
if (context.scene.avatar_toolkit.material_search_filter and
|
||||||
|
context.scene.avatar_toolkit.material_search_filter.lower() not in item.mat.name.lower()):
|
||||||
|
return
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
|
||||||
|
row.prop(item.mat, "include_in_atlas", text="", icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT')
|
||||||
|
|
||||||
|
row.prop(item.mat, "material_expanded",
|
||||||
|
text=item.mat.name,
|
||||||
|
icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW',
|
||||||
|
emboss=False)
|
||||||
|
|
||||||
|
if item.mat.material_expanded and item.mat.include_in_atlas:
|
||||||
|
box = layout.box()
|
||||||
|
col = box.column(align=True)
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_albedo", "IMAGE_RGB")
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_normal", "NORMALS_FACE")
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT")
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID")
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH")
|
||||||
|
self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL")
|
||||||
|
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
|
||||||
|
def draw_texture_row(self, layout, material, prop_name, icon):
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(material, prop_name, icon=icon)
|
||||||
|
if getattr(material, prop_name):
|
||||||
|
row.label(text="", icon='CHECKMARK')
|
||||||
|
else:
|
||||||
|
row.label(text="", icon='X')
|
||||||
|
|
||||||
|
def is_material_ready(self, material):
|
||||||
|
return bool(material.texture_atlas_albedo or
|
||||||
|
material.texture_atlas_normal or
|
||||||
|
material.texture_atlas_emission)
|
||||||
|
|
||||||
|
def calculate_atlas_size(self, context):
|
||||||
|
total_size = 0
|
||||||
|
for mat in context.scene.avatar_toolkit.materials:
|
||||||
|
if mat.mat.include_in_atlas:
|
||||||
|
if mat.mat.texture_atlas_albedo:
|
||||||
|
img = bpy.data.images[mat.mat.texture_atlas_albedo]
|
||||||
|
total_size += img.size[0] * img.size[1]
|
||||||
|
return f"{int(sqrt(total_size))}x{int(sqrt(total_size))}"
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_TextureAtlasPanel(Panel):
|
||||||
|
bl_label = t("TextureAtlas.label")
|
||||||
|
bl_idname = "OBJECT_PT_avatar_toolkit_texture_atlas"
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = CATEGORY_NAME
|
||||||
|
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
|
bl_order = 6
|
||||||
|
|
||||||
|
def draw(self, context: Context):
|
||||||
|
layout = self.layout
|
||||||
|
armature = get_active_armature(context)
|
||||||
|
|
||||||
|
if armature:
|
||||||
|
layout.label(text=t("TextureAtlas.label"), icon='TEXTURE')
|
||||||
|
layout.separator(factor=0.5)
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
row = box.row()
|
||||||
|
direction_icon = 'RIGHTARROW' if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
|
||||||
|
row.operator(AvatarToolKit_OT_ExpandSectionMaterials.bl_idname,
|
||||||
|
text=(t("TextureAtlas.reload_list") if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list")),
|
||||||
|
icon=direction_icon)
|
||||||
|
|
||||||
|
if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown:
|
||||||
|
row = box.row()
|
||||||
|
row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname,
|
||||||
|
'material_list',
|
||||||
|
context.scene.avatar_toolkit,
|
||||||
|
'materials',
|
||||||
|
context.scene.avatar_toolkit,
|
||||||
|
'texture_atlas_material_index',
|
||||||
|
rows=12,
|
||||||
|
type='DEFAULT')
|
||||||
|
|
||||||
|
layout.separator(factor=1.0)
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.scale_y = 1.5
|
||||||
|
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
|
||||||
|
text=t("TextureAtlas.atlas_materials"),
|
||||||
|
icon='NODE_TEXTURE')
|
||||||
|
else:
|
||||||
|
layout.label(text=t("Tools.select_armature"), icon='ERROR')
|
||||||
Reference in New Issue
Block a user