Files
spark/examples/animation-skinning/index.html
T
Diego Marcos 3332f609a9 Initial commit
2025-05-24 00:14:53 -07:00

106 lines
3.6 KiB
HTML

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Forge • Animation • Skinning</title>
<style> body {margin: 0;} </style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "/examples/js/vendor/three/build/three.module.js",
"@forge-gfx/forge": "/dist/forge.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { SplatMesh, SplatSkinning } from "@forge-gfx/forge";
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement)
function gridToBoneIndex(x, y) {
return 15 * (y + 7) + (x + 7);
}
const splatURL = await getAssetFileURL("butterfly.spz");
const butterfly = new SplatMesh({
url: splatURL,
onLoad: (mesh) => {
// Make a 15x15 uniform grid of bones on the XY plane
const skinning = new SplatSkinning({ mesh, numBones: 15 * 15 });
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
for (let y = -7; y <= 7; y++) {
for (let x = -7; x <= 7; x++) {
position.set(x * 0.05, y * 0.05, 0);
skinning.setRestQuatPos(gridToBoneIndex(x, y), quaternion, position);
}
}
// Compute bone indices and weights for each Gsplat such that
// it uses the 4 nearest bones in a bilinear manner.
const boneIndices = new THREE.Vector4();
const weights = new THREE.Vector4();
mesh.forEachSplat((index, center) => {
const x = center.y / 0.05;
const y = center.x / 0.05;
const gridX = Math.max(-7, Math.min(6, Math.floor(x)));
const gridY = Math.max(-7, Math.min(6, Math.floor(y)));
const fractX = Math.max(0, Math.min(1, x - gridX));
const fractY = Math.max(0, Math.min(1, y - gridY));
boneIndices.set(
gridToBoneIndex(gridX, gridY),
gridToBoneIndex(gridX + 1, gridY),
gridToBoneIndex(gridX, gridY + 1),
gridToBoneIndex(gridX + 1, gridY + 1),
);
weights.set(
(1 - fractX) * (1 - fractY),
fractX * (1 - fractY),
(1 - fractX) * fractY,
fractX * fractY,
);
skinning.setSplatBones(index, boneIndices, weights);
});
mesh.skinning = skinning;
mesh.updateGenerator();
},
});
butterfly.quaternion.set(1, 0, 0, 0);
butterfly.position.set(0, 0, -1);
scene.add(butterfly);
renderer.setAnimationLoop(function animate(time) {
renderer.render(scene, camera);
if (butterfly.skinning) {
// Making sure async loading and initialization is complete
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
for (let y = -7; y <= 7; y++) {
for (let x = -7; x <= 7; x++) {
position.set(x * 0.05, y * 0.05, 0);
// Rotate Gsplats to flap the wings
quaternion.setFromEuler(
new THREE.Euler(
x * 0.09 * Math.sin(time * 0.0011),
y * 0.05 * Math.cos(time * 0.0071),
0.2 * Math.sin(time * 0.0011),
),
);
butterfly.skinning.setBoneQuatPos(gridToBoneIndex(x, y), quaternion, position);
}
}
butterfly.skinning.updateBones();
}
});
</script>
</body>
</html>