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:
- After creating the desired shape, be sure to reset all transforms by pressing Ctrl + A: All Transforms.
- Do not modify the UV.
- Do not use Extrude.
- 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:
- Name your materials using only numbers within the range of 0 to 99.
- 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: