UNPKG

@react-three/drei

Version:

useful add-ons for react-three-fiber

314 lines (304 loc) 11.4 kB
import _extends from '@babel/runtime/helpers/esm/extends'; import * as THREE from 'three'; import * as React from 'react'; import { extend, useFrame } from '@react-three/fiber'; import { setUpdateRange } from '../helpers/deprecated.js'; function isFunctionChild(value) { return typeof value === 'function'; } const _instanceLocalMatrix = /* @__PURE__ */new THREE.Matrix4(); const _instanceWorldMatrix = /* @__PURE__ */new THREE.Matrix4(); const _instanceIntersects = []; const _mesh = /* @__PURE__ */new THREE.Mesh(); class PositionMesh extends THREE.Group { constructor() { super(); this.color = new THREE.Color('white'); this.instance = { current: undefined }; this.instanceKey = { current: undefined }; } // This will allow the virtual instance have bounds get geometry() { var _this$instance$curren; return (_this$instance$curren = this.instance.current) == null ? void 0 : _this$instance$curren.geometry; } // And this will allow the virtual instance to receive events raycast(raycaster, intersects) { const parent = this.instance.current; if (!parent) return; if (!parent.geometry || !parent.material) return; _mesh.geometry = parent.geometry; const matrixWorld = parent.matrixWorld; const instanceId = parent.userData.instances.indexOf(this.instanceKey); // If the instance wasn't found or exceeds the parents draw range, bail out if (instanceId === -1 || instanceId > parent.count) return; // calculate the world matrix for each instance parent.getMatrixAt(instanceId, _instanceLocalMatrix); _instanceWorldMatrix.multiplyMatrices(matrixWorld, _instanceLocalMatrix); // the mesh represents this single instance _mesh.matrixWorld = _instanceWorldMatrix; // raycast side according to instance material if (parent.material instanceof THREE.Material) _mesh.material.side = parent.material.side;else _mesh.material.side = parent.material[0].side; _mesh.raycast(raycaster, _instanceIntersects); // process the result of raycast for (let i = 0, l = _instanceIntersects.length; i < l; i++) { const intersect = _instanceIntersects[i]; intersect.instanceId = instanceId; intersect.object = this; intersects.push(intersect); } _instanceIntersects.length = 0; } } const globalContext = /* @__PURE__ */React.createContext(null); const parentMatrix = /* @__PURE__ */new THREE.Matrix4(); const instanceMatrix = /* @__PURE__ */new THREE.Matrix4(); const tempMatrix = /* @__PURE__ */new THREE.Matrix4(); const translation = /* @__PURE__ */new THREE.Vector3(); const rotation = /* @__PURE__ */new THREE.Quaternion(); const scale = /* @__PURE__ */new THREE.Vector3(); const isInstancedBufferAttribute = attr => attr.isInstancedBufferAttribute; const Instance = /* @__PURE__ */React.forwardRef(({ context, children, ...props }, ref) => { React.useMemo(() => extend({ PositionMesh }), []); const group = React.useRef(null); React.useImperativeHandle(ref, () => group.current, []); const { subscribe, getParent } = React.useContext(context || globalContext); React.useLayoutEffect(() => subscribe(group), []); return /*#__PURE__*/React.createElement("positionMesh", _extends({ instance: getParent(), instanceKey: group, ref: group }, props), children); }); const Instances = /* @__PURE__ */React.forwardRef(({ context, children, range, limit = 1000, frames = Infinity, ...props }, ref) => { const [{ localContext, instance }] = React.useState(() => { const localContext = /*#__PURE__*/React.createContext(null); return { localContext, instance: /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.createElement(Instance, _extends({ context: localContext }, props, { ref: ref }))) }; }); const parentRef = React.useRef(null); React.useImperativeHandle(ref, () => parentRef.current, []); const [instances, setInstances] = React.useState([]); const [[matrices, colors]] = React.useState(() => { const mArray = new Float32Array(limit * 16); for (let i = 0; i < limit; i++) tempMatrix.identity().toArray(mArray, i * 16); return [mArray, new Float32Array([...new Array(limit * 3)].map(() => 1))]; }); React.useEffect(() => { // We might be a frame too late? 🤷‍♂️ parentRef.current.instanceMatrix.needsUpdate = true; }); let iterations = 0; let count = 0; const attributes = React.useRef([]); React.useLayoutEffect(() => { attributes.current = Object.entries(parentRef.current.geometry.attributes).filter(([_name, value]) => isInstancedBufferAttribute(value)); }); useFrame(() => { if (frames === Infinity || iterations < frames) { parentRef.current.updateMatrix(); parentRef.current.updateMatrixWorld(); parentMatrix.copy(parentRef.current.matrixWorld).invert(); count = Math.min(limit, range !== undefined ? range : limit, instances.length); parentRef.current.count = count; setUpdateRange(parentRef.current.instanceMatrix, { start: 0, count: count * 16 }); setUpdateRange(parentRef.current.instanceColor, { start: 0, count: count * 3 }); for (let i = 0; i < instances.length; i++) { const instance = instances[i].current; // Multiply the inverse of the InstancedMesh world matrix or else // Instances will be double-transformed if <Instances> isn't at identity instance.matrixWorld.decompose(translation, rotation, scale); instanceMatrix.compose(translation, rotation, scale).premultiply(parentMatrix); instanceMatrix.toArray(matrices, i * 16); parentRef.current.instanceMatrix.needsUpdate = true; instance.color.toArray(colors, i * 3); parentRef.current.instanceColor.needsUpdate = true; } iterations++; } }); const api = React.useMemo(() => ({ getParent: () => parentRef, subscribe: ref => { setInstances(instances => [...instances, ref]); return () => setInstances(instances => instances.filter(item => item.current !== ref.current)); } }), []); return /*#__PURE__*/React.createElement("instancedMesh", _extends({ userData: { instances, limit, frames }, matrixAutoUpdate: false, ref: parentRef, args: [null, null, 0], raycast: () => null }, props), /*#__PURE__*/React.createElement("instancedBufferAttribute", { attach: "instanceMatrix", args: [matrices, 16], usage: THREE.DynamicDrawUsage }), /*#__PURE__*/React.createElement("instancedBufferAttribute", { attach: "instanceColor", args: [colors, 3], usage: THREE.DynamicDrawUsage }), isFunctionChild(children) ? /*#__PURE__*/React.createElement(localContext.Provider, { value: api }, children(instance)) : context ? /*#__PURE__*/React.createElement(context.Provider, { value: api }, children) : /*#__PURE__*/React.createElement(globalContext.Provider, { value: api }, children)); }); // TODO: make this non-recursive and type-safe const Merged = /* @__PURE__ */React.forwardRef(function Merged({ meshes, children, ...props }, ref) { const isArray = Array.isArray(meshes); // Filter out meshes from collections, which may contain non-meshes // @ts-expect-error if (!isArray) for (const key of Object.keys(meshes)) if (!meshes[key].isMesh) delete meshes[key]; const render = args => isArray ? // @ts-expect-error children(...args) : children( // @ts-expect-error Object.keys(meshes) // @ts-expect-error .filter(key => meshes[key].isMesh).reduce((acc, key, i) => ({ ...acc, [key]: args[i] }), {})); // @ts-expect-error const components = (isArray ? meshes : Object.values(meshes)).map(({ geometry, material }) => /*#__PURE__*/React.createElement(Instances, _extends({ key: geometry.uuid, geometry: geometry, material: material }, props))); return /*#__PURE__*/React.createElement("group", { ref: ref }, renderRecursive(render, components)); }); // https://github.com/jamesplease/react-composer function renderRecursive(render, components, results = []) { // Once components is exhausted, we can render out the results array. if (!components[0]) { return render(results); } // Continue recursion for remaining items. // results.concat([value]) ensures [...results, value] instead of [...results, ...value] function nextRender(value) { return renderRecursive(render, components.slice(1), results.concat([value])); } // Each props.components entry is either an element or function [element factory] return typeof components[0] === 'function' ? // When it is a function, produce an element by invoking it with "render component values". components[0]({ results, render: nextRender }) : /*#__PURE__*/ // When it is an element, enhance the element's props with the render prop. React.cloneElement(components[0], { children: nextRender }); } /** Idea and implementation for global instances and instanced attributes by /* Matias Gonzalez Fernandez https://x.com/matiNotFound /* and Paul Henschel https://x.com/0xca0a */ function createInstances() { const context = /*#__PURE__*/React.createContext(null); return [/*#__PURE__*/React.forwardRef((props, fref) => /*#__PURE__*/React.createElement(Instances, _extends({ ref: fref, context: context }, props))), /*#__PURE__*/React.forwardRef((props, fref) => /*#__PURE__*/React.createElement(Instance, _extends({ ref: fref, context: context }, props)))]; } const InstancedAttribute = /*#__PURE__*/React.forwardRef(({ name, defaultValue, normalized, usage = THREE.DynamicDrawUsage }, fref) => { const ref = React.useRef(null); React.useImperativeHandle(fref, () => ref.current, []); React.useLayoutEffect(() => { const parent = ref.current.__r3f.parent.object; parent.geometry.attributes[name] = ref.current; 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; // @ts-expect-error ref.current.count = array.length / ref.current.itemSize; return () => { delete parent.geometry.attributes[name]; }; }, [name]); let iterations = 0; useFrame(() => { const parent = ref.current.__r3f.parent.object; 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++; } }); // @ts-expect-error we're abusing three API here by mutating immutable args return /*#__PURE__*/React.createElement("instancedBufferAttribute", { ref: ref, usage: usage, normalized: normalized }); }); export { Instance, InstancedAttribute, Instances, Merged, PositionMesh, createInstances };