continual script stuff, minor stuff.. fix for bad return in group. Added back group.objects.append/remove but print deprectaed warning. also some epydoc changes.
		
			
				
	
	
		
			436 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			436 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!BPY
 | |
| 
 | |
| """
 | |
| Name: 'Auto Image Layout'
 | |
| Blender: 241
 | |
| Group: 'Image'
 | |
| Tooltip: 'Pack all texture images into 1 image and remap faces.'
 | |
| """
 | |
| 
 | |
| __author__ = "Campbell Barton"
 | |
| __url__ = ("blender", "blenderartists.org")
 | |
| __version__ = "1.0 2005/05/20"
 | |
| 
 | |
| __bpydoc__ = """\
 | |
| This script makes a new image from the used areas of all the images mapped to the selected mesh objects.
 | |
| Image are packed into 1 new image that is assigned to the original faces.
 | |
| 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.
 | |
| 
 | |
| """
 | |
| # -------------------------------------------------------------------------- 
 | |
| # Auto Texture Layout v1.0 by Campbell Barton (AKA Ideasman)
 | |
| # -------------------------------------------------------------------------- 
 | |
| # ***** BEGIN GPL LICENSE BLOCK ***** 
 | |
| # 
 | |
| # This program is free software; you can redistribute it and/or 
 | |
| # modify it under the terms of the GNU General Public License 
 | |
| # as published by the Free Software Foundation; either version 2 
 | |
| # of the License, or (at your option) any later version. 
 | |
| # 
 | |
| # This program is distributed in the hope that it will be useful, 
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 | |
| # GNU General Public License for more details. 
 | |
| # 
 | |
| # You should have received a copy of the GNU General Public License 
 | |
| # along with this program; if not, write to the Free Software Foundation, 
 | |
| # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 
 | |
| # f
 | |
| # ***** END GPL LICENCE BLOCK ***** 
 | |
| # -------------------------------------------------------------------------- 
 | |
| 
 | |
| 
 | |
| # Function to find all the images we use
 | |
| import Blender as B
 | |
| import boxpack2d
 | |
| from Blender.Mathutils import Vector, RotationMatrix
 | |
| from Blender.Scene import Render
 | |
| import BPyMathutils
 | |
| BIGNUM= 1<<30
 | |
| 
 | |
| def pointBounds(points):
 | |
| 	'''
 | |
| 	Takes a list of points and returns the
 | |
| 	area, center, bounds
 | |
| 	'''
 | |
| 	ymax= xmax= -BIGNUM
 | |
| 	ymin= xmin=  BIGNUM
 | |
| 	
 | |
| 	for p in points:
 | |
| 		x= p.x
 | |
| 		y= p.y
 | |
| 		
 | |
| 		if x>xmax: xmax=x
 | |
| 		if y>ymax: ymax=y
 | |
| 		
 | |
| 		if x<xmin: xmin=x
 | |
| 		if y<ymin: ymin=y
 | |
| 	
 | |
| 	# area and center	
 | |
| 	return\
 | |
| 	(xmax-xmin) * (ymax-ymin),\
 | |
| 	Vector((xmin+xmax)/2, (ymin+ymax)/2),\
 | |
| 	(xmin, ymin, xmax, ymax)
 | |
| 	
 | |
| 
 | |
| def bestBoundsRotation(current_points):
 | |
| 	'''
 | |
| 	Takes a list of points and returns the best rotation for those points
 | |
| 	so they fit into the samllest bounding box
 | |
| 	'''
 | |
| 	
 | |
| 	current_area, cent, bounds= pointBounds(current_points)
 | |
| 	
 | |
| 	total_rot_angle= 0.0
 | |
| 	rot_angle= 45
 | |
| 	while rot_angle > 0.1:
 | |
| 		mat_pos= RotationMatrix( rot_angle, 2)
 | |
| 		mat_neg= RotationMatrix( -rot_angle, 2)
 | |
| 		
 | |
| 		new_points_pos= [v*mat_pos for v in current_points]
 | |
| 		area_pos, cent_pos, bounds_pos= pointBounds(new_points_pos)
 | |
| 		
 | |
| 		# 45d rotations only need to be tested in 1 direction.
 | |
| 		if rot_angle == 45: 
 | |
| 			area_neg= area_pos
 | |
| 		else:
 | |
| 			new_points_neg= [v*mat_neg for v in current_points]
 | |
| 			area_neg, cent_neg, bounds_neg= pointBounds(new_points_neg)
 | |
| 		
 | |
| 		
 | |
| 		# Works!
 | |
