Big Guide: Texture Transitions and "Multishader", part 1

Hello.

I want to share my developments in the field of texture transitions and the resulting solution, which I call the Multishader. This approach allows for managing a large number of materials within a single shader, while also achieving high-quality transitions between them and a high level of optimization.

If you follow the instructions, you can achieve the same results as shown in the screenshots:

This is the visual result with textures:

The texture transitions are procedural, thus offering infinite quality.

This tutorial assumes you have some familiarity with Texture Arrays. If this is a new concept for you, please take a moment to read this article:

Part 1: Blender

I am using Blender v.4.1, but I believe this code should also be applicable to later versions.

First, it’s important to understand that this method is specifically designed for planes and will not work with other mesh types such as cubes, spheres, pyramids or torus.

To be clear, these too are fundamentally planes. The crucial understanding is that any form that was initially a plane and whose UV layout maintains the properties of a uniform plane will be compatible.

Remember these rules for creating your object to avoid errors:

  1. After creating the desired shape, be sure to reset all transforms by pressing Ctrl + A: All Transforms.
  2. Do not modify the UV.
  3. Do not use Extrude.
  4. Do not use Loop Cuts.

I know, these are very restrictive rules!
In practice, this method is primarily suitable for creating landscapes. And yes, I specifically developed this “technology” for landscapes because I’m currently creating a landscape for my game, hence the unexpected limitations.

So, we have our figure created. Next, we need to assign materials.

Rules for using and assigning materials:

  1. Name your materials using only numbers within the range of 0 to 99.
  2. If you don’t want a material to have a transition, name it using letters.

Now, in more detail:
Create your texture atlas beforehand, and name the materials so that the name corresponds to the index of the texture within the atlas. I recommend making the first texture red, because it will appear when there are errors.

Moving on to the code.
Proceed to the Scripting workspace and save your script in a convenient location.

I’ll say right away that I’m not a code master, so if there’s some magic one-line code that would make me feel like I wasted a lot of 1.5 months, please do share!

I should also mention that this code was developed in collaboration with AI

And one more thing – I was really lazy about translating the comments to English, so apologies for that. Feel free to tackle the translation yourself if you’re up for it. The comments are in Russian.

import bpy
from mathutils import Vector
from collections import Counter
import time
import cProfile
import os
import math

# Функция рисования изображения в формате.exr для запечатления индексов материалов соседей и шаблона пограничного материала
def create_texture(pixel_colors, width, height):
    # Создаём пустой массив пикселей (RGBA) для изображения
    flat_pixels = [0.0] * (width * height * 4)

    # Заполняем массив пикселей напрямую из `pixel_colors`
    for i in range(len(pixel_colors)):
        index = i * 4
        flat_pixels[index:index + 4] = pixel_colors[i]  # Записываем RGBA

    # Создаём новое изображение в Blender
    model_name = bpy.context.active_object.name
    img = bpy.data.images.new(model_name, width=width, height=height, alpha=True, float_buffer=True)
    img.colorspace_settings.name = "Non-Color"
    img.pixels.foreach_set(flat_pixels)  # Заполняем пиксели

    # Сохраняем изображение
    file_path = ""
    texture_path = os.path.join(file_path, f"{model_name}.exr")
    img.filepath_raw = texture_path
    img.file_format = 'OPEN_EXR'
    img.save()

    print(f" Текстура сохранена: {texture_path}")
#---------------------------------------------------------------------

