feat(pose_3d): modello reale G1 (URDF + STL meshes) al posto della silhouette finta

Sostituisce torso/head/hips/gambe/spalle primitive in tab Pose 3D con il
modello URDF G1 (g1_body29_hand14), caricato via urdf-loader 0.12.6.

Vendor:
- web/vendor/g1/g1.urdf + 64 mesh STL (~52 MB, copiati da
  G1_API_Server/assets/g1/)
- web/vendor/three/addons/loaders/URDFLoader.js (npm urdf-loader@0.12.6),
  import path adattato a 'three/addons/loaders/STLLoader.js' (importmap
  del sito) e ColladaLoader stubbato (G1 URDF è solo STL)
- web/vendor/three/addons/loaders/URDFClasses.js (companion del loader)

pose_3d.js:
- Aggiunge loadG1Urdf(scene) async che configura loader.loadMeshCb per
  servire le STL dal mount /static/vendor/g1/meshes/
- Best-effort: in caso di errore di parsing/fetch il body non viene
  aggiunto ma la tab continua a funzionare (polsi, UCS, gizmo)
This commit is contained in:
Adriano Dal Pastro committed 2026-05-21 17:14:16 +02:00
1 parent 18dce0d951
commit 8c09d79505
68 files changed
+2651 -37

No files matched your search

+45 -37
View File
@@ -4,6 +4,8 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
import URDFLoader from 'three/addons/loaders/URDFLoader.js';
const $ = (id) => document.getElementById(id);
const POLL_MS = 1000;
@@ -81,6 +83,45 @@ const SHOULDER = {
left: new THREE.Vector3(0, 0.20, 0.60),
};
// ----- URDF loader -----
// Carica il modello G1 reale a partire dal URDF vendored sotto
// /static/vendor/g1/. Le mesh STL referenziate nel URDF (path relativi tipo
// "meshes/pelvis.STL") vengono risolti contro lo stesso prefisso.
async function loadG1Urdf(scene) {
const loader = new URDFLoader();
loader.loadMeshCb = function (path, manager, onComplete) {
// path arrives like "meshes/pelvis.STL"; resolve to our static path
const url = `/static/vendor/g1/${path}`;
const stl = new STLLoader(manager);
stl.load(
url,
(geometry) => {
const mat = new THREE.MeshStandardMaterial({
color: 0x9099a3, metalness: 0.4, roughness: 0.6,
transparent: true, opacity: 0.85,
});
const mesh = new THREE.Mesh(geometry, mat);
onComplete(mesh);
},
undefined,
(err) => onComplete(null, err),
);
};
return new Promise((resolve, reject) => {
loader.load(
'/static/vendor/g1/g1.urdf',
(robot) => {
// G1 URDF uses Z-up too (Unitree convention) so no rotation needed
scene.add(robot);
console.log('[pose_3d] G1 URDF caricato:', robot.name || 'g1');
resolve(robot);
},
undefined,
(err) => reject(err),
);
});
}
// ----- scene init -----
function initScene() {
const canvas = $('p3Canvas');
@@ -117,45 +158,12 @@ function initScene() {
frontArrow.scale.set(0.18, 0.045, 1); // più largo ma sottile, "etichetta a terra"
scene.add(frontArrow);
// Body silhouette: torso + head + legs + shoulders
const bodyMat = new THREE.MeshStandardMaterial({
color: 0x4a5560, transparent: true, opacity: 0.6,
// Modello G1 reale via URDF (best-effort: se il loader fallisce, niente
// body — la tab funziona comunque coi soli polsi/UCS).
loadG1Urdf(scene).catch((e) => {
console.warn('[pose_3d] URDF G1 non caricato:', e.message);
});
// Torso: 20cm x 40cm x 50cm, centered at TORSO_POS
const torso = new THREE.Mesh(new THREE.BoxGeometry(0.20, 0.40, 0.50), bodyMat);
torso.position.copy(TORSO_POS);
scene.add(torso);
// Head: sfera 12cm sopra il torso
const head = new THREE.Mesh(
new THREE.SphereGeometry(0.09, 24, 16), bodyMat,
);
head.position.set(TORSO_POS.x, TORSO_POS.y, TORSO_POS.z + 0.25 + 0.09);
scene.add(head);
// Hips (rectangle) just below torso
const hips = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.36, 0.10), bodyMat);
hips.position.set(TORSO_POS.x, TORSO_POS.y, TORSO_POS.z - 0.25 - 0.05);
scene.add(hips);
// Gambe: 2 cilindri verticali, da hip a Z=0
const legGeom = new THREE.CylinderGeometry(0.06, 0.06, 0.55, 16);
for (const ly of [-0.10, 0.10]) {
const leg = new THREE.Mesh(legGeom, bodyMat);
leg.rotation.x = Math.PI / 2; // asse cilindro su Z
leg.position.set(TORSO_POS.x, TORSO_POS.y + ly, (TORSO_POS.z - 0.30 - 0.275));
scene.add(leg);
}
// Spalle: 2 sferette in corrispondenza degli SHOULDER
const shoulderGeom = new THREE.SphereGeometry(0.04, 16, 12);
for (const side of ['right', 'left']) {
const sh = new THREE.Mesh(shoulderGeom, bodyMat);
sh.position.copy(SHOULDER[side]);
scene.add(sh);
}
// Arms
for (const side of ['right', 'left']) {
const wrist = makeWristMarker(side === 'right' ? 0x4da3ff : 0xff5e7e);