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- CHANGELOG.md +1 -0
- app.js +7 -3
- index.html +2 -2
- sw.js +7 -4
- tear.js +1 -1
- 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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=
|
| 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 |
-
//
|
| 61 |
-
|
|
|
|
| 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": "
|
| 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.",
|