← back to Dw War Room
docs/plans/2026-03-02-figma-threejs-photorealism-plan.md
1624 lines
# Figma + Three.js Photorealistic Showroom — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build a Figma MCP-driven pipeline that generates photorealistic endless-wings showroom scenes for both the War Room (port 4060, TypeScript/Vite) and Boardroom-3D (port 7681, vanilla JS), featuring architectural trade showroom aesthetics with exposed brick, track lighting, and PBR materials.
**Architecture:** Figma design → MCP `get_design_context` extraction → Claude transform → TypeScript/JS modules. The showroom uses an endless corridor with 50 vendor wings (lazy-loaded), exposed brick walls with sample book displays, polished concrete floors, and industrial track lighting. Rendering upgraded with SSAO, HDR environment maps, procedural normal maps, depth of field, and color grading.
**Tech Stack:** Three.js r170 (war room) / r128 CDN (boardroom-3d), Vite 6, TypeScript 5.7, Figma MCP tools, procedural canvas normal/roughness maps, EffectComposer post-processing pipeline.
---
## Phase 1: Figma Showroom Design + MCP Pipeline
### Task 1: Create Figma Showroom Floor Plan
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/figma-layout.json` (extracted layout data)
**Step 1: Create the Figma design using generate_figma_design**
Use the Figma MCP `generate_figma_design` tool to create a new Figma file with the showroom floor plan. The design should include:
- Main corridor frame (40ft x 20ft, exposed brick fill)
- 6 vendor wing bay frames extending perpendicular (each 12ft x 10ft)
- Central display table rectangle
- Track lighting rail lines along ceiling
- Product display wall rectangles (4ft x 8ft each, wallpaper texture fills)
- Glass entry frame
- Color tokens: brick (#8b4513), concrete (#8a8580), aluminum (#c0c0c0), warm wood (#8b6914)
**Step 2: Extract design context via MCP**
```typescript
// Use get_design_context to extract the showroom layout
const designContext = await mcp.get_design_context({
nodeId: '<root-frame-id>',
fileKey: '<file-key>'
});
```
Save extracted JSON to `figma-layout.json`.
**Step 3: Extract design tokens via MCP**
```typescript
const tokens = await mcp.get_variable_defs({
nodeId: '<root-frame-id>',
fileKey: '<file-key>'
});
```
**Step 4: Commit**
```bash
git add src/showroom/figma-layout.json
git commit -m "feat(showroom): extract Figma showroom layout via MCP"
```
---
### Task 2: Build Figma-to-SceneGraph Transformer
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/FigmaTransformer.ts`
**Step 1: Write the transformer module**
```typescript
// FigmaTransformer.ts — Converts Figma design context into Three.js scene graph config
import type { FigmaNode, SceneConfig, WallConfig, WingConfig, LightConfig } from './types';
export interface SceneConfig {
corridor: {
width: number; // world units (1 unit = 1 foot)
depth: number;
height: number;
wallMaterial: 'brick' | 'concrete' | 'charcoal';
floorMaterial: 'polished-concrete' | 'wood' | 'marble';
};
wings: WingConfig[];
walls: WallConfig[];
lights: LightConfig[];
furniture: FurnitureConfig[];
entry: { position: [number, number, number]; signText: string };
}
export interface WingConfig {
index: number;
vendor: string;
position: [number, number, number];
rotation: number;
panelCount: number;
accentColor: string; // hex from Figma fill
}
export interface WallConfig {
position: [number, number, number];
dimensions: [number, number]; // width, height
material: string;
hasBookshelf: boolean;
hasSamplePanels: boolean;
sampleCount: number;
}
export interface LightConfig {
type: 'spot' | 'track' | 'ambient' | 'point';
position: [number, number, number];
target?: [number, number, number];
color: string;
intensity: number;
angle?: number; // SpotLight cone angle
penumbra?: number;
}
export interface FurnitureConfig {
type: 'display-table' | 'stool' | 'bookshelf' | 'sample-rack';
position: [number, number, number];
dimensions: [number, number, number];
}
/**
* Transform Figma design JSON into Three.js scene configuration.
* Maps Figma frames → positions, rectangles → geometry, fills → materials.
*/
export function transformFigmaToScene(figmaData: any): SceneConfig {
const root = figmaData.document || figmaData;
const config: SceneConfig = {
corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
wings: [],
walls: [],
lights: [],
furniture: [],
entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
};
// Traverse Figma node tree
function traverse(node: any, depth = 0) {
if (!node) return;
// Map FRAME nodes to wings or rooms based on name
if (node.type === 'FRAME' && node.name?.toLowerCase().includes('wing')) {
const bbox = node.absoluteBoundingBox || {};
config.wings.push({
index: config.wings.length,
vendor: node.name.replace(/wing[-_\s]*/i, '').trim(),
position: [bbox.x / 10 || config.wings.length * 4, 0, bbox.y / 10 || 0],
rotation: 0,
panelCount: (node.children || []).filter((c: any) => c.type === 'RECTANGLE').length || 10,
accentColor: extractFillColor(node) || '#c0c0c0',
});
}
// Map RECTANGLE nodes with IMAGE fills to wall sample panels
if (node.type === 'RECTANGLE' && node.fills?.some((f: any) => f.type === 'IMAGE')) {
const bbox = node.absoluteBoundingBox || {};
config.walls.push({
position: [bbox.x / 10 || 0, bbox.height / 20 || 2, bbox.y / 10 || 0],
dimensions: [bbox.width / 10 || 4, bbox.height / 10 || 8],
material: 'brick',
hasBookshelf: false,
hasSamplePanels: true,
sampleCount: 1,
});
}
// Map circles/ellipses to spotlights
if (node.type === 'ELLIPSE' && node.name?.toLowerCase().includes('light')) {
const bbox = node.absoluteBoundingBox || {};
config.lights.push({
type: 'spot',
position: [bbox.x / 10 || 0, 10, bbox.y / 10 || 0],
target: [bbox.x / 10 || 0, 0, bbox.y / 10 || 0],
color: extractFillColor(node) || '#ffffff',
intensity: 1.5,
angle: Math.PI / 6,
penumbra: 0.5,
});
}
if (node.children) node.children.forEach((c: any) => traverse(c, depth + 1));
}
traverse(root);
// Add default track lighting if none extracted
if (config.lights.length === 0) {
for (let i = 0; i < 8; i++) {
config.lights.push({
type: 'spot',
position: [i * 5 - 17.5, 11, 0],
target: [i * 5 - 17.5, 0, 0],
color: '#f5e6d0',
intensity: 1.2,
angle: Math.PI / 5,
penumbra: 0.4,
});
}
}
return config;
}
function extractFillColor(node: any): string | null {
const fill = (node.fills || []).find((f: any) => f.type === 'SOLID' && f.visible !== false);
if (!fill?.color) return null;
const r = Math.round(fill.color.r * 255);
const g = Math.round(fill.color.g * 255);
const b = Math.round(fill.color.b * 255);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
```
**Step 2: Commit**
```bash
git add src/showroom/FigmaTransformer.ts
git commit -m "feat(showroom): Figma-to-SceneGraph transformer"
```
---
## Phase 2: Photorealistic Material System
### Task 3: Procedural Normal Map Generator
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ProceduralMaps.ts`
**Step 1: Write procedural normal/roughness map generators**
```typescript
// ProceduralMaps.ts — Canvas-based procedural normal and roughness maps
// Zero external file dependencies — all generated at runtime
import * as THREE from 'three';
/**
* Generate a brick wall normal map using canvas.
* Creates realistic mortar lines and brick surface variation.
*/
export function generateBrickNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
// Base neutral normal (pointing straight out: RGB 128,128,255)
ctx.fillStyle = 'rgb(128, 128, 255)';
ctx.fillRect(0, 0, width, height);
const brickW = width / 8;
const brickH = height / 16;
const mortarW = 3;
for (let row = 0; row < 16; row++) {
const offset = (row % 2) * (brickW / 2); // stagger rows
for (let col = -1; col < 9; col++) {
const x = col * brickW + offset;
const y = row * brickH;
// Mortar lines (recessed = normal pointing inward)
// Horizontal mortar
ctx.fillStyle = 'rgb(128, 100, 255)'; // slight downward normal
ctx.fillRect(0, y, width, mortarW);
// Vertical mortar
ctx.fillStyle = 'rgb(100, 128, 255)'; // slight leftward normal
ctx.fillRect(x, y, mortarW, brickH);
// Brick surface noise (subtle variation)
for (let i = 0; i < 20; i++) {
const nx = x + Math.random() * brickW;
const ny = y + mortarW + Math.random() * (brickH - mortarW * 2);
const r = 128 + (Math.random() - 0.5) * 12;
const g = 128 + (Math.random() - 0.5) * 12;
ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
ctx.fillRect(nx, ny, 3 + Math.random() * 6, 2 + Math.random() * 4);
}
}
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(2, 2);
return tex;
}
/**
* Generate a polished concrete normal map.
* Subtle micro-texture with occasional aggregate patches.
*/
export function generateConcreteNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
// Base neutral normal
ctx.fillStyle = 'rgb(128, 128, 255)';
ctx.fillRect(0, 0, width, height);
// Micro-texture noise — polished concrete has subtle surface variation
for (let i = 0; i < 5000; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const r = 128 + (Math.random() - 0.5) * 6;
const g = 128 + (Math.random() - 0.5) * 6;
ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
ctx.fillRect(x, y, 1 + Math.random() * 3, 1 + Math.random() * 3);
}
// Aggregate patches (larger bumps)
for (let i = 0; i < 80; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const radius = 4 + Math.random() * 12;
const r = 128 + (Math.random() - 0.5) * 18;
const g = 128 + (Math.random() - 0.5) * 18;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
ctx.fill();
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(3, 3);
return tex;
}
/**
* Generate a brushed aluminum normal map.
* Directional brushing lines for metallic surfaces.
*/
export function generateBrushedAluminumNormalMap(width = 512, height = 512): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = 'rgb(128, 128, 255)';
ctx.fillRect(0, 0, width, height);
// Horizontal brush strokes
for (let y = 0; y < height; y++) {
const r = 128 + (Math.random() - 0.5) * 4;
const g = 128 + Math.sin(y * 0.3) * 3 + (Math.random() - 0.5) * 2;
ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
ctx.fillRect(0, y, width, 1);
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
return tex;
}
/**
* Generate a wood grain normal map for shelving.
*/
export function generateWoodNormalMap(width = 512, height = 512): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = 'rgb(128, 128, 255)';
ctx.fillRect(0, 0, width, height);
// Wood grain lines (vertical, slightly wavy)
for (let x = 0; x < width; x += 4 + Math.random() * 8) {
ctx.beginPath();
ctx.moveTo(x, 0);
for (let y = 0; y < height; y += 10) {
ctx.lineTo(x + Math.sin(y * 0.02 + x * 0.1) * 3, y);
}
const r = 128 + (Math.random() - 0.5) * 10;
ctx.strokeStyle = `rgb(${r}, 128, 255)`;
ctx.lineWidth = 1 + Math.random() * 2;
ctx.stroke();
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
return tex;
}
/**
* Generate a fabric/paper subtle normal map for wallcovering product panels.
* Very subtle — just enough to catch light at grazing angles.
*/
export function generateFabricNormalMap(width = 256, height = 256): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = 'rgb(128, 128, 255)';
ctx.fillRect(0, 0, width, height);
// Woven texture: alternating horizontal and vertical micro-lines
for (let y = 0; y < height; y += 2) {
for (let x = 0; x < width; x += 2) {
const horizontal = (y / 2) % 2 === 0;
const r = 128 + (horizontal ? 2 : -2) + (Math.random() - 0.5) * 3;
const g = 128 + (horizontal ? -2 : 2) + (Math.random() - 0.5) * 3;
ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
ctx.fillRect(x, y, 2, 2);
}
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(4, 4);
return tex;
}
```
**Step 2: Commit**
```bash
git add src/showroom/ProceduralMaps.ts
git commit -m "feat(showroom): procedural normal map generators (brick, concrete, aluminum, wood, fabric)"
```
---
### Task 4: PBR Material Factory
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomMaterials.ts`
**Step 1: Write the material factory**
```typescript
// ShowroomMaterials.ts — PBR material definitions for photorealistic showroom
import * as THREE from 'three';
import {
generateBrickNormalMap,
generateConcreteNormalMap,
generateBrushedAluminumNormalMap,
generateWoodNormalMap,
generateFabricNormalMap,
} from './ProceduralMaps';
export interface MaterialLibrary {
brick: THREE.MeshStandardMaterial;
concrete: THREE.MeshStandardMaterial;
aluminum: THREE.MeshStandardMaterial;
wood: THREE.MeshStandardMaterial;
fabric: THREE.MeshStandardMaterial;
steel: THREE.MeshStandardMaterial;
glass: THREE.MeshStandardMaterial;
trackLight: THREE.MeshStandardMaterial;
}
/**
* Create the full PBR material library for the showroom.
* Each material has proper normal maps for photorealistic surface detail.
*/
export function createMaterialLibrary(): MaterialLibrary {
return {
brick: new THREE.MeshStandardMaterial({
color: 0x8b4513,
metalness: 0.0,
roughness: 0.85,
normalMap: generateBrickNormalMap(),
normalScale: new THREE.Vector2(1.2, 1.2),
}),
concrete: new THREE.MeshStandardMaterial({
color: 0x8a8580,
metalness: 0.15,
roughness: 0.55,
normalMap: generateConcreteNormalMap(),
normalScale: new THREE.Vector2(0.5, 0.5),
}),
aluminum: new THREE.MeshStandardMaterial({
color: 0xc0c0c0,
metalness: 0.7,
roughness: 0.25,
normalMap: generateBrushedAluminumNormalMap(),
normalScale: new THREE.Vector2(0.3, 0.3),
envMapIntensity: 1.5,
}),
wood: new THREE.MeshStandardMaterial({
color: 0x8b6914,
metalness: 0.0,
roughness: 0.65,
normalMap: generateWoodNormalMap(),
normalScale: new THREE.Vector2(0.8, 0.8),
}),
fabric: new THREE.MeshStandardMaterial({
color: 0xf0ebe4,
metalness: 0.0,
roughness: 0.4,
normalMap: generateFabricNormalMap(),
normalScale: new THREE.Vector2(0.2, 0.2),
envMapIntensity: 0.3,
}),
steel: new THREE.MeshStandardMaterial({
color: 0x2a2a2a,
metalness: 0.6,
roughness: 0.3,
envMapIntensity: 1.0,
}),
glass: new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0.0,
roughness: 0.05,
transparent: true,
opacity: 0.15,
envMapIntensity: 2.0,
}),
trackLight: new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0.9,
roughness: 0.1,
emissive: new THREE.Color(0xf5e6d0),
emissiveIntensity: 0.8,
}),
};
}
/**
* Dispose all materials and their textures.
*/
export function disposeMaterialLibrary(lib: MaterialLibrary): void {
Object.values(lib).forEach(mat => {
if (mat.normalMap) mat.normalMap.dispose();
mat.dispose();
});
}
```
**Step 2: Commit**
```bash
git add src/showroom/ShowroomMaterials.ts
git commit -m "feat(showroom): PBR material factory with normal maps"
```
---
## Phase 3: HDR Environment + Post-Processing Upgrade
### Task 5: HDR Environment Map Loader
**Files:**
- Modify: `/root/Projects/dw-war-room/src/scene/WarRoom.ts:95-123`
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomEnvironment.ts`
**Step 1: Create showroom-specific environment module**
```typescript
// ShowroomEnvironment.ts — HDR environment map for photorealistic IBL
import * as THREE from 'three';
/**
* Generate a high-quality procedural environment map for gallery/showroom IBL.
* Creates a warm gallery interior environment rather than the cold command-center gradient.
*
* This procedural approach avoids needing an external HDRI file download.
* It simulates a well-lit interior with warm ceiling lights and neutral walls.
*/
export function createShowroomEnvMap(renderer: THREE.WebGLRenderer): THREE.Texture {
const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileEquirectangularShader();
const envScene = new THREE.Scene();
// Sky dome — warm neutral ceiling (gallery white)
const skyGeo = new THREE.SphereGeometry(50, 32, 16);
const skyMat = new THREE.MeshBasicMaterial({
color: 0x1a1815, // warm dark interior
side: THREE.BackSide,
});
envScene.add(new THREE.Mesh(skyGeo, skyMat));
// Ceiling highlight — warm white overhead panels (gallery lighting)
const ceilGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, 0, Math.PI * 0.2);
const ceilMat = new THREE.MeshBasicMaterial({
color: 0xf5e6d0, // warm white (3200K-ish)
side: THREE.BackSide,
});
const ceil = new THREE.Mesh(ceilGeo, ceilMat);
ceil.rotation.x = Math.PI;
envScene.add(ceil);
// Floor reflection — dark polished concrete
const floorGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, Math.PI * 0.75, Math.PI * 0.25);
const floorMat = new THREE.MeshBasicMaterial({
color: 0x0d0c0a,
side: THREE.BackSide,
});
envScene.add(new THREE.Mesh(floorGeo, floorMat));
// Wall bands — warm brick tones from sides
for (let i = 0; i < 4; i++) {
const wallGeo = new THREE.PlaneGeometry(30, 15);
const wallMat = new THREE.MeshBasicMaterial({
color: 0x3a2820, // dark warm brick
side: THREE.DoubleSide,
});
const wall = new THREE.Mesh(wallGeo, wallMat);
const angle = (i / 4) * Math.PI * 2;
wall.position.set(Math.cos(angle) * 35, 5, Math.sin(angle) * 35);
wall.lookAt(0, 5, 0);
envScene.add(wall);
}
// Track light spots — bright warm points at ceiling level
for (let i = 0; i < 6; i++) {
const spotGeo = new THREE.SphereGeometry(2, 8, 8);
const spotMat = new THREE.MeshBasicMaterial({ color: 0xffe8c0 });
const spot = new THREE.Mesh(spotGeo, spotMat);
const angle = (i / 6) * Math.PI * 2;
spot.position.set(Math.cos(angle) * 15, 18, Math.sin(angle) * 15);
envScene.add(spot);
}
const envRenderTarget = pmrem.fromScene(envScene as any, 0.02);
const texture = envRenderTarget.texture;
// Cleanup
pmrem.dispose();
skyMat.dispose();
ceilMat.dispose();
floorMat.dispose();
return texture;
}
```
**Step 2: Commit**
```bash
git add src/showroom/ShowroomEnvironment.ts
git commit -m "feat(showroom): warm gallery HDR environment map for IBL"
```
---
### Task 6: SSAO + Depth of Field + Color Grading
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomPostProcess.ts`
**Step 1: Write the enhanced post-processing pipeline**
```typescript
// ShowroomPostProcess.ts — Photorealistic post-processing for the showroom
// Adds SSAO, DOF, and color grading on top of existing bloom/grain pipeline
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
// ── Color Grading Shader ─────────────────────────────────────────────────────
// Warm architectural tones: slight orange lift in shadows, cool highlight compression
const ColorGradingShader = {
name: 'ColorGradingShader',
uniforms: {
tDiffuse: { value: null as THREE.Texture | null },
uSaturation: { value: 1.1 }, // slight boost
uContrast: { value: 1.08 }, // subtle contrast
uBrightness: { value: 0.02 }, // tiny lift
uWarmth: { value: 0.04 }, // warm shift
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform sampler2D tDiffuse;
uniform float uSaturation;
uniform float uContrast;
uniform float uBrightness;
uniform float uWarmth;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
// Saturation adjustment
float luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
color.rgb = mix(vec3(luminance), color.rgb, uSaturation);
// Contrast (pivot at mid-gray)
color.rgb = (color.rgb - 0.5) * uContrast + 0.5;
// Brightness lift
color.rgb += uBrightness;
// Warm tint — add slight orange to shadows, cool highlights slightly
float shadowMask = 1.0 - smoothstep(0.0, 0.5, luminance);
color.r += uWarmth * shadowMask;
color.g += uWarmth * 0.5 * shadowMask;
// Vignette — darkens corners for photographic depth
vec2 vUvCenter = vUv - 0.5;
float vignette = 1.0 - dot(vUvCenter, vUvCenter) * 0.5;
color.rgb *= vignette;
gl_FragColor = color;
}
`,
};
// ── Film Grain (reused from war room) ────────────────────────────────────────
const FilmGrainShader = {
name: 'FilmGrainShader',
uniforms: {
tDiffuse: { value: null as THREE.Texture | null },
uTime: { value: 0.0 },
uOpacity: { value: 0.006 },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
`,
fragmentShader: /* glsl */ `
uniform sampler2D tDiffuse;
uniform float uTime;
uniform float uOpacity;
varying vec2 vUv;
float hash(vec2 p) { p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); }
void main() {
vec4 color = texture2D(tDiffuse, vUv);
float grain = hash(vUv + fract(uTime * 0.1));
color.rgb += (grain - 0.5) * 2.0 * uOpacity;
gl_FragColor = color;
}
`,
};
export class ShowroomPostProcess {
private composer: EffectComposer;
private ssaoPass: SSAOPass;
private bloomPass: UnrealBloomPass;
private bokehPass: BokehPass;
private colorGradingPass: ShaderPass;
private filmGrainPass: ShaderPass;
private clock: THREE.Clock;
constructor(
renderer: THREE.WebGLRenderer,
scene: THREE.Scene,
camera: THREE.PerspectiveCamera
) {
this.clock = new THREE.Clock();
const size = renderer.getSize(new THREE.Vector2());
this.composer = new EffectComposer(renderer);
// Pass 1: Render
this.composer.addPass(new RenderPass(scene, camera));
// Pass 2: SSAO — ambient occlusion for depth
this.ssaoPass = new SSAOPass(scene, camera, size.x, size.y);
this.ssaoPass.kernelRadius = 0.5;
this.ssaoPass.minDistance = 0.001;
this.ssaoPass.maxDistance = 0.1;
(this.ssaoPass as any).output = SSAOPass.OUTPUT.Default;
this.composer.addPass(this.ssaoPass);
// Pass 3: Bloom — subtle architectural glow
this.bloomPass = new UnrealBloomPass(
new THREE.Vector2(size.x, size.y),
0.2, // strength (slightly less than war room — showroom is brighter)
0.4, // radius
0.7 // threshold
);
this.composer.addPass(this.bloomPass);
// Pass 4: Depth of Field — cinematic focus
this.bokehPass = new BokehPass(scene, camera, {
focus: 15.0, // focus distance (adjustable)
aperture: 0.002, // subtle DOF
maxblur: 0.005, // max blur radius
});
this.composer.addPass(this.bokehPass);
// Pass 5: Color Grading — warm architectural tones
this.colorGradingPass = new ShaderPass(ColorGradingShader);
this.composer.addPass(this.colorGradingPass);
// Pass 6: Film Grain — organic texture
this.filmGrainPass = new ShaderPass(FilmGrainShader);
this.composer.addPass(this.filmGrainPass);
}
render(): void {
this.filmGrainPass.uniforms['uTime'].value = this.clock.getElapsedTime();
this.composer.render();
}
setFocusDistance(distance: number): void {
(this.bokehPass as any).uniforms['focus'].value = distance;
}
resize(width: number, height: number): void {
this.composer.setSize(width, height);
this.bloomPass.resolution.set(width, height);
this.ssaoPass.setSize(width, height);
}
getComposer(): EffectComposer {
return this.composer;
}
}
```
**Step 2: Commit**
```bash
git add src/showroom/ShowroomPostProcess.ts
git commit -m "feat(showroom): SSAO + DOF + color grading post-processing pipeline"
```
---
## Phase 4: Showroom Scene Builder
### Task 7: Showroom Layout Builder (Corridor + Wings)
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomLayout.ts`
**Step 1: Write the main showroom scene builder**
This module creates the architectural corridor with endless wings, using the material library from Task 4 and scene config from Task 2.
```typescript
// ShowroomLayout.ts — Builds the photorealistic showroom corridor and wing bays
import * as THREE from 'three';
import type { SceneConfig, WingConfig, WallConfig, LightConfig } from './FigmaTransformer';
import type { MaterialLibrary } from './ShowroomMaterials';
export class ShowroomLayout {
private scene: THREE.Scene;
private materials: MaterialLibrary;
private corridorGroup: THREE.Group;
private wingGroups: THREE.Group[] = [];
private spotLights: THREE.SpotLight[] = [];
constructor(scene: THREE.Scene, materials: MaterialLibrary) {
this.scene = scene;
this.materials = materials;
this.corridorGroup = new THREE.Group();
this.corridorGroup.name = 'showroom-corridor';
scene.add(this.corridorGroup);
}
/**
* Build the showroom from a SceneConfig (output of FigmaTransformer).
*/
buildFromConfig(config: SceneConfig): void {
this.buildCorridor(config.corridor);
this.buildWings(config.wings);
this.buildWallDisplays(config.walls);
this.buildLighting(config.lights);
this.buildFurniture(config.furniture);
this.buildEntry(config.entry);
}
private buildCorridor(corridor: SceneConfig['corridor']): void {
const { width, depth, height } = corridor;
const halfW = width / 2;
const halfD = depth / 2;
// Floor — polished concrete with normal map
const floorGeo = new THREE.PlaneGeometry(width, depth, 1, 1);
const floor = new THREE.Mesh(floorGeo, this.materials.concrete);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
this.corridorGroup.add(floor);
// Left wall — exposed brick
const leftWallGeo = new THREE.PlaneGeometry(depth, height);
const leftWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
leftWall.position.set(-halfW, height / 2, 0);
leftWall.rotation.y = Math.PI / 2;
leftWall.receiveShadow = true;
this.corridorGroup.add(leftWall);
// Right wall — exposed brick
const rightWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
rightWall.position.set(halfW, height / 2, 0);
rightWall.rotation.y = -Math.PI / 2;
rightWall.receiveShadow = true;
this.corridorGroup.add(rightWall);
// Back wall — dark charcoal
const backWallGeo = new THREE.PlaneGeometry(width, height);
const backWallMat = this.materials.steel.clone();
backWallMat.color.setHex(0x1a1a1a);
const backWall = new THREE.Mesh(backWallGeo, backWallMat);
backWall.position.set(0, height / 2, -halfD);
backWall.receiveShadow = true;
this.corridorGroup.add(backWall);
// Ceiling — steel with exposed duct geometry
const ceilGeo = new THREE.PlaneGeometry(width, depth);
const ceiling = new THREE.Mesh(ceilGeo, this.materials.steel);
ceiling.rotation.x = Math.PI / 2;
ceiling.position.y = height;
this.corridorGroup.add(ceiling);
// Exposed ducts — cylindrical pipes along ceiling
const ductMat = this.materials.steel.clone();
ductMat.metalness = 0.5;
for (let i = 0; i < 3; i++) {
const ductGeo = new THREE.CylinderGeometry(0.15, 0.15, depth, 8);
const duct = new THREE.Mesh(ductGeo, ductMat);
duct.rotation.x = Math.PI / 2;
duct.position.set(-halfW / 2 + i * (halfW / 2), height - 0.3, 0);
this.corridorGroup.add(duct);
}
// Baseboards — warm wood trim
const baseGeo = new THREE.BoxGeometry(depth, 0.15, 0.04);
for (const xPos of [-halfW + 0.02, halfW - 0.02]) {
const base = new THREE.Mesh(baseGeo, this.materials.wood);
base.position.set(xPos, 0.075, 0);
base.rotation.y = Math.PI / 2;
this.corridorGroup.add(base);
}
}
private buildWings(wings: WingConfig[]): void {
wings.forEach((wingConfig, i) => {
const group = new THREE.Group();
group.name = `wing-${i}-${wingConfig.vendor}`;
group.userData = { vendor: wingConfig.vendor, index: i, loaded: false };
// Wing entrance frame — brushed aluminum
const frameGeo = new THREE.BoxGeometry(0.08, 3.5, 0.08);
const leftFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
leftFrame.position.set(-1.5, 1.75, 0);
group.add(leftFrame);
const rightFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
rightFrame.position.set(1.5, 1.75, 0);
group.add(rightFrame);
// Header bar
const headerGeo = new THREE.BoxGeometry(3.08, 0.08, 0.08);
const header = new THREE.Mesh(headerGeo, this.materials.aluminum);
header.position.set(0, 3.5, 0);
group.add(header);
// Vendor label (canvas texture)
const labelCanvas = document.createElement('canvas');
labelCanvas.width = 512;
labelCanvas.height = 64;
const ctx = labelCanvas.getContext('2d')!;
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, 512, 64);
ctx.fillStyle = '#f0ebe4';
ctx.font = 'bold 28px Inter, -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(wingConfig.vendor.toUpperCase(), 256, 42);
const labelTex = new THREE.CanvasTexture(labelCanvas);
const labelGeo = new THREE.PlaneGeometry(2.5, 0.3);
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex });
const label = new THREE.Mesh(labelGeo, labelMat);
label.position.set(0, 3.7, 0.01);
group.add(label);
// Position the wing in the corridor
group.position.set(
wingConfig.position[0],
wingConfig.position[1],
wingConfig.position[2]
);
group.rotation.y = wingConfig.rotation;
this.wingGroups.push(group);
this.corridorGroup.add(group);
});
}
private buildWallDisplays(walls: WallConfig[]): void {
walls.forEach(wall => {
if (wall.hasSamplePanels) {
// Large mounted sample panel
const panelGeo = new THREE.PlaneGeometry(wall.dimensions[0], wall.dimensions[1]);
const panel = new THREE.Mesh(panelGeo, this.materials.fabric);
panel.position.set(...wall.position);
this.corridorGroup.add(panel);
// Aluminum frame around panel
const frameThickness = 0.03;
const frameDepth = 0.04;
const frameMat = this.materials.aluminum;
// Top frame
const topGeo = new THREE.BoxGeometry(wall.dimensions[0] + frameThickness * 2, frameThickness, frameDepth);
const top = new THREE.Mesh(topGeo, frameMat);
top.position.set(wall.position[0], wall.position[1] + wall.dimensions[1] / 2, wall.position[2] + 0.02);
this.corridorGroup.add(top);
// Bottom frame
const bottom = top.clone();
bottom.position.y = wall.position[1] - wall.dimensions[1] / 2;
this.corridorGroup.add(bottom);
}
if (wall.hasBookshelf) {
// Glass-front bookshelf
const shelfGeo = new THREE.BoxGeometry(wall.dimensions[0], wall.dimensions[1], 0.3);
const shelf = new THREE.Mesh(shelfGeo, this.materials.wood);
shelf.position.set(...wall.position);
this.corridorGroup.add(shelf);
// Glass front
const glassGeo = new THREE.PlaneGeometry(wall.dimensions[0] - 0.05, wall.dimensions[1] - 0.05);
const glass = new THREE.Mesh(glassGeo, this.materials.glass);
glass.position.set(wall.position[0], wall.position[1], wall.position[2] + 0.16);
this.corridorGroup.add(glass);
}
});
}
private buildLighting(lights: LightConfig[]): void {
lights.forEach(lightConfig => {
if (lightConfig.type === 'spot') {
const spot = new THREE.SpotLight(
new THREE.Color(lightConfig.color),
lightConfig.intensity,
30, // distance
lightConfig.angle || Math.PI / 6,
lightConfig.penumbra || 0.4,
1 // decay
);
spot.position.set(...lightConfig.position);
if (lightConfig.target) {
spot.target.position.set(...lightConfig.target);
this.scene.add(spot.target);
}
spot.castShadow = true;
spot.shadow.mapSize.set(1024, 1024);
this.spotLights.push(spot);
this.corridorGroup.add(spot);
// Track light housing (small chrome cylinder)
const housingGeo = new THREE.CylinderGeometry(0.08, 0.12, 0.15, 8);
const housing = new THREE.Mesh(housingGeo, this.materials.trackLight);
housing.position.set(...lightConfig.position);
this.corridorGroup.add(housing);
// Volumetric light cone (subtle)
const coneH = lightConfig.position[1] - (lightConfig.target?.[1] || 0);
const coneR = Math.tan(lightConfig.angle || Math.PI / 6) * coneH;
const coneGeo = new THREE.ConeGeometry(coneR, coneH, 16, 1, true);
const coneMat = new THREE.MeshBasicMaterial({
color: lightConfig.color,
transparent: true,
opacity: 0.015,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
const cone = new THREE.Mesh(coneGeo, coneMat);
cone.position.set(
lightConfig.position[0],
lightConfig.position[1] - coneH / 2,
lightConfig.position[2]
);
this.corridorGroup.add(cone);
}
});
// Track rails (continuous aluminum rail along ceiling)
const railGeo = new THREE.BoxGeometry(40, 0.03, 0.05);
const rail = new THREE.Mesh(railGeo, this.materials.aluminum);
rail.position.set(0, 11.5, 0);
this.corridorGroup.add(rail);
}
private buildFurniture(furniture: SceneConfig['furniture']): void {
furniture.forEach(item => {
if (item.type === 'display-table') {
const tableGeo = new THREE.BoxGeometry(...item.dimensions);
const table = new THREE.Mesh(tableGeo, this.materials.wood);
table.position.set(...item.position);
table.castShadow = true;
this.corridorGroup.add(table);
}
});
}
private buildEntry(entry: SceneConfig['entry']): void {
// Entry signage
const signCanvas = document.createElement('canvas');
signCanvas.width = 1024;
signCanvas.height = 128;
const ctx = signCanvas.getContext('2d')!;
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, 1024, 128);
ctx.fillStyle = '#f0ebe4';
ctx.font = 'bold 36px Inter, -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(entry.signText, 512, 70);
ctx.font = '18px Inter, -apple-system, sans-serif';
ctx.fillStyle = '#888888';
ctx.fillText('ARCHITECTURAL TRADE SHOWROOM', 512, 105);
const signTex = new THREE.CanvasTexture(signCanvas);
const signGeo = new THREE.PlaneGeometry(6, 0.75);
const signMat = new THREE.MeshStandardMaterial({
map: signTex,
emissive: new THREE.Color(0xffffff),
emissiveMap: signTex,
emissiveIntensity: 0.3,
});
const sign = new THREE.Mesh(signGeo, signMat);
sign.position.set(entry.position[0], 10, entry.position[2]);
this.corridorGroup.add(sign);
}
/**
* Get wing groups for culling/visibility management.
*/
getWings(): THREE.Group[] {
return this.wingGroups;
}
/**
* Set visibility of wings by distance from camera for performance.
*/
updateVisibility(cameraPosition: THREE.Vector3, maxDistance = 30): void {
this.wingGroups.forEach(wing => {
const dist = wing.position.distanceTo(cameraPosition);
wing.visible = dist < maxDistance;
});
}
dispose(): void {
this.corridorGroup.traverse(child => {
if (child instanceof THREE.Mesh) {
child.geometry.dispose();
}
});
this.scene.remove(this.corridorGroup);
}
}
```
**Step 2: Commit**
```bash
git add src/showroom/ShowroomLayout.ts
git commit -m "feat(showroom): corridor + wings scene builder with PBR materials"
```
---
### Task 8: Showroom Scene Compositor
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/Showroom.ts`
**Step 1: Write the main Showroom class that ties everything together**
```typescript
// Showroom.ts — Main photorealistic showroom compositor
// Ties together: FigmaTransformer + Materials + Environment + Layout + PostProcess
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { createMaterialLibrary, disposeMaterialLibrary } from './ShowroomMaterials';
import { createShowroomEnvMap } from './ShowroomEnvironment';
import { ShowroomLayout } from './ShowroomLayout';
import { ShowroomPostProcess } from './ShowroomPostProcess';
import { transformFigmaToScene, SceneConfig } from './FigmaTransformer';
import type { MaterialLibrary } from './ShowroomMaterials';
export class Showroom {
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;
renderer: THREE.WebGLRenderer;
controls: OrbitControls;
private clock: THREE.Clock;
private materials: MaterialLibrary;
private layout: ShowroomLayout;
private postProcess: ShowroomPostProcess;
private dustParticles: THREE.Points;
constructor(container: HTMLElement) {
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0d0c0a);
this.scene.fog = new THREE.Fog(0x0d0c0a, 20, 60);
// Camera — human eye level
this.camera = new THREE.PerspectiveCamera(
55, container.clientWidth / container.clientHeight, 0.1, 100
);
this.camera.position.set(0, 1.7, 8); // standing at entry
this.camera.lookAt(0, 1.5, 0);
// Renderer — photorealistic pipeline
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.4;
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(this.renderer.domElement);
// Controls — walking speed
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.08;
this.controls.maxPolarAngle = Math.PI / 2;
this.controls.minDistance = 1;
this.controls.maxDistance = 50;
// Clock
this.clock = new THREE.Clock();
// Materials
this.materials = createMaterialLibrary();
// Environment map (warm gallery IBL)
this.renderer.environment = createShowroomEnvMap(this.renderer) as THREE.Texture;
// Ambient + key lighting
this.addBaseLighting();
// Layout builder
this.layout = new ShowroomLayout(this.scene, this.materials);
// Atmospheric dust particles
this.dustParticles = this.createDustMotes();
this.scene.add(this.dustParticles);
// Post-processing
this.postProcess = new ShowroomPostProcess(this.renderer, this.scene, this.camera);
// Resize
window.addEventListener('resize', () => this.onResize(container));
}
/**
* Load showroom from Figma design data.
*/
loadFromFigma(figmaData: any): void {
const config = transformFigmaToScene(figmaData);
this.layout.buildFromConfig(config);
}
/**
* Load showroom from a pre-built SceneConfig.
*/
loadFromConfig(config: SceneConfig): void {
this.layout.buildFromConfig(config);
}
private addBaseLighting(): void {
// Ambient — warm neutral
const ambient = new THREE.AmbientLight(0x2a2520, 0.4);
this.scene.add(ambient);
// Hemisphere — sky/ground color contrast
const hemi = new THREE.HemisphereLight(0xf5e6d0, 0x1a1510, 0.3);
this.scene.add(hemi);
// Main key light — warm directional
const key = new THREE.DirectionalLight(0xf5e6d0, 0.8);
key.position.set(5, 15, 10);
key.castShadow = true;
key.shadow.mapSize.set(2048, 2048);
key.shadow.camera.near = 0.5;
key.shadow.camera.far = 50;
key.shadow.camera.left = -25;
key.shadow.camera.right = 25;
key.shadow.camera.top = 15;
key.shadow.camera.bottom = -15;
key.shadow.bias = -0.0003;
this.scene.add(key);
}
private createDustMotes(): THREE.Points {
const count = 200;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 40; // x
positions[i + 1] = Math.random() * 12; // y
positions[i + 2] = (Math.random() - 0.5) * 20; // z
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xf5e6d0,
size: 0.02,
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
return new THREE.Points(geo, mat);
}
animate(): void {
requestAnimationFrame(() => this.animate());
const dt = Math.min(this.clock.getDelta(), 0.1);
const elapsed = this.clock.getElapsedTime();
this.controls.update();
// Animate dust motes
const dustPos = this.dustParticles.geometry.attributes.position;
for (let i = 0; i < dustPos.count; i++) {
const y = dustPos.getY(i) + Math.sin(elapsed * 0.3 + i) * 0.001;
dustPos.setY(i, y > 12 ? 0 : y);
}
dustPos.needsUpdate = true;
// Update wing visibility based on camera distance
this.layout.updateVisibility(this.camera.position);
// Update DOF focus to orbit target distance
const focusDist = this.camera.position.distanceTo(this.controls.target);
this.postProcess.setFocusDistance(focusDist);
// Render through post-processing pipeline
this.postProcess.render();
}
private onResize(container: HTMLElement): void {
const w = container.clientWidth;
const h = container.clientHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
this.postProcess.resize(w, h);
}
dispose(): void {
this.layout.dispose();
disposeMaterialLibrary(this.materials);
this.dustParticles.geometry.dispose();
(this.dustParticles.material as THREE.PointsMaterial).dispose();
this.renderer.dispose();
}
}
```
**Step 2: Commit**
```bash
git add src/showroom/Showroom.ts
git commit -m "feat(showroom): main Showroom compositor with dust particles and DOF"
```
---
### Task 9: Showroom Entry Point + HTML Page
**Files:**
- Create: `/root/Projects/dw-war-room/src/showroom/index.ts`
- Create: `/root/Projects/dw-war-room/showroom.html`
**Step 1: Write entry point**
```typescript
// src/showroom/index.ts — Showroom standalone entry point
import { Showroom } from './Showroom';
import type { SceneConfig } from './FigmaTransformer';
// Default showroom config (used when no Figma data available)
const DEFAULT_CONFIG: SceneConfig = {
corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
wings: Array.from({ length: 6 }, (_, i) => ({
index: i,
vendor: ['Thibaut', 'Schumacher', 'Arte', 'Scalamandre', 'Elitis', 'Phillip Jeffries'][i],
position: [-12 + i * 5, 0, -8] as [number, number, number],
rotation: 0,
panelCount: 12,
accentColor: ['#1a4b8c', '#c41e3a', '#d4a574', '#2d4a3e', '#e67e22', '#8b4513'][i],
})),
walls: [
{ position: [-18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: true, hasSamplePanels: false, sampleCount: 0 },
{ position: [18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: false, hasSamplePanels: true, sampleCount: 3 },
],
lights: Array.from({ length: 8 }, (_, i) => ({
type: 'spot' as const,
position: [-14 + i * 4, 11, 0] as [number, number, number],
target: [-14 + i * 4, 0, 0] as [number, number, number],
color: '#f5e6d0',
intensity: 1.2,
angle: Math.PI / 5,
penumbra: 0.4,
})),
furniture: [
{ type: 'display-table' as const, position: [0, 0.45, 0] as [number, number, number], dimensions: [3, 0.9, 1.5] as [number, number, number] },
],
entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
};
// Boot
const container = document.getElementById('showroom-container');
if (container) {
const showroom = new Showroom(container);
showroom.loadFromConfig(DEFAULT_CONFIG);
showroom.animate();
// Expose for MCP/debug
(window as any).__showroom = showroom;
}
```
**Step 2: Write HTML page**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DW Showroom — Photorealistic 3D</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #0d0c0a; font-family: Inter, -apple-system, sans-serif; }
#showroom-container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="showroom-container"></div>
<script type="module" src="/src/showroom/index.ts"></script>
</body>
</html>
```
**Step 3: Add Vite route for showroom page**
Modify `vite.config.ts` to add the showroom entry:
```typescript
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
server: { port: 4060, host: '0.0.0.0' },
preview: { port: 4060, host: '0.0.0.0' },
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
showroom: resolve(__dirname, 'showroom.html'),
},
},
},
});
```
**Step 4: Commit**
```bash
git add src/showroom/index.ts showroom.html vite.config.ts
git commit -m "feat(showroom): standalone showroom page with default 6-vendor config"
```
---
## Phase 5: Boardroom-3D Integration (Port 7681)
### Task 10: Backport Photorealistic Materials to wr-wings.js
**Files:**
- Modify: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-wings.js`
- Create: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-pbr-materials.js`
**Step 1: Create PBR materials module for the vanilla JS system**
Write a new module `wr-pbr-materials.js` that provides the same procedural normal maps and material factory, adapted for the older Three.js CDN version (r128). This module follows the existing `window.WR` namespace pattern.
Key adaptations:
- Use `THREE.MeshStandardMaterial` (available in r128+)
- Canvas normal maps generated identically to the TypeScript version
- Exposed via `window.DWS.materials` namespace
**Step 2: Integrate into wr-wings.js wing panel materials**
Replace the existing `MeshStandardMaterial({ roughness: 0.35, metalness: 0.0 })` wing panel material with the new PBR material that includes normal maps.
**Step 3: Build and test**
```bash
cd /root/DW-Agents/boardroom-3d
node build-warroom.js
node validate-warroom.js
```
**Step 4: Commit**
```bash
git add public/js/modules/wr-pbr-materials.js public/js/modules/wr-wings.js
git commit -m "feat(boardroom-3d): PBR materials with procedural normal maps for showroom wings"
```
---
### Task 11: Backport Post-Processing to wr-effects.js
**Files:**
- Modify: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-effects.js`
**Step 1: Add SSAO and color grading to the existing effects pipeline**
The boardroom-3d system currently has bloom disabled by default (performance). Add optional SSAO and color grading that can be toggled:
```javascript
// In wr-effects.js — add after existing toggleBloom function
window.toggleSSAO = function() { /* ... */ };
window.toggleColorGrading = function() { /* ... */ };
window.setQualityPreset = function(level) {
// 'performance' = no post-processing
// 'balanced' = bloom only
// 'quality' = bloom + color grading
// 'ultra' = bloom + SSAO + color grading + grain
};
```
**Step 2: Build and validate**
```bash
node build-warroom.js && node validate-warroom.js
```
**Step 3: Commit**
```bash
git commit -am "feat(boardroom-3d): quality presets with SSAO and color grading"
```
---
## Phase 6: Testing + Polish
### Task 12: Visual Verification
**Step 1: Start dev server and verify war room showroom**
```bash
cd /root/Projects/dw-war-room && npm run dev
```
Navigate to `http://45.61.58.125:4060/showroom.html` and verify:
- Brick walls have visible mortar normal map detail
- Polished concrete floor reflects environment
- Track lights cast visible spot cones
- SSAO darkens wall-floor junctions
- DOF blurs background when looking at near wing
- Color grading gives warm architectural tone
- 30+ FPS maintained
**Step 2: Verify boardroom-3d showroom**
```bash
cd /root/DW-Agents/boardroom-3d && pm2 restart boardroom-3d
```
Navigate to showroom wings at `http://45.61.58.125:7681/` and verify PBR materials apply.
**Step 3: Performance audit**
Check draw calls, FPS, texture memory via browser dev tools:
- Target: <300 draw calls, 30+ FPS, <256MB textures
---
### Task 13: Final Commit + Documentation
**Step 1: Update CLAUDE.md with showroom architecture**
Add showroom module documentation to `/root/Projects/dw-war-room/CLAUDE.md`.
**Step 2: Final commit**
```bash
cd /root/Projects/dw-war-room
git add -A
git commit -m "feat(showroom): complete Figma + Three.js photorealistic showroom pipeline"
```
**Step 3: Push to GitHub**
```bash
git push origin main
```
---
## Summary
| Phase | Tasks | Key Deliverables |
|-------|-------|-----------------|
| 1 | Tasks 1-2 | Figma design + MCP extraction pipeline |
| 2 | Tasks 3-4 | Procedural normal maps + PBR material factory |
| 3 | Tasks 5-6 | HDR environment + SSAO/DOF/color grading |
| 4 | Tasks 7-9 | Showroom layout builder + entry point |
| 5 | Tasks 10-11 | Boardroom-3D backport |
| 6 | Tasks 12-13 | Testing + documentation |
**Total new files:** ~8 TypeScript modules + 1 HTML page + 1 JS module
**Estimated draw calls:** ~150 base corridor, ~250 with 3 wings visible
**Target FPS:** 30+ at 1080p on desktop GPU