diff --git a/README.md b/README.md index 485c70a..29f136f 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,9 @@ Needs `git`, `ffmpeg`/`ffprobe` with libx264, and `python3` with Pillow and nump After building, `bun run serve` serves `/demos/head-turn/` from the isolated output. The integration entry uses ``, with separate `site-ui.css` / `site-ui.js` for its single preview caption and Controls toggle. Below 800 CSS pixels the controls start closed; desktop keeps them available. Loading status belongs to the original application and remains visible until it finishes. The preserved demo caption predates the Parts 1.68.0 producer release. The Examples and Parts pages distinguish that standalone snapshot from the released producer; this documentation update does not rebuild or change its interactive files. -`demos/head-turn/bundle/` contains the approved ten-file preview inventory. The 2026-10-26 depth revision replaces `depth.jpg` and re-bakes `scene.json` / `scene.bin.gz` from that same confirmed atlas; the bundle README records the input and output hashes. The field reader, scene extractor and exact tuple deduplicator first reproduced the previous fields and complete gzip bytes as a control. The revision preserves ordered triangle-expanded neutral positions and UVs, all 224,144 triangles, draw order, eye aperture/core data and controller parameters. Depth-dependent deduplication now stores 132,540 vertices instead of 132,534. `index.html`, `app.js`, `aperture.js`, `runtime.js`, `look.png` and `THIRD_PARTY_LICENSES.txt` retain their previous bytes, as do the integration UI and separate curvature page. The adjacent `bundle-manifest.json` pins the revised inventory's path-sorted lengths and SHA-256 values; the build checks its digest before fetching registry data. Verification checks the whole copied `demos/` tree and the complete deployment manifest. +`demos/head-turn/bundle/` contains the approved ten-file preview inventory. The 2026-10-26 depth revision replaced `depth.jpg` and re-baked `scene.json` / `scene.bin.gz` from that same confirmed atlas. The field reader, scene extractor and exact tuple deduplicator first reproduced the previous fields and complete gzip bytes as a control. It preserved ordered triangle-expanded neutral positions and UVs, all 224,144 triangles, draw order, eye aperture/core data and controller parameters; depth-dependent deduplication stores 132,540 vertices. + +The 2026-10-28 GPU revision moves depth displacement from the frame callback into a Pixi 8.22.0 mesh vertex shader. It prepares and uploads each mesh's base/UV/depth/index buffers once, then updates dial and gaze uniforms while retaining the original eye core clamp and aperture sprites. The controls, defaults, source geometry, atlases, entry and curvature page stay unchanged. The bundle README records source API links and the input/output SHA-256 rows. The adjacent `bundle-manifest.json` pins the ten files' path-sorted lengths and hashes; `HEAD_TURN_BUNDLE_SHA256` independently pins that inventory. The build checks it before fetching registry data; verification checks the whole copied `demos/` tree and the complete deployment manifest. `demos/head-turn/curvature/` is a separate static page linked from the preview's caption: one WebGL fragment shader samples `original.webp` at each pixel's position plus depth × camera offset (pointer on desktop, drag on touch), with depth gamma, contrast and bias and a Show depth view. It reads the depth map and is not the head-turn feature. `depth.webp` is the supplied 1359×2000 depth map resampled into the painting's 1368×2000 frame by `x_depth = 0.99908·x − 2.939` (pixel indices, y unchanged), fitted to the silhouette edges of both images; a uniform 1359/1368 stretch left a median edge residual of 2.77 px, this fit 0.17 px. `verifyCurvaturePage` (in `src/head-turn.ts`) checks its five files, caption, back link, that every reference is relative and resolves, and the preview's link to it. diff --git a/demos/head-turn/bundle-manifest.json b/demos/head-turn/bundle-manifest.json index da1791b..cce3cec 100644 --- a/demos/head-turn/bundle-manifest.json +++ b/demos/head-turn/bundle-manifest.json @@ -1,11 +1,11 @@ { "schema": 1, - "sha256": "515c89f1307308e5e41dcfe5a0fc7c69e5751539863ef7cb7c14e1cf6ffa38e2", + "sha256": "fb7bc15b188ed8f29a931067df44a4c3cdb61ba3f114c706722eee73c8a6c2fc", "files": [ { "path": "README.md", - "bytes": 3979, - "sha256": "4b0cb0bc44ccbb56674d3cfd2b149b79b10722989fb964fc273345574d3b79d7" + "bytes": 8823, + "sha256": "6b30d00f969e6201ba2b45db73123fd3c419d710b797ca1b58f699cc47aaa626" }, { "path": "THIRD_PARTY_LICENSES.txt", @@ -19,8 +19,8 @@ }, { "path": "app.js", - "bytes": 5410, - "sha256": "e2c3b1dff5a4efa14ed66498f693638c62cbb996db8d3be00a5ba6c17504b2d6" + "bytes": 7940, + "sha256": "4e2536d43f8ad7ef002ecaa65350c1cec92d24d3f809f2fd6299f21e6fb0e2ca" }, { "path": "depth.jpg", @@ -39,8 +39,8 @@ }, { "path": "runtime.js", - "bytes": 486531, - "sha256": "33551eef39683c12a0c3e340acba981f09a1fa4e0141c2d55dd02f9bbd269d54" + "bytes": 486648, + "sha256": "f55d9bda6ea33736753781895f2ab4c87a6b49e04ac4b40622c7ab10e3a0f0d4" }, { "path": "scene.bin.gz", diff --git a/demos/head-turn/bundle/README.md b/demos/head-turn/bundle/README.md index 8331140..80618dc 100644 --- a/demos/head-turn/bundle/README.md +++ b/demos/head-turn/bundle/README.md @@ -38,3 +38,36 @@ SHA-256 provenance: The gzip recipe is `gzip.compress(raw, compresslevel=9, mtime=0)`, reproduced with Python 3.13.1 and zlib 1.2.12 (compile and runtime). This is a reproduced invocation, not a recovered historical command log. The scene finalization changes only its `buffer` name from `scene.bin` to `scene.bin.gz`; gzip headers are not patched. The decoded payload is 6,294,257 bytes; its raw gzip file is 3,405,993 bytes. All 19 parts retain their ordered triangle-expanded Float64 base positions and UVs, 224,144 ordered triangles, common/iris drivers, eye masks and core data, bounds, basis and gain. Exact depth-dependent tuple deduplication increases the stored vertex count from 132,534 to 132,540. Packed indices and offsets may consequently differ. The renderer, gaze controller, colour atlas, original entry and runtime licence notices retain their previous bytes. + +## GPU displacement revision (2026-10-28) + +The static meshes, UVs, triangle indices and added `aDepth` attributes are uploaded once when the page loads. The frame callback updates typed shader uniforms for the depth rotation, gain, pivot, rear-hair common motion and bounded gaze. `PixiRenderer.render` prepares the neutral meshes once; retaining them with custom shaders avoids the adapter's CPU vertex scan and batch packing on subsequent frames. The four controls, default values, pointer damping and 1.5× canvas sizing are unchanged. + +The vertex shader evaluates `z = depth * depthGain * gain`, `dx = z * sin(yaw)` and `dy = z * sin(pitch)`, then adds the original basis transform, `common * pivot * sin` and the iris gaze shift. `common` is `-counter` for rear hair, `1` for common head parts and `0` otherwise. World y is negated for the existing Pixi coordinate convention. The unchanged `aperture.js` clamps gaze against the same core masks on the CPU; the two aperture sprites retain the original matrix formula. The constant-depth iris bounds follow the displaced iris with four scalar updates so Pixi's alpha filter still covers it. No per-frame vertex buffer writes are needed. + +`runtime.js` identifies its bundled Pixi as **8.22.0** (`pr="8.22.0"`, passed to `__PIXI_RENDERER_INIT__`). This revision only appends exports for its existing `Shader` (`Xt`) and `GlProgram` (`Gt`); it does not replace or rebuild Pixi. The API follows the versioned [Shader documentation](https://pixijs.download/v8.22.0/docs/rendering.Shader.html) and [Geometry.addAttribute documentation](https://pixijs.download/v8.22.0/docs/rendering.Geometry.html). Pixi's [MeshPipe](https://github.com/pixijs/pixijs/blob/v8.22.0/src/scene/mesh/shared/MeshPipe.ts) supplies local transform/color uniforms; [GlMeshAdaptor](https://github.com/pixijs/pixijs/blob/v8.22.0/src/scene/mesh/gl/GlMeshAdaptor.ts) supplies global and local bind groups to custom shaders. This page continues to use WebGL. + +Additional SHA-256 provenance (the scene, atlases and aperture controller retain the depth revision's bytes): + +| Input or output | SHA-256 | +| --- | --- | +| CPU `app.js` input | `e2c3b1dff5a4efa14ed66498f693638c62cbb996db8d3be00a5ba6c17504b2d6` | +| Bundled Pixi runtime input | `33551eef39683c12a0c3e340acba981f09a1fa4e0141c2d55dd02f9bbd269d54` | +| Initial ES300 GPU `app.js` (pre-repair) | `89aec03bc57f3363a7a37942bd3309486341a11f533626b55958e9080311a59c` | +| Runtime with shader exports | `f55d9bda6ea33736753781895f2ab4c87a6b49e04ac4b40622c7ab10e3a0f0d4` | +| Unchanged aperture controller | `b2c159cdd338649ad6833cfd04bb710da6e106d76655c71d3a9ab7c57cbd543c` | + +Visual parity and callback timing require a browser run against the frozen CPU reference. Bundle integrity tests alone do not establish either result. GPU arithmetic uses Float32, so the visual comparison must state its pixel error bounds rather than assume bit-exact CPU Float64 arithmetic. + +## WebGL1 compatibility repair (2026-10-28) + +The initial GPU shaders explicitly requested GLSL ES300 and therefore failed on the original renderer's WebGL1 fallback. This repair removes the two `#version 300 es` lines; the displacement, uniforms, mask/controller code and bundled runtime are unchanged. + +The exact bundled mechanism is `id`/`Gt` (`GlProgram`), which tests `fragment.indexOf("#version 300 es") !== -1` and passes that boolean to `addProgramDefines` and `insertVersion`. With no header, `Yu` (`addProgramDefines`) maps vertex `in`/`out` to `attribute`/`varying`, maps fragment `in` to `varying`, removes the exact `out vec4 finalColor;` declaration and maps `finalColor`/`texture` to `gl_FragColor`/`texture2D`. `Zu` (`insertVersion`) returns the source without adding an ES300 header. Precision handling remains in `qu` (`ensurePrecision`); highp is requested as before. This uses the shared GLSL100 compatibility path on WebGL1 and WebGL2. See the version-pinned [GlProgram constructor](https://github.com/pixijs/pixijs/blob/v8.22.0/src/rendering/renderers/gl/shader/GlProgram.ts#L134-L164), [addProgramDefines](https://github.com/pixijs/pixijs/blob/v8.22.0/src/rendering/renderers/gl/shader/program/preprocessors/addProgramDefines.ts#L6-L32), and [insertVersion](https://github.com/pixijs/pixijs/blob/v8.22.0/src/rendering/renderers/gl/shader/program/preprocessors/insertVersion.ts). The same control flow and replacements are present in the minified runtime. + +| Repair input or output | SHA-256 | +| --- | --- | +| Pre-repair GPU app | `89aec03bc57f3363a7a37942bd3309486341a11f533626b55958e9080311a59c` | +| Original CPU reference | `e2c3b1dff5a4efa14ed66498f693638c62cbb996db8d3be00a5ba6c17504b2d6` | +| Compatible GPU app | `4e2536d43f8ad7ef002ecaa65350c1cec92d24d3f809f2fd6299f21e6fb0e2ca` | +| Unchanged bundled runtime | `f55d9bda6ea33736753781895f2ab4c87a6b49e04ac4b40622c7ab10e3a0f0d4` | diff --git a/demos/head-turn/bundle/app.js b/demos/head-turn/bundle/app.js index 011f2ff..ea622d3 100644 --- a/demos/head-turn/bundle/app.js +++ b/demos/head-turn/bundle/app.js @@ -1,4 +1,4 @@ -import{PixiRenderer,WebGLRenderer,Container,Sprite,Texture,Rectangle,Matrix}from'./runtime.js'; +import{PixiRenderer,WebGLRenderer,Container,Sprite,Texture,Rectangle,Matrix,Shader,GlProgram}from'./runtime.js'; import{coreAnchorClamp}from'./aperture.js'; const $=id=>document.getElementById(id),host=$('demo'),image=new Image();image.src=new URL('./look.png',import.meta.url); try{ @@ -11,13 +11,73 @@ try{ const stage=new Container(),root=new Container(),masks=new Container();stage.addChild(root,masks);const painter=new PixiRenderer(root,{pageImages:new Map([['look.png',image]])});let width,height,scale,cameraX,cameraY; function fit(){width=Math.max(1,Math.round(innerWidth*1.5));height=Math.max(1,Math.round(innerHeight*1.5));renderer.resize(width,height);const b=scene.bounds;scale=Math.min(width*.82/b.width,height*.88/b.height);cameraX=width/2-(b.x+b.width/2)*scale;cameraY=height/2+(b.y+b.height/2)*scale;for(const c of[root,masks]){c.scale.set(scale);c.position.set(cameraX,cameraY)}}fit(); let gain=1,pivot=55,gaze=.10,counter=.10,x=0,y=0,vx=0,vy=0,tx=0,ty=0; + // Pixi 8.22.0: retain the meshes and their buffers; a custom shader disables batching. const frame={order:parts.map(p=>p.slot)},maskSprites=[]; + painter.render(frame); + const vertex=` + in vec2 aPosition; + in vec2 aUV; + in float aDepth; + uniform mat3 uProjectionMatrix; + uniform mat3 uWorldTransformMatrix; + uniform mat3 uTransformMatrix; + uniform vec4 uTurn; + uniform vec4 uBasis; + uniform float uCommon; + uniform vec2 uGaze; + out vec2 vUV; + void main(){ + float z=aDepth*uTurn.z; + float dx=z*uTurn.x,dy=z*uTurn.y; + vec2 displacement=vec2(uBasis.x*dx+uBasis.y*dy,uBasis.z*dx+uBasis.w*dy) + +uCommon*uTurn.w*uTurn.xy+uGaze; + // PixiRenderer stores world y with its sign flipped. + vec2 position=aPosition+vec2(displacement.x,-displacement.y); + mat3 mvp=uProjectionMatrix*uWorldTransformMatrix*uTransformMatrix; + gl_Position=vec4((mvp*vec3(position,1.0)).xy,0.0,1.0); + vUV=aUV; + }`; + const fragment=` + in vec2 vUV; + uniform sampler2D uTexture; + uniform vec4 uColor; + uniform vec4 uWorldColorAlpha; + out vec4 finalColor; + void main(){finalColor=texture(uTexture,vUV)*uColor*uWorldColorAlpha;}`; + const glProgram=GlProgram.from({vertex,fragment,name:'depth-head-turn',preferredFragmentPrecision:'highp'}); + const turn=new Float32Array(4),basis=new Float32Array([scene.basis.a,scene.basis.b,scene.basis.c,scene.basis.d]); + parts.forEach((p,i)=>{ + const mesh=root.children[i],depth=new Float32Array(p.base.length/2); + if(typeof p.z==='number')depth.fill(p.z);else depth.set(p.z); + mesh.geometry.addAttribute('aDepth',{buffer:depth,format:'float32'}); + mesh.shader=new Shader({glProgram,resources:{uTexture:mesh.texture.source,depthUniforms:{ + uTurn:{value:turn,type:'vec4'},uBasis:{value:basis,type:'vec4'}, + uCommon:{value:0,type:'f32'},uGaze:{value:new Float32Array(2),type:'vec2'} + }}}); + p.mesh=mesh;p.uniforms=mesh.shader.resources.depthUniforms.uniforms; + // Alpha-mask filter bounds must follow the displaced iris too. Its depth is constant; + // update four bounds scalars, without touching any vertex or GPU buffer. + if(p.iris>=0){if(typeof p.z!=='number')throw Error('Iris requires constant depth'); + let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity; + for(let j=0;j{ + const sprite=new Sprite(new Texture({source:painter.getPageSource('look.png'),frame:new Rectangle(...e.frame)})); + masks.addChild(sprite);maskSprites[i]=sprite; + root.children[e.iris].setMask({mask:sprite,channel:'alpha',inverse:false}); + }); function draw(){const sx=Math.sin(x*15*Math.PI/180),sy=Math.sin(y*12*Math.PI/180),strength=scene.depthGain*gain,T=[pivot*sx,pivot*sy],b=scene.basis; + turn[0]=sx;turn[1]=sy;turn[2]=strength;turn[3]=pivot; const eyeShift=scene.eyes.map(e=>coreAnchorClamp(e.core,e.size[0],e.size[1],e.anchor,[gaze*e.size[0]*x,-gaze*e.size[0]*y]).shift); - for(const p of parts){const a=p.slot.vertices=new Array(p.base.length),common=p.common===2?-counter:p.common===1?1:0,e=p.iris>=0?scene.eyes[p.iris]:null,shift=e?eyeShift[p.iris]:[0,0],gx=e?e.gazeBasis[0]*shift[0]+e.gazeBasis[1]*shift[1]:0,gy=e?e.gazeBasis[2]*shift[0]+e.gazeBasis[3]*shift[1]:0; - for(let i=0;i{let sprite=maskSprites[i];if(!sprite){sprite=new Sprite(new Texture({source:painter.getPageSource('look.png'),frame:new Rectangle(...e.frame)}));masks.addChild(sprite);maskSprites[i]=sprite}const p=parts[e.unit],z=p.z*strength,dx=z*sx,dy=z*sy,m=e.base;sprite.setFromMatrix(new Matrix(m[0],m[1],m[2],m[3],m[4]+T[0]+b.a*dx+b.b*dy,m[5]-(T[1]+b.c*dx+b.d*dy)));root.children[e.iris].setMask({mask:sprite,channel:'alpha',inverse:false})}); + for(const p of parts){const common=p.common===2?-counter:p.common===1?1:0,e=p.iris>=0?scene.eyes[p.iris]:null,shift=e?eyeShift[p.iris]:[0,0],gx=e?e.gazeBasis[0]*shift[0]+e.gazeBasis[1]*shift[1]:0,gy=e?e.gazeBasis[2]*shift[0]+e.gazeBasis[3]*shift[1]:0; + p.uniforms.uCommon=common;p.uniforms.uGaze[0]=gx;p.uniforms.uGaze[1]=gy; + if(p.irisBounds){const z=p.z*strength,dx=b.a*(z*sx)+b.b*(z*sy)+common*T[0]+gx,dy=b.c*(z*sx)+b.d*(z*sy)+common*T[1]+gy,q=p.irisBounds; + p.mesh.geometry.bounds.set(Math.fround(q[0]+dx),Math.fround(-q[3]-dy),Math.fround(q[2]+dx),Math.fround(-q[1]-dy));p.mesh.onViewUpdate(); + } + } + scene.eyes.forEach((e,i)=>{const sprite=maskSprites[i],p=parts[e.unit],z=p.z*strength,dx=z*sx,dy=z*sy,m=e.base;sprite.setFromMatrix(new Matrix(m[0],m[1],m[2],m[3],m[4]+T[0]+b.a*dx+b.b*dy,m[5]-(T[1]+b.c*dx+b.d*dy)))}); renderer.render({container:stage}); } const finite=(v,min,max)=>{if(!Number.isFinite(v))throw Error('Control must be finite');return Math.max(min,Math.min(max,v))}; diff --git a/demos/head-turn/bundle/runtime.js b/demos/head-turn/bundle/runtime.js index b58f68d..931735c 100644 --- a/demos/head-turn/bundle/runtime.js +++ b/demos/head-turn/bundle/runtime.js @@ -1020,3 +1020,6 @@ fn mainFragment( return vec4(rgb, result.a); }`;class Qs extends cr{constructor(t={}){let e=new ft({uColorMatrix:{value:[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0],type:"f32",size:20},uAlpha:{value:1,type:"f32"}}),r=kt.from({vertex:{source:sl,entryPoint:"mainVertex"},fragment:{source:sl,entryPoint:"mainFragment"}}),i=Gt.from({vertex:Ms,fragment:hm,name:"color-matrix-filter"});super({...t,gpuProgram:r,glProgram:i,resources:{colorMatrixUniforms:e}});this.alpha=1}_loadMatrix(t,e=!1){if(e){let r=[...t];this._multiply(r,this.matrix,t),this.resources.colorMatrixUniforms.uniforms.uColorMatrix=r}else this.resources.colorMatrixUniforms.uniforms.uColorMatrix=t;this.resources.colorMatrixUniforms.update()}_multiply(t,e,r){return t[0]=e[0]*r[0]+e[1]*r[5]+e[2]*r[10]+e[3]*r[15],t[1]=e[0]*r[1]+e[1]*r[6]+e[2]*r[11]+e[3]*r[16],t[2]=e[0]*r[2]+e[1]*r[7]+e[2]*r[12]+e[3]*r[17],t[3]=e[0]*r[3]+e[1]*r[8]+e[2]*r[13]+e[3]*r[18],t[4]=e[0]*r[4]+e[1]*r[9]+e[2]*r[14]+e[3]*r[19]+e[4],t[5]=e[5]*r[0]+e[6]*r[5]+e[7]*r[10]+e[8]*r[15],t[6]=e[5]*r[1]+e[6]*r[6]+e[7]*r[11]+e[8]*r[16],t[7]=e[5]*r[2]+e[6]*r[7]+e[7]*r[12]+e[8]*r[17],t[8]=e[5]*r[3]+e[6]*r[8]+e[7]*r[13]+e[8]*r[18],t[9]=e[5]*r[4]+e[6]*r[9]+e[7]*r[14]+e[8]*r[19]+e[9],t[10]=e[10]*r[0]+e[11]*r[5]+e[12]*r[10]+e[13]*r[15],t[11]=e[10]*r[1]+e[11]*r[6]+e[12]*r[11]+e[13]*r[16],t[12]=e[10]*r[2]+e[11]*r[7]+e[12]*r[12]+e[13]*r[17],t[13]=e[10]*r[3]+e[11]*r[8]+e[12]*r[13]+e[13]*r[18],t[14]=e[10]*r[4]+e[11]*r[9]+e[12]*r[14]+e[13]*r[19]+e[14],t[15]=e[15]*r[0]+e[16]*r[5]+e[17]*r[10]+e[18]*r[15],t[16]=e[15]*r[1]+e[16]*r[6]+e[17]*r[11]+e[18]*r[16],t[17]=e[15]*r[2]+e[16]*r[7]+e[17]*r[12]+e[18]*r[17],t[18]=e[15]*r[3]+e[16]*r[8]+e[17]*r[13]+e[18]*r[18],t[19]=e[15]*r[4]+e[16]*r[9]+e[17]*r[14]+e[18]*r[19]+e[19],t}brightness(t,e){let r=[t,0,0,0,0,0,t,0,0,0,0,0,t,0,0,0,0,0,1,0];this._loadMatrix(r,e)}tint(t,e){let[r,i,s]=K.shared.setValue(t).toArray(),n=[r,0,0,0,0,0,i,0,0,0,0,0,s,0,0,0,0,0,1,0];this._loadMatrix(n,e)}greyscale(t,e){let r=[t,t,t,0,0,t,t,t,0,0,t,t,t,0,0,0,0,0,1,0];this._loadMatrix(r,e)}grayscale(t,e){this.greyscale(t,e)}blackAndWhite(t){let e=[0.3,0.6,0.1,0,0,0.3,0.6,0.1,0,0,0.3,0.6,0.1,0,0,0,0,0,1,0];this._loadMatrix(e,t)}hue(t,e){t=(t||0)/180*Math.PI;let r=Math.cos(t),i=Math.sin(t),s=Math.sqrt,n=0.3333333333333333,o=s(n),a=r+(1-r)*n,l=n*(1-r)-o*i,c=n*(1-r)+o*i,h=n*(1-r)+o*i,u=r+n*(1-r),f=n*(1-r)-o*i,d=n*(1-r)-o*i,m=n*(1-r)+o*i,x=r+n*(1-r),_=[a,l,c,0,0,h,u,f,0,0,d,m,x,0,0,0,0,0,1,0];this._loadMatrix(_,e)}contrast(t,e){let r=(t||0)+1,i=-0.5*(r-1),s=[r,0,0,0,i,0,r,0,0,i,0,0,r,0,i,0,0,0,1,0];this._loadMatrix(s,e)}saturate(t=0,e){let r=t*2/3+1,i=(r-1)*-0.5,s=[r,i,i,0,0,i,r,i,0,0,i,i,r,0,0,0,0,0,1,0];this._loadMatrix(s,e)}desaturate(){this.saturate(-1)}negative(t){let e=[-1,0,0,1,0,0,-1,0,1,0,0,0,-1,1,0,0,0,0,1,0];this._loadMatrix(e,t)}sepia(t){let e=[0.393,0.7689999,0.18899999,0,0,0.349,0.6859999,0.16799999,0,0,0.272,0.5339999,0.13099999,0,0,0,0,0,1,0];this._loadMatrix(e,t)}technicolor(t){let e=[1.9125277891456083,-0.8545344976951645,-0.09155508482755585,0,0.046249425232852304,-0.3087833385928097,1.7658908555458428,-0.10601743074722245,0,-0.2758903984886823,-0.231103377548616,-0.7501899197440212,1.847597816108189,0,0.12137623870388682,0,0,0,1,0];this._loadMatrix(e,t)}polaroid(t){let e=[1.438,-0.062,-0.062,0,0,-0.122,1.378,-0.122,0,0,-0.016,-0.016,1.483,0,0,0,0,0,1,0];this._loadMatrix(e,t)}toBGR(t){let e=[0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0];this._loadMatrix(e,t)}kodachrome(t){let e=[1.1285582396593525,-0.3967382283601348,-0.03992559172921793,0,0.24991995145868634,-0.16404339962244616,1.0835251566291304,-0.05498805115633132,0,0.09698983488904393,-0.16786010706155763,-0.5603416277695248,1.6014850761964943,0,0.13972481597886063,0,0,0,1,0];this._loadMatrix(e,t)}browni(t){let e=[0.5997023498159715,0.34553243048391263,-0.2708298674538042,0,0.1860075629647401,-0.037703249837783157,0.8609577587992641,0.15059552388459913,0,-0.14497417640467167,0.24113635128153335,-0.07441037908422492,0.44972182064877153,0,-0.029655197167024642,0,0,0,1,0];this._loadMatrix(e,t)}vintage(t){let e=[0.6279345635605994,0.3202183420819367,-0.03965408211312453,0,0.037848179746251466,0.02578397704808868,0.6441188644374771,0.03259127616149294,0,0.029265996770472907,0.0466055556782719,-0.0851232987247891,0.5241648018700465,0,0.020232119953863904,0,0,0,1,0];this._loadMatrix(e,t)}colorTone(t,e,r,i,s){t||(t=0.2),e||(e=0.15),r||(r=16770432),i||(i=3375104);let n=K.shared,[o,a,l]=n.setValue(r).toArray(),[c,h,u]=n.setValue(i).toArray(),f=[0.3,0.59,0.11,0,0,o,a,l,t,0,c,h,u,e,0,o-c,a-h,l-u,0,0];this._loadMatrix(f,s)}night(t,e){t||(t=0.1);let r=[t*-2,-t,0,0,0,-t,0,t,0,0,0,t,t*2,0,0,0,0,0,1,0];this._loadMatrix(r,e)}predator(t,e){let r=[11.224130630493164*t,-4.794486999511719*t,-2.8746118545532227*t,0*t,0.40342438220977783*t,-3.6330697536468506*t,9.193157196044922*t,-2.951810836791992*t,0*t,-1.316135048866272*t,-3.2184197902679443*t,-4.2375030517578125*t,7.476448059082031*t,0*t,0.8044459223747253*t,0,0,0,1,0];this._loadMatrix(r,e)}lsd(t){let e=[2,-0.4,0.5,0,0,-0.5,2,-0.4,0,0,-0.4,-0.5,3,0,0,0,0,0,1,0];this._loadMatrix(e,t)}reset(){let t=[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0];this._loadMatrix(t,!1)}get matrix(){return this.resources.colorMatrixUniforms.uniforms.uColorMatrix}set matrix(t){this.resources.colorMatrixUniforms.uniforms.uColorMatrix=t}get alpha(){return this.resources.colorMatrixUniforms.uniforms.uAlpha}set alpha(t){this.resources.colorMatrixUniforms.uniforms.uAlpha=t}}Iu();dr();le();ot();st();Zi();ur();He();dr();st();var Mm=class t extends Qt{constructor(...e){let r=e[0]??{};if(r instanceof Float32Array)A(X,"use new MeshGeometry({ positions, uvs, indices }) instead"),r={positions:r,uvs:e[1],indices:e[2]};r={...t.defaultOptions,...r};let i=r.positions||new Float32Array([0,0,1,0,1,1,0,1]),s=r.uvs;if(!s)if(r.positions)s=new Float32Array(i.length);else s=new Float32Array([0,0,1,0,1,1,0,1]);let n=r.indices||new Uint32Array([0,1,2,0,2,3]),o=r.shrinkBuffersToFit,a=new Ct({data:i,label:"attribute-mesh-positions",shrinkToFit:o,usage:j.VERTEX|j.COPY_DST}),l=new Ct({data:s,label:"attribute-mesh-uvs",shrinkToFit:o,usage:j.VERTEX|j.COPY_DST}),c=new Ct({data:n,label:"index-mesh-buffer",shrinkToFit:o,usage:j.INDEX|j.COPY_DST});super({attributes:{aPosition:{buffer:a,format:"float32x2",stride:8,offset:0},aUV:{buffer:l,format:"float32x2",stride:8,offset:0}},indexBuffer:c,topology:r.topology});this.batchMode="auto"}get positions(){return this.attributes.aPosition.buffer.data}set positions(e){this.attributes.aPosition.buffer.data=e}get uvs(){return this.attributes.aUV.buffer.data}set uvs(e){this.attributes.aUV.buffer.data=e}get indices(){return this.indexBuffer.data}set indices(e){this.indexBuffer.data=e}};Mm.defaultOptions={topology:"triangle-list",shrinkBuffersToFit:!1};var xi=Mm;P();P();tt();lr();Zt();va();tn();class ll{constructor(){this.batcherName="default",this.packAsQuad=!1,this.indexOffset=0,this.attributeOffset=0,this.roundPixels=0,this._batcher=null,this._batch=null,this._textureMatrixUpdateId=-1,this._uvUpdateId=-1}get blendMode(){return this.renderable.groupBlendMode}get topology(){return this._topology||this.geometry.topology}set topology(t){this._topology=t}reset(){this.renderable=null,this.texture=null,this._batcher=null,this._batch=null,this.geometry=null,this._uvUpdateId=-1,this._textureMatrixUpdateId=-1}setTexture(t){if(this.texture===t)return;this.texture=t,this._textureMatrixUpdateId=-1}get uvs(){let e=this.geometry.getBuffer("aUV"),r=e.data,i=r,s=this.texture.textureMatrix;if(!s.isSimple){if(i=this._transformedUvs,this._textureMatrixUpdateId!==s._updateID||this._uvUpdateId!==e._updateID){if(!i||i.length"},uColor:{value:new Float32Array([1,1,1,1]),type:"vec4"},uRound:{value:0,type:"f32"}}),this.localUniformsBindGroup=new Ut({0:this.localUniforms}),this.renderer=t,this._adaptor=e,this._adaptor.init()}validateRenderable(t){let e=this._getMeshData(t),r=e.batched,i=t.batched;if(e.batched=i,r!==i)return!0;else if(i){let s=t._geometry;if(s.indices.length!==e.indexSize||s.positions.length!==e.vertexSize)return e.indexSize=s.indices.length,e.vertexSize=s.positions.length,!0;let n=this._getBatchableMesh(t);if(n.texture.uid!==t._texture.uid)n._textureMatrixUpdateId=-1;return!n._batcher.checkAndUpdateTexture(n,t._texture)}return!1}addRenderable(t,e){let r=this.renderer.renderPipes.batch,i=this._getMeshData(t);if(t.didViewUpdate)i.indexSize=t._geometry.indices?.length,i.vertexSize=t._geometry.positions?.length;if(i.batched){let s=this._getBatchableMesh(t);s.setTexture(t._texture),s.geometry=t._geometry,r.addToBatch(s,e)}else r.break(e),e.add(t)}updateRenderable(t){if(t.batched){let e=this._getBatchableMesh(t);e.setTexture(t._texture),e.geometry=t._geometry,e._batcher.updateElement(e)}}execute(t){if(!t.isRenderable)return;if(t.state.blend)t.state.blendMode=ci(t.groupBlendMode,t.texture._source);let e=this.localUniforms;e.uniforms.uTransformMatrix=t.groupTransform,e.uniforms.uRound=this.renderer._roundPixels|t._roundPixels,e.update(),Cr(t.groupColorAlpha,e.uniforms.uColor,0),this._adaptor.execute(this,t)}_getMeshData(t){var e,r;return(e=t._gpuData)[r=this.renderer.uid]||(e[r]=new hl),t._gpuData[this.renderer.uid].meshData||this._initMeshData(t)}_initMeshData(t){return t._gpuData[this.renderer.uid].meshData={batched:t.batched,indexSize:0,vertexSize:0},t._gpuData[this.renderer.uid].meshData}_getBatchableMesh(t){var e,r;return(e=t._gpuData)[r=this.renderer.uid]||(e[r]=new hl),t._gpuData[this.renderer.uid].batchableMesh||this._initBatchableMesh(t)}_initBatchableMesh(t){let e=new ll;return e.renderable=t,e.setTexture(t._texture),e.transform=t.groupTransform,e.roundPixels=this.renderer._roundPixels|t._roundPixels,t._gpuData[this.renderer.uid].batchableMesh=e,e}destroy(){this.localUniforms=null,this.localUniformsBindGroup=null,this._adaptor.destroy(),this._adaptor=null,this.renderer=null}}rn.extension={type:[y.WebGLPipes,y.WebGPUPipes],name:"mesh"};D.add(rn);class sn extends Ue{constructor(...t){let e=t[0];if(e instanceof Qt){if(A(X,"Mesh: use new Mesh({ geometry, shader }) instead"),e={geometry:e,shader:t[1]},t[3])A(X,"Mesh: drawMode argument has been removed, use geometry.topology instead"),e.geometry.topology=t[3]}let{geometry:r,shader:i,texture:s,roundPixels:n,state:o,...a}=e;super({label:"Mesh",...a});this.renderPipeId="mesh",this._shader=null,this.allowChildren=!1,this.shader=i??null,this.texture=s??i?.texture??C.WHITE,this.state=o??Mt.for2d(),this._geometry=r,this._geometry.on("update",this.onViewUpdate,this),this.roundPixels=n??!1}get material(){return A(X,"mesh.material property has been removed, use mesh.shader instead"),this._shader}set shader(t){if(this._shader===t)return;this._shader=t,this.onViewUpdate()}get shader(){return this._shader}set geometry(t){if(this._geometry===t)return;this._geometry?.off("update",this.onViewUpdate,this),t.on("update",this.onViewUpdate,this),this._geometry=t,this.onViewUpdate()}get geometry(){return this._geometry}set texture(t){t||(t=C.EMPTY);let e=this._texture;if(e===t)return;if(e&&e.dynamic)e.off("update",this.onViewUpdate,this);if(t.dynamic)t.on("update",this.onViewUpdate,this);if(this.shader)this.shader.texture=t;this._texture=t,this.onViewUpdate()}get texture(){return this._texture}get batched(){if(this._shader)return!1;if((this.state.data&12)!==0)return!1;if(this._geometry instanceof xi){if(this._geometry.batchMode==="auto")return this._geometry.positions.length/2<=100;return this._geometry.batchMode==="batch"}return!1}get bounds(){return this._geometry.bounds}updateBounds(){this._bounds=this._geometry.bounds}containsPoint(t){let{x:e,y:r}=t;if(!this.bounds.containsPoint(e,r))return!1;let i=this.geometry.getBuffer("aPosition").data,s=this.geometry.topology==="triangle-strip"?3:1;if(this.geometry.getIndex()){let n=this.geometry.getIndex().data,o=n.length;for(let a=0;a+2=Math.min(t,r)-0.000000001&&s<=Math.max(t,r)+0.000000001&&n>=Math.min(e,i)-0.000000001&&n<=Math.max(e,i)+0.000000001}function f0(t,e,r,i,s,n,o,a){let l=Pi(s,n,o,a,t,e),c=Pi(s,n,o,a,r,i),h=Pi(t,e,r,i,s,n),u=Pi(t,e,r,i,o,a);if((l>0.000000001&&c<-0.000000001||l<-0.000000001&&c>0.000000001)&&(h>0.000000001&&u<-0.000000001||h<-0.000000001&&u>0.000000001))return!0;if(Math.abs(l)<=0.000000001&&On(s,n,o,a,t,e))return!0;if(Math.abs(c)<=0.000000001&&On(s,n,o,a,r,i))return!0;if(Math.abs(h)<=0.000000001&&On(t,e,r,i,s,n))return!0;if(Math.abs(u)<=0.000000001&&On(t,e,r,i,o,a))return!0;return!1}function Mi(t,e,r,i,s){if(!Array.isArray(t)||t.length!==e)throw Error(`PixiRenderer: slot "${i}" on page "${s}" has invalid ${r} length ${Array.isArray(t)?t.length:"non-array"} (expected ${e})`);for(let n=0;n= ${e})`);for(let n=0;n= 6)`);let i=t.polygon;for(let a=0;a 0)`);if(t.convex){let a=n>0?1:-1;for(let l=0;l0?h.slot:"",f=s.get(u)??0;s.set(u,f+1);let d=f===0?u:`${u}\x00${f}`,m=i[c];if(!m)m={kind:"region",poolKey:d,slot:h,texture:C.EMPTY,blendMode:"normal",color:16777215,alpha:1,darkMatrix:null,clipPlan:null},i[c]=m;this.prepareSlot(h,d,m)}let n=!1;for(let c=0;c0?i[0].slot.clip:void 0,a=o!==void 0;if(a){for(let c=1;cr)this.root.removeChildAt(this.root.children.length-1);let l=0;for(let c=0;c0)e.filters=[];if(e.tint!==r.color)e.tint=r.color;if(e.alpha!==r.alpha)e.alpha=r.alpha;if(e.blendMode!==r.blendMode)e.blendMode=r.blendMode}}prepareSlot(t,e,r){if(!t||typeof t!=="object")throw Error("PixiRenderer: encountered invalid slot entry in frame.order (expected PosedSlot object)");let i=typeof t.slot==="string"&&t.slot.length>0?t.slot:"",s=typeof t.page==="string"&&t.page.length>0?t.page:"";if(t.kind!=="region"&&t.kind!=="mesh")throw Error(`PixiRenderer: slot "${i}" on page "${s}" has unsupported kind "${String(t.kind)}" (expected "region" or "mesh")`);let n=c0[t.blend];if(n===void 0)throw Error(`PixiRenderer: slot "${i}" on page "${s}" has unsupported blend mode "${String(t.blend)}" (expected Normal, Additive, Multiply, or Screen)`);if(typeof t.pma!=="boolean")throw Error(`PixiRenderer: slot "${i}" on page "${s}" has invalid pma=${String(t.pma)} (expected boolean)`);let o=this.validatedPages.get(t.page);if(!o){let b=this.assets.pageImages.get(t.page);if(!b)throw Error(`PixiRenderer: slot "${i}" references page "${s}", which is missing from assets.pageImages (expected HTMLImageElement for "${s}")`);let M=b.naturalWidth||b.width,T=b.naturalHeight||b.height;if(!Number.isFinite(M)||!Number.isFinite(T)||M<=0||T<=0)throw Error(`PixiRenderer: slot "${i}" page "${s}" has invalid image dimensions ${String(M)}x${String(T)} (expected positive decoded dimensions)`);o={pageImage:b,pw:M,ph:T},this.validatedPages.set(t.page,o)}let{pageImage:a,pw:l,ph:c}=o;Mi(t.tint,4,"tint",i,s);let h=null;if(t.dark!==void 0)Mi(t.dark,3,"dark",i,s),h=u0(t.tint,t.dark);let u=null;if(t.clip!==void 0){let b=this.validatedClips.get(t.clip);if(!b)b=m0(t.clip,i,s),this.validatedClips.set(t.clip,b);u=b}let f=Math.round(Lt(t.tint[0])*255),d=Math.round(Lt(t.tint[1])*255),m=Math.round(Lt(t.tint[2])*255),x=f<<16|d<<8|m,_=Lt(t.tint[3]);if(t.kind==="region"){Mi(t.corners,8,"corners",i,s);let b=this.regionPool.get(e),M;if(!this.alwaysReuploadGeometry&&b&&b.uvsRef===t.uvs&&b.page===t.page&&b.pma===t.pma&&b.texture!==null&&!b.texture.destroyed)this.sourceFor(a,t.pma),M=b.texture;else Mi(t.uvs,8,"uvs",i,s),M=this.regionTextureFor(a,l,c,t);r.kind="region",r.poolKey=e,r.slot=t,r.texture=M,r.blendMode=n,r.color=x,r.alpha=_,r.darkMatrix=h,r.clipPlan=u;return}p0(t.vertices,6,"vertices",i,s);let g=this.meshPool.get(e),S=!this.alwaysReuploadGeometry&&g!==void 0&&g.positions.length===t.vertices.length;if(!S||g.uvsRef!==t.uvs)Mi(t.uvs,t.vertices.length,"uvs",i,s);if(!S||g.trianglesRef!==t.triangles){if(!Array.isArray(t.triangles)||t.triangles.length<3||t.triangles.length%3!==0)throw Error(`PixiRenderer: slot "${i}" on page "${s}" has invalid triangles length ${Array.isArray(t.triangles)?t.triangles.length:"non-array"} (expected multiple of 3 >= 3)`);let b=t.vertices.length/2;for(let M=0;M=b)throw Error(`PixiRenderer: slot "${i}" on page "${s}" has out-of-range triangle index triangles[${M}]=${String(T)} (expected integer in 0..${b-1})`)}}if(typeof t.sequence!=="number"||!Number.isInteger(t.sequence))throw Error(`PixiRenderer: slot "${i}" on page "${s}" has invalid sequence=${String(t.sequence)} (expected integer)`);let v;if(!this.alwaysReuploadGeometry&&g&&g.page===t.page&&g.pma===t.pma&&g.texture!==null&&!g.texture.destroyed)this.sourceFor(a,t.pma),v=g.texture;else v=this.pageTextureFor(a,t.page,t.pma);r.kind="mesh",r.poolKey=e,r.slot=t,r.texture=v,r.blendMode=n,r.color=x,r.alpha=_,r.darkMatrix=h,r.clipPlan=u}sourceFor(t,e){let r=e?"premultiplied-alpha":"premultiply-alpha-on-upload",i=Ci.get(t);if(!i||i.source.destroyed){let s=new ee({resource:t,alphaMode:r,scaleMode:"linear",addressMode:"clamp-to-edge",autoGenerateMipmaps:!1});i={source:s,pma:e,holders:new Set},Ci.set(t,i),Jl.add(s)}else if(i.pma!==e)i.pma=e,i.source.alphaMode=r,i.source.update();if(!this.retainedPages.has(t))this.retainedPages.add(t),i.holders.add(this);return i.source}pageTextureFor(t,e,r){let i=`${e}\x00${r?"1":"0"}`,s=this.pageTextures.get(i);if(s)return this.sourceFor(t,r),s;let n=this.sourceFor(t,r);return s=new C({source:n}),this.pageTextures.set(i,s),s}regionTextureFor(t,e,r,i){let[s,n,o,a,l,c,h,u]=i.uvs,f=`${i.page}\x00${i.pma?"1":"0"}\x00${s},${n},${o},${a},${l},${c},${h},${u}`,d=this.regionTextures.get(f);if(d)return this.sourceFor(t,i.pma),d;let m=this.sourceFor(t,i.pma),x=Math.min(s,o,l,h),_=Math.max(s,o,l,h),g=Math.min(n,a,c,u),S=Math.max(n,a,c,u),v=Math.max(1,(_-x)*e),b=Math.max(1,(S-g)*r),M=new F(x*e,g*r,v,b),T=Math.abs(o-l) readFileSync(join(ROOT_DIR, path), 'utf8'); -test('approved depth revision bundle is byte exact, including original entry and raw gzip', () => { +test('approved GPU bundle is byte exact, including original entry and raw gzip', () => { verifyHeadTurnBundle(ROOT_DIR); const gzip = readFileSync(join(ROOT_DIR, HEAD_TURN_PATH, 'bundle/scene.bin.gz')); expect([...gzip.subarray(0, 2)]).toEqual([31, 139]); }); -test('changed depth image or compiled depth payload fails the approved inventory', () => { +test('changed geometry, shader, runtime or controller fails the approved inventory', () => { const copy = mkdtempSync(join(tmpdir(), 'head-turn-depth-')); try { const demo = join(copy, HEAD_TURN_PATH); cpSync(join(ROOT_DIR, HEAD_TURN_PATH, 'bundle'), join(demo, 'bundle'), { recursive: true }); cpSync(join(ROOT_DIR, HEAD_TURN_PATH, 'bundle-manifest.json'), join(demo, 'bundle-manifest.json')); - for (const name of ['depth.jpg', 'scene.bin.gz']) { + for (const name of ['depth.jpg', 'scene.bin.gz', 'app.js', 'runtime.js', 'aperture.js']) { const path = join(demo, 'bundle', name); const original = readFileSync(path); const changed = Buffer.from(original); @@ -90,3 +90,14 @@ test('curvature check verification names a missing file and a broken reference', rmSync(copy, { recursive: true, force: true }); } }); + +// The real WebGL1/2 compilation and pixel comparison run in the browser gate. +// This source contract rejects the explicit ES300 header that bypassed Pixi's compatibility path. +test('head-turn shader leaves GLSL compatibility preprocessing to the bundled Pixi', () => { + const app = read(`${HEAD_TURN_PATH}/bundle/app.js`); + const shaders = [...app.matchAll(/const (vertex|fragment)=`([\s\S]*?)`;/g)]; + expect(shaders.map((match) => match[1])).toEqual(['vertex', 'fragment']); + for (const shader of shaders) expect(shader[2]).not.toMatch(/#\s*version/); + // addProgramDefines consumes this exact output declaration and maps finalColor. + expect(shaders[1]![2]).toContain('out vec4 finalColor;'); +}); diff --git a/src/head-turn.ts b/src/head-turn.ts index 107b71d..1f0147c 100644 --- a/src/head-turn.ts +++ b/src/head-turn.ts @@ -3,8 +3,8 @@ import { join } from 'node:path'; import { artifactManifest, parseManifest } from './deployment.ts'; export const HEAD_TURN_PATH = 'demos/head-turn'; -// Approved ten-file preview inventory, including the confirmed depth revision. -export const HEAD_TURN_BUNDLE_SHA256 = '515c89f1307308e5e41dcfe5a0fc7c69e5751539863ef7cb7c14e1cf6ffa38e2'; +// Approved ten-file preview inventory, including the confirmed depth and GPU displacement revisions. +export const HEAD_TURN_BUNDLE_SHA256 = 'fb7bc15b188ed8f29a931067df44a4c3cdb61ba3f114c706722eee73c8a6c2fc'; export function verifyHeadTurnBundle(root: string): void { const demo = join(root, HEAD_TURN_PATH); const expected = parseManifest(readFileSync(join(demo, 'bundle-manifest.json'), 'utf8'));