Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tonemapping effect #148

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default defineConfig({
{ text: 'Glitch', link: '/guide/pmndrs/glitch' },
{ text: 'Noise', link: '/guide/pmndrs/noise' },
{ text: 'Outline', link: '/guide/pmndrs/outline' },
{ text: 'ToneMapping', link: '/guide/pmndrs/tonemapping' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{ text: 'ToneMapping', link: '/guide/pmndrs/tonemapping' },
{ text: 'Tone Mapping', link: '/guide/pmndrs/tonemapping' },

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified at next commit

{ text: 'Pixelation', link: '/guide/pmndrs/pixelation' },
{ text: 'Vignette', link: '/guide/pmndrs/vignette' },
],
Expand Down
75 changes: 75 additions & 0 deletions docs/.vitepress/theme/components/pmdrs/ToneMappingDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script setup lang="ts">
import { ContactShadows, Environment, Levioso, OrbitControls, useGLTF } from '@tresjs/cientos'
import { dispose, TresCanvas } from '@tresjs/core'
import { TresLeches, useControls } from '@tresjs/leches'
import { EffectComposer, ToneMapping } from '@tresjs/post-processing/pmndrs'
import { ToneMappingMode } from 'postprocessing'
import { NoToneMapping } from 'three'
import { onUnmounted, shallowRef } from 'vue'

import '@tresjs/leches/styles'

const gl = {
toneMappingExposure: 1,
toneMapping: NoToneMapping,
multisampling: 8,
}

const modelRef = shallowRef(null)

const { scene: model } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/realistic-pokeball/scene.gltf', { draco: true })

const { toneMappingExposure, mode } = useControls({
toneMappingExposure: {
value: 1,
min: 0,
max: 10,
step: 1,
},
mode: {
options: Object.keys(ToneMappingMode).map(key => ({
text: key,
value: ToneMappingMode[key],
})),
value: ToneMappingMode.AGX,
},
})

onUnmounted(() => {
dispose(model)
})
</script>

<template>
<TresLeches style="left: initial;right:10px; top:10px;" />

<TresCanvas
v-bind="gl"
:toneMappingExposure="toneMappingExposure.value"
>
<TresPerspectiveCamera
:position="[6.5, 6.5, 6.5]"
:look-at="[0, 1, 0]"
/>
<OrbitControls />

<Levioso :speed=".5" :rotation-factor=".5" :float-factor="2" :range="[0, .5]">
<primitive ref="modelRef" :object="model" :position-y="-.5" :scale=".25" />
</Levioso>

<Suspense>
<Environment background :blur=".35" preset="dawn" />
</Suspense>

<ContactShadows
:opacity=".5"
:position-y="-3.25"
/>

<Suspense>
<EffectComposer>
<ToneMapping :mode="Number(mode.value)" />
</EffectComposer>
</Suspense>
</TresCanvas>
</template>
1 change: 1 addition & 0 deletions docs/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SMAAThreeDemo: typeof import('./.vitepress/theme/components/three/SMAAThreeDemo.vue')['default']
ToneMappingDemo: typeof import('./.vitepress/theme/components/pmdrs/ToneMappingDemo.vue')['default']
UnrealBloomThreeDemo: typeof import('./.vitepress/theme/components/three/UnrealBloomThreeDemo.vue')['default']
VignetteDemo: typeof import('./.vitepress/theme/components/pmdrs/VignetteDemo.vue')['default']
}
Expand Down
71 changes: 71 additions & 0 deletions docs/guide/pmndrs/tonemapping.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I suggest calling this file tone-mapping.md to be uniform with the others (depth-of-field.md for example)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified at next commit

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ToneMapping

<DocsDemo>
<ToneMappingDemo />
</DocsDemo>

The `ToneMapping` effect from the [`postprocessing`](https://pmndrs.github.io/postprocessing/public/docs/class/src/effects/ToneMappingEffect.js~ToneMappingEffect.html) package provides an abstraction for various tone mapping algorithms to improve the visual rendering of HDR (high dynamic range) content. Tone mapping is used to map high-range brightness values to a range that is displayable on standard screens. This effect contributes significantly to the visual quality of your scene by controlling luminance and color balance.

::: info
If the colors in your scene look incorrect after adding the EffectComposer, it might be because tone mapping is deactivated by default, which is normal behavior. Add `<ToneMapping>` manually as an effect at the end of the `<EffectComposer>` to fix this.
:::

## Usage

The `<ToneMapping>` component is easy to set up and comes with multiple tone mapping modes to suit different visual requirements. Below is an example of how to use it in a Vue application.

```vue{2,4,7-8,32-36}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: While I like to have a full example on the page, I think it is better to stay uniform with the other effects and only show the usage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a shame that we don't produce slightly advanced demos to present concrete use cases.

What do you think @alvarosabu ?

<script setup lang="ts">
import { EffectComposer, ToneMapping } from '@tresjs/post-processing/pmndrs'
import { onUnmounted, shallowRef } from 'vue'
import { ToneMappingMode } from 'postprocessing'

const gl = {
toneMappingExposure: 1,
toneMapping: NoToneMapping,
multisampling: 8,
}

const modelRef = shallowRef(null)

const { scene: model } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/realistic-pokeball/scene.gltf', { draco: true })

onUnmounted(() => {
dispose(modelRef.value)
})
</script>

<template>
<TresCanvas
v-bind="gl"
>
<TresPerspectiveCamera
:position="[5, 5, 5]"
:look-at="[0, 0, 0]"
/>

<primitive ref="modelRef" :object="model" />

<Suspense>
<EffectComposer>
<ToneMapping :mode="ToneMappingMode.AGX" />
</EffectComposer>
</Suspense>
</TresCanvas>
</template>
```

## Props

| Prop | Description | Default |
| ----------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| mode | Tone mapping mode used, defined by [`ToneMappingMode`](https://pmndrs.github.io/postprocessing/public/docs/variable/index.html#static-variable-ToneMappingMode). | `ToneMappingMode.AGX` |
| blendFunction | Defines the [`BlendFunction`](https://pmndrs.github.io/postprocessing/public/docs/variable/index.html#static-variable-BlendFunction) used for the effect. | `BlendFunction.SRC` |
| resolution | Resolution of the luminance texture (must be a power of two, e.g., 256, 512, etc.). | `256` |
| averageLuminance | Average luminance value used in adaptive calculations. Only applicable to `ToneMappingMode.REINHARD2` | `1.0` |
| middleGrey | Factor to adjust the balance of grey in luminance calculations. Only applicable to `ToneMappingMode.REINHARD2` | `0.6` |
| minLuminance | Lower luminance limit, used to avoid overexposure effects in dark scenes. Only applicable to `ToneMappingMode.REINHARD2` | `0.01` |
| whitePoint | White point for tone mapping, used to balance luminance values. Only applicable to `ToneMappingMode.REINHARD2` | `4.0` |

## Further Reading
see [postprocessing docs](https://pmndrs.github.io/postprocessing/public/docs/class/src/effects/ToneMappingEffect.js~ToneMappingEffect.html)
1 change: 0 additions & 1 deletion playground/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ declare module 'vue' {
export interface GlobalComponents {
BasicScene: typeof import('./src/components/BasicScene.vue')['default']
BlenderCube: typeof import('./src/components/BlenderCube.vue')['default']
copy: typeof import('./src/components/UnrealBloom copy.vue')['default']
Ducky: typeof import('./src/components/Ducky.vue')['default']
EffectListItem: typeof import('./src/components/EffectListItem.vue')['default']
GlitchDemo: typeof import('./src/components/GlitchDemo.vue')['default']
Expand Down
77 changes: 77 additions & 0 deletions playground/src/pages/postprocessing/tonemapping.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script setup lang="ts">
import { ContactShadows, Environment, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { TresLeches, useControls } from '@tresjs/leches'
import { EffectComposer, ToneMapping } from '@tresjs/post-processing/pmndrs'
import { BlendFunction, ToneMappingMode } from 'postprocessing'
import { NoToneMapping } from 'three'

import '@tresjs/leches/styles'

const gl = {
clearColor: 'white',
toneMapping: NoToneMapping,
multisampling: 8,
}

const { blendFunction, resolution, mode } = useControls({
mode: {
options: Object.keys(ToneMappingMode).map(key => ({
text: key,
value: ToneMappingMode[key],
})),
value: ToneMappingMode.AGX,
},
resolution: {
value: 256,
options: [
{ text: '128', value: 128 },
{ text: '256', value: 256 },
{ text: '512', value: 512 },
{ text: '1024', value: 1024 },
{ text: '2048', value: 2048 },
],
},
blendFunction: {
options: Object.keys(BlendFunction).map(key => ({
text: key,
value: BlendFunction[key],
})),
value: BlendFunction.OVERLAY,
},
})
</script>

<template>
<TresLeches />

<TresCanvas
v-bind="gl"
>
<TresPerspectiveCamera
:position="[3, 3, 3]"
:look-at="[0, 0, 0]"
/>
<OrbitControls auto-rotate />

<TresMesh>
<TresBoxGeometry />
<TresMeshPhysicalMaterial color="#FFFFFF" :roughness=".25" :transmission=".85" />
</TresMesh>

<Suspense>
<Environment background :blur=".85" preset="dawn" />
</Suspense>

<ContactShadows
:opacity=".5"
:position-y="-.5"
/>

<Suspense>
<EffectComposer>
<ToneMapping :mode="Number(mode.value)" :resolution="Number(resolution.value)" :blendFunction="Number(blendFunction.value)" />
</EffectComposer>
</Suspense>
</TresCanvas>
</template>
1 change: 1 addition & 0 deletions playground/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const threeRoutes = [

export const postProcessingRoutes = [
makeRoute('Outline', '🔲', false),
makeRoute('Tonemapping', '🎨', false),
makeRoute('Glitch', '📺', false),
makeRoute('Depth of Field', '📷', false),
makeRoute('Pixelation', '👾', false),
Expand Down
79 changes: 79 additions & 0 deletions src/core/pmndrs/ToneMapping.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script lang="ts" setup>
import { BlendFunction, ToneMappingEffect, ToneMappingMode } from 'postprocessing'
import { defineExpose, defineProps, watchEffect, withDefaults } from 'vue'
import { useEffect } from './composables/useEffect'
import { makePropWatchers } from '../../util/prop'

export interface ToneMappingProps {
/**
* The tone mapping mode.
*/
mode?: ToneMappingMode

/**
* The blend function.
*/
blendFunction?: BlendFunction

/**
* The resolution for luminance texture. The resolution of the luminance texture. Must be a power of two.
*/
resolution?: number

/**
* The average luminance. Only for `REINHARD2`.
*/
averageLuminance?: number

/**
* The middle grey factor. Only for `REINHARD2`.
*/
middleGrey?: number

/**
* The minimum luminance. Only for `REINHARD2`.
*/
minLuminance?: number

/**
* The white point. Only for `REINHARD2`.
*/
whitePoint?: number
}

const props = withDefaults(
defineProps<ToneMappingProps>(),
{
mode: ToneMappingMode.AGX,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: We set defaults only in very rare cases to minimize maintenance effort when postprocessing changes the internals of their effects. I suggest omitting these.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified at next commit

blendFunction: BlendFunction.SRC,
resolution: 256,
averageLuminance: 1.0,
middleGrey: 0.6,
minLuminance: 0.01,
whitePoint: 4.0,
},
)

const { pass, effect } = useEffect(() => new ToneMappingEffect(props), props)

defineExpose({ pass, effect })

watchEffect(() => {
if (!effect.value) { return }

effect.value.blendMode.blendFunction = Number(props.blendFunction)
})

makePropWatchers(
[
[() => props.mode, 'mode'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: You can omit the seperate watcher like so:

Suggested change
watchEffect(() => {
if (!effect.value) { return }
effect.value.blendMode.blendFunction = Number(props.blendFunction)
})
makePropWatchers(
[
[() => props.mode, 'mode'],
makePropWatchers(
[
[() => props.blendFunction, 'blendMode.blendFunction'],
[() => props.mode, 'mode'],

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified at next commit

[() => props.resolution, 'resolution'],
[() => props.averageLuminance, 'averageLuminance'],
[() => props.middleGrey, 'middleGrey'],
[() => props.minLuminance, 'minLuminance'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I think this has to be like so:

Suggested change
[() => props.minLuminance, 'minLuminance'],
[() => props.minLuminance, 'adaptiveLuminanceMaterial.minLuminance'],

minLuminance is not a property of effect.value. These paths are used to update the effect when the props change. So we need the exact location of the property.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! Small mistake on my part! Modified at next commit

[() => props.whitePoint, 'whitePoint'],
],
effect,
() => new ToneMappingEffect(),
)
</script>
3 changes: 3 additions & 0 deletions src/core/pmndrs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Noise, { type NoiseProps } from './Noise.vue'
import Outline, { type OutlineProps } from './Outline.vue'
import Pixelation, { type PixelationProps } from './Pixelation.vue'
import Vignette, { type VignetteProps } from './Vignette.vue'
import ToneMapping, { type ToneMappingProps } from './ToneMapping.vue'

export {
Bloom,
Expand All @@ -20,6 +21,7 @@ export {
Pixelation,
useEffect,
Vignette,
ToneMapping,

BloomProps,
DepthOfFieldProps,
Expand All @@ -29,4 +31,5 @@ export {
OutlineProps,
PixelationProps,
VignetteProps,
ToneMappingProps,
}