Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ test_output.txt
*.swp
.DS_Store
*.spv
!assets/shaders/vulkan/lpv_inject.comp.spv
!assets/shaders/vulkan/lpv_propagate.comp.spv
wiki/
*.exr
*.hdr
Expand Down
2 changes: 2 additions & 0 deletions assets/shaders/vulkan/g_pass.frag
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ layout(set = 0, binding = 0) uniform GlobalUniforms {
vec4 pbr_params;
vec4 volumetric_params;
vec4 viewport_size;
vec4 lpv_params;
vec4 lpv_origin;
} global;

// 4x4 Bayer matrix for dithered LOD transitions
Expand Down
71 changes: 71 additions & 0 deletions assets/shaders/vulkan/lpv_inject.comp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#version 450

layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;

struct LightData {
vec4 pos_radius;
vec4 color;
};

// SH L1: 3 output images (R, G, B), each storing 4 SH coefficients (L0, L1x, L1y, L1z)
layout(set = 0, binding = 0, rgba32f) uniform writeonly image3D lpv_out_r;
layout(set = 0, binding = 1, rgba32f) uniform writeonly image3D lpv_out_g;
layout(set = 0, binding = 2, rgba32f) uniform writeonly image3D lpv_out_b;

layout(set = 0, binding = 3) readonly buffer Lights {
LightData lights[];
} light_buffer;

layout(push_constant) uniform InjectPush {
vec4 grid_origin_cell;
vec4 grid_params;
uint light_count;
} push_data;

// SH L1 basis functions (unnormalized for compact storage)
// Y_00 = 0.282095 (DC)
// Y_1m = 0.488603 * {x, y, z} (directional)
const float SH_C0 = 0.282095;
const float SH_C1 = 0.488603;

void main() {
int gridSize = int(push_data.grid_params.x);
ivec3 cell = ivec3(gl_GlobalInvocationID.xyz);
if (any(greaterThanEqual(cell, ivec3(gridSize)))) {
return;
}

vec3 world_pos = push_data.grid_origin_cell.xyz + vec3(cell) * push_data.grid_origin_cell.w + vec3(0.5 * push_data.grid_origin_cell.w);

// Accumulate SH coefficients per color channel
vec4 sh_r = vec4(0.0); // (L0, L1x, L1y, L1z) for red
vec4 sh_g = vec4(0.0); // for green
vec4 sh_b = vec4(0.0); // for blue

for (uint i = 0; i < push_data.light_count; i++) {
vec3 light_pos = light_buffer.lights[i].pos_radius.xyz;
float radius = max(light_buffer.lights[i].pos_radius.w, 0.001);
vec3 light_color = light_buffer.lights[i].color.rgb;

vec3 diff = world_pos - light_pos;
float d = length(diff);
if (d < radius) {
float att = 1.0 - (d / radius);
att *= att;

// Direction from light to cell (normalized), used for SH L1 directional encoding
vec3 dir = (d > 0.001) ? normalize(diff) : vec3(0.0, 1.0, 0.0);

// SH L1 projection: project the incoming radiance along direction
vec4 sh_coeffs = vec4(SH_C0, SH_C1 * dir.x, SH_C1 * dir.y, SH_C1 * dir.z);

sh_r += sh_coeffs * light_color.r * att;
sh_g += sh_coeffs * light_color.g * att;
sh_b += sh_coeffs * light_color.b * att;
}
}

imageStore(lpv_out_r, cell, sh_r);
imageStore(lpv_out_g, cell, sh_g);
imageStore(lpv_out_b, cell, sh_b);
}
Binary file added assets/shaders/vulkan/lpv_inject.comp.spv
Binary file not shown.
117 changes: 117 additions & 0 deletions assets/shaders/vulkan/lpv_propagate.comp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#version 450

layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;

// Source SH grids (read)
layout(set = 0, binding = 0, rgba32f) uniform readonly image3D src_r;
layout(set = 0, binding = 1, rgba32f) uniform readonly image3D src_g;
layout(set = 0, binding = 2, rgba32f) uniform readonly image3D src_b;

// Destination SH grids (write)
layout(set = 0, binding = 3, rgba32f) uniform writeonly image3D dst_r;
layout(set = 0, binding = 4, rgba32f) uniform writeonly image3D dst_g;
layout(set = 0, binding = 5, rgba32f) uniform writeonly image3D dst_b;

// Occlusion grid
layout(set = 0, binding = 6) readonly buffer OcclusionGrid {
uint data[];
} occlusion;

layout(push_constant) uniform PropPush {
uint grid_size;
uint _pad0[3];
vec4 propagation;
} push_data;

uint flatIndex(ivec3 cell, int gridSize) {
return uint(cell.x) + uint(cell.y) * uint(gridSize) + uint(cell.z) * uint(gridSize) * uint(gridSize);
}

// SH L1 constants
const float SH_C0 = 0.282095;
const float SH_C1 = 0.488603;

// Evaluate SH in a given direction to get the scalar irradiance contribution
float evaluateSH(vec4 sh, vec3 dir) {
return max(0.0, sh.x * SH_C0 + sh.y * SH_C1 * dir.x + sh.z * SH_C1 * dir.y + sh.w * SH_C1 * dir.z);
}

// Project a scalar value in a given direction into SH coefficients
vec4 projectSH(float value, vec3 dir) {
return vec4(value * SH_C0, value * SH_C1 * dir.x, value * SH_C1 * dir.y, value * SH_C1 * dir.z);
}

