diff --git a/.changeset/weak-dots-check.md b/.changeset/weak-dots-check.md new file mode 100644 index 000000000..24b14a5a7 --- /dev/null +++ b/.changeset/weak-dots-check.md @@ -0,0 +1,36 @@ +--- +"three-vfx": minor +--- + +**New:** Soft Particles support! `` now has new `softness`, `softnessFunction` and `depthTexture` props. + +```tsx +export const SoftParticlesExample = () => { + const depthBuffer = useDepthBuffer() + + return ( + + + + + + + { + c.lifetime = Infinity + }} + /> + + + ) +} +``` diff --git a/apps/examples/package.json b/apps/examples/package.json index 043a38eb6..bc3b7aa81 100644 --- a/apps/examples/package.json +++ b/apps/examples/package.json @@ -11,6 +11,7 @@ "@react-three/drei": "^9.11.3", "@react-three/fiber": "^8.0.22", "leva": "^0.9.27", + "postprocessing": "^6.28.0", "r3f-perf": "^6.2.6", "react": "^18.1.0", "react-dom": "^18.1.0", diff --git a/apps/examples/src/Game.tsx b/apps/examples/src/Game.tsx index 11dfcd536..9c7d90bd8 100644 --- a/apps/examples/src/Game.tsx +++ b/apps/examples/src/Game.tsx @@ -2,17 +2,16 @@ import { OrbitControls, PerspectiveCamera } from "@react-three/drei" import { Canvas } from "@react-three/fiber" import { button, useControls } from "leva" import { Perf } from "r3f-perf" -import { FC, useState } from "react" +import { FC, Suspense, useState } from "react" import { LinearEncoding } from "three" import { Repeat } from "three-vfx" import { Route, useRoute } from "wouter" import examples, { ExampleDefinition } from "./examples" -import { RenderPipeline } from "./RenderPipeline" +import { Rendering } from "./Rendering" import { Stage } from "./Stage" export const Game = () => { - const { beautiful, halfResolution } = useControls("Rendering", { - beautiful: true, + const { halfResolution } = useControls("Rendering", { halfResolution: false }) @@ -20,12 +19,11 @@ export const Game = () => { { shadow-radius={10} shadow-bias={-0.0001} /> - + { {/* Scene objects */} - + + + {/* Rendering, ECS, etc. */} - + ) diff --git a/apps/examples/src/RenderPipeline.tsx b/apps/examples/src/RenderPipeline.tsx deleted file mode 100644 index ef718a047..000000000 --- a/apps/examples/src/RenderPipeline.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Effects } from "@react-three/drei" -import { extend } from "@react-three/fiber" -import { useControls } from "leva" -import { HalfFloatType, LinearEncoding, Vector2 } from "three" -import { AdaptiveToneMappingPass } from "three/examples/jsm/postprocessing/AdaptiveToneMappingPass.js" -import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js" -import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js" -import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass" -import { VignetteShader } from "three/examples/jsm/shaders/VignetteShader.js" -import { AdaptiveResolution } from "./AdaptiveResolution" - -extend({ UnrealBloomPass, AdaptiveToneMappingPass, ShaderPass, RenderPass }) - -type EffectsPassProps = { - args?: ConstructorParameters - enabled?: boolean -} - -declare global { - namespace JSX { - interface IntrinsicElements { - unrealBloomPass: EffectsPassProps - adaptiveToneMappingPass: EffectsPassProps - } - } -} - -export const RenderPipeline = ({ beautiful = true }) => { - return ( - - - {/* */} - - - ) -} diff --git a/apps/examples/src/Rendering.tsx b/apps/examples/src/Rendering.tsx new file mode 100644 index 000000000..3d76c6057 --- /dev/null +++ b/apps/examples/src/Rendering.tsx @@ -0,0 +1,60 @@ +import { useFrame, useThree } from "@react-three/fiber" +import { + AdaptiveLuminancePass, + BlendFunction, + BloomEffect, + EffectComposer, + EffectPass, + Pass, + RenderPass, + SelectiveBloomEffect, + ToneMappingEffect +} from "postprocessing" +import { useEffect, useLayoutEffect, useMemo } from "react" +import { HalfFloatType } from "three" + +const usePass = ( + composer: EffectComposer, + factory: () => Pass, + deps: any[] = [] +) => { + useLayoutEffect(() => { + const pass = factory() + composer.addPass(pass) + return () => composer.removePass(pass) + }, [composer, ...deps]) +} + +export const Rendering = () => { + const { gl, scene, camera } = useThree() + + const composer = useMemo( + () => new EffectComposer(gl, { frameBufferType: HalfFloatType }), + [] + ) + + usePass(composer, () => new RenderPass(scene, camera), [scene, camera]) + + const bloomEffect = useMemo(() => { + const effect = new SelectiveBloomEffect(scene, camera, { + blendFunction: BlendFunction.ADD, + mipmapBlur: true, + luminanceThreshold: 0.7, + luminanceSmoothing: 0.3, + intensity: 4 + } as any) + effect.inverted = true + return effect + }, [scene, camera]) + + usePass(composer, () => new EffectPass(camera, bloomEffect), [ + bloomEffect, + camera + ]) + + useFrame(() => { + composer.render() + }, 1) + + return null +} diff --git a/apps/examples/src/Stage.tsx b/apps/examples/src/Stage.tsx index c7b4f25c0..0debf89d4 100644 --- a/apps/examples/src/Stage.tsx +++ b/apps/examples/src/Stage.tsx @@ -16,11 +16,19 @@ export const Stage: FC = ({ children, speed = 0, ...props }) => { return ( - + {/* Floor */} + + + + + + {/* Upper pedestral */} + + {/* Lower pedestral */} diff --git a/apps/examples/src/examples/DustExample.tsx b/apps/examples/src/examples/DustExample.tsx new file mode 100644 index 000000000..e1b00e62c --- /dev/null +++ b/apps/examples/src/examples/DustExample.tsx @@ -0,0 +1,48 @@ +import { useTexture } from "@react-three/drei" +import { between, plusMinus, upTo } from "randomish" +import { AdditiveBlending, DoubleSide, MeshStandardMaterial } from "three" +import { + Emitter, + MeshParticles, + ParticlesMaterial, + Repeat, + SpawnSetup, + VisualEffect +} from "three-vfx" + +export const DustExample = ({ intensity = 300 }) => { + const texture = useTexture("/textures/particle.png") + + return ( + + + + + + + { + c.quaternion.random() + c.position.set(plusMinus(30), plusMinus(30), plusMinus(30)) + c.velocity.randomDirection().multiplyScalar(upTo(0.2)) + c.lifetime = Infinity + + const scale = between(0.1, 0.2) + c.scale[0].setScalar(scale) + c.scale[1].setScalar(scale) + c.alpha = [1, 1] + }} + /> + + + ) +} diff --git a/apps/examples/src/examples/Explosion.tsx b/apps/examples/src/examples/Explosion.tsx index c58aef7ba..ef0769b10 100644 --- a/apps/examples/src/examples/Explosion.tsx +++ b/apps/examples/src/examples/Explosion.tsx @@ -1,6 +1,7 @@ import { CameraShake, useTexture } from "@react-three/drei" import { between, plusMinus, power, upTo } from "randomish" -import { Color, MeshStandardMaterial, TextureLoader, Vector3 } from "three" +import { FC } from "react" +import { Color, DepthTexture, MeshStandardMaterial, Vector3 } from "three" import { Delay, Emitter, @@ -11,14 +12,20 @@ import { VisualEffect, VisualEffectProps } from "three-vfx" +import { useDepthBuffer } from "./lib/useDepthBuffer" const gravity = new Vector3(0, -20, 0) const direction = new Vector3() -const SmokeRing = () => ( +const SmokeRing: FC<{ depthTexture: DepthTexture }> = ({ depthTexture }) => ( - + ( const Fireball = () => ( - + 5 + power(3) * 10} @@ -98,8 +109,8 @@ const Fireball = () => ( c.lifetime = between(0.8, 1.4) c.color[0].lerpColors( - new Color("red").multiplyScalar(10), - new Color("yellow").multiplyScalar(10), + new Color("red").multiplyScalar(30), + new Color("yellow").multiplyScalar(50), power(3) ) c.color[1].copy(c.color[0]) @@ -108,7 +119,7 @@ const Fireball = () => ( ) -const SmokeCloud = () => ( +const SmokeCloud: FC<{ depthTexture: DepthTexture }> = ({ depthTexture }) => ( @@ -117,11 +128,12 @@ const SmokeCloud = () => ( map={useTexture("/textures/smoke.png")} depthWrite={false} billboard - scaleFunction="smoothstep(0.0, 1.0, sin(v_progress * PI))" + softness={3} + depthTexture={depthTexture} /> between(60, 80)} + count={between(30, 60)} setup={(c) => { direction.randomDirection() @@ -140,7 +152,7 @@ const SmokeCloud = () => ( c.scale[0].setScalar(between(0.5, 1.5)) c.scale[1].setScalar(between(6, 20)) - c.delay = upTo(0.3) + c.delay = upTo(0.1) c.lifetime = between(1, 3) c.alpha = [0.5, 0] @@ -152,33 +164,37 @@ const SmokeCloud = () => ( ) -export const Explosion = (props: VisualEffectProps) => ( - - - +export const Explosion = (props: VisualEffectProps) => { + const depthTexture = useDepthBuffer().depthTexture - + return ( + + + - - - - - - - + + + + + + + + + + - - - -) + + + ) +} diff --git a/apps/examples/src/examples/FireflyExample.tsx b/apps/examples/src/examples/FireflyExample.tsx index e55e45a62..5d8df9305 100644 --- a/apps/examples/src/examples/FireflyExample.tsx +++ b/apps/examples/src/examples/FireflyExample.tsx @@ -1,7 +1,7 @@ import { useFrame } from "@react-three/fiber" import { upTo } from "randomish" import { useRef } from "react" -import { Color, Mesh, MeshStandardMaterial } from "three" +import { Color, Mesh, MeshStandardMaterial, NormalBlending } from "three" import { Emitter, MeshParticles, @@ -29,6 +29,7 @@ export const FireflyExample = () => { { + const depthTexture = useDepthBuffer().depthTexture + const texture = useTexture("/textures/smoke.png") const setup = ({ preDelay = 0 } = {}): SpawnSetup => (c) => { - c.position.copy(insideSphere(20) as Vector3) + c.position.set(0, 6, 0).add(insideSphere(5) as Vector3) c.velocity.randomDirection().multiplyScalar(between(0, 1)) c.delay = upTo(5) - preDelay c.lifetime = 30 @@ -31,18 +34,20 @@ export const Fog = () => { - + - + between(5, 10)} setup={setup()} /> diff --git a/apps/examples/src/examples/Simple.tsx b/apps/examples/src/examples/Simple.tsx index aa4e80a0c..80cf7118f 100644 --- a/apps/examples/src/examples/Simple.tsx +++ b/apps/examples/src/examples/Simple.tsx @@ -1,6 +1,6 @@ import { useTexture } from "@react-three/drei" import { between, plusMinus, upTo } from "randomish" -import { MeshStandardMaterial } from "three" +import { MeshStandardMaterial, NormalBlending } from "three" import { Emitter, MeshParticles, @@ -19,10 +19,12 @@ export const Simple = () => { diff --git a/apps/examples/src/examples/SoftParticlesExample.tsx b/apps/examples/src/examples/SoftParticlesExample.tsx new file mode 100644 index 000000000..ac8767433 --- /dev/null +++ b/apps/examples/src/examples/SoftParticlesExample.tsx @@ -0,0 +1,37 @@ +import { MeshStandardMaterial } from "three" +import { + Emitter, + MeshParticles, + ParticlesMaterial, + VisualEffect +} from "three-vfx" +import { useDepthBuffer } from "./lib/useDepthBuffer" + +export const SoftParticlesExample = () => { + const depthBuffer = useDepthBuffer() + + return ( + + + + + + + { + c.lifetime = Infinity + }} + /> + + + ) +} diff --git a/apps/examples/src/examples/index.tsx b/apps/examples/src/examples/index.tsx index 0fbcc019b..494503d68 100644 --- a/apps/examples/src/examples/index.tsx +++ b/apps/examples/src/examples/index.tsx @@ -1,10 +1,12 @@ import { ReactNode } from "react" +import { DustExample } from "./DustExample" import { Explosion } from "./Explosion" import { FireflyExample } from "./FireflyExample" import { Fog } from "./Fog" import { GLTFParticles } from "./GLTFParticles" import { Simple } from "./Simple" import { Snow } from "./Snow" +import { SoftParticlesExample } from "./SoftParticlesExample" export type ExampleDefinition = { path: string @@ -18,6 +20,12 @@ export default [ { path: "firefly", name: "Firefly", component: }, { path: "fog", name: "Fog", component: }, { path: "snow", name: "Snow", component: }, + { path: "dust", name: "Dust", component: }, + { + path: "softparticles", + name: "Soft Particles", + component: + }, { path: "gltf", name: "GLTF Particles", component: }, { path: "combined", @@ -26,6 +34,7 @@ export default [ <> + ) } diff --git a/apps/examples/src/examples/lib/useDepthBuffer.tsx b/apps/examples/src/examples/lib/useDepthBuffer.tsx new file mode 100644 index 000000000..190635a0c --- /dev/null +++ b/apps/examples/src/examples/lib/useDepthBuffer.tsx @@ -0,0 +1,37 @@ +import { useFrame, useThree } from "@react-three/fiber" +import { useLayoutEffect, useMemo } from "react" +import { DepthTexture, WebGLRenderTarget } from "three" + +export function useDepthBuffer(resolution = 0.5) { + /* Fetch some items we need from the R3F state. */ + const size = useThree((s) => s.size) + const dpr = useThree((s) => s.viewport.dpr) + + const renderTarget = useMemo(() => { + /* Create render target using the depth texture */ + return new WebGLRenderTarget(256, 256, { + depthTexture: new DepthTexture(256, 256) + }) + }, []) + + /* Update rendertarget dimensions when the viewport changes */ + useLayoutEffect(() => { + /* Calculate render target dimensions */ + const textureWidth = size.width * dpr * resolution + const textureHeight = size.height * dpr * resolution + + renderTarget.setSize(textureWidth, textureHeight) + }, [resolution, size.width, size.height, dpr]) + + /* Dispose of render target at unmount */ + useLayoutEffect(() => () => renderTarget.dispose(), []) + + /* Every frame, render to our render target so we get a fresh depth texture. */ + useFrame((state) => { + state.gl.setRenderTarget(renderTarget) + state.gl.render(state.scene, state.camera) + state.gl.setRenderTarget(null) + }) + + return renderTarget +} diff --git a/packages/vfx/src/MeshParticles.tsx b/packages/vfx/src/MeshParticles.tsx index 2a08d3db2..bbdf1d0aa 100644 --- a/packages/vfx/src/MeshParticles.tsx +++ b/packages/vfx/src/MeshParticles.tsx @@ -42,7 +42,7 @@ export type MeshParticlesProps = InstancedMeshProps & { export type SpawnOptions = typeof components -export type SpawnSetup = (options: SpawnOptions) => void +export type SpawnSetup = (options: SpawnOptions, index: number) => void export type ParticlesAPI = { spawnParticle: (count: number, setup?: SpawnSetup) => void @@ -131,7 +131,7 @@ export const MeshParticles = forwardRef( components.alpha = [1, 0] /* Run setup */ - setup?.(components) + setup?.(components, i) imesh.current.setMatrixAt( playhead.current, diff --git a/packages/vfx/src/ParticlesMaterial.tsx b/packages/vfx/src/ParticlesMaterial.tsx index e9fb993ef..44485f67b 100644 --- a/packages/vfx/src/ParticlesMaterial.tsx +++ b/packages/vfx/src/ParticlesMaterial.tsx @@ -1,38 +1,67 @@ -import React, { forwardRef, useMemo } from "react" -import { AddEquation, CustomBlending } from "three" +import { useFrame } from "@react-three/fiber" +import React, { forwardRef, useMemo, useRef } from "react" +import mergeRefs from "react-merge-refs" +import { AddEquation, CustomBlending, DepthTexture } from "three" import CustomShaderMaterial, { iCSMProps } from "three-custom-shader-material" import CustomShaderMaterialImpl from "three-custom-shader-material/vanilla" import { createShader } from "./shaders/shader" type ParticlesMaterialProps = Omit & { billboard?: boolean + softness?: number scaleFunction?: string colorFunction?: string + softnessFunction?: string + depthTexture?: DepthTexture } export const ParticlesMaterial = forwardRef< CustomShaderMaterialImpl, ParticlesMaterialProps ->(({ billboard = false, scaleFunction, colorFunction, ...props }, ref) => { - const shader = useMemo( - () => - createShader({ - billboard, - scaleFunction, - colorFunction - }), - [] - ) +>( + ( + { + billboard = false, + softness = 0, + scaleFunction, + colorFunction, + softnessFunction, + depthTexture, + ...props + }, + ref + ) => { + const material = useRef(null!) - return ( - - ) -}) + const shader = useMemo( + () => + createShader({ + billboard, + softness, + scaleFunction, + colorFunction, + softnessFunction + }), + [] + ) + + useFrame(({ camera, size }) => { + if (softness) { + material.current.uniforms.u_depth.value = depthTexture + material.current.uniforms.u_cameraNear.value = camera.near + material.current.uniforms.u_cameraFar.value = camera.far + material.current.uniforms.u_resolution.value = [size.width, size.height] + } + }) + + return ( + + ) + } +) diff --git a/packages/vfx/src/shaders/modules/index.ts b/packages/vfx/src/shaders/modules/index.ts index b2f896faf..1027d3795 100644 --- a/packages/vfx/src/shaders/modules/index.ts +++ b/packages/vfx/src/shaders/modules/index.ts @@ -12,3 +12,6 @@ export const module = (input: Partial): Module => ({ fragmentMain: "", ...input }) + +export const formatValue = (value: any) => + typeof value === "number" ? value.toFixed(5) : value diff --git a/packages/vfx/src/shaders/shader.ts b/packages/vfx/src/shaders/shader.ts index 7f6250c5b..00ba1dfa5 100644 --- a/packages/vfx/src/shaders/shader.ts +++ b/packages/vfx/src/shaders/shader.ts @@ -1,4 +1,4 @@ -import { Module, module } from "./modules" +import { formatValue, Module, module } from "./modules" import { easings } from "./modules/easings" const compile = (headers: string, main: string) => ` @@ -10,9 +10,10 @@ const compile = (headers: string, main: string) => ` export const createShader = ({ billboard = false, + softness = 0, scaleFunction = "v_progress", colorFunction = "v_progress", - alphaFunction = "v_progress" + softnessFunction = "clamp(distance / softness, 0.0, 1.0)" } = {}) => { const state = { vertexHeaders: "", @@ -23,9 +24,9 @@ export const createShader = ({ const addModule = (module: Module) => { state.vertexHeaders += module.vertexHeader - state.vertexMain += module.vertexMain + state.vertexMain += `{ ${module.vertexMain} }` state.fragmentHeaders += module.fragmentHeader - state.fragmentMain += module.fragmentMain + state.fragmentMain += `{ ${module.fragmentMain} }` } /* Easing functions */ @@ -50,6 +51,10 @@ export const createShader = ({ vertexMain: ` v_age = u_time - time.x; v_progress = v_age / (time.y - time.x); + + if (v_progress < 0.0 || v_progress > 1.0) { + csm_Position *= 0.0;; + } `, fragmentHeader: ` varying float v_progress; @@ -142,13 +147,67 @@ export const createShader = ({ }) ) + /* Soft particles */ + if (softness) { + addModule( + module({ + vertexHeader: ` + varying float v_viewZ; + `, + + vertexMain: ` + vec4 viewPosition = viewMatrix * instanceMatrix * modelMatrix * vec4(csm_Position, 1.0); + v_viewZ = viewPosition.z; + `, + + fragmentHeader: ` + uniform sampler2D u_depth; + uniform vec2 u_resolution; + uniform float u_cameraNear; + uniform float u_cameraFar; + + varying float v_viewZ; + + float readDepth(vec2 coord) { + float depthZ = texture2D(u_depth, coord).x; + float viewZ = perspectiveDepthToViewZ(depthZ, u_cameraNear, u_cameraFar); + return viewZ; + } + `, + + fragmentMain: ` + /* Normalize fragment coordinates to screen space */ + vec2 screenUv = gl_FragCoord.xy / u_resolution; + + /* Get the existing depth at the fragment position */ + float depth = readDepth(screenUv); + + { + /* Prepare some convenient local variables */ + float d = depth; + float z = v_viewZ; + float softness = ${formatValue(softness)}; + + /* Calculate the distance to the fragment */ + float distance = z - d; + + /* Apply the distance to the fragment alpha */ + csm_DiffuseColor.a *= ${softnessFunction}; + } + ` + }) + ) + } + return { vertexShader: compile(state.vertexHeaders, state.vertexMain), fragmentShader: compile(state.fragmentHeaders, state.fragmentMain), uniforms: { - u_time: { - value: 0 - } + u_time: { value: 0 }, + u_depth: { value: null }, + u_cameraNear: { value: 0 }, + u_cameraFar: { value: 1 }, + u_resolution: { value: [window.innerWidth, window.innerHeight] } } } } diff --git a/yarn.lock b/yarn.lock index 9595e3448..ddf0621db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5829,6 +5829,11 @@ postcss@^8.4.13: picocolors "^1.0.0" source-map-js "^1.0.2" +postprocessing@^6.28.0: + version "6.28.0" + resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.28.0.tgz#5d6a1de31341ace13e6d1737c3382ac6a1141763" + integrity sha512-IMdVAxG6G0dn43C0XaJQSaAXfsqIj8ccCTHK/nv0LLS+yFkpHqsXGGQZ9QyceDg7Y7rKRk8Pp7i8d3U7N3sbyA== + potpack@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14"