├── public ├── robots.txt ├── dog.glb ├── duck.glb ├── icons │ ├── share.png │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-icon-192x192.png │ ├── android-icon-512x512.png │ └── safari-pinned-tab.svg ├── img │ ├── scores │ │ ├── lighthouse.md │ │ └── lighthouse.svg │ └── logo.svg └── manifest.json ├── .eslintignore ├── src ├── helpers │ ├── global.js │ └── components │ │ └── Three.jsx ├── templates │ ├── Shader │ │ ├── glsl │ │ │ ├── shader.vert │ │ │ └── shader.frag │ │ └── Shader.jsx │ ├── Scroll.jsx │ └── hooks │ │ └── usePostprocess.jsx └── components │ ├── canvas │ ├── Scene.jsx │ ├── View.jsx │ └── Examples.jsx │ └── dom │ └── Layout.jsx ├── .prettierignore ├── postcss.config.js ├── jsconfig.json ├── sandbox.config.json ├── .editorconfig ├── .eslintrc ├── .vscode └── settings.json ├── app ├── global.css ├── layout.jsx ├── blob │ └── page.jsx ├── head.jsx └── page.jsx ├── tailwind.config.js ├── .prettierrc ├── .gitignore ├── LICENSE ├── package.json ├── next.config.js └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | dist 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /public/dog.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/dog.glb -------------------------------------------------------------------------------- /public/duck.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/duck.glb -------------------------------------------------------------------------------- /src/helpers/global.js: -------------------------------------------------------------------------------- 1 | import tunnel from 'tunnel-rat' 2 | 3 | export const r3f = tunnel() 4 | -------------------------------------------------------------------------------- /public/icons/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/icons/share.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/icons/favicon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/* 2 | dist/* 3 | public/* 4 | .next/* 5 | node_modules/* 6 | package.json 7 | *.log 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/icons/android-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-next/HEAD/public/icons/android-icon-512x512.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["app/*", "src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/img/scores/lighthouse.md: -------------------------------------------------------------------------------- 1 | npm install -g lighthouse-badges && lighthouse-badges --urls http://r3f-next-starter.vercel.app/ -o public/img/scores 2 | -------------------------------------------------------------------------------- /src/templates/Shader/glsl/shader.vert: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | void main() { 3 | vUv = uv; 4 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "view": "browser", 5 | "container": { 6 | "node": "12" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/components/Three.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { r3f } from '@/helpers/global' 4 | 5 | export const Three = ({ children }) => { 6 | return {children} 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [{*.json,.*.yml}] 10 | indent_style = space 11 | indent_size = 2 -------------------------------------------------------------------------------- /src/templates/Shader/glsl/shader.frag: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform vec3 color; 3 | varying vec2 vUv; 4 | #pragma glslify: random = require(glsl-random) 5 | 6 | void main() { 7 | gl_FragColor.rgba = vec4(color + sin(time) * 0.2, 1.0); 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier", "plugin:tailwindcss/recommended"], 3 | "rules": { 4 | "import/prefer-default-export": "off", 5 | "no-console": "warn", 6 | "no-var": "error", 7 | "no-html-link-for-pages": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit" 6 | }, 7 | "eslint.validate": ["javascript"], 8 | "eslint.workingDirectories": [{ "mode": "auto" }] 9 | } 10 | -------------------------------------------------------------------------------- /app/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | html, 11 | body, 12 | #root { 13 | width: 100%; 14 | height: 100%; 15 | margin: 0; 16 | padding: 0; 17 | overflow: hidden; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | content: ['./app/**/*.{js,ts,jsx,tsx}', './src/**/*.{js,ts,jsx,tsx}'], // remove unused styles in production 4 | darkMode: 'media', // or 'media' or 'class' 5 | theme: { 6 | extend: {}, 7 | }, 8 | variants: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /public/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxSingleQuote": true, 8 | "printWidth": 120, 9 | "proseWrap": "preserve", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": false, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "all", 16 | "useTabs": false, 17 | "vueIndentScriptAndStyle": false 18 | } 19 | -------------------------------------------------------------------------------- /src/components/canvas/Scene.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Canvas } from '@react-three/fiber' 4 | import { Preload } from '@react-three/drei' 5 | import { r3f } from '@/helpers/global' 6 | import * as THREE from 'three' 7 | 8 | export default function Scene({ ...props }) { 9 | // Everything defined in here will persist between route changes, only children are swapped 10 | return ( 11 | (state.gl.toneMapping = THREE.AgXToneMapping)} 13 | > 14 | {/* @ts-ignore */} 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # next.js 8 | /.next/ 9 | /out/ 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | *.pem 17 | 18 | # debug 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # local env files 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | # vercel 30 | .vercel 31 | 32 | # next-pwa 33 | /public/workbox-*.js 34 | /public/sw.js 35 | 36 | # prefer yarn than npm 37 | package-lock.json 38 | 39 | report.*.json 40 | 41 | # .vscode Debugging 42 | .vscode/*-debug-profile 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "R3F Next Starter", 3 | "short_name": "R3F Next Starter", 4 | "icons": [ 5 | { 6 | "src": "/icons/apple-touch-icon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "/icons/android-icon-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png", 15 | "purpose": "any maskable" 16 | }, 17 | { 18 | "src": "/icons/android-icon-512x512.png", 19 | "sizes": "512x512", 20 | "type": "image/png", 21 | "purpose": "any maskable" 22 | } 23 | ], 24 | "start_url": "/", 25 | "scope": "/", 26 | "display": "standalone", 27 | "theme_color": "#000000", 28 | "background_color": "#ffffff" 29 | } 30 | -------------------------------------------------------------------------------- /app/layout.jsx: -------------------------------------------------------------------------------- 1 | import { Layout } from '@/components/dom/Layout' 2 | import '@/global.css' 3 | 4 | export const metadata = { 5 | title: 'Next.js + Three.js', 6 | description: 'A minimal starter for Nextjs + React-three-fiber and Threejs.', 7 | } 8 | 9 | export default function RootLayout({ children }) { 10 | return ( 11 | 12 | {/* 13 | will contain the components returned by the nearest parent 14 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 15 | */} 16 | 17 | 18 | {/* To avoid FOUT with styled-components wrap Layout with StyledComponentsRegistry https://beta.nextjs.org/docs/styling/css-in-js#styled-components */} 19 | {children} 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/dom/Layout.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRef } from 'react' 4 | import dynamic from 'next/dynamic' 5 | const Scene = dynamic(() => import('@/components/canvas/Scene'), { ssr: false }) 6 | 7 | const Layout = ({ children }) => { 8 | const ref = useRef() 9 | 10 | return ( 11 |
21 | {children} 22 | 34 |
35 | ) 36 | } 37 | 38 | export { Layout } 39 | -------------------------------------------------------------------------------- /src/templates/Shader/Shader.jsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import * as THREE from 'three' 3 | import { extend, useFrame } from '@react-three/fiber' 4 | import { shaderMaterial } from '@react-three/drei' 5 | import vertex from './glsl/shader.vert' 6 | import fragment from './glsl/shader.frag' 7 | import { forwardRef, useImperativeHandle, useRef } from 'react' 8 | 9 | const ShaderImpl = shaderMaterial( 10 | { 11 | time: 0, 12 | color: new THREE.Color(0.05, 0.0, 0.025), 13 | }, 14 | vertex, 15 | fragment, 16 | ) 17 | 18 | extend({ ShaderImpl }) 19 | 20 | // eslint-disable-next-line react/display-name 21 | const Shader = forwardRef(({ children, ...props }, ref) => { 22 | const localRef = useRef() 23 | 24 | useImperativeHandle(ref, () => localRef.current) 25 | 26 | useFrame((_, delta) => (localRef.current.time += delta)) 27 | return 28 | }) 29 | 30 | export default Shader 31 | -------------------------------------------------------------------------------- /public/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/img/scores/lighthouse.svg: -------------------------------------------------------------------------------- 1 | lighthouse: 94%lighthouse94% -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2021 Scott Chacon and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/components/canvas/View.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { forwardRef, Suspense, useImperativeHandle, useRef } from 'react' 4 | import { OrbitControls, PerspectiveCamera, View as ViewImpl } from '@react-three/drei' 5 | import { Three } from '@/helpers/components/Three' 6 | 7 | export const Common = ({ color }) => ( 8 | 9 | {color && } 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | 17 | const View = forwardRef(({ children, orbit, ...props }, ref) => { 18 | const localRef = useRef(null) 19 | useImperativeHandle(ref, () => localRef.current) 20 | 21 | return ( 22 | <> 23 |
24 | 25 | 26 | {children} 27 | {orbit && } 28 | 29 | 30 | 31 | ) 32 | }) 33 | View.displayName = 'View' 34 | 35 | export { View } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-three-next", 3 | "version": "2.0.0", 4 | "authors": [ 5 | "Renaud ROHLINGER " 6 | ], 7 | "license": "MIT", 8 | "private": true, 9 | "engines": { 10 | "node": ">=14" 11 | }, 12 | "scripts": { 13 | "lint": "next lint --fix --dir app", 14 | "dev": "next dev", 15 | "build": "next build", 16 | "analyze": "ANALYZE=true next build", 17 | "start": "next start" 18 | }, 19 | "dependencies": { 20 | "@ducanh2912/next-pwa": "^10.0.0", 21 | "@react-three/drei": "^9.92.7", 22 | "@react-three/fiber": "^8.15.12", 23 | "glsl-random": "^0.0.5", 24 | "next": "^14.0.4", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "three": "^0.160.0", 28 | "three-stdlib": "^2.28.9", 29 | "tunnel-rat": "^0.1.2" 30 | }, 31 | "devDependencies": { 32 | "@next/bundle-analyzer": "^14.0.4", 33 | "autoprefixer": "^10.4.16", 34 | "eslint": "^8.56.0", 35 | "eslint-config-next": "^14.0.4", 36 | "eslint-config-prettier": "^9.1.0", 37 | "eslint-plugin-tailwindcss": "^3.13.0", 38 | "file-loader": "^6.2.0", 39 | "glslify": "^7.1.1", 40 | "glslify-loader": "^2.0.0", 41 | "postcss": "^8.4.32", 42 | "prettier": "^3.1.1", 43 | "raw-loader": "^4.0.2", 44 | "tailwindcss": "^3.4.0", 45 | "url-loader": "^4.1.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/blob/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import dynamic from 'next/dynamic' 4 | 5 | const Blob = dynamic(() => import('@/components/canvas/Examples').then((mod) => mod.Blob), { ssr: false }) 6 | const View = dynamic(() => import('@/components/canvas/View').then((mod) => mod.View), { 7 | ssr: false, 8 | loading: () => ( 9 |
10 | 11 | 12 | 17 | 18 |
19 | ), 20 | }) 21 | const Common = dynamic(() => import('@/components/canvas/View').then((mod) => mod.Common), { ssr: false }) 22 | 23 | export default function Page() { 24 | return ( 25 | <> 26 |
27 |
28 |

Next + React Three Fiber

29 |

Next 3D Starter

30 |

A minimalist starter for React, React-three-fiber and Threejs.

31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/templates/Scroll.jsx: -------------------------------------------------------------------------------- 1 | // https://github.com/studio-freight/lenis 2 | // TODO refactor for app-directory 3 | // See https://github.com/pmndrs/react-three-next/pull/123 4 | 5 | // 1 - wrap with in _app.jsx 6 | // 2 - add wherever in the canvas 7 | // 3 - enjoy 8 | import { addEffect, useFrame } from '@react-three/fiber' 9 | import Lenis from '@studio-freight/lenis' 10 | import { useEffect } from 'react' 11 | import { useRef } from 'react' 12 | import * as THREE from 'three' 13 | 14 | const state = { 15 | top: 0, 16 | progress: 0, 17 | } 18 | 19 | const { damp } = THREE.MathUtils 20 | 21 | export default function Scroll({ children }) { 22 | const content = useRef(null) 23 | const wrapper = useRef(null) 24 | 25 | useEffect(() => { 26 | const lenis = new Lenis({ 27 | wrapper: wrapper.current, 28 | content: content.current, 29 | duration: 1.2, 30 | easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // https://www.desmos.com/calculator/brs54l4xou 31 | direction: 'vertical', // vertical, horizontal 32 | gestureDirection: 'vertical', // vertical, horizontal, both 33 | smooth: true, 34 | smoothTouch: false, 35 | touchMultiplier: 2, 36 | infinite: false, 37 | }) 38 | 39 | lenis.on('scroll', ({ scroll, progress }) => { 40 | state.top = scroll 41 | state.progress = progress 42 | }) 43 | const effectSub = addEffect((time) => lenis.raf(time)) 44 | return () => { 45 | effectSub() 46 | lenis.destroy() 47 | } 48 | }, []) 49 | 50 | return ( 51 |
60 |
66 | {children} 67 |
68 |
69 | ) 70 | } 71 | 72 | export const ScrollTicker = ({ smooth = 9999999 }) => { 73 | useFrame(({ viewport, camera }, delta) => { 74 | camera.position.y = damp(camera.position.y, -state.progress * viewport.height, smooth, delta) 75 | }) 76 | 77 | return null 78 | } 79 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true', 3 | }) 4 | 5 | /** 6 | * A fork of 'next-pwa' that has app directory support 7 | * @see https://github.com/shadowwalker/next-pwa/issues/424#issuecomment-1332258575 8 | */ 9 | const withPWA = require('@ducanh2912/next-pwa').default({ 10 | dest: 'public', 11 | disable: process.env.NODE_ENV === 'development', 12 | }) 13 | 14 | const nextConfig = { 15 | // uncomment the following snippet if using styled components 16 | // compiler: { 17 | // styledComponents: true, 18 | // }, 19 | reactStrictMode: true, // Recommended for the `pages` directory, default in `app`. 20 | images: {}, 21 | webpack(config, { isServer }) { 22 | if (!isServer) { 23 | // We're in the browser build, so we can safely exclude the sharp module 24 | config.externals.push('sharp') 25 | } 26 | // audio support 27 | config.module.rules.push({ 28 | test: /\.(ogg|mp3|wav|mpe?g)$/i, 29 | exclude: config.exclude, 30 | use: [ 31 | { 32 | loader: require.resolve('url-loader'), 33 | options: { 34 | limit: config.inlineImageLimit, 35 | fallback: require.resolve('file-loader'), 36 | publicPath: `${config.assetPrefix}/_next/static/images/`, 37 | outputPath: `${isServer ? '../' : ''}static/images/`, 38 | name: '[name]-[hash].[ext]', 39 | esModule: config.esModule || false, 40 | }, 41 | }, 42 | ], 43 | }) 44 | 45 | // shader support 46 | config.module.rules.push({ 47 | test: /\.(glsl|vs|fs|vert|frag)$/, 48 | exclude: /node_modules/, 49 | use: ['raw-loader', 'glslify-loader'], 50 | }) 51 | 52 | return config 53 | }, 54 | } 55 | 56 | const KEYS_TO_OMIT = ['webpackDevMiddleware', 'configOrigin', 'target', 'analyticsId', 'webpack5', 'amp', 'assetPrefix'] 57 | 58 | module.exports = (_phase, { defaultConfig }) => { 59 | const plugins = [[withPWA], [withBundleAnalyzer, {}]] 60 | 61 | const wConfig = plugins.reduce((acc, [plugin, config]) => plugin({ ...acc, ...config }), { 62 | ...defaultConfig, 63 | ...nextConfig, 64 | }) 65 | 66 | const finalConfig = {} 67 | Object.keys(wConfig).forEach((key) => { 68 | if (!KEYS_TO_OMIT.includes(key)) { 69 | finalConfig[key] = wConfig[key] 70 | } 71 | }) 72 | 73 | return finalConfig 74 | } 75 | -------------------------------------------------------------------------------- /src/components/canvas/Examples.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useGLTF } from '@react-three/drei' 4 | import { useFrame } from '@react-three/fiber' 5 | import * as THREE from 'three' 6 | import { useMemo, useRef, useState } from 'react' 7 | import { Line, useCursor, MeshDistortMaterial } from '@react-three/drei' 8 | import { useRouter } from 'next/navigation' 9 | 10 | export const Blob = ({ route = '/', ...props }) => { 11 | const router = useRouter() 12 | const [hovered, hover] = useState(false) 13 | useCursor(hovered) 14 | return ( 15 | router.push(route)} 17 | onPointerOver={() => hover(true)} 18 | onPointerOut={() => hover(false)} 19 | {...props}> 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | export const Logo = ({ route = '/blob', ...props }) => { 27 | const mesh = useRef(null) 28 | const router = useRouter() 29 | 30 | const [hovered, hover] = useState(false) 31 | const points = useMemo(() => new THREE.EllipseCurve(0, 0, 3, 1.15, 0, 2 * Math.PI, false, 0).getPoints(100), []) 32 | 33 | useCursor(hovered) 34 | useFrame((state, delta) => { 35 | const t = state.clock.getElapsedTime() 36 | mesh.current.rotation.y = Math.sin(t) * (Math.PI / 8) 37 | mesh.current.rotation.x = Math.cos(t) * (Math.PI / 8) 38 | mesh.current.rotation.z -= delta / 4 39 | }) 40 | 41 | return ( 42 | 43 | {/* @ts-ignore */} 44 | 45 | {/* @ts-ignore */} 46 | 47 | {/* @ts-ignore */} 48 | 49 | router.push(route)} onPointerOver={() => hover(true)} onPointerOut={() => hover(false)}> 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | 57 | export function Duck(props) { 58 | const { scene } = useGLTF('/duck.glb') 59 | 60 | useFrame((state, delta) => (scene.rotation.y += delta)) 61 | 62 | return 63 | } 64 | export function Dog(props) { 65 | const { scene } = useGLTF('/dog.glb') 66 | 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /app/head.jsx: -------------------------------------------------------------------------------- 1 | const title = 'React Three Next Starter' 2 | const url = 'https://react-three-next.vercel.app/' 3 | const description = 'The easiest and fastest way to create a 3D website using React Three Fiber and NextJS' 4 | const author = 'Author' 5 | const twitter = '@pmndrs' 6 | 7 | export default function Head() { 8 | return ( 9 | <> 10 | {/* Recommended Meta Tags */} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {/* Search Engine Optimization Meta Tags */} 19 | {title} 20 | 21 | 25 | 26 | 27 | {/* 28 | Facebook Open Graph meta tags 29 | documentation: https://developers.facebook.com/docs/sharing/opengraph */} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {/* Meta Tags for HTML pages on Mobile */} 46 | {/* 47 | */} 48 | 49 | 50 | 51 | 52 | {/* 53 | Twitter Summary card 54 | documentation: https://dev.twitter.com/cards/getting-started 55 | Be sure validate your Twitter card markup on the documentation site. */} 56 | 57 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Downloads](https://img.shields.io/npm/dt/create-r3f-app.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/create-r3f-app) [![Discord Shield](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/ZZjjNvJ) 2 | 3 | # :japanese_castle: React-Three-Next starter 4 | 5 | A minimalist starter for NextJS, @react-three/fiber and Threejs. 6 | 7 | ![](https://user-images.githubusercontent.com/2223602/192515435-a3d2c1bb-b79a-428e-92e5-f44c97a54bf7.jpg) 8 | 9 | - TTL ~ 100ms 10 | - First load JS ~ 79kb 11 | - Lighthouse score of 100 (Performance, Accessibility, Best Practices, SEO) 12 | 13 | This starter allows you to navigate seamlessly between pages with dynamic dom and/or canvas content without reloading or creating a new canvas every time. 3D components are usable anywhere in the dom. The events, dom, viewport, everything is synchronized! 14 | 15 | ### ⚫ Demo : 16 | 17 | [![image](https://user-images.githubusercontent.com/15867665/231395343-fd4770e3-0e39-4f5c-ac30-71d823a9ef1c.png)](https://react-three-next.vercel.app/) 18 | 19 | ### How to use 20 | 21 | #### Installation 22 | 23 | _Tailwind is the default style. styled-components (styled) are also available._ 24 | 25 | ```sh 26 | yarn create r3f-app next my-app 27 | # yarn create r3f-app my-app ? -ts? 28 | # npx create-r3f-app next my-app 29 | ``` 30 | 31 | ### :passport_control: Typescript 32 | 33 | For typescript add the parameter `-ts` or `--typescript`: 34 | 35 | ```sh 36 | yarn create r3f-app next my-app -ts 37 | # npx create-r3f-app next my-app -ts 38 | ``` 39 | 40 | ### :mount_fuji: Features 41 | 42 | - [x] GLSL imports 43 | - [x] Canvas is not getting unmounted while navigating between pages 44 | - [x] Canvas components usable in any div of the page 45 | - [x] Based on the App directory architecture 46 | - [x] PWA Support 47 | 48 | ### :bullettrain_side: Architecture 49 | 50 | Thanks to [tunnel-rat](https://github.com/pmndrs/tunnel-rat) the starter can portal components between separate renderers. Anything rendered inside the `` component of the starter will be rendered in the 3D Context. For better performances it uses gl.scissor to cut the viewport into segments. 51 | 52 | ```jsx 53 |
54 | 55 | 56 | // Some 3D components will be rendered here 57 | 58 |
59 | ``` 60 | 61 | ### :control_knobs: Available Scripts 62 | 63 | - `yarn dev` - Next dev 64 | - `yarn analyze` - Generate bundle-analyzer 65 | - `yarn lint` - Audit code quality 66 | - `yarn build` - Next build 67 | - `yarn start` - Next start 68 | 69 | ### ⬛ Stack 70 | 71 | - [`create-r3f-app`](https://github.com/utsuboco/create-r3f-app) – Command line tool to simplify the installation. 72 | - [`threejs`](https://github.com/mrdoob/three.js/) – A lightweight, 3D library with a default WebGL renderer. 73 | - [`@react-three/fiber`](https://github.com/pmndrs/react-three-fiber) – A React renderer for Threejs on the web and react-native. 74 | - [`@react-three/drei` - Optional](https://github.com/pmndrs/drei) – useful helpers for react-three-fiber 75 | - [`@react-three/a11y` - Optional](https://github.com/pmndrs/react-three-a11y/) – Accessibility tools for React Three Fiber 76 | - [`r3f-perf` - Optional](https://github.com/RenaudRohlinger/r3f-perf) – Tool to easily monitor react threejs performances. 77 | 78 | ### How to contribute : 79 | 80 | ```bash 81 | git clone https://github.com/pmndrs/react-three-next 82 | && cd react-three-next && yarn install 83 | ``` 84 | 85 | ### Maintainers : 86 | 87 | - [`twitter 🐈‍⬛ @onirenaud`](https://twitter.com/onirenaud) 88 | -------------------------------------------------------------------------------- /src/templates/hooks/usePostprocess.jsx: -------------------------------------------------------------------------------- 1 | import { useFrame, useThree } from '@react-three/fiber' 2 | import { useEffect, useMemo } from 'react' 3 | import * as THREE from 'three' 4 | 5 | function getFullscreenTriangle() { 6 | const geometry = new THREE.BufferGeometry() 7 | const vertices = new Float32Array([-1, -1, 3, -1, -1, 3]) 8 | const uvs = new Float32Array([0, 0, 2, 0, 0, 2]) 9 | 10 | geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 2)) 11 | geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)) 12 | 13 | return geometry 14 | } 15 | 16 | // Basic shader postprocess based on the template https://gist.github.com/RenaudRohlinger/bd5d15316a04d04380e93f10401c40e7 17 | // USAGE: Simply call usePostprocess hook in your r3f component to apply the shader to the canvas as a postprocess effect 18 | const usePostProcess = () => { 19 | const [{ dpr }, size, gl] = useThree((s) => [s.viewport, s.size, s.gl]) 20 | 21 | const [screenCamera, screenScene, screen, renderTarget] = useMemo(() => { 22 | let screenScene = new THREE.Scene() 23 | const screenCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1) 24 | const screen = new THREE.Mesh(getFullscreenTriangle()) 25 | screen.frustumCulled = false 26 | screenScene.add(screen) 27 | 28 | const renderTarget = new THREE.WebGLRenderTarget(512, 512, { samples: 4, encoding: gl.encoding }) 29 | renderTarget.depthTexture = new THREE.DepthTexture() // fix depth issues 30 | 31 | // use ShaderMaterial for linearToOutputTexel 32 | screen.material = new THREE.RawShaderMaterial({ 33 | uniforms: { 34 | diffuse: { value: null }, 35 | time: { value: 0 }, 36 | }, 37 | vertexShader: /* glsl */ ` 38 | 39 | in vec2 uv; 40 | in vec3 position; 41 | precision highp float; 42 | 43 | out vec2 vUv; 44 | void main() { 45 | vUv = uv; 46 | gl_Position = vec4( position, 1.0 ); 47 | 48 | } 49 | `, 50 | fragmentShader: /* glsl */ ` 51 | precision highp float; 52 | out highp vec4 pc_fragColor; 53 | uniform sampler2D diffuse; 54 | uniform float time; 55 | in vec2 vUv; 56 | 57 | // based on https://www.shadertoy.com/view/llGXzR 58 | float radial(vec2 pos, float radius) 59 | { 60 | float result = length(pos)-radius; 61 | result = fract(result*1.0); 62 | float result2 = 1.0 - result; 63 | float fresult = result * result2; 64 | fresult = pow((fresult*3.),7.); 65 | return fresult; 66 | } 67 | 68 | void main() { 69 | vec2 c_uv = vUv * 2.0 - 1.0; 70 | vec2 o_uv = vUv * 0.8; 71 | float gradient = radial(c_uv, time*0.8); 72 | vec2 fuv = mix(vUv,o_uv,gradient); 73 | pc_fragColor = texture(diffuse, fuv); 74 | } 75 | `, 76 | glslVersion: THREE.GLSL3, 77 | }) 78 | screen.material.uniforms.diffuse.value = renderTarget.texture 79 | 80 | return [screenCamera, screenScene, screen, renderTarget] 81 | }, [gl.encoding]) 82 | useEffect(() => { 83 | const { width, height } = size 84 | const { w, h } = { 85 | w: width * dpr, 86 | h: height * dpr, 87 | } 88 | renderTarget.setSize(w, h) 89 | }, [dpr, size, renderTarget]) 90 | 91 | useFrame(({ scene, camera, gl }, delta) => { 92 | gl.setRenderTarget(renderTarget) 93 | gl.render(scene, camera) 94 | 95 | gl.setRenderTarget(null) 96 | if (screen) screen.material.uniforms.time.value += delta 97 | 98 | gl.render(screenScene, screenCamera) 99 | }, 1) 100 | return null 101 | } 102 | 103 | export default usePostProcess 104 | -------------------------------------------------------------------------------- /app/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import dynamic from 'next/dynamic' 4 | import { Suspense } from 'react' 5 | 6 | const Logo = dynamic(() => import('@/components/canvas/Examples').then((mod) => mod.Logo), { ssr: false }) 7 | const Dog = dynamic(() => import('@/components/canvas/Examples').then((mod) => mod.Dog), { ssr: false }) 8 | const Duck = dynamic(() => import('@/components/canvas/Examples').then((mod) => mod.Duck), { ssr: false }) 9 | const View = dynamic(() => import('@/components/canvas/View').then((mod) => mod.View), { 10 | ssr: false, 11 | loading: () => ( 12 |
13 | 14 | 15 | 20 | 21 |
22 | ), 23 | }) 24 | const Common = dynamic(() => import('@/components/canvas/View').then((mod) => mod.Common), { ssr: false }) 25 | 26 | export default function Page() { 27 | return ( 28 | <> 29 |
30 | {/* jumbo */} 31 |
32 |

Next + React Three Fiber

33 |

Next 3D Starter

34 |

A minimalist starter for React, React-three-fiber and Threejs.

35 |
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 | {/* first row */} 49 |
50 |

Events are propagated

51 |

Drag, scroll, pinch, and rotate the canvas to explore the 3D scene.

52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | {/* second row */} 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 |

Dom and 3D are synchronized

72 |

73 | 3D Divs are renderer through the View component. It uses gl.scissor to cut the viewport into segments. You 74 | tie a view to a tracking div which then controls the position and bounds of the viewport. This allows you to 75 | have multiple views with a single, performant canvas. These views will follow their tracking elements, 76 | scroll along, resize, etc. 77 |

78 |
79 |
80 | 81 | ) 82 | } 83 | --------------------------------------------------------------------------------