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: