Skip to content

Commit

Permalink
feat: update to R3F9 RC5, Drei 10 RC1, Next.js 15.1.6, add TSL uniform
Browse files Browse the repository at this point in the history
  • Loading branch information
verekia committed Feb 1, 2025
1 parent bc23053 commit e2f7bf7
Show file tree
Hide file tree
Showing 33 changed files with 584 additions and 2,155 deletions.
70 changes: 6 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Three.js WebGPU Ecosystem Integration Test Suite

This is a collection of tests that incrementally add complexity to the setup. Testing is done with Three.js **r173** (2025-01-31). All tests use **WebGPURenderer**, a call a **TSL** function, and a test of the graphics backend type used. With vanilla [Three.js](https://threejs.org/), [React Three Fiber](https://r3f.docs.pmnd.rs/), and [Threlte](https://threlte.xyz/).
This is a collection of tests that incrementally add complexity to the setup. Testing is done with Three.js **r173** (2025-01-31). All tests use **WebGPURenderer**, a **TSL** node, and a test of the graphics backend type used. With vanilla [Three.js](https://threejs.org/), [React Three Fiber](https://r3f.docs.pmnd.rs/), and [Threlte](https://threlte.xyz/).

## How to test

Expand All @@ -14,7 +14,7 @@ If you have Docker installed:
Otherwise, to test with your local Node.js version:

1. `npm i` (you might need `--legacy-peer-deps` React 19)
1. `npm i`
2. `npm run dev` to check how it works in development.
3. `npm run start` to check how it works in production.

Expand All @@ -38,31 +38,7 @@ A ✅ means the scene renders, and the project works in dev mode, and in product

- ⚠️ Importing a module with top-level await such as `three/examples/jsm/capabilities/WebGPU.js` requires a [Vite config change and causes warnings in Next.js](#top-level-await-issues).

- ⚠️ WebGPURenderer is initialized with WebGPUBackend before falling back to WebGLBackend. You should [await the init method](#testing-the-backend-type) before checking the backend type or if your wrapper such as R3F tries to render before the backend is initialized. With R3F, you can use `frameloop="never"` to delay the first render call. If you don't, you will get this [render warning](#r3f-render-called-before-backend-initialized-issue).

- ⚠️ Using React Three Fiber with React 19 requires installing with `npm i --legacy-peer-deps`.

### Removed test cases

The following test cases are less relevant now:

- `next15-app-r3f8-react18`: ❌ [`ReactCurrentOwner` error](#reactcurrentowner-issue)
- `next15-app-vanilla-react19`: ✅
- `next15-pages-r3f8-react18`: ✅ Unrelated Next.js [HMR warning](#hmr-appisrmanifest-issue)
- `next15-pages-r3f8-react19`: ❌ [`ReactCurrentOwner` error](#reactcurrentowner-issue)

## Renderer defaults

React Three Fiber normally creates a WebGLRenderer with [these defaults](https://r3f.docs.pmnd.rs/api/canvas#defaults) ([see the code](https://github.com/pmndrs/react-three-fiber/blob/261ee123fcb717be3bd3278f5b432156e1c7b483/packages/fiber/src/core/index.tsx#L113)). Same thing with [Threlte](https://github.com/threlte/threlte/blob/fbc229a52d0bab4a5a0b03b21a2e9849ff6764bc/packages/core/src/lib/lib/useRenderer.ts#L33). For a similar setup with WebGPURenderer, pass the same parameters to the WebGPURenderer constructor:

```js
const renderer = new WebGPURenderer({
canvas,
powerPreference: 'high-performance',
antialias: true,
alpha: true,
})
```
- ⚠️ WebGPURenderer is initialized with WebGPUBackend before falling back to WebGLBackend. You should [await the init method](#testing-the-backend-type) before checking the backend type or if your wrapper such as Threlte or Tres tries to render before the backend is initialized. With R3F, you can use `frameloop="never"` to delay the first render call. If you don't, you will get this [render warning](#render-called-before-backend-initialized-issue).

## Top-level Await issues

Expand Down Expand Up @@ -98,27 +74,6 @@ However, your target environment does not appear to support 'async/await'.
As a result, the code may not run as expected or may cause runtime errors.
```

### R3F render called before backend initialized issue

This warning is caused by using R3F with WebGPURenderer.

> ⚠️ `THREE.Renderer: .render() called before the backend is initialized. Try using .renderAsync() instead.`
There is a workaround:

```jsx
const [frameloop, setFrameloop] = useState('never')

<Canvas
frameloop={frameloop}
gl={(canvas) => {
const renderer = new WebGPURenderer({ canvas })
renderer.init().then(() => setFrameloop('always'))
return renderer
}}
/>
```

## SSR issues with Next.js and Node.js

Next.js uses Node.js to Server-Side Render pages on the server. When importing modules on the server, if those modules reference global browser objects like `window`, `document`, `self`, or `navigator` at the top level, you will get a compilation error. _Except_ for `navigator`, which got [added to Node.js 21](https://nodejs.org/en/blog/announcements/v21-release-announce#navigator-object-integration).
Expand Down Expand Up @@ -206,28 +161,15 @@ console.log(renderer.backend) // WebGPUBackend or WebGLBackend
With React Three Fiber:

```js
const [frameloop, setFrameloop] = useState('never')

<Canvas
frameloop={frameloop}
gl={(canvas) => {
const renderer = new WebGPURenderer({ canvas })
renderer.init().then(() => setFrameloop('always'))
gl={async (glProps) => {
const renderer = new WebGPURenderer(glProps)
await renderer.init()
return renderer
}}
/>
```

If checking the backend type is not critical (for example you just want to see which one is used when developing locally) you can use a `setTimeout` to keep things simple:

```js
setTimeout(() => {
console.log(
renderer.backend.isWebGPUBackend ? 'WebGPU Backend' : 'WebGL Backend'
)
}, 1000)
```

## Drei Compatibility

You should also expect to only be able to use a subset of [Drei](https://github.com/pmndrs/drei) and the Three.js ecosystem with WebGPU, since some libraries and composants are written in GLSL.
Expand Down
34 changes: 23 additions & 11 deletions next14-app-r3f8-react18/app/page.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
'use client'

import React, { useRef, useState, useEffect } from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import React, { useMemo, useRef, useState } from 'react'
import { Canvas, useFrame, useThree, extend } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import { WebGPURenderer } from 'three/webgpu'
import * as TSL from 'three/tsl'
import { WebGPURenderer, MeshStandardNodeMaterial } from 'three/webgpu'
import { uniform } from 'three/tsl'
import { Color } from 'three'

extend({ MeshStandardNodeMaterial })

const red = new Color('red')
const blue = new Color('blue')

function Box(props) {
const meshRef = useRef()
Expand All @@ -14,12 +20,12 @@ function Box(props) {

useFrame((state, delta) => (meshRef.current.rotation.x += delta))

useEffect(() => {
console.log(TSL.sqrt(2))
}, [])

console.log(gl.backend.isWebGPUBackend ? 'WebGPU Backend' : 'WebGL Backend')

const uColor = useMemo(() => uniform(blue), [])

uColor.value = hovered ? red : blue

return (
<mesh
{...props}
Expand All @@ -30,7 +36,7 @@ function Box(props) {
onPointerOut={() => setHover(false)}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
<meshStandardNodeMaterial colorNode={uColor} />
</mesh>
)
}
Expand All @@ -42,7 +48,7 @@ export default function IndexPage() {
<Canvas
style={{ height: '100vh' }}
frameloop={frameloop}
gl={canvas => {
gl={(canvas) => {
const renderer = new WebGPURenderer({
canvas,
powerPreference: 'high-performance',
Expand All @@ -55,7 +61,13 @@ export default function IndexPage() {
>
<OrbitControls />
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
<spotLight
position={[10, 10, 10]}
angle={0.15}
penumbra={1}
decay={0}
intensity={Math.PI}
/>
<pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
Expand Down
8 changes: 4 additions & 4 deletions next14-app-r3f8-react18/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions next14-pages-r3f8-react18/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 22 additions & 10 deletions next14-pages-r3f8-react18/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React, { useEffect, useRef, useState } from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import React, { useMemo, useRef, useState } from 'react'
import { Canvas, useFrame, useThree, extend } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import { WebGPURenderer } from 'three/webgpu'
import * as TSL from 'three/tsl'
import { WebGPURenderer, MeshStandardNodeMaterial } from 'three/webgpu'
import { uniform } from 'three/tsl'
import { Color } from 'three'

extend({ MeshStandardNodeMaterial })

const red = new Color('red')
const blue = new Color('blue')

function Box(props) {
const meshRef = useRef()
Expand All @@ -14,9 +20,9 @@ function Box(props) {

console.log(gl.backend.isWebGPUBackend ? 'WebGPU Backend' : 'WebGL Backend')

useEffect(() => {
console.log(TSL.sqrt(2))
}, [])
const uColor = useMemo(() => uniform(blue), [])

uColor.value = hovered ? red : blue

return (
<mesh
Expand All @@ -28,7 +34,7 @@ function Box(props) {
onPointerOut={() => setHover(false)}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
<meshStandardNodeMaterial colorNode={uColor} />
</mesh>
)
}
Expand All @@ -40,7 +46,7 @@ export default function IndexPage() {
<Canvas
style={{ height: '100vh' }}
frameloop={frameloop}
gl={canvas => {
gl={(canvas) => {
const renderer = new WebGPURenderer({
canvas,
powerPreference: 'high-performance',
Expand All @@ -53,7 +59,13 @@ export default function IndexPage() {
>
<OrbitControls />
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
<spotLight
position={[10, 10, 10]}
angle={0.15}
penumbra={1}
decay={0}
intensity={Math.PI}
/>
<pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
Expand Down
2 changes: 1 addition & 1 deletion next15-app-r3f9-react19-rsc/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ WORKDIR /app

COPY package.json package-lock.json .

RUN npm install --legacy-peer-deps
RUN npm install

COPY . .

Expand Down
18 changes: 15 additions & 3 deletions next15-app-r3f9-react19-rsc/app/ClientBox.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
'use client'

import { useRef, useState } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { useRef, useMemo, useState } from 'react'
import { useFrame, useThree, extend } from '@react-three/fiber'
import { MeshStandardNodeMaterial } from 'three/webgpu'
import { uniform } from 'three/tsl'
import { Color } from 'three'

extend({ MeshStandardNodeMaterial })

const red = new Color('red')
const blue = new Color('blue')

export function ClientBox(props) {
const meshRef = useRef()
Expand All @@ -13,6 +21,10 @@ export function ClientBox(props) {

console.log(gl.backend.isWebGPUBackend ? 'WebGPU Backend' : 'WebGL Backend')

const uColor = useMemo(() => uniform(blue), [])

uColor.value = hovered ? red : blue

return (
<mesh
{...props}
Expand All @@ -23,7 +35,7 @@ export function ClientBox(props) {
onPointerOut={() => setHover(false)}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
<meshStandardNodeMaterial colorNode={uColor} />
</mesh>
)
}
22 changes: 5 additions & 17 deletions next15-app-r3f9-react19-rsc/app/ClientCanvas.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
'use client'

import { Canvas } from '@react-three/fiber'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { WebGPURenderer } from 'three/webgpu'
import * as TSL from 'three/tsl'

export function ClientCanvas({ children }) {
const [frameloop, setFrameloop] = useState('never')

useEffect(() => {
console.log(TSL.sqrt(2))
}, [])

return (
<Canvas
style={{ height: '100vh' }}
frameloop={frameloop}
gl={(canvas) => {
const renderer = new WebGPURenderer({
canvas,
powerPreference: 'high-performance',
antialias: true,
alpha: true,
})
renderer.init().then(() => setFrameloop('always'))
gl={async (glProps) => {
console.log(glProps)
const renderer = new WebGPURenderer(glProps)
await renderer.init()
return renderer
}}
>
Expand Down
Loading

0 comments on commit e2f7bf7

Please sign in to comment.