#!/usr/bin/env python3 """How much leg is rendering IN FRONT OF the skirt, in pixels. python tools/measure_clipview.py That tool saves each pose twice, both drawing DISTANCE FROM THE CAMERA into the colour channel: `cloth_N.png` with only the cloth drawn and `clipview_N.png` with only the body. A pixel counts when the body is nearer than the nearest cloth there — which is exactly what "the thigh is showing through the skirt" means. Depth, not silhouettes. With the legs apart you see the FAR side of the skirt through the gap between them and the thigh is correctly in front of that; a mask test counts all of it, reported 25% of the cloth covered on poses that are fine, and sent two rounds of tuning after a defect that was not there. `hip_N.txt` gives the screen row of the hip joint; only rows below it count, because above it the torso is inside the skirt and in front of its waistband. Magenta is cloth, grey is body. So the cloth silhouette comes from the second image, and any grey inside it in the first is the leg in front of the skirt — the defect — while grey outside it is just the leg past the hem, which is correct. Reading that off the two pictures by eye is unreliable: the two failures look alike, the silhouettes interleave, and it was misjudged in both directions more than once. This counts it. WHAT THIS IS AND IS NOT FOR. It measures the garment's CONTINUITY well — a torn panel shows up as a hole in the cloth mask. It is NOT a clipping test. When the legs are apart you see the far side of the skirt through the gap between them, and the thighs are legitimately in front of that, so those pixels count as hits while being correct rendering. Every large reading here traced back to that. Judge clipping from debug/skirt_probe.gd and the plain textured render. It is also not repeatable enough to A/B tuning changes. The solver can be given a fixed timestep, but the AnimationTree still advances on the real frame delta, so the pose at a given frame drifts between runs: the same build measured 93k and 76k. Differences under about 20% here mean nothing. The cloth mask is eroded a little first. The two frames are one apart (the tool swaps materials between them), so the character has moved a fraction of a millimetre and the silhouette edges do not line up exactly; without the erosion every outline pixel reads as a hit. """ import os import sys from PIL import Image, ImageChops, ImageFilter ## Depth difference, in 8-bit steps, that counts as the body being in front. ## One step is about 6 mm over the 1.6 m the shader encodes; three keeps ## coincident surfaces and the one-frame offset between the two shots quiet. NEAR_EPS = 3 MIN_BLOB = 40 # ignore specks — anti-aliasing along an edge, not a defect def depth(path): """Per-pixel camera distance as an 8-bit band; 0 means no geometry.""" return Image.open(path).convert("RGB").split()[0] def main(): root = sys.argv[1] if len(sys.argv) > 1 else "." total = 0 shots = 0 for n in range(256): full = os.path.join(root, "clipview_%d.png" % n) only = os.path.join(root, "cloth_%d.png" % n) if not (os.path.exists(full) and os.path.exists(only)): continue # `clipview` draws EVERYTHING, so it holds the nearest of cloth-or-body; # `cloth` draws only the garment. Where the combined pass is nearer than # the cloth pass, something that is not cloth is in front of it. both_pass = depth(full) cloth = depth(only) w, h = cloth.size top = 0 hipf = os.path.join(root, "hip_%d.txt" % n) if os.path.exists(hipf): with open(hipf) as fh: top = max(0, int(fh.read().strip())) box = (0, top, w, h) b = both_pass.crop(box) c = cloth.crop(box) has_c = c.point(lambda p: 255 if p > 0 else 0) # Nearer than the nearest cloth, by more than a little depth noise. nearer = ImageChops.subtract(c, b).point(lambda p: 255 if p > NEAR_EPS else 0) hit_mask = ImageChops.multiply(has_c, nearer) hit = sum(hit_mask.point(lambda p: 1 if p else 0).get_flattened_data()) area = sum(has_c.point(lambda p: 1 if p else 0).get_flattened_data()) if hit >= MIN_BLOB: over = Image.merge("RGB", (c, c, c)).convert("RGB") over.paste(Image.new("RGB", over.size, (255, 32, 32)), (0, top), hit_mask) over.save(os.path.join(root, "over_%d.png" % n)) total += hit shots += 1 flag = "" if hit < MIN_BLOB else " <-- leg in front of cloth" print(" shot %d: %6d px of leg over %7d px of cloth (%.2f%%)%s" % (n, hit, area, 100.0 * hit / max(area, 1), flag)) if shots: print(" TOTAL %d px over %d shots" % (total, shots)) main()