Merge pull request #2030 from Naxela/master

Update lightmapper to version 0.4
This commit is contained in:
Lubos Lenco 2020-11-30 11:18:26 +01:00 committed by GitHub
commit 67e2b0171b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 4949 additions and 424 deletions

View file

@ -1,21 +1,20 @@
import bpy
#from .. operators import build
#from .. operators import clean
tlm_keymaps = []
def register():
pass
# winman = bpy.context.window_manager
# keyman = winman.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW")
# keyman.keymap_items.new(build.TLM_BuildLightmaps.bl_idname, type='F6', value='PRESS')
# keyman.keymap_items.new(clean.TLM_CleanLightmaps.bl_idname, type='F7', value='PRESS')
# tlm_keymaps.append(keyman)
if not bpy.app.background:
winman = bpy.context.window_manager
keyman = winman.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW")
keyman.keymap_items.new('tlm.build_lightmaps', type='F6', value='PRESS')
keyman.keymap_items.new('tlm.clean_lightmaps', type='F7', value='PRESS')
tlm_keymaps.append(keyman)
def unregister():
pass
# winman = bpy.context.window_manager
# for keyman in tlm_keymaps:
# winman.keyconfigs.addon.keymaps.remove(keyman)
# del tlm_keymaps[:]
winman = bpy.context.window_manager
for keyman in tlm_keymaps:
winman.keyconfigs.addon.keymaps.remove(keyman)
del tlm_keymaps[:]

View file

@ -0,0 +1,27 @@
import socket, json, os
def connect_client(machine, port, blendpath, obj_num):
# Create a socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to the remote host and port
sock.connect((machine, port))
#0: Blendpath,
#1: For all designated objects, run from 0 to number; 0 indicates all
args = [blendpath, obj_num]
command = json.dumps({'call':1, 'command':1, 'args':args})
# Send a request to the host
sock.send((command).encode())
# Get the host's response, no more than, say, 1,024 bytes
response_data = sock.recv(1024)
print(response_data.decode())
# Terminate
sock.close()

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python3
import bpy, socket, json, subprocess, os, platform, subprocess, select
def startServer():
active = True
baking = False
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('localhost', 9898))
sock.listen(1)
print("Server started")
while active:
connection,address = sock.accept()
data = connection.recv(1024)
if data:
parsed_data = json.loads(data.decode())
if parsed_data["call"] == 0: #Ping
print("Pinged by: " + str(connection.getsockname()))
connection.sendall(("Ping callback").encode())
elif parsed_data["call"] == 1: #Command
if parsed_data["command"] == 0: #Shutdown
print("Server shutdown")
active = False
if parsed_data["command"] == 1: #Baking
print("Baking...")
args = parsed_data["args"]
blenderpath = bpy.app.binary_path
if not baking:
baking = True
pipe = subprocess.Popen([blenderpath, "-b", str(args[0]), "--python-expr", 'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=True, stdout=subprocess.PIPE)
stdout = pipe.communicate()[0]
print("Baking finished...")
active = False
else:
print("Request denied, server busy...")
print("Data received: " + data.decode())
connection.send(('Callback from: ' + str(socket.gethostname())).encode())
connection.close()
print("Connection closed.")
sock.close()
print("Server closed.")

View file

@ -1,6 +1,6 @@
import bpy
from bpy.utils import register_class, unregister_class
from . import tlm, installopencv
from . import tlm, installopencv, imagetools
classes = [
tlm.TLM_BuildLightmaps,
@ -9,7 +9,20 @@ classes = [
tlm.TLM_EnableSelection,
tlm.TLM_DisableSelection,
tlm.TLM_RemoveLightmapUV,
installopencv.TLM_Install_OpenCV
tlm.TLM_SelectLightmapped,
installopencv.TLM_Install_OpenCV,
tlm.TLM_AtlasListNewItem,
tlm.TLM_AtlastListDeleteItem,
tlm.TLM_AtlasListMoveItem,
tlm.TLM_PostAtlasListNewItem,
tlm.TLM_PostAtlastListDeleteItem,
tlm.TLM_PostAtlasListMoveItem,
tlm.TLM_StartServer,
tlm.TLM_BuildEnvironmentProbes,
tlm.TLM_CleanBuildEnvironmentProbes,
imagetools.TLM_ImageUpscale,
imagetools.TLM_ImageDownscale
]
def register():

View file

@ -0,0 +1,25 @@
import bpy, os, time
class TLM_ImageUpscale(bpy.types.Operator):
bl_idname = "tlm.image_upscale"
bl_label = "Upscale image"
bl_description = "Upscales the image to double resolution"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
print("Upscale")
return {'RUNNING_MODAL'}
class TLM_ImageDownscale(bpy.types.Operator):
bl_idname = "tlm.image_downscale"
bl_label = "Downscale image"
bl_description = "Downscales the image to double resolution"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
print("Downscale")
return {'RUNNING_MODAL'}

View file

@ -19,6 +19,8 @@ class TLM_Install_OpenCV(bpy.types.Operator):
scene = context.scene
cycles = bpy.data.scenes[scene.name].cycles
print("Module OpenCV")
pythonbinpath = bpy.app.binary_path_python
if platform.system() == "Windows":
@ -29,10 +31,12 @@ class TLM_Install_OpenCV(bpy.types.Operator):
ensurepippath = os.path.join(pythonlibpath, "ensurepip")
cmda = [pythonbinpath, ensurepippath, "--upgrade", "--user"]
pip = subprocess.run(cmda, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pip = subprocess.run(cmda)
cmdc = [pythonbinpath, "-m", "pip", "install", "--upgrade", "pip"]
pipc = subprocess.run(cmdc)
if pip.returncode == 0:
print("Successfully installed pip!\n")
print("Sucessfully installed pip!\n")
else:
try:
@ -44,18 +48,27 @@ class TLM_Install_OpenCV(bpy.types.Operator):
if not module_pip:
print("Failed to install pip!\n")
ShowMessageBox("Failed to install pip - Please start Blender as administrator", "Restart", 'PREFERENCES')
if platform.system() == "Windows":
ShowMessageBox("Failed to install pip - Please start Blender as administrator", "Restart", 'PREFERENCES')
else:
ShowMessageBox("Failed to install pip - Try starting Blender with SUDO", "Restart", 'PREFERENCES')
return{'FINISHED'}
cmdb = [pythonbinpath, "-m", "pip", "install", "opencv-python"]
opencv = subprocess.run(cmdb, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#opencv = subprocess.run(cmdb, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
opencv = subprocess.run(cmdb)
if opencv.returncode == 0:
print("Sucessfully installed OpenCV!\n")
print("Successfully installed OpenCV!\n")
else:
print("Failed to install OpenCV!\n")
ShowMessageBox("Failed to install opencv - Please start Blender as administrator", "Restart", 'PREFERENCES')
if platform.system() == "Windows":
ShowMessageBox("Failed to install opencv - Please start Blender as administrator", "Restart", 'PREFERENCES')
else:
ShowMessageBox("Failed to install opencv - Try starting Blender with SUDO", "Restart", 'PREFERENCES')
return{'FINISHED'}
module_opencv = True

View file

@ -1,6 +1,8 @@
import bpy, os, time, blf, webbrowser
import bpy, os, time, blf, webbrowser, platform
import math, subprocess, multiprocessing
from .. utility import build
from .. utility.cycles import cache
from .. network import server
class TLM_BuildLightmaps(bpy.types.Operator):
bl_idname = "tlm.build_lightmaps"
@ -73,6 +75,45 @@ class TLM_CleanLightmaps(bpy.types.Operator):
if image.name.endswith("_baked"):
bpy.data.images.remove(image, do_unlink=True)
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_postpack_object:
atlas = obj.TLM_ObjectProperties.tlm_postatlas_pointer
atlas_resize = False
for atlasgroup in scene.TLM_PostAtlasList:
if atlasgroup.name == atlas:
atlas_resize = True
if atlas_resize:
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
#print(x)
uv_layers = obj.data.uv_layers
for i in range(0, len(uv_layers)):
if uv_layers[i].name == 'UVMap_Lightmap':
uv_layers.active_index = i
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Lightmap shift A")
break
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.pack_islands(rotate=False, margin=0.001)
bpy.ops.uv.select_all(action='DESELECT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
#print(obj.name + ": Active UV: " + obj.data.uv_layers[obj.data.uv_layers.active_index].name)
print("Resized for obj: " + obj.name)
return {'FINISHED'}
class TLM_ExploreLightmaps(bpy.types.Operator):
@ -92,12 +133,23 @@ class TLM_ExploreLightmaps(bpy.types.Operator):
filepath = bpy.data.filepath
dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir)
if platform.system() != "Linux":
if os.path.isdir(dirpath):
webbrowser.open('file://' + dirpath)
if os.path.isdir(dirpath):
webbrowser.open('file://' + dirpath)
else:
os.mkdir(dirpath)
webbrowser.open('file://' + dirpath)
else:
os.mkdir(dirpath)
webbrowser.open('file://' + dirpath)
if os.path.isdir(dirpath):
os.system('xdg-open "%s"' % dirpath)
#webbrowser.open('file://' + dirpath)
else:
os.mkdir(dirpath)
os.system('xdg-open "%s"' % dirpath)
#webbrowser.open('file://' + dirpath)
return {'FINISHED'}
@ -118,7 +170,13 @@ class TLM_EnableSelection(bpy.types.Operator):
if scene.TLM_SceneProperties.tlm_override_object_settings:
obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution
obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode
obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_margin = scene.TLM_SceneProperties.tlm_mesh_unwrap_margin
obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = scene.TLM_SceneProperties.tlm_mesh_unwrap_margin
obj.TLM_ObjectProperties.tlm_postpack_object = scene.TLM_SceneProperties.tlm_postpack_object
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
obj.TLM_ObjectProperties.tlm_atlas_pointer = scene.TLM_SceneProperties.tlm_atlas_pointer
obj.TLM_ObjectProperties.tlm_postatlas_pointer = scene.TLM_SceneProperties.tlm_postatlas_pointer
return{'FINISHED'}
@ -153,4 +211,494 @@ class TLM_RemoveLightmapUV(bpy.types.Operator):
if uvlayer.name == "UVMap_Lightmap":
uv_layers.remove(uvlayer)
return{'FINISHED'}
return{'FINISHED'}
class TLM_SelectLightmapped(bpy.types.Operator):
"""Select all objects for lightmapping"""
bl_idname = "tlm.select_lightmapped_objects"
bl_label = "Select lightmap objects"
bl_description = "Remove Lightmap UV for selection"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
obj.select_set(True)
return{'FINISHED'}
class TLM_AtlasListNewItem(bpy.types.Operator):
# Add a new item to the list
bl_idname = "tlm_atlaslist.new_item"
bl_label = "Add a new item"
bl_description = "Create a new AtlasGroup"
def execute(self, context):
scene = context.scene
scene.TLM_AtlasList.add()
scene.TLM_AtlasListItem = len(scene.TLM_AtlasList) - 1
scene.TLM_AtlasList[len(scene.TLM_AtlasList) - 1].name = "AtlasGroup"
return{'FINISHED'}
class TLM_PostAtlasListNewItem(bpy.types.Operator):
# Add a new item to the list
bl_idname = "tlm_postatlaslist.new_item"
bl_label = "Add a new item"
bl_description = "Create a new AtlasGroup"
bl_description = ""
def execute(self, context):
scene = context.scene
scene.TLM_PostAtlasList.add()
scene.TLM_PostAtlasListItem = len(scene.TLM_PostAtlasList) - 1
scene.TLM_PostAtlasList[len(scene.TLM_PostAtlasList) - 1].name = "AtlasGroup"
return{'FINISHED'}
class TLM_AtlastListDeleteItem(bpy.types.Operator):
# Delete the selected item from the list
bl_idname = "tlm_atlaslist.delete_item"
bl_label = "Deletes an item"
bl_description = "Delete an AtlasGroup"
@classmethod
def poll(self, context):
""" Enable if there's something in the list """
scene = context.scene
return len(scene.TLM_AtlasList) > 0
def execute(self, context):
scene = context.scene
list = scene.TLM_AtlasList
index = scene.TLM_AtlasListItem
for obj in bpy.data.objects:
atlasName = scene.TLM_AtlasList[index].name
if obj.TLM_ObjectProperties.tlm_atlas_pointer == atlasName:
obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = "SmartProject"
list.remove(index)
if index > 0:
index = index - 1
scene.TLM_AtlasListItem = index
return{'FINISHED'}
class TLM_PostAtlastListDeleteItem(bpy.types.Operator):
# Delete the selected item from the list
bl_idname = "tlm_postatlaslist.delete_item"
bl_label = "Deletes an item"
bl_description = "Delete an AtlasGroup"
@classmethod
def poll(self, context):
""" Enable if there's something in the list """
scene = context.scene
return len(scene.TLM_PostAtlasList) > 0
def execute(self, context):
scene = context.scene
list = scene.TLM_PostAtlasList
index = scene.TLM_PostAtlasListItem
for obj in bpy.data.objects:
atlasName = scene.TLM_PostAtlasList[index].name
if obj.TLM_ObjectProperties.tlm_atlas_pointer == atlasName:
obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = "SmartProject"
list.remove(index)
if index > 0:
index = index - 1
scene.TLM_PostAtlasListItem = index
return{'FINISHED'}
class TLM_AtlasListMoveItem(bpy.types.Operator):
# Move an item in the list
bl_idname = "tlm_atlaslist.move_item"
bl_label = "Move an item in the list"
bl_description = "Move an item in the list"
direction: bpy.props.EnumProperty(
items=(
('UP', 'Up', ""),
('DOWN', 'Down', ""),))
def move_index(self):
# Move index of an item render queue while clamping it
scene = context.scene
index = scene.TLM_AtlasListItem
list_length = len(scene.TLM_AtlasList) - 1
new_index = 0
if self.direction == 'UP':
new_index = index - 1
elif self.direction == 'DOWN':
new_index = index + 1
new_index = max(0, min(new_index, list_length))
scene.TLM_AtlasList.move(index, new_index)
scene.TLM_AtlasListItem = new_index
def execute(self, context):
scene = context.scene
list = scene.TLM_AtlasList
index = scene.TLM_AtlasListItem
if self.direction == 'DOWN':
neighbor = index + 1
self.move_index()
elif self.direction == 'UP':
neighbor = index - 1
self.move_index()
else:
return{'CANCELLED'}
return{'FINISHED'}
class TLM_PostAtlasListMoveItem(bpy.types.Operator):
# Move an item in the list
bl_idname = "tlm_postatlaslist.move_item"
bl_label = "Move an item in the list"
bl_description = "Move an item in the list"
direction: bpy.props.EnumProperty(
items=(
('UP', 'Up', ""),
('DOWN', 'Down', ""),))
def move_index(self):
# Move index of an item render queue while clamping it
scene = context.scene
index = scene.TLM_PostAtlasListItem
list_length = len(scene.TLM_PostAtlasList) - 1
new_index = 0
if self.direction == 'UP':
new_index = index - 1
elif self.direction == 'DOWN':
new_index = index + 1
new_index = max(0, min(new_index, list_length))
scene.TLM_PostAtlasList.move(index, new_index)
scene.TLM_PostAtlasListItem = new_index
def execute(self, context):
scene = context.scene
list = scene.TLM_PostAtlasList
index = scene.TLM_PostAtlasListItem
if self.direction == 'DOWN':
neighbor = index + 1
self.move_index()
elif self.direction == 'UP':
neighbor = index - 1
self.move_index()
else:
return{'CANCELLED'}
return{'FINISHED'}
class TLM_StartServer(bpy.types.Operator):
bl_idname = "tlm.start_server"
bl_label = "Start Network Server"
bl_description = "Start Network Server"
bl_options = {'REGISTER', 'UNDO'}
def modal(self, context, event):
#Add progress bar from 0.15
print("MODAL")
return {'PASS_THROUGH'}
def invoke(self, context, event):
server.startServer()
return {'RUNNING_MODAL'}
class TLM_BuildEnvironmentProbes(bpy.types.Operator):
bl_idname = "tlm.build_environmentprobe"
bl_label = "Build Environment Probes"
bl_description = "Build all environment probes from reflection cubemaps"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
for obj in bpy.data.objects:
if obj.type == "LIGHT_PROBE":
if obj.data.type == "CUBEMAP":
cam_name = "EnvPCam_" + obj.name
camera = bpy.data.cameras.new(cam_name)
camobj_name = "EnvPCamera_" + obj.name
cam_obj = bpy.data.objects.new(camobj_name, camera)
bpy.context.collection.objects.link(cam_obj)
cam_obj.location = obj.location
camera.angle = math.radians(90)
prevResx = bpy.context.scene.render.resolution_x
prevResy = bpy.context.scene.render.resolution_y
prevCam = bpy.context.scene.camera
prevEngine = bpy.context.scene.render.engine
bpy.context.scene.camera = cam_obj
bpy.context.scene.render.engine = bpy.context.scene.TLM_SceneProperties.tlm_environment_probe_engine
bpy.context.scene.render.resolution_x = int(bpy.context.scene.TLM_SceneProperties.tlm_environment_probe_resolution)
bpy.context.scene.render.resolution_y = int(bpy.context.scene.TLM_SceneProperties.tlm_environment_probe_resolution)
savedir = os.path.dirname(bpy.data.filepath)
directory = os.path.join(savedir, "Probes")
t = 90
inverted = bpy.context.scene.TLM_SceneProperties.tlm_invert_direction
if inverted:
positions = {
"xp" : (math.radians(t), 0, math.radians(0)),
"zp" : (math.radians(t), 0, math.radians(t)),
"xm" : (math.radians(t), 0, math.radians(t*2)),
"zm" : (math.radians(t), 0, math.radians(-t)),
"yp" : (math.radians(t*2), 0, math.radians(t)),
"ym" : (0, 0, math.radians(t))
}
else:
positions = {
"xp" : (math.radians(t), 0, math.radians(t*2)),
"zp" : (math.radians(t), 0, math.radians(-t)),
"xm" : (math.radians(t), 0, math.radians(0)),
"zm" : (math.radians(t), 0, math.radians(t)),
"yp" : (math.radians(t*2), 0, math.radians(-t)),
"ym" : (0, 0, math.radians(-t))
}
cam = cam_obj
image_settings = bpy.context.scene.render.image_settings
image_settings.file_format = "HDR"
image_settings.color_depth = '32'
for val in positions:
cam.rotation_euler = positions[val]
filename = os.path.join(directory, val) + "_" + camobj_name + ".hdr"
bpy.data.scenes['Scene'].render.filepath = filename
print("Writing out: " + val)
bpy.ops.render.render(write_still=True)
cmft_path = bpy.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_SceneProperties.tlm_cmft_path))
output_file_irr = camobj_name + ".hdr"
posx = directory + "/" + "xp_" + camobj_name + ".hdr"
negx = directory + "/" + "xm_" + camobj_name + ".hdr"
posy = directory + "/" + "yp_" + camobj_name + ".hdr"
negy = directory + "/" + "ym_" + camobj_name + ".hdr"
posz = directory + "/" + "zp_" + camobj_name + ".hdr"
negz = directory + "/" + "zm_" + camobj_name + ".hdr"
output = directory + "/" + camobj_name
if platform.system() == 'Windows':
envpipe = [cmft_path,
'--inputFacePosX', posx,
'--inputFaceNegX', negx,
'--inputFacePosY', posy,
'--inputFaceNegY', negy,
'--inputFacePosZ', posz,
'--inputFaceNegZ', negz,
'--output0', output,
'--output0params',
'hdr,rgbe,latlong']
else:
envpipe = [cmft_path + '--inputFacePosX' + posx
+ '--inputFaceNegX' + negx
+ '--inputFacePosY' + posy
+ '--inputFaceNegY' + negy
+ '--inputFacePosZ' + posz
+ '--inputFaceNegZ' + negz
+ '--output0' + output
+ '--output0params' + 'hdr,rgbe,latlong']
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Calling CMFT with:" + str(envpipe))
if bpy.context.scene.TLM_SceneProperties.tlm_create_spherical:
subprocess.call(envpipe, shell=True)
input2 = output + ".hdr"
output2 = directory + "/" + camobj_name
if platform.system() == 'Windows':
envpipe2 = [cmft_path,
'--input', input2,
'--filter', 'shcoeffs',
'--outputNum', '1',
'--output0', output2]
else:
envpipe2 = [cmft_path +
'--input' + input2
+ '-filter' + 'shcoeffs'
+ '--outputNum' + '1'
+ '--output0' + output2]
if bpy.context.scene.TLM_SceneProperties.tlm_write_sh:
subprocess.call(envpipe2, shell=True)
if bpy.context.scene.TLM_SceneProperties.tlm_write_radiance:
use_opencl = 'false'
cpu_count = 2
# 4096 = 256 face
# 2048 = 128 face
# 1024 = 64 face
target_w = int(512)
face_size = target_w / 8
if target_w == 2048:
mip_count = 9
elif target_w == 1024:
mip_count = 8
else:
mip_count = 7
output_file_rad = directory + "/" + camobj_name + "_rad.hdr"
if platform.system() == 'Windows':
envpipe3 = [
cmft_path,
'--input', input2,
'--filter', 'radiance',
'--dstFaceSize', str(face_size),
'--srcFaceSize', str(face_size),
'--excludeBase', 'false',
# '--mipCount', str(mip_count),
'--glossScale', '8',
'--glossBias', '3',
'--lightingModel', 'blinnbrdf',
'--edgeFixup', 'none',
'--numCpuProcessingThreads', str(cpu_count),
'--useOpenCL', use_opencl,
'--clVendor', 'anyGpuVendor',
'--deviceType', 'gpu',
'--deviceIndex', '0',
'--generateMipChain', 'true',
'--inputGammaNumerator', '1.0',
'--inputGammaDenominator', '1.0',
'--outputGammaNumerator', '1.0',
'--outputGammaDenominator', '1.0',
'--outputNum', '1',
'--output0', output_file_rad,
'--output0params', 'hdr,rgbe,latlong'
]
subprocess.call(envpipe3)
else:
envpipe3 = cmft_path + \
' --input "' + input2 + '"' + \
' --filter radiance' + \
' --dstFaceSize ' + str(face_size) + \
' --srcFaceSize ' + str(face_size) + \
' --excludeBase false' + \
' --glossScale 8' + \
' --glossBias 3' + \
' --lightingModel blinnbrdf' + \
' --edgeFixup none' + \
' --numCpuProcessingThreads ' + str(cpu_count) + \
' --useOpenCL ' + use_opencl + \
' --clVendor anyGpuVendor' + \
' --deviceType gpu' + \
' --deviceIndex 0' + \
' --generateMipChain true' + \
' --inputGammaNumerator ' + '1.0' + \
' --inputGammaDenominator 1.0' + \
' --outputGammaNumerator 1.0' + \
' --outputGammaDenominator 1.0' + \
' --outputNum 1' + \
' --output0 "' + output_file_rad + '"' + \
' --output0params hdr,rgbe,latlong'
subprocess.call([envpipe3], shell=True)
for obj in bpy.data.objects:
obj.select_set(False)
cam_obj.select_set(True)
bpy.ops.object.delete()
bpy.context.scene.render.resolution_x = prevResx
bpy.context.scene.render.resolution_y = prevResy
bpy.context.scene.camera = prevCam
bpy.context.scene.render.engine = prevEngine
print("Finished building environment probes")
return {'RUNNING_MODAL'}
class TLM_CleanBuildEnvironmentProbes(bpy.types.Operator):
bl_idname = "tlm.clean_environmentprobe"
bl_label = "Clean Environment Probes"
bl_description = "Clean Environment Probes"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
savedir = os.path.dirname(bpy.data.filepath)
dirpath = os.path.join(savedir, "Probes")
if os.path.isdir(dirpath):
for file in os.listdir(dirpath):
os.remove(os.path.join(dirpath + "/" + file))
return {'FINISHED'}
class TLM_MergeAdjacentActors(bpy.types.Operator):
bl_idname = "tlm.merge_adjacent_actors"
bl_label = "Merge adjacent actors"
bl_description = "Merges the adjacent faces/vertices of selected objects"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
return {'FINISHED'}
def TLM_DoubleResolution():
pass
def TLM_HalfResolution():
pass
def TLM_DivideLMGroups():
pass
def TLM_LoadFromFolder():
pass

View file

@ -1,15 +1,20 @@
import bpy
from bpy.utils import register_class, unregister_class
from . import scene, object
from . renderer import cycles
from . import scene, object, atlas
from . renderer import cycles, luxcorerender
from . denoiser import oidn, optix
classes = [
scene.TLM_SceneProperties,
object.TLM_ObjectProperties,
cycles.TLM_CyclesSceneProperties,
luxcorerender.TLM_LuxCoreSceneProperties,
oidn.TLM_OIDNEngineProperties,
optix.TLM_OptixEngineProperties
optix.TLM_OptixEngineProperties,
atlas.TLM_AtlasListItem,
atlas.TLM_UL_AtlasList,
atlas.TLM_PostAtlasListItem,
atlas.TLM_UL_PostAtlasList
]
def register():
@ -19,8 +24,15 @@ def register():
bpy.types.Scene.TLM_SceneProperties = bpy.props.PointerProperty(type=scene.TLM_SceneProperties)
bpy.types.Object.TLM_ObjectProperties = bpy.props.PointerProperty(type=object.TLM_ObjectProperties)
bpy.types.Scene.TLM_EngineProperties = bpy.props.PointerProperty(type=cycles.TLM_CyclesSceneProperties)
bpy.types.Scene.TLM_Engine2Properties = bpy.props.PointerProperty(type=luxcorerender.TLM_LuxCoreSceneProperties)
bpy.types.Scene.TLM_OIDNEngineProperties = bpy.props.PointerProperty(type=oidn.TLM_OIDNEngineProperties)
bpy.types.Scene.TLM_OptixEngineProperties = bpy.props.PointerProperty(type=optix.TLM_OptixEngineProperties)
bpy.types.Scene.TLM_AtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0)
bpy.types.Scene.TLM_AtlasList = bpy.props.CollectionProperty(type=atlas.TLM_AtlasListItem)
bpy.types.Scene.TLM_PostAtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0)
bpy.types.Scene.TLM_PostAtlasList = bpy.props.CollectionProperty(type=atlas.TLM_PostAtlasListItem)
bpy.types.Material.TLM_ignore = bpy.props.BoolProperty(name="Skip material", description="Ignore material for lightmapped object", default=False)
def unregister():
for cls in classes:
@ -29,5 +41,10 @@ def unregister():
del bpy.types.Scene.TLM_SceneProperties
del bpy.types.Object.TLM_ObjectProperties
del bpy.types.Scene.TLM_EngineProperties
del bpy.types.Scene.TLM_Engine2Properties
del bpy.types.Scene.TLM_OIDNEngineProperties
del bpy.types.Scene.TLM_OptixEngineProperties
del bpy.types.Scene.TLM_OptixEngineProperties
del bpy.types.Scene.TLM_AtlasListItem
del bpy.types.Scene.TLM_AtlasList
del bpy.types.Scene.TLM_PostAtlasListItem
del bpy.types.Scene.TLM_PostAtlasList

View file

@ -0,0 +1,130 @@
import bpy
from bpy.props import *
class TLM_PostAtlasListItem(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object, description="The object to bake")
tlm_atlas_lightmap_resolution : EnumProperty(
items = [('32', '32', 'TODO'),
('64', '64', 'TODO'),
('128', '128', 'TODO'),
('256', '256', 'TODO'),
('512', '512', 'TODO'),
('1024', '1024', 'TODO'),
('2048', '2048', 'TODO'),
('4096', '4096', 'TODO'),
('8192', '8192', 'TODO')],
name = "Atlas Lightmap Resolution",
description="TODO",
default='256')
tlm_atlas_repack_on_cleanup : BoolProperty(
name="Repack on cleanup",
description="Postpacking adjusts the UV's. Toggle to resize back to full scale on cleanup.",
default=True)
tlm_atlas_dilation : BoolProperty(
name="Dilation",
description="Adds a blurred background layer that acts as a dilation map.",
default=False)
tlm_atlas_unwrap_margin : FloatProperty(
name="Unwrap Margin",
default=0.1,
min=0.0,
max=1.0,
subtype='FACTOR')
tlm_atlas_lightmap_unwrap_mode : EnumProperty(
items = [('Lightmap', 'Lightmap', 'TODO'),
('SmartProject', 'Smart Project', 'TODO'),
('Xatlas', 'Xatlas', 'TODO')],
name = "Unwrap Mode",
description="TODO",
default='SmartProject')
class TLM_UL_PostAtlasList(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
custom_icon = 'OBJECT_DATAMODE'
if self.layout_type in {'DEFAULT', 'COMPACT'}:
#In list object counter
amount = 0
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_postpack_object:
if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name:
amount = amount + 1
row = layout.row()
row.prop(item, "name", text="", emboss=False, icon=custom_icon)
col = row.column()
col.label(text=item.tlm_atlas_lightmap_resolution)
col = row.column()
col.alignment = 'RIGHT'
col.label(text=str(amount))
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)
class TLM_AtlasListItem(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object, description="The object to bake")
tlm_atlas_lightmap_resolution : EnumProperty(
items = [('32', '32', 'TODO'),
('64', '64', 'TODO'),
('128', '128', 'TODO'),
('256', '256', 'TODO'),
('512', '512', 'TODO'),
('1024', '1024', 'TODO'),
('2048', '2048', 'TODO'),
('4096', '4096', 'TODO'),
('8192', '8192', 'TODO')],
name = "Atlas Lightmap Resolution",
description="TODO",
default='256')
tlm_atlas_unwrap_margin : FloatProperty(
name="Unwrap Margin",
default=0.1,
min=0.0,
max=1.0,
subtype='FACTOR')
tlm_atlas_lightmap_unwrap_mode : EnumProperty(
items = [('Lightmap', 'Lightmap', 'TODO'),
('SmartProject', 'Smart Project', 'TODO'),
('Xatlas', 'Xatlas', 'TODO')],
name = "Unwrap Mode",
description="TODO",
default='SmartProject')
class TLM_UL_AtlasList(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
custom_icon = 'OBJECT_DATAMODE'
if self.layout_type in {'DEFAULT', 'COMPACT'}:
amount = 0
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name:
amount = amount + 1
row = layout.row()
row.prop(item, "name", text="", emboss=False, icon=custom_icon)
col = row.column()
col.label(text=item.tlm_atlas_lightmap_resolution)
col = row.column()
col.alignment = 'RIGHT'
col.label(text=str(amount))
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)

View file

@ -1,4 +1,5 @@
import bpy
import bpy, os
from ...utility import utility
from bpy.props import *
class TLM_OIDNEngineProperties(bpy.types.PropertyGroup):

View file

@ -0,0 +1,10 @@
import bpy
from bpy.props import *
class TLM_ObjectProperties(bpy.types.PropertyGroup):
tlm_image_scale_method : EnumProperty(
items = [('Native', 'Native', 'TODO'),
('OpenCV', 'OpenCV', 'TODO')],
name = "Scaling engine",
description="TODO",
default='Native')

View file

@ -10,11 +10,31 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup):
description = "Atlas Lightmap Group",
default = "")
tlm_postatlas_pointer : StringProperty(
name = "Atlas Group",
description = "Atlas Lightmap Group",
default = "")
tlm_uvchannel_pointer : StringProperty(
name = "UV Channel",
description = "Select UV Channel to bake to",
default = "")
tlm_uvchannel_pointer : BoolProperty(
name="Enable Lightmapping",
description="TODO",
default=False)
tlm_mesh_lightmap_use : BoolProperty(
name="Enable Lightmapping",
description="TODO",
default=False)
tlm_material_ignore : BoolProperty(
name="Skip material",
description="Ignore material for lightmapped object",
default=False)
tlm_mesh_lightmap_resolution : EnumProperty(
items = [('32', '32', 'TODO'),
('64', '64', 'TODO'),
@ -29,7 +49,15 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup):
description="TODO",
default='256')
unwrap_modes = [('Lightmap', 'Lightmap', 'TODO'),('SmartProject', 'Smart Project', 'TODO'),('CopyExisting', 'Copy Existing', 'TODO'),('AtlasGroup', 'Atlas Group', 'TODO')]
unwrap_modes = [('Lightmap', 'Lightmap', 'TODO'),
('SmartProject', 'Smart Project', 'TODO'),
('CopyExisting', 'Copy Existing', 'TODO'),
('AtlasGroupA', 'Atlas Group (Prepack)', 'TODO')]
tlm_postpack_object : BoolProperty( #CHECK INSTEAD OF ATLASGROUPB
name="Postpack object",
description="Postpack object into an AtlasGroup",
default=False)
if "blender_xatlas" in addon_keys:
unwrap_modes.append(('Xatlas', 'Xatlas', 'TODO'))
@ -117,5 +145,4 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup):
name="Median kernel",
default=3,
min=1,
max=5)
max=5)

