├── .eslintignore ├── .node-version ├── bubbles.jpg ├── control.jpg ├── docs ├── logo.jpg ├── bubbles.jpg ├── control.jpg ├── effects │ ├── pixelation.mdx │ ├── brightness-contrast.mdx │ ├── color-average.mdx │ ├── smaa.mdx │ ├── sepia.mdx │ ├── chromatic-aberration.mdx │ ├── hue-saturation.mdx │ ├── noise.mdx │ ├── dot-screen.mdx │ ├── scanline.mdx │ ├── vignette.mdx │ ├── autofocus.mdx │ ├── grid.mdx │ ├── lensflare.mdx │ ├── custom-effects.mdx │ ├── depth-of-field.mdx │ ├── tone-mapping.mdx │ ├── glitch.mdx │ ├── ramp.mdx │ ├── outline.mdx │ ├── god-rays.mdx │ ├── selective-bloom.mdx │ ├── bloom.mdx │ └── ssao.mdx ├── effect-composer.mdx ├── selection.mdx └── introduction.mdx ├── .prettierrc ├── src ├── effects │ ├── FXAA.tsx │ ├── SMAA.tsx │ ├── Depth.tsx │ ├── Sepia.tsx │ ├── Vignette.tsx │ ├── DotScreen.tsx │ ├── ShockWave.tsx │ ├── ColorDepth.tsx │ ├── HueSaturation.tsx │ ├── BrightnessContrast.tsx │ ├── Noise.tsx │ ├── Bloom.tsx │ ├── TiltShift.tsx │ ├── ScanlineEffect.tsx │ ├── ToneMapping.tsx │ ├── ChromaticAberration.tsx │ ├── Pixelation.tsx │ ├── ColorAverage.tsx │ ├── Grid.tsx │ ├── GodRays.tsx │ ├── LUT.tsx │ ├── Texture.tsx │ ├── Water.tsx │ ├── Glitch.tsx │ ├── SSAO.tsx │ ├── N8AO.tsx │ ├── DepthOfField.tsx │ ├── Outline.tsx │ ├── TiltShift2.tsx │ ├── SelectiveBloom.tsx │ ├── ASCII.tsx │ ├── Ramp.tsx │ ├── Autofocus.tsx │ └── LensFlare.tsx ├── index.ts ├── Selection.tsx ├── util.tsx ├── EffectComposer.test.tsx └── EffectComposer.tsx ├── .npmignore ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── main.yml │ └── docs.yml ├── LICENSE ├── CONTRIBUTING.md ├── vite.config.ts ├── package.json ├── .eslintrc.json ├── CODE_OF_CONDUCT.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18.16.0 -------------------------------------------------------------------------------- /bubbles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-postprocessing/HEAD/bubbles.jpg -------------------------------------------------------------------------------- /control.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-postprocessing/HEAD/control.jpg -------------------------------------------------------------------------------- /docs/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-postprocessing/HEAD/docs/logo.jpg -------------------------------------------------------------------------------- /docs/bubbles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-postprocessing/HEAD/docs/bubbles.jpg -------------------------------------------------------------------------------- /docs/control.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-postprocessing/HEAD/docs/control.jpg -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "printWidth": 120, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /src/effects/FXAA.tsx: -------------------------------------------------------------------------------- 1 | import { FXAAEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const FXAA = /* @__PURE__ */ wrapEffect(FXAAEffect) 5 | -------------------------------------------------------------------------------- /src/effects/SMAA.tsx: -------------------------------------------------------------------------------- 1 | import { SMAAEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const SMAA = /* @__PURE__ */ wrapEffect(SMAAEffect) 5 | -------------------------------------------------------------------------------- /src/effects/Depth.tsx: -------------------------------------------------------------------------------- 1 | import { DepthEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const Depth = /* @__PURE__ */ wrapEffect(DepthEffect) 5 | -------------------------------------------------------------------------------- /src/effects/Sepia.tsx: -------------------------------------------------------------------------------- 1 | import { SepiaEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const Sepia = /* @__PURE__ */ wrapEffect(SepiaEffect) 5 | -------------------------------------------------------------------------------- /src/effects/Vignette.tsx: -------------------------------------------------------------------------------- 1 | import { VignetteEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const Vignette = /* @__PURE__ */ wrapEffect(VignetteEffect) 5 | -------------------------------------------------------------------------------- /src/effects/DotScreen.tsx: -------------------------------------------------------------------------------- 1 | import { DotScreenEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const DotScreen = /* @__PURE__ */ wrapEffect(DotScreenEffect) 5 | -------------------------------------------------------------------------------- /src/effects/ShockWave.tsx: -------------------------------------------------------------------------------- 1 | import { ShockWaveEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const ShockWave = /* @__PURE__ */ wrapEffect(ShockWaveEffect) 5 | -------------------------------------------------------------------------------- /src/effects/ColorDepth.tsx: -------------------------------------------------------------------------------- 1 | import { ColorDepthEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const ColorDepth = /* @__PURE__ */ wrapEffect(ColorDepthEffect) 5 | -------------------------------------------------------------------------------- /src/effects/HueSaturation.tsx: -------------------------------------------------------------------------------- 1 | import { HueSaturationEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const HueSaturation = /* @__PURE__ */ wrapEffect(HueSaturationEffect) 5 | -------------------------------------------------------------------------------- /src/effects/BrightnessContrast.tsx: -------------------------------------------------------------------------------- 1 | import { BrightnessContrastEffect } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const BrightnessContrast = /* @__PURE__ */ wrapEffect(BrightnessContrastEffect) 5 | -------------------------------------------------------------------------------- /src/effects/Noise.tsx: -------------------------------------------------------------------------------- 1 | import { NoiseEffect, BlendFunction } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const Noise = /* @__PURE__ */ wrapEffect(NoiseEffect, { blendFunction: BlendFunction.COLOR_DODGE }) 5 | -------------------------------------------------------------------------------- /src/effects/Bloom.tsx: -------------------------------------------------------------------------------- 1 | import { BloomEffect, BlendFunction } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const Bloom = /* @__PURE__ */ wrapEffect(BloomEffect, { 5 | blendFunction: BlendFunction.ADD, 6 | }) 7 | -------------------------------------------------------------------------------- /src/effects/TiltShift.tsx: -------------------------------------------------------------------------------- 1 | import { TiltShiftEffect, BlendFunction } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const TiltShift = /* @__PURE__ */ wrapEffect(TiltShiftEffect, { blendFunction: BlendFunction.ADD }) 5 | -------------------------------------------------------------------------------- /src/effects/ScanlineEffect.tsx: -------------------------------------------------------------------------------- 1 | import { ScanlineEffect, BlendFunction } from 'postprocessing' 2 | import { wrapEffect } from '../util' 3 | 4 | export const Scanline = /* @__PURE__ */ wrapEffect(ScanlineEffect, { 5 | blendFunction: BlendFunction.OVERLAY, 6 | density: 1.25, 7 | }) 8 | -------------------------------------------------------------------------------- /src/effects/ToneMapping.tsx: -------------------------------------------------------------------------------- 1 | import { ToneMappingEffect } from 'postprocessing' 2 | import { type EffectProps, wrapEffect } from '../util' 3 | 4 | export type ToneMappingProps = EffectProps 5 | 6 | export const ToneMapping = /* @__PURE__ */ wrapEffect(ToneMappingEffect) 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | example/ 4 | build/ 5 | Thumbs.db 6 | ehthumbs.db 7 | Desktop.ini 8 | $RECYCLE.BIN/ 9 | .DS_Store 10 | .vscode 11 | .docz/ 12 | package-lock.json 13 | coverage/ 14 | .idea 15 | yarn-error.log 16 | .size-snapshot.json 17 | pnpm-debug.log 18 | .parcel-cache 19 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | build/ 5 | Thumbs.db 6 | ehthumbs.db 7 | Desktop.ini 8 | $RECYCLE.BIN/ 9 | .DS_Store 10 | .vscode 11 | .docz/ 12 | package-lock.json 13 | coverage/ 14 | .idea 15 | yarn-error.log 16 | .size-snapshot.json 17 | pnpm-debug.log 18 | .parcel-cache 19 | pnpm-lock.yaml 20 | storybook-static -------------------------------------------------------------------------------- /src/effects/ChromaticAberration.tsx: -------------------------------------------------------------------------------- 1 | import { ChromaticAberrationEffect } from 'postprocessing' 2 | import { type EffectProps, wrapEffect } from '../util' 3 | 4 | export type ChromaticAberrationProps = EffectProps 5 | export const ChromaticAberration = /* @__PURE__ */ wrapEffect(ChromaticAberrationEffect) 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "moduleResolution": "bundler", 8 | "strict": true, 9 | "jsx": "react-jsx", 10 | "pretty": true, 11 | "declaration": true, 12 | "emitDeclarationOnly": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "paths": { 15 | "@react-three/postprocessing": ["./src"] 16 | } 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["src/**/*.test.*"] 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | ci: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | cache: 'yarn' 15 | - name: Install Dependencies 16 | run: yarn install --frozen-lockfile 17 | 18 | - name: Check build health 19 | run: yarn build 20 | 21 | - name: Check for regressions 22 | run: yarn eslint:ci 23 | 24 | - name: Run tests 25 | run: yarn test 26 | -------------------------------------------------------------------------------- /docs/effects/pixelation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pixelation 3 | nav: 1 4 | --- 5 | 6 | A pixelation effect. 7 | 8 | Warning: This effect cannot be merged with convolution effects. 9 | 10 | ```jsx 11 | import { Pixelation } from '@react-three/postprocessing' 12 | 13 | return ( 14 | 17 | ) 18 | ``` 19 | 20 | ## Example 21 | 22 | 23 | 24 | ## Props 25 | 26 | | Name | Type | Default | Description | 27 | | ----------- | ------ | ------- | ----------- | 28 | | granularity | Number | 30 | Pixel Size | 29 | -------------------------------------------------------------------------------- /docs/effect-composer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: EffectComposer 3 | nav: 0 4 | --- 5 | 6 | The `EffectComposer` must wrap all your effects. It will manage them for you. 7 | 8 | ```jsx 9 | 23 | {/* your effects go here */} 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /src/effects/Pixelation.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useMemo, Ref } from 'react' 2 | import { PixelationEffect } from 'postprocessing' 3 | 4 | export type PixelationProps = { 5 | granularity?: number 6 | } 7 | 8 | export const Pixelation = /* @__PURE__ */ forwardRef(function Pixelation( 9 | { granularity = 5 }: PixelationProps, 10 | ref: Ref 11 | ) { 12 | /** Because GlitchEffect granularity is not an object but a number, we have to define a custom prop "granularity" */ 13 | const effect = useMemo(() => new PixelationEffect(granularity), [granularity]) 14 | return 15 | }) 16 | -------------------------------------------------------------------------------- /docs/effects/brightness-contrast.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: BrightnessContrast 3 | nav: 1 4 | --- 5 | 6 | Alter brightness and contrast of your scene 7 | 8 | ```jsx 9 | import { BrightnessContrast } from '@react-three/postprocessing' 10 | 11 | return ( 12 | 16 | ) 17 | ``` 18 | 19 | ## Example 20 | 21 | 22 | 23 | ## Props 24 | 25 | | Name | Type | Default | Description | 26 | | ---------- | ------ | ------- | ---------------------- | 27 | | brightness | Number | 0 | Scene brightness shift | 28 | | contrast | Number | 0 | Scene contrast shift | 29 | -------------------------------------------------------------------------------- /docs/effects/color-average.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ColorAverage 3 | nav: 1 4 | --- 5 | 6 | A color average effect. 7 | 8 | ```jsx 9 | import { ColorAverage } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 16 | ) 17 | ``` 18 | 19 | ## Example 20 | 21 | 22 | 23 | ## Props 24 | 25 | | Name | Type | Default | Description | 26 | | ------------- | ------------- | -------------------- | ---------------------------------- | 27 | | blendFunction | BlendFunction | BlendFunction.NORMAL | The blend function of this effect. | 28 | -------------------------------------------------------------------------------- /src/effects/ColorAverage.tsx: -------------------------------------------------------------------------------- 1 | import { ColorAverageEffect, BlendFunction } from 'postprocessing' 2 | import React, { Ref, forwardRef, useMemo } from 'react' 3 | 4 | export type ColorAverageProps = Partial<{ 5 | blendFunction: BlendFunction 6 | }> 7 | 8 | export const ColorAverage = /* @__PURE__ */ forwardRef(function ColorAverage( 9 | { blendFunction = BlendFunction.NORMAL }: ColorAverageProps, 10 | ref: Ref 11 | ) { 12 | /** Because ColorAverage blendFunction is not an object but a number, we have to define a custom prop "blendFunction" */ 13 | const effect = useMemo(() => new ColorAverageEffect(blendFunction), [blendFunction]) 14 | return 15 | }) 16 | -------------------------------------------------------------------------------- /docs/effects/smaa.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SMAA 3 | nav: 1 4 | --- 5 | 6 | Subpixel Morphological Antialiasing (SMAA). 7 | 8 | https://github.com/iryoku/smaa/releases/tag/v2.8 9 | 10 | By default react-postprocessing uses webgl2 multisampling (MSAA) for native AA. In some effects this can result in artefacts. Should you either want to work with webgl1 exclusively, or you get artefacts, then you can switch MSAA off and use SMAA. This effect is async and relies on suspense! 11 | 12 | ```jsx 13 | import React, { Suspense } from 'react' 14 | import { EffectComposer, SMAA } from '@react-three/postprocessing' 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/effects/sepia.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sepia 3 | nav: 1 4 | --- 5 | 6 | A sepia effect. 7 | 8 | ```jsx 9 | import { Sepia } from '@react-three/postprocessing' 10 | 11 | return ( 12 | 16 | ) 17 | ``` 18 | 19 | ## Example 20 | 21 | 22 | 23 | ## Props 24 | 25 | | Name | Type | Default | Description | 26 | | ------------- | ------------- | -------------------- | ---------------------------------- | 27 | | intensity | Number | 1 | The intensity of the effect | 28 | | blendFunction | BlendFunction | BlendFunction.NORMAL | The blend function of this effect. | 29 | -------------------------------------------------------------------------------- /src/effects/Grid.tsx: -------------------------------------------------------------------------------- 1 | import React, { Ref, forwardRef, useMemo, useLayoutEffect } from 'react' 2 | import { GridEffect } from 'postprocessing' 3 | import { useThree } from '@react-three/fiber' 4 | 5 | type GridProps = ConstructorParameters[0] & 6 | Partial<{ 7 | size: { 8 | width: number 9 | height: number 10 | } 11 | }> 12 | 13 | export const Grid = /* @__PURE__ */ forwardRef(function Grid({ size, ...props }: GridProps, ref: Ref) { 14 | const invalidate = useThree((state) => state.invalidate) 15 | const effect = useMemo(() => new GridEffect(props), [props]) 16 | useLayoutEffect(() => { 17 | if (size) effect.setSize(size.width, size.height) 18 | invalidate() 19 | }, [effect, size, invalidate]) 20 | return 21 | }) 22 | -------------------------------------------------------------------------------- /src/effects/GodRays.tsx: -------------------------------------------------------------------------------- 1 | import { GodRaysEffect } from 'postprocessing' 2 | import React, { Ref, forwardRef, useMemo, useContext, useLayoutEffect } from 'react' 3 | import { Mesh, Points } from 'three' 4 | import { EffectComposerContext } from '../EffectComposer' 5 | import { resolveRef } from '../util' 6 | 7 | type GodRaysProps = ConstructorParameters[2] & { 8 | sun: Mesh | Points | React.RefObject 9 | } 10 | 11 | export const GodRays = /* @__PURE__ */ forwardRef(function GodRays(props: GodRaysProps, ref: Ref) { 12 | const { camera } = useContext(EffectComposerContext) 13 | const effect = useMemo(() => new GodRaysEffect(camera, resolveRef(props.sun), props), [camera, props]) 14 | useLayoutEffect(() => void (effect.lightSource = resolveRef(props.sun)), [effect, props.sun]) 15 | return 16 | }) 17 | -------------------------------------------------------------------------------- /docs/effects/chromatic-aberration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ChromaticAberration 3 | nav: 1 4 | --- 5 | 6 | A chromatic aberration effect. 7 | 8 | ```jsx 9 | import { ChromaticAberration } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 17 | ) 18 | ``` 19 | 20 | ## Example 21 | 22 | 23 | 24 | ## Props 25 | 26 | | Name | Type | Default | Description | 27 | | ------------- | ------------- | -------------------- | ---------------------------------- | 28 | | offset | Vector2 | | The color offset. | 29 | | blendFunction | BlendFunction | BlendFunction.Normal | The blend function of this effect. | 30 | -------------------------------------------------------------------------------- /docs/effects/hue-saturation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: HueSaturation 3 | nav: 1 4 | --- 5 | 6 | A hue/saturation effect. 7 | 8 | ```jsx 9 | import { HueSaturation } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 18 | ) 19 | ``` 20 | 21 | ## Example 22 | 23 | 24 | 25 | ## Props 26 | 27 | | Name | Type | Default | Description | 28 | | ------------- | ------------- | ------- | ---------------------------------- | 29 | | hue | Number | 0 | Hue shift in radians | 30 | | saturation | Number | 0 | Saturation value in radians | 31 | | blendFunction | BlendFunction | | The blend function of this effect. | 32 | -------------------------------------------------------------------------------- /docs/effects/noise.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Noise 3 | nav: 1 4 | --- 5 | 6 | A noise effect. 7 | 8 | ```jsx 9 | import { Noise } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 17 | ) 18 | ``` 19 | 20 | ## Example 21 | 22 | 23 | 24 | ## Props 25 | 26 | | Name | Type | Default | Description | 27 | | ------------- | ------------- | -------------------- | ------------------------------------------------------------ | 28 | | premultiply | Boolean | false | Whether the noise should be multiplied with the input color. | 29 | | blendFunction | BlendFunction | BlendFunction.SCREEN | The blend function of this effect. | 30 | -------------------------------------------------------------------------------- /src/effects/LUT.tsx: -------------------------------------------------------------------------------- 1 | import { useThree } from '@react-three/fiber' 2 | import { LUT3DEffect, BlendFunction } from 'postprocessing' 3 | import React, { forwardRef, Ref, useLayoutEffect, useMemo } from 'react' 4 | import type { Texture } from 'three' 5 | 6 | export type LUTProps = { 7 | lut: Texture 8 | blendFunction?: BlendFunction 9 | tetrahedralInterpolation?: boolean 10 | } 11 | 12 | export const LUT = /* @__PURE__ */ forwardRef(function LUT( 13 | { lut, tetrahedralInterpolation, ...props }: LUTProps, 14 | ref: Ref 15 | ) { 16 | const effect = useMemo(() => new LUT3DEffect(lut, props), [lut, props]) 17 | const invalidate = useThree((state) => state.invalidate) 18 | 19 | useLayoutEffect(() => { 20 | if (tetrahedralInterpolation) effect.tetrahedralInterpolation = tetrahedralInterpolation 21 | if (lut) effect.lut = lut 22 | invalidate() 23 | }, [effect, invalidate, lut, tetrahedralInterpolation]) 24 | 25 | return 26 | }) 27 | -------------------------------------------------------------------------------- /docs/effects/dot-screen.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dot Screen 3 | nav: 1 4 | --- 5 | 6 | A dot screen effect. 7 | 8 | ```jsx 9 | import { DotScreen } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 18 | ) 19 | ``` 20 | 21 | ## Example 22 | 23 | 24 | 25 | ## Props 26 | 27 | | Name | Type | Default | Description | 28 | | ------------- | ------------- | -------------------- | ---------------------------------- | 29 | | angle | Number | 1 .57 | The angle of the dot pattern. | 30 | | blendFunction | BlendFunction | BlendFunction.NORMAL | The blend function of this effect. | 31 | | scale | Number | 1 .57 | The scale of the dot pattern. | 32 | -------------------------------------------------------------------------------- /docs/effects/scanline.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scanline 3 | nav: 1 4 | --- 5 | 6 | A scanline effect. 7 | 8 | Based on an implementation by Georg 'Leviathan' Steinrohder (CC BY 3.0): http://www.truevision3d.com/forums/showcase/staticnoise_colorblackwhite_scanline_shaders-t18698.0.html 9 | 10 | ```jsx 11 | import { Scanline } from '@react-three/postprocessing' 12 | import { BlendFunction } from 'postprocessing' 13 | 14 | return ( 15 | 19 | ) 20 | ``` 21 | 22 | ## Example 23 | 24 | 25 | 26 | ## Props 27 | 28 | | Name | Type | Default | Description | 29 | | ------------- | ------------- | --------------------- | ---------------------------------- | 30 | | density | Number | 1.25 | The scanline density. | 31 | | blendFunction | BlendFunction | BlendFunction.OVERLAY | The blend function of this effect. | 32 | -------------------------------------------------------------------------------- /src/effects/Texture.tsx: -------------------------------------------------------------------------------- 1 | import { TextureEffect } from 'postprocessing' 2 | import { Ref, forwardRef, useMemo, useLayoutEffect } from 'react' 3 | import { useLoader } from '@react-three/fiber' 4 | import { TextureLoader, SRGBColorSpace, RepeatWrapping } from 'three' 5 | 6 | type TextureProps = ConstructorParameters[0] & { 7 | textureSrc: string 8 | /** opacity of provided texture */ 9 | opacity?: number 10 | } 11 | 12 | export const Texture = /* @__PURE__ */ forwardRef(function Texture( 13 | { textureSrc, texture, opacity = 1, ...props }: TextureProps, 14 | ref: Ref 15 | ) { 16 | const t = useLoader(TextureLoader, textureSrc) 17 | useLayoutEffect(() => { 18 | t.colorSpace = SRGBColorSpace 19 | t.wrapS = t.wrapT = RepeatWrapping 20 | }, [t]) 21 | const effect = useMemo(() => new TextureEffect({ ...props, texture: t || texture }), [props, t, texture]) 22 | return 23 | }) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 react-spring 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/effects/vignette.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vignette 3 | nav: 1 4 | --- 5 | 6 | A vignette effect. 7 | 8 | ```jsx 9 | import { Vignette } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 19 | ) 20 | ``` 21 | 22 | ## Example 23 | 24 | 25 | 26 | ## Props 27 | 28 | | Name | Type | Default | Description | 29 | | ------------- | ------------- | -------------------- | ----------------------------------- | 30 | | eskil | Boolean | false | Enables Eskil's vignette technique. | 31 | | blendFunction | BlendFunction | BlendFunction.NORMAL | The blend function of this effect. | 32 | | offset | Number | 0.5 | The vignette offset. | 33 | | darkness | Number | 0.5 | The vignette darkness. | 34 | -------------------------------------------------------------------------------- /docs/effects/autofocus.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Autofocus 3 | nav: 1 4 | --- 5 | 6 | An auto-focus effect, that extends ``. 7 | 8 | Based on [ektogamat/AutoFocusDOF](https://github.com/ektogamat/AutoFocusDOF). 9 | 10 | ```tsx 11 | export type AutofocusProps = typeof DepthOfField & { 12 | target?: [number, number, number] // undefined 13 | mouse?: boolean // false 14 | debug?: number // undefined 15 | manual?: boolean // false 16 | smoothTime?: number // .25 17 | } 18 | ``` 19 | 20 | ```tsx 21 | 22 | 23 | 24 | ``` 25 | 26 | Ref-api: 27 | 28 | ```tsx 29 | type AutofocusApi = { 30 | dofRef: RefObject 31 | hitpoint: THREE.Vector3 32 | update: (delta: number, updateTarget: boolean) => void 33 | } 34 | ``` 35 | 36 | ```tsx 37 | 38 | ``` 39 | 40 | Associated with `manual` prop, you can for example, animate the DOF target yourself: 41 | 42 | ```tsx 43 | useFrame((_, delta) => { 44 | const api = autofocusRef.current 45 | api.update(delta, false) // update hitpoint only 46 | easing.damp3(api.dofRef.curent.target, api.hitpoint, 0.5, delta) // custom easing 47 | }) 48 | ``` 49 | 50 | ## Example 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/effects/grid.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Grid 3 | nav: 1 4 | --- 5 | 6 | A Grid effect 7 | 8 | ```jsx 9 | import { Grid } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 19 | ) 20 | ``` 21 | 22 | ## Example 23 | 24 | 25 | 26 | ## Props 27 | 28 | | Name | Type | Default | Description | 29 | | ------------- | ------------- | -------------------- | ---------------------------------- | 30 | | blendFunction | BlendFunction | BlendFunction.NORMAL | The blend function of this effect. | 31 | | scale | Number | 1 | The scale of the grid pattern. | 32 | | lineWidth | Number | 0 | The blend function of this effect. | 33 | | width | Number | | Overrides the default pass width | 34 | | height | Number | | Overrides the default pass height | 35 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build documentation and deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: ['master'] 5 | workflow_dispatch: 6 | 7 | # Cancel previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | uses: pmndrs/docs/.github/workflows/build.yml@v2 15 | with: 16 | mdx: 'docs' 17 | libname: 'React Postprocessing' 18 | libname_short: 'pp' 19 | home_redirect: '/introduction' 20 | icon: '📬' 21 | logo: '/logo.jpg' 22 | github: 'https://github.com/pmndrs/react-postprocessing' 23 | 24 | deploy: 25 | needs: build 26 | runs-on: ubuntu-latest 27 | 28 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 29 | permissions: 30 | pages: write # to deploy to Pages 31 | id-token: write # to verify the deployment originates from an appropriate source 32 | 33 | # Deploy to the github-pages environment 34 | environment: 35 | name: github-pages 36 | url: ${{ steps.deployment.outputs.page_url }} 37 | 38 | steps: 39 | - id: deployment 40 | uses: actions/deploy-pages@v4 41 | -------------------------------------------------------------------------------- /src/effects/Water.tsx: -------------------------------------------------------------------------------- 1 | import { Uniform } from 'three' 2 | import { BlendFunction, Effect, EffectAttribute } from 'postprocessing' 3 | import { wrapEffect } from '../util' 4 | 5 | const WaterShader = { 6 | fragmentShader: /* glsl */ ` 7 | uniform float factor; 8 | 9 | void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { 10 | vec2 vUv = uv; 11 | float frequency = 6.0 * factor; 12 | float amplitude = 0.015 * factor; 13 | float x = vUv.y * frequency + time * 0.7; 14 | float y = vUv.x * frequency + time * 0.3; 15 | vUv.x += cos(x + y) * amplitude * cos(y); 16 | vUv.y += sin(x - y) * amplitude * cos(y); 17 | vec4 rgba = texture(inputBuffer, vUv); 18 | outputColor = rgba; 19 | } 20 | `, 21 | } 22 | 23 | export class WaterEffectImpl extends Effect { 24 | constructor({ blendFunction = BlendFunction.NORMAL, factor = 0 } = {}) { 25 | super('WaterEffect', WaterShader.fragmentShader, { 26 | blendFunction, 27 | attributes: EffectAttribute.CONVOLUTION, 28 | uniforms: new Map>([['factor', new Uniform(factor)]]), 29 | }) 30 | } 31 | } 32 | 33 | export const WaterEffect = /* @__PURE__ */ wrapEffect(WaterEffectImpl, { 34 | blendFunction: BlendFunction.NORMAL, 35 | }) 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Selection' 2 | export * from './EffectComposer' 3 | export * from './util' 4 | 5 | export * from './effects/Autofocus' 6 | export * from './effects/LensFlare' 7 | export * from './effects/Bloom' 8 | export * from './effects/BrightnessContrast' 9 | export * from './effects/ChromaticAberration' 10 | export * from './effects/ColorAverage' 11 | export * from './effects/ColorDepth' 12 | export * from './effects/Depth' 13 | export * from './effects/DepthOfField' 14 | export * from './effects/DotScreen' 15 | export * from './effects/Glitch' 16 | export * from './effects/GodRays' 17 | export * from './effects/Grid' 18 | export * from './effects/HueSaturation' 19 | export * from './effects/Noise' 20 | export * from './effects/Outline' 21 | export * from './effects/Pixelation' 22 | export * from './effects/ScanlineEffect' 23 | export * from './effects/SelectiveBloom' 24 | export * from './effects/Sepia' 25 | export * from './effects/SSAO' 26 | export * from './effects/SMAA' 27 | export * from './effects/FXAA' 28 | export * from './effects/Ramp' 29 | export * from './effects/Texture' 30 | export * from './effects/ToneMapping' 31 | export * from './effects/Vignette' 32 | export * from './effects/ShockWave' 33 | export * from './effects/LUT' 34 | export * from './effects/TiltShift' 35 | export * from './effects/TiltShift2' 36 | export * from './effects/ASCII' 37 | export * from './effects/Water' 38 | 39 | // These are not effect passes 40 | export * from './effects/N8AO' 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for wanting to make a contribution and wanting to improve this library for everyone! This repository uses Typescript so please continue to do so, you can always reach out in the repo or the [discord](https://pmnd.rs/discord). This is a guideline, use your initiative, if you don't think it makes sense to do a step in here, don't bother it's normally okay. we're chill. 4 | 5 | ## How to Contribute 6 | 7 | 1. Fork and clone the repo 8 | 2. Run `yarn install` to install dependencies 9 | 3. Create a branch for your PR with `git checkout -b pr-type/issue-number-your-branch-name` 10 | 4. Let's get cooking! 👨🏻‍🍳🥓 11 | 12 | You can also just [![Open in GitHub Codespaces](https://img.shields.io/static/v1?&message=Open%20in%20%20Codespaces&style=flat&colorA=000000&colorB=000000&label=GitHub&logo=github&logoColor=ffffff)](https://github.com/codespaces/new?template_repository=pmndrs%2Freact-postprocessing). 13 | 14 | ## Commit Guidelines 15 | 16 | Be sure your commit messages follow this specification: https://www.conventionalcommits.org/en/v1.0.0-beta.4/ 17 | 18 | ## Publishing 19 | 20 | We use `semantic-release` to deploy the package. Because of this only certain commits will trigger the action of creating a release: 21 | 22 | - `fix:` will create a `0.0.x` version 23 | - `feat:` will create a `0.x.0` version 24 | - `BREAKING CHANGE:` will create a `x.0.0` version 25 | 26 | We release on `master` branch. Any other commits will not fire a release. 27 | -------------------------------------------------------------------------------- /docs/selection.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Selection 3 | description: Selection/Select 4 | nav: 0 5 | --- 6 | 7 | Some effects, like Outline or SelectiveBloom can select specific objects. To 8 | manage this in a declarative scene with just references can be messy, especially 9 | when things have to be grouped. These two components take care of it: 10 | 11 | ```jsx 12 | 16 | 17 | 33 | 34 | 35 | 36 | ``` 37 | 38 | Selection can be nested and group multiple object, higher up selection take 39 | precence over lower ones. The following for instance will select everything. 40 | Remove the outmost `enabled` and only the two mesh group is selected. You can 41 | flip the selections or bind them to interactions and state. 42 | 43 | ```jsx 44 | 46 | 47 | 48 | 49 | 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/effects/lensflare.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lensflare 3 | nav: 1 4 | --- 5 | 6 | A Lens Flare adds the optical aberration caused by the dispersion of light entering the lens through its edges. 7 | 8 | Based on [ektogamat/R3F-Ultimate-Lens-Flare](https://github.com/ektogamat/R3F-Ultimate-Lens-Flare). 9 | 10 | ```jsx 11 | import { LensFlare } from '@react-three/postprocessing' 12 | 13 | return 14 | ``` 15 | 16 | ## Ignoring occlusion on some objects 17 | 18 | To disable the occlusion effect, simply add `userData={{ lensflare: 'no-occlusion' }}` to your object/mesh props. 19 | 20 | ## Improving performance 21 | 22 | Use bvh `` to enhance the internal raycaster performance. 23 | 24 | ## Limitations 25 | 26 | The Ultimate Lens Flare leverages the raycaster to examine the material type of objects and determine if they are `MeshTransmissionMaterial` or `MeshPhysicalMaterial`. It checks for the transmission parameter to identify glass-like materials. Therefore, for an object to behave like glass, its material should have either `transmission = 1` or `transparent = true` and `opacity = NUMBER`. The effect automatically interprets the opacity `NUMBER` value to determine the brightness of the flare. 27 | 28 | ## Credits 29 | 30 | - https://www.shadertoy.com/view/4sK3W3 31 | - https://www.shadertoy.com/view/4sX3Rs 32 | - https://www.shadertoy.com/view/dllSRX 33 | - https://www.shadertoy.com/view/Xlc3D2 34 | - https://www.shadertoy.com/view/XtKfRV 35 | 36 | ## Example 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/effects/custom-effects.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom effects 3 | nav: 2 4 | --- 5 | 6 | If you plan to use custom effects, make sure to expose the effect itself as a primitive! 7 | 8 | ```jsx 9 | import React, { forwardRef, useMemo } from 'react' 10 | import { PixelationEffect } from 'postprocessing' 11 | 12 | export const Pixelation = forwardRef(({ granularity = 5 }, ref) => { 13 | const effect = useMemo(() => new PixelationEffect(granularity), [granularity]) 14 | return 15 | }) 16 | ``` 17 | 18 | For effects that aren't present in `postprocessing` you should extend the `Effect` class: 19 | 20 | ```js 21 | import React, { forwardRef, useMemo } from 'react' 22 | import { Uniform } from 'three' 23 | import { Effect } from 'postprocessing' 24 | 25 | const fragmentShader = `some_shader_code` 26 | 27 | let _uParam 28 | 29 | // Effect implementation 30 | class MyCustomEffectImpl extends Effect { 31 | constructor({ param = 0.1 } = {}) { 32 | super('MyCustomEffect', fragmentShader, { 33 | uniforms: new Map([['param', new Uniform(param)]]), 34 | }) 35 | 36 | _uParam = param 37 | } 38 | 39 | update(renderer, inputBuffer, deltaTime) { 40 | this.uniforms.get('param').value = _uParam 41 | } 42 | } 43 | 44 | // Effect component 45 | export const MyCustomEffect = forwardRef(({ param }, ref) => { 46 | const effect = useMemo(() => new MyCustomEffectImpl(param), [param]) 47 | return 48 | }) 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/effects/depth-of-field.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: DepthOfField 3 | nav: 1 4 | --- 5 | 6 | A depth of field effect. 7 | 8 | Based on a graphics study by Adrian Courrèges and an article by Steve Avery: https://www.adriancourreges.com/blog/2016/09/09/doom-2016-graphics-study/ https://pixelmischiefblog.wordpress.com/2016/11/25/bokeh-depth-of-field/ 9 | 10 | ```jsx 11 | import { DepthOfField } from '@react-three/postprocessing' 12 | 13 | return ( 14 | 19 | ) 20 | ``` 21 | 22 | ## Example 23 | 24 | 25 | 26 | ## Props 27 | 28 | | Name | Type | Default | Description | 29 | | ------------- | ------------- | -------------------- | --------------------------------------------------- | 30 | | blendFunction | BlendFunction | BlendFunction.NORMAL | The blend function of this effect. | 31 | | focusDistance | Number | 0 | The normalized focus distance. Range is [0.0, 1.0]. | 32 | | focalLength | Number | 0.1 | The focal length. Range is [0.0, 1.0]. | 33 | | bokehScale | Number | 1.0 | The scale of the bokeh blur. | 34 | | width | Number | Resizer.width | The render width. | 35 | | height | Number | Resizer.height | The render height. | 36 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import * as vite from 'vite' 2 | import * as path from 'node:path' 3 | import { BlendFunction, EffectAttribute } from 'postprocessing' 4 | 5 | export default vite.defineConfig({ 6 | resolve: { 7 | alias: { 8 | '@react-three/postprocessing': path.resolve(__dirname, 'src/index.ts'), 9 | }, 10 | }, 11 | build: { 12 | sourcemap: true, 13 | target: 'es2020', 14 | lib: { 15 | formats: ['es'], 16 | entry: 'src/index.ts', 17 | fileName: '[name]', 18 | }, 19 | rollupOptions: { 20 | external: (id: string) => !id.startsWith('.') && !path.isAbsolute(id), 21 | output: { 22 | sourcemapExcludeSources: true, 23 | }, 24 | }, 25 | }, 26 | plugins: [ 27 | { 28 | name: 'vite-minify', 29 | transform(code, url) { 30 | if (!url.includes('node_modules')) { 31 | code = code.replaceAll(/EffectAttribute\.(\w+)/g, (_, key) => EffectAttribute[key]) 32 | code = code.replaceAll(/BlendFunction\.(\w+)/g, (_, key) => BlendFunction[key]) 33 | return vite.transformWithEsbuild(code, url) 34 | } 35 | }, 36 | renderChunk: { 37 | order: 'post', 38 | async handler(code, { fileName }) { 39 | // Preserve pure annotations, but remove all other comments and whitespace 40 | code = code.replaceAll('/* @__PURE__ */', '__PURE__ || ') 41 | const result = await vite.transformWithEsbuild(code, fileName, { minify: true, target: 'es2020' }) 42 | result.code = result.code.replaceAll('__PURE__||', '/*@__PURE__*/') 43 | return result 44 | }, 45 | }, 46 | }, 47 | ], 48 | }) 49 | -------------------------------------------------------------------------------- /src/effects/Glitch.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three' 2 | import { GlitchEffect, GlitchMode } from 'postprocessing' 3 | import { Ref, forwardRef, useMemo, useLayoutEffect, useEffect } from 'react' 4 | import { ReactThreeFiber, useThree } from '@react-three/fiber' 5 | import { useVector2 } from '../util' 6 | 7 | export type GlitchProps = ConstructorParameters[0] & 8 | Partial<{ 9 | mode: GlitchMode 10 | active: boolean 11 | delay: ReactThreeFiber.Vector2 12 | duration: ReactThreeFiber.Vector2 13 | chromaticAberrationOffset: ReactThreeFiber.Vector2 14 | strength: ReactThreeFiber.Vector2 15 | }> 16 | 17 | export const Glitch = /* @__PURE__ */ forwardRef(function Glitch( 18 | { active = true, ...props }: GlitchProps, 19 | ref: Ref 20 | ) { 21 | const invalidate = useThree((state) => state.invalidate) 22 | const delay = useVector2(props, 'delay') 23 | const duration = useVector2(props, 'duration') 24 | const strength = useVector2(props, 'strength') 25 | const chromaticAberrationOffset = useVector2(props, 'chromaticAberrationOffset') 26 | const effect = useMemo( 27 | () => new GlitchEffect({ ...props, delay, duration, strength, chromaticAberrationOffset }), 28 | [delay, duration, props, strength, chromaticAberrationOffset] 29 | ) 30 | useLayoutEffect(() => { 31 | effect.mode = active ? props.mode || GlitchMode.SPORADIC : GlitchMode.DISABLED 32 | invalidate() 33 | }, [active, effect, invalidate, props.mode]) 34 | useEffect(() => { 35 | return () => { 36 | effect.dispose?.() 37 | } 38 | }, [effect]) 39 | return 40 | }) 41 | -------------------------------------------------------------------------------- /src/effects/SSAO.tsx: -------------------------------------------------------------------------------- 1 | import { Ref, forwardRef, useContext, useMemo } from 'react' 2 | import { SSAOEffect, BlendFunction } from 'postprocessing' 3 | import { EffectComposerContext } from '../EffectComposer' 4 | 5 | // first two args are camera and texture 6 | type SSAOProps = ConstructorParameters[2] 7 | 8 | export const SSAO = /* @__PURE__ */ forwardRef(function SSAO( 9 | props: SSAOProps, 10 | ref: Ref 11 | ) { 12 | const { camera, normalPass, downSamplingPass, resolutionScale } = useContext(EffectComposerContext) 13 | const effect = useMemo(() => { 14 | if (normalPass === null && downSamplingPass === null) { 15 | console.error('Please enable the NormalPass in the EffectComposer in order to use SSAO.') 16 | return {} 17 | } 18 | return new SSAOEffect(camera, normalPass && !downSamplingPass ? (normalPass as any).texture : null, { 19 | blendFunction: BlendFunction.MULTIPLY, 20 | samples: 30, 21 | rings: 4, 22 | distanceThreshold: 1.0, 23 | distanceFalloff: 0.0, 24 | rangeThreshold: 0.5, 25 | rangeFalloff: 0.1, 26 | luminanceInfluence: 0.9, 27 | radius: 20, 28 | bias: 0.5, 29 | intensity: 1.0, 30 | color: undefined, 31 | // @ts-ignore 32 | normalDepthBuffer: downSamplingPass ? downSamplingPass.texture : null, 33 | resolutionScale: resolutionScale ?? 1, 34 | depthAwareUpsampling: true, 35 | ...props, 36 | }) 37 | // NOTE: `props` is an unstable reference, so we can't memoize it 38 | // eslint-disable-next-line react-hooks/exhaustive-deps 39 | }, [camera, downSamplingPass, normalPass, resolutionScale]) 40 | return 41 | }) 42 | -------------------------------------------------------------------------------- /src/Selection.tsx: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import React, { createContext, useState, useContext, useEffect, useRef, useMemo } from 'react' 3 | import { type ThreeElements } from '@react-three/fiber' 4 | 5 | export type Api = { 6 | selected: THREE.Object3D[] 7 | select: React.Dispatch> 8 | enabled: boolean 9 | } 10 | export type SelectApi = Omit & { 11 | enabled?: boolean 12 | } 13 | 14 | export const selectionContext = /* @__PURE__ */ createContext(null) 15 | 16 | export function Selection({ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }) { 17 | const [selected, select] = useState([]) 18 | const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled]) 19 | return {children} 20 | } 21 | 22 | export function Select({ enabled = false, children, ...props }: SelectApi) { 23 | const group = useRef(null!) 24 | const api = useContext(selectionContext) 25 | useEffect(() => { 26 | if (api && enabled) { 27 | let changed = false 28 | const current: THREE.Object3D[] = [] 29 | group.current.traverse((o) => { 30 | o.type === 'Mesh' && current.push(o) 31 | if (api.selected.indexOf(o) === -1) changed = true 32 | }) 33 | if (changed) { 34 | api.select((state) => [...state, ...current]) 35 | return () => { 36 | api.select((state) => state.filter((selected) => !current.includes(selected))) 37 | } 38 | } 39 | } 40 | }, [enabled, children, api]) 41 | return ( 42 | 43 | {children} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /docs/effects/tone-mapping.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ToneMapping 3 | nav: 1 4 | --- 5 | 6 | A tone mapping effect. 7 | 8 | ```jsx 9 | import { ToneMapping } from '@react-three/postprocessing' 10 | import { BlendFunction } from 'postprocessing' 11 | 12 | return ( 13 | 22 | ) 23 | ``` 24 | 25 | ## Example 26 | 27 | 28 | 29 | ## Props 30 | 31 | | Name | Type | Default | Description | 32 | | ---------------- | ------------- | ------- | ------------------------------------------------------------------- | 33 | | resolution | Number | 256 | The resolution of the luminance texture. Must be a power of two. | 34 | | adaptive | boolean | true | Toggle adaptive luminance map usage | 35 | | blendFunction | BlendFunction | | The blend function of this effect. | 36 | | middleGrey | Number | 0.6 | The middle grey factor. | 37 | | maxLuminance | Number | 16 | Maximum luminance | 38 | | minLuminance | Number | 0.01 | The minimum luminance. Prevents very high exposure in dark scenes. | 39 | | averageLuminance | Number | 1 | The average luminance. Used for the non-adaptive Reinhard operator. | 40 | | adaptationRate | Number | 1 | The luminance adaptation rate. | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-three/postprocessing", 3 | "version": "3.0.4", 4 | "description": "postprocessing wrapper for React and @react-three/fiber", 5 | "keywords": [ 6 | "postprocessing", 7 | "react", 8 | "three", 9 | "@react-three/fiber", 10 | "webgl", 11 | "3d" 12 | ], 13 | "license": "MIT", 14 | "files": [ 15 | "dist", 16 | "src" 17 | ], 18 | "type": "module", 19 | "main": "./dist/index.js", 20 | "exports": "./dist/index.js", 21 | "sideEffects": false, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/pmndrs/react-postprocessing.git" 25 | }, 26 | "scripts": { 27 | "build": "rimraf dist && vite build && tsc", 28 | "eslint": "eslint . --fix --ext=js,ts,jsx,tsx", 29 | "eslint:ci": "eslint . --ext=js,ts,jsx,tsx", 30 | "test": "vitest run", 31 | "typecheck": "tsc --noEmit" 32 | }, 33 | "dependencies": { 34 | "maath": "^0.6.0", 35 | "n8ao": "^1.9.4", 36 | "postprocessing": "^6.36.6" 37 | }, 38 | "devDependencies": { 39 | "@react-three/fiber": "^9.0.4", 40 | "@types/node": "^22.10.7", 41 | "@types/react": "^19.0.2", 42 | "@types/react-dom": "^19.0.2", 43 | "@types/three": "^0.156.0", 44 | "@typescript-eslint/eslint-plugin": "^5.59.1", 45 | "@typescript-eslint/parser": "^5.59.1", 46 | "eslint": "^8.39.0", 47 | "eslint-config-prettier": "^8.8.0", 48 | "eslint-import-resolver-alias": "^1.1.2", 49 | "eslint-plugin-import": "^2.27.5", 50 | "eslint-plugin-prettier": "^4.2.1", 51 | "eslint-plugin-react": "^7.32.2", 52 | "eslint-plugin-react-hooks": "^4.6.0", 53 | "prettier": "^2.8.8", 54 | "react": "^19.0.0", 55 | "react-dom": "^19.0.0", 56 | "rimraf": "^6.0.1", 57 | "three": "^0.156.0", 58 | "typescript": "^5.0.4", 59 | "vite": "^4.3.5", 60 | "vitest": "^2.1.8" 61 | }, 62 | "peerDependencies": { 63 | "@react-three/fiber": "^9.0.0", 64 | "react": "^19.0", 65 | "three": ">= 0.156.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/util.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from 'react' 2 | import { Vector2 } from 'three' 3 | import * as THREE from 'three' 4 | import { type ReactThreeFiber, type ThreeElement, extend, useThree } from '@react-three/fiber' 5 | import type { Effect, Pass, BlendFunction } from 'postprocessing' 6 | 7 | export const resolveRef = (ref: T | React.RefObject) => 8 | typeof ref === 'object' && ref != null && 'current' in ref ? ref.current : ref 9 | 10 | export type EffectConstructor = new (...args: any[]) => Effect | Pass 11 | 12 | export type EffectProps = ThreeElement & 13 | ConstructorParameters[0] & { 14 | blendFunction?: BlendFunction 15 | opacity?: number 16 | } 17 | 18 | let i = 0 19 | const components = new WeakMap | string>() 20 | 21 | export const wrapEffect = (effect: T, defaults?: EffectProps) => 22 | /* @__PURE__ */ function Effect({ blendFunction = defaults?.blendFunction, opacity = defaults?.opacity, ...props }) { 23 | let Component = components.get(effect) 24 | if (!Component) { 25 | const key = `@react-three/postprocessing/${effect.name}-${i++}` 26 | extend({ [key]: effect }) 27 | components.set(effect, (Component = key)) 28 | } 29 | 30 | const camera = useThree((state) => state.camera) 31 | const args = React.useMemo( 32 | () => [...(defaults?.args ?? []), ...(props.args ?? [{ ...defaults, ...props }])], 33 | // eslint-disable-next-line react-hooks/exhaustive-deps 34 | [JSON.stringify(props)] 35 | ) 36 | 37 | return ( 38 | 45 | ) 46 | } 47 | 48 | export const useVector2 = (props: Record, key: string): THREE.Vector2 => { 49 | const value = props[key] as ReactThreeFiber.Vector2 | undefined 50 | return React.useMemo(() => { 51 | if (typeof value === 'number') return new THREE.Vector2(value, value) 52 | else if (value) return new THREE.Vector2(...(value as THREE.Vector2Tuple)) 53 | else return new THREE.Vector2() 54 | }, [value]) 55 | } 56 | -------------------------------------------------------------------------------- /src/effects/N8AO.tsx: -------------------------------------------------------------------------------- 1 | // From https://github.com/N8python/n8ao 2 | // https://twitter.com/N8Programs/status/1660996748485984261 3 | 4 | import { Ref, forwardRef, useLayoutEffect, useMemo } from 'react' 5 | /* @ts-ignore */ 6 | import { N8AOPostPass } from 'n8ao' 7 | import { useThree, ReactThreeFiber, applyProps } from '@react-three/fiber' 8 | 9 | export type N8AOProps = { 10 | aoRadius?: number 11 | distanceFalloff?: number 12 | intensity?: number 13 | quality?: 'performance' | 'low' | 'medium' | 'high' | 'ultra' 14 | aoSamples?: number 15 | denoiseSamples?: number 16 | denoiseRadius?: number 17 | color?: ReactThreeFiber.Color 18 | halfRes?: boolean 19 | depthAwareUpsampling?: boolean 20 | screenSpaceRadius?: boolean 21 | renderMode?: 0 | 1 | 2 | 3 | 4 22 | } 23 | 24 | export const N8AO = /* @__PURE__ */ forwardRef( 25 | ( 26 | { 27 | halfRes, 28 | screenSpaceRadius, 29 | quality, 30 | depthAwareUpsampling = true, 31 | aoRadius = 5, 32 | aoSamples = 16, 33 | denoiseSamples = 4, 34 | denoiseRadius = 12, 35 | distanceFalloff = 1, 36 | intensity = 1, 37 | color, 38 | renderMode = 0, 39 | }, 40 | ref: Ref 41 | ) => { 42 | const { camera, scene } = useThree() 43 | const effect = useMemo(() => new N8AOPostPass(scene, camera), [camera, scene]) 44 | 45 | // TODO: implement dispose upstream; this effect has memory leaks without 46 | useLayoutEffect(() => { 47 | applyProps(effect.configuration, { 48 | color, 49 | aoRadius, 50 | distanceFalloff, 51 | intensity, 52 | aoSamples, 53 | denoiseSamples, 54 | denoiseRadius, 55 | screenSpaceRadius, 56 | renderMode, 57 | halfRes, 58 | depthAwareUpsampling, 59 | }) 60 | }, [ 61 | screenSpaceRadius, 62 | color, 63 | aoRadius, 64 | distanceFalloff, 65 | intensity, 66 | aoSamples, 67 | denoiseSamples, 68 | denoiseRadius, 69 | renderMode, 70 | halfRes, 71 | depthAwareUpsampling, 72 | effect, 73 | ]) 74 | 75 | useLayoutEffect(() => { 76 | if (quality) effect.setQualityMode(quality.charAt(0).toUpperCase() + quality.slice(1)) 77 | }, [effect, quality]) 78 | 79 | return 80 | } 81 | ) 82 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "shared-node-browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": [ 9 | "prettier", 10 | "plugin:prettier/recommended", 11 | "plugin:react-hooks/recommended", 12 | "plugin:import/errors", 13 | "plugin:import/warnings" 14 | ], 15 | "plugins": ["@typescript-eslint", "react", "prettier", "react-hooks", "import"], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module", 20 | "ecmaFeatures": { 21 | "jsx": true 22 | }, 23 | "rules": { 24 | "curly": ["warn", "multi-line", "consistent"], 25 | "no-console": "off", 26 | "no-empty-pattern": "warn", 27 | "no-duplicate-imports": "error", 28 | "import/no-unresolved": ["error", { "commonjs": true, "amd": true }], 29 | "import/export": "error", 30 | // https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md#eslint-plugin-import 31 | // We recommend you do not use the following import/* rules, as TypeScript provides the same checks as part of standard type checking: 32 | "import/named": "off", 33 | "import/namespace": "off", 34 | "import/default": "off", 35 | "@typescript-eslint/explicit-module-boundary-types": "off", 36 | "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 37 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 38 | "@typescript-eslint/no-use-before-define": "off", 39 | "@typescript-eslint/no-empty-function": "off", 40 | "@typescript-eslint/no-empty-interface": "off", 41 | "@typescript-eslint/no-explicit-any": "off" 42 | } 43 | }, 44 | "settings": { 45 | "react": { 46 | "version": "detect" 47 | }, 48 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 49 | "import/parsers": { 50 | "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] 51 | }, 52 | "import/resolver": { 53 | "node": { 54 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 55 | "paths": ["src"] 56 | }, 57 | "alias": { 58 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 59 | "map": [["@react-three/postprocessing", "./src/index.tsx"]] 60 | } 61 | } 62 | }, 63 | "overrides": [ 64 | { 65 | "files": ["src"], 66 | "parserOptions": { 67 | "project": "./tsconfig.json" 68 | } 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /docs/effects/glitch.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Glitch 3 | nav: 1 4 | --- 5 | 6 | A glitch effect. 7 | 8 | ```jsx 9 | import { Glitch } from '@react-three/postprocessing' 10 | import { GlitchMode } from 'postprocessing' 11 | 12 | return ( 13 | 21 | ) 22 | ``` 23 | 24 | ## Example 25 | 26 | 27 | 28 | ## Props 29 | 30 | | Name | Type | Default | Description | 31 | | ------------------------- | ------------- | -------------------- | ----------------------------------------------------------------------------------------- | 32 | | active | Boolean | true | Turn the effect on and off | 33 | | blendFunction | BlendFunction | BlendFunction.NORMAL | The blend function of this effect. | 34 | | chromaticAberrationOffset | Vector2 | | A chromatic aberration offset. If provided, the glitch effect will influence this offset. | 35 | | delay | Vector2 | | The minimum and maximum delay between glitch activations in seconds. | 36 | | duration | Vector2 | | The minimum and maximum duration of a glitch in seconds. | 37 | | strength | Vector2 | | The strength of weak and strong glitches. | 38 | | perturbationMap | Texture | | A perturbation map. If none is provided, a noise texture will be created. | 39 | | dtSize | Number | 64 | The size of the generated noise map. Will be ignored if a perturbation map is provided. | 40 | | columns | Number | 0.05 | The scale of the blocky glitch columns. | 41 | | ratio | Number | 0.85 | The threshold for strong glitches. | 42 | -------------------------------------------------------------------------------- /src/effects/DepthOfField.tsx: -------------------------------------------------------------------------------- 1 | import { DepthOfFieldEffect, MaskFunction } from 'postprocessing' 2 | import { Ref, forwardRef, useMemo, useEffect, useContext } from 'react' 3 | import { ReactThreeFiber } from '@react-three/fiber' 4 | import { type DepthPackingStrategies, type Texture, Vector3 } from 'three' 5 | import { EffectComposerContext } from '../EffectComposer' 6 | 7 | type DOFProps = ConstructorParameters[1] & 8 | Partial<{ 9 | target: ReactThreeFiber.Vector3 10 | depthTexture: { 11 | texture: Texture 12 | // TODO: narrow to DepthPackingStrategies 13 | packing: number 14 | } 15 | // TODO: not used 16 | blur: number 17 | }> 18 | 19 | export const DepthOfField = /* @__PURE__ */ forwardRef(function DepthOfField( 20 | { 21 | blendFunction, 22 | worldFocusDistance, 23 | worldFocusRange, 24 | focusDistance, 25 | focusRange, 26 | focalLength, 27 | bokehScale, 28 | resolutionScale, 29 | resolutionX, 30 | resolutionY, 31 | width, 32 | height, 33 | target, 34 | depthTexture, 35 | ...props 36 | }: DOFProps, 37 | ref: Ref 38 | ) { 39 | const { camera } = useContext(EffectComposerContext) 40 | const autoFocus = target != null 41 | const effect = useMemo(() => { 42 | const effect = new DepthOfFieldEffect(camera, { 43 | blendFunction, 44 | worldFocusDistance, 45 | worldFocusRange, 46 | focusDistance, 47 | focusRange, 48 | focalLength, 49 | bokehScale, 50 | resolutionScale, 51 | resolutionX, 52 | resolutionY, 53 | width, 54 | height, 55 | }) 56 | // Creating a target enables autofocus, R3F will set via props 57 | if (autoFocus) effect.target = new Vector3() 58 | // Depth texture for depth picking with optional packing strategy 59 | if (depthTexture) effect.setDepthTexture(depthTexture.texture, depthTexture.packing as DepthPackingStrategies) 60 | // Temporary fix that restores DOF 6.21.3 behavior, everything since then lets shapes leak through the blur 61 | const maskPass = (effect as any).maskPass 62 | maskPass.maskFunction = MaskFunction.MULTIPLY_RGB_SET_ALPHA 63 | return effect 64 | }, [ 65 | camera, 66 | blendFunction, 67 | worldFocusDistance, 68 | worldFocusRange, 69 | focusDistance, 70 | focusRange, 71 | focalLength, 72 | bokehScale, 73 | resolutionScale, 74 | resolutionX, 75 | resolutionY, 76 | width, 77 | height, 78 | autoFocus, 79 | depthTexture, 80 | ]) 81 | 82 | useEffect(() => { 83 | return () => { 84 | effect.dispose() 85 | } 86 | }, [effect]) 87 | 88 | return 89 | }) 90 | -------------------------------------------------------------------------------- /docs/effects/ramp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ramp 3 | nav: 1 4 | --- 5 | 6 | Ramp effect for linear and radial color gradients, as well as masking of effects before it in the effect array. 7 | 8 | ```jsx 9 | import { Ramp, RampType } from '@react-three/postprocessing' 10 | 11 | return ( 12 | 23 | ) 24 | ``` 25 | 26 | ## Example 27 | 28 | 29 | 30 | ## Props 31 | 32 | | Name | Type | Default | Description | 33 | | ---------- | -------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------- | 34 | | rampType | RampType | RampType.Linear | Type of ramp gradient. | 35 | | rampStart | [x: number, y: number] | [0.5, 0.5] | Starting point of the ramp gradient in normalized coordinates. | 36 | | rampEnd | [x: number, y: number] | [1.0, 1.0] | Ending point of the ramp gradient in normalized coordinates. | 37 | | startColor | [r: number, g: number, b: number, a: number] | [0, 0, 0, 1] | Color at the starting point of the gradient. | 38 | | endColor | [r: number, g: number, b: number, a: number] | [1, 1, 1, 1] | Color at the ending point of the gradient. | 39 | | rampBias | number | 0.5 | Bias for the interpolation curve when both bias and gain are 0.5. | 40 | | rampGain | number | 0.5 | Gain for the interpolation curve when both bias and gain are 0.5. | 41 | | rampMask | boolean | false | When enabled, the ramp gradient is used as an effect mask, and colors are ignored. | 42 | | rampInvert | boolean | false | Controls whether the ramp gradient is inverted. When disabled, rampStart is transparent and rampEnd is opaque. | 43 | -------------------------------------------------------------------------------- /docs/effects/outline.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Outline 3 | nav: 1 4 | --- 5 | 6 | An outline effect. 7 | 8 | ```jsx 9 | import { Outline } from '@react-three/postprocessing' 10 | import { BlendFunction, Resizer, KernelSize } from 'postprocessing' 11 | 12 | return ( 13 |