// O motor 3D do kit, em JavaScript puro (o render headless do Remotion não tem WebGL). // Raymarching de formas arredondadas (SDF) com luz de estúdio, sombra suave, oclusão de contato e // antisserrilhado só nas bordas. Determinístico: o mesmo quadro sempre sai igual. // Quem usa é o Produto3D; ninguém precisa mexer aqui pra trocar de produto. export type CenaRM = { n: number; tipo: Int8Array; pos: Float64Array; tam: Float64Array; raio: Float64Array; rot: Float64Array; mat: Int8Array; cor: Float64Array; un: Float64Array; corte: Float64Array; ext: Float64Array; esfera: [number, number, number, number]; chao: number; sombra: number; sombraCor: [number, number, number]; acento: [number, number, number]; brilho: number; estudio: [number, number, number]; }; export type CameraRM = {olho: [number, number, number]; alvo: [number, number, number]; tan: number}; const norm = (x: number, y: number, z: number): [number, number, number] => {const l = Math.hypot(x, y, z); return [x / l, y / l, z / l];}; const L1 = norm(-0.55, 0.75, 0.52), L2 = norm(0.85, 0.27, 0.36), LC = norm(-0.12, 1, 0.12); export const desenhar = (ctx: CanvasRenderingContext2D, W: number, H: number, c: CenaRM, cam: CameraRM) => { const {n, tipo, pos, tam, raio, rot, mat, cor, un, corte, ext} = c; let ID = -1; // ---------- distância até o produto (e qual peça) ---------- const mapa = (x: number, y: number, z: number): number => { let d = 1e5; ID = -1; for (let i = 0; i < n; i++) { const dx = x - pos[i * 3], dy = y - pos[i * 3 + 1], dz = z - pos[i * 3 + 2]; const k = un[i]; const lb = Math.sqrt(dx * dx + dy * dy + dz * dz) - ext[i]; if (k < 0 ? lb >= -d : lb > d + k) continue; // esta peça não pode mudar o resultado const r = i * 9; // local = Rᵀ·(p − pos) const qx = rot[r] * dx + rot[r + 3] * dy + rot[r + 6] * dz; const qy = rot[r + 1] * dx + rot[r + 4] * dy + rot[r + 7] * dz; const qz = rot[r + 2] * dx + rot[r + 5] * dy + rot[r + 8] * dz; const sx = tam[i * 3], sy = tam[i * 3 + 1], sz = tam[i * 3 + 2]; let di: number; const t = tipo[i]; if (t === 1) { // caixa arredondada const rr = raio[i]; const ax = Math.abs(qx) - sx + rr, ay = Math.abs(qy) - sy + rr, az = Math.abs(qz) - sz + rr; const mx = Math.max(ax, 0), my = Math.max(ay, 0), mz = Math.max(az, 0); di = Math.sqrt(mx * mx + my * my + mz * mz) + Math.min(Math.max(ax, Math.max(ay, az)), 0) - rr; } else if (t === 2) { // cápsula cônica: [r baixo, altura, r cima], centrada const r1 = sx, h = sy, r2 = sz, py = qy + h * 0.5; const b = (r1 - r2) / h, a = Math.sqrt(1 - b * b); const q0 = Math.sqrt(qx * qx + qz * qz); const kk = -b * q0 + a * py; if (kk < 0) di = Math.sqrt(q0 * q0 + py * py) - r1; else if (kk > a * h) di = Math.sqrt(q0 * q0 + (py - h) * (py - h)) - r2; else di = q0 * a + py * b - r1; } else if (t === 3) { // elipsoide const ex = qx / sx, ey = qy / sy, ez = qz / sz; const k0 = Math.sqrt(ex * ex + ey * ey + ez * ez); const fx = qx / (sx * sx), fy = qy / (sy * sy), fz = qz / (sz * sz); const k1 = Math.sqrt(fx * fx + fy * fy + fz * fz); di = (k0 * (k0 - 1)) / Math.max(k1, 1e-6); } else if (t === 4) { // cilindro arredondado const rr = raio[i]; const ax = Math.sqrt(qx * qx + qz * qz) - sx + rr, ay = Math.abs(qy) - sy + rr; di = Math.min(Math.max(ax, ay), 0) + Math.hypot(Math.max(ax, 0), Math.max(ay, 0)) - rr; } else { // anel const q0 = Math.sqrt(qx * qx + qz * qz) - sx; di = Math.sqrt(q0 * q0 + qy * qy) - sy; } const cs = corte[i * 2]; if (cs !== 0) di = Math.max(di, -cs * (qy - corte[i * 2 + 1])); if (k < 0) {if (-di > d) ID = i; d = Math.max(d, -di);} else if (k > 0) { if (di < d) ID = i; const hh = Math.max(k - Math.abs(d - di), 0) / k; d = Math.min(d, di) - hh * hh * k * 0.25; } else if (di < d) {d = di; ID = i;} } return d; }; const [ex, ey, ez, er] = c.esfera; // interseção raio × esfera que envolve o produto → [entra, sai] ou null const esfera = (ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, rr = er): [number, number] | null => { const cx = ox - ex, cy = oy - ey, cz = oz - ez; const b = cx * dx + cy * dy + cz * dz, cc = cx * cx + cy * cy + cz * cz - rr * rr, h = b * b - cc; if (h < 0) return null; const s = Math.sqrt(h); return [-b - s, -b + s]; }; const sombra = (ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, k: number, passos: number) => { const b = esfera(ox, oy, oz, dx, dy, dz); if (!b || b[1] < 0) return 1; let res = 1, t = Math.max(b[0], 0.012); for (let i = 0; i < passos; i++) { const h = mapa(ox + dx * t, oy + dy * t, oz + dz * t); res = Math.min(res, (k * h) / t); t += Math.min(Math.max(h, 0.014), 0.2); if (res < 0.003 || t > b[1]) break; } return Math.max(0, Math.min(1, res)); }; const [scR, scG, scB] = c.sombraCor; const [stR, stG, stB] = c.estudio; // ambiente de estúdio (o que o cetim e o metal refletem) const env = (dx: number, dy: number, dz: number, out: number[]) => { const s = Math.max(0, Math.min(1, (dy + 0.35) / 1.1)), t = s * s * (3 - 2 * s); const k1 = Math.max(0, Math.min(1, (dx * L1[0] + dy * L1[1] + dz * L1[2] - 0.86) / 0.125)); const k2 = Math.max(0, Math.min(1, (dx * L2[0] + dy * L2[1] + dz * L2[2] - 0.82) / 0.155)); const add = 0.75 * k1 * k1 * (3 - 2 * k1) + 0.45 * k2 * k2 * (3 - 2 * k2); out[0] = stR * 0.82 + (1.04 - stR * 0.82) * t + add; out[1] = stG * 0.82 + (1.04 - stG * 0.82) * t + add; out[2] = stB * 0.82 + (1.04 - stB * 0.82) * t + add; }; const E = [0, 0, 0]; // câmera const [cx0, cy0, cz0] = cam.olho; let [fx, fy, fz] = [cam.alvo[0] - cx0, cam.alvo[1] - cy0, cam.alvo[2] - cz0]; [fx, fy, fz] = norm(fx, fy, fz); let [rx, ry, rz] = norm(-fz, 0, fx); // fw × up(0,1,0) if (!isFinite(rx)) [rx, ry, rz] = [1, 0, 0]; const ux = ry * fz - rz * fy, uy = rz * fx - rx * fz, uz = rx * fy - ry * fx; const meio = 0.5 * H; const raioDe = (px: number, py: number): [number, number, number] => { const u = ((px - 0.5 * W) / meio) * cam.tan, v = ((0.5 * H - py) / meio) * cam.tan; return norm(fx + u * rx + v * ux, fy + u * ry + v * uy, fz + u * rz + v * uz); }; // ---------- um raio: devolve cor + alfa (pré-multiplicado) em out e o id da peça (−1 = nada) ---------- const out = [0, 0, 0, 0]; const traçar = (px: number, py: number): number => { const [dx, dy, dz] = raioDe(px, py); const b = esfera(cx0, cy0, cz0, dx, dy, dz); out[0] = out[1] = out[2] = out[3] = 0; if (!b || b[1] < 0) return -1; let t = Math.max(b[0], 0), hit = -1; for (let i = 0; i < 140; i++) { const h = mapa(cx0 + dx * t, cy0 + dy * t, cz0 + dz * t); if (h < 0.00025 * t + 0.0002) {hit = ID; break;} t += h * 0.95; if (t > b[1]) break; } if (hit < 0) return -1; const px3 = cx0 + dx * t, py3 = cy0 + dy * t, pz3 = cz0 + dz * t; // normal (tetraedro) const e = 0.0007; const a1 = mapa(px3 + e, py3 - e, pz3 - e), a2 = mapa(px3 - e, py3 - e, pz3 + e), a3 = mapa(px3 - e, py3 + e, pz3 - e), a4 = mapa(px3 + e, py3 + e, pz3 + e); let [nx, ny, nz] = norm(a1 - a2 - a3 + a4, -a1 - a2 + a3 + a4, -a1 + a2 - a3 + a4); if (!isFinite(nx)) {nx = 0; ny = 1; nz = 0;} const m = mat[hit]; const br = cor[hit * 3], bg = cor[hit * 3 + 1], bb = cor[hit * 3 + 2]; const sh = sombra(px3 + nx * 0.004, py3 + ny * 0.004, pz3 + nz * 0.004, L1[0], L1[1], L1[2], 9, 32); let o = 0, s = 1; for (let i = 0; i < 4; i++) {const hh = 0.012 + 0.05 * i; o += (hh - mapa(px3 + nx * hh, py3 + ny * hh, pz3 + nz * hh)) * s; s *= 0.75;} const ao = Math.max(0, Math.min(1, 1 - 2.2 * o)); const ndl = nx * L1[0] + ny * L1[1] + nz * L1[2]; const dif = Math.max(0, Math.min(1, ndl * 0.6 + 0.4)); const dif2 = Math.max(0, Math.min(1, (nx * L2[0] + ny * L2[1] + nz * L2[2]) * 0.5 + 0.5)); const sky = 0.62 + 0.38 * ny; const ndv = Math.max(0, Math.min(1, -(nx * dx + ny * dy + nz * dz))); const fre = Math.pow(1 - ndv, 4); const [hx, hy, hz] = norm(L1[0] - dx, L1[1] - dy, L1[2] - dz); const nh = Math.max(0, nx * hx + ny * hy + nz * hz); const dn = dx * nx + dy * ny + dz * nz; env(dx - 2 * dn * nx, dy - 2 * dn * ny, dz - 2 * dn * nz, E); let r: number, g: number, bl: number; if (m === 0 || m === 3) { // cetim const lumA = 0.45 * dif * (0.45 + 0.55 * sh) + 0.15 * dif2 + 0.5 * sky + 0.12; const bounce = 0.22 * Math.max(0, -ny); const aoK = 0.55 + 0.45 * ao, sp = 0.16 * Math.pow(nh, 28) * sh; r = br * (lumA + stR * bounce) * aoK + sp + E[0] * fre * 0.16; g = bg * (lumA + stG * bounce) * aoK + sp + E[1] * fre * 0.16; bl = bb * (lumA + stB * bounce) * aoK + sp + E[2] * fre * 0.16; if (m === 3 && c.brilho > 0) { const k = c.brilho; r = r + (c.acento[0] * 1.18 - r) * k; g = g + (c.acento[1] * 1.18 - g) * k; bl = bl + (c.acento[2] * 1.18 - bl) * k; } } else if (m === 1) { // metal const aoK = (0.5 + 0.5 * ao) * 0.92, sp = 0.9 * Math.pow(nh, 90) * sh; r = br * E[0] * aoK + sp; g = bg * E[1] * aoK + sp; bl = bb * E[2] * aoK + sp; } else { // grafite const k = (0.45 + 0.55 * dif) * ao, sp = 0.1 * Math.pow(nh, 40); r = br * k + sp + E[0] * fre * 0.06; g = bg * k + sp + E[1] * fre * 0.06; bl = bb * k + sp + E[2] * fre * 0.06; } out[0] = Math.min(1, r); out[1] = Math.min(1, g); out[2] = Math.min(1, bl); out[3] = 1; return hit; }; // ---------- 1) sombra de contato no chão, numa grade de 1/4 (é macia, não precisa de mais) ---------- const G = 4, gw = Math.ceil(W / G) + 2, gh = Math.ceil(H / G) + 2; const grade = new Float32Array(gw * gh); if (c.sombra > 0) { const raioChao2 = er * er * 9; for (let j = 0; j < gh; j++) for (let i = 0; i < gw; i++) { const [dx, dy, dz] = raioDe(i * G, j * G); if (dy >= 0) continue; const tf = (c.chao - cy0) / dy; const x = cx0 + dx * tf, z = cz0 + dz * tf; const qx = x - ex, qz = z - ez; if (qx * qx + qz * qz > raioChao2) continue; const sh = sombra(x, c.chao + 0.002, z, LC[0], LC[1], LC[2], 3.2, 28); let o = 0, s = 1; for (let k = 0; k < 5; k++) {const hh = 0.03 + 0.16 * k; o += Math.max(hh - mapa(x, c.chao + hh, z), 0) * s; s *= 0.72;} const ao = Math.max(0, Math.min(1, 1 - 1.1 * o)); grade[j * gw + i] = c.sombra * Math.max(0, Math.min(1, 0.5 * (1 - sh) + 0.6 * (1 - ao))); } } // ---------- 2) o produto, um raio por pixel ---------- const img = ctx.createImageData(W, H); const px = img.data; const ids = new Int16Array(W * H).fill(-1); const cor4 = new Float32Array(W * H * 4); // só varre o retângulo da tela onde a esfera do produto aparece const proj = (x: number, y: number, z: number): [number, number] | null => { const vx = x - cx0, vy = y - cy0, vz = z - cz0; const zc = vx * fx + vy * fy + vz * fz; if (zc <= 0.01) return null; return [0.5 * W + ((vx * rx + vy * ry + vz * rz) / zc / cam.tan) * meio, 0.5 * H - ((vx * ux + vy * uy + vz * uz) / zc / cam.tan) * meio]; }; let x0 = 0, x1 = W, y0 = 0, y1 = H; const pc = proj(ex, ey, ez); const dist = Math.hypot(ex - cx0, ey - cy0, ez - cz0); if (pc && dist > er * 1.05) { const rp = (er / Math.sqrt(dist * dist - er * er)) / cam.tan * meio * 1.08 + 2; x0 = Math.max(0, Math.floor(pc[0] - rp)); x1 = Math.min(W, Math.ceil(pc[0] + rp)); y0 = Math.max(0, Math.floor(pc[1] - rp)); y1 = Math.min(H, Math.ceil(pc[1] + rp)); } for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) { const id = traçar(x + 0.5, y + 0.5); if (id < 0) continue; const k = y * W + x; ids[k] = id; cor4[k * 4] = out[0]; cor4[k * 4 + 1] = out[1]; cor4[k * 4 + 2] = out[2]; cor4[k * 4 + 3] = 1; } // ---------- 3) bordas: 4 raios por pixel onde o id muda ---------- const SUB = [[0.125, 0.375], [0.375, -0.125], [-0.125, -0.375], [-0.375, 0.125]]; const borda = new Uint8Array(W * H); for (let y = Math.max(0, y0 - 1); y < Math.min(H, y1 + 1); y++) for (let x = Math.max(0, x0 - 1); x < Math.min(W, x1 + 1); x++) { const k = y * W + x, a = ids[k]; if ((x + 1 < W && ids[k + 1] !== a) || (y + 1 < H && ids[k + W] !== a)) {borda[k] = 1; if (x + 1 < W) borda[k + 1] = 1; if (y + 1 < H) borda[k + W] = 1;} } const somb = (x: number, y: number) => { // sombra bilinear da grade const gx = x / G, gy = y / G, i = Math.floor(gx), j = Math.floor(gy), tx = gx - i, ty = gy - j; const a = grade[j * gw + i], b = grade[j * gw + i + 1], cc = grade[(j + 1) * gw + i], d = grade[(j + 1) * gw + i + 1]; return (a * (1 - tx) + b * tx) * (1 - ty) + (cc * (1 - tx) + d * tx) * ty; }; for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const k = y * W + x; let r: number, g: number, b: number, a: number; if (borda[k]) { r = g = b = a = 0; for (const [sx, sy] of SUB) { const id = traçar(x + 0.5 + sx, y + 0.5 + sy); if (id >= 0) {r += out[0]; g += out[1]; b += out[2]; a += 1;} else {const s = somb(x + 0.5 + sx, y + 0.5 + sy); r += scR * s; g += scG * s; b += scB * s; a += s;} } r /= 4; g /= 4; b /= 4; a /= 4; } else if (ids[k] >= 0) {r = cor4[k * 4]; g = cor4[k * 4 + 1]; b = cor4[k * 4 + 2]; a = 1;} else {a = somb(x + 0.5, y + 0.5); if (a <= 0.002) continue; r = scR * a; g = scG * a; b = scB * a;} const p = k * 4; px[p] = (r / a) * 255; px[p + 1] = (g / a) * 255; px[p + 2] = (b / a) * 255; px[p + 3] = a * 255; } ctx.putImageData(img, 0, 0); };