First 2D shader: color, alpha and reveal
Runnable canvas_item example: Sprite2D, separate materials, modulation, alpha and C#-controlled animation.
Run the complete example
Download the project. Requires Reckfell 1.0.0 with the C# SDK. Open project.reckfell, build C# and run Main.rscene. The example is verified in Compatibility/OpenGL and Forward+/Vulkan.
This processes a saved image in 2D: the input texture comes from the 3D example rather than a live 3D world. All four cards are Sprite2D instances sharing one Shader but using separate ShaderMaterial resources.
| Card | Parameter | Result |
|---|---|---|
| 01 | reveal=1 | original image |
| 02 | desaturate=1 | grayscale |
| 03 | tint | color multiplier |
| 04 | reveal=0.5 | partial reveal with a soft boundary |
Set root Animate=true to animate the fourth card. Without animation, call SetReveal(value) after _Ready. It clamps the value to 0…1 and updates the caption.
Connect a canvas_item shader
Use shader_type canvas_item for 2D. In the current engine, the file extension is .reckfellshader. Create a ShaderMaterial with the program and assign it to Sprite2D.Material. The spatial shader from the 3D lesson is unsuitable here.
TEXTURE is the Sprite2D texture; UV provides sampling coordinates. fragment() computes COLOR. Declaring a uniform alone does not create an effect: the formula must use it.
The example saves vertex-stage COLOR into item_color: item color with modulation before texture sampling. fragment() samples the texture once and multiplies it by this varying. Reassigning COLOR to only the sampled texture can discard the intended modulation; multiplying already textured COLOR by TEXTURE again can darken the image through double sampling.
Card.reckfellshader
shader_type canvas_item;
render_mode blend_mix;
uniform vec4 tint : source_color = vec4(1.0);
uniform float desaturate : hint_range(0.0, 1.0) = 0.0;
uniform float reveal : hint_range(0.0, 1.0) = 1.0;
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
uniform vec4 edge_color : source_color = vec4(0.22, 0.55, 1.0, 1.0);
varying vec4 item_color;
void vertex() {
item_color = COLOR; // Includes the CanvasItem modulation, before texture sampling.
}
void fragment() {
vec4 source = texture(TEXTURE, UV) * item_color;
float gray = dot(source.rgb, vec3(0.2126, 0.7152, 0.0722));
vec3 color = mix(source.rgb, vec3(gray), desaturate) * tint.rgb;
float wave = 0.04 * sin(UV.y * 18.0) + 0.018 * sin(UV.y * 47.0);
float field = 0.05 + 0.9 * clamp(UV.x + wave, 0.0, 1.0);
float visible = smoothstep(field - 0.012, field + 0.012, reveal);
float edge = 1.0 - smoothstep(0.0, 0.025, abs(reveal - field));
edge *= step(0.001, reveal) * (1.0 - step(0.999, reveal));
color += edge_color.rgb * edge * 0.65;
COLOR = vec4(color, source.a * tint.a * visible * opacity);
}Understand the output
desaturate blends source RGB with its weighted grayscale value. tint multiplies RGB; tint.a and opacity contribute to output alpha. Texture alpha is retained too: a translucent pixel blends with the existing background.
reveal is compared against a field built from UV.x and smooth UV.y waves. At 0 the card is fully hidden; at 1 it is fully visible. Intermediate values retain part of the image and highlight its boundary. This changes visual transparency rather than collision or game state.
source_color marks color uniforms. hint_range supplies an editor-control range rather than validating every value supplied from C#. SetReveal explicitly clamps its argument.
Control it from C#
The archive Gallery.rdata contains an ImageTexture with prepared mipmaps. The scene loads it as a resource and downscales cards with linear mipmap filtering. This is separate from 3D geometry antialiasing: MSAA does not replace texture filtering here.
For your own image, import a suitable Texture2D or prepare a resource beforehand. Do not assume Image.LoadFromFile can read the original asset:// PNG after export. Example preparation reads PNG and packages the result as Gallery.rdata; the running scene uses the resource. Layout assumes a 16:9 image.
ShaderCards2D.cs
using Reckfell;
public partial class ShaderCards2D : Entity2D
{
[Export] public bool Animate { get; set; }
public Sprite2D[] Cards { get; private set; } = null!;
public ShaderMaterial[] Materials { get; private set; } = null!;
public ImageTexture Picture { get; private set; } = null!;
private double elapsed;
private Label revealCaption = null!;
public override void _Ready()
{
TextureFilter = TextureFilterEnum.LinearWithMipmaps;
Picture = ResourceLoader.Load<ImageTexture>("asset://Gallery.rdata");
if (Picture == null) throw new System.InvalidOperationException("Gallery.rdata could not be loaded.");
var shader = ResourceLoader.Load<Shader>("asset://Card.reckfellshader");
if (shader == null) throw new System.InvalidOperationException("Card.reckfellshader could not be loaded.");
var background = new ColorRect { Size = new Vector2(2560, 1440), Color = new Color(0.025f, 0.03f, 0.04f) };
AddChild(background);
AddLabel("ShaderMaterial / canvas_item", new Vector2(240, 52), 50, Colors.White);
Cards = new Sprite2D[4]; Materials = new ShaderMaterial[4];
string[] captions = { "01 / reveal=1", "02 / desaturate=1", "03 / tint", "04 / reveal=0.5" };
for (int i = 0; i < 4; i++)
{
Vector2 at = new(240 + (i % 2) * 1120, 180 + (i / 2) * 645);
var material = new ShaderMaterial { Shader = shader };
var card = new Sprite2D { Texture = Picture, Centered = false,
Position = at, Scale = Vector2.One * (960f / Picture.GetWidth()), Material = material };
Cards[i] = card; Materials[i] = material; AddChild(card);
var caption = AddLabel(captions[i], at + new Vector2(0, 548), 28, new Color(0.7f, 0.74f, 0.8f));
if (i == 3) revealCaption = caption;
}
Materials[1].SetShaderParameter("desaturate", 1f);
Materials[2].SetShaderParameter("tint", new Color(0.45f, 0.7f, 1));
SetReveal(0.5f);
}
private Label AddLabel(string text, Vector2 at, int size, Color color)
{
var label = new Label { Position = at, Text = text,
LabelSettings = new LabelSettings { FontSize = size, FontColor = color } };
AddChild(label); return label;
}
public void SetReveal(float value)
{
value = Mathf.Clamp(value, 0, 1);
Materials[3].SetShaderParameter("reveal", value);
revealCaption.Text = "04 / reveal=" + value.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);
}
public override void _Process(double delta)
{
if (!Animate) return;
elapsed += delta;
SetReveal((float)(0.5 - 0.5 * System.Math.Cos(elapsed)));
}
}Edit the intended material
Materials[1].SetShaderParameter("desaturate", 0f) restores color to the second card. Others retain their settings because their ShaderMaterial resources are separate. The shared Shader does not need modification.
Assigning one ShaderMaterial to several objects shares its regular uniforms. Before editing, check resource identity, name case and UseParentMaterial. With UseParentMaterial=true the child draws with the parent material, although its Material getter still returns its own assignment. Native checks show parent tint overriding grayscale and false restoring the child shader.
CanvasItem.Modulate still affects the example through the vertex varying. Shader opacity=0 hides the image rather than removing the Entity or stopping _Process.
Animate under explicit control
_Process(double delta) accumulates C# time and sends reveal. Animate=false stops updating that parameter; SetProcess(false) stops this instance’s processing. SetProcess(true) resumes the animation, as verified in the runtime.
At reveal=1, the fourth card is fully restored:
If an effect must follow game pause or time scaling, define and verify that policy in its controller. This example does not establish equivalent behavior for built-in shader TIME.
Diagnosis
| Symptom | Check |
|---|---|
| Card disappeared | reveal, opacity, tint.a, Modulate.a and texture alpha |
| Image is unexpectedly dark | Double texture multiplication or repeated color modulation |
| Material edit has no effect | Assigned resource, uniform name case and UseParentMaterial |
| Every card changed | They share one ShaderMaterial |
| Downscaling produces jagged detail | Filter and mipmap availability; pixel art may need Nearest |
| Animation stopped | Animate, _Process activity and SetProcess |
| Exported texture is missing | Packaged resource/dependencies rather than assuming the original PNG remains accessible |
Figures are actual 2560×1440 canvas_item runtime frames from the current local build, not editor screenshots. Verification covers the stated operations in both renderers; HDR 2D, custom light(), every CanvasItem subtype, animated-texture import and TIME pause behavior need separate validation.
CanvasItem API · ShaderMaterial API · Continue to a 3D shader