@@ -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
156152vec3 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