ADD: libktx

This commit is contained in:
2025-11-09 18:27:25 +09:00
parent 3078110429
commit d97db7d801
2 changed files with 54 additions and 29 deletions

View File

@@ -112,3 +112,27 @@ add_custom_command(TARGET vulkan_engine POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:vulkan_engine> $<TARGET_FILE_DIR:vulkan_engine> COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:vulkan_engine> $<TARGET_FILE_DIR:vulkan_engine>
COMMAND_EXPAND_LISTS COMMAND_EXPAND_LISTS
) )
option(ENABLE_LIBKTX "Enable KTX2 loading via libktx" ON)
if (ENABLE_LIBKTX)
find_package(ktx CONFIG QUIET)
if (NOT ktx_FOUND)
find_package(ktx QUIET)
endif()
set(_KTX_TARGET "")
if (TARGET ktx::ktx)
set(_KTX_TARGET ktx::ktx)
elseif (TARGET KTX::ktx)
set(_KTX_TARGET KTX::ktx)
elseif (TARGET ktx)
set(_KTX_TARGET ktx)
endif()
if (_KTX_TARGET STREQUAL "")
message(STATUS "libktx not found via find_package; looking for in-tree build...")
else()
target_link_libraries(vulkan_engine PUBLIC ${_KTX_TARGET})
target_compile_definitions(vulkan_engine PUBLIC VULKAN_ENGINE_HAS_KTX=1)
endif()
endif()

View File

