diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..e4f62aae --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,66 @@ +name: Deploy to GitHub Pages + +on: + push: + paths: + - "docs/**" + branches: + - main + - chore/docs + # Review gh actions docs if you want to further define triggers, paths, etc + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + +jobs: + build: + name: Build Docusaurus + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # As we use typedoc we need to install the node_modules in the package as well + - name: Install package/ deps + working-directory: package + run: | + corepack enable + yes | yarn install + + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: yarn + cache-dependency-path: docs + + - name: Install dependencies + run: yarn install --frozen-lockfile + working-directory: docs + + - name: Build website + run: yarn build + working-directory: docs + + - name: Upload Build Artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/build + + deploy: + name: Deploy to GitHub Pages + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test-deploy-docs.yml b/.github/workflows/test-deploy-docs.yml new file mode 100644 index 00000000..428afab4 --- /dev/null +++ b/.github/workflows/test-deploy-docs.yml @@ -0,0 +1,39 @@ +name: Test deployment + +on: + pull_request: + paths: + - "docs/**" + branches: + - main + # Review gh actions docs if you want to further define triggers, paths, etc + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + +jobs: + test-deploy: + name: Test deployment + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # As we use typedoc we need to install the node_modules in the package as well + - name: Install package/ deps + working-directory: package + run: | + corepack enable + yes | yarn install + + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: yarn + cache-dependency-path: docs + + - name: Install dependencies + run: yarn install --frozen-lockfile + working-directory: docs + - name: Test build website + run: yarn build + working-directory: docs diff --git a/README.md b/README.md index 753f31f0..4a39fedb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,12 @@ react-native-filament is a powerful 3D rendering engine for React Native. It features: -* ⚑ List features here with emojis +* ⚑ Native GPU accelerated rendering using [filament](https://github.com/google/filament) +* 🏎️ Uses Metal on iOS and OpenGL/Vulkan on android +* πŸ“Ή Rendering happens on a separate thread, nothing blocks the JS thread +* πŸ€ Comes with a physics engine as well, wrapping [bullet3](https://github.com/bulletphysics/bullet3) +* πŸ“¦ Easy to use declarative API +* βœ… Supports old and new arch ### Installation @@ -34,12 +39,12 @@ npm i react-native-worklets-core cd ios && pod install ``` -..and get started by [loading your model](https://docs.go.here)! +..and get started by [loading your model (docs)](https://margelo.github.io/react-native-filament/docs/guides)! ### Example ```tsx -import { FilamentScene, FilamentView, Model, Camera } from 'react-native-filament' +import { FilamentScene, FilamentView, Model, Camera, DefaultLight } from 'react-native-filament' function App() { return ( @@ -48,10 +53,14 @@ function App() { style={{ width: 100, height: 100 }} model={model} > - // Render with the default camera: + + {/* Render with the default camera and light: */} - // Add a model to the scene (only glb supported yet): + + + {/* Add a model to the scene (only glb supported yet): */} + ) @@ -60,6 +69,17 @@ function App() { > See the [example](./package/example/) app + +### Comparison with other libraries + +One library to render 3D content is [expo-gl](https://docs.expo.dev/versions/latest/sdk/gl-view) usually used together with [expo-three](https://www.npmjs.com/package/expo-three). react-native-filament has a few advantages over expo-gl: + +- Supports using `glb` files out of the box +- On iOS react-native-filament uses Metal, where expo-gl is using the [deprecated OpenGL ES](https://developer.apple.com/documentation/opengles) apple framework +- The rendering happens on the JS thread for `expo-three`, where react-native-filament uses a separate thread (and filament processes the rendering commands in a pool of different threads) +- filament is battle tested and react-native-filament is used in production apps with millions of users already, proven to be highly stable + + ### Contributing We welcome contributions to react-native-filament! πŸŽ‰ @@ -98,8 +118,13 @@ If you need help with integrating react-native-filament in your app or have addi * 🐦 [**Follow us on Twitter**](https://twitter.com/margelo) for updates * πŸ’¬ [**Join the Margelo Community Discord**](https://discord.gg/6CSHz2qAvA) for chatting about react-native-filament -#### Attributions +#### Attributions & thanks + +A big thanks to [filament](https://github.com/google/filament) for building such a great rendering engine! The example app in this project uses several free assets: -- Image by freepik +- Image by freepik +- Image by Freepik +- Image by Freepik +- Michelle by [mixamo](https://www.mixamo.com/#/?page=2&type=Character) - "Buster Drone" (https://skfb.ly/TBnX) by LaVADraGoN is licensed under Creative Commons Attribution-NonCommercial (http://creativecommons.org/licenses/by-nc/4.0/). diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..2ba4c7ea --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,21 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader +docs/api + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..0c6c2c27 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,41 @@ +# Website + +This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + +### Installation + +``` +$ yarn +``` + +### Local Development + +``` +$ yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +### Build + +``` +$ yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +### Deployment + +Using SSH: + +``` +$ USE_SSH=true yarn deploy +``` + +Not using SSH: + +``` +$ GIT_USER= yarn deploy +``` + +If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/docs/babel.config.js b/docs/babel.config.js new file mode 100644 index 00000000..e00595da --- /dev/null +++ b/docs/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/docs/docs/guides/ANIMATOR.mdx b/docs/docs/guides/ANIMATOR.mdx new file mode 100644 index 00000000..f1299bcb --- /dev/null +++ b/docs/docs/guides/ANIMATOR.mdx @@ -0,0 +1,8 @@ +--- +id: animator +title: Animator +sidebar_label: Animator +slug: /guides/animator +--- + +Documentation is added soon! \ No newline at end of file diff --git a/docs/docs/guides/ASSET_LOADING.mdx b/docs/docs/guides/ASSET_LOADING.mdx new file mode 100644 index 00000000..f424065c --- /dev/null +++ b/docs/docs/guides/ASSET_LOADING.mdx @@ -0,0 +1,86 @@ +--- +id: asset-loading +title: Asset loading +sidebar_label: Asset loading +slug: /guides/asset-loading +--- + +# Load from everywhere + +You can load assets from basically any source that comes to mind. Assets refer to anything that is not code, like 3D models, textures, material shaders, etc. +There are many components which accept a `source` prop, for example the `` component. For these components, the `source` prop can be of the following types: + +## πŸ§‘β€πŸ’» From assets in your project / metro + +Files that live next to your code. For this to work make sure you followed the metro bundle configuration in the [Getting Started](/docs/guides#configure-metro) guide. + +```tsx +import { Model } from 'react-native-filament'; +import BusterDrone from './BusterDrone.glb'; + + +``` + +:::info +Currently only loading `.glb` files is supported. You can convert any other 3D models (gltf, obj, FBX, etc) to .glb using blender or online converter tools. +::: + +### Debug + +In debug mode this will get the file from the metro bundler server, running on your machine. That means a network requests is made for example to `http://localhost:8081/assets/BusterDrone.glb`. + +:::warning +If your files are big loading over network can introduce some delay until the file is loaded and displayed. Make sure to profile your app in release! +::: + +### Release + +React Natives default behaviour is to bundle all assets into the app binary. This means that the file is not loaded over network but from the app binary itself, which is faster than in debug over network. + +:::warning +If you put your assets into other directories than the default `/assets` dir, you need to specify the path to the assets in your `react-native.config.js` file: +```js +module.exports = { + assets: [ + './app/assets/3d/assets', + ], +} +``` + +See the [React Native documentation](https://github.com/react-native-community/cli/blob/main/docs/projects.md#projectandroidassets) for more information. +::: + +## πŸ“¦ From native asset / bundle resources + +You can place assets directly in your native project and load them from there. This way the will load very fast, even in debug mode: + +- πŸ€– android: place assets under `android/app/main/assets/BusterDrone.glb` +- 🍏 ios: place assets under `ios/assets/BusterDrone.glb` and import them in your XCode project + +Then, you can simply load them by providing the name to the asset: + +```tsx + + +// If the asset is in a subdirectory: + +``` + + +## πŸ“ From a local file path + +Files can also be loaded from the file directory on the device. This is useful if you load models from the web and want to cache them, by downloading them to your app's file directory. +Note that if you want to open files from a public directory, such as the user's Downloads you might need to ask for permissions first. + +```tsx + +``` + + +## 🌐 From web urls + +You can also load assets from the web. This is useful if you want to load assets dynamically from a server or a CDN. + +```tsx + +``` \ No newline at end of file diff --git a/docs/docs/guides/CAMERA.mdx b/docs/docs/guides/CAMERA.mdx new file mode 100644 index 00000000..e5b50cf5 --- /dev/null +++ b/docs/docs/guides/CAMERA.mdx @@ -0,0 +1,103 @@ +--- +id: camera +title: Camera +sidebar_label: Camera +slug: /guides/camera +--- + +import useBaseUrl from '@docusaurus/useBaseUrl' + +## The `` components + +Your 3D scene needs to be projected to a 2D surface, your view. The camera is the "eye" through which you're looking +in the 3D space and which determines how the 3D scene is projected to the 2D surface. + +- Only one camera per scene is supported currently +- Only perspective cameras are supported currently + +The perspective camera has a + +- Position +- A point in space it looks at +- (An up vector that determines the orientation of the camera, but usually the default is fine) + +```tsx +import { Camera } from 'react-native-filament' + + +``` + + + +### Controlling the projection matrix + +The camera coordinate system defines the *view space*. The camera points towards its -z axis +and is oriented such that its top side is in the direction of +y, and its right side in the +direction of +x. + +There are three parameters that control the projection matrix: + +#### `near` plane + +- The closest distance from the camera at which objects will be rendered +- Objects closer to the camera than the near plane are clipped and not displayed +- Default is `0.1m` + +
+ Impact of the `near` plane on the level of detail / performance. + +The `near` plane greatly affects the depth buffer resolution (the level of detail). For performance reasons, it is recommended to keep the near plane as far as possible (default is `0.1m`). +The table below shows how the precision drops for various `near` plane values (smaller values are better). + + | near (m) | 1 m | 10 m | 100 m | 1 Km | + |----------|--------|--------|--------|--------| + | 0.001 | 7.2e-5 | 0.0043 | 0.4624 | 48.58 | + | 0.01 | 6.9e-6 | 0.0001 | 0.0430 | 4.62 | + | 0.1 | 3.6e-7 | 7.0e-5 | 0.0072 | 0.43 | + | 1.0 | 0 | 3.8e-6 | 0.0007 | 0.07 | + +
+ +#### `far` plane + +- The farthest distance from the camera at which objects will be rendered +- Objects farther from the camera than the far plane are clipped and not displayed +- Default is `100m` + +## The camera manipulator + +The camera can be controlled by a helper utility called [`CameraManipulator`](../api/interfaces/CameraManipulator), which enables complex gestures such as orbiting, panning, and zooming. + +Currently only a `ORBIT` mode is supported (in the future `MAP` and `FREE_FLIGHT` could be added as well). + +This shows how to implement a simple camera manipulator, for the full example see the [CameraPan example](https://github.com/margelo/react-native-filament/blob/main/package/example/Shared/src/CameraPan.tsx): + +```tsx +import { Camera, useCameraManipulator } from 'react-native-filament' +import { Gesture } from 'react-native-gesture-handler' + +const cameraManipulator = useCameraManipulator({ + orbitHomePosition: [0, 0, 8], // "Camera location" + targetPosition: [0, 0, 0], // "Looking at" + orbitSpeed: [0.003, 0.003], +}) + +const panGesture = Gesture.Pan() + .onBegin((event) => { + const yCorrected = viewHeight - event.translationY + cameraManipulator?.grabBegin(event.translationX, yCorrected, false) // false means rotation instead of translation + }) + .onUpdate((event) => { + const yCorrected = viewHeight - event.translationY + cameraManipulator?.grabUpdate(event.translationX, yCorrected) + }) + .onEnd(() => { + cameraManipulator?.grabEnd() + }) + +return +``` + diff --git a/docs/docs/guides/GETTING_STARTED.mdx b/docs/docs/guides/GETTING_STARTED.mdx new file mode 100644 index 00000000..79798ca7 --- /dev/null +++ b/docs/docs/guides/GETTING_STARTED.mdx @@ -0,0 +1,126 @@ +--- +id: getting-started +title: Getting Started +sidebar_label: Getting Started +slug: /guides +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +## Installing the library + +1. Install [react-native-filament](https://www.npmjs.com/package/react-native-filament): + +```sh +npm i react-native-filament +``` + +2. `react-native-filament` depends on [`react-native-worklets-core`](https://github.com/margelo/react-native-worklets-core): + + + + ```sh + npm i react-native-worklets-core + ``` + + + + + On the new arch you need to use the beta version of `react-native-worklets-core`: + + ```sh + npm i react-native-worklets-core@beta + ``` + + + + + + +3. Update your pods: + +```sh +cd ios && pod install +``` + +## Configure metro + +You'll likely import 3D related files when using react-native-filament. To make sure metro can resolve these files, you need to add the following to your `metro.config.js`: + +```js +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); + +/** + * Metro configuration + * https://metrobundler.dev/docs/configuration + * + * @type {import('metro-config').MetroConfig} + */ +const config = { + resolver: { + // This makes it possible to import .glb files in your code: + assetExts: ['glb'] + } +}; + +const defaultConfig = getDefaultConfig(__dirname) + +module.exports = mergeConfig(defaultConfig, config); +``` + +:::info +Currently only loading `.glb` files is supported. You can convert any other 3D models (gltf, obj, FBX, etc) to .glb using blender or online converter tools. +::: + +## Basic example: Render your first 3D model + +For seeing some 3D content on the screen in your app, you need the following things: + +- 🏞️ A view to draw the 3D content to (the `` component) +- πŸ’‘ A light source, otherwise the scene will be black (the `` component) +- πŸ“¦ A 3D model file (e.g. a .glb file) +- πŸ“Ή A camera through which the scene is observed and projected onto the view (the `` component) + +```jsx +import { FilamentScene, FilamentView, DefaultLight, Model, Camera } from "react-native-filament"; +import MyModel from "./MyModel.glb"; + +function MyScene() { + return ( + + + {/* 🏞️ A view to draw the 3D content to */} + + + {/* πŸ’‘ A light source, otherwise the scene will be black */} + + + {/* πŸ“¦ A 3D model */} + + + {/* πŸ“Ή A camera through which the scene is observed and projected onto the view */} + + + + + ); +} +``` + +Additionally you see that we use the `` component to wrap our scene. This is necessary to provide the necessary react context for the scene to work correctly. + +Everything that's part of your 3D scene should be rendered as part of the `` component. + +:::warning +Trying to render regular react-native components inside the `` might work, but is not directly supported and might lead to unexpected behavior. Instead you can +render on top of it by using a `position: absolute` view. +::: + +In general react-native-filament leans towards a declartive API that works well with React. \ No newline at end of file diff --git a/docs/docs/guides/IMAGES.mdx b/docs/docs/guides/IMAGES.mdx new file mode 100644 index 00000000..98e308ec --- /dev/null +++ b/docs/docs/guides/IMAGES.mdx @@ -0,0 +1,8 @@ +--- +id: images +title: Images +sidebar_label: Images +slug: /guides/images +--- + +Documentation is added soon! \ No newline at end of file diff --git a/docs/docs/guides/INSTANCING.mdx b/docs/docs/guides/INSTANCING.mdx new file mode 100644 index 00000000..c5a109bd --- /dev/null +++ b/docs/docs/guides/INSTANCING.mdx @@ -0,0 +1,8 @@ +--- +id: instancing +title: Instancing +sidebar_label: Instancing +slug: /guides/instancing +--- + +Documentation is added soon! \ No newline at end of file diff --git a/docs/docs/guides/LIGHT.mdx b/docs/docs/guides/LIGHT.mdx new file mode 100644 index 00000000..6289efdf --- /dev/null +++ b/docs/docs/guides/LIGHT.mdx @@ -0,0 +1,331 @@ +--- +id: light +title: Light +sidebar_label: Light +slug: /guides/light +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Your scene needs light otherwise nothing will be visible in your view (either transparent or fully black). The following light types are supported: + +- `DIRECTIONAL`: Directional light, emits light in a given direction. +- `SUN`: Directional light that also draws a sun's disk in the sky. +- `POINT`: Point light, emits light from a position, in all directions. +- `FOCUSED_SPOT`: Physically correct spot light. +- `SPOT`: Spot light with coupling of outer cone and illumination disabled. +- `Image based lighting (IBL)`: Environment light based on an image. + +## The `` component + +You can create a light source in your scene by using the `` component: + +```tsx +import { Light } from "react-native-filament"; + +; +``` + +The different types and props are explained below. + +## The `` component + +react-native-filament ships with a `` component that creates a default light source in your scene. +It uses a [directional light](#directional-lights) for shadows and an [IBL environment light](#image-based-lighting-ibl) for ambient lighting. + +### Opting out of the default IBL + +As the IBL is based on an image react-native-filament image ships with a default IBL image that is ~2mb in size. +If you don't use the `` component you can opt-out of shipping this file with your app to reduce the app size. + + + + Add the following at the top of your `ios/Podfile`: + + ```ruby + $RNFExcludeAssets = true + ``` + + + + + Add the following to your `android/gradle.properties`: + + ```properties + RNF_excludeAssets = true + ``` + + + + +## Directional lights + +- [Filament documentation](https://google.github.io/filament/Filament.html#lighting/directlighting) + +The main purpose of directional lights is to recreate important light sources for outdoor environment, i.e. the sun and/or the moon. + +![](https://google.github.io/filament/images/diagram_directional_light.png) + +### Intensity + +The unit for directional light is illuminance (lux). Here are useful reference values for the sun and sky illumination, measured on a clear day in March, in California: + +| Light | 10am | 12pm | 5:30pm | +|-----------|---------|---------|--------| +| Sky + Sun | 120,000 | 130,000 | 90,000 | +| Sky | 20,000 | 25,000 | 9,000 | +| Sun | 100,000 | 105,000 | 81,000 | + +Shadows are only working if you have one directional light source in your scene. + +### Color + +The color for directional light is specified in Kelvin. Here are some useful reference values: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Temperature (K) Light source Color
1,700-1,800 Match flame + +
 
