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, 80, 0),
24 "location": "View3D > Add > Curve > Simplify Curves",
25 "description": "Simplifies 3D Curve objects and animation F-Curves",
27 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Curve/Curve_Simplify",
29 "category": "Add Curve",
33 This script simplifies Curve objects and animation F-Curves.
37 from bpy
.props
import (
43 from mathutils
import Vector
48 from bpy
.types
import Operator
51 def error_handlers(self
, op_name
, errors
, reports
="ERROR"):
54 reports
+ ": some operations could not be performed "
55 "(See Console for more info)")
57 print("\n[Simplify Curves]\nOperator: {}\nErrors: {}\n".format(op_name
, errors
))
62 # ### simplipoly algorithm ###
64 # get SplineVertIndices to keep
65 def simplypoly(splineVerts
, options
):
67 newVerts
= [] # list of vertindices to keep
68 points
= splineVerts
# list of 3dVectors
69 pointCurva
= [] # table with curvatures
70 curvatures
= [] # averaged curvatures per vert
73 order
= options
[3] # order of sliding beziercurves
74 k_thresh
= options
[2] # curvature threshold
75 dis_error
= options
[6] # additional distance error
77 # get curvatures per vert
78 for i
, point
in enumerate(points
[: -(order
- 1)]):
79 BVerts
= points
[i
: i
+ order
]
80 for b
, BVert
in enumerate(BVerts
[1: -1]):
81 deriv1
= getDerivative(BVerts
, 1 / (order
- 1), order
- 1)
82 deriv2
= getDerivative(BVerts
, 1 / (order
- 1), order
- 2)
83 curva
= getCurvature(deriv1
, deriv2
)
84 pointCurva
[i
+ b
+ 1].append(curva
)
86 # average the curvatures
87 for i
in range(len(points
)):
88 avgCurva
= sum(pointCurva
[i
]) / (order
- 1)
89 curvatures
.append(avgCurva
)
91 # get distancevalues per vert - same as Ramer-Douglas-Peucker
93 distances
= [0.0] # first vert is always kept
94 for i
, point
in enumerate(points
[1: -1]):
95 dist
= altitude(points
[i
], points
[i
+ 2], points
[i
+ 1])
96 distances
.append(dist
)
97 distances
.append(0.0) # last vert is always kept
99 # generate list of vert indices to keep
100 # tested against averaged curvatures and distances of neighbour verts
101 newVerts
.append(0) # first vert is always kept
102 for i
, curv
in enumerate(curvatures
):
103 if (curv
>= k_thresh
* 0.01 or distances
[i
] >= dis_error
* 0.1):
105 newVerts
.append(len(curvatures
) - 1) # last vert is always kept
110 # get binomial coefficient
114 for i
in range(1, n
+ 1):
123 # get nth derivative of order(len(verts)) bezier curve
124 def getDerivative(verts
, t
, nth
):
125 order
= len(verts
) - 1 - nth
133 for i
in range(len(verts
) - 1):
134 derivVerts
.append(verts
[i
+ 1] - verts
[i
])
139 if len(verts
[0]) == 3:
140 point
= Vector((0, 0, 0))
141 if len(verts
[0]) == 2:
142 point
= Vector((0, 0))
144 for i
, vert
in enumerate(QVerts
):
145 point
+= binom(order
, i
) * pow(t
, i
) * pow(1 - t
, order
- i
) * vert
151 # get curvature from first, second derivative
152 def getCurvature(deriv1
, deriv2
):
153 if deriv1
.length
== 0: # in case of points in straight line
156 curvature
= (deriv1
.cross(deriv2
)).length
/ pow(deriv1
.length
, 3)
160 # ### Ramer-Douglas-Peucker algorithm ###
162 # get altitude of vert
163 def altitude(point1
, point2
, pointn
):
164 edge1
= point2
- point1
165 edge2
= pointn
- point1
166 if edge2
.length
== 0:
169 if edge1
.length
== 0:
170 altitude
= edge2
.length
172 alpha
= edge1
.angle(edge2
)
173 altitude
= sin(alpha
) * edge2
.length
177 # iterate through verts
178 def iterate(points
, newVerts
, error
):
180 for newIndex
in range(len(newVerts
) - 1):
183 for i
, point
in enumerate(points
[newVerts
[newIndex
] + 1: newVerts
[newIndex
+ 1]]):
184 alti
= altitude(points
[newVerts
[newIndex
]], points
[newVerts
[newIndex
+ 1]], point
)
185 if alti
> alti_store
:
187 if alti_store
>= error
:
188 bigVert
= i
+ 1 + newVerts
[newIndex
]
196 # get SplineVertIndices to keep
197 def simplify_RDP(splineVerts
, options
):
201 # set first and last vert
202 newVerts
= [0, len(splineVerts
) - 1]
204 # iterate through the points
206 while new
is not False:
207 new
= iterate(splineVerts
, newVerts
, error
)
214 # ### CURVE GENERATION ###
216 # set bezierhandles to auto
217 def setBezierHandles(newCurve
):
219 for spline
in newCurve
.data
.splines
:
220 for p
in spline
.bezier_points
:
221 p
.handle_left_type
= 'AUTO'
222 p
.handle_right_type
= 'AUTO'
225 # get array of new coords for new spline from vertindices
226 def vertsToPoints(newVerts
, splineVerts
, splineType
):
230 # array for BEZIER spline output
231 if splineType
== 'BEZIER':
233 newPoints
+= splineVerts
[v
].to_tuple()
235 # array for nonBEZIER output
238 newPoints
+= (splineVerts
[v
].to_tuple())
239 if splineType
== 'NURBS':
240 newPoints
.append(1) # for nurbs w = 1
241 else: # for poly w = 0
246 # ### MAIN OPERATIONS ###
248 def main(context
, obj
, options
, curve_dimension
):
251 degreeOut
= options
[5]
252 keepShort
= options
[7]
253 bpy
.ops
.object.select_all(action
='DESELECT')
254 scene
= context
.scene
255 splines
= obj
.data
.splines
.values()
257 # create curvedatablock
258 curve
= bpy
.data
.curves
.new("Simple_" + obj
.name
, type='CURVE')
259 curve
.dimensions
= curve_dimension
262 for spline_i
, spline
in enumerate(splines
):
263 # test if spline is a long enough
264 if len(spline
.points
) >= 7 or keepShort
:
265 # check what type of spline to create
266 if output
== 'INPUT':
267 splineType
= spline
.type
271 # get vec3 list to simplify
272 if spline
.type == 'BEZIER': # get bezierverts
273 splineVerts
= [splineVert
.co
.copy()
274 for splineVert
in spline
.bezier_points
.values()]
276 else: # verts from all other types of curves
277 splineVerts
= [splineVert
.co
.to_3d()
278 for splineVert
in spline
.points
.values()]
280 # simplify spline according to mode
281 if mode
== 'DISTANCE':
282 newVerts
= simplify_RDP(splineVerts
, options
)
284 if mode
== 'CURVATURE':
285 newVerts
= simplypoly(splineVerts
, options
)
287 # convert indices into vectors3D
288 newPoints
= vertsToPoints(newVerts
, splineVerts
, splineType
)
291 newSpline
= curve
.splines
.new(type=splineType
)
293 # put newPoints into spline according to type
294 if splineType
== 'BEZIER':
295 newSpline
.bezier_points
.add(int(len(newPoints
) * 0.33))
296 newSpline
.bezier_points
.foreach_set('co', newPoints
)
298 newSpline
.points
.add(int(len(newPoints
) * 0.25 - 1))
299 newSpline
.points
.foreach_set('co', newPoints
)
301 # set degree of outputNurbsCurve
302 if output
== 'NURBS':
303 newSpline
.order_u
= degreeOut
306 newSpline
.use_endpoint_u
= spline
.use_endpoint_u
308 # create new object and put into scene
309 newCurve
= bpy
.data
.objects
.new("Simple_" + obj
.name
, curve
)
310 coll
= context
.view_layer
.active_layer_collection
.collection
311 coll
.objects
.link(newCurve
)
312 newCurve
.select_set(True)
314 context
.view_layer
.objects
.active
= newCurve
315 newCurve
.matrix_world
= obj
.matrix_world
317 # set bezierhandles to auto
318 setBezierHandles(newCurve
)
323 # get preoperator fcurves
324 def getFcurveData(obj
):
326 for fc
in obj
.animation_data
.action
.fcurves
:
328 fcVerts
= [vcVert
.co
.to_3d()
329 for vcVert
in fc
.keyframe_points
.values()]
330 fcurves
.append(fcVerts
)
334 def selectedfcurves(obj
):
336 for i
, fc
in enumerate(obj
.animation_data
.action
.fcurves
):
338 fcurves_sel
.append(fc
)
343 def fcurves_simplify(context
, obj
, options
, fcurves
):
347 # get indices of selected fcurves
348 fcurve_sel
= selectedfcurves(obj
)
351 for fcurve_i
, fcurve
in enumerate(fcurves
):
352 # test if fcurve is long enough
354 # simplify spline according to mode
355 if mode
== 'DISTANCE':
356 newVerts
= simplify_RDP(fcurve
, options
)
358 if mode
== 'CURVATURE':
359 newVerts
= simplypoly(fcurve
, options
)
361 # convert indices into vectors3D
364 # this is different from the main() function for normal curves, different api...
366 newPoints
.append(fcurve
[v
])
368 # remove all points from curve first
369 for i
in range(len(fcurve
) - 1, 0, -1):
370 fcurve_sel
[fcurve_i
].keyframe_points
.remove(fcurve_sel
[fcurve_i
].keyframe_points
[i
])
371 # put newPoints into fcurve
373 fcurve_sel
[fcurve_i
].keyframe_points
.insert(frame
=v
[0], value
=v
[1])
377 # ### MENU append ###
379 def menu_func(self
, context
):
380 self
.layout
.operator("graph.simplify")
383 def menu(self
, context
):
384 self
.layout
.operator("curve.simplify", text
="Curve Simplify", icon
="CURVE_DATA")
387 # ### ANIMATION CURVES OPERATOR ###
389 class GRAPH_OT_simplify(Operator
):
390 bl_idname
= "graph.simplify"
391 bl_label
= "Simplify F-Curves"
392 bl_description
= ("Simplify selected Curves\n"
393 "Does not operate on short Splines (less than 6 points)")
394 bl_options
= {'REGISTER', 'UNDO'}
398 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
399 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')]
402 description
="Choose algorithm to use",
405 k_thresh
: FloatProperty(
408 default
=0, precision
=3,
409 description
="Threshold"
411 pointsNr
: IntProperty(
416 description
="Degree of curve to get averaged curvatures"
418 error
: FloatProperty(
420 description
="Maximum allowed distance error",
421 min=0.0, soft_min
=0.0,
422 default
=0, precision
=3
424 degreeOut
: IntProperty(
429 description
="Degree of new curve"
431 dis_error
: FloatProperty(
432 name
="Distance error",
433 description
="Maximum allowed distance error in Blender Units",
435 default
=0.0, precision
=3
439 def draw(self
, context
):
441 col
= layout
.column()
443 col
.label(text
="Distance Error:")
444 col
.prop(self
, "error", expand
=True)
447 def poll(cls
, context
):
449 obj
= context
.active_object
452 animdata
= obj
.animation_data
454 act
= animdata
.action
456 fcurves
= act
.fcurves
457 return (obj
and fcurves
)
459 def execute(self
, context
):
470 obj
= context
.active_object
473 self
.fcurves
= getFcurveData(obj
)
475 fcurves_simplify(context
, obj
, options
, self
.fcurves
)
480 # ### Curves OPERATOR ###
481 class CURVE_OT_simplify(Operator
):
482 bl_idname
= "curve.simplify"
483 bl_label
= "Simplify Curves"
484 bl_description
= ("Simplify the existing Curve based upon the chosen settings\n"
485 "Notes: Needs an existing Curve object,\n"
486 "Outputs a new Curve with the Simple prefix in the name")
487 bl_options
= {'REGISTER', 'UNDO'}
491 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
492 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')
496 description
="Choose algorithm to use",
500 ('INPUT', 'Input', 'Same type as input spline'),
501 ('NURBS', 'Nurbs', 'NURBS'),
502 ('BEZIER', 'Bezier', 'BEZIER'),
503 ('POLY', 'Poly', 'POLY')
505 output
: EnumProperty(
506 name
="Output splines",
507 description
="Type of splines to output",
510 k_thresh
: FloatProperty(
513 default
=0, precision
=3,
514 description
="Threshold"
516 pointsNr
: IntProperty(
521 description
="Degree of curve to get averaged curvatures"
523 error
: FloatProperty(
525 description
="Maximum allowed distance error in Blender Units",
527 default
=0.0, precision
=3
529 degreeOut
: IntProperty(
534 description
="Degree of new curve"
536 dis_error
: FloatProperty(
537 name
="Distance error",
538 description
="Maximum allowed distance error in Blender Units",
542 keepShort
: BoolProperty(
543 name
="Keep short splines",
544 description
="Keep short splines (less than 7 points)",
548 def draw(self
, context
):
550 col
= layout
.column()
552 col
.label(text
="Distance Error:")
553 col
.prop(self
, "error", expand
=True)
554 col
.prop(self
, "output", text
="Output", icon
="OUTLINER_OB_CURVE")
555 if self
.output
== "NURBS":
556 col
.prop(self
, "degreeOut", expand
=True)
558 col
.prop(self
, "keepShort", expand
=True)
561 def poll(cls
, context
):
562 obj
= context
.active_object
563 return (obj
and obj
.type == 'CURVE')
565 def execute(self
, context
):
577 global_undo
= bpy
.context
.preferences
.edit
.use_global_undo
578 context
.preferences
.edit
.use_global_undo
= False
580 bpy
.ops
.object.mode_set(mode
='OBJECT')
581 obj
= context
.active_object
582 curve_dimension
= obj
.data
.dimensions
584 main(context
, obj
, options
, curve_dimension
)
586 context
.preferences
.edit
.use_global_undo
= global_undo
587 except Exception as e
:
588 error_handlers(self
, "curve.simplify", e
, "Simplify Curves")
590 context
.preferences
.edit
.use_global_undo
= global_undo
604 from bpy
.utils
import register_class
608 bpy
.types
.GRAPH_MT_channel
.append(menu_func
)
609 bpy
.types
.DOPESHEET_MT_channel
.append(menu_func
)
610 bpy
.types
.VIEW3D_MT_curve_add
.append(menu
)
614 from bpy
.utils
import unregister_class
615 for cls
in reversed(classes
):
616 unregister_class(cls
)
618 bpy
.types
.GRAPH_MT_channel
.remove(menu_func
)
619 bpy
.types
.DOPESHEET_MT_channel
.remove(menu_func
)
620 bpy
.types
.VIEW3D_MT_curve_add
.remove(menu
)
623 if __name__
== "__main__":