View file

@ -84,4 +84,12 @@ class TLM_CyclesSceneProperties(bpy.types.PropertyGroup):
('zero', 'Zero', 'Temporarily set to 0 during baking, and reapply after')],
name = "Metallic handling",
description="Set metallic handling mode to prevent black-baking.",
default="ignore")
default="ignore")
tlm_lighting_mode : EnumProperty(
items = [('combined', 'Combined', 'Bake combined lighting'),
('indirect', 'Indirect', 'Bake indirect lighting'),
('ao', 'AO', 'Bake only Ambient Occlusion')],
name = "Lighting mode",
description="TODO.",
default="combined")

View file

@ -0,0 +1,11 @@
import bpy
from bpy.props import *
class TLM_LuxCoreSceneProperties(bpy.types.PropertyGroup):
#Luxcore specific here
tlm_luxcore_dir : StringProperty(
name="Luxcore Directory",
description="Standalone path to your LuxCoreRender binary.",
default="",
subtype="FILE_PATH")

View file

@ -5,9 +5,19 @@ class TLM_SceneProperties(bpy.types.PropertyGroup):
engines = [('Cycles', 'Cycles', 'Use Cycles for lightmapping')]
#engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping'))
engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping'))
#engines.append(('OctaneRender', 'Octane Render', 'Use Octane Render for lightmapping'))
tlm_atlas_pointer : StringProperty(
name = "Atlas Group",
description = "Atlas Lightmap Group",
default = "")
tlm_postatlas_pointer : StringProperty(
name = "Atlas Group",
description = "Atlas Lightmap Group",
default = "")
tlm_lightmap_engine : EnumProperty(
items = engines,
name = "Lightmap Engine",
@ -102,8 +112,8 @@ class TLM_SceneProperties(bpy.types.PropertyGroup):
#FILTERING SETTINGS GROUP
tlm_filtering_use : BoolProperty(
name="Enable filtering",
description="Enable filtering for lightmaps",
name="Enable Filtering",
description="Enable denoising for lightmaps",
default=False)
tlm_filtering_engine : EnumProperty(
@ -178,10 +188,30 @@ class TLM_SceneProperties(bpy.types.PropertyGroup):
description="Enable encoding for lightmaps",
default=False)
tlm_encoding_mode : EnumProperty(
items = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'),
tlm_encoding_device : EnumProperty(
items = [('CPU', 'CPU', 'Todo'),
('GPU', 'GPU', 'Todo.')],
name = "Encoding Device",
description="TODO",
default='CPU')
encoding_modes_1 = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'),
('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'),
('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')]
encoding_modes_2 = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'),
('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'),
('LogLuv', 'LogLuv', '8-bit HDR encoding. Different.'),
('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')],
('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')]
tlm_encoding_mode_a : EnumProperty(
items = encoding_modes_1,
name = "Encoding Mode",
description="TODO",
default='HDR')
tlm_encoding_mode_b : EnumProperty(
items = encoding_modes_2,
name = "Encoding Mode",
description="TODO",
default='HDR')
@ -191,10 +221,10 @@ class TLM_SceneProperties(bpy.types.PropertyGroup):
description="Higher gives a larger HDR range, but also gives more banding.",
default=6,
min=1,
max=10)
max=255)
tlm_encoding_armory_setup : BoolProperty(
name="Use Armory decoder",
tlm_decoder_setup : BoolProperty(
name="Use decoder",
description="TODO",
default=False)
@ -246,11 +276,17 @@ class TLM_SceneProperties(bpy.types.PropertyGroup):
items = [('Lightmap', 'Lightmap', 'TODO'),
('SmartProject', 'Smart Project', 'TODO'),
('CopyExisting', 'Copy Existing', 'TODO'),
('AtlasGroup', 'Atlas Group', 'TODO')],
('AtlasGroupA', 'Atlas Group (Prepack)', 'TODO'),
('Xatlas', 'Xatlas', 'TODO')],
name = "Unwrap Mode",
description="TODO",
default='SmartProject')
tlm_postpack_object : BoolProperty( #CHECK INSTEAD OF ATLASGROUPB
name="Postpack object",
description="Postpack object into an AtlasGroup",
default=False)
tlm_mesh_unwrap_margin : FloatProperty(
name="Unwrap Margin",
default=0.1,
@ -263,6 +299,13 @@ class TLM_SceneProperties(bpy.types.PropertyGroup):
description="Headless; Do not apply baked materials on finish.",
default=False)
tlm_atlas_mode : EnumProperty(
items = [('Prepack', 'Pre-packing', 'Todo.'),
('Postpack', 'Post-packing', 'Todo.')],
name = "Atlas mode",
description="TODO",
default='Prepack')
tlm_alert_sound : EnumProperty(
items = [('dash', 'Dash', 'Dash alert'),
('noot', 'Noot', 'Noot alert'),
@ -270,4 +313,100 @@ class TLM_SceneProperties(bpy.types.PropertyGroup):
('pingping', 'Ping', 'Ping alert')],
name = "Alert sound",
description="Alert sound when lightmap building finished.",
default="gentle")
default="gentle")
tlm_metallic_clamp : EnumProperty(
items = [('ignore', 'Ignore', 'Ignore clamping'),
('zero', 'Zero', 'Set zero'),
('limit', 'Limit', 'Clamp to 0.9')],
name = "Metallic clamping",
description="TODO.",
default="ignore")
tlm_verbose : BoolProperty(
name="Verbose",
description="Verbose console output",
default=False)
tlm_compile_statistics : BoolProperty(
name="Compile statistics",
description="Compile lightbuild statistics",
default=False)
tlm_override_bg_color : BoolProperty(
name="Override background",
description="Override background color, black by default.",
default=False)
tlm_override_color : FloatVectorProperty(name="Color",
description="Background color for baked maps",
subtype='COLOR',
default=[0.5,0.5,0.5])
tlm_reset_uv : BoolProperty(
name="Remove Lightmap UV",
description="Remove existing UV maps for lightmaps.",
default=False)
tlm_network_render : BoolProperty(
name="Enable network rendering",
description="Enable network rendering (Unstable).",
default=False)
tlm_network_paths : PointerProperty(
name="Network file",
description="Network instruction file",
type=bpy.types.Text)
tlm_network_dir : StringProperty(
name="Network directory",
description="Use a path that is accessible to all your network render devices.",
default="",
subtype="FILE_PATH")
tlm_cmft_path : StringProperty(
name="CMFT Path",
description="The path to the CMFT binaries",
default="",
subtype="FILE_PATH")
tlm_create_spherical : BoolProperty(
name="Create spherical texture",
description="Merge cubemap to a 360 spherical texture.",
default=False)
tlm_write_sh : BoolProperty(
name="Calculate SH coefficients",
description="Calculates spherical harmonics coefficients to a file.",
default=False)
tlm_write_radiance : BoolProperty(
name="Write radiance images",
description="Writes out the radiance images.",
default=False)
tlm_invert_direction : BoolProperty(
name="Invert direction",
description="Inverts the direction.",
default=False)
tlm_environment_probe_resolution : EnumProperty(
items = [('32', '32', 'TODO'),
('64', '64', 'TODO'),
('128', '128', 'TODO'),
('256', '256', 'TODO'),
('512', '512', 'TODO'),
('1024', '1024', 'TODO'),
('2048', '2048', 'TODO'),
('4096', '4096', 'TODO'),
('8192', '8192', 'TODO')],
name = "Probe Resolution",
description="TODO",
default='256')
tlm_environment_probe_engine : EnumProperty(
items = [('BLENDER_EEVEE', 'Eevee', 'TODO'),
('CYCLES', 'Cycles', 'TODO')],
name = "Probe Render Engine",
description="TODO",
default='BLENDER_EEVEE')

View file