+
1,850-1,930 Candle flame + +
 
+
2,000-3,000 Sun at sunrise/sunset + +
 
+
2,500-2,900 Household tungsten lightbulb + +
 
+
3,000 Tungsten lamp 1K + +
 
+
3,200-3,500 Quartz lights + +
 
+
3,200-3,700 Fluorescent lights + +
 
+
3,275 Tungsten lamp 2K + +
 
+
3,380 Tungsten lamp 5K, 10K + +
 
+
5,000-5,400 Sun at noon + +
 
+
5,500-6,500 Daylight (sun + sky) + +
 
+
5,500-6,500 Sun through clouds/haze + +
 
+
6,000-7,500 Overcast sky + +
 
+
6,500 RGB monitor white point + +
 
+
7,000-8,000 Shaded areas outdoors +
 
+
8,000-10,000 Partly cloudy sky +
 
+
+ +## Punctual lights + +- [Filament documentation](https://google.github.io/filament/Filament.html#lighting/directlighting/punctuallights) + +### Intensity + +The intensity for punctual lights is measured in luminous power (lumen). Here are some useful reference values: + +| Light source | Luminous power (lumen) | +|----------------|------------------------| +| Candle | 12.57 | +| 40W bulb | 450 | +| 100W bulb | 1,600 | +| 500W halogen | 10,000 | +| 1,000W halogen | 20,000 | + +### Point light + +A point light is defined only by a position in space, as shown here: + +![](https://google.github.io/filament/images/diagram_point_light.png) + +```tsx +import { Light } from "react-native-filament"; + + +``` + +### Spot light + +A spot light is defined by a position in space, a direction vector and two cone angles, πœƒπ‘–π‘›π‘›π‘’π‘Ÿ and πœƒπ‘œπ‘’π‘‘π‘’π‘Ÿ: + +![](https://google.github.io/filament/images/diagram_spot_light.png) + +```tsx +import { Light } from "react-native-filament"; + + +``` + +### Optimizing point lights + +Lights are very expensive in filament, especially if they overlap. You can optimize punctual lights by providing a `falloffRadius` prop. This is the radius length in meter after which the light has no more effect on objects. + + +## Image based lighting (IBL) + +In real life, light comes from every direction either directly from light sources or indirectly after bouncing off objects in the environment, being partially absorbed in the process. +In a way the whole environment around an object can be seen as a light source. Images, in particular cubemaps, are a great way to encode such an β€œenvironment light”. +This is called Image Based Lighting (IBL) or sometimes Indirect Lighting. + +The whole environment contributes light to a given point on the object's surface; this is called irradiance (𝐸). The resulting light bouncing off of the object is called radiance (πΏπ‘œπ‘’π‘‘). + +```tsx +import { EnvironmentalLight } from "react-native-filament"; + + +``` + +Example of an IBL: + +![](https://google.github.io/filament/images/ibl/ibl_river_roughness_m0.png) + +and its irradiance: + +![](https://google.github.io/filament/images/ibl/ibl_irradiance.png) + +:::note +react-native-filament ships by default with an IBL image, which can be used with the `` component or directly using: +```tsx + +``` + +You can opt-out of including the default IBL in your app as explained [here](#opting-out-of-the-default-ibl). +::: + +### Getting good IBL images + +https://polyhaven.com/hdris is a wonderful source for high-quality IBL images. +You'll download a `.hdr` file which you need to convert to a ktx file. + +TODO: explain how to convert hdr to ktx + + +## Additional resources + +Filament has a very detailed documentation on how their light system works. You can find it [here](https://google.github.io/filament/Filament.html#lighting). diff --git a/docs/docs/guides/LOGGING.mdx b/docs/docs/guides/LOGGING.mdx new file mode 100644 index 00000000..30404ce4 --- /dev/null +++ b/docs/docs/guides/LOGGING.mdx @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/docs/docs/guides/PHYSICS.mdx b/docs/docs/guides/PHYSICS.mdx new file mode 100644 index 00000000..154b6b98 --- /dev/null +++ b/docs/docs/guides/PHYSICS.mdx @@ -0,0 +1,124 @@ +--- +id: physics +title: Physics +sidebar_label: Physics +slug: /guides/physics +--- + +import useBaseUrl from '@docusaurus/useBaseUrl' + +# Physics API + +:::warning +The API for physics is still work in progress and might change in the future. +::: + +:::tip +As physics engine [**bullet3**](https://github.com/bulletphysics/bullet3) is used. We simply provide a JS/JSI wrapper around it. +So it can help to familiarize with their documentation first: +- [Bullet_User_Manual.pdf](https://github.com/bulletphysics/bullet3/blob/master/docs/Bullet_User_Manual.pdf) +::: + +
+ + + + +
+ +Hooks: + +The hooks should be used whenever possible. + +- [`useWorld`](../api/functions/useWorld) +- [`useRigidBody`](../api/functions/useRigidBody) +- [`useCylinderShape`](../api/functions/useCylinderShape) +- [`useBoxShape`](../api/functions/useBoxShape) +- [`useSphereShape`](../api/functions/useSphereShape) +- [`useStaticPlaneShape`](../api/functions/useStaticPlaneShape) + + +**Example:** + +First we need to create a physics world: + +```tsx +import { useWorld } from 'react-native-filament'; + +function App() { + const world = useWorld([0, -9.8, 0]); +} +``` + +Often times you want to create a physical body for an entity (asset) in the scene. +Here are a few helpers you likely will need to accomplish this: + +**Get the bounding box of an entity:** + +```tsx +const boundingBox = asset.boundingBox // returns an aabb bounding box +const halfExtents = boundingBox.halfExtents +const center = boundingBox.center +``` + +**Get the transform an entity:** + +```tsx +const {transformManager} = useFilamentContext() +const transform = transformManager.getTransform(entity) + +// Get the calculated world scale: +const [scaleX, scaleY, scaleZ] = transform.scale +``` + +**Create a physical shape (resembling the asset)** + +```tsx +// useBoxShape expects a vector of halfExtents, so we can just pass the once from our asset +const boxShape = useBoxShape(halfExtents) + +// Eventually we need to adjust the local scaling of the shape: +boxShape.localScaling = transform.scale +``` + +**Create a rigid body** + +```tsx +const rigidBody = useRigidBody({ + id: "box", + mass: 1, + shape: boxShape, + transform: transform, + // Pass the world for the body to be automatically added to the world: + world: world +}) +``` + +**Update the physical world** + +We need to simulate physics every frame: + +```tsx +const renderCallback = useCallback(({ timeSinceLastFrame }) => { + "worklet" + + // This updates the world at 60Hz/60 FPS. If our actual frame rate + // is different stepSimulation will interpolate the physics. + world.stepSimulation(timeSinceLastFrame, 1, 1 / 60) +}, [world]) +``` + +**Update the transform of our physic entities** + +Now that the physical world has been updated we need to update the transform of our entities. +The `transformManager` has a convenience method for that: + +```tsx +const renderCallback = useCallback(({ timeSinceLastFrame }) => { + "worklet" + + world.stepSimulation(timeSinceLastFrame, 1, 1 / 60) + + // Update our entity: + transformManager.updateTransformByRigidBody(entity, rigidBody) +}, [world]) \ No newline at end of file diff --git a/docs/docs/guides/SKYBOX.mdx b/docs/docs/guides/SKYBOX.mdx new file mode 100644 index 00000000..407dc110 --- /dev/null +++ b/docs/docs/guides/SKYBOX.mdx @@ -0,0 +1,34 @@ +--- +id: skybox +title: Skybox / Backgrounds +sidebar_label: Skybox +slug: /guides/skybox +--- + +:::info +By default the `` renders the background transparent. So every pixel that has no content will be transparent. +::: + +The "background" of your scene can be controlled using a [Skybox](https://en.wikipedia.org/wiki/Skybox_(video_games)). Every pixel that has no content will then +be filled with the skybox. The Skybox can be rendered from either: + +### 🎨 A static color + +```tsx +import { Skybox } from 'react-native-filament'; + + +``` + +### 🏞️ A [cubemap texture](https://learnopengl.com/img/advanced/cubemaps_skybox.png) + +```tsx +function Scene() { + const cubemap = useBuffer(require("./skybox_cubemap.ktx")) + + return ( + + + + ) +} \ No newline at end of file diff --git a/docs/docs/guides/TRANSFORMATION.mdx b/docs/docs/guides/TRANSFORMATION.mdx new file mode 100644 index 00000000..17976b85 --- /dev/null +++ b/docs/docs/guides/TRANSFORMATION.mdx @@ -0,0 +1,226 @@ +--- +id: transformation +title: Transforming objects in the scene +sidebar_label: Transformation +slug: /guides/transformation +--- + +import useBaseUrl from '@docusaurus/useBaseUrl' + +Transforming refers to changing the position, rotation or scale of an object in the scene. + +
+ + + + +
+ +## Translate / Position + +By default models are rendered at the origin `[0, 0, 0]`. You can change the position of a model by providing a `translate` prop: + +```tsx +import { Model } from "react-native-filament" + +const x = 0 +const y = -1 +const z = 0 + + +``` + +Units in filament are physically based, so 1 unit is 1 meter. + +## Rotation + +You can rotate a model by providing a `rotation` prop: + +```tsx +import { Model } from "react-native-filament" + +const rotationInDegree = 45; +const angleInRadians = rotationInDegree * (Math.PI / 180); +const rotateOnAxis = [0, angleInRadians, 0]; // Rotate on the y-axis + + +``` + +The `rotation` props expects a 3D vector (called `Float3` in RNF), where each value represents the rotation in radians around the x, y and z axis respectively. + +## Scale + +You can scale a model by providing a `scale` prop for the x, y and z axis: + +```tsx +import { Model } from "react-native-filament" + +const scale = 2; + + +``` + +## Matrix multiplication + +By default the transformation props are being multiplied to the current transform of the model. They are being multiplied in the order `scale` -> `rotation` -> `position` (SRT). +This means that if you first apply a scale of `[2,2,2]` and then later apply a scale of `[3,3,3]` the final scale will be `[6,6,6]`. To avoid this behaviour you +can specify the `multiplyWithCurrentTransform` prop and set it to false to disable this behaviour: + +```tsx +import { Model } from "react-native-filament" + +function GrowingModel() { + const [scale, setScale] = useState(1); + const growModel = () => setScale(scale + 1); + + return ( + + ) +} +``` + +:::warning +When **disabling** the multiplication with the current transform, you'll reset all previous transformations (including rotation and position) and only apply the new transformations. +::: + +:::warning +When matrix multiplication is **enabled (default)** all transform operations will be applied on a fast refresh. Keep this in mind during development if something starts to look off. +::: + +## Animate the transform props + +You may want to change the transformation props very frequently (e.g. every frame). To avoid performance issues, you should **not** update the props uusing `useState` directly, but use shared values. + +You may know shared values from `reanimated`. We are using the shared values from `react-native-worklets-core`, but they work very similary. + +You can create a new shared value using `useSharedValue`: + + +```tsx +import { useSharedValue } from "react-native-worklets-core"; + +function MyScene() { + const rotation = useSharedValue(0); + + // ... +} +``` + +You can pass a shared value to any of the transformation props: + +```tsx +function MyScene() { + const rotation = useSharedValue([0, 0, 0]); + + return +} +``` + +Now, lets update the rotation value every frame: + +```tsx +import { useCallback } from 'react' +import { useSharedValue } from "react-native-worklets-core" +import { RenderCallback, FilamentView, Model } from "react-native-filament" + +function MyScene() { + const rotation = useSharedValue([0, 0, 0]); + + const renderCallback: RenderCallback = useCallback(() => { + "worklet" + + // Add a rotation of 1 degree every frame + const newY = rotation.value[1] + 0.01, + + // We need to create a new array for the internal listeners to trigger + rotation.value = [0, newY, 0] + }, [rotation]) + + return ( + + + + ) +} +``` + +## Transform to unit cube + +There is a helper prop that you can use called `transformToUnitCube` which will make the asset fit into a 1x1x1 (unit) cube. + +## Apply imperatively using `TransformManager` + +Sometimes you have use cases where you want to update a model's transformation imperatively. +For this we can use the [`TransformManager`](../api/interfaces/TransformManager) directly. + +For this, you first need to render the model using the `useModel` and the accompanying `ModelRenderer` component: + +```tsx +import { useModel, ModelRenderer } from "react-native-filament" + +function MyScene() { + const model = useModel({ source: MyModel }) + + return +} +``` + +`ModelRenderer` accepts all props that `Model` does. + +The `TransformManager` only works on entities, so we need to select one first. A model is made out of many entities. When transforming our model we usually want to transform all entities of the model (isValidElement. the whole model). +For that we can use the `rootEntity`: + +```tsx +import { useModel } from "react-native-filament" + +const model = useModel({ source: MyModel }) +const rootEntity = model.state === "loaded" ? model.rootEntity : undefined +``` + +Now we can use the `TransformManager` to apply transformations. For example for rotating a model continuously, we can just apply a multipying rotation every frame: + +```tsx +import { useCallback } from 'react' +import { TransformManager, useModel, RenderCallback } from "react-native-filament" + +function MyScene() { + const model = useModel({ source: MyModel }) + const rootEntity = model.state === "loaded" ? model.rootEntity : undefined + + // We get the transformManager for the filament context. + // For this to work needs to be wrapped with a + const { transformManager } = useFilamentConext() + + const renderCallback: RenderCallback = useCallback(() => { + "worklet" + + // We have to check if the model is ready yet: + if (rootEntity == null) return; + + // Add a rotation of 1 degree every frame on the y-axis and + // multiply it with the current transform by setting `true` as last argument + transformManager.setEntityRotation(rootEntity, 0.01, [0, 1, 0], true) + }, []) +} +``` + +The `transformManager` has lots of APIs, you can [check them out in the API docs](../api/interfaces/TransformManager). + +### Setting all transform data yourself + +Using the `transformManager` its also possible to manually set a 4x4 matrix yourself, using a 16 element array: + +```tsx +transformManager.setTransform(entity, [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 +]) +``` + +Matrices in filament are column-major. \ No newline at end of file diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts new file mode 100644 index 00000000..ffa90c18 --- /dev/null +++ b/docs/docusaurus.config.ts @@ -0,0 +1,154 @@ +import {themes as prismThemes} from 'prism-react-renderer'; +import type {Config} from '@docusaurus/types'; +import type * as Preset from '@docusaurus/preset-classic'; + +const config: Config = { + title: 'React Native Filament Documentation', + tagline: 'Fastest 3D rendering for React Native', + favicon: 'img/favicon.ico', + + // Set the production url of your site here + url: 'https://margelo.github.io', + // Set the // pathname under which your site is served + // For GitHub pages deployment, it is often '//' + baseUrl: 'react-native-filament/', + trailingSlash: false, + + // GitHub pages deployment config. + // If you aren't using GitHub pages, you don't need these. + organizationName: 'margelo', // Usually your GitHub org/user name. + projectName: 'react-native-filament', // Usually your repo name. + + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + + // Even if you don't use internationalization, you can use this field to set + // useful metadata like html lang. For example, if your site is Chinese, you + // may want to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'en', + locales: ['en'], + }, + + presets: [ + [ + 'classic', + { + docs: { + sidebarPath: './sidebars.ts', + // Please change this to your repo. + // Remove this to remove the "edit this page" links. + editUrl: + 'https://github.com/margelo/react-native-filament/edit/main/docs/', + }, + theme: { + customCss: './src/css/custom.css', + }, + } satisfies Preset.Options, + ], + ], + + themeConfig: { + // Replace with your project's social card + image: 'img/banner-dark.jpg', + navbar: { + title: 'React Native Filament', + logo: { + alt: 'React Native Filament Logo', + src: 'img/penguin.png', + }, + items: [ + { + type: 'docSidebar', + sidebarId: 'sidebar', + position: 'left', + label: 'Docs', + }, + { + to: 'docs/api', + label: 'API', + position: 'left' + }, + { + href: 'https://github.com/margelo/react-native-filament', + label: 'GitHub', + position: 'right', + }, + ], + }, + footer: { + style: 'dark', + links: [ + { + title: 'Docs', + items: [ + { + label: 'Guides', + to: 'docs/guides' + }, + { + label: 'API', + to: 'docs/api', + }, + { + label: 'Example App', + href: 'https://github.com/margelo/react-native-filament/tree/main/package/example', + }, + ], + }, + { + title: 'Community', + items: [ + { + label: 'Discord', + href: 'https://discord.gg/SnEReRFA', + }, + { + label: 'Twitter', + href: 'https://twitter.com/margelo_io', + }, + ], + }, + { + title: 'More', + items: [ + { + label: 'GitHub', + href: 'https://github.com/margelo/react-native-filament', + }, + { + label: 'Margelo', + href: 'https://margelo.com', + } + ], + }, + ], + copyright: `Copyright Β© ${new Date().getFullYear()} Margelo GmbH. Built with Docusaurus.`, + }, + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + }, + } satisfies Preset.ThemeConfig, + plugins: [ + [ + 'docusaurus-plugin-typedoc', + { + name: "React Native Filament", + entryPoints: ['../package/src'], + tsconfig: '../package/tsconfig.json', + watch: process.env.TYPEDOC_WATCH, + excludePrivate: true, + excludeProtected: true, + excludeExternals: true, + excludeInternal: true, + readme: "none", + sidebar: { + indexLabel: 'Overview' + } + }, + ], + ], +}; + +export default config; diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..f3e822ae --- /dev/null +++ b/docs/package.json @@ -0,0 +1,51 @@ +{ + "name": "docs", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "typecheck": "tsc" + }, + "dependencies": { + "@docusaurus/core": "3.4.0", + "@docusaurus/preset-classic": "3.4.0", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.4.0", + "@docusaurus/tsconfig": "3.4.0", + "@docusaurus/types": "3.4.0", + "docusaurus-plugin-typedoc": "^1.0.3", + "typedoc": "^0.26.4", + "typedoc-plugin-markdown": "^4.2.0", + "typescript": "~5.2.2" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome version", + "last 3 firefox version", + "last 5 safari version" + ] + }, + "engines": { + "node": ">=18.0" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/docs/physics.md b/docs/physics.md deleted file mode 100644 index be3aedbd..00000000 --- a/docs/physics.md +++ /dev/null @@ -1,78 +0,0 @@ -# Physics API - -> [!TIP] -> As physics engine Bullet is used. We simply provide a JS/JSI wrapper around it. -> So it can help to familiarize with their documentation first: -> - [Bullet_User_Manual.pdf](https://github.com/bulletphysics/bullet3/blob/master/docs/Bullet_User_Manual.pdf) - -Imperative API: - -- [Bullet imperative API](../package/src/bullet/types/api.ts) - -Hooks: - -The hooks should be used whenever possible. - -- [`useWorld`](../package/src/bullet/hooks/useWorld.ts) -- [`useRigidBody`](../package/src/bullet/hooks/useRigidBody.ts) -- [`useCylinderShape`](../package/src/bullet/hooks/useCylinderShape.ts) -- [`useBoxShape`](../package/src/bullet/hooks/useBoxShape.ts) -- [`useSphereShape`](../package/src/bullet/hooks/useSphereShape.ts) -- [`useStaticPlaneShape`](../package/src/bullet/hooks/useStaticPlaneShape.ts) - - -**Example:** - -First we need to create a physics world: - -```tsx -import { useWorld } from 'react-native-filament'; - -function App() { - const world = useWorld([0, -9.8, 0]); -} -``` - -Often times you want to create a physical body for an entity (asset) in the scene. -Here are a few helpers you likely will need to accomplish this: - -**Get the bounding box of an entity:** - -```tsx -const boundingBox = asset.boundingBox // returns an aabb bounding box -const halfExtents = boundingBox.halfExtents -const center = boundingBox.center -``` - -**Get the transform an entity:** - -```tsx -const transformManager = useTransformManager(engine) -const transform = transformManager.getTransform(entity) - -// Get the calculated world scale: -const [scaleX, scaleY, scaleZ] = transform.scale -``` - -**Create a physical shape (resembling the asset)** - -```tsx -// useBoxShape expects a vector of halfExtents, so we can just pass the once from our asset -const boxShape = useBoxShape(halfExtents) - -// Eventually we need to adjust the local scaling of the shape: -boxShape.localScaling = transform.scale -``` - -**Create a rigid body** - -```tsx -const rigidBody = useRigidBody({ - id: "box", - mass: 1, - shape: boxShape, - transform: transform, - // Pass the world for the body tp be automatically added to the world: - world: world -}) -``` \ No newline at end of file diff --git a/docs/sidebars.ts b/docs/sidebars.ts new file mode 100644 index 00000000..a7b23a3b --- /dev/null +++ b/docs/sidebars.ts @@ -0,0 +1,24 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; + +const sidebars: SidebarsConfig = { + sidebar: { + Guides: [ + "guides/getting-started", + "guides/asset-loading", + "guides/transformation", + "guides/light", + "guides/skybox", + "guides/camera", + "guides/animator", + "guides/images", + "guides/physics", + "guides/instancing" + ], + API: [ + "api/index", + ...require('./docs/api/typedoc-sidebar.cjs'), + ], + }, +}; + +export default sidebars; diff --git a/docs/src/components/HomepageFeatures/index.tsx b/docs/src/components/HomepageFeatures/index.tsx new file mode 100644 index 00000000..4ea8dbf1 --- /dev/null +++ b/docs/src/components/HomepageFeatures/index.tsx @@ -0,0 +1,72 @@ +import clsx from 'clsx'; +import Heading from '@theme/Heading'; +import styles from './styles.module.css'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +type FeatureItem = { + title: string; + // Svg: React.ComponentType>; + imgUr: string; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Easy to Use', + // Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + imgUr: "img/easy.png", + description: ( + <> + React Native Filament is a React Native library that makes it easy to render 3D graphics in your app, by using react components. + + ), + }, + { + title: 'Native GPU accelerated rendering', + imgUr: "img/gpu.png", + description: ( + <> + Filament taps directly into your mobiles GPU by using Metal on iOS or OpenGL/Vulkan on Android. + + ), + }, + { + title: 'Made for mobile', + imgUr: "img/mobile.png", + description: ( + <> + Filament, the library behind React Native Filament, was build for mobile first. + Its native dependency adds only ~4MB to your app download size. + + ), + }, +]; + +function Feature({title, imgUr, description}: FeatureItem) { + return ( +
+
+ {/* */} + {title} +
+
+ {title} +

