DrawableTexture2D: запись, mipmap и рисование
Инициализация, полные C#-примеры, несколько выходов и правильное смешивание.
DrawableTexture2D — текстура, в которую можно выполнять GPU-операции BlitRect/BlitRectMulti. Её можно показывать через TextureRect или Sprite2D, как другую Texture2D. Изменение происходит по вызову операции: назначенный при blit материал не становится постоянным эффектом текстуры.
Создайте ресурс до первой записи
Вызовите Setup с положительными размерами от 1 до 16384, форматом и явным начальным цветом. Для прозрачного холста используйте Colors.Transparent: пропущенный/null цвет Setup означает белый, а не прозрачный.
Сохраните полный помощник DrawableOps.cs. Он создаёт проверенный RGBA8-ресурс, копирует источник целиком и выполняет пример с двумя источниками/четырьмя выходами:
using System;
using Reckfell;
public static class DrawableOps
{
public static DrawableTexture2D Create(Vector2I size, Color color, bool mipmaps = false)
{
if (size.X < 1 || size.Y < 1 || size.X > 16384 || size.Y > 16384)
throw new ArgumentOutOfRangeException(nameof(size));
var target = new DrawableTexture2D();
target.Setup(size.X, size.Y, DrawableTexture2D.DrawableFormat.Rgba8, color, mipmaps);
return target;
}
public static void Copy(DrawableTexture2D target, Texture2D source, Material material)
{
if (target.GetRid() == source.GetRid())
throw new ArgumentException("Source and destination must be different textures.", nameof(source));
target.BlitRect(new Rect2I(0, 0, target.GetWidth(), target.GetHeight()), source, Colors.White, 0, material);
}
public static void Split(DrawableTexture2D target, DrawableTexture2D output1,
DrawableTexture2D output2, DrawableTexture2D output3, Texture2D first,
Texture2D second, Rect2I rect, Material material)
{
var outputs = new[] { target, output1, output2, output3 };
for (int i = 0; i < outputs.Length; i++)
for (int j = 0; j < i; j++)
if (outputs[i].GetRid() == outputs[j].GetRid())
throw new ArgumentException("Output textures must be distinct.");
foreach (var output in outputs)
{
if (output.GetWidth() != target.GetWidth() || output.GetHeight() != target.GetHeight())
throw new ArgumentException("Output textures must have the same size.");
if (output.GetRid() == first.GetRid() || output.GetRid() == second.GetRid())
throw new ArgumentException("A source cannot also be an output.");
}
var sources = new Reckfell.Collections.Array<Texture2D> { first, second };
using var sourcesOwner = (Reckfell.Collections.Array)sources;
var extras = new Reckfell.Collections.Array<DrawableTexture2D> { output1, output2, output3 };
using var extrasOwner = (Reckfell.Collections.Array)extras;
target.BlitRectMulti(rect, sources, extras, Colors.White, 0, material);
}
}Для первого результата создайте DrawableOps.Create(new Vector2I(256, 256), Colors.Transparent), назначьте ресурс в Texture у TextureRect и вызовите target.BlitRect(new Rect2I(20, 30, 40, 40), null, Colors.Red). null-источник использует белую текстуру, поэтому цвет задаёт modulate. Эта операция рисует красный прямоугольник.
Setup повторно создаёт содержимое и сбрасывает предыдущие рисунки. В проверенном RD-пути ресурс сохраняет свой RID, но это не сохранение пикселей. SetFormat и SetUseMipmaps меняют настройки ресурса без переразмещения уже созданного хранилища; для нового формата/цепочки mipmap снова используйте Setup. GetUseMipmaps не доказывает наличие физических уровней после отдельного изменения флага.
Прямоугольник и смешивание
rect задаёт область назначения, а не вырезку из источника. В неё масштабируется весь источник; для обрезки изменяйте UV в собственном шейдере. Пиксели вне прямоугольника остаются прежними. Нельзя читать ту же текстуру, в которую идёт запись, включая дополнительные выходы. Для последовательных эффектов используйте две текстуры и меняйте их ролями.
Без явного material применяется готовый blend_mix: полупрозрачный красный поверх синего даёт смесь. Собственный texture_blit без render_mode использует blend_disabled и заменяет RGBA. MODULATE умножает результат стандартного шейдера; это не маска каналов. Например (0,0,1,0) при blend_disabled записывает синий с нулевой alpha и обнуляет R/G, а при обычном blend_mix alpha=0 не даёт видимого вклада.
BlitMaterial выбирает готовую операцию копирования с одним из проверенных режимов Mix/Add/Sub/Mul/Disabled. Он заменяет переданный материал, а не переопределяет render_mode произвольного пользовательского шейдера. Для собственной формулы используйте ShaderMaterial типа texture_blit. См. справочник шейдера.
Mipmap-уровни
Создайте цепочку через Setup(..., useMipmaps:true). Обычная запись в уровень 0 не означает автоматическую актуализацию всей цепочки. После серии операций вызовите GenerateMipmaps, если нижние уровни должны соответствовать базе.
Параметр mipmap выбирает уровень назначения. rect всё ещё задаётся относительно полного базового размера: у текстуры 16×16 прямоугольник (0,0,16,16) заполняет весь уровень 1 размером 8×8. Запись в уровень 1 оставляет базовый уровень прежним. GenerateMipmaps затем перезаписывает нижние уровни из базы, включая вручную нарисованный уровень 1. Не указывайте уровень, которого нет в выделенной цепочке.
Несколько источников и результатов
BlitRectMulti принимает 1–4 источника и до трёх дополнительных целей: вместе с текущей текстурой это 1–4 выхода. Все цели должны быть одинакового размера и иметь запрошенный mipmap-уровень. Источники не должны совпадать ни с одной целью; не используйте повторяющиеся выходные ресурсы.
Помощник Split выше показывает полный C#-вызов для двух источников и четырёх целей. Для него используйте BlitMulti.reckfellshader из справочника. COLOR0 пишет в target, COLOR1/2/3 — в output1/2/3. Источники с hint_blit_source0/1 не обязаны математически соответствовать выходам: это определяет ваш код.
Array<T> из Reckfell.Collections не реализует IDisposable напрямую. В помощнике освобождается принадлежащий ему базовый Array через явное преобразование; сами текстуры от этого не освобождаются. Не помещайте заимствованные ресурсы, которые ещё отображаются, в короткие using-области.
Полный пример рисования в TextureRect
Сохраните DrawablePaint.cs рядом с DrawableOps.cs и прикрепите его к TextureRect с ненулевым размером. Скрипт растягивает весь холст в элемент управления, переводит его локальные координаты в пиксели текстуры и рисует квадратной кистью левой кнопкой. Движение заполняет промежутки между полученными позициями.
using Reckfell;
public partial class DrawablePaint : TextureRect
{
[Export] public Vector2I CanvasSize { get; set; } = new Vector2I(256, 256);
[Export] public int BrushSize { get; set; } = 12;
[Export] public Color BrushColor { get; set; } = Colors.Red;
public DrawableTexture2D Canvas { get; private set; } = null!;
private bool drawing;
private Vector2 last;
public override void _Ready()
{
Canvas = DrawableOps.Create(CanvasSize, Colors.Transparent);
Texture = Canvas;
ExpandMode = ExpandModeEnum.IgnoreSize;
StretchMode = StretchModeEnum.Scale;
}
public override void _GuiInput(InputEvent input)
{
if (Size.X <= 0 || Size.Y <= 0) return;
if (input is InputEventMouseButton button && button.ButtonIndex == MouseButton.Left)
{
drawing = button.Pressed;
if (drawing)
{
last = ToPixel(button.Position);
Stamp(last);
}
}
else if (input is InputEventMouseMotion motion)
{
if ((motion.ButtonMask & MouseButtonMask.Left) == 0) { drawing = false; return; }
if (!drawing) return;
Vector2 current = ToPixel(motion.Position);
int steps = Mathf.Max(1, Mathf.CeilToInt(last.DistanceTo(current) / Mathf.Max(1, BrushSize / 2)));
for (int i = 1; i <= steps; i++) Stamp(last.Lerp(current, (float)i / steps));
last = current;
}
}
private Vector2 ToPixel(Vector2 point) => point / Size * new Vector2(CanvasSize.X, CanvasSize.Y);
private void Stamp(Vector2 point)
{
int width = Mathf.Max(1, BrushSize);
var rect = new Rect2I(Mathf.FloorToInt(point.X - width / 2f), Mathf.FloorToInt(point.Y - width / 2f), width, width);
Canvas.BlitRect(rect, null!, BrushColor);
}
}Не меняйте CanvasSize после _Ready без пересоздания холста. Пример рассчитан на StretchMode.Scale и ExpandMode.IgnoreSize; для KeepAspect, области изображения с полями, повёрнутой кисти или текстуры атласа нужен соответствующий перевод координат. BrushSize измеряется в пикселях текстуры, а не экрана.
Обновление, чтение и ограничения
Изменение uniform не переигрывает предыдущий blit. TIME берётся в момент новой операции, поэтому текстура сама по себе не анимируется. Повторяйте blit осознанно, когда меняются данные, а не просто каждый кадр без необходимости.
GetImage выполняет чтение результата с GPU и создаёт принадлежащий вызывающему Image. Освободите Image после использования; не делайте readback для каждого штриха, если данные нужны только на GPU. Rgba8, Rgba8Srgb, Rgbah и Rgbaf имеют разную интерпретацию цвета/точность. Обычные RGBA8-пиксели примера не доказывают одинаковую арифметику всех HDR/sRGB путей.
AtlasTexture как источник текущий ресурсный API не поддерживает и подставляет белый с предупреждением. Используйте сам атлас и явное преобразование UV. Для premultiplied alpha используйте проверенный ShaderMaterial с blend_premul_alpha: текущий кэш BlitMaterial не покрывает объявленное enum-значение PremultipliedAlpha, поэтому этот путь не следует выбирать у BlitMaterial данной версии.
Диагностика
| Симптом | Что проверить |
|---|---|
| Пустая или белая текстура | Setup, начальный цвет, корректный источник и выполненный BlitRect. |
| Прозрачный цвет не смешивается | Различие готового blend_mix и собственного blend_disabled. |
| После uniform-правки ничего не меняется | Выполните новую операцию blit. |
| Кисть смещена или имеет неверный размер | Перевод Size элемента в CanvasSize и режим растяжения. |
| Рисование начинается правой кнопкой/не останавливается | ButtonIndex, Pressed и ButtonMask; не переключайте флаг на любом клике. |
| Мелкий масштаб показывает старую картинку | Актуальность mipmap-цепочки. |
| Mipmap-уровень исчез после GenerateMipmaps | Генерация заменяет вручную записанные нижние уровни. |
| Несколько выходов дают ошибку | Число, равенство размеров, наличие уровней и отсутствие совпадений с источниками. |
В Windows Forward+/Vulkan проверены фактические Image-пиксели копирования, смешивания, нескольких выходов, mipmap и вызовов _GuiInput примера. Последние — программно переданные события, не проверка реального мышиного ввода. Все платформы/рендереры, HDR/sRGB, AtlasTexture и опасный путь PremultipliedAlpha у BlitMaterial здесь отдельно не запускались. Снимки этих функциональных проверок не публикуются как художественные иллюстрации.