Chase commited on
Commit
b24a488
·
1 Parent(s): bf9ab42

Fix first load flash and black screen: no pow() of a negative base in the intro sweep shader (NaN on real GPUs, spread by bloom), guarded normal and rim terms, service worker serves exact ?v= matches only (older copies are the offline fallback), cache bust nan1

Browse files
Files changed (6) hide show
  1. CHANGELOG.md +1 -0
  2. app.js +7 -3
  3. index.html +2 -2
  4. sw.js +7 -4
  5. tear.js +1 -1
  6. version.json +1 -1
CHANGELOG.md CHANGED
@@ -16,6 +16,7 @@ The 0.9.x entries below are the pre-release series that led up to 1.0.0. Version
16
  ## 1.2.0 (2026-10-16)
17
 
18
  - **Lower resolution while dragging**: the 3D view draws at 0.75x or 0.5x pixel ratio while the camera moves and returns to full sharpness when it settles. Settings and Performance panel control, `unrollView.dragRes`, `?dragRes=off|light|strong`. See docs/CT_VIEW.md.
 
19
 
20
  ## 1.1.0 (2026-10-16)
21
 
 
16
  ## 1.2.0 (2026-10-16)
17
 
18
  - **Lower resolution while dragging**: the 3D view draws at 0.75x or 0.5x pixel ratio while the camera moves and returns to full sharpness when it settles. Settings and Performance panel control, `unrollView.dragRes`, `?dragRes=off|light|strong`. See docs/CT_VIEW.md.
19
+ - **Fix first load on real GPUs**: the intro scan line squared a negative number with `pow()`, which GPUs return as NaN; the bloom pass spread it into colour flashes and a black view. It is now squared by hand, and the normal and rim terms are guarded the same way. The service worker no longer answers a new `?v=` asset with an older cached copy (that copy is now only the offline fallback).
20
 
21
  ## 1.1.0 (2026-10-16)
22
 
