feat: Add KoikatsuBlenderPipeline addon with import/export functionality
- Add core addon files (__init__.py, KKPanel.py, preferences.py, common.py) - Add import pipeline with armature, mesh, and material modification modules - Add export pipeline with material baking and FBX preparation utilities - Add material combiner tool for texture atlas generation and optimization - Add extras utilities for animation, rigging, and asset management - Add bone orientation data from better_fbx for accurate skeletal structure - Add comprehensive documentation and wiki with multi-language support (EN, JP, ZH) - Add animation library retargeting lists for ARP and Rokoko motion capture - Add Rigify integration scripts for advanced rigging workflows - Add shader file (KK Shader V8.0.blend) for material rendering - Add manifest and license files for addon distribution - Add changelog documenting version history and improvements - Initialize complete Blender addon project for Koikatsu character import/export
This commit is contained in:
21
exporting/material_combiner/LICENSE
Normal file
21
exporting/material_combiner/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 shotariya
|
||||
|
||||
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.
|
||||
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/combiner.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/combiner.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/globs.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/globs.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/images.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/images.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/objects.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/objects.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/packer.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/packer.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/textures.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/textures.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
126
exporting/material_combiner/combine_list.py
Normal file
126
exporting/material_combiner/combine_list.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
from typing import Set
|
||||
from typing import cast
|
||||
|
||||
import bpy
|
||||
from bpy.props import *
|
||||
|
||||
from . import globs
|
||||
from .type_annotations import CombineListData
|
||||
from .type_annotations import Scene
|
||||
from .materials import get_materials
|
||||
|
||||
|
||||
class RefreshObData(bpy.types.Operator):
|
||||
bl_idname = 'kkbp.refresh_ob_data'
|
||||
bl_label = 'Combine List'
|
||||
bl_description = 'Updates the material list'
|
||||
|
||||
@staticmethod
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
scn = context.scene
|
||||
ob_list = [ob for ob in context.visible_objects if
|
||||
ob.type == 'MESH' and ob.data.uv_layers.active and ob.data.materials]
|
||||
combine_list_data = self._cache_previous_values(scn)
|
||||
self._rebuild_items_list(scn, ob_list, combine_list_data)
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def _cache_previous_values(scn: Scene) -> CombineListData:
|
||||
combine_list_data = cast(CombineListData, defaultdict(lambda: {
|
||||
'used': True,
|
||||
'mats': defaultdict(lambda: {
|
||||
'used': True,
|
||||
'layer': 1,
|
||||
}),
|
||||
}))
|
||||
|
||||
for item in scn.kkbp_ob_data:
|
||||
if item.type == globs.CL_OBJECT:
|
||||
combine_list_data[item.ob]['used'] = item.used
|
||||
elif item.type == globs.CL_MATERIAL:
|
||||
mat_data = combine_list_data[item.ob]['mats'][item.mat]
|
||||
mat_data.update({'used': item.used, 'layer': item.layer})
|
||||
return combine_list_data
|
||||
|
||||
def _rebuild_items_list(self, scn: Scene, ob_list: Set[bpy.types.Object],
|
||||
combine_list_data: CombineListData) -> None:
|
||||
scn.kkbp_ob_data.clear()
|
||||
|
||||
for ob_id, ob in enumerate(ob_list):
|
||||
ob_data = combine_list_data[ob]
|
||||
ob_used = ob_data['used']
|
||||
self._create_ob_item(scn, ob, ob_id, ob_used)
|
||||
|
||||
for mat in get_materials(ob):
|
||||
if globs.is_blender_3_or_newer and not mat.preview:
|
||||
mat.preview_ensure()
|
||||
|
||||
mat_data = ob_data['mats'][mat]
|
||||
mat_used = ob_used and mat_data['used']
|
||||
mat_layer = mat_data['layer']
|
||||
self._create_mat_item(scn, ob, ob_id, mat, mat_used, mat_layer)
|
||||
self._create_separator_item(scn)
|
||||
|
||||
@staticmethod
|
||||
def _create_ob_item(scn: Scene, ob: bpy.types.Object, ob_id: int, used: bool) -> None:
|
||||
item = scn.kkbp_ob_data.add()
|
||||
item.ob = ob
|
||||
item.ob_id = ob_id
|
||||
item.type = 0
|
||||
item.used = used
|
||||
|
||||
@staticmethod
|
||||
def _create_mat_item(scn: Scene, ob: bpy.types.Object, ob_id: int, mat: bpy.types.Material, used: bool,
|
||||
layer: int) -> None:
|
||||
item = scn.kkbp_ob_data.add()
|
||||
item.ob = ob
|
||||
item.ob_id = ob_id
|
||||
item.mat = mat
|
||||
item.type = 1
|
||||
item.used = used
|
||||
item.layer = layer
|
||||
|
||||
@staticmethod
|
||||
def _create_separator_item(scn: Scene) -> None:
|
||||
item = scn.kkbp_ob_data.add()
|
||||
item.type = 2
|
||||
|
||||
|
||||
class CombineSwitch(bpy.types.Operator):
|
||||
bl_idname = 'kkbp.combine_switch'
|
||||
bl_label = 'Add Item'
|
||||
bl_description = 'Selected materials will be combined into one texture atlas'
|
||||
|
||||
list_id = IntProperty(default=0)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
scn = context.scene
|
||||
data = scn.kkbp_ob_data
|
||||
item = data[self.list_id]
|
||||
if item.type == globs.CL_OBJECT:
|
||||
self._switch_ob_state(data, item)
|
||||
elif item.type == globs.CL_MATERIAL:
|
||||
self._switch_mat_state(data, item)
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def _switch_ob_state(data: List[bpy.types.PropertyGroup], item: bpy.types.PropertyGroup) -> None:
|
||||
mat_list = [mat for mat in data if mat.ob_id == item.ob_id and mat.type == globs.CL_MATERIAL]
|
||||
if not mat_list:
|
||||
return
|
||||
|
||||
item.used = not item.used
|
||||
for mat in mat_list:
|
||||
mat.used = item.used
|
||||
|
||||
@staticmethod
|
||||
def _switch_mat_state(data: List[bpy.types.PropertyGroup], item: bpy.types.PropertyGroup) -> None:
|
||||
ob = next((ob for ob in data if ob.ob_id == item.ob_id and ob.type == globs.CL_OBJECT), None)
|
||||
if not ob:
|
||||
return
|
||||
|
||||
if not item.used:
|
||||
ob.used = True
|
||||
item.used = not item.used
|
||||
71
exporting/material_combiner/combiner.py
Normal file
71
exporting/material_combiner/combiner.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import bpy
|
||||
from bpy.props import *
|
||||
from .combiner_ops import *
|
||||
from .packer import BinPacker
|
||||
from ... import common as c
|
||||
|
||||
class Combiner(bpy.types.Operator):
|
||||
bl_idname = 'kkbp.combiner'
|
||||
bl_label = 'Create Atlas'
|
||||
bl_description = 'Combine materials'
|
||||
bl_options = {'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
#from invoke
|
||||
scn = context.scene
|
||||
bpy.ops.kkbp.refresh_ob_data()
|
||||
for index, object in enumerate([o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'MESH' and not o.hide_get()]):
|
||||
#check if this object is worth doing anything with
|
||||
if not [mat_slot.material for mat_slot in object.material_slots if mat_slot.material.get('simple')]:
|
||||
continue
|
||||
|
||||
set_ob_mode(context.view_layer, scn.kkbp_ob_data)
|
||||
self.data = get_data(scn.kkbp_ob_data, object)
|
||||
self.mats_uv = get_mats_uv(scn, self.data)
|
||||
clear_empty_mats(scn, self.data, self.mats_uv)
|
||||
get_duplicates(self.mats_uv)
|
||||
self.structure = get_structure(scn, self.data, self.mats_uv)
|
||||
|
||||
#from execute
|
||||
scn.kkbp_save_path = os.path.join(context.scene.kkbp.import_dir, 'atlas_files')
|
||||
self.structure = BinPacker(get_size(scn, self.structure)).fit()
|
||||
|
||||
size = get_atlas_size(self.structure)
|
||||
atlas_size = calculate_adjusted_size(scn, size)
|
||||
|
||||
if max(atlas_size, default=0) > 20000:
|
||||
text = 'The output image size of {0}x{1}px is too large'.format(*atlas_size)
|
||||
c.kklog(text)
|
||||
self.report({'ERROR'}, text)
|
||||
return {'FINISHED'}
|
||||
|
||||
bake_types = []
|
||||
if scn.kkbp.bake_light_bool:
|
||||
bake_types.append('light')
|
||||
if scn.kkbp.bake_dark_bool:
|
||||
bake_types.append('dark')
|
||||
if scn.kkbp.bake_norm_bool:
|
||||
bake_types.append('normal')
|
||||
|
||||
for type in bake_types:
|
||||
#replace all images
|
||||
for material in [mat_slot.material for mat_slot in object.material_slots if mat_slot.material.get('simple')]:
|
||||
image = material.node_tree.nodes['textures'].node_tree.nodes[type].image
|
||||
if image:
|
||||
if image.name == 'Template: Placeholder':
|
||||
image = None
|
||||
if not image:
|
||||
continue
|
||||
else:
|
||||
material.node_tree.nodes['Image Texture'].image = image
|
||||
|
||||
#then run the atlas creation
|
||||
atlas = get_atlas(scn, self.structure, atlas_size)
|
||||
comb_mats = get_comb_mats(scn, atlas, self.mats_uv, type, index)
|
||||
c.print_timer(f'save atlas for {object.name} {type}')
|
||||
|
||||
align_uvs(scn, self.structure, atlas.size, size)
|
||||
bpy.ops.kkbp.refresh_ob_data()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
482
exporting/material_combiner/combiner_ops.py
Normal file
482
exporting/material_combiner/combiner_ops.py
Normal file
@@ -0,0 +1,482 @@
|
||||
import io
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
from typing import cast
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
from ... import common as c
|
||||
from . import globs
|
||||
from .type_annotations import CombMats
|
||||
from .type_annotations import Diffuse
|
||||
from .type_annotations import MatsUV
|
||||
from .type_annotations import ObMats
|
||||
from .type_annotations import SMCObData
|
||||
from .type_annotations import SMCObDataItem
|
||||
from .type_annotations import Scene
|
||||
from .type_annotations import Structure
|
||||
from .type_annotations import StructureItem
|
||||
from .images import get_packed_file
|
||||
from .materials import get_diffuse
|
||||
from .materials import get_shader_type
|
||||
from .materials import shader_image_nodes
|
||||
from .materials import sort_materials
|
||||
from .objects import align_uv
|
||||
from .objects import get_polys
|
||||
from .objects import get_uv
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
ImageType = Image.Image
|
||||
except ImportError:
|
||||
Image = None
|
||||
ImageType = None
|
||||
|
||||
try:
|
||||
from PIL import ImageChops
|
||||
except ImportError:
|
||||
ImageChops = None
|
||||
|
||||
try:
|
||||
from PIL import ImageFile
|
||||
except ImportError:
|
||||
ImageFile = None
|
||||
|
||||
if Image:
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
try:
|
||||
resampling = Image.LANCZOS
|
||||
except AttributeError:
|
||||
resampling = Image.ANTIALIAS
|
||||
|
||||
if ImageFile:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
atlas_prefix = 'atlas_'
|
||||
atlas_texture_prefix = 'texture_atlas_'
|
||||
atlas_material_prefix = 'material_atlas_'
|
||||
|
||||
|
||||
def set_ob_mode(scn: Scene, data: SMCObData) -> None:
|
||||
scn.objects.active = bpy.data.objects['Body ' + c.get_name() + '.001']
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
|
||||
def get_data(data: Sequence[bpy.types.PropertyGroup], object) -> SMCObData:
|
||||
mats = defaultdict(dict)
|
||||
if object.type == 'MESH':
|
||||
for mat in [m for m in object.data.materials if 'Outline ' not in m.name]:
|
||||
mats[object.name][mat] = 1 #layer, just set to always 1
|
||||
return mats
|
||||
|
||||
|
||||
def get_mats_uv(scn: Scene, data: SMCObData) -> MatsUV:
|
||||
mats_uv = defaultdict(lambda: defaultdict(list))
|
||||
for ob_n, item in data.items():
|
||||
ob = scn.objects[ob_n]
|
||||
for idx, polys in get_polys(ob).items():
|
||||
mat = ob.data.materials[idx]
|
||||
if mat not in item:
|
||||
continue
|
||||
for poly in polys:
|
||||
mats_uv[ob_n][mat].extend(align_uv(get_uv(ob, poly)))
|
||||
return mats_uv
|
||||
|
||||
|
||||
def clear_empty_mats(scn: Scene, data: SMCObData, mats_uv: MatsUV) -> None:
|
||||
for ob_n, item in data.items():
|
||||
ob = scn.objects[ob_n]
|
||||
for mat in item:
|
||||
if mat not in mats_uv[ob_n]:
|
||||
_delete_material(ob, mat.name)
|
||||
|
||||
|
||||
def _delete_material(ob: bpy.types.Object, mat_name: str) -> None:
|
||||
ob_mats = ob.data.materials
|
||||
mat_idx = ob_mats.find(mat_name)
|
||||
if mat_idx > -1:
|
||||
ob_mats.pop(index=mat_idx)
|
||||
|
||||
|
||||
def get_duplicates(mats_uv: MatsUV) -> None:
|
||||
mat_list = list(chain.from_iterable(mats_uv.values()))
|
||||
sorted_mat_list = sort_materials(mat_list)
|
||||
for mats in sorted_mat_list:
|
||||
kkbp_root_mat = mats[0]
|
||||
for mat in mats[1:]:
|
||||
mat.kkbp_root_mat = kkbp_root_mat
|
||||
|
||||
|
||||
def get_structure(scn: Scene, data: SMCObData, mats_uv: MatsUV) -> Structure:
|
||||
structure = defaultdict(lambda: {
|
||||
'gfx': {
|
||||
'img_or_color': None,
|
||||
'size': (),
|
||||
'uv_size': ()
|
||||
},
|
||||
'dup': [],
|
||||
'ob': [],
|
||||
'uv': []
|
||||
})
|
||||
|
||||
for ob_n, item in data.items():
|
||||
ob = scn.objects[ob_n]
|
||||
for mat in item:
|
||||
if mat.name not in ob.data.materials:
|
||||
continue
|
||||
kkbp_root_mat = mat.kkbp_root_mat or mat
|
||||
if mat.kkbp_root_mat and mat.name not in structure[kkbp_root_mat]['dup']:
|
||||
structure[kkbp_root_mat]['dup'].append(mat.name)
|
||||
if ob.name not in structure[kkbp_root_mat]['ob']:
|
||||
structure[kkbp_root_mat]['ob'].append(ob.name)
|
||||
structure[kkbp_root_mat]['uv'].extend(mats_uv[ob_n][mat])
|
||||
return structure
|
||||
|
||||
|
||||
def clear_duplicates(scn: Scene, data: Structure) -> None:
|
||||
for item in data.values():
|
||||
for ob_n in item['ob']:
|
||||
ob = scn.objects[ob_n]
|
||||
for dup_name in item['dup']:
|
||||
_delete_material(ob, dup_name)
|
||||
|
||||
|
||||
def get_size(scn: Scene, data: Structure) -> Dict:
|
||||
for mat, item in data.items():
|
||||
img = _get_image(mat)
|
||||
packed_file = get_packed_file(img)
|
||||
max_x, max_y = _get_max_uv_coordinates(item['uv'])
|
||||
item['gfx']['uv_size'] = (np.clip(max_x, 1, 25), np.clip(max_y, 1, 25))
|
||||
|
||||
if not scn.kkbp_crop:
|
||||
item['gfx']['uv_size'] = tuple(math.ceil(x) for x in item['gfx']['uv_size'])
|
||||
|
||||
if packed_file:
|
||||
img_size = _get_image_size(mat, img)
|
||||
item['gfx']['size'] = _calculate_size(img_size, item['gfx']['uv_size'], scn.kkbp_gaps)
|
||||
else:
|
||||
item['gfx']['size'] = (scn.kkbp_diffuse_size + scn.kkbp_gaps,) * 2
|
||||
|
||||
return OrderedDict(sorted(data.items(), key=_size_sorting, reverse=True))
|
||||
|
||||
|
||||
def _size_sorting(item: Sequence[StructureItem]) -> Tuple[int, int, int, Union[str, Diffuse, None]]:
|
||||
gfx = item[1]['gfx']
|
||||
size_x, size_y = gfx['size']
|
||||
|
||||
img_or_color = gfx['img_or_color']
|
||||
name_or_color = None
|
||||
if isinstance(img_or_color, tuple):
|
||||
name_or_color = gfx['img_or_color']
|
||||
elif isinstance(img_or_color, bpy.types.PackedFile):
|
||||
name_or_color = img_or_color.id_data.name
|
||||
|
||||
return max(size_x, size_y), size_x * size_y, size_x, name_or_color
|
||||
|
||||
|
||||
def _get_image(mat: bpy.types.Material) -> Union[bpy.types.Image, None]:
|
||||
shader = get_shader_type(mat) if mat else None
|
||||
node = mat.node_tree.nodes.get(shader_image_nodes.get(shader, ''))
|
||||
return node.image if node else None
|
||||
|
||||
|
||||
def _get_image_size(mat: bpy.types.Material, img: bpy.types.Image) -> Tuple[int, int]:
|
||||
return (
|
||||
(
|
||||
min(mat.kkbp_size_width, img.size[0]),
|
||||
min(mat.kkbp_size_height, img.size[1]),
|
||||
)
|
||||
if mat.kkbp_size
|
||||
else cast(Tuple[int, int], img.size)
|
||||
)
|
||||
|
||||
|
||||
def _get_max_uv_coordinates(uv_loops: List[bpy.types.MeshUVLoop]) -> Tuple[float, float]:
|
||||
max_x = 1
|
||||
max_y = 1
|
||||
|
||||
for uv in uv_loops:
|
||||
if not math.isnan(uv.x):
|
||||
max_x = max(max_x, uv.x)
|
||||
if not math.isnan(uv.y):
|
||||
max_y = max(max_y, uv.y)
|
||||
|
||||
return max_x, max_y
|
||||
|
||||
|
||||
def _calculate_size(img_size: Tuple[int, int], uv_size: Tuple[int, int], gaps: int) -> Tuple[int, int]:
|
||||
return cast(Tuple[int, int], tuple(s * uv_s + gaps for s, uv_s in zip(img_size, uv_size)))
|
||||
|
||||
|
||||
def get_atlas_size(structure: Structure) -> Tuple[int, int]:
|
||||
max_x = 1
|
||||
max_y = 1
|
||||
|
||||
for item in structure.values():
|
||||
max_x = max(max_x, item['gfx']['fit']['x'] + item['gfx']['size'][0])
|
||||
max_y = max(max_y, item['gfx']['fit']['y'] + item['gfx']['size'][1])
|
||||
|
||||
return int(max_x), int(max_y)
|
||||
|
||||
|
||||
def calculate_adjusted_size(scn: Scene, size: Tuple[int, int]) -> Tuple[int, int]:
|
||||
if scn.kkbp_size == 'PO2':
|
||||
return cast(Tuple[int, int], tuple(1 << int(x - 1).bit_length() for x in size))
|
||||
elif scn.kkbp_size == 'QUAD':
|
||||
return (int(max(size)),) * 2
|
||||
return size
|
||||
|
||||
|
||||
def get_atlas(scn: Scene, data: Structure, atlas_size: Tuple[int, int]) -> ImageType:
|
||||
#create new atlas image
|
||||
kkbp_size = (scn.kkbp_size_width, scn.kkbp_size_height)
|
||||
img = Image.new('RGBA', atlas_size)
|
||||
half_gaps = int(scn.kkbp_gaps / 2)
|
||||
|
||||
#for every material in data items,
|
||||
for mat, item in data.items():
|
||||
_set_image_or_color(item, mat)
|
||||
_paste_gfx(scn, item, mat, img, half_gaps)
|
||||
|
||||
if scn.kkbp_size in ['CUST', 'STRICTCUST']:
|
||||
img.thumbnail(kkbp_size, resampling)
|
||||
|
||||
if scn.kkbp_size == 'STRICTCUST':
|
||||
canvas_img = Image.new('RGBA', kkbp_size)
|
||||
canvas_img.paste(img)
|
||||
return canvas_img
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def _set_image_or_color(item: StructureItem, mat: bpy.types.Material) -> None:
|
||||
shader = get_shader_type(mat) if mat else None
|
||||
node_name = shader_image_nodes.get(shader)
|
||||
item['gfx']['img_or_color'] = get_packed_file(mat.node_tree.nodes.get(node_name).image) if node_name else None
|
||||
|
||||
if not item['gfx']['img_or_color']:
|
||||
item['gfx']['img_or_color'] = get_diffuse(mat)
|
||||
|
||||
|
||||
def _paste_gfx(scn: Scene, item: StructureItem, mat: bpy.types.Material, img: ImageType, half_gaps: int) -> None:
|
||||
if not item['gfx']['fit']:
|
||||
return
|
||||
|
||||
img.paste(
|
||||
_get_gfx(scn, mat, item, item['gfx']['img_or_color']),
|
||||
(int(item['gfx']['fit']['x'] + half_gaps), int(item['gfx']['fit']['y'] + half_gaps))
|
||||
)
|
||||
|
||||
|
||||
def _get_gfx(scn: Scene, mat: bpy.types.Material, item: StructureItem,
|
||||
img_or_color: Union[bpy.types.PackedFile, Tuple, None]) -> ImageType:
|
||||
size = cast(Tuple[int, int], tuple(int(size - scn.kkbp_gaps) for size in item['gfx']['size']))
|
||||
|
||||
if not img_or_color:
|
||||
return Image.new('RGBA', size, (1, 1, 1, 1))
|
||||
|
||||
if isinstance(img_or_color, tuple):
|
||||
return Image.new('RGBA', size, img_or_color)
|
||||
|
||||
img = Image.open(io.BytesIO(img_or_color.data))
|
||||
if img.size != size:
|
||||
img.resize(size, resampling)
|
||||
if mat.kkbp_size:
|
||||
img.thumbnail((mat.kkbp_size_width, mat.kkbp_size_height), resampling)
|
||||
if max(item['gfx']['uv_size'], default=0) > 1:
|
||||
img = _get_uv_image(item, img, size)
|
||||
if mat.kkbp_diffuse:
|
||||
diffuse_img = Image.new(img.mode, size, get_diffuse(mat))
|
||||
img = ImageChops.multiply(img, diffuse_img)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def _get_uv_image(item: StructureItem, img: ImageType, size: Tuple[int, int]) -> ImageType:
|
||||
uv_img = Image.new('RGBA', size)
|
||||
size_height = size[1]
|
||||
img_width, img_height = img.size
|
||||
uv_width, uv_height = (math.ceil(x) for x in item['gfx']['uv_size'])
|
||||
|
||||
for h in range(uv_height):
|
||||
y = size_height - img_height - h * img_height
|
||||
for w in range(uv_width):
|
||||
x = w * img_width
|
||||
uv_img.paste(img, (x, y))
|
||||
|
||||
return uv_img
|
||||
|
||||
|
||||
def align_uvs(scn: Scene, data: Structure, atlas_size: Tuple[int, int], size: Tuple[int, int]) -> None:
|
||||
size_width, size_height = size
|
||||
|
||||
scaled_width, scaled_height = _get_scale_factors(atlas_size, size)
|
||||
|
||||
margin = scn.kkbp_gaps + (0 if scn.kkbp_pixel_art else 2)
|
||||
border_margin = int(scn.kkbp_gaps / 2) + (0 if scn.kkbp_pixel_art else 1)
|
||||
|
||||
for item in data.values():
|
||||
gfx_size = item['gfx']['size']
|
||||
gfx_height = gfx_size[1]
|
||||
|
||||
gfx_width_margin, gfx_height_margin = (x - margin for x in gfx_size)
|
||||
|
||||
uv_width, uv_height = item['gfx']['uv_size']
|
||||
|
||||
x_offset = item['gfx']['fit']['x'] + border_margin
|
||||
y_offset = item['gfx']['fit']['y'] - border_margin
|
||||
|
||||
for uv in item['uv']:
|
||||
reset_x = uv.x / uv_width * gfx_width_margin
|
||||
reset_y = uv.y / uv_height * gfx_height_margin - gfx_height
|
||||
|
||||
uv_x = (reset_x + x_offset) / size_width
|
||||
uv_y = (reset_y - y_offset) / size_height
|
||||
|
||||
uv.x = uv_x * scaled_width
|
||||
uv.y = uv_y * scaled_height + 1
|
||||
|
||||
|
||||
def _get_scale_factors(atlas_size: Tuple[int, int], size: Tuple[int, int]) -> Tuple[float, float]:
|
||||
scaled_factors = tuple(x / y for x, y in zip(size, atlas_size))
|
||||
|
||||
if all(factor <= 1 for factor in scaled_factors):
|
||||
return cast(Tuple[float, float], scaled_factors)
|
||||
|
||||
atlas_width, atlas_height = atlas_size
|
||||
size_width, size_height = size
|
||||
|
||||
aspect_ratio = (size_width * atlas_height) / (size_height * atlas_width)
|
||||
return (1, 1 / aspect_ratio) if aspect_ratio > 1 else (aspect_ratio, 1)
|
||||
|
||||
|
||||
def get_comb_mats(scn: Scene, atlas: ImageType, mats_uv: MatsUV, type: str, atlas_index) -> CombMats:
|
||||
layers = _get_layers(scn, mats_uv)
|
||||
path = _save_atlas(scn, atlas, atlas_index, type)
|
||||
texture = _create_texture(path, atlas_index)
|
||||
return cast(CombMats, {idx: _create_material(texture, atlas_index, idx) for idx in layers})
|
||||
|
||||
|
||||
def _get_layers(scn: Scene, mats_uv: MatsUV) -> Set[int]:
|
||||
return {}
|
||||
|
||||
|
||||
def _get_unique_id(scn: Scene) -> str:
|
||||
existed_ids = set()
|
||||
_add_its_from_existing_materials(scn, existed_ids)
|
||||
|
||||
if not os.path.isdir(scn.kkbp_save_path):
|
||||
return _generate_random_unique_id(existed_ids)
|
||||
|
||||
_add_ids_from_existing_files(scn, existed_ids)
|
||||
unique_id = next(x for x in itertools.count(start=1) if x not in existed_ids)
|
||||
return '{:05d}'.format(unique_id)
|
||||
|
||||
|
||||
def _add_its_from_existing_materials(scn: Scene, existed_ids: Set[int]) -> None:
|
||||
atlas_material_pattern = re.compile(r'{0}(\d+)_\d+'.format(atlas_material_prefix))
|
||||
for item in scn.kkbp_ob_data:
|
||||
if item.type != globs.CL_MATERIAL:
|
||||
continue
|
||||
|
||||
match = atlas_material_pattern.fullmatch(item.mat.name)
|
||||
if match:
|
||||
existed_ids.add(int(match.group(1)))
|
||||
|
||||
|
||||
def _generate_random_unique_id(existed_ids: Set[int]) -> str:
|
||||
unused_ids = set(range(10000, 99999)) - existed_ids
|
||||
return str(random.choice(list(unused_ids)))
|
||||
|
||||
|
||||
def _add_ids_from_existing_files(scn: Scene, existed_ids: Set[int]) -> None:
|
||||
atlas_file_pattern = re.compile(r'{0}(\d+).png'.format(atlas_prefix))
|
||||
for file_name in os.listdir(scn.kkbp_save_path):
|
||||
match = atlas_file_pattern.fullmatch(file_name)
|
||||
if match:
|
||||
existed_ids.add(int(match.group(1)))
|
||||
|
||||
|
||||
def _save_atlas(scn: Scene, atlas: ImageType, atlas_index: str, type: str) -> str:
|
||||
path = os.path.join(scn.kkbp_save_path, f'{atlas_index}_{type}.png')
|
||||
try:
|
||||
atlas.save(path)
|
||||
except:
|
||||
#atlas folder didn't exist
|
||||
os.mkdir(scn.kkbp_save_path)
|
||||
atlas.save(path)
|
||||
return path
|
||||
|
||||
|
||||
def _create_texture(path: str, unique_id: str) -> bpy.types.Texture:
|
||||
texture = bpy.data.textures.new('{0}{1}'.format(atlas_texture_prefix, unique_id), 'IMAGE')
|
||||
image = bpy.data.images.load(path)
|
||||
texture.image = image
|
||||
return texture
|
||||
|
||||
|
||||
def _create_material(texture: bpy.types.Texture, unique_id: str, idx: int) -> bpy.types.Material:
|
||||
mat = bpy.data.materials.new(name='{0}{1}_{2}'.format(atlas_material_prefix, unique_id, idx))
|
||||
_configure_material(mat, texture)
|
||||
return mat
|
||||
|
||||
|
||||
def _configure_material(mat: bpy.types.Material, texture: bpy.types.Texture) -> None:
|
||||
mat['atlas'] = True
|
||||
mat.blend_method = 'CLIP'
|
||||
mat.use_backface_culling = True
|
||||
mat.use_nodes = True
|
||||
|
||||
node_texture = mat.node_tree.nodes.new(type='ShaderNodeTexImage')
|
||||
node_texture.image = texture.image
|
||||
node_texture.label = 'Material Combiner Texture'
|
||||
node_texture.location = -300, 300
|
||||
|
||||
mat.node_tree.links.new(node_texture.outputs['Color'],
|
||||
mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'])
|
||||
mat.node_tree.links.new(node_texture.outputs['Alpha'],
|
||||
mat.node_tree.nodes['Principled BSDF'].inputs['Alpha'])
|
||||
|
||||
def assign_comb_mats(scn: Scene, data: SMCObData, comb_mats: CombMats) -> None:
|
||||
for ob_n, item in data.items():
|
||||
ob = scn.objects[ob_n]
|
||||
ob_materials = ob.data.materials
|
||||
_assign_mats(item, comb_mats, ob_materials)
|
||||
_assign_mats_to_polys(item, comb_mats, ob, ob_materials)
|
||||
|
||||
|
||||
def _assign_mats(item: SMCObDataItem, comb_mats: CombMats, ob_materials: ObMats) -> None:
|
||||
for idx in set(item.values()):
|
||||
if idx in comb_mats:
|
||||
ob_materials.append(comb_mats[idx])
|
||||
|
||||
|
||||
def _assign_mats_to_polys(item: SMCObDataItem, comb_mats: CombMats, ob: bpy.types.Object, ob_materials: ObMats) -> None:
|
||||
for idx, polys in get_polys(ob).items():
|
||||
if ob_materials[idx] not in item:
|
||||
continue
|
||||
mat_name = comb_mats[item[ob_materials[idx]]].name
|
||||
mat_idx = ob_materials.find(mat_name)
|
||||
for poly in polys:
|
||||
poly.material_index = mat_idx
|
||||
|
||||
|
||||
def clear_mats(scn: Scene, mats_uv: MatsUV) -> None:
|
||||
for ob_n, item in mats_uv.items():
|
||||
ob = scn.objects[ob_n]
|
||||
for mat in item:
|
||||
_delete_material(ob, mat.name)
|
||||
141
exporting/material_combiner/extend_types.py
Normal file
141
exporting/material_combiner/extend_types.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import bpy
|
||||
from bpy.props import *
|
||||
|
||||
class CombineList(bpy.types.PropertyGroup):
|
||||
ob = PointerProperty(
|
||||
name='Current Object',
|
||||
type=bpy.types.Object,
|
||||
)
|
||||
ob_id = IntProperty(default=0)
|
||||
mat = PointerProperty(
|
||||
name='Current Object Material',
|
||||
type=bpy.types.Material,
|
||||
)
|
||||
layer = IntProperty(
|
||||
name='Material Layers',
|
||||
description='Materials with the same number will be merged together.'
|
||||
'\nUse this to create multiple materials linked to the same atlas file',
|
||||
min=1,
|
||||
max=99,
|
||||
step=1,
|
||||
default=1,
|
||||
)
|
||||
used = BoolProperty(default=True)
|
||||
type = IntProperty(default=0)
|
||||
|
||||
def register_smc_types():
|
||||
bpy.types.Scene.kkbp_ob_data = CollectionProperty(type=CombineList)
|
||||
bpy.types.Scene.kkbp_ob_data_id = IntProperty(default=0)
|
||||
bpy.types.Scene.kkbp_list_id = IntProperty(default=0)
|
||||
bpy.types.Scene.kkbp_size = EnumProperty(
|
||||
name='Atlas size',
|
||||
items=[
|
||||
('PO2', 'Power of 2', 'Combined image size is power of 2'),
|
||||
('QUAD', 'Quadratic', 'Combined image has same width and height'),
|
||||
('AUTO', 'Automatic', 'Combined image has minimal size'),
|
||||
('CUST', 'Custom', 'Combined image has proportionally scaled to fit in custom size'),
|
||||
('STRICTCUST', 'Strict Custom', 'Combined image has exact custom width and height'),
|
||||
],
|
||||
description='Select atlas size',
|
||||
default='QUAD',
|
||||
)
|
||||
bpy.types.Scene.kkbp_size_width = IntProperty(
|
||||
name='Max width (px)',
|
||||
description='Select max width for combined image',
|
||||
min=8,
|
||||
max=8192,
|
||||
step=1,
|
||||
default=4096,
|
||||
)
|
||||
bpy.types.Scene.kkbp_size_height = IntProperty(
|
||||
name='Max height (px)',
|
||||
description='Select max height for combined image',
|
||||
min=8,
|
||||
max=8192,
|
||||
step=1,
|
||||
default=4096,
|
||||
)
|
||||
bpy.types.Scene.kkbp_crop = BoolProperty(
|
||||
name='Crop outside images by UV',
|
||||
description='Crop images by UV if materials UV outside of bounds',
|
||||
default=True,
|
||||
)
|
||||
bpy.types.Scene.kkbp_pixel_art = BoolProperty(
|
||||
name='Pixel Art / Small Textures',
|
||||
description='Avoids 1-pixel UV scaling for small textures.'
|
||||
'\nDisable for larger textures to avoid blending with nearby pixels',
|
||||
default=False,
|
||||
)
|
||||
bpy.types.Scene.kkbp_diffuse_size = IntProperty(
|
||||
name='Size of materials without image',
|
||||
description='Select the size of materials that only consist of a color',
|
||||
min=8,
|
||||
max=256,
|
||||
step=1,
|
||||
default=32,
|
||||
)
|
||||
bpy.types.Scene.kkbp_gaps = IntProperty(
|
||||
name='Size of gaps between images',
|
||||
description='Select size of gaps between images',
|
||||
min=0,
|
||||
max=32,
|
||||
step=200,
|
||||
default=0,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
bpy.types.Scene.kkbp_save_path = StringProperty(
|
||||
description='Select the directory in which the generated texture atlas will be saved',
|
||||
default='',
|
||||
)
|
||||
|
||||
bpy.types.Material.kkbp_root_mat = PointerProperty(
|
||||
name='Material Root',
|
||||
type=bpy.types.Material,
|
||||
)
|
||||
bpy.types.Material.kkbp_diffuse = BoolProperty(
|
||||
name='Multiply image with diffuse color',
|
||||
description='Multiply the materials image with its diffuse color.'
|
||||
'\nINFO: If this color is white the final image will be the same',
|
||||
default=True,
|
||||
)
|
||||
bpy.types.Material.kkbp_size = BoolProperty(
|
||||
name='Custom image size',
|
||||
description='Select the max size for this materials image in the texture atlas',
|
||||
default=False,
|
||||
)
|
||||
bpy.types.Material.kkbp_size_width = IntProperty(
|
||||
name='Max width (px)',
|
||||
description='Select max width for material image',
|
||||
min=8,
|
||||
max=8192,
|
||||
step=1,
|
||||
default=2048,
|
||||
)
|
||||
bpy.types.Material.kkbp_size_height = IntProperty(
|
||||
name='Max height (px)',
|
||||
description='Select max height for material image',
|
||||
min=8,
|
||||
max=8192,
|
||||
step=1,
|
||||
default=2048,
|
||||
)
|
||||
|
||||
|
||||
def unregister_smc_types() -> None:
|
||||
del bpy.types.Scene.kkbp_ob_data
|
||||
del bpy.types.Scene.kkbp_ob_data_id
|
||||
del bpy.types.Scene.kkbp_list_id
|
||||
del bpy.types.Scene.kkbp_size
|
||||
del bpy.types.Scene.kkbp_size_width
|
||||
del bpy.types.Scene.kkbp_size_height
|
||||
del bpy.types.Scene.kkbp_crop
|
||||
del bpy.types.Scene.kkbp_pixel_art
|
||||
del bpy.types.Scene.kkbp_diffuse_size
|
||||
del bpy.types.Scene.kkbp_gaps
|
||||
del bpy.types.Scene.kkbp_save_path
|
||||
|
||||
del bpy.types.Material.kkbp_root_mat
|
||||
del bpy.types.Material.kkbp_diffuse
|
||||
del bpy.types.Material.kkbp_size
|
||||
del bpy.types.Material.kkbp_size_width
|
||||
del bpy.types.Material.kkbp_size_height
|
||||
30
exporting/material_combiner/get_pillow.py
Normal file
30
exporting/material_combiner/get_pillow.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Set
|
||||
from . import globs
|
||||
from ... import common as c
|
||||
from ...interface.dictionary_en import t
|
||||
import bpy
|
||||
|
||||
class InstallPIL(bpy.types.Operator):
|
||||
bl_idname = 'kkbp.get_pillow'
|
||||
bl_label = 'Install Pillow'
|
||||
bl_description = t('pillow_tt')
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
try:
|
||||
from PIL import Image, ImageChops
|
||||
except ImportError:
|
||||
self._install_pillow()
|
||||
|
||||
globs.pil_exist = 'restart'
|
||||
|
||||
self.report({'INFO'}, 'Installation complete')
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def _install_pillow() -> None:
|
||||
from pip import _internal
|
||||
_internal.main(['install', 'pip', 'setuptools', 'wheel', '-U', '--user'])
|
||||
_internal.main(['install', 'Pillow', '--user'])
|
||||
23
exporting/material_combiner/globs.py
Normal file
23
exporting/material_combiner/globs.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import bpy
|
||||
import sys
|
||||
import site
|
||||
|
||||
sys.path.insert(0, site.getusersitepackages())
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import ImageChops
|
||||
pil_exist = 'yup'
|
||||
except ImportError:
|
||||
pil_exist = 'no'
|
||||
|
||||
is_blender_2_79_or_older = bpy.app.version < (2, 80, 0)
|
||||
is_blender_2_80_or_newer = bpy.app.version >= (2, 80, 0)
|
||||
is_blender_2_92_or_newer = bpy.app.version >= (2, 92, 0)
|
||||
is_blender_3_or_newer = bpy.app.version >= (3, 0, 0)
|
||||
|
||||
smc_pi = False
|
||||
|
||||
CL_OBJECT = 0
|
||||
CL_MATERIAL = 1
|
||||
CL_SEPARATOR = 2
|
||||
19
exporting/material_combiner/images.py
Normal file
19
exporting/material_combiner/images.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def get_image(tex: bpy.types.Texture) -> bpy.types.Image:
|
||||
return tex.image if tex and hasattr(tex, 'image') and tex.image else None
|
||||
|
||||
|
||||
def get_packed_file(image: Union[bpy.types.Image, None]) -> Union[bpy.types.PackedFile, None]:
|
||||
if image and not image.packed_file and _get_image_path(image):
|
||||
image.pack()
|
||||
return image.packed_file if image and image.packed_file else None
|
||||
|
||||
|
||||
def _get_image_path(img: Union[bpy.types.Image, None]) -> Union[str, None]:
|
||||
path = os.path.abspath(bpy.path.abspath(img.filepath)) if img else ''
|
||||
return path if os.path.isfile(path) and not path.lower().endswith(('.spa', '.sph')) else None
|
||||
131
exporting/material_combiner/materials.py
Normal file
131
exporting/material_combiner/materials.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from collections import OrderedDict
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
from typing import Union
|
||||
from typing import ValuesView
|
||||
from typing import cast
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
from .images import get_image
|
||||
from .images import get_packed_file
|
||||
from .textures import get_texture
|
||||
from . import globs
|
||||
from .type_annotations import Diffuse
|
||||
from .type_annotations import MatDict
|
||||
from .type_annotations import MatDictItem
|
||||
|
||||
shader_types = OrderedDict([
|
||||
('mmd', {'mmd_shader', 'mmd_base_tex'}),
|
||||
('mmdCol', {'mmd_shader'}),
|
||||
('mtoon', {'Mtoon1BaseColorTexture.Image'}),
|
||||
('mtoonCol', {'Mtoon1Material.Mtoon1Output'}),
|
||||
('principled', {'Principled BSDF', 'Image Texture'}),
|
||||
('principledCol', {'Principled BSDF'}),
|
||||
('diffuse', {'Diffuse BSDF', 'Image Texture'}),
|
||||
('diffuseCol', {'Diffuse BSDF'}),
|
||||
('emission', {'Emission', 'Image Texture'}),
|
||||
('emissionCol', {'Emission'}),
|
||||
])
|
||||
|
||||
shader_image_nodes = {
|
||||
'mmd': 'mmd_base_tex',
|
||||
'mtoon': 'Mtoon1BaseColorTexture.Image',
|
||||
'vrm': 'Image Texture',
|
||||
'xnalara': 'Image Texture',
|
||||
'principled': 'Image Texture',
|
||||
'diffuse': 'Image Texture',
|
||||
'emission': 'Image Texture',
|
||||
}
|
||||
|
||||
|
||||
def get_materials(ob: bpy.types.Object) -> List[bpy.types.Material]:
|
||||
return [mat_slot.material for mat_slot in ob.material_slots if mat_slot.material]
|
||||
|
||||
|
||||
def get_shader_type(mat: bpy.types.Material) -> Union[str, None]:
|
||||
if not mat.node_tree or not mat.node_tree.nodes:
|
||||
return
|
||||
|
||||
node_tree = mat.node_tree.nodes
|
||||
|
||||
if 'Group' in node_tree:
|
||||
node_tree_name = node_tree['Group'].node_tree.name
|
||||
if node_tree_name == 'Group':
|
||||
return 'xnalaraNewCol'
|
||||
if node_tree_name == 'MToon_unversioned':
|
||||
return 'vrm' if 'Image Texture' in node_tree else 'vrmCol'
|
||||
elif node_tree_name == 'XPS Shader' and 'Image Texture' in node_tree:
|
||||
return 'xnalara'
|
||||
|
||||
node_names_set = set(node_tree.keys())
|
||||
return next(
|
||||
(
|
||||
shader_type
|
||||
for shader_type, node_names in shader_types.items()
|
||||
if node_names.issubset(node_names_set)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def sort_materials(mat_list: List[bpy.types.Material]) -> ValuesView[MatDictItem]:
|
||||
for mat in bpy.data.materials:
|
||||
mat.kkbp_root_mat = None
|
||||
|
||||
mat_dict = cast(MatDict, defaultdict(list))
|
||||
for mat in mat_list:
|
||||
node_tree = mat.node_tree if mat else None
|
||||
|
||||
packed_file = None
|
||||
|
||||
if globs.is_blender_2_79_or_older:
|
||||
packed_file = get_packed_file(get_image(get_texture(mat)))
|
||||
elif node_tree:
|
||||
shader = get_shader_type(mat)
|
||||
node_name = shader_image_nodes.get(shader)
|
||||
if node_name:
|
||||
packed_file = get_packed_file(node_tree.nodes[node_name].image)
|
||||
|
||||
if packed_file:
|
||||
mat_dict[(packed_file, get_diffuse(mat) if mat.kkbp_diffuse else None)].append(mat)
|
||||
else:
|
||||
mat_dict[get_diffuse(mat)].append(mat)
|
||||
|
||||
return mat_dict.values()
|
||||
|
||||
|
||||
def rgb_to_255_scale(diffuse: Diffuse) -> Diffuse:
|
||||
rgb = np.empty(shape=(0,), dtype=int)
|
||||
for c in diffuse:
|
||||
if c < 0.0:
|
||||
srgb = 0
|
||||
elif c < 0.0031308:
|
||||
srgb = c * 12.92
|
||||
else:
|
||||
srgb = 1.055 * pow(c, 1.0 / 2.4) - 0.055
|
||||
rgb = np.append(rgb, np.clip(round(srgb * 255), 0, 255))
|
||||
return tuple(rgb)
|
||||
|
||||
|
||||
def get_diffuse(mat: bpy.types.Material) -> Diffuse:
|
||||
if globs.is_blender_2_79_or_older:
|
||||
return rgb_to_255_scale(mat.diffuse_color)
|
||||
|
||||
shader = get_shader_type(mat) if mat else False
|
||||
if shader == 'mmdCol':
|
||||
return rgb_to_255_scale(mat.node_tree.nodes['mmd_shader'].inputs['Diffuse Color'].default_value)
|
||||
elif shader == 'mtoonCol':
|
||||
return rgb_to_255_scale(mat.node_tree.nodes['Mtoon1PbrMetallicRoughness.BaseColorFactor'].color)
|
||||
elif shader == 'vrm':
|
||||
return rgb_to_255_scale(mat.node_tree.nodes['RGB'].outputs[0].default_value)
|
||||
elif shader == 'vrmCol':
|
||||
return rgb_to_255_scale(mat.node_tree.nodes['Group'].inputs[10].default_value)
|
||||
elif shader == 'diffuseCol':
|
||||
return rgb_to_255_scale(mat.node_tree.nodes['Diffuse BSDF'].inputs['Color'].default_value)
|
||||
elif shader == 'xnalaraNewCol':
|
||||
return rgb_to_255_scale(mat.node_tree.nodes['Group'].inputs['Diffuse'].default_value)
|
||||
elif shader in ['principledCol', 'xnalaraCol']:
|
||||
return rgb_to_255_scale(mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value)
|
||||
return 255, 255, 255
|
||||
38
exporting/material_combiner/objects.py
Normal file
38
exporting/material_combiner/objects.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from typing import List, Dict
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
def get_polys(ob: bpy.types.Object) -> Dict[int, bpy.types.MeshPolygon]:
|
||||
polys = defaultdict(list)
|
||||
for poly in ob.data.polygons:
|
||||
polys[poly.material_index].append(poly)
|
||||
return polys
|
||||
|
||||
|
||||
def get_uv(ob: bpy.types.Object, poly: bpy.types.MeshPolygon) -> List[Vector]:
|
||||
data = ob.data.uv_layers.active.data
|
||||
return [data[loop_idx].uv if loop_idx < len(data) else Vector((0, 0, 0)) for loop_idx in poly.loop_indices]
|
||||
|
||||
|
||||
def align_uv(face_uv: List[Vector]) -> List[Vector]:
|
||||
min_x = float('inf')
|
||||
min_y = float('inf')
|
||||
|
||||
for uv in face_uv:
|
||||
if not math.isnan(uv.x):
|
||||
min_x = min(min_x, uv.x)
|
||||
if not math.isnan(uv.y):
|
||||
min_y = min(min_y, uv.y)
|
||||
|
||||
min_x = math.floor(min_x)
|
||||
min_y = math.floor(min_y)
|
||||
|
||||
if min_x != 0 or min_y != 0:
|
||||
for uv in face_uv:
|
||||
uv.x -= min_x
|
||||
uv.y -= min_y
|
||||
return face_uv
|
||||
96
exporting/material_combiner/packer.py
Normal file
96
exporting/material_combiner/packer.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# 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.
|
||||
|
||||
from typing import Union
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class BinPacker(object):
|
||||
def __init__(self, images: Dict) -> None:
|
||||
self.root = {}
|
||||
self.bin = images
|
||||
|
||||
def fit(self) -> Dict:
|
||||
self.root = {'x': 0, 'y': 0, 'w': 0, 'h': 0}
|
||||
|
||||
if not self.bin:
|
||||
return self.bin
|
||||
|
||||
self.root['w'], self.root['h'] = next(iter(self.bin.values()))['gfx']['size']
|
||||
|
||||
for img in self.bin.values():
|
||||
w, h = img['gfx']['size']
|
||||
node = self.find_node(self.root, w, h)
|
||||
img['gfx']['fit'] = self.split_node(node, w, h) if node else self.grow_node(w, h)
|
||||
|
||||
return self.bin
|
||||
|
||||
def find_node(self, root: Dict, w: int, h: int) -> Union[Dict, None]:
|
||||
if 'used' in root and root['used']:
|
||||
return self.find_node(root['right'], w, h) or self.find_node(root['down'], w, h)
|
||||
elif w <= root['w'] and h <= root['h']:
|
||||
return root
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def split_node(node: Dict, w: int, h: int) -> Dict:
|
||||
node['used'] = True
|
||||
node['down'] = {'x': node['x'], 'y': node['y'] + h, 'w': node['w'], 'h': node['h'] - h}
|
||||
node['right'] = {'x': node['x'] + w, 'y': node['y'], 'w': node['w'] - w, 'h': h}
|
||||
return node
|
||||
|
||||
def grow_node(self, w: int, h: int) -> Union[Dict, None]:
|
||||
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 or not should_grow_down and can_grow_right:
|
||||
return self.grow_right(w, h)
|
||||
elif should_grow_down or can_grow_down:
|
||||
return self.grow_down(w, h)
|
||||
return None
|
||||
|
||||
def grow_right(self, w: int, h: int) -> Union[Dict, None]:
|
||||
self.root = {
|
||||
'used': True,
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'w': self.root['w'] + w,
|
||||
'h': self.root['h'],
|
||||
'down': self.root,
|
||||
'right': {'x': self.root['w'], 'y': 0, 'w': w, 'h': self.root['h']}
|
||||
}
|
||||
node = self.find_node(self.root, w, h)
|
||||
return self.split_node(node, w, h) if node else None
|
||||
|
||||
def grow_down(self, w: int, h: int) -> Union[Dict, None]:
|
||||
self.root = {
|
||||
'used': True,
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'w': self.root['w'],
|
||||
'h': self.root['h'] + h,
|
||||
'down': {'x': 0, 'y': self.root['h'], 'w': self.root['w'], 'h': h},
|
||||
'right': self.root
|
||||
}
|
||||
node = self.find_node(self.root, w, h)
|
||||
return self.split_node(node, w, h) if node else None
|
||||
13
exporting/material_combiner/textures.py
Normal file
13
exporting/material_combiner/textures.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import Dict
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def get_texture(mat: bpy.types.Material) -> bpy.types.Texture:
|
||||
return next((slot.texture for idx, slot in enumerate(mat.texture_slots) if
|
||||
slot is not None and mat.use_textures[idx]), None)
|
||||
|
||||
|
||||
def get_textures(mat: bpy.types.Material) -> Dict[int, bpy.types.Texture]:
|
||||
return {idx: slot.texture for idx, slot in enumerate(mat.texture_slots) if
|
||||
slot is not None and mat.use_textures[idx]}
|
||||
39
exporting/material_combiner/type_annotations.py
Normal file
39
exporting/material_combiner/type_annotations.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import DefaultDict
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
from . import globs
|
||||
|
||||
BlClasses = Union[
|
||||
bpy.types.Panel, bpy.types.Operator, bpy.types.PropertyGroup, bpy.types.AddonPreferences, bpy.types.UIList
|
||||
]
|
||||
|
||||
# SMCIcons = Union[bpy.utils.previews.ImagePreviewCollection, Dict[str, bpy.types.ImagePreview], None]
|
||||
|
||||
Scene = bpy.types.ViewLayer if globs.is_blender_2_80_or_newer else bpy.types.Scene
|
||||
|
||||
SMCObDataItem = Dict[bpy.types.Material, int]
|
||||
SMCObData = Dict[str, SMCObDataItem]
|
||||
|
||||
MatsUV = Dict[str, DefaultDict[bpy.types.Material, List[Vector]]]
|
||||
|
||||
StructureItem = Dict[str, Union[List, Dict[str, Union[Dict[str, int], Tuple, bpy.types.PackedFile, None]]]]
|
||||
Structure = Dict[bpy.types.Material, StructureItem]
|
||||
|
||||
ObMats = Union[bpy.types.bpy_prop_collection, List[bpy.types.Material]]
|
||||
|
||||
CombMats = Dict[int, bpy.types.Material]
|
||||
|
||||
MatDictItem = List[bpy.types.Material]
|
||||
MatDict = DefaultDict[Tuple, MatDictItem]
|
||||
|
||||
CombineListDataMat = Dict[str, Union[int, bool]]
|
||||
CombineListDataItem = Dict[str, Union[Dict[bpy.types.Material, CombineListDataMat], bool]]
|
||||
CombineListData = Dict[bpy.types.Object, CombineListDataItem]
|
||||
|
||||
Diffuse = Union[bpy.types.bpy_prop_collection, Tuple[float, float, float], Tuple[int, int, int]]
|
||||
Reference in New Issue
Block a user