# Функция расчета соседей полигонов и правильное составление массива
def border_material():
    # Запускаем таймер для измерения времени выполнения
    start_time = time.time()
    
    #--------------------------------------------------------------------------
    # Записывай полигональную сетку в массив и Конвертируем при этом координаты, так как по умолчанию они сдвинуты
    obj = bpy.context.active_object 
    uv_layer = obj.data.uv_layers.active.data 

    # Определяем размеры массива по длине сетки UV
    all_uvs_x = [round(uv_layer[loop_index].uv.x,3) for face in obj.data.polygons for loop_index in face.loop_indices]
    all_uvs_y = [round(uv_layer[loop_index].uv.y, 3) for face in obj.data.polygons for loop_index in face.loop_indices]
    unique_u = sorted(list(set(all_uvs_x)))
    unique_v = sorted(list(set(all_uvs_y)))

    GRID_SIZE_X = len(unique_u) - 1
    GRID_SIZE_Y = len(unique_v) - 1

    # Создаём пустые массивы
    polygon_grid = [["None"] * GRID_SIZE_X for _ in range(GRID_SIZE_Y)]
    
    # Формируем массив
    for poly_index, polygon in enumerate(obj.data.polygons):
        # Получаем UV-координаты первого вершины полигона
        uv_coords = uv_layer[polygon.loop_start].uv  # Берём UV первой вершины

        x = int(round(uv_coords.x * GRID_SIZE_X))  # Конвертация UV → координаты сетки
        y = int(round(uv_coords.y * GRID_SIZE_Y))  

        # Проверяем, что координаты в пределах массива
        if 0 <= x < GRID_SIZE_X and 0 <= y < GRID_SIZE_Y:
            material_name = obj.data.materials[polygon.material_index].name  
            if material_name.isdigit():
                polygon_grid[y][x] = int(material_name)
            else:
                polygon_grid[y][x] = -1  # Если материал не числовой, заменяем на -1
        
    #--------------------------------------------------------------------------

    #Находим стыки и "назначаем" материал 100, буквенные материалы игнорируются
    offsets = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]

    for y in range(GRID_SIZE_Y):
        for x in range(GRID_SIZE_X):
            current_value = polygon_grid[y][x]

            # Пропускаем уже изменённые ячейки (100 или -1)
            if current_value != "None" and current_value != 100 and current_value != -1:
                for dy, dx in offsets:
                    ny, nx = y + dy, x + dx

                    # Проверяем границы массива
                    if 0 <= ny < GRID_SIZE_Y and 0 <= nx < GRID_SIZE_X:
                        neighbor_value = polygon_grid[ny][nx]

                        # Пропускаем соседей с 100 или -1
                        if neighbor_value != "None" and neighbor_value != 100 and neighbor_value != -1:
                            if current_value < neighbor_value:
                                polygon_grid[y][x] = 100  # Меняем текущую ячейку
                            
    #Вызываем предварительные алгоритмы                          
    polygon_grid = array_editing_algorithms(polygon_grid, GRID_SIZE_X, GRID_SIZE_Y) 

    #Вызываем финальные алгоритмы
    algorithms(polygon_grid, GRID_SIZE_X, GRID_SIZE_Y)

    # Конец отсчета работы алгоритма
    end_time = time.time()
    # Вывод времени выполнения
    execution_time = end_time - start_time
    print(f"Время выполнения алгоритма: {execution_time:.2f} секунд")
#---------------------------------------------------------------------

#Функция удаления лишних 100 из основного массива
def array_editing_algorithms(array, width, height):

    for y in range(height):
        for x in range(width):
                
            if array[y][x] == 100:  # Если найдена ячейка 100
                # Находим значения материалов соседних полигонов
                top = array[y + 1][x] if y + 1 < len(array) else None
                bottom = array[y - 1][x] if y - 1 >= 0 else None
                left = array[y][x - 1] if x - 1 >= 0 else None
                right = array[y][x + 1] if x + 1 < len(array[0]) else None
                top_left = array[y + 1][x - 1] if y + 1 < len(array) and x - 1 >= 0 else None
                top_right = array[y + 1][x + 1] if y + 1 < len(array) and x + 1 < len(array[0]) else None
                bottom_left = array[y - 1][x - 1] if y - 1 >= 0 and x - 1 >= 0 else None
                bottom_right = array[y - 1][x + 1] if y - 1 >= 0 and x + 1 < len(array[0]) else None

                top_2 = array[y + 2][x] if y + 2 < len(array) else None
                bottom_2 = array[y - 2][x] if y - 2 >= 0 else None
                left_2 = array[y][x - 2] if x - 2 >= 0 else None
                right_2 = array[y][x + 2] if x + 2 < len(array[0]) else None

                # Алгоритм квадрат
                #1
                if top == 100 and left == 100 and top_left == 100 and right != 100 and left_2 != 100 and left_2 != bottom:
                    array[y][x] = bottom
                #2
                if bottom == 100 and right == 100 and bottom_right == 100 and left != 100 and right_2 != 100 and right_2 != top:
                    array[y][x] = top
                #3
                if left == 100 and bottom == 100 and bottom_left == 100 and top != 100 and bottom_2 != 100 and bottom_2 != right:
                    array[y][x] = right
                #4
                if right == 100 and top == 100 and top_right == 100 and bottom != 100 and top_2 != 100 and top_2 != left:
                    array[y][x] = left
                            
                #5
                if top == 100 and right == 100 and top_right == 100 and left != 100 and right_2 != 100 and right_2 != bottom:
                    array[y][x] = bottom
                #6
                if bottom == 100 and left == 100 and bottom_left == 100 and right != 100 and left_2 != 100 and left_2 != top:
                    array[y][x] = top
                #7
                if left == 100 and top == 100 and top_left == 100 and bottom != 100 and top_2 != 100 and top_2 != right:
                    array[y][x] = right
                #8
                if right == 100 and bottom == 100 and bottom_right == 100 and top != 100 and bottom_2 != 100 and bottom_2 != left:
                    array[y][x] = left
                        
    return array
    
#---------------------------------------------------------------------
    
