← back to Dw War Room
src/showroom/ProceduralMaps.ts
189 lines
// 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');
if (!ctx) throw new Error('Failed to create 2d canvas context');
// 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');
if (!ctx) throw new Error('Failed to create 2d canvas context');
// 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');
if (!ctx) throw new Error('Failed to create 2d canvas context');
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');
if (!ctx) throw new Error('Failed to create 2d canvas context');
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');
if (!ctx) throw new Error('Failed to create 2d canvas context');
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;
}