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