react-native-filament
Version:
A real-time physically based 3D rendering engine for React Native
137 lines (119 loc) • 3.83 kB
text/typescript
import { type BufferSource, useBuffer } from './useBuffer'
import { FilamentAsset } from '../types/FilamentAsset'
import { useFilamentContext } from './useFilamentContext'
import { useDisposableResource } from './useDisposableResource'
import usePrevious from './usePrevious'
import { useWorkletEffect } from './useWorkletEffect'
import { AABB, Entity } from '../types'
import { useMemo } from 'react'
export interface UseModelConfigParams {
/**
* Whether source data of the model should be released after loading, or not.
* @default true
*/
shouldReleaseSourceData?: boolean
/**
* Whether the model should be added to the scene.
* @default true
*/
addToScene?: boolean
/**
* Number of instances to create.
* @default 1
*/
instanceCount?: number
}
/**
* The resulting filament model, or `'loading'` if not yet available.
*/
export type FilamentModel =
| {
state: 'loaded'
asset: FilamentAsset
boundingBox: AABB
/**
* The root entity of the model.
*/
rootEntity: Entity
}
| {
state: 'loading'
}
/**
* Loads a model from the given source.
*
*
* If you are passing in a `.glb` model or similar from your app's bundle using `require(..)`, make sure to add `glb` as an asset extension to `metro.config.js`!
* If you are passing in a `{ url: ... }`, make sure the URL points directly to a `.glb` model. This can either be a web URL (`http://..`/`https://..`), a local file (`file://..`), or an native asset path (`path/to/asset.glb`)
*
* @worklet
* @example
* ```ts
* const model = useModel(require('model.glb'))
* ```
*/
export function useModel(source: BufferSource, props?: UseModelConfigParams): FilamentModel {
const { shouldReleaseSourceData = true, addToScene = true, instanceCount } = props ?? {}
const { engine, scene, workletContext } = useFilamentContext()
const assetBuffer = useBuffer({ source: source, releaseOnUnmount: false })
// Note: the native cleanup of the asset will remove it automatically from the scene
const asset = useDisposableResource(() => {
if (assetBuffer == null) return
if (instanceCount === 0) {
throw new Error('instanceCount must be greater than 0')
}
return workletContext.runAsync(() => {
'worklet'
let loadedAsset: FilamentAsset
if (instanceCount == null || instanceCount === 1) {
loadedAsset = engine.loadAsset(assetBuffer)
} else {
loadedAsset = engine.loadInstancedAsset(assetBuffer, instanceCount)
}
// After loading the asset we can release the buffer
assetBuffer.release()
return loadedAsset
})
}, [assetBuffer, workletContext, engine, instanceCount])
useWorkletEffect(() => {
'worklet'
if (asset == null || !shouldReleaseSourceData) {
return
}
// releases CPU memory for bindings
asset.releaseSourceData()
})
// Add or remove from the scene:
const previousAddToScene = usePrevious(addToScene)
useWorkletEffect(() => {
'worklet'
if (asset == null) return
if (addToScene) {
scene.addAssetEntities(asset)
} else if (!addToScene && previousAddToScene) {
// Only remove when it was previously added (ie. the user set addToScene: false)
scene.removeAssetEntities(asset)
}
})
const boundingBox = useMemo(() => {
if (asset == null) return undefined
return asset.getBoundingBox()
}, [asset])
const rootEntity = useMemo(() => {
if (asset == null) {
return null
}
return asset.getRoot()
}, [asset])
if (assetBuffer == null || asset == null || boundingBox == null || rootEntity == null) {
return {
state: 'loading',
}
}
return {
state: 'loaded',
asset: asset,
rootEntity: rootEntity,
boundingBox: boundingBox,
}
}