fix
This commit is contained in:
1 parent
5e6d870410
commit
038ad91bbe
1 file changed
+227
-64
@@ -204,51 +204,179 @@ function meshLocalBBox(mesh) {
|
|||||||
return { min, max };
|
return { min, max };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monta um "prisma fino" (caixa alongada) indo do ponto p0 ao p1, com
|
// Lê TODO vértice real da malha (posição + normal, os dois já calculados
|
||||||
// espessura `thickness` no plano perpendicular ao segmento e
|
// a partir da geometria de verdade) — é nisso que os sulcos vão se
|
||||||
// `depthThickness` ao longo de `depthUnit` — é o sulco decorativo em si.
|
// apoiar, em vez de uma forma geométrica desenhada à mão. Poucos milhares
|
||||||
// Dupla face (setDoubleSided) evita se preocupar com a ordem de
|
// de pontos por músculo, então busca por vizinho mais próximo por força
|
||||||
// enrolamento exata de cada uma das 6 faces (irrelevante pra um detalhe
|
// bruta é rápida o bastante (não precisa de estrutura de indexação).
|
||||||
// decorativo pequeno).
|
function collectSurfacePoints(mesh) {
|
||||||
function buildGrooveMesh(doc, material, p0, p1, thickness, depthThickness, depthUnit) {
|
const points = [];
|
||||||
const dir = vecSub(p1, p0);
|
for (const prim of mesh.listPrimitives()) {
|
||||||
const len = vecLen(dir);
|
const posAcc = prim.getAttribute("POSITION");
|
||||||
if (len < 1e-6) return null;
|
if (!posAcc) continue;
|
||||||
const dirN = vecScale(dir, 1 / len);
|
const normAcc = prim.getAttribute("NORMAL");
|
||||||
let perp = vecCross(dirN, depthUnit);
|
const posArr = posAcc.getArray();
|
||||||
if (vecLen(perp) < 1e-6) perp = vecCross(dirN, [1, 0, 0]);
|
const normArr = normAcc ? normAcc.getArray() : null;
|
||||||
perp = vecNorm(perp);
|
const count = posAcc.getCount();
|
||||||
const halfT = thickness / 2;
|
for (let i = 0; i < count; i++) {
|
||||||
const halfD = depthThickness / 2;
|
const pos = [posArr[i * 3], posArr[i * 3 + 1], posArr[i * 3 + 2]];
|
||||||
const corner = (pt, sPerp, sDepth) =>
|
const normal = normArr
|
||||||
vecAdd(vecAdd(pt, vecScale(perp, sPerp * halfT)), vecScale(depthUnit, sDepth * halfD));
|
? vecNorm([normArr[i * 3], normArr[i * 3 + 1], normArr[i * 3 + 2]])
|
||||||
|
: [0, 0, 0];
|
||||||
|
points.push({ pos, normal });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
const c = [
|
// Acha um ponto REPRESENTATIVO da superfície frontal perto do alvo —
|
||||||
corner(p0, -1, -1), corner(p1, -1, -1), corner(p1, 1, -1), corner(p0, 1, -1),
|
// dentro de uma pequena janela lateral/vertical, faz a MÉDIA dos vértices
|
||||||
corner(p0, -1, 1), corner(p1, -1, 1), corner(p1, 1, 1), corner(p0, 1, 1),
|
// que estão entre os mais frontais dali (não o vértice mais próximo de um
|
||||||
];
|
// ponto no espaço, nem só o único vértice mais frontal). Testamos as duas
|
||||||
const negDepth = vecScale(depthUnit, -1);
|
// alternativas mais simples e as duas falharam: com um "isca" longe da
|
||||||
const negPerp = vecScale(perp, -1);
|
// malha, a diferença de profundidade domina a distância e quase toda
|
||||||
const negDir = vecScale(dirN, -1);
|
// amostra cai perto do MESMO vértice mais extremo do músculo inteiro
|
||||||
const faces = [
|
// (colapsa tudo num ponto); pegando só o único vértice mais frontal
|
||||||
{ idx: [0, 1, 2, 3], n: negDepth },
|
// dentro da janela, a malha real tem uma textura fina de fibra muscular
|
||||||
{ idx: [7, 6, 5, 4], n: depthUnit },
|
// esculpida na geometria (visível como as "listras" na superfície), então
|
||||||
{ idx: [4, 5, 1, 0], n: negPerp },
|
// o "mais frontal" pula de uma micro-ondulação pra outra a cada amostra —
|
||||||
{ idx: [3, 2, 6, 7], n: perp },
|
// uma linha em ziguezague em vez de acompanhar a curva real. Fazer a
|
||||||
{ idx: [0, 3, 7, 4], n: negDir },
|
// MÉDIA de vários vértices próximos e frontais suaviza esse ruído fino
|
||||||
{ idx: [1, 5, 6, 2], n: dirN },
|
// sem deixar de seguir a curvatura de verdade (a média se desloca suave
|
||||||
];
|
// conforme a janela desliza pelo músculo).
|
||||||
|
function frontSurfacePointNear(points, target, lateralIdx, vertIdx, depthIdx, depthSign, lateralWindow0, vertWindow0) {
|
||||||
|
let lateralWindow = lateralWindow0;
|
||||||
|
let vertWindow = vertWindow0;
|
||||||
|
for (let attempt = 0; attempt < 8; attempt++) {
|
||||||
|
let maxDepth = -Infinity;
|
||||||
|
let minDepth = Infinity;
|
||||||
|
const candidates = [];
|
||||||
|
for (const p of points) {
|
||||||
|
if (Math.abs(p.pos[lateralIdx] - target[lateralIdx]) > lateralWindow) continue;
|
||||||
|
if (Math.abs(p.pos[vertIdx] - target[vertIdx]) > vertWindow) continue;
|
||||||
|
const depthVal = depthSign * p.pos[depthIdx];
|
||||||
|
candidates.push({ p, depthVal });
|
||||||
|
if (depthVal > maxDepth) maxDepth = depthVal;
|
||||||
|
if (depthVal < minDepth) minDepth = depthVal;
|
||||||
|
}
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
// Só os 35% mais frontais da janela — descarta a parte de trás do
|
||||||
|
// músculo (que também pode cair dentro da janela lateral/vertical)
|
||||||
|
// sem restringir tanto a ponto de voltar a ser um único vértice.
|
||||||
|
const threshold = maxDepth - (maxDepth - minDepth) * 0.35;
|
||||||
|
const front = candidates.filter((c) => c.depthVal >= threshold);
|
||||||
|
const pos = [0, 0, 0], normal = [0, 0, 0];
|
||||||
|
for (const { p } of front) {
|
||||||
|
pos[0] += p.pos[0]; pos[1] += p.pos[1]; pos[2] += p.pos[2];
|
||||||
|
normal[0] += p.normal[0]; normal[1] += p.normal[1]; normal[2] += p.normal[2];
|
||||||
|
}
|
||||||
|
const n = front.length;
|
||||||
|
return { pos: [pos[0] / n, pos[1] / n, pos[2] / n], normal: vecNorm(normal) };
|
||||||
|
}
|
||||||
|
lateralWindow *= 1.8;
|
||||||
|
vertWindow *= 1.8;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catmull-Rom: curva suave que passa exatamente pelos pontos-âncora
|
||||||
|
// (diferente de Bézier, que só passa pelas pontas). Usada pra desenhar
|
||||||
|
// uma linha fluida entre POUCOS pontos-âncora bem estáveis, em vez de
|
||||||
|
// tentar grudar em cada vértice individual da malha (ruidoso demais —
|
||||||
|
// ver frontSurfacePointNear).
|
||||||
|
function catmullRom1D(p0, p1, p2, p3, t) {
|
||||||
|
const t2 = t * t, t3 = t2 * t;
|
||||||
|
return 0.5 * (2 * p1 + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3);
|
||||||
|
}
|
||||||
|
function catmullRomVec(p0, p1, p2, p3, t) {
|
||||||
|
return [0, 1, 2].map((i) => catmullRom1D(p0[i], p1[i], p2[i], p3[i], t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpola uma curva suave passando por TODAS as âncoras (duplica a
|
||||||
|
// primeira/última pra servir de tangente nas pontas) e devolve uma
|
||||||
|
// sequência bem mais longa e fluida de pontos+normais interpolados —
|
||||||
|
// essa sequência, não as âncoras cruas, é o que vira a malha do sulco.
|
||||||
|
function smoothAnchoredPath(anchors, subdivisionsPerSegment) {
|
||||||
|
if (anchors.length < 2) return anchors;
|
||||||
|
const pos = anchors.map((a) => a.pos);
|
||||||
|
const norm = anchors.map((a) => a.normal);
|
||||||
|
const extPos = [pos[0], ...pos, pos[pos.length - 1]];
|
||||||
|
const extNorm = [norm[0], ...norm, norm[norm.length - 1]];
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < pos.length - 1; i++) {
|
||||||
|
const startJ = i === 0 ? 0 : 1; // evita duplicar o ponto na junção de segmentos
|
||||||
|
for (let j = startJ; j <= subdivisionsPerSegment; j++) {
|
||||||
|
const t = j / subdivisionsPerSegment;
|
||||||
|
const p = catmullRomVec(extPos[i], extPos[i + 1], extPos[i + 2], extPos[i + 3], t);
|
||||||
|
const n = vecNorm(catmullRomVec(extNorm[i], extNorm[i + 1], extNorm[i + 2], extNorm[i + 3], t));
|
||||||
|
result.push({ pos: p, normal: n });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uma LINHA de verdade (primitiva glTF modo LINE_STRIP — sem largura,
|
||||||
|
// sem "volume", sem faces pra triangular) passando pela sequência de
|
||||||
|
// pontos já suavizada. Chegamos a tentar isso como uma LINHA de verdade
|
||||||
|
// (primitiva glTF modo LINE_STRIP, sem volume nenhum) — funcionava bem
|
||||||
|
// nos dados (conferimos com log: os pontos seguem a superfície real e
|
||||||
|
// cobrem a largura toda do músculo, sem serrilhado), mas a renderização
|
||||||
|
// de linhas em WebGL via software (o ambiente headless que usamos pra
|
||||||
|
// validar aqui) é pouco confiável — não dá pra garantir que apareça igual
|
||||||
|
// num navegador de verdade sem depender só disso. Por segurança, voltamos
|
||||||
|
// a uma "crista" sólida (2 faces finas formando um "telhado" baixo,
|
||||||
|
// erguido ao longo da normal real de cada ponto) — geometria comum,
|
||||||
|
// renderiza igual em qualquer motor. A serrinha que essa abordagem tinha
|
||||||
|
// antes (ver histórico) era efeito da amostragem ruidosa ponto a ponto;
|
||||||
|
// agora a entrada já vem suavizada pela curva Catmull-Rom
|
||||||
|
// (smoothAnchoredPath), então a crista nasce lisa. Dupla face
|
||||||
|
// (setDoubleSided no material) dispensa acertar a ordem de enrolamento.
|
||||||
|
function addQuad(positions, normals, indices, p0, p1, p2, p3) {
|
||||||
|
const e1 = vecSub(p1, p0), e2 = vecSub(p2, p0);
|
||||||
|
let n = vecCross(e1, e2);
|
||||||
|
const len = vecLen(n);
|
||||||
|
n = len > 1e-9 ? vecScale(n, 1 / len) : [0, 0, 1];
|
||||||
|
const base = positions.length / 3;
|
||||||
|
for (const p of [p0, p1, p2, p3]) {
|
||||||
|
positions.push(...p);
|
||||||
|
normals.push(...n);
|
||||||
|
}
|
||||||
|
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `outward` é uma direção FIXA (mesma pra todo mundo na linha), não a
|
||||||
|
// normal individual de cada vértice amostrado. Tentamos com a normal real
|
||||||
|
// de cada ponto primeiro — parecia o mais "correto" em teoria — mas a
|
||||||
|
// malha real tem normais um pouco inconsistentes de vértice a vértice
|
||||||
|
// (não é perfeitamente lisa), então a crista ficava ora virada pra fora,
|
||||||
|
// ora quase de lado, sumindo atrás da própria malha em pedaços alternados.
|
||||||
|
// Uma direção fixa (a mesma usada pra achar a superfície frontal em
|
||||||
|
// frontSurfacePointNear) resolve isso: todo o traço se ergue igual, sem
|
||||||
|
// alternância.
|
||||||
|
function buildRidgeMesh(doc, material, samples, halfWidth, height, outward) {
|
||||||
|
if (samples.length < 2) return null;
|
||||||
|
const n = samples.length;
|
||||||
|
const cross = samples.map((s, i) => {
|
||||||
|
const prev = samples[Math.max(0, i - 1)].pos;
|
||||||
|
const next = samples[Math.min(n - 1, i + 1)].pos;
|
||||||
|
const tangent = vecNorm(vecSub(next, prev));
|
||||||
|
let binormal = vecCross(tangent, outward);
|
||||||
|
if (vecLen(binormal) < 1e-6) binormal = vecCross(tangent, [1, 0, 0]);
|
||||||
|
binormal = vecNorm(binormal);
|
||||||
|
return {
|
||||||
|
baseL: vecAdd(s.pos, vecScale(binormal, -halfWidth)),
|
||||||
|
baseR: vecAdd(s.pos, vecScale(binormal, halfWidth)),
|
||||||
|
apex: vecAdd(s.pos, vecScale(outward, height)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const positions = [];
|
const positions = [];
|
||||||
const normals = [];
|
const normals = [];
|
||||||
const indices = [];
|
const indices = [];
|
||||||
for (const face of faces) {
|
for (let i = 0; i < n - 1; i++) {
|
||||||
const base = positions.length / 3;
|
const a = cross[i], b = cross[i + 1];
|
||||||
for (const i of face.idx) {
|
addQuad(positions, normals, indices, a.baseL, a.apex, b.apex, b.baseL);
|
||||||
positions.push(...c[i]);
|
addQuad(positions, normals, indices, a.apex, a.baseR, b.baseR, b.apex);
|
||||||
normals.push(...face.n);
|
|
||||||
}
|
|
||||||
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const posAccessor = doc.createAccessor().setType("VEC3").setArray(new Float32Array(positions));
|
const posAccessor = doc.createAccessor().setType("VEC3").setArray(new Float32Array(positions));
|
||||||
@@ -265,9 +393,9 @@ function buildGrooveMesh(doc, material, p0, p1, thickness, depthThickness, depth
|
|||||||
|
|
||||||
// Adiciona os sulcos decorativos como filhos do próprio nó do músculo —
|
// Adiciona os sulcos decorativos como filhos do próprio nó do músculo —
|
||||||
// herdam a transformação do pai automaticamente, então as coordenadas
|
// herdam a transformação do pai automaticamente, então as coordenadas
|
||||||
// aqui usam o MESMO espaço local da malha real (o bounding box calculado
|
// aqui usam o MESMO espaço local da malha real. `slug` é o do músculo
|
||||||
// em cima dela). `slug` é o do músculo real: o sulco não é uma estrutura
|
// real: o sulco não é uma estrutura própria, só decoração com a MESMA
|
||||||
// própria, só decoração com a MESMA identidade.
|
// identidade (mesmo slug → clicar nele resolve pro nome certo).
|
||||||
function addCosmeticGrooves(doc, targetNode, kind, material, slug) {
|
function addCosmeticGrooves(doc, targetNode, kind, material, slug) {
|
||||||
const mesh = targetNode.getMesh();
|
const mesh = targetNode.getMesh();
|
||||||
if (!mesh) return;
|
if (!mesh) return;
|
||||||
@@ -278,9 +406,11 @@ function addCosmeticGrooves(doc, targetNode, kind, material, slug) {
|
|||||||
const lRange = max[lateralIdx] - min[lateralIdx];
|
const lRange = max[lateralIdx] - min[lateralIdx];
|
||||||
if (vRange <= 0 || dRange <= 0 || lRange <= 0) return;
|
if (vRange <= 0 || dRange <= 0 || lRange <= 0) return;
|
||||||
|
|
||||||
|
const surfacePoints = collectSurfacePoints(mesh);
|
||||||
|
if (surfacePoints.length === 0) return;
|
||||||
|
|
||||||
const vertAt = (frac) => (vertSign > 0 ? min[vertIdx] + frac * vRange : max[vertIdx] - frac * vRange);
|
const vertAt = (frac) => (vertSign > 0 ? min[vertIdx] + frac * vRange : max[vertIdx] - frac * vRange);
|
||||||
const lateralAt = (frac) => min[lateralIdx] + frac * lRange;
|
const lateralAt = (frac) => min[lateralIdx] + frac * lRange;
|
||||||
const frontDepth = (depthSign > 0 ? max[depthIdx] : min[depthIdx]) + depthSign * dRange * 0.08;
|
|
||||||
|
|
||||||
const assemble = (lateralVal, vertVal, depthVal) => {
|
const assemble = (lateralVal, vertVal, depthVal) => {
|
||||||
const p = [0, 0, 0];
|
const p = [0, 0, 0];
|
||||||
@@ -289,37 +419,70 @@ function addCosmeticGrooves(doc, targetNode, kind, material, slug) {
|
|||||||
p[depthIdx] = depthVal;
|
p[depthIdx] = depthVal;
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
const depthUnit = assemble(0, 0, 1).map((v, i) => (i === depthIdx ? depthSign : 0));
|
|
||||||
|
// Janela GRANDE (a média de uma porção generosa da malha ao redor de
|
||||||
|
// cada âncora) — poucos pontos-âncora, mas cada um bem estável. A
|
||||||
|
// suavidade da linha final vem da curva Catmull-Rom entre eles
|
||||||
|
// (smoothAnchoredPath), não de tentar grudar em cada vértice
|
||||||
|
// individual: testamos isso primeiro e a textura fina de fibra
|
||||||
|
// muscular esculpida na malha real fazia a linha "tremer" (ziguezague),
|
||||||
|
// mesmo com janela pequena e média local.
|
||||||
|
const lateralWindow0 = lRange * 0.16;
|
||||||
|
const vertWindow0 = vRange * 0.14;
|
||||||
|
const surfaceSampleAt = (lateralFrac, vertFrac) =>
|
||||||
|
frontSurfacePointNear(
|
||||||
|
surfacePoints,
|
||||||
|
assemble(lateralAt(lateralFrac), vertAt(vertFrac), 0),
|
||||||
|
lateralIdx, vertIdx, depthIdx, depthSign,
|
||||||
|
lateralWindow0, vertWindow0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ridgeHeight = Math.min(vRange, lRange) * 0.05;
|
||||||
|
const ridgeHalfWidth = Math.min(vRange, lRange) * 0.018;
|
||||||
|
const SUBDIVISIONS = 10; // pontos interpolados entre cada par de âncoras
|
||||||
|
|
||||||
|
// Direção "pra fora" FIXA (a mesma usada pra achar a superfície frontal
|
||||||
|
// acima) — ver comentário em buildRidgeMesh sobre por que não usamos a
|
||||||
|
// normal individual de cada vértice amostrado.
|
||||||
|
const outward = [0, 0, 0];
|
||||||
|
outward[depthIdx] = depthSign;
|
||||||
|
|
||||||
const grooveMeshes = [];
|
const grooveMeshes = [];
|
||||||
if (kind === "bands") {
|
if (kind === "bands") {
|
||||||
// Intersecções tendíneas do reto do abdome — aproximação: 3 faixas
|
// Intersecções tendíneas do reto do abdome — aproximação: 3 faixas
|
||||||
// horizontais igualmente espaçadas (a posição real varia de pessoa
|
// horizontais igualmente espaçadas (a posição real varia de pessoa
|
||||||
// pra pessoa, então não há um "certo" a buscar aqui).
|
// pra pessoa, então não há um "certo" a buscar aqui), cada uma com 5
|
||||||
|
// âncoras ao longo da largura, suavizadas numa curva só.
|
||||||
for (const frac of [0.3, 0.55, 0.78]) {
|
for (const frac of [0.3, 0.55, 0.78]) {
|
||||||
const v = vertAt(frac);
|
const anchors = [];
|
||||||
const p0 = assemble(lateralAt(0.12), v, frontDepth);
|
for (let i = 0; i <= 4; i++) {
|
||||||
const p1 = assemble(lateralAt(0.88), v, frontDepth);
|
const lateralFrac = 0.14 + (i / 4) * 0.72;
|
||||||
const m = buildGrooveMesh(doc, material, p0, p1, vRange * 0.03, dRange * 0.2, depthUnit);
|
const s = surfaceSampleAt(lateralFrac, frac);
|
||||||
|
if (s) anchors.push(s);
|
||||||
|
}
|
||||||
|
const m = buildRidgeMesh(
|
||||||
|
doc, material, smoothAnchoredPath(anchors, SUBDIVISIONS), ridgeHalfWidth, ridgeHeight, outward,
|
||||||
|
);
|
||||||
if (m) grooveMeshes.push(m);
|
if (m) grooveMeshes.push(m);
|
||||||
}
|
}
|
||||||
} else if (kind === "digitations") {
|
} else if (kind === "digitations") {
|
||||||
// Digitações (serrátil anterior / oblíquo externo nas costelas) —
|
// Digitações (serrátil anterior / oblíquo externo nas costelas) —
|
||||||
// aproximação: marcas diagonais CURTAS (relativas ao próprio espaço
|
// aproximação: marcas diagonais curtas espaçadas perto da borda
|
||||||
// entre elas, não ao tamanho total do músculo — a malha real desses
|
// superior do músculo, cada uma com 3 âncoras suavizadas.
|
||||||
// dois costuma ter um bounding box bem maior que a parte visível por
|
|
||||||
// fora, então usar frações grandes do bbox inteiro fazia as marcas
|
|
||||||
// saírem enormes, passando da silhueta do corpo).
|
|
||||||
const count = 4;
|
const count = 4;
|
||||||
const step = lRange / count;
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const latCenter = lateralAt((i + 0.5) / count);
|
const latCenter = (i + 0.5) / count;
|
||||||
const dx = step * 0.14;
|
const dLat = (1 / count) * 0.16;
|
||||||
const vBase = vertAt(0.72);
|
const dVert = Math.min(0.1, (1 / count) * 0.3);
|
||||||
const vTop = vertAt(0.72 + (vRange > 0 ? Math.min(0.1, (step * 0.28) / vRange) : 0.1));
|
const anchors = [];
|
||||||
const p0 = assemble(latCenter - dx, vBase, frontDepth);
|
for (let j = 0; j <= 2; j++) {
|
||||||
const p1 = assemble(latCenter + dx, vTop, frontDepth);
|
const t = j / 2;
|
||||||
const m = buildGrooveMesh(doc, material, p0, p1, step * 0.22, dRange * 0.2, depthUnit);
|
const s = surfaceSampleAt(latCenter - dLat + t * 2 * dLat, 0.7 + t * dVert);
|
||||||
|
if (s) anchors.push(s);
|
||||||
|
}
|
||||||
|
const m = buildRidgeMesh(
|
||||||
|
doc, material, smoothAnchoredPath(anchors, SUBDIVISIONS), ridgeHalfWidth * 0.85, ridgeHeight * 0.85, outward,
|
||||||
|
);
|
||||||
if (m) grooveMeshes.push(m);
|
if (m) grooveMeshes.push(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -750,7 +913,7 @@ for (const { fbx, key } of SOURCES) {
|
|||||||
if (!grooveMaterialCache.has(muscleColorKey)) {
|
if (!grooveMaterialCache.has(muscleColorKey)) {
|
||||||
const baseHex = systemBySlug.get("muscular")?.colorHex ?? "#cccccc";
|
const baseHex = systemBySlug.get("muscular")?.colorHex ?? "#cccccc";
|
||||||
const muscleHex = muscleColorVariant(baseHex, muscleColorKey);
|
const muscleHex = muscleColorVariant(baseHex, muscleColorKey);
|
||||||
const grooveHex = darkenHex(muscleHex, 0.16);
|
const grooveHex = darkenHex(muscleHex, 0.24);
|
||||||
const mat = doc
|
const mat = doc
|
||||||
.createMaterial(`groove::${muscleColorKey}`)
|
.createMaterial(`groove::${muscleColorKey}`)
|
||||||
.setBaseColorFactor(hexToRgb(grooveHex))
|
.setBaseColorFactor(hexToRgb(grooveHex))
|
||||||
|
|||||||
Reference in new issue
Block a user