diff --git a/README.md b/README.md index dfcae1a00..cb29be2f3 100644 --- a/README.md +++ b/README.md @@ -3473,6 +3473,31 @@ function App() { } ``` +If your custom materials need instanced attributes you can create them using the `InstancedAttribute` component. It will automatically create the buffer and update it when the component changes. The `defaultValue` can have any stride, from single floats to arrays. + +```jsx + + + + + + +``` + +```glsl +# vertex +attribute float foo; +varying float vFoo; +void main() { + ... + vFoo = foo; + +# fragment +varying float vFoo; +void main() { + ... +``` + 👉 Note: While creating instances declaratively keeps all the power of components with reduced draw calls, it comes at the cost of CPU overhead. For cases like foliage where you want no CPU overhead with thousands of intances you should use THREE.InstancedMesh such as in this [example](https://codesandbox.io/s/grass-shader-5xho4?file=/src/Grass.js). #### Merged diff --git a/src/core/Instances.tsx b/src/core/Instances.tsx index 82c5fb674..bf2df5617 100644 --- a/src/core/Instances.tsx +++ b/src/core/Instances.tsx @@ -29,6 +29,11 @@ export type InstanceProps = JSX.IntrinsicElements['positionMesh'] & { context?: React.Context } +export type InstancedAttributeProps = JSX.IntrinsicElements['instancedBufferAttribute'] & { + name: string + defaultValue: any +} + type InstancedMesh = Omit & { instanceMatrix: THREE.InstancedBufferAttribute instanceColor: THREE.InstancedBufferAttribute @@ -101,6 +106,9 @@ const translation = /* @__PURE__ */ new THREE.Vector3() const rotation = /* @__PURE__ */ new THREE.Quaternion() const scale = /* @__PURE__ */ new THREE.Vector3() +const isInstancedBufferAttribute = (attr: any): attr is THREE.InstancedBufferAttribute => + attr.isInstancedBufferAttribute + export const Instance = /* @__PURE__ */ React.forwardRef(({ context, children, ...props }: InstanceProps, ref) => { React.useMemo(() => extend({ PositionMesh }), []) const group = React.useRef() @@ -144,6 +152,14 @@ export const Instances: ForwardRefComponent let iterations = 0 let count = 0 + + const attributes = React.useRef<[string, THREE.InstancedBufferAttribute][]>([]) + React.useLayoutEffect(() => { + attributes.current = Object.entries(parentRef.current.geometry.attributes).filter(([name, value]) => + isInstancedBufferAttribute(value) + ) as [string, THREE.InstancedBufferAttribute][] + }) + useFrame(() => { if (frames === Infinity || iterations < frames) { parentRef.current.updateMatrix() @@ -183,7 +199,7 @@ export const Instances: ForwardRefComponent return ( { + const ref = React.useRef(null!) + React.useImperativeHandle(fref, () => ref.current, []) + React.useLayoutEffect(() => { + const parent = (ref.current as any).__r3f.parent + const value = Array.isArray(defaultValue) ? defaultValue : [defaultValue] + const array = Array.from({ length: parent.userData.limit }, () => value).flat() + ref.current.array = new Float32Array(array) + ref.current.itemSize = value.length + ref.current.count = array.length / ref.current.itemSize + }, [name]) + let iterations = 0 + useFrame(() => { + const parent = (ref.current as any).__r3f.parent + if (parent.userData.frames === Infinity || iterations < parent.userData.frames) { + for (let i = 0; i < parent.userData.instances.length; i++) { + const instance = parent.userData.instances[i].current + const value = instance[name] + if (value !== undefined) { + ref.current.set( + Array.isArray(value) ? value : typeof value.toArray === 'function' ? value.toArray() : [value], + i * ref.current.itemSize + ) + ref.current.needsUpdate = true + } + } + iterations++ + } + }) + return +})