Skip to content
8 changes: 4 additions & 4 deletions docs/API/canvas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const App = () => (
| resize | Resize config, see react-use-measure's options | `{ scroll: true, debounce: { scroll: 50, resize: 0 } }` |
| orthographic | Creates an orthographic camera | `false` |
| dpr | Pixel-ratio, use `window.devicePixelRatio`, or automatic: [min, max] | `[1, 2]` |
| legacy | Enables THREE.ColorManagement.legacyMode in three r139 or later | `false` |
| linear | Switch off automatic sRGB encoding and gamma correction | `false` |
| legacy | Enables THREE.ColorManagement in three r139 or later | `false` |
| linear | Switch off automatic sRGB color space and gamma correction | `false` |
| events | Configuration for the event manager, as a function of state | `import { events } from "@react-three/fiber"` |
| eventSource | The source where events are being subscribed to, HTMLElement | `React.MutableRefObject<HTMLElement>`, `gl.domElement.parentNode` |
| eventPrefix | The event prefix that is cast into canvas pointer x/y events | `offset` |
Expand All @@ -54,7 +54,7 @@ Canvas uses [createRoot](#createroot) which will create a translucent `THREE.Web

and with the following properties:

- outputEncoding = THREE.sRGBEncoding
- outputColorSpace = THREE.SRGBColorSpace
- toneMapping = THREE.ACESFilmicToneMapping

It will also create the following scene internals:
Expand All @@ -64,7 +64,7 @@ It will also create the following scene internals:
- A `THREE.PCFSoftShadowMap` if `shadows` is true
- A `THREE.Scene` (into which all the JSX is rendered) and a `THREE.Raycaster`

In recent versions of threejs, `THREE.ColorManagement.legacy` will be set to false to enable automatic conversion of colors according to the renderer's configured color space. R3F will handle texture encoding conversion. For more on this topic, see [https://threejs.org/docs/#manual/en/introduction/Color-management](https://threejs.org/docs/#manual/en/introduction/Color-management).
In recent versions of threejs, `THREE.ColorManagement.enabled` will be set to `true` to enable automatic conversion of colors according to the renderer's configured color space. R3F will handle texture color space conversion. For more on this topic, see [https://threejs.org/docs/#manual/en/introduction/Color-management](https://threejs.org/docs/#manual/en/introduction/Color-management).

## Custom Canvas

Expand Down
7 changes: 3 additions & 4 deletions docs/tutorials/v8-migration-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ This was the most common setting in the wild, so it was brought in as a better d

## Color management

Color management is now being handled by Three R139. Therefore we set `THREE.ColorManagement.legacyMode` to `false` and cede to touch colors and textures since everything will now be converted from sRGB to linear space by Three itself.
Color management is now being handled by Three R139. Therefore we set `THREE.ColorManagement.enabled` to `true` and cede to touch colors and textures since everything will now be converted from sRGB to linear color space by Three itself.

You can manipulate this yourself with the `legacy` prop:

```jsx
// sets THREE.ColorManagement.legacyMode = true
// sets THREE.ColorManagement.enabled = false
<Canvas legacy={true}>
```

Expand Down Expand Up @@ -217,8 +217,7 @@ This is also supported by all cameras that you create, be it a THREE.Perspective

```jsx
import { PerspectiveCamera } from '@react-three/drei'

<Canvas>
;<Canvas>
<PerspectiveCamera makeDefault manual />
</Canvas>
```
Expand Down
18 changes: 13 additions & 5 deletions packages/fiber/src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
Camera,
updateCamera,
getColorManagement,
hasColorSpace,
} from './utils'
import { useStore } from './hooks'
import type { Properties } from '../three-types'
Expand Down Expand Up @@ -62,7 +63,7 @@ export type RenderProps<TCanvas extends Canvas> = {
* @see https://threejs.org/docs/#manual/en/introduction/Color-management
*/
legacy?: boolean
/** Switch off automatic sRGB encoding and gamma correction */
/** Switch off automatic sRGB color space and gamma correction */
linear?: boolean
/** Use `THREE.NoToneMapping` instead of `THREE.ACESFilmicToneMapping` */
flat?: boolean
Expand Down Expand Up @@ -308,10 +309,17 @@ function createRoot<TCanvas extends Canvas>(canvas: TCanvas): ReconcilerRoot<TCa
if ('enabled' in ColorManagement) ColorManagement.enabled = !legacy
else if ('legacyMode' in ColorManagement) ColorManagement.legacyMode = legacy
}
const outputEncoding = linear ? THREE.LinearEncoding : THREE.sRGBEncoding
const toneMapping = flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping
if (gl.outputEncoding !== outputEncoding) gl.outputEncoding = outputEncoding
if (gl.toneMapping !== toneMapping) gl.toneMapping = toneMapping

// Set color space and tonemapping preferences
const LinearEncoding = 3000
const sRGBEncoding = 3001
applyProps(
gl as any,
{
outputEncoding: linear ? LinearEncoding : sRGBEncoding,
toneMapping: flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping,
} as Partial<Properties<THREE.WebGLRenderer>>,
)

// Update color management state
if (state.legacy !== legacy) state.set(() => ({ legacy }))
Expand Down
4 changes: 2 additions & 2 deletions packages/fiber/src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ export type RootState = {
pointer: THREE.Vector2
/** @deprecated Normalized event coordinates, use "pointer" instead! */
mouse: THREE.Vector2
/* Whether to enable r139's THREE.ColorManagement.legacyMode */
/* Whether to enable r139's THREE.ColorManagement */
legacy: boolean
/** Shortcut to gl.outputEncoding = LinearEncoding */
/** Shortcut to gl.outputColorSpace = THREE.LinearSRGBColorSpace */
linear: boolean
/** Shortcut to gl.toneMapping = NoTonemapping */
flat: boolean
Expand Down
35 changes: 32 additions & 3 deletions packages/fiber/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import * as React from 'react'
import { UseBoundStore } from 'zustand'
import { EventHandlers } from './events'
import { AttachType, catalogue, Instance, InstanceProps, LocalState } from './renderer'
import { Dpr, RootState, Size } from './store'
import { Dpr, Renderer, RootState, Size } from './store'

/**
* Returns `true` with correct TS type inference if an object has a configurable color space (since r152).
*/
export const hasColorSpace = <
T extends Renderer | THREE.Texture | object,
P = T extends Renderer ? { outputColorSpace: string } : { colorSpace: string },
>(
object: T,
): object is T & P => 'colorSpace' in object || 'outputColorSpace' in object

export type ColorManagementRepresentation = { enabled: boolean | never } | { legacyMode: boolean | never }

Expand Down Expand Up @@ -280,6 +290,23 @@ export function applyProps(instance: Instance, data: InstanceProps | DiffSet) {

for (let i = 0; i < changes.length; i++) {
let [key, value, isEvent, keys] = changes[i]

// Alias (output)encoding => (output)colorSpace (since r152)
// https://github.com/pmndrs/react-three-fiber/pull/2829
if (hasColorSpace(instance)) {
const sRGBEncoding = 3001
const SRGBColorSpace = 'srgb'
const LinearSRGBColorSpace = 'srgb-linear'

if (key === 'encoding') {
key = 'colorSpace'
value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace
} else if (key === 'outputEncoding') {
key = 'outputColorSpace'
value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace
}
}

let currentInstance = instance
let targetProp = currentInstance[key]

Expand Down Expand Up @@ -355,16 +382,18 @@ export function applyProps(instance: Instance, data: InstanceProps | DiffSet) {
// Else, just overwrite the value
} else {
currentInstance[key] = value

// Auto-convert sRGB textures, for now ...
// https://github.com/pmndrs/react-three-fiber/issues/344
if (
!rootState.linear &&
currentInstance[key] instanceof THREE.Texture &&
// sRGB textures must be RGBA8 since r137 https://github.com/mrdoob/three.js/pull/23129
currentInstance[key].format === THREE.RGBAFormat &&
currentInstance[key].type === THREE.UnsignedByteType
) {
currentInstance[key].encoding = THREE.sRGBEncoding
const texture = currentInstance[key] as THREE.Texture
if (hasColorSpace(texture) && hasColorSpace(rootState.gl)) texture.colorSpace = rootState.gl.outputColorSpace
else texture.encoding = rootState.gl.outputEncoding
}
}

Expand Down
47 changes: 31 additions & 16 deletions packages/fiber/tests/core/renderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -740,25 +740,40 @@ describe('renderer', () => {
})

it('should respect color management preferences via gl', async () => {
let gl: THREE.WebGLRenderer = null!
await act(async () => {
gl = root
.configure({ gl: { outputEncoding: THREE.LinearEncoding, toneMapping: THREE.NoToneMapping } })
.render(<group />)
.getState().gl
})
const texture = new THREE.Texture() as THREE.Texture & { colorSpace?: string }
let key = 0
function Test() {
return <meshBasicMaterial key={key++} map={texture} />
}

expect(gl.outputEncoding).toBe(THREE.LinearEncoding)
expect(gl.toneMapping).toBe(THREE.NoToneMapping)
const LinearEncoding = 3000
const sRGBEncoding = 3001

await act(async () => {
gl = root
.configure({ flat: true, linear: true })
.render(<group />)
.getState().gl
})
expect(gl.outputEncoding).toBe(THREE.LinearEncoding)
let gl: THREE.WebGLRenderer & { outputColorSpace?: string } = null!
await act(async () => (gl = root.render(<Test />).getState().gl))
expect(gl.outputEncoding).toBe(sRGBEncoding)
expect(gl.toneMapping).toBe(THREE.ACESFilmicToneMapping)
expect(texture.encoding).toBe(sRGBEncoding)

await act(async () => root.configure({ linear: true, flat: true }).render(<Test />))
expect(gl.outputEncoding).toBe(LinearEncoding)
expect(gl.toneMapping).toBe(THREE.NoToneMapping)
expect(texture.encoding).toBe(LinearEncoding)

// Sets outputColorSpace since r152
const SRGBColorSpace = 'srgb'
const LinearSRGBColorSpace = 'srgb-linear'

gl.outputColorSpace ??= ''
texture.colorSpace ??= ''

await act(async () => root.configure({ linear: true }).render(<Test />))
expect(gl.outputColorSpace).toBe(LinearSRGBColorSpace)
expect(texture.colorSpace).toBe(LinearSRGBColorSpace)

await act(async () => root.configure({ linear: false }).render(<Test />))
expect(gl.outputColorSpace).toBe(SRGBColorSpace)
expect(texture.colorSpace).toBe(SRGBColorSpace)
})

it('should respect legacy prop', async () => {
Expand Down