2.5: Top Menu
[blender.git] / release / scripts / mesh_cleanup.py
1 #!BPY
2 """
3 Name: 'Clean Meshes'
4 Blender: 245
5 Group: 'Mesh'
6 Tooltip: 'Clean unused data from all selected mesh objects.'
7 """
8
9 __author__ = "Campbell Barton aka ideasman42"
10 __url__ = ["www.blender.org", "blenderartists.org", "www.python.org"]
11 __version__ = "0.1"
12 __bpydoc__ = """\
13 Clean Meshes
14
15 Cleans unused data from selected meshes
16 """
17
18 # ***** BEGIN GPL LICENSE BLOCK *****
19 #
20 # Script copyright (C) Campbell J Barton
21 #
22 # This program is free software; you can redistribute it and/or
23 # modify it under the terms of the GNU General Public License
24 # as published by the Free Software Foundation; either version 2
25 # of the License, or (at your option) any later version.
26 #
27 # This program is distributed in the hope that it will be useful,
28 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
30 # GNU General Public License for more details.
31 #
32 # You should have received a copy of the GNU General Public License
33 # along with this program; if not, write to the Free Software Foundation,
34 # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
35 #
36 # ***** END GPL LICENCE BLOCK *****
37 # --------------------------------------------------------------------------
38
39
40 from Blender import *
41 import bpy
42 from Blender.Mathutils import TriangleArea
43
44 import Blender
45 import BPyMesh
46 dict2MeshWeight= BPyMesh.dict2MeshWeight
47 meshWeight2Dict= BPyMesh.meshWeight2Dict
48
49 def rem_free_verts(me):
50         vert_users= [0] * len(me.verts)
51         for f in me.faces:
52                 for v in f:
53                         vert_users[v.index]+=1
54         
55         for e in me.edges:
56                 for v in e: # loop on edge verts
57                         vert_users[v.index]+=1
58         
59         verts_free= [i for i, users in enumerate(vert_users) if not users]
60         
61         if verts_free:
62                 pass
63                 me.verts.delete(verts_free)
64         return len(verts_free)
65         
66 def rem_free_edges(me, limit=None):
67         ''' Only remove based on limit if a limit is set, else remove all '''
68         
69         edgeDict= {} # will use a set when python 2.4 is standard.
70         
71         for f in me.faces:
72                 for edkey in f.edge_keys:
73                         edgeDict[edkey] = None
74         
75         edges_free= []
76         for e in me.edges:
77                 if not edgeDict.has_key(e.key):
78                         edges_free.append(e)
79         
80         if limit != None:
81                 edges_free= [e for e in edges_free if e.length <= limit]
82         
83         me.edges.delete(edges_free)
84         return len(edges_free)
85
86 def rem_area_faces(me, limit=0.001):
87         ''' Faces that have an area below the limit '''
88         rem_faces= [f for f in me.faces if f.area <= limit]
89         if rem_faces:
90                 me.faces.delete( 0, rem_faces )
91         return len(rem_faces)
92
93 def rem_perimeter_faces(me, limit=0.001):
94         ''' Faces whos combine edge length is below the limit '''
95         def faceEdLen(f):
96                 v= f.v
97                 if len(v) == 3:
98                         return\
99                         (v[0].co-v[1].co).length +\
100                         (v[1].co-v[2].co).length +\
101                         (v[2].co-v[0].co).length
102                 else: # 4
103                         return\
104                         (v[0].co-v[1].co).length +\
105                         (v[1].co-v[2].co).length +\
106                         (v[2].co-v[3].co).length +\
107                         (v[3].co-v[0].co).length
108         rem_faces= [f for f in me.faces if faceEdLen(f) <= limit]
109         if rem_faces:
110                 me.faces.delete( 0, rem_faces )
111         return len(rem_faces)
112
113 def rem_unused_materials(me):
114         materials= me.materials
115         len_materials= len(materials)
116         if len_materials < 2:
117                 return 0
118                 
119         rem_materials= 0
120         
121         material_users= dict( [(i,0) for i in xrange(len_materials)] )
122         
123         for f in me.faces:
124                 f_mat = f.mat
125                 # Make sure the face index isnt too big. this happens sometimes.
126                 if f_mat >= len_materials:
127                         f_mat = f.mat = 0
128                 material_users[f_mat] += 1
129         
130         # mat_idx_subtract= 0
131         # reindex_mapping= dict( [(i,0) for i in xrange(len_materials)] )
132         
133         reindex_mapping_ls = range(len_materials)
134         for i in range(len_materials-1, -1, -1):
135                 if material_users[i] == 0:
136                         del reindex_mapping_ls[i]
137                         del materials[i]
138                         rem_materials+=1
139         
140         reindex_mapping= {}
141         
142         for i, mat in enumerate(reindex_mapping_ls):
143                 reindex_mapping[mat] = i                
144         
145         for f in me.faces:
146                 f.mat= reindex_mapping[f.mat]
147         
148         me.materials= materials
149         return rem_materials
150
151
152 def rem_free_groups(me, groupNames, vWeightDict):
153         ''' cound how many vert users a group has and remove unused groups '''
154         rem_groups              = 0
155         groupUserDict= dict([(group,0) for group in groupNames])
156         
157         for vertexWeight in vWeightDict:
158                 for group, weight in vertexWeight.iteritems():
159                         groupUserDict[group] += 1
160         
161         i=len(groupNames)
162         while i:
163                 i-=1
164                 group= groupNames[i]
165                 if groupUserDict[group] == 0:
166                         del groupNames[i]
167                         print '\tremoving, vgroup', group
168                         rem_groups+=1
169         return rem_groups
170
171 def rem_zero_weights(me, limit, groupNames, vWeightDict):
172         ''' remove verts from a group when their weight is zero.'''
173         rem_vweight_count= 0
174         for vertexWeight in vWeightDict:
175                 items= vertexWeight.items()
176                 for group, weight in items:
177                         if weight < limit:
178                                 del vertexWeight[group]
179                                 rem_vweight_count+= 1
180
181         return rem_vweight_count
182
183         
184 def normalize_vweight(me, groupNames, vWeightDict):
185         for vertexWeight in vWeightDict:
186                 unit= 0.0
187                 for group, weight in vertexWeight.iteritems():
188                         unit+= weight
189                 
190                 if unit != 1.0 and unit != 0.0:
191                         for group, weight in vertexWeight.iteritems():
192                                 vertexWeight[group]= weight/unit
193
194 def isnan(f):
195         fstring = str(f).lower()
196         if 'nan' in fstring:
197                 return True
198         if 'inf' in fstring:
199                 return True
200         
201         return False
202
203 def fix_nan_verts__internal(me):
204         rem_nan = 0
205         for v in me.verts:
206                 co = v.co
207                 for i in (0,1,2):
208                         if isnan(co[i]):
209                                 co[i] = 0.0
210                                 rem_nan += 1
211         return rem_nan
212
213 def fix_nan_verts(me):
214         rem_nan = 0
215         key = me.key
216         if key:
217                 # Find the object, and get a mesh thats thinked to the oblink.
218                 # this is a bit crap but needed to set the active key.
219                 me_oblink = None
220                 for ob in bpy.data.objects:
221                         me_oblink = ob.getData(mesh=1)
222                         if me_oblink == me:
223                                 me = me_oblink
224                                 break
225                 if not me_oblink:
226                         ob = None
227         
228         if key and ob:
229                 blocks = key.blocks
230                 # print blocks
231                 orig_pin = ob.pinShape
232                 orig_shape = ob.activeShape
233                 orig_relative = key.relative
234                 ob.pinShape = True
235                 for i, block in enumerate(blocks):
236                         ob.activeShape = i+1
237                         ob.makeDisplayList()
238                         rem_nan += fix_nan_verts__internal(me)
239                         me.update(block.name) # get the new verts
240                 ob.pinShape     = orig_pin
241                 ob.activeShape = orig_shape
242                 key.relative = orig_relative
243                 
244         else: # No keys, simple operation
245                 rem_nan = fix_nan_verts__internal(me)
246         
247         return rem_nan
248
249 def fix_nan_uvs(me):
250         rem_nan = 0
251         if me.faceUV:
252                 orig_uvlayer = me.activeUVLayer
253                 for uvlayer in me.getUVLayerNames():
254                         me.activeUVLayer = uvlayer
255                         for f in me.faces:
256                                 for uv in f.uv:
257                                         for i in (0,1):
258                                                 if isnan(uv[i]):
259                                                         uv[i] = 0.0
260                                                         rem_nan += 1
261                 me.activeUVLayer = orig_uvlayer
262         return rem_nan
263
264
265 def has_vcol(me):
266         for f in me.faces:
267                 for col in f.col:
268                         if not (255 == col.r == col.g == col.b):
269                                 return True
270         return False
271
272 def rem_white_vcol_layers(me):
273         vcols_removed = 0
274         if me.vertexColors:
275                 for col in me.getColorLayerNames():
276                         me.activeColorLayer = col
277                         if not has_vcol(me): 
278                                 me.removeColorLayer(col)
279                                 vcols_removed += 1
280         
281         return vcols_removed
282
283
284 def main():     
285         sce= bpy.data.scenes.active
286         obsel= list(sce.objects.context)
287         actob= sce.objects.active
288         
289         is_editmode= Window.EditMode()
290         
291         # Edit mode object is not active, add it to the list.
292         if is_editmode and (not actob.sel):
293                 obsel.append(actob)
294         
295         
296         #====================================#
297         # Popup menu to select the functions #
298         #====================================#
299         
300         CLEAN_ALL_DATA= Draw.Create(0)
301         CLEAN_VERTS_FREE= Draw.Create(1)
302         CLEAN_EDGE_NOFACE= Draw.Create(0)
303         CLEAN_EDGE_SMALL= Draw.Create(0)
304         CLEAN_FACE_PERIMETER= Draw.Create(0)
305         CLEAN_FACE_SMALL= Draw.Create(0)
306         
307         CLEAN_MATERIALS= Draw.Create(0)
308         CLEAN_WHITE_VCOL_LAYERS= Draw.Create(0)
309         CLEAN_GROUP= Draw.Create(0)
310         CLEAN_VWEIGHT= Draw.Create(0)
311         CLEAN_WEIGHT_NORMALIZE= Draw.Create(0)
312         limit= Draw.Create(0.01)
313         
314         CLEAN_NAN_VERTS= Draw.Create(0)
315         CLEAN_NAN_UVS= Draw.Create(0)
316         
317         # Get USER Options
318         
319         pup_block= [\
320         ('Verts: free', CLEAN_VERTS_FREE, 'Remove verts that are not used by an edge or a face.'),\
321         ('Edges: free', CLEAN_EDGE_NOFACE, 'Remove edges that are not in a face.'),\
322         ('Edges: short', CLEAN_EDGE_SMALL, 'Remove edges that are below the length limit.'),\
323         ('Faces: small perimeter', CLEAN_FACE_PERIMETER, 'Remove faces below the perimeter limit.'),\
324         ('Faces: small area', CLEAN_FACE_SMALL, 'Remove faces below the area limit (may remove faces stopping T-face artifacts).'),\
325         ('limit: ', limit, 0.001, 1.0, 'Limit for the area and length tests above (a higher limit will remove more data).'),\
326         ('Material Clean', CLEAN_MATERIALS, 'Remove unused materials.'),\
327         ('Color Layers', CLEAN_WHITE_VCOL_LAYERS, 'Remove vertex color layers that are totaly white'),\
328         ('VGroup Clean', CLEAN_GROUP, 'Remove vertex groups that have no verts using them.'),\
329         ('Weight Clean', CLEAN_VWEIGHT, 'Remove zero weighted verts from groups (limit is zero threshold).'),\
330         ('WeightNormalize', CLEAN_WEIGHT_NORMALIZE, 'Make the sum total of vertex weights accross vgroups 1.0 for each vertex.'),\
331         'Clean NAN values',\
332         ('NAN Verts', CLEAN_NAN_VERTS, 'Make NAN or INF verts (0,0,0)'),\
333         ('NAN UVs', CLEAN_NAN_UVS, 'Make NAN or INF UVs (0,0)'),\
334         '',\
335         ('All Mesh Data', CLEAN_ALL_DATA, 'Warning! Operate on ALL mesh objects in your Blend file. Use with care'),\
336         ]
337         
338         if not Draw.PupBlock('Clean Selected Meshes...', pup_block):
339                 return
340         
341         CLEAN_VERTS_FREE= CLEAN_VERTS_FREE.val
342         CLEAN_EDGE_NOFACE= CLEAN_EDGE_NOFACE.val
343         CLEAN_EDGE_SMALL= CLEAN_EDGE_SMALL.val
344         CLEAN_FACE_PERIMETER= CLEAN_FACE_PERIMETER.val
345         CLEAN_FACE_SMALL= CLEAN_FACE_SMALL.val
346         CLEAN_MATERIALS= CLEAN_MATERIALS.val
347         CLEAN_WHITE_VCOL_LAYERS= CLEAN_WHITE_VCOL_LAYERS.val
348         CLEAN_GROUP= CLEAN_GROUP.val
349         CLEAN_VWEIGHT= CLEAN_VWEIGHT.val
350         CLEAN_WEIGHT_NORMALIZE= CLEAN_WEIGHT_NORMALIZE.val
351         limit= limit.val
352         CLEAN_ALL_DATA= CLEAN_ALL_DATA.val
353         CLEAN_NAN_VERTS= CLEAN_NAN_VERTS.val
354         CLEAN_NAN_UVS= CLEAN_NAN_UVS.val
355         
356         if is_editmode: Window.EditMode(0)
357         
358         if CLEAN_ALL_DATA:
359                 if CLEAN_GROUP or CLEAN_VWEIGHT or CLEAN_WEIGHT_NORMALIZE:
360                         # For groups we need the objects linked to the mesh
361                         meshes= [ob.getData(mesh=1) for ob in bpy.data.objects if ob.type == 'Mesh' if not ob.lib]
362                 else:
363                         meshes= bpy.data.meshes
364         else:
365                 meshes= [ob.getData(mesh=1) for ob in obsel if ob.type == 'Mesh']
366         
367         tot_meshes = len(meshes) # so we can decrement libdata
368         rem_face_count= rem_edge_count= rem_vert_count= rem_material_count= rem_vcol_layer_count= rem_group_count= rem_vweight_count= fix_nan_vcount= fix_nan_uvcount= 0
369         if not meshes:
370                 if is_editmode: Window.EditMode(1)
371                 Draw.PupMenu('No meshes to clean')
372         
373         Blender.Window.WaitCursor(1)
374         bpy.data.meshes.tag = False
375         for me in meshes:
376                 
377                 # Dont touch the same data twice
378                 if me.tag:
379                         tot_meshes -= 1
380                         continue
381                 me.tag = True
382                 
383                 if me.lib:
384                         tot_meshes -= 1
385                         continue
386                 
387                 if me.multires:
388                         multires_level_orig = me.multiresDrawLevel
389                         me.multiresDrawLevel = 1
390                         print 'Warning, cannot perform destructive operations on multires mesh:', me.name
391                 else:
392                         if CLEAN_FACE_SMALL:
393                                 rem_face_count += rem_area_faces(me, limit)
394                                 
395                         if CLEAN_FACE_PERIMETER:
396                                 rem_face_count += rem_perimeter_faces(me, limit)
397                         
398                         if CLEAN_EDGE_SMALL: # for all use 2- remove all edges.
399                                 rem_edge_count += rem_free_edges(me, limit)
400                         
401                         if CLEAN_EDGE_NOFACE:
402                                 rem_edge_count += rem_free_edges(me)
403                         
404                         if CLEAN_VERTS_FREE:
405                                 rem_vert_count += rem_free_verts(me)
406                 
407                 if CLEAN_MATERIALS:
408                         rem_material_count += rem_unused_materials(me)
409                 
410                 if CLEAN_WHITE_VCOL_LAYERS:
411                         rem_vcol_layer_count += rem_white_vcol_layers(me)
412                 
413                 if CLEAN_VWEIGHT or CLEAN_GROUP or CLEAN_WEIGHT_NORMALIZE:
414                         groupNames, vWeightDict= meshWeight2Dict(me)
415                         
416                         if CLEAN_VWEIGHT:
417                                 rem_vweight_count += rem_zero_weights(me, limit, groupNames, vWeightDict)
418                         
419                         if CLEAN_GROUP:
420                                 rem_group_count += rem_free_groups(me, groupNames, vWeightDict)
421                                 pass
422                         
423                         if CLEAN_WEIGHT_NORMALIZE:
424                                 normalize_vweight(me, groupNames, vWeightDict)
425                         
426                         # Copy back to mesh vertex groups.
427                         dict2MeshWeight(me, groupNames, vWeightDict)
428                 
429                 if CLEAN_NAN_VERTS:
430                         fix_nan_vcount = fix_nan_verts(me)
431                         
432                 if CLEAN_NAN_UVS:
433                         fix_nan_uvcount = fix_nan_uvs(me)
434                 
435                 # restore multires.
436                 if me.multires:
437                         me.multiresDrawLevel = multires_level_orig
438                 
439         Blender.Window.WaitCursor(0)
440         if is_editmode: Window.EditMode(0)
441         stat_string= 'Removed from ' + str(tot_meshes) + ' Mesh(es)%t|'
442         
443         if CLEAN_VERTS_FREE:                                                    stat_string+= 'Verts: %i|' % rem_vert_count
444         if CLEAN_EDGE_SMALL or CLEAN_EDGE_NOFACE:               stat_string+= 'Edges: %i|' % rem_edge_count
445         if CLEAN_FACE_SMALL or CLEAN_FACE_PERIMETER:    stat_string+= 'Faces: %i|' % rem_face_count
446         if CLEAN_MATERIALS:                                                             stat_string+= 'Materials: %i|' % rem_material_count
447         if CLEAN_WHITE_VCOL_LAYERS:                                             stat_string+= 'Color Layers: %i|' % rem_vcol_layer_count
448         if CLEAN_VWEIGHT:                                                               stat_string+= 'VWeights: %i|' % rem_vweight_count
449         if CLEAN_GROUP:                                                                 stat_string+= 'VGroups: %i|' % rem_group_count
450         if CLEAN_NAN_VERTS:                                                             stat_string+= 'Vert Nan Fix: %i|' % fix_nan_vcount
451         if CLEAN_NAN_UVS:                                                               stat_string+= 'UV Nan Fix: %i|' % fix_nan_uvcount
452         Draw.PupMenu(stat_string)
453         
454         
455 if __name__ == '__main__':
456         main()