summaryrefslogtreecommitdiff
path: root/tools/portraits/render.html
blob: 8e0a26ecf9bb7c77ea802cd1b0b483d79cbd8c3e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<!doctype html>
<meta charset="utf-8">
<title>render</title>
<body style="margin:0">
<script src="/vendor/aframe-1.7.1.min.js"></script>
<script>
// Renders a transparent head-and-torso portrait of each avatar, arms relaxed,
// three-quarter view, and POSTs each PNG back to the helper server.
const SLUGS = (new URLSearchParams(location.search).get('slugs') ||
  'oldmoustache,bigbro,lilbro,teddy,froggy,astronaut,polybot,wizzir').split(',');
const SIZE = 384;

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(1);
renderer.setSize(SIZE, SIZE);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(0x000000, 0);
document.body.appendChild(renderer.domElement);

const loader = new THREE.GLTFLoader();
const load = (url) => new Promise((res, rej) => loader.load(url, res, undefined, rej));

function relaxArms(bone) {
  const shoulder = new THREE.Vector3(), wrist = new THREE.Vector3(), q = new THREE.Quaternion(), pq = new THREE.Quaternion();
  for (const [side, sign] of [['left', -1], ['right', 1]]) {
    const upper = bone(`${side}UpperArm`), hand = bone(`${side}Hand`);
    if (!upper || !hand) continue;
    upper.getWorldPosition(shoulder);
    hand.getWorldPosition(wrist);
    const from = wrist.sub(shoulder).normalize();
    const to = new THREE.Vector3(sign * 0.28, -1, -0.12).normalize();
    q.setFromUnitVectors(from, to);
    const world = upper.getWorldQuaternion(new THREE.Quaternion()).premultiply(q);
    upper.parent.getWorldQuaternion(pq).invert();
    upper.quaternion.copy(pq).multiply(world);
    upper.updateMatrixWorld(true);
  }
}

async function renderOne(slug) {
  const gltf = await load(`/assets/avatars/${slug}.vrm`);
  const vrm = gltf.parser.json.extensions.VRM;
  const nodes = new Map();
  gltf.scene.traverse((o) => {
    const a = gltf.parser.associations.get(o);
    if (a && a.nodes !== undefined) nodes.set(a.nodes, o);
    if (o.isMesh) o.frustumCulled = false;
  });
  const bone = (name) => {
    const hb = vrm.humanoid.humanBones.find((b) => b.bone === name);
    return hb ? nodes.get(hb.node) : null;
  };

  const scene = new THREE.Scene();
  scene.add(gltf.scene);
  scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 2));
  gltf.scene.updateMatrixWorld(true);
  relaxArms(bone);

  const top = new THREE.Box3().setFromObject(gltf.scene).max.y;
  const hips = bone('hips').getWorldPosition(new THREE.Vector3()).y;
  const bottom = hips - (top - hips) * 0.05;
  const centerY = (top + bottom) / 2;
  const height = (top - bottom) * 1.2;

  const camera = new THREE.PerspectiveCamera(28, 1, 0.01, 100);
  const dist = (height / 2) / Math.tan(THREE.MathUtils.degToRad(14));
  const yaw = THREE.MathUtils.degToRad(22);
  camera.position.set(Math.sin(yaw) * dist, centerY + height * 0.04, -Math.cos(yaw) * dist);
  camera.lookAt(0, centerY, 0);

  renderer.render(scene, camera);
  const blob = await new Promise((res) => renderer.domElement.toBlob(res, 'image/png'));
  await fetch(`/save/${slug}`, { method: 'POST', body: blob });
}

(async () => {
  const results = {};
  for (const slug of SLUGS) {
    try { await renderOne(slug); results[slug] = 'ok'; } catch (e) { results[slug] = String(e); }
  }
  window.results = results;
  document.title = 'done';
})();
</script>