#Функция расчета переходов
def algorithms(polygon_grid, width, height):

    # Получаем активный объект (меш)
    obj = bpy.context.active_object 
    # Создаём массив, где каждый пиксель представлен как [R, G, B, A]
    pixel_array = [[0.0, 0.0, 0.0, 1.0] for _ in range(len(obj.data.polygons))]

    # Создаём копию массива polygon_grid
    material_grid = [row[:] for row in polygon_grid]  # Глубокая копия

    # Определяем смещения для 8 соседей
    offsets = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]
    
    #Определяем где в структурах багует min и max
    for y in range(height):
        for x in range(width):
            if polygon_grid[y][x] == 100:  # Если найдена ячейка 100
                neighbor_values = []  # Список соседей

                # Проверяем всех соседей
                for dy, dx in offsets:
                    ny, nx = y + dy, x + dx

                    # Проверяем границы массива
                    if 0 <= ny < height and 0 <= nx < width:
                        neighbor_value = polygon_grid[ny][nx]

                        # Игнорируем 100 и -1
                        if neighbor_value != 100 and neighbor_value != -1:
                            neighbor_values.append(neighbor_value)

                # Определяем min/max среди соседей
                if neighbor_values:
                    material_min = min(neighbor_values)
                    material_max = max(neighbor_values)

                    # Если min и max совпадают, записываем None
                    if material_min == material_max:
                        material_min, material_max = -2, -2
                else:
                    material_min, material_max = -2, -2 

                # Записываем min/max (или None) в material_grid
                material_grid[y][x] = [material_max, material_min]
            
    # Исходя и полученных данных находим верные min и max           
    for y in range(height):
        for x in range(width):
            # Ищем ячейки с [-2, -2]
            if material_grid[y][x] == [-2, -2]:
                min_value = []
                max_value = []

                # Начинаем с ближайших соседей
                search_range = 1  
                
                while not min_value or not max_value:  # Если список пуст, расширяем зону поиска
                    for dy, dx in offsets:
                        ny, nx = y + dy * search_range, x + dx * search_range  # Расширяем поиск

                        # Проверяем границы массива
                        if 0 <= ny < height and 0 <= nx < width:
                            neighbor_value = material_grid[ny][nx]

                            # Если сосед — двойная ячейка, извлекаем значения
                            if isinstance(neighbor_value, list) and len(neighbor_value) == 2 and neighbor_value != [-2, -2]:
                                max_value.append(neighbor_value[0])  # Первое число — max
                                min_value.append(neighbor_value[1])  # Второе число — min

                    search_range += 1  # Увеличиваем радиус поиска

                    # Ограничиваем размер поиска, чтобы избежать бесконечного цикла
                    if search_range > max(height, width):
                        break

                # Если нашли хотя бы одно значение, обновляем `material_grid[y][x]`**
                if min_value and max_value:
                    material_grid[y][x] = [max(max_value), min(min_value)]
           
    # Находи соседей и с помощью алгоритмов кодируем в изображение номера переходов и индексы материалов
    for y in range(height):
        for x in range(width):
            #Проверка выполнения алгоритма                
            completed = False     
            # Конвертируем индекс массива polygon_grid в координаты массива pixel_array
            index = y * width + x
            
            if polygon_grid[y][x] == 100:  # Если найдена ячейка 100
                # Создаём список для хранения соседних значений
                neighbor_values = []
                
                # Находим значения материалов соседних полигонов
                top = polygon_grid[y + 1][x] if y + 1 < len(polygon_grid) else None
                bottom = polygon_grid[y - 1][x] if y - 1 >= 0 else None
                left = polygon_grid[y][x - 1] if x - 1 >= 0 else None
                right = polygon_grid[y][x + 1] if x + 1 < len(polygon_grid[0]) else None
                top_left = polygon_grid[y + 1][x - 1] if y + 1 < len(polygon_grid) and x - 1 >= 0 else None
                top_right = polygon_grid[y + 1][x + 1] if y + 1 < len(polygon_grid) and x + 1 < len(polygon_grid[0]) else None
                bottom_left = polygon_grid[y - 1][x - 1] if y - 1 >= 0 and x - 1 >= 0 else None
                bottom_right = polygon_grid[y - 1][x + 1] if y - 1 >= 0 and x + 1 < len(polygon_grid[0]) else None

                # Находим некоторых дальних соседей
                top_2_left = polygon_grid[y + 2][x - 1] if y + 2 < len(polygon_grid) and x - 1 >= 0 else None
                top_2_right = polygon_grid[y + 2][x + 1] if y + 2 < len(polygon_grid) and x + 1 < len(polygon_grid[0]) else None
                bottom_2_right = polygon_grid[y - 2][x + 1] if y - 2 >= 0 and x + 1 < len(polygon_grid[0]) else None
                bottom_2_left = polygon_grid[y - 2][x - 1] if y - 2 >= 0 and x - 1 >= 0 else None
                top_right_2 = polygon_grid[y + 1][x + 2] if y + 1 < len(polygon_grid) and x + 2 < len(polygon_grid[0]) else None
                top_left_2 = polygon_grid[y + 1][x - 2] if y + 1 < len(polygon_grid) and x - 2 >= 0 else None
                bottom_left_2 = polygon_grid[y - 1][x - 2] if y - 1 >= 0 and x - 2 >= 0 else None
                bottom_right_2 = polygon_grid[y - 1][x + 2] if y - 1 >= 0 and x + 2 < len(polygon_grid[0]) else None
                
                # Устанавливаем минимальный и максимальный материал соседей
                material_max = material_grid[y][x][0]
                material_min = material_grid[y][x][1]

                # Алгоритмы (1,2,3,4) Стороны
                #1
                if bottom == material_max:
                    pixel_array[index][0] =  (material_max * 100 + 1) / 10000
                    pixel_array[index][1] =  material_min / 255
                    completed = True
                #2
                if top == material_max:
                    pixel_array[index][0] = (material_max * 100 + 2) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #3
                if right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 3) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #4
                if left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 4) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                    
                # Алгоритмы (5,6,7,8) Внешние углы
                #1
                if bottom == 100 and right == 100 and bottom_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 5) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #2
                if bottom == 100 and left == 100 and bottom_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 6) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #3
                if top == 100 and right == 100 and top_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 7) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #4
                if top == 100 and left == 100 and top_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 8) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                    
                # Алгоритмы (9,10,11,12) Внутренние углы
                #1
                if bottom == material_max and right == material_max and bottom_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 9) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #2
                if bottom == material_max and left == material_max and bottom_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 10) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #3
                if top == material_max and right == material_max and top_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 11) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #4
                if top == material_max and left == material_max and top_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 12) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                    
                # Алгоритмы (1,2,3,4) Над пустым материалом
                #1
                if bottom == 100 and left == 100 and right == 100 and bottom_left == material_max and bottom_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 1) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #2
                if top == 100 and left == 100 and right == 100 and top_left == material_max and top_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 2) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #3
                if right == 100 and top == 100 and bottom == 100 and top_right == material_max and bottom_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 3) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #4
                if left == 100 and top == 100 and bottom == 100 and top_left == material_max and bottom_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 4) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                    
                # Алгоритмы (0) Пустой материал
                #1
                if top == 100 and bottom == 100 and left == 100 and right == 100 and top_left == material_max and top_right == material_max and bottom_left == material_max and bottom_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 0) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #2
                if left == material_max and right == material_max or top == material_max and bottom == material_max:
                    pixel_array[index][0] = (material_max * 100 + 0) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                    
                # Алгоритмы (9,10,11,12) Необычные
                #1
                if bottom == material_max and top == 100 and left == 100 and right == 100 and top_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 10) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #2
                if top == material_max and bottom == 100 and left == 100 and right == 100 and bottom_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 11) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #3
                if right == material_max and left == 100 and top == 100 and bottom == 100 and bottom_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 9) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #4
                if left == material_max and right == 100 and top == 100 and bottom == 100 and top_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 12) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                    
                #5
                if bottom == material_max and top == 100 and left == 100 and right == 100 and top_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 9) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #6
                if top == material_max and bottom == 100 and left == 100 and right == 100 and bottom_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 12) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #7
                if right == material_max and left == 100 and top == 100 and bottom == 100 and top_left == material_max:
                    pixel_array[index][0] = (material_max * 100 + 11) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True
                #8
                if left == material_max and right == 100 and top == 100 and bottom == 100 and bottom_right == material_max:
                    pixel_array[index][0] = (material_max * 100 + 10) / 10000
                    pixel_array[index][1] = material_min / 255
                    completed = True

                # Алгоритмы пересечения 3-х разных материалов. Простые
                #1
                if top != 100 and bottom_left != 100 and bottom_right != 100 and top != bottom_left and bottom_left != bottom_right and bottom_right != top:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if top is None else (top * 100 + 13) / 10000
                    pixel_array[index][1] = material_min / 255 if bottom_left is None else bottom_left / 255
                    pixel_array[index][2] = material_max / 255 if bottom_right is None else bottom_right / 255
                    completed = True
                #2
                if bottom != 100 and top_left != 100 and top_right != 100 and bottom != top_left and top_left != top_right and top_right != bottom:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if bottom is None else (bottom * 100 + 14) / 10000
                    pixel_array[index][1] = material_min / 255 if top_left is None else top_left / 255
                    pixel_array[index][2] = material_max / 255 if top_right is None else top_right / 255
                    completed = True
                #3
                if left != 100 and top_right != 100 and bottom_right != 100 and left != top_right and top_right != bottom_right and bottom_right != left:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if left is None else (left * 100 + 15) / 10000
                    pixel_array[index][1] = material_min / 255 if top_right is None else top_right / 255
                    pixel_array[index][2] = material_max / 255 if bottom_right is None else bottom_right / 255
                    completed = True
                #4
                if right != 100 and top_left != 100 and bottom_left != 100 and right != top_left and top_left != bottom_left and bottom_left != right:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if right is None else (right * 100 + 16) / 10000
                    pixel_array[index][1] = material_min / 255 if top_left is None else top_left / 255
                    pixel_array[index][2] = material_max / 255 if bottom_left is None else bottom_left / 255
                    completed = True
                    
                # Алгоритмы пересечения 3-х разных материалов. Сложные
                #1  
                if top == 100 and bottom == 100 and left == 100 and right == material_min and bottom_left == material_max and top_left == 100 and bottom_left != top_2_left < right:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if top_2_left is None else (top_2_left * 100 + 17) / 10000
                    pixel_array[index][1] = material_min / 255 if bottom_left is None else bottom_left / 255
                    pixel_array[index][2] = material_max / 255 if right is None else right / 255
                    completed = True
                #2 
                if top == 100 and bottom == 100 and right == 100 and left == material_min and top_right == material_max and bottom_right == 100 and top_right != bottom_2_right < left:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if bottom_2_right is None else (bottom_2_right * 100 + 18) / 10000
                    pixel_array[index][1] = material_min / 255 if top_right is None else top_right / 255
                    pixel_array[index][2] = material_max / 255 if left is None else left / 255
                    completed = True
                #3 
                if left == 100 and right == 100 and bottom == 100 and top == material_min and bottom_right == material_max and bottom_left == 100 and bottom_right != bottom_left_2 < top:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if bottom_left_2 is None else (bottom_left_2 * 100 + 19) / 10000
                    pixel_array[index][1] = material_min/ 255 if bottom_right is None else bottom_right / 255
                    pixel_array[index][2] = material_max / 255 if top is None else top / 255
                    completed = True
                #4 
                if left == 100 and right == 100 and top == 100 and bottom == material_min and top_left == material_max and top_right == 100 and top_left != top_right_2 < bottom:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if top_right_2 is None else (top_right_2 * 100 + 20) / 10000
                    pixel_array[index][1] = material_min / 255 if top_left is None else top_left / 255
                    pixel_array[index][2] = material_max / 255 if bottom is None else bottom / 255
                    completed = True
                #5  
                if top == 100 and bottom == 100 and right == 100 and left == material_min and bottom_right == material_max and top_right == 100 and bottom_right != top_2_right < left:
                    pixel_array[index][0] = (material_min * 100 + 0) / 10000 if top_2_right is None else (top_2_right * 100 + 21) / 10000
                    pixel_array[index][1] = material_min / 255 if bottom_right is None else bottom_right / 255
                    pixel_array[index][2] = material_max / 255 if left is None else left / 255
                    completed = True

