#version 430 core // Constants #define PI 3.14159265359 #define INV_PI 0.31830988618 #define EPSILON 1e-4 #define MAX_FLOAT 3.402823466e38 #define MAX_RAY_DEPTH 8 #define MAX_LIGHTS 16 #define RR_THRESHOLD 0.1 // Material types #define MATERIAL_DIFFUSE 0 #define MATERIAL_METAL 1 #define MATERIAL_DIELECTRIC 2 #define MATERIAL_EMISSIVE 3 // Light types #define LIGHT_DIRECTIONAL 0 #define LIGHT_POINT 1 #define LIGHT_SPOT 2 // Structures struct Material { vec3 albedo; float metallic; vec3 emission; float roughness; int type; float ior; vec2 padding; }; struct Light { vec3 position; int type; vec3 direction; float intensity; vec3 color; float range; vec2 spot_angles; vec2 padding; }; struct Ray { vec3 origin; vec3 direction; }; struct HitInfo { bool hit; float t; vec3 position; vec3 normal; vec2 texcoord; uint material_id; }; struct ScatterResult { bool scattered; vec3 attenuation; Ray scattered_ray; float pdf; }; layout(local_size_x = 16, local_size_y = 16) in; // G-Buffer inputs layout(binding = 0, rgba32f) uniform readonly image2D g_position; layout(binding = 1, rgba32f) uniform readonly image2D g_normal; layout(binding = 2, rgba8) uniform readonly image2D g_albedo; // Output layout(binding = 3, rgba32f) uniform image2D output_image; layout(binding = 4, rgba32f) uniform image2D accumulation_image; // Scene data layout(std430, binding = 0) readonly buffer MaterialBuffer { Material materials[]; }; layout(std430, binding = 1) readonly buffer LightBuffer { Light lights[]; }; // Uniforms uniform uint u_frame_count; uniform uint u_samples_per_pixel; uniform uint u_max_depth; uniform uint u_light_count; uniform vec3 u_camera_position; uniform mat4 u_inv_view_projection; uniform bool u_enable_accumulation; uniform bool u_use_bvh; // ============================================================================ // Utility Functions // ============================================================================ /** * @brief Saturate float value to [0, 1] */ float saturate(float x) { return clamp(x, 0.0, 1.0); } /** * @brief Saturate vec3 value to [0, 1] */ vec3 saturate(vec3 x) { return clamp(x, vec3(0.0), vec3(1.0)); } /** * @brief Check if vector is near zero */ bool near_zero(vec3 v) { return (abs(v.x) < EPSILON) && (abs(v.y) < EPSILON) && (abs(v.z) < EPSILON); } /** * @brief Reflect vector around normal */ vec3 reflect_vector(vec3 v, vec3 n) { return v - 2.0 * dot(v, n) * n; } /** * @brief Refract vector through surface */ vec3 refract_vector(vec3 uv, vec3 n, float etai_over_etat) { float cos_theta = min(dot(-uv, n), 1.0); vec3 r_out_perp = etai_over_etat * (uv + cos_theta * n); vec3 r_out_parallel = -sqrt(abs(1.0 - dot(r_out_perp, r_out_perp))) * n; return r_out_perp + r_out_parallel; } // ============================================================================ // Random Number Generation // ============================================================================ /** * @brief PCG hash function for random number generation */ uint pcg_hash(uint seed) { uint state = seed * 747796405u + 2891336453u; uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; return (word >> 22u) ^ word; } /** * @brief Generate random float in [0, 1) */ float random_float(inout uint seed) { seed = pcg_hash(seed); return float(seed) / 4294967296.0; } /** * @brief Generate random vec2 */ vec2 random_vec2(inout uint seed) { return vec2(random_float(seed), random_float(seed)); } /** * @brief Generate random vec3 */ vec3 random_vec3(inout uint seed) { return vec3(random_float(seed), random_float(seed), random_float(seed)); } /** * @brief Generate random vector in unit sphere */ vec3 random_in_unit_sphere(inout uint seed) { while (true) { vec3 p = 2.0 * random_vec3(seed) - vec3(1.0); if (dot(p, p) < 1.0) return p; } } /** * @brief Generate random unit vector */ vec3 random_unit_vector(inout uint seed) { return normalize(random_in_unit_sphere(seed)); } // ============================================================================ // Sampling Functions // ============================================================================ /** * @brief Cosine-weighted hemisphere sampling * @return Sampled direction in local space (z-up) */ vec3 cosine_weighted_hemisphere(inout uint seed) { vec2 r = random_vec2(seed); float phi = 2.0 * PI * r.x; float cos_theta = sqrt(r.y); float sin_theta = sqrt(1.0 - r.y); return vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); } /** * @brief Build orthonormal basis from normal */ void build_onb(vec3 normal, out vec3 tangent, out vec3 bitangent) { vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); tangent = normalize(cross(up, normal)); bitangent = cross(normal, tangent); } /** * @brief Transform direction from local to world space */ vec3 local_to_world(vec3 local_dir, vec3 normal) { vec3 tangent, bitangent; build_onb(normal, tangent, bitangent); return tangent * local_dir.x + bitangent * local_dir.y + normal * local_dir.z; } /** * @brief Sample GGX distribution for microfacet normal */ vec3 sample_ggx(vec3 normal, float roughness, inout uint seed) { vec2 r = random_vec2(seed); float a = roughness * roughness; float a2 = a * a; float phi = 2.0 * PI * r.x; float cos_theta = sqrt((1.0 - r.y) / (1.0 + (a2 - 1.0) * r.y)); float sin_theta = sqrt(1.0 - cos_theta * cos_theta); vec3 local_h = vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); return local_to_world(local_h, normal); } // ============================================================================ // BRDF Functions // ============================================================================ /** * @brief Schlick's approximation for Fresnel reflectance */ vec3 fresnel_schlick(float cos_theta, vec3 f0) { return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0); } /** * @brief Fresnel reflectance for dielectrics */ float fresnel_dielectric(float cos_theta, float ior) { float r0 = (1.0 - ior) / (1.0 + ior); r0 = r0 * r0; return r0 + (1.0 - r0) * pow(1.0 - cos_theta, 5.0); } /** * @brief GGX normal distribution function */ float distribution_ggx(vec3 N, vec3 H, float roughness) { float a = roughness * roughness; float a2 = a * a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH * NdotH; float nom = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return nom / max(denom, EPSILON); } /** * @brief Smith's geometry function for GGX */ float geometry_smith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float r = roughness + 1.0; float k = (r * r) / 8.0; float ggx1 = NdotV / (NdotV * (1.0 - k) + k); float ggx2 = NdotL / (NdotL * (1.0 - k) + k); return ggx1 * ggx2; } // ============================================================================ // Material Scattering // ============================================================================ /** * @brief Scatter ray for diffuse material */ ScatterResult scatter_diffuse(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult result; result.scattered = true; result.attenuation = mat.albedo; // Cosine-weighted hemisphere sampling vec3 local_dir = cosine_weighted_hemisphere(seed); vec3 scatter_direction = local_to_world(local_dir, hit.normal); // Prevent degenerate scatter direction if (near_zero(scatter_direction)) { scatter_direction = hit.normal; } result.scattered_ray.origin = hit.position + hit.normal * EPSILON; result.scattered_ray.direction = normalize(scatter_direction); result.pdf = max(dot(hit.normal, result.scattered_ray.direction), 0.0) * INV_PI; return result; } /** * @brief Scatter ray for metal material */ ScatterResult scatter_metal(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult result; vec3 reflected = reflect_vector(normalize(ray_in.direction), hit.normal); // Add roughness perturbation vec3 fuzz = mat.roughness * random_in_unit_sphere(seed); vec3 scatter_direction = reflected + fuzz; result.scattered = dot(scatter_direction, hit.normal) > 0.0; if (result.scattered) { result.scattered_ray.origin = hit.position + hit.normal * EPSILON; result.scattered_ray.direction = normalize(scatter_direction); vec3 V = -normalize(ray_in.direction); vec3 H = normalize(V + result.scattered_ray.direction); vec3 F0 = mat.albedo; result.attenuation = fresnel_schlick(max(dot(H, V), 0.0), F0); result.pdf = 1.0; // Delta distribution approximation } else { result.attenuation = vec3(0.0); result.pdf = 0.0; } return result; } /** * @brief Scatter ray for dielectric material (glass) */ ScatterResult scatter_dielectric(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult result; result.scattered = true; result.attenuation = vec3(1.0); float refraction_ratio = dot(ray_in.direction, hit.normal) < 0.0 ? (1.0 / mat.ior) : mat.ior; vec3 unit_direction = normalize(ray_in.direction); float cos_theta = min(dot(-unit_direction, hit.normal), 1.0); float sin_theta = sqrt(1.0 - cos_theta * cos_theta); bool cannot_refract = refraction_ratio * sin_theta > 1.0; float reflectance = fresnel_dielectric(cos_theta, refraction_ratio); vec3 direction; if (cannot_refract || reflectance > random_float(seed)) { direction = reflect_vector(unit_direction, hit.normal); } else { direction = refract_vector(unit_direction, hit.normal, refraction_ratio); } result.scattered_ray.origin = hit.position + direction * EPSILON; result.scattered_ray.direction = normalize(direction); result.pdf = 1.0; // Delta distribution return result; } /** * @brief Scatter ray based on material type */ ScatterResult scatter_ray(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { if (mat.type == MATERIAL_DIFFUSE) { return scatter_diffuse(ray_in, hit, mat, seed); } else if (mat.type == MATERIAL_METAL) { return scatter_metal(ray_in, hit, mat, seed); } else if (mat.type == MATERIAL_DIELECTRIC) { return scatter_dielectric(ray_in, hit, mat, seed); } else { // Emissive material doesn't scatter ScatterResult result; result.scattered = false; result.attenuation = vec3(0.0); result.pdf = 0.0; return result; } } // ============================================================================ // Scene Intersection (G-Buffer based) // ============================================================================ /** * @brief Trace ray against G-Buffer (single bounce only) * @note This is a simplified version - full path tracing needs scene geometry */ HitInfo trace_ray_gbuffer(Ray ray, ivec2 pixel_coords) { HitInfo hit; // Read from G-Buffer at current pixel vec4 position_data = imageLoad(g_position, pixel_coords); vec4 normal_data = imageLoad(g_normal, pixel_coords); vec4 albedo_data = imageLoad(g_albedo, pixel_coords); if (position_data.w > 0.5) { hit.hit = true; hit.position = position_data.xyz; hit.normal = normalize(normal_data.xyz); hit.material_id = uint(albedo_data.a * 255.0 + 0.5); hit.t = length(hit.position - ray.origin); } else { hit.hit = false; hit.t = MAX_FLOAT; } return hit; } // ============================================================================ // Path Tracing Core // ============================================================================ /** * @brief Sample direct lighting from light sources */ vec3 sample_direct_lighting(vec3 position, vec3 normal, Material mat, inout uint seed) { if (u_light_count == 0u) return vec3(0.0); vec3 direct_light = vec3(0.0); // Sample one random light (could be improved with MIS) uint light_idx = uint(random_float(seed) * float(u_light_count)) % u_light_count; Light light = lights[light_idx]; vec3 light_dir; float light_distance; float pdf_light = 1.0 / float(u_light_count); if (light.type == LIGHT_POINT) { vec3 to_light = light.position - position; light_distance = length(to_light); light_dir = to_light / light_distance; if (light_distance > light.range) return vec3(0.0); float NdotL = max(dot(normal, light_dir), 0.0); if (NdotL > 0.0) { float attenuation = 1.0 / max(light_distance * light_distance, 0.01); vec3 radiance = light.color * light.intensity * attenuation; // Simple BRDF evaluation (diffuse) direct_light = mat.albedo * INV_PI * radiance * NdotL / pdf_light; } } else if (light.type == LIGHT_DIRECTIONAL) { light_dir = normalize(-light.direction); float NdotL = max(dot(normal, light_dir), 0.0); if (NdotL > 0.0) { vec3 radiance = light.color * light.intensity; direct_light = mat.albedo * INV_PI * radiance * NdotL / pdf_light; } } return direct_light; } /** * @brief Trace path and accumulate radiance */ vec3 trace_path(Ray initial_ray, ivec2 pixel_coords, inout uint seed) { vec3 radiance = vec3(0.0); vec3 throughput = vec3(1.0); Ray current_ray = initial_ray; uint mat_count = uint(materials.length()); for (uint depth = 0u; depth < u_max_depth; depth++) { // Trace ray (only first bounce uses G-Buffer) HitInfo hit; if (depth == 0u) { hit = trace_ray_gbuffer(current_ray, pixel_coords); } else { // For subsequent bounces, we can't trace without full scene geometry // This is a limitation of G-Buffer based approach // In a full path tracer, you'd trace against the actual scene here hit.hit = false; } if (!hit.hit) { // Hit sky/background vec3 sky_color = vec3(0.1, 0.1, 0.15); radiance += throughput * sky_color; break; } // Get material Material mat; if (hit.material_id < mat_count) { mat = materials[hit.material_id]; } else { // Fallback material mat.albedo = vec3(0.5); mat.metallic = 0.0; mat.roughness = 0.5; mat.emission = vec3(0.0); mat.type = MATERIAL_DIFFUSE; mat.ior = 1.5; } // Add emission radiance += throughput * mat.emission; // Sample direct lighting (only for diffuse surfaces) if (mat.type == MATERIAL_DIFFUSE && depth == 0u) { radiance += throughput * sample_direct_lighting(hit.position, hit.normal, mat, seed); } // Scatter ray ScatterResult scatter = scatter_ray(current_ray, hit, mat, seed); if (!scatter.scattered || scatter.pdf < EPSILON) { break; } // Update throughput throughput *= scatter.attenuation; // Russian roulette path termination if (depth > 3u) { float rr_probability = max(throughput.r, max(throughput.g, throughput.b)); if (rr_probability < RR_THRESHOLD || random_float(seed) > rr_probability) { break; } throughput /= rr_probability; } // Continue with scattered ray current_ray = scatter.scattered_ray; // Safety check for throughput if (all(lessThan(throughput, vec3(EPSILON)))) { break; } } return radiance; } /** * @brief Enhanced direct lighting with G-Buffer */ vec3 render_direct_lighting(ivec2 pixel_coords, inout uint seed) { // Read G-Buffer vec4 position_data = imageLoad(g_position, pixel_coords); vec4 normal_data = imageLoad(g_normal, pixel_coords); vec4 albedo_data = imageLoad(g_albedo, pixel_coords); if (position_data.w < 0.5) { return vec3(0.1, 0.1, 0.15); // Sky } vec3 position = position_data.xyz; vec3 normal = normalize(normal_data.xyz); uint material_id = uint(albedo_data.a * 255.0 + 0.5); // Get material Material mat; uint mat_count = uint(materials.length()); if (material_id < mat_count) { mat = materials[material_id]; } else { // Fallback: use G-Buffer albedo mat.albedo = albedo_data.rgb; mat.metallic = 0.0; mat.roughness = 0.5; mat.emission = vec3(0.0); mat.type = MATERIAL_DIFFUSE; mat.ior = 1.5; } vec3 color = vec3(0.0); // Emission color += mat.emission; // View direction vec3 view_dir = normalize(u_camera_position - position); // Direct lighting from all lights for (uint i = 0u; i < u_light_count; i++) { Light light = lights[i]; vec3 light_dir; float light_distance; float attenuation = 1.0; if (light.type == LIGHT_POINT) { vec3 to_light = light.position - position; light_distance = length(to_light); light_dir = to_light / light_distance; attenuation = 1.0 / max(light_distance * light_distance, 0.01); if (light_distance > light.range) continue; } else if (light.type == LIGHT_DIRECTIONAL) { light_dir = normalize(-light.direction); light_distance = MAX_FLOAT; } else { continue; } float NdotL = max(dot(normal, light_dir), 0.0); if (NdotL > 0.0) { vec3 H = normalize(view_dir + light_dir); float NdotV = max(dot(normal, view_dir), 0.0); float NdotH = max(dot(normal, H), 0.0); float HdotV = max(dot(H, view_dir), 0.0); // Cook-Torrance BRDF vec3 F0 = mix(vec3(0.04), mat.albedo, mat.metallic); vec3 F = fresnel_schlick(HdotV, F0); float D = distribution_ggx(normal, H, max(mat.roughness, 0.04)); float G = geometry_smith(normal, view_dir, light_dir, mat.roughness); vec3 numerator = D * G * F; float denominator = 4.0 * NdotV * NdotL + EPSILON; vec3 specular = numerator / denominator; vec3 kS = F; vec3 kD = (vec3(1.0) - kS) * (1.0 - mat.metallic); vec3 radiance = light.color * light.intensity * attenuation; color += (kD * mat.albedo * INV_PI + specular) * radiance * NdotL; } } // Ambient occlusion approximation color += mat.albedo * 0.03; return color; } void main() { ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); ivec2 image_size = imageSize(output_image); if (pixel_coords.x >= image_size.x || pixel_coords.y >= image_size.y) { return; } // Initialize random seed uint base_seed = uint(pixel_coords.x) + uint(pixel_coords.y) * uint(image_size.x); uint seed = base_seed + u_frame_count * 719393u; vec3 color = render_direct_lighting(pixel_coords, seed); // Clamp color = clamp(color, vec3(0.0), vec3(10.0)); // Accumulation if (u_enable_accumulation && u_frame_count > 0u) { vec3 accumulated = imageLoad(accumulation_image, pixel_coords).rgb; float weight = 1.0 / float(u_frame_count + 1u); color = mix(accumulated, color, weight); } imageStore(accumulation_image, pixel_coords, vec4(color, 1.0)); imageStore(output_image, pixel_coords, vec4(color, 1.0)); }