struct MatrixUniform { view_proj: mat4x4f, viewport: vec4f, glyph: vec4f, color: vec4f, render_options: vec4f, image: vec4f, } @group(0) @binding(0) var u: MatrixUniform; @group(1) @binding(0) var base_color_texture: texture_2d; @group(1) @binding(1) var base_color_sampler: sampler; struct MaterialParams { base_color: vec4f, metallic_roughness: vec4f, emissive_alpha: vec4f, flags: vec4f, } @group(1) @binding(2) var material: MaterialParams; @group(1) @binding(3) var metallic_roughness_texture: texture_2d; @group(1) @binding(4) var metallic_roughness_sampler: sampler; @group(1) @binding(5) var normal_texture: texture_2d; @group(1) @binding(6) var normal_sampler: sampler; @group(1) @binding(7) var occlusion_texture: texture_2d; @group(1) @binding(8) var occlusion_sampler: sampler; @group(1) @binding(9) var emissive_texture: texture_2d; @group(1) @binding(10) var emissive_sampler: sampler; fn saturate(value: f32) -> f32 { return clamp(value, 0.0, 1.0); } fn linear_to_srgb(linear: vec3f) -> vec3f { let x = max(linear, vec3f(0.0)); let low = x * 12.92; let high = 1.055 * pow(x, vec3f(1.0 / 2.4)) - vec3f(0.055); return select(high, low, x <= vec3f(0.0031308)); } fn output_color(linear_rgb: vec3f, alpha: f32) -> vec4f { let clamped = clamp(linear_rgb, vec3f(0.0), vec3f(1.0)); if (u.color.w > 0.5) { return vec4f(clamped, alpha); } let encoded = linear_to_srgb(clamped); return vec4f(encoded, alpha); } fn range_stop_color(index: u32) -> vec3f { switch index { case 0u: { return vec3f(0.140, 0.690, 0.890); } case 1u: { return vec3f(0.250, 0.760, 0.380); } case 2u: { return vec3f(1.000, 0.670, 0.180); } default: { return vec3f(1.000, 0.255, 0.190); } } } fn sample_range_color(value: f32) -> vec3f { let t = saturate(value); if (t <= 0.33) { let local = smoothstep(0.0, 0.33, t); return mix(range_stop_color(0u), range_stop_color(1u), local); } if (t <= 0.66) { let local = smoothstep(0.33, 0.66, t); return mix(range_stop_color(1u), range_stop_color(2u), local); } let local = smoothstep(0.66, 1.0, t); return mix(range_stop_color(2u), range_stop_color(3u), local); } // background struct BackgroundVertexOutput { @builtin(position) clip_position: vec4f, } @vertex fn vs_background(@builtin(vertex_index) vertex_index: u32) -> BackgroundVertexOutput { let positions = array( vec2f(-1.0, -3.0), vec2f(3.0, 1.0), vec2f(-1.0, 1.0), ); var out: BackgroundVertexOutput; out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0); return out; } @fragment fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f { return output_color(vec3f(0.0, 0.0, 0.0), 1.0); } // hand image background struct HandImageVertexOutput { @builtin(position) clip_position: vec4f, @location(0) screen_uv: vec2f, } @vertex fn vs_hand_image(@builtin(vertex_index) vertex_index: u32) -> HandImageVertexOutput { let positions = array( vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0), vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0), ); let uvs = array( vec2f(0.0, 1.0), vec2f(1.0, 1.0), vec2f(0.0, 0.0), vec2f(0.0, 0.0), vec2f(1.0, 1.0), vec2f(1.0, 0.0), ); var out: HandImageVertexOutput; out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0); out.screen_uv = uvs[vertex_index]; return out; } @fragment fn fs_hand_image(in: HandImageVertexOutput) -> @location(0) vec4f { let viewport_aspect = u.viewport.x / max(u.viewport.y, 1.0); let image_aspect = u.image.x / max(u.image.y, 1.0); var uv = in.screen_uv; if (viewport_aspect > image_aspect) { let image_width = image_aspect / viewport_aspect; uv.x = (uv.x - (1.0 - image_width) * 0.5) / image_width; } else { let image_height = viewport_aspect / image_aspect; uv.y = (uv.y - (1.0 - image_height) * 0.5) / image_height; } if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { discard; } let color = textureSample(base_color_texture, base_color_sampler, uv); return output_color(color.rgb, color.a); } // glyph struct GlyphVertexInput { @location(0) local: vec2f, } struct GlyphInstanceInput { @location(1) world_position: vec4f, @location(2) style: vec4f, } struct GlyphVertexOutput { @builtin(position) clip_position: vec4f, @location(0) local: vec2f, @location(1) intensity: f32, @location(2) display_value: f32, } fn rect_alpha(point: vec2f, center: vec2f, half_size: vec2f) -> f32 { let delta = abs(point - center) - half_size; let outside = length(max(delta, vec2f(0.0, 0.0))); let inside = min(max(delta.x, delta.y), 0.0); let dist = outside + inside; return 1.0 - smoothstep(0.015, 0.045, dist); } fn digit_segment_on(digit: u32, segment: u32) -> bool { switch digit { case 0u: { return segment != 6u; } case 1u: { return segment == 1u || segment == 2u; } case 2u: { return segment == 0u || segment == 1u || segment == 6u || segment == 4u || segment == 3u; } case 3u: { return segment == 0u || segment == 1u || segment == 6u || segment == 2u || segment == 3u; } case 4u: { return segment == 5u || segment == 6u || segment == 1u || segment == 2u; } case 5u: { return segment == 0u || segment == 5u || segment == 6u || segment == 2u || segment == 3u; } case 6u: { return segment == 0u || segment == 5u || segment == 4u || segment == 3u || segment == 2u || segment == 6u; } case 7u: { return segment == 0u || segment == 1u || segment == 2u; } case 8u: { return true; } default: { return segment == 0u || segment == 1u || segment == 2u || segment == 3u || segment == 5u || segment == 6u; } } } fn seven_segment_digit_alpha(local: vec2f, digit: u32) -> f32 { var alpha = 0.0; if (digit_segment_on(digit, 0u)) { alpha = max(alpha, rect_alpha(local, vec2f(0.0, 0.70), vec2f(0.38, 0.078))); } if (digit_segment_on(digit, 1u)) { alpha = max(alpha, rect_alpha(local, vec2f(0.39, 0.36), vec2f(0.078, 0.335))); } if (digit_segment_on(digit, 2u)) { alpha = max(alpha, rect_alpha(local, vec2f(0.39, -0.36), vec2f(0.078, 0.335))); } if (digit_segment_on(digit, 3u)) { alpha = max(alpha, rect_alpha(local, vec2f(0.0, -0.70), vec2f(0.38, 0.078))); } if (digit_segment_on(digit, 4u)) { alpha = max(alpha, rect_alpha(local, vec2f(-0.39, -0.36), vec2f(0.078, 0.335))); } if (digit_segment_on(digit, 5u)) { alpha = max(alpha, rect_alpha(local, vec2f(-0.39, 0.36), vec2f(0.078, 0.335))); } if (digit_segment_on(digit, 6u)) { alpha = max(alpha, rect_alpha(local, vec2f(0.0, 0.0), vec2f(0.35, 0.075))); } return alpha; } fn digit_count(value: u32) -> u32 { if (value >= 1000u) { return 4u; } if (value >= 100u) { return 3u; } if (value >= 10u) { return 2u; } return 1u; } fn digit_at(value: u32, slot: u32, count: u32) -> u32 { if (count == 4u) { switch slot { case 0u: { return (value / 1000u) % 10u; } case 1u: { return (value / 100u) % 10u; } case 2u: { return (value / 10u) % 10u; } default: { return value % 10u; } } } if (count == 3u) { switch slot { case 0u: { return (value / 100u) % 10u; } case 1u: { return (value / 10u) % 10u; } default: { return value % 10u; } } } if (count == 2u) { return select(value % 10u, (value / 10u) % 10u, slot == 0u); } return value % 10u; } fn number_alpha(local: vec2f, display_value: f32) -> f32 { let value = min(u32(max(display_value + 0.5, 0.0)), 9999u); let count = digit_count(value); let count_f = f32(count); let slot_width = 1.74 / count_f; let start_x = -slot_width * (count_f - 1.0) * 0.5; var alpha = 0.0; for (var slot = 0u; slot < 4u; slot = slot + 1u) { if (slot < count) { let center_x = start_x + f32(slot) * slot_width; let digit_local = vec2f((local.x - center_x) / (slot_width * 0.78), local.y / 0.92); let digit = digit_at(value, slot, count); let in_slot = step(abs(local.x - center_x), slot_width * 0.48); alpha = max(alpha, seven_segment_digit_alpha(digit_local, digit) * in_slot); } } return alpha; } @vertex fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput { let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0); let shaped = pow(saturate(instance.style.x), 0.9); let pixel_size = u.glyph.x * mix(1.08, 2.20, shaped); let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0; var out: GlyphVertexOutput; out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w); out.local = vertex.local; out.intensity = instance.style.x; out.display_value = instance.style.y; return out; } @fragment fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f { let alpha = number_alpha(in.local, in.display_value); let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity)); return output_color(color, alpha); } // dot struct DotVertexInput { @location(0) local: vec2f, } struct DotInstanceInput { @location(1) world_position: vec4f, @location(2) style: vec4f } struct DotVertexOutput { @builtin(position) clip_position: vec4f, @location(0) local: vec2f, @location(1) intensity: f32, } fn circle_alpha(local: vec2f, radius: f32, softness: f32) -> f32 { let dist = length(local); return 1.0 - smoothstep(radius, radius + softness, dist); } // Convert a point authored in sensor-canvas UV space into aspect-fitted clip space. fn sensor_canvas_uv_to_clip(image_uv: vec2f) -> vec2f { let viewport_aspect = u.viewport.x / max(u.viewport.y, 1.0); let image_aspect = u.image.x / max(u.image.y, 1.0); var screen_uv = image_uv; if (viewport_aspect > image_aspect) { let image_width = image_aspect / viewport_aspect; screen_uv.x = image_uv.x * image_width + (1.0 - image_width) * 0.5; } else { let image_height = viewport_aspect / image_aspect; screen_uv.y = image_uv.y * image_height + (1.0 - image_height) * 0.5; } return vec2f(screen_uv.x * 2.0 - 1.0, 1.0 - screen_uv.y * 2.0); } struct HandMembraneVertexOutput { @builtin(position) clip_position: vec4f, @location(0) local: vec2f, } fn rotate_2d(point: vec2f, angle: f32) -> vec2f { let c = cos(angle); let s = sin(angle); return vec2f(point.x * c - point.y * s, point.x * s + point.y * c); } fn rounded_rect_alpha(local: vec2f, radius: f32, softness: f32) -> f32 { let q = abs(local) - vec2f(1.0 - radius, 1.0 - radius); let dist = length(max(q, vec2f(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - radius; return 1.0 - smoothstep(0.0, softness, dist); } fn fingertip_film_alpha( local: vec2f, half_width: f32, cap_center_y: f32, rear_edge_y: f32, rear_bulge: f32, softness: f32, ) -> f32 { // Top/front of the film is a closed round fingertip cap. let cap_dist = length(vec2f(local.x, local.y - cap_center_y)); let cap = (1.0 - smoothstep(half_width, half_width + softness, cap_dist)) * (1.0 - smoothstep(cap_center_y - softness, cap_center_y + softness, local.y)); // The rear half keeps nearly parallel sides and ends with a shallow arc; // it deliberately does not converge back into another capsule end. let x_norm = clamp(abs(local.x) / max(half_width, 0.001), 0.0, 1.0); let rear_curve_y = rear_edge_y + rear_bulge * (1.0 - x_norm * x_norm); let side = 1.0 - smoothstep(half_width, half_width + softness, abs(local.x)); let rear = 1.0 - smoothstep(rear_curve_y, rear_curve_y + softness, local.y); let body_gate = smoothstep(cap_center_y - softness, cap_center_y + softness, local.y); let body = side * rear * body_gate; return max(cap, body); } @vertex fn vs_hand_membrane(vertex: DotVertexInput, instance: DotInstanceInput) -> HandMembraneVertexOutput { let center_px = instance.world_position.xy * u.image.xy; let size_px = instance.style.yz; let angle = instance.style.x; let local_px = vertex.local * size_px * 0.5; let image_uv = (center_px + rotate_2d(local_px, angle)) / max(u.image.xy, vec2f(1.0, 1.0)); var out: HandMembraneVertexOutput; out.clip_position = vec4f(sensor_canvas_uv_to_clip(image_uv), 0.0, 1.0); out.local = vertex.local; return out; } @fragment fn fs_hand_membrane(in: HandMembraneVertexOutput) -> @location(0) vec4f { // The film shape matches the fingertip: closed round front, parallel rear sides, // and a shallow rear arc instead of a second capsule end. let panel = fingertip_film_alpha(in.local, 0.66, -0.38, 0.72, 0.18, 0.045); let inner = fingertip_film_alpha(in.local, 0.54, -0.36, 0.64, 0.12, 0.060); let border = clamp(panel - inner * 0.52, 0.0, 1.0); // Dense mesh: the live 12x7 pressure dots sit over this finer sensor lattice. let uv = clamp(in.local * 0.5 + vec2f(0.5, 0.5), vec2f(0.0, 0.0), vec2f(1.0, 1.0)); let grid = vec2f(18.0, 34.0); let cell = uv * grid - vec2f(0.5, 0.5); let nearest = abs(fract(cell + vec2f(0.5, 0.5)) - vec2f(0.5, 0.5)); let grid_line = max( 1.0 - smoothstep(0.014, 0.038, nearest.x), 1.0 - smoothstep(0.014, 0.038, nearest.y), ); let joint = 1.0 - smoothstep(0.070, 0.150, length(nearest * vec2f(1.18, 1.0))); let center_shadow = 1.0 - smoothstep(0.10, 0.76, length(in.local * vec2f(0.92, 0.66))); let top_light = smoothstep(-0.52, 0.16, -in.local.y) * 0.20; let side_glow = smoothstep(0.30, 0.70, abs(in.local.x)) * 0.26; let lift_shadow = smoothstep(0.48, 0.86, in.local.y) * (1.0 - smoothstep(0.50, 0.86, abs(in.local.x))) * 0.13; let scan = (0.5 + 0.5 * sin((uv.y * 76.0 + uv.x * 9.0) * 6.28318)) * 0.030; let membrane_color = vec3f(0.006, 0.28, 0.38); let line_color = vec3f(0.12, 0.72, 0.88); let rim_color = vec3f(0.18, 0.98, 1.0); let color = membrane_color * (0.68 + top_light + side_glow + scan) + line_color * (grid_line * 0.20 + joint * 0.42) + rim_color * (border * 0.92 + side_glow * 0.18) - vec3f(0.0, 0.16, 0.24) * center_shadow * 0.30 - vec3f(0.0, 0.10, 0.16) * lift_shadow; let alpha = panel * (0.24 + grid_line * 0.10 + joint * 0.23 + border * 0.42); return output_color(color, alpha); } @vertex fn vs_hand_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexOutput { let intensity = saturate(instance.style.x); let shaped = smoothstep(0.0, 1.0, intensity); // Hand instances store hand.png UV in world_position.xy instead of 3D world space. let center = sensor_canvas_uv_to_clip(instance.world_position.xy); // Hand fingertip matrices are much smaller than the full Finger view. // Keep each bead below the local cell spacing so the 12x7 matrix remains visibly separated. let pixel_size = u.glyph.x * mix(0.22, 0.34, shaped); let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0; var out: DotVertexOutput; out.clip_position = vec4f(center + ndc_offset, 0.0, 1.0); out.local = vertex.local; out.intensity = intensity; return out; } @fragment fn fs_hand_dot(in: DotVertexOutput) -> @location(0) vec4f { let intensity = saturate(in.intensity); // Use a compact bead so the response feels like it lives on the membrane mesh. let core = circle_alpha(in.local, 0.48, 0.07); let halo = circle_alpha(in.local, 0.74, 0.14) * (0.10 + intensity * 0.26); // Match Finger mode's pressure gradient so each hand region remains readable. let idle = vec3f(0.060, 0.250, 0.320); let gradient = sample_range_color(intensity); let color = mix(idle, gradient, smoothstep(0.0, 0.18, intensity)) * mix(0.78, 1.18, intensity); return output_color(color, max(core, halo)); } @vertex fn vs_hand_palm_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexOutput { let intensity = saturate(instance.style.x); let shaped = smoothstep(0.0, 1.0, intensity); // Use the same on-screen point size as Finger mode. let center = sensor_canvas_uv_to_clip(instance.world_position.xy); let pixel_size = u.glyph.x * mix(1.07, 2.23, shaped); let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0; var out: DotVertexOutput; out.clip_position = vec4f(center + ndc_offset, 0.0, 1.0); out.local = vertex.local; out.intensity = intensity; return out; } @fragment fn fs_hand_palm_dot(in: DotVertexOutput) -> @location(0) vec4f { let intensity = saturate(in.intensity); let base_color = sample_range_color(intensity); let alpha = circle_alpha(in.local, 0.46, 0.045); let color = base_color * mix(0.86, 1.06, intensity); return output_color(color, alpha); } @vertex fn vs_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexOutput { let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0); let intensity = saturate(instance.style.x); let shaped = smoothstep(0.0, 1.0, intensity); let pixel_size = u.glyph.x * mix(1.07, 2.23, shaped); let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0; var out: DotVertexOutput; out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w); out.local = vertex.local; out.intensity = intensity; return out; } @fragment fn fs_dot(in: DotVertexOutput) -> @location(0) vec4f { let intensity = saturate(in.intensity); let base_color = sample_range_color(intensity); let alpha = circle_alpha(in.local, 0.46, 0.045); let color = base_color * mix(0.86, 1.06, intensity); return output_color(color, alpha); } // model struct ModelVertexInput { @location(0) position: vec3f, @location(1) tex_coords: vec2f, @location(2) normal: vec3f, @location(3) color: vec4f, @location(4) tangent: vec4f, @location(5) model_0: vec4f, @location(6) model_1: vec4f, @location(7) model_2: vec4f, @location(8) model_3: vec4f, } struct ModelVertexOutput { @builtin(position) clip_position: vec4f, @location(0) world_normal: vec3f, @location(1) world_position: vec3f, @location(2) tex_coords: vec2f, @location(3) color: vec4f, @location(4) world_tangent: vec4f, } fn inverse_transpose3x3(m: mat3x3f) -> mat3x3f { let c0 = m[0]; let c1 = m[1]; let c2 = m[2]; let r0 = cross(c1, c2); let r1 = cross(c2, c0); let r2 = cross(c0, c1); let det = dot(c0, r0); let safe_det = select(det, select(-0.000001, 0.000001, det >= 0.0), abs(det) < 0.000001); let inv_det = 1.0 / safe_det; return mat3x3f(r0 * inv_det, r1 * inv_det, r2 * inv_det); } @vertex fn vs_model(vertex: ModelVertexInput) -> ModelVertexOutput { let model = mat4x4f(vertex.model_0, vertex.model_1, vertex.model_2, vertex.model_3); let world_position = model * vec4f(vertex.position, 1.0); let model3 = mat3x3f(model[0].xyz, model[1].xyz, model[2].xyz); let normal_matrix = inverse_transpose3x3(model3); var out: ModelVertexOutput; out.clip_position = u.view_proj * world_position; out.world_position = world_position.xyz; out.world_normal = normalize(normal_matrix * vertex.normal); out.tex_coords = vertex.tex_coords; out.color = vertex.color; out.world_tangent = vec4f(normalize(normal_matrix * vertex.tangent.xyz), vertex.tangent.w); return out; } fn distribution_ggx(normal: vec3f, half_dir: vec3f, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let n_dot_h = max(dot(normal, half_dir), 0.0); let n_dot_h2 = n_dot_h * n_dot_h; let denom = (n_dot_h2 * (a2 - 1.0) + 1.0); return a2 / max(3.14159265 * denom * denom, 0.000001); } fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return n_dot_v / max(n_dot_v * (1.0 - k) + k, 0.000001); } fn geometry_smith(normal: vec3f, view_dir: vec3f, light_dir: vec3f, roughness: f32) -> f32 { let n_dot_v = max(dot(normal, view_dir), 0.0); let n_dot_l = max(dot(normal, light_dir), 0.0); let ggx_v = geometry_schlick_ggx(n_dot_v, roughness); let ggx_l = geometry_schlick_ggx(n_dot_l, roughness); return ggx_v * ggx_l; } fn fresnel_schlick(cos_theta: f32, f0: vec3f) -> vec3f { let factor = pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); return f0 + (vec3f(1.0) - f0) * factor; } fn pbr_directional_light( albedo: vec3f, normal: vec3f, view_dir: vec3f, light_dir: vec3f, radiance: vec3f, metallic: f32, roughness: f32, ) -> vec3f { let half_dir = normalize(view_dir + light_dir); let n_dot_l = max(dot(normal, light_dir), 0.0); let n_dot_v = max(dot(normal, view_dir), 0.0); let h_dot_v = max(dot(half_dir, view_dir), 0.0); let f0 = mix(vec3f(0.04), albedo, metallic); let f = fresnel_schlick(h_dot_v, f0); let d = distribution_ggx(normal, half_dir, roughness); let g = geometry_smith(normal, view_dir, light_dir, roughness); let specular = (d * g * f) / max(4.0 * n_dot_v * n_dot_l, 0.000001); let k_s = f; let k_d = (vec3f(1.0) - k_s) * (1.0 - metallic); let diffuse = k_d * albedo / 3.14159265; return (diffuse + specular) * radiance * n_dot_l; } fn aces_tonemap(x: vec3f) -> vec3f { let a = 2.51; let b = 0.03; let c = 2.43; let d = 0.59; let e = 0.14; return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3f(0.0), vec3f(1.0)); } fn material_normal(in: ModelVertexOutput, front_facing: bool) -> vec3f { var n = normalize(in.world_normal); if (!front_facing && material.flags.w > 0.5) { n = -n; } if (material.flags.z < 0.5) { return n; } let t = normalize(in.world_tangent.xyz); let b = normalize(cross(n, t) * in.world_tangent.w); let tangent_normal_sample = textureSample(normal_texture, normal_sampler, in.tex_coords).xyz; var tangent_normal = tangent_normal_sample * 2.0 - vec3f(1.0); tangent_normal.x = tangent_normal.x * material.metallic_roughness.z; tangent_normal.y = tangent_normal.y * material.metallic_roughness.z; return normalize(mat3x3f(t, b, n) * tangent_normal); } @fragment fn fs_model(in: ModelVertexOutput, @builtin(front_facing) front_facing: bool) -> @location(0) vec4f { let base_sample = textureSample(base_color_texture, base_color_sampler, in.tex_coords); let base_color = base_sample * material.base_color * in.color; let alpha_mode = material.emissive_alpha.w; var alpha = base_color.a; if (alpha_mode < 0.5) { alpha = 1.0; } else if (alpha_mode < 1.5) { if (alpha < material.flags.x) { discard; } alpha = 1.0; } let debug_mode = u32(u.render_options.x + 0.5); if (debug_mode == 1u) { return output_color(base_color.rgb, alpha); } let mr_sample = textureSample( metallic_roughness_texture, metallic_roughness_sampler, in.tex_coords, ); let roughness = clamp(mr_sample.g * material.metallic_roughness.y, 0.04, 1.0); let metallic = clamp(mr_sample.b * material.metallic_roughness.x, 0.0, 1.0); let ao_sample = textureSample(occlusion_texture, occlusion_sampler, in.tex_coords).r; let ao = mix(1.0, ao_sample, clamp(material.metallic_roughness.w, 0.0, 1.0)); let emissive = textureSample(emissive_texture, emissive_sampler, in.tex_coords).rgb * material.emissive_alpha.rgb; let albedo = base_color.rgb; let normal = material_normal(in, front_facing); switch debug_mode { case 2u: { return output_color(normal * 0.5 + vec3f(0.5), 1.0); } case 3u: { return output_color(vec3f(roughness), 1.0); } case 4u: { return output_color(vec3f(metallic), 1.0); } case 5u: { return output_color(vec3f(ao), 1.0); } case 6u: { return output_color(emissive, 1.0); } case 7u: { return output_color(vec3f(fract(in.tex_coords), 0.0), 1.0); } default: {} } let view_dir = normalize(vec3f(0.0, 0.72, 0.70)); let key = pbr_directional_light( albedo, normal, view_dir, normalize(vec3f(-0.35, 0.82, 0.44)), vec3f(2.15, 2.08, 1.92), metallic, roughness, ); let fill = pbr_directional_light( albedo, normal, view_dir, normalize(vec3f(0.62, 0.44, -0.36)), vec3f(0.50, 0.50, 0.50), metallic, roughness, ); let f0 = mix(vec3f(0.04), albedo, metallic); let f_ambient = fresnel_schlick(max(dot(normal, view_dir), 0.0), f0); let ambient_color = vec3f(1.0); let ambient_strength = 0.08; // Temporary neutral linear ambient term until a real IBL/HDR environment is added. var ambient = vec3f(0.0); if (u.render_options.y > 0.5) { let ambient_diffuse = albedo * (1.0 - metallic) * ambient_color * ambient_strength; let ambient_specular = f_ambient * ambient_color * ambient_strength * (1.0 - roughness * 0.55); ambient = (ambient_diffuse + ambient_specular) * ao; } let exposed_color = (ambient + key + fill + emissive) * max(u.render_options.w, 0.0); var color = exposed_color; if (u.render_options.z > 0.5) { color = aces_tonemap(exposed_color); } return output_color(color, alpha); }