EditableImage from CreateEditableImageAsync results in error texture (magenta/cyan checkerboard)

Hello, developers.

I’m trying to create a procedural floor texture using the EditableImage API, but I’ve run into an issue I can’t solve.

My Goal:
My script is supposed to:

  1. Load a base texture from an Asset ID into an EditableImage.

  2. Load several brush textures from Asset IDs.

  3. Repeatedly draw the brushes onto the base texture at random positions to create a new, unique texture.

  4. Apply the final EditableImage to an ImageLabel on a SurfaceGui.

The Problem:
The script runs without any errors in the output. The pcall for AssetService:CreateEditableImageAsync reports success. However, from the very first step, the EditableImage object renders as the magenta and cyan checkerboard “error texture”, not the image from my asset. This happens for both the base texture and the brushes.

Here is a screenshot of my debug UI, which shows the texture state right after loading the base image (“1. Базовый слой” / “1. Base Layer”). As you can see, it’s already the error texture. The final result on the floor is the same.

What I’ve Already Tried:

  • Verified Asset IDs: All Asset IDs are correct, copied directly using “Copy ID to clipboard”, and have the rbxassetid:// prefix.

  • Checked Permissions: All assets (the base texture and all brushes) were uploaded by me, to my own account. The experience is also owned by me. I am not using any assets from the toolbox or other creators.

  • Enabled API Access: In Game Settings > Security, the Allow Editable Mesh / Image APIs toggle is enabled.

  • Correct Property Usage: I am assigning the final result to ImageLabel.ImageContent using Content.fromObject(), as required by the documentation.

  • Debugged the Process: The logs show that the script executes completely. The pcall succeeds, brushes are reported as “loaded”, and the generation loop completes. The issue appears to be that CreateEditableImageAsync itself is returning a “broken” image object despite not erroring.

Here is the full script. I’ve replaced my actual Asset IDs with placeholders for privacy.

-- Службы Roblox
local AssetService = game:GetService("AssetService")
local Workspace = game:GetService("Workspace")

-- Настройки генерации
local CANVAS_SIZE = Vector2.new(100, 100)
local NUM_SPLATS = 150

local BASE_TEXTURE_ID = workspace.LandTextures.ground_01.ground_01.Texture
local BRUSH_TEXTURE_IDS = {
	workspace.LandTextures.ground_01_obj_01.ground_01_obj_01.Texture,
	workspace.LandTextures.ground_01_obj_02.ground_01_obj_02.Texture,
	workspace.LandTextures.ground_01_obj_03.ground_01_obj_03.Texture,
}

-- ===============================================
-- СЕКЦИЯ ОТЛАДКИ
-- ===============================================
print("Создание отладочной панели...")
local debugFrame = Instance.new("ScreenGui")
debugFrame.Name = "EditableImage_Debug"
debugFrame.ResetOnSpawn = false

local debugLayout = Instance.new("UIListLayout")
debugLayout.FillDirection = Enum.FillDirection.Vertical
debugLayout.SortOrder = Enum.SortOrder.LayoutOrder
debugLayout.Padding = UDim.new(0, 10)
debugLayout.Parent = debugFrame

-- Функция для создания "снимка" на экране
local function createDebugSnapshot(image, labelText, order)
	local label = Instance.new("TextLabel")
	label.Size = UDim2.new(0, 200, 0, 20)
	label.Text = labelText
	label.TextColor3 = Color3.new(1, 1, 1)
	label.BackgroundColor3 = Color3.new(0,0,0)
	label.BackgroundTransparency = 0.5
	label.LayoutOrder = order * 2 - 1
	label.Parent = debugFrame

	local snapshot = Instance.new("ImageLabel")
	snapshot.Size = UDim2.new(0, 200, 0, 200)
	snapshot.BackgroundColor3 = Color3.new(0.1, 0.1, 0.1)
	snapshot.LayoutOrder = order * 2
	snapshot.Image = Content.fromObject(image)
	snapshot.Parent = debugFrame
end

-- Функция для копирования EditableImage (чтобы создать независимый снимок)
local function copyEditableImage(sourceImage)
	-- 1. Создаем пустое изображение такого же размера
	local copy = AssetService:CreateEditableImage({ Size = sourceImage.Size })

	-- 2. Рисуем одно изображение на другом, используя стандартный режим наложения AlphaBlend
	copy:DrawImage(Vector2.new(0,0), sourceImage, Enum.ImageCombineType.AlphaBlend)

	return copy
end

debugFrame.Parent = game.Players:WaitForChild("PP_Boat"):WaitForChild("PlayerGui") -- Отображаем первому игроку на сервере
-- ===============================================

