Notifications
Clear all

[Closed] How to create a simple tiled texture atlas?

Hi!

I’m not a scripter myself and spend quite some time to find a plugin that can simply create a texture atlas.

What I have here is a complex city model in 3DS Max 2008. Each building is a separate object (a few thousand of them actually) and each one has a unique texture assigned to it, which are all in the same resolution (1024×1024) and UVW coordinates between 0 and 1.

What I’m trying to do now is to merge either 4 textures (resulting 2048×2048) or 16 textures (resulting 4096×4096) into one and get the uvw coordinates in the model modified. Reason: Reducing draw calls. The UVW coordinates should basically stay as they are, just shifted to the position of the respective texture in the texture atlas (so scaling actually). I tried all sorts of plugins and scripts but all of them rearrange the textures parts with large blank areas in between. That’s why I want to stay with the original textures, just merged into one and modified UVW coordinates.

Nothing too hard I think but I haven’t been able yet to find a script that can do that. Is anyone out there able to point me into the right direction?

15 Replies
3 Replies
(@denist)
Joined: 11 months ago

Posts: 0

do any buildings share the same texture? if so, your packing algorithm needs a correction.

 lo1
(@lo1)
Joined: 11 months ago

Posts: 0

Can you elaborate? What I wrote correctly detects this situation and scales the shared UVs to the same coordinates.

(@denist)
Joined: 11 months ago

Posts: 0

if you want to reduce draw calls the you have to minimize number of materials and number of their reloads. Not always but usually – one texture – one material rule works. So if we have some building who share the same textures these building have to be packed the same way (in the same cells). The current algorithm doesn’t care about order of packing. So without specials instructions we are packing them in random order, and make random textures… The idea is to group all building by used texture, and create new NxN textures taking in account how often the used.

 lo1

Here are two functions that will help you, one will adjust the UV coords on an object, and the other will paste a smaller bitmap into an atlas bitmap.
The rest is a matter of interface… how does the user initiate this script? Do you select objects? Do you need it to segment the entire scene? where are the textures taken from? which UV channel(s) are manipulated, etc.
I don’t mind helping you with that part as well if you provide the necessary information of your workflow.

--toPosition should be a point2 coordinate such as [0,0], [0,1], etc. depicting the place of this object in the atlas
-- atlasSize is an integer representing the length of one dimension of the square atlas (e.g. '4' for  a 16-tile atlas)
fn scaleUVCoords obj toPosition atlasSize =
(
	local unwrap = uvwUnwrap()
	addModifier obj unwrap
	unwrap.selectVertices #{1..(unwrap.numberVertices())}
	
	local scaleFactor = 1.0 / atlasSize
	unwrap.scaleSelectedXY scaleFactor scaleFactor [0, 0, 0]
	
	local offset = [toPosition[1] * scaleFactor, 1 - scaleFactor * (1 - toPosition[2]), 0]
	unwrap.moveSelected offset
)

fn addBitmapToAtlas atlasBmp texBmp toPosition atlasSize =
(
	local step = atlasBmp.width / atlasSize
	local destPt = toPosition * step
	pasteBitmap texBmp atlasBmp [0,0] destPt type:#paste
)

google Binary Tree Bin Packing Algorithm

nice demo & source here
http://codeincomplete.com/posts/2011/5/7/bin_packing/

btw it’s not just the packing algorithm that decides how the maps are distributed in the atlas, but the initial map sorting routines have an important roll to play too. You can also “preprocess” the maps, rotating them so they’re all longest across the width or height if not square prior to sorting. This requires more coding and being a bit cleverer when dealing with the UVs but not overly difficult to implement.

1 Reply
(@sasa3)
Joined: 11 months ago

Posts: 0

Interesting read indeed. In my case this would be much more than I currently need though. But if Lo is considering to publish is script somewhere in a polished form this would be a nice feature for many for sure.

here’s a pretty crude starting point…

