"""
Topology Evaluation Handlers
Functions for evaluating retopology results by comparing high-poly to low-poly meshes.
Includes configurable threshold criteria for pass/fail determination.
"""
import bpy
import bmesh
import math
import traceback
from mathutils import Vector
from mathutils.bvhtree import BVHTree
def evaluate_retopology(
high_poly,
low_poly,
quad_threshold=0.85,
distance_threshold=0.01,
coverage_threshold=0.95,
valence_threshold=0.80
):
"""
Evaluate retopology result by comparing low-poly mesh to high-poly reference.
Parameters:
- high_poly: Name of the high-poly reference mesh
- low_poly: Name of the low-poly retopology mesh
- quad_threshold: Minimum quad ratio for pass (default: 0.85 = 85%)
- distance_threshold: Maximum distance for pass (default: 0.01 units)
- coverage_threshold: Minimum surface coverage for pass (default: 0.95 = 95%)
- valence_threshold: Minimum valence-4 ratio for pass (default: 0.80 = 80%)
Returns evaluation metrics and pass/fail status.
"""
try:
# Get objects
high_obj = bpy.data.objects.get(high_poly)
low_obj = bpy.data.objects.get(low_poly)
if not high_obj:
return {"error": f"High-poly object '{high_poly}' not found"}
if not low_obj:
return {"error": f"Low-poly object '{low_poly}' not found"}
if high_obj.type != 'MESH' or low_obj.type != 'MESH':
return {"error": "Both objects must be mesh type"}
# Create bmesh for low-poly analysis
bm_low = bmesh.new()
bm_low.from_mesh(low_obj.data)
bm_low.verts.ensure_lookup_table()
bm_low.faces.ensure_lookup_table()
# Calculate quad ratio
total_faces = len(bm_low.faces)
if total_faces == 0:
bm_low.free()
return {"error": "Low-poly mesh has no faces"}
quad_count = sum(1 for f in bm_low.faces if len(f.verts) == 4)
quad_ratio = quad_count / total_faces
# Calculate valence ratio
total_verts = len(bm_low.verts)
valence_4_count = sum(1 for v in bm_low.verts if len(v.link_edges) == 4)
valence_ratio = valence_4_count / total_verts if total_verts > 0 else 0
# Get high-poly face count
bm_high = bmesh.new()
bm_high.from_mesh(high_obj.data)
high_face_count = len(bm_high.faces)
bm_high.free()
# Calculate polycount ratio
polycount_ratio = total_faces / high_face_count if high_face_count > 0 else 0
# Calculate distance metrics using BVH tree
# Build BVH for high-poly
depsgraph = bpy.context.evaluated_depsgraph_get()
high_eval = high_obj.evaluated_get(depsgraph)
bvh_high = BVHTree.FromObject(high_eval, depsgraph)
distances = []
covered_count = 0
# Sample vertices from low-poly
for vert in bm_low.verts:
world_co = low_obj.matrix_world @ vert.co
# Find closest point on high-poly
location, normal, index, dist = bvh_high.find_nearest(world_co)
if location is not None:
distances.append(dist)
if dist <= distance_threshold:
covered_count += 1
bm_low.free()
# Calculate distance metrics
if distances:
distance_max = max(distances)
distance_avg = sum(distances) / len(distances)
distance_coverage = covered_count / len(distances)
else:
distance_max = 0
distance_avg = 0
distance_coverage = 0
# Determine quality grade
score = (quad_ratio * 40 + valence_ratio * 30 +
(1 - min(distance_max / 0.1, 1)) * 20 +
distance_coverage * 10)
if score >= 85:
quality_grade = "A"
elif score >= 70:
quality_grade = "B"
elif score >= 55:
quality_grade = "C"
elif score >= 40:
quality_grade = "D"
else:
quality_grade = "F"
# Evaluate pass/fail criteria
passed_criteria = {
"quad_ratio": quad_ratio >= quad_threshold,
"distance_max": distance_max <= distance_threshold,
"distance_coverage": distance_coverage >= coverage_threshold,
"valence_ratio": valence_ratio >= valence_threshold
}
passed = all(passed_criteria.values())
# Identify issues
issues = []
if not passed_criteria["quad_ratio"]:
issues.append(f"Quad ratio {quad_ratio:.1%} below threshold {quad_threshold:.0%}")
if not passed_criteria["distance_max"]:
issues.append(f"Max distance {distance_max:.4f} exceeds threshold {distance_threshold}")
if not passed_criteria["distance_coverage"]:
issues.append(f"Coverage {distance_coverage:.1%} below threshold {coverage_threshold:.0%}")
if not passed_criteria["valence_ratio"]:
issues.append(f"Valence-4 ratio {valence_ratio:.1%} below threshold {valence_threshold:.0%}")
return {
"success": True,
"high_poly": high_poly,
"low_poly": low_poly,
"high_face_count": high_face_count,
"low_face_count": total_faces,
"distance_max": round(distance_max, 6),
"distance_avg": round(distance_avg, 6),
"distance_coverage": round(distance_coverage, 4),
"quad_ratio": round(quad_ratio, 4),
"valence_ratio": round(valence_ratio, 4),
"polycount_ratio": round(polycount_ratio, 4),
"quality_grade": quality_grade,
"issues": issues,
"passed": passed,
"passed_criteria": passed_criteria,
"thresholds_used": {
"quad_threshold": quad_threshold,
"distance_threshold": distance_threshold,
"coverage_threshold": coverage_threshold,
"valence_threshold": valence_threshold
}
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to evaluate retopology: {str(e)}"}