diff --git a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal index 578ab7a6b..78935a528 100644 --- a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal +++ b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal @@ -331,7 +331,7 @@ fragment float4 keypath_keyboard_stage_fragment( // drive gloss, grain, and highlight variation; both derive from the key // code, so the pattern never shifts between moments or launches. float wear = isKey - ? saturate(input.material.x) * saturate(uniforms.tuningE.z) + ? saturate(input.material.x) * max(uniforms.tuningE.z, 0.0) : 0.0; float materialSeedA = keypath_hash(float2(input.material.y, 0.37)); float materialSeedB = keypath_hash(float2(input.material.y, 0.71)); @@ -565,7 +565,9 @@ fragment float4 keypath_keyboard_stage_fragment( (isDeck ? 0.31 : 0.47) + roughnessVariation * (isDeck ? 0.055 : 0.035) - wear * 0.11 - + (isKey ? (materialSeedA - 0.5) * 0.05 : 0.0), + + (isKey + ? (materialSeedA - 0.5) * 0.05 * min(uniforms.tuningE.w, 1.0) + : 0.0), 0.12, 0.82 ); @@ -584,8 +586,12 @@ fragment float4 keypath_keyboard_stage_fragment( * microfacetVisibility / max(0.04, 4.0 * normalDotView * normalDotLight); float specularPower = isDeck ? 88.0 : (isKey ? 42.0 : 34.0); + // Seed-only gloss deviation collapses to 1.0 as highlightJitter + // approaches zero, so the tuning knobs can fully restore the uniform + // pre-wear material. float keySpecularGain = isKey - ? (0.85 + materialSeedB * 0.30) * (1.0 + wear * 0.55) + ? (1.0 + (materialSeedB - 0.5) * 0.30 * min(uniforms.tuningE.w, 1.0)) + * (1.0 + wear * 0.55) : 1.0; float specular = pow(saturate(dot(normal, halfVector)), specularPower) * keySpecularGain; diff --git a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift index b14dc65d0..398795d14 100644 --- a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift +++ b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift @@ -49,7 +49,8 @@ struct KeyboardStageTuning: Equatable, Sendable { var fillLightStrength: Float = 0.95 var fillSpecularStrength: Float = 0.15 /// Per-key wear (polished crowns, calmer grain on high-traffic caps). - var wearStrength: Float = 1.5 + /// A plain multiplier: values above 1 amplify the per-key wear levels. + var wearStrength: Float = 1.0 /// Per-key highlight jitter (each cap catches the light slightly apart). var highlightJitter: Float = 2.2 /// Window-space corner falloff; relaxes to 30% at the settled light. diff --git a/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift b/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift index 595050dad..c942a29e6 100644 --- a/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift +++ b/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift @@ -32,6 +32,12 @@ final class KeyboardStageTuningTests: XCTestCase { XCTAssertEqual(tuning.gpuVectorC.w, tuning.backlightStrengthDark) XCTAssertEqual(tuning.gpuVectorD.y, tuning.legendEmissionMax) XCTAssertEqual(tuning.gpuVectorE.x, tuning.fillLightStrength) + XCTAssertEqual(tuning.gpuVectorE.y, tuning.fillSpecularStrength) + XCTAssertEqual(tuning.gpuVectorE.z, tuning.wearStrength) + XCTAssertEqual(tuning.gpuVectorE.w, tuning.highlightJitter) + XCTAssertEqual(tuning.vignetteVector.x, tuning.vignetteStrength) + XCTAssertEqual(tuning.vignetteVector.y, tuning.vignetteInnerRadius) + XCTAssertEqual(tuning.gpuVectorE.x, tuning.fillLightStrength) XCTAssertEqual(tuning.gpuVectorE.z, tuning.wearStrength) }