Spatial: surfaces, lighting, alpha and depth
Complete shaders and a 95-name registry: normals, transparency, stencil and material selection.
A spatial shader defines a 3D mesh surface. Assign a Shader to a ShaderMaterial, then to a MeshInstance3D surface or MaterialOverride. vertex() changes geometry/attributes, fragment() defines material properties, and optional light() replaces direct-light calculation.
First material and assignment
This complete opaque material uses a texture and tint. source_color identifies color data; hint_default_white makes tint work without a separate texture:
shader_type spatial;
uniform sampler2D albedo_texture : source_color, hint_default_white;
uniform vec4 tint : source_color = vec4(1.0);
void fragment() {
ALBEDO = texture(albedo_texture, UV).rgb * tint.rgb;
METALLIC = 0.0;
ROUGHNESS = 0.7;
}ALBEDO is base color, not guaranteed final RGB: lighting, a dielectric white highlight, reflections, fog and tonemapping change the pixel. Red ALBEDO can therefore produce nonzero G/B from specular lighting. Use unshaded diagnostically to inspect base color alone.
A complete C# helper creates a QuadMesh with the material:
using Reckfell;
public static class SpatialSetup
{
public static MeshInstance3D Create(Shader shader)
{
return new MeshInstance3D
{
Mesh = new QuadMesh { Size = new Vector2(1, 1) },
MaterialOverride = new ShaderMaterial { Shader = shader },
};
}
}Load the Shader using ResourceLoader.Load<Shader>("asset://PbrSurface.reckfellshader"), call SpatialSetup.Create(shader) and add the result to the tree. A camera at (0,0,5) looking along −Z sees this quad's front. A shaded material needs lights or suitable environment lighting.
Material selection priority is object MaterialOverride → per-surface override → the Mesh resource surface material. GetSurfaceOverrideMaterial returns the stored override, which may be inactive under MaterialOverride. GetActiveMaterial helps inspect the selected base material. GetSurfaceOverrideMaterialCount counts surface slots, not non-null overrides.
MaterialOverlay adds a draw pass governed by its queue, blending and depth, not an unconditional top layer. In the check, an opaque equal-depth overlay was not on top, while a transparent overlay blended with the base. Do not rely on the overlay name alone to fix transparency ordering.
Vertex and normal spaces
By default VERTEX, NORMAL, TANGENT and BINORMAL enter vertex() in mesh-local space. MODEL_MATRIX maps position to world, VIEW_MATRIX to view, and PROJECTION_MATRIX to clip. In fragment(), VERTEX and the ordinary NORMAL are already view-space values. A shadow pass matrix can refer to a light camera; MAIN_CAM_INV_VIEW_MATRIX refers to the viewport's main camera.
Nonuniform scale requires the inverse-transpose normal transform. Multiplying a normal by ordinary MODELVIEW_MATRIX as a direction gives the wrong angle. A complete manual-path diagnostic:
shader_type spatial;
render_mode unshaded, skip_vertex_transform;
void vertex() {
VERTEX = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
NORMAL = normalize(MODELVIEW_NORMAL_MATRIX * NORMAL);
TANGENT = normalize(mat3(MODELVIEW_MATRIX) * TANGENT);
BINORMAL = normalize(mat3(MODELVIEW_MATRIX) * BINORMAL);
}
void fragment() {
ALBEDO = normalize(NORMAL) * 0.5 + 0.5;
}skip_vertex_transform disables automatic model→view position/basis transformation; projection remains. NORMAL uses MODELVIEW_NORMAL_MATRIX, while tangent vectors use the linear MODELVIEW_MATRIX part. Complex deformation/nonorthogonal TBN can require rebuilding a consistent tangent basis. This is not a universal fix for every normal-mapped/skinned mesh.
world_vertex_coords supplies world-space attributes to user vertex(); do not apply MODEL_MATRIX twice. Writing POSITION requires a complete clip-space vec4 on every branch: automatic position projection is replaced, while fragment VERTEX still derives from VERTEX. Those outputs are not the same space. Normalized texture UV, viewport SCREEN_UV and pixel FRAGCOORD are also distinct.
Transparency and cutout
blend_mix is the spatial blend default, but ALPHA usage also determines the pipeline. PbrSurface does not use tint.a: setting the color's alpha to zero does not make it transparent. Reading/writing ALPHA can select the transparent queue even at 1, changing sorting and participation in other effects.
Write ALPHA for smooth transparency. Use ALPHA_SCISSOR_THRESHOLD for a cutout boundary. This complete shader keeps the quad's right half opaque:
shader_type spatial;
render_mode unshaded;
void fragment() {
ALBEDO = vec3(1.0, 0.0, 0.0);
ALPHA = UV.x;
ALPHA_SCISSOR_THRESHOLD = 0.5;
}Alpha hash uses ALPHA_HASH_SCALE for stochastic coverage. Alpha-to-coverage needs appropriate MSAA and antialiasing configuration; it does not replace texture detail. Transparent surfaces have sorting limitations, not arbitrary per-pixel layer ordering. depth_prepass_alpha helps some cases rather than solving all intersecting transparency.
NORMAL_MAP expects an encoded normal map; NORMAL is a view-space normal vector. Do not assign unprocessed texture RGB to NORMAL. Mark color textures/parameters as source_color and classify data maps by purpose; do not apply sRGB conversion to vectors, masks and physical parameters.
Custom light()
Without light(), built-in lighting applies. A custom function operates for a light in view space. LIGHT points toward it, ATTENUATION accounts for falloff/shadows and LIGHT_COLOR includes color/energy. Accumulate contributions with +=.
A complete simple diffuse calculation:
shader_type spatial;
uniform vec3 base_color : source_color = vec3(1.0);
void fragment() {
ALBEDO = base_color;
ROUGHNESS = 1.0;
}
void light() {
float ndotl = max(dot(NORMAL, LIGHT), 0.0);
DIFFUSE_LIGHT += ndotl * ATTENUATION * LIGHT_COLOR / PI;
}Current Forward+ multiplies the final diffuse by ALBEDO afterward. Multiplying by ALBEDO inside this light() example would darken it twice; the test confirms one application. source_color also converts entered color to linear space: an entered color component of 0.5 is not automatically linear intensity 0.5.
This example handles ordinary direct diffuse light, not a complete PBR model. Use specialized area-light data/multipliers for that path, and SPECULAR_LIGHT for a specular contribution. unshaded bypasses lighting; vertex_lighting can bypass the per-pixel custom light(). Verify the required path before extending a BRDF.
Depth depends on the pass
The checked Forward+/Vulkan path uses reverse-Z: DEPTH=0 is far and DEPTH=1 is near. CLIP_SPACE_FAR describes the renderer's far clip coordinate, not a world distance. Reconstruct screen-depth positions with the appropriate INV_PROJECTION_MATRIX and division by w; see screen-reading shaders.
If any branch writes DEPTH, provide a meaningful value on every branch. Check the depth prepass and shadows separately: they need not repeat an arbitrary fragment algorithm.
A specific installed Forward+ defect is reproduced: a simple opaque custom-DEPTH material can select a shared prepass that does not account for that write. Geometry occludes the rear object in the prepass, then its color fails the new-depth test, leaving a black region. Source selection of the shared prepass material does not check DEPTH writes. This is a configuration limitation, not evidence that reverse-Z is ordinary Z. A separate transparent path without depth writing confirms the 0/1 ordering against an opaque rear surface. Do not treat custom DEPTH as a universal overlay trick without checking every required pass.
Stencil mask writing and reading
Current syntax uses stencil_mode, a reference from 0 through 255, and write/read/write_depth_fail. The correct name is write_depth_fail, not write_if_depth_fail. Reading must use the transparent queue. These examples provide a blue mask writer and a red reader inside its mask:
shader_type spatial;
render_mode unshaded;
stencil_mode write, 1;
void fragment() {
ALBEDO = vec3(0.0, 0.0, 1.0);
}shader_type spatial;
render_mode unshaded, depth_test_disabled;
stencil_mode read, compare_equal, 1;
void fragment() {
ALBEDO = vec3(1.0, 0.0, 0.0);
ALPHA = 1.0;
}Assign them to two meshes; the reader can be larger. The check sees red only inside the mask, which updates when the writer moves. The reader deliberately writes ALPHA=1 and disables depth testing. This is a minimal mask check, not a complete portal or outline effect.
Comparisons are compare_always, compare_equal, compare_not_equal, compare_less, compare_less_or_equal, compare_greater and compare_greater_or_equal. Reference is compared with stencil contents. Stencil is not an ordinary color texture: spatial has no built-in sampler for it. Check each renderer/pass separately.
Complete built-in registry
G is global; V is vertex(); F is fragment(); L is light(). A star marks a writable stage; unstarred stages are read-only. Initial values and output semantics depend on the field; conditional output writes need values on every relevant branch.
| Name | Type | Stages | Meaning |
|---|---|---|---|
TIME | float | G | Renderer time following Engine.TimeScale; tree pause alone does not stop it. |
EXPOSURE | float | G | Reciprocal emission exposure normalization in current Forward+, not base color. |
IN_SHADOW_PASS | bool | G | Whether the current pass renders shadow maps. |
PI | float | G | Float mathematical constant. |
TAU | float | G | Float mathematical constant. |
E | float | G | Float mathematical constant. |
OUTPUT_IS_SRGB | bool | G | sRGB-output flag: false in current Forward+; Compatibility differs. |
CLIP_SPACE_FAR | float | G | Far clip coordinate: Forward+/Mobile 0, Compatibility −1; not world depth. |
IS_MULTIVIEW | bool | G | Whether output is multiview. |
VERTEX | vec3 | V* / F | Position: default vertex() input is local; fragment() input is view-space. |
NORMAL | vec3 | V* / F* / L | Normal vector; space depends on stage as described above. |
TANGENT | vec3 | V* / F* | Surface tangent basis; the built-in is named BINORMAL. |
BINORMAL | vec3 | V* / F* | Surface tangent basis; the built-in is named BINORMAL. |
POSITION | vec4 | V* | Complete clip-space position replacing automatic projection. |
UV | vec2 | V* / F / L | First/second mesh UV set, interpolated to fragments. |
UV2 | vec2 | V* / F / L | First/second mesh UV set, interpolated to fragments. |
COLOR | vec4 | V* / F | Vertex/instance color; does not automatically replace custom-shader ALBEDO. |
POINT_SIZE | float | V* | Point-primitive size, not arbitrary mesh scale. |
Z_CLIP_SCALE | float | V* | Clip-Z scale: current Forward+ mixes z from w toward normal z; skipped in shadow passes. |
INSTANCE_ID | int | V | Draw-instance index, not a gameplay Object/Entity instance ID. |
INSTANCE_CUSTOM | vec4 | V | Instance data; particles supply simulation CUSTOM. |
VERTEX_ID | int | V | Vertex index in the current buffer. |
ROUGHNESS | float | V* / F* / L | Roughness for the applicable lighting/reflection path. |
BONE_INDICES | uvec4 | V | Bone indices/weights from mesh attributes when supplied. |
BONE_WEIGHTS | vec4 | V | Bone indices/weights from mesh attributes when supplied. |
CUSTOM0 | vec4 | V | Additional mesh attributes; Mesh determines their data/packing. |
CUSTOM1 | vec4 | V | Additional mesh attributes; Mesh determines their data/packing. |
CUSTOM2 | vec4 | V | Additional mesh attributes; Mesh determines their data/packing. |
CUSTOM3 | vec4 | V | Additional mesh attributes; Mesh determines their data/packing. |
MODEL_MATRIX | mat4 | V / F / L | Local-to-world position transform. |
MODEL_NORMAL_MATRIX | mat3 | V / F | Local-to-world normal matrix. |
VIEW_MATRIX | mat4 | V / F / L | World→view for the current pass. |
INV_VIEW_MATRIX | mat4 | V / F / L | Current-pass view→world; a shadow pass need not use the main camera. |
PROJECTION_MATRIX | mat4 | V* / F / L | View→clip; writable in vertex(). |
INV_PROJECTION_MATRIX | mat4 | V / F / L | Clip→view; divide by w after transforming a position. |
MODELVIEW_MATRIX | mat4 | V* | Local→view, available for manual transformation. |
MODELVIEW_NORMAL_MATRIX | mat3 | V* | Local→view normal matrix, needed for nonuniform scale. |
VIEWPORT_SIZE | vec2 | V / F / L | Current viewport dimensions in pixels. |
MAIN_CAM_INV_VIEW_MATRIX | mat4 | V | Main viewport camera view→world matrix, distinct from shadow camera. |
NODE_POSITION_WORLD | vec3 | V / F | Object position in world/view according to suffix, not each vertex position. |
CAMERA_POSITION_WORLD | vec3 | V / F | World camera position/direction in the current context; account for stereo/passes. |
CAMERA_DIRECTION_WORLD | vec3 | V / F | World camera position/direction in the current context; account for stereo/passes. |
CAMERA_VISIBLE_LAYERS | uint | V / F | Current-pass camera visible-layer mask. |
NODE_POSITION_VIEW | vec3 | V / F | Object position in world/view according to suffix, not each vertex position. |
VIEW_INDEX | int | V / F | View index and mono/left/right-eye constants. |
VIEW_MONO_LEFT | int | V / F | View index and mono/left/right-eye constants. |
VIEW_RIGHT | int | V / F | View index and mono/left/right-eye constants. |
EYE_OFFSET | vec3 | V / F | Eye offset for multiview rendering. |
LIGHT_VERTEX | vec3 | F* | View-space lighting position; writing it does not move rasterized geometry. |
FRAGCOORD | vec4 | F / L | Fragment-center coordinates; xy in target-buffer pixels. |
FRONT_FACING | bool | F | Whether the current primitive is front-facing. |
VIEW | vec3 | F / L | Normalized direction toward the viewer in view space. |
NORMAL_MAP | vec3 | F* | Encoded tangent-space normal map, not a ready-made view NORMAL. |
NORMAL_MAP_DEPTH | float | F* | Normal-map application weight/depth. |
BENT_NORMAL_MAP | vec3 | F* | Encoded bent-normal direction for environment-occlusion paths. |
STREAMING_UV | vec2 | F* | UV for streaming feedback/mip selection, not an automatic texture() coordinate replacement. |
ALBEDO | vec3 | F* / L | Linear base material color, separate from final lighting/specular. |
PREMUL_ALPHA_FACTOR | float | F* | Premultiplied-alpha factor for shaded surfaces, not an ALPHA replacement. |
ALPHA | float | F* / L* | Transparency/coverage; usage affects queues and passes. |
METALLIC | float | F* / L | Metallic model weight, normally 0–1. |
SPECULAR | float | F* | Dielectric reflectance setting; metallic reflection also depends on ALBEDO/METALLIC. |
RIM | float | F* | Rim-light weight and base-color rim tinting. |
RIM_TINT | float | F* | Rim-light weight and base-color rim tinting. |
CLEARCOAT | float | F* | Top reflection-layer weight and roughness. |
CLEARCOAT_ROUGHNESS | float | F* | Top reflection-layer weight and roughness. |
ANISOTROPY | float | F* | Anisotropy strength and tangent-space flow direction. |
ANISOTROPY_FLOW | vec2 | F* | Anisotropy strength and tangent-space flow direction. |
SSS_STRENGTH | float | F* | Subsurface-scattering strength; support is renderer-dependent. |
SSS_TRANSMITTANCE_COLOR | vec4 | F* | Color, depth and boost for surface light transmittance. |
SSS_TRANSMITTANCE_DEPTH | float | F* | Color, depth and boost for surface light transmittance. |
SSS_TRANSMITTANCE_BOOST | float | F* | Color, depth and boost for surface light transmittance. |
BACKLIGHT | vec3 | F* / L | Backlighting/translucent scattering contribution. |
AO | float | F* | Material ambient occlusion, not an SSAO enable switch. |
AO_LIGHT_AFFECT | float | F* | Amount of AO influence on direct lighting. |
EMISSION | vec3 | F* | Surface HDR self-emission; does not itself create a Light3D. |
DEPTH | float | F* | Writable depth; check range, branches and prepass interaction. |
SCREEN_UV | vec2 | F / L | Normalized viewport coordinates, not mesh UV. |
POINT_COORD | vec2 | F | Coordinates within a point primitive. |
FOG | vec4 | F* | Custom fog RGB blending using A as weight. |
RADIANCE | vec4 | F* | RGBA environment-contribution override weighted by A; spatial values are not the sky RADIANCE sampler. |
IRRADIANCE | vec4 | F* | RGBA environment-contribution override weighted by A; spatial values are not the sky RADIANCE sampler. |
ALPHA_SCISSOR_THRESHOLD | float | F* | Hard ALPHA cutout threshold. |
ALPHA_HASH_SCALE | float | F* | Stochastic alpha-hash coverage scale. |
ALPHA_ANTIALIASING_EDGE | float | F* | Alpha antialiasing edge and texture coordinates for the applicable coverage path. |
ALPHA_TEXTURE_COORDINATE | vec2 | F* | Alpha antialiasing edge and texture coordinates for the applicable coverage path. |
SPECULAR_AMOUNT | float | L | Current light specular contribution multiplier. |
LIGHT | vec3 | L | Direction toward the light in view space. |
LIGHT_COLOR | vec3 | L | Light color/energy for the light function; account for its normalization. |
LIGHT_IS_DIRECTIONAL | bool | L | Whether this is a directional or area light. |
LIGHT_IS_AREA | bool | L | Whether this is a directional or area light. |
LIGHT_AREA_DIFFUSE_MULTIPLIER | vec3 | L | Area-light diffuse/specular multipliers for custom calculation. |
LIGHT_AREA_SPECULAR_MULTIPLIER | vec3 | L | Area-light diffuse/specular multipliers for custom calculation. |
ATTENUATION | float | L | Current-light attenuation, including shadows. |
DIFFUSE_LIGHT | vec3 | L* | Accumulated diffuse/specular light; use += for addition. |
SPECULAR_LIGHT | vec3 | L* | Accumulated diffuse/specular light; use += for addition. |
Render modes
Choose a coherent set rather than enabling every mode. Depth-write policy does not cancel depth testing or queue constraints.
| Mode | Purpose |
|---|---|
blend_mix / blend_add / blend_sub / blend_mul / blend_premul_alpha | Mix, additive, subtractive, multiply and premultiplied blending. Account for ALPHA and queues; do not multiply premultiplied input by alpha twice. |
depth_draw_opaque / depth_draw_always / depth_draw_never | Depth-write policy for opaque/all/no surfaces, subject to the selected pass. |
depth_prepass_alpha | Depth prepass for applicable transparent geometry, not a complete sorting solution. |
depth_test_default / depth_test_disabled / depth_test_inverted | Default, disabled or inverted depth test; testing and writing are distinct. |
cull_back / cull_front / cull_disabled | Cull back/front faces, or draw both sides. |
unshaded | Bypass the lighting model; useful for ALBEDO diagnosis. |
diffuse_burley / diffuse_lambert / diffuse_lambert_wrap / diffuse_toon | Diffuse model for built-in lighting. |
specular_schlick_ggx / specular_toon / specular_disabled | Direct specular model/disable; disabling it does not remove all environment reflections. |
skip_vertex_transform / world_vertex_coords | Manual model→view or world-space user vertex(), as described above. |
ensure_correct_normals | Accepted name, but its macro does not implement a separate correction in current Forward+ GLSL. Use the correct normal matrix. |
sss_mode_skin | Skin subsurface-scattering specialization where supported. |
shadows_disabled | Disables receiving shadows; shadow casting is configured separately. |
ambient_light_disabled | Disables environment ambient/radiance contribution in the supported path. |
shadow_to_opacity | Maps lighting/shadows to alpha for specialized shadow-catching effects. |
vertex_lighting | Vertex lighting rather than the ordinary per-pixel path; account for bypassed light(). |
particle_trails | Special GPU-particle trail geometry path, not automatic trail creation. |
alpha_to_coverage / alpha_to_coverage_and_one | Alpha coverage for the applicable MSAA path. |
fog_disabled | Disables surface fog reception. |
specular_occlusion_disabled | Disables the environment specular-occlusion branch in current RD. |
wireframe / debug_shadow_splits | Diagnostic wireframe / shadow-split colors. Wireframe data preparation is backend-dependent. |
TIME rolls over at rendering/limits/time/time_rollover_secs, default 3600 seconds. Supply a time uniform for pausable gameplay animation.
With ambient disabled, default specular occlusion can suppress indirect specular even with valid RADIANCE or ReflectionProbe data. Current Forward+ and Mobile use ambient luminance as a reflection-visibility approximation in the branch without a bent-normal map. Use specular_occlusion_disabled for an isolated check, or DisableSpecularOcclusion on StandardMaterial3D. This does not disable direct shadows or diffuse AO. Setup and checked conditions.
Troubleshooting
| Symptom | Check |
|---|---|
| Edited material is not visible | GetActiveMaterial, object MaterialOverride, surface override and Mesh material. |
| Red material has a white highlight | ALBEDO is not final RGB; inspect SPECULAR, METALLIC and lighting. |
| Color parameter 0.5 gives another linear intensity | source_color sRGB→linear conversion and output tonemapping. |
| Normals break when stretched | MODELVIEW_NORMAL_MATRIX for NORMAL and consistent TBN, not the position matrix. |
| Polygon disappears when rotated | Front-face orientation and cull mode. |
| Transparency does not work | ALPHA usage, material, queue and visibility behind the surface. |
| Black regions after DEPTH writes | Values on all branches and the reproduced shared-prepass limitation. |
| Custom light is too dark | Repeated ALBEDO, linear colors, normalization, ATTENUATION and vector spaces. |
| Stencil does not mask | stencil_mode, reference, correct write_depth_fail and transparent reader queue. |
| Overlay is not on top | Its blending, depth and pass ordering; this is not a UI layer. |
Installed Windows Forward+/Vulkan checks execute six complete shaders and the C# helper: base surface, normals, culling, ALPHA/scissor, direct light, DEPTH, stencil and material priority. The 95-name registry and remaining modes are source-reviewed. This is not complete acceptance of every BRDF, SSS, area-light, MSAA, deformation/skinning, streaming, XR, shadow or platform combination. Functional readbacks are not published beauty illustrations; the custom-DEPTH defect is retained in the checks.