Skip to content

Instantly share code, notes, and snippets.

@hmans
Last active January 12, 2021 11:41
Show Gist options
  • Save hmans/ea145d1c7c884fac174568acf7d0262a to your computer and use it in GitHub Desktop.
Save hmans/ea145d1c7c884fac174568acf7d0262a to your computer and use it in GitHub Desktop.
Managed Instanced Mesh API

Managed Instanced Mesh API

This is lifted from a work-in-progress library called Trinity, which is -- or was -- a remix of sorts of react-three-fiber. It should be relatively straight forward to port to react-three-fiber. It is provided here as-is.

Notably, it is designed to both support a declarative approach of defining instances, as well as doing it imperatively, which is nice for extreme cases where you want thousands (or tens of thousands, or more) instances, which would get React into trouble if each was a React element. :-)

Please note that I can't provide support for this (or even guarantee that it works), my Three.js-related work is now focusing on three-elements.

Have a good one! - @hmans

import { RefObject, useEffect, useRef } from "react"
import { ManagedInstancedMesh, Instance } from "./ManagedInstancedMesh"
type InstanceFactory = () => Pick<Instance, "object" | "color">
export const useInstance = (
imesh: ManagedInstancedMesh | RefObject<ManagedInstancedMesh>,
factory: InstanceFactory
) => {
const instanceRef = useRef<Instance>()
useEffect(() => {
const [instances, cleanup] = makeInstances(1, imesh, factory)
instanceRef.current = instances[0]
return cleanup
}, [])
return instanceRef
}
export const useInstances = (
count: number,
imesh: ManagedInstancedMesh | RefObject<ManagedInstancedMesh>,
factory: InstanceFactory
) => {
const instancesRef = useRef<Instance[]>([])
useEffect(() => {
const [instances, cleanup] = makeInstances(count, imesh, factory)
instancesRef.current = instances
return cleanup
}, [])
return instancesRef
}
const makeInstances = (
count: number,
imesh: ManagedInstancedMesh | RefObject<ManagedInstancedMesh>,
factory: InstanceFactory
): [Instance[], () => void] => {
const imeshActual = imesh instanceof ManagedInstancedMesh ? imesh : imesh.current!
const instances = new Array<Instance>()
for (let i = 0; i < count; i++) {
instances.push(imeshActual.addInstance(factory()))
}
const cleanup = () => {
for (let i = 0; i < count; i++) {
imeshActual.removeInstance(instances[i])
}
}
return [instances, cleanup]
}
import * as THREE from "three"
export type Instance = {
object: THREE.Object3D
color?: THREE.Color
index: number
data: any
}
/* Our ManagedInstancedMesh class that we can use with just plain old Three.js */
export class ManagedInstancedMesh extends THREE.InstancedMesh {
instances: Instance[] = []
addInstance({ object, color, data = {} }: Pick<Instance, "color" | "object" | "data">): Instance {
const instance: Instance = {
object,
color,
data,
index: this.instances.length
}
/* Store instance */
this.instances.push(instance)
/* Brute-force update instance so it definitely gets applied to our matrices */
this.updateInstance(instance, instance)
/* Update instance count */
this.count = this.instances.length
return instance
}
removeInstance(instance: Instance) {
/* Remove instance from our instance list */
this.instances = this.instances.filter((i) => i !== instance)
/* Regenerate indices */
this.instances.forEach((instance, index) => (instance.index = index))
}
updateInstance(instance: Instance, updates: Partial<Pick<Instance, "color" | "object">> = {}) {
/* Store new object if one was given */
if (updates.object !== undefined) instance.object = updates.object
/* Update the instance's matrix and store it. */
instance.object.updateMatrix()
this.setMatrixAt(instance.index, instance.object.matrix)
/* Apply new color if one was given */
if (updates.color !== undefined) {
instance.color = updates.color
this.setColorAt(instance.index, updates.color)
this.instanceColor!.needsUpdate = true
}
this.instanceMatrix.needsUpdate = true
}
}
import { Primitive, useManagedThreeObject } from "@hmans/trinity"
import { ThreeObjectWithOptionalEventHandlers, TrinityPointerEvent } from "@hmans/trinity"
import { forwardRefReactor } from "@hmans/trinity"
import { ManagedInstancedMesh } from "../ManagedInstancedMesh"
export const ManagedInstancedMeshComponent = forwardRefReactor<
ManagedInstancedMesh,
{ maxInstances: number }
>(({ children, geometry, material, maxInstances, ...props }, ref) => {
const imesh = useManagedThreeObject(
() => new ManagedInstancedMesh(geometry!, material!, maxInstances)
)
const forwardEventToInstance = (eventName: string) => (e: TrinityPointerEvent) => {
const instanceId = e.intersection.instanceId
if (!instanceId) return
const instance = imesh.instances[instanceId]
const object = instance?.object
const handlers = (object as ThreeObjectWithOptionalEventHandlers)?.__handlers
handlers && handlers[eventName]?.(e)
}
return (
<Primitive
ref={ref}
object={imesh}
onPointerUp={forwardEventToInstance("pointerup")}
onPointerDown={forwardEventToInstance("pointerdown")}
onPointerEnter={forwardEventToInstance("pointerenter")}
onPointerLeave={forwardEventToInstance("pointerleave")}
onPointerMove={forwardEventToInstance("pointermove")}
{...props}
>
{children}
</Primitive>
)
})
import T, { applyEventProps, useEngine, useOnUpdate } from "@hmans/trinity"
import React, { FC, RefObject, useEffect, useRef } from "react"
import * as THREE from "three"
import { ManagedInstancedMesh, useInstance, useInstances } from "../trinity-extras/instances"
import { ManagedInstancedMeshComponent } from "../trinity-extras/instances/trinity"
import { inside, insideUnitSphere, pick } from "../trinity-math/Random"
import { make } from "../wurlde/tools"
import { DemoSetup } from "./DemoSetup"
import { getSimplexVector } from "./simplex"
export const DodecahedronCloud = ({ rotationPerSecond = 0.5 }) => {
const { triggerFrame } = useEngine()
const imeshRef = useRef<ManagedInstancedMesh>(null)
/* Animate the whole InstancedMesh */
useOnUpdate((dt) => {
imeshRef.current!.rotateY(rotationPerSecond * dt)
triggerFrame()
})
return (
<ManagedInstancedMeshComponent ref={imeshRef} maxInstances={6000} castShadow receiveShadow>
<T.DodecahedronBufferGeometry />
<T.MeshStandardMaterial />
<InstanceSwarm
imeshRef={imeshRef}
count={2000}
offset={2000}
colors={[new THREE.Color("#111"), new THREE.Color("#c22")]}
/>
<InstanceSwarm
imeshRef={imeshRef}
count={2000}
offset={3000}
colors={[new THREE.Color("#eee"), new THREE.Color("#393")]}
/>
{make(2000, (i) => (
<SingleInstance key={i} imeshRef={imeshRef} />
))}
</ManagedInstancedMeshComponent>
)
}
const singleInstanceColors = [
new THREE.Color("#111"),
new THREE.Color("#333"),
new THREE.Color("#555")
]
const SingleInstance: FC<{ imeshRef: RefObject<ManagedInstancedMesh> }> = ({ imeshRef }) => {
/* Reference to a scene object we will be rendering below */
const ref = useRef<THREE.Object3D>(null)
/* Create an instance with a random color. The factory function will run as a side-effect
after React is done rendering, so we're safe to use our refs here. */
const instanceRef = useInstance(imeshRef, () => ({
object: ref.current!,
color: pick(singleInstanceColors)
}))
/* Animate the instance on every frame */
useOnUpdate((dt) => {
const imesh = imeshRef.current!
const instance = instanceRef.current!
const object = ref.current!
/* Note we're just modifying the facade object directly. */
object.rotation.x = object.rotation.z += dt
/* We do have to tell the managed imesh that this instance has updated, though. */
imesh.updateInstance(instance)
})
/* Here's our scene object that acts as a facade for the actual instance. Use it like
any other scene object. */
return (
<T.Object3D
ref={ref}
position={insideUnitSphere().multiplyScalar(50).toArray()}
scale={Math.pow(inside(0.3, 1), 5)}
/*
Pointer events can be created on the facade object like on any other scene object!
*/
onPointerEnter={(e) => {
const imesh = imeshRef.current!
const instance = instanceRef.current!
instance.data.oldColor = instance.color
imesh.updateInstance(instance, {
color: new THREE.Color("hotpink")
})
}}
onPointerLeave={(e) => {
const imesh = imeshRef.current!
const instance = instanceRef.current!
imesh.updateInstance(instance, {
color: instance.data.oldColor
})
}}
/>
)
}
/*
Here's an example of a component that imperatively creates and animates a whole bunch
of instances (instead of rendering a React component for each, which would become problematic
as soon as you want to render tens of thousands of them.)
*/
const InstanceSwarm: FC<{
count?: number
offset?: number
imeshRef: RefObject<ManagedInstancedMesh>
colors?: THREE.Color[]
}> = ({
count = 100,
offset = 0,
colors = [new THREE.Color("#222"), new THREE.Color("#c22")],
imeshRef
}) => {
const instancesRef = useInstances(count, imeshRef, () => {
const object = new THREE.Object3D()
object.position.copy(insideUnitSphere().multiplyScalar(50))
object.scale.setScalar(inside(0.05, 0.3))
const color = pick(colors)
return { object, color }
})
/* Assign event handlers */
useEffect(() => {
const imesh = imeshRef.current!
for (const instance of instancesRef.current) {
applyEventProps(instance.object, {
onPointerEnter: () => {
instance.data.oldColor = instance.color
imesh.updateInstance(instance, {
color: new THREE.Color("hotpink")
})
},
onPointerLeave: () => {
imesh.updateInstance(instance, {
color: instance.data.oldColor
})
}
})
}
})
/* Animate instances */
let accTime = 0
const offsetPosition = new THREE.Vector3()
const rotation = new THREE.Vector3()
useOnUpdate((dt) => {
accTime += dt
const instances = instancesRef.current
const imesh = imeshRef.current!
/* Animate the instances */
for (let i = 0; i < instances.length; i++) {
const instance = instances[i]
const t = (accTime + i * 0.0001) * 0.2
/* Offset */
getSimplexVector(t * 0.5, offset + i * 200 + t, offsetPosition).multiplyScalar(
5 * Math.pow(Math.sin(i), 3)
)
/* Position */
getSimplexVector(t, offset, instance.object.position).multiplyScalar(10).add(offsetPosition)
/* Rotation */
getSimplexVector(t * 2, offset + i * 200 + t, rotation)
instance.object.rotation.set(rotation.x, rotation.y, rotation.z)
imesh.updateInstance(instance)
}
})
return null
}
export const InstancedMeshesDemo = () => (
<DemoSetup shadowMapSize={20}>
<DodecahedronCloud />
</DemoSetup>
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment