Update Healer Man server

This commit is contained in:
phenom committed 2026-10-06 22:25:34 -04:00
1 parent 6114a2295f
commit 69809f7228
593 files changed
+903643 -40774

No files matched your search

@@ -16,6 +16,8 @@ from pathlib import Path
import bpy
from mathutils import Vector
sys.path.insert(0, str(Path(__file__).resolve().parent))
from native_material_alpha import native_alpha_action, native_material_identity
COLLISION_CHUNK_TRIANGLE_LIMIT = 90_000
@@ -135,12 +137,14 @@ def image_alpha_role(image: bpy.types.Image | None) -> str:
return "cutout"
def preserve_material_alpha(material: bpy.types.Material) -> str:
image = material_image(material)
role = image_alpha_role(image)
if role == "opaque" or material.node_tree is None:
def apply_material_alpha_role(
material: bpy.types.Material,
image: bpy.types.Image | None,
role: str,
threshold: float = 0.5,
) -> str:
if role == "opaque" or image is None or material.node_tree is None:
return role
image_node = next(
node
for node in material.node_tree.nodes
@@ -167,32 +171,117 @@ def preserve_material_alpha(material: bpy.types.Material) -> str:
alpha_clip = material.node_tree.nodes.new("ShaderNodeMath")
alpha_clip.name = "RuneWaker Alpha Clip"
alpha_clip.operation = "GREATER_THAN"
alpha_clip.inputs[1].default_value = 0.5
alpha_clip.inputs[1].default_value = threshold
material.node_tree.links.new(alpha_output, alpha_clip.inputs[0])
material.node_tree.links.new(alpha_clip.outputs[0], alpha_input)
material.blend_method = "CLIP"
material.alpha_threshold = 0.5
material.alpha_threshold = threshold
return role
def deduplicate_materials(objects: list[bpy.types.Object]) -> dict[str, int]:
def preserve_material_alpha(
material: bpy.types.Material,
source_report: dict[str, object] | None,
) -> tuple[str, dict[str, object] | None, dict[str, object]]:
image = material_image(material)
role = image_alpha_role(image)
image_path = bpy.path.abspath(image.filepath) if image is not None else ""
identity = native_material_identity(material.name, image_path, source_report)
decision = native_alpha_action(identity, role)
action = str(decision.get("action", "unsupported"))
if action == "pixel-alpha-heuristic":
applied_role = apply_material_alpha_role(material, image, role)
decision = {
"action": applied_role,
"reason": decision.get("reason", "no-verified-native-material-match"),
}
elif action in {"blend", "mask", "additive"}:
applied_role = apply_material_alpha_role(
material,
image,
"cutout" if action == "mask" else "blend",
float(decision.get("threshold", 0.5)),
)
decision = {**decision, "applied": applied_role}
if identity is not None:
material["nativeSourceModel"] = identity["sourceModel"]
material["nativeSourceResourceSha256"] = identity["sourceResourceSha256"]
material["nativeMaterialIndex"] = identity["materialIndex"]
material["nativeSourceTexture"] = identity["sourceTexture"]
material["nativeAlphaPolicy"] = str(decision.get("reason", action))
lighting = identity.get("lightingType")
lighting_name = lighting.get("name") if isinstance(lighting, dict) else None
if isinstance(lighting_name, str) and lighting_name.strip():
material["nativeLightingType"] = lighting_name.strip().lower()
elif "nativeLightingType" in material:
del material["nativeLightingType"]
if action == "additive":
material["nativeBlendMode"] = "additive"
elif "nativeBlendMode" in material:
del material["nativeBlendMode"]
return role, identity, decision
def deduplicate_materials(
objects: list[bpy.types.Object],
source_report: dict[str, object] | None,
) -> dict[str, object]:
canonical: dict[str, bpy.types.Material] = {}
replacements = 0
native_alpha_rows: dict[int, dict[str, object]] = {}
for obj in objects:
for slot in obj.material_slots:
material = slot.material
if material is None:
continue
key = material_image_key(material)
alpha_role, identity, decision = preserve_material_alpha(material, source_report)
semantics_key = (
repr((identity["sourceModel"], identity["sourceResourceSha256"],
identity["materialIndex"], identity["sourceTexture"],
sorted(identity["alphaBlendParameters"].items())))
if identity is not None
else "pixel-alpha-heuristic:" + alpha_role
)
key = material_image_key(material) + "|" + semantics_key
if identity is not None:
native_alpha_rows[identity["materialIndex"]] = {
"material": identity["materialName"],
"materialIndex": identity["materialIndex"],
"sourceModel": identity["sourceModel"],
"sourceResourceSha256": identity["sourceResourceSha256"],
"sourceTexture": identity["sourceTexture"],
"stagedTexture": identity["stagedTexture"],
"materialType": identity["materialType"],
"lightingType": identity["lightingType"],
"alphaBlendParameters": identity["alphaBlendParameters"],
"sourceImageAlphaRole": alpha_role,
"conversionPolicy": decision,
}
if key in canonical:
if slot.material != canonical[key]:
slot.material = canonical[key]
replacements += 1
else:
canonical[key] = material
material["semanticRole"] = preserve_material_alpha(material)
material["semanticRole"] = decision.get("action", alpha_role)
material["sourceFormat"] = "RuneWaker DDS"
return {"unique": len(canonical), "replacements": replacements}
source_model = source_report.get("sourceModel") if isinstance(source_report, dict) else None
source_hash = source_report.get("sourceResourceSha256") if isinstance(source_report, dict) else None
native_alpha_materials = [native_alpha_rows[index] for index in sorted(native_alpha_rows)]
return {
"unique": len(canonical),
"replacements": replacements,
"nativeAlpha": {
"sourceModel": source_model,
"sourceResourceSha256": source_hash,
"matchedMaterialCount": len(native_alpha_materials),
"materials": native_alpha_materials,
"unsupportedPolicies": [
row for row in native_alpha_materials
if row["conversionPolicy"].get("action") == "unsupported"
],
},
}
def export_glb(
@@ -302,6 +391,12 @@ def render_topdown_preview(
def main() -> None:
source_obj, output_dir, slug, unit_scale = arguments()
output_dir.mkdir(parents=True, exist_ok=True)
source_report_file = source_obj.parent / "export-report.json"
source_report = (
json.loads(source_report_file.read_text(encoding="utf-8"))
if source_report_file.is_file()
else None
)
for stale in output_dir.glob(f"{slug}-collision*.glb"):
stale.unlink()
@@ -330,7 +425,7 @@ def main() -> None:
obj["sourceFormat"] = "RuneWaker ROS via OBJ bridge"
obj["sourceUnitScaleMeters"] = unit_scale
material_stats = deduplicate_materials(visual)
material_stats = deduplicate_materials(visual, source_report)
bounds_min, bounds_max = three_bounds(visual)
source_triangles = sum(triangles(obj) for obj in visual)
@@ -364,12 +459,6 @@ def main() -> None:
blend_file = output_dir / f"{slug}.source.blend"
bpy.ops.wm.save_as_mainfile(filepath=str(blend_file))
source_report_file = source_obj.parent / "export-report.json"
source_report = (
json.loads(source_report_file.read_text(encoding="utf-8"))
if source_report_file.is_file()
else None
)
report = {
"schemaVersion": 1,
"dungeonId": slug,
@@ -379,6 +468,7 @@ def main() -> None:
"obj": source_obj.name,
"report": source_report_file.name if source_report is not None else None,
"model": source_report.get("sourceModel") if source_report else None,
"resourceSha256": source_report.get("sourceResourceSha256") if source_report else None,
},
"coordinateTransform": {
"nativeBridge": "three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)",