Skip to content

VPFX Post Effect Graph Format

This document describes the current VPFX post_effect/main.json graph format.

The graph file is referenced from pack.json through:

"entry_post_effect": "post_effect/main.json"

The graph defines:

Which render targets the pack declares
Which fullscreen passes the pack runs
Which inputs each pass reads
Which target each pass writes to
Which shader files each pass uses

This document is based on the current VPFX v1 graph parser and validator.


1. Basic structure

A VPFX post-effect graph is a JSON object with two required root fields:

{
"targets": {},
"passes": []
}

Both fields are required.

FieldTypeRequiredDescription
targetsobjectYesDeclares custom render targets used by the graph. Can be empty.
passesarrayYesOrdered list of fullscreen post-processing passes. Must contain at least one pass.

A minimal valid graph looks like this:

{
"targets": {},
"passes": [
{
"id": "final_composite",
"debug_label": "Final Composite",
"vertex_shader": "example_minimal_pack:composite/final",
"fragment_shader": "example_minimal_pack:composite/final",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "minecraft:main"
}
]
}

This graph reads minecraft:scene_color, runs one fullscreen shader pass, and writes to minecraft:main.


2. Execution model

VPFX v1 graph passes are executed in the order they appear in the passes array.

Example:

passes[0] -> passes[1] -> passes[2] -> ...

For now, community-facing VPFX passes should be treated as fullscreen triangle passes.

A normal color grading pack usually looks like this:

minecraft:scene_color
|
v
final_composite pass
|
v
minecraft:main

A simple two-pass pack may look like this:

minecraft:scene_color
|
v
example_pack:temp
|
v
minecraft:main

The graph must write to:

minecraft:main

at least once, otherwise it will not produce a visible final image and validation fails.


3. Built-in targets

VPFX currently recognizes these built-in target identifiers:

minecraft:main
minecraft:scene_color
minecraft:scene_depth
minecraft:shadow_depth
vulkanpostfx:scene_depth
vulkanpostfx:shadow_depth

3.1 minecraft:main

minecraft:main is the final output target.

Use it as the output of your last visible pass:

"output": "minecraft:main"

A graph must write to minecraft:main at least once.


3.2 minecraft:scene_color

minecraft:scene_color is the scene color snapshot captured before VPFX post-processing.

Most packs should start by reading this:

{
"sampler_name": "In",
"target": "minecraft:scene_color"
}

In GLSL:

uniform sampler2D InSampler;

3.3 minecraft:scene_depth and vulkanpostfx:scene_depth

These are scene depth input aliases.

They represent the main camera depth captured by VPFX.

Recommended target:

vulkanpostfx:scene_depth

Example:

{
"sampler_name": "Depth",
"target": "vulkanpostfx:scene_depth"
}

In GLSL:

uniform sampler2D DepthSampler;

Use scene depth for effects such as:

depth fog
depth debug views
distance-based color grading
depth-aware outlines

3.4 minecraft:shadow_depth and vulkanpostfx:shadow_depth

These are VPFX shadow depth input aliases.

Recommended target:

vulkanpostfx:shadow_depth

Example:

{
"sampler_name": "Shadow",
"target": "vulkanpostfx:shadow_depth"
}

In GLSL:

uniform sampler2D ShadowSampler;

Important:

shadow_depth is not scene_depth.
shadow_depth is not main camera depth.
shadow_depth uses shadow-space depth.
shadow_depth uses reversed-Z.
shadow_depth should be used with VPFX shadow uniforms.

Do not use shadow_depth in your first pack. Read the dedicated shadow guide first.


4. Custom targets

Custom targets are declared in the root targets object.

Example:

{
"targets": {
"example_pack:temp": {
"scale": 1.0,
"use_depth": false,
"clear_color": [0.0, 0.0, 0.0, 0.0]
}
},
"passes": []
}

A custom target ID must match this pattern:

^[a-z0-9_.-]+:[a-z0-9_./-]+$

Good target IDs:

example_pack:temp
example_pack:bloom/downsample_0
author_pack:color_grade
debug_pack:shadow_view

Bad target IDs:

temp
ExamplePack:Temp
example pack:temp
example_pack:/bad

Recommended rule:

Use your pack_id as the target namespace.

For example, if your pack_id is:

"pack_id": "example_pack"

then your custom targets should look like:

example_pack:temp
example_pack:bloom/downsample_0
example_pack:history/color

5. Target fields

Each target definition is an object.

Supported fields:

