reckfellENGINE

CanvasItem: 2D color, coordinates and lighting

Complete shaders, built-in tables, Sprite2D regions and troubleshooting.

On this page

A CanvasItem shader controls 2D drawing: sprites, UI and other CanvasItem objects. Assign a Shader to a ShaderMaterial, then assign the material to the object. The Entity hierarchy and a VisualShader graph are separate mechanisms.

First working material

  1. Create a Sprite2D with a visible texture. Keep Modulate and SelfModulate white.
  2. Assign a new ShaderMaterial to Material.
  3. Create a Shader in the material's Shader property and paste the complete code below.
  4. Edit the material's tint: white preserves the source, green keeps its green channel, and Alpha=0 hides the object.
Shader
shader_type canvas_item;
render_mode unshaded;
uniform vec4 tint : source_color = vec4(1.0);
void fragment() {
    COLOR *= tint;
}

In C#, call material.SetShaderParameter("tint", new Color(0, 1, 0, 1)). The name must match the uniform. Two different ShaderMaterials can share one Shader with independent values; a shared material shares parameter values. Passing default(Variant) removes the material value and restores the uniform initializer.

The example uses unshaded so lighting does not interfere with color inspection. Remove it to respond to Light2D. Use source_color for a color parameter; an ordinary numeric vector is not automatically a color.

COLOR already contains the texture

LocationCOLOR contents
vertex() inputPrimitive color × Modulate × SelfModulate.
fragment() inputInterpolated vertex color × the main TEXTURE sample.
fragment() outputColor and alpha passed to subsequent lighting and blending.

Consequently, COLOR *= texture(TEXTURE, UV) normally multiplies the texture twice, a common cause of darkening. To use only the texture and bypass vertex modulation, assign COLOR directly:

Shader
shader_type canvas_item;
render_mode unshaded;
void fragment() {
    COLOR = texture(TEXTURE, UV);
}

To use vertex color without the main texture, pass it through a varying:

Shader
shader_type canvas_item;
render_mode unshaded;
varying vec4 vertex_color;
void vertex() {
    vertex_color = COLOR;
}
void fragment() {
    COLOR = vertex_color;
}

The test uses texture RGB (0.8, 0.4, 0.2), Modulate (0.5, 1, 1) and SelfModulate (0.5, 1, 1), producing (0.2, 0.4, 0.2). Sampling and multiplying again produces (0.16, 0.16, 0.04). This checks ordinary 2D arithmetic, not every HDR/color pipeline.

Keep coordinate spaces distinct

In vertex(), VERTEX contains local geometry coordinates in pixels. The default path applies MODEL_MATRIX, then CANVAS_MATRIX and SCREEN_MATRIX. The latter stages account for the canvas/camera and projection. Fragment VERTEX already contains the position after the canvas transform; it is not the original local VERTEX.

UV addresses the texture; SCREEN_UV addresses the current viewport. FRAGCOORD.xy contains viewport pixel-center coordinates, not values from 0 to 1. The tested Vulkan path uses a top-left origin: FRAGCOORD.xy * SCREEN_PIXEL_SIZE matches SCREEN_UV. OS window size and Sprite2D size are not substitutes for viewport resolution.

TEXTURE_PIXEL_SIZE is the reciprocal source texture size: an 8×4 texture gives (0.125, 0.25) even when the sprite is enlarged. For a separate custom sampler, use that sampler's own dimensions rather than the main TEXTURE size.

For a region-enabled Sprite2D, UV already addresses a subrectangle of the full texture. In the tested rectangle path, REGION_RECT contains normalized offset and size: region (2,1,4,2) in an 8×4 texture gives (0.25,0.25,0.5,0.5). To obtain coordinates within that region, subtract REGION_RECT.xy and divide by nonzero REGION_RECT.zw. Do not apply the region offset again to the existing UV. Inspect actual coordinates when using flips, atlases, animation frames or other primitive paths.

skip_vertex_transform disables only the automatic local-to-world step. This complete example performs that step manually; canvas transformation and projection remain automatic:

