notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

shader.wgsl (8900B)


      1 const PI: f32 = 3.14159265;
      2 const EPS: f32 = 1e-4;
      3 
      4 const MIN_ROUGHNESS: f32 = 0.04;
      5 const INV_GAMMA: f32 = 1.0 / 2.2;
      6 
      7 struct Globals {
      8     time: f32,
      9     _pad0: f32,
     10     resolution: vec2<f32>,
     11 
     12     cam_pos: vec3<f32>,
     13     _pad3: f32,
     14 
     15     light_dir: vec3<f32>,
     16     _pad1: f32,
     17 
     18     light_color: vec3<f32>,
     19     _pad2: f32,
     20 
     21     fill_light_dir: vec3<f32>,
     22     _pad4: f32,
     23 
     24     fill_light_color: vec3<f32>,
     25     _pad5: f32,
     26 
     27     view_proj: mat4x4<f32>,
     28     inv_view_proj: mat4x4<f32>,
     29     light_view_proj: mat4x4<f32>,
     30 };
     31 
     32 @group(0) @binding(0)
     33 var<uniform> globals: Globals;
     34 @group(0) @binding(1) var shadow_map: texture_depth_2d;
     35 @group(0) @binding(2) var shadow_sampler: sampler_comparison;
     36 
     37 struct Object {
     38   model: mat4x4<f32>,
     39   normal: mat4x4<f32>,
     40 };
     41 
     42 @group(1) @binding(0)
     43 var<uniform> object: Object;
     44 
     45 struct Material {
     46   base_color_factor: vec4<f32>,
     47   metallic_factor: f32,
     48   roughness_factor: f32,
     49   ao_strength: f32,
     50   _pad0: f32,
     51 };
     52 
     53 @group(2) @binding(0) var<uniform> material: Material;
     54 @group(2) @binding(1) var material_sampler: sampler;
     55 @group(2) @binding(2) var basecolor_tex: texture_2d<f32>;
     56 @group(2) @binding(3) var ao_mr_tex: texture_2d<f32>;
     57 @group(2) @binding(4) var normal_tex: texture_2d<f32>;
     58 
     59 // IBL
     60 @group(3) @binding(0) var irradiance_map: texture_cube<f32>;
     61 @group(3) @binding(1) var ibl_sampler: sampler;
     62 @group(3) @binding(2) var prefiltered_map: texture_cube<f32>;
     63 @group(3) @binding(3) var brdf_lut: texture_2d<f32>;
     64 
     65 struct VSIn {
     66   @location(0) pos: vec3<f32>,
     67   @location(1) normal: vec3<f32>,
     68   @location(2) uv:  vec2<f32>,
     69   @location(3) tangent: vec4<f32>,
     70 };
     71 
     72 struct VSOut {
     73   @builtin(position) clip: vec4<f32>,
     74   @location(0) world_pos: vec3<f32>,
     75   @location(1) world_normal: vec3<f32>,
     76   @location(2) uv: vec2<f32>,
     77   @location(3) world_tangent: vec4<f32>, // xyz + w
     78 };
     79 
     80 
     81 @vertex
     82 fn vs_main(v: VSIn) -> VSOut {
     83   var out: VSOut;
     84 
     85   // For now: identity model matrix. Next step is per-object transforms.
     86   let world4 = object.model * vec4<f32>(v.pos, 1.0);
     87   out.world_pos = world4.xyz;
     88 
     89   let n4 = object.normal * vec4<f32>(v.normal, 0.0);
     90   let t4 = object.model * vec4<f32>(v.tangent.xyz, 0.0);
     91 
     92   out.world_normal = normalize(n4.xyz);
     93   out.uv = v.uv;
     94   out.clip = globals.view_proj * world4;
     95   out.world_tangent = vec4<f32>(normalize(t4.xyz), v.tangent.w);
     96   return out;
     97 }
     98 
     99 fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
    100 fn saturate3(v: vec3<f32>) -> vec3<f32> { return clamp(v, vec3<f32>(0.0), vec3<f32>(1.0)); }
    101 
    102 fn safe_normalize(v: vec3<f32>) -> vec3<f32> {
    103   let l2 = dot(v, v);
    104   if (l2 <= EPS) { return vec3<f32>(0.0, 0.0, 1.0); }
    105   return v * inverseSqrt(l2);
    106 }
    107 
    108 // Fresnel (Schlick)
    109 fn fresnel_schlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
    110   let ct = saturate(cosTheta);
    111   let f = pow(1.0 - ct, 5.0);
    112   return F0 + (1.0 - F0) * f;
    113 }
    114 
    115 // Fresnel with roughness attenuation for IBL
    116 fn fresnel_schlick_roughness(cosTheta: f32, F0: vec3<f32>, roughness: f32) -> vec3<f32> {
    117   let ct = saturate(cosTheta);
    118   let f = pow(1.0 - ct, 5.0);
    119   return F0 + (max(vec3<f32>(1.0 - roughness), F0) - F0) * f;
    120 }
    121 
    122 // Specular IBL using split-sum approximation
    123 fn specular_ibl(N: vec3<f32>, V: vec3<f32>, F0: vec3<f32>, roughness: f32) -> vec3<f32> {
    124   let R = reflect(-V, N);
    125 
    126   // Sample pre-filtered environment at roughness-based mip level
    127   let MAX_REFLECTION_LOD = 4.0;  // mip_count - 1
    128   let prefiltered = textureSampleLevel(
    129     prefiltered_map,
    130     ibl_sampler,
    131     R,
    132     roughness * MAX_REFLECTION_LOD
    133   ).rgb;
    134 
    135   // Sample BRDF LUT
    136   let NdotV = saturate(dot(N, V));
    137   let brdf = textureSample(brdf_lut, ibl_sampler, vec2<f32>(NdotV, roughness)).rg;
    138 
    139   // Combine using split-sum: prefiltered * (F0 * scale + bias)
    140   return prefiltered * (F0 * brdf.x + brdf.y);
    141 }
    142 
    143 // GGX / Trowbridge-Reitz NDF
    144 fn ggx_ndf(NdotH: f32, alpha: f32) -> f32 {
    145   let a2 = alpha * alpha;
    146   let d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
    147   return a2 / (PI * d * d);
    148 }
    149 
    150 // Smith geometry with Schlick-GGX (UE4 k)
    151 fn smith_g_schlick_ggx(NdotV: f32, NdotL: f32, alpha: f32) -> f32 {
    152   let k = alpha + 1.0;
    153   let k2 = (k * k) / 8.0;
    154 
    155   let gv = NdotV / (NdotV * (1.0 - k2) + k2);
    156   let gl = NdotL / (NdotL * (1.0 - k2) + k2);
    157   return gv * gl;
    158 }
    159 
    160 fn diffuse_lambert(diffuseColor: vec3<f32>) -> vec3<f32> {
    161   return diffuseColor / PI;
    162 }
    163 
    164 // RG normal map decode (optionally flips Y for your convention)
    165 fn decode_normal_rg(tex: vec3<f32>) -> vec3<f32> {
    166   let x = tex.r * 2.0 - 1.0;
    167   var y = tex.g * 2.0 - 1.0;
    168   y = -y; // <- keep your current behavior here
    169 
    170   let z2 = max(1.0 - x*x - y*y, 0.0);
    171   let z = sqrt(z2);
    172   return safe_normalize(vec3<f32>(x, y, z));
    173 }
    174 
    175 fn calc_shadow(world_pos: vec3<f32>) -> f32 {
    176     let light_clip = globals.light_view_proj * vec4<f32>(world_pos, 1.0);
    177     let ndc = light_clip.xyz / light_clip.w;
    178 
    179     // Convert from NDC [-1,1] to UV [0,1], flip Y for texture coords
    180     let shadow_uv = vec2<f32>(ndc.x * 0.5 + 0.5, -ndc.y * 0.5 + 0.5);
    181 
    182     // Out-of-bounds = fully lit
    183     if shadow_uv.x < 0.0 || shadow_uv.x > 1.0 || shadow_uv.y < 0.0 || shadow_uv.y > 1.0 {
    184         return 1.0;
    185     }
    186 
    187     let ref_depth = ndc.z;
    188 
    189     // 3x3 PCF for soft shadow edges
    190     let texel_size = 1.0 / 2048.0;
    191     var shadow = 0.0;
    192     for (var y = -1i; y <= 1i; y++) {
    193         for (var x = -1i; x <= 1i; x++) {
    194             let offset = vec2<f32>(f32(x), f32(y)) * texel_size;
    195             shadow += textureSampleCompareLevel(
    196                 shadow_map,
    197                 shadow_sampler,
    198                 shadow_uv + offset,
    199                 ref_depth,
    200             );
    201         }
    202     }
    203     return shadow / 9.0;
    204 }
    205 
    206 fn build_tbn(Ng: vec3<f32>, world_tangent: vec4<f32>) -> mat3x3<f32> {
    207   var T = safe_normalize(world_tangent.xyz);
    208   T = safe_normalize(T - Ng * dot(Ng, T));
    209   let B = safe_normalize(cross(Ng, T)) * world_tangent.w;
    210   return mat3x3<f32>(T, B, Ng);
    211 }
    212 
    213 @fragment
    214 fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
    215   let Ng = safe_normalize(in.world_normal);
    216   let tbn = build_tbn(Ng, in.world_tangent);
    217 
    218   let n_ts = decode_normal_rg(textureSample(normal_tex, material_sampler, in.uv).xyz);
    219   let N = safe_normalize(tbn * n_ts);
    220 
    221   let V = safe_normalize(globals.cam_pos - in.world_pos);
    222   let L = safe_normalize(-globals.light_dir);
    223   let H = safe_normalize(V + L);
    224 
    225   let NdotL = saturate(dot(N, L));
    226   let NdotV = saturate(dot(N, V));
    227   let NdotH = saturate(dot(N, H));
    228   let VdotH = saturate(dot(V, H));
    229 
    230   let bc_tex = textureSample(basecolor_tex, material_sampler, in.uv);
    231   let ao_mr  = textureSample(ao_mr_tex, material_sampler, in.uv);
    232 
    233   // glTF metallicRoughnessTexture: G=roughness, B=metallic
    234   let baseColor = bc_tex.rgb * material.base_color_factor.rgb;
    235   let metallic  = saturate(ao_mr.b * material.metallic_factor);
    236   let rough_in  = ao_mr.g * material.roughness_factor;
    237 
    238   // AO: R channel; strength lerp from 1 -> ao
    239   let ao_tex = ao_mr.r;
    240   let ao = 1.0 + (ao_tex - 1.0) * saturate(material.ao_strength);
    241   //let ao = 1.0;
    242 
    243   let roughness = clamp(rough_in, MIN_ROUGHNESS, 1.0);
    244   let alpha = roughness * roughness;
    245 
    246   let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
    247   let diffuseColor = baseColor * (1.0 - metallic);
    248 
    249   let D = ggx_ndf(NdotH, alpha);
    250   let Gs = smith_g_schlick_ggx(NdotV, NdotL, alpha);
    251   let F = fresnel_schlick(VdotH, F0);
    252 
    253   let denom = max(4.0 * NdotV * NdotL, EPS);
    254   let spec = (D * Gs) * F / denom;
    255 
    256   let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
    257   let diff = kd * diffuse_lambert(diffuseColor);
    258 
    259   let shadow = calc_shadow(in.world_pos);
    260   let direct = (diff + spec) * (globals.light_color * NdotL) * shadow;
    261 
    262   // Fill light contribution
    263   let L2 = safe_normalize(-globals.fill_light_dir);
    264   let H2 = safe_normalize(V + L2);
    265   let NdotL2 = saturate(dot(N, L2));
    266   let NdotH2 = saturate(dot(N, H2));
    267   let VdotH2 = saturate(dot(V, H2));
    268 
    269   let D2 = ggx_ndf(NdotH2, alpha);
    270   let Gs2 = smith_g_schlick_ggx(NdotV, NdotL2, alpha);
    271   let F2 = fresnel_schlick(VdotH2, F0);
    272   let denom2 = max(4.0 * NdotV * NdotL2, EPS);
    273   let spec2 = (D2 * Gs2) * F2 / denom2;
    274   let kd2 = (vec3<f32>(1.0) - F2) * (1.0 - metallic);
    275   let diff2 = kd2 * diffuse_lambert(diffuseColor);
    276   let fill = (diff2 + spec2) * (globals.fill_light_color * NdotL2);
    277 
    278   // IBL ambient lighting with energy conservation
    279   let irradiance = textureSample(irradiance_map, ibl_sampler, N).rgb;
    280 
    281   // Specular IBL
    282   let specular_ambient = specular_ibl(N, V, F0, roughness);
    283 
    284   // Energy conservation: kS is already accounted for in specular_ibl via F0
    285   // kD ensures diffuse doesn't add energy where specular dominates
    286   let kS = fresnel_schlick_roughness(NdotV, F0, roughness);
    287   let kD = (1.0 - kS) * (1.0 - metallic);
    288 
    289   let diffuse_ambient = kD * diffuseColor * irradiance;
    290   let ambient = (diffuse_ambient + specular_ambient) * ao;
    291 
    292   var col = direct + fill + ambient;
    293 
    294   // simple tonemap + gamma
    295   col = col / (col + vec3<f32>(1.0));
    296   col = pow(col, vec3<f32>(INV_GAMMA));
    297 
    298   return vec4<f32>(saturate3(col), 1.0);
    299 }