PBR
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user