Shader
shader_type canvas_item;
render_mode unshaded, skip_vertex_transform;
void vertex() {
    VERTEX = (MODEL_MATRIX * vec4(VERTEX, 0.0, 1.0)).xy;
}
void fragment() {
    COLOR = vec4(UV, 0.0, 1.0);
}

With world_vertex_coords, MODEL_MATRIX is instead applied before the user vertex() function. Do not apply it a second time.

Render modes

ModeBehavior
blend_mixOrdinary alpha blending; the default.
blend_addAdditive blending.
blend_subSubtractive blending.
blend_mulMultiplicative blending.
blend_premul_alphaRGB must already be multiplied by alpha; do not multiply it twice.
blend_disabledBlending is disabled; alpha=0 alone does not hide RGB.
unshadedBypasses lighting and canvas modulation; object modulation still applies.
light_onlyShows the lighting contribution instead of the ordinary base color.
skip_vertex_transformPerform the local-to-world transform manually in vertex().
world_vertex_coordsThe user vertex() function receives world coordinates.

Choose one blend mode. For premultiplied alpha, calculate final alpha first and multiply RGB by it exactly once. discard removes the selected fragment; merely setting alpha to zero depends on the blend mode.

Built-ins by stage

in is read-only; out is a result without a guaranteed meaningful initial value; inout has an initial value and can be changed. A sampler is used for texture reads. Names are stage-specific: a varying transfers selected data between stages, but does not make every built-in available everywhere.

TIME is global renderer time in seconds and follows Engine.TimeScale. Pausing the scene tree alone does not stop it. rendering/limits/time/time_rollover_secs sets rollover, defaulting to 3600 seconds. Supply your own time uniform for controlled gameplay animation. Global PI, TAU and E are float mathematical constants.

vertex()

Type and nameMeaning
inout vec2 VERTEXLocal position; transform modes are described above.
inout vec2 UVTexture coordinates passed to fragment().
inout vec4 COLORVertex/object modulation before the main texture.
inout float POINT_SIZEPoint primitive size.
in mat4 MODEL_MATRIX / CANVAS_MATRIX / SCREEN_MATRIXLocal → world → canvas → clip transforms.
in int INSTANCE_ID / VERTEX_IDDraw-instance and vertex indices; not gameplay Entity IDs.
in vec4 INSTANCE_CUSTOMCustom instance data defined by its producer; particles commonly use xyz for rotation, phase and animation frame.
in vec4 CUSTOM0 / CUSTOM1Additional primitive attributes when supplied.
in vec2 TEXTURE_PIXEL_SIZEReciprocal main texture dimensions.
in bool AT_LIGHT_PASSAlways false; not a repeated-pass switch.

fragment()

Type and nameMeaning
in vec4 FRAGCOORDViewport pixel center; xy in pixels.
in vec2 SCREEN_UV / SCREEN_PIXEL_SIZEViewport UV / reciprocal viewport resolution.
in vec2 UV / POINT_COORDTexture UV / coordinates within a point primitive.
in vec4 REGION_RECTSource rectangle of the current draw path; normalized for the tested Sprite2D.
sampler2D TEXTURE / NORMAL_TEXTURE / SPECULAR_SHININESS_TEXTUREMain texture, normals and specular/shininess texture.
in vec2 TEXTURE_PIXEL_SIZEReciprocal TEXTURE dimensions.
in vec4 SPECULAR_SHININESSSampled specular color/shininess.
inout vec3 NORMALNormal used by 2D lighting.
out vec3 NORMAL_MAP / out float NORMAL_MAP_DEPTHEncoded normal-map value and its depth; NORMAL_MAP overrides NORMAL.
inout vec2 VERTEX / SHADOW_VERTEXFragment position / separate shadow position; writing these does not move rasterized geometry.
inout vec3 LIGHT_VERTEXLighting position; z is height.
inout vec4 COLORColor including the main texture; stage output.
in bool AT_LIGHT_PASSAlways false.

light()

