@plasius/gpu-demo-viewer 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -1
- package/NFR.md +4 -0
- package/README.md +30 -0
- package/docs/adrs/adr-0003-reviewed-model-library.md +13 -0
- package/docs/adrs/adr-0004-model-quality-controls.md +13 -0
- package/docs/adrs/index.md +2 -0
- package/docs/amber-fixed-qualification.json +54 -0
- package/docs/catalogue-production-rollout.md +80 -0
- package/docs/model-inspection-uhd.md +11 -0
- package/docs/model-library-review.md +38 -0
- package/docs/model-review-release.md +11 -0
- package/model-library/acquire.js +54 -0
- package/model-library/catalogue.json +965 -0
- package/model-library/controller.js +45 -0
- package/model-library/index.html +35 -0
- package/model-library/main.js +76 -0
- package/model-library/research-catalogue.json +2625 -0
- package/model-library/rollout.js +28 -0
- package/model-library/rollout.json +461 -0
- package/model-library/styles.css +9 -0
- package/model-library/vite.config.js +12 -0
- package/package.json +14 -6
- package/scripts/acquire-model-library.mjs +8 -0
- package/viewer-manifest.js +15 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function reviewEnabled(href) {
|
|
2
|
+
const url = new URL(href);
|
|
3
|
+
return ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
|
|
4
|
+
&& url.searchParams.get('review') === 'first-twelve';
|
|
5
|
+
}
|
|
6
|
+
export function fixtureRequest(asset, base) {
|
|
7
|
+
const safe = path => typeof path === 'string' && /^[\w./-]+$/u.test(path)
|
|
8
|
+
&& !path.startsWith('/') && !path.split('/').includes('..');
|
|
9
|
+
if (!safe(asset.key) || asset.key.includes('/') || !safe(asset.entry)
|
|
10
|
+
|| !asset.files.every(file => safe(file.path))) throw new Error('Unsafe fixture path.');
|
|
11
|
+
const root = new URL(`assets/${asset.key}/`, base);
|
|
12
|
+
return { modelUrl: new URL(asset.entry, root).href,
|
|
13
|
+
integrity: Object.fromEntries(asset.files.map(f => [new URL(f.path, root).href, f.integrity])) };
|
|
14
|
+
}
|
|
15
|
+
export function createReviewController({ load, onState }) {
|
|
16
|
+
let abort, active, generation = 0, disposed = false, paused = false;
|
|
17
|
+
return {
|
|
18
|
+
async select(asset) {
|
|
19
|
+
if (disposed) return;
|
|
20
|
+
const id = ++generation;
|
|
21
|
+
abort?.abort(); active?.destroy(); active = undefined;
|
|
22
|
+
abort = new AbortController();
|
|
23
|
+
if (!asset.entry) { onState({kind:'missing', asset, message:'Download deferred. This model is not installed.'}); return; }
|
|
24
|
+
onState({kind:'loading',asset});
|
|
25
|
+
try {
|
|
26
|
+
const runtime = await load(asset, abort.signal);
|
|
27
|
+
if (disposed || id !== generation) { runtime.destroy(); return; }
|
|
28
|
+
active = runtime; active.setPaused?.(paused); onState({kind:'ready',asset,runtime});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!disposed && id === generation) onState({kind:'error',asset,message:error.message});
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async setQuality(quality) {
|
|
34
|
+
const runtime=active;
|
|
35
|
+
try { await runtime?.setQuality(quality); }
|
|
36
|
+
catch(error) { if(!disposed && active===runtime) throw error; }
|
|
37
|
+
},
|
|
38
|
+
setPaused(value) { paused=value;active?.setPaused?.(value); },
|
|
39
|
+
reset() { active?.reset(); },
|
|
40
|
+
destroy() {
|
|
41
|
+
if (disposed) return;
|
|
42
|
+
disposed = true; generation++; abort?.abort(); active?.destroy(); active = undefined;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3
|
+
<title>Plasius · Model studies</title><link rel="icon" href="data:,"><link rel="stylesheet" href="./styles.css">
|
|
4
|
+
<script type="importmap">
|
|
5
|
+
{
|
|
6
|
+
"imports": {
|
|
7
|
+
"@plasius/gpu-shared/model-inspection": "../node_modules/@plasius/gpu-shared/dist/model-inspection.js",
|
|
8
|
+
"@plasius/gpu-shared/feedback-diagnostics": "../node_modules/@plasius/gpu-shared/dist/feedback-diagnostics.js",
|
|
9
|
+
"@plasius/gpu-renderer": "../node_modules/@plasius/gpu-renderer/dist/index.js",
|
|
10
|
+
"@plasius/gpu-camera": "../node_modules/@plasius/gpu-camera/dist/index.js",
|
|
11
|
+
"@plasius/gpu-camera-controls": "../node_modules/@plasius/gpu-camera-controls/dist/index.js",
|
|
12
|
+
"@plasius/gpu-camera-controls/browser": "../node_modules/@plasius/gpu-camera-controls/dist/browser.js",
|
|
13
|
+
"@plasius/schema/feedback-diagnostics-vocabulary": "../node_modules/@plasius/schema/dist/feedback-diagnostics-vocabulary.js"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</script></head>
|
|
17
|
+
<body><header><a href="https://plasius.co.uk/" target="_blank" rel="noopener">PLASIUS</a><span>Renderer workshop · Local review</span></header>
|
|
18
|
+
<main><div class="intro"><p class="eyebrow">From source to scene</p><h1>Model studies</h1><p>Eleven assets. One renderer. Explore the surfaces, silhouettes and details that will shape our next improvements.</p></div>
|
|
19
|
+
<div class="workspace"><aside><label for="model">Choose a model</label><select id="model" disabled></select><p id="category" class="muted"></p><h2 id="name"></h2><p id="target"></p><p id="note"></p><div id="metrics" class="metrics"></div>
|
|
20
|
+
<details><summary>Source and licence</summary><p id="credits"></p><p><a id="source" target="_blank" rel="noopener">Original model</a> · <a id="licence" target="_blank" rel="noopener"></a></p><p>Original files retained. Framing is normalized for inspection. Powered by Poly Haven for its asset metadata.</p></details>
|
|
21
|
+
<details><summary>Camera help</summary><p id="help">Drag to orbit. Shift-drag or right-drag to truck. Alt-drag to pan. Scroll to move closer; Shift-scroll changes the lens. Touch: one finger orbits; two fingers move and pinch. Keyboard: arrows orbit, Shift-arrows truck, + / − move closer or further, Home resets. Keys 1–5 choose orbit, truck, pan, dolly or zoom. Press ? for a brief reminder.</p></details>
|
|
22
|
+
<details open><summary>Review focus</summary><p id="review"></p><p>This is an inspection pass, not a visual-quality approval. Source animations are retained on disk; this view displays static geometry.</p></details></aside>
|
|
23
|
+
<section class="stage" aria-label="Model inspection">
|
|
24
|
+
<fieldset id="quality" class="quality" disabled><legend>Render quality</legend>
|
|
25
|
+
<div><label for="samples">Sample target</label><select id="samples" aria-describedby="quality-help"><option>1</option><option>8</option><option>16</option><option>32</option><option selected>64</option><option>128</option><option>256</option></select></div>
|
|
26
|
+
<div><label for="bounces">Light bounces</label><select id="bounces"><option>2</option><option>4</option><option selected>6</option><option>8</option><option>10</option><option>12</option></select></div>
|
|
27
|
+
<div><label for="resolution">Render size</label><select id="resolution"><option value="480">480 px · Quick</option><option value="800" selected>800 px · Balanced</option><option value="1280">1280 px · Detailed</option><option value="1920x1080">1920 × 1080 · Full HD</option><option value="2560x1440">2560 × 1440 · QHD</option><option value="3840x2160">3840 × 2160 · 4K UHD</option></select></div>
|
|
28
|
+
<label class="toggle" for="denoise"><input id="denoise" type="checkbox" checked> Denoising</label>
|
|
29
|
+
<p id="quality-help">Hold the camera still to refine. Higher targets reduce noise; more bounces reveal indirect light. HD, QHD and 4K trace at the selected size, even in a smaller window. Higher resolution costs more GPU time; use one sample for a quick inspection.</p>
|
|
30
|
+
</fieldset>
|
|
31
|
+
<div class="refinement"><label id="refinement" for="sample-progress">Waiting for a model…</label><progress id="sample-progress" max="64" value="0"></progress></div>
|
|
32
|
+
<div class="canvas-wrap"><canvas id="canvas" width="800" height="600" tabindex="0" aria-label="Interactive Plasius model view" aria-describedby="help"></canvas><p id="hint" hidden>Drag to orbit · Shift-drag to move · Scroll to approach<br>Touch: drag or pinch · Keyboard: arrows, + / −, Home</p></div><p id="status" role="status" aria-live="polite">Preparing the review…</p></section></div>
|
|
33
|
+
<p class="footnote">Eleven retained source models. Gem is excluded from this rollout; no substitute is shown.</p></main>
|
|
34
|
+
<script type="module">import { mountLibrary } from './main.js'; mountLibrary().catch(error => { document.querySelector('#status').textContent = `Unable to open review: ${error.message}`; });</script>
|
|
35
|
+
</body></html>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createReviewController, fixtureRequest, reviewEnabled } from './controller.js';
|
|
2
|
+
|
|
3
|
+
export async function mountLibrary({document:doc=globalThis.document,window:win=globalThis.window,
|
|
4
|
+
fetch:request=globalThis.fetch,importRuntime=()=>import('@plasius/gpu-shared/model-inspection')}={}) {
|
|
5
|
+
const el=id=>doc.getElementById(id),status=el('status');
|
|
6
|
+
if (!reviewEnabled(win.location.href)) {
|
|
7
|
+
status.textContent='Local review is disabled. Open this page on localhost with ?review=first-twelve.';
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const response=await request(new URL('./catalogue.json',win.location.href));
|
|
11
|
+
if (!response.ok) throw new Error('Model catalogue could not be loaded.');
|
|
12
|
+
const {assets}=await response.json();
|
|
13
|
+
for(const asset of assets) {
|
|
14
|
+
const option=doc.createElement('option');option.value=asset.key;
|
|
15
|
+
option.textContent=asset.name+(asset.entry?'':' — deferred');el('model').append(option);
|
|
16
|
+
}
|
|
17
|
+
const {createModelInspection}=await importRuntime();
|
|
18
|
+
el('samples').value='64';el('bounces').value='6';el('resolution').value='800';el('denoise').checked=true;
|
|
19
|
+
const quality=()=>{
|
|
20
|
+
const [width,fixedHeight]=el('resolution').value.split('x').map(Number);
|
|
21
|
+
return {samplesPerPixel:Number(el('samples').value),maxDepth:Number(el('bounces').value),denoise:el('denoise').checked,
|
|
22
|
+
width,height:fixedHeight??Math.max(1,Math.min(1440,Math.round(width*el('canvas').clientHeight/el('canvas').clientWidth)))};
|
|
23
|
+
};
|
|
24
|
+
const refinement=p=>{
|
|
25
|
+
el('sample-progress').max=p.target;el('sample-progress').value=p.samples;
|
|
26
|
+
const phase={complete:'Complete',paused:'Paused',rendering:'Rendering',refining:'Refining'}[p.phase];
|
|
27
|
+
el('refinement').textContent=`${phase} · ${p.samples} / ${p.target} samples · ${p.width} × ${p.height}`;
|
|
28
|
+
};
|
|
29
|
+
const controller=createReviewController({
|
|
30
|
+
load:async(asset,signal)=>{
|
|
31
|
+
const request=fixtureRequest(asset,new URL('./',win.location.href));
|
|
32
|
+
el('canvas').hidden=false;
|
|
33
|
+
return createModelInspection({canvas:el('canvas'),...request,signal,enabled:true,
|
|
34
|
+
...quality(),quality:quality(),
|
|
35
|
+
onProgress:p=>{if(!signal.aborted)refinement(p);},
|
|
36
|
+
onHintChange:visible=>{el('hint').hidden=!visible;},
|
|
37
|
+
onError:error=>{if(signal.aborted)return;status.textContent=`Render failed: ${error.message}`;el('refinement').textContent='Refinement stopped';el('quality').disabled=true;}});
|
|
38
|
+
},
|
|
39
|
+
onState:state=>{
|
|
40
|
+
el('hint').hidden=true;el('quality').disabled=state.kind!=='ready';
|
|
41
|
+
if(state.kind==='loading'){el('sample-progress').value=0;el('refinement').textContent='Waiting for the first sample…';}
|
|
42
|
+
if(state.kind==='loading')status.textContent=`Loading ${state.asset.name} and checking its files…`;
|
|
43
|
+
else if(state.kind==='ready') {
|
|
44
|
+
status.textContent=`${state.asset.review_status==='needs-fix'?'Rendered — visual defect recorded':'Ready'} · ${state.asset.name} · ${state.runtime.triangleCount.toLocaleString()} triangles · ${state.runtime.primitiveCount} mesh primitives`;
|
|
45
|
+
el('metrics').textContent=`${state.runtime.triangleCount.toLocaleString()} triangles · ${state.asset.inspection.materials} source materials`;
|
|
46
|
+
} else {status.textContent=state.message;el('canvas').hidden=true;el('refinement').textContent='Refinement unavailable';}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const change=()=>{
|
|
50
|
+
const asset=assets.find(a=>a.key===el('model').value);
|
|
51
|
+
el('category').textContent=asset.category;el('name').textContent=asset.name;
|
|
52
|
+
el('target').textContent=asset.renderer_target;el('review').textContent=asset.observed_issue??asset.preparation;
|
|
53
|
+
el('credits').textContent=asset.authors;el('source').href=asset.source_url;
|
|
54
|
+
el('licence').href=asset.licence_url;el('licence').textContent=asset.licence;
|
|
55
|
+
el('metrics').textContent=asset.entry?`${(asset.files.reduce((n,f)=>n+f.bytes,0)/1048576).toFixed(1)} MiB on disk`:'';
|
|
56
|
+
const clips=asset.inspection?.animations??[];
|
|
57
|
+
el('note').textContent=clips.length?`Static pose only. ${clips.length} source clips are present: ${clips.join(', ')}. Animation playback needs importer work.`:
|
|
58
|
+
asset.inspection?.extensions.length?`Material features to inspect: ${asset.inspection.extensions.join(', ')}.`:'Source materials under the same inspection lighting.';
|
|
59
|
+
return controller.select(asset);
|
|
60
|
+
};
|
|
61
|
+
el('model').value=assets.find(a=>a.entry).key;
|
|
62
|
+
el('model').addEventListener('change',change);
|
|
63
|
+
el('model').disabled=false;
|
|
64
|
+
const qualityChange=async()=>{
|
|
65
|
+
try{await controller.setQuality(quality());}catch(error){status.textContent=`Render failed: ${error.message}`;}
|
|
66
|
+
};
|
|
67
|
+
for(const id of ['samples','bounces','resolution','denoise'])el(id).addEventListener('change',qualityChange);
|
|
68
|
+
const visibility=()=>controller.setPaused(doc.hidden===true);
|
|
69
|
+
doc.addEventListener('visibilitychange',visibility);visibility();
|
|
70
|
+
let resizeTimer;
|
|
71
|
+
const resize=()=>{clearTimeout(resizeTimer);resizeTimer=setTimeout(qualityChange,250);};
|
|
72
|
+
win.addEventListener('resize',resize);
|
|
73
|
+
const destroy=()=>{for(const id of ['samples','bounces','resolution','denoise'])el(id).removeEventListener('change',qualityChange);doc.removeEventListener('visibilitychange',visibility);clearTimeout(resizeTimer);controller.destroy();el('model').removeEventListener('change',change);win.removeEventListener('pagehide',destroy);win.removeEventListener('resize',resize);};
|
|
74
|
+
win.addEventListener('pagehide',destroy);
|
|
75
|
+
await change();return {destroy};
|
|
76
|
+
}
|