VPFX shadow_depth Guide
This document explains the current VPFX shadow_depth target.
It is written for VPFX shaderpack authors who want to inspect, debug, or eventually use VPFX shadow maps.
Important:
shadow_depth is not scene_depth.shadow_depth is not main camera depth.shadow_depth is a light-space shadow map.shadow_depth currently uses reversed-Z depth.If you are writing your first VPFX pack, do not start here. Start with scene color, then scene depth, then read this guide.
1. What shadow_depth is
shadow_depth is the VPFX shadow map depth target.
It is rendered before VPFX post-processing passes so that a shaderpack can sample the shadow map as an input texture.
Current public target aliases:
vulkanpostfx:shadow_depthminecraft:shadow_depthRecommended target name:
vulkanpostfx:shadow_depthUse minecraft:shadow_depth only as a compatibility alias.
2. What shadow_depth is not
shadow_depth is not:
the main camera depthminecraft:scene_deptha screen-space depth buffera color texturean Iris-compatible shadow map APIa complete custom receiver API by itselfDo not treat it like this:
float sceneDepth = texture(ShadowSampler, texCoord).r;That reads the shadow map at screen UVs, not the scene depth at the current pixel.
3. Current shadow_depth contents
In the current VPFX shadow pass, shadow_depth is written by the shadow caster pipeline.
The current code path guarantees:
terrain shadow castersentity shadow castersplayer model shadow casters through entity renderingheld item shadow casters through entity renderingmoving/block-model submits that arrive through eligible entity renderingEntity shadow submission filters out:
vanilla circular entity blob shadowsname tagstextflameleashoutlinesgizmosparticlescustom geometry that is not explicitly allowedThis means shadow_depth is intended to contain real caster geometry, not vanilla entity shadow decals.
Dedicated block entity caster coverage may be version-dependent. If you are testing block entity shadows, use the shadow debug view and verify whether the object actually appears in shadow_depth.
4. How shadow_depth is created
VPFX allocates a square shadow target.
Current default request:
8192 x 8192If allocation fails, the runtime can fall back to a lower size. The minimum fallback size in the current code is:
1024 x 1024The actual current size is exposed to shaders as:
vpfx_ShadowMapSizeIf no shadow map is available, this value may be 0.0.
5. Shadow map depth convention
VPFX currently uses reversed-Z for the shadow map.
Important rules:
clear depth = 0.0nearer shadow casters write larger depth valuesfarther values are smallerdepth test uses greater-or-equal style behaviorThis is different from the common beginner assumption that “smaller depth means closer.”
For VPFX shadow depth:
larger value = closer to the shadow camera0.0 = cleared / empty / far side6. How to request shadow_depth in pack.json
If your pack needs shadow_depth, declare it in pack.json.
Example:
{ "format_version": 1, "pack_id": "example_shadow_debug", "name": "Example Shadow Debug", "version": "1.0.0", "author": "Your Name", "description": "A VPFX pack that visualizes shadow_depth.", "entry_post_effect": "post_effect/main.json", "capabilities": { "scene_color": true, "scene_depth": false, "shadow_depth": true, "custom_targets": true, "compute": false }, "targets": { "shadow_depth": "vulkanpostfx:shadow_depth" }}Rules:
capabilities.shadow_depth must be true.targets.shadow_depth must exist.targets.shadow_depth should map to vulkanpostfx:shadow_depth.compute must currently remain false.If shadow_depth is enabled but targets.shadow_depth is missing, VPFX rejects the manifest.
7. How to read shadow_depth in post_effect/main.json
Use vulkanpostfx:shadow_depth as a pass input.
Recommended example:
{ "targets": {}, "passes": [ { "id": "shadow_depth_debug", "debug_label": "Shadow Depth Debug", "vertex_shader": "example_shadow_debug:post/fullscreen", "fragment_shader": "example_shadow_debug:post/shadow_depth_debug", "inputs": [ { "sampler_name": "Shadow", "target": "vulkanpostfx:shadow_depth", "use_depth_buffer": true } ], "output": "minecraft:main" } ]}In the current native graph path, vulkanpostfx:shadow_depth is treated as a depth input automatically, but use_depth_buffer: true is recommended for clarity.
GLSL sampler name:
uniform sampler2D ShadowSampler;because:
"sampler_name": "Shadow"maps to:
ShadowSampler8. Minimal shadow_depth debug shader
This shader displays the shadow map directly.
#version 150
uniform sampler2D ShadowSampler;
in vec2 texCoord;out vec4 fragColor;
void main() { float d = texture(ShadowSampler, texCoord).r; fragColor = vec4(vec3(d), 1.0);}This is useful for checking whether shadow_depth exists and whether casters are being written.
It is not a shadow receiver.
9. Better debug visualization
Because reversed-Z uses larger values for closer casters, raw depth may look unintuitive.
Try this:
#version 150
uniform sampler2D ShadowSampler;
in vec2 texCoord;out vec4 fragColor;
void main() { float d = texture(ShadowSampler, texCoord).r;
// Empty cleared areas are near 0.0. float visible = smoothstep(0.0001, 0.01, d);
// Boost contrast for debugging. float boosted = pow(clamp(d, 0.0, 1.0), 0.25);
vec3 color = mix(vec3(0.02, 0.02, 0.04), vec3(boosted), visible); fragColor = vec4(color, 1.0);}This makes very small non-zero depth values easier to see.
10. Built-in debug view
VPFX includes a built-in shadow depth debug effect.
The current built-in graph reads:
vulkanpostfx:shadow_depthwith:
"use_depth_buffer": trueand displays the depth value directly.
The default debug key is:
F9Use this when testing whether terrain, entities, players, or other casters are present in the shadow map.
11. Required uniforms for shadow-aware work
VPFX currently exposes these public built-in shadow-related values through VpfxBuiltins:
vpfx_ShadowMapSizevpfx_ShadowBiasvpfx_ShadowViewMatrixvpfx_InverseShadowViewMatrixvpfx_ShadowProjectionMatrixvpfx_InverseShadowProjectionMatrixvpfx_ShadowViewProjectionMatrixvpfx_ShadowLightPositionvpfx_ShadowAngleRaw matrix aliases also exist:
shadowModelViewshadowProjectionshadowViewProjectionThese are public post-effect uniforms.
12. Important current limitation: shadowOrigin
The current internal VPFX shadow caster and terrain receiver pipeline uses a stable shadow origin.
Internal caster and receiver logic uses this rule:
shadow-relative position = world position - shadowOriginThen:
shadow clip position = ShadowViewProjectionMatrix * vec4(shadow-relative position, 1.0)However, in the current public VpfxBuiltins block, shadowOrigin is not exposed as a public post-effect uniform.
This matters.
If an external pack reconstructs world position from scene_depth, it gets a main-camera world position. To correctly project that position into VPFX shadow space, the pack also needs the same shadowOrigin used by the shadow caster pass.
Without public shadowOrigin, a pack should not assume it can write a correct full custom terrain shadow receiver only from:
vpfx_WorldPositionFromRaw(...)vpfx_ShadowViewProjectionMatrixThat is incomplete for the current internal shadow-space convention.
13. Safe uses of shadow_depth right now
The following uses are safe for community packs in the current public API:
direct shadow_depth debug displayshadow map inspectioncaster coverage debuggingstylized full-screen visualizationpack loading tests requiring shadow_depthexperimental tools for checking whether the shadow target is availableExamples:
show shadow_depth on screentint the image when shadow_depth is availablevisualize shadow map occupancymake a debug pack for pack authors14. Risky uses of shadow_depth right now
The following uses are risky with the current public API:
writing a physically correct custom terrain shadow receiverreprojecting scene pixels into shadow map spacedoing screen-space shadow compare from scene_depthreplacing the built-in terrain receiverporting Iris shadow receiver code directlyassuming shadow_depth uses main-camera UVsThese require a stable public shadow-space contract that includes shadowOrigin.
A future VPFX version should expose shadowOrigin publicly before external packs are encouraged to implement full custom receivers.
15. Why texCoord sampling is not enough
This code samples the shadow map as an image:
float d = texture(ShadowSampler, texCoord).r;That is fine for debug display.
But this does not answer:
Is the current screen pixel in shadow?To answer that, a shader must:
1. Read scene depth.2. Reconstruct the screen pixel's world position.3. Convert world position into VPFX shadow-relative space.4. Apply VPFX shadow view-projection.5. Convert to shadow map UV.6. Compare receiver depth against stored shadow depth.7. Apply bias and filtering.The missing public piece today is:
shadowOriginSo direct texCoord sampling is only a debug view, not a real receiver.
16. Internal terrain receiver behavior
VPFX currently has an internal terrain receiver path.
The internal receiver uses an internal uniform block:
layout(std140) uniform VpfxTerrainShadowThis internal block includes:
ShadowInfoShadowLightInfoShadowOriginInfoShadowViewProjMatThe internal receiver can correctly compute:
worldPos - ShadowOriginInfo.xyzbecause ShadowOriginInfo is provided internally.
External VPFX post-effect packs should not rely on VpfxTerrainShadow, because it is not the public post-effect uniform contract.
Use VpfxBuiltins for external pack shaders.
17. Internal receiver side-face artifact rule
The internal terrain receiver avoids using player-screen derivatives to decide whether block faces can receive shadows.
The current receiver uses stable block/face information from chunk vertex color to avoid side-face diagonal artifacts.
For external authors, the important rule is:
Do not use player camera direction or screen-space derivatives to decide shadow direction.Shadow direction must come from:
the light directionthe shadow matricesthe VPFX shadow-space contractnot from the player look direction.
18. Correct mental model
Think of shadow_depth like this:
A square texture rendered from the sun or moon's shadow view.It stores caster depth in shadow space.It is centered around a stable shadow origin near the player for coverage.It does not rotate with the player's camera.It is not laid out like the screen.Wrong mental model:
shadow_depth is a grayscale screen overlay.Correct mental model:
shadow_depth is a light-space map.19. Current shadow light selection
VPFX chooses a primary shadow light.
Current possible primary lights:
sunmoonnoneAt low-light transition periods, such as sunrise or sunset, VPFX may choose:
noneWhen primary light is none, dynamic shadow rendering may be disabled or faded out.
The shadow map should not be assumed to be valid at all times of day.
Use:
vpfx_ShadowMapSize > 0.5as a basic shader-side availability check, but remember that this only checks target size, not whether a physically useful shadow pass was rendered this frame.
20. Shadow map availability checklist
A shadow-aware pack should expect shadow_depth to be unavailable or visually empty when:
VPFX is disabled.The pack did not request shadow_depth capability.The runtime cannot allocate the shadow target.The world is not loaded.The shadow light is not renderable.The current time is in a weak-light transition zone.The shadow pass failed.The active runtime has fallen back to vanilla/disabled behavior.Do not crash or assume valid shadow content every frame.
21. Shader-side availability pattern
A simple debug shader can handle missing shadow maps like this:
#version 150
uniform sampler2D InSampler;uniform sampler2D ShadowSampler;
in vec2 texCoord;out vec4 fragColor;
void main() { vec4 scene = texture(InSampler, texCoord);
if (vpfx_ShadowMapSize <= 0.5) { fragColor = scene; return; }
float d = texture(ShadowSampler, texCoord).r; float visible = smoothstep(0.0001, 0.01, d);
vec3 debug = mix(scene.rgb, vec3(d), visible * 0.75); fragColor = vec4(debug, scene.a);}Graph inputs:
"inputs": [ { "sampler_name": "In", "target": "minecraft:scene_color" }, { "sampler_name": "Shadow", "target": "vulkanpostfx:shadow_depth", "use_depth_buffer": true }]22. Reversed-Z compare concept
If you eventually write a custom receiver after the public API exposes the required data, remember the compare direction.
For reversed-Z shadow depth:
storedDepth > receiverDepth + biasusually means there is a closer caster between the light and the receiver.
A simplified compare shape looks like this:
float blocker = smoothstep( 0.00018, 0.00155, storedDepth - receiverDepth - bias);This is only a conceptual example. A real receiver also needs correct shadow UVs, edge checks, bias, filtering, and the correct shadow-origin-relative coordinate.
23. PCF concept
PCF means percentage-closer filtering.
The idea is:
sample several nearby shadow texelscompare each oneaverage the resultConceptual shape:
float sum = 0.0;float count = 0.0;
for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { vec2 uv = shadowUv + vec2(x, y) * shadowTexel; float stored = texture(ShadowSampler, uv).r; sum += storedDepthCompare(stored, receiverDepth, bias); count += 1.0; }}
float shadow = sum / count;Do not use this directly until the public shadow receiver API is complete.
24. Bias concept
Shadow bias is used to reduce self-shadow artifacts.
VPFX exposes:
vpfx_ShadowBiasInternal receiver logic also applies additional stability rules.
Common problems:
bias too low -> acne, stripes, diagonal artifactsbias too high -> peter-panning, detached shadowsFor external packs, treat vpfx_ShadowBias as a guide value, not a complete final solution.
25. Edge fade concept
Shadow maps only cover a finite region.
If a receiver samples near the edge of the shadow map, artifacts are likely.
A typical receiver should fade near edges:
float edgeFade(vec2 uv) { vec2 edge = min(uv, 1.0 - uv); return smoothstep(0.0, 0.04, min(edge.x, edge.y));}Again, this is a receiver concept, not needed for a direct debug view.
26. No-player-view rule
VPFX shadow rendering must not depend on the player’s view direction.
Allowed:
Use player/camera position to choose a local coverage region.Use light direction to build shadow view.Use shadowOrigin to stabilize caster and receiver coordinates.Not allowed:
Use player yaw to rotate the shadow map.Use player pitch to rotate the shadow map.Use player look vector as shadow light direction.Use screen-space derivatives to decide physical shadow direction.Use main camera relative coordinates as shadow caster coordinates.For pack authors, this means:
If a shadow changes shape only because the player rotates the camera, something is wrong.27. Relationship with vanilla entity shadows
Minecraft has vanilla circular entity shadows, sometimes called blob shadows.
VPFX suppresses those while the VPFX shadow map is active.
This avoids double shadows:
vanilla circular blob shadow+VPFX shadow map entity shadow=incorrect double shadowThe suppressor only affects vanilla circular shadow submits. It does not remove VPFX entity shadow casters.
A useful test:
VPFX off:vanilla circular entity shadow may appear.
VPFX shadow map active:vanilla circular entity shadow should disappear.VPFX shadow map entity shadow should remain.28. Relationship with external shaderpacks
If an external VPFX pack declares shadow_depth, it is saying:
This pack expects the runtime to provide a VPFX shadow depth target.The pack is not saying:
The pack can automatically use all VPFX internal terrain receiver data.Current safe authoring pattern:
Use shadow_depth for debug and inspection.Use it to verify caster coverage.Wait for a documented public shadowOrigin uniform before writing full custom receivers.29. Recommended shadow_depth debug pack
pack.json:
{ "format_version": 1, "pack_id": "example_shadow_depth_debug", "name": "Example Shadow Depth Debug", "version": "1.0.0", "author": "Your Name", "description": "Displays VPFX shadow_depth for debugging.", "entry_post_effect": "post_effect/main.json", "capabilities": { "scene_color": true, "scene_depth": false, "shadow_depth": true, "custom_targets": true, "compute": false }, "targets": { "shadow_depth": "vulkanpostfx:shadow_depth" }, "metadata": { "tags": [ "debug", "shadow-depth" ] }}post_effect/main.json:
{ "targets": {}, "passes": [ { "id": "shadow_debug", "debug_label": "Shadow Depth Debug", "vertex_shader": "example_shadow_depth_debug:post/fullscreen", "fragment_shader": "example_shadow_depth_debug:post/shadow_debug", "inputs": [ { "sampler_name": "Shadow", "target": "vulkanpostfx:shadow_depth", "use_depth_buffer": true } ], "output": "minecraft:main" } ]}shaders/post/fullscreen.vsh:
#version 150
out vec2 texCoord;
void main() { vec2 pos;
if (gl_VertexID == 0) { pos = vec2(-1.0, -1.0); } else if (gl_VertexID == 1) { pos = vec2(3.0, -1.0); } else { pos = vec2(-1.0, 3.0); }
texCoord = pos * 0.5 + 0.5; gl_Position = vec4(pos, 0.0, 1.0);}shaders/post/shadow_debug.fsh:
#version 150
uniform sampler2D ShadowSampler;
in vec2 texCoord;out vec4 fragColor;
void main() { float d = texture(ShadowSampler, texCoord).r;
float visible = smoothstep(0.0001, 0.01, d); float boosted = pow(clamp(d, 0.0, 1.0), 0.25);
vec3 color = mix(vec3(0.02, 0.02, 0.04), vec3(boosted), visible); fragColor = vec4(color, 1.0);}30. Testing shadow_depth
When testing shadow_depth, check:
flat terrainwallsstairsentitiesplayersheld itemsboatsminecartsmoving entitiestime of daycamera rotation stabilityImportant tests:
Open F9 shadow debug.Check whether casters appear.Rotate the camera without moving.Check whether the shadow map content incorrectly changes with view direction.Move around.Check whether the shadow coverage updates near the player.Change time of day.Check whether shadows fade or disable near weak-light periods.31. Common mistakes
31.1 Treating shadow_depth as scene_depth
Wrong:
float viewDepth = texture(ShadowSampler, texCoord).r;This samples the shadow map at screen UVs. It is not view depth.
31.2 Writing a receiver without shadowOrigin
Wrong assumption:
worldPos * vpfx_ShadowViewProjectionMatrix is enough.Current VPFX shadow space uses:
(worldPos - shadowOrigin) * shadow matrixshadowOrigin is not currently public in VpfxBuiltins.
31.3 Using player view direction
Wrong:
Use camera look vector as shadow direction.Correct:
Use VPFX shadow light / shadow matrices.31.4 Expecting shadow_depth to always be valid
Wrong:
shadow_depth always contains useful data.Correct:
shadow_depth may be empty or unavailable depending on runtime state, world state, time of day, and fallback behavior.31.5 Forgetting manifest capabilities
Wrong:
"shadow_depth": falsewhile reading:
"target": "vulkanpostfx:shadow_depth"Correct:
"shadow_depth": trueand:
"targets": { "shadow_depth": "vulkanpostfx:shadow_depth"}32. Current authoring recommendation
For current VPFX pack authors:
1. Use shadow_depth for debug packs first.2. Use F9 to compare your output with the built-in shadow debug view.3. Do not write a full custom terrain receiver until shadowOrigin is public.4. Do not port Iris shadow receiver logic directly.5. Do not assume shadow_depth is scene_depth.6. Do not use player camera direction to drive shadow behavior.The best current community shadow packs are likely to be:
shadow debug packsshadow map visualizerscaster coverage test packstools for checking shadow stabilityexperimental stylized shadow previewsFull receiver packs should wait for a more complete public shadow API.
33. Future public API needs
For external custom shadow receivers, VPFX should eventually expose:
shadowOriginshadow frame validityprimaryLightshadow light intensityshadow distanceterrain/entity caster flagsreceiver-space helper functionspossibly cascade data if cascaded shadows are added laterOnce these are public and documented, external packs can safely implement full shadow receivers.
34. Summary
Remember these rules:
shadow_depth is light-space depth.shadow_depth uses reversed-Z.shadow_depth is not scene_depth.shadow_depth is sampled through vulkanpostfx:shadow_depth.Use use_depth_buffer: true for clarity.Declare shadow_depth in pack.json capabilities.Declare targets.shadow_depth in pack.json.Use shadow_depth safely for debug and visualization.Do not write a full receiver without public shadowOrigin.Do not use player view direction for shadow logic.The next document should be:
07 - Common Errors and TroubleshootingThat document should cover common loading, validation, shader compile, target, sampler, scene depth, and shadow depth mistakes.