diff --git a/apps/docs/package.json b/apps/docs/package.json
index e878a65..69259f8 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -15,9 +15,12 @@
"@astrojs/starlight-tailwind": "^2.0.1",
"@astrojs/svelte": "^4.0.3",
"@astrojs/tailwind": "^5.0.2",
+ "@threejs-kit/materials": "workspace:^",
+ "@threejs-kit/instanced-sprite-mesh": "workspace:^",
"astro": "^3.5.4",
"sharp": "^0.32.6",
"svelte": "^4.2.3",
- "tailwindcss": "^3.3.5"
+ "tailwindcss": "^3.3.5",
+ "three": "^0.158.0"
}
}
diff --git a/apps/docs/src/content/docs/sprites/instanced-sprite-mesh.mdx b/apps/docs/src/content/docs/sprites/instanced-sprite-mesh.mdx
index bcac141..1af0637 100644
--- a/apps/docs/src/content/docs/sprites/instanced-sprite-mesh.mdx
+++ b/apps/docs/src/content/docs/sprites/instanced-sprite-mesh.mdx
@@ -1,9 +1,7 @@
---
title: InstancedSpriteMesh
-description: Parallax Occlusion material for three.js
+description: InstancedSpriteMesh
---
-import { Image } from "astro:assets";
-
### InstancedSpriteMesh
diff --git a/apps/playground/src/routes/+layout.svelte b/apps/playground/src/routes/+layout.svelte
index 8b7497c..0183694 100644
--- a/apps/playground/src/routes/+layout.svelte
+++ b/apps/playground/src/routes/+layout.svelte
@@ -1,22 +1,5 @@
-
-
-
-
+
diff --git a/apps/playground/src/routes/glint/+page.svelte b/apps/playground/src/routes/glint/+page.svelte
index 045633e..0e7aacb 100644
--- a/apps/playground/src/routes/glint/+page.svelte
+++ b/apps/playground/src/routes/glint/+page.svelte
@@ -1,5 +1,23 @@
-
-
+
+
+
diff --git a/apps/playground/src/routes/instanced-sprite/+page.svelte b/apps/playground/src/routes/instanced-sprite/+page.svelte
index 715c257..303ef8a 100644
--- a/apps/playground/src/routes/instanced-sprite/+page.svelte
+++ b/apps/playground/src/routes/instanced-sprite/+page.svelte
@@ -1,5 +1,23 @@
-
+
+
+
diff --git a/apps/playground/src/routes/instanced-sprite/vanilla/+page.svelte b/apps/playground/src/routes/instanced-sprite/vanilla/+page.svelte
new file mode 100644
index 0000000..87e33a0
--- /dev/null
+++ b/apps/playground/src/routes/instanced-sprite/vanilla/+page.svelte
@@ -0,0 +1,8 @@
+
diff --git a/apps/playground/src/routes/instanced-sprite/vanilla/instancedSprite.ts b/apps/playground/src/routes/instanced-sprite/vanilla/instancedSprite.ts
new file mode 100644
index 0000000..8ccd8e8
--- /dev/null
+++ b/apps/playground/src/routes/instanced-sprite/vanilla/instancedSprite.ts
@@ -0,0 +1,295 @@
+import * as THREE from 'three';
+import Stats from 'three/examples/jsm/libs/stats.module.js';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+import rawSpritesheet from './player.json?raw';
+import { InstancedSpriteMesh, parseAseprite } from '@threejs-kit/instanced-sprite-mesh';
+
+export const start = async () => {
+ const INSTANCE_COUNT = 10000;
+
+ // GENERAL SCENE SETUP
+ const camera = new THREE.PerspectiveCamera(
+ 36,
+ window.innerWidth / window.innerHeight,
+ 0.01,
+ 2000
+ );
+ camera.position.set(0, 7, 15);
+ const scene = new THREE.Scene();
+ const renderer = new THREE.WebGLRenderer();
+ renderer.shadowMap.enabled = true;
+ renderer.setPixelRatio(window.devicePixelRatio);
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ document.body.appendChild(renderer.domElement);
+
+ const stats = new Stats();
+ document.body.appendChild(stats.dom);
+
+ sceneSetup();
+
+ // INSTANCED SPRITE SETUP
+ type SpriteAnimations =
+ | 'RunRight'
+ | 'RunLeft'
+ | 'RunForward'
+ | 'IdleRight'
+ | 'IdleLeft'
+ | 'IdleForward'
+ | 'RunBackward'
+ | 'IdleBackward';
+ const sprite = spriteMeshSetup();
+ scene.add(sprite);
+ sprite.castShadow = true;
+
+ // UPDATING AND MOVING SPRITES
+
+ let dirtyInstanceMatrix = false;
+
+ const tempMatrix = new THREE.Matrix4();
+ function updatePosition(id: number, position: THREE.Vector3Tuple) {
+ tempMatrix.setPosition(...position);
+ sprite.setMatrixAt(id, tempMatrix);
+ dirtyInstanceMatrix = true;
+ }
+
+ const posX: number[] = new Array(INSTANCE_COUNT).fill(0);
+ const posZ: number[] = new Array(INSTANCE_COUNT).fill(0);
+
+ type Agent = {
+ action: 'Idle' | 'Run';
+ velocity: [number, number];
+ timer: number;
+ };
+
+ const agents: Agent[] = [];
+ const { updateAgents, pickAnimation } = setupRandomAgents();
+
+ const dirs = {
+ up: false,
+ down: false,
+ left: false,
+ right: false
+ };
+
+ // Player movement & indicator
+ const playerMoveVector = new THREE.Vector2(0, 0);
+ const playerIndicator = new THREE.Mesh(
+ new THREE.SphereGeometry(0.15, 3, 2),
+ new THREE.MeshBasicMaterial({ color: 'lime' })
+ );
+ scene.add(playerIndicator);
+
+ const updatePlayerMovement = () => {
+ playerMoveVector.setX((dirs.left ? -1 : 0) + (dirs.right ? 1 : 0));
+ playerMoveVector.setY((dirs.up ? -1 : 0) + (dirs.down ? 1 : 0));
+
+ // player is agent 0
+ agents[0].velocity = playerMoveVector.normalize().multiplyScalar(3).toArray();
+
+ const animation = pickAnimation(0);
+ sprite.play(animation).at(0);
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'a' || e.key === 'ArrowLeft') dirs.left = true;
+ if (e.key === 'd' || e.key === 'ArrowRight') dirs.right = true;
+ if (e.key === 'w' || e.key === 'ArrowUp') dirs.up = true;
+ if (e.key === 's' || e.key === 'ArrowDown') dirs.down = true;
+ updatePlayerMovement();
+ };
+
+ const handleKeyUp = (e: KeyboardEvent) => {
+ if (e.key === 'a' || e.key === 'ArrowLeft') dirs.left = false;
+ if (e.key === 'd' || e.key === 'ArrowRight') dirs.right = false;
+ if (e.key === 'w' || e.key === 'ArrowUp') dirs.up = false;
+ if (e.key === 's' || e.key === 'ArrowDown') dirs.down = false;
+ updatePlayerMovement();
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ window.addEventListener('keyup', handleKeyUp);
+
+ animate();
+
+ function setupRandomAgents() {
+ const spread = 400;
+ const minCenterDistance = 5;
+ const maxCenterDistance = spread;
+ const rndPosition: any = () => {
+ const x = Math.random() * spread - spread / 2;
+ const y = Math.random() * spread - spread / 2;
+
+ /** min distance from 0,0. Recursive reroll if too close */
+
+ if (Math.sqrt(x ** 2 + y ** 2) < minCenterDistance) {
+ return rndPosition();
+ }
+
+ return { x, y };
+ };
+
+ /** update from 1 because 0 is user controlled and set at 0,0 */
+ for (let i = 1; i < INSTANCE_COUNT; i++) {
+ const pos = rndPosition();
+ posX[i] = pos.x;
+ posZ[i] = pos.y;
+ }
+
+ for (let i = 0; i < INSTANCE_COUNT; i++) {
+ agents.push({
+ action: 'Run',
+ timer: 0.1,
+ velocity: [0, 1]
+ });
+ }
+
+ const pickAnimation = (i: number) => {
+ const dirWords = ['Forward', 'Backward', 'Left', 'Right'];
+
+ const isHorizontal =
+ Math.abs(agents[i].velocity[0] * 2) > Math.abs(agents[i].velocity[1]) ? 2 : 0;
+ const isLeft = agents[i].velocity[0] > 0 ? 1 : 0;
+ const isUp = agents[i].velocity[1] > 0 ? 0 : 1;
+
+ const secondMod = isHorizontal ? isLeft : isUp;
+ const chosenWord = dirWords.slice(0 + isHorizontal, 2 + isHorizontal);
+
+ const animationName = `${agents[i].action}${chosenWord[secondMod]}` as SpriteAnimations;
+
+ return animationName;
+ };
+ const velocityHelper = new THREE.Vector2(0, 0);
+
+ const updateAgents = (delta: number) => {
+ for (let i = 0; i < agents.length; i++) {
+ // timer
+ agents[i].timer -= delta;
+
+ // apply velocity
+ posX[i] += agents[i].velocity[0] * delta;
+ posZ[i] += agents[i].velocity[1] * delta;
+
+ // roll new behaviour when time runs out or agent gets out of bounds
+ if (i > 0) {
+ const dist = Math.sqrt((posX[i] || 0) ** 2 + (posZ[i] || 0) ** 2);
+ if (agents[i].timer < 0 || dist < minCenterDistance || dist > maxCenterDistance) {
+ const runChance = 0.6 + (agents[i].action === 'Idle' ? 0.3 : 0);
+ agents[i].action = Math.random() < runChance ? 'Run' : 'Idle';
+
+ agents[i].timer = 5 + Math.random() * 5;
+
+ if (agents[i].action === 'Run') {
+ velocityHelper
+ .set(Math.random() - 0.5, Math.random() - 0.5)
+ .normalize()
+ .multiplyScalar(3);
+ agents[i].velocity = velocityHelper.toArray();
+ }
+
+ const animation: SpriteAnimations = pickAnimation(i);
+ if (agents[i].action === 'Idle') {
+ agents[i].velocity = [0, 0];
+ }
+
+ sprite.play(animation).at(i);
+ }
+ }
+ }
+
+ for (let i = 0; i < INSTANCE_COUNT; i++) {
+ updatePosition(i, [posX[i] || 0, 0.5, posZ[i] || 0]);
+ }
+ };
+
+ return { updateAgents, pickAnimation };
+ }
+
+ function sceneSetup() {
+ // Lights
+ scene.add(new THREE.AmbientLight(0xcccccc));
+
+ const dirLight = new THREE.DirectionalLight(0x55505a, 3);
+ dirLight.position.set(0, 4, -10);
+ dirLight.castShadow = true;
+ dirLight.shadow.camera.near = 1;
+ dirLight.shadow.camera.far = 128;
+
+ dirLight.shadow.camera.right = 20;
+ dirLight.shadow.camera.left = -20;
+ dirLight.shadow.camera.top = 20;
+ dirLight.shadow.camera.bottom = -20;
+ dirLight.shadow.bias = -0.001;
+
+ dirLight.shadow.mapSize.width = 1024;
+ dirLight.shadow.mapSize.height = 1024;
+ scene.add(dirLight);
+
+ const ground = new THREE.Mesh(
+ new THREE.PlaneGeometry(2000, 2000, 1, 1),
+ new THREE.MeshPhongMaterial({ color: 0x99cc88, shininess: 0 })
+ );
+
+ ground.rotation.x = -Math.PI / 2; // rotates X/Y to X/Z
+ ground.receiveShadow = true;
+ scene.add(ground);
+
+ // Stats
+
+ // Renderer
+
+ window.addEventListener('resize', onWindowResize);
+
+ // Controls
+ const controls = new OrbitControls(camera, renderer.domElement);
+ controls.target.set(0, 1, 0);
+ controls.update();
+ }
+
+ function spriteMeshSetup() {
+ // dataUrl="/textures/sprites/player.json"
+ const texture = new THREE.TextureLoader().load('/textures/sprites/player.png');
+ texture.minFilter = THREE.NearestFilter;
+ texture.magFilter = THREE.NearestFilter;
+
+ const baseMaterial = new THREE.MeshBasicMaterial({
+ transparent: true,
+ alphaTest: 0.01,
+ // needs to be double side for shading
+ side: THREE.DoubleSide,
+ map: texture
+ });
+
+ const mesh: InstancedSpriteMesh =
+ new InstancedSpriteMesh(baseMaterial, INSTANCE_COUNT);
+
+ mesh.material.uniforms.fps.value = 15;
+
+ const spritesheet = parseAseprite(JSON.parse(rawSpritesheet));
+ mesh.spritesheet = spritesheet;
+
+ return mesh;
+ }
+
+ function onWindowResize() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ }
+
+ function animate() {
+ requestAnimationFrame(animate);
+ stats.begin();
+ renderer.render(scene, camera);
+ playerIndicator.position.set(posX[0], 2, posZ[0]);
+ updateAgents(0.01);
+
+ sprite.updateTime();
+ if (dirtyInstanceMatrix) {
+ sprite.instanceMatrix.needsUpdate = true;
+ dirtyInstanceMatrix = false;
+ }
+ stats.end();
+ }
+};
diff --git a/apps/playground/src/routes/instanced-sprite/vanilla/player.json b/apps/playground/src/routes/instanced-sprite/vanilla/player.json
new file mode 100644
index 0000000..559e86a
--- /dev/null
+++ b/apps/playground/src/routes/instanced-sprite/vanilla/player.json
@@ -0,0 +1,605 @@
+{ "frames": {
+ "0": {
+ "frame": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "1": {
+ "frame": { "x": 32, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "2": {
+ "frame": { "x": 64, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "3": {
+ "frame": { "x": 96, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "4": {
+ "frame": { "x": 128, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "5": {
+ "frame": { "x": 160, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "6": {
+ "frame": { "x": 192, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "7": {
+ "frame": { "x": 224, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "8": {
+ "frame": { "x": 256, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "9": {
+ "frame": { "x": 288, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "10": {
+ "frame": { "x": 320, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "11": {
+ "frame": { "x": 352, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "12": {
+ "frame": { "x": 384, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "13": {
+ "frame": { "x": 416, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "14": {
+ "frame": { "x": 448, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "15": {
+ "frame": { "x": 480, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "16": {
+ "frame": { "x": 512, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "17": {
+ "frame": { "x": 544, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "18": {
+ "frame": { "x": 576, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "19": {
+ "frame": { "x": 608, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "20": {
+ "frame": { "x": 640, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "21": {
+ "frame": { "x": 672, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "22": {
+ "frame": { "x": 704, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "23": {
+ "frame": { "x": 736, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "24": {
+ "frame": { "x": 768, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "25": {
+ "frame": { "x": 800, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "26": {
+ "frame": { "x": 832, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "27": {
+ "frame": { "x": 864, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "28": {
+ "frame": { "x": 896, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "29": {
+ "frame": { "x": 928, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "30": {
+ "frame": { "x": 960, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "31": {
+ "frame": { "x": 992, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "32": {
+ "frame": { "x": 1024, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "33": {
+ "frame": { "x": 1056, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "34": {
+ "frame": { "x": 1088, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "35": {
+ "frame": { "x": 1120, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "36": {
+ "frame": { "x": 1152, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "37": {
+ "frame": { "x": 1184, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "38": {
+ "frame": { "x": 1216, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "39": {
+ "frame": { "x": 1248, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "40": {
+ "frame": { "x": 1280, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "41": {
+ "frame": { "x": 1312, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "42": {
+ "frame": { "x": 1344, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "43": {
+ "frame": { "x": 1376, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "44": {
+ "frame": { "x": 1408, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "45": {
+ "frame": { "x": 1440, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "46": {
+ "frame": { "x": 1472, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "47": {
+ "frame": { "x": 1504, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "48": {
+ "frame": { "x": 1536, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ },
+ "49": {
+ "frame": { "x": 1568, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ },
+ "50": {
+ "frame": { "x": 1600, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ },
+ "51": {
+ "frame": { "x": 1632, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ },
+ "52": {
+ "frame": { "x": 1664, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ },
+ "53": {
+ "frame": { "x": 1696, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ },
+ "54": {
+ "frame": { "x": 1728, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "55": {
+ "frame": { "x": 1760, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "56": {
+ "frame": { "x": 1792, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "57": {
+ "frame": { "x": 1824, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "58": {
+ "frame": { "x": 1856, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "59": {
+ "frame": { "x": 1888, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "60": {
+ "frame": { "x": 1920, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "61": {
+ "frame": { "x": 1952, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "62": {
+ "frame": { "x": 1984, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "63": {
+ "frame": { "x": 2016, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "64": {
+ "frame": { "x": 2048, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "65": {
+ "frame": { "x": 2080, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "66": {
+ "frame": { "x": 2112, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "67": {
+ "frame": { "x": 2144, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "68": {
+ "frame": { "x": 2176, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "69": {
+ "frame": { "x": 2208, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 25
+ },
+ "70": {
+ "frame": { "x": 2240, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ },
+ "71": {
+ "frame": { "x": 2272, "y": 0, "w": 32, "h": 32 },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
+ "sourceSize": { "w": 32, "h": 32 },
+ "duration": 2000
+ }
+ },
+ "meta": {
+ "app": "http://www.aseprite.org/",
+ "version": "1.2.25-x64",
+ "image": "player.png",
+ "format": "RGBA8888",
+ "size": { "w": 2304, "h": 32 },
+ "scale": "1",
+ "frameTags": [
+ { "name": "RunRight", "from": 0, "to": 15, "direction": "forward" },
+ { "name": "RunLeft", "from": 16, "to": 31, "direction": "forward" },
+ { "name": "RunForward", "from": 32, "to": 47, "direction": "forward" },
+ { "name": "IdleRight", "from": 48, "to": 49, "direction": "forward" },
+ { "name": "IdleLeft", "from": 50, "to": 51, "direction": "forward" },
+ { "name": "IdleForward", "from": 52, "to": 53, "direction": "forward" },
+ { "name": "RunBackward", "from": 54, "to": 69, "direction": "forward" },
+ { "name": "IdleBackward", "from": 70, "to": 71, "direction": "forward" }
+ ],
+ "layers": [
+ { "name": "Legs", "opacity": 255, "blendMode": "normal" },
+ { "name": "Torso", "opacity": 255, "blendMode": "normal" },
+ { "name": "Arms", "opacity": 255, "blendMode": "normal" },
+ { "name": "Head", "opacity": 255, "blendMode": "normal" }
+ ],
+ "slices": [
+ ]
+ }
+}
diff --git a/packages/instanced-sprite-mesh/src/InstancedSpriteMesh.ts b/packages/instanced-sprite-mesh/src/InstancedSpriteMesh.ts
index 327a034..cde5ef6 100644
--- a/packages/instanced-sprite-mesh/src/InstancedSpriteMesh.ts
+++ b/packages/instanced-sprite-mesh/src/InstancedSpriteMesh.ts
@@ -40,6 +40,8 @@ export class InstancedSpriteMesh<
this._spriteMaterial.uniforms.spritesheetData.value = dataTexture;
this._spriteMaterial.uniforms.dataSize.value.x = dataWidth;
this._spriteMaterial.uniforms.dataSize.value.y = dataHeight;
+ // @ts-ignore
+ // todo type this with named animations?
this._animationMap = animMap;
}
@@ -107,6 +109,20 @@ export class InstancedSpriteMesh<
},
};
}
+
+ play(animation: V, loop: boolean = true) {
+ return {
+ at: (instanceId: number) => {
+ this.loop.setAt(instanceId, loop);
+ this.animation.setAt(instanceId, animation);
+ },
+ global: () => {
+ this.loop.setGlobal(loop);
+ this.animation.setGlobal(animation);
+ },
+ };
+ }
+
/** HSV shift tinting */
get tint() {
/**
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dfccca7..e2f9101 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -39,6 +39,12 @@ importers:
'@astrojs/tailwind':
specifier: ^5.0.2
version: 5.0.2(astro@3.5.4)(tailwindcss@3.3.5)
+ '@threejs-kit/instanced-sprite-mesh':
+ specifier: workspace:^
+ version: link:../../packages/instanced-sprite-mesh
+ '@threejs-kit/materials':
+ specifier: workspace:^
+ version: link:../../packages/materials
astro:
specifier: ^3.5.4
version: 3.5.4(typescript@5.2.2)
@@ -51,6 +57,9 @@ importers:
tailwindcss:
specifier: ^3.3.5
version: 3.3.5
+ three:
+ specifier: ^0.158.0
+ version: 0.158.0
apps/playground:
dependencies: