@tool extends RefCounted # Validator: Polygon Count # # Checks: # - Total mesh triangle count across all MeshInstance3D nodes ≤ 50K # - Per-mesh breakdown printed for debugging # - Warns on individual meshes > 5K triangles const MAX_TOTAL_TRIANGLES := 50000 const PER_MESH_WARN_THRESHOLD := 5000 func validate(scene_root: Node, scene_path: String) -> Dictionary: var errors: Array[String] = [] var warnings: Array[String] = [] var mesh_instances: Array[MeshInstance3D] = [] _collect_mesh_instances(scene_root, mesh_instances) var total_triangles := 0 var mesh_breakdown: Array[Dictionary] = [] for mi in mesh_instances: var mesh: Mesh = mi.mesh if mesh == null: continue var tri_count := _count_triangles(mesh) total_triangles += tri_count var entry := { "node": mi.name, "path": _node_path_to_string(scene_root, mi), "triangles": tri_count, "mesh_type": mesh.get_class(), } mesh_breakdown.append(entry) if tri_count > PER_MESH_WARN_THRESHOLD: warnings.append("High-poly mesh: \"%s\" (%s) — %d triangles (limit: %d per mesh recommend)" % [ mi.name, entry["path"], tri_count, PER_MESH_WARN_THRESHOLD]) # Print breakdown print(" Meshes scanned: ", mesh_instances.size()) print(" Total triangles: ", total_triangles) print(" Budget: ", MAX_TOTAL_TRIANGLES) print("") # Sort and show top contributors mesh_breakdown.sort_custom(func(a, b): return a["triangles"] > b["triangles"]) var top := mesh_breakdown.slice(0, min(10, mesh_breakdown.size())) if not top.is_empty(): print(" Top meshes by triangle count:") for m in top: print(" %6d %s (%s)" % [m["triangles"], m["node"], m["mesh_type"]]) if mesh_breakdown.size() > 10: print(" ... and ", mesh_breakdown.size() - 10, " more") if total_triangles > MAX_TOTAL_TRIANGLES: errors.append("Map exceeds %d triangle budget: %d total (%.1f%% over budget)" % [ MAX_TOTAL_TRIANGLES, total_triangles, float(total_triangles - MAX_TOTAL_TRIANGLES) / MAX_TOTAL_TRIANGLES * 100.0]) else: var pct := float(total_triangles) / MAX_TOTAL_TRIANGLES * 100.0 print(" ✓ Within budget (%.1f%% of %d)" % [pct, MAX_TOTAL_TRIANGLES]) # Warn if mesh_instances is empty (suspicious map with no geometry) if mesh_instances.is_empty(): warnings.append("No MeshInstance3D nodes found in scene — map may be empty or using unsupported geometry type") return { "pass": errors.is_empty(), "errors": errors, "warnings": warnings, } func _collect_mesh_instances(node: Node, result: Array) -> void: if node is MeshInstance3D: result.append(node) for child in node.get_children(): _collect_mesh_instances(child, result) func _count_triangles(mesh: Mesh) -> int: var total := 0 for i in mesh.get_surface_count(): var arrays := mesh.surface_get_arrays(i) if arrays.is_empty(): continue var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array if indices and indices.size() > 0: total += indices.size() / 3 else: # Non-indexed mesh — count vertices / 3 as approximation var verts := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array if verts: total += verts.size() / 3 return total func _node_path_to_string(root: Node, node: Node) -> String: var full_path: String = node.get_path_to(root) # full_path is a NodePath string like "../NodeA/NodeB" — split and drop the root var parts := full_path.split("/") # Remove the first segment (the "../" back-reference) and last if empty var effective: Array[String] = [] for p in parts: if p.is_empty() or p == "." or p.begins_with(".."): continue effective.append(p) return "/".join(effective)