1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 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. 7# 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. 12# 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. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19# <pep8 compliant> 20 21import bpy 22from bpy.types import Operator 23import mathutils 24 25 26class prettyface: 27 __slots__ = ( 28 "uv", 29 "width", 30 "height", 31 "children", 32 "xoff", 33 "yoff", 34 "has_parent", 35 "rot", 36 ) 37 38 def __init__(self, data): 39 self.has_parent = False 40 self.rot = False # only used for triangles 41 self.xoff = 0 42 self.yoff = 0 43 44 if type(data) == list: # list of data 45 self.uv = None 46 47 # join the data 48 if len(data) == 2: 49 # 2 vertical blocks 50 data[1].xoff = data[0].width 51 self.width = data[0].width * 2 52 self.height = data[0].height 53 54 elif len(data) == 4: 55 # 4 blocks all the same size 56 d = data[0].width # dimension x/y are the same 57 58 data[1].xoff += d 59 data[2].yoff += d 60 61 data[3].xoff += d 62 data[3].yoff += d 63 64 self.width = self.height = d * 2 65 66 # else: 67 # print(len(data), data) 68 # raise "Error" 69 70 for pf in data: 71 pf.has_parent = True 72 73 self.children = data 74 75 elif type(data) == tuple: 76 # 2 blender faces 77 # f, (len_min, len_mid, len_max) 78 self.uv = data 79 80 _f1, lens1, lens1ord = data[0] 81 if data[1]: 82 _f2, lens2, lens2ord = data[1] 83 self.width = (lens1[lens1ord[0]] + lens2[lens2ord[0]]) / 2.0 84 self.height = (lens1[lens1ord[1]] + lens2[lens2ord[1]]) / 2.0 85 else: # 1 tri :/ 86 self.width = lens1[0] 87 self.height = lens1[1] 88 89 self.children = [] 90 91 else: # blender face 92 uv_layer = data.id_data.uv_layers.active.data 93 self.uv = [uv_layer[i].uv for i in data.loop_indices] 94 95 # cos = [v.co for v in data] 96 cos = [data.id_data.vertices[v].co for v in data.vertices] # XXX25 97 98 if len(self.uv) == 4: 99 self.width = ((cos[0] - cos[1]).length + (cos[2] - cos[3]).length) / 2.0 100 self.height = ((cos[1] - cos[2]).length + (cos[0] - cos[3]).length) / 2.0 101 else: 102 # ngon, note: 103 # for ngons to calculate the width/height we need to do the 104 # whole projection, unlike other faces 105 # we store normalized UV's in the faces coords to avoid 106 # calculating the projection and rotating it twice. 107 108 no = data.normal 109 r = no.rotation_difference(mathutils.Vector((0.0, 0.0, 1.0))) 110 cos_2d = [(r @ co).xy for co in cos] 111 # print(cos_2d) 112 angle = mathutils.geometry.box_fit_2d(cos_2d) 113 114 mat = mathutils.Matrix.Rotation(angle, 2) 115 cos_2d = [(mat @ co) for co in cos_2d] 116 xs = [co.x for co in cos_2d] 117 ys = [co.y for co in cos_2d] 118 119 xmin = min(xs) 120 ymin = min(ys) 121 xmax = max(xs) 122 ymax = max(ys) 123 124 xspan = xmax - xmin 125 yspan = ymax - ymin 126 127 self.width = xspan 128 self.height = yspan 129 130 # ngons work different, we store projected result 131 # in UV's to avoid having to re-project later. 132 for i, co in enumerate(cos_2d): 133 self.uv[i][:] = ((co.x - xmin) / xspan, 134 (co.y - ymin) / yspan) 135 136 self.children = [] 137 138 def spin(self): 139 if self.uv and len(self.uv) == 4: 140 self.uv = self.uv[1], self.uv[2], self.uv[3], self.uv[0] 141 142 self.width, self.height = self.height, self.width 143 self.xoff, self.yoff = self.yoff, self.xoff # not needed? 144 self.rot = not self.rot # only for tri pairs and ngons. 145 # print("spinning") 146 for pf in self.children: 147 pf.spin() 148 149 def place(self, xoff, yoff, xfac, yfac, margin_w, margin_h): 150 from math import pi 151 152 xoff += self.xoff 153 yoff += self.yoff 154 155 for pf in self.children: 156 pf.place(xoff, yoff, xfac, yfac, margin_w, margin_h) 157 158 uv = self.uv 159 if not uv: 160 return 161 162 x1 = xoff 163 y1 = yoff 164 x2 = xoff + self.width 165 y2 = yoff + self.height 166 167 # Scale the values 168 x1 = x1 / xfac + margin_w 169 x2 = x2 / xfac - margin_w 170 y1 = y1 / yfac + margin_h 171 y2 = y2 / yfac - margin_h 172 173 # 2 Tri pairs 174 if len(uv) == 2: 175 # match the order of angle sizes of the 3d verts with the UV angles and rotate. 176 def get_tri_angles(v1, v2, v3): 177 a1 = (v2 - v1).angle(v3 - v1, pi) 178 a2 = (v1 - v2).angle(v3 - v2, pi) 179 a3 = pi - (a1 + a2) # a3= (v2 - v3).angle(v1 - v3) 180 181 return [(a1, 0), (a2, 1), (a3, 2)] 182 183 def set_uv(f, p1, p2, p3): 184 185 # cos = 186 #v1 = cos[0]-cos[1] 187 #v2 = cos[1]-cos[2] 188 #v3 = cos[2]-cos[0] 189 190 # angles_co = get_tri_angles(*[v.co for v in f]) 191 angles_co = get_tri_angles(*[f.id_data.vertices[v].co for v in f.vertices]) # XXX25 192 193 angles_co.sort() 194 I = [i for a, i in angles_co] 195 196 uv_layer = f.id_data.uv_layers.active.data 197 fuv = [uv_layer[i].uv for i in f.loop_indices] 198 199 if self.rot: 200 fuv[I[2]][:] = p1 201 fuv[I[1]][:] = p2 202 fuv[I[0]][:] = p3 203 else: 204 fuv[I[2]][:] = p1 205 fuv[I[0]][:] = p2 206 fuv[I[1]][:] = p3 207 208 f = uv[0][0] 209 210 set_uv(f, (x1, y1), (x1, y2 - margin_h), (x2 - margin_w, y1)) 211 212 if uv[1]: 213 f = uv[1][0] 214 set_uv(f, (x2, y2), (x2, y1 + margin_h), (x1 + margin_w, y2)) 215 216 else: # 1 QUAD 217 if len(uv) == 4: 218 uv[1][:] = x1, y1 219 uv[2][:] = x1, y2 220 uv[3][:] = x2, y2 221 uv[0][:] = x2, y1 222 else: 223 # NGon 224 xspan = x2 - x1 225 yspan = y2 - y1 226 for uvco in uv: 227 x, y = uvco 228 uvco[:] = ((x1 + (x * xspan)), 229 (y1 + (y * yspan))) 230 231 def __hash__(self): 232 # None unique hash 233 return self.width, self.height 234 235 236def lightmap_uvpack( 237 meshes, 238 PREF_SEL_ONLY=True, 239 PREF_NEW_UVLAYER=False, 240 PREF_PACK_IN_ONE=False, 241 PREF_APPLY_IMAGE=False, 242 PREF_IMG_PX_SIZE=512, 243 PREF_BOX_DIV=8, 244 PREF_MARGIN_DIV=512, 245): 246 """ 247 BOX_DIV if the maximum division of the UV map that 248 a box may be consolidated into. 249 Basically, a lower value will be slower but waist less space 250 and a higher value will have more clumpy boxes but more wasted space 251 """ 252 import time 253 from math import sqrt 254 255 if not meshes: 256 return 257 258 t = time.time() 259 260 if PREF_PACK_IN_ONE: 261 if PREF_APPLY_IMAGE: 262 image = bpy.data.images.new(name="lightmap", width=PREF_IMG_PX_SIZE, height=PREF_IMG_PX_SIZE, alpha=False) 263 face_groups = [[]] 264 else: 265 face_groups = [] 266 267 for me in meshes: 268 if PREF_SEL_ONLY: 269 faces = [f for f in me.polygons if f.select] 270 else: 271 faces = me.polygons[:] 272 273 if PREF_PACK_IN_ONE: 274 face_groups[0].extend(faces) 275 else: 276 face_groups.append(faces) 277 278 if PREF_NEW_UVLAYER: 279 me.uv_layers.new() 280 281 # Add face UV if it does not exist. 282 # All new faces are selected. 283 if not me.uv_layers: 284 me.uv_layers.new() 285 286 for face_sel in face_groups: 287 print("\nStarting unwrap") 288 289 if not face_sel: 290 continue 291 292 pretty_faces = [prettyface(f) for f in face_sel if f.loop_total >= 4] 293 294 # Do we have any triangles? 295 if len(pretty_faces) != len(face_sel): 296 297 # Now add triangles, not so simple because we need to pair them up. 298 def trylens(f): 299 # f must be a tri 300 301 # cos = [v.co for v in f] 302 cos = [f.id_data.vertices[v].co for v in f.vertices] # XXX25 303 304 lens = [(cos[0] - cos[1]).length, (cos[1] - cos[2]).length, (cos[2] - cos[0]).length] 305 306 lens_min = lens.index(min(lens)) 307 lens_max = lens.index(max(lens)) 308 for i in range(3): 309 if i != lens_min and i != lens_max: 310 lens_mid = i 311 break 312 lens_order = lens_min, lens_mid, lens_max 313 314 return f, lens, lens_order 315 316 tri_lengths = [trylens(f) for f in face_sel if f.loop_total == 3] 317 del trylens 318 319 def trilensdiff(t1, t2): 320 return (abs(t1[1][t1[2][0]] - t2[1][t2[2][0]]) + 321 abs(t1[1][t1[2][1]] - t2[1][t2[2][1]]) + 322 abs(t1[1][t1[2][2]] - t2[1][t2[2][2]])) 323 324 while tri_lengths: 325 tri1 = tri_lengths.pop() 326 327 if not tri_lengths: 328 pretty_faces.append(prettyface((tri1, None))) 329 break 330 331 best_tri_index = -1 332 best_tri_diff = 100000000.0 333 334 for i, tri2 in enumerate(tri_lengths): 335 diff = trilensdiff(tri1, tri2) 336 if diff < best_tri_diff: 337 best_tri_index = i 338 best_tri_diff = diff 339 340 pretty_faces.append(prettyface((tri1, tri_lengths.pop(best_tri_index)))) 341 342 # Get the min, max and total areas 343 max_area = 0.0 344 min_area = 100000000.0 345 tot_area = 0 346 for f in face_sel: 347 area = f.area 348 if area > max_area: 349 max_area = area 350 if area < min_area: 351 min_area = area 352 tot_area += area 353 354 max_len = sqrt(max_area) 355 min_len = sqrt(min_area) 356 side_len = sqrt(tot_area) 357 358 # Build widths 359 360 curr_len = max_len 361 362 print("\tGenerating lengths...", end="") 363 364 lengths = [] 365 while curr_len > min_len: 366 lengths.append(curr_len) 367 curr_len = curr_len / 2.0 368 369 # Don't allow boxes smaller then the margin 370 # since we contract on the margin, boxes that are smaller will create errors 371 # print(curr_len, side_len/MARGIN_DIV) 372 if curr_len / 4.0 < side_len / PREF_MARGIN_DIV: 373 break 374 375 if not lengths: 376 lengths.append(curr_len) 377 378 # convert into ints 379 lengths_to_ints = {} 380 381 l_int = 1 382 for l in reversed(lengths): 383 lengths_to_ints[l] = l_int 384 l_int *= 2 385 386 lengths_to_ints = list(lengths_to_ints.items()) 387 lengths_to_ints.sort() 388 print("done") 389 390 # apply quantized values. 391 392 for pf in pretty_faces: 393 w = pf.width 394 h = pf.height 395 bestw_diff = 1000000000.0 396 besth_diff = 1000000000.0 397 new_w = 0.0 398 new_h = 0.0 399 for l, i in lengths_to_ints: 400 d = abs(l - w) 401 if d < bestw_diff: 402 bestw_diff = d 403 new_w = i # assign the int version 404 405 d = abs(l - h) 406 if d < besth_diff: 407 besth_diff = d 408 new_h = i # ditto 409 410 pf.width = new_w 411 pf.height = new_h 412 413 if new_w > new_h: 414 pf.spin() 415 416 print("...done") 417 418 # Since the boxes are sized in powers of 2, we can neatly group them into bigger squares 419 # this is done hierarchically, so that we may avoid running the pack function 420 # on many thousands of boxes, (under 1k is best) because it would get slow. 421 # Using an off and even dict us useful because they are packed differently 422 # where w/h are the same, their packed in groups of 4 423 # where they are different they are packed in pairs 424 # 425 # After this is done an external pack func is done that packs the whole group. 426 427 print("\tConsolidating Boxes...", end="") 428 even_dict = {} # w/h are the same, the key is an int (w) 429 odd_dict = {} # w/h are different, the key is the (w,h) 430 431 for pf in pretty_faces: 432 w, h = pf.width, pf.height 433 if w == h: 434 even_dict.setdefault(w, []).append(pf) 435 else: 436 odd_dict.setdefault((w, h), []).append(pf) 437 438 # Count the number of boxes consolidated, only used for stats. 439 c = 0 440 441 # This is tricky. the total area of all packed boxes, then sqrt() that to get an estimated size 442 # this is used then converted into out INT space so we can compare it with 443 # the ints assigned to the boxes size 444 # and divided by BOX_DIV, basically if BOX_DIV is 8 445 # ...then the maximum box consolidation (recursive grouping) will have a max width & height 446 # ...1/8th of the UV size. 447 # ...limiting this is needed or you end up with bug unused texture spaces 448 # ...however if its too high, box-packing is way too slow for high poly meshes. 449 float_to_int_factor = lengths_to_ints[0][0] 450 if float_to_int_factor > 0: 451 max_int_dimension = int(((side_len / float_to_int_factor)) / PREF_BOX_DIV) 452 ok = True 453 else: 454 max_int_dimension = 0.0 # won't be used 455 ok = False 456 457 # RECURSIVE pretty face grouping 458 while ok: 459 ok = False 460 461 # Tall boxes in groups of 2 462 for d, boxes in list(odd_dict.items()): 463 if d[1] < max_int_dimension: 464 # boxes.sort(key=lambda a: len(a.children)) 465 while len(boxes) >= 2: 466 # print("foo", len(boxes)) 467 ok = True 468 c += 1 469 pf_parent = prettyface([boxes.pop(), boxes.pop()]) 470 pretty_faces.append(pf_parent) 471 472 w, h = pf_parent.width, pf_parent.height 473 assert(w <= h) 474 475 if w == h: 476 even_dict.setdefault(w, []).append(pf_parent) 477 else: 478 odd_dict.setdefault((w, h), []).append(pf_parent) 479 480 # Even boxes in groups of 4 481 for d, boxes in list(even_dict.items()): 482 if d < max_int_dimension: 483 boxes.sort(key=lambda a: len(a.children)) 484 485 while len(boxes) >= 4: 486 # print("bar", len(boxes)) 487 ok = True 488 c += 1 489 490 pf_parent = prettyface([boxes.pop(), boxes.pop(), boxes.pop(), boxes.pop()]) 491 pretty_faces.append(pf_parent) 492 w = pf_parent.width # width and weight are the same 493 even_dict.setdefault(w, []).append(pf_parent) 494 495 del even_dict 496 del odd_dict 497 498 # orig = len(pretty_faces) 499 500 pretty_faces = [pf for pf in pretty_faces if not pf.has_parent] 501 502 # spin every second pretty-face 503 # if there all vertical you get less efficiently used texture space 504 i = len(pretty_faces) 505 d = 0 506 while i: 507 i -= 1 508 pf = pretty_faces[i] 509 if pf.width != pf.height: 510 d += 1 511 if d % 2: # only pack every second 512 pf.spin() 513 # pass 514 515 print("Consolidated", c, "boxes, done") 516 # print("done", orig, len(pretty_faces)) 517 518 # boxes2Pack.append([islandIdx, w,h]) 519 print("\tPacking Boxes", len(pretty_faces), end="...") 520 boxes2Pack = [[0.0, 0.0, pf.width, pf.height, i] for i, pf in enumerate(pretty_faces)] 521 packWidth, packHeight = mathutils.geometry.box_pack_2d(boxes2Pack) 522 523 # print(packWidth, packHeight) 524 525 packWidth = float(packWidth) 526 packHeight = float(packHeight) 527 528 margin_w = ((packWidth) / PREF_MARGIN_DIV) / packWidth 529 margin_h = ((packHeight) / PREF_MARGIN_DIV) / packHeight 530 531 # print(margin_w, margin_h) 532 print("done") 533 534 # Apply the boxes back to the UV coords. 535 print("\twriting back UVs", end="") 536 for i, box in enumerate(boxes2Pack): 537 pretty_faces[i].place(box[0], box[1], packWidth, packHeight, margin_w, margin_h) 538 # pf.place(box[1][1], box[1][2], packWidth, packHeight, margin_w, margin_h) 539 print("done") 540 541 if PREF_APPLY_IMAGE: 542 pass 543 # removed with texface 544 ''' 545 if not PREF_PACK_IN_ONE: 546 image = bpy.data.images.new(name="lightmap", 547 width=PREF_IMG_PX_SIZE, 548 height=PREF_IMG_PX_SIZE, 549 ) 550 551 for f in face_sel: 552 f.image = image 553 ''' 554 555 for me in meshes: 556 me.update() 557 558 print("finished all %.2f " % (time.time() - t)) 559 560 561def unwrap(operator, context, **kwargs): 562 # switch to object mode 563 is_editmode = context.object and context.object.mode == 'EDIT' 564 if is_editmode: 565 bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 566 567 # define list of meshes 568 meshes = list({ 569 me for obj in context.selected_objects 570 if obj.type == 'MESH' 571 for me in (obj.data,) 572 if me.polygons and me.library is None 573 }) 574 575 if not meshes: 576 operator.report({'ERROR'}, "No mesh object") 577 return {'CANCELLED'} 578 579 lightmap_uvpack(meshes, **kwargs) 580 581 # switch back to edit mode 582 if is_editmode: 583 bpy.ops.object.mode_set(mode='EDIT', toggle=False) 584 585 return {'FINISHED'} 586 587 588from bpy.props import BoolProperty, FloatProperty, IntProperty 589 590 591class LightMapPack(Operator): 592 """Pack each faces UV's into the UV bounds""" 593 bl_idname = "uv.lightmap_pack" 594 bl_label = "Lightmap Pack" 595 596 # Disable REGISTER flag for now because this operator might create new 597 # images. This leads to non-proper operator redo because current undo 598 # stack is local for edit mode and can not remove images created by this 599 # operator. 600 # Proper solution would be to make undo stack aware of such things, 601 # but for now just disable redo. Keep undo here so unwanted changes to uv 602 # coords might be undone. 603 # This fixes infinite image creation reported there T30968 (sergey) 604 bl_options = {'UNDO'} 605 606 PREF_CONTEXT: bpy.props.EnumProperty( 607 name="Selection", 608 items=( 609 ('SEL_FACES', "Selected Faces", "Space all UVs evenly"), 610 ('ALL_FACES', "All Faces", "Average space UVs edge length of each loop"), 611 ), 612 ) 613 614 # Image & UVs... 615 PREF_PACK_IN_ONE: BoolProperty( 616 name="Share Texture Space", 617 description=( 618 "Objects Share texture space, map all objects " 619 "into 1 uvmap" 620 ), 621 default=True, 622 ) 623 PREF_NEW_UVLAYER: BoolProperty( 624 name="New UV Map", 625 description="Create a new UV map for every mesh packed", 626 default=False, 627 ) 628 PREF_APPLY_IMAGE: BoolProperty( 629 name="New Image", 630 description=( 631 "Assign new images for every mesh (only one if " 632 "shared tex space enabled)" 633 ), 634 default=False, 635 ) 636 PREF_IMG_PX_SIZE: IntProperty( 637 name="Image Size", 638 description="Width and Height for the new image", 639 min=64, max=5000, 640 default=512, 641 ) 642 # UV Packing... 643 PREF_BOX_DIV: IntProperty( 644 name="Pack Quality", 645 description="Pre Packing before the complex boxpack", 646 min=1, max=48, 647 default=12, 648 ) 649 PREF_MARGIN_DIV: FloatProperty( 650 name="Margin", 651 description="Size of the margin as a division of the UV", 652 min=0.001, max=1.0, 653 default=0.1, 654 ) 655 656 def draw(self, context): 657 layout = self.layout 658 layout.use_property_split = True 659 layout.use_property_decorate = False 660 661 is_editmode = context.active_object.mode == 'EDIT' 662 if is_editmode: 663 layout.prop(self, "PREF_CONTEXT") 664 665 layout.prop(self, "PREF_PACK_IN_ONE") 666 layout.prop(self, "PREF_NEW_UVLAYER") 667 layout.prop(self, "PREF_APPLY_IMAGE") 668 layout.prop(self, "PREF_IMG_PX_SIZE") 669 layout.prop(self, "PREF_BOX_DIV") 670 layout.prop(self, "PREF_MARGIN_DIV") 671 672 @classmethod 673 def poll(cls, context): 674 ob = context.active_object 675 return ob and ob.type == 'MESH' 676 677 def execute(self, context): 678 kwargs = self.as_keywords() 679 PREF_CONTEXT = kwargs.pop("PREF_CONTEXT") 680 681 is_editmode = context.active_object.mode == 'EDIT' 682 683 if not is_editmode: 684 kwargs["PREF_SEL_ONLY"] = False 685 elif PREF_CONTEXT == 'SEL_FACES': 686 kwargs["PREF_SEL_ONLY"] = True 687 elif PREF_CONTEXT == 'ALL_FACES': 688 kwargs["PREF_SEL_ONLY"] = False 689 else: 690 raise Exception("invalid context") 691 692 kwargs["PREF_MARGIN_DIV"] = int(1.0 / (kwargs["PREF_MARGIN_DIV"] / 100.0)) 693 694 return unwrap(self, context, **kwargs) 695 696 def invoke(self, context, _event): 697 wm = context.window_manager 698 return wm.invoke_props_dialog(self) 699 700 701classes = ( 702 LightMapPack, 703) 704