FieldTypeRequiredDefaultDescription
scalenumberNo1.0Relative size compared to the main screen.
use_depthbooleanNofalseWhether the target has a depth buffer.
clear_colornumber[4]Noruntime defaultRGBA clear color.
persistentbooleanNofalseKeeps the target alive across frames.
historybooleanNofalseIntended for previous-frame sampling.
ping_pongbooleanNofalseIntended for double-buffered history targets.

6. scale

Optional.

"scale": 0.5

scale controls the target size relative to the main screen.

Allowed range:

0.0 < scale <= 1.0

Examples:

"scale": 1.0

Full resolution.

"scale": 0.5

Half resolution.

"scale": 0.25

Quarter resolution.

Use scaled targets for blur, bloom, downsample, and performance-friendly intermediate effects.

Invalid:

"scale": 0

Invalid:

"scale": 2.0

7. use_depth

Optional.

Default:

"use_depth": false

If use_depth is true, the target is created with a depth buffer.

Example:

"example_pack:depth_target": {
"scale": 1.0,
"use_depth": true
}

Only enable this if you actually need to sample the target’s depth buffer with:

"use_depth_buffer": true

Most color-only post-processing targets do not need depth.


8. clear_color

Optional.

"clear_color": [0.0, 0.0, 0.0, 0.0]

It must be an array of exactly four numbers:

red
green
blue
alpha

Examples:

"clear_color": [0.0, 0.0, 0.0, 0.0]
"clear_color": [1.0, 0.0, 1.0, 1.0]

Do not rely on clear_color as a replacement for writing all pixels in a pass. A fullscreen post-processing pass should normally cover the whole output target.


9. Persistent, history, and ping-pong targets

The graph parser currently accepts these fields:

"persistent": true
"history": true
"ping_pong": true

Their intended meaning:

persistent:
Keeps the target storage alive across frames.
history:
Marks the target as intended for previous-frame sampling.
ping_pong:
Uses two buffers so one can be read as previous frame while the other is written this frame.

For early community packs, avoid temporal targets unless you are deliberately testing temporal effects.

Recommended first-pack rule:

Do not use persistent, history, or ping_pong in your first pack.

Start with transient targets and simple passes.


10. Pass definition

Each pass is an object inside the passes array.

Example:

{
"id": "final_composite",
"debug_label": "Final Composite",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "minecraft:main"
}

Supported pass fields:

FieldTypeRequiredDescription
idstringNoStable pass identifier. Recommended.
debug_labelstringNoHuman-readable label for logs or debugging.
vertex_shaderstringYesVertex shader resource ID.
fragment_shaderstringYesFragment shader resource ID.
inputsarrayYesList of target or texture inputs. Must not be empty.
outputstringYesOutput target. Must be minecraft:main or a declared custom target.

11. id

Optional but strongly recommended.

"id": "final_composite"

A pass ID helps logs and diagnostics identify the pass.

Good IDs:

final_composite
bloom_downsample_0
bloom_upsample_1
shadow_debug
tone_map

Avoid spaces and special characters.


12. debug_label

Optional.

"debug_label": "Final Composite"

This is a human-readable label.

Use it for clarity:

"debug_label": "Bloom Downsample 0"

If id exists, VPFX generally uses it as the stronger identity. If id is missing, debug_label may be used as a fallback identity in logs.


13. Shader references

Each pass must specify:

"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final"

Shader references use this format:

namespace:path

The recommended namespace is your pack_id.

The path maps to files inside the zip:

shaders/<path>.vsh
shaders/<path>.fsh

Example:

"vertex_shader": "example_pack:composite/final"

requires:

shaders/composite/final.vsh

Example:

"fragment_shader": "example_pack:composite/final"

requires:

shaders/composite/final.fsh

Shader paths must not:

be blank
use absolute paths
contain ..
contain backslashes

Good shader references:

example_pack:composite/final
example_pack:bloom/downsample
example_pack:debug/shadow_depth

Bad shader references:

composite/final
example_pack:
example_pack:../final
example_pack:/final
example_pack:folder\final

14. Inputs

Each pass must have at least one input.

"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
]

Each input must contain:

sampler_name
exactly one of target or texture

Valid target input:

{
"sampler_name": "In",
"target": "minecraft:scene_color"
}

Valid texture input:

{
"sampler_name": "BlueNoise",
"texture": "BlueNoise"
}

Invalid input with both target and texture:

{
"sampler_name": "Bad",
"target": "minecraft:scene_color",
"texture": "BlueNoise"
}

