Per-model arm spread against limb/body intersection

Dance clips are authored for the standard mannequin: on chunkier models
the same joint angles drive the arms through the torso (the ninja is
clean because its proportions match). New 'Apertura braccia' slider
(0-35deg, saved per model path): the upper arms get an extra outward
rotation about the world Z axis during clip playback (left +, right -),
solved back into each bone's local frame through the parent rotation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-31 11:58:17 +02:00
1 parent 26dea77c70
commit a69cac0ebd
5 files changed
+47

No files matched your search

+4
View File
@@ -169,6 +169,10 @@
con la camera lo comandi in “Interattivo Avatar”.</div>
<label>I miei modelli</label>
<div id="glb-models" class="glb-models"></div>
<label>Apertura braccia <span id="glb-spread-val">0°</span></label>
<input type="range" id="glb-spread" min="0" max="35" step="1" value="0" />
<div class="hint-mini">Se durante i balli le braccia trapassano il corpo (modelli più larghi
del manichino standard), aumenta finché sono libere. Salvato per ogni modello.</div>
<label>Sfondo</label>
<select id="glb-bg">
<option value="gradient">Gradiente</option>
+21
View File
@@ -162,6 +162,7 @@ function glbSend() {
glbAnimFiles.forEach(p => send({ type: 'animAdd', path: p }));
send({ type: 'glbAnims', names: glbClipSel });
send({ type: 'modelBpm', bpm: glbBpm });
spreadSend();
}
function renderGlbClips(names) {
const box = $('#glb-anims');
@@ -215,6 +216,8 @@ function renderGlbModels() {
localStorage.setItem('glb3d', glbPath);
send({ type: 'glb', path: glbPath });
glbLabel(mo.name);
spreadUi();
spreadSend();
renderGlbModels();
});
const del = document.createElement('button');
@@ -231,6 +234,16 @@ function renderGlbModels() {
box.appendChild(row);
});
}
// Arm spread, saved per model path (chunky models need more room).
let glbSpreadMap = {};
try { glbSpreadMap = JSON.parse(localStorage.getItem('glbspread') || '{}'); } catch (e) {}
function spreadFor(p) { return glbSpreadMap[p] || 0; }
function spreadUi() {
const v = spreadFor(glbPath);
$('#glb-spread').value = v;
$('#glb-spread-val').textContent = v + '°';
}
function spreadSend() { send({ type: 'modelSpread', deg: spreadFor(glbPath) }); }
function glbLabel(name) {
$('#glb-name').textContent = name
? '🧊 ' + name + ' — attiva la famiglia “Modello 3D” per vederlo.'
@@ -258,6 +271,13 @@ $('#glb-bg').addEventListener('change', (e) => {
send({ type: 'modelBg', mode: glbBg });
});
renderGlbClips([]);
spreadUi();
$('#glb-spread').addEventListener('input', (e) => {
const v = parseInt(e.target.value, 10) || 0;
if (glbPath) { glbSpreadMap[glbPath] = v; localStorage.setItem('glbspread', JSON.stringify(glbSpreadMap)); }
$('#glb-spread-val').textContent = v + '°';
send({ type: 'modelSpread', deg: v });
});
$('#btn-anim-load').addEventListener('click', () => $('#anim-input').click());
$('#anim-input').addEventListener('change', async (e) => {
for (const f of [...e.target.files]) {
@@ -357,6 +377,7 @@ $('#glb-input').addEventListener('change', async (e) => {
glbSend();
glbLabel(f.name.replace(/\.fbx$/i, '.glb'));
addGlbModel(f.name.replace(/\.fbx$/i, '.glb'), p);
spreadUi();
});
// --- Effect sequence (playlist of effects with auto-cycle) ---
+15
View File
@@ -476,6 +476,7 @@ class ModelSim {
this.libAnims = []; // library clips bound to the current skeleton
this.clipFilter = null; // Set of allowed clip names (empty/null = all)
this.manualBpm = 0; // 0 = follow the detected beat
this.armSpread = 0; // radians: swing the arms away from the torso
this._upload(torusKnot());
}
@@ -852,6 +853,20 @@ class ModelSim {
}
}
}
// arm spread: clips authored for the standard mannequin make chunky
// models self-intersect — rotate the upper arms outward (world Z,
// the character faces the camera) by the per-model amount
if (anim && this.armSpread) {
const b2 = base(n.name);
if (b2 === 'LeftArm' || b2 === 'RightArm') {
const sa = b2 === 'LeftArm' ? this.armSpread : -this.armSpread;
const cs = Math.cos(sa), sn = Math.sin(sa);
const Rz = [cs, sn, 0, -sn, cs, 0, 0, 0, 1];
const pr = parentWorld ? m4Rot3(parentWorld) : [1,0,0, 0,1,0, 0,0,1];
const prT = [pr[0],pr[3],pr[6], pr[1],pr[4],pr[7], pr[2],pr[5],pr[8]];
r3 = m3Mul(m3Mul(m3Mul(prT, Rz), pr), r3);
}
}
const s = n.s;
local = m4FromM3T([r3[0]*s[0], r3[1]*s[0], r3[2]*s[0],
r3[3]*s[1], r3[4]*s[1], r3[5]*s[1],
+1
View File
@@ -323,6 +323,7 @@ djv.onControl(async (m) => {
case 'modelBg': viz.modelBg = m.mode || 'gradient'; break;
case 'glbAnims': viz.setClipFilter(m.names || []); break;
case 'modelBpm': viz.setManualBpm(m.bpm || 0); break;
case 'modelSpread': viz.setArmSpread(m.deg || 0); break;
case 'animAdd':
// extra animation GLB for the dance library
try {
+6
View File
@@ -97,6 +97,7 @@ class Visualizer {
});
if (this._clipFilter) this.model3d.clipFilter = this._clipFilter;
if (this._manualBpm) this.model3d.manualBpm = this._manualBpm;
if (this._armSpread) this.model3d.armSpread = this._armSpread;
}
if (this._pendingGlb) { this.model3d.setModel(this._pendingGlb); this._pendingGlb = null; }
return this.model3d;
@@ -130,6 +131,11 @@ class Visualizer {
if (this.model3d) this.model3d.manualBpm = this._manualBpm;
}
setArmSpread(deg) {
this._armSpread = (deg || 0)*Math.PI/180;
if (this.model3d) this.model3d.armSpread = this._armSpread;
}
// Upload a custom source (HTMLImageElement / HTMLCanvasElement) for the
// SVG/Image effect family. Flipped on the Y axis to match screen space.
setTexture(source) {