Part 2:

1 Like

For that, a very special thanks :beers: :smiley_cat:

A small addition to the guide.
When copying the Blender code, pay attention to these three lines. They automatically select suitable materials for places where errors occur that the Blender code could not calculate. This code does not correct these errors, but only masks them.
But if you want to visually observe the problem areas, then comment out these lines. In my case, the errors are displayed as a red texture.

Screenshot_70
Screenshot_71

To eliminate these errors, you simply need to slightly adjust the material placement – reduce the occupied area in some places, increase it in others, or move the materials around through trial and error until the errors are gone. The code has a limitation: for proper operation, there must be at least 2 polygons separating the structures of materials. Otherwise, these errors may occur.

1 Like

Maybe it’s like this

Непрошедшие ни по одному алгоритму. Нулевые

:red_question_mark:

Although perhaps it doesn’t really matter here.

Big, good job :+1:

I’ve corrected the addition to the guide, it’s now much clearer what those three lines of code do.

1 Like

This is a new script for Blender. It doesn’t replace the main script, but only extends its functionality.. Its unique feature is that it’s 100% generated by a neural network, I didn’t write any of it myself. I simply fed the neural network my previous work, and it produced this. This is very convenient, especially considering we spent almost a month working with the neural network on the old script.

import bpy
import bmesh
from mathutils import Vector
import os
import math # Необходим для floor, хотя в новой логике напрямую не используется, оставим на всякий случай