@ -1,15 +1,25 @@
import bpy, os, importlib, subprocess, sys, threading, platform, aud
from . import encoding
import bpy, os, subprocess, sys, platform, aud, json, datetime, socket
import threading
from . import encoding, pack
from . cycles import lightmap, prepare, nodes, cache
from . denoiser import integrated, oidn, optix
from . filtering import opencv
from .. network import client
from os import listdir
from os.path import isfile, join
from time import time, sleep
from importlib import util
previous_settings = {}
def prepare_build(self=0, background_mode=False):
postprocess_shutdown = False
def prepare_build(self=0, background_mode=False, shutdown_after_build=False):
if shutdown_after_build:
postprocess_shutdown = True
print("Building lightmaps")
if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Foreground" or background_mode==True:
@ -19,20 +29,6 @@ def prepare_build(self=0, background_mode=False):
scene = bpy.context.scene
sceneProperties = scene.TLM_SceneProperties
#We dynamically load the renderer and denoiser, instead of loading something we don't use
if sceneProperties.tlm_lightmap_engine == "Cycles":
pass
if sceneProperties.tlm_lightmap_engine == "LuxCoreRender":
pass
if sceneProperties.tlm_lightmap_engine == "OctaneRender":
pass
#Timer start here bound to global
if check_save():
@ -56,6 +52,12 @@ def prepare_build(self=0, background_mode=False):
self.report({'INFO'}, "Error:Filtering - OpenCV not installed")
return{'FINISHED'}
#TODO DO some resolution change
#if checkAtlasSize():
# print("Error: AtlasGroup overflow")
# self.report({'INFO'}, "Error: AtlasGroup overflow - Too many objects")
# return{'FINISHED'}
setMode()
dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir)
@ -65,6 +67,17 @@ def prepare_build(self=0, background_mode=False):
#Naming check
naming_check()
# if sceneProperties.tlm_reset_uv or sceneProperties.tlm_atlas_mode == "Postpack":
# for obj in bpy.data.objects:
# if obj.type == "MESH":
# uv_layers = obj.data.uv_layers
#for uvlayer in uv_layers:
# if uvlayer.name == "UVMap_Lightmap":
# uv_layers.remove(uvlayer)
## RENDER DEPENDENCY FROM HERE
if sceneProperties.tlm_lightmap_engine == "Cycles":
@ -73,7 +86,7 @@ def prepare_build(self=0, background_mode=False):
if sceneProperties.tlm_lightmap_engine == "LuxCoreRender":
pass
setup.init(self, previous_settings)
if sceneProperties.tlm_lightmap_engine == "OctaneRender":
@ -91,6 +104,8 @@ def prepare_build(self=0, background_mode=False):
filepath = bpy.data.filepath
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
start_time = time()
scene = bpy.context.scene
@ -140,19 +155,113 @@ def prepare_build(self=0, background_mode=False):
#Naming check
naming_check()
pipe_open([sys.executable,"-b",filepath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], finish_assemble)
if scene.TLM_SceneProperties.tlm_network_render:
def finish_assemble():
pass
#bpy.ops.wm.revert_mainfile() We cannot use this, as Blender crashes...
print("Background baking finished")
print("NETWORK RENDERING")
if scene.TLM_SceneProperties.tlm_network_paths != None:
HOST = bpy.data.texts[scene.TLM_SceneProperties.tlm_network_paths.name].lines[0].body # The server's hostname or IP address
else:
HOST = '127.0.0.1' # The server's hostname or IP address
PORT = 9898 # The port used by the server
client.connect_client(HOST, PORT, bpy.data.filepath, 0)
# with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# s.connect((HOST, PORT))
# message = {
# "call" : 1,
# "command" : 1,
# "enquiry" : 0,
# "args" : bpy.data.filepath
# }
# s.sendall(json.dumps(message).encode())
# data = s.recv(1024)
# print(data.decode())
finish_assemble()
else:
bpy.app.driver_namespace["alpha"] = 0
bpy.app.driver_namespace["tlm_process"] = False
if os.path.exists(os.path.join(dirpath, "process.tlm")):
os.remove(os.path.join(dirpath, "process.tlm"))
bpy.app.timers.register(distribute_building)
def distribute_building():
#CHECK IF THERE'S AN EXISTING SUBPROCESS
if not os.path.isfile(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir, "process.tlm")):
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("No process file - Creating one...")
write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir)
blendPath = bpy.data.filepath
process_status = [blendPath,
{'bake': 'all',
'completed': False
}]
with open(os.path.join(write_directory, "process.tlm"), 'w') as file:
json.dump(process_status, file, indent=2)
bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Started process: " + str(bpy.app.driver_namespace["tlm_process"]) + " at " + str(datetime.datetime.now()))
else:
write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir)
process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read())
if process_status[1]["completed"]:
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Baking finished")
bpy.app.timers.unregister(distribute_building)
finish_assemble()
else:
#Open the json and check the status!
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Baking in progress")
process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read())
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(process_status)
return 1.0
def finish_assemble(self=0):
print("Finishing assembly")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Background baking finished")
scene = bpy.context.scene
sceneProperties = scene.TLM_SceneProperties
if sceneProperties.tlm_lightmap_engine == "Cycles":
prepare.init(previous_settings)
prepare.init(self, previous_settings)
if sceneProperties.tlm_lightmap_engine == "LuxCoreRender":
pass
@ -162,20 +271,10 @@ def finish_assemble():
manage_build(True)
def pipe_open(args, callback):
def thread_process(args, callback):
process = subprocess.Popen(args)
process.wait()
callback()
return
thread = threading.Thread(target=thread_process, args=(args, callback))
thread.start()
return thread
def begin_build():
print("Beginning build")
dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir)
scene = bpy.context.scene
@ -192,7 +291,6 @@ def begin_build():
pass
#Denoiser
if sceneProperties.tlm_denoise_use:
if sceneProperties.tlm_denoise_engine == "Integrated":
@ -205,7 +303,8 @@ def begin_build():
if file.endswith("_baked.hdr"):
baked_image_array.append(file)
print(baked_image_array)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(baked_image_array)
denoiser = integrated.TLM_Integrated_Denoise()
@ -267,22 +366,52 @@ def begin_build():
filter.init(dirpath, useDenoise)
if sceneProperties.tlm_encoding_use:
#Encoding
if sceneProperties.tlm_encoding_use and scene.TLM_EngineProperties.tlm_bake_mode != "Background":
if sceneProperties.tlm_encoding_mode == "HDR":
if sceneProperties.tlm_encoding_device == "CPU":
if sceneProperties.tlm_format == "EXR":
if sceneProperties.tlm_encoding_mode_a == "HDR":
print("EXR Format")
if sceneProperties.tlm_format == "EXR":
ren = bpy.context.scene.render
ren.image_settings.file_format = "OPEN_EXR"
#ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec"
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("EXR Format")
ren = bpy.context.scene.render
ren.image_settings.file_format = "OPEN_EXR"
#ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec"
end = "_baked"
baked_image_array = []
if sceneProperties.tlm_denoise_use:
end = "_denoised"
if sceneProperties.tlm_filtering_use:
end = "_filtered"
#For each image in folder ending in denoised/filtered
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
for file in dirfiles:
if file.endswith(end + ".hdr"):
img = bpy.data.images.load(os.path.join(dirpath,file))
img.save_render(img.filepath_raw[:-4] + ".exr")
if sceneProperties.tlm_encoding_mode_a == "RGBM":
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("ENCODING RGBM")
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
end = "_baked"
baked_image_array = []
if sceneProperties.tlm_denoise_use:
end = "_denoised"
@ -290,65 +419,155 @@ def begin_build():
if sceneProperties.tlm_filtering_use:
end = "_filtered"
#For each image in folder ending in denoised/filtered
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
for file in dirfiles:
if file.endswith(end + ".hdr"):
img = bpy.data.images.load(os.path.join(dirpath,file))
img.save_render(img.filepath_raw[:-4] + ".exr")
img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Encoding:" + str(file))
encoding.encodeImageRGBMCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0)
if sceneProperties.tlm_encoding_mode == "LogLuv":
if sceneProperties.tlm_encoding_mode_b == "RGBD":
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("ENCODING RGBD")
end = "_baked"
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
if sceneProperties.tlm_denoise_use:
end = "_baked"
end = "_denoised"
if sceneProperties.tlm_denoise_use:
if sceneProperties.tlm_filtering_use:
end = "_denoised"
end = "_filtered"
if sceneProperties.tlm_filtering_use:
for file in dirfiles:
if file.endswith(end + ".hdr"):
end = "_filtered"
img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False)
for file in dirfiles:
if file.endswith(end + ".hdr"):
img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Encoding:" + str(file))
encoding.encodeImageRGBDCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0)
else:
if sceneProperties.tlm_encoding_mode_b == "HDR":
if sceneProperties.tlm_format == "EXR":
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("EXR Format")
ren = bpy.context.scene.render
ren.image_settings.file_format = "OPEN_EXR"
#ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec"
end = "_baked"
baked_image_array = []
if sceneProperties.tlm_denoise_use:
end = "_denoised"
if sceneProperties.tlm_filtering_use:
end = "_filtered"
encoding.encodeLogLuv(img, dirpath, 0)
#For each image in folder ending in denoised/filtered
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
if sceneProperties.tlm_encoding_mode == "RGBM":
for file in dirfiles:
if file.endswith(end + ".hdr"):
print("ENCODING RGBM")
img = bpy.data.images.load(os.path.join(dirpath,file))
img.save_render(img.filepath_raw[:-4] + ".exr")
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
if sceneProperties.tlm_encoding_mode_b == "LogLuv":
end = "_baked"
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
if sceneProperties.tlm_denoise_use:
end = "_baked"
end = "_denoised"
if sceneProperties.tlm_denoise_use:
if sceneProperties.tlm_filtering_use:
end = "_denoised"
end = "_filtered"
if sceneProperties.tlm_filtering_use:
for file in dirfiles:
if file.endswith(end + ".hdr"):
end = "_filtered"
img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False)
print("Encoding:" + str(file))
encoding.encodeImageRGBM(img, sceneProperties.tlm_encoding_range, dirpath, 0)
for file in dirfiles:
if file.endswith(end + ".hdr"):
img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False)
encoding.encodeLogLuvGPU(img, dirpath, 0)
if sceneProperties.tlm_encoding_mode_b == "RGBM":
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("ENCODING RGBM")
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
end = "_baked"
if sceneProperties.tlm_denoise_use:
end = "_denoised"
if sceneProperties.tlm_filtering_use:
end = "_filtered"
for file in dirfiles:
if file.endswith(end + ".hdr"):
img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Encoding:" + str(file))
encoding.encodeImageRGBMGPU(img, sceneProperties.tlm_encoding_range, dirpath, 0)
if sceneProperties.tlm_encoding_mode_b == "RGBD":
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("ENCODING RGBD")
dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))]
end = "_baked"
if sceneProperties.tlm_denoise_use:
end = "_denoised"
if sceneProperties.tlm_filtering_use:
end = "_filtered"
for file in dirfiles:
if file.endswith(end + ".hdr"):
img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Encoding:" + str(file))
encoding.encodeImageRGBDGPU(img, sceneProperties.tlm_encoding_range, dirpath, 0)
manage_build()
def manage_build(background_pass=False):
print("Managing build")
scene = bpy.context.scene
sceneProperties = scene.TLM_SceneProperties
@ -371,25 +590,108 @@ def manage_build(background_pass=False):
formatEnc = ".hdr"
if sceneProperties.tlm_encoding_use:
if sceneProperties.tlm_encoding_use and scene.TLM_EngineProperties.tlm_bake_mode != "Background":
if sceneProperties.tlm_encoding_mode == "HDR":
if sceneProperties.tlm_encoding_device == "CPU":
if sceneProperties.tlm_format == "EXR":
print("CPU Encoding")
formatEnc = ".exr"
if sceneProperties.tlm_encoding_mode_a == "HDR":
if sceneProperties.tlm_encoding_mode == "LogLuv":
if sceneProperties.tlm_format == "EXR":
formatEnc = "_encoded.png"
formatEnc = ".exr"
if sceneProperties.tlm_encoding_mode == "RGBM":
if sceneProperties.tlm_encoding_mode_a == "RGBM":
formatEnc = "_encoded.png"
formatEnc = "_encoded.png"
if sceneProperties.tlm_encoding_mode_a == "RGBD":
formatEnc = "_encoded.png"
else:
print("GPU Encoding")
if sceneProperties.tlm_encoding_mode_b == "HDR":
if sceneProperties.tlm_format == "EXR":
formatEnc = ".exr"
if sceneProperties.tlm_encoding_mode_b == "LogLuv":
formatEnc = "_encoded.png"
if sceneProperties.tlm_encoding_mode_b == "RGBM":
formatEnc = "_encoded.png"
if sceneProperties.tlm_encoding_mode_b == "RGBD":
formatEnc = "_encoded.png"
if not background_pass:
nodes.exchangeLightmapsToPostfix("_baked", end, formatEnc)
if scene.TLM_EngineProperties.tlm_setting_supersample == "2x":
supersampling_scale = 2
elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x":
supersampling_scale = 4
else:
supersampling_scale = 1
pack.postpack()
for image in bpy.data.images:
if image.users < 1:
bpy.data.images.remove(image)
if scene.TLM_SceneProperties.tlm_headless:
filepath = bpy.data.filepath
dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir)
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
cache.backup_material_restore(obj)
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
cache.backup_material_rename(obj)
for mat in bpy.data.materials:
if mat.users < 1:
bpy.data.materials.remove(mat)
for mat in bpy.data.materials:
if mat.name.startswith("."):
if "_Original" in mat.name:
bpy.data.materials.remove(mat)
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
img_name = obj.name + '_baked'
Lightmapimage = bpy.data.images[img_name]
obj["Lightmap"] = Lightmapimage.filepath_raw
for image in bpy.data.images:
if image.name.endswith("_baked"):
bpy.data.images.remove(image, do_unlink=True)
total_time = sec_to_hours((time() - start_time))
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(total_time)
bpy.context.scene["TLM_Buildstat"] = total_time
reset_settings(previous_settings["settings"])
if sceneProperties.tlm_lightmap_engine == "LuxCoreRender":
pass
@ -400,66 +702,6 @@ def manage_build(background_pass=False):
if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Background":
pass
#bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath + "baked") #Crashes Blender
if scene.TLM_EngineProperties.tlm_setting_supersample == "2x":
supersampling_scale = 2
elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x":
supersampling_scale = 4
else:
supersampling_scale = 1
# for image in bpy.data.images:
# if image.name.endswith("_baked"):
# resolution = image.size[0]
# rescale = resolution / supersampling_scale
# image.scale(rescale, rescale)
# image.save()
for image in bpy.data.images:
if image.users < 1:
bpy.data.images.remove(image)
if scene.TLM_SceneProperties.tlm_headless:
filepath = bpy.data.filepath
dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir)
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
cache.backup_material_restore(obj)
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
cache.backup_material_rename(obj)
for mat in bpy.data.materials:
if mat.users < 1:
bpy.data.materials.remove(mat)
for mat in bpy.data.materials:
if mat.name.startswith("."):
if "_Original" in mat.name:
bpy.data.materials.remove(mat)
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
img_name = obj.name + '_baked'
Lightmapimage = bpy.data.images[img_name]
obj["Lightmap"] = Lightmapimage.filepath_raw
for image in bpy.data.images:
if image.name.endswith("_baked"):
bpy.data.images.remove(image, do_unlink=True)
total_time = sec_to_hours((time() - start_time))
print(total_time)
reset_settings(previous_settings["settings"])
if scene.TLM_SceneProperties.tlm_alert_on_finish:
@ -481,6 +723,29 @@ def manage_build(background_pass=False):
sound = aud.Sound.file(sound_path)
device.play(sound)
print("Lightmap building finished")
if bpy.app.background:
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Writing background process report")
write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir)
if os.path.exists(os.path.join(write_directory, "process.tlm")):
process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read())
process_status[1]["completed"] = True
with open(os.path.join(write_directory, "process.tlm"), 'w') as file:
json.dump(process_status, file, indent=2)
if postprocess_shutdown:
sys.exit()
#TODO - SET BELOW TO UTILITY
def reset_settings(prev_settings):
scene = bpy.context.scene
cycles = scene.cycles
@ -546,7 +811,7 @@ def naming_check():
def opencv_check():
cv2 = importlib.util.find_spec("cv2")
cv2 = util.find_spec("cv2")
if cv2 is not None:
return 0
@ -589,7 +854,8 @@ def check_materials():
mat = slot.material
if mat is None:
print("MatNone")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("MatNone")
mat = bpy.data.materials.new(name="Material")
mat.use_nodes = True
slot.material = mat
@ -609,4 +875,41 @@ def sec_to_hours(seconds):
def setMode():
bpy.ops.object.mode_set(mode='OBJECT')
#TODO Make some checks that returns to previous selection
#TODO Make some checks that returns to previous selection
def checkAtlasSize():
overflow = False
scene = bpy.context.scene
if scene.TLM_EngineProperties.tlm_setting_supersample == "2x":
supersampling_scale = 2
elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x":
supersampling_scale = 4
else:
supersampling_scale = 1
for atlas in bpy.context.scene.TLM_PostAtlasList:
atlas_resolution = int(int(atlas.tlm_atlas_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale))
utilized = 0
atlasUsedArea = 0
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_postpack_object:
if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name:
atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2
utilized = atlasUsedArea / (int(atlas_resolution) ** 2)
if (utilized * 100) > 100:
overflow = True
print("Overflow for: " + str(atlas.name))
if overflow == True:
return True
else:
return False

View file

@ -13,28 +13,35 @@ def backup_material_cache(slot, path):
bpy.ops.wm.save_as_mainfile(filepath=path, copy=True)
def backup_material_cache_restore(slot, path):
print("Restore cache")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Restore cache")
def backup_material_rename(obj):
if "TLM_PrevMatArray" in obj:
print("Has PrevMat B")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Renaming material for: " + obj.name)
for slot in obj.material_slots:
if slot.material is not None:
if slot.material.name.endswith("_Original"):
newname = slot.material.name[1:-9]
if newname in bpy.data.materials:
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Removing material: " + bpy.data.materials[newname].name)
bpy.data.materials.remove(bpy.data.materials[newname])
slot.material.name = newname
del obj["TLM_PrevMatArray"]
def backup_material_restore(obj):
print("RESTORE")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Restoring material for: " + obj.name)
if "TLM_PrevMatArray" in obj:
print("Has PrevMat A")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Material restore array found: " + str(obj["TLM_PrevMatArray"]))
#Running through the slots
prevMatArray = obj["TLM_PrevMatArray"]
slotsLength = len(prevMatArray)
@ -43,12 +50,17 @@ def backup_material_restore(obj):
for idx, slot in enumerate(obj.material_slots): #For each slot, we get the index
#We only need the index, corresponds to the array index
try:
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Attempting to set material")
originalMaterial = prevMatArray[idx]
except IndexError:
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Material restore failed - Resetting")
originalMaterial = ""
if slot.material is not None:
slot.material.user_clear()
#slot.material.user_clear() Seems to be bad; See: https://developer.blender.org/T49837
bpy.data.materials.remove(slot.material)
if "." + originalMaterial + "_Original" in bpy.data.materials:
slot.material = bpy.data.materials["." + originalMaterial + "_Original"]

View file

@ -32,9 +32,20 @@ def bake():
obj.hide_render = False
scene.render.bake.use_clear = False
print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name)
bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False)
if scene.TLM_EngineProperties.tlm_lighting_mode == "combined" or scene.TLM_EngineProperties.tlm_lighting_mode == "combinedAO":
bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False)
elif scene.TLM_EngineProperties.tlm_lighting_mode == "indirect" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectAO":
bpy.ops.object.bake(type="DIFFUSE", pass_filter={"INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False)
elif scene.TLM_EngineProperties.tlm_lighting_mode == "ao":
bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False)
elif scene.TLM_EngineProperties.tlm_lighting_mode == "complete":
bpy.ops.object.bake(type="COMBINED", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False)
else:
bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False)
bpy.ops.object.select_all(action='DESELECT')
currentIterNum = currentIterNum + 1
@ -46,5 +57,6 @@ def bake():
filepath_ext = ".hdr"
image.filepath_raw = bakemap_path + filepath_ext
image.file_format = "HDR"
print("Saving to: " + image.filepath_raw)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Saving to: " + image.filepath_raw)
image.save()

View file

