import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0d16);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(0, 0.5, 6.2);
const S = 96;
const colorCv = document.createElement('canvas'); colorCv.width = colorCv.height = S;
const cc = colorCv.getContext('2d');
cc.fillStyle = '#caa46a'; cc.fillRect(0, 0, S, S);
const bumps = [];
for (let i = 0; i < 36; i++) bumps.push({ x: Math.random() * S, y: Math.random() * S, r: 3 + Math.random() * 5 });
for (const b of bumps) {
const g = cc.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r);
g.addColorStop(0, 'rgba(60,40,20,0.5)'); g.addColorStop(1, 'rgba(60,40,20,0)');
cc.fillStyle = g; cc.beginPath(); cc.arc(b.x, b.y, b.r, 0, Math.PI * 2); cc.fill();
}
const colorTex = new THREE.CanvasTexture(colorCv);
function heightAt(x, y) {
let h = 0;
for (const b of bumps) {
const d = Math.hypot(x - b.x, y - b.y) / b.r;
if (d < 1) h += Math.pow(Math.cos(d * Math.PI * 0.5), 2);
}
return h;
}
const normalCv = document.createElement('canvas'); normalCv.width = normalCv.height = S;
const nc = normalCv.getContext('2d');
const nimg = nc.createImageData(S, S);
for (let y = 0; y < S; y++) {
for (let x = 0; x < S; x++) {
const hl = heightAt(x - 1, y), hr = heightAt(x + 1, y), hu = heightAt(x, y - 1), hd = heightAt(x, y + 1);
const vx = -(hr - hl) * 5, vy = -(hd - hu) * 5, vz = 1;
const len = Math.hypot(vx, vy, vz);
const i = (y * S + x) * 4;
nimg.data[i] = ((vx / len) * 0.5 + 0.5) * 255;
nimg.data[i + 1] = ((vy / len) * 0.5 + 0.5) * 255;
nimg.data[i + 2] = ((vz / len) * 0.5 + 0.5) * 255;
nimg.data[i + 3] = 255;
}
}
nc.putImageData(nimg, 0, 0);
const normalTex = new THREE.CanvasTexture(normalCv);
const geo = new THREE.IcosahedronGeometry(1.05, 2);
const raw = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x8a7454, roughness: 0.75 }));
raw.position.x = -1.7;
const baked = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ map: colorTex, normalMap: normalTex, roughness: 0.6 }));
baked.position.x = 1.7;
scene.add(raw, baked);
const key = new THREE.DirectionalLight(0xffffff, 1.9); key.position.set(3, 4, 5); scene.add(key);
scene.add(new THREE.AmbientLight(0xffffff, 0.45));
function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
const r = t * 0.00026;
raw.rotation.y = r; baked.rotation.y = r;
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
});