Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeb41dec40 | |||
| 10fb112de7 | |||
| c532e2a6a0 | |||
| 4df5369cbb | |||
| 36550a42e5 | |||
| 2dc3a19283 | |||
| 30115eeaac | |||
| 285c331f79 | |||
| 57ded41f2f | |||
| 948b1bb352 | |||
| 9104bfae67 | |||
| 6d71669849 | |||
| f043c6099e | |||
| 3eb0029b5e | |||
| 686bc0bda1 | |||
| 1482632405 | |||
| 2a7cb16fea | |||
| 1187949280 | |||
| d7cc8096b9 | |||
| 08f37d3202 | |||
| cb0abf3053 | |||
| 2bb1826346 | |||
| 9ad760bfb8 |
@@ -11,6 +11,9 @@ Join the Neoneko Discord here: https://discord.catsblenderplugin.xyz
|
|||||||
|
|
||||||
Need a more stable toolset while Avatar Toolkit is in Alpha? Then please use Blender 4.x and use our Unofficial Cats Blender Plugin which you can find [here](https://github.com/unofficalcats/Cats-Blender-Plugin-Unofficial-).
|
Need a more stable toolset while Avatar Toolkit is in Alpha? Then please use Blender 4.x and use our Unofficial Cats Blender Plugin which you can find [here](https://github.com/unofficalcats/Cats-Blender-Plugin-Unofficial-).
|
||||||
|
|
||||||
|
### Support us:
|
||||||
|
If you like what we do and want to help support the development of cats you can do it on our pally.gg [here](https://pally.gg/p/teamneoneko) all money is split automatically between all developers and any support is appreciated.
|
||||||
|
|
||||||
## Blender version support policies.
|
## Blender version support policies.
|
||||||
|
|
||||||
You can find them on the wiki here [HERE](https://avatartoolkit.xyz/wiki.html?version=0.1.0#what-is-avatar-toolkits-version-support-policy)
|
You can find them on the wiki here [HERE](https://avatartoolkit.xyz/wiki.html?version=0.1.0#what-is-avatar-toolkits-version-support-policy)
|
||||||
|
|||||||
+31
@@ -1,7 +1,38 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
modules = None
|
modules = None
|
||||||
ordered_classes = None
|
ordered_classes = None
|
||||||
|
|
||||||
|
def show_version_error_popup():
|
||||||
|
def draw(self, context):
|
||||||
|
self.layout.label(text="Sorry, this version of Avatar Toolkit does not work on this version of Blender.")
|
||||||
|
self.layout.label(text="Please check the GitHub repository for the correct version for your Blender.")
|
||||||
|
self.layout.operator("wm.url_open", text="Open GitHub Repository").url = "https://github.com/teamneoneko/Avatar-Toolkit"
|
||||||
|
|
||||||
|
bpy.context.window_manager.popup_menu(draw, title="Avatar Toolkit Version Error", icon='ERROR')
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
# Check Blender version first
|
||||||
|
version = bpy.app.version
|
||||||
|
if version[0] > 4 or (version[0] == 4 and version[1] > 3):
|
||||||
|
show_version_error_popup()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add wheel installation check
|
||||||
|
try:
|
||||||
|
import lz4
|
||||||
|
except ImportError:
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import site
|
||||||
|
import pip
|
||||||
|
wheels_dir = os.path.join(os.path.dirname(__file__), "wheels")
|
||||||
|
for wheel in os.listdir(wheels_dir):
|
||||||
|
if wheel.endswith(".whl"):
|
||||||
|
pip.main(['install', os.path.join(wheels_dir, wheel)])
|
||||||
|
site.addsitedir(site.getsitepackages()[0])
|
||||||
|
|
||||||
from .core import auto_load
|
from .core import auto_load
|
||||||
print("Starting registration")
|
print("Starting registration")
|
||||||
auto_load.init()
|
auto_load.init()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "avatar_toolkit"
|
id = "avatar_toolkit"
|
||||||
version = "0.1.1"
|
version = "0.1.3"
|
||||||
name = "Avatar Toolkit"
|
name = "Avatar Toolkit"
|
||||||
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
||||||
maintainer = "Team NekoNeo"
|
maintainer = "Team NekoNeo"
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ def register() -> None:
|
|||||||
def unregister() -> None:
|
def unregister() -> None:
|
||||||
"""Unregister all classes and modules in reverse order"""
|
"""Unregister all classes and modules in reverse order"""
|
||||||
for cls in reversed(ordered_classes):
|
for cls in reversed(ordered_classes):
|
||||||
|
try:
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
except RuntimeError:
|
||||||
|
continue
|
||||||
|
|
||||||
for module in modules:
|
for module in modules:
|
||||||
if module.__name__ == __name__:
|
if module.__name__ == __name__:
|
||||||
|
|||||||
+45
-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
|
||||||
@@ -19,6 +19,50 @@ from ..core.logging_setup import logger
|
|||||||
from ..core.translations import t
|
from ..core.translations import t
|
||||||
from ..core.dictionaries import bone_names
|
from ..core.dictionaries import bone_names
|
||||||
|
|
||||||
|
class SceneMatClass(PropertyGroup):
|
||||||
|
mat: PointerProperty(type=Material)
|
||||||
|
|
||||||
|
register_class(SceneMatClass)
|
||||||
|
|
||||||
|
class MaterialListBool:
|
||||||
|
#For the love that is holy do not ever touch these. If this was java I would make these private
|
||||||
|
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
|
||||||
|
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
|
||||||
|
|
||||||
|
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
|
||||||
|
old_list: dict[str,list[Material]] = {}
|
||||||
|
bool_material_list_expand: dict[str,bool] = {}
|
||||||
|
|
||||||
|
def set_bool(self, value: bool) -> None:
|
||||||
|
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
|
||||||
|
if value == False:
|
||||||
|
MaterialListBool.old_list[bpy.context.scene.name] = []
|
||||||
|
|
||||||
|
def get_bool(self) -> bool:
|
||||||
|
newlist: list[Material] = []
|
||||||
|
for obj in bpy.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)
|
||||||
|
|
||||||
|
still_the_same: bool = True
|
||||||
|
if bpy.context.scene.name in MaterialListBool.old_list:
|
||||||
|
for item in newlist:
|
||||||
|
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
|
||||||
|
still_the_same = False
|
||||||
|
break
|
||||||
|
for item in MaterialListBool.old_list[bpy.context.scene.name]:
|
||||||
|
if item not in newlist:
|
||||||
|
still_the_same = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
still_the_same = False
|
||||||
|
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
|
||||||
|
|
||||||
|
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
|
||||||
|
|
||||||
class ProgressTracker:
|
class ProgressTracker:
|
||||||
"""Universal progress tracking for Avatar Toolkit operations"""
|
"""Universal progress tracking for Avatar Toolkit operations"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import typing
|
import typing
|
||||||
|
from bpy.types import Operator, Context
|
||||||
|
from bpy_extras.io_utils import ImportHelper
|
||||||
from typing import Optional, Callable, Dict, List, Union, Set
|
from typing import Optional, Callable, Dict, List, Union, Set
|
||||||
from ..common import clear_default_objects
|
from ..common import clear_default_objects
|
||||||
from .import_pmx import import_pmx
|
from .import_pmx import import_pmx
|
||||||
from .import_pmd import import_pmd
|
from .import_pmd import import_pmd
|
||||||
|
from ..translations import t
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
@@ -118,7 +122,12 @@ import_types: Dict[str, ImportMethod] = {
|
|||||||
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
|
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
|
||||||
),
|
),
|
||||||
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
|
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
|
||||||
"pmx": lambda directory, files, filepath: import_pmx(filepath),
|
"pmx": lambda directory, files, filepath: import_pmx(bpy.context, filepath,
|
||||||
|
scale=1.0,
|
||||||
|
use_mipmap=True,
|
||||||
|
sph_blend_factor=1.0,
|
||||||
|
spa_blend_factor=1.0
|
||||||
|
),
|
||||||
"pmd": lambda directory, files, filepath: import_pmd(filepath),
|
"pmd": lambda directory, files, filepath: import_pmd(filepath),
|
||||||
"animx": (lambda directory, files, filepath : bpy.ops.avatar_toolkit.animx_importer(directory=directory,files=files,filepath=filepath)),
|
"animx": (lambda directory, files, filepath : bpy.ops.avatar_toolkit.animx_importer(directory=directory,files=files,filepath=filepath)),
|
||||||
}
|
}
|
||||||
@@ -128,3 +137,68 @@ def concat_imports_filter(imports: Dict[str, ImportMethod]) -> str:
|
|||||||
return "".join(f"*.{importer};" for importer in imports.keys())
|
return "".join(f"*.{importer};" for importer in imports.keys())
|
||||||
|
|
||||||
imports: str = concat_imports_filter(import_types)
|
imports: str = concat_imports_filter(import_types)
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarToolKit_OT_Import(Operator, ImportHelper):
|
||||||
|
"""Import files into Blender with Avatar Toolkit settings"""
|
||||||
|
bl_idname: str = "avatar_toolkit.import"
|
||||||
|
bl_label: str = t("QuickAccess.import")
|
||||||
|
|
||||||
|
files: bpy.props.CollectionProperty(
|
||||||
|
type=bpy.types.OperatorFileListElement,
|
||||||
|
options={'HIDDEN', 'SKIP_SAVE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
filter_glob: bpy.props.StringProperty(
|
||||||
|
default=imports,
|
||||||
|
options={'HIDDEN', 'SKIP_SAVE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
directory: bpy.props.StringProperty(
|
||||||
|
maxlen=1024,
|
||||||
|
subtype='FILE_PATH',
|
||||||
|
options={'HIDDEN', 'SKIP_SAVE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
clear_default_objects()
|
||||||
|
|
||||||
|
file_grouping_dict: Dict[str, List[Dict[str, str]]] = {}
|
||||||
|
is_multi = len(self.files) > 0
|
||||||
|
|
||||||
|
if is_multi:
|
||||||
|
for file in self.files:
|
||||||
|
fullpath = os.path.join(self.directory, os.path.basename(file.name))
|
||||||
|
ext = pathlib.Path(fullpath).suffix.replace(".", "")
|
||||||
|
|
||||||
|
if ext not in file_grouping_dict:
|
||||||
|
file_grouping_dict[ext] = []
|
||||||
|
file_grouping_dict[ext].append({"name": os.path.basename(file.name)})
|
||||||
|
else:
|
||||||
|
fullpath = os.path.join(os.path.dirname(self.filepath), os.path.basename(self.filepath))
|
||||||
|
ext = pathlib.Path(fullpath).suffix.replace(".", "")
|
||||||
|
|
||||||
|
if ext not in file_grouping_dict:
|
||||||
|
file_grouping_dict[ext] = []
|
||||||
|
file_grouping_dict[ext].append({"name": fullpath})
|
||||||
|
|
||||||
|
for file_group_name, files in file_grouping_dict.items():
|
||||||
|
try:
|
||||||
|
if file_group_name == "vrm" and not hasattr(bpy.ops.import_scene, "vrm"):
|
||||||
|
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
directory = self.directory if self.directory else ""
|
||||||
|
import_types[file_group_name](directory, files, self.filepath)
|
||||||
|
|
||||||
|
except AttributeError as e:
|
||||||
|
if file_group_name == "vrm":
|
||||||
|
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
|
||||||
|
else:
|
||||||
|
self.report({'ERROR'}, t('Importing.need_importer').format(extension=file_group_name))
|
||||||
|
logger.error(f"Importer error: {e}")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
self.report({'INFO'}, t('Quick_Access.import_success'))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
from bpy.types import Context
|
from bpy.types import Context
|
||||||
|
|
||||||
logger = logging.getLogger('avatar_toolkit')
|
logger = logging.getLogger('avatar_toolkit')
|
||||||
|
_original_error = logger.error
|
||||||
|
|
||||||
def configure_logging(enabled: bool = False) -> None:
|
def configure_logging(enabled: bool = False) -> None:
|
||||||
"""Configure logging for Avatar Toolkit"""
|
"""Configure logging for Avatar Toolkit"""
|
||||||
@@ -19,6 +21,15 @@ def configure_logging(enabled: bool = False) -> None:
|
|||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
def error_with_traceback(msg, *args, **kwargs):
|
||||||
|
if kwargs.get('exc_info', False) or isinstance(msg, Exception):
|
||||||
|
full_msg = f"{msg}\n{traceback.format_exc()}"
|
||||||
|
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
|
||||||
|
else:
|
||||||
|
_original_error(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
logger.error = error_with_traceback
|
||||||
|
|
||||||
def update_logging_state(self: Any, context: Context) -> None:
|
def update_logging_state(self: Any, context: Context) -> None:
|
||||||
"""Update logging state based on user preference"""
|
"""Update logging state based on user preference"""
|
||||||
from .addon_preferences import save_preference
|
from .addon_preferences import save_preference
|
||||||
|
|||||||
@@ -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
|
||||||
+112
-1
@@ -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
|
||||||
|
|
||||||
@@ -367,14 +367,125 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
material_search_filter: StringProperty(
|
||||||
|
name=t("TextureAtlas.search_materials"),
|
||||||
|
description=t("TextureAtlas.search_materials_desc"),
|
||||||
|
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")
|
||||||
|
try:
|
||||||
|
bpy.utils.register_class(AvatarToolkitSceneProperties)
|
||||||
|
except ValueError:
|
||||||
|
# Class already registered, we can continue
|
||||||
|
pass
|
||||||
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
||||||
logger.debug("Properties registered successfully")
|
logger.debug("Properties registered successfully")
|
||||||
|
|
||||||
def unregister() -> None:
|
def unregister() -> None:
|
||||||
"""Unregister the Avatar Toolkit property group"""
|
"""Unregister the Avatar Toolkit property group"""
|
||||||
logger.info("Unregistering Avatar Toolkit properties")
|
logger.info("Unregistering Avatar Toolkit properties")
|
||||||
|
try:
|
||||||
del bpy.types.Scene.avatar_toolkit
|
del bpy.types.Scene.avatar_toolkit
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
logger.debug("Properties unregistered successfully")
|
logger.debug("Properties unregistered successfully")
|
||||||
|
|
||||||
|
|||||||
+76
-5
@@ -17,11 +17,17 @@ from typing import Dict, List, Tuple, Optional, Set, Any
|
|||||||
|
|
||||||
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
||||||
|
|
||||||
|
# Define which version series this installation can update to
|
||||||
|
# For example: ["0.1"] means only look for 0.1.x updates
|
||||||
|
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates
|
||||||
|
ALLOWED_VERSION_SERIES = ["0.1"] # Change this based on which version you're building
|
||||||
|
|
||||||
is_checking_for_update: bool = False
|
is_checking_for_update: bool = False
|
||||||
update_needed: bool = False
|
update_needed: bool = False
|
||||||
latest_version: Optional[str] = None
|
latest_version: Optional[str] = None
|
||||||
latest_version_str: str = ''
|
latest_version_str: str = ''
|
||||||
version_list: Optional[Dict[str, List[str]]] = None
|
version_list: Optional[Dict[str, List[str]]] = None
|
||||||
|
last_manual_check_time: float = 0
|
||||||
|
|
||||||
main_dir: str = os.path.dirname(os.path.dirname(__file__))
|
main_dir: str = os.path.dirname(os.path.dirname(__file__))
|
||||||
downloads_dir: str = os.path.join(main_dir, "downloads")
|
downloads_dir: str = os.path.join(main_dir, "downloads")
|
||||||
@@ -34,7 +40,9 @@ class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
|
|||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||||
|
global last_manual_check_time
|
||||||
check_for_update_background()
|
check_for_update_background()
|
||||||
|
last_manual_check_time = time.time() # Reset the timer on manual check
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
@@ -80,7 +88,16 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
|
|||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context) -> None:
|
def draw(self, context: bpy.types.Context) -> None:
|
||||||
|
global last_manual_check_time
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
|
# Auto-check for updates when panel is drawn, but not too frequently
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - last_manual_check_time > 300: # 5 minutes between auto-checks
|
||||||
|
if not is_checking_for_update and not update_needed:
|
||||||
|
check_for_update_background()
|
||||||
|
last_manual_check_time = current_time
|
||||||
|
|
||||||
draw_updater_panel(context, layout)
|
draw_updater_panel(context, layout)
|
||||||
|
|
||||||
|
|
||||||
@@ -158,11 +175,23 @@ def get_github_releases() -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def check_for_update_available() -> bool:
|
def check_for_update_available() -> bool:
|
||||||
global latest_version, latest_version_str
|
global latest_version, latest_version_str, version_list
|
||||||
if not version_list:
|
if not version_list:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
latest_version = max(version_list.keys(), key=lambda v: [int(x) for x in v.split('.')])
|
# Filter versions by allowed version series
|
||||||
|
compatible_versions = {}
|
||||||
|
for v, info in version_list.items():
|
||||||
|
for prefix in ALLOWED_VERSION_SERIES:
|
||||||
|
if v.startswith(prefix):
|
||||||
|
compatible_versions[v] = info
|
||||||
|
break
|
||||||
|
|
||||||
|
if not compatible_versions:
|
||||||
|
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
latest_version = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
|
||||||
latest_version_str = latest_version
|
latest_version_str = latest_version
|
||||||
|
|
||||||
current_version = get_current_version()
|
current_version = get_current_version()
|
||||||
@@ -197,9 +226,35 @@ def update_now(latest: bool = False) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if latest:
|
if latest:
|
||||||
update_link = version_list[latest_version_str][0]
|
# Filter compatible versions
|
||||||
|
compatible_versions = {}
|
||||||
|
for v, info in version_list.items():
|
||||||
|
for prefix in ALLOWED_VERSION_SERIES:
|
||||||
|
if v.startswith(prefix):
|
||||||
|
compatible_versions[v] = info
|
||||||
|
break
|
||||||
|
|
||||||
|
if not compatible_versions:
|
||||||
|
print(f"No compatible versions found in series: {', '.join(ALLOWED_VERSION_SERIES)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
latest_compatible = max(compatible_versions.keys(), key=lambda v: [int(x) for x in v.split('.')])
|
||||||
|
update_link = version_list[latest_compatible][0]
|
||||||
else:
|
else:
|
||||||
update_link = version_list[bpy.context.scene.avatar_toolkit_updater_version_list][0]
|
selected_version = bpy.context.scene.avatar_toolkit_updater_version_list
|
||||||
|
|
||||||
|
# Check if selected version is compatible
|
||||||
|
is_compatible = False
|
||||||
|
for prefix in ALLOWED_VERSION_SERIES:
|
||||||
|
if selected_version.startswith(prefix):
|
||||||
|
is_compatible = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_compatible:
|
||||||
|
print(f"Selected version {selected_version} is not in allowed series: {', '.join(ALLOWED_VERSION_SERIES)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_link = version_list[selected_version][0]
|
||||||
|
|
||||||
download_file(update_link)
|
download_file(update_link)
|
||||||
ui_refresh()
|
ui_refresh()
|
||||||
@@ -274,7 +329,17 @@ def finish_update(error: str = '') -> None:
|
|||||||
ui_refresh()
|
ui_refresh()
|
||||||
|
|
||||||
def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
|
||||||
return [(v, v, '') for v in version_list.keys()] if version_list else []
|
if not version_list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
compatible_versions = []
|
||||||
|
for v in version_list.keys():
|
||||||
|
for prefix in ALLOWED_VERSION_SERIES:
|
||||||
|
if v.startswith(prefix):
|
||||||
|
compatible_versions.append(v)
|
||||||
|
break
|
||||||
|
|
||||||
|
return [(v, v, '') for v in compatible_versions]
|
||||||
|
|
||||||
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
@@ -287,6 +352,12 @@ def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -
|
|||||||
|
|
||||||
col.separator()
|
col.separator()
|
||||||
|
|
||||||
|
# Show compatibility info
|
||||||
|
col.label(text=f"Update series: {', '.join(s + '.x' for s in ALLOWED_VERSION_SERIES)}", icon='INFO')
|
||||||
|
col.label(text=f"Blender version: {bpy.app.version_string}", icon='BLENDER')
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
|
||||||
# Update check/status section
|
# Update check/status section
|
||||||
if is_checking_for_update:
|
if is_checking_for_update:
|
||||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
|
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -30,8 +30,8 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
|||||||
wm.progress_begin(0, 100)
|
wm.progress_begin(0, 100)
|
||||||
|
|
||||||
# Get both armatures
|
# Get both armatures
|
||||||
base_armature_name: str = context.scene.merge_armature_into
|
base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
|
||||||
merge_armature_name: str = context.scene.merge_armature
|
merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
|
||||||
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
|
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
|
||||||
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
|
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
|
||||||
|
|
||||||
|
|||||||
+19
-3
@@ -126,15 +126,21 @@ class ATOOLKIT_OT_preview_visemes(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
# Check if we're in object mode first
|
# Check if we're in object mode
|
||||||
if context.mode != 'OBJECT':
|
if context.mode != 'OBJECT':
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Get mesh from UI selection
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
||||||
|
|
||||||
|
# Validate armature and mesh
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
valid, _ = validate_armature(armature)
|
valid, _ = validate_armature(armature)
|
||||||
return valid and context.active_object and context.active_object.type == 'MESH'
|
return valid and mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
@@ -179,11 +185,21 @@ class ATOOLKIT_OT_create_visemes(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context: Context) -> bool:
|
def poll(cls, context: Context) -> bool:
|
||||||
|
# Check if we're in object mode
|
||||||
|
if context.mode != 'OBJECT':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get mesh from UI selection
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
||||||
|
|
||||||
|
# Validate armature and mesh
|
||||||
armature = get_active_armature(context)
|
armature = get_active_armature(context)
|
||||||
if not armature:
|
if not armature:
|
||||||
return False
|
return False
|
||||||
valid, _ = validate_armature(armature)
|
valid, _ = validate_armature(armature)
|
||||||
return valid and context.active_object and context.active_object.type == 'MESH'
|
return valid and mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.1)",
|
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.3)",
|
||||||
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
||||||
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
||||||
"AvatarToolkit.desc3": "please report it on our Github.",
|
"AvatarToolkit.desc3": "please report it on our Github.",
|
||||||
@@ -380,6 +380,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",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "アバターツールキット (Alpha 0.1.1)",
|
"AvatarToolkit.label": "アバターツールキット (Alpha 0.1.3)",
|
||||||
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中で",
|
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中で",
|
||||||
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
||||||
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
||||||
@@ -380,6 +380,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": "インターフェース言語を選択",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.1.1)",
|
"AvatarToolkit.label": "아바타 툴킷 (알파 0.1.3)",
|
||||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계입니다",
|
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계입니다",
|
||||||
"AvatarToolkit.desc2": "문제가 발생할 수 있으며, 문제를 발견하시면",
|
"AvatarToolkit.desc2": "문제가 발생할 수 있으며, 문제를 발견하시면",
|
||||||
"AvatarToolkit.desc3": "Github에 보고해 주시기 바랍니다.",
|
"AvatarToolkit.desc3": "Github에 보고해 주시기 바랍니다.",
|
||||||
@@ -380,6 +380,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": "인터페이스 언어 선택",
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -18,7 +18,6 @@ from ..core.common import (
|
|||||||
get_armature_list,
|
get_armature_list,
|
||||||
get_armature_stats
|
get_armature_stats
|
||||||
)
|
)
|
||||||
from ..core.importers.importer import import_types, imports
|
|
||||||
from ..functions.pose_mode import (
|
from ..functions.pose_mode import (
|
||||||
AvatarToolkit_OT_StartPoseMode,
|
AvatarToolkit_OT_StartPoseMode,
|
||||||
AvatarToolkit_OT_StopPoseMode,
|
AvatarToolkit_OT_StopPoseMode,
|
||||||
@@ -26,16 +25,6 @@ from ..functions.pose_mode import (
|
|||||||
AvatarToolkit_OT_ApplyPoseAsRest
|
AvatarToolkit_OT_ApplyPoseAsRest
|
||||||
)
|
)
|
||||||
|
|
||||||
class AvatarToolKit_OT_Import(Operator):
|
|
||||||
"""Import FBX files into Blender with Avatar Toolkit settings"""
|
|
||||||
bl_idname: str = "avatar_toolkit.import"
|
|
||||||
bl_label: str = t("QuickAccess.import")
|
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
|
||||||
clear_default_objects()
|
|
||||||
bpy.ops.import_scene.fbx('INVOKE_DEFAULT', filter_glob=imports)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
class AvatarToolKit_OT_ExportFBX(Operator):
|
class AvatarToolKit_OT_ExportFBX(Operator):
|
||||||
"""Export selected objects as FBX"""
|
"""Export selected objects as FBX"""
|
||||||
bl_idname: str = "avatar_toolkit.export_fbx"
|
bl_idname: str = "avatar_toolkit.export_fbx"
|
||||||
@@ -153,5 +142,3 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
button_row.scale_y = 1.5
|
button_row.scale_y = 1.5
|
||||||
button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT')
|
button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT')
|
||||||
button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT')
|
button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user