Invalid input with neither:

{
"sampler_name": "Bad"
}

15. sampler_name

Required.

"sampler_name": "In"

The sampler name must match:

^[A-Za-z_][A-Za-z0-9_]*$

Good sampler names:

In
Color
Depth
Shadow
BlueNoise
_ColorLut

Bad sampler names:

1Input
color-texture
scene color
shadow.depth

The GLSL uniform name is:

<sampler_name>Sampler

Example:

"sampler_name": "In"

GLSL:

uniform sampler2D InSampler;

Example:

"sampler_name": "Shadow"

GLSL:

uniform sampler2D ShadowSampler;

Sampler names must be unique within one pass.

Invalid:

"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
},
{
"sampler_name": "In",
"target": "vulkanpostfx:scene_depth"
}
]

16. Target inputs

A target input reads from a render target.

Example:

{
"sampler_name": "In",
"target": "minecraft:scene_color"
}

The target value can be:

a built-in target
a custom target declared in the root targets object

Built-in examples:

minecraft:scene_color
vulkanpostfx:scene_depth
vulkanpostfx:shadow_depth

Custom target example:

example_pack:temp

A custom target must be declared before it can be used in the graph.


17. Read order rule

Custom targets must be written by an earlier pass before a later pass reads them.

Valid:

{
"targets": {
"example_pack:temp": {
"scale": 1.0,
"use_depth": false
}
},
"passes": [
{
"id": "write_temp",
"vertex_shader": "example_pack:composite/copy",
"fragment_shader": "example_pack:composite/copy",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "example_pack:temp"
},
{
"id": "final",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "Temp",
"target": "example_pack:temp"
}
],
"output": "minecraft:main"
}
]
}

Invalid:

{
"targets": {
"example_pack:temp": {
"scale": 1.0,
"use_depth": false
}
},
"passes": [
{
"id": "bad_read",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "Temp",
"target": "example_pack:temp"
}
],
"output": "minecraft:main"
}
]
}

The second example reads example_pack:temp before any pass has written it.


18. Self-read/write rule

A pass must not read from the same custom target it writes to.

Invalid:

{
"id": "bad_feedback",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "Temp",
"target": "example_pack:temp"
}
],
"output": "example_pack:temp"
}

This creates a read/write feedback hazard.

Use two targets instead:

example_pack:temp_a
example_pack:temp_b

or a later ping-pong/history pattern when temporal target support is documented for your VPFX version.

Reading minecraft:scene_color while writing minecraft:main is safe because VPFX uses a scene color snapshot.


19. Texture inputs

A texture input reads a texture declared in pack.json.

Manifest example:

"textures": {
"BlueNoise": {
"path": "textures/blue_noise.png",
"filter": "nearest",
"wrap": "repeat"
}
}

Graph input:

{
"sampler_name": "BlueNoise",
"texture": "BlueNoise"
}

GLSL:

uniform sampler2D BlueNoiseSampler;

The texture value is the logical texture name from pack.json, not a file path.

Invalid:

{
"sampler_name": "BlueNoise",
"texture": "textures/blue_noise.png"
}

Texture inputs do not support:

"use_depth_buffer": true

20. Depth inputs

Input objects may contain:

"use_depth_buffer": true

This is only valid for target inputs, not texture inputs.

Example:

{
"sampler_name": "Depth",
"target": "vulkanpostfx:scene_depth"
}

Usually, explicit depth target names are preferred over use_depth_buffer.

Recommended scene depth input:

{
"sampler_name": "Depth",
"target": "vulkanpostfx:scene_depth"
}

Recommended shadow depth input:

{
"sampler_name": "Shadow",
"target": "vulkanpostfx:shadow_depth"
}

For a custom target depth buffer, the target must be declared with:

"use_depth": true

Example:

{
"targets": {
"example_pack:depth_target": {
"scale": 1.0,
"use_depth": true
}
},
"passes": [
{
"id": "read_depth_target",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "Depth",
"target": "example_pack:depth_target",
"use_depth_buffer": true
}
],
"output": "minecraft:main"
}
]
}

Do not use use_depth_buffer with texture inputs.

Invalid:

{
"sampler_name": "Noise",
"texture": "BlueNoise",
"use_depth_buffer": true
}

21. Output targets

Each pass must have one output.

Valid outputs:

minecraft:main
a declared custom target

Example final output:

"output": "minecraft:main"

Example custom output:

"output": "example_pack:temp"

Invalid output:

"output": "vulkanpostfx:scene_depth"

Invalid output:

"output": "vulkanpostfx:shadow_depth"

Depth targets are input-only.

The graph must include at least one pass that writes to:

minecraft:main

22. One-pass color grading example

{
"targets": {},
"passes": [
{
"id": "final_composite",
"debug_label": "Final Composite",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "minecraft:main"
}
]
}

This is the recommended first graph.


23. Two-pass example

{
"targets": {
"example_pack:temp": {
"scale": 1.0,
"use_depth": false,
"clear_color": [0.0, 0.0, 0.0, 0.0]
}
},
"passes": [
{
"id": "first_pass",
"debug_label": "First Pass",
"vertex_shader": "example_pack:composite/first",
"fragment_shader": "example_pack:composite/first",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "example_pack:temp"
},
{
"id": "final_pass",
"debug_label": "Final Pass",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "Temp",
"target": "example_pack:temp"
}
],
"output": "minecraft:main"
}
]
}

This is the basic pattern for multi-pass effects.


24. Downsample target example

{
"targets": {
"example_pack:half_res": {
"scale": 0.5,
"use_depth": false,
"clear_color": [0.0, 0.0, 0.0, 0.0]
}
},
"passes": [
{
"id": "downsample",
"vertex_shader": "example_pack:composite/downsample",
"fragment_shader": "example_pack:composite/downsample",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "example_pack:half_res"
},
{
"id": "final",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "HalfRes",
"target": "example_pack:half_res"
},
{
"sampler_name": "Scene",
"target": "minecraft:scene_color"
}
],
"output": "minecraft:main"
}
]
}

This pattern is useful for blur and bloom-style effects.


25. Scene depth example

Make sure your pack.json declares:

"capabilities": {
"scene_color": true,
"scene_depth": true,
"shadow_depth": false,
"custom_targets": true,
"compute": false
}

Graph:

{
"targets": {},
"passes": [
{
"id": "depth_debug",
"vertex_shader": "example_pack:debug/depth",
"fragment_shader": "example_pack:debug/depth",
"inputs": [
{
"sampler_name": "Depth",
"target": "vulkanpostfx:scene_depth"
}
],
"output": "minecraft:main"
}
]
}

GLSL:

uniform sampler2D DepthSampler;

26. Shadow depth example

Make sure your pack.json declares:

"capabilities": {
"scene_color": true,
"scene_depth": true,
"shadow_depth": true,
"custom_targets": true,
"compute": false
},
"targets": {
"shadow_depth": "vulkanpostfx:shadow_depth"
}

Graph:

{
"targets": {},
"passes": [
{
"id": "shadow_debug",
"vertex_shader": "example_pack:debug/shadow",
"fragment_shader": "example_pack:debug/shadow",
"inputs": [
{
"sampler_name": "Shadow",
"target": "vulkanpostfx:shadow_depth"
}
],
"output": "minecraft:main"
}
]
}

GLSL:

uniform sampler2D ShadowSampler;

Remember:

shadow_depth is reversed-Z shadow-space depth.
Do not treat it as main camera depth.

27. Current validation rules

The current graph validator checks:

The graph has at least one pass.
Custom target IDs are valid.
Target scale is finite and in (0, 1].
Each pass has at least one input.
Each sampler_name is valid.
Sampler names are unique within one pass.
Each input has exactly one of target or texture.
Target inputs reference built-in or declared targets.
Texture inputs reference textures declared in pack.json.
Custom targets are written before being read.
A pass does not read and write the same custom target.
Each output is minecraft:main or a declared target.
At least one pass writes to minecraft:main.
Capabilities required by the pack are supported by the runtime.

If validation fails, VPFX skips the pack and logs an error.


28. Common graph errors

Missing targets

Invalid:

{
"passes": []
}

Valid:

{
"targets": {},
"passes": []
}

Missing passes

Invalid:

{
"targets": {}
}

Valid:

{
"targets": {},
"passes": [
{
"id": "final",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "minecraft:main"
}
]
}

Empty passes

Invalid:

{
"targets": {},
"passes": []
}

A graph must contain at least one pass.


Missing output to minecraft:main

Invalid:

{
"targets": {
"example_pack:temp": {
"scale": 1.0
}
},
"passes": [
{
"id": "only_temp",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "example_pack:temp"
}
]
}

The pack writes to example_pack:temp, but never writes to minecraft:main.


Invalid sampler name