-- 1. ПОДГОТОВКА СЦЕНЫ
-- ... (код остался тот же, что и раньше) ...
local baseplate = Workspace.Baseplate
baseplate.Anchored = true
local floorCanvasPart = Instance.new("Part", Workspace)
floorCanvasPart.Name = "ProceduralFloorCanvas"
floorCanvasPart.Size = baseplate.Size
floorCanvasPart.Position = baseplate.Position + Vector3.new(0, 0.05, 0)
floorCanvasPart.Anchored = true
floorCanvasPart.Transparency = 1
local surfaceGui = Instance.new("SurfaceGui", floorCanvasPart)
surfaceGui.Face = Enum.NormalId.Top
surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
surfaceGui.PixelsPerStud = CANVAS_SIZE.X / floorCanvasPart.Size.X
local imageLabel = Instance.new("ImageLabel", surfaceGui)
imageLabel.Size = UDim2.fromScale(1, 1)
imageLabel.BackgroundTransparency = 1

-- 2. ЗАГРУЗКА РЕСУРСОВ
local success, finalFloorTexture = pcall(function()
	return AssetService:CreateEditableImageAsync(BASE_TEXTURE_ID)
end)

if not success then
	warn("!!! КРИТИЧЕСКАЯ ОШИБКА: Не удалось загрузить базовую текстуру!", finalFloorTexture)
	return
end
print("Базовая текстура успешно загружена.")
createDebugSnapshot(copyEditableImage(finalFloorTexture), "1. Базовый слой", 1) -- << ДЕЛАЕМ ПЕРВЫЙ СНИМОК

local brushTextures = {}
-- ... (загрузка кистей, код не изменился) ...
for i, id in ipairs(BRUSH_TEXTURE_IDS) do
	local ok, brush = pcall(function() return AssetService:CreateEditableImageAsync(id) end)
	if ok then table.insert(brushTextures, brush) print("Кисть #"..i.." загружена.") else warn("Не удалось загрузить кисть #"..i, brush) end
end
if #brushTextures == 0 then warn("Ни одной кисти не было загружено.") finalFloorTexture:Destroy() return end

-- 3. ПРОЦЕСС ГЕНЕРАЦИИ
print("Начинаю генерацию...")
for i = 1, NUM_SPLATS do
	local randomBrush = brushTextures[math.random(#brushTextures)]
	local pos = Vector2.new(math.random(-200, CANVAS_SIZE.X + 200), math.random(-200, CANVAS_SIZE.Y + 200))
	local rotation = math.random(0, 360)
	local scaleFactor = math.random(50, 120) / 100
	local scale = Vector2.new(scaleFactor, scaleFactor)

	finalFloorTexture:DrawImageTransformed(pos, scale, rotation, randomBrush)

	-- << ДЕЛАЕМ ПРОМЕЖУТОЧНЫЕ СНИМКИ >>
	if i == 50 then
		createDebugSnapshot(copyEditableImage(finalFloorTexture), "2. После 50 сплэтов", 2)
		print("Сделан снимок после 50 итераций.")
	elseif i == 100 then
		createDebugSnapshot(copyEditableImage(finalFloorTexture), "3. После 100 сплэтов", 3)
		print("Сделан снимок после 100 итераций.")
	end
end
print("Генерация завершена!")
createDebugSnapshot(copyEditableImage(finalFloorTexture), "4. Финальный результат", 4) -- << ДЕЛАЕМ ФИНАЛЬНЫЙ СНИМОК

-- 4. ОТОБРАЖЕНИЕ РЕЗУЛЬТАТА
imageLabel.Image = Content.fromObject(finalFloorTexture)

-- 5. ОЧИСТКА ПАМЯТИ
print("Очистка временных ресурсов...")
for _, brush in ipairs(brushTextures) do
	brush:Destroy()
end
print("Процедурный пол готов!")

My output log is clean and shows every step completing as expected:
Создание отладочной панели…
Базовая текстура успешно загружена.
Кисть #1 загружена.
Кисть #2 загружена.
Кисть #3 загружена.
Начинаю генерацию…
Генерация завершена!
Очистка временных ресурсов…
Процедурный пол готов!

Has anyone else experienced this? Is there anything else I can check? Could this be an issue with the asset format itself (e.g., something wrong with the .png file), or a temporary engine bug?

Thank you for any help you can provide.

roblox has made it pretty clear that editable images dont replicate.

Any advice for editable images?

they cannot be replicated right now
You have to create them locally.