Refactor to hand-mode UI and reorganize WGSL shader

Replace spatial force / stats panels with unified hand force panels; simplify app.rs wiring and reorganize shader.wgsl for the hand rendering layout.
This commit is contained in:
lenn
2026-07-03 13:41:02 +08:00
parent 71d314ac44
commit a999d9c064
3 changed files with 113 additions and 144 deletions

View File

@@ -63,7 +63,7 @@ fn linear_to_srgb(linear: vec3f) -> vec3f {
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) {
if u.color.w > 0.5 {
return vec4f(clamped, alpha);
}
@@ -91,12 +91,12 @@ fn range_stop_color(index: u32) -> vec3f {
fn sample_range_color(value: f32) -> vec3f {
let t = saturate(value);
if (t <= 0.33) {
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) {
if t <= 0.66 {
let local = smoothstep(0.33, 0.66, t);
return mix(range_stop_color(1u), range_stop_color(2u), local);
}
@@ -105,7 +105,6 @@ fn sample_range_color(value: f32) -> vec3f {
return mix(range_stop_color(2u), range_stop_color(3u), local);
}
// background
struct BackgroundVertexOutput {
@builtin(position) clip_position: vec4f,
@@ -129,7 +128,6 @@ 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,
@@ -167,7 +165,7 @@ fn fs_hand_image(in: HandImageVertexOutput) -> @location(0) vec4f {
let image_aspect = u.image.x / max(u.image.y, 1.0);
var uv = in.screen_uv;
if (viewport_aspect > image_aspect) {
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 {
@@ -175,7 +173,7 @@ fn fs_hand_image(in: HandImageVertexOutput) -> @location(0) vec4f {
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) {
if uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 {
discard;
}
@@ -183,7 +181,6 @@ fn fs_hand_image(in: HandImageVertexOutput) -> @location(0) vec4f {
return output_color(color.rgb, color.a);
}
// glyph
struct GlyphVertexInput {
@location(0) local: vec2f,
@@ -226,45 +223,45 @@ fn digit_segment_on(digit: u32, segment: u32) -> bool {
fn seven_segment_digit_alpha(local: vec2f, digit: u32) -> f32 {
var alpha = 0.0;
if (digit_segment_on(digit, 0u)) {
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)) {
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)) {
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)) {
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)) {
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)) {
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)) {
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) {
if value >= 1000u {
return 4u;
}
if (value >= 100u) {
if value >= 100u {
return 3u;
}
if (value >= 10u) {
if value >= 10u {
return 2u;
}
return 1u;
}
fn digit_at(value: u32, slot: u32, count: u32) -> u32 {
if (count == 4u) {
if count == 4u {
switch slot {
case 0u: { return (value / 1000u) % 10u; }
case 1u: { return (value / 100u) % 10u; }
@@ -272,14 +269,14 @@ fn digit_at(value: u32, slot: u32, count: u32) -> u32 {
default: { return value % 10u; }
}
}
if (count == 3u) {
if count == 3u {
switch slot {
case 0u: { return (value / 100u) % 10u; }
case 1u: { return (value / 10u) % 10u; }
default: { return value % 10u; }
}
}
if (count == 2u) {
if count == 2u {
return select(value % 10u, (value / 10u) % 10u, slot == 0u);
}
return value % 10u;
@@ -294,7 +291,7 @@ fn number_alpha(local: vec2f, display_value: f32) -> f32 {
var alpha = 0.0;
for (var slot = 0u; slot < 4u; slot = slot + 1u) {
if (slot < count) {
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);
@@ -335,8 +332,7 @@ struct DotVertexInput {
struct DotInstanceInput {
@location(1) world_position: vec4f,
@location(2) style: vec4f
}
@location(2) style: vec4f}
struct DotVertexOutput {
@builtin(position) clip_position: vec4f,
@@ -358,7 +354,7 @@ fn hand_image_uv_to_clip(image_uv: vec2f) -> vec2f {
var screen_uv = image_uv;
if (viewport_aspect > image_aspect) {
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 {
@@ -390,8 +386,6 @@ 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.
@@ -399,14 +393,10 @@ fn fingertip_film_alpha(
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);
// 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 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;
let body = side * body_gate;
return max(cap, body);
}
@@ -427,38 +417,77 @@ fn vs_hand_membrane(vertex: DotVertexInput, instance: DotInstanceInput) -> HandM
@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);
let p = in.local;
// 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 halo_shape = fingertip_film_alpha(p, 0.72, -0.39, 0.060);
let panel = fingertip_film_alpha(p, 0.66, -0.38, 0.040);
let rim_inner = fingertip_film_alpha(p, 0.60, -0.36, 0.040);
let clear_inner = fingertip_film_alpha(p, 0.49, -0.31, 0.180);
let halo = clamp(halo_shape - panel, 0.0, 1.0);
let border = clamp(panel - rim_inner, 0.0, 1.0);
let edge_fade = pow(clamp(1.0 - clear_inner, 0.0, 1.0), 1.15) * panel;
// 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 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 grid = vec2f(18.0, 34.0);
let cell_uv = fract(uv * grid) - vec2f(0.5, 0.5);
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);
let node = 1.0 - smoothstep(
0.070,
0.165,
length(cell_uv * vec2f(1.0, 1.08)),
);
let mesh_x = smoothstep(0.455, 0.495, abs(cell_uv.x));
let mesh_y = smoothstep(0.455, 0.495, abs(cell_uv.y));
let mesh = max(mesh_x, mesh_y);
let lattice = (node * 0.90 + mesh * 0.10) * rim_inner;
// Blue glass highlights.
let side_glow = smoothstep(0.28, 0.66, abs(p.x)) * panel;
let top_light = smoothstep(0.10, 0.78, -p.y) * panel;
let sheen_axis = p.x * 0.88 + p.y * 0.22 + 0.16;
let sheen = (1.0 - smoothstep(0.0, 0.11, abs(sheen_axis))) * panel;
let scan = (0.5 + 0.5 * sin((uv.y * 76.0 + uv.x * 9.0) * 6.28318)) * 0.025;
// Temporary synthetic pressure hotspot. Real hand pressure dots are drawn above this pass.
let pressure_center = vec2f(-0.10, -0.15);
let pressure_dist = length((p - pressure_center) * vec2f(1.0, 0.72));
let pressure = (1.0 - smoothstep(0.04, 0.37, pressure_dist)) * rim_inner;
let pressure_hot = (1.0 - smoothstep(0.015, 0.13, pressure_dist)) * rim_inner;
let membrane_color = vec3f(0.006, 0.028, 0.110);
let inner_cyanblue = vec3f(0.012, 0.110, 0.320);
let lattice_color = vec3f(0.040, 0.420, 0.760);
let rim_color = vec3f(0.120, 0.720, 0.950);
let pressure_green = vec3f(0.00, 0.66, 0.22);
let pressure_lime = vec3f(0.18, 1.00, 0.50);
let color = membrane_color * panel * (0.36 + top_light * 0.08 + scan)
+ inner_cyanblue * edge_fade * 0.72
+ lattice_color * lattice * 0.66
+ rim_color * (border * 0.90 + edge_fade * 0.16 + halo * 0.16 + side_glow * 0.08 + sheen * 0.10)
+ pressure_green * pressure * 0.70
+ pressure_lime * pressure_hot * 0.82;
let alpha = clamp(
panel * 0.08
+ edge_fade * 0.43
+ border * 0.20
+ halo * 0.04
+ lattice * 0.055
+ pressure * 0.05,
0.0,
0.78,
);
return output_color(color, alpha);
}
@@ -621,7 +650,6 @@ fn fs_dot(in: DotVertexOutput) -> @location(0) vec4f {
return output_color(color, alpha);
}
// model
struct ModelVertexInput {
@location(0) position: vec3f,
@@ -740,11 +768,11 @@ fn aces_tonemap(x: vec3f) -> vec3f {
fn material_normal(in: ModelVertexOutput, front_facing: bool) -> vec3f {
var n = normalize(in.world_normal);
if (!front_facing && material.flags.w > 0.5) {
if !front_facing && material.flags.w > 0.5 {
n = -n;
}
if (material.flags.z < 0.5) {
if material.flags.z < 0.5 {
return n;
}
@@ -764,17 +792,17 @@ fn fs_model(in: ModelVertexOutput, @builtin(front_facing) front_facing: bool) ->
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) {
if alpha_mode < 0.5 {
alpha = 1.0;
} else if (alpha_mode < 1.5) {
if (alpha < material.flags.x) {
} 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) {
if debug_mode == 1u {
return output_color(base_color.rgb, alpha);
}
@@ -840,7 +868,7 @@ fn fs_model(in: ModelVertexOutput, @builtin(front_facing) front_facing: bool) ->
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) {
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;
@@ -848,7 +876,7 @@ fn fs_model(in: ModelVertexOutput, @builtin(front_facing) front_facing: bool) ->
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) {
if u.render_options.z > 0.5 {
color = aces_tonemap(exposed_color);
}