File indexing completed on 2025-01-19 03:59:55

0001 import os
0002 import sys
0003 import math
0004 from dataclasses import dataclass
0005 
0006 sys.path.insert(0, os.path.dirname(__file__))
0007 import lottie
0008 from lottie import NVector
0009 
0010 import bpy
0011 import bpy_extras
0012 import mathutils
0013 
0014 
0015 @dataclass
0016 class RenderOptions:
0017     scene: bpy.types.Scene
0018     line_width: float = 0
0019     camera_angles: NVector = NVector(0, 0, 0)
0020 
0021     @property
0022     def camera(self):
0023         return scene.camera
0024 
0025     def vector_to_camera_norm(self, vector):
0026         return NVector(*bpy_extras.object_utils.world_to_camera_view(
0027             self.scene,
0028             self.scene.camera,
0029             vector
0030         ))
0031 
0032     def vpix3d(self, vector):
0033         v3d = self.vector_to_camera_norm(vector)
0034         v3d.x *= self.scene.render.resolution_x
0035         v3d.y *= -self.scene.render.resolution_y
0036         return v3d
0037 
0038     def vpix(self, vector):
0039         v2d = self.vpix3d(vector)
0040         v2d.components.pop()
0041         return v2d
0042 
0043     def vpix3d_r(self, obj, vector):
0044         return (
0045             self.vpix3d(obj.matrix_world @ vector)
0046         )
0047 
0048     def vpix_r(self, obj, vector):
0049         v2d = self.vpix3d_r(obj, vector)
0050         v2d.components.pop()
0051         return v2d
0052 
0053 
0054 class AnimatedProperty:
0055     def __init__(self, wrapper, name):
0056         self.wrapper = wrapper
0057         self.name = name
0058 
0059     @property
0060     def is_animated(self):
0061         return self.name in self.wrapper.animation
0062 
0063     @property
0064     def value(self):
0065         return self.wrapper.object.path_resolve(self.name)
0066 
0067     @property
0068     def keyframes(self):
0069         return self.wrapper.animation[self.name]
0070 
0071     def to_lottie_prop(self, value_transform=lambda x: x, animatable=None):
0072         v = self.value
0073         if isinstance(v, mathutils.Vector):
0074             def_animatable = lottie.objects.MultiDimensional
0075             kf_getter = AnimationKeyframe.to_vector
0076         else:
0077             def_animatable = lottie.objects.Value
0078             kf_getter = AnimationKeyframe.to_scalar
0079 
0080         if animatable is None:
0081             animatable = def_animatable
0082 
0083         md = animatable()
0084         if self.is_animated:
0085             for keyframe in self.keyframes:
0086                 md.add_keyframe(
0087                     keyframe.time,
0088                     value_transform(kf_getter(keyframe)),
0089                     keyframe.easing()
0090                 )
0091         else:
0092             md.value = value_transform(v)
0093         return md
0094 
0095 
0096 class AnimationKeyframe:
0097     def __init__(self, fc, kf):
0098         self.time = kf.co.x
0099         self.value = {
0100             fc.array_index: kf.co.y
0101         }
0102 
0103     def __setitem__(self, key, value):
0104         self.value[key] = value
0105 
0106     def to_vector(self):
0107         return NVector(*(v for k, v in sorted(self.value.items())))
0108 
0109     def to_scalar(self):
0110         return next(iter(self.value.values()))
0111 
0112     # TODO pull easing
0113     def easing(self):
0114         return lottie.objects.easing.Linear()
0115 
0116 
0117 class AnimationWrapper:
0118     def __init__(self, object):
0119         self.object = object
0120         self.animation = {}
0121         if object.animation_data and object.animation_data.action:
0122             for fc in object.animation_data.action.fcurves:
0123                 if fc.data_path not in self.animation:
0124                     self.animation[fc.data_path] = [
0125                         AnimationKeyframe(fc, kf)
0126                         for kf in fc.keyframe_points
0127                     ]
0128                 else:
0129                     for internal, kf in zip(self.animation[fc.data_path], fc.keyframe_points):
0130                         internal[fc.array_index] = kf.co.y
0131 
0132     def __getattr__(self, name):
0133         return self.property(name)
0134 
0135     def property(self, name):
0136         return AnimatedProperty(self, name)
0137 
0138     def keyframe_times(self):
0139         kft = set()
0140         for kfl in self.animation.values():
0141             kft |= set(kf.time for kf in kfl)
0142         return kft
0143 
0144 
0145 def context_to_tgs(context):
0146     scene = context.scene
0147     root = context.view_layer.layer_collection
0148     initial_frame = scene.frame_current
0149     try:
0150         animation = lottie.objects.Animation()
0151         animation.in_point = scene.frame_start
0152         animation.out_point = scene.frame_end
0153         animation.frame_rate = scene.render.fps
0154         animation.width = scene.render.resolution_x
0155         animation.height = scene.render.resolution_y
0156         animation.name = scene.name
0157         layer = animation.add_layer(lottie.objects.ShapeLayer())
0158 
0159         ro = RenderOptions(scene)
0160         if scene.render.use_freestyle:
0161             ro.line_width = scene.render.line_thickness
0162         else:
0163             ro.line_width = 0
0164         ro.camera_angles = NVector(*scene.camera.rotation_euler) * 180 / math.pi
0165 
0166         collection_to_group(root, layer, ro)
0167         adjust_animation(scene, animation, ro)
0168 
0169         return animation
0170     finally:
0171         scene.frame_set(initial_frame)
0172 
0173 
0174 def adjust_animation(scene, animation, ro):
0175     layer = animation.layers[0]
0176     layer.transform.position.value.y += animation.height
0177     layer.shapes = list(sorted(layer.shapes, key=lambda x: x._z))
0178 
0179 
0180 def collection_to_group(collection, parent, ro: RenderOptions):
0181     if collection.exclude or collection.collection.hide_render:
0182         return
0183 
0184     for obj in collection.children:
0185         collection_to_group(obj, parent, ro)
0186 
0187     for obj in collection.collection.objects:
0188         object_to_shape(obj, parent, ro)
0189 
0190 
0191 def curve_to_shape(obj, parent, ro: RenderOptions):
0192     g = parent.add_shape(lottie.objects.Group())
0193     g.name = obj.name
0194     beziers = []
0195 
0196     animated = AnimationWrapper(obj)
0197     for spline in obj.data.splines:
0198         sh = lottie.objects.Path()
0199         g.add_shape(sh)
0200         sh.shape.value = curve_get_bezier(spline, obj, ro)
0201         beziers.append(sh.shape.value)
0202 
0203     times = animated.keyframe_times()
0204     shapekeys = None
0205     if obj.data.shape_keys:
0206         shapekeys = AnimationWrapper(obj.data.shape_keys)
0207         times |= shapekeys.keyframe_times()
0208 
0209     times = list(sorted(times))
0210     for time in times:
0211         ro.scene.frame_set(time)
0212         if not shapekeys:
0213             for spline, sh in zip(obj.data.splines, g.shapes):
0214                 sh.shape.add_keyframe(time, curve_get_bezier(spline, obj, ro))
0215         else:
0216             obj.shape_key_add(from_mix=True)
0217             shape_key = obj.data.shape_keys.key_blocks[-1]
0218             start = 0
0219             for spline, sh, bezier in zip(obj.data.splines, g.shapes, beziers):
0220                 end = start + len(bezier.vertices)
0221                 bez = lottie.objects.Bezier()
0222                 bez.closed = bezier.closed
0223                 for i in range(start, end):
0224                     add_point_to_bezier(bez, shape_key.data[i], ro, obj)
0225                 sh.shape.add_keyframe(time, bez)
0226                 start = end
0227             obj.shape_key_remove(shape_key)
0228 
0229     curve_apply_material(obj, g, ro)
0230     return g
0231 
0232 
0233 def get_fill(obj, ro):
0234     # TODO animation
0235     fillc = obj.active_material.diffuse_color
0236     fill = lottie.objects.Fill(NVector(*fillc[:-1]))
0237     fill.name = obj.active_material.name
0238     fill.opacity.value = fillc[-1] * 100
0239     return fill
0240 
0241 
0242 def curve_apply_material(obj, g, ro):
0243     if obj.data.fill_mode != "NONE":
0244         g.add_shape(get_fill(obj, ro))
0245 
0246     if ro.line_width > 0:
0247         # TODO animation
0248         strokec = obj.active_material.line_color
0249         stroke = lottie.objects.Stroke(NVector(*strokec[:-1]), ro.line_width)
0250         stroke.opacity.value = fillc[-1] * 100
0251         g.add_shape(stroke)
0252 
0253 
0254 def curve_get_bezier(spline, obj, ro):
0255     bez = lottie.objects.Bezier()
0256     bez.closed = spline.use_cyclic_u
0257     if spline.type == "BEZIER":
0258         for point in spline.bezier_points:
0259             add_point_to_bezier(bez, point, ro, obj)
0260     else:
0261         for point in spline.points:
0262             add_point_to_poly(bez, point, ro, obj)
0263     return bez
0264 
0265 
0266 def add_point_to_bezier(bez, point, ro: RenderOptions, obj):
0267     vert = ro.vpix_r(obj, point.co)
0268     in_t = ro.vpix_r(obj, point.handle_left) - vert
0269     out_t = ro.vpix_r(obj, point.handle_right) - vert
0270     bez.add_point(vert, in_t, out_t)
0271 
0272 
0273 def add_point_to_poly(bez, point, ro, obj):
0274     bez.add_point(ro.vpix_r(obj, point.co))
0275 
0276 
0277 def mesh_to_shape(obj, parent, ro):
0278     # TODO concave hull to optimize
0279     g = parent.add_shape(lottie.objects.Group())
0280     g.name = obj.name
0281     verts = list(ro.vpix_r(obj, v.co) for v in obj.data.vertices)
0282     fill = get_fill(obj, ro)
0283     animated = AnimationWrapper(obj)
0284     times = list(sorted(animated.keyframe_times()))
0285 
0286     def f_bez(f):
0287         bez = lottie.objects.Bezier()
0288         bez.close()
0289         for v in f.vertices:
0290             bez.add_point(verts[v])
0291         return bez
0292 
0293     for f in obj.data.polygons:
0294         shp = g.add_shape(lottie.objects.Group())
0295         sh = shp.add_shape(lottie.objects.Path())
0296         shp.add_shape(fill)
0297         sh.shape.value = f_bez(f)
0298 
0299     if times:
0300         for time in times:
0301             ro.scene.frame_set(time)
0302             verts = list(ro.vpix_r(obj, v.co) for v in obj.data.vertices)
0303 
0304             for f, shp in zip(obj.data.polygons, g.shapes):
0305                 sh = shp.shapes[0]
0306                 sh.shape.add_keyframe(time, f_bez(f))
0307 
0308     return g
0309 
0310 
0311 def gpencil_to_shape(obj, parent, ro):
0312     # Object / GreasePencil
0313     gpen = parent.add_shape(lottie.objects.Group())
0314     gpen.name = obj.name
0315 
0316     animated = AnimationWrapper(obj.data)
0317     # GPencilLayer
0318     for layer in reversed(obj.data.layers):
0319         if layer.hide:
0320             continue
0321         glay = gpen.add_shape(lottie.objects.Group())
0322         glay.name = layer.info
0323         opacity = animated.property('layers["%s"].opacity' % layer.info)
0324         glay.transform.opacity = opacity.to_lottie_prop(lambda x: x*100)
0325 
0326         gframe = None
0327         # GPencilFrame
0328         for frame in layer.frames:
0329             if gframe:
0330                 if not gframe.transform.opacity.animated:
0331                     gframe.transform.opacity.add_keyframe(0, 100, lottie.objects.easing.Jump())
0332                 gframe.transform.opacity.add_keyframe(frame.frame_number, 0)
0333 
0334             gframe = glay.add_shape(lottie.objects.Group())
0335             gframe.name = "frame %s" % frame.frame_number
0336             if frame.frame_number != 0:
0337                 gframe.transform.opacity.add_keyframe(0, 0, lottie.objects.easing.Jump())
0338                 gframe.transform.opacity.add_keyframe(frame.frame_number, 100)
0339 
0340             # GPencilStroke
0341             for stroke in reversed(frame.strokes):
0342                 gstroke = gframe.add_shape(lottie.objects.Group())
0343 
0344                 path = gstroke.add_shape(lottie.objects.Path())
0345                 path.shape.value.closed = stroke.draw_cyclic
0346                 pressure = 0
0347                 for p in stroke.points:
0348                     add_point_to_poly(path.shape.value, p, ro, obj)
0349                     pressure += p.pressure
0350                 pressure /= len(stroke.points)
0351 
0352                 # Material
0353                 matp = obj.data.materials[stroke.material_index]
0354                 # TODO Gradients / animations
0355                 # MaterialGPencilStyle
0356                 material = matp.grease_pencil
0357 
0358                 if material.show_fill:
0359                     fill_sh = gstroke.add_shape(lottie.objects.Fill())
0360                     fill_sh.name = matp.name
0361                     fill_sh.color.value = NVector(*material.fill_color[:-1])
0362                     fill_sh.opacity.value = material.fill_color[-1] * 100
0363 
0364                 if material.show_stroke:
0365                     stroke_sh = lottie.objects.Stroke()
0366                     gstroke.add_shape(stroke_sh)
0367                     stroke_sh.name = matp.name
0368                     if stroke.end_cap_mode == "ROUND":
0369                         stroke_sh.line_cap = lottie.objects.LineCap.Round
0370                     elif stroke.end_cap_mode == "FLAT":
0371                         stroke_sh.line_cap = lottie.objects.LineCap.Butt
0372                     stroke_w = stroke.line_width * pressure * obj.data.pixel_factor
0373                     if obj.data.stroke_thickness_space == "WORLDSPACE":
0374                         # TODO do this properly
0375                         stroke_w /= 9
0376                     stroke_sh.width.value = stroke_w
0377                     stroke_sh.color.value = NVector(*material.color[:-1])
0378                     stroke_sh.opacity.value = material.color[-1] * 100
0379 
0380 
0381 
0382     return gpen
0383 
0384 
0385 def object_to_shape(obj, parent, ro: RenderOptions):
0386     if obj.hide_render:
0387         return
0388 
0389     g = None
0390     ro.scene.frame_set(0)
0391 
0392     if obj.type == "CURVE":
0393         g = curve_to_shape(obj, parent, ro)
0394     elif obj.type == "MESH":
0395         g = mesh_to_shape(obj, parent, ro)
0396     elif obj.type == "GPENCIL":
0397         g = gpencil_to_shape(obj, parent, ro)
0398 
0399     if g:
0400         ro.scene.frame_set(0)
0401         g._z = ro.vpix3d(obj.location).z