880 lines
29 KiB
WebGPU Shading Language
880 lines
29 KiB
WebGPU Shading Language
struct MatrixUniform {
|
|
view_proj: mat4x4f,
|
|
viewport: vec4f,
|
|
glyph: vec4f,
|
|
color: vec4f,
|
|
render_options: vec4f,
|
|
image: vec4f,
|
|
}
|
|
|
|
@group(0) @binding(0)
|
|
var<uniform> u: MatrixUniform;
|
|
|
|
@group(1) @binding(0)
|
|
var base_color_texture: texture_2d<f32>;
|
|
|
|
@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<uniform> material: MaterialParams;
|
|
|
|
@group(1) @binding(3)
|
|
var metallic_roughness_texture: texture_2d<f32>;
|
|
|
|
@group(1) @binding(4)
|
|
var metallic_roughness_sampler: sampler;
|
|
|
|
@group(1) @binding(5)
|
|
var normal_texture: texture_2d<f32>;
|
|
|
|
@group(1) @binding(6)
|
|
var normal_sampler: sampler;
|
|
|
|
@group(1) @binding(7)
|
|
var occlusion_texture: texture_2d<f32>;
|
|
|
|
@group(1) @binding(8)
|
|
var occlusion_sampler: sampler;
|
|
|
|
@group(1) @binding(9)
|
|
var emissive_texture: texture_2d<f32>;
|
|
|
|
@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, 3>(
|
|
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, 6>(
|
|
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, 6>(
|
|
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 hand.png UV space into clip space.
|
|
// This mirrors fs_hand_image's aspect-fit math, so fingertip dots stay attached
|
|
// to the same image pixels when the app window changes shape.
|
|
fn hand_image_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 dot_matrix(uv: vec2f, grid: vec2f, dot_radius: f32, dot_softness: f32) -> f32 {
|
|
let cell = fract(uv * grid) - vec2f(0.5, 0.5);
|
|
return 1.0 - smoothstep(dot_radius, dot_radius + dot_softness, length(cell));
|
|
}
|
|
|
|
fn fingertip_film_alpha(
|
|
local: vec2f,
|
|
half_width: f32,
|
|
cap_center_y: 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 remains open; the carrier quad clips the membrane at its end.
|
|
let side = 1.0 - smoothstep(half_width, half_width + softness, abs(local.x));
|
|
let body_gate = smoothstep(cap_center_y - softness, cap_center_y + softness, local.y);
|
|
let body = side * 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(hand_image_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 {
|
|
let p = in.local;
|
|
|
|
let halo_shape = fingertip_film_alpha(p, 0.72, -0.20, 0.055);
|
|
let panel = fingertip_film_alpha(p, 0.66, -0.22, 0.035);
|
|
let inner = fingertip_film_alpha(p, 0.58, -0.25, 0.045);
|
|
let clear_inner = fingertip_film_alpha(p, 0.48, -0.24, 0.160);
|
|
|
|
let halo = clamp(halo_shape - panel, 0.0, 1.0);
|
|
let rim = clamp(panel - inner, 0.0, 1.0);
|
|
let edge_fade = pow(clamp(1.0 - clear_inner, 0.0, 1.0), 1.20) * panel;
|
|
|
|
// Strong pseudo extrusion / bevel.
|
|
let depth_offset = vec2f(0.045, 0.055);
|
|
let back_shape_1 = fingertip_film_alpha(p - depth_offset * 0.55, 0.66, -0.22, 0.045);
|
|
let back_shape_2 = fingertip_film_alpha(p - depth_offset, 0.66, -0.22, 0.060);
|
|
let extrusion = clamp(max(back_shape_1, back_shape_2) - panel, 0.0, 1.0);
|
|
|
|
let bevel_offset = vec2f(0.028, 0.034);
|
|
let shifted_down_right = fingertip_film_alpha(p - bevel_offset, 0.66, -0.22, 0.035);
|
|
let bevel_light = clamp(panel - shifted_down_right, 0.0, 1.0);
|
|
let shifted_up_left = fingertip_film_alpha(p + bevel_offset, 0.66, -0.22, 0.035);
|
|
let bevel_dark = clamp(panel - shifted_up_left, 0.0, 1.0);
|
|
|
|
let broad_bevel = edge_fade * clamp(0.58 - p.x * 0.20 - p.y * 0.18, 0.0, 1.0);
|
|
|
|
// Micro lattice.
|
|
let uv = clamp(
|
|
p * vec2f(0.52, 0.46) + vec2f(0.5, 0.46),
|
|
vec2f(0.0, 0.0),
|
|
vec2f(1.0, 1.0),
|
|
);
|
|
|
|
let grid = vec2f(18.0, 34.0);
|
|
let cell_uv = fract(uv * grid) - vec2f(0.5, 0.5);
|
|
let dot_dist = length(cell_uv * vec2f(1.0, 1.06));
|
|
let dot_aa = max(fwidth(dot_dist) * 1.35, 0.006);
|
|
let dots = (1.0 - smoothstep(0.105 - dot_aa, 0.105 + dot_aa, dot_dist)) * inner;
|
|
|
|
let sheen_axis = p.x * 0.88 + p.y * 0.22 + 0.16;
|
|
let sheen = (1.0 - smoothstep(0.018, 0.105, abs(sheen_axis))) * panel;
|
|
let shell_sheen = sheen * (0.26 + edge_fade * 0.74);
|
|
|
|
// Temporary synthetic pressure hotspot. Real hand pressure dots are drawn above this pass.
|
|
let pressure_center = vec2f(-0.08, -0.12);
|
|
let pressure_dist = length((p - pressure_center) * vec2f(1.0, 0.74));
|
|
let pressure = (1.0 - smoothstep(0.05, 0.36, pressure_dist)) * inner;
|
|
let pressure_hot = (1.0 - smoothstep(0.02, 0.13, pressure_dist)) * inner;
|
|
|
|
let glass_base = vec3f(0.006, 0.055, 0.105);
|
|
let glass_cyan = vec3f(0.025, 0.42, 0.58);
|
|
let rim_color = vec3f(0.10, 0.88, 1.00);
|
|
let extrusion_color = vec3f(0.004, 0.080, 0.110);
|
|
let extrusion_edge_color = vec3f(0.015, 0.30, 0.38);
|
|
let bevel_highlight = vec3f(0.48, 1.00, 1.00);
|
|
let bevel_dark_color = vec3f(0.004, 0.035, 0.060);
|
|
let dot_color = vec3f(0.08, 0.48, 0.58);
|
|
let pressure_color = dot_color;
|
|
let pressure_hot_color = dot_color;
|
|
|
|
let color = extrusion_color * extrusion * 0.90
|
|
+ extrusion_edge_color * extrusion * halo_shape * 0.32
|
|
+ glass_base * panel * 0.26
|
|
+ glass_cyan * edge_fade * 0.46
|
|
+ glass_cyan * broad_bevel * 0.24
|
|
+ rim_color * rim * 0.88
|
|
+ bevel_highlight * bevel_light * 1.35
|
|
+ bevel_dark_color * bevel_dark * 0.95
|
|
+ bevel_highlight * shell_sheen * 0.28
|
|
+ rim_color * halo * 0.16
|
|
+ dot_color * dots * 0.68
|
|
+ pressure_color * dots * pressure * 0.82
|
|
+ pressure_hot_color * dots * pressure_hot * 0.90;
|
|
|
|
let alpha = clamp(
|
|
extrusion * 0.44
|
|
+ panel * 0.055
|
|
+ edge_fade * 0.30
|
|
+ rim * 0.34
|
|
+ bevel_light * 0.34
|
|
+ bevel_dark * 0.20
|
|
+ shell_sheen * 0.09
|
|
+ halo * 0.045
|
|
+ dots * 0.09
|
|
+ dots * pressure * 0.10,
|
|
0.0,
|
|
0.90,
|
|
);
|
|
|
|
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 = hand_image_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));
|
|
}
|
|
|
|
fn chip_pixel_alpha(local: vec2f, half_size: f32, softness: f32) -> f32 {
|
|
let q = abs(local) - vec2f(half_size, half_size);
|
|
let dist = length(max(q, vec2f(0.0, 0.0))) + min(max(q.x, q.y), 0.0);
|
|
return 1.0 - smoothstep(0.0, softness, dist);
|
|
}
|
|
|
|
struct HandPalmChipVertexOutput {
|
|
@builtin(position) clip_position: vec4f,
|
|
@location(0) local: vec2f,
|
|
@location(1) grid: vec2f,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_hand_palm_chip(vertex: DotVertexInput, instance: DotInstanceInput) -> HandPalmChipVertexOutput {
|
|
let center_px = instance.world_position.xy * u.image.xy;
|
|
let size_px = instance.style.yz;
|
|
let angle = instance.style.x;
|
|
let packed_shape = instance.style.w;
|
|
let shape_rows = floor(packed_shape / 100.0);
|
|
let shape_cols = max(packed_shape - shape_rows * 100.0, 1.0);
|
|
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: HandPalmChipVertexOutput;
|
|
out.clip_position = vec4f(hand_image_uv_to_clip(image_uv), 0.0, 1.0);
|
|
out.local = vertex.local;
|
|
out.grid = vec2f(shape_cols, max(shape_rows, 1.0));
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_hand_palm_chip(in: HandPalmChipVertexOutput) -> @location(0) vec4f {
|
|
// The live palm-dot pass draws every chip cell, including the idle dots.
|
|
// Keeping this background pass transparent prevents a second offset dot grid.
|
|
return output_color(vec3f(0.0, 0.0, 0.0), 0.0);
|
|
}
|
|
|
|
@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);
|
|
|
|
let center = hand_image_uv_to_clip(instance.world_position.xy);
|
|
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_palm_dot(in: DotVertexOutput) -> @location(0) vec4f {
|
|
let intensity = saturate(in.intensity);
|
|
|
|
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);
|
|
|
|
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_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);
|
|
}
|