Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/useSpriteLoader #1790

Merged
merged 15 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
<li><a href="#usevideotexture">useVideoTexture</a></li>
<li><a href="#usetrailtexture">useTrailTexture</a></li>
<li><a href="#usefont">useFont</a></li>
<li><a href="#usespriteloader"></a></li>
</ul>
<li><a href="#performance">Performance</a></li>
<ul>
Expand Down Expand Up @@ -2572,6 +2573,8 @@ type Props = {
instanceItems?: any[]
/** The max number of items to instance (optional) */
maxItems?: number
/** External parsed sprite data, usually from useSpriteLoader ready for use */
spriteDataset?: any
}
```

Expand All @@ -2580,8 +2583,9 @@ The SpriteAnimator component provided by drei is a powerful tool for animating s
Notes:

- The SpriteAnimator component internally uses the useFrame hook from react-three-fiber (r3f) for efficient frame updates and rendering.
- The sprites should contain equal size frames
- The sprites (without a JSON file) should contain equal size frames
- Trimming of spritesheet frames is not yet supported
- Internally uses the `useSpriteLoader` or can use data from it directly

```jsx
<SpriteAnimator
Expand All @@ -2598,7 +2602,7 @@ Notes:
ScrollControls example

```jsx
;<ScrollControls damping={0.2} maxSpeed={0.5} pages={2}>
<ScrollControls damping={0.2} maxSpeed={0.5} pages={2}>
<SpriteAnimator
position={[0.0, -1.5, -1.5]}
startFrame={0}
Expand Down Expand Up @@ -3234,6 +3238,58 @@ In order to preload you do this:
useFont.preload('/fonts/helvetiker_regular.typeface.json')
```

#### useSpriteLoader

Loads texture and JSON files with multiple or single animations and parses them into appropriate format. These assets can be used by multiple SpriteAnimator components to save memory and loading times.

```jsx
/** The texture url to load the sprite frames from */
input?: Url | null,
/** The JSON data describing the position of the frames within the texture (optional) */
json?: string | null,
/** The animation names into which the frames will be divided into (optional) */
animationNames?: string[] | null,
/** The number of frames on a standalone (no JSON data) spritesheet (optional)*/
numberOfFrames?: number | null,
/** The callback to call when all textures and data have been loaded and parsed */
onLoad?: (texture: Texture, textureData?: any) => void
```

```jsx
const { spriteObj } = useSpriteLoader(
'multiasset.png',
'multiasset.json',

['orange', 'Idle Blinking', '_Bat'],
null
)

<SpriteAnimator
position={[4.5, 0.5, 0.1]}
autoPlay={true}
loop={true}
scale={5}
frameName={'_Bat'}
animationNames={['_Bat']}
spriteDataset={spriteObj}
alphaTest={0.01}
asSprite={false}
/>

<SpriteAnimator
position={[5.5, 0.5, 5.8]}
autoPlay={true}
loop={true}
scale={5}
frameName={'Idle Blinking'}
animationNames={['Idle Blinking']}
spriteDataset={spriteObj}
alphaTest={0.01}
asSprite={false}
/>
```


# Performance

#### Instances
Expand Down
156 changes: 33 additions & 123 deletions src/core/SpriteAnimator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import * as React from 'react'
import { useFrame, Vector3 } from '@react-three/fiber'
import * as THREE from 'three'
import { Instances, Instance } from './Instances'
import { useSpriteLoader } from './useSpriteLoader'

