A custom Visual Shader block in C#
Ports, generated code, procedural graphs and C# block persistence.
A custom Visual Shader block is a C# resource that declares ports and returns shader source. It is not a game Entity: “node” here means an element of a computation graph. This example, BlendTint, blends source RGB toward a tint with an amount from 0 to 1.
Use the Reckfell 1.0.0 C# editor and a project that builds. A single custom block needs neither EditorPlugin nor plugin.cfg: [GlobalClass] makes the class discoverable, while [Tool] allows its C# methods to run in the editor.
1. Add the block
Create asset://addons/docs_visual/BlendTint.cs. The filename must match the class name:
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]});";
}Build the project. If the block has not appeared in the add dialog, wait for filesystem scanning and reopen the graph; restart the editor if needed. Adding a fictitious plugin.cfg cannot fix a C# build failure or a missing [GlobalClass] attribute.
2. Build the graph in the editor
- Select a
Sprite2Dwith a texture or aColorRectwith nonzero size. Create Material → ShaderMaterial. - Create a VisualShader in its Shader property and open it. Select CanvasItem mode and the Fragment stage.
- Find
BlendTintin the add dialog. Its category is Addons → Reckfell → Color._GetCategory()returnsReckfell/Color; the editor adds theAddonsprefix. - Add the block and connect its
rgboutput to Color on Output. This example does not modify the Alpha port. - Keep
source = (1,1,1),tint = (0.1,0.35,1)andamount = 0.5. The formula produces RGB(0.55,0.675,1). This is a shader calculation, not a promise of an identical screen pixel under every output configuration. - Change
amount: 0 producessource, 1 producestint.clamp()restricts values outside the range. - Save the VisualShader resource in the project, for example as
asset://tint-graph.rdata.
For 3D, use Spatial → Fragment and connect rgb to Albedo. The RGB calculation is the same, but material lighting affects the image. BlendTint does not sample a sprite texture itself: connect a color from the graph to source instead of constant white to preserve its pattern. Alpha remains a separate connection.
Ports and generated source
| Port | Type | Value when unconnected |
|---|---|---|
source | Vector3D → vec3 | (1,1,1) |
tint | Vector3D → vec3 | (0.1,0.35,1) |
amount | Scalar → float | 0.5 |
rgb | Vector3D → vec3 | Calculated output |
_GetInputPortName() defines a label. Do not substitute that label for inputVars[i]: the generator supplies expressions and temporary variable names determined by the graph. _GetCode() must assign each result to the corresponding outputVars[i] and terminate its statement with a semicolon.
The string returned by the C# method contains Reckfell shader language, not C#. That is why it uses vec3, mix() and clamp(). Building C# checks the wrapper but does not prove GPU source correctness.
_GetGlobalCode() declares the shared helper. The generator emits it once for the used C# block type even when the graph has several instances. Use a unique function name: two different scripts declaring the same global function can conflict. Do not put per-instance state there.
_IsAvailable() limits the add list to CanvasItem/Spatial Fragment graphs. This is an availability filter, not a way to translate code between shader modes.
3. Create the same graph from C#
Save TintGraph.cs with your project scripts:
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() returns a VisualShader resource; assign it to ShaderMaterial.Shader. The caller and the assigned material control the resource lifetime. Do not call Dispose() on the resource or its C# block while a material or graph still uses them.
A 1.0.0 distinction: AddNode() updates port descriptions but does not apply _GetInputPortDefaultValue() automatically. The editor calls those methods when inserting through the UI. The factory therefore explicitly writes the defaults after AddNode().
Do not wrap new BlendTint() in a using statement inside the factory: disposing at method exit detaches the C# behavior from the still-used native resource. An object remains in the graph, but its overrides no longer work. This was reproduced on the installed version; let the graph and C# bridge manage a live block's lifetime.
Use the same stage, VisualShader.Type.Fragment, for all connections. ID 0 belongs to Output; request a free ID with GetValidNodeId(). CanConnectNodes() is useful before an interactive connection: it rejects missing blocks, invalid ports, duplicate connections and cycles. Still check the result of ConnectNodes().
Diagnose failures
| Symptom | Cause and action |
|---|---|
BlendTint is absent | Check the build, [Tool, GlobalClass], VisualShaderNodeCustom inheritance and matching filename/class. Check graph mode and stage. |
| Invalid block name | _GetName() must return a valid ASCII identifier without spaces; use BlendTint. Unlike the name, the category supports a path separated by /. |
| Output is black | Check unconnected port defaults, the output connection and premature Dispose(). Explicitly initialize defaults when constructing the graph programmatically. |
| Shader compilation error | Inspect generated source and its error message. Check vec3/float types, helper declarations, identifiers and semicolons. A successful C# build does not check these. |
| Several blocks cause redefinition | Put the shared function in _GetGlobalCode(), give it a unique name and do not repeat its declaration in _GetCode(). |
| Effect disappears after removing a block | Incoming and outgoing connections are removed with it. Reconnect the remaining graph to Output. |
| Saved resource fails after restart | Keep both the graph and BlendTint.cs; do not move a .rdata without its C# dependency. Rebuild the project. |
Verification limits
The installed headless 1.0.0 editor verified global C# registration, typed ports and defaults, graph construction and connections, generated CanvasItem Fragment source, save/reload with the script, two instances sharing one helper and connection cleanup. This does not establish GPU compilation, screen color, physical graph interaction, Spatial rendering, game export or other platforms. This example is not a rendering-quality illustration.