122 lines
4.7 KiB
Python
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)))
|