export type SpriteAnimatorProps = {
startFrame?: number
endFrame?: number
fps?: number
frameName?: string
textureDataURL?: string
textureImageURL: string
textureImageURL?: string
loop?: boolean
numberOfFrames?: number
autoPlay?: boolean
Expand All @@ -29,6 +30,7 @@ export type SpriteAnimatorProps = {
resetOnEnd?: boolean
maxItems?: number
instanceItems?: any[]
spriteDataset?: any
} & JSX.IntrinsicElements['group']

type SpriteAnimatorState = {
Expand Down Expand Up @@ -74,17 +76,16 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
resetOnEnd,
maxItems,
instanceItems,
spriteDataset,
...props
},
fref
) => {
const ref = React.useRef<any>()
const spriteData = React.useRef<any>(null)
//const hasEnded = React.useRef(false)
const matRef = React.useRef<any>()
const spriteRef = React.useRef<any>()
const timerOffset = React.useRef(window.performance.now())
const textureData = React.useRef<any>()
const currentFrame = React.useRef<number>(startFrame || 0)
const currentFrameName = React.useRef<string>(frameName || '')
const fpsInterval = 1000 / (fps || 30)
Expand All @@ -97,6 +98,7 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
const pos = React.useRef(offset)
const softEnd = React.useRef(false)
const frameBuffer = React.useRef<any[]>([])
const { spriteObj, loadJsonAndTexture } = useSpriteLoader(null, null, animationNames, numberOfFrames)
//

function reset() {}
Expand All @@ -110,7 +112,7 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
hasEnded: false,
ref: fref,
}),
[textureImageURL]
[textureImageURL, spriteDataset]
)

React.useImperativeHandle(fref, () => ref.current, [])
Expand All @@ -119,22 +121,6 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
pos.current = offset
}, [offset])

function loadJsonAndTextureAndExecuteCallback(
jsonUrl: string,
textureUrl: string,
callback: (json: any, texture: THREE.Texture) => void
): void {
const textureLoader = new THREE.TextureLoader()
const jsonPromise = fetch(jsonUrl).then((response) => response.json())
const texturePromise = new Promise<THREE.Texture>((resolve) => {
textureLoader.load(textureUrl, resolve)
})

Promise.all([jsonPromise, texturePromise]).then((response) => {
callback(response[0], response[1])
})
}

const calculateAspectRatio = (width: number, height: number): Vector3 => {
const aspectRatio = height / width
if (spriteRef.current) {
Expand All @@ -145,18 +131,18 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea

// initial loads
React.useEffect(() => {
if (textureDataURL && textureImageURL) {
loadJsonAndTextureAndExecuteCallback(textureDataURL, textureImageURL, parseSpriteData)
} else if (textureImageURL) {
// only load the texture, this is an image sprite only
const textureLoader = new THREE.TextureLoader()
new Promise<THREE.Texture>((resolve) => {
textureLoader.load(textureImageURL, resolve)
}).then((texture) => {
parseSpriteData(null, texture)
})
if (spriteDataset) {
parseSpriteDataLite(spriteDataset?.spriteTexture?.clone(), spriteDataset.spriteData)
} else {
loadJsonAndTexture(textureImageURL, textureDataURL)
}
}, [spriteDataset])

React.useEffect(() => {
if (spriteObj) {
parseSpriteDataLite(spriteObj?.spriteTexture?.clone(), spriteObj?.spriteData)
}
}, [])
}, [spriteObj])

React.useEffect(() => {
setDisplayAsSprite(asSprite ?? true)
Expand Down Expand Up @@ -196,17 +182,14 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
}
}, [frameName])

// parse sprite-data from JSON file (jsonHash or jsonArray)
const parseSpriteData = (json: any, _spriteTexture: THREE.Texture): void => {
// sprite only case
if (json === null) {
// lite version for pre-loaded assets
const parseSpriteDataLite = (textureData: THREE.Texture, frameData: any = null) => {
if (frameData === null) {
if (numberOfFrames) {
//get size from texture
const width = _spriteTexture.image.width
const height = _spriteTexture.image.height
const frameWidth = width / numberOfFrames
const frameHeight = height
textureData.current = _spriteTexture
const width = textureData.image.width
const height = textureData.image.height

totalFrames.current = numberOfFrames

if (playBackwards) {
Expand All @@ -222,35 +205,22 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
},
}

if (parseInt(frameWidth.toString(), 10) === frameWidth) {
// if it fits
for (let i = 0; i < numberOfFrames; i++) {
spriteData.current.frames.push({
frame: { x: i * frameWidth, y: 0, w: frameWidth, h: frameHeight },
rotated: false,
trimmed: false,
spriteSourceSize: { x: 0, y: 0, w: frameWidth, h: frameHeight },
sourceSize: { w: frameWidth, h: height },
})
}
}
spriteData.current.frames = frameData
}
} else {
spriteData.current = json
spriteData.current.frames = Array.isArray(json.frames) ? json.frames : parseFrames()
totalFrames.current = Array.isArray(json.frames) ? json.frames.length : Object.keys(json.frames).length
textureData.current = _spriteTexture
spriteData.current = frameData
totalFrames.current = spriteData.current.frames.length

if (playBackwards) {
currentFrame.current = totalFrames.current - 1
}

const { w, h } = getFirstItem(json.frames).sourceSize
const { w, h } = getFirstItem(spriteData.current.frames).sourceSize
const aspect = calculateAspectRatio(w, h)

setAspect(aspect)
if (matRef.current) {
matRef.current.map = _spriteTexture
matRef.current.map = textureData
}
}

Expand All @@ -269,51 +239,7 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
}
}

