Skip to content

Commit fa580f4

Browse files
committed
feat: first version of body-types with humanoid glbs
1 parent 48e9950 commit fa580f4

6 files changed

Lines changed: 195 additions & 227 deletions

File tree

website/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717
</head>
1818

1919
<body>
20+
<script type="importmap">
21+
{
22+
"imports": {
23+
"three": "https://unpkg.com/three@0.160.0/build/three.module.js"
24+
}
25+
}
26+
</script>
27+
2028
<!-- fullPage.js wrapper — must not be <body> -->
2129
<div id="website">
2230

website/js/AthleteRenderer.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import * as THREE from "https://unpkg.com/three@0.160.0/build/three.module.js";
2+
import { GLTFLoader } from "https://unpkg.com/three@0.160.0/examples/jsm/loaders/GLTFLoader.js";
3+
4+
export class AthleteRenderer {
5+
static loader = new GLTFLoader();
6+
7+
static cache = {
8+
man: null,
9+
woman: null
10+
};
11+
12+
static loadModels(manUrl, womanUrl) {
13+
return Promise.all([
14+
this._load(manUrl).then(m => (this.cache.man = m)),
15+
this._load(womanUrl).then(m => (this.cache.woman = m))
16+
]);
17+
}
18+
19+
static _load(url) {
20+
return new Promise((resolve, reject) => {
21+
this.loader.load(url, gltf => resolve(gltf.scene), undefined, reject);
22+
});
23+
}
24+
25+
static addHuman(scene, athlete, pos, gender = "man") {
26+
const base = this.cache[gender].clone(true);
27+
28+
const group = new THREE.Group();
29+
group.position.set(pos.x, pos.podiumHeight, 0);
30+
31+
const height = athlete.height || 170;
32+
const weight = athlete.weight || 65;
33+
34+
const heightScale = THREE.MathUtils.clamp(height / 175, 0.85, 1.2);
35+
const weightScale = THREE.MathUtils.clamp(weight / 70, 0.85, 1.25);
36+
37+
base.scale.setScalar(heightScale);
38+
base.scale.x *= weightScale;
39+
base.scale.z *= weightScale;
40+
base.position.y = 0;
41+
42+
base.traverse((obj) => {
43+
if (!obj.isMesh) return;
44+
const mat = obj.material;
45+
mat.transparent = false;
46+
mat.opacity = 1;
47+
mat.side = THREE.DoubleSide;
48+
if (mat.map) mat.map.colorSpace = THREE.SRGBColorSpace;
49+
if (mat.emissiveMap) mat.emissiveMap.colorSpace = THREE.SRGBColorSpace;
50+
});
51+
52+
base.traverse((obj) => {
53+
if (!obj.isMesh) return;
54+
const name = obj.name.toLowerCase();
55+
if (name.includes("cloth") || name.includes("shirt") || name.includes("body")) {
56+
obj.material = new THREE.MeshStandardMaterial({
57+
color: 0xf2f2f2,
58+
roughness: 0.8
59+
});
60+
}
61+
});
62+
63+
group.add(base);
64+
65+
// Real bounding box so all positions adapt to any model scale
66+
const tempBox = new THREE.Box3().setFromObject(group);
67+
const modelWorldHeight = tempBox.max.y - tempBox.min.y;
68+
69+
const chestWorldY = tempBox.min.y + modelWorldHeight * 0.72;
70+
const neckWorldY = tempBox.min.y + modelWorldHeight * 0.88;
71+
const frontWorldZ = tempBox.max.z + 0.02;
72+
73+
// Group-local Y (group sits at pos.podiumHeight)
74+
const chestLocalY = chestWorldY - pos.podiumHeight;
75+
const neckLocalY = neckWorldY - pos.podiumHeight;
76+
77+
const medalRadius = modelWorldHeight * 0.032;
78+
const strapRadius = medalRadius * 0.12;
79+
const neckHalfWidth = modelWorldHeight * 0.045; // where straps leave the neck
80+
const neckZ = frontWorldZ - medalRadius * 1.5; // straps start slightly behind
81+
82+
// Two diagonal ribbon straps forming a V around the neck
83+
const strapMat = new THREE.MeshStandardMaterial({ color: 0xcc1111, roughness: 0.65 });
84+
const medalCenter = new THREE.Vector3(0, chestLocalY, frontWorldZ);
85+
86+
[
87+
new THREE.Vector3(-neckHalfWidth, neckLocalY, neckZ),
88+
new THREE.Vector3( neckHalfWidth, neckLocalY, neckZ)
89+
].forEach(strapStart => {
90+
this._addSegment(group, strapStart, medalCenter, strapRadius, strapMat);
91+
});
92+
93+
// Medal disc (drawn after straps so it sits on top)
94+
const medalMat = new THREE.MeshStandardMaterial({
95+
color: this._medalColor(athlete.medal),
96+
metalness: 0.75,
97+
roughness: 0.25
98+
});
99+
const medalDisc = new THREE.Mesh(
100+
new THREE.CylinderGeometry(medalRadius, medalRadius, medalRadius * 0.25, 32),
101+
medalMat
102+
);
103+
medalDisc.position.set(0, chestLocalY, frontWorldZ);
104+
medalDisc.rotation.x = Math.PI / 2;
105+
group.add(medalDisc);
106+
107+
scene.add(group);
108+
return group;
109+
}
110+
111+
// Draws a cylinder aligned between two arbitrary 3D points
112+
static _addSegment(group, from, to, radius, material) {
113+
const dir = new THREE.Vector3().subVectors(to, from);
114+
const length = dir.length();
115+
const mid = new THREE.Vector3().lerpVectors(from, to, 0.5);
116+
117+
const mesh = new THREE.Mesh(
118+
new THREE.CylinderGeometry(radius, radius, length, 8),
119+
material
120+
);
121+
mesh.position.copy(mid);
122+
// CylinderGeometry is Y-up by default; rotate to align with dir
123+
mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.clone().normalize());
124+
group.add(mesh);
125+
}
126+
127+
static _medalColor(medal) {
128+
switch (medal) {
129+
case "Gold": return 0xf4c542;
130+
case "Silver": return 0xcfd5db;
131+
case "Bronze": return 0xd88c32;
132+
default: return 0xffffff;
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)