{description}

+
+
+ ); +} + +export default function HomepageFeatures(): JSX.Element { + return ( +
+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+
+ ); +} diff --git a/docs/src/components/HomepageFeatures/styles.module.css b/docs/src/components/HomepageFeatures/styles.module.css new file mode 100644 index 00000000..3922f0bb --- /dev/null +++ b/docs/src/components/HomepageFeatures/styles.module.css @@ -0,0 +1,11 @@ +.features { + display: flex; + align-items: center; + padding: 2rem 0; + width: 100%; +} + +.featureSvg { + height: 200px; + /* width: 200px; */ +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css new file mode 100644 index 00000000..4178a697 --- /dev/null +++ b/docs/src/css/custom.css @@ -0,0 +1,55 @@ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #2e8555; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); +} + +/* For readability concerns, you should choose a lighter palette in dark mode. */ +[data-theme='dark'] { + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: #21af90; + --ifm-color-primary-darker: #1fa588; + --ifm-color-primary-darkest: #1a8870; + --ifm-color-primary-light: #29d5b0; + --ifm-color-primary-lighter: #32d8b4; + --ifm-color-primary-lightest: #4fddbf; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); +} + +/* align image to the right */ +.image-container { + float: right; +} +/* mobile screen; align center */ +@media (max-width: 600px) { + .image-container { + float: none; + text-align: center; + } +} +/* wider screen; align right again */ +@media (min-width: 600px) and (max-width: 997px) { + .image-container { + float: right; + } +} +/* even wider screen but with sidebars; align center cuz we dont have enough space for right */ +@media (min-width: 997px) and (max-width: 1145px) { + .image-container { + float: none; + text-align: center; + } +} diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css new file mode 100644 index 00000000..4f391ae8 --- /dev/null +++ b/docs/src/pages/index.module.css @@ -0,0 +1,27 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding: 2rem; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 40vh; + left: 0px; + right: 0px; +} diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx new file mode 100644 index 00000000..1af63ca8 --- /dev/null +++ b/docs/src/pages/index.tsx @@ -0,0 +1,42 @@ +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Link from '@docusaurus/Link'; +import Layout from '@theme/Layout'; +import HomepageFeatures from '@site/src/components/HomepageFeatures'; +import styles from './index.module.css'; + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +function HomepageHeader() { + return ( +
+