# --- Конфигурация ---
# Ширина и высота выходной текстуры ID материалов
OUTPUT_TEXTURE_WIDTH = 512
OUTPUT_TEXTURE_HEIGHT = 512
# Путь для сохранения выходной текстуры. Убедитесь, что эта директория существует.
OUTPUT_FILE_PATH = "C:\\"
# ID материала по умолчанию для граней без назначенного материала,
# или если ID не может быть извлечен из имени материала.
# Рекомендуется использовать ID, который не используется для ваших основных материалов (например, 0).
DEFAULT_MATERIAL_ID = 0

# Количество пикселей, на которое нужно "расширить" область запекания вокруг каждого Material ID.
# Это создаст тот самый "отступ" (padding) и решит проблему разрывов.
DILATION_PIXEL_AMOUNT = 2 # Расширение на 2 пикселя во все стороны

# --- Функции кодирования/декодирования ---
# Кодирует числовой ID материала в цвет пикселя (Красный канал).
# Material ID M (1-99) будет закодирован в Красный канал как M/100.
def encode_material_id(material_id_int):
    alpha_value = 1.0 # Объект полностью непрозрачный
    # Красный канал: Material ID / 100.0 (например, 0.01 для ID 1, 0.99 для ID 99, 0.0 для ID 0)
    return [material_id_int / 100.0, 0.0, 0.0, alpha_value] # R, G, B, A