Invalid:

{
"sampler_name": "Scene Color",
"target": "minecraft:scene_color"
}

Valid:

{
"sampler_name": "SceneColor",
"target": "minecraft:scene_color"
}

Duplicate sampler names in one pass

Invalid:

"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
},
{
"sampler_name": "In",
"target": "vulkanpostfx:scene_depth"
}
]

Valid:

"inputs": [
{
"sampler_name": "Scene",
"target": "minecraft:scene_color"
},
{
"sampler_name": "Depth",
"target": "vulkanpostfx:scene_depth"
}
]

Texture input not declared in manifest

Invalid graph input:

{
"sampler_name": "Noise",
"texture": "BlueNoise"
}

if pack.json does not declare:

"textures": {
"BlueNoise": {
"path": "textures/blue_noise.png"
}
}

Reading a target before writing it

Invalid:

{
"targets": {
"example_pack:temp": {
"scale": 1.0
}
},
"passes": [
{
"id": "final",
"vertex_shader": "example_pack:composite/final",
"fragment_shader": "example_pack:composite/final",
"inputs": [
{
"sampler_name": "Temp",
"target": "example_pack:temp"
}
],
"output": "minecraft:main"
}
]
}

example_pack:temp is declared, but no earlier pass writes to it.


29. Error code reference

Common parser and validator error codes:

CodeMeaning
G001Graph entry file not found.
G002Graph root must be an object, or graph has no passes depending on validation stage.
G003Failed to read graph JSON, or pass has no inputs depending on validation stage.
G004Output target not declared.
G005Input target not found.
G006Invalid depth-buffer usage or missing runtime depth capability.
G007Invalid target identifier.
G008Invalid sampler name.
G009Duplicate sampler name within one pass.
G010Target definition must be an object.
G011Invalid target scale.
G012Input must contain exactly one of target or texture.
G013Texture input not declared in manifest.
G014Texture input cannot use use_depth_buffer=true.
G015Self-read/write hazard.
G016Graph does not write to minecraft:main.
G017Custom target is read before being written.
G020Each pass must be an object.
G030Each input must be an object.
G031use_depth_buffer must be boolean.
G032Input must contain exactly one of target or texture.
G033Texture input cannot use use_depth_buffer=true.
G040clear_color must be an array.
G041clear_color must contain exactly 4 elements.
G042clear_color elements must be numbers.
G050Missing required object field.
G052Missing required array field.
G054Missing required string field.
G056Required string field is blank.
G058Optional string field has wrong type.
G059Optional boolean field has wrong type.
S001Vertex shader file not found.
S002Fragment shader file not found.
S003Invalid shader resource ID or shader path.
S004Vertex shader include/preprocess error.
S005Fragment shader include/preprocess error.

30. Graph authoring checklist

Before sharing a pack, check:

post_effect/main.json exists at the path referenced by pack.json.
The root JSON object contains targets and passes.
targets is an object, even if empty.
passes is a non-empty array.
Every custom target ID is namespaced.
Every scale is in (0, 1].
Every pass has vertex_shader and fragment_shader.
Every pass has at least one input.
Every sampler_name is GLSL-safe.
Sampler names are unique within each pass.
Every input has exactly one of target or texture.
Every texture input is declared in pack.json.
Every custom target is written before being read.
No pass reads and writes the same custom target.
At least one pass outputs to minecraft:main.
Shader files exist under shaders/.
Shader references use namespace:path.

For a first VPFX pack, use this:

{
"targets": {},
"passes": [
{
"id": "final_composite",
"debug_label": "Final Composite",
"vertex_shader": "my_first_vpfx_pack:composite/final",
"fragment_shader": "my_first_vpfx_pack:composite/final",
"inputs": [
{
"sampler_name": "In",
"target": "minecraft:scene_color"
}
],
"output": "minecraft:main"
}
]
}

Required files:

shaders/composite/final.vsh
shaders/composite/final.fsh

This graph is intentionally simple. Make this work before adding custom targets, depth, shadow depth, textures, or temporal effects.


32. Next document

This document covers:

post_effect/main.json this work before adding custom targets, depth, shadow depth, textures, or temporal effects.
---
##
targets
passes
inputs
outputs
sampler names
custom target validation
depth and shadow target references

The next document should cover:

05 - VPFX Uniform Reference

That document should explain:

Built-in uniform block
Frame data
Screen size
Time
Scene depth parameters
Shadow matrices
Shadow origin
Light direction
Shadow map size