mirror of
https://github.com/storytold/spark.git
synced 2026-10-13 23:52:44 +00:00
106 lines
3.6 KiB
HTML
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>
|