| 		#print 'Testing angle', rot_angle, current_area, area_pos, area_neg
 | |
| 		
 | |
| 		best_area= min(area_pos, area_neg, current_area)
 | |
| 		if area_pos == best_area:
 | |
| 			current_area= area_pos
 | |
| 			cent= cent_pos
 | |
| 			bounds= bounds_pos
 | |
| 			current_points= new_points_pos
 | |
| 			total_rot_angle+= rot_angle
 | |
| 		elif rot_angle != 45 and area_neg == best_area:
 | |
| 			current_area= area_neg
 | |
| 			cent= cent_neg
 | |
| 			bounds= bounds_neg
 | |
| 			current_points= new_points_neg
 | |
| 			total_rot_angle-= rot_angle
 | |
| 		
 | |
| 		rot_angle *= 0.5
 | |
| 	
 | |
| 	# Return the optimal rotation.
 | |
| 	return total_rot_angle
 | |
| 
 | |
| 
 | |
| class faceGroup(object):
 | |
| 	'''
 | |
| 	A Group of faces that all use the same image, each group has its UVs packed into a square.
 | |
| 	'''
 | |
| 	__slots__= 'xmax', 'ymax', 'xmin', 'ymin',\
 | |
| 	'image', 'faces', 'box_pack', 'size', 'ang', 'rot_mat', 'cent'\
 | |
| 	
 | |
| 	def __init__(self, mesh_list, image, size, PREF_IMAGE_MARGIN):
 | |
| 		self.image= image
 | |
| 		self.size= size
 | |
| 		self.faces= [f for me in mesh_list for f in me.faces if f.image == image]
 | |
| 		
 | |
| 		# Find the best rotation.
 | |
| 		all_points= [uv for f in self.faces for uv in f.uv]
 | |
| 		bountry_indicies= BPyMathutils.convexHull(all_points)
 | |
| 		bountry_points= [all_points[i] for i in bountry_indicies]
 | |
| 		
 | |
| 		# Pre Rotation bounds
 | |
| 		self.cent= pointBounds(bountry_points)[1]
 | |
| 		
 | |
| 		# Get the optimal rotation angle
 | |
| 		self.ang= bestBoundsRotation(bountry_points)
 | |
| 		self.rot_mat= RotationMatrix(self.ang, 2), RotationMatrix(-self.ang, 2)
 | |
| 		
 | |
| 		# Post rotation bounds
 | |
| 		bounds= pointBounds([\
 | |
| 		((uv-self.cent) * self.rot_mat[0]) + self.cent\
 | |
| 		for uv in bountry_points])[2]
 | |
| 		
 | |
| 		# Break the bounds into useable values.
 | |
| 		xmin, ymin, xmax, ymax= bounds
 | |
| 		
 | |
| 		# Store the bounds, include the margin.
 | |
| 		# The bounds rect will need to be rotated to the rotation angle.
 | |
| 		self.xmax= xmax + (PREF_IMAGE_MARGIN/size[0])
 | |
| 		self.xmin= xmin - (PREF_IMAGE_MARGIN/size[0])
 | |
| 		self.ymax= ymax + (PREF_IMAGE_MARGIN/size[1])
 | |
| 		self.ymin= ymin - (PREF_IMAGE_MARGIN/size[1])
 | |
| 		
 | |
| 		self.box_pack=[\
 | |
| 		image.name,\
 | |
| 		size[0]*(self.xmax - self.xmin),\
 | |
| 		size[1]*(self.ymax - self.ymin)] 
 | |
| 		
 | |
| 	'''
 | |
| 		# default.
 | |
| 		self.scale= 1.0
 | |
| 
 | |
| 	def set_worldspace_scale(self):
 | |
| 		scale_uv= 0.0
 | |
| 		scale_3d= 0.0
 | |
| 		for f in self.faces:
 | |
| 			for i in xrange(len(f.v)):
 | |
| 				scale_uv+= (f.uv[i]-f.uv[i-1]).length * 0.1
 | |
| 				scale_3d+= (f.v[i].co-f.v[i-1].co).length * 0.1
 | |
| 		self.scale= scale_3d/scale_uv
 | |
| 	'''
 | |
| 		
 | |
| 		
 | |
| 	
 | |
| 	def move2packed(self, width, height):
 | |
| 		'''
 | |
| 		Moves the UV coords to their packed location
 | |
| 		using self.box_pack as the offset, scaler.
 | |
| 		box_pack must be set to its packed location.
 | |
| 		width and weight are the w/h of the overall packed area's bounds.
 | |
| 		'''
 | |
