Align UV script optimizations


#1

Hi,
I’ve made a script that aligns UV islands to the bottom left. It needs to be faster though.
Can anyone have a look through and see if there are any optimizations possible?

Can I somehow avoid selecting vertices? Are there other methods of selecting UV elements?

Since the script is WIP, there’s some commented out stuff you can ignore. I’ve commented out the actual moving of the UV islands.

Alternative 1
Here I use the selection bounding box to align the islands to the bottom left.

uv = modPanel.getCurrentObject()

TVsub = uv.unwrap2.getTVSubObjectMode()

case TVsub of (
		2: (uv.unwrap2.edgeToVertSelect()
			uv.unwrap2.setTVSubObjectMode 1)
		3: (uv.unwrap2.faceToVertSelect()
			uv.unwrap2.setTVSubObjectMode 1)
)

bitarr = uv.unwrap.getSelectedVertices()
bitarrc = bitarr.count

rollout progresspopup "Progress"
(
	progressBar progbar value:0.0
)
--createDialog progresspopup

start = timeStamp()

with redraw off
for v = 1 to bitarrc do (
	--progresspopup.progbar.value = 100.0*v/bitarrc
	if bitarr[v] == 1 then (
		uv.unwrap.selectVertices #{v}
		--uv.unwrap.selectVertices #{v}
		uv.unwrap2.selectElement()
		--uv.unwrap2.selectElement()
		
		uv.unwrap2.snapPivot 2
		elemoffset=uv.unwrap2.getPivotOffset()
		elempos=uv.unwrap2.getSelCenter()
		elemmove=elemoffset+elempos
		--uv.unwrap2.moveSelected -elemmove
	
		elemarr = #()
		elemarr = uv.unwrap.getSelectedVertices() as array
		for i = 1 to elemarr.count do
			bitarr[elemarr[i]] = 0
	)
)

end = timeStamp()
format "Processing took % seconds\n" ((end - start) / 1000.0)

--destroyDialog progresspopup
uv.unwrap2.setTVSubObjectMode TVsub

Alternative 2
Here I put all vertex coordinates in two arrays and find the smallest x and y values.
Slightly slower, but can maybe be made faster.

uv = modPanel.getCurrentObject()

TVsub = uv.unwrap2.getTVSubObjectMode()

case TVsub of (
		2: (uv.unwrap2.edgeToVertSelect()
			uv.unwrap2.setTVSubObjectMode 1)
		3: (uv.unwrap2.faceToVertSelect()
			uv.unwrap2.setTVSubObjectMode 1)
)

bitarr = uv.unwrap.getSelectedVertices()
bitarrc = bitarr.count

rollout progresspopup "Progress"
(
	progressBar progbar value:0.0
)
--createDialog progresspopup

start = timeStamp()

with redraw off
for v = 1 to bitarrc do (
	--progresspopup.progbar.value = 100.0*v/bitarrc
	if bitarr[v] == 1 then (
		uv.unwrap.selectVertices #{v}
		uv.unwrap2.selectElement()
		
		tempselba = uv.unwrap.getSelectedVertices()
		tempsel = tempselba as array
		temparrx = #()
		temparry = #()
		for t=1 to tempsel.count do (
			--if tempsel[t] == 1 then (
				temppos = uv.unwrap.getVertexPosition 0 tempsel[t]
				append temparrx temppos[1]
				append temparry temppos[2]
			--)
		)
		--xmin = amin temparrx
		--ymin = amin temparry
		/*
		uv.unwrap2.moveSelected [-xmin,-ymin,0]*/
		
		bitarr -= tempselba
	)
)

end = timeStamp()
format "Processing took % seconds\n" ((end - start) / 1000.0)

--destroyDialog progresspopup
uv.unwrap2.setTVSubObjectMode TVsub

Grateful for any help :pray:


#2

just in case you need a good tool to work with UVs there’s a UVTools

You can start with caching functions
instead of uv.unwrap.getSelectedVertices use

