Skip to content

Commit 6fb2126

Browse files
committed
Heatmap diff: additive per-sign blending so red + green coexist
Over-blend volume compositing let bright green peaks (concentrated bond density) saturate the ray's alpha before diffuse red (broadly distributed "DFT removes density from atom cores") could contribute. For signed/diff data both signs are physically present at comparable integrals — the old path just couldn't show it. New path: in signed mode, integrate positive and negative magnitudes separately along the ray, then soft-saturate each (`1 - exp(-x*opacity)`) and combine with fixed green/red color anchors. No front-to-back occlusion between the two signs, so concentrated peaks don't hide diffuse signal. Unsigned (turbo) path is unchanged.
1 parent d84630b commit 6fb2126

1 file changed

Lines changed: 46 additions & 26 deletions

File tree

pkgs/core/src/components/HeatmapRenderer.tsx

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,13 @@ float chebyshevDist(vec3 p) {
141141
max(max(0.0, -p.z), max(p.z - 1.0, 0.0))));
142142
}
143143
144-
// Diverging colormap for signed diffs: green (added) for val > 0.5, red (removed)
145-
// for val < 0.5. Critically, color goes to (0,0,0) at val=0.5 so ray-march accumulation
146-
// near zero stays invisible regardless of alpha — fixes the issue where turbo's
147-
// yellow-green midpoint leaked visible color into low-magnitude regions.
148-
vec3 diverging(float val) {
149-
float u = (val - 0.5) * 2.0; // [-1, 1]
150-
float mag = pow(abs(u), 0.65); // emphasize mid-range, still 0 at u=0
151-
vec3 pos = vec3(0.10, 0.95, 0.30); // green for "DFT added density"
152-
vec3 neg = vec3(1.00, 0.20, 0.10); // red for "DFT removed density"
153-
return mag * (u >= 0.0 ? pos : neg);
154-
}
144+
// Diverging color anchors for signed diffs. Green (added) is "DFT > input";
145+
// red (removed) is "DFT < input". Used by the additive signed code path which
146+
// accumulates positive and negative integrals along each ray separately so
147+
// concentrated peaks (bonds, green) cannot occlude diffuse low-magnitude signal
148+
// (anti-bond, red) the way front-to-back over-blending did.
149+
const vec3 SIGNED_POS = vec3(0.10, 0.95, 0.30);
150+
const vec3 SIGNED_NEG = vec3(1.00, 0.20, 0.10);
155151
156152
vec3 turbo(float x) {
157153
const vec4 kRedVec4 = vec4(0.13572138, 4.61539260, -42.66032258, 132.13108234);
@@ -195,11 +191,16 @@ void main() {
195191
float dtRef = 1.0 / 256.0;
196192
float t = tHit.x;
197193
194+
// Unsigned uses over-blend (front-to-back compositing); signed uses additive
195+
// per-sign integrals, then converts to color at the end. Both update inside
196+
// the same loop so the ray-march, atom-clip, and padding-fade logic stay shared.
198197
vec4 acc = vec4(0.0);
198+
float posInt = 0.0;
199+
float negInt = 0.0;
199200
200201
for (int i = 0; i < 512; i++) {
201202
if (t > tHit.y) break;
202-
if (acc.a > 0.99) break;
203+
if (uSigned == 0 && acc.a > 0.99) break;
203204
204205
vec3 fracP = fracOrigin + fracDir * t;
205206
@@ -241,24 +242,32 @@ void main() {
241242
if (uPadding > 0.0 && uFade > 0.0) {
242243
float d = chebyshevDist(fracP);
243244
if (d > 0.0) {
244-
float t = clamp(d / uPadding, 0.0, 1.0);
245-
float linearFade = pow(1.0 - t, uFade);
246-
float shellCutoff = 1.0 - smoothstep(0.7, 1.0, t);
245+
float tFade = clamp(d / uPadding, 0.0, 1.0);
246+
float linearFade = pow(1.0 - tFade, uFade);
247+
float shellCutoff = 1.0 - smoothstep(0.7, 1.0, tFade);
247248
float effFade = linearFade * shellCutoff;
248249
float neutralVal = uSigned > 0 ? 0.5 : 0.0;
249250
val = mix(neutralVal, val, effFade);
250251
}
251252
}
252253
253-
// In signed (diverging) mode the texture stores values centered at 0.5
254-
// (val=0.5 == raw 0). Color always uses val so 0 lands at turbo's mid-point;
255-
// alpha gates on abs(val - 0.5) * 2 so the near-zero band is transparent.
256-
float effVal = uSigned > 0 ? clamp(abs(val - 0.5) * 2.0, 0.0, 1.0) : val;
257-
258-
if (effVal > uLowCutoff) {
259-
vec3 col = uSigned > 0 ? diverging(val) : turbo(val);
260-
// Renormalize after subtracting low cutoff so effVal=1 still maps to alpha=1.
261-
float v = clamp((effVal - uLowCutoff) / max(1.0 - uLowCutoff, 1e-4), 0.0, 1.0);
254+
if (uSigned > 0) {
255+
// Signed (diff) mode: split each sample into a positive or negative
256+
// magnitude and add it to a per-sign integral along the ray. No
257+
// front-to-back occlusion between the two signs, so a concentrated
258+
// green peak cannot block diffuse red behind it (and vice versa).
259+
float sv = val - 0.5; // [-0.5, 0.5]
260+
float mag = clamp(abs(sv) * 2.0, 0.0, 1.0); // [0, 1]
261+
if (mag > uLowCutoff) {
262+
float m = (mag - uLowCutoff) / max(1.0 - uLowCutoff, 1e-4);
263+
float w = pow(m, uGamma) * (dt / dtRef);
264+
if (sv > 0.0) posInt += w;
265+
else negInt += w;
266+
}
267+
} else if (val > uLowCutoff) {
268+
// Unsigned (density) mode: turbo + over-blend.
269+
vec3 col = turbo(val);
270+
float v = clamp((val - uLowCutoff) / max(1.0 - uLowCutoff, 1e-4), 0.0, 1.0);
262271
float baseA = pow(v, uGamma) * uOpacity;
263272
float a = 1.0 - pow(max(1.0 - baseA, 0.0), dt / dtRef);
264273
@@ -269,8 +278,19 @@ void main() {
269278
t += dt;
270279
}
271280
272-
if (acc.a < 0.001) discard;
273-
gl_FragColor = acc;
281+
if (uSigned > 0) {
282+
// Soft-saturate each integral so a long red ray and a short bright green
283+
// ray can both peak at full intensity without nuking the other.
284+
float p = 1.0 - exp(-posInt * uOpacity);
285+
float n = 1.0 - exp(-negInt * uOpacity);
286+
vec3 col = p * SIGNED_POS + n * SIGNED_NEG;
287+
float a = max(p, n);
288+
if (a < 0.001) discard;
289+
gl_FragColor = vec4(col, a);
290+
} else {
291+
if (acc.a < 0.001) discard;
292+
gl_FragColor = acc;
293+
}
274294
}
275295
`
276296

0 commit comments

Comments
 (0)