| 		# packedLs is a list of [(anyUniqueID, left, bottom, width, height)...]
 | |
| 		# Width and height in float pixel space.
 | |
| 		
 | |
| 		# X Is flipped :/
 | |
| 		#offset_x= (1-(self.box_pack[1]/d)) - (((self.xmax-self.xmin) * self.image.size[0])/d)
 | |
| 		offset_x= self.box_pack[1]/width
 | |
| 		offset_y= self.box_pack[2]/height
 | |
| 		
 | |
| 		for f in self.faces:
 | |
| 			for uv in f.uv:
 | |
| 				uv_rot= ((uv-self.cent) * self.rot_mat[0]) + self.cent
 | |
| 				uv.x= offset_x+ (((uv_rot.x-self.xmin) * self.size[0])/width)
 | |
| 				uv.y= offset_y+ (((uv_rot.y-self.ymin) * self.size[1])/height)
 | |
| 
 | |
| def auto_layout_tex(mesh_list, scn, PREF_IMAGE_PATH, PREF_IMAGE_SIZE, PREF_KEEP_ASPECT, PREF_IMAGE_MARGIN): #, PREF_SIZE_FROM_UV=True):
 | |
| 	'''Main packing function'''
 | |
| 	face_groups= {}
 | |
| 	
 | |
| 	for me in mesh_list:
 | |
| 		for f in me.faces:
 | |
| 			image= f.image
 | |
| 			if image:
 | |
| 				try:
 | |
| 					face_groups[image.name] # will fail if teh groups not added.
 | |
| 				except:
 | |
| 					try:
 | |
| 						size= image.size
 | |
| 					except:
 | |
| 						B.Draw.PupMenu('Aborting: Image cold not be loaded|' + image.name)
 | |
| 						return
 | |
| 						
 | |
| 					face_groups[image.name]= faceGroup(mesh_list, image, size, PREF_IMAGE_MARGIN)
 | |
| 	
 | |
| 	if not face_groups:
 | |
| 		B.Draw.PupMenu('No Images found in mesh. aborting.')
 | |
| 		return
 | |
| 	
 | |
| 	if len(face_groups)<2:
 | |
| 		B.Draw.PupMenu('Only 1 image found|Select a mesh using 2 or more images.')
 | |
| 		return
 | |
| 		
 | |
| 	'''
 | |
| 	if PREF_SIZE_FROM_UV:
 | |
| 		for fg in face_groups.itervalues():
 | |
| 			fg.set_worldspace_scale()
 | |
| 	'''
 | |
| 	
 | |
| 	# RENDER THE FACES.
 | |
| 	render_scn= B.Scene.New()
 | |
| 	render_scn.makeCurrent()
 | |
| 	render_context= render_scn.getRenderingContext()
 | |
| 	render_context.setRenderPath('') # so we can ignore any existing path and save to the abs path.
 | |
| 	
 | |
| 	PREF_IMAGE_PATH_EXPAND= B.sys.expandpath(PREF_IMAGE_PATH) + '.png'
 | |
| 	
 | |
| 	# TEST THE FILE WRITING.
 | |
| 	try:
 | |
| 		# Can we write to this file???
 | |
| 		f= open(PREF_IMAGE_PATH_EXPAND, 'w')
 | |
| 		f.close()
 | |
| 	except:
 | |
| 		B.Draw.PupMenu('Error: Could not write to path|' + PREF_IMAGE_PATH_EXPAND)
 | |
| 		return
 | |
| 	
 | |
| 	render_context.imageSizeX(PREF_IMAGE_SIZE)
 | |
| 	render_context.imageSizeY(PREF_IMAGE_SIZE)
 | |
| 	render_context.enableOversampling(True) 
 | |
| 	render_context.setOversamplingLevel(16) 
 | |
| 	render_context.setRenderWinSize(100)
 | |
| 	render_context.setImageType(Render.PNG)
 | |
| 	render_context.enableExtensions(True) 
 | |
| 	render_context.enableSky() # No alpha needed.
 | |
| 	render_context.enableRGBColor()
 | |
| 	
 | |
| 	#Render.EnableDispView() # Broken??
 | |
| 	
 | |
| 	# New Mesh and Object
 | |
| 	render_mat= B.Material.New()
 | |
| 	render_mat.mode |= B.Material.Modes.SHADELESS
 | |
| 	render_mat.mode |= B.Material.Modes.TEXFACE
 | |
| 	
 | |
| 	
 | |
| 	render_me= B.Mesh.New()
 | |
