Palette assets improved.
This commit is contained in:
@@ -5,10 +5,10 @@
|
||||
|
||||
# Function that adds an asset to be compiled
|
||||
function(add_asset ASSET_TYPE ASSET_PATH)
|
||||
message(STATUS "Adding asset: ${ASSET_PATH} (type: ${ASSET_TYPE})")
|
||||
message(STATUS " Options: ${ARGN}")
|
||||
|
||||
set(FULL_ASSET_PATH "${CMAKE_CURRENT_LIST_DIR}/${ASSET_PATH}")
|
||||
list(APPEND DUSK_ASSETS ${FULL_ASSET_PATH})
|
||||
string(JOIN "%" ASSETS_ARGS ${ARGN})
|
||||
list(APPEND DUSK_ASSETS
|
||||
"${ASSET_TYPE}#${FULL_ASSET_PATH}#${ASSETS_ARGS}$"
|
||||
)
|
||||
set(DUSK_ASSETS ${DUSK_ASSETS} CACHE INTERNAL ${DUSK_CACHE_TARGET})
|
||||
endfunction()
|
@@ -6,16 +6,45 @@ import sys
|
||||
parser = argparse.ArgumentParser(description="Generate chunk header files")
|
||||
parser.add_argument('--assets', required=True, help='Dir to output built assets')
|
||||
parser.add_argument('--build-type', choices=['wad', 'header'], default='raw', help='Type of build to perform')
|
||||
parser.add_argument('--output-file', help='Output file for built assets (required for wad build)')
|
||||
parser.add_argument('--output-file', required=True, help='Output file for built assets (required for wad build)')
|
||||
parser.add_argument('--headers-dir', required=True, help='Directory to output individual asset headers (required for header build)')
|
||||
parser.add_argument('--output-headers', help='Output header file for built assets (required for header build)')
|
||||
parser.add_argument('--output-assets', help='Output directory for built assets (required for raw build)')
|
||||
parser.add_argument('--output-assets', required=True, help='Output directory for built assets')
|
||||
parser.add_argument('--input', required=True, help='Input assets to process', nargs='+')
|
||||
args = parser.parse_args()
|
||||
|
||||
inputAssets = []
|
||||
for inputArg in args.input:
|
||||
inputAssets.extend(inputArg.split(','))
|
||||
|
||||
files = inputArg.split('$')
|
||||
for file in files:
|
||||
if str(file).strip() == '':
|
||||
continue
|
||||
|
||||
pieces = file.split('#')
|
||||
|
||||
if len(pieces) < 2:
|
||||
print(f"Error: Invalid input asset format '{file}'. Expected format: type#path[#option1%option2...]")
|
||||
sys.exit(1)
|
||||
|
||||
options = {}
|
||||
if len(pieces) > 2:
|
||||
optionParts = pieces[2].split('%')
|
||||
for part in optionParts:
|
||||
partSplit = part.split('=')
|
||||
|
||||
if len(partSplit) < 1:
|
||||
continue
|
||||
if len(partSplit) == 2:
|
||||
options[partSplit[0]] = partSplit[1]
|
||||
else:
|
||||
options[partSplit[0]] = True
|
||||
|
||||
inputAssets.append({
|
||||
'type': pieces[0],
|
||||
'path': pieces[1],
|
||||
'options': options
|
||||
})
|
||||
|
||||
if not inputAssets:
|
||||
print("Error: No input assets provided.")
|
||||
sys.exit(1)
|
@@ -1,6 +1,7 @@
|
||||
import sys, os
|
||||
from args import inputAssets, args
|
||||
from processasset import processAsset
|
||||
from processpalette import processPaletteList
|
||||
from assethelpers import getBuiltAssetsRelativePath
|
||||
import zipfile
|
||||
|
||||
@@ -26,6 +27,9 @@ with zipfile.ZipFile(outputFileName, 'w') as zipf:
|
||||
relativeOutputPath = getBuiltAssetsRelativePath(file)
|
||||
zipf.write(file, arcname=relativeOutputPath)
|
||||
|
||||
# Generate additional headers.
|
||||
processPaletteList()
|
||||
|
||||
# Finalize build
|
||||
if args.build_type == 'header':
|
||||
print("Error: Header build not implemented yet.")
|
@@ -1,19 +1,24 @@
|
||||
from processtileset import processTileset
|
||||
from processimage import processPalette, processImage
|
||||
import sys
|
||||
# from processtileset import processTileset
|
||||
# from processimage import processPalette, processImage
|
||||
from processpalette import processPalette
|
||||
|
||||
processedAssets = []
|
||||
|
||||
def processAsset(assetPath):
|
||||
if assetPath in processedAssets:
|
||||
def processAsset(asset):
|
||||
if asset['path'] in processedAssets:
|
||||
return
|
||||
|
||||
processedAssets.append(assetPath)
|
||||
processedAssets.append(asset['path'])
|
||||
|
||||
# Handle tiled tilesets
|
||||
if assetPath.endswith('.tsx'):
|
||||
return processTileset(assetPath)
|
||||
elif assetPath.endswith('.png'):
|
||||
if assetPath.endswith('.palette.png'):
|
||||
return processPalette(assetPath)
|
||||
else:
|
||||
return processImage(assetPath)
|
||||
t = asset['type'].lower()
|
||||
if t == 'palette':
|
||||
return processPalette(asset)
|
||||
# elif t == 'image':
|
||||
# return processImage(asset)
|
||||
# elif t == 'tileset':
|
||||
# return processTileset(asset)
|
||||
else:
|
||||
print(f"Error: Unknown asset type '{asset['type']}' for path '{asset['path']}'")
|
||||
sys.exit(1)
|
@@ -1,116 +0,0 @@
|
||||
import os
|
||||
from args import args
|
||||
from PIL import Image
|
||||
import struct
|
||||
import sys
|
||||
from assethelpers import getAssetRelativePath
|
||||
|
||||
PALETTES = []
|
||||
|
||||
def extractPaletteFromImage(image):
|
||||
# goes through and finds all unique colors in the image
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
pixels = list(image.getdata())
|
||||
uniqueColors = []
|
||||
for color in pixels:
|
||||
# We treat alpha 0 as rgba(0,0,0,0) for palette purposes
|
||||
if color[3] == 0:
|
||||
color = (0, 0, 0, 0)
|
||||
if color not in uniqueColors:
|
||||
uniqueColors.append(color)
|
||||
return uniqueColors
|
||||
|
||||
def savePalette(pixels, outputFilePath):
|
||||
colorCount = len(pixels)
|
||||
buf = bytearray(b"DPF")
|
||||
buf += struct.pack("<i", colorCount) # little-endian int32_t
|
||||
for r, g, b, a in pixels: # each channel 0..255
|
||||
buf += struct.pack("BBBB", r, g, b, a) # raw bytes
|
||||
os.makedirs(os.path.dirname(outputFilePath), exist_ok=True)
|
||||
with open(outputFilePath, "wb") as f:
|
||||
f.write(buf)
|
||||
|
||||
def processPalette(assetPath):
|
||||
# Process the image file
|
||||
print(f"Processing palette: {assetPath}")
|
||||
|
||||
# Load the image
|
||||
image = Image.open(assetPath)
|
||||
pixels = extractPaletteFromImage(image)
|
||||
|
||||
# Save the processed image to the output directory
|
||||
fileNameWithoutExt = os.path.splitext(os.path.basename(assetPath))[0]
|
||||
outputFilePath = os.path.join(args.output_assets, f"{fileNameWithoutExt}.dpf")
|
||||
os.makedirs(os.path.dirname(outputFilePath), exist_ok=True)
|
||||
savePalette(pixels, outputFilePath)
|
||||
|
||||
outputRelative = os.path.relpath(outputFilePath, args.output_assets)
|
||||
|
||||
palette = {
|
||||
"outputFile": outputRelative,
|
||||
"paletteColors": len(pixels),
|
||||
"files": [ outputFilePath ],
|
||||
"pixels": pixels
|
||||
}
|
||||
|
||||
PALETTES.append(palette)
|
||||
|
||||
return palette
|
||||
|
||||
def processImage(assetPath):
|
||||
print(f"Processing image: {assetPath}")
|
||||
|
||||
# Load the image
|
||||
image = Image.open(assetPath)
|
||||
|
||||
# Get the image's palette because we are going to try and find a matching
|
||||
# palette from the already processed palettes...
|
||||
imagePalette = extractPaletteFromImage(image)
|
||||
|
||||
# Now find which palette has every single color in this image's palette
|
||||
paletteIndex = -1
|
||||
for i, palette in enumerate(PALETTES):
|
||||
paletteColors = palette["pixels"]
|
||||
if all(color in paletteColors for color in imagePalette):
|
||||
paletteIndex = i
|
||||
break
|
||||
|
||||
# Did we manage to find a matching palette?
|
||||
if paletteIndex == -1:
|
||||
print(f"Error: No matching palette found for image {assetPath}. Please process a suitable palette first.")
|
||||
sys.exit(1)
|
||||
|
||||
# We found the palette, so now we can convert the image to use that palette
|
||||
palette = PALETTES[paletteIndex]
|
||||
indexes = []
|
||||
for color in imagePalette:
|
||||
if color in palette["pixels"]:
|
||||
index = palette["pixels"].index(color)
|
||||
indexes.append(index)
|
||||
else:
|
||||
print(f"Error: Color {color} in image {assetPath} not found in palette.")
|
||||
sys.exit(1)
|
||||
|
||||
relative = getAssetRelativePath(assetPath)
|
||||
fileNameWithoutExt = os.path.splitext(os.path.basename(assetPath))[0]
|
||||
outputFileRelative = os.path.join(os.path.dirname(relative), f"{fileNameWithoutExt}.dpi")
|
||||
outputFilePath = os.path.join(args.output_assets, outputFileRelative)
|
||||
os.makedirs(os.path.dirname(outputFilePath), exist_ok=True)
|
||||
with open(outputFilePath, "wb") as f:
|
||||
# Write header
|
||||
f.write(b"DPI") # Dusk Palettized Image
|
||||
f.write(struct.pack("<i", image.width)) # Width
|
||||
f.write(struct.pack("<i", image.height)) # Height
|
||||
f.write(struct.pack("B", paletteIndex)) # Palette index
|
||||
|
||||
# Write uint8_t pixel index.
|
||||
for index in indexes:
|
||||
f.write(struct.pack("B", index))
|
||||
|
||||
return {
|
||||
"paletteIndex": paletteIndex,
|
||||
"palette": PALETTES[paletteIndex],
|
||||
"outputFile": outputFileRelative,
|
||||
"files": [ assetPath, outputFilePath ]
|
||||
}
|
80
tools/assetstool/processpalette.py
Normal file
80
tools/assetstool/processpalette.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
from PIL import Image
|
||||
from args import args
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
palettes = []
|
||||
|
||||
def extractPaletteFromImage(image):
|
||||
# goes through and finds all unique colors in the image
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
pixels = list(image.getdata())
|
||||
uniqueColors = []
|
||||
for color in pixels:
|
||||
# We treat alpha 0 as rgba(0,0,0,0) for palette purposes
|
||||
if color[3] == 0:
|
||||
color = (0, 0, 0, 0)
|
||||
if color not in uniqueColors:
|
||||
uniqueColors.append(color)
|
||||
return uniqueColors
|
||||
|
||||
def processPalette(asset):
|
||||
print(f"Processing palette: {asset['path']}")
|
||||
|
||||
paletteIndex = len(palettes)
|
||||
image = Image.open(asset['path'])
|
||||
pixels = extractPaletteFromImage(image)
|
||||
|
||||
fileNameWithoutExt = os.path.splitext(os.path.basename(asset['path']))[0]
|
||||
fileNameWithoutPalette = os.path.splitext(fileNameWithoutExt)[0]
|
||||
|
||||
# Header
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
data = f"// Palette Generated for {asset['path']} at {now}\n"
|
||||
data += f"#include \"display/palette/palette.h\"\n\n"
|
||||
data += f"#define PALETTE_{paletteIndex}_COLOR_COUNT {len(pixels)}\n\n"
|
||||
data += f"static const color_t PALETTE_{paletteIndex}_COLORS[PALETTE_{paletteIndex}_COLOR_COUNT] = {{\n"
|
||||
for pixel in pixels:
|
||||
data += f" {{ 0x{pixel[0]:02X}, 0x{pixel[1]:02X}, 0x{pixel[2]:02X}, 0x{pixel[3]:02X} }},\n"
|
||||
data += f"}};\n\n"
|
||||
data += f"static const palette_t PALETTE_{paletteIndex} = {{\n"
|
||||
data += f" .colorCount = PALETTE_{paletteIndex}_COLOR_COUNT,\n"
|
||||
data += f" .colors = PALETTE_{paletteIndex}_COLORS,\n"
|
||||
data += f"}};\n"
|
||||
|
||||
# Write Header
|
||||
outputFile = os.path.join(args.headers_dir, "display", "palette", f"palette_{paletteIndex}.h")
|
||||
os.makedirs(os.path.dirname(outputFile), exist_ok=True)
|
||||
with open(outputFile, "w") as f:
|
||||
f.write(data)
|
||||
|
||||
palette = {
|
||||
"paletteIndex": paletteIndex,
|
||||
"paletteName": fileNameWithoutPalette,
|
||||
"pixels": pixels,
|
||||
"headerFile": os.path.relpath(outputFile, args.headers_dir),
|
||||
"asset": asset,
|
||||
"files": [ ],# No zippable files.
|
||||
}
|
||||
|
||||
palettes.append(palette)
|
||||
return palette
|
||||
|
||||
def processPaletteList():
|
||||
data = f"// Auto-generated palette list\n"
|
||||
for palette in palettes:
|
||||
data += f"#include \"{palette['headerFile']}\"\n"
|
||||
data += f"\n"
|
||||
data += f"#define PALETTE_LIST_COUNT {len(palettes)}\n\n"
|
||||
data += f"static const palette_t* PALETTE_LIST[PALETTE_LIST_COUNT] = {{\n"
|
||||
for palette in palettes:
|
||||
data += f" &PALETTE_{palette['paletteIndex']},\n"
|
||||
data += f"}};\n"
|
||||
|
||||
# Write the palette list to a header file
|
||||
outputFile = os.path.join(args.headers_dir, "display", "palette", "palettelist.h")
|
||||
os.makedirs(os.path.dirname(outputFile), exist_ok=True)
|
||||
with open(outputFile, "w") as f:
|
||||
f.write(data)
|
@@ -1,82 +0,0 @@
|
||||
import sys, os
|
||||
import xml.etree.ElementTree as ET
|
||||
from assethelpers import getAssetRelativePath
|
||||
from args import args
|
||||
from constants import ASSET_FILE_NAME_MAX_LENGTH
|
||||
from processimage import processPalette, processImage
|
||||
|
||||
def processTileset(assetPath):
|
||||
# Process the tileset file
|
||||
print(f"Processing tileset: {assetPath}")
|
||||
|
||||
# Load the tileset XML
|
||||
tree = ET.parse(assetPath)
|
||||
root = tree.getroot()
|
||||
|
||||
# Needs tilewidth, tileheight, tilecount and columns attributes
|
||||
if not all(attr in root.attrib for attr in ['tilewidth', 'tileheight', 'tilecount', 'columns']):
|
||||
print(f"Error: Tileset {assetPath} is missing required attributes.")
|
||||
return
|
||||
|
||||
tilewidth = int(root.attrib.get('tilewidth', 0))
|
||||
tileheight = int(root.attrib.get('tileheight', 0))
|
||||
tilecount = int(root.attrib.get('tilecount', 0))
|
||||
columns = int(root.attrib.get('columns', 0))
|
||||
|
||||
if tilewidth <= 0 or tileheight <= 0 or tilecount <= 0 or columns <= 0:
|
||||
print(f"Error: Tileset {assetPath} has invalid attribute values.")
|
||||
return
|
||||
|
||||
# Exactly one image element is required
|
||||
imagesNode = root.findall('image')
|
||||
if len(imagesNode) != 1:
|
||||
print(f"Error: Tileset {assetPath} must have exactly one image element.")
|
||||
return
|
||||
|
||||
imageNode = imagesNode[0]
|
||||
if 'source' not in imageNode.attrib:
|
||||
print(f"Error: Tileset {assetPath} is missing image source.")
|
||||
return
|
||||
|
||||
imageSource = imageNode.attrib['source']
|
||||
|
||||
directory = os.path.dirname(assetPath)
|
||||
image = processImage(os.path.join(directory, imageSource))
|
||||
|
||||
# Build
|
||||
relativeFile = getAssetRelativePath(assetPath)
|
||||
relativeDir = os.path.dirname(relativeFile)
|
||||
|
||||
rows = (tilecount + columns - 1) // columns
|
||||
|
||||
buf = bytearray(b"DTF")
|
||||
buf += tilewidth.to_bytes(4, byteorder='little')
|
||||
buf += tileheight.to_bytes(4, byteorder='little')
|
||||
buf += tilecount.to_bytes(4, byteorder='little')
|
||||
buf += columns.to_bytes(4, byteorder='little')
|
||||
buf += rows.to_bytes(4, byteorder='little')
|
||||
|
||||
# Write image source file name, padd to ASSET_FILE_NAME_MAX_LENGTH
|
||||
imageSourceBytes = image["outputFile"].encode('utf-8')
|
||||
buf += len(imageSourceBytes).to_bytes(4, byteorder='little')
|
||||
buf += imageSourceBytes
|
||||
paddingLength = max(0, ASSET_FILE_NAME_MAX_LENGTH - len(imageSourceBytes))
|
||||
buf += b'\x00' * paddingLength
|
||||
|
||||
# Write to output file
|
||||
fileNameWithoutExt = os.path.splitext(os.path.basename(assetPath))[0]
|
||||
outputFilePath = os.path.join(args.output_assets, relativeDir, f"{fileNameWithoutExt}.dtf")
|
||||
os.makedirs(os.path.dirname(outputFilePath), exist_ok=True)
|
||||
with open(outputFilePath, 'wb') as f:
|
||||
f.write(buf)
|
||||
|
||||
return {
|
||||
"outputFile": os.path.relpath(outputFilePath, args.output_assets),
|
||||
"tileWidth": tilewidth,
|
||||
"tileHeight": tileheight,
|
||||
"tileCount": tilecount,
|
||||
"columns": columns,
|
||||
"rows": rows,
|
||||
"image": image,
|
||||
"files": [ outputFilePath ] + image["files"]
|
||||
}
|
Reference in New Issue
Block a user