diff --git a/cuburn/genome/blend.py b/cuburn/genome/blend.py index 6abd1d8..f9f4241 100644 --- a/cuburn/genome/blend.py +++ b/cuburn/genome/blend.py @@ -9,14 +9,73 @@ from itertools import izip_longest from scipy.ndimage.filters import gaussian_filter1d import spectypes -import spec -import util -from util import get +import specs +from use import Wrapper +from util import get, json_encode, resolve_spec import variations -# TODO: move to better place before checkin -default_blend_opts = {'nloops': 2, 'duration': 2, 'xform_sort': 'weightflip'} +def node_to_anim(node, half): + if half: + osrc, odst = -0.25, 0.25 + else: + osrc, odst = 0, 1 + src = apply_temporal_offset(node, osrc) + dst = apply_temporal_offset(node, odst) + edge = dict(blend=dict(duration=odst-osrc, xform_sort='natural')) + return blend(src, dst, edge) +def edge_to_anim(gdb, edge): + edge = resolve(gdb, edge) + src, osrc = _split_ref_id(edge.link.src) + dst, odst = _split_ref_id(edge.link.dst) + src = apply_temporal_offset(resolve(gdb, src), osrc) + dst = apply_temporal_offset(resolve(gdb, dst), odst) + return blend(src, dst, edit) + +def resolve(gdb, item): + """ + Given an item, recursively retrieve its base items, then merge according + to type. Returns the merged dict. + """ + is_edge = (item['type'] == 'edge') + spec = specs.toplevels[item['type']] + def go(i): + if i.get('base') is not None: + return go(gdb.get(i['base'])) + [i] + return [i] + items = map(flatten, go(item)) + out = {} + + for k in set(ik for i in items for ik in i): + sp = _resolve_spec(spec, k) + vs = [i.get(k) for i in items if k in i] + # TODO: dict and list negation; early-stage removal of negated knots? + if is_edge and isinstance(sp, (Spline, List)): + r = sum(vs, []) + else: + r = vs[-1] + out[k] = r + return unflatten(out) + +def _split_ref_id(s): + sp = s.split('@') + if len(sp) == 1: + return sp, 0 + return sp[0], float(sp[1]) + +def apply_temporal_offset(node, offset=0): + """ + Given a ``node`` dict, return a node with all periodic splines rotated by + ``offset * velocity``, with the same velocity. + """ + class TemporalOffsetWrapper(Wrapper): + def wrap_spline(self, path, spec, val): + if spec.period is not None and isinstance(val, list) and val[1]: + position, velocity = val + return [position + offset * velocity, velocity] + return val + wr = TemporalOffsetWrapper(node) + return wr.visit(wr) def blend(src, dst, edit={}): """ @@ -26,19 +85,22 @@ def blend(src, dst, edit={}): animation. These should be plain node dicts (hierarchical, pre-merged, and adjusted for loop temporal offset). - ``edit`` is an optional edit dict, also hierarchical and pre-merged. + ``edge`` is an edge dict, also hierarchical and pre-merged. (It can be + empty, in violation of the spec, to support rendering straight from nodes + without having to insert anything into the genome database.) Returns the animation spec as a plain dict. """ # By design, the blend element will contain only scalar values (no # splines or hierarchy), so this can be done blindly - opts = dict(default_blend_opts) + opts = {} for d in src, dst, edit: opts.update(d.get('blend', {})) + opts = Wrapper(opts, specs.blend) - blended = merge_nodes(specs.node, src, dst, edit, opts['nloops']) - name_map = sort_xforms(src['xforms'], dst['xforms'], opts['xform_sort'], - explicit=zip(*opts.get('xform_map', []))) + blended = merge_nodes(specs.node, src, dst, edit, opts.nloops) + name_map = sort_xforms(src['xforms'], dst['xforms'], opts.xform_sort, + explicit=zip(*opts.xform_map)) blended['xforms'] = {} for (sxf_key, dxf_key) in name_map: @@ -49,7 +111,7 @@ def blend(src, dst, edit={}): blended['xforms'][bxf_key] = blend_xform( src['xforms'].get(sxf_key), dst['xforms'].get(dxf_key), - xf_edits, opts['nloops']) + xf_edits, opts.nloops) if 'final_xform' in src or 'final_xform' in dst: blended['final_xform'] = blend_xform(src.get('final_xform'), @@ -58,7 +120,7 @@ def blend(src, dst, edit={}): # TODO: write 'info' section # TODO: palflip blended['type'] = 'animation' - blended.setdefault('time', {})['duration'] = opts['duration'] + blended.setdefault('time', {})['duration'] = opts.duration return blended def merge_edits(sv, av, bv): @@ -75,16 +137,16 @@ def merge_edits(sv, av, bv): else: return bv if bv is not None else av -def tospline(spl, src, dst, edit, loops): - def split_node_val(val): - if val is None: - return spl.default, 0 - if isinstance(val, (int, float)): - return val, 0 - return val +def split_node_val(spl, val): + if val is None: + return spl.default, 0 + if isinstance(val, (int, float)): + return val, 0 + return val - sp, sv = split_node_val(src) # position, velocity - dp, dv = split_node_val(dst) +def tospline(spl, src, dst, edit, loops): + sp, sv = split_node_val(spl, src) # position, velocity + dp, dv = split_node_val(spl, dst) # For variation parameters, copy missing values instead of using defaults if spl.var: @@ -285,4 +347,4 @@ def palflip(gnm): if __name__ == "__main__": import sys, json a, b, c = [json.load(open(f+'.json')) for f in 'abc'] - print util.json_encode(blend(a, b, c)) + print json_encode(blend(a, b, c)) diff --git a/cuburn/genome/convert.py b/cuburn/genome/convert.py index ecde73f..b66ddd8 100644 --- a/cuburn/genome/convert.py +++ b/cuburn/genome/convert.py @@ -8,6 +8,9 @@ import numpy as np from variations import var_params import util +# Re-exported +from blend import node_to_anim, edge_to_anim + class XMLGenomeParser(object): """ Parse an XML genome into a list of dictionaries. @@ -163,8 +166,10 @@ def apply_structure(struct, src): out[l[0]] = v return out -def convert_flame(flame): - return util.unflatten(util.flatten(apply_structure(flame_structure, flame))) +def flam3_to_node(flame): + n = util.unflatten(util.flatten(apply_structure(flame_structure, flame))) + n['type'] = 'node' + return n def convert_file(path): """Quick one-shot conversion for an XML genome.""" @@ -173,7 +178,7 @@ def convert_file(path): warnings.warn("Lot of flames in this file. Sure it's not a " "frame-based animation?") for flame in flames: - yield convert_flame(flame) + yield flam3_to_node(flame) if __name__ == "__main__": import sys diff --git a/cuburn/genome/specs.py b/cuburn/genome/specs.py index fb136e8..9f420c4 100644 --- a/cuburn/genome/specs.py +++ b/cuburn/genome/specs.py @@ -68,6 +68,13 @@ time = ( , 'frame_width': scalespline(d='Scale of profile temporal width per frame.') }) +blend = ( + { 'nloops': scalar(2) + , 'duration': scalar(2) + , 'xform_sort': enum('weightflip weight natural color') + , 'xform_map': list_(list_(String('xfid'), d='A pair of src, dst IDs')) + }) + base = ( { 'name': String("Human-readable name of this work") , 'camera': camera diff --git a/cuburn/genome/use.py b/cuburn/genome/use.py index bc45a5c..b5ebd46 100644 --- a/cuburn/genome/use.py +++ b/cuburn/genome/use.py @@ -62,6 +62,19 @@ class Wrapper(object): return self.spec.type return self.spec[name] + @classmethod + def visit(cls, obj): + """ + Visit every node. Note that for simplicity, this function will be + called on all elements (i.e. pivoting to a new Wrapper type inside the + wrapping function and overriding visit() won't do anything). + """ + if isinstance(obj, (Wrapper, dict)): + return dict((k, cls.visit(obj[k])) for k in obj) + elif isinstance(obj, list): + return [cls.visit(o) for o in obj] + return obj + def __getattr__(self, name): return self.wrap(name, self.get_spec(name), self._val.get(name)) @@ -155,6 +168,7 @@ class SplineEval(object): return times, vals, t, scale def __call__(self, itime, deriv=0): + # TODO: respect 'interp' THIS IS IMPORTANT. times, vals, t, scale = self.find_knots(itime) m1 = (vals[2] - vals[0]) / (1.0 - times[0]) diff --git a/cuburn/genome/util.py b/cuburn/genome/util.py index 32e0ddd..51c193c 100644 --- a/cuburn/genome/util.py +++ b/cuburn/genome/util.py @@ -2,6 +2,7 @@ import base64 import numpy as np from cuburn.code.util import crep +import spectypes def get(dct, default, *keys): if len(keys) == 1: @@ -49,6 +50,13 @@ def unflatten(kvlist): go(out, k.split('.'), v) return out +def resolve_spec(sp, path): + for name in path: + if isinstance(sp, spectypes.Map): + sp = sp.type + else: + sp = sp[name] + return sp def palette_decode(datastrs): """