You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Godot引擎下体素游戏(类Minecraft)动态生成最小2的幂次尺寸纹理图集:不同尺寸纹理密集打包及UV坐标获取方案

Dynamic Texture Atlas Generation for Voxel Game in Godot

Great question—this is a common pain point for moddable voxel games where pre-built atlases can't keep up with custom content. Let's walk through how to implement a tightly packed, power-of-two square texture atlas with UV mapping in Godot, step by step.

1. Pick a Dense Packing Algorithm

To squeeze variable-sized textures into the smallest possible space, the Skyline Algorithm is ideal. It prioritizes placing each texture in the tightest available spot and works well for dynamic, unsorted texture sets. Here's the core idea:

  • Maintain a "skyline" array that tracks the height of every column in the atlas so far.
  • Sort textures from largest to smallest (by area) first—this minimizes wasted space by placing big textures before smaller ones fill gaps.
  • For each texture, scan the skyline to find the leftmost position where it fits without overlapping. Update the skyline to reflect the new height after placement.

2. Implement Packing Logic in Godot

First, define a helper class to track each texture's packed position and UV data:

class_name PackedTextureData
var texture: Texture2D
var x: int = 0
var y: int = 0
var uv_rect: Rect2 = Rect2.ZERO # Precomputed UV coordinates

Then, write the skyline packing function:

func pack_textures(textures: Array[Texture2D]) -> Dictionary:
    # Sort textures from largest to smallest to optimize packing density
    var sorted_textures = textures.duplicate()
    sorted_textures.sort_custom(func(a, b):
        return a.get_width() * a.get_height() > b.get_width() * b.get_height()
    )

    var skyline: Array[int] = []
    var max_width: int = 0
    var max_height: int = 0
    var packed_data: Array[PackedTextureData] = []

    for tex in sorted_textures:
        var tex_w = tex.get_width()
        var tex_h = tex.get_height()
        var best_x = -1
        var best_y = -1
        var best_height_increase = INF

        # Find the optimal position for the current texture
        for x in range(0, skyline.size() - tex_w + 1):
            var current_max_y = 0
            for i in range(x, x + tex_w):
                current_max_y = max(current_max_y, skyline[i])
            var height_increase = current_max_y + tex_h - max_height
            if height_increase < best_height_increase:
                best_x = x
                best_y = current_max_y
                best_height_increase = height_increase

        # Expand the skyline if no existing spot fits
        if best_x == -1:
            best_x = skyline.size()
            best_y = max_height
            # Extend skyline to accommodate the new texture's width
            for i in range(skyline.size(), skyline.size() + tex_w):
                skyline.append(max_height)

        # Update skyline and track maximum atlas dimensions
        for i in range(best_x, best_x + tex_w):
            skyline[i] = best_y + tex_h
        max_width = max(max_width, best_x + tex_w)
        max_height = max(max_height, best_y + tex_h)

        # Store packed position data
        var data = PackedTextureData.new()
        data.texture = tex
        data.x = best_x
        data.y = best_y
        packed_data.append(data)

    return {
        "packed_data": packed_data,
        "required_width": max_width,
        "required_height": max_height
    }

3. Calculate the Smallest Power-of-Two Square Size

Once you have the minimum required width and height for all textures, compute the smallest power-of-two value that's larger than or equal to the maximum of these two dimensions (since the atlas must be square):

func get_min_power_of_two(value: int) -> int:
    if value <= 0:
        return 1
    var pot = 1
    while pot < value:
        pot *= 2
    return pot

# Usage example after packing:
var packing_result = pack_textures(your_custom_textures_list)
var atlas_size = get_min_power_of_two(max(packing_result.required_width, packing_result.required_height))

Matching your examples:

  • 4x16x16 textures: Required dimensions are 32x32 → atlas size stays 32.
  • 5x16x16 textures: Required dimensions are 32x48 → max is 48 → smallest power-of-two is 64.

4. Build the Atlas Texture

Now create the final atlas image and draw all packed textures onto it:

func build_atlas(packed_data: Array[PackedTextureData], atlas_size: int) -> Texture2D:
    # Create a blank RGBA8 image (adjust format if your textures use a different type)
    var atlas_image = Image.create(atlas_size, atlas_size, false, Image.FORMAT_RGBA8)
    atlas_image.fill(Color.TRANSPARENT)

    for data in packed_data:
        var tex_image = data.texture.get_image()
        # Draw the texture onto the atlas at its packed position
        atlas_image.blit_rect(tex_image, Rect2(0, 0, tex_image.get_width(), tex_image.get_height()), Rect2(data.x, data.y, tex_image.get_width(), tex_image.get_height()))

        # Precompute UV coordinates (Godot uses top-left (0,0) to bottom-right (1,1) for UVs)
        var uv_x = data.x / float(atlas_size)
        var uv_y = data.y / float(atlas_size)
        var uv_w = data.texture.get_width() / float(atlas_size)
        var uv_h = data.texture.get_height() / float(atlas_size)
        data.uv_rect = Rect2(uv_x, uv_y, uv_w, uv_h)

    # Convert the image to a usable texture
    var atlas_texture = ImageTexture.new()
    atlas_texture.set_image(atlas_image)
    return atlas_texture

5. Use UVs for Block Faces

When rendering voxel faces, you can directly use the uv_rect from each PackedTextureData to map the atlas texture. For example, in a mesh builder:

# For a quad face, define UVs using the precomputed rect
var uv_coords = [
    data.uv_rect.position,
    data.uv_rect.position + Vector2(data.uv_rect.size.x, 0),
    data.uv_rect.position + data.uv_rect.size,
    data.uv_rect.position + Vector2(0, data.uv_rect.size.y)
]
# Assign these UVs to your mesh's vertex array

Key Optimizations & Notes

  • Prevent Texture Bleeding: If you see edge artifacts from adjacent textures, add a 1-pixel padding around each texture when packing. Modify the packing logic to include padding, and copy edge pixels into the padding area to avoid transparency gaps.
  • Asynchronous Processing: For large texture sets, run the packing and atlas building in a Thread to avoid freezing the main game loop.
  • Format Consistency: Ensure all input textures use the same image format (e.g., RGBA8). Convert mismatched textures with Image.convert() before packing.

内容的提问来源于stack exchange,提问作者RedSmarty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 20:08:13