[object Object]

← back to Quadrille Showroom

V9 Walk-In Room: photo-real depth — soft contact shadows ground sofa/rug/lamp, vertex-baked corner AO modulated by window light (bright near window, deep far), procedural paper bump for surface tooth; killed the stacked-multiply black wedge by softening + offsetting layers

cbee4bb6f3a0a80d5b312aa99481eb4b174b7b1e · 2026-06-28 09:30:52 -0700 · Steve

Files touched

Diff

commit cbee4bb6f3a0a80d5b312aa99481eb4b174b7b1e
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 28 09:30:52 2026 -0700

    V9 Walk-In Room: photo-real depth — soft contact shadows ground sofa/rug/lamp, vertex-baked corner AO modulated by window light (bright near window, deep far), procedural paper bump for surface tooth; killed the stacked-multiply black wedge by softening + offsetting layers
---
 public/proto/shots/v9-walkin-floor.png | Bin 0 -> 985614 bytes
 public/proto/shots/v9-walkin-sofa.png  | Bin 0 -> 1617340 bytes
 public/proto/shots/v9-walkin.png       | Bin 0 -> 1230824 bytes
 public/proto/v9-walkin.html            | 148 +++++++++++++++++++++++++++++----
 4 files changed, 134 insertions(+), 14 deletions(-)

diff --git a/public/proto/shots/v9-walkin-floor.png b/public/proto/shots/v9-walkin-floor.png
new file mode 100644
index 0000000..73000c6
Binary files /dev/null and b/public/proto/shots/v9-walkin-floor.png differ
diff --git a/public/proto/shots/v9-walkin-sofa.png b/public/proto/shots/v9-walkin-sofa.png
new file mode 100644
index 0000000..1b48d6f
Binary files /dev/null and b/public/proto/shots/v9-walkin-sofa.png differ
diff --git a/public/proto/shots/v9-walkin.png b/public/proto/shots/v9-walkin.png
new file mode 100644
index 0000000..8e52f23
Binary files /dev/null and b/public/proto/shots/v9-walkin.png differ
diff --git a/public/proto/v9-walkin.html b/public/proto/v9-walkin.html
index 7722f98..9300731 100644
--- a/public/proto/v9-walkin.html
+++ b/public/proto/v9-walkin.html
@@ -124,7 +124,7 @@
     // ================= LIGHTING (warm, soft) =================
     const hemi = new THREE.HemisphereLight(0xfff3e0, 0x3a2f26, 0.42);
     scene.add(hemi);
-    const ambient = new THREE.AmbientLight(0xffe8cf, 0.20);
+    const ambient = new THREE.AmbientLight(0xffe8cf, 0.26);
     scene.add(ambient);
 
     // Daylight pouring through the window (key light), warm + directional so the
@@ -156,25 +156,81 @@
     const tex = { loader: new THREE.TextureLoader() };
     tex.loader.crossOrigin = 'anonymous';
 
