├── .gitignore ├── LICENSE.txt ├── README.md ├── TERMS.txt ├── assets ├── fonts │ ├── icons.svg │ ├── icons.ttf │ └── icons.woff ├── images │ ├── background-removal-preview.jpg │ ├── custom-shaders-preview.jpg │ ├── github-mark-16.svg │ ├── hello-world-preview.jpg │ ├── og-image.jpg │ ├── react-three-fiber-preview.jpg │ ├── scene-lighting-preview.jpg │ ├── three.js-fog-preview.jpg │ ├── transmission-preview.jpg │ └── vr-preview.jpg ├── logo.svg ├── models │ └── ufo_b11_d_model.glb └── venice_sunset_1k.hdr ├── index.html ├── misc ├── common.sh └── deploy.sh ├── package-lock.json ├── package.json ├── src ├── DemoBackgroundRemoval.ts ├── DemoCustomShaders.ts ├── DemoFog.ts ├── DemoHelloWorld.ts ├── DemoLighting.ts ├── DemoReactThreeFiber.tsx ├── DemoTransmission.ts ├── DemoVR.ts ├── LumaSplatsReact.ts ├── index.tsx ├── minimal.html └── util │ ├── DownloadArtifacts.ts │ ├── Environment.ts │ ├── EnvironmentProbes.ts │ ├── GUIUtils.ts │ └── MarkdownImport.d.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Luma AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![luma-logo](./assets/logo.svg)](https://lumalabs.ai) Luma WebGL Library 2 | 3 | `luma-web` is a [npm package](https://www.npmjs.com/package/@lumaai/luma-web) for rendering photoreal interactive scenes captured by the [Luma app](https://lumalabs.ai/). It includes `LumaSplatsWebGL`, which is a WebGL-only gaussian splatting implementation designed to be integrated with 3D frameworks, and `LumaSplatsThree`, which is a Three.js implementation that uses `LumaSplatsWebGL` under the hood. For these examples we'll use [Three.js](https://threejs.org/). 4 | 5 | 6 | **Request features and report bugs on our [![github-logo](./assets/images/github-mark-16.svg) GitHub repo](https://github.com/lumalabs/luma-web-library)** 7 | 8 | ### Contents 9 | - [Getting Started](#getting-started) 10 | - [Background Removal](#background-removal) 11 | - [Three Fog](#three-fog) 12 | - [Scene Lighting](#scene-lighting) 13 | - [Custom Shaders](#custom-shaders) 14 | - [React Three Fiber](#react-three-fiber) 15 | - [Transmission](#transmission) 16 | - [VR](#vr) 17 | 18 | ## Getting Started 19 | [![hello-world-demo](./assets/images/hello-world-preview.jpg)](#getting-started) 20 | 21 | The simplest way to get started is to create a .html file and load the library from a CDN. Here we load three.js and the luma library and setup a minimal scene with mouse controls. Copy and paste this into a file named `index.html` and open it in your browser (no server needed). 22 | 23 | **[minimal.html](./src/minimal.html)** 24 | ```html 25 | 26 | 35 | 67 | ``` 68 | 69 | Alternatively you can install the library from npm: 70 | 71 | ```bash 72 | npm install @lumaai/luma-web 73 | ``` 74 | 75 | **Usage** 76 | 77 | Import the `LumaSplatsThree` class: 78 | 79 | ```ts 80 | import { LumaSplatsThree } from "@lumaai/luma-web"; 81 | ``` 82 | 83 | Then create an instance of `LumaSplatsThree` with a splat `source`, and add it to your scene. 84 | 85 | `source` can be either of: 86 | - URL to a capture on [lumalabs.ai](https://lumalabs.ai) 87 | - path to a luma splats file or folder containing a luma splats artifacts 88 | 89 | **[DemoHelloWorld.ts](./src/DemoHelloWorld.ts)** 90 | ```ts 91 | let splats = new LumaSplatsThree({ 92 | source: 'https://lumalabs.ai/capture/ca9ea966-ca24-4ec1-ab0f-af665cb546ff', 93 | // controls the particle entrance animation 94 | particleRevealEnabled: true, 95 | }); 96 | 97 | scene.add(splats); 98 | ``` 99 | 100 | Splats will integrate with the three.js rendering pipeline and interact with other objects via depth testing. However, splats do not currently write to the depth buffer themselves. 101 | 102 | ### Performance tips 103 | 104 | - Use `antialias: false` when creating the renderer to disable MSAA on the canvas. Splats are already anti-aliased and the high instance count in splats is expensive to render with MSAA 105 | - Set `enableThreeShaderIntegration: false` to disable integration with the three.js rendering pipeline. This will disable features like fog and tone mapping, but will improve performance 106 | 107 | ## Background Removal 108 | [![background-removal-demo](./assets/images/background-removal-preview.jpg)](#background-removal) 109 | 110 | Luma scenes can include multiple semantic layers. By default, all layers are rendered. To filter layers, use the `semanticsMask` property. This is a bit mask, so for example, to show only the foreground layer, set `semanticsMask = LumaSplatsSemantics.FOREGROUND`. To show both foreground and background, set `semanticsMask = LumaSplatsSemantics.FOREGROUND | LumaSplatsSemantics.BACKGROUND` 111 | 112 | **[DemoBackgroundRemoval.ts](./src/DemoBackgroundRemoval.ts)** 113 | ```ts 114 | import { LumaSplatsSemantics, LumaSplatsThree } from "@lumaai/luma-web"; 115 | 116 | let splats = new LumaSplatsThree({ 117 | source: 'https://lumalabs.ai/capture/1b5f3e33-3900-4398-8795-b585ae13fd2d', 118 | }); 119 | 120 | scene.add(splats); 121 | 122 | // filter splats to only show foreground layers 123 | splats.semanticsMask = LumaSplatsSemantics.FOREGROUND; 124 | ``` 125 | 126 | ## Three Fog 127 | [![three.js-fog-demo](./assets/images/three.js-fog-preview.jpg)](#three-fog) 128 | 129 | Luma splats integrate with the three.js rendering pipeline including features like tone mapping, color spaces and fog. Ensure `enableThreeShaderIntegration` is set to `true` (the default) and set the scene fog 130 | 131 | **[DemoFog.ts](./src/DemoFog.ts)** 132 | ```ts 133 | scene.fog = new FogExp2(new Color(0xe0e1ff).convertLinearToSRGB(), 0.15); 134 | scene.background = scene.fog.color; 135 | ``` 136 | 137 | ## Scene Lighting 138 | [![scene-lighting-demo](./assets/images/scene-lighting-preview.jpg)](#scene-lighting) 139 | 140 | It's possible to illuminate three.js scenes with Luma splats. To do so, we can render a cubemap of the splats and use it as the scene environment. This is done by calling `captureCubemap()` on the splats object. We first wait for the splats to fully load before capturing the cubemap. To ensure the splats are fully rendered at the time of capture, we disable the loading animation. 141 | 142 | **[DemoLighting.ts](./src/DemoLighting.ts)** 143 | ```ts 144 | let splats = new LumaSplatsThree({ 145 | source: 'https://lumalabs.ai/capture/4da7cf32-865a-4515-8cb9-9dfc574c90c2', 146 | 147 | // disable loading animation so model is fully rendered after onLoad 148 | loadingAnimationEnabled: false, 149 | }); 150 | 151 | splats.onLoad = () => { 152 | splats.captureCubemap(renderer).then((capturedTexture) => { 153 | scene.environment = capturedTexture; 154 | scene.background = capturedTexture; 155 | scene.backgroundBlurriness = 0.5; 156 | }); 157 | } 158 | ``` 159 | 160 | ## Custom Shaders 161 | [![custom-shaders-demo](./assets/images/custom-shaders-preview.jpg)](#custom-shaders) 162 | 163 | You can inject code into the splat shaders to customize them. To do this, call `setShaderHooks({ ... })` on your splat and provide GLSL functions, uniforms and globals to override default behavior. For example, in this demo we apply a transform matrix to each splat by setting the vertex shader hook `getSplatTransform`. It generates a transform matrix for time-varying sinusoidal offset to the y coordinate. 164 | 165 | The syntax for shader hook function is a GLSL function without a function name. The GLSL function arguments and return are given as documentation on the shader hook fields (see below). 166 | 167 | **[DemoCustomShaders.ts](./src/DemoCustomShaders.ts)** 168 | ```ts 169 | splats.setShaderHooks({ 170 | vertexShaderHooks: { 171 | additionalUniforms: { 172 | time_s: ['float', uniformTime], 173 | }, 174 | 175 | getSplatTransform: /*glsl*/` 176 | (vec3 position, uint layersBitmask) { 177 | // sin wave on x-axis 178 | float x = 0.; 179 | float z = 0.; 180 | float y = sin(position.x * 1.0 + time_s) * 0.1; 181 | return mat4( 182 | 1., 0., 0., 0, 183 | 0., 1., 0., 0, 184 | 0., 0., 1., 0, 185 | x, y, z, 1. 186 | ); 187 | } 188 | `, 189 | } 190 | }); 191 | ``` 192 | 193 | ### Shader Hook API 194 | ```typescript 195 | type LumaShaderHooks = { 196 | 197 | /** Hooks added to the vertex shader */ 198 | vertexShaderHooks?: { 199 | additionalUniforms?: { [name: string]: [UniformTypeGLSL, { value: any }] }, 200 | 201 | /** Inject into global space (for example, to add a varying) */ 202 | additionalGlobals?: string, 203 | 204 | /** 205 | * Example `(vec3 splatPosition, uint layersBitmask) { return mat4(1.); }` 206 | * @param {vec3} splatPosition, object-space 207 | * @param {uint} layersBitmask, bit mask of layers, where bit 0 is background and bit 1 is foreground 208 | * @returns {mat4} per-splat local transform 209 | */ 210 | getSplatTransform?: string, 211 | 212 | /** 213 | * Executed at the end of the main function after gl_Position is set 214 | * 215 | * Example `() { 216 | * vPosition = gl_Position; 217 | * }` 218 | * @returns {void} 219 | */ 220 | onMainEnd?: string, 221 | 222 | /** 223 | * Example `(vec4 splatColor, vec3 splatPosition) { return pow(splatColor.rgb, vec3(2.2), splatColor.a); }` 224 | * Use `gl_Position` is available 225 | * @param {vec4} splatColor, default splat color 226 | * @param {vec3} splatPosition, object-space 227 | * @param {uint} layersBitmask, bit mask of layers, where bit 0 is background and bit 1 is foreground 228 | * @returns {vec4} updated splat color 229 | */ 230 | getSplatColor?: string, 231 | }, 232 | 233 | /** Hooks added to the fragment shader */ 234 | fragmentShaderHooks?: { 235 | additionalUniforms?: { [name: string]: [UniformTypeGLSL, { value: any }] }, 236 | 237 | /** Inject into global space (for example, to add a varying) */ 238 | additionalGlobals?: string, 239 | 240 | /** 241 | * Example `(vec4 fragColor) { return tonemap(fragColor); }` 242 | * @param {vec4} fragColor, default fragment color 243 | * @returns {vec4} updated fragment color 244 | */ 245 | getFragmentColor?: string, 246 | } 247 | } 248 | ``` 249 | 250 | ## React Three Fiber 251 | [![react-three-fiber-demo](./assets/images/react-three-fiber-preview.jpg)](#react-three-fiber) 252 | 253 | Luma splats can be used with [React Three Fiber](https://docs.pmnd.rs/), a React renderer for Three.js. To do so, we need to extend R3F to include the `LumaSplatsThree` class. This is done by calling `extend` with the class and a name (in this case `LumaSplats` which will be used as the component name). If using TypeScript, we also need to declare the component type. 254 | 255 | **[DemoReactThreeFiber.tsx](./src/DemoReactThreeFiber.tsx)** 256 | ```typescript 257 | import { Object3DNode, extend } from '@react-three/fiber'; 258 | import { LumaSplatsThree, LumaSplatsSemantics } from "@lumaai/luma-web"; 259 | 260 | // Make LumaSplatsThree available to R3F 261 | extend( { LumaSplats: LumaSplatsThree } ); 262 | 263 | // For typeScript support: 264 | declare module '@react-three/fiber' { 265 | interface ThreeElements { 266 | lumaSplats: Object3DNode 267 | } 268 | } 269 | 270 | function Scene() { 271 | return 277 | } 278 | ``` 279 | 280 | ## Transmission 281 | [![transmission-demo](./assets/images/transmission-preview.jpg)](#transmission) 282 | 283 | Splats can be used in combination with three.js transmission effects, however some care should be taken to make this work. Splats are considered `transparent` materials in three.js which means by default they're not rendered in the transmissive pass, so initially you won't see your splats in transmissive materials. To fix we set `splats.material.transparent = false;`. 284 | 285 | In this example, we draw two splat scenes, one inside a refractive globe and the other outside. To make this work, we want the inner splat scene to _only_ render to the transmission buffer and the outer to the canvas. We do this by checking the render target before rendering and selectively disabling. 286 | 287 | **[DemoTransmission.ts](./src/DemoTransmission.ts)** 288 | ```typescript 289 | // inner splat 290 | let globeSplats = new LumaSplatsThree({ 291 | // Chateau de Menthon - Annecy 292 | source: 'https://lumalabs.ai/capture/da82625c-9c8d-4d05-a9f7-3367ecab438c', 293 | enableThreeShaderIntegration: true, 294 | onBeforeRender: (renderer) => { 295 | // disable MSAA on render targets (in this case the transmission render target) 296 | // this improves splatting performance 297 | let target = renderer.getRenderTarget(); 298 | if (target) { 299 | target.samples = 0; 300 | } 301 | 302 | // only render in targets and not the canvas 303 | globeSplats.preventDraw = target == null; 304 | } 305 | }); 306 | 307 | // disable transparency so the renderer considers it an opaque object 308 | // opaque objects are rendered in the transmission pass (whereas transparent objects are not) 309 | globeSplats.material.transparent = false; 310 | 311 | scene.add(globeSplats); 312 | 313 | // outer splat 314 | let environmentSplats = new LumaSplatsThree({ 315 | // Arosa Hörnli - Switzerland 316 | source: 'https://lumalabs.ai/capture/4da7cf32-865a-4515-8cb9-9dfc574c90c2', 317 | // disable animation for lighting capture 318 | loadingAnimationEnabled: false, 319 | // disable three.js shader integration for performance 320 | enableThreeShaderIntegration: false, 321 | }); 322 | 323 | scene.add(environmentSplats); 324 | 325 | // add a refractive transmissive sphere 326 | let glassSphere = new Mesh( 327 | new SphereGeometry(1, 32, 32), 328 | new MeshPhysicalMaterial({ 329 | roughness: 0, 330 | metalness: 0, 331 | transmission: 1, 332 | ior: 1.341, 333 | thickness: 1.52, 334 | envMapIntensity: 1.2, 335 | clearcoat: 1, 336 | side: FrontSide, 337 | transparent: true, 338 | }) 339 | ); 340 | 341 | scene.add(glassSphere); 342 | ``` 343 | 344 | ## VR 345 | [![vr-demo](./assets/images/vr-preview.jpg)](#vr) 346 | 347 | Viewing your splats in VR is as simple as enabling XR in three.js and adding a VR button. View this demo with a VR headset (or through a headset browser) and click "Enter VR"! It will work best on PC VR, standalone VR tends to struggle with splats presently 348 | 349 | **[DemoVR.ts](./src/DemoVR.ts)** 350 | ```typescript 351 | import { VRButton } from "three/examples/jsm/webxr/VRButton.js"; 352 | 353 | renderer.xr.enabled = true; 354 | 355 | let vrButton = VRButton.createButton(renderer); 356 | 357 | document.body.appendChild(vrButton); 358 | 359 | let splats = new LumaSplatsThree({ 360 | // Kind Humanoid @RyanHickman 361 | source: 'https://lumalabs.ai/capture/83e9aae8-7023-448e-83a6-53ccb377ec86', 362 | }); 363 | 364 | scene.add(splats); 365 | ``` 366 | -------------------------------------------------------------------------------- /TERMS.txt: -------------------------------------------------------------------------------- 1 | Terms of Service 2 | See https://lumalabs.ai/legal/tos 3 | 4 | 5 | Privacy Policy 6 | See https://lumalabs.ai/legal/privacy -------------------------------------------------------------------------------- /assets/fonts/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/fonts/icons.woff -------------------------------------------------------------------------------- /assets/images/background-removal-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/background-removal-preview.jpg -------------------------------------------------------------------------------- /assets/images/custom-shaders-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/custom-shaders-preview.jpg -------------------------------------------------------------------------------- /assets/images/github-mark-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/images/hello-world-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/hello-world-preview.jpg -------------------------------------------------------------------------------- /assets/images/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/og-image.jpg -------------------------------------------------------------------------------- /assets/images/react-three-fiber-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/react-three-fiber-preview.jpg -------------------------------------------------------------------------------- /assets/images/scene-lighting-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/scene-lighting-preview.jpg -------------------------------------------------------------------------------- /assets/images/three.js-fog-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/three.js-fog-preview.jpg -------------------------------------------------------------------------------- /assets/images/transmission-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/transmission-preview.jpg -------------------------------------------------------------------------------- /assets/images/vr-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/images/vr-preview.jpg -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /assets/models/ufo_b11_d_model.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/models/ufo_b11_d_model.glb -------------------------------------------------------------------------------- /assets/venice_sunset_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/luma-web-examples/fefe15436908140788d061f962e92684e3413cb8/assets/venice_sunset_1k.hdr -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 165 | 203 | 204 | 205 | 206 |
207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /misc/common.sh: -------------------------------------------------------------------------------- 1 | # bash colors 2 | RED='\033[31m' 3 | GREEN='\033[32m' 4 | ORANGE='\033[33m' 5 | RESET='\033[0m' 6 | BOLD='\033[1m' 7 | 8 | HAS_ERRORS=false 9 | 10 | # log functions 11 | error() { 12 | ERROR="${RED}ERROR: " 13 | echo -e "${ERROR}$1${RESET}" 14 | HAS_ERRORS=true 15 | } 16 | fatal() { 17 | error "$1" 18 | exit 1 19 | } 20 | warning() { 21 | WARNING="${ORANGE}WARNING: " 22 | echo -e "${WARNING}$1${RESET}" 23 | HAS_ERRORS=true 24 | } 25 | ok() { 26 | OK="${BOLD}${GREEN}✓${RESET}${GREEN} " 27 | echo -e "${OK}$1${RESET}" 28 | } 29 | doYouWantToContinue() { 30 | echo -e "Do you want to continue? (${BOLD}y${RESET}/${BOLD}n${RESET})" 31 | read -p "> " -n 1 -r 32 | echo 33 | if [[ ! $REPLY =~ ^[Yy]$ ]] 34 | then 35 | exit 36 | fi 37 | } -------------------------------------------------------------------------------- /misc/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # include common.sh 4 | source ./misc/common.sh 5 | 6 | # Ensure the script stops if any command fails 7 | set -e 8 | # set -x 9 | 10 | # Get the version of the package @lumaai/luma-web 11 | version_info=$(npm list --depth=0 | grep "@lumaai/luma-web") 12 | # Extract just the version number 13 | luma_package_version=$(echo $version_info | grep -oE '\b[0-9]+\.[0-9]+\.[0-9]+\b') 14 | # We only care about major.minor, so remove the patch version 15 | luma_package_version="${luma_package_version%.*}" 16 | # Get the latest git commit hash 17 | git_hash=$(git rev-parse --short HEAD) 18 | 19 | # Check for gsutil, explain how to install it if not found 20 | if ! command -v gsutil &> /dev/null 21 | then 22 | fatal "Command \`gsutil\` could not be found, you need Google Cloud SDK CLI to publish (https://cloud.google.com/sdk/docs/install). Check it it available in PATH" 23 | fi 24 | 25 | # check we're in the root directory by the presence of package.json 26 | if [ ! -f "package.json" ] 27 | then 28 | fatal "You must run this script from the root directory of the project" 29 | fi 30 | 31 | # ensure tsc runs without error 32 | npx tsc 33 | 34 | # check if version is correct and if the user wants to continue 35 | echo -e "@lumaai/luma-web version: ${BOLD}${luma_package_version}${RESET}" 36 | doYouWantToContinue 37 | 38 | echo -e "This will delete and rebuild ${BOLD}dist/${RESET}" 39 | doYouWantToContinue 40 | 41 | # Delete the dist directory 42 | rm -rf dist 43 | # Rebuild 44 | npm run build 45 | 46 | # Buckets: 47 | # - prod: cdn-luma.com 48 | # - sandbox: sandbox.cdn-luma.com 49 | # Ask for user to type "prod" or "sandbox" publish and loop until valid input 50 | publish_env="" 51 | while [ "$publish_env" != "prod" ] && [ "$publish_env" != "sandbox" ] 52 | do 53 | echo "" 54 | echo -e "Which bucket do you want to publish to? Type \"${BOLD}prod${RESET}\" or \"${BOLD}sandbox${RESET}\":" 55 | # display input caret 56 | read -p "> " publish_env 57 | done 58 | 59 | # Set bucket name based on user input 60 | if [ "$publish_env" == "prod" ] 61 | then 62 | bucket_name="cdn-luma.com" 63 | else 64 | bucket_name="sandbox.cdn-luma.com" 65 | fi 66 | 67 | parent_path="public/lumalabs.ai/luma-web-library" 68 | path="${parent_path}/${luma_package_version}/${git_hash}" 69 | 70 | gsutil_dir="gs://${bucket_name}/${path}" 71 | 72 | # Upload the root directory to Google Cloud Storage, overwriting any existing files 73 | # excluding node_modules/, misc/, .git/, .DS_Store 74 | EXCLUDE="node_modules/*|misc/*|.git/*|.*\.DS_Store" 75 | 76 | # Dry run, add -n to only show what would be uploaded 77 | gsutil -m rsync -r -d -n -x "${EXCLUDE}" ./ "${gsutil_dir}" 78 | 79 | doYouWantToContinue 80 | 81 | # Run the upload 82 | gsutil -m rsync -r -d -x "${EXCLUDE}" ./ "${gsutil_dir}" 83 | 84 | echo "" 85 | ok "Files have been published" 86 | 87 | public_url="https://${bucket_name}/${path}/index.html" 88 | storage_url="https://console.cloud.google.com/storage/browser/${bucket_name}/${path}" 89 | 90 | echo "" 91 | echo -e "${BOLD}Public URL:${RESET} ${public_url}" 92 | echo -e "${BOLD}Storage URL:${RESET} ${storage_url}" -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "three", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@lumaai/luma-web": "^0.2.1", 13 | "@react-three/drei": "^9.88.17", 14 | "@react-three/fiber": "^8.15.11", 15 | "@types/react": "^18.2.38", 16 | "@types/react-dom": "^18.2.17", 17 | "@types/react-syntax-highlighter": "^15.5.10", 18 | "lil-gui": "^0.19.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-markdown": "^9.0.1", 22 | "react-syntax-highlighter": "^15.5.0", 23 | "three": "^0.157.0" 24 | }, 25 | "devDependencies": { 26 | "@types/three": "^0.157.2", 27 | "esbuild": "^0.19.5", 28 | "typescript": "^5.3.2" 29 | } 30 | }, 31 | "../..": { 32 | "extraneous": true 33 | }, 34 | "node_modules/@babel/runtime": { 35 | "version": "7.23.4", 36 | "license": "MIT", 37 | "dependencies": { 38 | "regenerator-runtime": "^0.14.0" 39 | }, 40 | "engines": { 41 | "node": ">=6.9.0" 42 | } 43 | }, 44 | "node_modules/@esbuild/darwin-arm64": { 45 | "version": "0.19.5", 46 | "cpu": [ 47 | "arm64" 48 | ], 49 | "dev": true, 50 | "license": "MIT", 51 | "optional": true, 52 | "os": [ 53 | "darwin" 54 | ], 55 | "engines": { 56 | "node": ">=12" 57 | } 58 | }, 59 | "node_modules/@lumaai/luma-web": { 60 | "version": "0.2.1", 61 | "resolved": "https://registry.npmjs.org/@lumaai/luma-web/-/luma-web-0.2.1.tgz", 62 | "integrity": "sha512-2m01qEZZgQiCbY8fA+5GLICZwmhvVT17IeR/rXG4xMhbw8RtaQQzqkodZhjIEClZ0CxBcmCvYP8oOEbob+YPdQ==", 63 | "peerDependencies": { 64 | "@types/three": "^0.157.2", 65 | "three": "^0.157.0" 66 | } 67 | }, 68 | "node_modules/@mediapipe/tasks-vision": { 69 | "version": "0.10.8", 70 | "license": "Apache-2.0" 71 | }, 72 | "node_modules/@react-spring/animated": { 73 | "version": "9.6.1", 74 | "license": "MIT", 75 | "dependencies": { 76 | "@react-spring/shared": "~9.6.1", 77 | "@react-spring/types": "~9.6.1" 78 | }, 79 | "peerDependencies": { 80 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 81 | } 82 | }, 83 | "node_modules/@react-spring/core": { 84 | "version": "9.6.1", 85 | "license": "MIT", 86 | "dependencies": { 87 | "@react-spring/animated": "~9.6.1", 88 | "@react-spring/rafz": "~9.6.1", 89 | "@react-spring/shared": "~9.6.1", 90 | "@react-spring/types": "~9.6.1" 91 | }, 92 | "funding": { 93 | "type": "opencollective", 94 | "url": "https://opencollective.com/react-spring/donate" 95 | }, 96 | "peerDependencies": { 97 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 98 | } 99 | }, 100 | "node_modules/@react-spring/rafz": { 101 | "version": "9.6.1", 102 | "license": "MIT" 103 | }, 104 | "node_modules/@react-spring/shared": { 105 | "version": "9.6.1", 106 | "license": "MIT", 107 | "dependencies": { 108 | "@react-spring/rafz": "~9.6.1", 109 | "@react-spring/types": "~9.6.1" 110 | }, 111 | "peerDependencies": { 112 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 113 | } 114 | }, 115 | "node_modules/@react-spring/three": { 116 | "version": "9.6.1", 117 | "license": "MIT", 118 | "dependencies": { 119 | "@react-spring/animated": "~9.6.1", 120 | "@react-spring/core": "~9.6.1", 121 | "@react-spring/shared": "~9.6.1", 122 | "@react-spring/types": "~9.6.1" 123 | }, 124 | "peerDependencies": { 125 | "@react-three/fiber": ">=6.0", 126 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0", 127 | "three": ">=0.126" 128 | } 129 | }, 130 | "node_modules/@react-spring/types": { 131 | "version": "9.6.1", 132 | "license": "MIT" 133 | }, 134 | "node_modules/@react-three/drei": { 135 | "version": "9.88.17", 136 | "license": "MIT", 137 | "dependencies": { 138 | "@babel/runtime": "^7.11.2", 139 | "@mediapipe/tasks-vision": "0.10.8", 140 | "@react-spring/three": "~9.6.1", 141 | "@use-gesture/react": "^10.2.24", 142 | "camera-controls": "^2.4.2", 143 | "cross-env": "^7.0.3", 144 | "detect-gpu": "^5.0.28", 145 | "glsl-noise": "^0.0.0", 146 | "lodash.clamp": "^4.0.3", 147 | "lodash.omit": "^4.5.0", 148 | "lodash.pick": "^4.4.0", 149 | "maath": "^0.9.0", 150 | "meshline": "^3.1.6", 151 | "react-composer": "^5.0.3", 152 | "react-merge-refs": "^1.1.0", 153 | "stats-gl": "^1.0.4", 154 | "stats.js": "^0.17.0", 155 | "suspend-react": "^0.1.3", 156 | "three-mesh-bvh": "^0.6.7", 157 | "three-stdlib": "^2.28.0", 158 | "troika-three-text": "^0.47.2", 159 | "utility-types": "^3.10.0", 160 | "uuid": "^9.0.1", 161 | "zustand": "^3.5.13" 162 | }, 163 | "peerDependencies": { 164 | "@react-three/fiber": ">=8.0", 165 | "react": ">=18.0", 166 | "react-dom": ">=18.0", 167 | "three": ">=0.137" 168 | }, 169 | "peerDependenciesMeta": { 170 | "react-dom": { 171 | "optional": true 172 | } 173 | } 174 | }, 175 | "node_modules/@react-three/fiber": { 176 | "version": "8.15.11", 177 | "license": "MIT", 178 | "dependencies": { 179 | "@babel/runtime": "^7.17.8", 180 | "@types/react-reconciler": "^0.26.7", 181 | "@types/webxr": "*", 182 | "base64-js": "^1.5.1", 183 | "buffer": "^6.0.3", 184 | "its-fine": "^1.0.6", 185 | "react-reconciler": "^0.27.0", 186 | "react-use-measure": "^2.1.1", 187 | "scheduler": "^0.21.0", 188 | "suspend-react": "^0.1.3", 189 | "zustand": "^3.7.1" 190 | }, 191 | "peerDependencies": { 192 | "expo": ">=43.0", 193 | "expo-asset": ">=8.4", 194 | "expo-file-system": ">=11.0", 195 | "expo-gl": ">=11.0", 196 | "react": ">=18.0", 197 | "react-dom": ">=18.0", 198 | "react-native": ">=0.64", 199 | "three": ">=0.133" 200 | }, 201 | "peerDependenciesMeta": { 202 | "expo": { 203 | "optional": true 204 | }, 205 | "expo-asset": { 206 | "optional": true 207 | }, 208 | "expo-file-system": { 209 | "optional": true 210 | }, 211 | "expo-gl": { 212 | "optional": true 213 | }, 214 | "react-dom": { 215 | "optional": true 216 | }, 217 | "react-native": { 218 | "optional": true 219 | } 220 | } 221 | }, 222 | "node_modules/@types/debug": { 223 | "version": "4.1.12", 224 | "license": "MIT", 225 | "dependencies": { 226 | "@types/ms": "*" 227 | } 228 | }, 229 | "node_modules/@types/draco3d": { 230 | "version": "1.4.9", 231 | "license": "MIT" 232 | }, 233 | "node_modules/@types/hast": { 234 | "version": "3.0.3", 235 | "license": "MIT", 236 | "dependencies": { 237 | "@types/unist": "*" 238 | } 239 | }, 240 | "node_modules/@types/mdast": { 241 | "version": "4.0.3", 242 | "license": "MIT", 243 | "dependencies": { 244 | "@types/unist": "*" 245 | } 246 | }, 247 | "node_modules/@types/ms": { 248 | "version": "0.7.34", 249 | "license": "MIT" 250 | }, 251 | "node_modules/@types/offscreencanvas": { 252 | "version": "2019.7.3", 253 | "license": "MIT" 254 | }, 255 | "node_modules/@types/prop-types": { 256 | "version": "15.7.11", 257 | "license": "MIT" 258 | }, 259 | "node_modules/@types/react": { 260 | "version": "18.2.38", 261 | "license": "MIT", 262 | "dependencies": { 263 | "@types/prop-types": "*", 264 | "@types/scheduler": "*", 265 | "csstype": "^3.0.2" 266 | } 267 | }, 268 | "node_modules/@types/react-dom": { 269 | "version": "18.2.17", 270 | "license": "MIT", 271 | "dependencies": { 272 | "@types/react": "*" 273 | } 274 | }, 275 | "node_modules/@types/react-reconciler": { 276 | "version": "0.26.7", 277 | "license": "MIT", 278 | "dependencies": { 279 | "@types/react": "*" 280 | } 281 | }, 282 | "node_modules/@types/react-syntax-highlighter": { 283 | "version": "15.5.10", 284 | "license": "MIT", 285 | "dependencies": { 286 | "@types/react": "*" 287 | } 288 | }, 289 | "node_modules/@types/scheduler": { 290 | "version": "0.16.8", 291 | "license": "MIT" 292 | }, 293 | "node_modules/@types/stats.js": { 294 | "version": "0.17.2", 295 | "license": "MIT" 296 | }, 297 | "node_modules/@types/three": { 298 | "version": "0.157.2", 299 | "license": "MIT", 300 | "dependencies": { 301 | "@types/stats.js": "*", 302 | "@types/webxr": "*", 303 | "fflate": "~0.6.10", 304 | "meshoptimizer": "~0.18.1" 305 | } 306 | }, 307 | "node_modules/@types/unist": { 308 | "version": "3.0.2", 309 | "license": "MIT" 310 | }, 311 | "node_modules/@types/webxr": { 312 | "version": "0.5.6", 313 | "license": "MIT" 314 | }, 315 | "node_modules/@ungap/structured-clone": { 316 | "version": "1.2.0", 317 | "license": "ISC" 318 | }, 319 | "node_modules/@use-gesture/core": { 320 | "version": "10.3.0", 321 | "license": "MIT" 322 | }, 323 | "node_modules/@use-gesture/react": { 324 | "version": "10.3.0", 325 | "license": "MIT", 326 | "dependencies": { 327 | "@use-gesture/core": "10.3.0" 328 | }, 329 | "peerDependencies": { 330 | "react": ">= 16.8.0" 331 | } 332 | }, 333 | "node_modules/bail": { 334 | "version": "2.0.2", 335 | "license": "MIT", 336 | "funding": { 337 | "type": "github", 338 | "url": "https://github.com/sponsors/wooorm" 339 | } 340 | }, 341 | "node_modules/base64-js": { 342 | "version": "1.5.1", 343 | "funding": [ 344 | { 345 | "type": "github", 346 | "url": "https://github.com/sponsors/feross" 347 | }, 348 | { 349 | "type": "patreon", 350 | "url": "https://www.patreon.com/feross" 351 | }, 352 | { 353 | "type": "consulting", 354 | "url": "https://feross.org/support" 355 | } 356 | ], 357 | "license": "MIT" 358 | }, 359 | "node_modules/bidi-js": { 360 | "version": "1.0.3", 361 | "license": "MIT", 362 | "dependencies": { 363 | "require-from-string": "^2.0.2" 364 | } 365 | }, 366 | "node_modules/buffer": { 367 | "version": "6.0.3", 368 | "funding": [ 369 | { 370 | "type": "github", 371 | "url": "https://github.com/sponsors/feross" 372 | }, 373 | { 374 | "type": "patreon", 375 | "url": "https://www.patreon.com/feross" 376 | }, 377 | { 378 | "type": "consulting", 379 | "url": "https://feross.org/support" 380 | } 381 | ], 382 | "license": "MIT", 383 | "dependencies": { 384 | "base64-js": "^1.3.1", 385 | "ieee754": "^1.2.1" 386 | } 387 | }, 388 | "node_modules/camera-controls": { 389 | "version": "2.7.3", 390 | "license": "MIT", 391 | "peerDependencies": { 392 | "three": ">=0.126.1" 393 | } 394 | }, 395 | "node_modules/character-entities": { 396 | "version": "2.0.2", 397 | "license": "MIT", 398 | "funding": { 399 | "type": "github", 400 | "url": "https://github.com/sponsors/wooorm" 401 | } 402 | }, 403 | "node_modules/character-entities-legacy": { 404 | "version": "1.1.4", 405 | "license": "MIT", 406 | "funding": { 407 | "type": "github", 408 | "url": "https://github.com/sponsors/wooorm" 409 | } 410 | }, 411 | "node_modules/character-reference-invalid": { 412 | "version": "1.1.4", 413 | "license": "MIT", 414 | "funding": { 415 | "type": "github", 416 | "url": "https://github.com/sponsors/wooorm" 417 | } 418 | }, 419 | "node_modules/comma-separated-tokens": { 420 | "version": "2.0.3", 421 | "license": "MIT", 422 | "funding": { 423 | "type": "github", 424 | "url": "https://github.com/sponsors/wooorm" 425 | } 426 | }, 427 | "node_modules/cross-env": { 428 | "version": "7.0.3", 429 | "license": "MIT", 430 | "dependencies": { 431 | "cross-spawn": "^7.0.1" 432 | }, 433 | "bin": { 434 | "cross-env": "src/bin/cross-env.js", 435 | "cross-env-shell": "src/bin/cross-env-shell.js" 436 | }, 437 | "engines": { 438 | "node": ">=10.14", 439 | "npm": ">=6", 440 | "yarn": ">=1" 441 | } 442 | }, 443 | "node_modules/cross-spawn": { 444 | "version": "7.0.3", 445 | "license": "MIT", 446 | "dependencies": { 447 | "path-key": "^3.1.0", 448 | "shebang-command": "^2.0.0", 449 | "which": "^2.0.1" 450 | }, 451 | "engines": { 452 | "node": ">= 8" 453 | } 454 | }, 455 | "node_modules/csstype": { 456 | "version": "3.1.2", 457 | "license": "MIT" 458 | }, 459 | "node_modules/debounce": { 460 | "version": "1.2.1", 461 | "license": "MIT" 462 | }, 463 | "node_modules/debug": { 464 | "version": "4.3.4", 465 | "license": "MIT", 466 | "dependencies": { 467 | "ms": "2.1.2" 468 | }, 469 | "engines": { 470 | "node": ">=6.0" 471 | }, 472 | "peerDependenciesMeta": { 473 | "supports-color": { 474 | "optional": true 475 | } 476 | } 477 | }, 478 | "node_modules/decode-named-character-reference": { 479 | "version": "1.0.2", 480 | "license": "MIT", 481 | "dependencies": { 482 | "character-entities": "^2.0.0" 483 | }, 484 | "funding": { 485 | "type": "github", 486 | "url": "https://github.com/sponsors/wooorm" 487 | } 488 | }, 489 | "node_modules/dequal": { 490 | "version": "2.0.3", 491 | "license": "MIT", 492 | "engines": { 493 | "node": ">=6" 494 | } 495 | }, 496 | "node_modules/detect-gpu": { 497 | "version": "5.0.37", 498 | "license": "MIT", 499 | "dependencies": { 500 | "webgl-constants": "^1.1.1" 501 | } 502 | }, 503 | "node_modules/devlop": { 504 | "version": "1.1.0", 505 | "license": "MIT", 506 | "dependencies": { 507 | "dequal": "^2.0.0" 508 | }, 509 | "funding": { 510 | "type": "github", 511 | "url": "https://github.com/sponsors/wooorm" 512 | } 513 | }, 514 | "node_modules/draco3d": { 515 | "version": "1.5.6", 516 | "license": "Apache-2.0" 517 | }, 518 | "node_modules/esbuild": { 519 | "version": "0.19.5", 520 | "dev": true, 521 | "hasInstallScript": true, 522 | "license": "MIT", 523 | "bin": { 524 | "esbuild": "bin/esbuild" 525 | }, 526 | "engines": { 527 | "node": ">=12" 528 | }, 529 | "optionalDependencies": { 530 | "@esbuild/android-arm": "0.19.5", 531 | "@esbuild/android-arm64": "0.19.5", 532 | "@esbuild/android-x64": "0.19.5", 533 | "@esbuild/darwin-arm64": "0.19.5", 534 | "@esbuild/darwin-x64": "0.19.5", 535 | "@esbuild/freebsd-arm64": "0.19.5", 536 | "@esbuild/freebsd-x64": "0.19.5", 537 | "@esbuild/linux-arm": "0.19.5", 538 | "@esbuild/linux-arm64": "0.19.5", 539 | "@esbuild/linux-ia32": "0.19.5", 540 | "@esbuild/linux-loong64": "0.19.5", 541 | "@esbuild/linux-mips64el": "0.19.5", 542 | "@esbuild/linux-ppc64": "0.19.5", 543 | "@esbuild/linux-riscv64": "0.19.5", 544 | "@esbuild/linux-s390x": "0.19.5", 545 | "@esbuild/linux-x64": "0.19.5", 546 | "@esbuild/netbsd-x64": "0.19.5", 547 | "@esbuild/openbsd-x64": "0.19.5", 548 | "@esbuild/sunos-x64": "0.19.5", 549 | "@esbuild/win32-arm64": "0.19.5", 550 | "@esbuild/win32-ia32": "0.19.5", 551 | "@esbuild/win32-x64": "0.19.5" 552 | } 553 | }, 554 | "node_modules/extend": { 555 | "version": "3.0.2", 556 | "license": "MIT" 557 | }, 558 | "node_modules/fault": { 559 | "version": "1.0.4", 560 | "license": "MIT", 561 | "dependencies": { 562 | "format": "^0.2.0" 563 | }, 564 | "funding": { 565 | "type": "github", 566 | "url": "https://github.com/sponsors/wooorm" 567 | } 568 | }, 569 | "node_modules/fflate": { 570 | "version": "0.6.10", 571 | "license": "MIT" 572 | }, 573 | "node_modules/format": { 574 | "version": "0.2.2", 575 | "engines": { 576 | "node": ">=0.4.x" 577 | } 578 | }, 579 | "node_modules/glsl-noise": { 580 | "version": "0.0.0", 581 | "license": "MIT" 582 | }, 583 | "node_modules/hast-util-parse-selector": { 584 | "version": "2.2.5", 585 | "license": "MIT", 586 | "funding": { 587 | "type": "opencollective", 588 | "url": "https://opencollective.com/unified" 589 | } 590 | }, 591 | "node_modules/hast-util-to-jsx-runtime": { 592 | "version": "2.2.0", 593 | "license": "MIT", 594 | "dependencies": { 595 | "@types/hast": "^3.0.0", 596 | "@types/unist": "^3.0.0", 597 | "comma-separated-tokens": "^2.0.0", 598 | "hast-util-whitespace": "^3.0.0", 599 | "property-information": "^6.0.0", 600 | "space-separated-tokens": "^2.0.0", 601 | "style-to-object": "^0.4.0", 602 | "unist-util-position": "^5.0.0", 603 | "vfile-message": "^4.0.0" 604 | }, 605 | "funding": { 606 | "type": "opencollective", 607 | "url": "https://opencollective.com/unified" 608 | } 609 | }, 610 | "node_modules/hast-util-whitespace": { 611 | "version": "3.0.0", 612 | "license": "MIT", 613 | "dependencies": { 614 | "@types/hast": "^3.0.0" 615 | }, 616 | "funding": { 617 | "type": "opencollective", 618 | "url": "https://opencollective.com/unified" 619 | } 620 | }, 621 | "node_modules/hastscript": { 622 | "version": "6.0.0", 623 | "license": "MIT", 624 | "dependencies": { 625 | "@types/hast": "^2.0.0", 626 | "comma-separated-tokens": "^1.0.0", 627 | "hast-util-parse-selector": "^2.0.0", 628 | "property-information": "^5.0.0", 629 | "space-separated-tokens": "^1.0.0" 630 | }, 631 | "funding": { 632 | "type": "opencollective", 633 | "url": "https://opencollective.com/unified" 634 | } 635 | }, 636 | "node_modules/hastscript/node_modules/@types/hast": { 637 | "version": "2.3.8", 638 | "license": "MIT", 639 | "dependencies": { 640 | "@types/unist": "^2" 641 | } 642 | }, 643 | "node_modules/hastscript/node_modules/@types/unist": { 644 | "version": "2.0.10", 645 | "license": "MIT" 646 | }, 647 | "node_modules/hastscript/node_modules/comma-separated-tokens": { 648 | "version": "1.0.8", 649 | "license": "MIT", 650 | "funding": { 651 | "type": "github", 652 | "url": "https://github.com/sponsors/wooorm" 653 | } 654 | }, 655 | "node_modules/hastscript/node_modules/property-information": { 656 | "version": "5.6.0", 657 | "license": "MIT", 658 | "dependencies": { 659 | "xtend": "^4.0.0" 660 | }, 661 | "funding": { 662 | "type": "github", 663 | "url": "https://github.com/sponsors/wooorm" 664 | } 665 | }, 666 | "node_modules/hastscript/node_modules/space-separated-tokens": { 667 | "version": "1.1.5", 668 | "license": "MIT", 669 | "funding": { 670 | "type": "github", 671 | "url": "https://github.com/sponsors/wooorm" 672 | } 673 | }, 674 | "node_modules/highlight.js": { 675 | "version": "10.7.3", 676 | "license": "BSD-3-Clause", 677 | "engines": { 678 | "node": "*" 679 | } 680 | }, 681 | "node_modules/html-url-attributes": { 682 | "version": "3.0.0", 683 | "license": "MIT", 684 | "funding": { 685 | "type": "opencollective", 686 | "url": "https://opencollective.com/unified" 687 | } 688 | }, 689 | "node_modules/ieee754": { 690 | "version": "1.2.1", 691 | "funding": [ 692 | { 693 | "type": "github", 694 | "url": "https://github.com/sponsors/feross" 695 | }, 696 | { 697 | "type": "patreon", 698 | "url": "https://www.patreon.com/feross" 699 | }, 700 | { 701 | "type": "consulting", 702 | "url": "https://feross.org/support" 703 | } 704 | ], 705 | "license": "BSD-3-Clause" 706 | }, 707 | "node_modules/inline-style-parser": { 708 | "version": "0.1.1", 709 | "license": "MIT" 710 | }, 711 | "node_modules/is-alphabetical": { 712 | "version": "1.0.4", 713 | "license": "MIT", 714 | "funding": { 715 | "type": "github", 716 | "url": "https://github.com/sponsors/wooorm" 717 | } 718 | }, 719 | "node_modules/is-alphanumerical": { 720 | "version": "1.0.4", 721 | "license": "MIT", 722 | "dependencies": { 723 | "is-alphabetical": "^1.0.0", 724 | "is-decimal": "^1.0.0" 725 | }, 726 | "funding": { 727 | "type": "github", 728 | "url": "https://github.com/sponsors/wooorm" 729 | } 730 | }, 731 | "node_modules/is-decimal": { 732 | "version": "1.0.4", 733 | "license": "MIT", 734 | "funding": { 735 | "type": "github", 736 | "url": "https://github.com/sponsors/wooorm" 737 | } 738 | }, 739 | "node_modules/is-hexadecimal": { 740 | "version": "1.0.4", 741 | "license": "MIT", 742 | "funding": { 743 | "type": "github", 744 | "url": "https://github.com/sponsors/wooorm" 745 | } 746 | }, 747 | "node_modules/is-plain-obj": { 748 | "version": "4.1.0", 749 | "license": "MIT", 750 | "engines": { 751 | "node": ">=12" 752 | }, 753 | "funding": { 754 | "url": "https://github.com/sponsors/sindresorhus" 755 | } 756 | }, 757 | "node_modules/isexe": { 758 | "version": "2.0.0", 759 | "license": "ISC" 760 | }, 761 | "node_modules/its-fine": { 762 | "version": "1.1.1", 763 | "license": "MIT", 764 | "dependencies": { 765 | "@types/react-reconciler": "^0.28.0" 766 | }, 767 | "peerDependencies": { 768 | "react": ">=18.0" 769 | } 770 | }, 771 | "node_modules/its-fine/node_modules/@types/react-reconciler": { 772 | "version": "0.28.8", 773 | "license": "MIT", 774 | "dependencies": { 775 | "@types/react": "*" 776 | } 777 | }, 778 | "node_modules/js-tokens": { 779 | "version": "4.0.0", 780 | "license": "MIT" 781 | }, 782 | "node_modules/lil-gui": { 783 | "version": "0.19.1", 784 | "license": "MIT" 785 | }, 786 | "node_modules/lodash.clamp": { 787 | "version": "4.0.3", 788 | "license": "MIT" 789 | }, 790 | "node_modules/lodash.omit": { 791 | "version": "4.5.0", 792 | "license": "MIT" 793 | }, 794 | "node_modules/lodash.pick": { 795 | "version": "4.4.0", 796 | "license": "MIT" 797 | }, 798 | "node_modules/loose-envify": { 799 | "version": "1.4.0", 800 | "license": "MIT", 801 | "dependencies": { 802 | "js-tokens": "^3.0.0 || ^4.0.0" 803 | }, 804 | "bin": { 805 | "loose-envify": "cli.js" 806 | } 807 | }, 808 | "node_modules/lowlight": { 809 | "version": "1.20.0", 810 | "license": "MIT", 811 | "dependencies": { 812 | "fault": "^1.0.0", 813 | "highlight.js": "~10.7.0" 814 | }, 815 | "funding": { 816 | "type": "github", 817 | "url": "https://github.com/sponsors/wooorm" 818 | } 819 | }, 820 | "node_modules/maath": { 821 | "version": "0.9.0", 822 | "license": "MIT", 823 | "peerDependencies": { 824 | "@types/three": ">=0.144.0", 825 | "three": ">=0.144.0" 826 | } 827 | }, 828 | "node_modules/mdast-util-from-markdown": { 829 | "version": "2.0.0", 830 | "license": "MIT", 831 | "dependencies": { 832 | "@types/mdast": "^4.0.0", 833 | "@types/unist": "^3.0.0", 834 | "decode-named-character-reference": "^1.0.0", 835 | "devlop": "^1.0.0", 836 | "mdast-util-to-string": "^4.0.0", 837 | "micromark": "^4.0.0", 838 | "micromark-util-decode-numeric-character-reference": "^2.0.0", 839 | "micromark-util-decode-string": "^2.0.0", 840 | "micromark-util-normalize-identifier": "^2.0.0", 841 | "micromark-util-symbol": "^2.0.0", 842 | "micromark-util-types": "^2.0.0", 843 | "unist-util-stringify-position": "^4.0.0" 844 | }, 845 | "funding": { 846 | "type": "opencollective", 847 | "url": "https://opencollective.com/unified" 848 | } 849 | }, 850 | "node_modules/mdast-util-to-hast": { 851 | "version": "13.0.2", 852 | "license": "MIT", 853 | "dependencies": { 854 | "@types/hast": "^3.0.0", 855 | "@types/mdast": "^4.0.0", 856 | "@ungap/structured-clone": "^1.0.0", 857 | "devlop": "^1.0.0", 858 | "micromark-util-sanitize-uri": "^2.0.0", 859 | "trim-lines": "^3.0.0", 860 | "unist-util-position": "^5.0.0", 861 | "unist-util-visit": "^5.0.0" 862 | }, 863 | "funding": { 864 | "type": "opencollective", 865 | "url": "https://opencollective.com/unified" 866 | } 867 | }, 868 | "node_modules/mdast-util-to-string": { 869 | "version": "4.0.0", 870 | "license": "MIT", 871 | "dependencies": { 872 | "@types/mdast": "^4.0.0" 873 | }, 874 | "funding": { 875 | "type": "opencollective", 876 | "url": "https://opencollective.com/unified" 877 | } 878 | }, 879 | "node_modules/meshline": { 880 | "version": "3.1.7", 881 | "license": "MIT", 882 | "peerDependencies": { 883 | "three": ">=0.137" 884 | } 885 | }, 886 | "node_modules/meshoptimizer": { 887 | "version": "0.18.1", 888 | "license": "MIT" 889 | }, 890 | "node_modules/micromark": { 891 | "version": "4.0.0", 892 | "funding": [ 893 | { 894 | "type": "GitHub Sponsors", 895 | "url": "https://github.com/sponsors/unifiedjs" 896 | }, 897 | { 898 | "type": "OpenCollective", 899 | "url": "https://opencollective.com/unified" 900 | } 901 | ], 902 | "license": "MIT", 903 | "dependencies": { 904 | "@types/debug": "^4.0.0", 905 | "debug": "^4.0.0", 906 | "decode-named-character-reference": "^1.0.0", 907 | "devlop": "^1.0.0", 908 | "micromark-core-commonmark": "^2.0.0", 909 | "micromark-factory-space": "^2.0.0", 910 | "micromark-util-character": "^2.0.0", 911 | "micromark-util-chunked": "^2.0.0", 912 | "micromark-util-combine-extensions": "^2.0.0", 913 | "micromark-util-decode-numeric-character-reference": "^2.0.0", 914 | "micromark-util-encode": "^2.0.0", 915 | "micromark-util-normalize-identifier": "^2.0.0", 916 | "micromark-util-resolve-all": "^2.0.0", 917 | "micromark-util-sanitize-uri": "^2.0.0", 918 | "micromark-util-subtokenize": "^2.0.0", 919 | "micromark-util-symbol": "^2.0.0", 920 | "micromark-util-types": "^2.0.0" 921 | } 922 | }, 923 | "node_modules/micromark-core-commonmark": { 924 | "version": "2.0.0", 925 | "funding": [ 926 | { 927 | "type": "GitHub Sponsors", 928 | "url": "https://github.com/sponsors/unifiedjs" 929 | }, 930 | { 931 | "type": "OpenCollective", 932 | "url": "https://opencollective.com/unified" 933 | } 934 | ], 935 | "license": "MIT", 936 | "dependencies": { 937 | "decode-named-character-reference": "^1.0.0", 938 | "devlop": "^1.0.0", 939 | "micromark-factory-destination": "^2.0.0", 940 | "micromark-factory-label": "^2.0.0", 941 | "micromark-factory-space": "^2.0.0", 942 | "micromark-factory-title": "^2.0.0", 943 | "micromark-factory-whitespace": "^2.0.0", 944 | "micromark-util-character": "^2.0.0", 945 | "micromark-util-chunked": "^2.0.0", 946 | "micromark-util-classify-character": "^2.0.0", 947 | "micromark-util-html-tag-name": "^2.0.0", 948 | "micromark-util-normalize-identifier": "^2.0.0", 949 | "micromark-util-resolve-all": "^2.0.0", 950 | "micromark-util-subtokenize": "^2.0.0", 951 | "micromark-util-symbol": "^2.0.0", 952 | "micromark-util-types": "^2.0.0" 953 | } 954 | }, 955 | "node_modules/micromark-factory-destination": { 956 | "version": "2.0.0", 957 | "funding": [ 958 | { 959 | "type": "GitHub Sponsors", 960 | "url": "https://github.com/sponsors/unifiedjs" 961 | }, 962 | { 963 | "type": "OpenCollective", 964 | "url": "https://opencollective.com/unified" 965 | } 966 | ], 967 | "license": "MIT", 968 | "dependencies": { 969 | "micromark-util-character": "^2.0.0", 970 | "micromark-util-symbol": "^2.0.0", 971 | "micromark-util-types": "^2.0.0" 972 | } 973 | }, 974 | "node_modules/micromark-factory-label": { 975 | "version": "2.0.0", 976 | "funding": [ 977 | { 978 | "type": "GitHub Sponsors", 979 | "url": "https://github.com/sponsors/unifiedjs" 980 | }, 981 | { 982 | "type": "OpenCollective", 983 | "url": "https://opencollective.com/unified" 984 | } 985 | ], 986 | "license": "MIT", 987 | "dependencies": { 988 | "devlop": "^1.0.0", 989 | "micromark-util-character": "^2.0.0", 990 | "micromark-util-symbol": "^2.0.0", 991 | "micromark-util-types": "^2.0.0" 992 | } 993 | }, 994 | "node_modules/micromark-factory-space": { 995 | "version": "2.0.0", 996 | "funding": [ 997 | { 998 | "type": "GitHub Sponsors", 999 | "url": "https://github.com/sponsors/unifiedjs" 1000 | }, 1001 | { 1002 | "type": "OpenCollective", 1003 | "url": "https://opencollective.com/unified" 1004 | } 1005 | ], 1006 | "license": "MIT", 1007 | "dependencies": { 1008 | "micromark-util-character": "^2.0.0", 1009 | "micromark-util-types": "^2.0.0" 1010 | } 1011 | }, 1012 | "node_modules/micromark-factory-title": { 1013 | "version": "2.0.0", 1014 | "funding": [ 1015 | { 1016 | "type": "GitHub Sponsors", 1017 | "url": "https://github.com/sponsors/unifiedjs" 1018 | }, 1019 | { 1020 | "type": "OpenCollective", 1021 | "url": "https://opencollective.com/unified" 1022 | } 1023 | ], 1024 | "license": "MIT", 1025 | "dependencies": { 1026 | "micromark-factory-space": "^2.0.0", 1027 | "micromark-util-character": "^2.0.0", 1028 | "micromark-util-symbol": "^2.0.0", 1029 | "micromark-util-types": "^2.0.0" 1030 | } 1031 | }, 1032 | "node_modules/micromark-factory-whitespace": { 1033 | "version": "2.0.0", 1034 | "funding": [ 1035 | { 1036 | "type": "GitHub Sponsors", 1037 | "url": "https://github.com/sponsors/unifiedjs" 1038 | }, 1039 | { 1040 | "type": "OpenCollective", 1041 | "url": "https://opencollective.com/unified" 1042 | } 1043 | ], 1044 | "license": "MIT", 1045 | "dependencies": { 1046 | "micromark-factory-space": "^2.0.0", 1047 | "micromark-util-character": "^2.0.0", 1048 | "micromark-util-symbol": "^2.0.0", 1049 | "micromark-util-types": "^2.0.0" 1050 | } 1051 | }, 1052 | "node_modules/micromark-util-character": { 1053 | "version": "2.0.1", 1054 | "funding": [ 1055 | { 1056 | "type": "GitHub Sponsors", 1057 | "url": "https://github.com/sponsors/unifiedjs" 1058 | }, 1059 | { 1060 | "type": "OpenCollective", 1061 | "url": "https://opencollective.com/unified" 1062 | } 1063 | ], 1064 | "license": "MIT", 1065 | "dependencies": { 1066 | "micromark-util-symbol": "^2.0.0", 1067 | "micromark-util-types": "^2.0.0" 1068 | } 1069 | }, 1070 | "node_modules/micromark-util-chunked": { 1071 | "version": "2.0.0", 1072 | "funding": [ 1073 | { 1074 | "type": "GitHub Sponsors", 1075 | "url": "https://github.com/sponsors/unifiedjs" 1076 | }, 1077 | { 1078 | "type": "OpenCollective", 1079 | "url": "https://opencollective.com/unified" 1080 | } 1081 | ], 1082 | "license": "MIT", 1083 | "dependencies": { 1084 | "micromark-util-symbol": "^2.0.0" 1085 | } 1086 | }, 1087 | "node_modules/micromark-util-classify-character": { 1088 | "version": "2.0.0", 1089 | "funding": [ 1090 | { 1091 | "type": "GitHub Sponsors", 1092 | "url": "https://github.com/sponsors/unifiedjs" 1093 | }, 1094 | { 1095 | "type": "OpenCollective", 1096 | "url": "https://opencollective.com/unified" 1097 | } 1098 | ], 1099 | "license": "MIT", 1100 | "dependencies": { 1101 | "micromark-util-character": "^2.0.0", 1102 | "micromark-util-symbol": "^2.0.0", 1103 | "micromark-util-types": "^2.0.0" 1104 | } 1105 | }, 1106 | "node_modules/micromark-util-combine-extensions": { 1107 | "version": "2.0.0", 1108 | "funding": [ 1109 | { 1110 | "type": "GitHub Sponsors", 1111 | "url": "https://github.com/sponsors/unifiedjs" 1112 | }, 1113 | { 1114 | "type": "OpenCollective", 1115 | "url": "https://opencollective.com/unified" 1116 | } 1117 | ], 1118 | "license": "MIT", 1119 | "dependencies": { 1120 | "micromark-util-chunked": "^2.0.0", 1121 | "micromark-util-types": "^2.0.0" 1122 | } 1123 | }, 1124 | "node_modules/micromark-util-decode-numeric-character-reference": { 1125 | "version": "2.0.1", 1126 | "funding": [ 1127 | { 1128 | "type": "GitHub Sponsors", 1129 | "url": "https://github.com/sponsors/unifiedjs" 1130 | }, 1131 | { 1132 | "type": "OpenCollective", 1133 | "url": "https://opencollective.com/unified" 1134 | } 1135 | ], 1136 | "license": "MIT", 1137 | "dependencies": { 1138 | "micromark-util-symbol": "^2.0.0" 1139 | } 1140 | }, 1141 | "node_modules/micromark-util-decode-string": { 1142 | "version": "2.0.0", 1143 | "funding": [ 1144 | { 1145 | "type": "GitHub Sponsors", 1146 | "url": "https://github.com/sponsors/unifiedjs" 1147 | }, 1148 | { 1149 | "type": "OpenCollective", 1150 | "url": "https://opencollective.com/unified" 1151 | } 1152 | ], 1153 | "license": "MIT", 1154 | "dependencies": { 1155 | "decode-named-character-reference": "^1.0.0", 1156 | "micromark-util-character": "^2.0.0", 1157 | "micromark-util-decode-numeric-character-reference": "^2.0.0", 1158 | "micromark-util-symbol": "^2.0.0" 1159 | } 1160 | }, 1161 | "node_modules/micromark-util-encode": { 1162 | "version": "2.0.0", 1163 | "funding": [ 1164 | { 1165 | "type": "GitHub Sponsors", 1166 | "url": "https://github.com/sponsors/unifiedjs" 1167 | }, 1168 | { 1169 | "type": "OpenCollective", 1170 | "url": "https://opencollective.com/unified" 1171 | } 1172 | ], 1173 | "license": "MIT" 1174 | }, 1175 | "node_modules/micromark-util-html-tag-name": { 1176 | "version": "2.0.0", 1177 | "funding": [ 1178 | { 1179 | "type": "GitHub Sponsors", 1180 | "url": "https://github.com/sponsors/unifiedjs" 1181 | }, 1182 | { 1183 | "type": "OpenCollective", 1184 | "url": "https://opencollective.com/unified" 1185 | } 1186 | ], 1187 | "license": "MIT" 1188 | }, 1189 | "node_modules/micromark-util-normalize-identifier": { 1190 | "version": "2.0.0", 1191 | "funding": [ 1192 | { 1193 | "type": "GitHub Sponsors", 1194 | "url": "https://github.com/sponsors/unifiedjs" 1195 | }, 1196 | { 1197 | "type": "OpenCollective", 1198 | "url": "https://opencollective.com/unified" 1199 | } 1200 | ], 1201 | "license": "MIT", 1202 | "dependencies": { 1203 | "micromark-util-symbol": "^2.0.0" 1204 | } 1205 | }, 1206 | "node_modules/micromark-util-resolve-all": { 1207 | "version": "2.0.0", 1208 | "funding": [ 1209 | { 1210 | "type": "GitHub Sponsors", 1211 | "url": "https://github.com/sponsors/unifiedjs" 1212 | }, 1213 | { 1214 | "type": "OpenCollective", 1215 | "url": "https://opencollective.com/unified" 1216 | } 1217 | ], 1218 | "license": "MIT", 1219 | "dependencies": { 1220 | "micromark-util-types": "^2.0.0" 1221 | } 1222 | }, 1223 | "node_modules/micromark-util-sanitize-uri": { 1224 | "version": "2.0.0", 1225 | "funding": [ 1226 | { 1227 | "type": "GitHub Sponsors", 1228 | "url": "https://github.com/sponsors/unifiedjs" 1229 | }, 1230 | { 1231 | "type": "OpenCollective", 1232 | "url": "https://opencollective.com/unified" 1233 | } 1234 | ], 1235 | "license": "MIT", 1236 | "dependencies": { 1237 | "micromark-util-character": "^2.0.0", 1238 | "micromark-util-encode": "^2.0.0", 1239 | "micromark-util-symbol": "^2.0.0" 1240 | } 1241 | }, 1242 | "node_modules/micromark-util-subtokenize": { 1243 | "version": "2.0.0", 1244 | "funding": [ 1245 | { 1246 | "type": "GitHub Sponsors", 1247 | "url": "https://github.com/sponsors/unifiedjs" 1248 | }, 1249 | { 1250 | "type": "OpenCollective", 1251 | "url": "https://opencollective.com/unified" 1252 | } 1253 | ], 1254 | "license": "MIT", 1255 | "dependencies": { 1256 | "devlop": "^1.0.0", 1257 | "micromark-util-chunked": "^2.0.0", 1258 | "micromark-util-symbol": "^2.0.0", 1259 | "micromark-util-types": "^2.0.0" 1260 | } 1261 | }, 1262 | "node_modules/micromark-util-symbol": { 1263 | "version": "2.0.0", 1264 | "funding": [ 1265 | { 1266 | "type": "GitHub Sponsors", 1267 | "url": "https://github.com/sponsors/unifiedjs" 1268 | }, 1269 | { 1270 | "type": "OpenCollective", 1271 | "url": "https://opencollective.com/unified" 1272 | } 1273 | ], 1274 | "license": "MIT" 1275 | }, 1276 | "node_modules/micromark-util-types": { 1277 | "version": "2.0.0", 1278 | "funding": [ 1279 | { 1280 | "type": "GitHub Sponsors", 1281 | "url": "https://github.com/sponsors/unifiedjs" 1282 | }, 1283 | { 1284 | "type": "OpenCollective", 1285 | "url": "https://opencollective.com/unified" 1286 | } 1287 | ], 1288 | "license": "MIT" 1289 | }, 1290 | "node_modules/ms": { 1291 | "version": "2.1.2", 1292 | "license": "MIT" 1293 | }, 1294 | "node_modules/object-assign": { 1295 | "version": "4.1.1", 1296 | "license": "MIT", 1297 | "engines": { 1298 | "node": ">=0.10.0" 1299 | } 1300 | }, 1301 | "node_modules/parse-entities": { 1302 | "version": "2.0.0", 1303 | "license": "MIT", 1304 | "dependencies": { 1305 | "character-entities": "^1.0.0", 1306 | "character-entities-legacy": "^1.0.0", 1307 | "character-reference-invalid": "^1.0.0", 1308 | "is-alphanumerical": "^1.0.0", 1309 | "is-decimal": "^1.0.0", 1310 | "is-hexadecimal": "^1.0.0" 1311 | }, 1312 | "funding": { 1313 | "type": "github", 1314 | "url": "https://github.com/sponsors/wooorm" 1315 | } 1316 | }, 1317 | "node_modules/parse-entities/node_modules/character-entities": { 1318 | "version": "1.2.4", 1319 | "license": "MIT", 1320 | "funding": { 1321 | "type": "github", 1322 | "url": "https://github.com/sponsors/wooorm" 1323 | } 1324 | }, 1325 | "node_modules/path-key": { 1326 | "version": "3.1.1", 1327 | "license": "MIT", 1328 | "engines": { 1329 | "node": ">=8" 1330 | } 1331 | }, 1332 | "node_modules/potpack": { 1333 | "version": "1.0.2", 1334 | "license": "ISC" 1335 | }, 1336 | "node_modules/prismjs": { 1337 | "version": "1.29.0", 1338 | "license": "MIT", 1339 | "engines": { 1340 | "node": ">=6" 1341 | } 1342 | }, 1343 | "node_modules/prop-types": { 1344 | "version": "15.8.1", 1345 | "license": "MIT", 1346 | "dependencies": { 1347 | "loose-envify": "^1.4.0", 1348 | "object-assign": "^4.1.1", 1349 | "react-is": "^16.13.1" 1350 | } 1351 | }, 1352 | "node_modules/property-information": { 1353 | "version": "6.4.0", 1354 | "license": "MIT", 1355 | "funding": { 1356 | "type": "github", 1357 | "url": "https://github.com/sponsors/wooorm" 1358 | } 1359 | }, 1360 | "node_modules/react": { 1361 | "version": "18.2.0", 1362 | "license": "MIT", 1363 | "dependencies": { 1364 | "loose-envify": "^1.1.0" 1365 | }, 1366 | "engines": { 1367 | "node": ">=0.10.0" 1368 | } 1369 | }, 1370 | "node_modules/react-composer": { 1371 | "version": "5.0.3", 1372 | "license": "MIT", 1373 | "dependencies": { 1374 | "prop-types": "^15.6.0" 1375 | }, 1376 | "peerDependencies": { 1377 | "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" 1378 | } 1379 | }, 1380 | "node_modules/react-dom": { 1381 | "version": "18.2.0", 1382 | "license": "MIT", 1383 | "dependencies": { 1384 | "loose-envify": "^1.1.0", 1385 | "scheduler": "^0.23.0" 1386 | }, 1387 | "peerDependencies": { 1388 | "react": "^18.2.0" 1389 | } 1390 | }, 1391 | "node_modules/react-dom/node_modules/scheduler": { 1392 | "version": "0.23.0", 1393 | "license": "MIT", 1394 | "dependencies": { 1395 | "loose-envify": "^1.1.0" 1396 | } 1397 | }, 1398 | "node_modules/react-is": { 1399 | "version": "16.13.1", 1400 | "license": "MIT" 1401 | }, 1402 | "node_modules/react-markdown": { 1403 | "version": "9.0.1", 1404 | "license": "MIT", 1405 | "dependencies": { 1406 | "@types/hast": "^3.0.0", 1407 | "devlop": "^1.0.0", 1408 | "hast-util-to-jsx-runtime": "^2.0.0", 1409 | "html-url-attributes": "^3.0.0", 1410 | "mdast-util-to-hast": "^13.0.0", 1411 | "remark-parse": "^11.0.0", 1412 | "remark-rehype": "^11.0.0", 1413 | "unified": "^11.0.0", 1414 | "unist-util-visit": "^5.0.0", 1415 | "vfile": "^6.0.0" 1416 | }, 1417 | "funding": { 1418 | "type": "opencollective", 1419 | "url": "https://opencollective.com/unified" 1420 | }, 1421 | "peerDependencies": { 1422 | "@types/react": ">=18", 1423 | "react": ">=18" 1424 | } 1425 | }, 1426 | "node_modules/react-merge-refs": { 1427 | "version": "1.1.0", 1428 | "license": "MIT", 1429 | "funding": { 1430 | "type": "github", 1431 | "url": "https://github.com/sponsors/gregberge" 1432 | } 1433 | }, 1434 | "node_modules/react-reconciler": { 1435 | "version": "0.27.0", 1436 | "license": "MIT", 1437 | "dependencies": { 1438 | "loose-envify": "^1.1.0", 1439 | "scheduler": "^0.21.0" 1440 | }, 1441 | "engines": { 1442 | "node": ">=0.10.0" 1443 | }, 1444 | "peerDependencies": { 1445 | "react": "^18.0.0" 1446 | } 1447 | }, 1448 | "node_modules/react-syntax-highlighter": { 1449 | "version": "15.5.0", 1450 | "license": "MIT", 1451 | "dependencies": { 1452 | "@babel/runtime": "^7.3.1", 1453 | "highlight.js": "^10.4.1", 1454 | "lowlight": "^1.17.0", 1455 | "prismjs": "^1.27.0", 1456 | "refractor": "^3.6.0" 1457 | }, 1458 | "peerDependencies": { 1459 | "react": ">= 0.14.0" 1460 | } 1461 | }, 1462 | "node_modules/react-use-measure": { 1463 | "version": "2.1.1", 1464 | "license": "MIT", 1465 | "dependencies": { 1466 | "debounce": "^1.2.1" 1467 | }, 1468 | "peerDependencies": { 1469 | "react": ">=16.13", 1470 | "react-dom": ">=16.13" 1471 | } 1472 | }, 1473 | "node_modules/refractor": { 1474 | "version": "3.6.0", 1475 | "license": "MIT", 1476 | "dependencies": { 1477 | "hastscript": "^6.0.0", 1478 | "parse-entities": "^2.0.0", 1479 | "prismjs": "~1.27.0" 1480 | }, 1481 | "funding": { 1482 | "type": "github", 1483 | "url": "https://github.com/sponsors/wooorm" 1484 | } 1485 | }, 1486 | "node_modules/refractor/node_modules/prismjs": { 1487 | "version": "1.27.0", 1488 | "license": "MIT", 1489 | "engines": { 1490 | "node": ">=6" 1491 | } 1492 | }, 1493 | "node_modules/regenerator-runtime": { 1494 | "version": "0.14.0", 1495 | "license": "MIT" 1496 | }, 1497 | "node_modules/remark-parse": { 1498 | "version": "11.0.0", 1499 | "license": "MIT", 1500 | "dependencies": { 1501 | "@types/mdast": "^4.0.0", 1502 | "mdast-util-from-markdown": "^2.0.0", 1503 | "micromark-util-types": "^2.0.0", 1504 | "unified": "^11.0.0" 1505 | }, 1506 | "funding": { 1507 | "type": "opencollective", 1508 | "url": "https://opencollective.com/unified" 1509 | } 1510 | }, 1511 | "node_modules/remark-rehype": { 1512 | "version": "11.0.0", 1513 | "license": "MIT", 1514 | "dependencies": { 1515 | "@types/hast": "^3.0.0", 1516 | "@types/mdast": "^4.0.0", 1517 | "mdast-util-to-hast": "^13.0.0", 1518 | "unified": "^11.0.0", 1519 | "vfile": "^6.0.0" 1520 | }, 1521 | "funding": { 1522 | "type": "opencollective", 1523 | "url": "https://opencollective.com/unified" 1524 | } 1525 | }, 1526 | "node_modules/require-from-string": { 1527 | "version": "2.0.2", 1528 | "license": "MIT", 1529 | "engines": { 1530 | "node": ">=0.10.0" 1531 | } 1532 | }, 1533 | "node_modules/scheduler": { 1534 | "version": "0.21.0", 1535 | "license": "MIT", 1536 | "dependencies": { 1537 | "loose-envify": "^1.1.0" 1538 | } 1539 | }, 1540 | "node_modules/shebang-command": { 1541 | "version": "2.0.0", 1542 | "license": "MIT", 1543 | "dependencies": { 1544 | "shebang-regex": "^3.0.0" 1545 | }, 1546 | "engines": { 1547 | "node": ">=8" 1548 | } 1549 | }, 1550 | "node_modules/shebang-regex": { 1551 | "version": "3.0.0", 1552 | "license": "MIT", 1553 | "engines": { 1554 | "node": ">=8" 1555 | } 1556 | }, 1557 | "node_modules/space-separated-tokens": { 1558 | "version": "2.0.2", 1559 | "license": "MIT", 1560 | "funding": { 1561 | "type": "github", 1562 | "url": "https://github.com/sponsors/wooorm" 1563 | } 1564 | }, 1565 | "node_modules/stats-gl": { 1566 | "version": "1.0.7", 1567 | "license": "MIT" 1568 | }, 1569 | "node_modules/stats.js": { 1570 | "version": "0.17.0", 1571 | "license": "MIT" 1572 | }, 1573 | "node_modules/style-to-object": { 1574 | "version": "0.4.4", 1575 | "license": "MIT", 1576 | "dependencies": { 1577 | "inline-style-parser": "0.1.1" 1578 | } 1579 | }, 1580 | "node_modules/suspend-react": { 1581 | "version": "0.1.3", 1582 | "license": "MIT", 1583 | "peerDependencies": { 1584 | "react": ">=17.0" 1585 | } 1586 | }, 1587 | "node_modules/three": { 1588 | "version": "0.157.0", 1589 | "license": "MIT" 1590 | }, 1591 | "node_modules/three-mesh-bvh": { 1592 | "version": "0.6.8", 1593 | "license": "MIT", 1594 | "peerDependencies": { 1595 | "three": ">= 0.151.0" 1596 | } 1597 | }, 1598 | "node_modules/three-stdlib": { 1599 | "version": "2.28.7", 1600 | "license": "MIT", 1601 | "dependencies": { 1602 | "@types/draco3d": "^1.4.0", 1603 | "@types/offscreencanvas": "^2019.6.4", 1604 | "@types/webxr": "^0.5.2", 1605 | "draco3d": "^1.4.1", 1606 | "fflate": "^0.6.9", 1607 | "potpack": "^1.0.1" 1608 | }, 1609 | "peerDependencies": { 1610 | "three": ">=0.128.0" 1611 | } 1612 | }, 1613 | "node_modules/trim-lines": { 1614 | "version": "3.0.1", 1615 | "license": "MIT", 1616 | "funding": { 1617 | "type": "github", 1618 | "url": "https://github.com/sponsors/wooorm" 1619 | } 1620 | }, 1621 | "node_modules/troika-three-text": { 1622 | "version": "0.47.2", 1623 | "license": "MIT", 1624 | "dependencies": { 1625 | "bidi-js": "^1.0.2", 1626 | "troika-three-utils": "^0.47.2", 1627 | "troika-worker-utils": "^0.47.2", 1628 | "webgl-sdf-generator": "1.1.1" 1629 | }, 1630 | "peerDependencies": { 1631 | "three": ">=0.125.0" 1632 | } 1633 | }, 1634 | "node_modules/troika-three-utils": { 1635 | "version": "0.47.2", 1636 | "license": "MIT", 1637 | "peerDependencies": { 1638 | "three": ">=0.125.0" 1639 | } 1640 | }, 1641 | "node_modules/troika-worker-utils": { 1642 | "version": "0.47.2", 1643 | "license": "MIT" 1644 | }, 1645 | "node_modules/trough": { 1646 | "version": "2.1.0", 1647 | "license": "MIT", 1648 | "funding": { 1649 | "type": "github", 1650 | "url": "https://github.com/sponsors/wooorm" 1651 | } 1652 | }, 1653 | "node_modules/typescript": { 1654 | "version": "5.3.2", 1655 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", 1656 | "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", 1657 | "dev": true, 1658 | "bin": { 1659 | "tsc": "bin/tsc", 1660 | "tsserver": "bin/tsserver" 1661 | }, 1662 | "engines": { 1663 | "node": ">=14.17" 1664 | } 1665 | }, 1666 | "node_modules/unified": { 1667 | "version": "11.0.4", 1668 | "license": "MIT", 1669 | "dependencies": { 1670 | "@types/unist": "^3.0.0", 1671 | "bail": "^2.0.0", 1672 | "devlop": "^1.0.0", 1673 | "extend": "^3.0.0", 1674 | "is-plain-obj": "^4.0.0", 1675 | "trough": "^2.0.0", 1676 | "vfile": "^6.0.0" 1677 | }, 1678 | "funding": { 1679 | "type": "opencollective", 1680 | "url": "https://opencollective.com/unified" 1681 | } 1682 | }, 1683 | "node_modules/unist-util-is": { 1684 | "version": "6.0.0", 1685 | "license": "MIT", 1686 | "dependencies": { 1687 | "@types/unist": "^3.0.0" 1688 | }, 1689 | "funding": { 1690 | "type": "opencollective", 1691 | "url": "https://opencollective.com/unified" 1692 | } 1693 | }, 1694 | "node_modules/unist-util-position": { 1695 | "version": "5.0.0", 1696 | "license": "MIT", 1697 | "dependencies": { 1698 | "@types/unist": "^3.0.0" 1699 | }, 1700 | "funding": { 1701 | "type": "opencollective", 1702 | "url": "https://opencollective.com/unified" 1703 | } 1704 | }, 1705 | "node_modules/unist-util-stringify-position": { 1706 | "version": "4.0.0", 1707 | "license": "MIT", 1708 | "dependencies": { 1709 | "@types/unist": "^3.0.0" 1710 | }, 1711 | "funding": { 1712 | "type": "opencollective", 1713 | "url": "https://opencollective.com/unified" 1714 | } 1715 | }, 1716 | "node_modules/unist-util-visit": { 1717 | "version": "5.0.0", 1718 | "license": "MIT", 1719 | "dependencies": { 1720 | "@types/unist": "^3.0.0", 1721 | "unist-util-is": "^6.0.0", 1722 | "unist-util-visit-parents": "^6.0.0" 1723 | }, 1724 | "funding": { 1725 | "type": "opencollective", 1726 | "url": "https://opencollective.com/unified" 1727 | } 1728 | }, 1729 | "node_modules/unist-util-visit-parents": { 1730 | "version": "6.0.1", 1731 | "license": "MIT", 1732 | "dependencies": { 1733 | "@types/unist": "^3.0.0", 1734 | "unist-util-is": "^6.0.0" 1735 | }, 1736 | "funding": { 1737 | "type": "opencollective", 1738 | "url": "https://opencollective.com/unified" 1739 | } 1740 | }, 1741 | "node_modules/utility-types": { 1742 | "version": "3.10.0", 1743 | "license": "MIT", 1744 | "engines": { 1745 | "node": ">= 4" 1746 | } 1747 | }, 1748 | "node_modules/uuid": { 1749 | "version": "9.0.1", 1750 | "funding": [ 1751 | "https://github.com/sponsors/broofa", 1752 | "https://github.com/sponsors/ctavan" 1753 | ], 1754 | "license": "MIT", 1755 | "bin": { 1756 | "uuid": "dist/bin/uuid" 1757 | } 1758 | }, 1759 | "node_modules/vfile": { 1760 | "version": "6.0.1", 1761 | "license": "MIT", 1762 | "dependencies": { 1763 | "@types/unist": "^3.0.0", 1764 | "unist-util-stringify-position": "^4.0.0", 1765 | "vfile-message": "^4.0.0" 1766 | }, 1767 | "funding": { 1768 | "type": "opencollective", 1769 | "url": "https://opencollective.com/unified" 1770 | } 1771 | }, 1772 | "node_modules/vfile-message": { 1773 | "version": "4.0.2", 1774 | "license": "MIT", 1775 | "dependencies": { 1776 | "@types/unist": "^3.0.0", 1777 | "unist-util-stringify-position": "^4.0.0" 1778 | }, 1779 | "funding": { 1780 | "type": "opencollective", 1781 | "url": "https://opencollective.com/unified" 1782 | } 1783 | }, 1784 | "node_modules/webgl-constants": { 1785 | "version": "1.1.1" 1786 | }, 1787 | "node_modules/webgl-sdf-generator": { 1788 | "version": "1.1.1", 1789 | "license": "MIT" 1790 | }, 1791 | "node_modules/which": { 1792 | "version": "2.0.2", 1793 | "license": "ISC", 1794 | "dependencies": { 1795 | "isexe": "^2.0.0" 1796 | }, 1797 | "bin": { 1798 | "node-which": "bin/node-which" 1799 | }, 1800 | "engines": { 1801 | "node": ">= 8" 1802 | } 1803 | }, 1804 | "node_modules/xtend": { 1805 | "version": "4.0.2", 1806 | "license": "MIT", 1807 | "engines": { 1808 | "node": ">=0.4" 1809 | } 1810 | }, 1811 | "node_modules/zustand": { 1812 | "version": "3.7.2", 1813 | "license": "MIT", 1814 | "engines": { 1815 | "node": ">=12.7.0" 1816 | }, 1817 | "peerDependencies": { 1818 | "react": ">=16.8" 1819 | }, 1820 | "peerDependenciesMeta": { 1821 | "react": { 1822 | "optional": true 1823 | } 1824 | } 1825 | } 1826 | } 1827 | } 1828 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.tsx", 6 | "scripts": { 7 | "build": "npx esbuild src/index.tsx --bundle --outdir=dist --sourcemap --preserve-symlinks --loader:.md=text --minify", 8 | "dev": "npx esbuild src/index.tsx --bundle --outdir=dist --sourcemap --preserve-symlinks --watch --serve=localhost:3002 --servedir=. --loader:.md=text", 9 | "start": "npm run dev", 10 | "deploy": "./misc/deploy.sh" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@lumaai/luma-web": "^0.2.1", 16 | "@react-three/drei": "^9.88.17", 17 | "@react-three/fiber": "^8.15.11", 18 | "@types/react": "^18.2.38", 19 | "@types/react-dom": "^18.2.17", 20 | "@types/react-syntax-highlighter": "^15.5.10", 21 | "lil-gui": "^0.19.1", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-markdown": "^9.0.1", 25 | "react-syntax-highlighter": "^15.5.0", 26 | "three": "^0.157.0" 27 | }, 28 | "devDependencies": { 29 | "@types/three": "^0.157.2", 30 | "esbuild": "^0.19.5", 31 | "typescript": "^5.3.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DemoBackgroundRemoval.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsSemantics, LumaSplatsThree } from "@lumaai/luma-web"; 2 | import { DemoProps } from "."; 3 | import { loadEnvironment } from "./util/Environment"; 4 | 5 | export function DemoBackgroundRemoval(props: DemoProps) { 6 | let { renderer, scene, gui } = props; 7 | 8 | let splats = new LumaSplatsThree({ 9 | // Jules Desbois La Femme à l’arc @HouseofJJD 10 | source: 'https://lumalabs.ai/capture/1b5f3e33-3900-4398-8795-b585ae13fd2d', 11 | enableThreeShaderIntegration: false, 12 | }); 13 | 14 | scene.add(splats); 15 | 16 | let layersEnabled = { 17 | Background: false, 18 | Foreground: true, 19 | } 20 | 21 | function updateSemanticMask() { 22 | splats.semanticsMask = 23 | (layersEnabled.Background ? LumaSplatsSemantics.BACKGROUND : 0) | 24 | (layersEnabled.Foreground ? LumaSplatsSemantics.FOREGROUND : 0); 25 | } 26 | 27 | updateSemanticMask(); 28 | 29 | gui.add(layersEnabled, 'Background').onChange(updateSemanticMask); 30 | 31 | loadEnvironment(renderer, scene, 'assets/venice_sunset_1k.hdr'); 32 | 33 | return { 34 | dispose: () => { 35 | splats.dispose(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/DemoCustomShaders.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsLoader, LumaSplatsThree } from "@lumaai/luma-web"; 2 | import { FogExp2, Uniform } from "three"; 3 | import { DemoProps } from '.'; 4 | import { downloadArtifacts } from "./util/DownloadArtifacts"; 5 | 6 | export function DemoCustomShaders(props: DemoProps) { 7 | let { renderer, camera, scene, gui } = props; 8 | 9 | let uniformTime = new Uniform(0); 10 | 11 | let splats = new LumaSplatsThree({ 12 | // Chateau de Menthon - Annecy @Yannick_Cerrutti 13 | source: `https://lumalabs.ai/capture/da82625c-9c8d-4d05-a9f7-3367ecab438c`, 14 | enableThreeShaderIntegration: true, 15 | onBeforeRender: () => { 16 | uniformTime.value = performance.now() / 1000; 17 | } 18 | }); 19 | scene.add(splats); 20 | 21 | gui.add(splats, 'enableThreeShaderIntegration').name("Use Three Pipeline"); 22 | 23 | splats.setShaderHooks({ 24 | vertexShaderHooks: { 25 | additionalUniforms: { 26 | time_s: ['float', uniformTime], 27 | }, 28 | 29 | // additionalGlobals: /*glsl*/``, 30 | 31 | // onMainEnd: /*glsl*/` 32 | // (vec4 p) { 33 | // return p; 34 | // } 35 | // `, 36 | 37 | // returns a mat4 38 | getSplatTransform: /*glsl*/` 39 | (vec3 position, uint layersBitmask) { 40 | // sin wave on x-axis 41 | float x = 0.; 42 | float z = 0.; 43 | float y = sin(position.x * 1.0 + time_s) * 0.1; 44 | return mat4( 45 | 1., 0., 0., 0, 46 | 0., 1., 0., 0, 47 | 0., 0., 1., 0, 48 | x, y, z, 1. 49 | ); 50 | } 51 | `, 52 | 53 | // getSplatColor: ` 54 | // (vec4 rgba, vec3 position, uint layersBitmask) { 55 | // return rgba * vec4(abs(normalize(position)), 1.0); 56 | // } 57 | // ` 58 | } 59 | }); 60 | 61 | splats.onInitialCameraTransform = transform => { 62 | camera.matrix.copy(transform); 63 | camera.matrix.decompose(camera.position, camera.quaternion, camera.scale); 64 | camera.updateMatrixWorld(); 65 | }; 66 | 67 | 68 | let layersEnabled = { 69 | background: true, 70 | foreground: true, 71 | } 72 | 73 | function updateSemanticMask() { 74 | splats.semanticsMask = 75 | (layersEnabled.background ? 1 : 0) | 76 | (layersEnabled.foreground ? 2 : 0); 77 | } 78 | 79 | updateSemanticMask(); 80 | 81 | let layersFolder = gui.addFolder('layers'); 82 | 83 | layersFolder.add(layersEnabled, 'background').onChange(updateSemanticMask); 84 | layersFolder.add(layersEnabled, 'foreground').onChange(updateSemanticMask); 85 | layersFolder.hide(); 86 | 87 | // fog 88 | scene.fog = new FogExp2(0xEEEEEE, 0.05); 89 | scene.background = scene.fog.color; 90 | 91 | // gui for fog 92 | gui.add(scene.fog, 'density', 0, 0.3).name('fog density'); 93 | gui.addColor(scene.fog, 'color').name('fog color'); 94 | 95 | return { 96 | dispose: () => { 97 | splats.dispose(); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/DemoFog.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsThree } from "@lumaai/luma-web"; 2 | import { Color, FogExp2 } from "three"; 3 | import { DemoProps } from '.'; 4 | import { EnvironmentProbes } from './util/EnvironmentProbes'; 5 | 6 | export function DemoFog(props: DemoProps) { 7 | let { renderer, camera, scene, gui } = props; 8 | 9 | // fog 10 | scene.fog = new FogExp2(new Color(0xe0e1ff).convertLinearToSRGB(), 0.20); 11 | scene.background = scene.fog.color; 12 | 13 | let splats = new LumaSplatsThree({ 14 | // HOLLYWOOD @DroneFotoBooth 15 | source: 'https://lumalabs.ai/capture/b5faf515-7932-4000-ab23-959fc43f0d94', 16 | loadingAnimationEnabled: false, 17 | }); 18 | scene.add(splats); 19 | 20 | // set ideal initial camera transform 21 | splats.onInitialCameraTransform = (transform) => { 22 | camera.matrix.copy(transform); 23 | camera.matrix.decompose(camera.position, camera.quaternion, camera.scale); 24 | camera.updateMatrixWorld(); 25 | }; 26 | 27 | splats.onLoad = () => { 28 | splats.captureCubemap(renderer).then(environmentMap => { 29 | scene.environment = environmentMap; 30 | let environmentProbes = new EnvironmentProbes(4); 31 | environmentProbes.position.set(-3, 1, 0.25); 32 | environmentProbes.rotation.y = Math.PI / 2; 33 | environmentProbes.scale.setScalar(3); 34 | scene.add(environmentProbes); 35 | }); 36 | } 37 | 38 | // gui for fog 39 | gui.add(renderer, 'toneMappingExposure', 0, 10).name('Exposure'); 40 | gui.add(scene.fog, 'density', 0, 0.3).name('Fog Density'); 41 | gui.addColor(scene.fog, 'color').name('Fog Color'); 42 | 43 | return { 44 | dispose: () => { 45 | // stop worker, free resources 46 | splats.dispose(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/DemoHelloWorld.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsThree } from "@lumaai/luma-web"; 2 | import { Color, DoubleSide, Mesh, MeshStandardMaterial, PlaneGeometry, Texture, Vector3 } from "three"; 3 | import { DemoProps } from "."; 4 | 5 | export function DemoHelloWorld(props: DemoProps) { 6 | let { renderer, camera, scene, gui } = props; 7 | 8 | scene.background = new Color('white'); 9 | 10 | let splats = new LumaSplatsThree({ 11 | // MIT WPU Globe @krazyykrunal 12 | source: 'https://lumalabs.ai/capture/ca9ea966-ca24-4ec1-ab0f-af665cb546ff', 13 | // we disable full three.js for performance 14 | enableThreeShaderIntegration: false, 15 | // particle entrance animation 16 | particleRevealEnabled: true, 17 | }); 18 | 19 | scene.add(splats); 20 | 21 | // the splats file can provide an ideal initial viewing location 22 | splats.onInitialCameraTransform = transform => { 23 | transform.decompose(camera.position, camera.quaternion, new Vector3()); 24 | }; 25 | 26 | scene.add(createText()); 27 | 28 | return { 29 | dispose: () => { 30 | // stop worker, free resources 31 | splats.dispose(); 32 | } 33 | } 34 | } 35 | 36 | // create a plane with "Hello World" text 37 | function createText() { 38 | // create canvas 39 | const canvas = document.createElement('canvas'); 40 | const context = canvas.getContext('2d')!; 41 | canvas.width = 1024; 42 | canvas.height = 512; 43 | 44 | // clear white, 0 alpha 45 | context.fillStyle = 'rgba(255, 255, 255, 0)'; 46 | context.fillRect(0, 0, canvas.width, canvas.height); 47 | 48 | // draw text 49 | context.fillStyle = 'white'; 50 | // 100px helvetica, arial, sans-serif 51 | context.font = '200px sans-serif'; 52 | context.textAlign = 'center'; 53 | context.textBaseline = 'middle'; 54 | // stroke 55 | context.strokeStyle = 'rgba(0, 0, 0, 0.5)' 56 | context.lineWidth = 5; 57 | context.fillText('Hello World', canvas.width / 2, canvas.height / 2); 58 | context.strokeText('Hello World', canvas.width / 2, canvas.height / 2); 59 | 60 | // create texture from canvas 61 | const texture = new Texture(canvas); 62 | texture.needsUpdate = true; 63 | 64 | // create plane geometry and mesh with the texture 65 | const geometry = new PlaneGeometry(5, 2.5); 66 | const material = new MeshStandardMaterial({ 67 | map: texture, 68 | transparent: false, 69 | alphaTest: 0.5, 70 | side: DoubleSide, 71 | premultipliedAlpha: true, 72 | emissive: 'white', 73 | emissiveIntensity: 2, 74 | }); 75 | const textPlane = new Mesh(geometry, material); 76 | 77 | // position and rotate 78 | textPlane.position.set(0.8, -0.9, 0); 79 | textPlane.rotation.y = Math.PI / 2; 80 | textPlane.scale.setScalar(0.6); 81 | 82 | return textPlane; 83 | } -------------------------------------------------------------------------------- /src/DemoLighting.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsThree } from "@lumaai/luma-web"; 2 | import { Mesh } from "three"; 3 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; 4 | import { EnvironmentProbes } from "./util/EnvironmentProbes"; 5 | import { DemoProps } from "."; 6 | 7 | export function DemoLighting(props: DemoProps) { 8 | let { renderer, camera, scene, gui } = props; 9 | 10 | let splats = new LumaSplatsThree({ 11 | // Arosa Hörnli - Switzerland @splnlss 12 | source: 'https://lumalabs.ai/capture/4da7cf32-865a-4515-8cb9-9dfc574c90c2', 13 | // disable loading animation so model is fully rendered after onLoad 14 | loadingAnimationEnabled: false, 15 | }); 16 | 17 | splats.onLoad = () => { 18 | splats.captureCubemap(renderer).then(capturedTexture => { 19 | scene.environment = capturedTexture; 20 | scene.background = capturedTexture; 21 | scene.backgroundBlurriness = 0.5; 22 | }); 23 | } 24 | 25 | scene.add(splats); 26 | 27 | // move camera to ideal viewing position 28 | splats.onInitialCameraTransform = transform => { 29 | camera.matrix.copy(transform); 30 | camera.matrix.decompose(camera.position, camera.quaternion, camera.scale); 31 | camera.position.y = 0.25; 32 | }; 33 | 34 | // load ufo glb 35 | let loader = new GLTFLoader(); 36 | 37 | /** 38 | * UFO_B11 D Model by Zuncho Multimedia is licensed under Creative Commons Attribution 39 | * https://sketchfab.com/3d-models/ufo-b11-d-model-75cc3cf1fbb648e5b5a046c055df017a#download 40 | */ 41 | loader.load('assets/models/ufo_b11_d_model.glb', (gltf) => { 42 | let ufo = gltf.scene; 43 | ufo.scale.setScalar((1/100) * 0.8); 44 | ufo.scale.y *= 0.75; 45 | ufo.position.y = 0.5; 46 | ufo.position.x = -0.8; 47 | scene.add(ufo); 48 | 49 | // make shiny metal 50 | ufo.traverse((child) => { 51 | if (child instanceof Mesh) { 52 | child.material.metalness = 1.0; 53 | child.material.roughness = 0.0; 54 | } 55 | }); 56 | 57 | // animate 58 | scene.onBeforeRender = () => { 59 | let t_s = performance.now() / 1000; 60 | ufo.position.y = Math.sin(t_s * 0.25) * 0.05 + 0.5; 61 | } 62 | }); 63 | 64 | let probes = new EnvironmentProbes(); 65 | probes.position.x = 2.5; 66 | scene.add(probes); 67 | 68 | return { 69 | dispose: () => splats.dispose() 70 | } 71 | } -------------------------------------------------------------------------------- /src/DemoReactThreeFiber.tsx: -------------------------------------------------------------------------------- 1 | import { Environment, Grid, PivotControls } from '@react-three/drei'; 2 | import { LumaSplatsSemantics } from "@lumaai/luma-web"; 3 | import React from 'react'; 4 | import './LumaSplatsReact'; 5 | 6 | export function DemoReactThreeFiber() { 7 | const gridConfig = { 8 | gridSize: [10.5, 10.5], 9 | cellSize: 0.6, 10 | cellThickness: 1, 11 | cellColor: '#6f6f6f', 12 | sectionSize: 3.3, 13 | sectionThickness: 1.5, 14 | sectionColor: '#c1c1c1', 15 | fadeDistance: 25, 16 | fadeStrength: 1, 17 | followCamera: false, 18 | infiniteGrid: true 19 | } 20 | 21 | return <> 22 | 27 | 28 | 29 | 30 | 37 | 38 | 41 | 49 | 50 | ; 51 | } -------------------------------------------------------------------------------- /src/DemoTransmission.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsThree } from "@lumaai/luma-web"; 2 | import { NoToneMapping, LinearToneMapping, CineonToneMapping, ReinhardToneMapping, ACESFilmicToneMapping, WebGLRenderer, WebGLRenderTarget, CubeTexture, FrontSide, MathUtils, Mesh, MeshPhysicalMaterial, PerspectiveCamera, SphereGeometry, Vector3 } from "three"; 3 | import { DemoProps } from "."; 4 | import { addMaterial } from "./util/GUIUtils"; 5 | 6 | const worldSources = [ 7 | // Chateau de Menthon - Annecy @Yannick_Cerrutti 8 | { source: 'https://lumalabs.ai/capture/da82625c-9c8d-4d05-a9f7-3367ecab438c', scale: 1 }, 9 | // Arosa Hörnli - Switzerland @splnlss 10 | { source: 'https://lumalabs.ai/capture/4da7cf32-865a-4515-8cb9-9dfc574c90c2', scale: 1 }, 11 | // Fish reefs – Okinawa @BBCG 12 | { source: 'https://lumalabs.ai/capture/6331c1bb-3352-4c8e-b691-32b9b70ec768', scale: 1 }, 13 | // Glacial Erratic - Aspen, Colorado @VibrantNebula_Luma 14 | // { source: 'https://lumalabs.ai/capture/f513900b-69fe-43c8-a72e-80b8d5a16fa4', scale: 1 }, 15 | // Meta Girl (Oleg Lobykin) | Burning Man 2022 @VibrantNebula_Luma 16 | // { source: 'https://lumalabs.ai/capture/2d57866c-83dc-47a6-a725-69c27f75ddb0', scale: 1 }, 17 | // Pinkerton Hot Springs @VibrantNebula_Luma 18 | // { source: 'https://lumalabs.ai/capture/a5e98f35-3759-4cf5-a226-079b15c805da', scale: 1 }, 19 | // HOLLYWOOD @DroneFotoBooth 20 | // { source: 'https://lumalabs.ai/capture/b5faf515-7932-4000-ab23-959fc43f0d94', scale: 1 }, 21 | // Metropolis @fotozhora_sk 22 | { source: 'https://lumalabs.ai/capture/d2d2badd-8bdd-4874-84f7-9df2aae27f29', scale: 1 }, 23 | ]; 24 | 25 | const innerGlobeRadius = 1; 26 | const outerGlobeRadius = 8; 27 | const radiusGap = outerGlobeRadius - innerGlobeRadius; 28 | 29 | export function DemoTransmission(props: DemoProps) { 30 | let { renderer, camera, scene, controls, gui } = props; 31 | 32 | renderer.toneMapping = NoToneMapping; 33 | renderer.localClippingEnabled = false; 34 | 35 | controls.enablePan = false; 36 | controls.autoRotate = false; 37 | camera.position.y = 3; 38 | camera.position.setLength(innerGlobeRadius + radiusGap * 0.25); 39 | 40 | // state, updated in mainLoop before rendering 41 | let state = { 42 | level: 0, 43 | innerSplatIndex: NaN, 44 | outerSplatIndex: NaN, 45 | animate: true, 46 | } 47 | 48 | // add tonemapping gui 49 | gui.add(renderer, 'toneMappingExposure', 0, 10).name('Exposure'); 50 | gui.add(renderer, 'toneMapping', { 51 | NoToneMapping, 52 | LinearToneMapping, 53 | CineonToneMapping, 54 | ReinhardToneMapping, 55 | ACESFilmicToneMapping, 56 | }).name('Tone Mapping'); 57 | gui.add(state, 'animate').name('Animate'); 58 | 59 | // space key to toggle animation 60 | window.addEventListener('keydown', (e) => { 61 | if (e.code === 'Space') { 62 | state.animate = !state.animate; 63 | } 64 | }); 65 | 66 | // add a refractive transmissive sphere 67 | let glassGlobe = new Mesh( 68 | new SphereGeometry(1, 32, 32), 69 | new MeshPhysicalMaterial({ 70 | roughness: 0, 71 | metalness: 0, 72 | transparent: true, 73 | transmission: 1, 74 | ior: 1.341, 75 | // thickness: 1.52, 76 | thickness: 2, 77 | envMapIntensity: 1.2, // push up the environment map intensity a little 78 | clearcoat: 1, 79 | side: FrontSide, 80 | }) 81 | ); 82 | const initialMaterialProperties = { ...glassGlobe.material }; 83 | addMaterial(gui, glassGlobe.material, 'Glass Globe'); 84 | gui.add(glassGlobe, 'renderOrder', -1, 1).step(1); 85 | glassGlobe.scale.setScalar(innerGlobeRadius); 86 | scene.add(glassGlobe); 87 | 88 | const splatWorlds = worldSources.map((world, index) => { 89 | let splat = new LumaSplatsThree({ 90 | source: world.source, 91 | // disable loading animation so we can capture an environment map as soon as it's loaded 92 | loadingAnimationEnabled: false, 93 | onBeforeRender: (renderer) => { 94 | let renderTarget = renderer.getRenderTarget(); 95 | disableMSAA(renderTarget); 96 | 97 | let isWithinGlobe = state.innerSplatIndex === index; 98 | let isCubeRenderTarget = (renderTarget?.texture as any)?.isCubeTexture === true; 99 | if (isCubeRenderTarget) { 100 | splat.preventDraw = false; 101 | } else { 102 | if (isWithinGlobe) { 103 | // disable rendering to canvas 104 | splat.preventDraw = renderTarget == null; 105 | } else { 106 | // disable rendering to transmission 107 | splat.preventDraw = renderTarget != null; 108 | } 109 | } 110 | } 111 | }); 112 | splat.scale.setScalar(world.scale); 113 | 114 | let splatWorld = { 115 | environmentMap: null as CubeTexture | null, 116 | splat, 117 | } 118 | 119 | // capture environment lighting after load 120 | splat.onLoad = () => { 121 | splat.captureCubemap(renderer).then((cubemap) => { 122 | splatWorld.environmentMap = cubemap; 123 | }); 124 | } 125 | 126 | return splatWorld; 127 | }); 128 | 129 | // main loop 130 | let lastFrameTime_ms = performance.now(); 131 | scene.onBeforeRender = () => { 132 | let t_ms = performance.now(); 133 | 134 | let dt_s = (t_ms - lastFrameTime_ms) / 1000; 135 | lastFrameTime_ms = t_ms; 136 | 137 | if (state.animate) { 138 | let cameraRadialVector = camera.position.clone().normalize(); 139 | camera.position.addScaledVector(cameraRadialVector, dt_s); 140 | // rotate around y axis 141 | camera.position.applyAxisAngle(new Vector3(0, 1, 0), dt_s * 0.5); 142 | camera.lookAt(new Vector3(0, 0, 0)); 143 | } 144 | 145 | // check if camera's near plane is inside the globe 146 | camera.updateWorldMatrix(true, false); 147 | let near = (camera as PerspectiveCamera).near; 148 | // let nearVector = new Vector3(0, 0, -near); 149 | // let nearWorld = nearVector.applyMatrix4(camera.matrixWorld); 150 | // let distanceToCenter = nearWorld.distanceTo(glassGlobe.position); 151 | let nearPlaneDistanceToCenter = camera.position.length() - near; 152 | let innerSurfaceDistance = nearPlaneDistanceToCenter - innerGlobeRadius; 153 | 154 | function applyCameraModulo() { 155 | let newInnerSurfaceDistance = MathUtils.euclideanModulo(innerSurfaceDistance, radiusGap); 156 | let newCameraDistance = newInnerSurfaceDistance + innerGlobeRadius + near; 157 | camera.position.setLength(newCameraDistance); 158 | // update innerSurfaceDistance and outerSurfaceDistance 159 | innerSurfaceDistance = newInnerSurfaceDistance; 160 | 161 | camera.updateWorldMatrix(true, false); 162 | } 163 | 164 | if (innerSurfaceDistance > radiusGap) { 165 | applyCameraModulo(); 166 | state.level++; 167 | } 168 | 169 | if (innerSurfaceDistance < 0) { 170 | applyCameraModulo(); 171 | state.level--; 172 | } 173 | 174 | // determine camera position in the range [0, 1] where 0 is inside the inner globe and 1 is outside the outer globe 175 | // after modulo 176 | let cameraU = innerSurfaceDistance / radiusGap; 177 | 178 | // use level to set scene state 179 | state.innerSplatIndex = MathUtils.euclideanModulo(state.level - 1, splatWorlds.length); 180 | state.outerSplatIndex = MathUtils.euclideanModulo(state.level, splatWorlds.length); 181 | 182 | for (let i = 0; i < splatWorlds.length; i++) { 183 | let splatWorld = splatWorlds[i]; 184 | let splat = splatWorld.splat; 185 | 186 | let isInnerSplat = state.innerSplatIndex === i; 187 | let isOuterSplat = state.outerSplatIndex === i; 188 | let isVisible = state.innerSplatIndex === i || state.outerSplatIndex === i; 189 | // disable enableThreeShaderIntegration to improve performance when not required 190 | // we must do this before splat.onBeforeRender is called because by then it's too late to change material for the frame 191 | splat.enableThreeShaderIntegration = isInnerSplat; 192 | splat.material.transparent = !isInnerSplat; 193 | 194 | if (isVisible) { 195 | scene.add(splat); 196 | } else { 197 | scene.remove(splat); 198 | } 199 | 200 | // make the world lit by the outer splat 201 | if (isOuterSplat) { 202 | if (scene.environment != splatWorld.environmentMap) { 203 | scene.environment = splatWorld.environmentMap; 204 | scene.background = splatWorld.environmentMap; 205 | } 206 | } 207 | 208 | // scale the inner splat for continuity across the boundary 209 | if (isInnerSplat) { 210 | let r = innerGlobeRadius / outerGlobeRadius; 211 | // splat.scale.setScalar(MathUtils.lerp(r, 1, cameraU)); 212 | splat.scale.setScalar(r); 213 | } else { 214 | splat.scale.setScalar(1); 215 | } 216 | 217 | if (isVisible) { 218 | splat.updateMatrix(); 219 | splat.updateMatrixWorld(); 220 | } 221 | } 222 | 223 | // adjust globe thickness 224 | glassGlobe.material.thickness = MathUtils.lerp(initialMaterialProperties.thickness, 0, MathUtils.smoothstep(0.2, 0, innerSurfaceDistance)); 225 | // glassGlobe.visible = innerSurfaceDistance > 0; 226 | 227 | // scale the globe to appear 228 | let s = 1 - cameraU; 229 | glassGlobe.scale.setScalar(MathUtils.smootherstep(s, 0.0, 0.2)); 230 | glassGlobe.updateMatrix(); 231 | glassGlobe.updateMatrixWorld(); 232 | // glassGlobe.material.opacity = MathUtils.lerp(1, 0, cameraU * cameraU * cameraU); 233 | } 234 | 235 | return { 236 | dispose: () => { 237 | for (let splatWorld of splatWorlds) { 238 | splatWorld.splat.dispose(); 239 | } 240 | } 241 | } 242 | } 243 | 244 | function disableMSAA(target: WebGLRenderTarget | null) { 245 | // disable MSAA on render targets (in this case the transmission render target) 246 | // this improves splatting performance 247 | if (target) { 248 | target.samples = 0; 249 | } 250 | } -------------------------------------------------------------------------------- /src/DemoVR.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsThree } from "@lumaai/luma-web"; 2 | import { VRButton } from "three/examples/jsm/webxr/VRButton.js"; 3 | import { DemoProps } from "."; 4 | 5 | export function DemoVR(props: DemoProps) { 6 | let { renderer, camera, scene, controls, gui } = props; 7 | 8 | renderer.xr.enabled = true; 9 | 10 | let vrButton = VRButton.createButton(renderer); 11 | let canvas = renderer.getContext().canvas as HTMLCanvasElement; 12 | canvas.parentElement!.append(vrButton); 13 | 14 | let splats = new LumaSplatsThree({ 15 | // Kind Humanoid @RyanHickman 16 | source: 'https://lumalabs.ai/capture/83e9aae8-7023-448e-83a6-53ccb377ec86', 17 | // disable three.js shader integration for performance 18 | enableThreeShaderIntegration: false, 19 | }); 20 | 21 | scene.add(splats); 22 | 23 | return { 24 | dispose: () => { 25 | splats.dispose(); 26 | vrButton.remove(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/LumaSplatsReact.ts: -------------------------------------------------------------------------------- 1 | import { Object3DNode, extend } from '@react-three/fiber'; 2 | import { LumaSplatsThree } from "@lumaai/luma-web"; 3 | 4 | // Make LumaSplatsThree available to R3F 5 | extend( { LumaSplats: LumaSplatsThree } ); 6 | 7 | // For typeScript support: 8 | declare module '@react-three/fiber' { 9 | interface ThreeElements { 10 | lumaSplats: Object3DNode 11 | } 12 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { AdaptiveDpr, OrbitControls, PerspectiveCamera } from '@react-three/drei'; 2 | import { OrbitControls as OrbitControlsStdLib } from 'three-stdlib'; 3 | import { Canvas, useThree } from '@react-three/fiber'; 4 | import GUI from 'lil-gui'; 5 | import React, { useEffect, useRef, useState } from 'react'; 6 | import { createRoot } from 'react-dom/client'; 7 | import Markdown from 'react-markdown'; 8 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 9 | import { nord as syntaxTheme } from 'react-syntax-highlighter/dist/esm/styles/prism'; 10 | import { Camera, CineonToneMapping, Scene, WebGLRenderer } from 'three'; 11 | import readme from '../README.md'; 12 | import { DemoBackgroundRemoval } from './DemoBackgroundRemoval'; 13 | import { DemoCustomShaders } from './DemoCustomShaders'; 14 | import { DemoFog } from './DemoFog'; 15 | import { DemoHelloWorld } from './DemoHelloWorld'; 16 | import { DemoLighting } from './DemoLighting'; 17 | import { DemoReactThreeFiber } from './DemoReactThreeFiber'; 18 | import { DemoTransmission } from './DemoTransmission'; 19 | import { DemoVR } from './DemoVR'; 20 | 21 | export type DemoProps = { 22 | renderer: WebGLRenderer, 23 | scene: Scene, 24 | camera: Camera, 25 | controls: OrbitControlsStdLib, 26 | gui: GUI, 27 | } 28 | 29 | type DemoFn = 30 | (props: DemoProps) 31 | => { dispose: () => void } | void; 32 | 33 | const demos = { 34 | basic: { 35 | "getting-started": DemoHelloWorld, 36 | "three-fog": DemoFog, 37 | "background-removal": DemoBackgroundRemoval, 38 | "scene-lighting": DemoLighting, 39 | "custom-shaders": DemoCustomShaders, 40 | "transmission": DemoTransmission, 41 | "vr": DemoVR, 42 | } as Record, 43 | react: { 44 | "react-three-fiber": DemoReactThreeFiber 45 | } as Record> 46 | } 47 | 48 | let globalGUI: GUI | null = null; 49 | 50 | function DemoScene(props: { 51 | expanded: boolean, 52 | demoBasicFn: DemoFn | null, 53 | demoReactFn: React.FC<{ gui: GUI }> | null, 54 | onExpandToggle: (expanded: boolean) => void, 55 | }) { 56 | let { scene, gl: renderer, camera } = useThree(); 57 | 58 | let [gui, setGUI] = useState(globalGUI); 59 | let [autoRotate, setAutoRotate] = useState(true); 60 | let [showUI, setShowUI] = useState(true); 61 | let controlsRef = useRef(null); 62 | 63 | useEffect(() => { 64 | globalGUI?.destroy(); 65 | 66 | globalGUI = new GUI({ 67 | container: renderer.domElement.parentElement!, 68 | }); 69 | globalGUI.close(); 70 | globalGUI.domElement.style.position = 'absolute'; 71 | globalGUI.domElement.style.top = '0'; 72 | globalGUI.domElement.style.right = '0'; 73 | globalGUI.domElement.style.zIndex = '1000'; 74 | globalGUI.domElement.addEventListener('pointerdown', (e) => { 75 | e.stopPropagation(); 76 | }); 77 | 78 | let pixelRatioProxy = { 79 | get pixelRatio() { 80 | return renderer.getPixelRatio() 81 | }, 82 | set pixelRatio(value: number) { 83 | renderer.setPixelRatio(value); 84 | 85 | // update url parameter 86 | let url = new URL(window.location.href); 87 | url.searchParams.set('pixelRatio', value.toString()); 88 | window.history.replaceState({}, '', url.href); 89 | } 90 | } 91 | // initial pixel ratio from url parameter if available 92 | const url = new URL(window.location.href); 93 | let pixelRatioParam = url.searchParams.get('pixelRatio'); 94 | if (pixelRatioParam != null) { 95 | pixelRatioProxy.pixelRatio = parseFloat(pixelRatioParam); 96 | } 97 | 98 | globalGUI.add(pixelRatioProxy, 'pixelRatio', 0.5, window.devicePixelRatio, 0.25).name('Pixel Ratio'); 99 | 100 | setGUI(globalGUI); 101 | 102 | let demoProps = { 103 | renderer, 104 | scene, 105 | camera, 106 | controls: controlsRef.current!, 107 | gui: globalGUI, 108 | } 109 | 110 | if (props.demoBasicFn) { 111 | let demoDispose = props.demoBasicFn(demoProps)?.dispose; 112 | 113 | return () => { 114 | // call .dispose() on all objects in the scene 115 | scene.traverse((obj) => { 116 | (obj as any).dispose?.(); 117 | }); 118 | 119 | demoDispose?.(); 120 | 121 | renderer.dispose(); 122 | 123 | globalGUI?.destroy(); 124 | globalGUI = null; 125 | } 126 | } 127 | }, []); 128 | 129 | useEffect(() => { 130 | // h key to hide/show gui 131 | function toggleGUI(e: KeyboardEvent) { 132 | if (e.key === 'h') { 133 | if (showUI) { 134 | gui?.hide(); 135 | setShowUI(false); 136 | } else { 137 | gui?.show(); 138 | setShowUI(true); 139 | } 140 | } 141 | } 142 | 143 | window.addEventListener('keydown', toggleGUI); 144 | 145 | let expandButton = document.createElement('div'); 146 | expandButton.style.visibility = showUI ? 'visible' : 'hidden'; 147 | expandButton.className = 'expand-button ' + (props.expanded ? 'icon-compress' : 'icon-expand'); 148 | expandButton.onclick = () => { 149 | props.onExpandToggle(!props.expanded); 150 | } 151 | renderer.domElement.parentElement!.prepend(expandButton); 152 | 153 | return () => { 154 | window.removeEventListener('keydown', toggleGUI); 155 | expandButton.remove(); 156 | } 157 | }, [props.expanded, showUI, gui]); 158 | 159 | return <> 160 | 161 | { 168 | setAutoRotate(false); 169 | }} 170 | makeDefault 171 | /> 172 | {props.demoReactFn && gui && } 173 | 174 | } 175 | 176 | function App() { 177 | const demoKeys = Object.keys(demos.basic).concat(Object.keys(demos.react)); 178 | 179 | const [demoKey, setDemoKey] = useState(() => { 180 | // get url parameter 181 | const url = new URL(window.location.href); 182 | let demoParam = url.hash.replace(/^#/, ''); 183 | let demoExists = demoParam != null && demoKeys.includes(demoParam); 184 | return demoExists ? demoParam : demoKeys[0]; 185 | }); 186 | 187 | const [showDocs, setShowDocs] = useState(() => { 188 | // get url parameter 189 | const url = new URL(window.location.href); 190 | return url.searchParams.get('hide-docs') == null; 191 | }); 192 | 193 | useEffect(() => { 194 | // update hide-docs url parameter 195 | let url = new URL(window.location.href); 196 | if (!showDocs) { 197 | url.searchParams.set('hide-docs', ''); 198 | } else { 199 | url.searchParams.delete('hide-docs'); 200 | } 201 | window.history.replaceState({}, '', url.href); 202 | }, [showDocs]); 203 | 204 | const demoBasicFn = demoKey != null ? demos.basic[demoKey] : null; 205 | const demoReactFn = demoKey != null ? demos.react[demoKey] : null; 206 | const hasDemo = demoBasicFn != null || demoReactFn != null; 207 | 208 | useEffect(() => { 209 | // react to url changes 210 | window.addEventListener('hashchange', () => { 211 | setDemoKey(window.location.hash.replace(/^#/, '')); 212 | }); 213 | 214 | // scroll to demo 215 | if (demoKey) { 216 | document.getElementById(demoKey)?.scrollIntoView({ behavior: 'smooth' }); 217 | } 218 | 219 | // press e to expand demo 220 | function onKeyDown(e: KeyboardEvent) { 221 | if (e.key === 'e') { 222 | setShowDocs(e => !e); 223 | } 224 | } 225 | window.addEventListener('keydown', onKeyDown); 226 | 227 | return () => { 228 | window.removeEventListener('keydown', onKeyDown); 229 | } 230 | }, []); 231 | 232 | return <> 233 | {showDocs &&
234 | ) { 241 | document.getElementById(id)?.querySelector('a')?.click(); 242 | // setDemoKey(id); 243 | // document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }); 244 | } 245 | // make clickable 246 | return

252 | {children} 253 | {!isActive && } 254 |

; 255 | }, 256 | code(props) { 257 | const { children, className, node, ref, ...rest } = props 258 | const match = /language-(\w+)/.exec(className || '') 259 | return match ? ( 260 | 266 | ) : ( 267 | 268 | {children} 269 | 270 | ) 271 | }, 272 | a(props) { 273 | let { href, ...rest } = props; 274 | 275 | // replace ./src links with github links for better readability 276 | if (href?.startsWith('./src')) { 277 | href = 'https://github.com/lumalabs/luma-web-examples/blob/main/' + href.slice(1); 278 | } 279 | 280 | let isAbsolute = /^https?:\/\//.test(href ?? ''); 281 | if (isAbsolute) { 282 | // open in new tab 283 | return 284 | } else { 285 | return 286 | } 287 | } 288 | }} 289 | >{readme}
290 |
} 291 | 292 | {hasDemo && { 302 | // prevent text selection 303 | e.preventDefault(); 304 | }} 305 | > 306 | 307 | { 311 | setShowDocs(!expanded); 312 | }} 313 | demoBasicFn={demoBasicFn} 314 | demoReactFn={demoReactFn} 315 | /> 316 | } 317 | 318 | } 319 | 320 | const reactRoot = document.getElementById('react-root'); 321 | createRoot(reactRoot!).render(); -------------------------------------------------------------------------------- /src/minimal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 23 | 70 | 71 | -------------------------------------------------------------------------------- /src/util/DownloadArtifacts.ts: -------------------------------------------------------------------------------- 1 | import { LumaSplatsLoader } from "@lumaai/luma-web"; 2 | 3 | export async function downloadArtifacts(splatLoader: LumaSplatsLoader, onProgress: (progress: number) => void = ()=>{}) { 4 | let artifacts: { [key: string]: string | undefined } = await splatLoader.getArtifacts(); 5 | 6 | // we care about the following 7 | let required = [ 8 | 'gs_web_meta', 9 | 'gs_web_gauss1', 10 | 'gs_web_gauss2', 11 | 'gs_web_sh', 12 | 'gs_web_webmeta', 13 | 'gs_compressed_meta', 14 | 'gs_compressed', 15 | 'with_background_gs_camera_params', 16 | 'semantics', 17 | ]; 18 | 19 | let totalFiles = required.reduce((total, key) => { 20 | return artifacts[key] ? (total + 1) : total 21 | }, 0); 22 | let filesCompete = 0; 23 | 24 | onProgress(0); 25 | 26 | let blobs = required.map(async key => { 27 | let url = artifacts[key]; 28 | if (!url) { 29 | console.log('missing', key); 30 | return null; 31 | } 32 | 33 | let urlFilename = url?.split('/').pop(); 34 | let fileType = urlFilename?.split('.').pop(); 35 | 36 | let filename = `${key}.${fileType}`; 37 | 38 | console.log('downloading', filename); 39 | 40 | let response = await fetch(url); 41 | let blob = await response.blob(); 42 | 43 | filesCompete++; 44 | onProgress(filesCompete / totalFiles); 45 | 46 | return {filename, url, blob}; 47 | }); 48 | 49 | // trigger download for all the files 50 | let files = await Promise.all(blobs); 51 | files.forEach(file => { 52 | if (!file) { 53 | return; 54 | } 55 | let {filename, blob} = file; 56 | let url = URL.createObjectURL(blob); 57 | let link = document.createElement('a'); 58 | link.href = url; 59 | link.download = filename; 60 | link.click(); 61 | }); 62 | 63 | onProgress(1); 64 | } 65 | 66 | export function createDownloadArtifactsButton(splatLoader: LumaSplatsLoader) { 67 | 68 | 69 | // add download button 70 | let downloadButton = document.createElement('button'); 71 | downloadButton.innerText = 'Download Artifacts'; 72 | downloadButton.style.position = 'absolute'; 73 | downloadButton.style.bottom = '5px'; 74 | downloadButton.style.left = '5px'; 75 | downloadButton.style.zIndex = '100'; 76 | downloadButton.onclick = (e) => { 77 | downloadArtifacts(splatLoader, progress => { 78 | if (progress < 1) { 79 | downloadButton.innerText = `Downloading ${Math.floor(progress * 100)}%`; 80 | } else { 81 | downloadButton.innerText = 'Download Artifacts'; 82 | } 83 | }); 84 | }; 85 | 86 | return downloadButton; 87 | } -------------------------------------------------------------------------------- /src/util/Environment.ts: -------------------------------------------------------------------------------- 1 | import { PMREMGenerator, Scene, Texture, WebGLRenderer } from "three"; 2 | import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; 3 | 4 | export function loadEnvironment(renderer: WebGLRenderer, scene: Scene, path: string) { 5 | // environment 6 | let rgbeLoader = new RGBELoader().load( 7 | path, 8 | (texture: Texture) => { 9 | const pmremGenerator = new PMREMGenerator(renderer) 10 | pmremGenerator.compileEquirectangularShader(); 11 | const environment = pmremGenerator.fromEquirectangular(texture).texture; 12 | scene.environment = environment; 13 | scene.background = environment; 14 | scene.backgroundBlurriness = 0.5; 15 | 16 | texture.dispose(); 17 | pmremGenerator.dispose(); 18 | rgbeLoader.dispose(); 19 | } 20 | ); 21 | } -------------------------------------------------------------------------------- /src/util/EnvironmentProbes.ts: -------------------------------------------------------------------------------- 1 | import { Color, Mesh, MeshStandardMaterial, Object3D, SphereGeometry } from "three"; 2 | 3 | export class EnvironmentProbes extends Object3D { 4 | 5 | constructor(gridSize: number = 3) { 6 | super(); 7 | // add grid of spheres to test lighting 8 | let sphereGeometry = new SphereGeometry(0.05, 32, 32); 9 | 10 | for (let i = 0; i < gridSize; i++) { 11 | for (let j = 0; j < gridSize; j++) { 12 | for (let k = 0; k < gridSize; k++) { 13 | let roughness = i / (gridSize - 1); 14 | let metalness = j / (gridSize - 1); 15 | let color = k / (gridSize - 1); 16 | let sphere = new Mesh(sphereGeometry, new MeshStandardMaterial({ 17 | color: new Color(color, color, color), 18 | roughness: roughness, 19 | metalness: metalness, 20 | })); 21 | sphere.position.set(i, j, k).subScalar((gridSize - 1) / 2).multiplyScalar(0.25); 22 | this.add(sphere); 23 | } 24 | } 25 | } 26 | } 27 | 28 | dispose() { 29 | this.traverse((child) => { 30 | if (child instanceof Mesh) { 31 | child.geometry.dispose(); 32 | child.material.dispose(); 33 | } 34 | }); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/util/GUIUtils.ts: -------------------------------------------------------------------------------- 1 | import GUI from "lil-gui"; 2 | import { AdditiveBlending, CustomBlending, MeshPhysicalMaterial, MultiplyBlending, NoBlending, NormalBlending, SubtractiveBlending } from "three"; 3 | 4 | export function addMaterial(gui: GUI, material: MeshPhysicalMaterial, name: string) { 5 | let materialFolder = gui.addFolder(name); 6 | materialFolder.addColor(material, 'color'); 7 | materialFolder.add(material, 'flatShading'); 8 | materialFolder.add(material, 'depthWrite'); 9 | materialFolder.add(material, 'depthTest'); 10 | materialFolder.add(material, 'transparent'); 11 | materialFolder.add(material, 'blending', { 12 | NoBlending, 13 | NormalBlending, 14 | AdditiveBlending, 15 | SubtractiveBlending, 16 | MultiplyBlending, 17 | CustomBlending, 18 | }); 19 | materialFolder.add(material, 'premultipliedAlpha'); 20 | materialFolder.add(material, 'opacity', 0, 1); 21 | materialFolder.add(material, 'metalness', 0, 1); 22 | materialFolder.add(material, 'roughness', 0, 1); 23 | materialFolder.add(material, 'emissiveIntensity', 0, 4); 24 | materialFolder.add(material, 'iridescence', 0, 1); 25 | materialFolder.add(material, 'iridescenceIOR', 0, 3); 26 | let iridescenceThicknessRange = { min: 0, max: 1 }; 27 | materialFolder.add(iridescenceThicknessRange, 'min', 0, 1).onChange(() => { 28 | material.iridescenceThicknessRange = [iridescenceThicknessRange.min, iridescenceThicknessRange.max]; 29 | }); 30 | materialFolder.add(iridescenceThicknessRange, 'max', 0, 1).onChange(() => { 31 | material.iridescenceThicknessRange = [iridescenceThicknessRange.min, iridescenceThicknessRange.max]; 32 | }); 33 | materialFolder.add(material, 'envMapIntensity', 0, 4); 34 | 35 | // transmission 36 | materialFolder.add(material, 'transmission', 0, 1); 37 | materialFolder.add(material, 'ior', 0, 3); 38 | materialFolder.add(material, 'thickness', 0, 10); 39 | materialFolder.addColor(material, 'attenuationColor'); 40 | materialFolder.add(material, 'attenuationDistance', 0, 10); 41 | 42 | // clearcoat 43 | materialFolder.add(material, 'clearcoat', 0, 1); 44 | materialFolder.add(material, 'clearcoatRoughness', 0, 1); 45 | materialFolder.add(material, 'reflectivity', 0, 1); 46 | 47 | return materialFolder; 48 | } -------------------------------------------------------------------------------- /src/util/MarkdownImport.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.md" { 2 | const value: string; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "outDir": "./dist/", 7 | "noImplicitAny": true, 8 | "module": "ES2020", 9 | "target": "ES2020", 10 | "moduleResolution": "node", 11 | "jsx": "react", 12 | "allowSyntheticDefaultImports": true, 13 | "allowJs": true, 14 | "strict": true, 15 | "skipLibCheck": true, 16 | // we use esbuild to compile and typescript for typechecking 17 | "noEmit": true, 18 | } 19 | } --------------------------------------------------------------------------------