app.js CHANGED
@@ -394,7 +394,8 @@ void main() {
394
  // soaked papyrus softens and sags
395
  // soaked charcoal swells a little and slumps where it turns to mush; it does not sag like wet paper
396
  if (uDmgOn > 0.5) { float wv = texture2D(uDmg, aLocal).a; p.z += wv * 0.35; p.y -= wv * wv * 0.35; }
397
- vN = normalize(mix(nc, vec3(0.0, 0.0, 1.0), e));
 
398
  vLocal = aLocal;
399
  vE = e;
400
  vec4 w = modelMatrix * vec4(p, 1.0);
@@ -422,7 +423,7 @@ void main() {
422
  vec3 v = normalize(cameraPosition - vW);
423
  float ndl = abs(dot(n, normalize(vec3(-0.35, 0.75, 0.55))));
424
  float ndv = abs(dot(n, v));
425
- float rim = pow(1.0 - ndv, 3.0);
426
  float diff = 0.28 + 0.72 * ndl;
427
 
428
  // X-ray: the CT intensity on the surface, cool and clinical
@@ -451,7 +452,10 @@ void main() {
451
  float revealed = smoothstep(uSweep - 1.5, uSweep + 1.5, vW.y);
452
  float layer = uLayer * revealed;
453
  vec3 col = mix(xray, inked, layer);
454
- float line = exp(-pow((vW.y - uSweep) / 0.35, 2.0));
 
 
 
455
  col += vec3(1.0, 0.7, 0.35) * line * 1.6;
456
  col += vec3(1.0, 0.6, 0.25) * uHi * 0.12;
457
  gl_FragColor = vec4(col, 1.0);
 
394
  // soaked papyrus softens and sags
395
  // soaked charcoal swells a little and slumps where it turns to mush; it does not sag like wet paper
396
  if (uDmgOn > 0.5) { float wv = texture2D(uDmg, aLocal).a; p.z += wv * 0.35; p.y -= wv * wv * 0.35; }
397
+ vec3 nm = mix(nc, vec3(0.0, 0.0, 1.0), e);
398
+ vN = dot(nm, nm) > 1e-8 ? normalize(nm) : vec3(0.0, 0.0, 1.0); // a zero normal would normalize to NaN
399
  vLocal = aLocal;
400
  vE = e;
401
  vec4 w = modelMatrix * vec4(p, 1.0);
 
423
  vec3 v = normalize(cameraPosition - vW);
424
  float ndl = abs(dot(n, normalize(vec3(-0.35, 0.75, 0.55))));
425
  float ndv = abs(dot(n, v));
426
+ float rim = pow(max(1.0 - ndv, 0.0), 3.0);
427
  float diff = 0.28 + 0.72 * ndl;
428
 
429
  // X-ray: the CT intensity on the surface, cool and clinical
 
452
  float revealed = smoothstep(uSweep - 1.5, uSweep + 1.5, vW.y);
453
  float layer = uLayer * revealed;
454
  vec3 col = mix(xray, inked, layer);
455
+ // pow() with a negative base is undefined in GLSL: real GPUs return NaN there, which the bloom pass smears over the
456
+ // whole frame (colour flashes, then black) during the intro sweep. Square it by hand.
457
+ float sd = (vW.y - uSweep) / 0.35;
458
+ float line = exp(-sd * sd);
459
  col += vec3(1.0, 0.7, 0.35) * line * 1.6;
460
  col += vec3(1.0, 0.6, 0.25) * uHi * 0.12;
461
  gl_FragColor = vec4(col, 1.0);
index.html CHANGED
@@ -63,7 +63,7 @@ if (!/(^|[#&])s=/.test(location.hash)) ['scrolls/index.json', 'scrolls/PHerc0139
63
  "./lib/apispec.js": "./lib/apispec.js?v=20261016vol3",
64
  "./lib/embed.js": "./lib/embed.js?v=20261016vol3",
65
  "./lib/version.js": "./lib/version.js?v=20261016vol3",
66
- "./tear.js": "./tear.js?v=20261016vol3",
67
  "./damage.js": "./damage.js?v=20261016vol3"
68
  } }
69
  </script>
@@ -251,7 +251,7 @@ if (!/(^|[#&])s=/.test(location.hash)) ['scrolls/index.json', 'scrolls/PHerc0139
251
  </div>
252
  </section>
253
 
254
- <script type="module" src="app.js?v=20261016vol3"></script>
255
  <script type="module" src="polish.js?v=20261016vol3"></script>
256
  <script type="module" src="lib/bench.js?v=20261016vol3"></script>
257
  <script type="module" src="ui.js?v=20261016vol3"></script>
 
63
  "./lib/apispec.js": "./lib/apispec.js?v=20261016vol3",
64
  "./lib/embed.js": "./lib/embed.js?v=20261016vol3",
65
  "./lib/version.js": "./lib/version.js?v=20261016vol3",
66
+ "./tear.js": "./tear.js?v=20261016vol3nan",
67
  "./damage.js": "./damage.js?v=20261016vol3"
68
  } }
69
  </script>
 
251
  </div>
252
  </section>
253
 
254
+ <script type="module" src="app.js?v=20261016vol3nan"></script>
255
  <script type="module" src="polish.js?v=20261016vol3"></script>
256
  <script type="module" src="lib/bench.js?v=20261016vol3"></script>
257
  <script type="module" src="ui.js?v=20261016vol3"></script>
sw.js CHANGED
@@ -57,9 +57,12 @@ self.addEventListener('message', (e) => { if (e.data === 'version') e.source?.po
57
 
58
  const isData = (url) => /\/(data|scrolls)\//.test(url.pathname);
59
  async function fromCache(req) {
60
- // assets are requested as file.js?v=..., so fall back to a match that ignores the query string
61
- return (await caches.match(req)) || (await caches.match(req, { ignoreSearch: true }));
 
62
  }
 
 
63
 
64
  self.addEventListener('fetch', (e) => {
65
  const req = e.request;
@@ -80,7 +83,7 @@ self.addEventListener('fetch', (e) => {
80
  e.respondWith(fromCache(req).then((hit) => hit || fetch(req).then((r) => {
81
  if (r.ok && r.status === 200) { const copy = r.clone(); caches.open(url.pathname.includes('/data/hi/') || !DATA_FILES.some((f) => url.pathname.endsWith(f)) ? RUNTIME : DATA).then((c) => c.put(req, copy)); }
82
  return r;
83
- })));
84
  return;
85
  }
86
  // shell, three.js and fonts: stale while revalidate
@@ -88,7 +91,7 @@ self.addEventListener('fetch', (e) => {
88
  const net = fetch(req).then((r) => {
89
  if (r.ok || r.type === 'opaque') { const copy = r.clone(); caches.open(same ? SHELL : RUNTIME).then((c) => c.put(req, copy)); }
90
  return r;
91
- }).catch(() => hit || Response.error());
92
  if (hit) { e.waitUntil(net.catch(() => {})); return hit; }
93
  return net;
94
  }));
 
57
 
58
  const isData = (url) => /\/(data|scrolls)\//.test(url.pathname);
59
  async function fromCache(req) {
60
+ // exact match only: a new release asks for file.js?v=<new>, and answering that with the old file.js?v=<old> mixes
61
+ // versions on the first load after an update (an old app.js against a new index.html)
62
+ return caches.match(req);
63
  }
64
+ // offline fallback: any cached copy of the same path, whatever its ?v=
65
+ const anyCopy = (req) => caches.match(req, { ignoreSearch: true });
66
 
67
  self.addEventListener('fetch', (e) => {
68
  const req = e.request;
 
83
  e.respondWith(fromCache(req).then((hit) => hit || fetch(req).then((r) => {
84
  if (r.ok && r.status === 200) { const copy = r.clone(); caches.open(url.pathname.includes('/data/hi/') || !DATA_FILES.some((f) => url.pathname.endsWith(f)) ? RUNTIME : DATA).then((c) => c.put(req, copy)); }
85
  return r;
86
+ }).catch(async () => (await anyCopy(req)) || Response.error())));
87
  return;
88
  }
89
  // shell, three.js and fonts: stale while revalidate
 
91
  const net = fetch(req).then((r) => {
92
  if (r.ok || r.type === 'opaque') { const copy = r.clone(); caches.open(same ? SHELL : RUNTIME).then((c) => c.put(req, copy)); }
93
  return r;
94
+ }).catch(async () => hit || (await anyCopy(req)) || Response.error());
95
  if (hit) { e.waitUntil(net.catch(() => {})); return hit; }
96
  return net;
97
  }));
tear.js CHANGED
@@ -1169,7 +1169,7 @@ ${DMG_EDGE_T}
1169
  // black carbon with a faint glassy sheen on fresh fracture faces
1170
  float spec = pow(max(dot(reflect(-l, n), v), 0.0), 24.0) * 0.38;
1171
  vec3 base = mix(vec3(0.035, 0.026, 0.019), vec3(0.17, 0.12, 0.085), vTone);
1172
- diff += 0.25 * pow(1.0 - abs(dot(n, v)), 2.0); // rim light so dark flakes read against the dark floor
1173
  gl_FragColor = vec4(base * diff + spec, 1.0);
1174
  }`,
1175
  });
 
1169
  // black carbon with a faint glassy sheen on fresh fracture faces
1170
  float spec = pow(max(dot(reflect(-l, n), v), 0.0), 24.0) * 0.38;
1171
  vec3 base = mix(vec3(0.035, 0.026, 0.019), vec3(0.17, 0.12, 0.085), vTone);
1172
+ diff += 0.25 * pow(max(1.0 - abs(dot(n, v)), 0.0), 2.0); // rim light so dark flakes read against the dark floor
1173
  gl_FragColor = vec4(base * diff + spec, 1.0);
1174
  }`,
1175
  });
version.json CHANGED
@@ -2,7 +2,7 @@
2
  "name": "unroll",
3
  "version": "1.4.0",
4
  "date": "2026-10-16",
5
- "sha": "ce6b88b",
6
  "license": "Apache-2.0",
7
  "data_license": "CC-BY-NC-4.0",
8
  "shaNote": "Short SHA of the commit this build was stamped on (its parent, since a commit cannot contain its own hash). Run node tools/stamp_version.mjs patch before each push.",
 
2
  "name": "unroll",
3
  "version": "1.4.0",
4
  "date": "2026-10-16",
5
+ "sha": "bf9ab42",
6
  "license": "Apache-2.0",
7
  "data_license": "CC-BY-NC-4.0",
8
  "shaNote": "Short SHA of the commit this build was stamped on (its parent, since a commit cannot contain its own hash). Run node tools/stamp_version.mjs patch before each push.",