getSelVert = uv.unwrap.getSelectedVertices
...
for i = 1 to ... do
(
    vsel = getSelVert()

and also why do you convert bitarrays to arrays?
just substract elem array from bitarr
bitarr -= elemarr like this


#3

I’ve used both to do speed tests.

In the second script I have to use arrays to store the coordinates, though.


#4

select element is the slowest part in your code
it could be helpful to get the mesh representation of the UVs using channelinfo and get all the data from there

local meshUV = mesh mesh:(snapshotasmesh obj)
      channelInfo.CopyChannel meshUV 3 mapch
      channelInfo.PasteChannel meshUV 1 0
with redraw off
(
selectVertices      = uv.unwrap.selectVertices
selectElement       = uv.unwrap2.selectElement
getSelectedVertices = uv.unwrap.getSelectedVertices
getVertexPosition   = uv.unwrap.getVertexPosition

for v in bitarr where bitarr[v] do
(
		selectVertices #{v}
		selectElement()
		
		elem  = getSelectedVertices()
		minxy = [1e9,1e9]
				
		for t in elem do
		(
			pt = getVertexPosition 0 t
			if  pt.x < minxy.x do minxy.x = pt.x
			if  pt.y < minxy.y do minxy.y = pt.y
		)		
		
-- 		uv.unwrap2.moveSelected [-minxy.x,-minxy.y,0]
		
		bitarr -= elem
	
)
)

#5

I’m not sure how to use the first part of your suggestion.
As for the second part, it looks nice (although I’m not sure how everything works), but I didn’t see a speed increase.

Do you think it’s possible to get away with not selecting any verts? Just working with the values in the bitarray and then moving them without selecting them?
Also get vert position seems slow.

I had a look at this:
https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-F1B3ECFD-2649-48C8-B130-15A17527433C

To my untrained eye it looks like they are manually growing the selection to select the UV islands. Maybe that’s faster? Or maybe just to avoid Unwrap modifier. I haven’t found a way to use the same method for Epolys anyhow.


#6

Of course, it depends on the polycount of the model and the selection. And sometimes all you get with caching is removing memory leaks

Sure, forget about unwrap mod and its methods where possible and get all the needed data from the uv mesh object. I’m not sure if you can move or set unwrap verts position without selection.
It was long time ago when we dicussed how to improve things in uvtools. But making custom c++ methods to improve performance was the only way to go in the end…


#7

this no longer helps in 2019+ versions. In early versions this was mostly a memory issue which could also affect performance.


#8

we need a test scene to do correct performance tests… I suggest this:

modi = 
(
	max create mode
	delete objects
	sp = geosphere name:"test_mesh" segs:2 mapcoords:on
	addmodifier sp (Uvwmap maptype:5)
	sp = converttopoly sp
	for k=1 to 3 do polyop.attach sp (copy sp)
	update sp

	modi = Unwrap_UVW()
	addmodifier sp modi
	max modify mode
	modpanel.setcurrentobject modi
	modi
)

as first step I can suggest the most simple way to get all uv clusters using built-in UVWUnwrap methods:

fn uvwElements0 modi: = 
(
	if modi == unsupplied do modi = modpanel.getcurrentobject()
	if iskindof modi Unwrap_UVW do undo off with redraw off
	(
		disablerefmsgs()
		modi.setTVSubObjectMode 1

		elements = #()
		verts = #{1..modi.numbervertices()}
		done = #{}
		for v in verts where not done[v] do
		(
			modi.selectvertices #{v}
			modi.selectelement()
			element = modi.getselectedvertices()
			append elements element
			join done element 
		)
		
		enablerefmsgs()
		elements
	)
)

so we get:

(
	gc()
	t1 = timestamp()
	m1 = heapfree
	elements = uvwElements0 modi:modi
	format "ELEMENTS0 elements:% time:% memory:%  >> %\n" (try(elements.count) catch(-1)) (timestamp() - t1) (m1 - heapfree) elements
)

/*
#(#{1, 13..14}, #{2, 18, 43}, #{3, 47..48}, #{4, 56..57}, #{5, 65..66}, #{6, 74..75}, #{7, 92..93}, #{8, 100..101}, #{9, 108..109}, #{10, 116..117}, #{11, 37, 88}, #{12, 38..39}, #{15, 49..50}, #{16, 58..59}, #{17, 67..68}, #{19, 51..52}, #{20, 60..61}, #{21, 69..70}, #{22, 79..80}, #{23, 28, 87}, ...)
ELEMENTS0 elements:640 time:4996 memory:90368L  >> #(#{1, 13..14}, #{2, 18, 43}, #{3, 47..48}, #{4, 56..57}, #{5, 65..66}, #{6, 74..75}, #{7, 92..93}, #{8, 100..101}, #{9, 108..109}, #{10, 116..117}, #{11, 37, 88}, #{12, 38..39}, #{15, 49..50}, #{16, 58..59}, #{17, 67..68}, #{19, 51..52}, #{20, 60..61}, #{21, 69..70}, #{22, 79..80}, #{23, 28, 87}, ...)

*/

#9

the problem that is the Unwrap methods are slow… for example my c++ SDK implementation of the same task is:
count:640 time:10 heap:54120L

but we can do (collect clusters) with MXS, and it won’t be much slower than c++.
there is a thread on this forum about how best to find mesh geometry elements. The final algorithm can easily be used for uvw elements as well.


#10

bad thing about unwrap if I remember correctly was a ‘dead’ vertex indexes after some operations like welding etc… it was complicating mapvert to poly/mesh index mapping


#11

I haven’t touched Unwrap UVW for a long time, but I remember that they “drastically improved” something about it, and the performance numbers I see for this example tell that something is still really broken.


#12

I’m afraid this goes over my head for the moment.
Up until now I’ve been using a script in Blender, which is very fast, but as a 20+ years Max user it was a little annoying not being able to do it in Max.
I will look into some of the things you have mentioned. Thanks :slight_smile:
If you have any brainwaves feel free to share :slight_smile:


#13

I just want to say that what you are doing (code above) is pretty reasonable, but the built-in methods and their representation in MXS is not very good. Which is not your fault.
The best way to improve performance is to do the same without Unwrap UVW using the POLY or MESH map methods.


#14

Do I understand correctly that you want to align the bottom-left corner of the UV cluster’s bounding box to [0,0]?

… and to specify the uv cluster by geometry face or vertex selection?


#15

I remember Serejah already found this topic once
:wink:

Thanks to the developers of this site for an unmatched search engine!


#16

Exactly.
The purpose is to be able to quickly texture thousands of low poly buildings (Blender-OSM plugin), which are at varying heights, so that the bottom of each building is at the bottom of the UV space. This gives a little more control and prevents any building from starting with a row of cut off windows at the base.
The left align prevents one side from having cut off windows, so better than nothing. The ability to select which islands to align is just nice to have, but not strictly necessary.


#17
fn getMapElements node channel:1 = if iskindof node Editable_Poly or iskindof node.baseobject Editable_Poly do
(
	if polyop.getmapsupport node channel do
	(
		mesh = snapshotasmesh node
		if not iskindof node Editable_Poly do node = node.baseobject
		elements = #()
		
		done = #{}
		num = mesh.numfaces
		for f=1 to num while not keyboard.escpressed where not done[f] do 
		(
			last = -1
			faces = #{f}
			verts = #{}
			while faces.numberset != last and not keyboard.escpressed do
			(
				last = faces.numberset
				verts = meshop.getmapvertsusingmapface mesh channel faces
				faces = meshop.getmapfacesusingmapvert mesh channel verts
			)
			append elements verts
			join done faces
		)
		free mesh
		elements
	)
)

fn getMapElementBBox3 node tverts channel:1 = if iskindof node Editable_Poly or iskindof node.baseobject Editable_Poly do
(
	if polyop.getmapsupport node channel do
	(
		mesh = snapshotasmesh node

		bmin = [1e9,1e9,1e9]
		bmax = -[1e9,1e9,1e9]
		
		pp = for tv in tverts collect 
		(
			p = meshop.getMapVert mesh channel tv

			if p.x < bmin.x do bmin.x = p.x
			if p.y < bmin.y do bmin.y = p.y
			if p.z < bmin.z do bmin.z = p.z
				
			if p.x > bmax.x do bmax.x = p.x
			if p.y > bmax.y do bmax.y = p.y
			if p.z > bmax.z do bmax.z = p.z
				
			p
		)
		free mesh
		#(box3 bmin bmax, tverts, pp)
	)
)


fn alignMapElement node data channel:1 = if iskindof node Editable_Poly or iskindof node.baseobject Editable_Poly do
(
	if polyop.getmapsupport node channel do
	(
		-- data == #(box3 bmin bmax, tverts, pp)
		shift = data[1].min 
		k = 1
		for tv in data[2] do
		(
			p = data[3][k] - shift
			polyop.setmapvert node channel tv p
			k += 1
		)
	)
)


/****************************************************************************************/

sp = 
(
	max create mode
	delete objects
	sp = teapot name:"test_mesh" mapcoords:on
	addmodifier sp (Uvwmap maptype:0)
	sp = converttopoly sp
	for k=1 to 3 do polyop.attach sp (copy sp)
	update sp
	
	sp
)


clusters = getMapElements sp
data = for cluster in clusters collect (getMapElementBBox3 sp cluster)
for d in data do alignMapElement sp d 

(
	modi = Unwrap_UVW()
	addmodifier sp modi
	max modify mode
	modpanel.setcurrentobject modi
)

I hope I didn’t forget anything… of course, this can be algorithmically optimized, but already much faster than the built-in unwrap…


#18

Very cool, I did a few tests and, according to those tests, this is about twice as fast as my previous script. The blender python script (uv align distribute) is still about 8x faster than this, which is pretty crazy. Maybe Blender has less overhead or something.

I’m not exactly sure what happens in this script, so I don’t know how to optimize it further, but I’ll take some time to go through it and learn.

It shouldn’t matter whether you’re using ms or python, right? They should be equally fast/slow within the Max environment?


#19

maxscript has not very good loop iteration performance and you’re bound to use high level abstractions, so on large polycounts mxs will lose to python for sure. For python you can try MaxPlus or whatever came as a replacement in newer max versions to improve timings. But you’ll have to use c++ sdk sources to learn how it all works under the hood. Which can be overwhelming
In general x10 speedup can be achieved depending on the task if you switch to low level implementation with c++ or c#.


#20

you can rewrite it so that you don’t have to make a separate mesh snapshot for each uv-element. If you have lots of elements it should definitely improve timings

...
fn getMapElementBBox3 node tverts channel:1 = if iskindof node Editable_Poly or iskindof node.baseobject Editable_Poly do
(
	if polyop.getmapsupport node channel do
	(
		mesh = snapshotasmesh node