This commit is contained in:
Jérôme Bousquié
2026-09-25 14:40:47 +02:00
parent 8ece89ccba
commit 54a482e354
12 changed files with 866 additions and 370 deletions
+166
View File
@@ -100,6 +100,7 @@ struct FrameUniforms {
struct ObjectUniform {
model: mat4x4<f32>, // 64 bytes (offset 0)
emissive: vec4<f32>, // 16 bytes (offset 64): rgb = color, a = intensity (can be > 1.0 in HDR)
pbr: vec4<f32>, // 16 bytes (offset 80): .x=metallic .y=roughness (Étape 27)
};
@group(0) @binding(0) var<uniform> frame: FrameUniforms;
@@ -108,6 +109,9 @@ struct ObjectUniform {
// texture lie le placeholder blanc 1×1 (D2), d'où l'échantillonnage inconditionnel.
@group(2) @binding(0) var texture_sampler: sampler;
@group(2) @binding(1) var diffuse_texture: texture_2d<f32>;
// Étape 27 : normal map (binding 2) + son sampler (binding 3). Placeholder (128,128,255) si absent.
@group(2) @binding(2) var normal_texture: texture_2d<f32>;
@group(2) @binding(3) var normal_sampler: sampler;
// Étape 14 (DRAFT D1/D5) : groupe ombre — comparaison sampler (0) + carte de profondeur (1).
// Toujours lié (layout unifié) ; inutilisé tant que `options.y == 0` (ombres désactivées).
@group(3) @binding(0) var shadow_sampler: sampler_comparison;
@@ -119,6 +123,7 @@ struct VertexOutput {
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
@location(3) color: vec4<f32>,
@location(4) tangent: vec3<f32>, // Étape 27 : tangente pour normal mapping
};
@vertex
@@ -139,6 +144,15 @@ fn vs_main(input: VertexInput) -> VertexOutput {
out.normal = normal_matrix * input.normal;
out.uv = input.uv;
out.color = input.color;
// Étape 27 : tangente approximée par cross(normal, référence) — évite un attribut tangent.
// La référence est choisie pour éviter la dégénérescence (normal parallèle à l'axe Y).
let ref_dir = select(
vec3<f32>(0.0, 1.0, 0.0),
vec3<f32>(1.0, 0.0, 0.0),
abs(input.normal.y) > 0.99,
);
let tangent_local = normalize(cross(ref_dir, input.normal));
out.tangent = normal_matrix * tangent_local;
return out;
}
@@ -291,3 +305,155 @@ fn compute_shadow(world_pos: vec3<f32>, normal: vec3<f32>) -> f32 {
}
return lit_count / 9.0;
}
// ============================================================================
// Étape 27 : PBR Cook-Torrance (GGX + Smith + Schlick) + IBL hémisphère + normal mapping
// ============================================================================
const PI: f32 = 3.14159265;
// GGX/Trowbridge-Reitz distribution : contrôle la largeur du lobe spéculaire.
fn distribution_ggx(ndh: f32, roughness: f32) -> f32 {
let a = roughness * roughness;
let a2 = a * a;
let d = ndh * ndh * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
// Smith visibility (GGX correlated) : occlusion microsurface.
fn geometry_smith(ndh: f32, ndv: f32, ndl: f32, roughness: f32) -> f32 {
let a2 = roughness * roughness;
// Heuristic : approxime D * V / 4 (voir "A Practical Improvement to the Direct
// Analytic Approximation of the Smith Microsurface Model").
let gv = ndl / (ndv * (1.0 - a2) + a2);
let gl = ndv * (ndl * (1.0 - a2) + a2);
return 0.5 * min(gv, gl);
}
// Fresnel-Schlick : interpolation entre F0 et 1 selon l'angle de vue.
fn fresnel_schlick(hv: f32, f0: vec3<f32>) -> vec3<f32> {
return f0 + (vec3<f32>(1.0) - f0) * pow(1.0 - hv, 5.0);
}
// BRDF PBR complet : diffuse (Lambert × (1-metallic) × (1-F)) + spéculaire (D×G×F).
fn brdf_pbr(n: vec3<f32>, v: vec3<f32>, l: vec3<f32>,
base: vec3<f32>, metallic: f32, roughness: f32) -> vec3<f32> {
let h = normalize(v + l);
let f0 = mix(vec3<f32>(0.04), base, metallic);
let ndl = max(dot(n, l), 0.0);
let ndv = max(dot(n, v), 0.0);
let ndh = max(dot(n, h), 0.0);
let hv = max(dot(h, v), 0.0);
let d = distribution_ggx(ndh, roughness);
let g = geometry_smith(ndh, ndv, ndl, roughness);
let f = fresnel_schlick(hv, f0);
// Diffuse : Lambert × (1 - F) × (1 - metallic) — énergie conservée.
let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
let diffuse = kd * base / PI;
// Speculaire : D × G × F / (4 × N·V × N·L)
let denom = 4.0 * ndv * ndl + 1e-4;
let specular = d * g * f / denom;
return (diffuse + specular) * ndl;
}
// IBL hémisphérique analytique : sky/ground mix + spéculaire approximé par roughness.
fn compute_ibl(n: vec3<f32>, base: vec3<f32>, metallic: f32, roughness: f32) -> vec3<f32> {
let ambient = frame.ambient.rgb;
let sky = ambient;
let ground = ambient * 0.3;
let ibl_diffuse = mix(ground, sky, n.y * 0.5 + 0.5);
// Diffuse IBL : Lambert × (1 - metallic) × IBL color
let f0 = mix(vec3<f32>(0.04), base, metallic);
let f = fresnel_schlick(0.0, f0);
let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
let diffuse = kd * base * ibl_diffuse / PI;
// Speculaire IBL : approximation — plus la roughness est faible, plus le spéculaire est "vif".
let spec_ibl = mix(ibl_diffuse, vec3<f32>(1.0), (1.0 - roughness) * 0.5);
let specular = f * spec_ibl * (0.1 + 0.4 * (1.0 - roughness));
return diffuse + specular;
}
// Normal mapping : construit la normale perturbée à partir du TBN + normal map.
// La tangente vient du vertex shader (cross produit avec une référence anti-dégénérescence).
fn compute_pbr_normal(in: VertexOutput) -> vec3<f32> {
let n = normalize(in.normal);
let t = normalize(in.tangent);
let b = normalize(cross(n, t));
let tbn = mat3x3<f32>(t, b, n);
// Échantillonner la normal map (placeholder 128,128,255 → nmap = (0,0,1) → aucun effet).
let nmap = textureSample(normal_texture, normal_sampler, in.uv).rgb * 2.0 - 1.0;
return normalize(tbn * nmap);
}
// Fragment PBR complet : IBL + lumières (BRDF Cook-Torrance) + emissive + fog.
@fragment
fn fs_pbr(in: VertexOutput) -> @location(0) vec4<f32> {
let texel = textureSample(diffuse_texture, texture_sampler, in.uv);
let base = texel.rgb * in.color.rgb;
// Unlit mode (identique à fs_main).
if (frame.options.x != 0u) {
let emissive_contrib = base * object.emissive.rgb * object.emissive.a;
let final_rgb = base + emissive_contrib;
return vec4<f32>(apply_fog(final_rgb, in.world_pos), in.color.a);
}
let metallic = object.pbr.x;
let roughness = clamp(object.pbr.y, 0.045, 1.0);
// Normal mapping (derivative tangent + normal map texture).
let n = compute_pbr_normal(in);
let v = normalize(frame.cam_pos.xyz - in.world_pos);
// IBL (hémisphère analytique).
var color = compute_ibl(n, base, metallic, roughness);
// Lumières directionnelles.
for (var i = 0u; i < frame.num_directional; i++) {
let l = normalize(frame.lights[i].position_dir.xyz);
let light_color = frame.lights[i].color.rgb * frame.lights[i].color.a;
let shadow = compute_shadow(in.world_pos, n);
color += brdf_pbr(n, v, l, base, metallic, roughness) * light_color * shadow;
}
// Lumières ponctuelles.
for (var i = frame.num_directional; i < frame.num_directional + frame.num_point; i++) {
let to_light = frame.lights[i].position_dir.xyz - in.world_pos;
let dist = length(to_light);
let l = to_light / max(dist, 1e-4);
let falloff = clamp(1.0 - dist / max(frame.lights[i].radius.x, 1e-4), 0.0, 1.0);
let light_color = frame.lights[i].color.rgb * frame.lights[i].color.a * falloff;
let shadow = compute_shadow(in.world_pos, n);
color += brdf_pbr(n, v, l, base, metallic, roughness) * light_color * shadow;
}
// Lumières spot.
let spot_base = frame.num_directional + frame.num_point;
for (var i = spot_base; i < spot_base + frame.num_spot; i++) {
let to_light = frame.lights[i].position_dir.xyz - in.world_pos;
let dist = length(to_light);
let l = to_light / max(dist, 1e-4);
let to_point = -l;
let cone = dot(to_point, normalize(frame.lights[i].dir_angle.xyz));
let cos_inner = frame.lights[i].dir_angle.w;
let cos_outer = cos_inner - 0.1;
let spot_factor = clamp((cone - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0);
let falloff = clamp(1.0 - dist / max(frame.lights[i].radius.x, 1e-4), 0.0, 1.0);
let light_color = frame.lights[i].color.rgb * frame.lights[i].color.a * falloff * spot_factor;
let shadow = compute_shadow(in.world_pos, n);
color += brdf_pbr(n, v, l, base, metallic, roughness) * light_color * shadow;
}
// Emissive.
let emissive_contrib = base * object.emissive.rgb * object.emissive.a;
let final_rgb = color + emissive_contrib;
return vec4<f32>(apply_fog(final_rgb, in.world_pos), in.color.a);
}