# Builds assets/Dragon.obj (+ .mtl) from the tile polygons that util/traceDragon.py exports: every # tile becomes a slab with a chamfered front edge, standing upright in the XY plane with depth along +Z, # resting on Y = 0 and centred on X = 0. Planar UVs run across the whole figure so a rendering of # Dragon.svg can be used as a texture. # Usage: python3 util/dragonObj.py pieces.json assets/Dragon.obj [--scale 0.01] [--depth 10] [--bevel 3] import argparse import json import os import numpy as np from objWriter import ObjWriterT, writeMtl, DRAGON_MATERIALS parser = argparse.ArgumentParser() parser.add_argument('pieces') parser.add_argument('out') parser.add_argument('--scale', type=float, default=0.01, help='model units per image pixel') parser.add_argument('--depth', type=float, default=10.0, help='slab thickness in image pixels') parser.add_argument('--bevel', type=float, default=3.0, help='front chamfer size in image pixels') args = parser.parse_args() data = json.load(open(args.pieces)) x0, y0, x1, y1 = data['bounds'] width = x1 - x0 height = y1 - y0 def lineMeet(a0, a1, b0, b1): da = a1 - a0 db = b1 - b0 den = da[0] * db[1] - da[1] * db[0] if abs(den) < 1e-9: return None t = ((b0[0] - a0[0]) * db[1] - (b0[1] - a0[1]) * db[0]) / den return a0 + da * t def offsetPolygon(verts, amount): # Positive amount grows a counter-clockwise polygon, negative shrinks it. n = len(verts) shifted = [] for i in range(n): a = verts[i] b = verts[(i + 1) % n] d = b - a d = d / np.hypot(*d) normal = np.array([d[1], -d[0]]) shifted.append((a + normal * amount, b + normal * amount)) result = [] for i in range(n): meet = lineMeet(*shifted[i - 1], *shifted[i]) result.append(meet if meet is not None else verts[i]) return np.array(result) def signedArea(verts): n = len(verts) return 0.5 * sum(verts[i][0] * verts[(i + 1) % n][1] - verts[(i + 1) % n][0] * verts[i][1] for i in range(n)) def isConvexCcw(verts): n = len(verts) for i in range(n): a = verts[i] b = verts[(i + 1) % n] c = verts[(i + 2) % n] if np.hypot(*(b - a)) < 1e-6: return False if (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]) <= 0: return False return True def insetForBevel(verts, bevel): # Shrink the chamfer until the inset outline is still a valid convex polygon (sharp tips need less). while bevel > 0.25: inner = offsetPolygon(verts, -bevel) if isConvexCcw(inner): return inner, bevel bevel = bevel * 0.5 return verts.copy(), 0.0 writer = ObjWriterT() def addVertex(p): # Image space to model space: X right, Y up (image y flipped), Z toward the viewer. x = (p[0] - (x0 + width / 2)) * args.scale y = (y1 - p[1]) * args.scale z = p[2] * args.scale return writer.vertex((x, y, z), ((p[0] - x0) / width, 1.0 - (p[1] - y0) / height)) bevelUsed = [] for index, piece in enumerate(data['pieces']): verts = np.array(piece['points'], dtype=float) # The offset code wants a positive signed area in image space (y down); flipping y for the model # reverses the winding, so the rings are emitted reversed to come out counter-clockwise from +Z. if signedArea(verts) < 0: verts = verts[::-1] inner, bevel = insetForBevel(verts, args.bevel) bevelUsed.append(bevel) verts = verts[::-1] inner = inner[::-1] n = len(verts) writer.object('tile%02d_%s' % (index + 1, piece['material'])) writer.material(piece['material']) back = [addVertex((p[0], p[1], 0.0)) for p in verts] rim = [addVertex((p[0], p[1], args.depth - bevel)) for p in verts] front = [addVertex((p[0], p[1], args.depth)) for p in inner] # Front cap, chamfer ring, side walls, back cap. writer.face(front, textured=True) for i in range(n): j = (i + 1) % n if bevel > 0: writer.face([rim[i], rim[j], front[j], front[i]], textured=True) writer.face([back[i], back[j], rim[j], rim[i]], textured=True) writer.face(back[::-1], textured=True) mtlName = os.path.splitext(os.path.basename(args.out))[0] + '.mtl' writer.write(args.out, mtlName, ['Singe dragon, extruded from the tiles of Dragon.jpeg by util/dragonObj.py', 'Units: %g per image pixel; %d tiles; Y up, front faces toward +Z, resting on Y = 0' % (args.scale, len(data['pieces']))]) writeMtl(os.path.join(os.path.dirname(args.out), mtlName), DRAGON_MATERIALS, 'Materials for %s' % os.path.basename(args.out)) print('%s: %d vertices, %d faces, %d normals; bevel %.2f..%.2f px' % (args.out, len(writer.vertices), writer.faceCount, len(writer.normals), min(bevelUsed), max(bevelUsed)))