Skip to content

How to adapt your Shaderpack

Essentuan edited this page May 28, 2026 · 2 revisions

This section will not cover adding photonics settings to your shaderpack, and expects you have read the documentation. To understand what options Photonics gives you access to see properties.

There are two ways to add Photonics to your shaderpack

  1. Use the provided photonics pass (in deferred) that accepts gbuffer data and populates a lighting buffer; this is the easy route where photonics does everything for you
  2. Use the tracing API directly, works in gbuffers, you are responsible for usage and optimization.

This section will only cover option 1.

Adding lighting

To add raytraced lighting to your shaderpack you need to create 2 files:

/photonics/shader_interface.glsl

Create a file under /shaders/photonics/ called shader_interface.glsl

This is the main file Photonics uses to obtain information from your shader for the sake of its lighting pass.

You need to implement the following functions:


vec3 load_world_position();

Should return the world position for the current fragment.


void load_fragment_variables(
    out vec3 albedo,
    out vec3 world_pos,
    out vec3 geometry_normal,
    out vec3 texture_normal
);

This function is responsible for loading information about the current fragment into its arguments.

out vec3 albedo;

This argument is pretty simple, it's just the albedo for the current fragment.

out vec3 geometry_normal;

The geometry normal for the current fragment in world space.

out vec3 texture_normal;

The texture normal of the current fragment in world space.

out vec3 world_pos;

This argument is the world position for the current fragment, you first however need to nudge the position outside the block. To do this you can subtract 0.01f * world_normal from the result of load_world_position()


vec3 sun_direction;

The direction of the sun/moon in world space.


vec3 indirect_light_color;

The indirect light color used by GI for the sun/sky, should be your sky color.


vec2 get_taa_jitter();

Should return the taa jitter for the current fragment in NDC space.

bool is_in_world();

Should return true if the current fragment is in the world.

In most cases this can default to:

bool is_in_world() {
    return texelFetch(depthtex0, ivec2(gl_FragCoord.xy), 0).x <= 0.99999f;
}

/photonics/write_indirect.glsl

Create a file under /shaders/photonics/ called write_indirect.glsl

Photonics uses this file during its lighting pass in a fragment shader.

You need to implement the following functions:

void write_indirect(vec3 color);

This function writes the GI contribution for a fragment, and is unused if photonics.restirCombinedGi is set to true Photonics doesn't store GI in an accessible way for you, you instead must store and sample it yourself.

Sampling

To sample light contributions see ph_samplers.glsl.

Programs

Photonics supports a special kind of rendering (referred to here as voxelized blocks) for blocks that utilizes its Volume Rendering techniques that are used for all other graphical effects in Photonics. Under the hood, voxelized blocks have the same mesh as a normal solid block (e.g. Stone Block). The only difference, is that they use 2 custom programs: gbuffers_voxels and shadow_voxels.

Voxelized blocks do not support any PBR features or transparency.

gbuffers_voxels

Responsible for rendering voxelized blocks for terrain.

A mock implementation for gbuffers_voxels.fsh for sampling albedo & normals. This implementation requires block_normal from the vertex shader, which is equal to gl_Normal

// Placeholder for obtaining the screen pos
vec3 screen_pos = get_screen_pos();

// These functions are placeholders for however you do these conversions
vec3 view_pos = screen_to_view_space(screen_pos); // You should not account for TAA jitter
vec3 player_pos = view_to_player_space(view_pos);

vec3 rt_pos = player_pos + rt_camera_position;

RayJob ray = RayJob(
    // The offset along the normal is very important to ensure that the ray doesn't start outside the block
    rt_pos - 0.001f * block_normal, // Ray origin

    // View direction of the current pixel is calculated by subtracting the camera position from the player position
    normalize(player_pos - gbufferModelViewInverse[3].xyz), // Ray direction
    
    // Initialize results to default
    vec3(0), vec3(0), vec3(0), false
);

// stop raytracing once the ray leaves the block
ray_constraint = ivec3(ray.origin);
trace_ray(ray);

// Ray didn't hit any part of the 3d block (e.g. corners of the Crafting Table) 
if (!ray.result_hit) discard;
if (ray.result_normal == vec3(0.0)) ray.result_normal = block_normal;

player_pos = ray.result_position - rt_camera_position;

// These functions are placeholders for however you do these conversions
view_pos = player_to_view_space(player_pos);
screen_pos = view_to_screen_space(view_pos);

// Also update the depth!
gl_FragDepth = screen_pos.z;

vec3 world_normal = ray.result_normal; // The normal in world space 
vec4 albedo = vec4(ray.result_color, 1f);

shadow_voxels

Responsible for rendering voxelized blocks in shadows.

A mock implementation for shadow_voxels.fsh for detecting hits. This implementation requires:

  1. block_normal from the vertex shader, which is equal to gl_Normal
  2. player_pos from the vertex shader, which is the position of the vertex in player space.
RayJob ray = RayJob(
    // The offset along the normal is very important to ensure that the ray doesn't start outside the block
    player_pos + rt_camera_position - 0.01f * block_normal, // Ray origin
        
    mat3(shadowModelViewInverse) * vec3(0f, 0f, -1f), // Ray direction

    // Initialize results to default
    vec3(0f), vec3(0f), vec3(0f), false
);

ray_constraint = ivec3(ray.origin);
trace_ray(ray);

// Ray didn't hit any part of the 3d block (e.g. corners of the Crafting Table) 
if (!ray.result_hit) discard;

// In shadow!

// If you need the color you can use this, but it will always be opaque.
vec4 voxel_color = vec4(ray.result_color, 1f);

Clone this wiki locally