| 	render_me.verts.extend([Vector(0,0,0)]) # Stupid, dummy vert, preverts errors. when assigning UV's/
 | |
| 	render_ob= B.Object.New('Mesh')
 | |
| 	render_ob.link(render_me)
 | |
| 	render_scn.link(render_ob)
 | |
| 	render_me.materials= [render_mat]
 | |
| 	
 | |
| 	
 | |
| 	# New camera and object
 | |
| 	render_cam_data= B.Camera.New('ortho')
 | |
| 	render_cam_ob= B.Object.New('Camera')
 | |
| 	render_cam_ob.link(render_cam_data)
 | |
| 	render_scn.link(render_cam_ob)
 | |
| 	render_scn.setCurrentCamera(render_cam_ob)
 | |
| 	
 | |
| 	render_cam_data.type= 1 # ortho
 | |
| 	render_cam_data.scale= 1.0
 | |
| 	
 | |
| 	
 | |
| 	# Position the camera
 | |
| 	render_cam_ob.LocZ= 1.0
 | |
| 	render_cam_ob.LocX= 0.5
 | |
| 	render_cam_ob.LocY= 0.5
 | |
| 	
 | |
| 	# List to send to to boxpack function.
 | |
| 	boxes2Pack= [ fg.box_pack for fg in face_groups.itervalues()]
 | |
| 	
 | |
| 	packWidth, packHeight, packedLs = boxpack2d.boxPackIter(boxes2Pack)
 | |
| 	
 | |
| 	if PREF_KEEP_ASPECT:
 | |
| 		packWidth= packHeight= max(packWidth, packHeight)
 | |
| 	
 | |
| 	
 | |
| 	# packedLs is a list of [(anyUniqueID, left, bottom, width, height)...]
 | |
| 	# Re assign the face groups boxes to the face_group.
 | |
| 	for box in packedLs:
 | |
| 		face_groups[ box[0] ].box_pack= box # box[0] is the ID (image name)
 | |
| 	
 | |
| 	
 | |
| 	# Add geometry to the mesh
 | |
| 	for fg in face_groups.itervalues():
 | |
| 		# Add verts clockwise from the bottom left.
 | |
| 		_x= fg.box_pack[1] / packWidth
 | |
| 		_y= fg.box_pack[2] / packHeight
 | |
| 		_w= fg.box_pack[3] / packWidth
 | |
| 		_h= fg.box_pack[4] / packHeight
 | |
| 		
 | |
| 		render_me.verts.extend([\
 | |
| 		Vector(_x, _y, 0),\
 | |
| 		Vector(_x, _y +_h, 0),\
 | |
| 		Vector(_x + _w, _y +_h, 0),\
 | |
| 		Vector(_x + _w, _y, 0),\
 | |
| 		])
 | |
| 		
 | |
| 		render_me.faces.extend([\
 | |
| 		render_me.verts[-1],\
 | |
| 		render_me.verts[-2],\
 | |
| 		render_me.verts[-3],\
 | |
| 		render_me.verts[-4],\
 | |
| 		])
 | |
| 		
 | |
| 		target_face= render_me.faces[-1]
 | |
| 		target_face.image= fg.image
 | |
| 		target_face.mode |= B.Mesh.FaceModes.TEX
 | |
| 		
 | |
| 		# Set the UV's, we need to flip them HOZ?
 | |
| 		target_face.uv[0].x= target_face.uv[1].x= fg.xmax
 | |
| 		target_face.uv[2].x= target_face.uv[3].x= fg.xmin
 | |
| 		
 | |
| 		target_face.uv[0].y= target_face.uv[3].y= fg.ymin
 | |
| 		target_face.uv[1].y= target_face.uv[2].y= fg.ymax
 | |
| 		
 | |
| 		for uv in target_face.uv:
 | |
| 			uv_rot= ((uv-fg.cent) * fg.rot_mat[1]) + fg.cent
 | |
| 			uv.x= uv_rot.x
 | |
| 			uv.y= uv_rot.y
 | |
| 			
 | |
| 		# VCOLS
 | |
| 		# Set them white.
 | |
| 		for c in target_face.col:
 | |
| 			c.r= c.g= c.b= 255
 | |
| 	
 | |
| 	render_context.render()
 | |
| 	Render.CloseRenderWindow()
 | |
| 	render_context.saveRenderedImage(PREF_IMAGE_PATH_EXPAND)
 | |
| 	
 | |
| 	#if not B.sys.exists(PREF_IMAGE_PATH_EXPAND):
 | |
| 	#	raise 'Error!!!'
 | |
| 	
 | |
