1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 "name": "Simplify Curves",
21 "author": "testscreenings",
23 "blender": (2, 59, 0),
24 "location": "Search > Simplify Curves",
25 "description": "Simplifies 3D Curve objects and animation F-Curves",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"\
28 "Scripts/Curve/Curve_Simplify",
29 "tracker_url": "https://projects.blender.org/tracker/index.php?"\
30 "func=detail&aid=22327",
31 "category": "Add Curve"}
34 This script simplifies Curve objects and animation F-Curves.
37 ####################################################
39 from bpy
.props
import *
43 ##############################
44 #### simplipoly algorithm ####
45 ##############################
46 # get SplineVertIndices to keep
47 def simplypoly(splineVerts
, options
):
49 newVerts
= [] # list of vertindices to keep
50 points
= splineVerts
# list of 3dVectors
51 pointCurva
= [] # table with curvatures
52 curvatures
= [] # averaged curvatures per vert
55 order
= options
[3] # order of sliding beziercurves
56 k_thresh
= options
[2] # curvature threshold
57 dis_error
= options
[6] # additional distance error
59 # get curvatures per vert
60 for i
, point
in enumerate(points
[:-(order
-1)]):
61 BVerts
= points
[i
:i
+order
]
62 for b
, BVert
in enumerate(BVerts
[1:-1]):
63 deriv1
= getDerivative(BVerts
, 1/(order
-1), order
-1)
64 deriv2
= getDerivative(BVerts
, 1/(order
-1), order
-2)
65 curva
= getCurvature(deriv1
, deriv2
)
66 pointCurva
[i
+b
+1].append(curva
)
68 # average the curvatures
69 for i
in range(len(points
)):
70 avgCurva
= sum(pointCurva
[i
]) / (order
-1)
71 curvatures
.append(avgCurva
)
73 # get distancevalues per vert - same as Ramer-Douglas-Peucker
75 distances
= [0.0] #first vert is always kept
76 for i
, point
in enumerate(points
[1:-1]):
77 dist
= altitude(points
[i
], points
[i
+2], points
[i
+1])
78 distances
.append(dist
)
79 distances
.append(0.0) # last vert is always kept
81 # generate list of vertindices to keep
82 # tested against averaged curvatures and distances of neighbour verts
83 newVerts
.append(0) # first vert is always kept
84 for i
, curv
in enumerate(curvatures
):
85 if (curv
>= k_thresh
*0.01
86 or distances
[i
] >= dis_error
*0.1):
88 newVerts
.append(len(curvatures
)-1) # last vert is always kept
92 # get binomial coefficient
96 for i
in range(1, n
+1):
104 # get nth derivative of order(len(verts)) bezier curve
105 def getDerivative(verts
, t
, nth
):
106 order
= len(verts
) - 1 - nth
114 for i
in range(len(verts
)-1):
115 derivVerts
.append(verts
[i
+1] - verts
[i
])
120 if len(verts
[0]) == 3:
121 point
= mathutils
.Vector((0, 0, 0))
122 if len(verts
[0]) == 2:
123 point
= mathutils
.Vector((0, 0))
125 for i
, vert
in enumerate(QVerts
):
126 point
+= binom(order
, i
) * math
.pow(t
, i
) * math
.pow(1-t
, order
-i
) * vert
131 # get curvature from first, second derivative
132 def getCurvature(deriv1
, deriv2
):
133 if deriv1
.length
== 0: # in case of points in straight line
136 curvature
= (deriv1
.cross(deriv2
)).length
/ math
.pow(deriv1
.length
, 3)
139 #########################################
140 #### Ramer-Douglas-Peucker algorithm ####
141 #########################################
142 # get altitude of vert
143 def altitude(point1
, point2
, pointn
):
144 edge1
= point2
- point1
145 edge2
= pointn
- point1
146 if edge2
.length
== 0:
149 if edge1
.length
== 0:
150 altitude
= edge2
.length
152 alpha
= edge1
.angle(edge2
)
153 altitude
= math
.sin(alpha
) * edge2
.length
156 # iterate through verts
157 def iterate(points
, newVerts
, error
):
159 for newIndex
in range(len(newVerts
)-1):
162 for i
, point
in enumerate(points
[newVerts
[newIndex
]+1:newVerts
[newIndex
+1]]):
163 alti
= altitude(points
[newVerts
[newIndex
]], points
[newVerts
[newIndex
+1]], point
)
164 if alti
> alti_store
:
166 if alti_store
>= error
:
167 bigVert
= i
+1+newVerts
[newIndex
]
174 #### get SplineVertIndices to keep
175 def simplify_RDP(splineVerts
, options
):
179 # set first and last vert
180 newVerts
= [0, len(splineVerts
)-1]
182 # iterate through the points
185 new
= iterate(splineVerts
, newVerts
, error
)
191 ##########################
192 #### CURVE GENERATION ####
193 ##########################
194 # set bezierhandles to auto
195 def setBezierHandles(newCurve
):
197 #bpy.ops.object.mode_set(mode='EDIT', toggle=True)
198 #bpy.ops.curve.select_all(action='SELECT')
199 #bpy.ops.curve.handle_type_set(type='AUTOMATIC')
200 #bpy.ops.object.mode_set(mode='OBJECT', toggle=True)
203 for spline
in newCurve
.data
.splines
:
204 for p
in spline
.bezier_points
:
205 p
.handle_left_type
= 'AUTO'
206 p
.handle_right_type
= 'AUTO'
208 # get array of new coords for new spline from vertindices
209 def vertsToPoints(newVerts
, splineVerts
, splineType
):
213 # array for BEZIER spline output
214 if splineType
== 'BEZIER':
216 newPoints
+= splineVerts
[v
].to_tuple()
218 # array for nonBEZIER output
221 newPoints
+= (splineVerts
[v
].to_tuple())
222 if splineType
== 'NURBS':
223 newPoints
.append(1) #for nurbs w=1
228 #########################
229 #### MAIN OPERATIONS ####
230 #########################
232 def main(context
, obj
, options
):
233 #print("\n_______START_______")
237 degreeOut
= options
[5]
238 keepShort
= options
[7]
239 bpy
.ops
.object.select_all(action
='DESELECT')
240 scene
= context
.scene
241 splines
= obj
.data
.splines
.values()
243 # create curvedatablock
244 curve
= bpy
.data
.curves
.new("Simple_"+obj
.name
, type = 'CURVE')
247 for spline_i
, spline
in enumerate(splines
):
248 # test if spline is a long enough
249 if len(spline
.points
) >= 7 or keepShort
:
250 #check what type of spline to create
251 if output
== 'INPUT':
252 splineType
= spline
.type
256 # get vec3 list to simplify
257 if spline
.type == 'BEZIER': # get bezierverts
258 splineVerts
= [splineVert
.co
.copy()
259 for splineVert
in spline
.bezier_points
.values()]
261 else: # verts from all other types of curves
262 splineVerts
= [splineVert
.co
.to_3d()
263 for splineVert
in spline
.points
.values()]
265 # simplify spline according to mode
266 if mode
== 'DISTANCE':
267 newVerts
= simplify_RDP(splineVerts
, options
)
269 if mode
== 'CURVATURE':
270 newVerts
= simplypoly(splineVerts
, options
)
272 # convert indices into vectors3D
273 newPoints
= vertsToPoints(newVerts
, splineVerts
, splineType
)
276 newSpline
= curve
.splines
.new(type = splineType
)
278 # put newPoints into spline according to type
279 if splineType
== 'BEZIER':
280 newSpline
.bezier_points
.add(int(len(newPoints
)*0.33))
281 newSpline
.bezier_points
.foreach_set('co', newPoints
)
283 newSpline
.points
.add(int(len(newPoints
)*0.25 - 1))
284 newSpline
.points
.foreach_set('co', newPoints
)
286 # set degree of outputNurbsCurve
287 if output
== 'NURBS':
288 newSpline
.order_u
= degreeOut
291 newSpline
.use_endpoint_u
= spline
.use_endpoint_u
293 # create ne object and put into scene
294 newCurve
= bpy
.data
.objects
.new("Simple_"+obj
.name
, curve
)
295 scene
.objects
.link(newCurve
)
296 newCurve
.select
= True
297 scene
.objects
.active
= newCurve
298 newCurve
.matrix_world
= obj
.matrix_world
300 # set bezierhandles to auto
301 setBezierHandles(newCurve
)
303 #print("________END________\n")
307 ## get preoperator fcurves
308 def getFcurveData(obj
):
310 for fc
in obj
.animation_data
.action
.fcurves
:
312 fcVerts
= [vcVert
.co
.to_3d()
313 for vcVert
in fc
.keyframe_points
.values()]
314 fcurves
.append(fcVerts
)
317 def selectedfcurves(obj
):
319 for i
, fc
in enumerate(obj
.animation_data
.action
.fcurves
):
321 fcurves_sel
.append(fc
)
324 ###########################################################
326 def fcurves_simplify(context
, obj
, options
, fcurves
):
330 #get indices of selected fcurves
331 fcurve_sel
= selectedfcurves(obj
)
334 for fcurve_i
, fcurve
in enumerate(fcurves
):
335 # test if fcurve is long enough
338 # simplify spline according to mode
339 if mode
== 'DISTANCE':
340 newVerts
= simplify_RDP(fcurve
, options
)
342 if mode
== 'CURVATURE':
343 newVerts
= simplypoly(fcurve
, options
)
345 # convert indices into vectors3D
348 #this is different from the main() function for normal curves, different api...
350 newPoints
.append(fcurve
[v
])
352 #remove all points from curve first
353 for i
in range(len(fcurve
)-1,0,-1):
354 fcurve_sel
[fcurve_i
].keyframe_points
.remove(fcurve_sel
[fcurve_i
].keyframe_points
[i
])
355 # put newPoints into fcurve
357 fcurve_sel
[fcurve_i
].keyframe_points
.insert(frame
=v
[0],value
=v
[1])
358 #fcurve.points.foreach_set('co', newPoints)
361 #################################################
362 #### ANIMATION CURVES OPERATOR ##################
363 #################################################
364 class GRAPH_OT_simplify(bpy
.types
.Operator
):
366 bl_idname
= "graph.simplify"
367 bl_label
= "Simplifiy F-Curves"
368 bl_description
= "Simplify selected F-Curves"
369 bl_options
= {'REGISTER', 'UNDO'}
373 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
374 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')]
375 mode
= EnumProperty(name
="Mode",
376 description
="Choose algorithm to use",
378 k_thresh
= FloatProperty(name
="k",
380 default
=0, precision
=3,
381 description
="Threshold")
382 pointsNr
= IntProperty(name
="n",
386 description
="Degree of curve to get averaged curvatures")
387 error
= FloatProperty(name
="Error",
388 description
="Maximum error to allow - distance",
389 min=0.0, soft_min
=0.0,
390 default
=0, precision
=3)
391 degreeOut
= IntProperty(name
="Degree",
395 description
="Degree of new curve")
396 dis_error
= FloatProperty(name
="Distance error",
397 description
="Maximum error in Blender Units to allow - distance",
399 default
=0.0, precision
=3)
402 ''' Remove curvature mode as long as it isn't significantly improved
404 def draw(self, context):
406 col = layout.column()
408 col.prop(self, 'mode', expand=True)
409 if self.mode == 'DISTANCE':
411 box.label(self.mode, icon='ARROW_LEFTRIGHT')
412 box.prop(self, 'error', expand=True)
413 if self.mode == 'CURVATURE':
415 box.label('Degree', icon='SMOOTHCURVE')
416 box.prop(self, 'pointsNr', expand=True)
417 box.label('Threshold', icon='PARTICLE_PATH')
418 box.prop(self, 'k_thresh', expand=True)
419 box.label('Distance', icon='ARROW_LEFTRIGHT')
420 box.prop(self, 'dis_error', expand=True)
421 col = layout.column()
424 def draw(self
, context
):
426 col
= layout
.column()
427 col
.prop(self
, 'error', expand
=True)
429 ## Check for animdata
431 def poll(cls
, context
):
432 obj
= context
.active_object
435 animdata
= obj
.animation_data
437 act
= animdata
.action
439 fcurves
= act
.fcurves
440 return (obj
and fcurves
)
443 def execute(self
, context
):
444 #print("------START------")
455 obj
= context
.active_object
458 self
.fcurves
= getFcurveData(obj
)
460 fcurves_simplify(context
, obj
, options
, self
.fcurves
)
462 #print("-------END-------")
465 ###########################
466 ##### Curves OPERATOR #####
467 ###########################
468 class CURVE_OT_simplify(bpy
.types
.Operator
):
470 bl_idname
= "curve.simplify"
471 bl_label
= "Simplifiy Curves"
472 bl_description
= "Simplify Curves"
473 bl_options
= {'REGISTER', 'UNDO'}
477 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
478 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')]
479 mode
= EnumProperty(name
="Mode",
480 description
="Choose algorithm to use",
483 ('INPUT', 'Input', 'Same type as input spline'),
484 ('NURBS', 'Nurbs', 'NURBS'),
485 ('BEZIER', 'Bezier', 'BEZIER'),
486 ('POLY', 'Poly', 'POLY')]
487 output
= EnumProperty(name
="Output splines",
488 description
="Type of splines to output",
490 k_thresh
= FloatProperty(name
="k",
492 default
=0, precision
=3,
493 description
="Threshold")
494 pointsNr
= IntProperty(name
="n",
498 description
="Degree of curve to get averaged curvatures")
499 error
= FloatProperty(name
="Error in Blender Units",
500 description
="Maximum error in Blender Units to allow - distance",
502 default
=0.0, precision
=3)
503 degreeOut
= IntProperty(name
="Degree",
507 description
="Degree of new curve")
508 dis_error
= FloatProperty(name
="Distance error",
509 description
="Maximum error in Blender Units to allow - distance",
512 keepShort
= BoolProperty(name
="Keep short splines",
513 description
="Keep short splines (less then 7 points)",
516 ''' Remove curvature mode as long as it isn't significantly improved
518 def draw(self, context):
520 col = layout.column()
522 col.prop(self, 'mode', expand=True)
523 if self.mode == 'DISTANCE':
525 box.label(self.mode, icon='ARROW_LEFTRIGHT')
526 box.prop(self, 'error', expand=True)
527 if self.mode == 'CURVATURE':
529 box.label('Degree', icon='SMOOTHCURVE')
530 box.prop(self, 'pointsNr', expand=True)
531 box.label('Threshold', icon='PARTICLE_PATH')
532 box.prop(self, 'k_thresh', expand=True)
533 box.label('Distance', icon='ARROW_LEFTRIGHT')
534 box.prop(self, 'dis_error', expand=True)
535 col = layout.column()
537 col.prop(self, 'output', text='Output', icon='OUTLINER_OB_CURVE')
538 if self.output == 'NURBS':
539 col.prop(self, 'degreeOut', expand=True)
540 col.prop(self, 'keepShort', expand=True)
543 def draw(self
, context
):
545 col
= layout
.column()
546 col
.prop(self
, 'error', expand
=True)
547 col
.prop(self
, 'output', text
='Output', icon
='OUTLINER_OB_CURVE')
548 if self
.output
== 'NURBS':
549 col
.prop(self
, 'degreeOut', expand
=True)
550 col
.prop(self
, 'keepShort', expand
=True)
555 def poll(cls
, context
):
556 obj
= context
.active_object
557 return (obj
and obj
.type == 'CURVE')
560 def execute(self
, context
):
561 #print("------START------")
574 bpy
.context
.user_preferences
.edit
.use_global_undo
= False
576 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=True)
577 obj
= context
.active_object
579 main(context
, obj
, options
)
581 bpy
.context
.user_preferences
.edit
.use_global_undo
= True
583 #print("-------END-------")
586 #################################################
587 #### REGISTER ###################################
588 #################################################
590 bpy
.utils
.register_module(__name__
)
595 bpy
.utils
.unregister_module(__name__
)
599 if __name__
== "__main__":