Back to Soybert 3D
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.

src/rig.ts
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(); } };
}