# --- Вспомогательная функция для РАСТЕРИЗАЦИИ ТРЕУГОЛЬНИКОВ (ТОЧНОЙ, без внутреннего расширения) ---
# Эта функция заполняет пиксели внутри UV-треугольника.
# Она использует правило "одной стороны" для проверки точки в треугольнике.
def fill_triangle_pixels_base(pixel_array_flat, uv_coords, material_id_encoded, width, height):
    # Преобразуем UV-координаты (диапазон 0-1) в пиксельные координаты
    p_coords = [Vector((uv.x * width, uv.y * height)) for uv in uv_coords]

    # Находим ограничивающий прямоугольник треугольника в пиксельном пространстве
    min_x = int(min(p.x for p in p_coords))
    max_x = int(max(p.x for p in p_coords))
    min_y = int(min(p.y for p in p_coords))
    max_y = int(max(p.y for p in p_coords))

    # Обрезаем до границ текстуры (0 до width/height - 1)
    min_x = max(0, min_x)
    max_x = min(width - 1, max_x)
    min_y = max(0, min_y)
    max_y = min(height - 1, max_y)

    # Функция sign
    def sign(p1, p2, p3):
        return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)

    # Итерируем по пикселям внутри ограничивающего прямоугольника
    for y in range(min_y, max_y + 1):
        for x in range(min_x, max_x + 1):
            p = Vector((x + 0.5, y + 0.5)) # Проверяем центр пикселя

            # Проверка точки в треугольнике с использованием правила "одной стороны"
            d1 = sign(p_coords[0], p_coords[1], p)
            d2 = sign(p_coords[1], p_coords[2], p)
            d3 = sign(p_coords[2], p_coords[0], p)

            has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
            has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)

            # Если точка находится внутри или на краю треугольника
            if not (has_neg and has_pos):
                index_flat = (y * width + x) * 4 # Индекс в плоском RGBA массиве
                if 0 <= index_flat < len(pixel_array_flat):
                    pixel_array_flat[index_flat : index_flat + 4] = material_id_encoded

# --- ОПТИМИЗИРОВАННАЯ ФУНКЦИЯ: Морфологическая дилатация (расширение) пикселей ---
# Использует два буфера для более эффективной работы без создания полных копий на каждой итерации.
def dilate_pixels(pixel_array_flat, width, height, amount):
    if amount <= 0:
        return pixel_array_flat

    default_id_encoded = encode_material_id(DEFAULT_MATERIAL_ID)
    default_r_value = default_id_encoded[0] 

    # Создаем два буфера:
    # current_pixels: содержит состояние пикселей на начало текущей итерации дилатации
    # next_pixels: сюда записываются результаты текущей итерации дилатации
    current_pixels = list(pixel_array_flat) # Исходное состояние для первой итерации
    next_pixels = [0.0] * len(pixel_array_flat) # Инициализируем пустым буфером

    # Выполняем 'amount' итераций дилатации. Каждая итерация расширяет на 1 пиксель.
    for _ in range(amount):
        # Копируем текущее состояние в next_pixels как основу для записи
        # Это более эффективно, чем list(source_pixels) каждый раз.
        next_pixels[:] = current_pixels[:]

        for y in range(height):
            for x in range(width):
                current_pixel_idx = (y * width + x) * 4
                
                # Проверяем, является ли пиксель из буфера READ (current_pixels) закрашенным (не DEFAULT_MATERIAL_ID)
                if current_pixels[current_pixel_idx] != default_r_value:
                    # Если он закрашен, "распространяем" его цвет на соседей в буфере WRITE (next_pixels)
                    for dy in range(-1, 2): # -1, 0, 1 (для соседних строк)
                        for dx in range(-1, 2): # -1, 0, 1 (для соседних столбцов)
                            nx, ny = x + dx, y + dy
                            
                            # Проверяем, что соседний пиксель находится в пределах границ текстуры
                            if 0 <= nx < width and 0 <= ny < height:
                                neighbor_pixel_idx = (ny * width + nx) * 4
                                
                                # Только если соседний пиксель в буфере READ (current_pixels) является DEFAULT_MATERIAL_ID,
                                # мы его перезаписываем в буфере WRITE (next_pixels).
                                # Это предотвращает перезапись уже закрашенных ID.
                                if current_pixels[neighbor_pixel_idx] == default_r_value:
                                    next_pixels[neighbor_pixel_idx : neighbor_pixel_idx + 4] = current_pixels[current_pixel_idx : current_pixel_idx + 4]
        
        # После завершения прохода, результат из next_pixels становится исходным состоянием для следующей итерации
        current_pixels[:] = next_pixels[:] 
    
    # В конце, копируем окончательный результат обратно в исходный pixel_array_flat
    pixel_array_flat[:] = current_pixels[:] 
    return pixel_array_flat # Возвращаем измененный массив

