Skip to content

Commit

Permalink
Partial uploads (#138)
Browse files Browse the repository at this point in the history
* Change signature

* In component, too

* Cool

* Eh

* Implement partial uploads

* Cleanup

* Tweak

* changeset

* Hmpf

* Do cursor handling and partial uploads in onBeforeRender

* Cleanup

* Okay!

* Bursts

* Don't use Timeline Composer for a moment

* Cleanup

* Hrm

* Simplify

* Cleanup

* I hate everything

* Meh

* ts-ignore

* Use Repeat from Timeline Composer

* Upgrade Timeline Composer

* Upgrade dev dependencies

* Disable partical uploads for now

* Only upload our own attributes

* Make partial uploads safer

* Upgrade TC

* delay test

* wrap in repeat

* times

* Use timeline components from 3vfx for now

* Remove debug code

* Simplifty
  • Loading branch information
hmans committed Aug 2, 2022
1 parent 07ebc9e commit c4ef849
Show file tree
Hide file tree
Showing 16 changed files with 300 additions and 143 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-pumpkins-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vfx-composer": minor
---

**Added:** Partial attribute buffer uploads! Now only the parts of the buffers that have been used for newly spawned particles are actually uploaded to the GPU.
2 changes: 1 addition & 1 deletion apps/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"three": "^0.143.0",
"three-custom-shader-material": "^4.0.0",
"three-vfx": "^0.2.0",
"timeline-composer": "^0.1.1",
"timeline-composer": "^0.1.3",
"vfx-composer": "^0.1.0",
"wouter": "^2.8.0-alpha.2"
},
Expand Down
6 changes: 3 additions & 3 deletions apps/examples/src/Game.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { button, useControls } from "leva"
import { Perf } from "r3f-perf"
import { R3FStage } from "r3f-stage"
import { FC, Suspense, useState } from "react"
import { Repeat } from "three-vfx"
import { Repeat } from "timeline-composer"
import { Route, useRoute } from "wouter"
import examples, { ExampleDefinition } from "./examples"
import { Perf } from "r3f-perf"

import "r3f-stage/styles.css"

Expand Down Expand Up @@ -43,7 +43,7 @@ const Example: FC<{ example: ExampleDefinition }> = ({ example }) => {
})

return (
<Repeat key={v} times={loop ? Infinity : 0} interval={interval}>
<Repeat key={v} times={loop ? Infinity : 0} seconds={interval}>
{example.component}
</Repeat>
)
Expand Down
64 changes: 33 additions & 31 deletions apps/examples/src/examples/Simple.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { between, plusMinus, upTo } from "randomish"
import { between, plusMinus, random, upTo } from "randomish"
import { useState } from "react"
import { OneMinus, Time } from "shader-composer"
import { Color, MeshStandardMaterial, Vector2, Vector3 } from "three"
import { Delay, Repeat } from "three-vfx"
import { makeParticles, VFX, VFXMaterial } from "vfx-composer/fiber"
import { Lifetime } from "vfx-composer/modules"
import { ParticleAttribute } from "vfx-composer/units"

const Effect = makeParticles()

const FREQ = 8