@ -49,6 +49,21 @@ def apply_materials():
old = slot.material
slot.material = bpy.data.materials[old.name.split('_' + obj.name)[0]]
if(scene.TLM_SceneProperties.tlm_decoder_setup):
tlm_rgbm = bpy.data.node_groups.get('RGBM Decode')
tlm_rgbd = bpy.data.node_groups.get('RGBD Decode')
tlm_logluv = bpy.data.node_groups.get('LogLuv Decode')
if tlm_rgbm == None:
load_library('RGBM Decode')
if tlm_rgbd == None:
load_library('RGBD Decode')
if tlm_logluv == None:
load_library('LogLuv Decode')
if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0):
tlm_exposure = bpy.data.node_groups.get("Exposure")
@ -56,88 +71,174 @@ def apply_materials():
load_library("Exposure")
#Apply materials
print(obj.name)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(obj.name)
for slot in obj.material_slots:
mat = slot.material
print(slot.material)
node_tree = mat.node_tree
nodes = mat.node_tree.nodes
foundBakedNode = False
#Find nodes
for node in nodes:
if node.name == "Baked Image":
lightmapNode = node
lightmapNode.location = -800, 300
lightmapNode.name = "TLM_Lightmap"
foundBakedNode = True
img_name = obj.name + '_baked'
mat = slot.material
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(slot.material)
if not foundBakedNode:
lightmapNode = node_tree.nodes.new(type="ShaderNodeTexImage")
lightmapNode.location = -300, 300
lightmapNode.image = bpy.data.images[img_name]
lightmapNode.name = "TLM_Lightmap"
lightmapNode.interpolation = "Smart"
if not mat.TLM_ignore:
#Find output node
outputNode = nodes[0]
if(outputNode.type != "OUTPUT_MATERIAL"):
for node in node_tree.nodes:
if node.type == "OUTPUT_MATERIAL":
outputNode = node
break
node_tree = mat.node_tree
nodes = mat.node_tree.nodes
#Find mainnode
mainNode = outputNode.inputs[0].links[0].from_node
foundBakedNode = False
#Add all nodes first
#Add lightmap multipliction texture
mixNode = node_tree.nodes.new(type="ShaderNodeMixRGB")
mixNode.name = "Lightmap_Multiplication"
mixNode.location = -300, 300
mixNode.blend_type = 'MULTIPLY'
mixNode.inputs[0].default_value = 1.0
#Find nodes
for node in nodes:
if node.name == "Baked Image":
lightmapNode = node
lightmapNode.location = -800, 300
lightmapNode.name = "TLM_Lightmap"
foundBakedNode = True
img_name = obj.name + '_baked'
UVLightmap = node_tree.nodes.new(type="ShaderNodeUVMap")
UVLightmap.uv_map = "UVMap_Lightmap"
UVLightmap.name = "Lightmap_UV"
UVLightmap.location = -1000, 300
if not foundBakedNode:
lightmapNode = node_tree.nodes.new(type="ShaderNodeTexImage")
lightmapNode.location = -300, 300
lightmapNode.name = "TLM_Lightmap"
lightmapNode.interpolation = "Smart"
if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0):
ExposureNode = node_tree.nodes.new(type="ShaderNodeGroup")
ExposureNode.node_tree = bpy.data.node_groups["Exposure"]
ExposureNode.inputs[1].default_value = scene.TLM_EngineProperties.tlm_exposure_multiplier
ExposureNode.location = -500, 300
ExposureNode.name = "Lightmap_Exposure"
if (obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and obj.TLM_ObjectProperties.tlm_atlas_pointer != ""):
lightmapNode.image = bpy.data.images[obj.TLM_ObjectProperties.tlm_atlas_pointer + "_baked"]
else:
lightmapNode.image = bpy.data.images[img_name]
#Add Basecolor node
if len(mainNode.inputs[0].links) == 0:
baseColorValue = mainNode.inputs[0].default_value
baseColorNode = node_tree.nodes.new(type="ShaderNodeRGB")
baseColorNode.outputs[0].default_value = baseColorValue
baseColorNode.location = ((mainNode.location[0] - 500, mainNode.location[1] - 300))
baseColorNode.name = "Lightmap_BasecolorNode_A"
else:
baseColorNode = mainNode.inputs[0].links[0].from_node
baseColorNode.name = "LM_P"
#Find output node
outputNode = nodes[0]
if(outputNode.type != "OUTPUT_MATERIAL"):
for node in node_tree.nodes:
if node.type == "OUTPUT_MATERIAL":
outputNode = node
break
#Linking
if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0):
mat.node_tree.links.new(lightmapNode.outputs[0], ExposureNode.inputs[0]) #Connect lightmap node to mixnode
mat.node_tree.links.new(ExposureNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode
else:
mat.node_tree.links.new(lightmapNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode
mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[2]) #Connect basecolor to pbr node
mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node
mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode
#Find mainnode
mainNode = outputNode.inputs[0].links[0].from_node
#Clamp metallic
# if scene.TLM_SceneProperties.tlm_metallic_clamp != "ignore":
# if mainNode.type == "BSDF_PRINCIPLED":
# if len(mainNode.inputs[4].links) == 0:
# if scene.TLM_SceneProperties.tlm_metallic_clamp == "zero":
# mainNode.inputs[4].default_value = 0.0
# else:
# mainNode.inputs[4].default_value = 0.99
# else:
# pass
#Add all nodes first
#Add lightmap multipliction texture
mixNode = node_tree.nodes.new(type="ShaderNodeMixRGB")
mixNode.name = "Lightmap_Multiplication"
mixNode.location = -300, 300
if scene.TLM_EngineProperties.tlm_lighting_mode == "indirect" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectAO":
mixNode.blend_type = 'ADD'
else:
mixNode.blend_type = 'MULTIPLY'
mixNode.inputs[0].default_value = 1.0
UVLightmap = node_tree.nodes.new(type="ShaderNodeUVMap")
UVLightmap.uv_map = "UVMap_Lightmap"
UVLightmap.name = "Lightmap_UV"
UVLightmap.location = -1000, 300
if(scene.TLM_SceneProperties.tlm_decoder_setup):
if scene.TLM_SceneProperties.tlm_encoding_device == "CPU":
if scene.TLM_SceneProperties.tlm_encoding_mode_a == 'RGBM':
DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup")
DecodeNode.node_tree = bpy.data.node_groups["RGBM Decode"]
DecodeNode.location = -400, 300
DecodeNode.name = "Lightmap_RGBM_Decode"
decoding = True
if scene.TLM_SceneProperties.tlm_encoding_mode_b == "RGBD":
DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup")
DecodeNode.node_tree = bpy.data.node_groups["RGBD Decode"]
DecodeNode.location = -400, 300
DecodeNode.name = "Lightmap_RGBD_Decode"
decoding = True
else:
if scene.TLM_SceneProperties.tlm_encoding_mode_b == 'RGBM':
DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup")
DecodeNode.node_tree = bpy.data.node_groups["RGBM Decode"]
DecodeNode.location = -400, 300
DecodeNode.name = "Lightmap_RGBM_Decode"
decoding = True
if scene.TLM_SceneProperties.tlm_encoding_mode_b == "RGBD":
DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup")
DecodeNode.node_tree = bpy.data.node_groups["RGBD Decode"]
DecodeNode.location = -400, 300
DecodeNode.name = "Lightmap_RGBD_Decode"
decoding = True
if scene.TLM_SceneProperties.tlm_encoding_mode_b == "LogLuv":
DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup")
DecodeNode.node_tree = bpy.data.node_groups["LogLuv Decode"]
DecodeNode.location = -400, 300
DecodeNode.name = "Lightmap_LogLuv_Decode"
decoding = True
if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0):
ExposureNode = node_tree.nodes.new(type="ShaderNodeGroup")
ExposureNode.node_tree = bpy.data.node_groups["Exposure"]
ExposureNode.inputs[1].default_value = scene.TLM_EngineProperties.tlm_exposure_multiplier
ExposureNode.location = -500, 300
ExposureNode.name = "Lightmap_Exposure"
#Add Basecolor node
if len(mainNode.inputs[0].links) == 0:
baseColorValue = mainNode.inputs[0].default_value
baseColorNode = node_tree.nodes.new(type="ShaderNodeRGB")
baseColorNode.outputs[0].default_value = baseColorValue
baseColorNode.location = ((mainNode.location[0] - 500, mainNode.location[1] - 300))
baseColorNode.name = "Lightmap_BasecolorNode_A"
else:
baseColorNode = mainNode.inputs[0].links[0].from_node
baseColorNode.name = "LM_P"
#Linking
if decoding and scene.TLM_SceneProperties.tlm_encoding_use:
if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0):
mat.node_tree.links.new(lightmapNode.outputs[0], DecodeNode.inputs[0]) #Connect lightmap node to mixnode
mat.node_tree.links.new(lightmapNode.outputs[1], DecodeNode.inputs[1]) #Connect lightmap node to mixnode
mat.node_tree.links.new(DecodeNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode
mat.node_tree.links.new(ExposureNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode
else:
mat.node_tree.links.new(lightmapNode.outputs[0], DecodeNode.inputs[0]) #Connect lightmap node to mixnode
mat.node_tree.links.new(lightmapNode.outputs[1], DecodeNode.inputs[1]) #Connect lightmap node to mixnode
mat.node_tree.links.new(DecodeNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode
mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[2]) #Connect basecolor to pbr node
mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node
mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode
else:
if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0):
mat.node_tree.links.new(lightmapNode.outputs[0], ExposureNode.inputs[0]) #Connect lightmap node to mixnode
mat.node_tree.links.new(ExposureNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode
else:
mat.node_tree.links.new(lightmapNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode
mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[2]) #Connect basecolor to pbr node
mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node
mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode
def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"):
print(ext_postfix, new_postfix, formatHDR)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(ext_postfix, new_postfix, formatHDR)
for obj in bpy.data.objects:
if obj.type == "MESH":
@ -151,7 +252,8 @@ def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"):
if node.name == "Baked Image" or node.name == "TLM_Lightmap":
img_name = node.image.filepath_raw
cutLen = len(ext_postfix + formatHDR)
print("Len:" + str(len(ext_postfix + formatHDR)) + "|" + ext_postfix + ".." + formatHDR)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Len:" + str(len(ext_postfix + formatHDR)) + "|" + ext_postfix + ".." + formatHDR)
#Simple way to sort out objects with multiple materials
if formatHDR == ".hdr" or formatHDR == ".exr":
@ -172,7 +274,7 @@ def load_library(asset_name):
if bpy.data.filepath.endswith('tlm_data.blend'): # Prevent load in library itself
return
data_path = os.path.abspath(os.path.join(scriptDir, '..', '..', 'Assets/tlm_data.blend'))
data_path = os.path.abspath(os.path.join(scriptDir, '..', '..', 'assets/tlm_data.blend'))
data_names = [asset_name]
# Import

View file

@ -64,9 +64,83 @@ def configure_meshes(self):
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
for slot in obj.material_slots:
if "." + slot.name + '_Original' in bpy.data.materials:
print("The material: " + slot.name + " shifted to " + "." + slot.name + '_Original')
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("The material: " + slot.name + " shifted to " + "." + slot.name + '_Original')
slot.material = bpy.data.materials["." + slot.name + '_Original']
#ATLAS
for atlasgroup in scene.TLM_AtlasList:
atlas = atlasgroup.name
atlas_items = []
bpy.ops.object.select_all(action='DESELECT')
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
uv_layers = obj.data.uv_layers
if not "UVMap_Lightmap" in uv_layers:
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("UVMap made A")
uvmap = uv_layers.new(name="UVMap_Lightmap")
uv_layers.active_index = len(uv_layers) - 1
else:
print("Existing found...skipping")
for i in range(0, len(uv_layers)):
if uv_layers[i].name == 'UVMap_Lightmap':
uv_layers.active_index = i
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Lightmap shift A")
break
atlas_items.append(obj)
obj.select_set(True)
if scene.TLM_SceneProperties.tlm_apply_on_unwrap:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
if atlasgroup.tlm_atlas_lightmap_unwrap_mode == "SmartProject":
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Smart Project A for: " + str(atlas_items))
for obj in atlas_items:
print(obj.name + ": Active UV: " + obj.data.uv_layers[obj.data.uv_layers.active_index].name)
if len(atlas_items) > 0:
bpy.context.view_layer.objects.active = atlas_items[0]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=atlasgroup.tlm_atlas_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False)
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Lightmap":
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=atlasgroup.tlm_atlas_unwrap_margin)
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Xatlas":
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Temporary skip: COPYING SMART PROJECT")
for obj in atlas_items:
obj.select_set(True)
if len(atlas_items) > 0:
bpy.context.view_layer.objects.active = atlas_items[0]
bpy.ops.object.mode_set(mode='EDIT')
Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj)
bpy.ops.object.mode_set(mode='OBJECT')
else:
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Copied Existing A")
for obj in bpy.data.objects:
if obj.type == "MESH":
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
@ -96,10 +170,11 @@ def configure_meshes(self):
preprocess_material(obj, scene)
#UV Layer management here
if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup":
if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
uv_layers = obj.data.uv_layers
if not "UVMap_Lightmap" in uv_layers:
print("UVMap made B")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("UVMap made B")
uvmap = uv_layers.new(name="UVMap_Lightmap")
uv_layers.active_index = len(uv_layers) - 1
@ -111,7 +186,8 @@ def configure_meshes(self):
#If smart project
elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject":
print("Smart Project B")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Smart Project B")
if scene.TLM_SceneProperties.tlm_apply_on_unwrap:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
bpy.ops.object.select_all(action='DESELECT')
@ -132,22 +208,24 @@ def configure_meshes(self):
#bpy.ops.object.setup_unwrap()
Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj)
elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup":
elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer)
else: #if copy existing
print("Copied Existing B")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Copied Existing B")
#Here we copy an existing map
pass
else:
print("Existing found...skipping")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Existing found...skipping")
for i in range(0, len(uv_layers)):
if uv_layers[i].name == 'UVMap_Lightmap':
uv_layers.active_index = i
print("Lightmap shift B")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Lightmap shift B")
break
#Sort out nodes
@ -178,29 +256,32 @@ def configure_meshes(self):
mainNode = find_node_by_type(nodetree.nodes, Node_Types.diffuse)[0]
else:
self.report({'INFO'}, "No supported nodes. Continuing anyway.")
pass
if mainNode.type == 'GROUP':
if mainNode.node_tree != "Armory PBR":
print("The material group is not supported!")
pass
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("The material group is not supported!")
if (mainNode.type == "BSDF_PRINCIPLED"):
print("BSDF_Principled")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("BSDF_Principled")
if scene.TLM_EngineProperties.tlm_directional_mode == "None":
print("Directional mode")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Directional mode")
if not len(mainNode.inputs[19].links) == 0:
print("NOT LEN 0")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("NOT LEN 0")
ninput = mainNode.inputs[19].links[0]
noutput = mainNode.inputs[19].links[0].from_node
nodetree.links.remove(noutput.outputs[0].links[0])
#Clamp metallic
# if(mainNode.inputs[4].default_value == 1 and scene.TLM_SceneProperties.tlm_clamp_metallic):
# mainNode.inputs[4].default_value = 0.99
if(mainNode.inputs[4].default_value == 1):
mainNode.inputs[4].default_value = 0.0
if (mainNode.type == "BSDF_DIFFUSE"):
print("BSDF_Diffuse")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("BSDF_Diffuse")
for slot in obj.material_slots:
@ -290,44 +371,95 @@ def preprocess_material(obj, scene):
else:
supersampling_scale = 1
res = int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale)
#If image not in bpy.data.images or if size changed, make a new image
if img_name not in bpy.data.images or bpy.data.images[img_name].size[0] != res or bpy.data.images[img_name].size[1] != res:
img = bpy.data.images.new(img_name, res, res, alpha=True, float_buffer=True)
if (obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and obj.TLM_ObjectProperties.tlm_atlas_pointer != ""):
num_pixels = len(img.pixels)
result_pixel = list(img.pixels)
atlas_image_name = obj.TLM_ObjectProperties.tlm_atlas_pointer + "_baked"
for i in range(0,num_pixels,4):
# result_pixel[i+0] = scene.TLM_SceneProperties.tlm_default_color[0]
# result_pixel[i+1] = scene.TLM_SceneProperties.tlm_default_color[1]
# result_pixel[i+2] = scene.TLM_SceneProperties.tlm_default_color[2]
result_pixel[i+0] = 0.0
result_pixel[i+1] = 0.0
result_pixel[i+2] = 0.0
result_pixel[i+3] = 1.0
res = int(scene.TLM_AtlasList[obj.TLM_ObjectProperties.tlm_atlas_pointer].tlm_atlas_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale)
img.pixels = result_pixel
#If image not in bpy.data.images or if size changed, make a new image
if atlas_image_name not in bpy.data.images or bpy.data.images[atlas_image_name].size[0] != res or bpy.data.images[atlas_image_name].size[1] != res:
img = bpy.data.images.new(img_name, res, res, alpha=True, float_buffer=True)
img.name = img_name
else:
img = bpy.data.images[img_name]
num_pixels = len(img.pixels)
result_pixel = list(img.pixels)
for slot in obj.material_slots:
mat = slot.material
mat.use_nodes = True
nodes = mat.node_tree.nodes
for i in range(0,num_pixels,4):
if "Baked Image" in nodes:
img_node = nodes["Baked Image"]
if scene.TLM_SceneProperties.tlm_override_bg_color:
result_pixel[i+0] = scene.TLM_SceneProperties.tlm_override_color[0]
result_pixel[i+1] = scene.TLM_SceneProperties.tlm_override_color[1]
result_pixel[i+2] = scene.TLM_SceneProperties.tlm_override_color[2]
else:
result_pixel[i+0] = 0.0
result_pixel[i+1] = 0.0
result_pixel[i+2] = 0.0
result_pixel[i+3] = 1.0
img.pixels = result_pixel
img.name = atlas_image_name
else:
img_node = nodes.new('ShaderNodeTexImage')
img_node.name = 'Baked Image'
img_node.location = (100, 100)
img_node.image = img
img_node.select = True
nodes.active = img_node
img = bpy.data.images[atlas_image_name]
for slot in obj.material_slots:
mat = slot.material
mat.use_nodes = True
nodes = mat.node_tree.nodes
if "Baked Image" in nodes:
img_node = nodes["Baked Image"]
else:
img_node = nodes.new('ShaderNodeTexImage')
img_node.name = 'Baked Image'
img_node.location = (100, 100)
img_node.image = img
img_node.select = True
nodes.active = img_node
else:
res = int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale)
#If image not in bpy.data.images or if size changed, make a new image
if img_name not in bpy.data.images or bpy.data.images[img_name].size[0] != res or bpy.data.images[img_name].size[1] != res:
img = bpy.data.images.new(img_name, res, res, alpha=True, float_buffer=True)
num_pixels = len(img.pixels)
result_pixel = list(img.pixels)
for i in range(0,num_pixels,4):
if scene.TLM_SceneProperties.tlm_override_bg_color:
result_pixel[i+0] = scene.TLM_SceneProperties.tlm_override_color[0]
result_pixel[i+1] = scene.TLM_SceneProperties.tlm_override_color[1]
result_pixel[i+2] = scene.TLM_SceneProperties.tlm_override_color[2]
else:
result_pixel[i+0] = 0.0
result_pixel[i+1] = 0.0
result_pixel[i+2] = 0.0
result_pixel[i+3] = 1.0
img.pixels = result_pixel
img.name = img_name
else:
img = bpy.data.images[img_name]
for slot in obj.material_slots:
mat = slot.material
mat.use_nodes = True
nodes = mat.node_tree.nodes
if "Baked Image" in nodes:
img_node = nodes["Baked Image"]
else:
img_node = nodes.new('ShaderNodeTexImage')
img_node.name = 'Baked Image'
img_node.location = (100, 100)
img_node.image = img
img_node.select = True
nodes.active = img_node
def set_settings():
@ -337,6 +469,13 @@ def set_settings():
sceneProperties = scene.TLM_SceneProperties
engineProperties = scene.TLM_EngineProperties
cycles.device = scene.TLM_EngineProperties.tlm_mode
if cycles.device == "GPU":
scene.render.tile_x = 256
scene.render.tile_y = 256
else:
scene.render.tile_x = 32
scene.render.tile_y = 32
if engineProperties.tlm_quality == "0":
cycles.samples = 32

View file

@ -36,7 +36,8 @@ class TLM_Integrated_Denoise:
for image in self.image_array:
print("Image...: " + image)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Image...: " + image)
img = bpy.data.images.load(self.image_output_destination + "/" + image)

View file

@ -27,22 +27,22 @@ class TLM_OIDN_Denoise:
file = os.path.basename(os.path.realpath(oidnPath))
filename, file_extension = os.path.splitext(file)
if(file_extension == ".exe"):
#if file exists oidnDenoise or denoise
pass
else:
#if file exists oidnDenoise or denoise
self.oidnProperties.tlm_oidn_path = os.path.join(self.oidnProperties.tlm_oidn_path,"oidnDenoise.exe")
if platform.system() == 'Windows':
if(file_extension == ".exe"):
pass
else:
self.oidnProperties.tlm_oidn_path = os.path.join(self.oidnProperties.tlm_oidn_path,"oidnDenoise.exe")
else:
print("Please provide OIDN path")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Please provide OIDN path")
def denoise(self):
@ -71,9 +71,10 @@ class TLM_OIDN_Denoise:
self.save_pfm(fileWritePFM, image_output_array)
#Denoise
print("Loaded image: " + str(loaded_image))
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Loaded image: " + str(loaded_image))
verbose = self.oidnProperties.tlm_oidn_verbose
verbose = bpy.context.scene.TLM_SceneProperties.tlm_verbose
affinity = self.oidnProperties.tlm_oidn_affinity
if verbose:
@ -98,6 +99,9 @@ class TLM_OIDN_Denoise:
pipePath = [oidnPath + ' -f ' + ' RTLightmap ' + ' -hdr ' + image_output_denoise_destination + ' -o ' + image_output_denoise_result_destination + ' -verbose ' + v]
else:
oidnPath = bpy.path.abspath(self.oidnProperties.tlm_oidn_path)
oidnPath = oidnPath.replace(' ', '\\ ')
image_output_denoise_destination = image_output_denoise_destination.replace(' ', '\\ ')
image_output_denoise_result_destination = image_output_denoise_result_destination.replace(' ', '\\ ')
pipePath = [oidnPath + ' -f ' + ' RTLightmap ' + ' -hdr ' + image_output_denoise_destination + ' -o ' + image_output_denoise_result_destination + ' -verbose ' + v]
if not verbose:
@ -107,6 +111,9 @@ class TLM_OIDN_Denoise:
denoisePipe.communicate()[0]
if platform.system() != 'Windows':
image_output_denoise_result_destination = image_output_denoise_result_destination.replace('\\', '')
with open(image_output_denoise_result_destination, "rb") as f:
denoise_data, scale = self.load_pfm(f)
@ -197,4 +204,4 @@ class TLM_OIDN_Denoise:
image.tofile(file)
#print("PFM export took %.3f s" % (time() - start))
#print("PFM export took %.3f s" % (time() - start))

View file