# --- Основной скрипт ---
def generate_material_id_texture(obj_name=None):
    # Получаем активный объект. Если указано имя, пытаемся найти по имени.
    active_obj = bpy.context.active_object
    if obj_name:
        active_obj = bpy.data.objects.get(obj_name)

    if not active_obj or active_obj.type != 'MESH':
        print("Ошибка: Активный объект не является мешем или не найден. Выберите меш-объект.")
        return

    # Переключаемся в Object Mode, чтобы гарантировать, что данные меша актуальны
    if bpy.context.object.mode != 'OBJECT':
        bpy.ops.object.mode_set(mode='OBJECT')

    mesh_data = active_obj.data

    # Проверяем наличие UV-развертки
    if not mesh_data.uv_layers:
        print("Ошибка: У объекта нет UV-развёртки.")
        return

    # Получаем активный UV-слой
    uv_layer = mesh_data.uv_layers.active

    # Инициализируем плоский массив пикселей (RGBA) для выходной текстуры
    # По умолчанию все пиксели устанавливаются в значение DEFAULT_MATERIAL_ID
    # Это предотвращает "красные разрывы" в местах, где нет UV-островов.
    default_encoded_color = encode_material_id(DEFAULT_MATERIAL_ID)
    flat_pixels = [val for _ in range(OUTPUT_TEXTURE_WIDTH * OUTPUT_TEXTURE_HEIGHT) for val in default_encoded_color]

    # Используем bmesh для более гибкого доступа к данным меша, особенно к UV-лупам (вершинам UV)
    bm = bmesh.new()
    bm.from_mesh(mesh_data)
    # Эти таблицы lookup не строго обязательны для итерации по граням, но полезны для быстрого доступа.
    bm.verts.ensure_lookup_table()
    bm.edges.ensure_lookup_table()
    bm.faces.ensure_lookup_table()

    # Получаем активный UV-слой из bmesh
    bm_uv_layer = bm.loops.layers.uv.active
    if not bm_uv_layer:
        print("Ошибка: Активный UV слой не найден в BMesh. Возможно, UV-развертка повреждена.")
        bm.free()
        return

    print(f"Запекание Material ID для '{active_obj.name}' в разрешение {OUTPUT_TEXTURE_WIDTH}x{OUTPUT_TEXTURE_HEIGHT}...")

    # --- ШАГ 1: БАЗОВАЯ РАСТЕРИЗАЦИЯ ---
    # Запекаем каждый треугольник строго по его UV-границам.
    for face in bm.faces:
        material_id_int = DEFAULT_MATERIAL_ID # Инициализируем ID по умолчанию

        # Проверяем, назначен ли материал грани
        if face.material_index >= 0 and face.material_index < len(active_obj.data.materials):
            material = active_obj.data.materials[face.material_index]
            if material:
                try:
                    # Пытаемся преобразовать имя материала в целое число
                    id_from_name = int(''.join(filter(str.isdigit, material.name)))
                    
                    # Используем ID, если оно разумно (0-99)
                    if 0 <= id_from_name <= 99:
                        material_id_int = id_from_name
                    else:
                        print(f"Предупреждение: ID из имени материала '{material.name}' ({id_from_name}) выходит за ожидаемый диапазон 0-99. Используется DEFAULT_MATERIAL_ID ({DEFAULT_MATERIAL_ID}).")
                except ValueError:
                    print(f"Предупреждение: Имя материала '{material.name}' не содержит числового ID. Используется DEFAULT_MATERIAL_ID ({DEFAULT_MATERIAL_ID}).")
        
        encoded_color = encode_material_id(material_id_int)

        # Получаем UV-координаты для вершин этой грани
        uv_coords_for_face = []
        for loop in face.loops:
            uv_coords_for_face.append(loop[bm_uv_layer].uv)

        # Растеризуем грань. Для квадов (4 вершины) разбиваем на два треугольника.
        if len(uv_coords_for_face) == 3:
            fill_triangle_pixels_base(flat_pixels, uv_coords_for_face, encoded_color, OUTPUT_TEXTURE_WIDTH, OUTPUT_TEXTURE_HEIGHT)
        elif len(uv_coords_for_face) == 4:
            fill_triangle_pixels_base(flat_pixels, [uv_coords_for_face[0], uv_coords_for_face[1], uv_coords_for_face[2]], encoded_color, OUTPUT_TEXTURE_WIDTH, OUTPUT_TEXTURE_HEIGHT)
            fill_triangle_pixels_base(flat_pixels, [uv_coords_for_face[0], uv_coords_for_face[2], uv_coords_for_face[3]], encoded_color, OUTPUT_TEXTURE_WIDTH, OUTPUT_TEXTURE_HEIGHT)
        else:
            print(f"Предупреждение: Пропущен многоугольник с {len(uv_coords_for_face)} вершинами (не треугольник и не квад) в UV-пространстве для материала '{material.name if 'material' in locals() and material else 'Нет материала'}'.")

    bm.free() # Освобождаем данные bmesh

    # --- ШАГ 2: ПРИМЕНЯЕМ ДИЛАТАЦИЮ ---
    # После того как все треугольники запечены, применяем дилатацию, чтобы создать отступы.
    if DILATION_PIXEL_AMOUNT > 0:
        print(f"Применяем дилатацию на {DILATION_PIXEL_AMOUNT} пикселей...")
        # Передаем flat_pixels по ссылке, функция изменит его содержимое напрямую
        dilate_pixels(flat_pixels, OUTPUT_TEXTURE_WIDTH, OUTPUT_TEXTURE_HEIGHT, DILATION_PIXEL_AMOUNT)

    # --- Создание и сохранение текстуры ---
    # Убеждаемся, что выходная директория существует
    os.makedirs(OUTPUT_FILE_PATH, exist_ok=True)

    model_name = active_obj.name
    # Генерируем имя для изображения в Blender
    img_name = f"{model_name}_material_ids_map"

    # Удаляем существующее изображение с таким же именем, чтобы избежать конфликтов
    if img_name in bpy.data.images:
        bpy.data.images.remove(bpy.data.images[img_name])

    # Создаём новое изображение в Blender
    img = bpy.data.images.new(img_name, width=OUTPUT_TEXTURE_WIDTH, height=OUTPUT_TEXTURE_HEIGHT, alpha=True, float_buffer=True)
    
    # Очень важно: устанавливаем цветовое пространство "Non-Color" для карт ID,
    # чтобы Blender не применял цветовые преобразования к нашим числовым данным.
    img.colorspace_settings.name = "Non-Color"
    
    # Заполняем пиксели изображения из нашего подготовленного массива
    img.pixels.foreach_set(flat_pixels)

    # Определяем полный путь для сохранения файла
    texture_path = os.path.join(OUTPUT_FILE_PATH, f"{img_name}.exr")
    img.filepath_raw = texture_path
    img.file_format = 'OPEN_EXR' # Формат EXR рекомендован для float-буферных данных
    img.save()

    print(f"Текстура ID материалов '{img_name}.exr' успешно создана и сохранена в: {texture_path}")

