// Material handling and PBR scattering #ifndef MATERIAL_GLSL #define MATERIAL_GLSL // Helper function to sample texture from array by index vec4 sample_texture_array(int slot, int index, vec2 uv) { if (index <= 0) return vec4(1.0); if (slot == 0) return texture(u_texture_albedo_array, vec3(uv, float(index - 1))); if (slot == 1) return texture(u_texture_normal_array, vec3(uv, float(index - 1))); if (slot == 2) return texture(u_texture_metallic_array, vec3(uv, float(index - 1))); if (slot == 3) return texture(u_texture_roughness_array, vec3(uv, float(index - 1))); if (slot == 4) return texture(u_texture_ao_array, vec3(uv, float(index - 1))); if (slot == 5) return texture(u_texture_emission_array, vec3(uv, float(index - 1))); return vec4(1.0); } // Apply normal map in world space vec3 apply_normal_map(vec3 normal, vec2 texcoord, vec3 tangent, uint normal_handle) { if (normal_handle == 0 || !u_enable_textures) return normal; vec3 T = normalize(tangent - normal * dot(tangent, normal)); vec3 B = cross(normal, T); mat3 TBN = mat3(T, B, normal); vec3 map_n = sample_texture_array(1, int(normal_handle), texcoord).xyz * 2.0 - 1.0; return normalize(TBN * map_n); } // Apply material textures to get final PBR values void apply_material_textures(inout Material mat, inout vec3 normal, vec2 texcoord, vec3 tangent) { if (!u_enable_textures) return; if (mat.texture_handles[0] != 0) { mat.albedo = sample_texture_array(0, int(mat.texture_handles[0]), texcoord).rgb; } if (mat.texture_handles[1] != 0) { normal = apply_normal_map(normal, texcoord, tangent, mat.texture_handles[1]); } if (mat.texture_handles[2] != 0) { mat.metallic = sample_texture_array(2, int(mat.texture_handles[2]), texcoord).r; } if (mat.texture_handles[3] != 0) { mat.roughness = sample_texture_array(3, int(mat.texture_handles[3]), texcoord).r; } if (mat.texture_handles[4] != 0) { mat.ao = sample_texture_array(4, int(mat.texture_handles[4]), texcoord).r; } if (mat.texture_handles[5] != 0) { mat.emission = sample_texture_array(5, int(mat.texture_handles[5]), texcoord).rgb; } } // Fresnel functions vec3 fresnel_schlick(float cos_theta, vec3 f0) { return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0); } 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); } // GGX/Trowbridge-Reitz normal distribution function float distribution_ggx(float NdotH, float roughness) { float a = roughness * roughness; float a2 = a * a; float d = NdotH * NdotH * (a2 - 1.0) + 1.0; return a2 / (PI * d * d); } // Scatter functions ScatterResult scatter_diffuse(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult r; r.scattered = true; r.attenuation = mat.albedo; vec3 dir = hit.normal + random_unit_vector(seed); if (near_zero(dir)) dir = hit.normal; r.scattered_ray.origin = hit.position + hit.normal * EPSILON; r.scattered_ray.direction = normalize(dir); return r; } ScatterResult scatter_metal(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult r; vec3 V = normalize(-ray_in.direction); vec3 N = hit.normal; // Clamp roughness to avoid division by zero float roughness = max(mat.roughness, 0.04); // Sample microfacet normal using GGX importance sampling vec3 H = sample_ggx_half_vector(roughness, N, seed); // Reflect view direction around half vector vec3 L = reflect(-V, H); // Check if reflected direction is above surface float NdotL = dot(N, L); if (NdotL <= 0.0) { r.scattered = false; r.attenuation = vec3(0.0); return r; } float NdotV = max(dot(N, V), 0.001); float HdotV = max(dot(H, V), 0.001); // Fresnel term (using albedo as F0 for metals) vec3 F = fresnel_schlick(HdotV, mat.albedo); // With proper GGX importance sampling of H, the BRDF contribution // simplifies to just the Fresnel term. // The D and geometry terms are canceled by the PDF. r.attenuation = F; r.scattered = true; r.scattered_ray.origin = hit.position + N * EPSILON; r.scattered_ray.direction = normalize(L); return r; } ScatterResult scatter_dielectric(Ray ray_in, HitInfo hit, Material mat, inout uint seed) { ScatterResult r; r.scattered = true; r.attenuation = vec3(1.0); vec3 unit_dir = normalize(ray_in.direction); float cos_theta = dot(-unit_dir, hit.normal); float sin_theta = sqrt(max(0.0, 1.0 - cos_theta * cos_theta)); bool entering = cos_theta > 0.0; float eta = entering ? (1.0 / mat.ior) : mat.ior; vec3 normal = entering ? hit.normal : -hit.normal; float sin_theta_t = eta * sin_theta; bool total_internal_reflection = sin_theta_t >= 1.0; float f0 = pow((1.0 - mat.ior) / (1.0 + mat.ior), 2.0); float f = f0 + (1.0 - f0) * pow(1.0 - abs(cos_theta), 5.0); vec3 dir; if (total_internal_reflection || random_float(seed) < f) { dir = reflect_vector(unit_dir, normal); } else { dir = refract_vector(unit_dir, normal, eta); } r.scattered_ray.origin = hit.position + dir * EPSILON; r.scattered_ray.direction = normalize(dir); return r; } 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); if (mat.type == MATERIAL_METAL) return scatter_metal(ray_in, hit, mat, seed); if (mat.type == MATERIAL_DIELECTRIC) return scatter_dielectric(ray_in, hit, mat, seed); ScatterResult r; r.scattered = false; r.attenuation = vec3(0.0); return r; } // Fetch material with fallback Material fetch_material(uint material_id) { uint cnt = uint(materials.length()); if (material_id < cnt) return materials[material_id]; Material m; m.albedo = vec3(0.5); m.metallic = 0.0; m.emission = vec3(0.0); m.roughness = 0.5; m.type = MATERIAL_DIFFUSE; m.ior = 1.5; m.ao = 1.0; return m; } #endif // MATERIAL_GLSL