← 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
A public/proto/shots/v9-walkin-floor.pngA public/proto/shots/v9-walkin-sofa.pngA public/proto/shots/v9-walkin.pngM public/proto/v9-walkin.html
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 →