Hopefully a working merge with trunk (could be one error left in raytrace.c - will...
[blender.git] / release / scripts / image_auto_layout.py
1 #!BPY
2
3 """
4 Name: 'Consolidate into one image'
5 Blender: 243
6 Group: 'Image'
7 Tooltip: 'Pack all texture images into 1 image and remap faces.'
8 """
9
10 __author__ = "Campbell Barton"
11 __url__ = ("blender", "blenderartists.org")
12 __version__ = "1.1a 2009/04/01"
13
14 __bpydoc__ = """\
15 This script makes a new image from the used areas of all the images mapped to the selected mesh objects.
16 Image are packed into 1 new image that is assigned to the original faces.
17 This is usefull for game models where 1 image is faster then many, and saves the labour of manual texture layout in an image editor.
18
19 """
20 # -------------------------------------------------------------------------- 
21 # Auto Texture Layout v1.0 by Campbell Barton (AKA Ideasman)
22 # -------------------------------------------------------------------------- 
23 # ***** BEGIN GPL LICENSE BLOCK ***** 
24
25 # This program is free software; you can redistribute it and/or 
26 # modify it under the terms of the GNU General Public License 
27 # as published by the Free Software Foundation; either version 2 
28 # of the License, or (at your option) any later version. 
29
30 # This program is distributed in the hope that it will be useful, 
31 # but WITHOUT ANY WARRANTY; without even the implied warranty of 
32 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
33 # GNU General Public License for more details. 
34
35 # You should have received a copy of the GNU General Public License 
36 # along with this program; if not, write to the Free Software Foundation, 
37 # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 
38
39 # ***** END GPL LICENCE BLOCK ***** 
40 # -------------------------------------------------------------------------- 
41
42
43 # Function to find all the images we use
44 import Blender as B
45 from Blender.Mathutils import Vector, RotationMatrix
46 from Blender.Scene import Render
47 import BPyMathutils
48 BIGNUM= 1<<30
49 TEXMODE= B.Mesh.FaceModes.TEX
50
51 def pointBounds(points):
52         '''
53         Takes a list of points and returns the
54         area, center, bounds
55         '''
56         ymax= xmax= -BIGNUM
57         ymin= xmin=  BIGNUM
58         
59         for p in points:
60                 x= p.x
61                 y= p.y
62                 
63                 if x>xmax: xmax=x
64                 if y>ymax: ymax=y
65                 
66                 if x<xmin: xmin=x
67                 if y<ymin: ymin=y
68         
69         # area and center       
70         return\
71         (xmax-xmin) * (ymax-ymin),\
72         Vector((xmin+xmax)/2, (ymin+ymax)/2),\
73         (xmin, ymin, xmax, ymax)
74         
75
76 def bestBoundsRotation(current_points):
77         '''
78         Takes a list of points and returns the best rotation for those points
79         so they fit into the samllest bounding box
80         '''
81         
82         current_area, cent, bounds= pointBounds(current_points)
83         
84         total_rot_angle= 0.0
85         rot_angle= 45
86         while rot_angle > 0.1:
87                 mat_pos= RotationMatrix( rot_angle, 2)
88                 mat_neg= RotationMatrix( -rot_angle, 2)
89                 
90                 new_points_pos= [v*mat_pos for v in current_points]
91                 area_pos, cent_pos, bounds_pos= pointBounds(new_points_pos)
92                 
93                 # 45d rotations only need to be tested in 1 direction.
94                 if rot_angle == 45: 
95                         area_neg= area_pos
96                 else:
97                         new_points_neg= [v*mat_neg for v in current_points]
98                         area_neg, cent_neg, bounds_neg= pointBounds(new_points_neg)
99                 
100                 
101                 # Works!
102                 #print 'Testing angle', rot_angle, current_area, area_pos, area_neg
103                 
104                 best_area= min(area_pos, area_neg, current_area)
105                 if area_pos == best_area:
106                         current_area= area_pos
107                         cent= cent_pos
108                         bounds= bounds_pos
109                         current_points= new_points_pos
110                         total_rot_angle+= rot_angle
111                 elif rot_angle != 45 and area_neg == best_area:
112                         current_area= area_neg
113                         cent= cent_neg
114                         bounds= bounds_neg
115                         current_points= new_points_neg
116                         total_rot_angle-= rot_angle
117                 
118                 rot_angle *= 0.5
119         
120         # Return the optimal rotation.
121         return total_rot_angle
122
123
124 class faceGroup(object):
125         '''
126         A Group of faces that all use the same image, each group has its UVs packed into a square.
127         '''
128         __slots__= 'xmax', 'ymax', 'xmin', 'ymin',\
129         'image', 'faces', 'box_pack', 'size', 'ang', 'rot_mat', 'cent'\
130         
131         def __init__(self, mesh_list, image, size, PREF_IMAGE_MARGIN):
132                 self.image= image
133                 self.size= size
134                 self.faces= [f for me in mesh_list for f in me.faces if f.mode & TEXMODE and f.image == image]
135                 
136                 # Find the best rotation.
137                 all_points= [uv for f in self.faces for uv in f.uv]
138                 bountry_indicies= BPyMathutils.convexHull(all_points)
139                 bountry_points= [all_points[i] for i in bountry_indicies]
140                 
141                 # Pre Rotation bounds
142                 self.cent= pointBounds(bountry_points)[1]
143                 
144                 # Get the optimal rotation angle
145                 self.ang= bestBoundsRotation(bountry_points)
146                 self.rot_mat= RotationMatrix(self.ang, 2), RotationMatrix(-self.ang, 2)
147                 
148                 # Post rotation bounds
149                 bounds= pointBounds([\
150                 ((uv-self.cent) * self.rot_mat[0]) + self.cent\
151                 for uv in bountry_points])[2]
152                 
153                 # Break the bounds into useable values.
154                 xmin, ymin, xmax, ymax= bounds
155                 
156                 # Store the bounds, include the margin.
157                 # The bounds rect will need to be rotated to the rotation angle.
158                 self.xmax= xmax + (PREF_IMAGE_MARGIN/size[0])
159                 self.xmin= xmin - (PREF_IMAGE_MARGIN/size[0])
160                 self.ymax= ymax + (PREF_IMAGE_MARGIN/size[1])
161                 self.ymin= ymin - (PREF_IMAGE_MARGIN/size[1])
162                 
163                 self.box_pack=[\
164                 0.0, 0.0,\
165                 size[0]*(self.xmax - self.xmin),\
166                 size[1]*(self.ymax - self.ymin),\
167                 image.name] 
168                 
169         '''
170                 # default.
171                 self.scale= 1.0
172
173         def set_worldspace_scale(self):
174                 scale_uv= 0.0
175                 scale_3d= 0.0
176                 for f in self.faces:
177                         for i in xrange(len(f.v)):
178                                 scale_uv+= (f.uv[i]-f.uv[i-1]).length * 0.1
179                                 scale_3d+= (f.v[i].co-f.v[i-1].co).length * 0.1
180                 self.scale= scale_3d/scale_uv
181         '''
182                 
183                 
184         
185         def move2packed(self, width, height):
186                 '''
187                 Moves the UV coords to their packed location
188                 using self.box_pack as the offset, scaler.
189                 box_pack must be set to its packed location.
190                 width and weight are the w/h of the overall packed area's bounds.
191                 '''
192                 # packedLs is a list of [(anyUniqueID, left, bottom, width, height)...]
193                 # Width and height in float pixel space.
194                 
195                 # X Is flipped :/
196                 #offset_x= (1-(self.box_pack[1]/d)) - (((self.xmax-self.xmin) * self.image.size[0])/d)
197                 offset_x= self.box_pack[0]/width
198                 offset_y= self.box_pack[1]/height
199                 
200                 for f in self.faces:
201                         for uv in f.uv:
202                                 uv_rot= ((uv-self.cent) * self.rot_mat[0]) + self.cent
203                                 uv.x= offset_x+ (((uv_rot.x-self.xmin) * self.size[0])/width)
204                                 uv.y= offset_y+ (((uv_rot.y-self.ymin) * self.size[1])/height)
205
206 def consolidate_mesh_images(mesh_list, scn, PREF_IMAGE_PATH, PREF_IMAGE_SIZE, PREF_KEEP_ASPECT, PREF_IMAGE_MARGIN): #, PREF_SIZE_FROM_UV=True):
207         '''
208         Main packing function
209         
210         All meshes from mesh_list must have faceUV else this function will fail.
211         '''
212         face_groups= {}
213         
214         for me in mesh_list:
215                 for f in me.faces:
216                         if f.mode & TEXMODE:
217                                 image= f.image
218                                 if image:
219                                         try:
220                                                 face_groups[image.name] # will fail if teh groups not added.
221                                         except:
222                                                 try:
223                                                         size= image.size
224                                                 except:
225                                                         B.Draw.PupMenu('Aborting: Image cold not be loaded|' + image.name)
226                                                         return
227                                                         
228                                                 face_groups[image.name]= faceGroup(mesh_list, image, size, PREF_IMAGE_MARGIN)
229         
230         if not face_groups:
231                 B.Draw.PupMenu('No Images found in mesh(es). Aborting!')
232                 return
233         
234         if len(face_groups)<2:
235                 B.Draw.PupMenu('Only 1 image found|Select a mesh(es) using 2 or more images.')
236                 return
237                 
238         '''
239         if PREF_SIZE_FROM_UV:
240                 for fg in face_groups.itervalues():
241                         fg.set_worldspace_scale()
242         '''
243         
244         # RENDER THE FACES.
245         render_scn= B.Scene.New()
246         render_scn.makeCurrent()
247         render_context= render_scn.getRenderingContext()
248         render_context.setRenderPath('') # so we can ignore any existing path and save to the abs path.
249         
250         PREF_IMAGE_PATH_EXPAND= B.sys.expandpath(PREF_IMAGE_PATH) + '.png'
251         
252         # TEST THE FILE WRITING.
253         try:
254                 # Can we write to this file???
255                 f= open(PREF_IMAGE_PATH_EXPAND, 'w')
256                 f.close()
257         except:
258                 B.Draw.PupMenu('Error%t|Could not write to path|' + PREF_IMAGE_PATH_EXPAND)
259                 return
260         
261         render_context.imageSizeX(PREF_IMAGE_SIZE)
262         render_context.imageSizeY(PREF_IMAGE_SIZE)
263         render_context.enableOversampling(True) 
264         render_context.setOversamplingLevel(16) 
265         render_context.setRenderWinSize(100)
266         render_context.setImageType(Render.PNG)
267         render_context.enableExtensions(True) 
268         render_context.enableSky() # No alpha needed.
269         render_context.enableRGBColor()
270         render_context.threads = 2
271         
272         #Render.EnableDispView() # Broken??
273         
274         # New Mesh and Object
275         render_mat= B.Material.New()
276         render_mat.mode |= \
277                         B.Material.Modes.SHADELESS | \
278                         B.Material.Modes.TEXFACE | \
279                         B.Material.Modes.TEXFACE_ALPHA | \
280                         B.Material.Modes.ZTRANSP
281         
282         render_mat.setAlpha(0.0)
283                 
284         render_me= B.Mesh.New()
285         render_me.verts.extend([Vector(0,0,0)]) # Stupid, dummy vert, preverts errors. when assigning UV's/
286         render_ob= B.Object.New('Mesh')
287         render_ob.link(render_me)
288         render_scn.link(render_ob)
289         render_me.materials= [render_mat]
290         
291         
292         # New camera and object
293         render_cam_data= B.Camera.New('ortho')
294         render_cam_ob= B.Object.New('Camera')
295         render_cam_ob.link(render_cam_data)
296         render_scn.link(render_cam_ob)
297         render_scn.objects.camera = render_cam_ob
298         
299         render_cam_data.type= 'ortho'
300         render_cam_data.scale= 1.0
301         
302         
303         # Position the camera
304         render_cam_ob.LocZ= 1.0
305         render_cam_ob.LocX= 0.5
306         render_cam_ob.LocY= 0.5
307         
308         # List to send to to boxpack function.
309         boxes2Pack= [ fg.box_pack for fg in face_groups.itervalues()]
310         packWidth, packHeight = B.Geometry.BoxPack2D(boxes2Pack)
311         
312         if PREF_KEEP_ASPECT:
313                 packWidth= packHeight= max(packWidth, packHeight)
314         
315         
316         # packedLs is a list of [(anyUniqueID, left, bottom, width, height)...]
317         # Re assign the face groups boxes to the face_group.
318         for box in boxes2Pack:
319                 face_groups[ box[4] ].box_pack= box # box[4] is the ID (image name)
320         
321         
322         # Add geometry to the mesh
323         for fg in face_groups.itervalues():
324                 # Add verts clockwise from the bottom left.
325                 _x= fg.box_pack[0] / packWidth
326                 _y= fg.box_pack[1] / packHeight
327                 _w= fg.box_pack[2] / packWidth
328                 _h= fg.box_pack[3] / packHeight
329                 
330                 render_me.verts.extend([\
331                 Vector(_x, _y, 0),\
332                 Vector(_x, _y +_h, 0),\
333                 Vector(_x + _w, _y +_h, 0),\
334                 Vector(_x + _w, _y, 0),\
335                 ])
336                 
337                 render_me.faces.extend([\
338                 render_me.verts[-1],\
339                 render_me.verts[-2],\
340                 render_me.verts[-3],\
341                 render_me.verts[-4],\
342                 ])
343                 
344                 target_face= render_me.faces[-1]
345                 target_face.image= fg.image
346                 target_face.mode |= TEXMODE
347                 
348                 # Set the UV's, we need to flip them HOZ?
349                 target_face.uv[0].x= target_face.uv[1].x= fg.xmax
350                 target_face.uv[2].x= target_face.uv[3].x= fg.xmin
351                 
352                 target_face.uv[0].y= target_face.uv[3].y= fg.ymin
353                 target_face.uv[1].y= target_face.uv[2].y= fg.ymax
354                 
355                 for uv in target_face.uv:
356                         uv_rot= ((uv-fg.cent) * fg.rot_mat[1]) + fg.cent
357                         uv.x= uv_rot.x
358                         uv.y= uv_rot.y
359         
360         render_context.render()
361         Render.CloseRenderWindow()
362         render_context.saveRenderedImage(PREF_IMAGE_PATH_EXPAND)
363         
364         #if not B.sys.exists(PREF_IMAGE_PATH_EXPAND):
365         #       raise 'Error!!!'
366         
367         
368         # NOW APPLY THE SAVED IMAGE TO THE FACES!
369         #print PREF_IMAGE_PATH_EXPAND
370         try:
371                 target_image= B.Image.Load(PREF_IMAGE_PATH_EXPAND)
372         except:
373                 B.Draw.PupMenu('Error: Could not render or load the image at path|' + PREF_IMAGE_PATH_EXPAND)
374                 return
375         
376         # Set to the 1 image.
377         for me in mesh_list:
378                 for f in me.faces:
379                         if f.mode & TEXMODE and f.image:
380                                 f.image= target_image
381         
382         for fg in face_groups.itervalues():
383                 fg.move2packed(packWidth, packHeight)
384         
385         scn.makeCurrent()
386         render_me.verts= None # free a tiny amount of memory.
387         B.Scene.Unlink(render_scn)
388         target_image.makeCurrent()
389
390
391 def main():
392         scn= B.Scene.GetCurrent()
393         scn_objects = scn.objects
394         ob= scn_objects.active
395         
396         if not ob or ob.type != 'Mesh':
397                 B.Draw.PupMenu('Error, no active mesh object, aborting.')
398                 return
399         
400         # Create the variables.
401         # Filename without path or extension.
402         newpath= B.Get('filename').split('/')[-1].split('\\')[-1].replace('.blend', '')
403         
404         PREF_IMAGE_PATH = B.Draw.Create('//%s_grp' % newpath)
405         PREF_IMAGE_SIZE = B.Draw.Create(1024)
406         PREF_IMAGE_MARGIN = B.Draw.Create(6)
407         PREF_KEEP_ASPECT = B.Draw.Create(0)
408         PREF_ALL_SEL_OBS = B.Draw.Create(0)
409         
410         pup_block = [\
411         'Image Path: (no ext)',\
412         ('', PREF_IMAGE_PATH, 3, 100, 'Path to new Image. "//" for curent blend dir.'),\
413         'Image Options',
414         ('Pixel Size:', PREF_IMAGE_SIZE, 64, 4096, 'Image Width and Height.'),\
415         ('Pixel Margin:', PREF_IMAGE_MARGIN, 0, 64, 'Use a margin to stop mipmapping artifacts.'),\
416         ('Keep Aspect', PREF_KEEP_ASPECT, 'If disabled, will stretch the images to the bounds of the texture'),\
417         'Texture Source',\
418         ('All Sel Objects', PREF_ALL_SEL_OBS, 'Combine all selected objects into 1 texture, otherwise active object only.'),\
419         ]
420         
421         if not B.Draw.PupBlock('Consolidate images...', pup_block):
422                 return
423         
424         PREF_IMAGE_PATH= PREF_IMAGE_PATH.val
425         PREF_IMAGE_SIZE= PREF_IMAGE_SIZE.val
426         PREF_IMAGE_MARGIN= float(PREF_IMAGE_MARGIN.val) # important this is a float otherwise division wont work properly
427         PREF_KEEP_ASPECT= PREF_KEEP_ASPECT.val
428         PREF_ALL_SEL_OBS= PREF_ALL_SEL_OBS.val
429         
430         if PREF_ALL_SEL_OBS:
431                 mesh_list= [ob.getData(mesh=1) for ob in scn_objects.context if ob.type=='Mesh']
432                 # Make sure we have no doubles- dict by name, then get the values back.
433                 
434                 for me in mesh_list: me.tag = False
435                 
436                 mesh_list_new = []
437                 for me in mesh_list:
438                         if me.faceUV and me.tag==False:
439                                 me.tag = True
440                                 mesh_list_new.append(me)
441                 
442                 # replace list with possible doubles
443                 mesh_list = mesh_list_new
444                 
445         else:
446                 mesh_list= [ob.getData(mesh=1)]
447                 if not mesh_list[0].faceUV:
448                         B.Draw.PupMenu('Error, active mesh has no images, Aborting!')
449                         return
450         
451         consolidate_mesh_images(mesh_list, scn, PREF_IMAGE_PATH, PREF_IMAGE_SIZE, PREF_KEEP_ASPECT, PREF_IMAGE_MARGIN)
452         B.Window.RedrawAll()
453         
454 if __name__=='__main__':
455         main()