_spriteTexture.premultiplyAlpha = false

setSpriteTexture(_spriteTexture)
}

// for frame based JSON Hash sprite data
const parseFrames = (): any => {
const sprites: any = {}
const data = spriteData.current
const delimiters = animationNames
if (delimiters) {
for (let i = 0; i < delimiters.length; i++) {
sprites[delimiters[i]] = []

for (const innerKey in data['frames']) {
const value = data['frames'][innerKey]
const frameData = value['frame']
const x = frameData['x']
const y = frameData['y']
const width = frameData['w']
const height = frameData['h']
const sourceWidth = value['sourceSize']['w']
const sourceHeight = value['sourceSize']['h']

if (innerKey.toLowerCase().indexOf(delimiters[i].toLowerCase()) !== -1) {
sprites[delimiters[i]].push({
x: x,
y: y,
w: width,
h: height,
frame: frameData,
sourceSize: { w: sourceWidth, h: sourceHeight },
})
}
}
}
return sprites
} else if (frameName) {
const spritesArr: any[] = []
for (const key in data.frames) {
spritesArr.push(data.frames[key])
}

return spritesArr
}
setSpriteTexture(textureData)
}

// modify the sprite material after json is parsed and state updated
Expand Down Expand Up @@ -348,6 +274,7 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
// run the animation on each frame
const runAnimation = (): void => {
//if (!frameName) return

const now = window.performance.now()
const diff = now - timerOffset.current
const {
Expand Down Expand Up @@ -383,10 +310,6 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
currentFrame: currentFrame.current,
})

if (!_offset) {
console.log('will end')
}

state.hasEnded = resetOnEnd ? false : true
if (resetOnEnd) {
pauseRef.current = true
Expand Down Expand Up @@ -515,6 +438,7 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
{displayAsSprite && (
<sprite ref={spriteRef} scale={aspect}>
<spriteMaterial
premultipliedAlpha={false}
toneMapped={false}
ref={matRef}
map={spriteTexture}
Expand All @@ -529,6 +453,7 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
>
<planeGeometry args={[1, 1]} />
<meshBasicMaterial
premultipliedAlpha={false}
toneMapped={false}
side={THREE.DoubleSide}
ref={matRef}
Expand All @@ -538,22 +463,7 @@ export const SpriteAnimator: React.FC<SpriteAnimatorProps> = /* @__PURE__ */ Rea
/>

{(instanceItems ?? [0]).map((item, index) => {
const texture = spriteTexture.clone()
if (matRef.current && frameBuffer.current[index]) {
texture.offset.set(frameBuffer.current[index].offset.x, frameBuffer.current[index].offset.y) // Set the offset for this item
}

return (
<Instance key={index} ref={spriteRef} position={item} scale={aspect}>
<meshBasicMaterial
toneMapped={false}
side={THREE.DoubleSide}
map={texture}
transparent={true}
alphaTest={alphaTest ?? 0.0}
/>
</Instance>
)
return <Instance key={index} ref={spriteRef} position={item} scale={aspect}></Instance>
})}
</Instances>
)}
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export * from './useProgress'
export * from './useTexture'
export * from './useVideoTexture'
export * from './useFont'
export * from './useSpriteLoader'

// Misc
export * from './Stats'
Expand Down
Loading