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, Michael Soluyanov",
23 "blender": (2, 80, 0),
24 "location": "3D View, Dopesheet & Graph Editors",
25 "description": "Simplify Curves: 3dview, Dopesheet, Graph. Distance Merge: 3d view curve edit",
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
34 This script will also Merge by Distance 3d view curves in edit mode
38 from bpy
.props
import (
49 from bpy
.types
import Operator
52 def error_handlers(self
, op_name
, errors
, reports
="ERROR"):
55 reports
+ ": some operations could not be performed "
56 "(See Console for more info)")
58 print("\n[Simplify Curves]\nOperator: {}\nErrors: {}\n".format(op_name
, errors
))
63 # ### simplipoly algorithm ###
65 # get SplineVertIndices to keep
66 def simplypoly(splineVerts
, options
):
68 newVerts
= [] # list of vertindices to keep
69 points
= splineVerts
# list of 3dVectors
70 pointCurva
= [] # table with curvatures
71 curvatures
= [] # averaged curvatures per vert
74 order
= options
[3] # order of sliding beziercurves
75 k_thresh
= options
[2] # curvature threshold
76 dis_error
= options
[6] # additional distance error
78 # get curvatures per vert
79 for i
, point
in enumerate(points
[: -(order
- 1)]):
80 BVerts
= points
[i
: i
+ order
]
81 for b
, BVert
in enumerate(BVerts
[1: -1]):
82 deriv1
= getDerivative(BVerts
, 1 / (order
- 1), order
- 1)
83 deriv2
= getDerivative(BVerts
, 1 / (order
- 1), order
- 2)
84 curva
= getCurvature(deriv1
, deriv2
)
85 pointCurva
[i
+ b
+ 1].append(curva
)
87 # average the curvatures
88 for i
in range(len(points
)):
89 avgCurva
= sum(pointCurva
[i
]) / (order
- 1)
90 curvatures
.append(avgCurva
)
92 # get distancevalues per vert - same as Ramer-Douglas-Peucker
94 distances
= [0.0] # first vert is always kept
95 for i
, point
in enumerate(points
[1: -1]):
96 dist
= altitude(points
[i
], points
[i
+ 2], points
[i
+ 1])
97 distances
.append(dist
)
98 distances
.append(0.0) # last vert is always kept
100 # generate list of vert indices to keep
101 # tested against averaged curvatures and distances of neighbour verts
102 newVerts
.append(0) # first vert is always kept
103 for i
, curv
in enumerate(curvatures
):
104 if (curv
>= k_thresh
* 0.01 or distances
[i
] >= dis_error
* 0.1):
106 newVerts
.append(len(curvatures
) - 1) # last vert is always kept
111 # get binomial coefficient
115 for i
in range(1, n
+ 1):
124 # get nth derivative of order(len(verts)) bezier curve
125 def getDerivative(verts
, t
, nth
):
126 order
= len(verts
) - 1 - nth
134 for i
in range(len(verts
) - 1):
135 derivVerts
.append(verts
[i
+ 1] - verts
[i
])
140 if len(verts
[0]) == 3:
141 point
= Vector((0, 0, 0))
142 if len(verts
[0]) == 2:
143 point
= Vector((0, 0))
145 for i
, vert
in enumerate(QVerts
):
146 point
+= binom(order
, i
) * pow(t
, i
) * pow(1 - t
, order
- i
) * vert
152 # get curvature from first, second derivative
153 def getCurvature(deriv1
, deriv2
):
154 if deriv1
.length
== 0: # in case of points in straight line
157 curvature
= (deriv1
.cross(deriv2
)).length
/ pow(deriv1
.length
, 3)
161 # ### Ramer-Douglas-Peucker algorithm ###
163 # get altitude of vert
164 def altitude(point1
, point2
, pointn
):
165 edge1
= point2
- point1
166 edge2
= pointn
- point1
167 if edge2
.length
== 0:
170 if edge1
.length
== 0:
171 altitude
= edge2
.length
173 alpha
= edge1
.angle(edge2
)
174 altitude
= sin(alpha
) * edge2
.length
178 # iterate through verts
179 def iterate(points
, newVerts
, error
):
181 for newIndex
in range(len(newVerts
) - 1):
184 for i
, point
in enumerate(points
[newVerts
[newIndex
] + 1: newVerts
[newIndex
+ 1]]):
185 alti
= altitude(points
[newVerts
[newIndex
]], points
[newVerts
[newIndex
+ 1]], point
)
186 if alti
> alti_store
:
188 if alti_store
>= error
:
189 bigVert
= i
+ 1 + newVerts
[newIndex
]
197 # get SplineVertIndices to keep
198 def simplify_RDP(splineVerts
, options
):
202 # set first and last vert
203 newVerts
= [0, len(splineVerts
) - 1]
205 # iterate through the points
207 while new
is not False:
208 new
= iterate(splineVerts
, newVerts
, error
)
215 # ### CURVE GENERATION ###
217 # set bezierhandles to auto
218 def setBezierHandles(newCurve
):
220 for spline
in newCurve
.data
.splines
:
221 for p
in spline
.bezier_points
:
222 p
.handle_left_type
= 'AUTO'
223 p
.handle_right_type
= 'AUTO'
226 # get array of new coords for new spline from vertindices
227 def vertsToPoints(newVerts
, splineVerts
, splineType
):
231 # array for BEZIER spline output
232 if splineType
== 'BEZIER':
234 newPoints
+= splineVerts
[v
].to_tuple()
236 # array for nonBEZIER output
239 newPoints
+= (splineVerts
[v
].to_tuple())
240 if splineType
== 'NURBS':
241 newPoints
.append(1) # for nurbs w = 1
242 else: # for poly w = 0
247 # ### MAIN OPERATIONS ###
249 def main(context
, obj
, options
, curve_dimension
):
252 degreeOut
= options
[5]
253 keepShort
= options
[7]
254 bpy
.ops
.object.select_all(action
='DESELECT')
255 scene
= context
.scene
256 splines
= obj
.data
.splines
.values()
258 # create curvedatablock
259 curve
= bpy
.data
.curves
.new("Simple_" + obj
.name
, type='CURVE')
260 curve
.dimensions
= curve_dimension
263 for spline_i
, spline
in enumerate(splines
):
264 # test if spline is a long enough
265 if len(spline
.points
) >= 7 or keepShort
:
266 # check what type of spline to create
267 if output
== 'INPUT':
268 splineType
= spline
.type
272 # get vec3 list to simplify
273 if spline
.type == 'BEZIER': # get bezierverts
274 splineVerts
= [splineVert
.co
.copy()
275 for splineVert
in spline
.bezier_points
.values()]
277 else: # verts from all other types of curves
278 splineVerts
= [splineVert
.co
.to_3d()
279 for splineVert
in spline
.points
.values()]
281 # simplify spline according to mode
282 if mode
== 'DISTANCE':
283 newVerts
= simplify_RDP(splineVerts
, options
)
285 if mode
== 'CURVATURE':
286 newVerts
= simplypoly(splineVerts
, options
)
288 # convert indices into vectors3D
289 newPoints
= vertsToPoints(newVerts
, splineVerts
, splineType
)
292 newSpline
= curve
.splines
.new(type=splineType
)
294 # put newPoints into spline according to type
295 if splineType
== 'BEZIER':
296 newSpline
.bezier_points
.add(int(len(newPoints
) * 0.33))
297 newSpline
.bezier_points
.foreach_set('co', newPoints
)
299 newSpline
.points
.add(int(len(newPoints
) * 0.25 - 1))
300 newSpline
.points
.foreach_set('co', newPoints
)
302 # set degree of outputNurbsCurve
303 if output
== 'NURBS':
304 newSpline
.order_u
= degreeOut
307 newSpline
.use_endpoint_u
= spline
.use_endpoint_u
309 # create new object and put into scene
310 newCurve
= bpy
.data
.objects
.new("Simple_" + obj
.name
, curve
)
311 coll
= context
.view_layer
.active_layer_collection
.collection
312 coll
.objects
.link(newCurve
)
313 newCurve
.select_set(True)
315 context
.view_layer
.objects
.active
= newCurve
316 newCurve
.matrix_world
= obj
.matrix_world
318 # set bezierhandles to auto
319 setBezierHandles(newCurve
)
324 # get preoperator fcurves
325 def getFcurveData(obj
):
327 for fc
in obj
.animation_data
.action
.fcurves
:
329 fcVerts
= [vcVert
.co
.to_3d()
330 for vcVert
in fc
.keyframe_points
.values()]
331 fcurves
.append(fcVerts
)
335 def selectedfcurves(obj
):
337 for i
, fc
in enumerate(obj
.animation_data
.action
.fcurves
):
339 fcurves_sel
.append(fc
)
344 def fcurves_simplify(context
, obj
, options
, fcurves
):
348 # get indices of selected fcurves
349 fcurve_sel
= selectedfcurves(obj
)
352 for fcurve_i
, fcurve
in enumerate(fcurves
):
353 # test if fcurve is long enough
355 # simplify spline according to mode
356 if mode
== 'DISTANCE':
357 newVerts
= simplify_RDP(fcurve
, options
)
359 if mode
== 'CURVATURE':
360 newVerts
= simplypoly(fcurve
, options
)
362 # convert indices into vectors3D
365 # this is different from the main() function for normal curves, different api...
367 newPoints
.append(fcurve
[v
])
369 # remove all points from curve first
370 for i
in range(len(fcurve
) - 1, 0, -1):
371 fcurve_sel
[fcurve_i
].keyframe_points
.remove(fcurve_sel
[fcurve_i
].keyframe_points
[i
])
372 # put newPoints into fcurve
374 fcurve_sel
[fcurve_i
].keyframe_points
.insert(frame
=v
[0], value
=v
[1])
378 # ### MENU append ###
380 def menu_func(self
, context
):
381 self
.layout
.operator("graph.simplify")
384 def menu(self
, context
):
385 self
.layout
.operator("curve.simplify", text
="Curve Simplify", icon
="CURVE_DATA")
388 # ### ANIMATION CURVES OPERATOR ###
390 class GRAPH_OT_simplify(Operator
):
391 bl_idname
= "graph.simplify"
392 bl_label
= "Simplify F-Curves"
393 bl_description
= ("Simplify selected Curves\n"
394 "Does not operate on short Splines (less than 6 points)")
395 bl_options
= {'REGISTER', 'UNDO'}
399 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
400 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')]
403 description
="Choose algorithm to use",
406 k_thresh
: FloatProperty(
409 default
=0, precision
=3,
410 description
="Threshold"
412 pointsNr
: IntProperty(
417 description
="Degree of curve to get averaged curvatures"
419 error
: FloatProperty(
421 description
="Maximum allowed distance error",
422 min=0.0, soft_min
=0.0,
423 default
=0, precision
=3
425 degreeOut
: IntProperty(
430 description
="Degree of new curve"
432 dis_error
: FloatProperty(
433 name
="Distance error",
434 description
="Maximum allowed distance error in Blender Units",
436 default
=0.0, precision
=3
440 def draw(self
, context
):
442 col
= layout
.column()
444 col
.label(text
="Distance Error:")
445 col
.prop(self
, "error", expand
=True)
448 def poll(cls
, context
):
450 obj
= context
.active_object
453 animdata
= obj
.animation_data
455 act
= animdata
.action
457 fcurves
= act
.fcurves
458 return (obj
and fcurves
)
460 def execute(self
, context
):
471 obj
= context
.active_object
474 self
.fcurves
= getFcurveData(obj
)
476 fcurves_simplify(context
, obj
, options
, self
.fcurves
)
481 # ### Curves OPERATOR ###
482 class CURVE_OT_simplify(Operator
):
483 bl_idname
= "curve.simplify"
484 bl_label
= "Simplify Curves"
485 bl_description
= ("Simplify the existing Curve based upon the chosen settings\n"
486 "Notes: Needs an existing Curve object,\n"
487 "Outputs a new Curve with the Simple prefix in the name")
488 bl_options
= {'REGISTER', 'UNDO'}
492 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
493 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')
497 description
="Choose algorithm to use",
501 ('INPUT', 'Input', 'Same type as input spline'),
502 ('NURBS', 'Nurbs', 'NURBS'),
503 ('BEZIER', 'Bezier', 'BEZIER'),
504 ('POLY', 'Poly', 'POLY')
506 output
: EnumProperty(
507 name
="Output splines",
508 description
="Type of splines to output",
511 k_thresh
: FloatProperty(
514 default
=0, precision
=3,
515 description
="Threshold"
517 pointsNr
: IntProperty(
522 description
="Degree of curve to get averaged curvatures"
524 error
: FloatProperty(
526 description
="Maximum allowed distance error in Blender Units",
528 default
=0.0, precision
=3
530 degreeOut
: IntProperty(
535 description
="Degree of new curve"
537 dis_error
: FloatProperty(
538 name
="Distance error",
539 description
="Maximum allowed distance error in Blender Units",
543 keepShort
: BoolProperty(
544 name
="Keep short splines",
545 description
="Keep short splines (less than 7 points)",
549 def draw(self
, context
):
551 col
= layout
.column()
553 col
.label(text
="Distance Error:")
554 col
.prop(self
, "error", expand
=True)
555 col
.prop(self
, "output", text
="Output", icon
="OUTLINER_OB_CURVE")
556 if self
.output
== "NURBS":
557 col
.prop(self
, "degreeOut", expand
=True)
559 col
.prop(self
, "keepShort", expand
=True)
562 def poll(cls
, context
):
563 obj
= context
.active_object
564 return (obj
and obj
.type == 'CURVE')
566 def execute(self
, context
):
578 bpy
.ops
.object.mode_set(mode
='OBJECT')
579 obj
= context
.active_object
580 curve_dimension
= obj
.data
.dimensions
582 main(context
, obj
, options
, curve_dimension
)
583 except Exception as e
:
584 error_handlers(self
, "curve.simplify", e
, "Simplify Curves")
589 ## Initial use Curve Remove Doubles ##
591 def main_rd(context
, distance
= 0.01):
593 obj
= context
.active_object
596 for spline
in obj
.data
.splines
:
597 if len(spline
.bezier_points
) > 1:
598 for i
in range(0, len(spline
.bezier_points
)):
601 ii
= len(spline
.bezier_points
) - 1
605 dot
= spline
.bezier_points
[i
];
606 dot1
= spline
.bezier_points
[ii
];
608 while dot1
in dellist
and i
!= ii
:
611 ii
= len(spline
.bezier_points
)-1
612 dot1
= spline
.bezier_points
[ii
]
614 if dot
.select_control_point
and dot1
.select_control_point
and (i
!=0 or spline
.use_cyclic_u
):
616 if (dot
.co
-dot1
.co
).length
< distance
:
617 # remove points and recreate hangles
618 dot1
.handle_right_type
= "FREE"
619 dot1
.handle_right
= dot
.handle_right
620 dot1
.co
= (dot
.co
+ dot1
.co
) / 2
624 # Handles that are on main point position converts to vector,
625 # if next handle are also vector
626 if dot
.handle_left_type
== 'VECTOR' and (dot1
.handle_right
- dot1
.co
).length
< distance
:
627 dot1
.handle_right_type
= "VECTOR"
628 if dot1
.handle_right_type
== 'VECTOR' and (dot
.handle_left
- dot
.co
).length
< distance
:
629 dot
.handle_left_type
= "VECTOR"
633 bpy
.ops
.curve
.select_all(action
= 'DESELECT')
636 dot
.select_control_point
= True
640 bpy
.ops
.curve
.delete(type = 'VERT')
642 bpy
.ops
.curve
.select_all(action
= 'SELECT')
648 class Curve_OT_CurveRemvDbs(bpy
.types
.Operator
):
649 """Merge consecutive points that are near to each other"""
650 bl_idname
= 'curve.remove_doubles'
651 bl_label
= 'Merge By Distance'
652 bl_options
= {'REGISTER', 'UNDO'}
654 distance
: bpy
.props
.FloatProperty(name
= 'Distance', default
= 0.01, min = 0.0001, max = 10.0, step
= 1)
657 def poll(cls
, context
):
658 obj
= context
.active_object
659 return (obj
and obj
.type == 'CURVE')
661 def execute(self
, context
):
662 removed
=main_rd(context
, self
.distance
)
663 self
.report({'INFO'}, "Removed %d bezier points" % removed
)
666 def menu_func_rd(self
, context
):
667 self
.layout
.operator(Curve_OT_CurveRemvDbs
.bl_idname
, text
='Merge By Distance')
673 Curve_OT_CurveRemvDbs
,
678 from bpy
.utils
import register_class
682 bpy
.types
.GRAPH_MT_channel
.append(menu_func
)
683 bpy
.types
.DOPESHEET_MT_channel
.append(menu_func
)
684 bpy
.types
.VIEW3D_MT_curve_add
.append(menu
)
685 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.prepend(menu
)
686 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.prepend(menu_func_rd
)
690 from bpy
.utils
import unregister_class
691 for cls
in reversed(classes
):
692 unregister_class(cls
)
694 bpy
.types
.GRAPH_MT_channel
.remove(menu_func
)
695 bpy
.types
.DOPESHEET_MT_channel
.remove(menu_func
)
696 bpy
.types
.VIEW3D_MT_curve_add
.remove(menu
)
697 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.remove(menu
)
698 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.remove(menu_func_rd
)
700 if __name__
== "__main__":