From 88838792e12f784d0103efb1a14613e75b0fb609 Mon Sep 17 00:00:00 2001 From: luciano Date: Thu, 30 Jul 2026 20:44:43 +0200 Subject: [PATCH] Avatar redesign: armoured robot instead of primitive boxes New primitives (tapered capsule with rounded caps, superellipsoid rounded box) and a full body redesign: three-segment torso (chest plate, dark abdomen, pelvis plate) with glowing core and belt light, helmet with visor/jaw/ears/antenna, shoulder pads, tapered capsule limbs with elbow/knee joints, hand plates with wrist lights, boots with toe lights. Two-tone armour tinted by the palette, emissive details in colorB. Co-Authored-By: Claude Fable 5 --- src/interactive.js | 69 ++++++++++++++++++++++++++++++---------------- src/model3d.js | 42 ++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 25 deletions(-) diff --git a/src/interactive.js b/src/interactive.js index fbb7843..568fbb8 100644 --- a/src/interactive.js +++ b/src/interactive.js @@ -1042,41 +1042,64 @@ class InteractiveSim { const hp2 = hipsVis ? W(24) : [neck[0]+shw*0.28, neck[1]-shw*1.25, neck[2]]; const pelvis = mid(hp1, hp2); const t = shw; - const metal = [0.60, 0.64, 0.72], dark = [0.28, 0.30, 0.36]; + // two-tone armour + palette-emissive details + const ca = e.colorA || [0.05, 0, 0.2]; const acc = e.colorB || [0.2, 1, 1]; + const metal = [0.60+ca[0]*0.18, 0.64+ca[1]*0.18, 0.72+ca[2]*0.18]; + const dark = [0.15, 0.16, 0.21]; // undersuit / joints + const dark2 = [0.30, 0.32, 0.40]; // secondary plates const parts = []; - const limb = (a, b, r, col) => parts.push({ a, b, rx: r, rz: r, kind: 'cube', col }); + const seg = (a, b, r, col, kind) => parts.push({ a, b, rx: r, rz: r, kind: kind || 'caps', col }); const ball = (a, r, col) => parts.push({ a, b: null, rx: r, kind: 'sphere', col }); - // torso + chest light - parts.push({ a: neck, b: pelvis, rx: shw*0.52, rz: shw*0.28, kind: 'cube', col: metal }); - ball(mid(neck, mid(neck, pelvis)), t*0.10, acc); - // head with visor + antenna + const along = (a, b, k) => [a[0]+(b[0]-a[0])*k, a[1]+(b[1]-a[1])*k, a[2]+(b[2]-a[2])*k]; + // --- torso in three segments: chest plate, abdomen, pelvis + const chestC = along(neck, pelvis, 0.30); + parts.push({ a: along(neck, pelvis, 0.02), b: along(neck, pelvis, 0.56), + rx: shw*0.55, rz: t*0.32, kind: 'rbox', col: metal }); + parts.push({ a: along(neck, pelvis, 0.52), b: along(neck, pelvis, 0.80), + rx: shw*0.36, rz: t*0.26, kind: 'rbox', col: dark }); + parts.push({ a: along(neck, pelvis, 0.78), b: along(neck, pelvis, 1.06), + rx: shw*0.45, rz: t*0.28, kind: 'rbox', col: dark2 }); + // glowing chest core + belt light + ball([chestC[0], chestC[1], chestC[2]+t*0.28], t*0.09, acc); + ball([pelvis[0], pelvis[1]+t*0.02, pelvis[2]+t*0.24], t*0.05, acc); + // --- head: neck, helmet, visor, jaw, ears, antenna const nose = L[0].vis > 0.4 ? W(0) : [neck[0], neck[1]+t*0.55, neck[2]]; - const headC = [nose[0], nose[1] + t*0.05, nose[2]]; - parts.push({ a: headC, b: null, rx: t*0.34, ry: t*0.38, rz: t*0.30, kind: 'cube', col: metal }); - parts.push({ a: [headC[0], headC[1]+t*0.02, headC[2]+t*0.30], b: null, - rx: t*0.24, ry: t*0.08, rz: t*0.06, kind: 'cube', col: acc }); - ball([headC[0], headC[1]+t*0.52, headC[2]], t*0.07, acc); - // arms (shoulder->elbow->wrist), glowing hands + const headC = [nose[0], nose[1] + t*0.06, nose[2]]; + seg(neck, headC, t*0.10, dark); + parts.push({ a: headC, b: null, rx: t*0.30, ry: t*0.30, rz: t*0.28, kind: 'rbox', col: metal }); + parts.push({ a: [headC[0], headC[1]+t*0.03, headC[2]+t*0.26], b: null, + rx: t*0.215, ry: t*0.085, rz: t*0.06, kind: 'rbox', col: acc }); + parts.push({ a: [headC[0], headC[1]-t*0.20, headC[2]+t*0.18], b: null, + rx: t*0.16, ry: t*0.075, rz: t*0.10, kind: 'rbox', col: dark2 }); + ball([headC[0]-t*0.30, headC[1], headC[2]], t*0.085, dark2); + ball([headC[0]+t*0.30, headC[1], headC[2]], t*0.085, dark2); + seg([headC[0]+t*0.16, headC[1]+t*0.26, headC[2]], [headC[0]+t*0.22, headC[1]+t*0.52, headC[2]], t*0.025, dark2); + ball([headC[0]+t*0.22, headC[1]+t*0.55, headC[2]], t*0.05, acc); + // --- arms: shoulder pads, tapered capsules, elbow, hand plate const arm = (s, elI, wrI) => { + ball([s[0], s[1]+t*0.04, s[2]], t*0.19, metal); // shoulder pad if (L[elI].vis < 0.4 || L[wrI].vis < 0.4) return; const el = W(elI), wr = W(wrI); - limb(s, el, t*0.14, metal); - limb(el, wr, t*0.12, metal); - ball(el, t*0.15, dark); - ball(wr, t*0.17, acc); + seg(s, el, t*0.115, dark); // upper arm + ball(el, t*0.10, dark2); // elbow + seg(el, wr, t*0.095, metal); // forearm + const hand = along(el, wr, 1.18); + parts.push({ a: wr, b: hand, rx: t*0.095, rz: t*0.05, kind: 'rbox', col: dark2 }); + ball(along(el, wr, 1.10), t*0.045, acc); // wrist light }; arm(sh1, 13, 15); arm(sh2, 14, 16); - ball(sh1, t*0.16, dark); ball(sh2, t*0.16, dark); - // legs with feet (only when actually visible: seated = bust only) + // --- legs: tapered capsules, knee, boot with toe const leg = (h, knI, anI) => { if (!hipsVis || L[knI].vis < 0.4 || L[anI].vis < 0.4) return; const kn = W(knI), an = W(anI); - limb(h, kn, t*0.16, metal); - limb(kn, an, t*0.13, metal); - ball(kn, t*0.16, dark); - parts.push({ a: [an[0], an[1]-t*0.06, an[2]+t*0.12], b: null, - rx: t*0.14, ry: t*0.08, rz: t*0.22, kind: 'cube', col: dark }); + seg(h, kn, t*0.15, metal); // thigh + ball(kn, t*0.115, dark2); // knee + seg(kn, an, t*0.115, dark); // shin + parts.push({ a: [an[0], an[1]-t*0.04, an[2]-t*0.02], + b: [an[0], an[1]-t*0.09, an[2]+t*0.30], rx: t*0.115, rz: t*0.09, + kind: 'rbox', col: metal }); // boot + ball([an[0], an[1]-t*0.06, an[2]+t*0.30], t*0.04, acc); // toe light }; leg(hp1, 25, 27); leg(hp2, 26, 28); this.modelSim.renderAvatar(timeSec, audio, e, canvas, parts); diff --git a/src/model3d.js b/src/model3d.js index fe8711f..7c623dd 100644 --- a/src/model3d.js +++ b/src/model3d.js @@ -404,7 +404,45 @@ class ModelSim { } } } - this.prims = { cube, sphere: this._primMesh(SP, SN, SI) }; + // tapered capsule (limb): rounded ends, slightly narrower at the top + const CP = [], CN = [], CI = [], SEC = 14, CAP = 5; + const prof = []; + for (let i = 0; i <= CAP; i++) { const a = Math.PI/2*(1 - i/CAP); + prof.push({ r: 0.8*Math.cos(a), y: 0.72 + 0.28*Math.sin(a) }); } + prof.push({ r: 1.0, y: -0.72 }); + for (let i = 1; i <= CAP; i++) { const a = Math.PI/2*i/CAP; + prof.push({ r: Math.cos(a), y: -0.72 - 0.28*Math.sin(a) }); } + prof.forEach((p, pi) => { + for (let j = 0; j <= SEC; j++) { + const th = j/SEC*Math.PI*2, x = p.r*Math.cos(th), z = p.r*Math.sin(th); + CP.push(x, p.y, z); + // capsule-style normal: radiate from the nearest axis point + const cy = Math.max(-0.72, Math.min(0.72, p.y)); + const nl = Math.hypot(x, p.y-cy, z) || 1; + CN.push(x/nl, (p.y-cy)/nl, z/nl); + if (pi < prof.length-1 && j < SEC) { + const a0 = pi*(SEC+1)+j; + CI.push(a0, a0+SEC+1, a0+1, a0+1, a0+SEC+1, a0+SEC+2); + } + } + }); + // rounded box: superellipsoid built from the sphere directions + const RP = [], RN = [], RI = [], RST = 10, RSE = 16, EE = 0.42; + const se = (c) => Math.sign(c)*Math.pow(Math.abs(c), EE); + for (let i = 0; i <= RST; i++) { + const ph = i/RST*Math.PI, y = Math.cos(ph), r = Math.sin(ph); + for (let j = 0; j <= RSE; j++) { + const th = j/RSE*Math.PI*2, x = r*Math.cos(th), z = r*Math.sin(th); + RP.push(se(x), se(y), se(z)); + RN.push(x, y, z); // smooth rounded-cube shading + if (i < RST && j < RSE) { + const a0 = i*(RSE+1)+j; + RI.push(a0, a0+RSE+1, a0+1, a0+1, a0+RSE+1, a0+RSE+2); + } + } + } + this.prims = { cube, sphere: this._primMesh(SP, SN, SI), + caps: this._primMesh(CP, CN, CI), rbox: this._primMesh(RP, RN, RI) }; } // Matrix placing a unit primitive as a limb from a to b (thickness rx/rz) @@ -470,7 +508,7 @@ class ModelSim { gl.uniform1f(this.um.uRim, 0.15 + 0.3*beat); gl.uniform1i(this.um.uHasTex, 0); for (const spec of parts) { - const m = this.prims[spec.kind === 'sphere' ? 'sphere' : 'cube']; + const m = this.prims[spec.kind] || this.prims.cube; gl.uniformMatrix4fv(this.um.uModel, false, this._partMatrix(spec)); gl.uniform3fv(this.um.uBase, spec.col); const bind = (buf, loc, n) => {