forked from uhlik/bpy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
uv_tube_unwrap.py
622 lines (530 loc) · 21.5 KB
/
uv_tube_unwrap.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# ##### 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {"name": "Tube UV Unwrap",
"description": "UV unwrap tube-like meshes (all quads, no caps, fixed number of vertices in each ring)",
"author": "Jakub Uhlik",
"version": (0, 3, 0),
"blender": (2, 80, 0),
"location": "Edit mode > UV > Tube UV Unwrap",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "UV", }
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import BoolProperty
from mathutils import Vector
# notes:
# - Works only on tube-like parts of mesh defined by selection and active vertex
# (therefore you must be in vertex selection mode) and the selection must have a start
# and an end ring. Tube-like mesh is: all quads, no caps, fixed number of vertices
# in each ring. (Best example of such mesh is mesh circle extruded several times
# or beveled curve (not cyclic) converted to mesh.) There must be an active vertex
# on one of the boundary loops in selection. This active vertex define place where
# mesh will be 'cut' - where seam will be placed.
# - Result is rectangular UV for easy texturing, scaled to fit square, horizontal
# and vertical distances between vertices are averaged and proportional to each other.
# usage:
# 1 tab to Edit mode
# 2 select part of mesh you want to unwrap, tube type explained above
# 3 make sure your selection has boundaries and there is an active vertex on one border of selection
# 4 hit "U" and select "Tube UV Unwrap"
# 5 optionally check/uncheck 'Mark Seams' or 'Flip' in operator properties
# changelog:
# 2018.12.16 updated to blender 2.8
# 2014.10.08 removed 'Rectangular' option, it was buggy and now i really don't
# see why would anyone need this.. sorry if you used it, but i guess
# nobody will miss that..
# 2014.08.29 clarified docs (i hope so)
# 2014.08.28 fixed maximum recursion depth exceeded error on large meshes
# 2014.08.27 new option, 'Rectangular': if true, all faces will be rectangular,
# if false, horizontal edges will be scaled proportionally and whole
# island will be centered in layout (just a by-product)
# 2014.08.27 almost full rewrite, now it works on selection only,
# any mesh will work, if selection comply to requirements
# 2014.06.16 fail nicely when encountered 2 ring cylinder
# 2014.06.16 got rid of changing edit/object mode
# 2014.06.13 fixed accidental freeze on messy geometry
# fixed first loop vertex order (also on messy geometry)
# uv creation part completely rewritten from scratch
# 2014.06.12 first release
class UnsuitableMeshError(Exception):
pass
class ActiveVertexError(Exception):
pass
class SelectionError(Exception):
pass
def tube_unwrap(operator, context, mark_seams, flip, ):
ob = context.active_object
me = ob.data
bm = bmesh.from_edit_mesh(me)
# make a copy and remove unselected, this will be 'working' bmesh
bm2 = bm.copy()
selected_verts = [v for v in bm2.verts if v.select is True]
not_selected_verts = [v for v in bm2.verts if v.select is False]
for v in not_selected_verts:
bm2.verts.remove(v)
# now i have to determine, if this is continuous cylinder from quads
active2 = bm2.select_history.active
if(active2 is None):
raise ActiveVertexError("No active vertex found.")
# verts checks
if(not active2.is_boundary):
raise SelectionError("Active vertex is not on selection boundary.")
boundary_verts = [v for v in bm2.verts if v.is_boundary is True]
if(len(boundary_verts) == 0):
# no faces = no boundary verts
raise UnsuitableMeshError("Unsuitable mesh or selection.")
if(len(boundary_verts) % 2 != 0):
raise UnsuitableMeshError("Unsuitable mesh or selection.")
if(len(bm2.verts) % (len(boundary_verts) / 2) != 0):
raise UnsuitableMeshError("Unsuitable mesh or selection.")
num_rings = int(len(bm2.verts) / (len(boundary_verts) / 2))
verts_per_ring = int(len(boundary_verts) / 2)
# if(len(bm2.verts) - len(boundary_verts) == 0):
# # this should be handled by special function
# raise UnsuitableMeshError("only 2 rings selected")
# edges checks
if(len(bm2.edges) != (num_rings * verts_per_ring) + ((num_rings - 1) * verts_per_ring)):
raise UnsuitableMeshError("Unexpected number of edges.")
# polygon checks
not_quads = [f for f in bm2.faces if len(f.verts) != 4]
if(len(not_quads) != 0):
raise UnsuitableMeshError("Mesh is not quad only.")
# all linked a bit more sophisticated check, but maybe it is already checked above..
# but, this kind of recursion is good as an exercise
linked = []
def get_neighbours(v):
r = []
for le in v.link_edges:
# a = le.verts[0]
# b = le.verts[1]
# if(a == v):
# r.append(b)
# else:
# r.append(a)
# hmm, better to read docs thoroughly, didn't know about this until now..
r.append(le.other_vert(v))
return r
# changed to iteration, it is a bit slow i think, searching for element in list twice in row and removing from list by value..
def walk(v, linked):
ok = True
other = [v, ]
while(ok):
v = other[0]
linked.append(v)
other.remove(v)
ns = get_neighbours(v)
for n in ns:
if(n not in linked and n not in other):
other.append(n)
if(len(other) == 0):
ok = False
walk(active2, linked)
if(len(linked) != len(bm2.verts)):
raise UnsuitableMeshError("Mesh or selection is not continuous.")
def get_seam_and_rings(vert):
def decide_direction(v, a, b, ):
if(flip):
return b
return a
# get ring from active vertex around selection edge
def get_boundary_edge_loop(vert):
def is_boundary(v):
if(v.is_boundary):
return True
le = v.link_edges
stats = [False] * len(le)
for i, e in enumerate(le):
a = e.verts[0]
b = e.verts[1]
if(v == a):
if(b.select):
stats[i] = True
else:
if(a.select):
stats[i] = True
if(sum(stats) != len(stats) - 1):
return False
return True
def get_next_boundary_vertices(vert):
lf = vert.link_faces
fs = []
for f in lf:
if(f.select):
fs.append(f)
if(len(fs) != 2):
raise SelectionError("Selection is not continuous. Select all rings you want to unwrap without gaps.")
fa = fs[0]
fb = fs[1]
a = None
b = None
for i, v in enumerate(fa.verts):
if(is_boundary(v) and v is not vert):
a = v
for i, v in enumerate(fb.verts):
if(is_boundary(v) and v is not vert):
b = v
return a, b
def walk_verts(v, path):
path.append(v)
a, b = get_next_boundary_vertices(v)
if(len(path) == 1):
# i need a second vert, decide one direction..
# path = walk_verts(a, path)
nv = decide_direction(v, a, b)
path = walk_verts(nv, path)
if(a in path):
if(b not in path):
path = walk_verts(b, path)
else:
return path
elif(b in path):
if(a not in path):
path = walk_verts(a, path)
else:
return path
# else:
# raise UnsuitableMeshError("Selection with only two rings both boundary detected. Add a loop cut between or select more loops in order to make unwrap work.")
return path
verts = walk_verts(vert, [])
return verts
def get_seam_and_rings_2ring_mesh(vert):
# got vert - active vertex, go by link_edges, and use only e.is_boundary = True
# choose one and walk around until start vertex is reached..
# walk next ring and now i have rings, and seam (both start point)
# now i can skip straight to uv creation
def get_next_boundary_vertices(vert):
le = [e for e in vert.link_edges if e.is_boundary is True]
vs = []
for e in le:
vs.extend(e.verts)
r = [v for v in vs if v is not vert]
return r
def walk_verts(v, path):
path.append(v)
a, b = get_next_boundary_vertices(v)
if(len(path) == 1):
# i need a second vert, decide one direction..
# path = walk_verts(a, path)
nv = decide_direction(v, a, b)
path = walk_verts(nv, path)
if(a in path):
if(b not in path):
path = walk_verts(b, path)
else:
return path
elif(b in path):
if(a not in path):
path = walk_verts(a, path)
else:
return path
return path
ring = walk_verts(vert, [])
e = [e for e in vert.link_edges if e.is_boundary is not True][0]
if(e.verts[0] == vert):
vert2 = e.verts[1]
else:
vert2 = e.verts[0]
ring2 = []
for i, v in enumerate(ring):
e = [e for e in v.link_edges if e.is_boundary is not True][0]
if(e.verts[0] == v):
a = e.verts[1]
else:
a = e.verts[0]
ring2.append(a)
return [vert, vert2, ], [ring, ring2, ]
if(num_rings == 2):
seam, rings = get_seam_and_rings_2ring_mesh(vert)
# skip right to uv creation
return (seam, rings)
else:
boundary_ring = get_boundary_edge_loop(vert)
# if(len(selected_verts) % len(boundary_ring) != 0):
# raise UnsuitableMeshError("Number of vertices != number of rings * number of ring vertices.")
# num_loops = int(len(selected_verts) / len(boundary_ring))
# old code, just swap names
num_loops = num_rings
# get all rings
def is_in_rings(vert, rings):
for r in rings:
for v in r:
if(v == vert):
return True
return False
def get_next_ring(rings):
prev_ring = rings[len(rings) - 1]
nr = []
for v in prev_ring:
le = v.link_edges
for e in le:
for v in e.verts:
if(v not in prev_ring and is_in_rings(v, rings) is False and v.select):
nr.append(v)
return nr
rings = [boundary_ring, ]
for i in range(num_loops - 1):
r = get_next_ring(rings)
rings.append(r)
# and seam vertices
def get_seam():
seam = [vert, ]
for i in range(num_loops - 1):
for v in rings[i + 1]:
sle = seam[i].link_edges
for e in sle:
if(v in e.verts):
if(e.verts[0] == seam[i]):
seam.append(e.verts[1])
else:
seam.append(e.verts[0])
return seam
seam = get_seam()
return (seam, rings)
seam, rings = get_seam_and_rings(active2)
# sum all seam edges lengths
def calc_seam_length(seam):
l = 0
for i in range(len(seam) - 1):
v = seam[i]
le = v.link_edges
for e in le:
if(seam[i + 1] in e.verts):
l += e.calc_length()
return l
seam_length = calc_seam_length(seam)
# sum all ring edges lengths
def calc_circumference(r):
def get_edge(av, bv):
for e in bm2.edges:
if(av in e.verts and bv in e.verts):
return e
return None
l = 0
for i in range(len(r)):
ei = i + 1
if(ei >= len(r)):
ei = 0
e = get_edge(r[i], r[ei])
l += e.calc_length()
return l
# ideal uv layout width and height, and scale_ratio to fit
def calc_sizes(rings, seam_length, seam):
ac = 0
for r in rings:
ac += calc_circumference(r)
ac = ac / len(rings)
if(ac > seam_length):
scale_ratio = 1 / ac
w = 0
h = (seam_length / len(seam)) * scale_ratio
else:
scale_ratio = 1 / seam_length
w = (ac / len(rings[0])) * scale_ratio
h = 0
return scale_ratio, w, h
scale_ratio, w, h = calc_sizes(rings, seam_length, seam)
# create uv
def make_uvmap(bm, name):
uvs = bm.loops.layers.uv
if(uvs.active is None):
uvs.new(name)
uv_lay = uvs.active
return uv_lay
uv_lay = make_uvmap(bm, "UVMap")
# convert verts from bm2 to bm
if(bpy.app.version >= (2, 73, 0)):
bm.verts.ensure_lookup_table()
seam = [bm.verts[v.index] for v in seam]
rs = []
for ring in rings:
r = [bm.verts[v.index] for v in ring]
rs.append(r)
rings2 = rings
rings = rs
# make uv, scale it correctly
def make_uvs(uv_lay, scale_ratio, w, h, rings, seam, ):
def get_edge(av, bv):
for e in bm.edges:
if(av in e.verts and bv in e.verts):
return e
return None
def get_face(verts):
a = set(verts[0].link_faces)
b = a.intersection(verts[1].link_faces, verts[2].link_faces, verts[3].link_faces)
return list(b)[0]
def get_face_loops(f, vo):
lo = []
for i, v in enumerate(vo):
for j, l in enumerate(f.loops):
if(l.vert == v):
lo.append(j)
return lo
x = 0
y = 0
for ir, ring in enumerate(rings):
if(len(rings) > ir + 1):
if(w == 0):
# circumference <= length
fw = 1 / len(rings[0])
fh = get_edge(seam[ir], seam[ir + 1]).calc_length() * scale_ratio
else:
# circumference > length
fw = w
fh = get_edge(seam[ir], seam[ir + 1]).calc_length() * scale_ratio
for iv, vert in enumerate(ring):
if(len(rings) > ir + 1):
next_ring = rings[ir + 1]
# d - c
# | |
# a - b
if(len(ring) == iv + 1):
poly = (vert, ring[0], next_ring[0], next_ring[iv])
else:
poly = (vert, ring[iv + 1], next_ring[iv + 1], next_ring[iv])
face = get_face(poly)
loops = get_face_loops(face, poly)
luv = face.loops[loops[0]][uv_lay]
luv.uv = Vector((x, y))
x += fw
luv = face.loops[loops[1]][uv_lay]
luv.uv = Vector((x, y))
y += fh
luv = face.loops[loops[2]][uv_lay]
luv.uv = Vector((x, y))
x -= fw
luv = face.loops[loops[3]][uv_lay]
luv.uv = Vector((x, y))
x += fw
y -= fh
x = 0
y += fh
fw = 0
fh = 0
make_uvs(uv_lay, scale_ratio, w, h, rings, seam, )
def remap(v, min1, max1, min2, max2):
def clamp(v, vmin, vmax):
if(vmax <= vmin):
raise ValueError("Maximum value is smaller than or equal to minimum.")
if(v <= vmin):
return vmin
if(v >= vmax):
return vmax
return v
def normalize(v, vmin, vmax):
return (v - vmin) / (vmax - vmin)
def interpolate(nv, vmin, vmax):
return vmin + (vmax - vmin) * nv
v = clamp(v, min1, max1)
r = interpolate(normalize(v, min1, max1), min2, max2)
r = clamp(r, min2, max2)
return r
# mark seams, both boundary rings and seam between them
if(mark_seams):
def get_edge(av, bv):
for e in bm.edges:
if(av in e.verts and bv in e.verts):
return e
return None
def mark_seam(seam):
for i, v in enumerate(seam):
if(i < len(seam) - 1):
nv = seam[i + 1]
e = get_edge(v, nv)
e.seam = True
mark_seam(seam)
# me.show_edge_seams = True
def mark_additional_seams(r):
for i in range(len(r) - 1):
a = r[i]
b = r[i + 1]
e = get_edge(a, b)
e.seam = True
a = r[0]
b = r[len(r) - 1]
e = get_edge(a, b)
e.seam = True
mark_additional_seams(rings[0])
mark_additional_seams(rings[len(rings) - 1])
# put back
bmesh.update_edit_mesh(me)
# cleanup
bm2.free()
return True
class TUVUW_OT_tube_uv_unwrap(Operator):
bl_idname = "uv.tube_uv_unwrap"
bl_label = "Tube UV Unwrap"
bl_description = "UV unwrap tube-like mesh selection. Selection must be all quads, no caps, fixed number of vertices in each ring."
bl_options = {'REGISTER', 'UNDO'}
mark_seams: BoolProperty(name="Mark seams", description="Marks seams around all island edges.", default=True, )
flip: BoolProperty(name="Flip", description="Flip unwrapped island.", default=False, )
@classmethod
def poll(cls, context):
ob = context.active_object
msm = context.scene.tool_settings.mesh_select_mode
return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH' and msm[0])
def execute(self, context):
r = False
import traceback
print_errors = False
try:
r = tube_unwrap(self, context, self.mark_seams, self.flip, )
except UnsuitableMeshError as e:
self.report({'ERROR'}, str(e))
if(print_errors):
tb = traceback.print_exc()
print(tb)
except ActiveVertexError as e:
self.report({'ERROR'}, str(e))
if(print_errors):
tb = traceback.print_exc()
print(tb)
except SelectionError as e:
self.report({'ERROR'}, str(e))
if(print_errors):
tb = traceback.print_exc()
print(tb)
if(not r):
return {'CANCELLED'}
return {'FINISHED'}
def draw(self, context):
layout = self.layout
c = layout.column()
r = c.row()
r.prop(self, "mark_seams")
r = c.row()
r.prop(self, "flip")
def menu_func(self, context):
l = self.layout
l.separator()
l.operator(TUVUW_OT_tube_uv_unwrap.bl_idname, text=TUVUW_OT_tube_uv_unwrap.bl_label)
classes = (TUVUW_OT_tube_uv_unwrap, )
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.IMAGE_MT_uvs.append(menu_func)
bpy.types.VIEW3D_MT_uv_map.append(menu_func)
def unregister():
bpy.types.IMAGE_MT_uvs.remove(menu_func)
bpy.types.VIEW3D_MT_uv_map.remove(menu_func)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()