Add GLB XKT pipeline and LOD controls

This commit is contained in:
CODEX
2026-06-25 16:24:48 +03:00
parent 761cb56fac
commit abf899fefd
7 changed files with 1144 additions and 37 deletions
+560 -15
View File
@@ -25,7 +25,7 @@ from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ShapeTool
CONVERTER_NAME = "NodeDcBimConverter"
CONVERTER_VERSION = "0.4.7"
CONVERTER_VERSION = "0.4.9"
UPLOADS_DIR = Path(os.environ.get("NODEDC_BIM_CONVERTER_UPLOADS_DIR", "/beam/uploads")).resolve()
POLL_INTERVAL_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_INTERVAL_SECONDS", "10"))
PROCESS_ONCE = os.environ.get("NODEDC_BIM_CONVERTER_ONCE", "").lower() in {"1", "true", "yes"}
@@ -37,6 +37,11 @@ REPROCESS_FAILED_ON_VERSION_CHANGE = os.environ.get(
).lower() in {"1", "true", "yes"}
XKT_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_XKT_ENABLED", "1").lower() not in {"0", "false", "no"}
XKT_COMMAND = os.environ.get("NODEDC_BIM_CONVERTER_XKT_COMMAND", "xeokit-convert")
GLB_XKT_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_XKT_ENABLED", "1").lower() not in {
"0",
"false",
"no",
}
GLB_DRACO_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_DRACO_ENABLED", "1").lower() not in {
"0",
"false",
@@ -44,6 +49,11 @@ GLB_DRACO_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_DRACO_ENABLED", "1"
}
GLTF_TRANSFORM_COMMAND = os.environ.get("NODEDC_BIM_CONVERTER_GLTF_TRANSFORM_COMMAND", "gltf-transform")
NODE_OPTIONS = os.environ.get("NODE_OPTIONS", "--max-old-space-size=8192")
GLB_LOD_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_LOD_ENABLED", "1").lower() not in {
"0",
"false",
"no",
}
STEP_MESH_LINEAR_DEFLECTION = float(os.environ.get("NODEDC_BIM_CONVERTER_STEP_LINEAR_DEFLECTION", "0.1"))
STEP_MESH_ANGULAR_DEFLECTION = float(os.environ.get("NODEDC_BIM_CONVERTER_STEP_ANGULAR_DEFLECTION", "0.1"))
PROCESSING_STALE_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_PROCESSING_STALE_SECONDS", "60"))
@@ -52,12 +62,41 @@ CONVERSION_TIMEOUT_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_TIMEOUT_
GLB_DRACO_TIMEOUT_SECONDS = float(
os.environ.get("NODEDC_BIM_CONVERTER_GLB_DRACO_TIMEOUT_SECONDS", str(CONVERSION_TIMEOUT_SECONDS))
)
GLB_LOD_TIMEOUT_SECONDS = float(
os.environ.get("NODEDC_BIM_CONVERTER_GLB_LOD_TIMEOUT_SECONDS", str(max(CONVERSION_TIMEOUT_SECONDS, 900)))
)
XKT_TIMEOUT_SECONDS = float(
os.environ.get("NODEDC_BIM_CONVERTER_XKT_TIMEOUT_SECONDS", str(max(CONVERSION_TIMEOUT_SECONDS, 900)))
)
XCAF_TIMEOUT_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_XCAF_TIMEOUT_SECONDS", str(CONVERSION_TIMEOUT_SECONDS)))
CADQUERY_TIMEOUT_SECONDS = float(
os.environ.get("NODEDC_BIM_CONVERTER_CADQUERY_TIMEOUT_SECONDS", str(CONVERSION_TIMEOUT_SECONDS))
)
STEP_EXTENSIONS = {".step", ".stp"}
GLB_EXTENSIONS = {".glb"}
GLB_LOD_PROFILES = [
{
"id": "full",
"label": "Полный",
"suffix": "lod-full",
"ratio": None,
"error": None,
},
{
"id": "desktop",
"label": "Средний",
"suffix": "lod-desktop",
"ratio": 0.35,
"error": 0.005,
},
{
"id": "mobile",
"label": "Мобильный",
"suffix": "lod-mobile",
"ratio": 0.12,
"error": 0.02,
},
]
def now_iso() -> str:
@@ -123,6 +162,26 @@ def draco_optimization_attempt_count(manifest: dict[str, Any]) -> int:
return 0
def xkt_conversion_attempt_count(manifest: dict[str, Any]) -> int:
conversion = manifest.get("xktConversion")
if not isinstance(conversion, dict):
return 0
try:
return max(0, int(conversion.get("attempts") or 0))
except Exception:
return 0
def lod_generation_attempt_count(manifest: dict[str, Any]) -> int:
generation = manifest.get("lodGeneration")
if not isinstance(generation, dict):
return 0
try:
return max(0, int(generation.get("attempts") or 0))
except Exception:
return 0
def update_asset_manifest_version(source_path: Path, version_payload: dict[str, Any]) -> None:
asset_path = asset_manifest_path(source_path)
if not asset_path:
@@ -740,15 +799,23 @@ def convert_glb_to_xkt(glb_path: Path, xkt_path: Path) -> dict[str, Any]:
print(f"[{CONVERTER_NAME}] XKT export {glb_path.name}", flush=True)
with log_path.open("w", encoding="utf-8", errors="replace") as log_file:
result = subprocess.run(
command,
cwd=str(glb_path.parent),
env=env,
stdout=log_file,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
try:
result = subprocess.run(
command,
cwd=str(glb_path.parent),
env=env,
stdout=log_file,
stderr=subprocess.STDOUT,
text=True,
timeout=XKT_TIMEOUT_SECONDS,
check=False,
)
except subprocess.TimeoutExpired as exc:
try:
tmp_xkt_path.unlink()
except Exception:
pass
raise TimeoutError(f"XKT export timed out after {int(XKT_TIMEOUT_SECONDS)} seconds.") from exc
if result.returncode != 0:
tail = read_log_tail(log_path)
@@ -1196,6 +1263,29 @@ def glb_draco_artifact_path(source_path: Path) -> Path:
return source_path.with_name(f"{source_path.stem}.draco.glb")
def glb_xkt_artifact_path(source_path: Path) -> Path:
return source_path.with_name(f"{source_path.stem}.xkt")
def glb_lod_artifact_path(source_path: Path, profile: dict[str, Any]) -> Path:
return source_path.with_name(f"{source_path.stem}.{profile['suffix']}.glb")
def lod_records_ready(lods: Any) -> bool:
if not isinstance(lods, list) or len(lods) < len(GLB_LOD_PROFILES):
return False
by_id = {
str(item.get("id")): item
for item in lods
if isinstance(item, dict) and item.get("id")
}
for profile in GLB_LOD_PROFILES:
item = by_id.get(profile["id"])
if not item or not upload_src_exists(item.get("artifactSrc")):
return False
return True
def should_optimize_glb(source_path: Path, manifest: dict[str, Any], artifact_path: Path) -> bool:
if not GLB_DRACO_ENABLED or not source_path.exists() or not manifest:
return False
@@ -1331,18 +1421,32 @@ def optimize_glb_one(source_path: Path) -> bool:
source_size = source_path.stat().st_size
artifact_size = artifact_path.stat().st_size
draco_artifact_src = src_from_path(artifact_path)
existing_artifact_src = manifest.get("artifactSrc")
existing_artifact_type = manifest.get("artifactType") or manifest.get("targetFormat")
preserve_xkt_artifact = (
existing_artifact_type == "xkt"
and isinstance(existing_artifact_src, str)
and upload_src_exists(existing_artifact_src)
)
preferred_artifact_src = existing_artifact_src if preserve_xkt_artifact else draco_artifact_src
preferred_artifact_type = "xkt" if preserve_xkt_artifact else "gltf"
ready_manifest = {
**base_manifest,
"artifactSha256": calculate_file_sha256(artifact_path),
"artifactSrc": src_from_path(artifact_path),
"artifactType": "gltf",
"artifactSrc": preferred_artifact_src,
"artifactType": preferred_artifact_type,
"dracoArtifactSrc": draco_artifact_src,
"dracoArtifactType": "gltf",
"fallbackArtifactSrc": source_src,
"fallbackArtifactType": "gltf",
"message": "Model is ready. GLB geometry optimized with Draco.",
"targetFormat": "gltf",
"targetFormat": "xkt" if preserve_xkt_artifact else "gltf",
"dracoOptimization": {
"status": "ready",
"attempts": attempt,
"artifactSrc": draco_artifact_src,
"artifactType": "gltf",
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"command": command,
@@ -1358,7 +1462,7 @@ def optimize_glb_one(source_path: Path) -> bool:
write_json_atomic(manifest_file, ready_manifest)
update_asset_manifest_version(source_path, ready_manifest)
print(
f"[{CONVERTER_NAME}] GLB Draco ready {ready_manifest['artifactSrc']} "
f"[{CONVERTER_NAME}] GLB Draco ready {draco_artifact_src} "
f"({source_size} -> {artifact_size} bytes)",
flush=True,
)
@@ -1389,6 +1493,445 @@ def optimize_glb_one(source_path: Path) -> bool:
return False
def should_convert_glb_to_xkt(source_path: Path, manifest: dict[str, Any], xkt_path: Path) -> bool:
if not XKT_ENABLED or not GLB_XKT_ENABLED or not source_path.exists() or not manifest:
return False
if source_path.name.endswith(".draco.glb") or ".lod-" in source_path.name:
return False
source_format = str(manifest.get("sourceFormat") or "").replace(".", "").lower()
if source_format != "glb":
return False
if manifest.get("status") not in {None, "ready"}:
return False
conversion = manifest.get("xktConversion")
if not isinstance(conversion, dict):
conversion = {}
conversion_status = conversion.get("status")
conversion_version = conversion.get("converterVersion")
artifact_src = manifest.get("artifactSrc")
artifact_type = manifest.get("artifactType") or manifest.get("targetFormat")
artifact_ready = artifact_type == "xkt" and isinstance(artifact_src, str) and upload_src_exists(artifact_src)
if conversion_status == "ready" and artifact_ready:
return REPROCESS_READY_ON_VERSION_CHANGE and conversion_version != CONVERTER_VERSION
if conversion_status == "ready" and xkt_path.exists() and not artifact_ready:
return True
if conversion_status == "failed":
attempts = xkt_conversion_attempt_count(manifest)
if attempts >= CONVERSION_MAX_ATTEMPTS:
return REPROCESS_FAILED_ON_VERSION_CHANGE and conversion_version != CONVERTER_VERSION
return True
if conversion_status == "processing":
updated_at = conversion.get("updatedAt") or manifest.get("updatedAt")
if updated_at:
try:
updated = datetime.fromisoformat(str(updated_at).replace("Z", "+00:00"))
if (datetime.now(timezone.utc) - updated).total_seconds() < PROCESSING_STALE_SECONDS:
return False
except Exception:
pass
return True
return True
def convert_glb_to_xkt_one(source_path: Path) -> bool:
manifest_file = manifest_path(source_path)
if not manifest_file.exists():
return False
manifest = read_json(manifest_file)
xkt_path = glb_xkt_artifact_path(source_path)
if not should_convert_glb_to_xkt(source_path, manifest, xkt_path):
return False
source_src = src_from_path(source_path)
base_manifest = {
**manifest,
"createdAt": manifest.get("createdAt") or now_iso(),
"downloadSrc": manifest.get("downloadSrc") or source_src,
"sourceFormat": "glb",
"sourceSrc": manifest.get("sourceSrc") or source_src,
"status": "ready",
}
attempts = 0
conversion = manifest.get("xktConversion")
if isinstance(conversion, dict) and conversion.get("converterVersion") == CONVERTER_VERSION:
attempts = xkt_conversion_attempt_count(manifest)
if isinstance(conversion, dict) and conversion.get("status") == "processing" and attempts >= CONVERSION_MAX_ATTEMPTS:
failed_manifest = {
**base_manifest,
"message": "Model is ready. XKT conversion did not finish; GLB fallback is used.",
"xktConversion": {
**conversion,
"status": "failed",
"attempts": attempts,
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"error": f"XKT conversion stopped before completion after {attempts} attempt(s).",
"updatedAt": now_iso(),
},
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, failed_manifest)
update_asset_manifest_version(source_path, failed_manifest)
print(
f"[{CONVERTER_NAME}] GLB XKT failed {source_path}: max attempts reached ({attempts})",
file=sys.stderr,
flush=True,
)
return False
attempt = attempts + 1
command = [
XKT_COMMAND,
"-s",
str(source_path),
"-f",
"glb",
"-o",
str(xkt_path.with_name(f"{xkt_path.name}.tmp")),
"-t",
"-n",
"-e",
"0",
"-b",
]
processing_manifest = {
**base_manifest,
"message": "Model is ready. Converting GLB to XKT viewer artifact.",
"targetFormat": "xkt",
"xktConversion": {
"status": "processing",
"attempts": attempt,
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"command": command,
"updatedAt": now_iso(),
},
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, processing_manifest)
update_asset_manifest_version(source_path, processing_manifest)
print(f"[{CONVERTER_NAME}] converting GLB to XKT {source_path}", flush=True)
try:
xkt_stats = convert_glb_to_xkt(source_path, xkt_path)
source_size = source_path.stat().st_size
artifact_size = xkt_path.stat().st_size
xkt_artifact_src = src_from_path(xkt_path)
fallback_artifact_src = (
manifest.get("dracoArtifactSrc")
or (
manifest.get("artifactSrc")
if (manifest.get("artifactType") or manifest.get("targetFormat")) == "gltf"
else None
)
or source_src
)
ready_manifest = {
**base_manifest,
"artifactSha256": calculate_file_sha256(xkt_path),
"artifactSrc": xkt_artifact_src,
"artifactType": "xkt",
"fallbackArtifactSrc": fallback_artifact_src,
"fallbackArtifactType": "gltf",
"glbArtifactSrc": source_src,
"targetFormat": "xkt",
"xktArtifactSrc": xkt_artifact_src,
"xkt": xkt_stats,
"xktConversion": {
"status": "ready",
"attempts": attempt,
"artifactSrc": xkt_artifact_src,
"artifactType": "xkt",
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"command": command,
"sourceSize": source_size,
"artifactSize": artifact_size,
"compressionRatio": round(artifact_size / source_size, 6) if source_size else None,
"updatedAt": now_iso(),
},
"message": "Model is ready. XKT viewer artifact generated.",
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, ready_manifest)
update_asset_manifest_version(source_path, ready_manifest)
print(
f"[{CONVERTER_NAME}] GLB XKT ready {xkt_artifact_src} "
f"({source_size} -> {artifact_size} bytes)",
flush=True,
)
return True
except Exception as exc:
tmp_path = xkt_path.with_name(f"{xkt_path.name}.tmp")
if tmp_path.exists():
try:
tmp_path.unlink()
except Exception:
pass
failed_manifest = {
**base_manifest,
"message": "Model is ready. XKT conversion failed; GLB fallback is used.",
"xktConversion": {
"status": "failed",
"attempts": attempt,
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"command": command,
"error": str(exc),
"updatedAt": now_iso(),
},
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, failed_manifest)
update_asset_manifest_version(source_path, failed_manifest)
print(f"[{CONVERTER_NAME}] GLB XKT failed {source_path}: {exc}", file=sys.stderr, flush=True)
return False
def should_generate_glb_lods(source_path: Path, manifest: dict[str, Any]) -> bool:
if not GLB_LOD_ENABLED or not source_path.exists() or not manifest:
return False
if source_path.name.endswith(".draco.glb") or ".lod-" in source_path.name:
return False
source_format = str(manifest.get("sourceFormat") or "").replace(".", "").lower()
if source_format != "glb":
return False
if manifest.get("status") not in {None, "ready"}:
return False
generation = manifest.get("lodGeneration")
if not isinstance(generation, dict):
return False
generation_status = generation.get("status")
generation_version = generation.get("converterVersion")
if generation_status == "ready" and lod_records_ready(manifest.get("lods")):
return REPROCESS_READY_ON_VERSION_CHANGE and generation_version != CONVERTER_VERSION
if generation_status == "failed":
attempts = lod_generation_attempt_count(manifest)
if attempts >= CONVERSION_MAX_ATTEMPTS:
return REPROCESS_FAILED_ON_VERSION_CHANGE and generation_version != CONVERTER_VERSION
return True
if generation_status == "processing":
updated_at = generation.get("updatedAt") or manifest.get("updatedAt")
if updated_at:
try:
updated = datetime.fromisoformat(str(updated_at).replace("Z", "+00:00"))
if (datetime.now(timezone.utc) - updated).total_seconds() < PROCESSING_STALE_SECONDS:
return False
except Exception:
pass
return True
return generation_status == "queued"
def glb_lod_command(source_path: Path, target_path: Path, profile: dict[str, Any]) -> list[str]:
if profile.get("ratio") is None:
return [GLTF_TRANSFORM_COMMAND, "draco", str(source_path), str(target_path)]
return [
GLTF_TRANSFORM_COMMAND,
"optimize",
str(source_path),
str(target_path),
"--compress",
"draco",
"--texture-compress",
"false",
"--simplify-ratio",
str(profile["ratio"]),
"--simplify-error",
str(profile["error"]),
]
def run_gltf_transform_lod(source_path: Path, target_path: Path, profile: dict[str, Any]) -> tuple[list[str], str, str]:
tmp_path = target_path.with_suffix(".tmp.glb")
if tmp_path.exists():
tmp_path.unlink()
command = glb_lod_command(source_path, tmp_path, profile)
run_env = os.environ.copy()
run_env["NODE_OPTIONS"] = NODE_OPTIONS
result = subprocess.run(
command,
cwd=str(Path(__file__).resolve().parent),
env=run_env,
text=True,
capture_output=True,
timeout=GLB_LOD_TIMEOUT_SECONDS,
check=False,
)
if result.returncode != 0:
raise RuntimeError(
limited_process_output(result.stderr)
or limited_process_output(result.stdout)
or f"gltf-transform exited with code {result.returncode}"
)
if not tmp_path.exists() or tmp_path.stat().st_size <= 0:
raise RuntimeError("gltf-transform did not produce a LOD GLB artifact")
tmp_path.replace(target_path)
return command, limited_process_output(result.stdout), limited_process_output(result.stderr)
def generate_glb_lods_one(source_path: Path) -> bool:
manifest_file = manifest_path(source_path)
if not manifest_file.exists():
return False
manifest = read_json(manifest_file)
if not should_generate_glb_lods(source_path, manifest):
return False
source_src = src_from_path(source_path)
base_manifest = {
**manifest,
"createdAt": manifest.get("createdAt") or now_iso(),
"downloadSrc": manifest.get("downloadSrc") or source_src,
"sourceFormat": "glb",
"sourceSrc": manifest.get("sourceSrc") or source_src,
"status": "ready",
}
attempts = 0
generation = manifest.get("lodGeneration")
if isinstance(generation, dict) and generation.get("converterVersion") == CONVERTER_VERSION:
attempts = lod_generation_attempt_count(manifest)
if isinstance(generation, dict) and generation.get("status") == "processing" and attempts >= CONVERSION_MAX_ATTEMPTS:
failed_manifest = {
**base_manifest,
"message": "Model is ready. LOD generation did not finish.",
"lodGeneration": {
**generation,
"status": "failed",
"attempts": attempts,
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"error": f"LOD generation stopped before completion after {attempts} attempt(s).",
"updatedAt": now_iso(),
},
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, failed_manifest)
update_asset_manifest_version(source_path, failed_manifest)
print(
f"[{CONVERTER_NAME}] GLB LOD failed {source_path}: max attempts reached ({attempts})",
file=sys.stderr,
flush=True,
)
return False
attempt = attempts + 1
processing_manifest = {
**base_manifest,
"message": "Model is ready. Generating GLB LOD levels.",
"lodGeneration": {
**(generation if isinstance(generation, dict) else {}),
"status": "processing",
"attempts": attempt,
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"profiles": [
{key: profile[key] for key in ("id", "label", "ratio", "error") if key in profile}
for profile in GLB_LOD_PROFILES
],
"updatedAt": now_iso(),
},
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, processing_manifest)
update_asset_manifest_version(source_path, processing_manifest)
source_size = source_path.stat().st_size
records = []
commands = []
print(f"[{CONVERTER_NAME}] generating GLB LODs {source_path}", flush=True)
try:
for profile in GLB_LOD_PROFILES:
artifact_path = glb_lod_artifact_path(source_path, profile)
command, stdout, stderr = run_gltf_transform_lod(source_path, artifact_path, profile)
commands.append({
"id": profile["id"],
"command": command,
"stdout": stdout,
"stderr": stderr,
})
artifact_size = artifact_path.stat().st_size
records.append({
"id": profile["id"],
"label": profile["label"],
"artifactSrc": src_from_path(artifact_path),
"type": "gltf",
"ratio": profile.get("ratio"),
"error": profile.get("error"),
"size": artifact_size,
"sha256": calculate_file_sha256(artifact_path),
"sourceSize": source_size,
"compressionRatio": round(artifact_size / source_size, 6) if source_size else None,
"generatedAt": now_iso(),
})
ready_manifest = {
**base_manifest,
"lods": records,
"lodGeneration": {
"status": "ready",
"attempts": attempt,
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"commands": commands,
"sourceSize": source_size,
"updatedAt": now_iso(),
},
"message": "Model is ready. GLB LOD levels generated.",
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, ready_manifest)
update_asset_manifest_version(source_path, ready_manifest)
print(
f"[{CONVERTER_NAME}] GLB LOD ready {source_src}: "
+ ", ".join(f"{item['id']}={item['size']}" for item in records),
flush=True,
)
return True
except Exception as exc:
for profile in GLB_LOD_PROFILES:
tmp_path = glb_lod_artifact_path(source_path, profile).with_suffix(".tmp.glb")
if tmp_path.exists():
try:
tmp_path.unlink()
except Exception:
pass
failed_manifest = {
**base_manifest,
"message": "Model is ready. LOD generation failed.",
"lodGeneration": {
**(generation if isinstance(generation, dict) else {}),
"status": "failed",
"attempts": attempt,
"converterName": CONVERTER_NAME,
"converterVersion": CONVERTER_VERSION,
"commands": commands,
"error": str(exc),
"updatedAt": now_iso(),
},
"updatedAt": now_iso(),
}
write_json_atomic(manifest_file, failed_manifest)
update_asset_manifest_version(source_path, failed_manifest)
print(f"[{CONVERTER_NAME}] GLB LOD failed {source_path}: {exc}", file=sys.stderr, flush=True)
return False
def scan_once() -> int:
if not UPLOADS_DIR.exists():
print(f"[{CONVERTER_NAME}] uploads dir does not exist: {UPLOADS_DIR}", flush=True)
@@ -1403,7 +1946,9 @@ def scan_once() -> int:
if suffix in STEP_EXTENSIONS:
did_process = process_one(source_path)
elif suffix in GLB_EXTENSIONS:
did_process = optimize_glb_one(source_path)
did_process = convert_glb_to_xkt_one(source_path)
did_process = optimize_glb_one(source_path) or did_process
did_process = generate_glb_lods_one(source_path) or did_process
if did_process:
processed += 1
return processed