@ -40,12 +40,13 @@ class TLM_Optix_Denoise:
self.optixProperties.tlm_optix_path = os.path.join(self.optixProperties.tlm_optix_path,"Denoiser.exe")
else:
print("Please provide Optix path")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Please provide Optix path")
def denoise(self):
print("Optix: Denoising")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Optix: Denoising")
for image in self.image_array:
if image not in self.denoised_array:
@ -58,9 +59,11 @@ class TLM_Optix_Denoise:
optixPath = bpy.path.abspath(self.optixProperties.tlm_optix_path)
pipePath = [optixPath, '-i', image_path, '-o', denoise_output_destination]
elif platform.system() == 'Darwin':
print("Mac for Optix is still unsupported")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Mac for Optix is still unsupported")
else:
print("Linux for Optix is still unsupported")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Linux for Optix is still unsupported")
if self.optixProperties.tlm_optix_verbose:
denoisePipe = subprocess.Popen(pipePath, shell=True)

View file

@ -4,7 +4,7 @@ from . import utility
from fractions import Fraction
from gpu_extras.batch import batch_for_shader
def encodeLogLuv(image, outDir, quality):
def encodeLogLuvGPU(image, outDir, quality):
input_image = bpy.data.images[image.name]
image_name = input_image.name
@ -90,7 +90,8 @@ def encodeLogLuv(image, outDir, quality):
image_name = image_name[:-4]
target_image = bpy.data.images.get(image_name + '_encoded')
print(image_name + '_encoded')
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(image_name + '_encoded')
if not target_image:
target_image = bpy.data.images.new(
name = image_name + '_encoded',
@ -130,7 +131,8 @@ def encodeLogLuv(image, outDir, quality):
input_image = target_image
#Save LogLuv
print(input_image.name)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(input_image.name)
input_image.filepath_raw = outDir + "/" + input_image.name + ".png"
#input_image.filepath_raw = outDir + "_encoded.png"
input_image.file_format = "PNG"
@ -141,7 +143,196 @@ def encodeLogLuv(image, outDir, quality):
#Todo - Find a way to save
#bpy.ops.image.save_all_modified()
def encodeImageRGBM(image, maxRange, outDir, quality):
def encodeImageRGBDGPU(image, maxRange, outDir, quality):
input_image = bpy.data.images[image.name]
image_name = input_image.name
offscreen = gpu.types.GPUOffScreen(input_image.size[0], input_image.size[1])
image = input_image
vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec2 texCoord;
in vec2 pos;
out vec2 texCoord_interp;
void main()
{
//gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f);
//gl_Position.z = 1.0;
gl_Position = vec4(pos.xy, 100, 100);
texCoord_interp = texCoord;
}
'''
fragment_shader = '''
in vec2 texCoord_interp;
out vec4 fragColor;
uniform sampler2D image;
//Code from here: https://github.com/BabylonJS/Babylon.js/blob/master/src/Shaders/ShadersInclude/helperFunctions.fx
const float PI = 3.1415926535897932384626433832795;
const float HALF_MIN = 5.96046448e-08; // Smallest positive half.
const float LinearEncodePowerApprox = 2.2;
const float GammaEncodePowerApprox = 1.0 / LinearEncodePowerApprox;
const vec3 LuminanceEncodeApprox = vec3(0.2126, 0.7152, 0.0722);
const float Epsilon = 0.0000001;
#define saturate(x) clamp(x, 0.0, 1.0)
float maxEps(float x) {
return max(x, Epsilon);
}
float toLinearSpace(float color)
{
return pow(color, LinearEncodePowerApprox);
}
vec3 toLinearSpace(vec3 color)
{
return pow(color, vec3(LinearEncodePowerApprox));
}
vec4 toLinearSpace(vec4 color)
{
return vec4(pow(color.rgb, vec3(LinearEncodePowerApprox)), color.a);
}
vec3 toGammaSpace(vec3 color)
{
return pow(color, vec3(GammaEncodePowerApprox));
}
vec4 toGammaSpace(vec4 color)
{
return vec4(pow(color.rgb, vec3(GammaEncodePowerApprox)), color.a);
}
float toGammaSpace(float color)
{
return pow(color, GammaEncodePowerApprox);
}
float square(float value)
{
return value * value;
}
// Check if configurable value is needed.
const float rgbdMaxRange = 255.0;
vec4 toRGBD(vec3 color) {
float maxRGB = maxEps(max(color.r, max(color.g, color.b)));
float D = max(rgbdMaxRange / maxRGB, 1.);
D = clamp(floor(D) / 255.0, 0., 1.);
vec3 rgb = color.rgb * D;
// Helps with png quantization.
rgb = toGammaSpace(rgb);
return vec4(rgb, D);
}
vec3 fromRGBD(vec4 rgbd) {
// Helps with png quantization.
rgbd.rgb = toLinearSpace(rgbd.rgb);
// return rgbd.rgb * ((rgbdMaxRange / 255.0) / rgbd.a);
return rgbd.rgb / rgbd.a;
}
void main()
{
fragColor = toRGBD(texture(image, texCoord_interp).rgb);
}
'''
x_screen = 0
off_x = -100
off_y = -100
y_screen_flip = 0
sx = 200
sy = 200
vertices = (
(x_screen + off_x, y_screen_flip - off_y),
(x_screen + off_x, y_screen_flip - sy - off_y),
(x_screen + off_x + sx, y_screen_flip - sy - off_y),
(x_screen + off_x + sx, y_screen_flip - off_x))
if input_image.colorspace_settings.name != 'Linear':
input_image.colorspace_settings.name = 'Linear'
# Removing .exr or .hdr prefix
if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr':
image_name = image_name[:-4]
target_image = bpy.data.images.get(image_name + '_encoded')
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(image_name + '_encoded')
if not target_image:
target_image = bpy.data.images.new(
name = image_name + '_encoded',
width = input_image.size[0],
height = input_image.size[1],
alpha = True,
float_buffer = False
)
shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
batch = batch_for_shader(
shader, 'TRI_FAN',
{
"pos": vertices,
"texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)),
},
)
if image.gl_load():
raise Exception()
with offscreen.bind():
bgl.glActiveTexture(bgl.GL_TEXTURE0)
bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode)
shader.bind()
shader.uniform_int("image", 0)
batch.draw(shader)
buffer = bgl.Buffer(bgl.GL_BYTE, input_image.size[0] * input_image.size[1] * 4)
bgl.glReadBuffer(bgl.GL_BACK)
bgl.glReadPixels(0, 0, input_image.size[0], input_image.size[1], bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer)
offscreen.free()
target_image.pixels = [v / 255 for v in buffer]
input_image = target_image
#Save LogLuv
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(input_image.name)
input_image.filepath_raw = outDir + "/" + input_image.name + ".png"
#input_image.filepath_raw = outDir + "_encoded.png"
input_image.file_format = "PNG"
bpy.context.scene.render.image_settings.quality = quality
#input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene)
input_image.save()
#Todo - Find a way to save
#bpy.ops.image.save_all_modified()
def encodeImageRGBMCPU(image, maxRange, outDir, quality):
input_image = bpy.data.images[image.name]
image_name = input_image.name
@ -153,7 +344,8 @@ def encodeImageRGBM(image, maxRange, outDir, quality):
image_name = image_name[:-4]
target_image = bpy.data.images.get(image_name + '_encoded')
print(image_name + '_encoded')
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(image_name + '_encoded')
if not target_image:
target_image = bpy.data.images.new(
name = image_name + '_encoded',
@ -178,7 +370,8 @@ def encodeImageRGBM(image, maxRange, outDir, quality):
input_image = target_image
#Save RGBM
print(input_image.name)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(input_image.name)
input_image.filepath_raw = outDir + "/" + input_image.name + ".png"
input_image.file_format = "PNG"
bpy.context.scene.render.image_settings.quality = quality
@ -193,13 +386,16 @@ def encodeImageRGBM(image, maxRange, outDir, quality):
#input_image.save()
def saturate(num, floats=True):
if num < 0:
if num <= 0:
num = 0
elif num > (1 if floats else 255):
num = (1 if floats else 255)
return num
def encodeImageRGBD(image, maxRange, outDir, quality):
def maxEps(x):
return max(x, 1e-6)
def encodeImageRGBDCPU(image, maxRange, outDir, quality):
input_image = bpy.data.images[image.name]
image_name = input_image.name
@ -223,23 +419,63 @@ def encodeImageRGBD(image, maxRange, outDir, quality):
num_pixels = len(input_image.pixels)
result_pixel = list(input_image.pixels)
rgbdMaxRange = 255.0
for i in range(0,num_pixels,4):
m = utility.saturate(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2], 1e-6))
d = max(maxRange / m, 1)
d = utility.saturate(math.floor(d) / 255 )
maxRGB = maxEps(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2]))
D = max(rgbdMaxRange/maxRGB, 1.0)
D = np.clip((math.floor(D) / 255.0), 0.0, 1.0)
result_pixel[i] = result_pixel[i] * d * 255 / maxRange
result_pixel[i+1] = result_pixel[i+1] * d * 255 / maxRange
result_pixel[i+2] = result_pixel[i+2] * d * 255 / maxRange
result_pixel[i+3] = d
result_pixel[i] = math.pow(result_pixel[i] * D, 1/2.2)
result_pixel[i+1] = math.pow(result_pixel[i+1] * D, 1/2.2)
result_pixel[i+2] = math.pow(result_pixel[i+2] * D, 1/2.2)
result_pixel[i+3] = D
# for i in range(0,num_pixels,4):
# m = saturate(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2], 1e-6))
# d = max(maxRange / m, 1)
# #d = saturate(math.floor(d) / 255.0)
# d = np.clip((math.floor(d) / 255.0), 0.0, 1.0)
# #TODO TO GAMMA SPACE
# result_pixel[i] = math.pow(result_pixel[i] * d * 255 / maxRange, 1/2.2)
# result_pixel[i+1] = math.pow(result_pixel[i+1] * d * 255 / maxRange, 1/2.2)
# result_pixel[i+2] = math.pow(result_pixel[i+2] * d * 255 / maxRange, 1/2.2)
# result_pixel[i+3] = d
target_image.pixels = result_pixel
input_image = target_image
#Save RGBD
input_image.filepath_raw = outDir + "_encoded.png"
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print(input_image.name)
input_image.filepath_raw = outDir + "/" + input_image.name + ".png"
input_image.file_format = "PNG"
bpy.context.scene.render.image_settings.quality = quality
input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene)
input_image.save()
# const float rgbdMaxRange = 255.0;
# vec4 toRGBD(vec3 color) {
# float maxRGB = maxEps(max(color.r, max(color.g, color.b)));
# float D = max(rgbdMaxRange / maxRGB, 1.);
# D = clamp(floor(D) / 255.0, 0., 1.);
# vec3 rgb = color.rgb * D;
# // Helps with png quantization.
# rgb = toGammaSpace(rgb);
# return vec4(rgb, D);
# }
# const float Epsilon = 0.0000001;
# #define saturate(x) clamp(x, 0.0, 1.0)
# float maxEps(float x) {
# return max(x, Epsilon);
# }

View file

@ -10,7 +10,8 @@ class TLM_CV_Filtering:
scene = bpy.context.scene
print("Beginning filtering for files: ")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Beginning filtering for files: ")
if denoise:
file_ending = "_denoised.hdr"
@ -22,7 +23,8 @@ class TLM_CV_Filtering:
cv2 = importlib.util.find_spec("cv2")
if cv2 is None:
print("CV2 not found - Ignoring filtering")
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("CV2 not found - Ignoring filtering")
return 0
else:
cv2 = importlib.__import__("cv2")
@ -43,13 +45,31 @@ class TLM_CV_Filtering:
opencv_process_image = cv2.imread(file_input, -1)
print("Filtering: " + os.path.basename(file_input))
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Filtering: " + os.path.basename(file_input))
obj_name = os.path.basename(file_input).split("_")[0]
if bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override:
#SEAM TESTING# #####################
print("OVERRIDE!")
# obj = bpy.data.objects[obj_name]
# bpy.context.view_layer.objects.active = obj
# bpy.ops.object.mode_set(mode='EDIT')
# bpy.ops.uv.export_layout(filepath=os.path.join(lightmap_dir,obj_name), export_all=True, mode='PNG', opacity=0.0)
# bpy.ops.object.mode_set(mode='OBJECT')
# print("Exported")
#SEAM TESTING# #####################
if obj_name in bpy.data.objects:
override = bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override
elif obj_name in scene.TLM_AtlasList:
override = False
else:
override = False
if override:
print(os.path.join(lightmap_dir, file))
@ -101,7 +121,8 @@ class TLM_CV_Filtering:
cv2.imwrite(filter_file_output, opencv_bl_result)
print("Written to: " + filter_file_output)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Written to: " + filter_file_output)
else:
@ -153,8 +174,5 @@ class TLM_CV_Filtering:
cv2.imwrite(filter_file_output, opencv_bl_result)
print("Written to: " + filter_file_output)
# if file.endswith(file_ending):
# print()
# baked_image_array.append(file)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Written to: " + filter_file_output)

View file

@ -0,0 +1,303 @@
import bpy, os, sys, math, mathutils, importlib
import numpy as np
from . rectpack import newPacker, PackingMode, PackingBin
def postpack():
cv_installed = False
cv2 = importlib.util.find_spec("cv2")
if cv2 is None:
print("CV2 not found - Ignoring postpacking")
return 0
else:
cv2 = importlib.__import__("cv2")
cv_installed = True
if cv_installed:
lightmap_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir)
packedAtlas = {}
#TODO - TEST WITH ONLY 1 ATLAS AT FIRST (1 Atlas for each, but only 1 bin (no overflow))
#PackedAtlas = Packer
#Each atlas has bins
#Each bins has rects
#Each rect corresponds to a pack_object
scene = bpy.context.scene
sceneProperties = scene.TLM_SceneProperties
end = "_baked"
if sceneProperties.tlm_denoise_use:
end = "_denoised"
if sceneProperties.tlm_filtering_use:
end = "_filtered"
formatEnc = ".hdr"
image_channel_depth = cv2.IMREAD_ANYDEPTH
linear_straight = False
if sceneProperties.tlm_encoding_use and scene.TLM_EngineProperties.tlm_bake_mode != "Background":
if sceneProperties.tlm_encoding_device == "CPU":
if sceneProperties.tlm_encoding_mode_a == "HDR":
if sceneProperties.tlm_format == "EXR":
formatEnc = ".exr"
if sceneProperties.tlm_encoding_mode_a == "RGBM":
formatEnc = "_encoded.png"
image_channel_depth = cv2.IMREAD_UNCHANGED
else:
if sceneProperties.tlm_encoding_mode_b == "HDR":
if sceneProperties.tlm_format == "EXR":
formatEnc = ".exr"
if sceneProperties.tlm_encoding_mode_b == "LogLuv":
formatEnc = "_encoded.png"
image_channel_depth = cv2.IMREAD_UNCHANGED
linear_straight = True
if sceneProperties.tlm_encoding_mode_b == "RGBM":
formatEnc = "_encoded.png"
image_channel_depth = cv2.IMREAD_UNCHANGED
if sceneProperties.tlm_encoding_mode_b == "RGBD":
formatEnc = "_encoded.png"
image_channel_depth = cv2.IMREAD_UNCHANGED
packer = {}
for atlas in bpy.context.scene.TLM_PostAtlasList: #For each atlas
packer[atlas.name] = newPacker(PackingMode.Offline, PackingBin.BFF, rotation=False)
if scene.TLM_EngineProperties.tlm_setting_supersample == "2x":
supersampling_scale = 2
elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x":
supersampling_scale = 4
else:
supersampling_scale = 1
atlas_resolution = int(int(atlas.tlm_atlas_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale))
packer[atlas.name].add_bin(atlas_resolution, atlas_resolution, 1)
#AtlasList same name prevention?
rect = []
#For each object that targets the atlas
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_postpack_object:
if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name:
res = int(int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale))
rect.append((res, res, obj.name))
for r in rect:
packer[atlas.name].add_rect(*r)
packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 3), dtype="float32")
#Continue here...overwrite value if using 8-bit encoding
if sceneProperties.tlm_encoding_use:
if sceneProperties.tlm_encoding_device == "CPU":
if sceneProperties.tlm_encoding_mode_a == "RGBM":
packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8)
if sceneProperties.tlm_encoding_mode_a == "RGBD":
packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8)
if sceneProperties.tlm_encoding_device == "GPU":
if sceneProperties.tlm_encoding_mode_b == "RGBM":
packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8)
if sceneProperties.tlm_encoding_mode_b == "RGBD":
packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8)
if sceneProperties.tlm_encoding_mode_b == "LogLuv":
packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8)
packer[atlas.name].pack()
for idy, rect in enumerate(packer[atlas.name].rect_list()):
aob = rect[5]
src = cv2.imread(os.path.join(lightmap_directory, aob + end + formatEnc), image_channel_depth) #"_baked.hdr"
print("Obj name is: " + aob)
x,y,w,h = rect[1],rect[2],rect[3],rect[4]
print(src.shape)
print(packedAtlas[atlas.name].shape)
packedAtlas[atlas.name][y:h+y, x:w+x] = src
obj = bpy.data.objects[aob]
for idx, layer in enumerate(obj.data.uv_layers):
if layer.name == "UVMap_Lightmap":
obj.data.uv_layers.active_index = idx
print("UVLayer set to: " + str(obj.data.uv_layers.active_index))
for uv_verts in obj.data.uv_layers.active.data:
#SET UV LAYER TO
atlasRes = atlas_resolution
texRes = rect[3]
x,y,w,z = x,y,texRes,texRes
ratio = atlasRes/texRes
if x == 0:
x_offset = 0
else:
x_offset = 1/(atlasRes/x)
if y == 0:
y_offset = 0
else:
y_offset = 1/(atlasRes/y)
vertex_x = (uv_verts.uv[0] * 1/(ratio)) + x_offset
vertex_y = (1 - ((uv_verts.uv[1] * 1/(ratio)) + y_offset))
uv_verts.uv[0] = vertex_x
uv_verts.uv[1] = vertex_y
scaleUV(obj.data.uv_layers.active, (1, -1), getBoundsCenter(obj.data.uv_layers.active))
print(getCenter(obj.data.uv_layers.active))
cv2.imwrite(os.path.join(lightmap_directory, atlas.name + end + formatEnc), packedAtlas[atlas.name])
print("Written: " + str(os.path.join(lightmap_directory, atlas.name + end + formatEnc)))
#Change the material for each material, slot
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_postpack_object:
if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name:
for slot in obj.material_slots:
nodetree = slot.material.node_tree
for node in nodetree.nodes:
if node.name == "TLM_Lightmap":
existing_image = node.image
atlasImage = bpy.data.images.load(os.path.join(lightmap_directory, atlas.name + end + formatEnc), check_existing=True)
if linear_straight:
if atlasImage.colorspace_settings.name != 'Linear':
atlasImage.colorspace_settings.name = 'Linear'
node.image = atlasImage
os.remove(os.path.join(lightmap_directory, obj.name + end + formatEnc))
existing_image.user_clear()
#Add dilation map here...
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_postpack_object:
if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name:
if atlas.tlm_atlas_dilation:
for slot in obj.material_slots:
nodetree = slot.material.node_tree
for node in nodetree.nodes:
if node.name == "TLM_Lightmap":
existing_image = node.image
atlasImage = bpy.data.images.load(os.path.join(lightmap_directory, atlas.name + end + formatEnc), check_existing=True)
img = cv2.imread(atlasImage.filepath_raw, image_channel_depth)
kernel = np.ones((5,5), dtype="float32")
img_dilation = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
cv2.imshow('Dilation', img_dilation)
cv2.waitKey(0)
print("TODO: Adding dilation for: " + obj.name)
#TODO MASKING OPTION!
else:
print("OpenCV not installed. Skipping postpacking process.")
def getCenter(uv_layer):
total_x, total_y = 0,0
len = 0
for uv_verts in uv_layer.data:
total_x += uv_verts.uv[0]
total_y += uv_verts.uv[1]
len += 1
center_x = total_x / len
center_y = total_y / len
return (center_x, center_y)
def getBoundsCenter(uv_layer):
min_x = getCenter(uv_layer)[0]
max_x = getCenter(uv_layer)[0]
min_y = getCenter(uv_layer)[1]
max_y = getCenter(uv_layer)[1]
len = 0
for uv_verts in uv_layer.data:
if uv_verts.uv[0] < min_x:
min_x = uv_verts.uv[0]
if uv_verts.uv[0] > max_x:
max_x = uv_verts.uv[0]
if uv_verts.uv[1] < min_y:
min_y = uv_verts.uv[1]
if uv_verts.uv[1] > max_y:
max_y = uv_verts.uv[1]
center_x = (max_x - min_x) / 2 + min_x
center_y = (max_y - min_y) / 2 + min_y
return (center_x, center_y)
def scale2D(v, s, p):
return (p[0] + s[0]*(v[0] - p[0]), p[1] + s[1]*(v[1] - p[1]))
def scaleUV( uvMap, scale, pivot ):
for uvIndex in range( len(uvMap.data) ):
uvMap.data[uvIndex].uv = scale2D(uvMap.data[uvIndex].uv, scale, pivot)

View file

@ -0,0 +1,23 @@
from .guillotine import GuillotineBssfSas, GuillotineBssfLas, \
GuillotineBssfSlas, GuillotineBssfLlas, GuillotineBssfMaxas, \
GuillotineBssfMinas, GuillotineBlsfSas, GuillotineBlsfLas, \
GuillotineBlsfSlas, GuillotineBlsfLlas, GuillotineBlsfMaxas, \
GuillotineBlsfMinas, GuillotineBafSas, GuillotineBafLas, \
GuillotineBafSlas, GuillotineBafLlas, GuillotineBafMaxas, \
GuillotineBafMinas
from .maxrects import MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf
from .skyline import SkylineMwf, SkylineMwfl, SkylineBl, \
SkylineBlWm, SkylineMwfWm, SkylineMwflWm
from .packer import SORT_AREA, SORT_PERI, SORT_DIFF, SORT_SSIDE, \
SORT_LSIDE, SORT_RATIO, SORT_NONE
from .packer import PackerBNF, PackerBFF, PackerBBF, PackerOnlineBNF, \
PackerOnlineBFF, PackerOnlineBBF, PackerGlobal, newPacker, \
PackingMode, PackingBin, float2dec

View file

@ -0,0 +1,148 @@
import heapq # heapq.heappush, heapq.heappop
from .packer import newPacker, PackingMode, PackingBin, SORT_LSIDE
from .skyline import SkylineBlWm
class Enclose(object):
def __init__(self, rectangles=[], max_width=None, max_height=None, rotation=True):
"""
Arguments:
rectangles (list): Rectangle to be enveloped
[(width1, height1), (width2, height2), ...]
max_width (number|None): Enveloping rectangle max allowed width.
max_height (number|None): Enveloping rectangle max allowed height.
rotation (boolean): Enable/Disable rectangle rotation.
"""
# Enclosing rectangle max width
self._max_width = max_width
# Encloseing rectangle max height
self._max_height = max_height
# Enable or disable rectangle rotation
self._rotation = rotation
# Default packing algorithm
self._pack_algo = SkylineBlWm
# rectangles to enclose [(width, height), (width, height, ...)]
self._rectangles = []
for r in rectangles:
self.add_rect(*r)
def _container_candidates(self):
"""Generate container candidate list
Returns:
tuple list: [(width1, height1), (width2, height2), ...]
"""
if not self._rectangles:
return []
if self._rotation:
sides = sorted(side for rect in self._rectangles for side in rect)
max_height = sum(max(r[0], r[1]) for r in self._rectangles)
min_width = max(min(r[0], r[1]) for r in self._rectangles)
max_width = max_height
else:
sides = sorted(r[0] for r in self._rectangles)
max_height = sum(r[1] for r in self._rectangles)
min_width = max(r[0] for r in self._rectangles)
max_width = sum(sides)
if self._max_width and self._max_width < max_width:
max_width = self._max_width
if self._max_height and self._max_height < max_height:
max_height = self._max_height
assert(max_width>min_width)
# Generate initial container widths
candidates = [max_width, min_width]
width = 0
for s in reversed(sides):
width += s
candidates.append(width)
width = 0
for s in sides:
width += s
candidates.append(width)
candidates.append(max_width)
candidates.append(min_width)
# Remove duplicates and widths too big or small
seen = set()
seen_add = seen.add
candidates = [x for x in candidates if not(x in seen or seen_add(x))]
candidates = [x for x in candidates if not(x>max_width or x<min_width)]
# Remove candidates too small to fit all the rectangles
min_area = sum(r[0]*r[1] for r in self._rectangles)
return [(c, max_height) for c in candidates if c*max_height>=min_area]
def _refine_candidate(self, width, height):
"""
Use bottom-left packing algorithm to find a lower height for the
container.
Arguments:
width
height
Returns:
tuple (width, height, PackingAlgorithm):
"""
packer = newPacker(PackingMode.Offline, PackingBin.BFF,
pack_algo=self._pack_algo, sort_algo=SORT_LSIDE,
rotation=self._rotation)
packer.add_bin(width, height)
for r in self._rectangles:
packer.add_rect(*r)
packer.pack()
# Check all rectangles where packed
if len(packer[0]) != len(self._rectangles):
return None
# Find highest rectangle
new_height = max(packer[0], key=lambda x: x.top).top
return(width, new_height, packer)
def generate(self):
# Generate initial containers
candidates = self._container_candidates()
if not candidates:
return None
# Refine candidates and return the one with the smaller area
containers = [self._refine_candidate(*c) for c in candidates]
containers = [c for c in containers if c]
if not containers:
return None
width, height, packer = min(containers, key=lambda x: x[0]*x[1])
packer.width = width
packer.height = height
return packer
def add_rect(self, width, height):
"""
Add anoter rectangle to be enclosed
Arguments:
width (number): Rectangle width
height (number): Rectangle height
"""
self._rectangles.append((width, height))

View file

@ -0,0 +1,344 @@
from math import sqrt
class Point(object):
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return (self.x == other.x and self.y == other.y)
def __repr__(self):
return "P({}, {})".format(self.x, self.y)
def distance(self, point):
"""
Calculate distance to another point
"""
return sqrt((self.x-point.x)**2+(self.y-point.y)**2)
def distance_squared(self, point):
return (self.x-point.x)**2+(self.y-point.y)**2
class Segment(object):
__slots__ = ('start', 'end')
def __init__(self, start, end):
"""
Arguments:
start (Point): Segment start point
end (Point): Segment end point
"""
assert(isinstance(start, Point) and isinstance(end, Point))
self.start = start
self.end = end
def __eq__(self, other):
if not isinstance(other, self.__class__):
None
return self.start==other.start and self.end==other.end
def __repr__(self):
return "S({}, {})".format(self.start, self.end)
@property
def length_squared(self):
"""Faster than length and useful for some comparisons"""
return self.start.distance_squared(self.end)
@property
def length(self):
return self.start.distance(self.end)
@property
def top(self):
return max(self.start.y, self.end.y)
@property
def bottom(self):
return min(self.start.y, self.end.y)
@property
def right(self):
return max(self.start.x, self.end.x)
@property
def left(self):
return min(self.start.x, self.end.x)
class HSegment(Segment):
"""Horizontal Segment"""
def __init__(self, start, length):
"""
Create an Horizontal segment given its left most end point and its
length.
Arguments:
- start (Point): Starting Point
- length (number): segment length
"""
assert(isinstance(start, Point) and not isinstance(length, Point))
super(HSegment, self).__init__(start, Point(start.x+length, start.y))
@property
def length(self):
return self.end.x-self.start.x
class VSegment(Segment):
"""Vertical Segment"""
def __init__(self, start, length):
"""
Create a Vertical segment given its bottom most end point and its
length.
Arguments:
- start (Point): Starting Point
- length (number): segment length
"""
assert(isinstance(start, Point) and not isinstance(length, Point))
super(VSegment, self).__init__(start, Point(start.x, start.y+length))
@property
def length(self):
return self.end.y-self.start.y
class Rectangle(object):
"""Basic rectangle primitive class.
x, y-> Lower right corner coordinates
width -
height -
"""
__slots__ = ('width', 'height', 'x', 'y', 'rid')
def __init__(self, x, y, width, height, rid = None):
"""
Args:
x (int, float):
y (int, float):
width (int, float):
height (int, float):
rid (int):
"""
assert(height >=0 and width >=0)
self.width = width
self.height = height
self.x = x
self.y = y
self.rid = rid
@property
def bottom(self):
"""
Rectangle bottom edge y coordinate
"""
return self.y
@property
def top(self):
"""
Rectangle top edge y coordiante
"""
return self.y+self.height
@property
def left(self):
"""
Rectangle left ednge x coordinate
"""
return self.x
@property
def right(self):
"""
Rectangle right edge x coordinate
"""
return self.x+self.width
@property
def corner_top_l(self):
return Point(self.left, self.top)
@property
def corner_top_r(self):
return Point(self.right, self.top)
@property
def corner_bot_r(self):
return Point(self.right, self.bottom)
@property
def corner_bot_l(self):
return Point(self.left, self.bottom)
def __lt__(self, other):
"""
Compare rectangles by area (used for sorting)
"""
return self.area() < other.area()
def __eq__(self, other):
"""
Equal rectangles have same area.
"""
if not isinstance(other, self.__class__):
return False
return (self.width == other.width and \
self.height == other.height and \
self.x == other.x and \
self.y == other.y)
def __hash__(self):
return hash((self.x, self.y, self.width, self.height))
def __iter__(self):
"""
Iterate through rectangle corners
"""
yield self.corner_top_l
yield self.corner_top_r
yield self.corner_bot_r
yield self.corner_bot_l
def __repr__(self):
return "R({}, {}, {}, {})".format(self.x, self.y, self.width, self.height)
def area(self):
"""
Rectangle area
"""
return self.width * self.height
def move(self, x, y):
"""
Move Rectangle to x,y coordinates
Arguments:
x (int, float): X coordinate
y (int, float): Y coordinate
"""
self.x = x
self.y = y
def contains(self, rect):
"""
Tests if another rectangle is contained by this one
Arguments:
rect (Rectangle): The other rectangle
Returns:
bool: True if it is container, False otherwise
"""
return (rect.y >= self.y and \
rect.x >= self.x and \
rect.y+rect.height <= self.y+self.height and \
rect.x+rect.width <= self.x+self.width)
def intersects(self, rect, edges=False):
"""
Detect intersections between this and another Rectangle.
Parameters:
rect (Rectangle): The other rectangle.
edges (bool): True to consider rectangles touching by their
edges or corners to be intersecting.
(Should have been named include_touching)
Returns:
bool: True if the rectangles intersect, False otherwise
"""
if edges:
if (self.bottom > rect.top or self.top < rect.bottom or\
self.left > rect.right or self.right < rect.left):
return False
else:
if (self.bottom >= rect.top or self.top <= rect.bottom or
self.left >= rect.right or self.right <= rect.left):
return False
return True
def intersection(self, rect, edges=False):
"""
Returns the rectangle resulting of the intersection between this and another
rectangle. If the rectangles are only touching by their edges, and the
argument 'edges' is True the rectangle returned will have an area of 0.
Returns None if there is no intersection.
Arguments:
rect (Rectangle): The other rectangle.
edges (bool): If True Rectangles touching by their edges are
considered to be intersection. In this case a rectangle of
0 height or/and width will be returned.
Returns:
Rectangle: Intersection.
None: There was no intersection.
"""
if not self.intersects(rect, edges=edges):
return None
bottom = max(self.bottom, rect.bottom)
left = max(self.left, rect.left)
top = min(self.top, rect.top)
right = min(self.right, rect.right)
return Rectangle(left, bottom, right-left, top-bottom)
def join(self, other):
"""
Try to join a rectangle to this one, if the result is also a rectangle
and the operation is successful and this rectangle is modified to the union.
Arguments:
other (Rectangle): Rectangle to join
Returns:
bool: True when successfully joined, False otherwise
"""
if self.contains(other):
return True
if other.contains(self):
self.x = other.x
self.y = other.y
self.width = other.width
self.height = other.height
return True
if not self.intersects(other, edges=True):
return False
# Other rectangle is Up/Down from this
if self.left == other.left and self.width == other.width:
y_min = min(self.bottom, other.bottom)
y_max = max(self.top, other.top)
self.y = y_min
self.height = y_max-y_min
return True
# Other rectangle is Right/Left from this
if self.bottom == other.bottom and self.height == other.height:
x_min = min(self.left, other.left)
x_max = max(self.right, other.right)
self.x = x_min
self.width = x_max-x_min
return True
return False

View file

@ -0,0 +1,368 @@
from .pack_algo import PackingAlgorithm
from .geometry import Rectangle
import itertools
import operator
class Guillotine(PackingAlgorithm):
"""Implementation of several variants of Guillotine packing algorithm
For a more detailed explanation of the algorithm used, see:
Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
"""
def __init__(self, width, height, rot=True, merge=True, *args, **kwargs):
"""
Arguments:
width (int, float):
height (int, float):
merge (bool): Optional keyword argument
"""
self._merge = merge
super(Guillotine, self).__init__(width, height, rot, *args, **kwargs)
def _add_section(self, section):
"""Adds a new section to the free section list, but before that and if
section merge is enabled, tries to join the rectangle with all existing
sections, if successful the resulting section is again merged with the
remaining sections until the operation fails. The result is then
appended to the list.
Arguments:
section (Rectangle): New free section.
"""
section.rid = 0
plen = 0
while self._merge and self._sections and plen != len(self._sections):
plen = len(self._sections)
self._sections = [s for s in self._sections if not section.join(s)]
self._sections.append(section)
def _split_horizontal(self, section, width, height):
"""For an horizontal split the rectangle is placed in the lower
left corner of the section (section's xy coordinates), the top
most side of the rectangle and its horizontal continuation,
marks the line of division for the split.
+-----------------+
| |
| |
| |
| |
+-------+---------+
|#######| |
|#######| |
|#######| |
+-------+---------+
If the rectangle width is equal to the the section width, only one
section is created over the rectangle. If the rectangle height is
equal to the section height, only one section to the right of the
rectangle is created. If both width and height are equal, no sections
are created.
"""
# First remove the section we are splitting so it doesn't
# interfere when later we try to merge the resulting split
# rectangles, with the rest of free sections.
#self._sections.remove(section)
# Creates two new empty sections, and returns the new rectangle.
if height < section.height:
self._add_section(Rectangle(section.x, section.y+height,
section.width, section.height-height))
if width < section.width:
self._add_section(Rectangle(section.x+width, section.y,
section.width-width, height))
def _split_vertical(self, section, width, height):
"""For a vertical split the rectangle is placed in the lower
left corner of the section (section's xy coordinates), the
right most side of the rectangle and its vertical continuation,
marks the line of division for the split.
+-------+---------+
| | |
| | |
| | |
| | |
+-------+ |
|#######| |
|#######| |
|#######| |
+-------+---------+
If the rectangle width is equal to the the section width, only one
section is created over the rectangle. If the rectangle height is
equal to the section height, only one section to the right of the
rectangle is created. If both width and height are equal, no sections
are created.
"""
# When a section is split, depending on the rectangle size
# two, one, or no new sections will be created.
if height < section.height:
self._add_section(Rectangle(section.x, section.y+height,
width, section.height-height))
if width < section.width:
self._add_section(Rectangle(section.x+width, section.y,
section.width-width, section.height))
def _split(self, section, width, height):
"""
Selects the best split for a section, given a rectangle of dimmensions
width and height, then calls _split_vertical or _split_horizontal,
to do the dirty work.
Arguments:
section (Rectangle): Section to split
width (int, float): Rectangle width
height (int, float): Rectangle height
"""
raise NotImplementedError
def _section_fitness(self, section, width, height):
"""The subclass for each one of the Guillotine selection methods,
BAF, BLSF.... will override this method, this is here only
to asure a valid value return if the worst happens.
"""
raise NotImplementedError
def _select_fittest_section(self, w, h):
"""Calls _section_fitness for each of the sections in free section
list. Returns the section with the minimal fitness value, all the rest
is boilerplate to make the fitness comparison, to rotatate the rectangles,
and to take into account when _section_fitness returns None because
the rectangle couldn't be placed.
Arguments:
w (int, float): Rectangle width
h (int, float): Rectangle height
Returns:
(section, was_rotated): Returns the tuple
section (Rectangle): Section with best fitness
was_rotated (bool): The rectangle was rotated
"""
fitn = ((self._section_fitness(s, w, h), s, False) for s in self._sections
if self._section_fitness(s, w, h) is not None)
fitr = ((self._section_fitness(s, h, w), s, True) for s in self._sections
if self._section_fitness(s, h, w) is not None)
if not self.rot:
fitr = []
fit = itertools.chain(fitn, fitr)
try:
_, sec, rot = min(fit, key=operator.itemgetter(0))
except ValueError:
return None, None
return sec, rot
def add_rect(self, width, height, rid=None):
"""
Add rectangle of widthxheight dimensions.
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
rid: Optional rectangle user id
Returns:
Rectangle: Rectangle with placemente coordinates
None: If the rectangle couldn be placed.
"""
assert(width > 0 and height >0)
# Obtain the best section to place the rectangle.
section, rotated = self._select_fittest_section(width, height)
if not section:
return None
if rotated:
width, height = height, width
# Remove section, split and store results
self._sections.remove(section)
self._split(section, width, height)
# Store rectangle in the selected position
rect = Rectangle(section.x, section.y, width, height, rid)
self.rectangles.append(rect)
return rect
def fitness(self, width, height):
"""
In guillotine algorithm case, returns the min of the fitness of all
free sections, for the given dimension, both normal and rotated
(if rotation enabled.)
"""
assert(width > 0 and height > 0)
# Get best fitness section.
section, rotated = self._select_fittest_section(width, height)
if not section:
return None
# Return fitness of returned section, with correct dimmensions if the
# the rectangle was rotated.
if rotated:
return self._section_fitness(section, height, width)
else:
return self._section_fitness(section, width, height)
def reset(self):
super(Guillotine, self).reset()
self._sections = []
self._add_section(Rectangle(0, 0, self.width, self.height))
class GuillotineBaf(Guillotine):
"""Implements Best Area Fit (BAF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return section.area()-width*height
class GuillotineBlsf(Guillotine):
"""Implements Best Long Side Fit (BLSF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return max(section.width-width, section.height-height)
class GuillotineBssf(Guillotine):
"""Implements Best Short Side Fit (BSSF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return min(section.width-width, section.height-height)
class GuillotineSas(Guillotine):
"""Implements Short Axis Split (SAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if section.width < section.height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineLas(Guillotine):
"""Implements Long Axis Split (LAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if section.width >= section.height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineSlas(Guillotine):
"""Implements Short Leftover Axis Split (SLAS) selection rule for
Guillotine algorithm.
"""
def _split(self, section, width, height):
if section.width-width < section.height-height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineLlas(Guillotine):
"""Implements Long Leftover Axis Split (LLAS) selection rule for
Guillotine algorithm.
"""
def _split(self, section, width, height):
if section.width-width >= section.height-height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineMaxas(Guillotine):
"""Implements Max Area Axis Split (MAXAS) selection rule for Guillotine
algorithm. Maximize the larger area == minimize the smaller area.
Tries to make the rectangles more even-sized.
"""
def _split(self, section, width, height):
if width*(section.height-height) <= height*(section.width-width):
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineMinas(Guillotine):
"""Implements Min Area Axis Split (MINAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if width*(section.height-height) >= height*(section.width-width):
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
# Guillotine algorithms GUILLOTINE-RECT-SPLIT, Selecting one
# Axis split, and one selection criteria.
class GuillotineBssfSas(GuillotineBssf, GuillotineSas):
pass
class GuillotineBssfLas(GuillotineBssf, GuillotineLas):
pass
class GuillotineBssfSlas(GuillotineBssf, GuillotineSlas):
pass
class GuillotineBssfLlas(GuillotineBssf, GuillotineLlas):
pass
class GuillotineBssfMaxas(GuillotineBssf, GuillotineMaxas):
pass
class GuillotineBssfMinas(GuillotineBssf, GuillotineMinas):
pass
class GuillotineBlsfSas(GuillotineBlsf, GuillotineSas):
pass
class GuillotineBlsfLas(GuillotineBlsf, GuillotineLas):
pass
class GuillotineBlsfSlas(GuillotineBlsf, GuillotineSlas):
pass
class GuillotineBlsfLlas(GuillotineBlsf, GuillotineLlas):
pass
class GuillotineBlsfMaxas(GuillotineBlsf, GuillotineMaxas):
pass
class GuillotineBlsfMinas(GuillotineBlsf, GuillotineMinas):
pass
class GuillotineBafSas(GuillotineBaf, GuillotineSas):
pass
class GuillotineBafLas(GuillotineBaf, GuillotineLas):
pass
class GuillotineBafSlas(GuillotineBaf, GuillotineSlas):
pass
class GuillotineBafLlas(GuillotineBaf, GuillotineLlas):
pass
class GuillotineBafMaxas(GuillotineBaf, GuillotineMaxas):
pass
class GuillotineBafMinas(GuillotineBaf, GuillotineMinas):
pass

View file

@ -0,0 +1,244 @@
from .pack_algo import PackingAlgorithm
from .geometry import Rectangle
import itertools
import collections
import operator
first_item = operator.itemgetter(0)
class MaxRects(PackingAlgorithm):
def __init__(self, width, height, rot=True, *args, **kwargs):
super(MaxRects, self).__init__(width, height, rot, *args, **kwargs)
def _rect_fitness(self, max_rect, width, height):
"""
Arguments:
max_rect (Rectangle): Destination max_rect
width (int, float): Rectangle width
height (int, float): Rectangle height
Returns:
None: Rectangle couldn't be placed into max_rect
integer, float: fitness value
"""
if width <= max_rect.width and height <= max_rect.height:
return 0
else:
return None
def _select_position(self, w, h):
"""
Find max_rect with best fitness for placing a rectangle
of dimentsions w*h
Arguments:
w (int, float): Rectangle width
h (int, float): Rectangle height
Returns:
(rect, max_rect)
rect (Rectangle): Placed rectangle or None if was unable.
max_rect (Rectangle): Maximal rectangle were rect was placed
"""
if not self._max_rects:
return None, None
# Normal rectangle
fitn = ((self._rect_fitness(m, w, h), w, h, m) for m in self._max_rects
if self._rect_fitness(m, w, h) is not None)
# Rotated rectangle
fitr = ((self._rect_fitness(m, h, w), h, w, m) for m in self._max_rects
if self._rect_fitness(m, h, w) is not None)
if not self.rot:
fitr = []
fit = itertools.chain(fitn, fitr)
try:
_, w, h, m = min(fit, key=first_item)
except ValueError:
return None, None
return Rectangle(m.x, m.y, w, h), m
def _generate_splits(self, m, r):
"""
When a rectangle is placed inside a maximal rectangle, it stops being one
and up to 4 new maximal rectangles may appear depending on the placement.
_generate_splits calculates them.
Arguments:
m (Rectangle): max_rect rectangle
r (Rectangle): rectangle placed
Returns:
list : list containing new maximal rectangles or an empty list
"""
new_rects = []
if r.left > m.left:
new_rects.append(Rectangle(m.left, m.bottom, r.left-m.left, m.height))
if r.right < m.right:
new_rects.append(Rectangle(r.right, m.bottom, m.right-r.right, m.height))
if r.top < m.top:
new_rects.append(Rectangle(m.left, r.top, m.width, m.top-r.top))
if r.bottom > m.bottom:
new_rects.append(Rectangle(m.left, m.bottom, m.width, r.bottom-m.bottom))
return new_rects
def _split(self, rect):
"""
Split all max_rects intersecting the rectangle rect into up to
4 new max_rects.
Arguments:
rect (Rectangle): Rectangle
Returns:
split (Rectangle list): List of rectangles resulting from the split
"""
max_rects = collections.deque()
for r in self._max_rects:
if r.intersects(rect):
max_rects.extend(self._generate_splits(r, rect))
else:
max_rects.append(r)
# Add newly generated max_rects
self._max_rects = list(max_rects)
def _remove_duplicates(self):
"""
Remove every maximal rectangle contained by another one.
"""
contained = set()
for m1, m2 in itertools.combinations(self._max_rects, 2):
if m1.contains(m2):
contained.add(m2)
elif m2.contains(m1):
contained.add(m1)
# Remove from max_rects
self._max_rects = [m for m in self._max_rects if m not in contained]
def fitness(self, width, height):
"""
Metric used to rate how much space is wasted if a rectangle is placed.
Returns a value greater or equal to zero, the smaller the value the more
'fit' is the rectangle. If the rectangle can't be placed, returns None.
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
Returns:
int, float: Rectangle fitness
None: Rectangle can't be placed
"""
assert(width > 0 and height > 0)
rect, max_rect = self._select_position(width, height)
if rect is None:
return None
# Return fitness
return self._rect_fitness(max_rect, rect.width, rect.height)
def add_rect(self, width, height, rid=None):
"""
Add rectangle of widthxheight dimensions.
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
rid: Optional rectangle user id
Returns:
Rectangle: Rectangle with placemente coordinates
None: If the rectangle couldn be placed.
"""
assert(width > 0 and height >0)
# Search best position and orientation
rect, _ = self._select_position(width, height)
if not rect:
return None
# Subdivide all the max rectangles intersecting with the selected
# rectangle.
self._split(rect)
# Remove any max_rect contained by another
self._remove_duplicates()
# Store and return rectangle position.
rect.rid = rid
self.rectangles.append(rect)
return rect
def reset(self):
super(MaxRects, self).reset()
self._max_rects = [Rectangle(0, 0, self.width, self.height)]
class MaxRectsBl(MaxRects):
def _select_position(self, w, h):
"""
Select the position where the y coordinate of the top of the rectangle
is lower, if there are severtal pick the one with the smallest x
coordinate
"""
fitn = ((m.y+h, m.x, w, h, m) for m in self._max_rects
if self._rect_fitness(m, w, h) is not None)
fitr = ((m.y+w, m.x, h, w, m) for m in self._max_rects
if self._rect_fitness(m, h, w) is not None)
if not self.rot:
fitr = []
fit = itertools.chain(fitn, fitr)
try:
_, _, w, h, m = min(fit, key=first_item)
except ValueError:
return None, None
return Rectangle(m.x, m.y, w, h), m
class MaxRectsBssf(MaxRects):
"""Best Sort Side Fit minimize short leftover side"""
def _rect_fitness(self, max_rect, width, height):
if width > max_rect.width or height > max_rect.height:
return None
return min(max_rect.width-width, max_rect.height-height)
class MaxRectsBaf(MaxRects):
"""Best Area Fit pick maximal rectangle with smallest area
where the rectangle can be placed"""
def _rect_fitness(self, max_rect, width, height):
if width > max_rect.width or height > max_rect.height:
return None
return (max_rect.width*max_rect.height)-(width*height)
class MaxRectsBlsf(MaxRects):
"""Best Long Side Fit minimize long leftover side"""
def _rect_fitness(self, max_rect, width, height):
if width > max_rect.width or height > max_rect.height:
return None
return max(max_rect.width-width, max_rect.height-height)

View file

@ -0,0 +1,140 @@
from .geometry import Rectangle
class PackingAlgorithm(object):
"""PackingAlgorithm base class"""
def __init__(self, width, height, rot=True, bid=None, *args, **kwargs):
"""
Initialize packing algorithm
Arguments:
width (int, float): Packing surface width
height (int, float): Packing surface height
rot (bool): Rectangle rotation enabled or disabled
bid (string|int|...): Packing surface identification
"""
self.width = width
self.height = height
self.rot = rot
self.rectangles = []
self.bid = bid
self._surface = Rectangle(0, 0, width, height)
self.reset()
def __len__(self):
return len(self.rectangles)
def __iter__(self):
return iter(self.rectangles)
def _fits_surface(self, width, height):
"""
Test surface is big enough to place a rectangle
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
Returns:
boolean: True if it could be placed, False otherwise
"""
assert(width > 0 and height > 0)
if self.rot and (width > self.width or height > self.height):
width, height = height, width
if width > self.width or height > self.height:
return False
else:
return True
def __getitem__(self, key):
"""
Return rectangle in selected position.
"""
return self.rectangles[key]
def used_area(self):
"""
Total area of rectangles placed
Returns:
int, float: Area
"""
return sum(r.area() for r in self)
def fitness(self, width, height, rot = False):
"""
Metric used to rate how much space is wasted if a rectangle is placed.
Returns a value greater or equal to zero, the smaller the value the more
'fit' is the rectangle. If the rectangle can't be placed, returns None.
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
rot (bool): Enable rectangle rotation
Returns:
int, float: Rectangle fitness
None: Rectangle can't be placed
"""
raise NotImplementedError
def add_rect(self, width, height, rid=None):
"""
Add rectangle of widthxheight dimensions.
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
rid: Optional rectangle user id
Returns:
Rectangle: Rectangle with placemente coordinates
None: If the rectangle couldn be placed.
"""
raise NotImplementedError
def rect_list(self):
"""
Returns a list with all rectangles placed into the surface.
Returns:
List: Format [(x, y, width, height, rid), ...]
"""
rectangle_list = []
for r in self:
rectangle_list.append((r.x, r.y, r.width, r.height, r.rid))
return rectangle_list
def validate_packing(self):
"""
Check for collisions between rectangles, also check all are placed
inside surface.
"""
surface = Rectangle(0, 0, self.width, self.height)
for r in self:
if not surface.contains(r):
raise Exception("Rectangle placed outside surface")
rectangles = [r for r in self]
if len(rectangles) <= 1:
return
for r1 in range(0, len(rectangles)-2):
for r2 in range(r1+1, len(rectangles)-1):
if rectangles[r1].intersects(rectangles[r2]):
raise Exception("Rectangle collision detected")
def is_empty(self):
# Returns true if there is no rectangles placed.
return not bool(len(self))
def reset(self):
self.rectangles = [] # List of placed Rectangles.

View file

@ -0,0 +1,580 @@
from .maxrects import MaxRectsBssf
import operator
import itertools
import collections
import decimal
# Float to Decimal helper
def float2dec(ft, decimal_digits):
"""
Convert float (or int) to Decimal (rounding up) with the
requested number of decimal digits.
Arguments:
ft (float, int): Number to convert
decimal (int): Number of digits after decimal point
Return:
Decimal: Number converted to decima
"""
with decimal.localcontext() as ctx:
ctx.rounding = decimal.ROUND_UP
places = decimal.Decimal(10)**(-decimal_digits)
return decimal.Decimal.from_float(float(ft)).quantize(places)
# Sorting algos for rectangle lists
SORT_AREA = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: r[0]*r[1]) # Sort by area
SORT_PERI = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: r[0]+r[1]) # Sort by perimeter
SORT_DIFF = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: abs(r[0]-r[1])) # Sort by Diff
SORT_SSIDE = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: (min(r[0], r[1]), max(r[0], r[1]))) # Sort by short side
SORT_LSIDE = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: (max(r[0], r[1]), min(r[0], r[1]))) # Sort by long side
SORT_RATIO = lambda rectlist: sorted(rectlist, reverse=True,
key=lambda r: r[0]/r[1]) # Sort by side ratio
SORT_NONE = lambda rectlist: list(rectlist) # Unsorted
class BinFactory(object):
def __init__(self, width, height, count, pack_algo, *args, **kwargs):
self._width = width
self._height = height
self._count = count
self._pack_algo = pack_algo
self._algo_kwargs = kwargs
self._algo_args = args
self._ref_bin = None # Reference bin used to calculate fitness
self._bid = kwargs.get("bid", None)
def _create_bin(self):
return self._pack_algo(self._width, self._height, *self._algo_args, **self._algo_kwargs)
def is_empty(self):
return self._count<1
def fitness(self, width, height):
if not self._ref_bin:
self._ref_bin = self._create_bin()
return self._ref_bin.fitness(width, height)
def fits_inside(self, width, height):
# Determine if rectangle widthxheight will fit into empty bin
if not self._ref_bin:
self._ref_bin = self._create_bin()
return self._ref_bin._fits_surface(width, height)
def new_bin(self):
if self._count > 0:
self._count -= 1
return self._create_bin()
else:
return None
def __eq__(self, other):
return self._width*self._height == other._width*other._height
def __lt__(self, other):
return self._width*self._height < other._width*other._height
def __str__(self):
return "Bin: {} {} {}".format(self._width, self._height, self._count)
class PackerBNFMixin(object):
"""
BNF (Bin Next Fit): Only one open bin at a time. If the rectangle
doesn't fit, close the current bin and go to the next.
"""
def add_rect(self, width, height, rid=None):
while True:
# if there are no open bins, try to open a new one
if len(self._open_bins)==0:
# can we find an unopened bin that will hold this rect?
new_bin = self._new_open_bin(width, height, rid=rid)
if new_bin is None:
return None
# we have at least one open bin, so check if it can hold this rect
rect = self._open_bins[0].add_rect(width, height, rid=rid)
if rect is not None:
return rect
# since the rect doesn't fit, close this bin and try again
closed_bin = self._open_bins.popleft()
self._closed_bins.append(closed_bin)
class PackerBFFMixin(object):
"""
BFF (Bin First Fit): Pack rectangle in first bin it fits
"""
def add_rect(self, width, height, rid=None):
# see if this rect will fit in any of the open bins
for b in self._open_bins:
rect = b.add_rect(width, height, rid=rid)
if rect is not None:
return rect
while True:
# can we find an unopened bin that will hold this rect?
new_bin = self._new_open_bin(width, height, rid=rid)
if new_bin is None:
return None
# _new_open_bin may return a bin that's too small,
# so we have to double-check
rect = new_bin.add_rect(width, height, rid=rid)
if rect is not None:
return rect
class PackerBBFMixin(object):
"""
BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness
"""
# only create this getter once
first_item = operator.itemgetter(0)
def add_rect(self, width, height, rid=None):
# Try packing into open bins
fit = ((b.fitness(width, height), b) for b in self._open_bins)
fit = (b for b in fit if b[0] is not None)
try:
_, best_bin = min(fit, key=self.first_item)
best_bin.add_rect(width, height, rid)
return True
except ValueError:
pass
# Try packing into one of the empty bins
while True:
# can we find an unopened bin that will hold this rect?
new_bin = self._new_open_bin(width, height, rid=rid)
if new_bin is None:
return False
# _new_open_bin may return a bin that's too small,
# so we have to double-check
if new_bin.add_rect(width, height, rid):
return True
class PackerOnline(object):
"""
Rectangles are packed as soon are they are added
"""
def __init__(self, pack_algo=MaxRectsBssf, rotation=True):
"""
Arguments:
pack_algo (PackingAlgorithm): What packing algo to use
rotation (bool): Enable/Disable rectangle rotation
"""
self._rotation = rotation
self._pack_algo = pack_algo
self.reset()
def __iter__(self):
return itertools.chain(self._closed_bins, self._open_bins)
def __len__(self):
return len(self._closed_bins)+len(self._open_bins)
def __getitem__(self, key):
"""
Return bin in selected position. (excluding empty bins)
"""
if not isinstance(key, int):
raise TypeError("Indices must be integers")
size = len(self) # avoid recalulations
if key < 0:
key += size
if not 0 <= key < size:
raise IndexError("Index out of range")
if key < len(self._closed_bins):
return self._closed_bins[key]
else:
return self._open_bins[key-len(self._closed_bins)]
def _new_open_bin(self, width=None, height=None, rid=None):
"""
Extract the next empty bin and append it to open bins
Returns:
PackingAlgorithm: Initialized empty packing bin.
None: No bin big enough for the rectangle was found
"""
factories_to_delete = set() #
new_bin = None
for key, binfac in self._empty_bins.items():
# Only return the new bin if the rect fits.
# (If width or height is None, caller doesn't know the size.)
if not binfac.fits_inside(width, height):
continue
# Create bin and add to open_bins
new_bin = binfac.new_bin()
if new_bin is None:
continue
self._open_bins.append(new_bin)
# If the factory was depleted mark for deletion
if binfac.is_empty():
factories_to_delete.add(key)
break
# Delete marked factories
for f in factories_to_delete:
del self._empty_bins[f]
return new_bin
def add_bin(self, width, height, count=1, **kwargs):
# accept the same parameters as PackingAlgorithm objects
kwargs['rot'] = self._rotation
bin_factory = BinFactory(width, height, count, self._pack_algo, **kwargs)
self._empty_bins[next(self._bin_count)] = bin_factory
def rect_list(self):
rectangles = []
bin_count = 0
for abin in self:
for rect in abin:
rectangles.append((bin_count, rect.x, rect.y, rect.width, rect.height, rect.rid))
bin_count += 1
return rectangles
def bin_list(self):
"""
Return a list of the dimmensions of the bins in use, that is closed
or open containing at least one rectangle
"""
return [(b.width, b.height) for b in self]
def validate_packing(self):
for b in self:
b.validate_packing()
def reset(self):
# Bins fully packed and closed.
self._closed_bins = collections.deque()
# Bins ready to pack rectangles
self._open_bins = collections.deque()
# User provided bins not in current use
self._empty_bins = collections.OrderedDict() # O(1) deletion of arbitrary elem
self._bin_count = itertools.count()
class Packer(PackerOnline):
"""
Rectangles aren't packed untils pack() is called
"""
def __init__(self, pack_algo=MaxRectsBssf, sort_algo=SORT_NONE,
rotation=True):
"""
"""
super(Packer, self).__init__(pack_algo=pack_algo, rotation=rotation)
self._sort_algo = sort_algo
# User provided bins and Rectangles
self._avail_bins = collections.deque()
self._avail_rect = collections.deque()
# Aux vars used during packing
self._sorted_rect = []
def add_bin(self, width, height, count=1, **kwargs):
self._avail_bins.append((width, height, count, kwargs))
def add_rect(self, width, height, rid=None):
self._avail_rect.append((width, height, rid))
def _is_everything_ready(self):
return self._avail_rect and self._avail_bins
def pack(self):
self.reset()
if not self._is_everything_ready():
# maybe we should throw an error here?
return
# Add available bins to packer
for b in self._avail_bins:
width, height, count, extra_kwargs = b
super(Packer, self).add_bin(width, height, count, **extra_kwargs)
# If enabled sort rectangles
self._sorted_rect = self._sort_algo(self._avail_rect)
# Start packing
for r in self._sorted_rect:
super(Packer, self).add_rect(*r)
class PackerBNF(Packer, PackerBNFMixin):
"""
BNF (Bin Next Fit): Only one open bin, if rectangle doesn't fit
go to next bin and close current one.
"""
pass
class PackerBFF(Packer, PackerBFFMixin):
"""
BFF (Bin First Fit): Pack rectangle in first bin it fits
"""
pass
class PackerBBF(Packer, PackerBBFMixin):
"""
BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness
"""
pass
class PackerOnlineBNF(PackerOnline, PackerBNFMixin):
"""
BNF Bin Next Fit Online variant
"""
pass
class PackerOnlineBFF(PackerOnline, PackerBFFMixin):
"""
BFF Bin First Fit Online variant
"""
pass
class PackerOnlineBBF(PackerOnline, PackerBBFMixin):
"""
BBF Bin Best Fit Online variant
"""
pass
class PackerGlobal(Packer, PackerBNFMixin):
"""
GLOBAL: For each bin pack the rectangle with the best fitness.
"""
first_item = operator.itemgetter(0)
def __init__(self, pack_algo=MaxRectsBssf, rotation=True):
"""
"""
super(PackerGlobal, self).__init__(pack_algo=pack_algo,
sort_algo=SORT_NONE, rotation=rotation)
def _find_best_fit(self, pbin):
"""
Return best fitness rectangle from rectangles packing _sorted_rect list
Arguments:
pbin (PackingAlgorithm): Packing bin
Returns:
key of the rectangle with best fitness
"""
fit = ((pbin.fitness(r[0], r[1]), k) for k, r in self._sorted_rect.items())
fit = (f for f in fit if f[0] is not None)
try:
_, rect = min(fit, key=self.first_item)
return rect
except ValueError:
return None
def _new_open_bin(self, remaining_rect):
"""
Extract the next bin where at least one of the rectangles in
rem
Arguments:
remaining_rect (dict): rectangles not placed yet
Returns:
PackingAlgorithm: Initialized empty packing bin.
None: No bin big enough for the rectangle was found
"""
factories_to_delete = set() #
new_bin = None
for key, binfac in self._empty_bins.items():
# Only return the new bin if at least one of the remaining
# rectangles fit inside.
a_rectangle_fits = False
for _, rect in remaining_rect.items():
if binfac.fits_inside(rect[0], rect[1]):
a_rectangle_fits = True
break
if not a_rectangle_fits:
factories_to_delete.add(key)
continue
# Create bin and add to open_bins
new_bin = binfac.new_bin()
if new_bin is None:
continue
self._open_bins.append(new_bin)
# If the factory was depleted mark for deletion
if binfac.is_empty():
factories_to_delete.add(key)
break
# Delete marked factories
for f in factories_to_delete:
del self._empty_bins[f]
return new_bin
def pack(self):
self.reset()
if not self._is_everything_ready():
return
# Add available bins to packer
for b in self._avail_bins:
width, height, count, extra_kwargs = b
super(Packer, self).add_bin(width, height, count, **extra_kwargs)
# Store rectangles into dict for fast deletion
self._sorted_rect = collections.OrderedDict(
enumerate(self._sort_algo(self._avail_rect)))
# For each bin pack the rectangles with lowest fitness until it is filled or
# the rectangles exhausted, then open the next bin where at least one rectangle
# will fit and repeat the process until there aren't more rectangles or bins
# available.
while len(self._sorted_rect) > 0:
# Find one bin where at least one of the remaining rectangles fit
pbin = self._new_open_bin(self._sorted_rect)
if pbin is None:
break
# Pack as many rectangles as possible into the open bin
while True:
# Find 'fittest' rectangle
best_rect_key = self._find_best_fit(pbin)
if best_rect_key is None:
closed_bin = self._open_bins.popleft()
self._closed_bins.append(closed_bin)
break # None of the remaining rectangles can be packed in this bin
best_rect = self._sorted_rect[best_rect_key]
del self._sorted_rect[best_rect_key]
PackerBNFMixin.add_rect(self, *best_rect)
# Packer factory
class Enum(tuple):
__getattr__ = tuple.index
PackingMode = Enum(["Online", "Offline"])
PackingBin = Enum(["BNF", "BFF", "BBF", "Global"])
def newPacker(mode=PackingMode.Offline,
bin_algo=PackingBin.BBF,
pack_algo=MaxRectsBssf,
sort_algo=SORT_AREA,
rotation=True):
"""
Packer factory helper function
Arguments:
mode (PackingMode): Packing mode
Online: Rectangles are packed as soon are they are added
Offline: Rectangles aren't packed untils pack() is called
bin_algo (PackingBin): Bin selection heuristic
pack_algo (PackingAlgorithm): Algorithm used
rotation (boolean): Enable or disable rectangle rotation.
Returns:
Packer: Initialized packer instance.
"""
packer_class = None
# Online Mode
if mode == PackingMode.Online:
sort_algo=None
if bin_algo == PackingBin.BNF:
packer_class = PackerOnlineBNF
elif bin_algo == PackingBin.BFF:
packer_class = PackerOnlineBFF
elif bin_algo == PackingBin.BBF:
packer_class = PackerOnlineBBF
else:
raise AttributeError("Unsupported bin selection heuristic")
# Offline Mode
elif mode == PackingMode.Offline:
if bin_algo == PackingBin.BNF:
packer_class = PackerBNF
elif bin_algo == PackingBin.BFF:
packer_class = PackerBFF
elif bin_algo == PackingBin.BBF:
packer_class = PackerBBF
elif bin_algo == PackingBin.Global:
packer_class = PackerGlobal
sort_algo=None
else:
raise AttributeError("Unsupported bin selection heuristic")
else:
raise AttributeError("Unknown packing mode.")
if sort_algo:
return packer_class(pack_algo=pack_algo, sort_algo=sort_algo,
rotation=rotation)
else:
return packer_class(pack_algo=pack_algo, rotation=rotation)

View file

@ -0,0 +1,303 @@
import collections
import itertools
import operator
import heapq
import copy
from .pack_algo import PackingAlgorithm
from .geometry import Point as P
from .geometry import HSegment, Rectangle
from .waste import WasteManager
class Skyline(PackingAlgorithm):
""" Class implementing Skyline algorithm as described by
Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
_skyline: stores all the segments at the top of the skyline.
_waste: Handles all wasted sections.
"""
def __init__(self, width, height, rot=True, *args, **kwargs):
"""
_skyline is the list used to store all the skyline segments, each
one is a list with the format [x, y, width] where x is the x
coordinate of the left most point of the segment, y the y coordinate
of the segment, and width the length of the segment. The initial
segment is allways [0, 0, surface_width]
Arguments:
width (int, float):
height (int, float):
rot (bool): Enable or disable rectangle rotation
"""
self._waste_management = False
self._waste = WasteManager(rot=rot)
super(Skyline, self).__init__(width, height, rot, merge=False, *args, **kwargs)
def _placement_points_generator(self, skyline, width):
"""Returns a generator for the x coordinates of all the placement
points on the skyline for a given rectangle.
WARNING: In some cases could be duplicated points, but it is faster
to compute them twice than to remove them.
Arguments:
skyline (list): Skyline HSegment list
width (int, float): Rectangle width
Returns:
generator
"""
skyline_r = skyline[-1].right
skyline_l = skyline[0].left
# Placements using skyline segment left point
ppointsl = (s.left for s in skyline if s.left+width <= skyline_r)
# Placements using skyline segment right point
ppointsr = (s.right-width for s in skyline if s.right-width >= skyline_l)
# Merge positions
return heapq.merge(ppointsl, ppointsr)
def _generate_placements(self, width, height):
"""
Generate a list with
Arguments:
skyline (list): SkylineHSegment list
width (number):
Returns:
tuple (Rectangle, fitness):
Rectangle: Rectangle in valid position
left_skyline: Index for the skyline under the rectangle left edge.
right_skyline: Index for the skyline under the rectangle right edte.
"""
skyline = self._skyline
points = collections.deque()
left_index = right_index = 0 # Left and right side skyline index
support_height = skyline[0].top
support_index = 0
placements = self._placement_points_generator(skyline, width)
for p in placements:
# If Rectangle's right side changed segment, find new support
if p+width > skyline[right_index].right:
for right_index in range(right_index+1, len(skyline)):
if skyline[right_index].top >= support_height:
support_index = right_index
support_height = skyline[right_index].top
if p+width <= skyline[right_index].right:
break
# If left side changed segment.
if p >= skyline[left_index].right:
left_index +=1
# Find new support if the previous one was shifted out.
if support_index < left_index:
support_index = left_index
support_height = skyline[left_index].top
for i in range(left_index, right_index+1):
if skyline[i].top >= support_height:
support_index = i
support_height = skyline[i].top
# Add point if there is enought room at the top
if support_height+height <= self.height:
points.append((Rectangle(p, support_height, width, height),\
left_index, right_index))
return points
def _merge_skyline(self, skylineq, segment):
"""
Arguments:
skylineq (collections.deque):
segment (HSegment):
"""
if len(skylineq) == 0:
skylineq.append(segment)
return
if skylineq[-1].top == segment.top:
s = skylineq[-1]
skylineq[-1] = HSegment(s.start, s.length+segment.length)
else:
skylineq.append(segment)
def _add_skyline(self, rect):
"""
Arguments:
seg (Rectangle):
"""
skylineq = collections.deque([]) # Skyline after adding new one
for sky in self._skyline:
if sky.right <= rect.left or sky.left >= rect.right:
self._merge_skyline(skylineq, sky)
continue
if sky.left < rect.left and sky.right > rect.left:
# Skyline section partially under segment left
self._merge_skyline(skylineq,
HSegment(sky.start, rect.left-sky.left))
sky = HSegment(P(rect.left, sky.top), sky.right-rect.left)
if sky.left < rect.right:
if sky.left == rect.left:
self._merge_skyline(skylineq,
HSegment(P(rect.left, rect.top), rect.width))
# Skyline section partially under segment right
if sky.right > rect.right:
self._merge_skyline(skylineq,
HSegment(P(rect.right, sky.top), sky.right-rect.right))
sky = HSegment(sky.start, rect.right-sky.left)
if sky.left >= rect.left and sky.right <= rect.right:
# Skyline section fully under segment, account for wasted space
if self._waste_management and sky.top < rect.bottom:
self._waste.add_waste(sky.left, sky.top,
sky.length, rect.bottom - sky.top)
else:
# Segment
self._merge_skyline(skylineq, sky)
# Aaaaand ..... Done
self._skyline = list(skylineq)
def _rect_fitness(self, rect, left_index, right_index):
return rect.top
def _select_position(self, width, height):
"""
Search for the placement with the bes fitness for the rectangle.
Returns:
tuple (Rectangle, fitness) - Rectangle placed in the fittest position
None - Rectangle couldn't be placed
"""
positions = self._generate_placements(width, height)
if self.rot and width != height:
positions += self._generate_placements(height, width)
if not positions:
return None, None
return min(((p[0], self._rect_fitness(*p))for p in positions),
key=operator.itemgetter(1))
def fitness(self, width, height):
"""Search for the best fitness
"""
assert(width > 0 and height >0)
if width > max(self.width, self.height) or\
height > max(self.height, self.width):
return None
# If there is room in wasted space, FREE PACKING!!
if self._waste_management:
if self._waste.fitness(width, height) is not None:
return 0
# Get best fitness segment, for normal rectangle, and for
# rotated rectangle if rotation is enabled.
rect, fitness = self._select_position(width, height)
return fitness
def add_rect(self, width, height, rid=None):
"""
Add new rectangle
"""
assert(width > 0 and height > 0)
if width > max(self.width, self.height) or\
height > max(self.height, self.width):
return None
rect = None
# If Waste managment is enabled, first try to place the rectangle there
if self._waste_management:
rect = self._waste.add_rect(width, height, rid)
# Get best possible rectangle position
if not rect:
rect, _ = self._select_position(width, height)
if rect:
self._add_skyline(rect)
if rect is None:
return None
# Store rectangle, and recalculate skyline
rect.rid = rid
self.rectangles.append(rect)
return rect
def reset(self):
super(Skyline, self).reset()
self._skyline = [HSegment(P(0, 0), self.width)]
self._waste.reset()
class SkylineWMixin(Skyline):
"""Waste managment mixin"""
def __init__(self, width, height, *args, **kwargs):
super(SkylineWMixin, self).__init__(width, height, *args, **kwargs)
self._waste_management = True
class SkylineMwf(Skyline):
"""Implements Min Waste fit heuristic, minimizing the area wasted under the
rectangle.
"""
def _rect_fitness(self, rect, left_index, right_index):
waste = 0
for seg in self._skyline[left_index:right_index+1]:
waste +=\
(min(rect.right, seg.right)-max(rect.left, seg.left)) *\
(rect.bottom-seg.top)
return waste
def _rect_fitnes2s(self, rect, left_index, right_index):
waste = ((min(rect.right, seg.right)-max(rect.left, seg.left)) for seg in self._skyline[left_index:right_index+1])
return sum(waste)
class SkylineMwfl(Skyline):
"""Implements Min Waste fit with low profile heuritic, minimizing the area
wasted below the rectangle, at the same time it tries to keep the height
minimal.
"""
def _rect_fitness(self, rect, left_index, right_index):
waste = 0
for seg in self._skyline[left_index:right_index+1]:
waste +=\
(min(rect.right, seg.right)-max(rect.left, seg.left)) *\
(rect.bottom-seg.top)
return waste*self.width*self.height+rect.top
class SkylineBl(Skyline):
"""Implements Bottom Left heuristic, the best fit option is that which
results in which the top side of the rectangle lies at the bottom-most
position.
"""
def _rect_fitness(self, rect, left_index, right_index):
return rect.top
class SkylineBlWm(SkylineBl, SkylineWMixin):
pass
class SkylineMwfWm(SkylineMwf, SkylineWMixin):
pass
class SkylineMwflWm(SkylineMwfl, SkylineWMixin):
pass

View file

@ -0,0 +1,23 @@
from .guillotine import GuillotineBafMinas
from .geometry import Rectangle
class WasteManager(GuillotineBafMinas):
def __init__(self, rot=True, merge=True, *args, **kwargs):
super(WasteManager, self).__init__(1, 1, rot=rot, merge=merge, *args, **kwargs)
def add_waste(self, x, y, width, height):
"""Add new waste section"""
self._add_section(Rectangle(x, y, width, height))
def _fits_surface(self, width, height):
raise NotImplementedError
def validate_packing(self):
raise NotImplementedError
def reset(self):
super(WasteManager, self).reset()
self._sections = []

View file

@ -97,7 +97,8 @@ def get_file_size(filepath):
size = os.path.getsize(path)
size /= 1024
except:
print("error getting file path for " + filepath)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("error getting file path for " + filepath)
return (size)
@ -144,7 +145,8 @@ def clean_empty_materials(self):
for slot in obj.material_slots:
mat = slot.material
if mat is None:
print("Removed Empty Materials from " + obj.name)
if bpy.context.scene.TLM_SceneProperties.tlm_verbose:
print("Removed Empty Materials from " + obj.name)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.ops.object.material_slot_remove()

View file

@ -60,6 +60,9 @@ class ARM_PT_ObjectPropsPanel(bpy.types.Panel):
# Lightmapping props
if obj.type == "MESH":
row = layout.row(align=True)
scene = bpy.context.scene
if scene == None:
return
row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use")
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
@ -69,8 +72,34 @@ class ARM_PT_ObjectPropsPanel(bpy.types.Panel):
row = layout.row()
row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode")
row = layout.row()
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup":
pass
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0:
row = layout.row()
item = scene.TLM_AtlasList[scene.TLM_AtlasListItem]
row.prop_search(obj.TLM_ObjectProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group')
row = layout.row()
else:
row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.")
row = layout.row()
else:
row = layout.row()
row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object")
row = layout.row()
if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA":
if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0:
row = layout.row()
item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem]
row.prop_search(obj.TLM_ObjectProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group')
row = layout.row()
else:
row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.")
row = layout.row()
row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin")
row = layout.row()
row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override")
@ -1428,6 +1457,29 @@ class ARM_PT_BakePanel(bpy.types.Panel):
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_alert_on_finish")
if sceneProperties.tlm_alert_on_finish:
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_alert_sound")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_verbose")
#row = layout.row(align=True)
#row.prop(sceneProperties, "tlm_compile_statistics")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_override_bg_color")
if sceneProperties.tlm_override_bg_color:
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_override_color")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_reset_uv")
row = layout.row(align=True)
try:
if bpy.context.scene["TLM_Buildstat"] is not None:
row.label(text="Last build completed in: " + str(bpy.context.scene["TLM_Buildstat"][0]))
except:
pass
row = layout.row(align=True)
row.label(text="Cycles Settings")
@ -1439,10 +1491,20 @@ class ARM_PT_BakePanel(bpy.types.Panel):
row.prop(engineProperties, "tlm_resolution_scale")
row = layout.row(align=True)
row.prop(engineProperties, "tlm_bake_mode")
row = layout.row(align=True)
row.prop(engineProperties, "tlm_lighting_mode")
if scene.TLM_EngineProperties.tlm_bake_mode == "Background":
row = layout.row(align=True)
row.label(text="Warning! Background mode is currently unstable", icon_value=2)
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_network_render")
if sceneProperties.tlm_network_render:
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_network_paths")
#row = layout.row(align=True)
#row.prop(sceneProperties, "tlm_network_dir")
row = layout.row(align=True)
row = layout.row(align=True)
row.prop(engineProperties, "tlm_caching_mode")
row = layout.row(align=True)
@ -1455,6 +1517,8 @@ class ARM_PT_BakePanel(bpy.types.Panel):
row.prop(engineProperties, "tlm_exposure_multiplier")
row = layout.row(align=True)
row.prop(engineProperties, "tlm_setting_supersample")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_metallic_clamp")
elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender":
@ -1516,8 +1580,6 @@ class ARM_PT_BakePanel(bpy.types.Panel):
row.prop(denoiseProperties, "tlm_optix_verbose")
row = layout.row(align=True)
row.prop(denoiseProperties, "tlm_optix_maxmem")
row = layout.row(align=True)
row.prop(denoiseProperties, "tlm_denoise_ao")
##################
@ -1581,52 +1643,44 @@ class ARM_PT_BakePanel(bpy.types.Panel):
if sceneProperties.tlm_encoding_use:
row.prop(sceneProperties, "tlm_encoding_mode", expand=True)
if sceneProperties.tlm_encoding_mode == "RGBM" or sceneProperties.tlm_encoding_mode == "RGBD":
if scene.TLM_EngineProperties.tlm_bake_mode == "Background":
row.label(text="Encoding options disabled in background mode")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_encoding_range")
if sceneProperties.tlm_encoding_mode == "LogLuv":
pass
if sceneProperties.tlm_encoding_mode == "HDR":
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_format")
row = layout.row(align=True)
row.label(text="Encoding Settings")
row.prop(sceneProperties, "tlm_encoding_device", expand=True)
row = layout.row(align=True)
row = layout.row(align=True)
row.operator("tlm.enable_selection")
row = layout.row(align=True)
row.operator("tlm.disable_selection")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_override_object_settings")
if sceneProperties.tlm_encoding_device == "CPU":
row.prop(sceneProperties, "tlm_encoding_mode_a", expand=True)
else:
row.prop(sceneProperties, "tlm_encoding_mode_b", expand=True)
if sceneProperties.tlm_override_object_settings:
if sceneProperties.tlm_encoding_device == "CPU":
if sceneProperties.tlm_encoding_mode_a == "RGBM":
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_encoding_range")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_decoder_setup")
if sceneProperties.tlm_encoding_mode_a == "RGBD":
pass
if sceneProperties.tlm_encoding_mode_a == "HDR":
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_format")
else:
row = layout.row(align=True)
row = layout.row()
row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode")
row = layout.row()
if sceneProperties.tlm_encoding_mode_b == "RGBM":
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_encoding_range")
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_decoder_setup")
if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup":
if scene.TLM_AtlasList_index >= 0 and len(scene.TLM_AtlasList) > 0:
row = layout.row()
item = scene.TLM_AtlasList[scene.TLM_AtlasList_index]
row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group')
else:
row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.")
else:
row.prop(sceneProperties, "tlm_mesh_lightmap_resolution")
row = layout.row()
row.prop(sceneProperties, "tlm_mesh_unwrap_margin")
row = layout.row(align=True)
row.operator("tlm.remove_uv_selection")
row = layout.row(align=True)
if sceneProperties.tlm_encoding_mode_b == "LogLuv" and sceneProperties.tlm_encoding_device == "GPU":
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_decoder_setup")
if sceneProperties.tlm_encoding_mode_b == "HDR":
row = layout.row(align=True)
row.prop(sceneProperties, "tlm_format")
##################
#SELECTION OPERATORS!
@ -1649,17 +1703,32 @@ class ARM_PT_BakePanel(bpy.types.Panel):
row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode")
row = layout.row()
if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup":
if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
if scene.TLM_AtlasList_index >= 0 and len(scene.TLM_AtlasList) > 0:
if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0:
row = layout.row()
item = scene.TLM_AtlasList[scene.TLM_AtlasList_index]
item = scene.TLM_AtlasList[scene.TLM_AtlasListItem]
row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group')
else:
row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.")
else:
row = layout.row()
row.prop(sceneProperties, "tlm_postpack_object")
row = layout.row()
if sceneProperties.tlm_postpack_object and sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA":
if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0:
row = layout.row()
item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem]
row.prop_search(sceneProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group')
row = layout.row()
else:
row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.")
row = layout.row()
if sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA":
row.prop(sceneProperties, "tlm_mesh_lightmap_resolution")
row = layout.row()
row.prop(sceneProperties, "tlm_mesh_unwrap_margin")
@ -1667,6 +1736,112 @@ class ARM_PT_BakePanel(bpy.types.Panel):
row = layout.row(align=True)
row.operator("tlm.remove_uv_selection")
row = layout.row(align=True)
row.operator("tlm.select_lightmapped_objects")
row = layout.row(align=True)
##################
#Additional settings
row = layout.row(align=True)
row.label(text="Additional options")
sceneProperties = scene.TLM_SceneProperties
atlasListItem = scene.TLM_AtlasListItem
atlasList = scene.TLM_AtlasList
postatlasListItem = scene.TLM_PostAtlasListItem
postatlasList = scene.TLM_PostAtlasList
layout.label(text="Atlas Groups")
row = layout.row()
row.prop(sceneProperties, "tlm_atlas_mode", expand=True)
if sceneProperties.tlm_atlas_mode == "Prepack":
rows = 2
if len(atlasList) > 1:
rows = 4
row = layout.row()
row.template_list("TLM_UL_AtlasList", "Atlas List", scene, "TLM_AtlasList", scene, "TLM_AtlasListItem", rows=rows)
col = row.column(align=True)
col.operator("tlm_atlaslist.new_item", icon='ADD', text="")
col.operator("tlm_atlaslist.delete_item", icon='REMOVE', text="")
#col.menu("ARM_MT_BakeListSpecials", icon='DOWNARROW_HLT', text="")
# if len(scene.TLM_AtlasList) > 1:
# col.separator()
# op = col.operator("arm_bakelist.move_item", icon='TRIA_UP', text="")
# op.direction = 'UP'
# op = col.operator("arm_bakelist.move_item", icon='TRIA_DOWN', text="")
# op.direction = 'DOWN'
if atlasListItem >= 0 and len(atlasList) > 0:
item = atlasList[atlasListItem]
#layout.prop_search(item, "obj", bpy.data, "objects", text="Object")
#layout.prop(item, "res_x")
layout.prop(item, "tlm_atlas_lightmap_unwrap_mode")
layout.prop(item, "tlm_atlas_lightmap_resolution")
layout.prop(item, "tlm_atlas_unwrap_margin")
amount = 0
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA":
if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name:
amount = amount + 1
layout.label(text="Objects: " + str(amount))
# layout.use_property_split = True
# layout.use_property_decorate = False
# layout.label(text="Enable for selection")
# layout.label(text="Disable for selection")
# layout.label(text="Something...")
else:
layout.label(text="Postpacking is unstable.")
rows = 2
if len(atlasList) > 1:
rows = 4
row = layout.row()
row.template_list("TLM_UL_PostAtlasList", "PostList", scene, "TLM_PostAtlasList", scene, "TLM_PostAtlasListItem", rows=rows)
col = row.column(align=True)
col.operator("tlm_postatlaslist.new_item", icon='ADD', text="")
col.operator("tlm_postatlaslist.delete_item", icon='REMOVE', text="")
if postatlasListItem >= 0 and len(postatlasList) > 0:
item = postatlasList[postatlasListItem]
layout.prop(item, "tlm_atlas_lightmap_resolution")
#Below list object counter
amount = 0
utilized = 0
atlasUsedArea = 0
atlasSize = item.tlm_atlas_lightmap_resolution
for obj in bpy.data.objects:
if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use:
if obj.TLM_ObjectProperties.tlm_postpack_object:
if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name:
amount = amount + 1
atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2
row = layout.row()
row.prop(item, "tlm_atlas_repack_on_cleanup")
#TODO SET A CHECK FOR THIS! ADD A CV2 CHECK TO UTILITY!
cv2 = True
if cv2:
row = layout.row()
row.prop(item, "tlm_atlas_dilation")
layout.label(text="Objects: " + str(amount))
utilized = atlasUsedArea / (int(atlasSize) ** 2)
layout.label(text="Utilized: " + str(utilized * 100) + "%")
if (utilized * 100) > 100:
layout.label(text="Warning! Overflow not yet supported")
class ArmGenLodButton(bpy.types.Operator):
'''Automatically generate LoD levels'''