| 	
 | |
| 	# NOW APPLY THE SAVED IMAGE TO THE FACES!
 | |
| 	#print PREF_IMAGE_PATH_EXPAND
 | |
| 	try:
 | |
| 		target_image= B.Image.Load(PREF_IMAGE_PATH_EXPAND)
 | |
| 	except:
 | |
| 		B.Draw.PupMenu('Error: Could not render or load the image at path|' + PREF_IMAGE_PATH_EXPAND)
 | |
| 		return
 | |
| 	
 | |
| 	# Set to the 1 image.
 | |
| 	for me in mesh_list:
 | |
| 		for f in me.faces:
 | |
| 			if f.image:
 | |
| 				f.image= target_image
 | |
| 	
 | |
| 	for fg in face_groups.itervalues():
 | |
| 		fg.move2packed(packWidth, packHeight)
 | |
| 		
 | |
| 	scn.makeCurrent()
 | |
| 	B.Scene.Unlink(render_scn)
 | |
| 	render_me.verts= None # free a tiny amount of memory.
 | |
| 
 | |
| 
 | |
| def main():
 | |
| 	scn= B.Scene.GetCurrent()
 | |
| 	ob= scn.objects.active
 | |
| 	
 | |
| 	if not ob or ob.type != 'Mesh':
 | |
| 		B.Draw.PupMenu('Error, no active mesh object, aborting.')
 | |
| 		return
 | |
| 	
 | |
| 	# Create the variables.
 | |
| 	# Filename without path or extension.
 | |
| 	newpath= B.Get('filename').split('/')[-1].split('\\')[-1].replace('.blend', '')
 | |
| 	
 | |
| 	PREF_IMAGE_PATH = B.Draw.Create('//%s_grp' % newpath)
 | |
| 	PREF_IMAGE_SIZE = B.Draw.Create(1024)
 | |
| 	PREF_IMAGE_MARGIN = B.Draw.Create(6)
 | |
| 	PREF_KEEP_ASPECT = B.Draw.Create(0)
 | |
| 	PREF_ALL_SEL_OBS = B.Draw.Create(0)
 | |
| 	
 | |
| 	pup_block = [\
 | |
| 	'Image Path: (no ext)',\
 | |
| 	('', PREF_IMAGE_PATH, 3, 100, 'Path to new Image. "//" for curent blend dir.'),\
 | |
| 	'Image Options',
 | |
| 	('Pixel Size:', PREF_IMAGE_SIZE, 64, 4096, 'Image Width and Height.'),\
 | |
| 	('Pixel Margin:', PREF_IMAGE_MARGIN, 0, 64, 'Image Width and Height.'),\
 | |
| 	('Keep Image Aspect', PREF_KEEP_ASPECT, 'If disabled, will stretch the images to the bounds of the texture'),\
 | |
| 	'Texture Source',\
 | |
| 	('All Sel Objects', PREF_ALL_SEL_OBS, 'Combine and replace textures from all objects into 1 texture.'),\
 | |
| 	]
 | |
| 	
 | |
| 	if not B.Draw.PupBlock('Auto Texture Layout', pup_block):
 | |
| 		return
 | |
| 	
 | |
| 	PREF_IMAGE_PATH= PREF_IMAGE_PATH.val
 | |
| 	PREF_IMAGE_SIZE= PREF_IMAGE_SIZE.val
 | |
| 	PREF_IMAGE_MARGIN= PREF_IMAGE_MARGIN.val
 | |
| 	PREF_KEEP_ASPECT= PREF_KEEP_ASPECT.val
 | |
| 	PREF_ALL_SEL_OBS= PREF_ALL_SEL_OBS.val
 | |
| 	
 | |
| 	if PREF_ALL_SEL_OBS:
 | |
| 		mesh_list= [ob.getData(mesh=1) for ob in B.Object.GetSelected() if ob.getType()=='Mesh']
 | |
| 		# Make sure we have no doubles- dict by name, then get the values back.
 | |
| 		mesh_list= dict([(me.name, me) for me in mesh_list])
 | |
| 		mesh_list= mesh_list.values()
 | |
| 	else:
 | |
| 		mesh_list= [ob.getData(mesh=1)]
 | |
| 	
 | |
| 	auto_layout_tex(mesh_list, scn, PREF_IMAGE_PATH, PREF_IMAGE_SIZE, PREF_KEEP_ASPECT, PREF_IMAGE_MARGIN)
 | |
| 	B.Window.RedrawAll()
 | |
| 	
 | |
| if __name__=='__main__':
 | |
| 	main()
 |