-    // wall material starts neutral; texture swapped in on load
-    const wallMat = new THREE.MeshStandardMaterial({ color:0xece4d6, roughness:0.92, metalness:0.0, side:THREE.FrontSide });
+    // CONTRARIAN FIX — a procedural PAPER bump so the wall has surface tooth: the
+    // warm window key grazes it and catches micro-relief, instead of reading as a
+    // flat decal pasted on cardboard. China Seas is hand screen-printed on paper.
+    function paperBumpTex(){
+      const c=document.createElement('canvas'); c.width=c.height=256;
+      const g=c.getContext('2d');
+      g.fillStyle='#808080'; g.fillRect(0,0,256,256);
+      const img=g.getImageData(0,0,256,256); const d=img.data;
+      for(let i=0;i<d.length;i+=4){ const n=128+(Math.random()-0.5)*46;  // fine fibre grain
+        d[i]=d[i+1]=d[i+2]=n; }
+      g.putImageData(img,0,0);
+      // a few faint long fibres for a laid-paper feel
+      g.globalAlpha=0.05; g.strokeStyle='#fff';
+      for(let k=0;k<40;k++){ g.beginPath(); const y=Math.random()*256;
+        g.moveTo(0,y); g.lineTo(256,y+(Math.random()-0.5)*6); g.stroke(); }
+      const t=new THREE.CanvasTexture(c);
+      t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(8,8);
+      return t;
+    }
+    const PAPER_BUMP=paperBumpTex();
+
+    // wall material starts neutral; texture swapped in on load.
+    // vertexColors:true lets the baked corner-AO (dark toward edges) multiply the
+    // wallpaper map in-shader → soft corner occlusion with zero floating planes.
+    const wallMat = new THREE.MeshStandardMaterial({ color:0xece4d6, roughness:0.94, metalness:0.0,
+      side:THREE.FrontSide, vertexColors:true, bumpMap:PAPER_BUMP, bumpScale:0.012 });
+
+    // CONTRARIAN FIX — bake ambient occlusion into a wall's vertices: darken toward
+    // the two vertical corner seams and the floor/ceiling so the room reads as lit,
+    // not a flat-bright plastic box. Subdivided so the falloff is smooth. The AO is
+    // MODULATED by window proximity (windowFn returns 0..1 brightness boost) so the
+    // shade AGREES with the warm key light from the left-wall window instead of
+    // fighting it — corners near the window stay light, far corners go deeper.
+    function addCornerAO(geo, w, h, windowFn){
+      const pos=geo.attributes.position; const cols=[]; const col=new THREE.Color();
+      for(let i=0;i<pos.count;i++){
+        const x=pos.getX(i), y=pos.getY(i);
+        const fx=(x+w/2)/w, fy=(y+h/2)/h;            // 0..1 across / up the wall
+        const edgeX=Math.min(fx,1-fx)*2;             // 0 at vertical corner → 1 mid
+        const edgeY=Math.min(fy,1-fy)*2;             // 0 at floor/ceil → 1 mid
+        const aoX=Math.max(0, 1-edgeX/0.34);
+        const aoY=Math.max(0, 1-edgeY/0.20);
+        let ao=Math.min(0.55, aoX*0.42 + aoY*0.30 + aoX*aoY*0.25);
+        if(windowFn){ ao *= windowFn(fx); }          // lift AO near the window
+        const v=1-ao; col.setRGB(v,v,v); cols.push(col.r,col.g,col.b);
+      }
+      geo.setAttribute('color', new THREE.Float32BufferAttribute(cols,3));
+    }
 
-    function wall(w, h){
-      const g = new THREE.PlaneGeometry(w, h);
+    // returns 0..1 AO strength across a wall given which local-x end faces the
+    // window (left wall). nearEnd in {0,1}: which fx is closest to the window.
+    function windowAO(nearEnd){
+      return (fx)=>{
+        const distToWindow = nearEnd===0 ? fx : (1-fx);   // 0 at window end
+        return THREE.MathUtils.clamp(0.35 + distToWindow*0.95, 0.35, 1.0);
+      };
+    }
+
+    function wall(w, h, windowFn){
+      const g = new THREE.PlaneGeometry(w, h, 24, 24);
+      addCornerAO(g, w, h, windowFn);
       const m = wallMat.clone();
       m.userData.wallW = w;   // physical width of this wall (m)
       return new THREE.Mesh(g, m);
     }
 
     const walls = [];
-    // back wall (-Z)
-    let m = wall(W, H); m.position.set(0, H/2, -D/2); walls.push(m);
-    // front wall (+Z) faces inward
-    m = wall(W, H); m.position.set(0, H/2, D/2); m.rotation.y = Math.PI; walls.push(m);
-    // left wall (-X)
-    m = wall(D, H); m.position.set(-W/2, H/2, 0); m.rotation.y = Math.PI/2; walls.push(m);
-    // right wall (+X)
-    m = wall(D, H); m.position.set(W/2, H/2, 0); m.rotation.y = -Math.PI/2; walls.push(m);
+    // back wall (-Z): local +x → world +x, so fx=0 (world -W/2) is the window side
+    let m = wall(W, H, windowAO(0)); m.position.set(0, H/2, -D/2); walls.push(m);
+    // front wall (+Z, rotated π): local +x → world -x, so fx=1 is the window side
+    m = wall(W, H, windowAO(1)); m.position.set(0, H/2, D/2); m.rotation.y = Math.PI; walls.push(m);
+    // left wall (-X) = THE WINDOW WALL: keep it bright (window light washes it)
+    m = wall(D, H, ()=>0.30); m.position.set(-W/2, H/2, 0); m.rotation.y = Math.PI/2; walls.push(m);
+    // right wall (+X) = farthest from the window: full AO
+    m = wall(D, H, ()=>1.0); m.position.set(W/2, H/2, 0); m.rotation.y = -Math.PI/2; walls.push(m);
     walls.forEach(w => { w.receiveShadow = true; scene.add(w); });
 
     // ceiling — soft off-white
@@ -224,6 +280,60 @@
       hbar.position.set(wx-0.01, wy, -D*0.10); scene.add(hbar);
     })();
 