# --- Запуск скрипта ---
# Вызовите функцию. Она будет обрабатывать текущий активный объект в Blender.
generate_material_id_texture()

This script can now work with objects of absolutely any shape and size, the limitation called “plane” is gone. However, in exchange, there are no transitions between textures. But let’s be honest, transitions are used in 99% of cases only when creating landscapes, so just remember to update your UVs after any changes.

There’s really nothing to show here, as this script works exactly the same as the previous one, and it’s 100% compatible with the Godot shader. Therefore, a detailed tutorial isn’t needed, I’ll just go over the main points.

Screenshot_41
Here you need to specify the path to save the texture, I recommend specifying the path to the project folder for dynamic updating

Screenshot_40
This code operates on the principles of texture baking, so you need to specify the texture resolution here. A higher resolution will result in a better overlay. However, from observation, it is better to use this code for texturing buildings, as they usually have a low polygon count, and mostly straight polygons, which means there is no need to set the resolution too high.

Screenshot_39
Also, in the shader parameters, specify the same values for scale_1.
There is no need to touch anything else in the Godot shader, everything will work as with the previous code, only without texture transitions.

Screenshot_42
If you notice gaps between textures, just increase this parameter.

I still haven’t figured out how to combine smooth transitions and any shape of objects into one shader, so most likely there won’t be any more updates to this code, at least until such a need arises.

The main thing to remember is that this Blender script and its accompanying Godot shader are only needed for objects that require a large number of different materials, a scenario which would otherwise inevitably lead to FPS drops. If you can bake your textures into an atlas, or if you only have 1, 2, or 3 materials, then there’s no point in using this code.

Game development is about combining different technologies and methods, not relying on a single method for every situation

1 Like