@@ -9,11 +9,13 @@ except Exception:
PIL_OK = False PIL_OK = False
DEFAULT_SUFFIX = { DEFAULT_SUFFIX = {
"albedo": ["_albedo", "_basecolor", "_base_colour", "_base_color", "_base"], "albedo": ["_albedo", "_basecolor", "_base_colour", "_base_color", "_base", "baseColor"],
"mr": ["_mr", "_orm", "_metalrough", "_metallicroughness"], "mr": ["_mr", "_orm", "_metalrough", "_metallicroughness", "metallicRoughness"],
"normal": ["_normal", "_norm", "_nrm", "_normalgl"] "normal": ["_normal", "_norm", "_nrm", "_normalgl"]
} }
SUPPORTED_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".tga", ".tif", ".tiff"}
def which_or_die(cmd): def which_or_die(cmd):
from shutil import which from shutil import which
if which(cmd) is None: if which(cmd) is None:
@@ -78,24 +80,22 @@ def parse_gltf_roles(gltf_path: Path):
return roles return roles
def has_meaningful_alpha(png_path: Path) -> bool: def has_meaningful_alpha(img_path: Path) -> bool:
if not PIL_OK: if not PIL_OK:
return False return False
try: try:
with Image.open(png_path) as im: with Image.open(img_path) as im:
if im.mode in ("RGBA", "LA") or ("transparency" in im.info): if ("A" in im.getbands()) or ("transparency" in im.info):
im = im.convert("RGBA") im = im.convert("RGBA")
alpha = im.getchannel("A") alpha = im.getchannel("A")
extrema = alpha.getextrema() extrema = alpha.getextrema()
if extrema and extrema != (255, 255): return bool(extrema and extrema != (255, 255))
return True return False
return False
except Exception: except Exception:
return False return False
return False return False
def decide_targets(role, albedo_target, png_path): def decide_targets(role, albedo_target, img_path):
""" return transcode target(BCn), OETF(srgb/linear)""" """ return transcode target(BCn), OETF(srgb/linear)"""
if role == "normal": if role == "normal":
return "bc5", "linear" return "bc5", "linear"
@@ -103,7 +103,7 @@ def decide_targets(role, albedo_target, png_path):
return "bc7", "linear" return "bc7", "linear"
# albedo # albedo
if albedo_target == "auto": if albedo_target == "auto":
if has_meaningful_alpha(png_path): if has_meaningful_alpha(img_path):
return "bc3", "srgb" return "bc3", "srgb"
else: else:
return "bc1", "srgb" return "bc1", "srgb"
@@ -124,15 +124,15 @@ def run_cmd(args_list, dry_run=False):
print(f"[ERR] {cmd}\n -> exit {e.returncode}", file=sys.stderr) print(f"[ERR] {cmd}\n -> exit {e.returncode}", file=sys.stderr)
return e.returncode return e.returncode
def process_one(png_path: Path, out_dir: Path, role, opts): def process_one(img_path: Path, out_dir: Path, role, opts):
stem = png_path.stem stem = img_path.stem
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
tmp_dir = out_dir / ".intermediate" tmp_dir = out_dir / ".intermediate"
tmp_dir.mkdir(parents=True, exist_ok=True) tmp_dir.mkdir(parents=True, exist_ok=True)
tmp_ktx2 = tmp_dir / f"{stem}.uastc.ktx2" tmp_ktx2 = tmp_dir / f"{stem}.uastc.ktx2"
# 1) PNG -> KTX2(UASTC) # 1) PNG -> KTX2(UASTC)
target_bc, oetf = decide_targets(role, opts.albedo_target, png_path) target_bc, oetf = decide_targets(role, opts.albedo_target, img_path)
toktx = [ toktx = [
"toktx", "toktx",
"--t2", "--t2",
@@ -147,7 +147,7 @@ def process_one(png_path: Path, out_dir: Path, role, opts):
toktx += ["--lower_left_maps_to_s0t0"] toktx += ["--lower_left_maps_to_s0t0"]
# albedo: srgb, else linear # albedo: srgb, else linear
toktx += ["--assign_oetf", oetf] toktx += ["--assign_oetf", oetf]
toktx += [str(tmp_ktx2), str(png_path)] toktx += [str(tmp_ktx2), str(img_path)]
rc = run_cmd(toktx, dry_run=opts.dry_run) rc = run_cmd(toktx, dry_run=opts.dry_run)
if rc != 0: return rc if rc != 0: return rc
@@ -170,8 +170,8 @@ def process_one(png_path: Path, out_dir: Path, role, opts):
return 0 return 0
def main(): def main():
p = argparse.ArgumentParser(description="PNG → KTX2(BCn) encoder (toktx + ktx transcode)") p = argparse.ArgumentParser(description="Image → KTX2(BCn) encoder (toktx + ktx transcode)")
p.add_argument("-i", "--input", required=True, help="Input folder(recursive) or PNG file") p.add_argument("-i", "--input", required=True, help="Input folder(recursive) or image file")
p.add_argument("-o", "--output", required=True, help="Output folder") p.add_argument("-o", "--output", required=True, help="Output folder")
p.add_argument("--gltf", help=".gltf File path (optional). glTF first, suffix last") p.add_argument("--gltf", help=".gltf File path (optional). glTF first, suffix last")
p.add_argument("--suffix-albedo", default=",".join(DEFAULT_SUFFIX["albedo"]), p.add_argument("--suffix-albedo", default=",".join(DEFAULT_SUFFIX["albedo"]),
@@ -202,13 +202,14 @@ def main():
gltf_roles = parse_gltf_roles(Path(opts.gltf)) gltf_roles = parse_gltf_roles(Path(opts.gltf))
in_path = Path(opts.input) in_path = Path(opts.input)
png_files = [] img_files = []
if in_path.is_file() and in_path.suffix.lower() == ".png": if in_path.is_file() and in_path.suffix.lower() in SUPPORTED_IMAGE_EXTS:
png_files = [in_path] img_files = [in_path]
else: else:
png_files = list(in_path.rglob("*.png")) for ext in SUPPORTED_IMAGE_EXTS:
if not png_files: img_files.extend(in_path.rglob(f"*{ext}"))
sys.exit("No input PNGs.") if not img_files:
sys.exit("No input images (supported: .png .jpg .jpeg .tga .tif .tiff).")
out_dir = Path(opts.output) out_dir = Path(opts.output)
@@ -224,16 +225,16 @@ def main():
tasks = [] tasks = []
with ThreadPoolExecutor(max_workers=opts.jobs) as ex: with ThreadPoolExecutor(max_workers=opts.jobs) as ex:
futs = {} futs = {}
for png in png_files: for img in img_files:
role = decide_role_for_path(png) role = decide_role_for_path(img)
fut = ex.submit(process_one, png, out_dir, role, opts) fut = ex.submit(process_one, img, out_dir, role, opts)
futs[fut] = (png, role) futs[fut] = (img, role)
any_err = False any_err = False
for fut in as_completed(futs): for fut in as_completed(futs):
png, role = futs[fut] img, role = futs[fut]
rc = fut.result() rc = fut.result()
status = "OK" if rc == 0 else f"ERR({rc})" status = "OK" if rc == 0 else f"ERR({rc})"
print(f"[{status}] {png.name} -> role={role}") print(f"[{status}] {img.name} -> role={role}")
if rc != 0: if rc != 0:
any_err = True any_err = True
if any_err: if any_err: