singe/util/dragonObj.py

122 lines
4.7 KiB
Python

# 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)))