void main() {
int gridSize = int(push_data.grid_size);
ivec3 cell = ivec3(gl_GlobalInvocationID.xyz);
if (any(greaterThanEqual(cell, ivec3(gridSize)))) {
return;
}

// If current cell is opaque, zero out
uint selfOcc = occlusion.data[flatIndex(cell, gridSize)];
if (selfOcc != 0u) {
imageStore(dst_r, cell, vec4(0.0));
imageStore(dst_g, cell, vec4(0.0));
imageStore(dst_b, cell, vec4(0.0));
return;
}

// Center retention
float retention = push_data.propagation.y;
vec4 center_r = imageLoad(src_r, cell) * retention;
vec4 center_g = imageLoad(src_g, cell) * retention;
vec4 center_b = imageLoad(src_b, cell) * retention;

vec4 accum_r = center_r;
vec4 accum_g = center_g;
vec4 accum_b = center_b;

float f = push_data.propagation.x;

// 6-connected neighbor propagation with SH directional transfer
// For each face, evaluate neighbor SH in the transfer direction and re-project
ivec3 offsets[6] = ivec3[6](
ivec3(-1, 0, 0), ivec3(1, 0, 0),
ivec3(0, -1, 0), ivec3(0, 1, 0),
ivec3(0, 0, -1), ivec3(0, 0, 1)
);
// Direction from neighbor to current cell (transfer direction)
vec3 dirs[6] = vec3[6](
vec3( 1, 0, 0), vec3(-1, 0, 0),
vec3( 0, 1, 0), vec3( 0,-1, 0),
vec3( 0, 0, 1), vec3( 0, 0,-1)
);

for (int i = 0; i < 6; i++) {
ivec3 n = cell + offsets[i];
if (any(lessThan(n, ivec3(0))) || any(greaterThanEqual(n, ivec3(gridSize)))) {
continue;
}
uint nOcc = occlusion.data[flatIndex(n, gridSize)];
if (nOcc != 0u) {
continue;
}

vec3 transferDir = dirs[i];

// Load neighbor SH coefficients
vec4 n_r = imageLoad(src_r, n);
vec4 n_g = imageLoad(src_g, n);
vec4 n_b = imageLoad(src_b, n);

// Evaluate how much light the neighbor sends in the transfer direction
float eval_r = evaluateSH(n_r, transferDir);
float eval_g = evaluateSH(n_g, transferDir);
float eval_b = evaluateSH(n_b, transferDir);

// Re-project into SH at current cell using the same direction
accum_r += projectSH(eval_r, transferDir) * f;
accum_g += projectSH(eval_g, transferDir) * f;
accum_b += projectSH(eval_b, transferDir) * f;
}

imageStore(dst_r, cell, accum_r);
imageStore(dst_g, cell, accum_g);
imageStore(dst_b, cell, accum_b);
}
Binary file added assets/shaders/vulkan/lpv_propagate.comp.spv
Binary file not shown.
32 changes: 28 additions & 4 deletions assets/shaders/vulkan/post_process.frag
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ layout(location = 0) out vec4 outColor;

layout(set = 0, binding = 0) uniform sampler2D uHDRBuffer;
layout(set = 0, binding = 2) uniform sampler2D uBloomTexture;
layout(set = 0, binding = 3) uniform sampler3D uColorLUT;

layout(push_constant) uniform PostProcessParams {
float bloomEnabled; // 0.0 = disabled, 1.0 = enabled
float bloomIntensity; // Final bloom blend intensity
float vignetteIntensity; // 0.0 = none, 1.0 = full vignette
float filmGrainIntensity;// 0.0 = none, 1.0 = heavy grain
float bloomEnabled; // 0.0 = disabled, 1.0 = enabled
float bloomIntensity; // Final bloom blend intensity
float vignetteIntensity; // 0.0 = none, 1.0 = full vignette
float filmGrainIntensity; // 0.0 = none, 1.0 = heavy grain
float colorGradingEnabled; // 0.0 = disabled, 1.0 = enabled
float colorGradingIntensity; // LUT blend intensity (0.0 = original, 1.0 = full LUT)
} postParams;

layout(set = 0, binding = 1) uniform GlobalUniforms {
Expand Down Expand Up @@ -131,6 +134,22 @@ float random(vec2 uv) {
return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453);
}

// LUT-based color grading using a 3D lookup texture.
// Input color should be in [0,1] range (post-tonemapping).
vec3 applyColorGrading(vec3 color, float intensity) {
if (intensity <= 0.0) return color;

// Clamp to valid LUT range and apply half-texel offset for correct sampling
vec3 lutCoord = clamp(color, 0.0, 1.0);

// Scale and bias for correct 3D LUT sampling (avoid edge texels)
const float LUT_SIZE = 32.0;
lutCoord = lutCoord * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;

vec3 graded = texture(uColorLUT, lutCoord).rgb;
return mix(color, graded, intensity);
}

// Film grain effect - adds animated noise
vec3 applyFilmGrain(vec3 color, vec2 uv, float intensity, float time) {
if (intensity <= 0.0) return color;
Expand Down Expand Up @@ -163,6 +182,11 @@ void main() {
color = ACESFilm(hdrColor * global.pbr_params.y);
}

// Apply LUT-based color grading (after tone mapping, in [0,1] range)
if (postParams.colorGradingEnabled > 0.5) {
color = applyColorGrading(color, postParams.colorGradingIntensity);
}

// Apply vignette effect
color = applyVignette(color, inUV, postParams.vignetteIntensity);

Expand Down
Loading