struct MatrixUniform { view_proj: mat4x4f, viewport: vec4f, glyph: vec4f, color: vec4f, } @group(0) @binding(0) var u: MatrixUniform; @group(1) @binding(0) var model_texture: texture_2d; @group(1) @binding(1) var model_sampler: sampler; fn saturate(value: f32) -> f32 { return clamp(value, 0.0, 1.0); } 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 { let pixel = frag_coord.xy; let viewport = u.viewport.xy; let uv = pixel / max(viewport, vec2f(1.0, 1.0)); var color = mix(vec3f(0.112, 0.126, 0.160), vec3f(0.145, 0.160, 0.198), 1.0 - uv.y); let vignette = smoothstep(0.18, 0.92, length((uv - vec2f(0.52, 0.48)) * vec2f(viewport.x / viewport.y, 1.0))); color *= 1.0 - vignette * 0.30; color += vec3f(0.035, 0.070, 0.090) * (1.0 - smoothstep(0.0, 0.9, abs(uv.y - 0.52))); let track_width = clamp(viewport.x * 0.42, 260.0, 560.0); let track_height = 12.0; let track_center = vec2f(viewport.x * 0.5, viewport.y - 38.0); let local = pixel - track_center; let half_size = vec2f(track_width * 0.5, track_height * 0.5); let inside_track = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y); let border = step(abs(local.x), half_size.x + 1.0) * step(abs(local.y), half_size.y + 1.0) - inside_track; let t = saturate((local.x + half_size.x) / track_width); if (inside_track > 0.5) { let shade = mix(0.72, 1.0, 1.0 - saturate((local.y + half_size.y) / track_height)); color = mix(color, sample_range_color(t) * shade, 0.96); } color = max(color, vec3f(0.34, 0.41, 0.46) * border); let tick_area = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y + 8.0); if (tick_area > 0.5) { let ticks = array(0.0, 0.33, 0.66, 1.0); for (var index = 0u; index < 4u; index = index + 1u) { let tick_x = (ticks[index] - 0.5) * track_width; let tick = step(abs(local.x - tick_x), 1.0) * step(abs(local.y), half_size.y + 7.0); color = max(color, vec3f(0.78, 0.84, 0.90) * tick); } } return vec4f(color, 1.0); } // 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 vec4f(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); } @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 = pow(intensity, 0.9); let pixel_size = u.glyph.x * mix(0.72, 1.85, 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 core = circle_alpha(in.local, 0.42, 0.055); let glow = circle_alpha(in.local, 0.78, 0.18) * intensity * 0.45; let highlight_pos = in.local - vec2f(-0.18, 0.20); let highlight = circle_alpha(highlight_pos, 0.16, 0.08) * 0.45; let brightness = mix(0.82, 1.22, intensity); let color = base_color * brightness + vec3f(1.0, 1.0, 1.0) * highlight; let alpha = max(core, glow); return vec4f(color, alpha); } // model struct ModelVertexInput { @location(0) position: vec3f, @location(1) tex_coords: vec2f, @location(2) normal: vec3f, @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, } @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 normal_matrix = mat3x3f(model[0].xyz, model[1].xyz, model[2].xyz); 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 = vec2f(vertex.tex_coords.x, 1.0 - vertex.tex_coords.y); return out; } @fragment fn fs_model(in: ModelVertexOutput) -> @location(0) vec4f { let light_dir = normalize(vec3f(-0.35, 0.85, 0.45)); let normal = normalize(in.world_normal); let diffuse = max(dot(normal, light_dir), 0.0); let rim = pow(1.0 - saturate(abs(normal.y)), 2.0) * 0.16; let base = textureSample(model_texture, model_sampler, in.tex_coords).rgb; let color = base * (0.28 + diffuse * 0.72) + vec3f(0.14, 0.24, 0.32) * rim; return vec4f(color, 1.0); }