export const Simple = () => {
const [variables] = useState(() => ({
time: Time(),
Expand All @@ -16,45 +19,44 @@ export const Simple = () => {
color: ParticleAttribute(new Color())
}))

const lifetime = Lifetime(variables.lifetime, variables.time)
const { ParticleProgress, ParticleAge, module: lifetimeModule } = Lifetime(
variables.lifetime,
variables.time
)

return (
<group>
<Effect.Root maxParticles={1000}>
<boxGeometry />
<Effect.Root maxParticles={1_000_000} safetyBuffer={1_000}>
<planeGeometry />

<VFXMaterial baseMaterial={MeshStandardMaterial} color="hotpink">
<VFX.Scale scale={OneMinus(lifetime.ParticleProgress)} />
<VFX.Velocity
velocity={variables.velocity}
time={lifetime.ParticleAge}
/>
<VFX.Acceleration
force={new Vector3(0, -10, 0)}
time={lifetime.ParticleAge}
/>
<VFX.Scale scale={OneMinus(ParticleProgress)} />
<VFX.Velocity velocity={variables.velocity} time={ParticleAge} />
<VFX.Acceleration force={new Vector3(0, -10, 0)} time={ParticleAge} />
<VFX.SetColor color={variables.color} />
<VFX.Module module={lifetime.module} />
<VFX.Module module={lifetimeModule} />
</VFXMaterial>
</Effect.Root>

<Effect.Emitter
count={1}
continuous
setup={({ position, rotation }) => {
const t = variables.time.uniform.value
const { lifetime, velocity, color } = variables

/* Randomize the instance transform */
position.randomDirection().multiplyScalar(upTo(6))
rotation.random()

/* Write values into the instanced attributes */
lifetime.value.set(t, t + between(1, 2))
velocity.value.set(plusMinus(5), between(5, 18), plusMinus(5))
color.value.setRGB(Math.random(), Math.random(), Math.random())
}}
/>
<Repeat interval={1 / FREQ}>
<Effect.Emitter
count={100_000 / FREQ}
setup={({ position, rotation }) => {
const t = variables.time.uniform.value
const { lifetime, velocity, color } = variables

/* Randomize the instance transform */
position.randomDirection().multiplyScalar(upTo(6))
rotation.random()

/* Write values into the instanced attributes */
const start = t + random() / FREQ
lifetime.value.set(start, start + between(1, 3))
velocity.value.set(plusMinus(5), between(5, 18), plusMinus(5))
color.value.setRGB(Math.random(), Math.random(), Math.random())
}}
/>
</Repeat>
</group>
)
}
3 changes: 3 additions & 0 deletions apps/examples/src/examples/Vanilla.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,16 @@ const vanillaCode = (parent: Object3D) => {

return () => {
stopLoop()

parent.remove(particles)
parent.remove(particles2)

particles.geometry.dispose()
particles.dispose()

particles2.geometry.dispose()
particles2.dispose()

material.dispose()
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/r3f-stage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"docs": "typedoc"
},
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "^7.17.12",
"@react-three/fiber": "^8.0.22",
Expand All @@ -48,7 +48,7 @@
"three": "^0.143.0",
"ts-jest": "^28.0.4",
"tslib": "^2.4.0",
"typedoc": "^0.23.4",
"typedoc": "^0.23.10",
"typescript": "^4.7.3"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/vfx-composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
"docs": "typedoc src/index.ts"
},
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "^7.17.12",
"@react-three/fiber": "^8.0.22",
Expand Down
72 changes: 57 additions & 15 deletions packages/vfx-composer/src/Particles.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { collectFromTree } from "shader-composer"
import {
BufferGeometry, InstancedMesh,
BufferAttribute,
BufferGeometry,
InstancedMesh,
Matrix4,
Quaternion,
Vector3
} from "three"
import { VFXMaterial } from "./VFXMaterial"
import { ParticleAttribute } from "./units"
import { VFXMaterial } from "./VFXMaterial"

export type InstanceSetupCallback = (config: {
index: number
Expand All @@ -21,29 +23,69 @@ const tmpRotation = new Quaternion()
const tmpScale = new Vector3(1, 1, 1)
const tmpMatrix = new Matrix4()

export class Particles extends InstancedMesh<
BufferGeometry,
VFXMaterial
> {
export class Particles extends InstancedMesh<BufferGeometry, VFXMaterial> {
public cursor: number = 0
public maxParticles: number
public safetyBuffer: number

private attributeUnits: ParticleAttribute[] = []
private lastCursor = 0

constructor(
geometry: BufferGeometry | undefined,
material: VFXMaterial | undefined,
count: number,
safetyBuffer: number = 100
) {
super(geometry, material, count + safetyBuffer)
this.maxParticles = count
this.safetyBuffer = safetyBuffer

this.onBeforeRender = () => {
const emitted = this.cursor - this.lastCursor

if (emitted > 0) {
/* Mark all attribute ranges that need to be uploaded to the GPU this frame. */
const userAttributes = this.attributeUnits.map(
(unit) => this.geometry.attributes[unit.name]
)

constructor(...args: ConstructorParameters<typeof InstancedMesh<BufferGeometry, VFXMaterial>>) {
super(...args)
const allAttributes = [this.instanceMatrix, ...userAttributes]

allAttributes.forEach((attribute) => {
attribute.needsUpdate = true
if (attribute instanceof BufferAttribute) {
attribute.updateRange.offset = this.lastCursor * attribute.itemSize
attribute.updateRange.count = emitted * attribute.itemSize
}
})

/* If we've gone past the safe limit, go back to the beginning. */
if (this.cursor >= this.maxParticles) {
this.cursor = 0
}
}

this.lastCursor = this.cursor
}
}

public setupParticles() {
/* TODO: hopefully this can live in SC at some point. https://github.com/hmans/shader-composer/issues/60 */
if (this.material.shaderRoot) {
this.attributeUnits = collectFromTree(this.material.shaderRoot, (item) => item.setupMesh)
this.attributeUnits = collectFromTree(
this.material.shaderRoot,
(item) => item.setupMesh
)

for (const unit of this.attributeUnits) {
for (const unit of this.attributeUnits) {
unit.setupMesh(this)
}
}
}

public emit(count: number = 1, setupInstance?: InstanceSetupCallback) {
/* Emit the requested number of particles. */
for (let i = 0; i < count; i++) {
/* Reset instance configuration values */
tmpPosition.set(0, 0, 0)
Expand All @@ -58,19 +100,19 @@ export class Particles extends InstancedMesh<
scale: tmpScale
})

tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale)

/* Store and upload matrix */
this.setMatrixAt(this.cursor, tmpMatrix)
this.instanceMatrix.needsUpdate = true
this.setMatrixAt(
this.cursor,
tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale)
)

/* Write all known attributes */
for (const unit of this.attributeUnits) {
unit.setupParticle(this)
}

/* Advance cursor */
this.cursor = (this.cursor + 1) % this.count
this.cursor++
}
}
}
7 changes: 3 additions & 4 deletions packages/vfx-composer/src/fiber/Emitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ export const Emitter = forwardRef<Object3D, EmitterProps>(
useEffect(() => {
if (continuous) return
particles.current?.emit(count, setup)
}, [])
}, [particles])

useFrame(() => {
if (continuous) {
particles.current?.emit(count, setup)
}
if (!continuous) return
particles.current?.emit(count, setup)
})

useImperativeHandle(ref, () => object.current)
Expand Down
53 changes: 32 additions & 21 deletions packages/vfx-composer/src/fiber/Particles.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
import { InstancedMeshProps } from "@react-three/fiber"
import { extend, InstancedMeshProps, Node } from "@react-three/fiber"
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useState
useRef
} from "react"
import { Particles as ParticlesImpl } from "../Particles"
import { VFXMaterial as ParticlesMaterialImpl } from "../VFXMaterial"
import { VFXMaterial as VFXMaterialImpl } from "../VFXMaterial"