--********************************************************************
  
  struct leafnode
  (
  	pos,
  	width,
  	height,
  	down,
  	right,
  	used,
  	fn init xy w h d:undefined r:undefined u:false =
  	(
  		pos = xy; width = w; height = h; down = d;  right = r; used = u;
  	)
  )
  
  --********************************************************************
  
  struct rect
  ( 	
  	pos,
  	width,
  	height,
	fn makeshape =
	(
		rct = rectangle length:height width:width cornerRadius:0 pos:[width * 0.5,height * 0.5,0] 
		rct.wirecolor = random (color 0 0 0) (color 255 255 255);
		rct.pivot = [0,0,0];
		rct.pos = [pos.x,pos.y,0];
	)	
  )	
  
  --********************************************************************
  
  struct packer 
  (
  	width,
  	height,
  	root,
  	fn init w h =
  	(
  		root = leafnode();
  		root.init [0.0,0.0] w h;
  	),
  	fn findleafnode leaf w h = 
  	(
  		result = undefined;
  		if leaf != undefined and leaf.used then
  		(
  			newleaf = findleafnode 	leaf.right w h;
  			if newleaf != undefined then
  				result = newleaf;
  			else
  				result = findleafnode leaf.down w h;
  		)
  		else if leaf != undefined and w <= leaf.width and h <= leaf.height then
  			result = leaf;
  	
  		result;
  	),
  	fn splitleafnode leaf w h =
  	(
  		leaf.used = true;
  		leaf.down = leafnode();
  		leaf.right = leafnode();
  		leaf.down.init [leaf.pos.x,(leaf.pos.y + h)] leaf.width (leaf.height - h);
  		leaf.right.init [(leaf.pos.x + w), leaf.pos.y] (leaf.width - w) h;
  		leaf.pos;
  	),
  	fn pack rectangles =
  	(
  		for r in rectangles do
  		(
  			if (leaf = findleafnode root r.width r.height) != undefined then
  				r.pos = splitleafnode leaf r.width r.height;
  		)	
  	)		
  )	
  
  --********************************************************************
  
  rectangles = #();
  
  -- create a pre-sorted list of rectangles
  
  append rectangles (rect [0,0] 100 100);
  append rectangles (rect [0,0] 100 100);
  append rectangles (rect [0,0] 50 100);
  append rectangles (rect [0,0] 100 50);
  append rectangles (rect [0,0] 50 50);
  append rectangles (rect [0,0] 50 50);
  append rectangles (rect [0,0] 50 50);
  	
  thePacker = packer();
  thePacker.init 200 200;
  thePacker.pack rectangles;
  
  for i in rectangles do i.makeshape()

Thank you so much for offering your help!
To make things a bit easier I have attached a simplified project ZIP file with the necessary files:
[ul]
[li]Test1.max represents the source model of the city, which shows you that each building comes with a separate texture applied to it.[/li][li]In this model I will select bundles of 4 buildings to be merged. I think it will be easiest for you if I supply the info in a CSV file like this, which can then be called by the script?:[/li]Box01;Sphere01;Pyramid01;Teapot01
Box02;Sphere02;Pyramid02;Teapot02

In this case each line represents a bundle of 4 buildings to be merged (attached). It would be great if the script asks for the name of the city and then creates the names of the merged objects with matching texture names with an ascending number, e.g.:
Geometry name: TelAviv01 -> Texture name: TelAviv01.bmp
Geometry name: TelAviv02 -> Texture name: TelAviv02.bmp
…and so on…
[li]The texture BMPs will be all in the same directory as the MAX files (if TGA’s are accepted as well, this would be great, although converting my existing TGAs to BMPs wouldn’t be a big issue of course)[/li][li]The textures are one diffuse material each, no other materials (e.g. ambient, specular…) or submaterials exist.[/li][li]UV channel should be always 1.[/li][/ul]Thank you very much in advance!

 lo1

Claude666’s method is no doubt superior in any any case where texture sizes vary and/or are not square.
If we are following the assumption that in your particular case all your textures are 1024×1024 there should be no advantage.

 lo1

Would it be easier for you to just select 4 objects in the viewport and press a button?

1 Reply
(@sasa3)
Joined: 11 months ago

Posts: 0

That would be easier indeed.

 lo1

Here you go. Rename to remove the .txt extension (can’t attach a .ms to this forum) and run it. Usage should be self explanatory: select the objects and press go.
If there are 4 uniquely textured objects selected, a 2×2 atlas is created.
if there are 5-9, a 3×3 atlas is created. If 10-16, then 4×4.

It should correctly merge several objects which share a texture to the same atlas position.
It should correctly ignore objects that have no texture.

It will mess up if:

  • one of the textures is a different size than the size you specify.
  • you are using any material other than standard.

Barely tested, let me know if it works.

2 Replies
(@sasa3)
Joined: 11 months ago

Posts: 0

An excellent script! Thank you so much!
It was a good idea to select the objects manually for each merge instead of supplying a list with the objects to be merged. Because of that I was able to bring something of around 1800 draw calls down to a mere 120 in about an hour! This makes the model considerably faster as I hoped.

I merged object sets with textures of either 512×512 or 256×256, usually 10-16 of them. Always worked without a problem. Max crashed 2 times (“an error occured… needs to close… blahblah) but considering the workload it had to deal with that is something to expect I guess. In both cases it was able to save a backup copy before shutting down.

Let me know if you need me to test anything else with that script and thanks again!

 lo1
(@lo1)
Joined: 11 months ago

Posts: 0

Using exactly 4, 9, or 16 objects in a set is the most efficient in terms of wasted texture space.

The script was written specifically for your purpose. I have no plans to continue working on it for myself.