# # This Blender add-on creates writing animation for the selected Bezier curves # Supported Blender Version: 2.79b # # License: GPL (https://github.com/Shriinivas/writinganimation/blob/master/LICENSE) # # Not yet pep8 compliant import bpy import bmesh from bpy.props import IntProperty, FloatProperty, BoolProperty from bpy.props import EnumProperty, PointerProperty from mathutils import Vector, Euler, Quaternion from math import radians, floor, ceil from enum import Enum DEF_ERR_MARGIN = 0.0001 DEFAULT_DEPTH = 0.001 OBJTYPE_MODIFIER = 'MODIFIER' OBJTYPE_NONMODIFIER = 'NONMODIFIER' NEW_DATA_PREFIX = 'WritingAnim_' def floatCmpWithMargin(float1, float2, margin = DEF_ERR_MARGIN): return abs(float1 - float2) < margin def vectCmpWithMargin(vect1, vect2, margin = DEF_ERR_MARGIN): return all(abs(vect2[i] - vect1[i]) < margin for i in range(0, len(vect1))) def isBezier(bObj): return bObj.type == 'CURVE' and len(bObj.data.splines) > 0 \ and bObj.data.splines[0].type == 'BEZIER' class DrawableCurve: def __init__(self, curveObj, objType = OBJTYPE_NONMODIFIER): self.bCurveObj = curveObj self.scale = curveObj.scale[:] curveCopyData = curveObj.data.copy() #Non-zero values of the following attributes impacts length curveCopyData.bevel_depth = 0 curveCopyData.extrude = 0 curveCopyData.offset = 0 curveCopyObj = curveObj.copy() curveCopyObj.data = curveCopyData apply_modifiers = (objType == OBJTYPE_MODIFIER) self.curveMeshData = curveCopyObj.to_mesh(bpy.context.scene, \ apply_modifiers = apply_modifiers, settings = 'PREVIEW') #Convert co to world space and calculate length and approximate normal tmpBM = bmesh.new() self.curveLength = 0 self.mw = self.bCurveObj.matrix_world for i in range(0, len(self.curveMeshData.vertices)): self.curveMeshData.vertices[i].co = \ self.mw * self.curveMeshData.vertices[i].co if(i > 0): segLen = (self.curveMeshData.vertices[i].co - \ self.curveMeshData.vertices[i-1].co).length self.curveLength += segLen tmpBM.verts.new(self.curveMeshData.vertices[i].co) self.startCo = self.curveMeshData.vertices[0].co self.endCo = self.curveMeshData.vertices[-1].co tmpFace = tmpBM.faces.new(tmpBM.verts) tmpBM.faces.ensure_lookup_table() #Normal of co-linear verts is not zero every time, so check area if(floatCmpWithMargin(tmpFace.calc_area(), 0)): self.curveNormal = Vector((0,0,0)) else: tmpFace.normal_update() self.curveNormal = tmpFace.normal.copy() tmpBM.free() #TODO: copying the object also copies the old bbox; #find a way to force recalculation # ~ worldBBox = [] # ~ for val in self.bCurveObj.bound_box: # ~ worldBBox.append(mw * Vector((val[0], val[1], val[2]))) #leftBot_rgtTop # ~ self.bbox = [min(b[0] for b in worldBBox), # ~ min(b[1] for b in worldBBox), # ~ max(b[0] for b in worldBBox), # ~ max(b[1] for b in worldBBox)] def copySrcObjProps(copyObjData, newCurveData): #Copying just a few attributes newCurveData.dimensions = copyObjData.dimensions newCurveData.resolution_u = copyObjData.resolution_u newCurveData.render_resolution_u = copyObjData.render_resolution_u newCurveData.fill_mode = copyObjData.fill_mode newCurveData.use_fill_deform = copyObjData.use_fill_deform newCurveData.use_radius = copyObjData.use_radius newCurveData.use_stretch = copyObjData.use_stretch newCurveData.use_deform_bounds = copyObjData.use_deform_bounds newCurveData.twist_smooth = copyObjData.twist_smooth newCurveData.twist_mode = copyObjData.twist_mode newCurveData.offset = copyObjData.offset newCurveData.extrude = copyObjData.extrude newCurveData.bevel_depth = copyObjData.bevel_depth newCurveData.bevel_resolution = copyObjData.bevel_resolution for material in copyObjData.materials: newCurveData.materials.append(material) #static method def copyBezierPt(src, target): target.co = src.co target.handle_left = src.handle_left target.handle_left_type = 'FREE' target.handle_right = src.handle_right target.handle_right_type = 'FREE' #static method def createNoncyclicSpline(curveData, srcSpline, forceNoncyclic): spline = curveData.splines.new('BEZIER') spline.bezier_points.add(len(srcSpline.bezier_points)-1) if(forceNoncyclic): spline.use_cyclic_u = False else: spline.use_cyclic_u = srcSpline.use_cyclic_u for i in range(0, len(srcSpline.bezier_points)): DrawableCurve.copyBezierPt(srcSpline.bezier_points[i], spline.bezier_points[i]) if(forceNoncyclic == True and srcSpline.use_cyclic_u == True): spline.bezier_points.add(1) DrawableCurve.copyBezierPt(srcSpline.bezier_points[0], spline.bezier_points[-1]) #static method def getDCObjsForSpline(curveObj, objType, defaultDepth, nameStartIdx, group = None, copyPropObj = None): #Nurbs curve excuded for now if(not isBezier(curveObj)): return [] activeIdx = None #Needed, because active_index is object (not data) attribute copyData = bpy.data.curves.new(NEW_DATA_PREFIX+'tmp', 'CURVE') if(copyPropObj != None): #If object is bezier curve copy curve properties and material if(isBezier(copyPropObj)): DrawableCurve.copySrcObjProps(copyPropObj.data, copyData) #If not a curve copy only material else: DrawableCurve.copySrcObjProps(curveObj.data, copyData) if(len(copyPropObj.data.materials) > 0): copyMatIdx = copyPropObj.active_material_index mat = copyPropObj.data.materials[copyMatIdx] if(len(copyData.materials) == 0 or mat.name not in copyData.materials): copyData.materials.append(mat) activeIdx = -1 #Last else: activeIdx = copyData.materials.find(mat.name) else: DrawableCurve.copySrcObjProps(curveObj.data, copyData) dcObjs = [] idSuffix = 0 for i, spline in enumerate(curveObj.data.splines): dataCopy = copyData.copy() dataCopy.splines.clear() DrawableCurve.createNoncyclicSpline(dataCopy, spline, forceNoncyclic = True) dataCopy.animation_data_clear() #Default settings dataCopy.bevel_factor_mapping_end = 'SPLINE' if(dataCopy.bevel_depth == 0): dataCopy.bevel_depth = defaultDepth dataCopy.offset = -defaultDepth / 2 objCopy = curveObj.copy() objCopy.name = curveObj.name + str(idSuffix).zfill(2) objCopy.data = dataCopy if(dataCopy.shape_keys != None): for i in range(0, len(dataCopy.shape_keys.key_blocks)): objCopy.shape_key_remove(dataCopy.shape_keys.key_blocks[0]) objCopy.animation_data_clear() bpy.context.scene.objects.link(objCopy) if(objType == OBJTYPE_MODIFIER): dcObj = ModifierDrawableCurve(objCopy) else: dcObj = DrawableCurve(objCopy) if(activeIdx != None): dcObj.active_material_index = activeIdx dcObjs.append(dcObj) if(group != None): group.objects.link(dcObj.bCurveObj) bpy.data.curves.remove(copyData) return dcObjs class ModifierDrawableCurve(DrawableCurve): def __init__(self, curveObj): DrawableCurve.__init__(self, curveObj, OBJTYPE_MODIFIER) #Given the count, return the intgerpolated coordinates of the equally spaced vertices #numPts is numSegs + 1 (first and last verts are included) def getInterpolatedVertsCo(self, numPts): totalLength = self.curveLength if(floatCmpWithMargin(totalLength, 0)): return [self.curveMeshData.vertices[0].co] * numPts segLen = totalLength / (numPts-1) vertCos = [self.curveMeshData.vertices[0].co] actualLen = 0 vertIdx = 0 for i in range(1, numPts - 1): co = None targetLen = i * segLen while(not floatCmpWithMargin(actualLen, targetLen) and actualLen < targetLen): vert = self.curveMeshData.vertices[vertIdx] vertIdx += 1 nextVert = self.curveMeshData.vertices[vertIdx] actualLen += (nextVert.co - vert.co).length if(floatCmpWithMargin(actualLen, targetLen)): co = self.curveMeshData.vertices[vertIdx].co else: #interpolate diff = actualLen - targetLen co = (nextVert.co - (nextVert.co - vert.co) * \ (diff/(nextVert.co - vert.co).length)) #Revert to last pt vertIdx -= 1 actualLen -= (nextVert.co - vert.co).length vertCos.append(co) vertCos.append(self.curveMeshData.vertices[-1].co) return vertCos def insertKF(obj, dataPath, frame): obj.keyframe_insert(data_path = dataPath, frame = frame) CreateWritingAnimOp.keyframeCnt += 1 #Needed in follow path constraint based animation def setInterpolationLinear(empty): fcs = empty.animation_data.action.fcurves for fc in fcs: if(fc.data_path.endswith('offset') or fc.data_path.endswith('location')): for k in fc.keyframe_points: k.interpolation = 'LINEAR' def createEmptyWithInitKF(name, startFrame, initCo, initDirection, parentObjs, hide, group): empty = bpy.data.objects.new(name, None) empty.empty_draw_type = 'PLAIN_AXES' bpy.context.scene.objects.link(empty) bpy.context.scene.update() empty.hide = hide if(initDirection != None): empty.rotation_mode = 'QUATERNION' empty.rotation_quaternion = initDirection.to_track_quat('Z','Y') insertKF(obj = empty, dataPath = 'rotation_quaternion', frame = startFrame) if( len(parentObjs) > 0 ): for parentObj in parentObjs: const = empty.constraints.new(type='FOLLOW_PATH') const.target = parentObj const.name = parentObj.name const.forward_axis = 'FORWARD_Y' const.influence = 0 const.offset = 0 empty.location = initCo insertKF(obj = empty, dataPath = 'location', frame = startFrame) if(group != None): group.objects.link(empty) return empty def addCustomWriterKFs(customWriter, empty, startFrame, endFrame, resetLocation): if(resetLocation == True): insertKF(obj = customWriter, dataPath = 'location', frame = (startFrame - 1)) customWriter.location = (0,0,0) insertKF(obj = customWriter, dataPath = 'location', frame = startFrame) insertKF(obj = customWriter, dataPath = 'location', frame = endFrame) const = customWriter.constraints.new(type='CHILD_OF') const.target = empty const.name = NEW_DATA_PREFIX + 'Constraint' const.influence = 0 insertKF(obj = const, dataPath = 'influence', frame = (startFrame - 1)) const.influence = 1 insertKF(obj = const, dataPath = 'influence', frame = startFrame) insertKF(obj = const, dataPath = 'influence', frame = endFrame) const.influence = 0 insertKF(obj = const, dataPath = 'influence', frame = (endFrame + 1)) def createPencil(name, co, segs = 12, tipDepth = .1, height = 1, diameter = .033, endTipPerc = 15, endTipDia = 0.0033, sharpCutOffset = 0.01, tilt = 45, group = None): def createMaterialNode(obj, colorVal, matName): if(bpy.data.materials.get(matName)== None): mat = bpy.data.materials.new(matName) if(bpy.context.scene.render.engine == 'CYCLES'): mat.use_nodes = True defNode = mat.node_tree.nodes.get("Diffuse BSDF") defNode.inputs["Color"].default_value = colorVal elif(bpy.context.scene.render.engine == 'BLENDER_RENDER'): mat.diffuse_color = colorVal[:-1] else: mat = bpy.data.materials[matName] obj.data.materials.append(mat) return mat bm = bmesh.new() bmesh.ops.create_cone(bm, cap_ends = True, segments = segs, depth = tipDepth * (100-endTipPerc)/100, diameter1 = endTipDia, diameter2 = diameter) up = Vector((0, 0, 1)) bm.normal_update() topFace = [f for f in bm.faces if f.normal.angle(up) < radians(3)][0] oldFaces = [f for f in bm.faces] info = bmesh.ops.extrude_face_region(bm, geom=[topFace]) bmesh.ops.translate(bm, vec = Vector((0, 0, height)), verts=[v for v in info["geom"] if isinstance(v, bmesh.types.BMVert)]) for f in bm.faces: if (f not in oldFaces): f.material_index = 0 else: f.material_index = 1 oldFaces = [f for f in bm.faces] offsetVerts = [v for i, v in enumerate(topFace.verts) if i % 2 == 0] bmesh.ops.translate(bm, vec = (0, 0, sharpCutOffset), verts = offsetVerts) info = bmesh.ops.create_cone(bm, cap_ends = True, segments = segs * 2, depth = tipDepth * endTipPerc/100, diameter1 = 0, diameter2 = endTipDia) bmesh.ops.translate(bm, vec = (0, 0, -tipDepth * .5), verts = info['verts']) for f in bm.faces: if (f not in oldFaces): f.material_index = 2 mesh = bpy.data.meshes.new(name) pObj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(pObj) bpy.context.scene.update() bm.to_mesh(mesh) bodyColor = [0.00, 0.35, 0.44, 1.00] sharpenedTipColor = [0.80, 0.55, 0.09, 1.00] endTipColor = [0.00, 0.00, 0.00, 1.00] createMaterialNode(pObj, bodyColor, NEW_DATA_PREFIX+'BodyMat') createMaterialNode(pObj, sharpenedTipColor, NEW_DATA_PREFIX + 'Tip1Mat') createMaterialNode(pObj, endTipColor, NEW_DATA_PREFIX + 'Tip2Mat') pObj.data.uv_textures.new() for vert in mesh.vertices: vert.co.z += (tipDepth + tipDepth * endTipPerc/100) / 2 pObj.rotation_euler = Euler((radians(tilt), 0,0), 'XYZ') if(group != None): group.objects.link(pObj) return pObj def createKfs(empty, dcObj, startFrame, curveFrameCnt, alignToVert, objType): curveObj = dcObj.bCurveObj curveObj.data.bevel_factor_end = 0 insertKF(obj = curveObj.data, dataPath = 'bevel_factor_end', frame = startFrame) currFrame = startFrame if(objType == OBJTYPE_NONMODIFIER): oldRot = empty.rotation_euler.copy() if(curveFrameCnt > 1): if(alignToVert): empty.rotation_euler = dcObj.mw.to_euler() insertKF(obj = empty, dataPath = 'rotation_euler', frame = startFrame) insertKF(obj = empty, dataPath = 'location', frame = startFrame) if(not alignToVert): empty.rotation_euler = dcObj.mw.inverted().to_euler() else: empty.rotation_euler = empty.matrix_world.inverted().to_euler() insertKF(obj = empty, dataPath = 'rotation_euler', frame = (startFrame + 1)) empty.location = [0,0,0] insertKF(obj = empty, dataPath = 'location', frame = (startFrame + 1)) con = empty.constraints[curveObj.name] con.influence = 0 insertKF(obj = con, dataPath = 'influence', frame = startFrame) con.influence = 1 insertKF(obj = con, dataPath = 'influence', frame = (startFrame + 1)) con.offset = 0 insertKF(obj = empty, dataPath = 'constraints["'+curveObj.name+'"].offset', \ frame = startFrame) con.offset = -100 insertKF(obj = empty, dataPath = 'constraints["'+curveObj.name+'"].offset', \ frame = (startFrame + curveFrameCnt)) con.influence = 1 insertKF(obj = con, dataPath = 'influence', \ frame = (startFrame + curveFrameCnt - 1)) con.influence = 0 insertKF(obj = con, dataPath = 'influence', \ frame = (startFrame + curveFrameCnt)) insertKF(obj = empty, dataPath = 'location', \ frame = (startFrame + curveFrameCnt - 1)) insertKF(obj = empty, dataPath = 'rotation_euler', \ frame = (startFrame + curveFrameCnt - 1)) empty.location = dcObj.endCo insertKF(obj = empty, dataPath = 'location', frame = (startFrame + curveFrameCnt)) if(not alignToVert): empty.rotation_euler = oldRot else: empty.rotation_euler = dcObj.mw.to_euler() insertKF(obj = empty, dataPath = 'rotation_euler', \ frame = (startFrame + curveFrameCnt)) else: objCos = dcObj.getInterpolatedVertsCo(curveFrameCnt + 1) if(alignToVert): oldRot = empty.rotation_quaternion rotate = dcObj.curveNormal.rotation_difference(empty.matrix_world.to_euler()) empty.rotation_quaternion = rotate insertKF(obj = empty, dataPath = 'rotation_quaternion', frame = startFrame) if(curveFrameCnt > 1): insertKF(obj = empty, dataPath = 'rotation_quaternion', \ frame = (startFrame + curveFrameCnt - 1)) #Start from the second, since first is already covered by lift for i in range(1, len(objCos)): currFrame += 1 empty.location = objCos[i] insertKF(obj = empty, dataPath = 'location', frame = currFrame) curveObj.data.bevel_factor_end = 1 insertKF(obj = curveObj.data, dataPath = 'bevel_factor_end', \ frame = (startFrame + curveFrameCnt)) fc = curveObj.data.animation_data.action.fcurves.find('bevel_factor_end') for kfp in fc.keyframe_points: kfp.interpolation = 'LINEAR' def getLiftMidPtCo(objStart, objEnd, liftAxis, lift, reverseLift): startCo = objStart.endCo endCo = objEnd.startCo if(all(co==0 for co in objStart.curveNormal)): moveAxis = liftAxis else: moveAxis = [i for i in range(0, len(objStart.curveNormal)) \ if(abs(objStart.curveNormal[i]) == abs(max(objStart.curveNormal, key=abs)))][0] midPtCo = startCo + (endCo - startCo) / 2. midPtCo[moveAxis] = max(startCo[moveAxis], endCo[moveAxis], key=abs) dirn = 1 if(endCo[moveAxis] < startCo[moveAxis]): dirn = -1 if(reverseLift): dirn *= -1 midPtCo[moveAxis] += dirn * lift return midPtCo def addLiftKeyFrames(empty, objStart, objEnd, liftAxis, lift, currFrame, \ liftFrameCnt, reverseLift): empty.location = getLiftMidPtCo(objStart, objEnd, liftAxis, lift, reverseLift) insertKF(obj = empty, dataPath = 'location', \ frame = round(currFrame + liftFrameCnt / 2)) empty.location = objEnd.startCo insertKF(obj = empty, dataPath = 'location', frame = (currFrame + liftFrameCnt)) def getTransitionInfo(allDcObjs, liftAxis, maxLift, transitionSpeed, proportionalLift, reverseLift): transitionLengths = [] transitionLifts = [] if(len(allDcObjs) <= 1): return transitionLengths, transitionLifts flatTransitionLengths = [(allDcObjs[i].startCo - \ allDcObjs[i-1].endCo).length for i in range(1, len(allDcObjs))] maxFlatLength = max(flatTransitionLengths) for i, fl in enumerate(flatTransitionLengths): transitionLength = 0 lift = 0 startObj = allDcObjs[i] endObj = allDcObjs[i+1] if(maxFlatLength > 0): if(proportionalLift): lift = maxLift * (fl / maxFlatLength) else: lift = maxLift midPtCo = getLiftMidPtCo(startObj, endObj, liftAxis, lift, reverseLift) transitionLength = ((midPtCo - startObj.endCo).length + \ (endObj.startCo - midPtCo).length) / transitionSpeed transitionLengths.append(transitionLength) transitionLifts.append(lift) return transitionLengths, transitionLifts def getCurveDCObjs(selObjs, objType, defaultDepth, retain, copyPropObj, group = None): curveDCObjs = [] idx = 0 #Only for naming the new objects for obj in selObjs: dcObjs = DrawableCurve.getDCObjsForSpline(obj, objType, defaultDepth, idx, group, copyPropObj) if(len(dcObjs) == 0 ): continue idx += len(dcObjs) if(retain == 'Copy'): obj.hide = True obj.hide_render = True curveDCObjs.append(dcObjs) return curveDCObjs def showOrigCurve(zeroFrameCurveDCObjs, zeroFrameOrigCurves, currFrame, retain): for i, origCurve in enumerate(zeroFrameOrigCurves): origCurve.scale = [0,0,0] insertKF(obj = origCurve, dataPath = 'scale', frame = (currFrame-1)) origCurve.scale = zeroFrameCurveDCObjs[i][0].scale[:] insertKF(obj = origCurve, dataPath = 'scale', frame = (currFrame)) if(retain != 'Both'): for dcObj in zeroFrameCurveDCObjs[i]: dcObj.bCurveObj.scale = dcObj.scale[:] insertKF(obj = dcObj.bCurveObj, dataPath = 'scale', frame = (currFrame-1)) dcObj.bCurveObj.scale = [0,0,0] insertKF(obj = dcObj.bCurveObj, dataPath = 'scale', frame = (currFrame)) #TODO: Find a better way to calculate frame count proportional to the length def getFrameCntForLength(totalFrames, totalLength, remainingLength, remainingFrames, elemLength): cnt1 = remainingFrames * elemLength / remainingLength return floor(cnt1) def main(retain, defaultDepth, startFrame, totalFrames, liftAxis, maxLift, transitionSpeed, alignToVert, proportionalLift, objType, copyPropObj, customWriter, reverseLift, resetLocation): selObjs = [o for o in bpy.data.objects if o in bpy.context.selected_objects and o != customWriter and isBezier(o)] # ~ selObjs = bpy.context.selected_objects[:] #sequence incorrect group = bpy.data.groups.new(NEW_DATA_PREFIX+'Group') curveDCObjs = getCurveDCObjs(selObjs, objType, defaultDepth, retain, copyPropObj, group) currFrame = -1 if(len(curveDCObjs) == 0): return currFrame initCo = curveDCObjs[0][0].startCo initTangent = None if(alignToVert and objType == OBJTYPE_MODIFIER): initTangent = curveDCObjs[0][0].curveNormal allDcObjs = [d for c in curveDCObjs for d in c] parentObjs = [] if(objType == OBJTYPE_NONMODIFIER): parentObjs = [o.bCurveObj for o in allDcObjs] empty = createEmptyWithInitKF(NEW_DATA_PREFIX + 'Guide', startFrame, initCo, initTangent, parentObjs = parentObjs, hide = False, group = group) if(customWriter == None): pObj = createPencil(NEW_DATA_PREFIX + 'Writer', initCo, tilt = 45, group = group) pObj.parent = empty transitionLengths, transitionLifts = \ getTransitionInfo(allDcObjs, liftAxis, maxLift, transitionSpeed, \ proportionalLift, reverseLift) totalLength = sum(o.curveLength for o in allDcObjs) + sum(transitionLengths) remainingLength = totalLength currFrame = startFrame zeroFrameCurves = [] oldOrigCurveIdx = 0 lastButOneFrame = False i = 0 #inline method, makes changes to local variables def _createPendingKfs(empty, alignToVert): nonlocal zeroFrameCurves nonlocal currFrame nonlocal remainingLength length = 0 for dcObj in zeroFrameCurves: createKfs(empty, dcObj, currFrame, curveFrameCnt = 1, alignToVert = alignToVert, objType = objType) length += dcObj.curveLength zeroFrameCurves = [] currFrame += 1 remainingLength -= length for j, curveDcObj in enumerate(curveDCObjs): for k, dcObj in enumerate(curveDcObj): if(lastButOneFrame): zeroFrameCurves.append(dcObj) continue if(i > 0): remainingFrames = totalFrames - currFrame + startFrame liftFrameCnt = getFrameCntForLength(totalFrames, totalLength, remainingLength, remainingFrames, transitionLengths[i-1]) if(liftFrameCnt == remainingFrames and i < len(allDcObjs)-1): lastButOneFrame = True zeroFrameCurves.append(dcObj) continue if(liftFrameCnt > 0): if(len(zeroFrameCurves) > 0): addedLength = \ _createPendingKfs(empty, alignToVert) liftFrameCnt -= 1 if(liftFrameCnt > 0): addLiftKeyFrames(empty, prevDcObj, dcObj, liftAxis, transitionLifts[i-1], currFrame, liftFrameCnt, reverseLift) currFrame += liftFrameCnt remainingLength -= transitionLengths[i-1] remainingFrames = totalFrames - currFrame + startFrame if(i == len(allDcObjs)-1): curveFrameCnt = remainingFrames else: curveFrameCnt = getFrameCntForLength(totalFrames, totalLength, remainingLength, remainingFrames, dcObj.curveLength) #Condition will be rare, but should be taken care of if(curveFrameCnt >= remainingFrames and i < len(allDcObjs)-1): lastButOneFrame = True curveFrameCnt -= 1 if(curveFrameCnt == 0): zeroFrameCurves.append(dcObj) totalEndingFrameCnt = round(remainingFrames * \ (sum(o.curveLength for o in zeroFrameCurves) / remainingLength)) if(totalEndingFrameCnt > 0 and not lastButOneFrame and remainingFrames > 1): _createPendingKfs(empty, alignToVert) else: if(curveFrameCnt == 1): zeroFrameCurves.append(dcObj) _createPendingKfs(empty, alignToVert) else: if(len(zeroFrameCurves) > 0): _createPendingKfs(empty, alignToVert) curveFrameCnt -= 1 createKfs(empty, dcObj, currFrame, curveFrameCnt, alignToVert, objType) currFrame += curveFrameCnt remainingLength -= dcObj.curveLength prevDcObj = dcObj i += 1 if(retain != 'Copy'): totalProcessed = sum(len(c) for x, c in enumerate(curveDCObjs) if x <= j) totalProcessed -= len(zeroFrameCurves) newOrigCurveIdx = 0 processed = 0 while(processed < totalProcessed): processed += len(curveDCObjs[newOrigCurveIdx]) newOrigCurveIdx += 1 if(processed > totalProcessed): newOrigCurveIdx -= 1 showOrigCurve(curveDCObjs[oldOrigCurveIdx:newOrigCurveIdx], selObjs[oldOrigCurveIdx:newOrigCurveIdx], currFrame, retain) oldOrigCurveIdx = newOrigCurveIdx if(len(zeroFrameCurves) > 0): addedLength = _createPendingKfs(empty, alignToVert) if(retain != 'Copy'): showOrigCurve(curveDCObjs[oldOrigCurveIdx:], selObjs[oldOrigCurveIdx:], currFrame, retain) setInterpolationLinear(empty) if(customWriter != None): addCustomWriterKFs(customWriter, empty, startFrame, currFrame, resetLocation) if(bpy.context.scene.frame_end < currFrame): bpy.context.scene.frame_end = currFrame bpy.context.scene.frame_current = startFrame return currFrame class CreateWritingAnimParams(bpy.types.PropertyGroup): retain = EnumProperty(name="Retain", items = [('Both', 'Both', ""), ('Original', 'Original', ""), ('Copy', 'Copy', "")], default = 'Copy', description='What to Retain After Finishing Animation') startFrame = IntProperty( name = "Start", description = "Start Frame of Animation", min = 1, default = 1) totalFrames = IntProperty( name = "Length", description = "Total Animation Frames", min = 1, default = 1000) transitionSpeed = FloatProperty( name = "Speed", description = "Speed of Writer During Lift (X Times Normal Speed)", min = 0.1, default = 1.5) maxLift = FloatProperty( name = "Max Lift", description = "Maximum Upward Distance to Traverse During Transition", precision = 3, default = 0.3) proportionalLift = BoolProperty( name = "Proportional Lift", description = "Should Lift Height Be Proportional to Transition Distance", default = True) liftAxis = EnumProperty(name="Axis", items = [('0', 'X', ""), ('1', 'Y', ""), ('2', 'Z', "")], default = '2', description='Axis Along Which Writer Is to Be Lifted (If Curve Is Linear)') reverseLift = BoolProperty( name = "Reverse", description = "Reverse Direction of Lift", default = False) alignToVert = BoolProperty( name = "Aligned (Experimental)", description = "Align Writer to Curve Normal (Works Differently Based on Anim Types)", default = False) resetLocation = BoolProperty( name = "Reset Location", description = "Reset Location of Custom Writer to Origin For Anim Duration", default = True) animType = EnumProperty(name = "Type", description='Follow Path Or Location Based Animation.', items = [(OBJTYPE_NONMODIFIER, 'Follow Path', ""), (OBJTYPE_MODIFIER, 'Location', "")], default = OBJTYPE_MODIFIER) copyPropertiesCurve = PointerProperty( name = 'Properties of', description = "Copy Properties (Material, Bevel Depth etc.) of Object", type = bpy.types.Object) customWriter = PointerProperty( name = 'Custom Writer', description = "Custom Object To Be Used As Writer", type = bpy.types.Object) class CreateWritingAnimOp(bpy.types.Operator): bl_idname = "object.create_writing_anim" bl_label = "Create Writing Animation" bl_options = {'REGISTER', 'UNDO'} keyframeCnt = 0 def execute(self, context): CreateWritingAnimOp.keyframeCnt = 0 retain = context.window_manager.createWritingAnimParams.retain startFrame = context.window_manager.createWritingAnimParams.startFrame totalFrames = context.window_manager.createWritingAnimParams.totalFrames transitionSpeed = context.window_manager.createWritingAnimParams.transitionSpeed liftAxis = int(context.window_manager.createWritingAnimParams.liftAxis) maxLift = context.window_manager.createWritingAnimParams.maxLift alignToVert = context.window_manager.createWritingAnimParams.alignToVert proportionalLift = context.window_manager.createWritingAnimParams.proportionalLift reverseLift = context.window_manager.createWritingAnimParams.reverseLift animType = context.window_manager.createWritingAnimParams.animType copyPropObj = context.window_manager.createWritingAnimParams.copyPropertiesCurve customWriter = context.window_manager.createWritingAnimParams.customWriter resetLocation = context.window_manager.createWritingAnimParams.resetLocation if(copyPropObj == None or not hasattr(copyPropObj, 'type') or \ copyPropObj.type not in(['CURVE','MESH'])): copyPropObj = None if(customWriter == None or not hasattr(customWriter, 'type') or \ customWriter.type not in(['CURVE','MESH'])): customWriter = None endFrame = main(retain, DEFAULT_DEPTH, startFrame, totalFrames, liftAxis, maxLift, transitionSpeed, alignToVert, proportionalLift, animType, copyPropObj, customWriter, reverseLift, resetLocation) if(endFrame < 0): self.report({'WARNING'}, "No Curve Objects Selected to Create Animation") else: self.report({'INFO'}, "Created "+ str(CreateWritingAnimOp.keyframeCnt)+ " new keyframes") return {'FINISHED'} class SeparateSplinesObjsOp(bpy.types.Operator): bl_idname = "object.separate_splines" bl_label = "Separate Bezier Splines" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): selObjs = bpy.context.selected_objects changeCnt = 0 splineCnt = 0 if(len(selObjs) == 0): self.report({'WARNING'}, "No Curve Objects Selected") return {'FINISHED'} for obj in selObjs: if(not isBezier(obj) or len(obj.data.splines) <= 1): continue for i, spline in enumerate(obj.data.splines): objCopy = obj.copy() objCopy.name = obj.name+"_"+ str(i).zfill(4) dataCopy = obj.data.copy() dataCopy.splines.clear() objCopy.data = dataCopy DrawableCurve.createNoncyclicSpline(dataCopy, spline, forceNoncyclic = False) bpy.context.scene.objects.link(objCopy) bpy.context.scene.objects.unlink(obj) bpy.data.curves.remove(obj.data) bpy.data.objects.remove(obj) changeCnt += 1 splineCnt += (i + 1) self.report({'INFO'}, "Separated "+ str(changeCnt) + " curve object" + \ ("s" if(changeCnt > 1) else "") + " into " +str(splineCnt) + " new ones") return {'FINISHED'} bl_info = { "name": "Create Writing Animation", "category": "Animation", } class CreateWritingAnimPanel(bpy.types.Panel): bl_label = "Writing Animation" bl_idname = "object.writinganim" bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' bl_category = "Writing Animation" bl_context = "objectmode" def draw(self, context): layout = self.layout obj = context.object col = layout.column() col.operator("object.separate_splines") col.separator() col.label(text="Animation", icon="ANIM") col.prop(context.window_manager.createWritingAnimParams, "animType") col.prop(context.window_manager.createWritingAnimParams, "startFrame") col.prop(context.window_manager.createWritingAnimParams, "totalFrames") col.prop(context.window_manager.createWritingAnimParams, "retain") col.prop(context.window_manager.createWritingAnimParams, "copyPropertiesCurve") col.separator() col.label(text="Transition", icon="MAN_TRANS") col.prop(context.window_manager.createWritingAnimParams, "transitionSpeed") col.prop(context.window_manager.createWritingAnimParams, "maxLift") col.prop(context.window_manager.createWritingAnimParams, "liftAxis") col.prop(context.window_manager.createWritingAnimParams, "proportionalLift") col.prop(context.window_manager.createWritingAnimParams, "reverseLift") col.separator() col.label(text="Writer", icon="LINE_DATA") col.prop(context.window_manager.createWritingAnimParams, "alignToVert") col.prop(context.window_manager.createWritingAnimParams, "customWriter") col.prop(context.window_manager.createWritingAnimParams, "resetLocation") col.separator() col.operator("object.create_writing_anim") def register(): bpy.utils.register_class(CreateWritingAnimPanel) bpy.utils.register_class(CreateWritingAnimOp) bpy.utils.register_class(SeparateSplinesObjsOp) bpy.utils.register_class(CreateWritingAnimParams) bpy.types.WindowManager.createWritingAnimParams = \ bpy.props.PointerProperty(type=CreateWritingAnimParams) def unregister(): bpy.utils.unregister_class(CreateWritingAnimPanel) bpy.utils.unregister_class(CreateWritingAnimOp) bpy.utils.unregister_class(SeparateSplinesObjsOp) del bpy.types.WindowManager.createWritingAnimParams bpy.utils.unregister_class(CreateWritingAnimParams) if __name__ == "__main__": register()