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:
@@ -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
|
||||
+25
-17
@@ -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
|
||||
for item in newlist:
|
||||
if item not in material_list_bool.old_list:
|
||||
still_the_same = False
|
||||
break
|
||||
for item in material_list_bool.old_list:
|
||||
if item not in newlist:
|
||||
still_the_same = False
|
||||
break
|
||||
|
||||
material_list_bool.bool_material_list_expand = still_the_same
|
||||
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
|
||||
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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -16,8 +16,8 @@ rather than just duct taping the textures together like material combiner and th
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context: Context) -> set:
|
||||
|
||||
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
||||
@@ -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!")
|
||||
Reference in New Issue
Block a user