Свой блок Visual Shader на C#
Порты, генерация кода, программный граф и сохранение C#-блока.
Собственный блок Visual Shader — C#-ресурс, который объявляет порты и возвращает фрагмент шейдерного кода. Это не игровая Entity: слово «узел» здесь означает элемент графа вычислений. В примере BlendTint смешивает исходный RGB с оттенком по весу amount от 0 до 1.
Нужен C#-редактор Reckfell 1.0.0 и собирающийся проект. Для одного такого блока EditorPlugin и plugin.cfg не нужны: редактор обнаруживает класс через [GlobalClass], а [Tool] позволяет выполнять его C#-методы в редакторе.
1. Добавьте блок
Создайте asset://addons/docs_visual/BlendTint.cs. Имя файла должно совпадать с именем класса:
using Reckfell;
using Reckfell.Collections;
[Tool, GlobalClass]
public partial class BlendTint : VisualShaderNodeCustom
{
public override string _GetName() => "BlendTint";
public override string _GetCategory() => "Reckfell/Color";
public override string _GetDescription() => "Blend RGB toward a tint; amount is clamped to 0..1.";
public override PortType _GetReturnIconType() => PortType.Vector3D;
public override int _GetInputPortCount() => 3;
public override string _GetInputPortName(int port) => port switch
{
0 => "source", 1 => "tint", 2 => "amount", _ => ""
};
public override PortType _GetInputPortType(int port) =>
port == 2 ? PortType.Scalar : PortType.Vector3D;
public override Variant _GetInputPortDefaultValue(int port) => port switch
{
0 => Vector3.One,
1 => new Vector3(0.1f, 0.35f, 1.0f),
2 => 0.5f,
_ => default
};
public override int _GetOutputPortCount() => 1;
public override string _GetOutputPortName(int port) => "rgb";
public override PortType _GetOutputPortType(int port) => PortType.Vector3D;
public override bool _IsAvailable(Shader.Mode mode, VisualShader.Type type) =>
(mode == Shader.Mode.CanvasItem || mode == Shader.Mode.Spatial) &&
type == VisualShader.Type.Fragment;
public override string _GetGlobalCode(Shader.Mode mode) => """
vec3 reckfell_docs_blend_tint(vec3 source, vec3 tint, float amount) {
return mix(source, tint, clamp(amount, 0.0, 1.0));
}
""";
public override string _GetCode(Array<string> inputVars, Array<string> outputVars,
Shader.Mode mode, VisualShader.Type type) =>
$"{outputVars[0]} = reckfell_docs_blend_tint({inputVars[0]}, {inputVars[1]}, {inputVars[2]});";
}Соберите проект. Если класс ещё не появился в списке добавления блоков, дождитесь сканирования файлов и заново откройте граф; при необходимости перезапустите редактор. Не добавляйте фиктивный plugin.cfg: он не исправит ошибку C#-сборки или отсутствие [GlobalClass].
2. Соберите граф в редакторе
- Выберите
Sprite2Dс текстурой либоColorRectс ненулевым размером. Создайте у него ресурс Material → ShaderMaterial. - В свойстве Shader создайте VisualShader и откройте его. Установите режим CanvasItem, этап Fragment.
- В окне добавления найдите
BlendTint. Категория — Addons → Reckfell → Color._GetCategory()возвращает вложенный путьReckfell/Color; префиксAddonsдобавляет редактор. - Добавьте блок. Соедините выход
rgbс входом Color блока Output. Порт Alpha этот пример не изменяет. - Оставьте значения
source = (1,1,1),tint = (0.1,0.35,1),amount = 0.5. Формула даёт RGB(0.55,0.675,1). Это результат вычисления в шейдере, а не обещание идентичного экранного пикселя при любых настройках вывода. - Измените
amount: при 0 получаетсяsource, при 1 —tint. Значения вне диапазона ограничиваются функциейclamp(). - Сохраните ресурс VisualShader в проекте, например как
asset://tint-graph.rdata.
Для 3D используйте Spatial → Fragment и соедините rgb с Albedo. Это тот же RGB-расчёт, но освещение материала влияет на изображение. Самостоятельный BlendTint не читает текстуру спрайта: чтобы сохранить её рисунок, подключите цвет из графа к source вместо постоянного белого значения. Обработка альфа-канала остаётся отдельной связью.
Порты и генерируемый код
| Порт | Тип | Значение без соединения |
|---|---|---|
source | Vector3D → vec3 | (1,1,1) |
tint | Vector3D → vec3 | (0.1,0.35,1) |
amount | Scalar → float | 0.5 |
rgb | Vector3D → vec3 | Выход вычисления |
_GetInputPortName() задаёт подпись порта. Не подставляйте её вместо inputVars[i]: генератор передаёт готовые выражения и имена временных переменных, которые зависят от графа. _GetCode() должен присвоить результат соответствующему outputVars[i] и закончить инструкцию точкой с запятой.
Строка, возвращаемая C#-методом, написана на шейдерном языке Reckfell, а не на C#. Поэтому внутри неё используются vec3, mix() и clamp(). C#-сборка проверяет обёртку, но сама по себе не доказывает корректность GPU-кода.
_GetGlobalCode() объявляет общую функцию. Генератор добавляет её один раз для использованного типа C#-блока, даже если в графе несколько экземпляров. Дайте функции уникальное имя: два разных скрипта с одинаковым именем глобальной функции могут конфликтовать. Не помещайте туда состояние, уникальное для отдельного экземпляра.
_IsAvailable() ограничивает список добавления режимами CanvasItem/Spatial и этапом Fragment. Это фильтр доступности, а не механизм перевода кода из одного шейдерного режима в другой.
3. Тот же граф из C#
Для процедурного создания графа сохраните TintGraph.cs рядом с проектными скриптами:
using System;
using Reckfell;
public static class TintGraph
{
public static VisualShader Create()
{
var shader = new VisualShader();
shader.SetMode(Shader.Mode.CanvasItem);
var tint = new BlendTint();
int id = shader.GetValidNodeId(VisualShader.Type.Fragment);
shader.AddNode(VisualShader.Type.Fragment, tint, new Vector2(100, 100), id);
// The editor applies these callbacks when inserting a node through its UI.
// AddNode alone does not initialize the default input values.
for (int port = 0; port < tint._GetInputPortCount(); port++)
tint.SetInputPortDefaultValue(port, tint._GetInputPortDefaultValue(port));
var error = shader.ConnectNodes(VisualShader.Type.Fragment, id, 0,
(int)VisualShader.NodeIdOutput, 0);
if (error != Error.Ok)
{
shader.Dispose();
throw new InvalidOperationException("Cannot connect tint output: " + error);
}
return shader;
}
}TintGraph.Create() возвращает ресурс VisualShader; назначьте его свойству ShaderMaterial.Shader. Результат принадлежит вызывающему коду и материалу, которому он назначен. Не вызывайте Dispose() у ресурса или его C#-блока, пока их ещё использует материал/граф.
Важное различие 1.0.0: AddNode() обновляет описание портов, но не применяет _GetInputPortDefaultValue() автоматически. Редактор вызывает эти методы при добавлении через UI. Поэтому фабрика явно записывает исходные значения после AddNode().
Нельзя заключать new BlendTint() внутри фабрики в using: при выходе из метода Dispose() отсоединяет C#-поведение от ещё используемого native-ресурса. В графе останется объект, но нужные переопределения уже не будут работать. Это проверено на установленной версии; для живого блока оставляйте управление временем жизни графу и C#-мосту.
Для связей используйте один и тот же этап VisualShader.Type.Fragment. ID 0 принадлежит Output; свободный ID запрашивайте через GetValidNodeId(). Перед интерактивным соединением полезен CanConnectNodes(): он отклоняет отсутствующие блоки, неверные порты, повторные связи и циклы. Всё равно проверяйте результат ConnectNodes().
Ошибки и исправления
| Симптом | Причина и действие |
|---|---|
Нет BlendTint в списке | Проверьте сборку, [Tool, GlobalClass], наследование VisualShaderNodeCustom, совпадение имени файла и класса. Перепроверьте режим и этап графа. |
| Ошибка имени блока | _GetName() должен быть допустимым ASCII-идентификатором без пробелов; используйте BlendTint. Категория, в отличие от имени, допускает путь через /. |
| Порт выдаёт чёрный цвет | Проверьте значения неподключённых портов, выходную связь и отсутствие преждевременного Dispose(). При программном создании явно назначьте значения по умолчанию. |
| Ошибка компиляции шейдера | Откройте сгенерированный код и сообщение ошибки. Проверьте типы vec3/float, объявления вспомогательной функции, имена и ;. Успешная C#-сборка этого не проверяет. |
| Несколько блоков вызывают повторное объявление | Общая функция должна жить в _GetGlobalCode(), иметь уникальное имя и не повторяться вручную в _GetCode(). |
| После удаления блока пропал эффект | Входящие и исходящие связи удаляются вместе с ним. Соедините оставшийся граф с Output заново. |
| После перезапуска ресурс не работает | Сохраните и сам граф, и исходный BlendTint.cs; не переносите один .rdata без его C#-зависимости. Пересоберите проект. |
Границы проверки
В headless-редакторе 1.0.0 проверены регистрация глобального C#-класса, типы и значения портов, построение и связи графа, генерация текста CanvasItem Fragment, сохранение/загрузка со скриптом, два экземпляра с одной общей функцией и удаление связей. Пример не подтверждает GPU-компиляцию, экранный цвет, физическое управление графом, картинку Spatial, экспорт или работу на других платформах. Не используйте этот пример как иллюстрацию качества рендера.