├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── demo.gif ├── eslint.config.mjs ├── package.json ├── public ├── phone.mp4 └── tablet.mp4 ├── remotion.config.ts ├── src ├── Phone.tsx ├── Root.tsx ├── RoundedBox.tsx ├── Scene.tsx ├── helpers │ ├── layout.ts │ └── rounded-rectangle.ts ├── index.ts └── use-texture.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .env 5 | 6 | # Ignore the output video from Git but not videos you import into src/. 7 | out 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "bracketSpacing": true, 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Jonny Burger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remotion + React Three Fiber Starter Template 2 | 3 |

4 | 5 |

6 | 7 | [This is a template repository, click "Use this template" to create a repository based off this template!](https://github.com/JonnyBurger/remotion-template-three/generate) 8 | 9 | This is a lightweight boilerplate for [Remotion](https://github.com/jonnyburger/remotion) with [React Three Fiber](https://github.com/pmndrs/react-three-fiber) and [@remotion/three](http://remotion.dev/docs/three) preinstalled. 10 | 11 | - [Remotion documentation](https://remotion.dev) 12 | - [React Three Fiber documentation](https://docs.pmnd.rs/react-three-fiber) 13 | - [@remotion/three documentation](http://remotion.dev/docs/three) 14 | 15 | This example features a phone with a screen. You can easily switch out the video and change a series of parameters, like size, color, aspect ratio, corner radius etc. of the phone. 16 | 17 | You can also simply delete everything inside the canvas to start off with your own 3D project. 18 | 19 | ## Commands 20 | 21 | **Install Dependencies** 22 | 23 | ```console 24 | npm install 25 | ``` 26 | 27 | **Start Preview** 28 | 29 | ```console 30 | npm run dev 31 | ``` 32 | 33 | **Render MP4 video** 34 | 35 | ```console 36 | npx remotion render 37 | ``` 38 | 39 | **Upgrade Remotion** 40 | 41 | ```console 42 | npx remotion upgrade 43 | ``` 44 | 45 | ## Docs 46 | 47 | Get started with Remotion by reading the [fundamentals page](https://www.remotion.dev/docs/the-fundamentals). 48 | 49 | ## Help 50 | 51 | We provide help on our [Discord server](https://discord.gg/6VzzNDwUwV). 52 | 53 | ## Issues 54 | 55 | Found an issue with Remotion? [File an issue here](https://github.com/JonnyBurger/remotion/issues/new). 56 | 57 | ## License 58 | 59 | Note that for some entities a company license is needed. [Read the terms here](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md). 60 | 61 | The content of this template is licensed under MIT. 62 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/template-three/ddcc3daad0c2d4692c4fb1ea75d9330bdd4ad600/demo.gif -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { config } from "@remotion/eslint-config-flat"; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-three", 3 | "version": "1.0.0", 4 | "description": "A React Three Fiber Project", 5 | "scripts": { 6 | "dev": "remotion studio", 7 | "build": "remotion bundle", 8 | "upgrade": "remotion upgrade", 9 | "lint": "eslint src && tsc" 10 | }, 11 | "repository": {}, 12 | "license": "UNLICENSED", 13 | "dependencies": { 14 | "@react-three/fiber": "9.1.2", 15 | "@remotion/cli": "^4.0.0", 16 | "@remotion/media-utils": "^4.0.0", 17 | "@remotion/three": "^4.0.0", 18 | "@remotion/zod-types": "^4.0.0", 19 | "react": "19.0.0", 20 | "react-dom": "19.0.0", 21 | "remotion": "^4.0.0", 22 | "three": "0.171.0", 23 | "zod": "3.22.3" 24 | }, 25 | "devDependencies": { 26 | "@remotion/eslint-config-flat": "^4.0.0", 27 | "@types/react": "19.0.0", 28 | "@types/three": "0.170.0", 29 | "@types/web": "0.0.166", 30 | "eslint": "9.19.0", 31 | "prettier": "3.3.3", 32 | "typescript": "5.8.2" 33 | }, 34 | "private": true 35 | } 36 | -------------------------------------------------------------------------------- /public/phone.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/template-three/ddcc3daad0c2d4692c4fb1ea75d9330bdd4ad600/public/phone.mp4 -------------------------------------------------------------------------------- /public/tablet.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/template-three/ddcc3daad0c2d4692c4fb1ea75d9330bdd4ad600/public/tablet.mp4 -------------------------------------------------------------------------------- /remotion.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Note: When using the Node.JS APIs, the config file 3 | * doesn't apply. Instead, pass options directly to the APIs. 4 | * 5 | * All configuration options: https://remotion.dev/docs/config 6 | */ 7 | 8 | import { Config } from "@remotion/cli/config"; 9 | 10 | Config.setChromiumOpenGlRenderer("angle"); 11 | Config.setVideoImageFormat("jpeg"); 12 | -------------------------------------------------------------------------------- /src/Phone.tsx: -------------------------------------------------------------------------------- 1 | import { useThree } from "@react-three/fiber"; 2 | import React, { useEffect, useMemo } from "react"; 3 | import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; 4 | import { Texture } from "three"; 5 | import { 6 | CAMERA_DISTANCE, 7 | getPhoneLayout, 8 | PHONE_CURVE_SEGMENTS, 9 | PHONE_SHININESS, 10 | } from "./helpers/layout"; 11 | import { roundedRect } from "./helpers/rounded-rectangle"; 12 | import { RoundedBox } from "./RoundedBox"; 13 | 14 | export const Phone: React.FC<{ 15 | readonly videoTexture: Texture | null; 16 | readonly aspectRatio: number; 17 | readonly baseScale: number; 18 | readonly phoneColor: string; 19 | }> = ({ aspectRatio, videoTexture, baseScale, phoneColor }) => { 20 | const frame = useCurrentFrame(); 21 | const { fps, durationInFrames } = useVideoConfig(); 22 | 23 | const layout = useMemo( 24 | () => getPhoneLayout(aspectRatio, baseScale), 25 | [aspectRatio, baseScale], 26 | ); 27 | 28 | // Place a camera and set the distance to the object. 29 | // Then make it look at the object. 30 | const camera = useThree((state) => state.camera); 31 | useEffect(() => { 32 | camera.position.set(0, 0, CAMERA_DISTANCE); 33 | camera.near = 0.2; 34 | camera.far = Math.max(5000, CAMERA_DISTANCE * 2); 35 | camera.lookAt(0, 0, 0); 36 | }, [camera]); 37 | 38 | // Make the video fill the phone texture 39 | useEffect(() => { 40 | if (videoTexture) { 41 | videoTexture.repeat.y = 1 / layout.screen.height; 42 | videoTexture.repeat.x = 1 / layout.screen.width; 43 | } 44 | }, [aspectRatio, layout.screen.height, layout.screen.width, videoTexture]); 45 | 46 | // During the whole scene, the phone is rotating. 47 | // 2 * Math.PI is a full rotation. 48 | const constantRotation = interpolate( 49 | frame, 50 | [0, durationInFrames], 51 | [0, Math.PI * 6], 52 | ); 53 | 54 | // When the composition starts, there is some extra 55 | // rotation and translation. 56 | const entranceAnimation = spring({ 57 | frame, 58 | fps, 59 | config: { 60 | damping: 200, 61 | mass: 3, 62 | }, 63 | }); 64 | 65 | // Calculate the entrance rotation, 66 | // doing one full spin 67 | const entranceRotation = interpolate( 68 | entranceAnimation, 69 | [0, 1], 70 | [-Math.PI, Math.PI], 71 | ); 72 | 73 | // Calculating the total rotation of the phone 74 | const rotateY = entranceRotation + constantRotation; 75 | 76 | // Calculating the translation of the phone at the beginning. 77 | // The start position of the phone is set to 4 "units" 78 | const translateY = interpolate(entranceAnimation, [0, 1], [-4, 0]); 79 | 80 | // Calculate a rounded rectangle for the phone screen 81 | const screenGeometry = useMemo(() => { 82 | return roundedRect({ 83 | width: layout.screen.width, 84 | height: layout.screen.height, 85 | radius: layout.screen.radius, 86 | }); 87 | }, [layout.screen.height, layout.screen.radius, layout.screen.width]); 88 | 89 | return ( 90 | 95 | 103 | 104 | 105 | 106 | 107 | {videoTexture ? ( 108 | 113 | ) : null} 114 | 115 | 116 | ); 117 | }; 118 | -------------------------------------------------------------------------------- /src/Root.tsx: -------------------------------------------------------------------------------- 1 | import { Composition } from "remotion"; 2 | import { Scene, myCompSchema } from "./Scene"; 3 | 4 | // Welcome to the Remotion Three Starter Kit! 5 | // Two compositions have been created, showing how to use 6 | // the `ThreeCanvas` component and the `useVideoTexture` hook. 7 | 8 | // You can play around with the example or delete everything inside the canvas. 9 | 10 | // Remotion Docs: 11 | // https://remotion.dev/docs 12 | 13 | // @remotion/three Docs: 14 | // https://remotion.dev/docs/three 15 | 16 | // React Three Fiber Docs: 17 | // https://docs.pmnd.rs/react-three-fiber/getting-started/introduction 18 | 19 | export const RemotionRoot: React.FC = () => { 20 | return ( 21 | <> 22 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/RoundedBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { JSX, useMemo } from "react"; 2 | import { roundedRect } from "./helpers/rounded-rectangle"; 3 | 4 | type Props = { 5 | readonly width: number; 6 | readonly height: number; 7 | readonly radius: number; 8 | readonly curveSegments: number; 9 | readonly depth: number; 10 | } & Omit; 11 | 12 | export const RoundedBox: React.FC = ({ 13 | width, 14 | height, 15 | radius, 16 | curveSegments, 17 | children, 18 | depth, 19 | ...otherProps 20 | }) => { 21 | const shape = useMemo( 22 | () => roundedRect({ width, height, radius }), 23 | [height, radius, width], 24 | ); 25 | 26 | const params = useMemo( 27 | () => ({ 28 | depth, 29 | bevelEnabled: true, 30 | bevelSize: 0, 31 | bevelThickness: 0, 32 | curveSegments, 33 | }), 34 | [curveSegments, depth], 35 | ); 36 | 37 | return ( 38 | 39 | 40 | {children} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/Scene.tsx: -------------------------------------------------------------------------------- 1 | import { staticFile } from "remotion"; 2 | import { getVideoMetadata, VideoMetadata } from "@remotion/media-utils"; 3 | import { ThreeCanvas } from "@remotion/three"; 4 | import React, { useEffect, useRef, useState } from "react"; 5 | import { AbsoluteFill, useVideoConfig, Video } from "remotion"; 6 | import { Phone } from "./Phone"; 7 | import { z } from "zod"; 8 | import { zColor } from "@remotion/zod-types"; 9 | import { useTexture } from "./use-texture"; 10 | 11 | const container: React.CSSProperties = { 12 | backgroundColor: "white", 13 | }; 14 | 15 | const videoStyle: React.CSSProperties = { 16 | position: "absolute", 17 | opacity: 0, 18 | }; 19 | 20 | export const myCompSchema = z.object({ 21 | phoneColor: zColor(), 22 | deviceType: z.enum(["phone", "tablet"]), 23 | }); 24 | 25 | type MyCompSchemaType = z.infer; 26 | 27 | export const Scene: React.FC< 28 | { 29 | readonly baseScale: number; 30 | } & MyCompSchemaType 31 | > = ({ baseScale, phoneColor, deviceType }) => { 32 | const videoRef = useRef(null); 33 | const { width, height } = useVideoConfig(); 34 | const [videoData, setVideoData] = useState(null); 35 | 36 | const videoSrc = 37 | deviceType === "phone" ? staticFile("phone.mp4") : staticFile("tablet.mp4"); 38 | 39 | useEffect(() => { 40 | getVideoMetadata(videoSrc) 41 | .then((data) => setVideoData(data)) 42 | .catch((err) => console.log(err)); 43 | }, [videoSrc]); 44 | 45 | const texture = useTexture(videoSrc, videoRef); 46 | 47 | return ( 48 | 49 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/helpers/layout.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "@react-three/fiber"; 2 | 3 | // The distance from which the camera is pointing to the phone. 4 | export const CAMERA_DISTANCE = 2.5; 5 | 6 | // A small number to avoid z-index flickering 7 | export const Z_FLICKER_PREVENTION = 0.001; 8 | 9 | // Shininess of the phone 10 | export const PHONE_SHININESS = 30; 11 | 12 | // In how many segments the phone rounded corners 13 | // are divided. Increase number for smoother phone 14 | export const PHONE_CURVE_SEGMENTS = 8; 15 | 16 | // Calculate phone size. Whichever side is smaller gets 17 | // normalized to the base scale. 18 | const getPhoneHeight = (aspectRatio: number, baseScale: number): number => { 19 | if (aspectRatio > 1) { 20 | return baseScale; 21 | } 22 | return baseScale / aspectRatio; 23 | }; 24 | 25 | const getPhoneWidth = (aspectRatio: number, baseScale: number): number => { 26 | if (aspectRatio < 1) { 27 | return baseScale; 28 | } 29 | return baseScale * aspectRatio; 30 | }; 31 | 32 | type Layout = { 33 | position: Vector3; 34 | height: number; 35 | width: number; 36 | radius: number; 37 | }; 38 | 39 | type PhoneLayout = { 40 | phone: Layout & { 41 | thickness: number; 42 | bevel: number; 43 | }; 44 | screen: Layout; 45 | }; 46 | 47 | export const getPhoneLayout = ( 48 | // I recommend building the phone layout based 49 | // on the aspect ratio of the phone 50 | aspectRatio: number, 51 | // This value can be increased or decreased to tweak the 52 | // base value of the phone. 53 | baseScale: number, 54 | ): PhoneLayout => { 55 | // The depth of the phone body 56 | const phoneThickness = baseScale * 0.15; 57 | 58 | // How big the border of the phone is. 59 | const phoneBevel = baseScale * 0.04; 60 | 61 | // The inner radius of the phone, aka the screen radius 62 | const screenRadius = baseScale * 0.07; 63 | 64 | const phoneHeight = getPhoneHeight(aspectRatio, baseScale); 65 | const phoneWidth = getPhoneWidth(aspectRatio, baseScale); 66 | const phonePosition: Vector3 = [-phoneWidth / 2, -phoneHeight / 2, 0]; 67 | const screenWidth = phoneWidth - phoneBevel * 2; 68 | const screenHeight = phoneHeight - phoneBevel * 2; 69 | const screenPosition: Vector3 = [ 70 | -screenWidth / 2, 71 | -screenHeight / 2, 72 | phoneThickness + Z_FLICKER_PREVENTION, 73 | ]; 74 | 75 | // Define the outer radius of the phone. 76 | // It looks better if the outer radius is a bit bigger than the screen radios, 77 | // formula taken from https://twitter.com/joshwcomeau/status/134978208002102886 78 | const phoneRadius = 79 | screenRadius + (getPhoneWidth(aspectRatio, baseScale) - screenWidth) / 2; 80 | 81 | return { 82 | phone: { 83 | position: phonePosition, 84 | height: phoneHeight, 85 | width: phoneWidth, 86 | radius: phoneRadius, 87 | thickness: phoneThickness, 88 | bevel: phoneBevel, 89 | }, 90 | screen: { 91 | position: screenPosition, 92 | height: screenHeight, 93 | width: screenWidth, 94 | radius: screenRadius, 95 | }, 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /src/helpers/rounded-rectangle.ts: -------------------------------------------------------------------------------- 1 | import { Shape } from "three"; 2 | 3 | export function roundedRect({ 4 | width, 5 | height, 6 | radius, 7 | }: { 8 | width: number; 9 | height: number; 10 | radius: number; 11 | }): Shape { 12 | const roundedRectShape = new Shape(); 13 | roundedRectShape.moveTo(0, radius); 14 | roundedRectShape.lineTo(0, height - radius); 15 | roundedRectShape.quadraticCurveTo(0, height, radius, height); 16 | roundedRectShape.lineTo(width - radius, height); 17 | roundedRectShape.quadraticCurveTo(width, height, width, height - radius); 18 | roundedRectShape.lineTo(width, radius); 19 | roundedRectShape.quadraticCurveTo(width, 0, width - radius, 0); 20 | roundedRectShape.lineTo(radius, 0); 21 | roundedRectShape.quadraticCurveTo(0, 0, 0, radius); 22 | return roundedRectShape; 23 | } 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { registerRoot } from "remotion"; 2 | import { RemotionRoot } from "./Root"; 3 | 4 | registerRoot(RemotionRoot); 5 | -------------------------------------------------------------------------------- /src/use-texture.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { useOffthreadVideoTexture, useVideoTexture } from "@remotion/three"; 3 | import { getRemotionEnvironment } from "remotion"; 4 | 5 | export const useTexture = ( 6 | src: string, 7 | videoRef: React.RefObject, 8 | ) => { 9 | if (getRemotionEnvironment().isRendering) { 10 | return useOffthreadVideoTexture({ 11 | src, 12 | }); 13 | } 14 | 15 | return useVideoTexture(videoRef); 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "jsx": "react-jsx", 6 | "strict": true, 7 | "noEmit": true, 8 | "lib": ["es2015"], 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noUnusedLocals": true 13 | }, 14 | "exclude": ["remotion.config.ts"] 15 | } 16 | --------------------------------------------------------------------------------