├── .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 |
50 | {videoData ? (
51 |
52 |
53 |
54 |
60 |
61 | ) : null}
62 |
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 |
--------------------------------------------------------------------------------