#import nightshade_renderer::material_data::{Material, TextureTransform}
#import nightshade_renderer::material_sampling::{UvGrad, sample_srgb_layer, sample_linear_layer}
#import nightshade_renderer::pbr_brdf::{DistributionGGX, V_SmithGGXCorrelated, fresnelSchlick, fresnelSchlickRoughness}
#import nightshade_renderer::meshlet_data::{Meshlet, meshlet_payload_cluster, meshlet_payload_triangle}
#import nightshade_renderer::meshlet_streams::{raster_clusters, meshlets, meshlet_instances, get_meshlet_vertex_id, get_meshlet_vertex_position, get_meshlet_vertex_normal, get_meshlet_vertex_uv}
const VISIBILITY_BUFFER_EMPTY: u32 = 0xFFFFFFFFu;
const NO_TEXTURE_LAYER: u32 = 0xFFFFFFFFu;
const NORMAL_MAP_FLIP_Y: u32 = 1u;
const NORMAL_MAP_TWO_COMPONENT: u32 = 2u;
struct MeshletResolveView {
clip_from_world: mat4x4<f32>,
camera_position: vec4<f32>,
sun_direction: vec4<f32>,
sun_color: vec4<f32>,
screen_size: vec4<f32>,
visualization: vec4<f32>,
fog_color: vec4<f32>,
fog_params: vec4<f32>,
}
/// Mixes a shaded color towards the fog, by the same rules the mesh pass
/// applies, so meshlet geometry and everything else recede together instead of
/// one staying sharp against a fogged sky.
///
/// Distance is measured to the camera rather than along the view. The clip
/// position's w would give the latter for a conventional projection, but this
/// renderer's is reversed, and a fog that reads its depth with the wrong sign
/// silently fogs what is near and spares what is far.
fn apply_fog(color: vec3<f32>, view_depth: f32) -> vec3<f32> {
let mode = view.fog_color.w;
if mode < 0.5 {
return color;
}
let start = view.fog_params.x;
let end = view.fog_params.y;
var fog_factor: f32;
if mode < 1.5 {
fog_factor = clamp((view_depth - start) / (end - start), 0.0, 1.0);
} else {
let distance = max(view_depth - start, 0.0);
let scaled = distance * 3.0 / max(end - start, 0.001);
if mode < 2.5 {
fog_factor = 1.0 - exp(-scaled);
} else {
fog_factor = 1.0 - exp(-scaled * scaled);
}
}
return mix(color, view.fog_color.rgb, fog_factor);
}
#ifdef MESHLET_ATOMIC_VISIBILITY
@group(0) @binding(0) var visibility_buffer: texture_storage_2d<r64uint, read>;
#else
@group(0) @binding(0) var visibility_buffer: texture_2d<u32>;
#endif
@group(0) @binding(1) var<uniform> view: MeshletResolveView;
@group(0) @binding(2) var<storage, read> materials: array<Material>;
/// A saturated, well-spread color per meshlet. The golden-ratio stride keeps
/// neighbouring meshlets far apart in hue, so the decomposition reads as
/// distinct patches rather than a gradient.
///
/// Keyed on the meshlet's own index rather than its slot in the raster list:
/// that list is rebuilt every frame from whatever the level of detail cut
/// selected, so a slot addresses a different meshlet as the camera moves and
/// would make the colors crawl. A meshlet's index is stable for the life of the
/// asset, so a surface only changes color when the cut genuinely swaps it for a
/// coarser or finer one.
fn cluster_color(meshlet_id: u32) -> vec3<f32> {
let hue = fract(f32(meshlet_id) * 0.618033988);
let rgb = clamp(
abs(fract(vec3<f32>(hue) + vec3<f32>(0.0, 0.6666667, 0.3333333)) * 6.0 - 3.0) - 1.0,
vec3<f32>(0.0),
vec3<f32>(1.0),
);
return rgb * rgb * (3.0 - 2.0 * rgb);
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
}
/// Where the atomic rasters write depth, they write no depth attachment: the
/// atomic settles meshlet against meshlet by itself, and nothing else. So this
/// pass is where meshlet geometry meets the rest of the scene. It hands the
/// depth it recovered from the visibility buffer to the fixed function test,
/// which rejects what ordinary geometry already covers and writes what survives,
/// so later passes occlude against meshlets correctly.
struct ResolveOutput {
@location(0) color: vec4<f32>,
#ifdef MESHLET_ATOMIC_VISIBILITY
@builtin(frag_depth) depth: f32,
#endif
}
fn resolved(color: vec3<f32>, depth: f32) -> ResolveOutput {
#ifdef MESHLET_ATOMIC_VISIBILITY
return ResolveOutput(vec4<f32>(color, 1.0), depth);
#else
return ResolveOutput(vec4<f32>(color, 1.0));
#endif
}
@vertex
fn vertex_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
let corner = vec2<f32>(
f32((vertex_index << 1u) & 2u),
f32(vertex_index & 2u),
);
return VertexOutput(vec4<f32>(corner * 2.0 - 1.0, 0.0, 1.0));
}
/// Perspective-correct barycentrics for `pixel_ndc` inside the triangle formed
/// by the three clip positions, recovered from the clip positions alone rather
/// than from interpolated attributes.
fn triangle_barycentrics(
clip_position_a: vec4<f32>,
clip_position_b: vec4<f32>,
clip_position_c: vec4<f32>,
pixel_ndc: vec2<f32>,
) -> vec3<f32> {
let inverse_w = 1.0 / vec3<f32>(clip_position_a.w, clip_position_b.w, clip_position_c.w);
let ndc_a = clip_position_a.xy * inverse_w.x;
let ndc_b = clip_position_b.xy * inverse_w.y;
let ndc_c = clip_position_c.xy * inverse_w.z;
let inverse_determinant = 1.0 / determinant(mat2x2<f32>(ndc_c - ndc_b, ndc_a - ndc_b));
let gradient_x = vec3<f32>(ndc_b.y - ndc_c.y, ndc_c.y - ndc_a.y, ndc_a.y - ndc_b.y)
* inverse_determinant * inverse_w;
let gradient_y = vec3<f32>(ndc_c.x - ndc_b.x, ndc_a.x - ndc_c.x, ndc_b.x - ndc_a.x)
* inverse_determinant * inverse_w;
let delta = pixel_ndc - ndc_a;
let interpolated_inverse_w = inverse_w.x
+ delta.x * dot(gradient_x, vec3<f32>(1.0))
+ delta.y * dot(gradient_y, vec3<f32>(1.0));
let interpolated_w = 1.0 / interpolated_inverse_w;
return vec3<f32>(
interpolated_w * (inverse_w.x + delta.x * gradient_x.x + delta.y * gradient_y.x),
interpolated_w * (delta.x * gradient_x.y + delta.y * gradient_y.y),
interpolated_w * (delta.x * gradient_x.z + delta.y * gradient_y.z),
);
}
fn transformed_uv(transform: TextureTransform, uv: vec2<f32>) -> vec2<f32> {
return vec2<f32>(
dot(transform.row_u.xy, uv) + transform.row_u.z,
dot(transform.row_v.xy, uv) + transform.row_v.z,
);
}
/// How far the uv moves per pixel, worked out from the triangle rather than
/// from screen derivatives.
///
/// A fullscreen resolve has no derivatives worth having: neighbouring pixels in
/// a quad may have landed on different triangles, or different meshlets
/// entirely, so `dpdx` across them is meaningless. The barycentrics one pixel
/// step away are recoverable from the same clip positions the triangle was
/// rasterized from, though, so the gradient is computed rather than sampled.
/// Without it every texture reads at mip zero and shimmers.
fn uv_gradients(
clip_a: vec4<f32>,
clip_b: vec4<f32>,
clip_c: vec4<f32>,
uv_a: vec2<f32>,
uv_b: vec2<f32>,
uv_c: vec2<f32>,
pixel_ndc: vec2<f32>,
) -> UvGrad {
let ndc_per_pixel = vec2<f32>(2.0 / view.screen_size.x, -2.0 / view.screen_size.y);
let here = triangle_barycentrics(clip_a, clip_b, clip_c, pixel_ndc);
let right = triangle_barycentrics(
clip_a,
clip_b,
clip_c,
pixel_ndc + vec2<f32>(ndc_per_pixel.x, 0.0),
);
let down = triangle_barycentrics(
clip_a,
clip_b,
clip_c,
pixel_ndc + vec2<f32>(0.0, ndc_per_pixel.y),
);
let uv_here = uv_a * here.x + uv_b * here.y + uv_c * here.z;
let uv_right = uv_a * right.x + uv_b * right.y + uv_c * right.z;
let uv_down = uv_a * down.x + uv_b * down.y + uv_c * down.z;
return UvGrad(uv_right - uv_here, uv_down - uv_here);
}
/// The triangle a pixel landed on, and where on it the pixel landed. Bundled
/// because wgsl will not take a storage pointer as an argument, so the values a
/// helper needs have to travel as plain data.
struct SurfaceTriangle {
position_a: vec3<f32>,
position_b: vec3<f32>,
position_c: vec3<f32>,
uv_a: vec2<f32>,
uv_b: vec2<f32>,
uv_c: vec2<f32>,
uv: vec2<f32>,
}
/// The normal map's parameters, unpacked from the material for the same reason.
struct NormalMapParams {
layer: u32,
transform: TextureTransform,
scale: f32,
flags: u32,
}
/// Perturbs the normal by the material's normal map, building the tangent basis
/// from the triangle's own positions and uvs. A meshlet vertex carries no
/// tangent, and one derived per triangle is exactly the basis the map was
/// authored against.
fn apply_normal_map(
params: NormalMapParams,
normal: vec3<f32>,
surface: SurfaceTriangle,
grad: UvGrad,
) -> vec3<f32> {
let position_a = surface.position_a;
let position_b = surface.position_b;
let position_c = surface.position_c;
let uv_a = surface.uv_a;
let uv_b = surface.uv_b;
let uv_c = surface.uv_c;
let uv = surface.uv;
let edge_1 = position_b - position_a;
let edge_2 = position_c - position_a;
let delta_uv_1 = uv_b - uv_a;
let delta_uv_2 = uv_c - uv_a;
let determinant = delta_uv_1.x * delta_uv_2.y - delta_uv_2.x * delta_uv_1.y;
if abs(determinant) < 1.0e-12 {
return normal;
}
let inverse_determinant = 1.0 / determinant;
var tangent = (edge_1 * delta_uv_2.y - edge_2 * delta_uv_1.y) * inverse_determinant;
tangent = normalize(tangent - normal * dot(normal, tangent));
if !all(tangent == tangent) {
return normal;
}
let bitangent = cross(normal, tangent);
let sampled = sample_linear_layer(params.layer, transformed_uv(params.transform, uv), grad);
var mapped = sampled.xyz * 2.0 - 1.0;
if (params.flags & NORMAL_MAP_TWO_COMPONENT) != 0u {
mapped = vec3<f32>(mapped.xy, sqrt(max(1.0 - dot(mapped.xy, mapped.xy), 0.0)));
}
if (params.flags & NORMAL_MAP_FLIP_Y) != 0u {
mapped.y = -mapped.y;
}
mapped = vec3<f32>(mapped.xy * params.scale, mapped.z);
return normalize(mat3x3<f32>(tangent, bitangent, normal) * mapped);
}
/// The sun through a standard microfacet lobe, plus a sky-ish ambient term.
/// Clustered point lights, shadows, and image based lighting are not wired into
/// this pass yet, so this is the sun and an approximation of everything else.
fn shade_surface(
base_color: vec3<f32>,
normal: vec3<f32>,
to_camera: vec3<f32>,
roughness: f32,
metallic: f32,
) -> vec3<f32> {
let to_light = normalize(view.sun_direction.xyz);
let half_vector = normalize(to_light + to_camera);
let n_dot_l = max(dot(normal, to_light), 0.0);
let n_dot_v = max(dot(normal, to_camera), 1.0e-4);
let fresnel_0 = mix(vec3<f32>(0.04), base_color, metallic);
let distribution = DistributionGGX(normal, half_vector, roughness);
let visibility = V_SmithGGXCorrelated(n_dot_v, n_dot_l, roughness);
let fresnel = fresnelSchlick(max(dot(half_vector, to_camera), 0.0), fresnel_0);
let specular = distribution * visibility * fresnel;
let diffuse = (vec3<f32>(1.0) - fresnel) * (1.0 - metallic) * base_color
/ 3.14159265;
let radiance = view.sun_color.rgb * view.sun_color.a;
let direct = (diffuse + specular) * radiance * n_dot_l;
let sky = mix(vec3<f32>(0.10, 0.11, 0.13), vec3<f32>(0.26, 0.30, 0.38), normal.y * 0.5 + 0.5);
let ambient_fresnel = fresnelSchlickRoughness(n_dot_v, fresnel_0, roughness);
let ambient = sky * base_color * (1.0 - metallic) + sky * ambient_fresnel;
return direct + ambient;
}
@fragment
fn fragment_main(vertex_output: VertexOutput) -> ResolveOutput {
let pixel = vec2<u32>(vertex_output.clip_position.xy);
#ifdef MESHLET_ATOMIC_VISIBILITY
// Zero is the cleared value and cannot collide with real geometry: both
// rasters refuse to write a depth of zero, which is the far plane, so
// anything written here has a non-zero high half.
let packed = textureLoad(visibility_buffer, vec2<i32>(pixel)).r;
if packed == u64(0u) {
discard;
}
let packed_ids = u32(packed & u64(0xFFFFFFFFu));
let fragment_depth = bitcast<f32>(u32(packed >> 32u));
#else
let packed_ids = textureLoad(visibility_buffer, pixel, 0).r;
if packed_ids == VISIBILITY_BUFFER_EMPTY {
discard;
}
let fragment_depth = 0.0;
#endif
let cluster_id = meshlet_payload_cluster(packed_ids);
let triangle_id = meshlet_payload_triangle(packed_ids);
let cluster = raster_clusters[cluster_id];
var meshlet = meshlets[cluster.offset];
let instance = meshlet_instances[cluster.instance_id];
let index_id = meshlet.start_index_id + triangle_id * 3u;
let vertex_id_a = get_meshlet_vertex_id(index_id);
let vertex_id_b = get_meshlet_vertex_id(index_id + 1u);
let vertex_id_c = get_meshlet_vertex_id(index_id + 2u);
let world_position_a = instance.world_from_local
* vec4<f32>(get_meshlet_vertex_position(&meshlet, vertex_id_a), 1.0);
let world_position_b = instance.world_from_local
* vec4<f32>(get_meshlet_vertex_position(&meshlet, vertex_id_b), 1.0);
let world_position_c = instance.world_from_local
* vec4<f32>(get_meshlet_vertex_position(&meshlet, vertex_id_c), 1.0);
let pixel_ndc = vec2<f32>(
vertex_output.clip_position.x / view.screen_size.x * 2.0 - 1.0,
1.0 - vertex_output.clip_position.y / view.screen_size.y * 2.0,
);
let barycentrics = triangle_barycentrics(
view.clip_from_world * world_position_a,
view.clip_from_world * world_position_b,
view.clip_from_world * world_position_c,
pixel_ndc,
);
let world_position = world_position_a.xyz * barycentrics.x
+ world_position_b.xyz * barycentrics.y
+ world_position_c.xyz * barycentrics.z;
let local_normal = get_meshlet_vertex_normal(&meshlet, vertex_id_a) * barycentrics.x
+ get_meshlet_vertex_normal(&meshlet, vertex_id_b) * barycentrics.y
+ get_meshlet_vertex_normal(&meshlet, vertex_id_c) * barycentrics.z;
let normal_from_local = mat3x3<f32>(
instance.world_from_local[0].xyz,
instance.world_from_local[1].xyz,
instance.world_from_local[2].xyz,
);
let world_normal = normalize(normal_from_local * local_normal);
let uv = get_meshlet_vertex_uv(&meshlet, vertex_id_a) * barycentrics.x
+ get_meshlet_vertex_uv(&meshlet, vertex_id_b) * barycentrics.y
+ get_meshlet_vertex_uv(&meshlet, vertex_id_c) * barycentrics.z;
let uv_a = get_meshlet_vertex_uv(&meshlet, vertex_id_a);
let uv_b = get_meshlet_vertex_uv(&meshlet, vertex_id_b);
let uv_c = get_meshlet_vertex_uv(&meshlet, vertex_id_c);
let to_camera = normalize(view.camera_position.xyz - world_position);
let facing_normal = select(-world_normal, world_normal, dot(world_normal, to_camera) >= 0.0);
let view_depth = (view.clip_from_world * vec4<f32>(world_position, 1.0)).w;
if view.visualization.x > 0.0 {
let tint = cluster_color(cluster.offset);
let lambert = max(dot(facing_normal, normalize(view.sun_direction.xyz)), 0.0);
return resolved(apply_fog(tint * (0.25 + lambert * 0.75), view_depth), fragment_depth);
}
let material = &materials[instance.material_id];
let grad = uv_gradients(
view.clip_from_world * world_position_a,
view.clip_from_world * world_position_b,
view.clip_from_world * world_position_c,
uv_a,
uv_b,
uv_c,
pixel_ndc,
);
var base_color = (*material).base_color;
if (*material).base_layer != NO_TEXTURE_LAYER {
base_color *= sample_srgb_layer(
(*material).base_layer,
transformed_uv((*material).base_transform, uv),
grad,
);
}
if (*material).unlit != 0u {
return resolved(apply_fog(base_color.rgb, view_depth), fragment_depth);
}
var normal = facing_normal;
if (*material).normal_layer != NO_TEXTURE_LAYER {
normal = apply_normal_map(
NormalMapParams(
(*material).normal_layer,
(*material).normal_transform,
(*material).normal_scale,
(*material).normal_map_flags,
),
normal,
SurfaceTriangle(
world_position_a.xyz,
world_position_b.xyz,
world_position_c.xyz,
uv_a,
uv_b,
uv_c,
uv,
),
grad,
);
}
var roughness = (*material).roughness;
var metallic = (*material).metallic;
if (*material).metallic_roughness_layer != NO_TEXTURE_LAYER {
let sampled = sample_linear_layer(
(*material).metallic_roughness_layer,
transformed_uv((*material).metallic_roughness_transform, uv),
grad,
);
roughness *= sampled.g;
metallic *= sampled.b;
}
roughness = clamp(roughness, 0.045, 1.0);
metallic = clamp(metallic, 0.0, 1.0);
var emissive = (*material).emissive_factor * (*material).emissive_strength;
if (*material).emissive_layer != NO_TEXTURE_LAYER {
emissive *= sample_srgb_layer(
(*material).emissive_layer,
transformed_uv((*material).emissive_transform, uv),
grad,
).rgb;
}
let shaded = shade_surface(base_color.rgb, normal, to_camera, roughness, metallic);
return resolved(apply_fog(shaded + emissive, view_depth), fragment_depth);
}