Some context
I am working on a voxel game (like minecraft). I can not use a prebuilt texture atlas as I need to allow players to add custom blocks to the game. That's why I must create texture atlases at the runtime based on the textures provided. These textures need not be same in size (one can be 16 by 16 while other can be 64 by 64) so I can't just paste the images at a regular distance.
Actual question
How can I combine multiple images which can be different sizes into one while cramming as much images I can, i.e., images should be as densely packed as possible and if an image can be placed at a smaller place on the final image, it should be placed there as to not use the space that may be used by other images. The final image must be a square and its size must be the smallest power of 2. For example if there are 4 16 by 16 images, only a 32 by 32 image should be created as its enough to contain those images. But say that there were 5 images, it should create a 64 by 64 image as its the smallest size that can contain those images. I would also need the UV values of the textures that I can map to blockfaces.
Game engine I use
I use godot game engine.
CodePudding user response:
Inputs
Let us say you have a series of Image
s you want to put into your texture atlas. Let us say it is an array:
var images:Array
And the values of that array can be either:
- Image files imported as
Image
. - Got from calling
get_data
onTexture
s. Image
s created at runtime by other means.
Compiling the sizes
Now, we can get their sizes. We are going to need them in a PoolVector2Array
(we can actually get them in a regular Array
and convert it, but there is no need to do that):
var sizes:PoolVector2Array
for image in images:
sizes.append(image.get_size())
Computing the atlas
Once we have the sizes, we can call Geometry.make_atlas
:
var atlas_data := Geometry.make_atlas(sizes)
The algorithm of Geometry.make_atlas
will try to make the atlas square.
We should query what size we got:
var min_atlas_size:Vector2 = atlas_data.size
Sadly Godot does not expose a "next power of 2" function. Given the context, I believe a simple loop will do well on this case:
var atlas_size := 16
while min_atlas_size.x > atlas_size or min_atlas_size.y > atlas_size:
atlas_size *= 2
Building the atlas
Create an atlas Image
Now that we have decided the size, we can create a new Image
for the atlas:
var atlas := Image.new()
atlas.create(atlas_size, atlas_size, true, Image.FORMAT_RGBA8)
As you might suspect the first two parameters are width
and height
. The third parameter is whether or not you want to generate mipmaps. And the last is the pixel format, the one I'm picking here has four channels (Red, Green, Blue, Alpha) of 8 bits (color depth) each.
Copy the input Image
s to the atlas Image
Now copy your original images to the atlas. We get the position for each one on the result we got from make_atlas
:
var points:PoolVector2Array = atlas_data.points
for index in images.size():
var image:Image = images[index]
var point:Vector2 = points[index]
var size:Vector2 = image.get_size()
atlas.blit_rect(image, Rect2(Vector2.ZERO, size), point)
Here the Rect2
is the area of the Image
we want to draw (all of it).
Note depending on the format of the Image
you want to draw, you may have to convert it first:
source.convert(Image.FORMAT_RGBA8)
Also, usually, the images are imported in a compressed format that cannot be converted directly, and thus you would have to decompress first:
source.decompress()
source.convert(Image.FORMAT_RGBA8)
If the blit_rect
line is giving you problems, try that.
UV Coordinates
By the way, you say you want UV coordinates, we should get them from the same loop:
var points:PoolVector2Array = atlas_data.points
var uv_scale := 1.0/atlas_size
var uv_rects := []
for index in images.size():
var image:Image = images[index]
var point:Vector2 = points[index]
var size:Vector2 = image.get_size()
atlas.blit_rect(image, Rect2(Vector2.ZERO, size), point)
uv_rects.append(Rect2(point * uv_scale, size * uv_scale))
Converting to Texture
And since you want UV coordinates, I presume this is going to be a Texture
for some shader. So, let us make a Texture
:
var texture_atlas := ImageTexture.new()
texture_atlas.create_from_image(atlas)
Then you should be able to set it where you need it. I leave that to you.