Merge pull request #34 from 989onan/Texture-Atlasing

Texture Atlasing hot fix
This commit is contained in:
Yusarina
2024-07-25 01:49:57 +01:00
committed by GitHub
5 changed files with 462 additions and 10 deletions
+5 -4
View File
@@ -25,13 +25,14 @@ def register():
# Order the classes before registration # Order the classes before registration
core.register.order_classes() core.register.order_classes()
# Register the properties
core.register.register_properties()
# Register the UI classes # Register the UI classes
for cls in __bl_ordered_classes: for cls in core.register.__bl_ordered_classes:
print("registering" + str(cls)) print("registering " + str(cls))
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
#finally register properties that may use some classes.
core.register.register_properties()
def unregister(): def unregister():
print("Unregistering Avatar Toolkit") print("Unregistering Avatar Toolkit")
# Unregister the UI classes # Unregister the UI classes
+51 -1
View File
@@ -7,8 +7,58 @@ import webbrowser
import typing import typing
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from bpy.types import Object, ShapeKey, Mesh, Context from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup
from functools import lru_cache from functools import lru_cache
from bpy.props import PointerProperty
from bpy.utils import register_class
class SceneMatClass(PropertyGroup):
mat: PointerProperty(type=Material)
register_class(SceneMatClass)
class material_list_bool:
#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:
material_list_bool.bool_material_list_expand[bpy.context.scene.name] = value
if value == False:
material_list_bool.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 material_list_bool.old_list:
for item in newlist:
if item not in material_list_bool.old_list[bpy.context.scene.name]:
still_the_same = False
break
for item in material_list_bool.old_list[bpy.context.scene.name]:
if item not in newlist:
still_the_same = False
break
else:
still_the_same = False
material_list_bool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
return material_list_bool.bool_material_list_expand[bpy.context.scene.name]
### Clean up material names in the given mesh by removing the '.001' suffix. ### Clean up material names in the given mesh by removing the '.001' suffix.
def clean_material_names(mesh: Mesh) -> None: def clean_material_names(mesh: Mesh) -> None:
+35 -4
View File
@@ -1,12 +1,19 @@
import bpy import bpy
from ..functions.translations import t, get_languages_list, update_language from ..functions.translations import t, get_languages_list, update_language
from ..core.register import register_property
from bpy.types import Scene, Object, Material, TextureNode, Context, SceneObjects, PropertyGroup
from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty
from bpy.utils import register_class
from ..core.register import register_wrap
from ..core.addon_preferences import get_preference from ..core.addon_preferences import get_preference
from .common import get_armatures, get_mesh_items from ..core.common import SceneMatClass, material_list_bool, get_armatures, get_mesh_items
def register() -> None: def register() -> None:
default_language = get_preference("language", 0) default_language = get_preference("language", 0)
bpy.types.Scene.avatar_toolkit_language = bpy.props.EnumProperty( bpy.types.Scene.avatar_toolkit_language = bpy.props.EnumProperty(
name=t("Settings.language.label", "Language"), name=t("Settings.language.label", "Language"),
description=t("Settings.language.desc", "Select the language for the addon"), description=t("Settings.language.desc", "Select the language for the addon"),
@@ -49,6 +56,31 @@ def register() -> None:
description="The currently selected armature for Avatar Toolkit operations" description="The currently selected armature for Avatar Toolkit operations"
) )
#happy with how compressed this get_texture_node_list method is - @989onan
def get_texture_node_list(self: Material, context: Context) -> list[set[3]]:
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 = [("ERROR", "THIS MATERIAL HAS NO IMAGES!", "ERROR", 0)]
else:
Object.Enum = [("ERROR", "THIS MATERIAL DOES NOT USE NODES!", "ERROR", 0)]
Object.Enum.append(("None", "None", "None", 0))
return Object.Enum
register_property((Material, "texture_atlas_albedo", EnumProperty(name="Albedo", description="The texture that will be used for the albedo map atlas", default=0, items=get_texture_node_list)))
register_property((Material, "texture_atlas_normal", EnumProperty(name="Normal", description="The texture that will be used for the normal map atlas", default=0, items=get_texture_node_list)))
register_property((Material, "texture_atlas_emission", EnumProperty(name="Emission", description="The texture that will be used for the emission map atlas", default=0, items=get_texture_node_list)))
register_property((Material, "texture_atlas_ambient_occlusion", EnumProperty(name="Ambient Occlusion", description="The texture that will be used for the ambient occlusion map atlas", default=0, items=get_texture_node_list)))
register_property((Material, "texture_atlas_height", EnumProperty(name="Height", description="The texture that will be used for the height map atlas", default=0, items=get_texture_node_list)))
register_property((Material, "texture_atlas_roughness", EnumProperty(name="Roughness", description="The texture that will be used for the roughness map atlas", default=0, items=get_texture_node_list)))
register_property((Scene, "texture_atlas_material_index", IntProperty(default=-1, get=(lambda self : -1), set=(lambda self,context : None))))
register_property((Scene, "materials", CollectionProperty(type=SceneMatClass)))
register_property((Scene, "texture_atlas_Has_Mat_List_Shown", BoolProperty(default=False, get=material_list_bool.get_bool, set=material_list_bool.set_bool)))
def unregister() -> None: def unregister() -> None:
if hasattr(bpy.types.Scene, "avatar_toolkit_language"): if hasattr(bpy.types.Scene, "avatar_toolkit_language"):
del bpy.types.Scene.avatar_toolkit_language del bpy.types.Scene.avatar_toolkit_language
@@ -70,4 +102,3 @@ def unregister() -> None:
if hasattr(bpy.types.Scene, "selected_armature"): if hasattr(bpy.types.Scene, "selected_armature"):
del bpy.types.Scene.selected_armature del bpy.types.Scene.selected_armature
+278
View File
@@ -0,0 +1,278 @@
from pathlib import Path
import numpy
import bpy
import re
import os
from typing import List, Tuple, Optional
from mathutils import Vector
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeNormalMap
from ..core.register import register_wrap
from ..core.common import SceneMatClass, material_list_bool
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
def scale_images_to_largest(images:list[Image]) -> set:
print([image.name for image in images])
x: int=0
y: int=0
for image in images:
x = max(x,image.size[0])
y = max(y,image.size[1])
print(x,y)
for image in images:
image.scale(width=int(x), height=int(y))
return x,y
def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image]:
list_of_images: list[Image] = []
list_of_images.append(classitem.albedo)
list_of_images.append(classitem.normal)
list_of_images.append(classitem.emission)
list_of_images.append(classitem.ambient_occlusion)
list_of_images.append(classitem.height)
list_of_images.append(classitem.roughness)
return list_of_images
def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
mat: SceneMatClass = None
material_image_list: list[MaterialImageList] = []
for mat in context.scene.materials:
new_mat_image_item: MaterialImageList = MaterialImageList()
try:
new_mat_image_item.albedo = bpy.data.images[mat.mat.texture_atlas_albedo]
except Exception as e:
name: str = mat.mat.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.mat.texture_atlas_normal]
except Exception:
name: str = mat.mat.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.mat.texture_atlas_emission]
except Exception:
name: str = mat.mat.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.mat.texture_atlas_ambient_occlusion]
except Exception:
name: str = mat.mat.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.mat.texture_atlas_height]
except Exception:
name: str = mat.mat.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.mat.texture_atlas_roughness]
except Exception:
name: str = mat.mat.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.mat
material_image_list.append(new_mat_image_item)
return material_image_list
def prep_images_in_scene(context: Context) -> list[MaterialImageList]:
preped_images: list[MaterialImageList] = get_material_images_from_scene(context)
for MaterialImageClass in preped_images:
ImageList: list[Image] = MaterialImageList_to_Image_list(MaterialImageClass)
MaterialImageClass.w, MaterialImageClass.h = scale_images_to_largest(ImageList)
return preped_images
@register_wrap
class Atlas_Materials(Operator):
bl_idname = "avatar_toolkit.atlas_materials"
bl_label = "Atlas Materials"
bl_description = "Atlas materials to optimize the model"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.scene.texture_atlas_Has_Mat_List_Shown
def execute(self, context: Context) -> set:
try:
mat_images: list[MaterialImageList] = prep_images_in_scene(context)
packer: BinPacker = BinPacker(mat_images)
mat_images = packer.fit()
size: list[int] = [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])]
print([matimg.fit.w + matimg.albedo.size[1] for matimg in mat_images])
atlased_mat: MaterialImageList = MaterialImageList()
for mat in mat_images:
x: int = int(mat.fit.x)
y: int = int(mat.fit.y)
w: int = int(mat.albedo.size[0])
h: int = int(mat.albedo.size[1])
for obj in bpy.data.objects:
mesh: Mesh = obj.data
for layer in mesh.polygons:
if obj.material_slots[layer.material_index].material:
if obj.material_slots[layer.material_index].material == mat.material:
for loop_idx in layer.loop_indices:
layer_loops: MeshUVLoopLayer
for layer_loops in mesh.uv_layers:
uv_item: Float2AttributeValue = 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])
for type in ["albedo","normal", "emission","ambient_occlusion","height", "roughness"]:
new_image_name: str= "Atlas_"+type+"_"+context.scene.name+"_"+Path(bpy.data.filepath).stem
print("Processing "+type+" atlas image")
if new_image_name in bpy.data.images:
bpy.data.images.remove(bpy.data.images[new_image_name])
canvas: Image = bpy.data.images.new(name=new_image_name, width=int(size[0]),height=int(size[1]), alpha=True)
c_w = canvas.size[0]
#c_h = canvas.size[1]
canvas_pixels: list[float] = list(canvas.pixels[:])
for mat in mat_images:
x: int = int(mat.fit.x)
y: int = int(mat.fit.y)
w: int = int(mat.albedo.size[0])
h: int = int(mat.albedo.size[1])
image_var: Image = eval("mat."+type)
image_pixels: list[float] = list(image_var.pixels[:])
print("writing image \""+image_var.name+"\" to canvas.")
print("x: \""+str(x)+"\" "+"y: \""+str(y)+"\" "+"w: \""+str(w)+"\" "+"h: \""+str(h)+"\" ")
for k in range(0,h):
for i in range(0, w):
for channel in range(0,4):
canvas_pixels[
int((((k+y)*c_w)
+
(i+x))*4)
+int(channel)
] = image_pixels[
int((
(k*w)
+i)*4)
+int(channel)]
canvas.pixels[:] = canvas_pixels[:]
canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath),new_image_name+".png"))
exec("atlased_mat."+type+" = canvas")
atlased_mat.material = bpy.data.materials.new(name="Atlas_Final_"+bpy.context.scene.name+"_"+Path(bpy.data.filepath).stem)
atlased_mat.material.use_nodes = True
atlased_mat.material.node_tree.nodes.clear()
#I am sorry for the amount of nodes I'm instanciating here and their values.
#This is so that the nodes look pretty in the UI, which I think looks kinda nice. - @989onan
principled_node: ShaderNodeBsdfPrincipled = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
principled_node.location.x = 7.29706335067749
principled_node.location.y = 298.918212890625
output_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
output_node.location.x = 297.29705810546875
output_node.location.y = 298.918212890625
albedo_node: ShaderNodeTexImage = 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: ShaderNodeTexImage = 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: ShaderNodeTexImage = 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: ShaderNodeNormalMap = 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: ShaderNodeTexImage = 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: ShaderNodeTexImage = 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: ShaderNodeTexImage = 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"])
for obj in context.scene.objects:
mesh: Mesh = obj.data
mesh.materials.clear()
mesh.materials.append(atlased_mat.material)
return {"FINISHED"}
except Exception as e:
raise e
return {"FINISHED"}
+92
View File
@@ -0,0 +1,92 @@
from bpy.types import UIList, Panel, UILayout, Object, Context,Material, Operator
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolkitPanel
from ..core.common import SceneMatClass, material_list_bool
from ..functions.atlas_materials import Atlas_Materials
@register_wrap
class ExpandSection_Materials(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.texture_atlas_Has_Mat_List_Shown:
context.scene.materials.clear()
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)
newitem: SceneMatClass = context.scene.materials.add()
newitem.mat = mat_slot.material
material_list_bool.old_list[context.scene.name] = newlist
else:
context.scene.texture_atlas_Has_Mat_List_Shown = False
return {'FINISHED'}
@register_wrap
class MaterialTextureAtlasProperties(UIList):
bl_label = "Texture Atlas Material List Material"
bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list_mat"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def draw_item(self , context: Context, layout: UILayout, data: bpy.types.Object, item:SceneMatClass, icon, active_data, active_propname, index):
if context.scene.texture_atlas_Has_Mat_List_Shown:
box = layout.box()
row = box.row()
row.label(text=item.mat.name, icon = "MATERIAL")
col = box.row()
col.prop(item.mat, "texture_atlas_albedo")
col = box.row()
col.prop(item.mat, "texture_atlas_normal")
col = box.row()
col.prop(item.mat, "texture_atlas_emission")
col = box.row()
col.prop(item.mat, "texture_atlas_ambient_occlusion")
col = box.row()
col.prop(item.mat, "texture_atlas_height")
col = box.row()
col.prop(item.mat, "texture_atlas_roughness")
@register_wrap
class TextureAtlasPanel(Panel):
bl_label = "Texture Atlasing"
bl_idname = "OBJECT_PT_avatar_toolkit_texture_atlas"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
bl_parent_id = "OBJECT_PT_avatar_toolkit"
def draw(self, context: Context):
layout = self.layout
row = layout.row()
boxoutter = row.box()
direction_icon = 'RIGHTARROW' if not context.scene.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
row = boxoutter.row()
row.operator(ExpandSection_Materials.bl_idname, text=("Reload Texture Atlas Material List" if not context.scene.texture_atlas_Has_Mat_List_Shown else "Loaded Texture Atlas Material List"), icon=direction_icon)
if context.scene.texture_atlas_Has_Mat_List_Shown:
#get_texture_node_list(bpy.context)
row = boxoutter.row()
row.template_list(MaterialTextureAtlasProperties.bl_idname, 'material_list', context.scene, 'materials',
context.scene, 'texture_atlas_material_index', rows=12, type='DEFAULT')
row = layout.row()
row.operator(Atlas_Materials.bl_idname, text="Atlas Materials!")