Type and nameMeaning
in vec4 FRAGCOORDThe same pixel center in pixels, not normalized UV.
in vec3 NORMAL / LIGHT_VERTEXNormal / lighting position after fragment().
in vec4 COLOR / SPECULAR_SHININESSColor after fragment() / specular properties.
in vec2 UV / SCREEN_UV / POINT_COORD / TEXTURE_PIXEL_SIZETexture UV, viewport UV, point coordinate and reciprocal texture size.
sampler2D TEXTUREMain object texture.
in vec4 LIGHT_COLOR / in float LIGHT_ENERGYCurrent light color and energy; PointLight2D includes its light texture.
in vec3 LIGHT_POSITION / LIGHT_DIRECTIONLight position and direction in lighting space; directional light position is zero.
in bool LIGHT_IS_DIRECTIONALWhether this is a DirectionalLight2D.
inout vec4 LIGHTCurrent light result; a custom function replaces the default.
out vec4 SHADOW_MODULATEShadow color multiplier at this point.

Lighting and normal maps

Without light(), standard lighting applies. Declaring a custom light() replaces it even when the body is empty: the tested empty function contributes zero. unshaded bypasses custom lighting. Check the light texture, energy, visibility and matching item/light masks; CanvasModulate affects base lighting.

A complete normal-aware light shader:

Shader
shader_type canvas_item;
void light() {
    float ndotl = max(0.0, dot(NORMAL, LIGHT_DIRECTION));
    LIGHT = vec4(LIGHT_COLOR.rgb * COLOR.rgb * LIGHT_ENERGY * ndotl, LIGHT_COLOR.a);
}

This is a simple diffuse calculation without a separate specular model. CanvasTexture can supply a normal map. For an explicit RGB-encoded sample, write NORMAL_MAP, for example assign texture(NORMAL_TEXTURE, UV).rgb to it inside fragment(); NORMAL is already the lighting normal vector. Do not replace it with unprocessed RGB. Verify map orientation under an actual light.

SDF uses a separate coordinate space

A LightOccluder2D with an OccluderPolygon2D and SDF Collision enabled participates in the viewport distance field. A new LightOccluder2D enables this flag in its constructor. Visibility and viewport SDF coverage/scale affect the result.

FunctionPurpose
vec2 screen_uv_to_sdf(vec2 uv)Viewport UV → SDF coordinates.
vec2 sdf_to_screen_uv(vec2 sdf_pos)Convert back to viewport UV.
float texture_sdf(vec2 sdf_pos)Read a distance in SDF space, not arbitrary window pixels.
vec2 texture_sdf_normal(vec2 sdf_pos)Field gradient direction, not a 3D model normal.

These functions are available from fragment()/light() and functions called from them, not vertex(). Do not pass ordinary texture UV to texture_sdf without converting spaces. For screen reads, declare your own sampler with hint_screen_texture; the current registry has no SCREEN_TEXTURE built-in. See screen-reading shaders.

Troubleshooting

SymptomCheck
Texture is too darkRepeated TEXTURE multiplication, Modulate/SelfModulate and lighting.
tint changes have no effectExact name, intended ShaderMaterial and later COLOR assignments.
RGB remains visible with alpha=0blend_disabled, additive blending or incorrect premultiplied RGB.
Effect stretches when scaledTexture, object and viewport dimensions may be mixed up.
Region is selected twiceUV already includes the source region; inspect REGION_RECT.
Object moves/disappears after vertex()MODEL_MATRIX applied twice, or a missing step with skip_vertex_transform.
Light disappears after adding light()The custom function must write LIGHT; check mode, light and masks.
SDF does not match objectsOccluder polygon, SDF Collision, viewport coverage and screen_uv_to_sdf conversion.

Windows Forward+/Vulkan checks execute the five complete examples above and inspect actual color, alpha, coordinate, region, transform and custom-light pixels. The complete built-in table, SDF, TIME/pause, remaining blend modes and world_vertex_coords are source-reviewed; this does not claim separate acceptance for every platform, renderer or export.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.