forked from bartvdbraak/blender
e7767e39bb
More efficient texture usage, packer now rotates the convex hull of the UV's for each image to fit the most image into the smallest rectangle.
443 lines
13 KiB
Python
443 lines
13 KiB
Python
#!BPY
|
|
|
|
"""
|
|
Name: 'Auto Image Layout'
|
|
Blender: 241
|
|
Group: 'UV'
|
|
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.
|
|
#
|
|
# ***** 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
|
|
|
|
|
|
'''
|
|
RotMatStepRotation = []
|
|
rot_angle = 22.5
|
|
while rot_angle > 0.1:
|
|
RotMatStepRotation.append([\
|
|
(rot_angle, RotationMatrix( rot_angle, 2)),\
|
|
(-rot_angle, RotationMatrix( -rot_angle, 2))])
|
|
rot_angle = rot_angle/2.0
|
|
'''
|
|
|
|
def pointBoundsArea(points):
|
|
x= [p.x for p in points] # Lazy use of LC's
|
|
y= [p.y for p in points]
|
|
return (max(x)-min(x)) * (max(y)-min(y))
|
|
|
|
|
|
def bestBoundsRotation(current_points):
|
|
current_area= pointBoundsArea(current_points)
|
|
|
|
main_rot_angle= 0.0
|
|
rot_angle= 45.0
|
|
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]
|
|
new_points_neg= [v*mat_neg for v in current_points]
|
|
area_pos= pointBoundsArea(new_points_pos)
|
|
area_neg= pointBoundsArea(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
|
|
current_points= new_points_pos
|
|
main_rot_angle+= rot_angle
|
|
elif area_neg == best_area:
|
|
current_area= area_neg
|
|
current_points= new_points_neg
|
|
main_rot_angle-= rot_angle
|
|
|
|
rot_angle *= 0.5
|
|
|
|
# Return the optimal rotation.
|
|
return main_rot_angle
|
|
|
|
BIGNUM= 1<<30
|
|
class faceGroup(object):
|
|
__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
|
|
|
|
# Find the best rotation.
|
|
all_points= [Vector(uv) for me in mesh_list for f in me.faces for uv in f.uv if f.image==image]
|
|
boundry_indicies= BPyMathutils.convexHull(all_points)
|
|
bountry_points= [all_points[i] for i in boundry_indicies]
|
|
|
|
# Yay this works.
|
|
self.ang= bestBoundsRotation(bountry_points)
|
|
self.rot_mat= RotationMatrix(self.ang, 2), RotationMatrix(-self.ang, 2),
|
|
|
|
#print 'ANGLE', image.name, ang
|
|
|
|
# Add to our face group and set bounds.
|
|
# Find the centre
|
|
xmin=ymin= BIGNUM
|
|
xmax=ymax= -BIGNUM
|
|
self.faces= []
|
|
for me in mesh_list:
|
|
for f in me.faces:
|
|
if f.image==image:
|
|
self.faces.append(f)
|
|
for uv in f.uv:
|
|
#uv= uv * self.rot_mat
|
|
|
|
xmax= max(xmax, uv.x)
|
|
xmin= min(xmin, uv.x)
|
|
ymax= max(ymax, uv.y)
|
|
ymin= min(ymin, uv.y)
|
|
|
|
self.cent= Vector((xmax-xmin)/2, (ymax+ymin)/2 )
|
|
|
|
# now get the bounds after rotation about the cent.
|
|
xmin=ymin= BIGNUM
|
|
xmax=ymax= -BIGNUM
|
|
for f in self.faces:
|
|
for uv in f.uv:
|
|
uv= ((uv-self.cent) * self.rot_mat[0]) + self.cent
|
|
xmax= max(xmax, uv.x)
|
|
xmin= min(xmin, uv.x)
|
|
ymax= max(ymax, uv.y)
|
|
ymin= min(ymin, uv.y)
|
|
|
|
|
|
# The box pack list is to be passed to the external function "boxpack2d"
|
|
# format is ID, w,h
|
|
|
|
|
|
# Store the bounds, impliment 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):
|
|
|
|
# Get all images used by the mesh
|
|
face_groups= {}
|
|
|
|
for me in mesh_list:
|
|
for f in me.faces:
|
|
if f.image:
|
|
try:
|
|
face_groups[f.image.name] # will fail if teh groups not added.
|
|
except:
|
|
image= f.image
|
|
try:
|
|
size= image.size
|
|
except:
|
|
B.Draw.PupMenu('Aborting: Image cold not be loaded|' + image.name)
|
|
return
|
|
|
|
face_groups[f.image.name]= faceGroup(mesh_list, f.image, size, PREF_IMAGE_MARGIN)
|
|
|
|
if not face_groups:
|
|
B.Draw.PupMenu('No Images found in mesh. aborting.')
|
|
return
|
|
|
|
if len(face_groups)==1:
|
|
B.Draw.PupMenu('Only 1 image found|use meshes 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.endFrame(1)
|
|
render_context.startFrame(1)
|
|
render_context.currentFrame(1)
|
|
render_context.setRenderPath(PREF_IMAGE_PATH)
|
|
|
|
# Set the render context
|
|
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 # set back 1
|
|
render_cam_ob.LocX= 0.5 # set back 1
|
|
render_cam_ob.LocY= 0.5 # set back 1
|
|
#render_cam_ob.RotY= 180 * 0.017453292519943295 # pi/180.0
|
|
|
|
# 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_context.renderAnim()
|
|
Render.CloseRenderWindow()
|
|
|
|
#print 'attempting to save an image', PREF_IMAGE_PATH_EXPAND
|
|
|
|
#render_context.saveRenderedImage(PREF_IMAGE_PATH_EXPAND)
|
|
|
|
#if not B.sys.exists(PREF_IMAGE_PATH):
|
|
# raise 'Error!!!'
|
|
|
|
|
|
# NOW APPLY THE SAVED IMAGE TO THE FACES!
|
|
#print PREF_IMAGE_PATH_EXPAND
|
|
target_image= B.Image.Load(PREF_IMAGE_PATH_EXPAND)
|
|
|
|
# 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)
|
|
|
|
|
|
def main():
|
|
scn= B.Scene.GetCurrent()
|
|
ob= scn.getActiveObject()
|
|
|
|
if not ob or ob.getType() != '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(512)
|
|
PREF_IMAGE_MARGIN = B.Draw.Create(6)
|
|
PREF_KEEP_ASPECT = B.Draw.Create(1)
|
|
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)
|
|
|
|
if __name__=='__main__':
|
|
main()
|