+    // ================= CONTACT SHADOWS + CORNER AO (CONTRARIAN FIX) =========
+    // Real photos read as real because objects are anchored to the floor by a soft
+    // contact shadow, and room corners fall into ambient-occlusion darkness. The
+    // hardware shadow map alone leaves a "plastic box" look (hard-edged, no corner
+    // falloff), so we bake soft radial/edge gradients and lay them over the floor.
+    function radialShadowTex(soft){
+      const c=document.createElement('canvas'); c.width=c.height=256;
+      const g=c.getContext('2d');
+      const grd=g.createRadialGradient(128,128,8, 128,128,128);
+      grd.addColorStop(0,`rgba(0,0,0,${soft?0.42:0.6})`);
+      grd.addColorStop(0.55,`rgba(0,0,0,${soft?0.18:0.28})`);
+      grd.addColorStop(1,'rgba(0,0,0,0)');
+      g.fillStyle=grd; g.fillRect(0,0,256,256);
+      const t=new THREE.CanvasTexture(c);
+      if('colorSpace' in t) t.colorSpace=THREE.SRGBColorSpace;
+      return t;
+    }
+    const SHADOW_TEX = radialShadowTex(false);
+    function contactShadow(x,z,w,d,opacity){
+      const m=new THREE.Mesh(new THREE.PlaneGeometry(w,d),
+        new THREE.MeshBasicMaterial({ map:SHADOW_TEX, transparent:true,
+          opacity:opacity==null?0.85:opacity, depthWrite:false,
+          blending:THREE.MultiplyBlending, polygonOffset:true, polygonOffsetFactor:-2 }));
+      m.rotation.x=-Math.PI/2; m.position.set(x,0.006,z); m.renderOrder=2; scene.add(m);
+      return m;
+    }
+
+    // Floor perimeter AO — a soft floor overlay darkening toward the floor/wall
+    // seams. Kept GENTLE (low opacity, short reach) so it does NOT stack with the
+    // furniture contact shadows into a crushed-black wedge — the combined floor
+    // darkness must read as soft shade, never a void.
+    (function cornerAO(){
+      const c=document.createElement('canvas'); c.width=c.height=512;
+      const g=c.getContext('2d');
+      g.fillStyle='rgba(0,0,0,0)'; g.fillRect(0,0,512,512);
+      // four edge gradients darkening inward from each wall (softer + shorter reach)
+      const edge=(x0,y0,x1,y1)=>{ const gr=g.createLinearGradient(x0,y0,x1,y1);
+        gr.addColorStop(0,'rgba(0,0,0,0.26)'); gr.addColorStop(0.30,'rgba(0,0,0,0.07)');
+        gr.addColorStop(1,'rgba(0,0,0,0)'); g.fillStyle=gr; g.fillRect(0,0,512,512); };
+      edge(0,0,150,0); edge(512,0,362,0); edge(0,0,0,150); edge(0,512,0,362);
+      const t=new THREE.CanvasTexture(c);
+      if('colorSpace' in t) t.colorSpace=THREE.SRGBColorSpace;
+      const ao=new THREE.Mesh(new THREE.PlaneGeometry(W,D),
+        new THREE.MeshBasicMaterial({ map:t, transparent:true, opacity:0.7,
+          depthWrite:false, blending:THREE.MultiplyBlending }));
+      ao.rotation.x=-Math.PI/2; ao.position.set(0,0.003,0); ao.renderOrder=1; scene.add(ao);
+    })();
+
+    // Wall-corner AO is baked directly into the wall geometry as VERTEX COLORS
+    // (see addCornerAO below, applied when each wall is built) — darkening toward
+    // the vertical corner seams + the floor/ceiling edges. This multiplies with the
+    // wallpaper texture in-shader, so there are NO floating planes / black-bar
+    // artifacts; the corners simply fall into soft shade like a real lit room.
+
     // ================= FURNITURE silhouette =================
     (function sofa(){
       const grp = new THREE.Group();
@@ -235,19 +345,29 @@
       const seat = new THREE.Mesh(new THREE.BoxGeometry(2.05, 0.18, 0.78), new THREE.MeshStandardMaterial({color:0xe7ddc8, roughness:1}));
       seat.position.set(0, 0.58, 0.04); seat.castShadow=true; grp.add(seat);
       [-1.0,1.0].forEach(x=>{ const a=new THREE.Mesh(new THREE.BoxGeometry(0.2,0.6,0.95),mat); a.position.set(x,0.45,0); a.castShadow=true; grp.add(a); });
-      grp.position.set(0, 0, -D/2 + 0.78);  // against the back wall
+      const sofaZ = -D/2 + 0.78;
+      grp.position.set(0, 0, sofaZ);  // against the back wall
       grp.rotation.y = 0;
       scene.add(grp);
+      // CONTRARIAN FIX — soft contact shadow grounding the sofa to the floor so it
+      // doesn't float like a plastic prop. Kept soft (0.6) so it doesn't stack with
+      // the floor perimeter AO into a crushed-black wedge; offset forward of the
+      // back wall so the two shadows sit side-by-side, not on top of each other.
+      contactShadow(0, sofaZ+0.30, 3.0, 1.5, 0.6);
       // round rug
       const rug = new THREE.Mesh(new THREE.CircleGeometry(1.7, 48),
         new THREE.MeshStandardMaterial({ color:0xb9a88c, roughness:1 }));
       rug.rotation.x = -Math.PI/2; rug.position.set(0, 0.012, -D/2 + 1.7); rug.receiveShadow=true; scene.add(rug);
+      // soft shadow seating the rug onto the floor
+      contactShadow(0, -D/2+1.9, 3.9, 3.9, 0.4);
       // a floor lamp by the sofa (gives the lamp glow a body)
       const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.02,0.02,1.5,12), new THREE.MeshStandardMaterial({color:0x3a3026}));
       pole.position.set(W*0.32, 0.75, -D*0.30); scene.add(pole);
       const shade = new THREE.Mesh(new THREE.CylinderGeometry(0.18,0.24,0.28,20),
         new THREE.MeshStandardMaterial({color:0xfff1d8, emissive:0xffe6c0, emissiveIntensity:0.6, roughness:1}));
       shade.position.set(W*0.32, 1.5, -D*0.30); scene.add(shade);
+      // contact shadow pooling under the floor lamp base
+      contactShadow(W*0.32, -D*0.30, 0.7, 0.7, 0.8);
     })();
 
     // ================= WALLPAPER TEXTURE =================

← 7f3989c V8 Swipe Tower: keep biases the queue via enrichment taste m  ·  back to Quadrille Showroom  ·  V10 Concierge Nook: boards now genuinely LIE on the table — 27c72af →