export type ParticlesProps = InstancedMeshProps & {
material?: ParticlesMaterialImpl
export type ParticlesProps = Omit<InstancedMeshProps, "material" | "args"> & {
args?: ConstructorParameters<typeof ParticlesImpl>
material?: VFXMaterialImpl
maxParticles?: number
safetyBuffer?: number
}

export const Particles = forwardRef<ParticlesImpl, ParticlesProps>(
({ maxParticles = 1000, geometry, material, ...props }, ref) => {
/* We're using useState because it gives better guarantees than useMemo. */
const [particles, setParticles] = useState(
() => new ParticlesImpl(geometry, material, maxParticles)
)
extend({ VfxComposerParticles: ParticlesImpl })

/* We still want to update the particles when the props change. */
useEffect(() => {
setParticles(new ParticlesImpl(geometry, material, maxParticles))
}, [geometry, material, maxParticles])
declare global {
namespace JSX {
interface IntrinsicElements {
vfxComposerParticles: ParticlesProps
}
}
}

export const Particles = forwardRef<ParticlesImpl, ParticlesProps>(
(
{ maxParticles = 1000, safetyBuffer = 100, geometry, material, ...props },
ref
) => {
const particles = useRef<ParticlesImpl>(null!)

/* Setup particles in an effect (after materials and geometry are assigned) */
useEffect(() => {
particles.setupParticles()
return () => particles.dispose()
}, [particles])
particles.current.setupParticles()
}, [])

useImperativeHandle(ref, () => particles)
useImperativeHandle(ref, () => particles.current)

return <primitive object={particles} {...props} />
return (
<vfxComposerParticles
args={[geometry, material, maxParticles, safetyBuffer]}
ref={particles}
{...props}
/>
)
}
)
Loading

0 comments on commit c4ef849

Please sign in to comment.