diff --git a/data-pipeline/scripts/extract-manifest.mjs b/data-pipeline/scripts/extract-manifest.mjs index a3db726..a43c6ae 100644 --- a/data-pipeline/scripts/extract-manifest.mjs +++ b/data-pipeline/scripts/extract-manifest.mjs @@ -34,6 +34,78 @@ function hexToRgb(hex) { return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1]; } +function hexToHsl(hex) { + const [r, g, b] = hexToRgb(hex); + const max = Math.max(r, g, b), min = Math.min(r, g, b); + const l = (max + min) / 2; + if (max === min) return [0, 0, l]; + const d = max - min; + const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + let h; + if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + else if (max === g) h = ((b - r) / d + 2) / 6; + else h = ((r - g) / d + 4) / 6; + return [h, s, l]; +} + +function hslToHex(h, s, l) { + h = ((h % 1) + 1) % 1; // normaliza pra [0,1) mesmo se ficou negativo + s = Math.min(1, Math.max(0, s)); + l = Math.min(1, Math.max(0, l)); + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + let r, g, b; + if (s === 0) { + r = g = b = l; + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + const toHex = (v) => Math.round(v * 255).toString(16).padStart(2, "0"); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +} + +// Hash determinístico simples (não precisa ser criptográfico, só estável +// entre execuções) — usado pra variar a cor de cada músculo sempre do +// MESMO jeito, sem depender de sorte/aleatoriedade a cada rodada do +// pipeline. +function stringHash(s) { + let h = 0; + for (let i = 0; i < s.length; i++) { + h = (h * 31 + s.charCodeAt(i)) | 0; + } + return h >>> 0; +} + +// O sistema muscular real do Z-Anatomy tem uma limitação: no tronco +// (peitoral, oblíquos, reto do abdome, serrátil), a malha em si é um +// "casco" externo liso, sem o relevo 3D que separa um músculo do outro +// (confirmado comparando com a exportação bruta, antes de qualquer +// processamento nosso — não é algo que a simplificação introduziu). Com +// UMA cor lisa pro sistema inteiro (como todo outro sistema usa), músculos +// vizinhos ficam visualmente indistinguíveis um do outro ali, mesmo sendo +// objetos e nomes diferentes por baixo — "parece tudo agrupado". Damos a +// cada músculo (mesma cor nos dois lados esquerdo/direito, já que é o +// MESMO músculo) uma variação pequena e determinística de matiz/luminosidade +// em torno da cor-base do sistema, só o bastante pra criar um limite visível +// entre estruturas vizinhas sem fugir da identidade de cor do sistema. +function muscleColorVariant(baseHex, variantKey) { + const [h, s, l] = hexToHsl(baseHex); + const hash = stringHash(variantKey); + const hueShift = (((hash % 21) - 10) / 360) * 2; // até ±10° de matiz + const lightShift = (((hash >> 8) % 21) - 10) / 100; // até ±10% de luminosidade + return hslToHex(h + hueShift, s, Math.min(0.85, Math.max(0.25, l + lightShift))); +} + const COMBINING_MARKS = new RegExp("[\\u0300-\\u036f]", "g"); function slugify(s) { @@ -267,17 +339,23 @@ for (const { fbx, key } of SOURCES) { } // Um material por sistema alvo deste arquivo (recoloração consistente, - // independente de como a FBX/Unity original colorizava as malhas). + // independente de como a FBX/Unity original colorizava as malhas) — ou, + // quando variantKey é passado (sistema muscular, ver muscleColorVariant), + // um material por músculo, com uma variação de cor em torno da cor-base + // do sistema. const materialCache = new Map(); - function materialFor(systemSlug) { - if (materialCache.has(systemSlug)) return materialCache.get(systemSlug); + function materialFor(systemSlug, variantKey) { + const cacheKey = variantKey ? `${systemSlug}::${variantKey}` : systemSlug; + if (materialCache.has(cacheKey)) return materialCache.get(cacheKey); const sys = systemBySlug.get(systemSlug); + const baseHex = sys?.colorHex ?? "#cccccc"; + const colorHex = variantKey ? muscleColorVariant(baseHex, variantKey) : baseHex; const mat = doc - .createMaterial(systemSlug) - .setBaseColorFactor(hexToRgb(sys?.colorHex ?? "#cccccc")) + .createMaterial(cacheKey) + .setBaseColorFactor(hexToRgb(colorHex)) .setRoughnessFactor(0.55) .setMetallicFactor(0.05); - materialCache.set(systemSlug, mat); + materialCache.set(cacheKey, mat); return mat; } @@ -294,7 +372,7 @@ for (const { fbx, key } of SOURCES) { const uniqueMeshName = n === 1 ? rawName : `${rawName} (${n})`; node.setName(uniqueMeshName); - let systemSlug, namePt, nameEn, description; + let systemSlug, namePt, nameEn, description, muscleColorKey; if (key === "skeletal") { const resolved = resolveSkeletalBone(rawName); @@ -352,9 +430,10 @@ for (const { fbx, key } of SOURCES) { : key === "nervous" ? NERVE_DESCRIPTIONS[base.toLowerCase()] ?? "" : ""; + if (key === "muscular") muscleColorKey = base.toLowerCase(); } - const material = materialFor(systemSlug); + const material = materialFor(systemSlug, muscleColorKey); for (const prim of mesh.listPrimitives()) prim.setMaterial(material); const sys = systemBySlug.get(systemSlug);