#!/usr/bin/env python3 """ spl.py -- reader/evaluator for DPL3 ".SPL" camera-path splines. Faithful port of DPL3/EXAMPLES/SPLINE.C. A .SPL is plain text: N number of control points x y z ax ay az x N -- position + euler angles (degrees) The path is a CLOSED LOOP of cubic-Hermite segments with Catmull-Rom tangents (vel = (next - prev) / 2). Position and the three euler angles are each splined. We sample the loop densely and bake, per sample, a camera basis (eye / center / up) so downstream code just needs lookAt(). Camera looks down local -Z (proved by CAMERA.SPL starting at ay=180 and facing into the +Z scene). """ import math import sys import os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import scn # reuse the DPL row-vector rotation matrices def _solve_cubic(v0, v1, d0, d1): # v = a t^3 + b t^2 + c t + d ; endpoints v0,v1 and tangents d0,d1 a = d1 + d0 - 2.0 * v1 + 2.0 * v0 b = v1 - v0 - d0 - a return (a, b, d0, v0) # (c0=a, c1=b, c2=c, c3=d) matching eval order def _solve_rot_cubic(v0, v1, d0, d1): if d0 > 360.0: d0 -= 360.0 if d1 > 360.0: d1 -= 360.0 a = d1 + d0 - 2.0 * v1 + 2.0 * v0 b = v1 - v0 - d0 - a return (a, b, d0, v0) def _eval(c, t): return c[0]*t*t*t + c[1]*t*t + c[2]*t + c[3] def load_points(path): with open(path, "r", encoding="latin-1") as fp: toks = fp.read().split() n = int(toks[0]) vals = [float(x) for x in toks[1:]] pts = [] for i in range(n): base = i * 6 if base + 6 > len(vals): break pts.append({"pos": vals[base:base+3], "ang": vals[base+3:base+6]}) return pts def build_segments(pts): """Compute per-knot tangents (closed loop) and per-segment cubic coeffs.""" n = len(pts) for i in range(n): p, c, nx = pts[(i-1) % n], pts[i], pts[(i+1) % n] c["vel"] = [(nx["pos"][k] - p["pos"][k]) / 2.0 for k in range(3)] rot = [] for k in range(3): delta = nx["ang"][k] - c["ang"][k] while delta > 180: delta -= 180 while delta < -180: delta += 180 rot.append(delta / 2.0) while c["ang"][k] > 360.0: c["ang"][k] -= 360.0 c["rot"] = rot segs = [] for i in range(n): a, b = pts[i], pts[(i+1) % n] seg = {"pos": [], "rot": []} for k in range(3): seg["pos"].append(_solve_cubic(a["pos"][k], b["pos"][k], a["vel"][k], b["vel"][k])) seg["rot"].append(_solve_rot_cubic(a["ang"][k], b["ang"][k], a["rot"][k], b["rot"][k])) segs.append(seg) return segs def _basis(pos, ang): """eye/center/up from position + euler (ax,ay,az) via DPL rotation order.""" R = scn.matmul(scn.matmul(scn.m_rotZ(ang[2]), scn.m_rotX(ang[0])), scn.m_rotY(ang[1])) fwd = scn.xform_dir(R, 0.0, 0.0, -1.0) # camera looks down local -Z up = scn.xform_dir(R, 0.0, 1.0, 0.0) return {"eye": list(pos), "center": [pos[i] + fwd[i] for i in range(3)], "up": list(up)} def sample_flythrough(path, per_seg=12): """Return a list of camera frames {eye,center,up} around the closed loop.""" pts = load_points(path) segs = build_segments(pts) frames = [] for seg in segs: for s in range(per_seg): t = s / float(per_seg) pos = [_eval(seg["pos"][k], t) for k in range(3)] ang = [_eval(seg["rot"][k], t) for k in range(3)] frames.append(_basis(pos, ang)) return frames if __name__ == "__main__": pts = load_points(sys.argv[1]) frames = sample_flythrough(sys.argv[1]) print("%d control points -> %d camera frames" % (len(pts), len(frames))) xs = [f["eye"][0] for f in frames]; ys = [f["eye"][1] for f in frames]; zs = [f["eye"][2] for f in frames] print("eye path X[%.0f,%.0f] Y[%.0f,%.0f] Z[%.0f,%.0f]" % (min(xs), max(xs), min(ys), max(ys), min(zs), max(zs))) print("frame0:", {k: [round(x, 1) for x in v] for k, v in frames[0].items()})