Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions apps/overlay/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@
.setting-row button:hover {
background: rgba(255,255,255,0.15);
}
/* The select's translucent dark bg + light text looks right on the panel,
but the OPEN option list is drawn by the OS — on Windows it came up
light-mode, leaving unselected options as light text on a white popup
(unreadable). `color-scheme: dark` renders the native popup dark; the
explicit option colors are a solid-background fallback across platforms. */
.setting-row select { color-scheme: dark; }
.setting-row select option {
background: #26262c;
color: #eee;
}

.setting-row input[type="color"] {
width: 32px;
Expand Down Expand Up @@ -1045,15 +1055,15 @@ <h3>Window Display</h3>
<input type="range" id="grip-glow-length" min="1" max="5" step="1" value="4" style="width:90px;accent-color:#33ddaa;">
</div>
<div class="setting-row">
<label title="Show the grip-sense bars on the controller's left/right sides (Steam Controller). When shown, the bars highlight on touch.">Show Grip Sense Bars</label>
<label title="Show the grip-sense bars on the controller's left/right sides (Steam Controller). The bars sit grey at rest and light up to the grip color while that side is gripped — independent of the Grip Sense Glow.">Show Grip Sense Bars</label>
<input type="checkbox" id="grip-bars-toggle" autocomplete="off">
</div>
<div class="setting-row">
<label>Grip Brightness</label>
<label title="Peak brightness of the Grip Sense Glow while a grip is held. Affects the glow only, not the bars.">Grip Glow Brightness</label>
<input type="range" id="grip-brightness" min="10" max="100" value="50" style="width:90px;accent-color:#33ddaa;">
</div>
<div class="setting-row">
<label title="Color of the grip-sense glow + bar highlight. Defaults to the Highlight Color; set here to override it.">Grip Sense Color</label>
<label title="Color of the grip-sense glow and the lit grip bars. Defaults to the Highlight Color; set here to override it.">Grip Sense Color</label>
<input type="color" id="grip-color" value="#ff0000">
</div>
<div class="setting-row">
Expand Down
147 changes: 117 additions & 30 deletions packages/visualizer/src/controller-overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ const PRESS_GLOW = 1.5;
// between render frames still shows a noticeably-sized circle, then keeps
// expanding to full while held.
const TRACKPAD_FILL_MIN = 0.45;
// Grip-sense bars sit at this inert dark grey at rest and brighten to the grip
// color while that side is gripped (issue #74 — activation should read on the
// bar itself, not just the handle glow).
const GRIP_BAR_REST_COLOR = 0x44444c;
// Emissive intensity of a grip-sense BAR while its side is gripped. Fixed on
// purpose — the Grip Glow Brightness slider drives the GLOW only, so the bar and
// the glow stay independently defined.
const GRIP_BAR_LIT_EMISSIVE = 0.6;
const FLOAT_ZERO = new THREE.Vector3(); // shared read-only lerp target (parts seated)

export class ControllerOverlay {
Expand Down Expand Up @@ -583,6 +591,10 @@ export class ControllerOverlay {
this._repositionGripBars(profile);
// Honor the current "show grip bars" setting on this freshly-loaded model.
this._applyGripBarsVisible();
// Set the bars to their inert rest grey + clone their materials (they're not
// in a theme group, so this is the only thing that colors them); setGripState
// then lights them to the grip color while a side is gripped.
this._applyGripBarColor();

// Diagnostic: per-mapping check at load time so we can tell whether
// each gamepad-index → mesh path will animate at runtime. The press
Expand Down Expand Up @@ -2011,40 +2023,57 @@ export class ControllerOverlay {

/**
* Highlight the capacitive grip sensors while held (digital on/off). Two
* visuals: (1) emissive glow on the grip meshes — only seen when the back of
* the controller faces the camera; and (2) a billboard glow marker per grip
* rendered ON TOP (depthTest off, at the top of the controller) so grip state
* reads at ANY angle. Toggle both via setGripVisible. Intensity lerps.
* INDEPENDENT indicators, each with its own toggle:
* • GLOW — an on-top billboard/cylinder marker per grip, rendered with
* depthTest off so it reads at ANY angle. Peak opacity = the Grip Glow
* Brightness setting. Gated by setGripVisible (_gripEnabled).
* • BARS — the left/right side strip meshes. They ease from a rest grey to
* the grip color (with a fixed emissive lift) while that side is gripped,
* so the bar itself shows activation (issue #74). Gated by
* setGripBarsVisible (_gripBarsVisible); brightness does NOT affect them.
* @param {{left:boolean, right:boolean}} grips
*/
setGripState(grips) {
const map = PROFILES[this.controllerType]?.gripMeshes;
if (!map || !grips) return;
const gripCol = this._gripColorC || (this._gripColorC = new THREE.Color());
gripCol.setHex(this._gripColor);
const restCol = this._gripBarRestC || (this._gripBarRestC = new THREE.Color(GRIP_BAR_REST_COLOR));
const black = this._gripBlackC || (this._gripBlackC = new THREE.Color(0x000000));
for (const side of ['left', 'right']) {
const gripped = !!grips[side];
// The bars and the on-top glow markers are INDEPENDENT:
// - bars highlight on touch whenever the bars are shown (their own glow),
// - the on-top markers are the separate, toggleable "glow".
const barOn = this._gripBarsVisible && gripped;
const glowOn = this._gripEnabled && gripped;
// (1) bar mesh emissive highlight (on touch, if the bars are visible)
const obj = this.meshes[map[side]];
const mesh = obj && (obj.isMesh ? obj : obj.children?.find((c) => c.isMesh));
const mat = mesh?.material;
if (mat && 'emissive' in mat) {
mat.emissive.set(this._gripColor); // set each frame so a color change applies live
// Bar glow scales with the brightness slider too, so it can reach full
// brightness at max (and stays subtle at the lower default).
const barTarget = barOn ? this._gripBrightness * 1.5 : 0;
mat.emissiveIntensity = THREE.MathUtils.lerp(mat.emissiveIntensity, barTarget, LERP_SPEED);
}
// (2) always-on-top billboard glow marker (brightness = peak opacity)
// On-top billboard glow marker — the handle activation cue. Brightness
// sets its peak opacity; lerps in/out. Gated by the glow toggle.
const marker = this._gripMarkers?.[side];
if (marker) marker.material.opacity = THREE.MathUtils.lerp(marker.material.opacity, glowOn ? this._gripBrightness : 0, LERP_SPEED);
if (marker) {
marker.material.opacity = THREE.MathUtils.lerp(
marker.material.opacity, (this._gripEnabled && gripped) ? this._gripBrightness : 0, LERP_SPEED
);
}
// Bar mesh — ease toward the grip color (lit) while that side is gripped,
// back to the inert rest grey when released, so the bar itself reads the
// activation (issue #74). Gated by the bars toggle, independent of glow.
const barActive = this._gripBarsVisible && gripped;
const bar = this._gripBarMesh(map[side]);
if (bar?.material?.color) {
bar.material.color.lerp(barActive ? gripCol : restCol, LERP_SPEED);
if ('emissive' in bar.material) {
bar.material.emissive.lerp(barActive ? gripCol : black, LERP_SPEED);
bar.material.emissiveIntensity = THREE.MathUtils.lerp(
bar.material.emissiveIntensity ?? 0, barActive ? GRIP_BAR_LIT_EMISSIVE : 0, LERP_SPEED
);
}
}
}
}

/** Toggle the grip-sense glow (mesh emissive + on-top markers). */
// The Mesh that carries a grip-bar's material (the GLB node may wrap it).
_gripBarMesh(name) {
const obj = this.meshes[name];
return obj && (obj.isMesh ? obj : obj.children?.find((c) => c.isMesh)) || null;
}

/** Toggle the grip-sense GLOW (the on-top marker only — not the bars). */
setGripVisible(enabled) {
this._gripEnabled = !!enabled;
}
Expand Down Expand Up @@ -2081,12 +2110,39 @@ export class ControllerOverlay {
this._gripBrightness = Math.max(0, Math.min(1, value));
}

/** Grip highlight color (markers + mesh glow); CSS hex e.g. '#3388ff'. */
/** Grip highlight color (markers + the bar's lit state); CSS hex e.g. '#3388ff'. */
setGripColor(hexColor) {
this._gripColor = new THREE.Color(hexColor).getHex();
if (this._gripMarkers) {
for (const side of ['left', 'right']) this._gripMarkers[side]?.material.color.setHex(this._gripColor);
}
// The bars' rest grey is independent of the grip color; setGripState picks
// up the new _gripColor for the lit state on the next report.
}

// Reset the grip-sense BAR meshes to their inert rest appearance (grey, no
// emissive); setGripState lights them to the grip color while gripped. The
// bars are excluded from the body/accent theme groups (PR #76) — otherwise
// they'd take the body color and vanish into it — so they're driven here.
// Each bar's material is cloned once so left/right light up independently
// (GLB models often share one material across both meshes).
_applyGripBarColor() {
const map = PROFILES[this.controllerType]?.gripMeshes;
if (!map) return;
for (const side of ['left', 'right']) {
const mesh = this._gripBarMesh(map[side]);
if (!mesh?.material?.color) continue;
if (!mesh.material._gripCloned) {
mesh.material = mesh.material.clone();
mesh.material._gripCloned = true;
}
mesh.material.color.setHex(GRIP_BAR_REST_COLOR);
if ('emissive' in mesh.material) {
mesh.material.emissive.setHex(0x000000);
mesh.material.emissiveIntensity = 0;
}
mesh.material.needsUpdate = true;
}
}

// Set up the on-top grip glow markers. Computes each marker's pinned handle
Expand Down Expand Up @@ -2165,6 +2221,27 @@ export class ControllerOverlay {
const w = this._gripGlowWidth;
const len = this._gripGlowLength;
const isCircle = w === 1 && len === 1;
// Yaw (about the vertical Y axis) that swings each glow's front (+Z, the
// bumper/trigger end) toward the controller's centerline — see it in the Top
// view. Per-profile knob in degrees; sign flips per side below.
const yaw = THREE.MathUtils.degToRad(PROFILES[this.controllerType]?.gripMarkerYaw ?? 0);
// Band texture for the cylinder glow: uniform AROUND the tube (so the color
// wraps the whole circumference, not just one facing strip like the radial
// texture did), and a bright plateau filling MOST of the length with just a
// soft fade at the very ends. The Length slider scales the cylinder height,
// so this keeps the glow filling ~the whole cylinder (max Length ≈ the full
// handle) instead of only a short middle band. Maps v (0..1) → height.
if (!this._barGlowTexture) {
const c = document.createElement('canvas'); c.width = 4; c.height = 64;
const ctx = c.getContext('2d');
const g = ctx.createLinearGradient(0, 0, 0, 64);
g.addColorStop(0.00, 'rgba(255,255,255,0)');
g.addColorStop(0.10, 'rgba(255,255,255,0.95)');
g.addColorStop(0.90, 'rgba(255,255,255,0.95)');
g.addColorStop(1.00, 'rgba(255,255,255,0)');
ctx.fillStyle = g; ctx.fillRect(0, 0, 4, 64);
this._barGlowTexture = new THREE.CanvasTexture(c);
}
const markers = {};
for (const side of ['left', 'right']) {
const spec = specs[side];
Expand All @@ -2184,20 +2261,26 @@ export class ControllerOverlay {
const d = thin * 0.9;
obj.scale.set(d, d, 1);
} else {
// NORMAL blending (not additive): additive adds the glow to whatever's
// behind it, so it washed out over the bright white body but popped over
// the dark trackpads. Normal blending shows the grip color consistently
// regardless of what's behind/in front (fully solid at full brightness).
// Read as a soft GLOW that's still visible over the light handles. The
// band texture wraps the FULL circumference (color all the way around the
// tube, consistent from any angle) and fills most of the length, fading
// softly only at the very ends. NORMAL blending keeps
// it visible on any background. (Pure additive looked like a real glow
// but vanished over the bright body — additive adds to the destination,
// and there's no headroom over near-white, so it only showed over dark
// areas. A radial texture only lit one facing strip of the tube.)
const mat = new THREE.MeshBasicMaterial({
map: this._barGlowTexture,
color: this._gripColor, transparent: true, opacity: 0,
blending: THREE.NormalBlending, depthTest: false, depthWrite: false,
});
// Cylinder: radius ≤ half the handle's thin cross-section; height ≤ the
// handle's Z length. Built along Y, rotated so its axis runs
// front-to-back (model Z, the handle's long axis).
// front-to-back (model Z, the handle's long axis). Open-ended (no cap
// disks) so the band fades cleanly at the ends with no flat cap showing.
const radius = (thin * 0.45) * (w / 5);
const height = (spec.dimZ * 1.35) * (len / 5); // ~50% longer than before
const geo = new THREE.CylinderGeometry(radius, radius, height, 20);
const geo = new THREE.CylinderGeometry(radius, radius, height, 20, 1, true);
// Lay the cylinder front-to-back (axis → Z), then tilt it so the back
// (−Z) end rises ~1/3 of its length, angling it inline with the handle.
// asin(2/3): with a centre pivot the end rises (height/2)·(2/3) = height/3.
Expand All @@ -2209,6 +2292,10 @@ export class ControllerOverlay {
obj.renderOrder = 10; // draw after the model so it sits on top
obj.position.copy(spec.pos);
obj.position.y += yLift;
// Angle the bumper/trigger end inward toward center. Sign mirrors per
// side: left handle (−X) yaws −, right handle (+X) yaws +. No-op on the
// circle sprite (it billboards).
obj.rotation.y = (spec.pos.x < 0 ? -1 : 1) * yaw;
this.bodyGroup.add(obj);
markers[side] = obj;
}
Expand Down
20 changes: 15 additions & 5 deletions packages/visualizer/src/controller-profiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,24 @@ export const PROFILES = {
// Lift the glow up a little into the handle body (fraction toward the top).
gripMarkerHeight: 0.1,
// Nudge each glow outward into the handle (away from the body center), as a
// fraction of the model width.
gripMarkerSideOffset: 0.06,
// Two-tone (ceski/Larf SC2 look, issue #61): light body = shells + grips
// only. Everything else is dark accent — the trackpads, the system buttons
// fraction of the model width. Pushed farther out (was 0.06) so the glows
// sit over the controller handles where the hands grip, instead of bunched
// near the center (PR #76 feedback).
gripMarkerSideOffset: 0.14,
// Yaw each glow about the vertical axis (degrees) so its front (bumper/
// trigger) end angles toward the controller's centerline — best seen in the
// Top view. Sign is applied per side in code (left/right mirror).
gripMarkerYaw: 18,
// Two-tone (ceski/Larf SC2 look, issue #61): light body = shells only.
// The grip-sense bars (left/right_gripsense) are deliberately NOT in a
// theme group — they're driven on their own (a rest grey that lights to the
// grip color while that side is gripped) so they read as the grip-sense
// indicator instead of disappearing into the body color (PR #76, #74).
// Everything else is dark accent — the trackpads, the system buttons
// (view/menu/steam + the "…" quick-access), and the controls (sticks, face
// buttons, dpad, shoulders, triggers, paddles).
bodyColorMeshes: [
'top_shell', 'bottom_shell', 'left_gripsense', 'right_gripsense',
'top_shell', 'bottom_shell',
],
accentColorMeshes: [
'misc1', 'misc2', 'touchpad', 'touch_point1', 'touch_point2',
Expand Down
Loading