diff --git a/src/render.rs b/src/render.rs index 370fd07..0c5b543 100644 --- a/src/render.rs +++ b/src/render.rs @@ -115,6 +115,7 @@ pub struct BackgroundRenderResources { hand_image_pipeline: wgpu::RenderPipeline, glyph_pipeline: wgpu::RenderPipeline, dot_pipeline: wgpu::RenderPipeline, + hand_membrane_pipeline: wgpu::RenderPipeline, hand_dot_pipeline: wgpu::RenderPipeline, hand_image_bind_group: wgpu::BindGroup, hand_image_texture: texture::Texture, @@ -122,6 +123,8 @@ pub struct BackgroundRenderResources { glyph_vertex_buffer: wgpu::Buffer, glyph_instance_buffer: wgpu::Buffer, glyph_instances: Vec, + hand_membrane_instance_buffer: wgpu::Buffer, + hand_membrane_instances: Vec, hand_dot_instance_buffer: wgpu::Buffer, hand_dot_instances: Vec, render_options: RenderOptions, @@ -445,6 +448,8 @@ impl BackgroundRenderResources { let glyph_pipeline = create_glyph_pipeline(device, target_format, &shader, &pipeline_layout); let dot_pipeline = create_dot_pipeline(device, target_format, &shader, &pipeline_layout); + let hand_membrane_pipeline = + create_hand_membrane_pipeline(device, target_format, &shader, &pipeline_layout); let hand_dot_pipeline = create_hand_dot_pipeline(device, target_format, &shader, &pipeline_layout); @@ -474,6 +479,16 @@ impl BackgroundRenderResources { contents: bytemuck::cast_slice(&glyph_instances), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); + let hand_membrane_instances = build_hand_membrane_instances( + hand_image_texture.width as f32, + hand_image_texture.height as f32, + ); + let hand_membrane_instance_buffer = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Hand Fingertip Membrane Instance Buffer"), + contents: bytemuck::cast_slice(&hand_membrane_instances), + usage: wgpu::BufferUsages::VERTEX, + }); let hand_dot_instances = build_hand_dot_instances( rows, cols, @@ -500,12 +515,15 @@ impl BackgroundRenderResources { hand_image_pipeline, glyph_pipeline, dot_pipeline, + hand_membrane_pipeline, hand_dot_pipeline, hand_image_bind_group, hand_image_texture, glyph_vertex_buffer, glyph_instance_buffer, glyph_instances, + hand_membrane_instance_buffer, + hand_membrane_instances, hand_dot_instance_buffer, hand_dot_instances, render_options, @@ -595,7 +613,12 @@ impl BackgroundRenderResources { fn paint_hand(&self, render_pass: &mut wgpu::RenderPass<'_>, mode: &HandGatewayMode) { let _range = mode.range.clone(); - // Draw the five fingertip dot matrices after hand.png, so they sit on top of the texture. + // First draw the translucent sensor membranes, then draw live pressure beads on their grid. + render_pass.set_pipeline(&self.hand_membrane_pipeline); + render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); + render_pass.set_vertex_buffer(1, self.hand_membrane_instance_buffer.slice(..)); + render_pass.draw(0..6, 0..self.hand_membrane_instances.len() as u32); + render_pass.set_pipeline(&self.hand_dot_pipeline); render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); render_pass.set_vertex_buffer(1, self.hand_dot_instance_buffer.slice(..)); @@ -867,6 +890,39 @@ fn create_dot_pipeline( }) } +fn create_hand_membrane_pipeline( + device: &wgpu::Device, + target_format: &wgpu::TextureFormat, + shader: &wgpu::ShaderModule, + layout: &wgpu::PipelineLayout, +) -> wgpu::RenderPipeline { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Hand Fingertip Sensor Membrane Pipeline"), + layout: Some(layout), + vertex: wgpu::VertexState { + module: shader, + entry_point: Some("vs_hand_membrane"), + compilation_options: Default::default(), + buffers: &[GlyphVertex::desc(), GlyphInstance::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: shader, + entry_point: Some("fs_hand_membrane"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: *target_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }) +} + fn create_hand_dot_pipeline( device: &wgpu::Device, target_format: &wgpu::TextureFormat, @@ -900,6 +956,24 @@ fn create_hand_dot_pipeline( }) } +fn build_hand_membrane_instances(image_width: f32, image_height: f32) -> Vec { + HAND_TIP_MATRICES + .iter() + .map(|tip| { + let uv_x = tip.center_px[0] / image_width.max(1.0); + let uv_y = tip.center_px[1] / image_height.max(1.0); + let membrane_size = [tip.size_px[0] * 1.62, tip.size_px[1] * 1.56]; + + GlyphInstance { + // Membrane shaders read xy as hand.png UV. + world_position: [uv_x, uv_y, 0.0, 1.0], + // Store angle and an oversized source-image pixel size for the floating sensor film. + style: [tip.angle_rad, membrane_size[0], membrane_size[1], 0.0], + } + }) + .collect() +} + fn build_hand_dot_instances( rows: u32, cols: u32, diff --git a/static/wgsl/shader.wgsl b/static/wgsl/shader.wgsl index e392e45..b342834 100644 --- a/static/wgsl/shader.wgsl +++ b/static/wgsl/shader.wgsl @@ -377,6 +377,100 @@ fn hand_image_uv_to_clip(image_uv: vec2f) -> vec2f { 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(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 { + // 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); @@ -387,7 +481,7 @@ fn vs_hand_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexO 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.38, 0.56, shaped); + 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; @@ -401,15 +495,15 @@ fn vs_hand_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexO fn fs_hand_dot(in: DotVertexOutput) -> @location(0) vec4f { let intensity = saturate(in.intensity); - // Use a compact round bead instead of the larger Finger-mode glow marker. - let core = circle_alpha(in.local, 0.56, 0.08); - let halo = circle_alpha(in.local, 0.78, 0.14) * 0.16; + // 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.12; // Keep the fingertip matrix visually close to JE-Skin's cyan model dots, // while still letting pressure brighten the bead a little. let cyan = vec3f(0.34, 0.86, 1.0); let hot = sample_range_color(intensity); - let color = mix(cyan, hot, 0.22) * mix(0.88, 1.18, intensity); + let color = mix(cyan, hot, 0.22) * mix(0.82, 1.16, intensity); return output_color(color, max(core, halo)); } @@ -435,19 +529,8 @@ 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.92, 0.20) * intensity * 0.32; - - 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); + 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); }