Got images working

- does not do UVs yet
- is able to pack images using a split algorithm. I think I broke the size finding though for the output canvas.
- does not combine materials after packing
This commit is contained in:
989onan
2024-07-14 23:55:20 -04:00
parent e875f9192a
commit 942e7e2868
6 changed files with 360 additions and 21 deletions
+151
View File
@@ -0,0 +1,151 @@
# thank you https://stackoverflow.com/a/71432759
from __future__ import annotations
from typing import Optional
from bpy.types import Image
# 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
fit: Rectangle_Obj
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
+19 -11
View File
@@ -6,13 +6,18 @@ from bpy.utils import register_class
class material_list_bool:
old_list: list[Material] = []
bool_material_list_expand: bool = False
#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 = value
material_list_bool.bool_material_list_expand[bpy.context.scene.name] = value
if value == False:
material_list_bool.old_list = []
material_list_bool.old_list[bpy.context.scene.name] = []
def get_bool(self) -> bool:
newlist: list[Material] = []
@@ -24,18 +29,20 @@ class material_list_bool:
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:
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:
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
material_list_bool.bool_material_list_expand = still_the_same
return material_list_bool.bool_material_list_expand
return material_list_bool.bool_material_list_expand[bpy.context.scene.name]
class SceneMatClass(PropertyGroup):
mat: PointerProperty(type=Material)
@@ -48,7 +55,7 @@ def register_properties():
#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.name+"_image",(i.image.name if i.image else "node with no image..."),i.name,index+1) for index,i in enumerate(self.node_tree.nodes) if i.bl_idname == "ShaderNodeTexImage"]
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:
@@ -57,8 +64,9 @@ def register_properties():
return Object.Enum
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)
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)
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)
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)
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)
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)
+1 -1
View File
@@ -32,7 +32,7 @@ def toposort(deps_dict):
unsorted.append(value)
deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted}
sort_order(sorted_list) #to sort by 'bl_order' so we can choose how things may appear in the ui
#sort_order(sorted_list) #to sort by 'bl_order' so we can choose how things may appear in the ui
return sorted_list
+178
View File
@@ -0,0 +1,178 @@
from pathlib import Path
import bpy
import re
import os
from typing import List, Tuple, Optional
from bpy.types import Material, Operator, Context, Object, Image
from ..core.register import register_wrap
from ..core.properties import material_list_bool, SceneMatClass
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)
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)
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)
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)
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)
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)
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])
for type in ["albedo","normal", "emission","ambient_occlusion","height"]:
new_image_name: str= "Atlas_"+type+"_"+bpy.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"))
return {"FINISHED"}
except Exception as e:
raise e
return {"FINISHED"}
+4 -2
View File
@@ -3,6 +3,7 @@ import bpy
from ..core.register import register_wrap
from .panel import AvatarToolkitPanel
from ..core.properties import SceneMatClass, material_list_bool
from ..functions.atlas_materials import Atlas_Materials
@register_wrap
@@ -29,7 +30,7 @@ class ExpandSection_Materials(Operator):
newlist.append(mat_slot.material)
newitem: SceneMatClass = context.scene.materials.add()
newitem.mat = mat_slot.material
material_list_bool.old_list = newlist
material_list_bool.old_list[context.scene.name] = newlist
else:
context.scene.texture_atlas_Has_Mat_List_Shown = False
return {'FINISHED'}
@@ -85,4 +86,5 @@ class TextureAtlasPanel(Panel):
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!")