SOURCE / PINNED RELEASE
Made of little things.
Soybert 3D
- Release
- 4bf80e758af1…
- Author-recorded commit
- 298fa2d0e8b1…
- License
- LICENSE
- Author’s source reference
- nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-9021e1b6a96
Archive hash verified: 7083223a53bbd320…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { Bone, BufferAttribute, BufferGeometry, Color, Group, MeshStandardMaterial, Skeleton, SkinnedMesh, Vector3 } from 'three';
import data from './assets/soybert-rig.json';
function bytes(encoded: string): ArrayBuffer {
const decoded = atob(encoded);
const result = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; i++) result[i] = decoded.charCodeAt(i);
return result.buffer;
}
/** The Blender export is bundled data, so no network-capable model loader is needed. */
export function createSoybert() {
const geometry = new BufferGeometry();
geometry.setAttribute('position', new BufferAttribute(new Float32Array(bytes(data.positions)), 3));
geometry.setAttribute('normal', new BufferAttribute(new Float32Array(bytes(data.normals)), 3));
geometry.setIndex(new BufferAttribute(new Uint32Array(bytes(data.indices)), 1));
const skin = new Uint8Array(bytes(data.skin));
const indices = new Uint16Array(skin.length * 4);
const weights = new Float32Array(skin.length * 4);
skin.forEach((bone, i) => { indices[i * 4] = bone; weights[i * 4] = 1; });
geometry.setAttribute('skinIndex', new BufferAttribute(indices, 4));
geometry.setAttribute('skinWeight', new BufferAttribute(weights, 4));
for (const g of data.groups) geometry.addGroup(g.start, g.count, g.material);
const materials = data.materials.map(m => new MeshStandardMaterial({
name: m.name, color: new Color().setRGB(m.color[0], m.color[1], m.color[2]),
roughness: m.roughness, metalness: m.metalness,
}));
const bones: Record<string, Bone> = {};
for (const b of data.bones) {
const bone = new Bone(); bone.name = b.name; bone.position.fromArray(b.position); bones[b.name] = bone;
if (b.parent) bones[b.parent].add(bone);
}
const mesh = new SkinnedMesh(geometry, materials);
mesh.name = 'Soybert'; mesh.add(bones.root); mesh.castShadow = true; mesh.receiveShadow = true;
mesh.frustumCulled = false;
mesh.bind(new Skeleton(data.bones.map(b => bones[b.name])));
const group = new Group(); group.add(mesh);
const rest = new Map(Object.values(bones).map(b => [b, b.position.clone()]));
const muzzle = bones.shotgun.worldToLocal(new Vector3().fromArray(data.muzzle));
const barrel = new Vector3().fromArray(data.barrelDirection);
function pose(time: number, stridePhase: number, speed: number, airborne: boolean, verticalSpeed: number, recoil: number, celebration: number, landing: number, turnLean: number, reduced: boolean) {
for (const bone of Object.values(bones)) { bone.position.copy(rest.get(bone)!); bone.rotation.set(0, 0, 0); bone.scale.set(1, 1, 1); }
const stride = Math.sin(stridePhase);
const intensity = Math.min(1, speed / 5.5);
const bounce = reduced ? .035 : .09;
bones.body.position.y += intensity * bounce * Math.abs(stride) + (reduced ? 0 : Math.sin(time * 2.4) * .018);
bones.body.rotation.x = intensity * .15;
bones.body.rotation.z = stride * .065 * intensity + (reduced ? 0 : turnLean);
bones.leg_L.rotation.x = stride * .95 * intensity;
bones.leg_R.rotation.x = -stride * .95 * intensity;
bones.leg_L.position.y += Math.max(0, -stride) * .12 * intensity;
bones.leg_R.position.y += Math.max(0, stride) * .12 * intensity;
bones.shotgun.position.y += Math.sin(stridePhase * 2) * .025 * intensity;
if (airborne) {
bones.leg_L.rotation.x = -.48; bones.leg_R.rotation.x = .42;
bones.body.rotation.x = verticalSpeed > 0 ? -.10 : .08;
if (!reduced) bones.body.scale.set(.98, 1.045, .98);
} else if (!reduced && landing > 0) {
bones.body.scale.set(1 + landing * .09, 1 - landing * .13, 1 + landing * .09);
bones.body.position.y -= landing * .055;
}
const kick = Math.max(0, 1 - recoil / .17);
bones.shotgun.position.addScaledVector(barrel, -.14 * kick);
bones.shotgun.rotation.x = -.05 * kick;
bones.body.rotation.x -= .07 * kick;
const pump = recoil > .17 && recoil < .60 ? Math.sin((recoil - .17) / .43 * Math.PI) : 0;
bones.pump?.position.addScaledVector(barrel, -.21 * pump);
if (celebration > 0) {
const t = 2.8 - celebration;
const envelope = Math.sin(Math.min(1, t / .3) * Math.PI / 2) * Math.min(1, celebration / .4);
bones.body.rotation.z = Math.sin(t * 10) * .14 * envelope;
bones.body.position.y += Math.abs(Math.sin(t * 8)) * .14 * envelope;
bones.leg_L.rotation.x = Math.sin(t * 8) * .28 * envelope;
bones.leg_R.rotation.x = -Math.sin(t * 8) * .28 * envelope;
if (!reduced) bones.root.rotation.y = Math.PI * 2 * Math.min(1, t / 2.25);
}
}
function muzzleWorld() {
group.updateMatrixWorld(true);
return { origin: bones.shotgun.localToWorld(muzzle.clone()), direction: barrel.clone().transformDirection(bones.shotgun.matrixWorld) };
}
return { group, mesh, bones, pose, muzzleWorld, dispose() { geometry.dispose(); materials.forEach(m => m.dispose()); mesh.skeleton.dispose(); } };
}
