├── .clang-format ├── .github └── workflows │ ├── docs.yml │ ├── eslint.yml │ ├── formatting.yaml │ ├── publish.yml │ ├── pyright.yml │ ├── pyright_examples.yml │ ├── pytest.yml │ └── typescript-compile.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── Makefile ├── README.md ├── requirements.txt ├── source │ ├── _static │ │ ├── css │ │ │ └── custom.css │ │ └── logo.svg │ ├── _templates │ │ └── sidebar │ │ │ └── brand.html │ ├── camera_handles.rst │ ├── client_handles.rst │ ├── conf.py │ ├── conventions.rst │ ├── development.rst │ ├── embedded_visualizations.rst │ ├── events.rst │ ├── examples │ │ ├── 00_coordinate_frames.rst │ │ ├── 01_image.rst │ │ ├── 02_gui.rst │ │ ├── 03_gui_callbacks.rst │ │ ├── 04_camera_poses.rst │ │ ├── 05_camera_commands.rst │ │ ├── 06_mesh.rst │ │ ├── 07_record3d_visualizer.rst │ │ ├── 08_smpl_visualizer.rst │ │ ├── 09_urdf_visualizer.rst │ │ ├── 10_realsense.rst │ │ ├── 11_colmap_visualizer.rst │ │ ├── 12_click_meshes.rst │ │ ├── 13_theming.rst │ │ ├── 14_markdown.rst │ │ ├── 15_gui_in_scene.rst │ │ ├── 16_modal.rst │ │ ├── 17_background_composite.rst │ │ ├── 18_lines.rst │ │ ├── 19_get_renders.rst │ │ ├── 20_scene_pointer.rst │ │ ├── 21_set_up_direction.rst │ │ ├── 22_games.rst │ │ ├── 23_plotly.rst │ │ ├── 24_plots_as_images.rst │ │ ├── 25_smpl_visualizer_skinned.rst │ │ ├── 26_lighting.rst │ │ └── 27_notifications.rst │ ├── extras.rst │ ├── gui_api.rst │ ├── gui_handles.rst │ ├── icons.rst │ ├── index.rst │ ├── infrastructure.rst │ ├── scene_api.rst │ ├── scene_handles.rst │ ├── server.rst │ ├── state_serializer.rst │ └── transforms.rst └── update_example_docs.py ├── examples ├── 00_coordinate_frames.py ├── 01_image.py ├── 02_gui.py ├── 03_gui_callbacks.py ├── 04_camera_poses.py ├── 05_camera_commands.py ├── 06_mesh.py ├── 07_record3d_visualizer.py ├── 08_smpl_visualizer.py ├── 09_urdf_visualizer.py ├── 10_realsense.py ├── 11_colmap_visualizer.py ├── 12_click_meshes.py ├── 13_theming.py ├── 14_markdown.py ├── 15_gui_in_scene.py ├── 16_modal.py ├── 17_background_composite.py ├── 18_lines.py ├── 19_get_renders.py ├── 20_scene_pointer.py ├── 21_set_up_direction.py ├── 22_games.py ├── 23_plotly.py ├── 24_plots_as_images.py ├── 25_smpl_visualizer_skinned.py ├── 26_lighting.py ├── 27_notifications.py ├── assets │ ├── .gitignore │ ├── Cal_logo.png │ ├── download_colmap_garden.sh │ ├── download_dragon_mesh.sh │ ├── download_record3d_dance.sh │ └── mdx_example.mdx └── experimental │ └── gaussian_splats.py ├── examples_dev ├── 00_coordinate_frames.py ├── 01_image.py ├── 02_gui.py ├── 03_gui_callbacks.py ├── 04_camera_poses.py ├── 05_camera_commands.py ├── 06_mesh.py ├── 07_record3d_visualizer.py ├── 08_smpl_visualizer.py ├── 09_urdf_visualizer.py ├── 10_realsense.py ├── 11_colmap_visualizer.py ├── 12_click_meshes.py ├── 13_theming.py ├── 14_markdown.py ├── 15_gui_in_scene.py ├── 16_modal.py ├── 17_background_composite.py ├── 18_lines.py ├── 19_get_renders.py ├── 20_scene_pointer.py ├── 21_set_up_direction.py ├── 22_games.py ├── 23_plotly.py ├── 24_plots_as_images.py ├── 25_smpl_visualizer_skinned.py ├── 26_lighting.py ├── 27_notifications.py ├── 28_meshes_batched.py └── README.md ├── pyproject.toml ├── src └── viser │ ├── __init__.py │ ├── _assignable_props_api.py │ ├── _client_autobuild.py │ ├── _gui_api.py │ ├── _gui_handles.py │ ├── _icons.py │ ├── _icons │ └── tabler-icons.zip │ ├── _icons_enum.py │ ├── _icons_enum.pyi │ ├── _icons_generate_enum.py │ ├── _messages.py │ ├── _notification_handle.py │ ├── _scene_api.py │ ├── _scene_handles.py │ ├── _threadpool_exceptions.py │ ├── _tunnel.py │ ├── _viser.py │ ├── client │ ├── .eslintrc.js │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ │ ├── Inter-VariableFont_slnt,wght.ttf │ │ ├── hdri │ │ │ ├── dikhololo_night_1k.hdr │ │ │ ├── empty_warehouse_01_1k.hdr │ │ │ ├── forest_slope_1k.hdr │ │ │ ├── kiara_1_dawn_1k.hdr │ │ │ ├── lebombo_1k.hdr │ │ │ ├── potsdamer_platz_1k.hdr │ │ │ ├── rooitou_park_1k.hdr │ │ │ ├── st_fagans_interior_1k.hdr │ │ │ ├── studio_small_03_1k.hdr │ │ │ └── venice_sunset_1k.hdr │ │ ├── logo.svg │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css.ts │ │ ├── App.tsx │ │ ├── AppTheme.ts │ │ ├── BrowserWarning.tsx │ │ ├── CameraControls.tsx │ │ ├── ControlPanel │ │ │ ├── BottomPanel.tsx │ │ │ ├── ControlPanel.tsx │ │ │ ├── FloatingPanel.tsx │ │ │ ├── Generated.tsx │ │ │ ├── GuiComponentContext.tsx │ │ │ ├── GuiState.tsx │ │ │ ├── SceneTreeTable.css.ts │ │ │ ├── SceneTreeTable.tsx │ │ │ ├── ServerControls.tsx │ │ │ └── SidebarPanel.tsx │ │ ├── CsmDirectionalLight.tsx │ │ ├── FilePlayback.tsx │ │ ├── HoverContext.ts │ │ ├── Line.tsx │ │ ├── MacWindowWrapper.tsx │ │ ├── Markdown.tsx │ │ ├── MessageHandler.tsx │ │ ├── Modal.tsx │ │ ├── Outlines.tsx │ │ ├── OutlinesIfHovered.tsx │ │ ├── SceneTree.tsx │ │ ├── SceneTreeState.tsx │ │ ├── SearchParamsUtils.tsx │ │ ├── ShadowArgs.tsx │ │ ├── Splatting │ │ │ ├── GaussianSplats.tsx │ │ │ ├── GaussianSplatsHelpers.ts │ │ │ ├── SplatSortWorker.ts │ │ │ └── WasmSorter │ │ │ │ ├── Sorter.mjs │ │ │ │ ├── Sorter.wasm │ │ │ │ ├── build.sh │ │ │ │ └── sorter.cpp │ │ ├── ThreeAssets.tsx │ │ ├── Titlebar.tsx │ │ ├── VersionInfo.ts │ │ ├── ViewerContext.ts │ │ ├── WebsocketInterface.tsx │ │ ├── WebsocketMessages.ts │ │ ├── WebsocketServerWorker.ts │ │ ├── WebsocketUtils.ts │ │ ├── WorldTransformUtils.ts │ │ ├── components │ │ │ ├── Button.tsx │ │ │ ├── ButtonGroup.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── ComponentStyles.css.ts │ │ │ ├── Dropdown.tsx │ │ │ ├── Folder.css.ts │ │ │ ├── Folder.tsx │ │ │ ├── Html.tsx │ │ │ ├── Image.tsx │ │ │ ├── Markdown.tsx │ │ │ ├── MultiSlider.tsx │ │ │ ├── MultiSliderComponent.css │ │ │ ├── MultiSliderComponent.tsx │ │ │ ├── NumberInput.tsx │ │ │ ├── PlotlyComponent.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── Rgb.tsx │ │ │ ├── Rgba.tsx │ │ │ ├── Slider.tsx │ │ │ ├── TabGroup.tsx │ │ │ ├── TextInput.tsx │ │ │ ├── UploadButton.tsx │ │ │ ├── Vector2.tsx │ │ │ ├── Vector3.tsx │ │ │ ├── colorUtils.ts │ │ │ └── common.tsx │ │ ├── csm │ │ │ ├── CSM.d.ts │ │ │ ├── CSM.js │ │ │ ├── CSMFrustum.js │ │ │ ├── CSMHelper.js │ │ │ ├── CSMShader.js │ │ │ └── CSMShadowNode.js │ │ ├── index.css │ │ ├── index.tsx │ │ ├── mesh │ │ │ ├── BasicMesh.tsx │ │ │ ├── BatchedGlbAsset.tsx │ │ │ ├── BatchedMesh.tsx │ │ │ ├── BatchedMeshBase.tsx │ │ │ ├── BatchedMeshHoverOutlines.tsx │ │ │ ├── GlbLoaderUtils.tsx │ │ │ ├── MeshUtils.tsx │ │ │ ├── SingleGlbAsset.tsx │ │ │ └── SkinnedMesh.tsx │ │ └── react-app-env.d.ts │ ├── tsconfig.json │ ├── vite-env.d.ts │ ├── vite.config.mts │ └── yarn.lock │ ├── extras │ ├── __init__.py │ ├── _record3d.py │ ├── _urdf.py │ └── colmap │ │ ├── __init__.py │ │ └── _colmap_utils.py │ ├── infra │ ├── __init__.py │ ├── _async_message_buffer.py │ ├── _infra.py │ ├── _messages.py │ └── _typescript_interface_gen.py │ ├── py.typed │ ├── theme │ ├── __init__.py │ └── _titlebar.py │ └── transforms │ ├── __init__.py │ ├── _base.py │ ├── _se2.py │ ├── _se3.py │ ├── _so2.py │ ├── _so3.py │ ├── hints │ └── __init__.py │ └── utils │ ├── __init__.py │ └── _utils.py ├── sync_client_server.py └── tests ├── test_garbage_collection.py ├── test_message_annotations.py ├── test_server_stop.py ├── test_transforms_axioms.py ├── test_transforms_bijective.py ├── test_transforms_ops.py ├── test_version_sync.py └── utils.py /.clang-format: -------------------------------------------------------------------------------- 1 | # C++ formatting rules; used for WebAssembly code. 2 | BasedOnStyle: LLVM 3 | AlignAfterOpenBracket: BlockIndent 4 | BinPackArguments: false 5 | BinPackParameters: false 6 | IndentWidth: 4 7 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | release: 9 | types: [created] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | docs: 14 | runs-on: ubuntu-latest 15 | steps: 16 | # Check out source. 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 # This ensures the entire history is fetched so we can switch branches 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v1 23 | with: 24 | python-version: "3.10" 25 | 26 | - name: Set up dependencies 27 | run: | 28 | pip install uv 29 | uv pip install --system -e ".[dev,examples]" 30 | uv pip install --system -r docs/requirements.txt 31 | 32 | # Get version from pyproject.toml. 33 | - name: Get version + subdirectory 34 | run: | 35 | VERSION=$(python -c "import viser; print(viser.__version__)") 36 | echo "VERSION=$VERSION" >> $GITHUB_ENV 37 | echo "DOCS_SUBDIR=versions/$VERSION" >> $GITHUB_ENV 38 | 39 | # Hack to overwrite version. 40 | - name: Set version to 'main' for pushes (this will appear in the doc banner) 41 | run: | 42 | echo "VISER_VERSION_STR_OVERRIDE=main" >> $GITHUB_ENV 43 | if: github.event_name == 'push' 44 | 45 | # Build documentation. 46 | - name: Building documentation 47 | run: | 48 | sphinx-build docs/source docs/build -b dirhtml 49 | 50 | # Get version from pyproject.toml. 51 | - name: Override subdirectory to `main/` for pushes 52 | run: | 53 | echo "DOCS_SUBDIR=main" >> $GITHUB_ENV 54 | if: github.event_name == 'push' 55 | 56 | # Deploy to version-dependent subdirectory. 57 | - name: Deploy to GitHub Pages 58 | uses: peaceiris/actions-gh-pages@v4 59 | with: 60 | github_token: ${{ secrets.GITHUB_TOKEN }} 61 | publish_dir: ./docs/build 62 | destination_dir: ${{ env.DOCS_SUBDIR }} 63 | keep_files: false # This will only erase the destination subdirectory. 64 | cname: viser.studio 65 | if: github.event_name != 'pull_request' 66 | 67 | # We'll maintain an index of all versions under viser.studio/versions. 68 | # This will be useful for dynamically generating lists of possible doc links. 69 | - name: Update versions index.txt 70 | run: | 71 | git checkout . # Revert change to pyproject.toml from earlier... 72 | git checkout gh-pages 73 | git pull 74 | git config --global user.email "yibrenth@gmail.com" 75 | git config --global user.name "Brent Yi" 76 | FILE="versions/index.txt" # Replace with your file path 77 | if ! grep -qx "$VERSION" "$FILE"; then 78 | echo "$VERSION" >> "$FILE" 79 | git add $FILE 80 | git commit -m "Update versions.txt with new version $VERSION" 81 | git push origin gh-pages 82 | fi 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | VERSION: ${{ env.VERSION }} 86 | if: github.event_name == 'release' 87 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: eslint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | eslint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | - name: Run eslint 19 | run: | 20 | cd src/viser/client 21 | yarn 22 | yarn eslint . 23 | -------------------------------------------------------------------------------- /.github/workflows/formatting.yaml: -------------------------------------------------------------------------------- 1 | name: formatting 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 3.12 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: 3.12 19 | - name: Install dependencies 20 | run: | 21 | pip install uv 22 | uv pip install --system -e ".[dev,examples]" 23 | - name: Run Ruff 24 | run: ruff check docs/ src/ examples/ 25 | - name: Run Ruff format 26 | run: ruff format docs/ src/ examples/ --check 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Poetry when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.8" 20 | - name: Install dependencies and build client 21 | run: | 22 | pip install uv 23 | uv pip install --system -e ".[dev]" 24 | uv pip install --system build twine 25 | # Build client files. 26 | python -c "import viser; viser.ViserServer()" 27 | - name: Strip unsupported tags in README 28 | run: | 29 | sed -i '//,//d' README.md 30 | - name: Only bundle client build for PyPI release 31 | run: | 32 | # This should delete everything in src/viser/client except for the 33 | # build folder + original source files. We don't want to package 34 | # .nodeenv, node_modules, etc in the release. 35 | mv src/viser/client/build ./__built_client 36 | rm -rf src/viser/client/* 37 | git checkout src/viser/client 38 | mv ./__built_client src/viser/client/build 39 | - name: Build and publish 40 | env: 41 | PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} 42 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 43 | run: | 44 | python -m build 45 | twine upload --username $PYPI_USERNAME --password $PYPI_PASSWORD dist/* 46 | -------------------------------------------------------------------------------- /.github/workflows/pyright.yml: -------------------------------------------------------------------------------- 1 | name: pyright 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | pyright: 11 | runs-on: ubuntu-22.04 12 | strategy: 13 | matrix: 14 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v1 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install uv 25 | uv pip install --system -e ".[dev,examples]" 26 | - name: Run pyright 27 | run: | 28 | pyright ./src 29 | pyright ./examples_dev 30 | -------------------------------------------------------------------------------- /.github/workflows/pyright_examples.yml: -------------------------------------------------------------------------------- 1 | # Check that pyright passes on the examples when using the latest release. We 2 | # want to avoid breaking examples. 3 | 4 | name: pyright (examples) 5 | 6 | # Let's only run on pull requests. We'd expect this check to fail when making 7 | # pushes for new releases. 8 | on: 9 | # push: 10 | # branches: [main] 11 | pull_request: 12 | branches: [main] 13 | 14 | jobs: 15 | pyright: 16 | runs-on: ubuntu-22.04 17 | strategy: 18 | matrix: 19 | python-version: ["3.12"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v1 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | pip install uv 30 | uv pip install --system "viser[dev,examples]" 31 | - name: Run pyright 32 | run: | 33 | pyright examples 34 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.8", "3.8", "3.9", "3.10", "3.11", "3.12"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install uv 25 | uv pip install --system -e ".[dev,examples]" 26 | - name: Test with pytest 27 | run: | 28 | pytest 29 | -------------------------------------------------------------------------------- /.github/workflows/typescript-compile.yml: -------------------------------------------------------------------------------- 1 | name: typescript-compile 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | typescript-compile: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | - name: Run tsc 19 | run: | 20 | cd src/viser/client 21 | yarn 22 | yarn tsc 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.pyc 4 | *.egg-info 5 | *.ipynb_checkpoints 6 | __pycache__ 7 | .coverage 8 | htmlcov 9 | .mypy_cache 10 | .dmypy.json 11 | .hypothesis 12 | .envrc 13 | .lvimrc 14 | .DS_Store 15 | .envrc 16 | .vite 17 | build 18 | src/viser/client/build 19 | src/viser/client/.nodeenv 20 | 21 | **/.claude/settings.local.json 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | default_language_version: 4 | python: python3 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v3.2.0 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | # Ruff version. 13 | rev: v0.6.2 14 | hooks: 15 | # Run the linter. 16 | - id: ruff 17 | args: [--fix] 18 | # Run the formatter. 19 | - id: ruff-format 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.mjs 2 | build/ 3 | csm/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | viser logo 3 | viser 4 | viser logo 5 |

6 | 7 |

8 | pyright 9 | typescript-compile 10 | 11 | codecov 12 | 13 |

14 | 15 | `viser` is a library for interactive 3D visualization in Python. 16 | 17 | Features include: 18 | 19 | - API for visualizing 3D primitives 20 | - GUI building blocks: buttons, checkboxes, text inputs, sliders, etc. 21 | - Scene interaction tools (clicks, selection, transform gizmos) 22 | - Programmatic camera control and rendering 23 | - An entirely web-based client, for easy use over SSH! 24 | 25 | For usage and API reference, see our documentation. 26 | 27 | ## Installation 28 | 29 | You can install `viser` with `pip`: 30 | 31 | ```bash 32 | pip install viser 33 | ``` 34 | 35 | To include example dependencies: 36 | 37 | ```bash 38 | pip install viser[examples] 39 | ``` 40 | 41 | After an example script is running, you can connect by navigating to the printed 42 | URL (default: `http://localhost:8080`). 43 | 44 | See also: our [development docs](https://viser.studio/main/development/). 45 | 46 | ## Examples 47 | 48 | **Point cloud visualization** 49 | 50 | https://github.com/nerfstudio-project/viser/assets/6992947/df35c6ee-78a3-43ad-a2c7-1dddf83f7458 51 | 52 | Source: `./examples/07_record3d_visualizer.py` 53 | 54 | **Gaussian splatting visualization** 55 | 56 | https://github.com/nerfstudio-project/viser/assets/6992947/c51b4871-6cc8-4987-8751-2bf186bcb1ae 57 | 58 | Source: 59 | [WangFeng18/3d-gaussian-splatting](https://github.com/WangFeng18/3d-gaussian-splatting) 60 | and 61 | [heheyas/gaussian_splatting_3d](https://github.com/heheyas/gaussian_splatting_3d). 62 | 63 | **SMPLX visualizer** 64 | 65 | https://github.com/nerfstudio-project/viser/assets/6992947/78ba0e09-612d-4678-abf3-beaeeffddb01 66 | 67 | Source: `./example/08_smpl_visualizer.py` 68 | 69 | ## Acknowledgements 70 | 71 | `viser` is heavily inspired by packages like 72 | [Pangolin](https://github.com/stevenlovegrove/Pangolin), 73 | [rviz](https://wiki.ros.org/rviz/), 74 | [meshcat](https://github.com/rdeits/meshcat), and 75 | [Gradio](https://github.com/gradio-app/gradio). 76 | It's made possible by several open-source projects. 77 | 78 | The web client is implemented using [React](https://react.dev/), with: 79 | 80 | - [Vite](https://vitejs.dev/) / [Rollup](https://rollupjs.org/) for bundling 81 | - [three.js](https://threejs.org/) via [react-three-fiber](https://github.com/pmndrs/react-three-fiber) and [drei](https://github.com/pmndrs/drei) 82 | - [Mantine](https://mantine.dev/) for UI components 83 | - [zustand](https://github.com/pmndrs/zustand) for state management 84 | - [vanilla-extract](https://vanilla-extract.style/) for stylesheets 85 | 86 | The Python API communicates via [msgpack](https://msgpack.org/index.html) and [websockets](https://websockets.readthedocs.io/en/stable/index.html). 87 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = viser 8 | SOURCEDIR = source 9 | BUILDDIR = ./build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Viser Documentation 2 | 3 | This directory contains the documentation for Viser. 4 | 5 | ## Building the Documentation 6 | 7 | To build the documentation: 8 | 9 | 1. Install the documentation dependencies: 10 | 11 | ```bash 12 | pip install -r docs/requirements.txt 13 | ``` 14 | 15 | 2. Build the documentation: 16 | 17 | ```bash 18 | cd docs 19 | make html 20 | ``` 21 | 22 | 3. View the documentation: 23 | 24 | ```bash 25 | # On macOS 26 | open build/html/index.html 27 | 28 | # On Linux 29 | xdg-open build/html/index.html 30 | ``` 31 | 32 | ## Contributing Screenshots 33 | 34 | When adding new documentation, screenshots and visual examples significantly improve user understanding. 35 | 36 | We need screenshots for: 37 | 38 | - The Getting Started guide 39 | - GUI element examples 40 | - Scene API visualization examples 41 | - Customization/theming examples 42 | 43 | See [Contributing Visuals](./source/contributing_visuals.md) for guidelines on capturing and adding images to the documentation. 44 | 45 | ## Documentation Structure 46 | 47 | - `source/` - Source files for the documentation 48 | - `_static/` - Static files (CSS, images, etc.) 49 | - `images/` - Screenshots and other images 50 | - `examples/` - Example code with documentation 51 | - `*.md` - Markdown files for documentation pages 52 | - `conf.py` - Sphinx configuration 53 | 54 | ## Auto-Generated Example Documentation 55 | 56 | Example documentation is automatically generated from the examples in the `examples/` directory using the `update_example_docs.py` script. To update the example documentation after making changes to examples: 57 | 58 | ```bash 59 | cd docs 60 | python update_example_docs.py 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==8.0.2 2 | furo==2024.8.6 3 | docutils==0.20.1 4 | toml==0.10.2 5 | git+https://github.com/brentyi/sphinxcontrib-programoutput.git 6 | git+https://github.com/brentyi/ansi.git 7 | git+https://github.com/sphinx-contrib/googleanalytics.git 8 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | img.sidebar-logo { 2 | width: 5em; 3 | margin: 1em 0 0 0; 4 | } 5 | -------------------------------------------------------------------------------- /docs/source/_static/logo.svg: -------------------------------------------------------------------------------- 1 | ../../../src/viser/client/public/logo.svg -------------------------------------------------------------------------------- /docs/source/_templates/sidebar/brand.html: -------------------------------------------------------------------------------- 1 | 11 | {%- endif %} {%- if theme_light_logo and theme_dark_logo %} 12 | 24 | {%- endif %} 25 | 26 | 27 | {% endblock brand_content %} 28 | 29 | 30 | 31 |
32 | 66 |
75 | 76 | Version: {{ version }} 77 | 78 |
79 |
80 | 81 |
82 | 83 |
84 | 85 | 93 | Github 94 | 95 |
96 | -------------------------------------------------------------------------------- /docs/source/camera_handles.rst: -------------------------------------------------------------------------------- 1 | Camera Handles 2 | ============== 3 | 4 | .. autoclass:: viser.CameraHandle 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/client_handles.rst: -------------------------------------------------------------------------------- 1 | Client Handles 2 | ============== 3 | 4 | .. autoclass:: viser.ClientHandle 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | 9 | .. autoclass:: viser.NotificationHandle 10 | :members: 11 | :undoc-members: 12 | :inherited-members: 13 | -------------------------------------------------------------------------------- /docs/source/conventions.rst: -------------------------------------------------------------------------------- 1 | Frame Conventions 2 | ================= 3 | 4 | In this note, we describe the coordinate frame conventions used in ``viser``. 5 | 6 | Scene tree naming 7 | ----------------- 8 | 9 | Each object that we add to the scene in viser is instantiated as a node in a 10 | scene tree. The structure of this tree is determined by the names assigned to 11 | the nodes. 12 | 13 | If we add a coordinate frame called ``/base_link/shoulder/wrist``, it signifies 14 | three nodes: the ``wrist`` is a child of the ``shoulder`` which is a child of the 15 | ``base_link``. 16 | 17 | If we set the transformation of a given node like ``/base_link/shoulder``, both 18 | it and its child ``/base_link/shoulder/wrist`` will move. Its parent, 19 | ``/base_link``, will be unaffected. 20 | 21 | Poses 22 | ----- 23 | 24 | Poses in ``viser`` are defined using a pair of fields: 25 | 26 | - ``wxyz``, a unit quaternion orientation term. This should always be 4D. 27 | - ``position``, a translation term. This should always be 3D. 28 | 29 | These correspond to a transformation from coordinates in the local frame to the 30 | parent frame: 31 | 32 | .. math:: 33 | 34 | p_\mathrm{parent} = \begin{bmatrix} R & t \end{bmatrix}\begin{bmatrix}p_\mathrm{local} \\ 1\end{bmatrix} 35 | 36 | where ``wxyz`` is the quaternion form of the :math:`\mathrm{SO}(3)` matrix 37 | :math:`R` and ``position`` is the :math:`\mathbb{R}^3` translation term 38 | :math:`t`. 39 | 40 | World coordinates 41 | ----------------- 42 | 43 | In the world coordinate space, +Z points upward by default. This can be 44 | overridden with :func:`viser.SceneApi.set_up_direction()`. 45 | 46 | Cameras 47 | ------- 48 | 49 | In ``viser``, all camera parameters exposed to the Python API use the 50 | COLMAP/OpenCV convention: 51 | 52 | - Forward: +Z 53 | - Up: -Y 54 | - Right: +X 55 | 56 | Confusingly, this is different from Nerfstudio, which adopts the OpenGL/Blender 57 | convention: 58 | 59 | - Forward: -Z 60 | - Up: +Y 61 | - Right: +X 62 | 63 | Conversion between the two is a simple 180 degree rotation around the local X-axis. 64 | -------------------------------------------------------------------------------- /docs/source/events.rst: -------------------------------------------------------------------------------- 1 | Events 2 | ====== 3 | 4 | We define a small set of event types, which are passed to callback functions 5 | when events like clicks or GUI updates are triggered. 6 | 7 | .. autoclass:: viser.ScenePointerEvent() 8 | 9 | .. autoclass:: viser.SceneNodePointerEvent() 10 | 11 | .. autoclass:: viser.GuiEvent() 12 | -------------------------------------------------------------------------------- /docs/source/examples/00_coordinate_frames.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Coordinate frames 5 | ========================================== 6 | 7 | 8 | In this basic example, we visualize a set of coordinate frames. 9 | 10 | Naming for all scene nodes are hierarchical; /tree/branch, for example, is defined 11 | relative to /tree. 12 | 13 | 14 | 15 | .. code-block:: python 16 | :linenos: 17 | 18 | 19 | import random 20 | import time 21 | 22 | import viser 23 | 24 | server = viser.ViserServer() 25 | 26 | while True: 27 | # Add some coordinate frames to the scene. These will be visualized in the viewer. 28 | server.scene.add_frame( 29 | "/tree", 30 | wxyz=(1.0, 0.0, 0.0, 0.0), 31 | position=(random.random() * 2.0, 2.0, 0.2), 32 | ) 33 | server.scene.add_frame( 34 | "/tree/branch", 35 | wxyz=(1.0, 0.0, 0.0, 0.0), 36 | position=(random.random() * 2.0, 2.0, 0.2), 37 | ) 38 | leaf = server.scene.add_frame( 39 | "/tree/branch/leaf", 40 | wxyz=(1.0, 0.0, 0.0, 0.0), 41 | position=(random.random() * 2.0, 2.0, 0.2), 42 | ) 43 | time.sleep(5.0) 44 | 45 | # Remove the leaf node from the scene. 46 | leaf.remove() 47 | time.sleep(0.5) 48 | -------------------------------------------------------------------------------- /docs/source/examples/01_image.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Images 5 | ========================================== 6 | 7 | 8 | Example for sending images to the viewer. 9 | 10 | We can send backgrond images to display behind the viewer (useful for visualizing 11 | NeRFs), or images to render as 3D textures. 12 | 13 | 14 | 15 | .. code-block:: python 16 | :linenos: 17 | 18 | 19 | import time 20 | from pathlib import Path 21 | 22 | import imageio.v3 as iio 23 | import numpy as np 24 | 25 | import viser 26 | 27 | 28 | def main() -> None: 29 | server = viser.ViserServer() 30 | 31 | # Add a background image. 32 | server.scene.set_background_image( 33 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), 34 | format="png", 35 | ) 36 | 37 | # Add main image. 38 | server.scene.add_image( 39 | "/img", 40 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), 41 | 4.0, 42 | 4.0, 43 | format="png", 44 | wxyz=(1.0, 0.0, 0.0, 0.0), 45 | position=(2.0, 2.0, 0.0), 46 | ) 47 | while True: 48 | server.scene.add_image( 49 | "/noise", 50 | np.random.randint(0, 256, size=(400, 400, 3), dtype=np.uint8), 51 | 4.0, 52 | 4.0, 53 | format="jpeg", 54 | wxyz=(1.0, 0.0, 0.0, 0.0), 55 | position=(2.0, 2.0, -1e-2), 56 | ) 57 | time.sleep(0.2) 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /docs/source/examples/04_camera_poses.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Camera poses 5 | ========================================== 6 | 7 | 8 | Example showing how we can detect new clients and read camera poses from them. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | 18 | import viser 19 | 20 | server = viser.ViserServer() 21 | server.scene.world_axes.visible = True 22 | 23 | 24 | @server.on_client_connect 25 | def _(client: viser.ClientHandle) -> None: 26 | print("new client!") 27 | 28 | # This will run whenever we get a new camera! 29 | @client.camera.on_update 30 | def _(_: viser.CameraHandle) -> None: 31 | print(f"New camera on client {client.client_id}!") 32 | 33 | # Show the client ID in the GUI. 34 | gui_info = client.gui.add_text("Client ID", initial_value=str(client.client_id)) 35 | gui_info.disabled = True 36 | 37 | 38 | while True: 39 | # Get all currently connected clients. 40 | clients = server.get_clients() 41 | print("Connected client IDs", clients.keys()) 42 | 43 | for id, client in clients.items(): 44 | print(f"Camera pose for client {id}") 45 | print(f"\twxyz: {client.camera.wxyz}") 46 | print(f"\tposition: {client.camera.position}") 47 | print(f"\tfov: {client.camera.fov}") 48 | print(f"\taspect: {client.camera.aspect}") 49 | print(f"\tlast update: {client.camera.update_timestamp}") 50 | 51 | time.sleep(2.0) 52 | -------------------------------------------------------------------------------- /docs/source/examples/06_mesh.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Meshes 5 | ========================================== 6 | 7 | 8 | Visualize a mesh. To get the demo data, see ``./assets/download_dragon_mesh.sh``. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | from pathlib import Path 18 | 19 | import numpy as np 20 | import trimesh 21 | 22 | import viser 23 | import viser.transforms as tf 24 | 25 | mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj")) 26 | assert isinstance(mesh, trimesh.Trimesh) 27 | mesh.apply_scale(0.05) 28 | 29 | vertices = mesh.vertices 30 | faces = mesh.faces 31 | print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces") 32 | 33 | server = viser.ViserServer() 34 | server.scene.add_mesh_simple( 35 | name="/simple", 36 | vertices=vertices, 37 | faces=faces, 38 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz, 39 | position=(0.0, 0.0, 0.0), 40 | ) 41 | server.scene.add_mesh_trimesh( 42 | name="/trimesh", 43 | mesh=mesh, 44 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz, 45 | position=(0.0, 5.0, 0.0), 46 | ) 47 | grid = server.scene.add_grid( 48 | "grid", 49 | width=20.0, 50 | height=20.0, 51 | position=np.array([0.0, 0.0, -2.0]), 52 | ) 53 | 54 | while True: 55 | time.sleep(10.0) 56 | -------------------------------------------------------------------------------- /docs/source/examples/12_click_meshes.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Mesh click events 5 | ========================================== 6 | 7 | 8 | Click on meshes to select them. The index of the last clicked mesh is displayed in the GUI. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | 18 | import matplotlib 19 | 20 | import viser 21 | 22 | 23 | def main() -> None: 24 | grid_shape = (4, 5) 25 | server = viser.ViserServer() 26 | 27 | with server.gui.add_folder("Last clicked"): 28 | x_value = server.gui.add_number( 29 | label="x", 30 | initial_value=0, 31 | disabled=True, 32 | hint="x coordinate of the last clicked mesh", 33 | ) 34 | y_value = server.gui.add_number( 35 | label="y", 36 | initial_value=0, 37 | disabled=True, 38 | hint="y coordinate of the last clicked mesh", 39 | ) 40 | 41 | def add_swappable_mesh(i: int, j: int) -> None: 42 | """Simple callback that swaps between: 43 | - a gray box 44 | - a colored box 45 | - a colored sphere 46 | 47 | Color is chosen based on the position (i, j) of the mesh in the grid. 48 | """ 49 | 50 | colormap = matplotlib.colormaps["tab20"] 51 | 52 | def create_mesh(counter: int) -> None: 53 | if counter == 0: 54 | color = (0.8, 0.8, 0.8) 55 | else: 56 | index = (i * grid_shape[1] + j) / (grid_shape[0] * grid_shape[1]) 57 | color = colormap(index)[:3] 58 | 59 | if counter in (0, 1): 60 | handle = server.scene.add_box( 61 | name=f"/sphere_{i}_{j}", 62 | position=(i, j, 0.0), 63 | color=color, 64 | dimensions=(0.5, 0.5, 0.5), 65 | ) 66 | else: 67 | handle = server.scene.add_icosphere( 68 | name=f"/sphere_{i}_{j}", 69 | radius=0.4, 70 | color=color, 71 | position=(i, j, 0.0), 72 | ) 73 | 74 | @handle.on_click 75 | def _(_) -> None: 76 | x_value.value = i 77 | y_value.value = j 78 | 79 | # The new mesh will replace the old one because the names 80 | # /sphere_{i}_{j} are the same. 81 | create_mesh((counter + 1) % 3) 82 | 83 | create_mesh(0) 84 | 85 | for i in range(grid_shape[0]): 86 | for j in range(grid_shape[1]): 87 | add_swappable_mesh(i, j) 88 | 89 | while True: 90 | time.sleep(10.0) 91 | 92 | 93 | if __name__ == "__main__": 94 | main() 95 | -------------------------------------------------------------------------------- /docs/source/examples/14_markdown.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Markdown demonstration 5 | ========================================== 6 | 7 | 8 | Viser GUI has MDX 2 support. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | from pathlib import Path 18 | 19 | import viser 20 | 21 | server = viser.ViserServer() 22 | server.scene.world_axes.visible = True 23 | 24 | markdown_counter = server.gui.add_markdown("Counter: 0") 25 | 26 | here = Path(__file__).absolute().parent 27 | 28 | button = server.gui.add_button("Remove blurb") 29 | checkbox = server.gui.add_checkbox("Visibility", initial_value=True) 30 | 31 | markdown_source = (here / "./assets/mdx_example.mdx").read_text() 32 | markdown_blurb = server.gui.add_markdown( 33 | content=markdown_source, 34 | image_root=here, 35 | ) 36 | 37 | 38 | @button.on_click 39 | def _(_): 40 | markdown_blurb.remove() 41 | 42 | 43 | @checkbox.on_update 44 | def _(_): 45 | markdown_blurb.visible = checkbox.value 46 | 47 | 48 | counter = 0 49 | while True: 50 | markdown_counter.content = f"Counter: {counter}" 51 | counter += 1 52 | time.sleep(0.1) 53 | -------------------------------------------------------------------------------- /docs/source/examples/16_modal.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Modal basics 5 | ========================================== 6 | 7 | 8 | Examples of using modals in Viser. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | 18 | import viser 19 | 20 | 21 | def main(): 22 | server = viser.ViserServer() 23 | 24 | @server.on_client_connect 25 | def _(client: viser.ClientHandle) -> None: 26 | with client.gui.add_modal("Modal example"): 27 | client.gui.add_markdown( 28 | "**The input below determines the title of the modal...**" 29 | ) 30 | 31 | gui_title = client.gui.add_text( 32 | "Title", 33 | initial_value="My Modal", 34 | ) 35 | 36 | modal_button = client.gui.add_button("Show more modals") 37 | 38 | @modal_button.on_click 39 | def _(_) -> None: 40 | with client.gui.add_modal(gui_title.value) as modal: 41 | client.gui.add_markdown("This is content inside the modal!") 42 | client.gui.add_button("Close").on_click(lambda _: modal.close()) 43 | 44 | while True: 45 | time.sleep(0.15) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /docs/source/examples/17_background_composite.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Depth compositing 5 | ========================================== 6 | 7 | 8 | In this example, we show how to use a background image with depth compositing. This can 9 | be useful when we want a 2D image to occlude 3D geometry, such as for NeRF rendering. 10 | 11 | 12 | 13 | .. code-block:: python 14 | :linenos: 15 | 16 | 17 | import time 18 | 19 | import numpy as np 20 | import trimesh 21 | import trimesh.creation 22 | 23 | import viser 24 | 25 | server = viser.ViserServer() 26 | 27 | 28 | img = np.random.randint(0, 255, size=(1000, 1000, 3), dtype=np.uint8) 29 | depth = np.ones((1000, 1000, 1), dtype=np.float32) 30 | 31 | # Make a square middle portal. 32 | depth[250:750, 250:750, :] = 10.0 33 | img[250:750, 250:750, :] = 255 34 | 35 | mesh = trimesh.creation.box((0.5, 0.5, 0.5)) 36 | server.scene.add_mesh_trimesh( 37 | name="/cube", 38 | mesh=mesh, 39 | position=(0, 0, 0.0), 40 | ) 41 | server.scene.set_background_image(img, depth=depth) 42 | 43 | 44 | while True: 45 | time.sleep(1.0) 46 | -------------------------------------------------------------------------------- /docs/source/examples/18_lines.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Lines 5 | ========================================== 6 | 7 | 8 | Make a ball with some random line segments and splines. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | 18 | import numpy as np 19 | 20 | import viser 21 | 22 | 23 | def main() -> None: 24 | server = viser.ViserServer() 25 | 26 | # Line segments. 27 | # 28 | # This will be much faster than creating separate scene objects for 29 | # individual line segments or splines. 30 | N = 2000 31 | points = np.random.normal(size=(N, 2, 3)) * 3.0 32 | colors = np.random.randint(0, 255, size=(N, 2, 3)) 33 | server.scene.add_line_segments( 34 | "/line_segments", 35 | points=points, 36 | colors=colors, 37 | line_width=3.0, 38 | ) 39 | 40 | # Spline helpers. 41 | # 42 | # If many lines are needed, it'll be more efficient to batch them in 43 | # `add_line_segments()`. 44 | for i in range(10): 45 | points = np.random.normal(size=(30, 3)) * 3.0 46 | server.scene.add_spline_catmull_rom( 47 | f"/catmull/{i}", 48 | positions=points, 49 | tension=0.5, 50 | line_width=3.0, 51 | color=np.random.uniform(size=3), 52 | segments=100, 53 | ) 54 | 55 | control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0 56 | server.scene.add_spline_cubic_bezier( 57 | f"/cubic_bezier/{i}", 58 | positions=points, 59 | control_points=control_points, 60 | line_width=3.0, 61 | color=np.random.uniform(size=3), 62 | segments=100, 63 | ) 64 | 65 | while True: 66 | time.sleep(10.0) 67 | 68 | 69 | if __name__ == "__main__": 70 | main() 71 | -------------------------------------------------------------------------------- /docs/source/examples/19_get_renders.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Get renders 5 | ========================================== 6 | 7 | 8 | Example for getting renders from a client's viewport to the Python API. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | 18 | import imageio.v3 as iio 19 | import numpy as np 20 | 21 | import viser 22 | 23 | 24 | def main(): 25 | server = viser.ViserServer() 26 | 27 | button = server.gui.add_button("Render a GIF") 28 | 29 | @button.on_click 30 | def _(event: viser.GuiEvent) -> None: 31 | client = event.client 32 | assert client is not None 33 | 34 | client.scene.reset() 35 | 36 | images = [] 37 | 38 | for i in range(20): 39 | positions = np.random.normal(size=(30, 3)) 40 | client.scene.add_spline_catmull_rom( 41 | f"/catmull_{i}", 42 | positions, 43 | tension=0.5, 44 | line_width=3.0, 45 | color=np.random.uniform(size=3), 46 | ) 47 | images.append(client.get_render(height=720, width=1280)) 48 | print("Got image with shape", images[-1].shape) 49 | 50 | print("Generating and sending GIF...") 51 | client.send_file_download( 52 | "image.gif", iio.imwrite("", images, extension=".gif", loop=0) 53 | ) 54 | print("Done!") 55 | 56 | while True: 57 | time.sleep(10.0) 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /docs/source/examples/21_set_up_direction.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Set up direction 5 | ========================================== 6 | 7 | 8 | ``.set_up_direction()`` can help us set the global up direction. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | 18 | import viser 19 | 20 | 21 | def main() -> None: 22 | server = viser.ViserServer() 23 | server.scene.world_axes.visible = True 24 | gui_up = server.gui.add_vector3( 25 | "Up Direction", 26 | initial_value=(0.0, 0.0, 1.0), 27 | step=0.01, 28 | ) 29 | 30 | @gui_up.on_update 31 | def _(_) -> None: 32 | server.scene.set_up_direction(gui_up.value) 33 | 34 | while True: 35 | time.sleep(1.0) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /docs/source/examples/23_plotly.rst: -------------------------------------------------------------------------------- 1 | .. Comment: this file is automatically generated by `update_example_docs.py`. 2 | It should not be modified manually. 3 | 4 | Plotly 5 | ========================================== 6 | 7 | 8 | Examples of visualizing plotly plots in Viser. 9 | 10 | 11 | 12 | .. code-block:: python 13 | :linenos: 14 | 15 | 16 | import time 17 | 18 | import numpy as np 19 | import plotly.express as px 20 | import plotly.graph_objects as go 21 | from PIL import Image 22 | 23 | import viser 24 | 25 | 26 | def create_sinusoidal_wave(t: float) -> go.Figure: 27 | """Create a sinusoidal wave plot, starting at time t.""" 28 | x_data = np.linspace(t, t + 6 * np.pi, 50) 29 | y_data = np.sin(x_data) * 10 30 | 31 | fig = px.line( 32 | x=list(x_data), 33 | y=list(y_data), 34 | labels={"x": "x", "y": "sin(x)"}, 35 | title="Sinusoidal Wave", 36 | ) 37 | 38 | # this sets the margins to be tight around the title. 39 | fig.layout.title.automargin = True # type: ignore 40 | fig.update_layout( 41 | margin=dict(l=20, r=20, t=20, b=20), 42 | ) # Reduce plot margins. 43 | 44 | return fig 45 | 46 | 47 | def main() -> None: 48 | server = viser.ViserServer() 49 | 50 | # Plot type 1: Line plot. 51 | line_plot_time = 0.0 52 | line_plot = server.gui.add_plotly(figure=create_sinusoidal_wave(line_plot_time)) 53 | 54 | # Plot type 2: Image plot. 55 | fig = px.imshow(Image.open("assets/Cal_logo.png")) 56 | fig.update_layout( 57 | margin=dict(l=20, r=20, t=20, b=20), 58 | ) 59 | server.gui.add_plotly(figure=fig, aspect=1.0) 60 | 61 | # Plot type 3: 3D Scatter plot. 62 | fig = px.scatter_3d( 63 | px.data.iris(), 64 | x="sepal_length", 65 | y="sepal_width", 66 | z="petal_width", 67 | color="species", 68 | ) 69 | fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)) 70 | fig.update_layout( 71 | margin=dict(l=20, r=20, t=20, b=20), 72 | ) 73 | server.gui.add_plotly(figure=fig, aspect=1.0) 74 | 75 | while True: 76 | # Update the line plot. 77 | line_plot_time += 0.1 78 | line_plot.figure = create_sinusoidal_wave(line_plot_time) 79 | 80 | time.sleep(0.01) 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /docs/source/extras.rst: -------------------------------------------------------------------------------- 1 | Record3D + URDF Helpers 2 | ======================= 3 | 4 | .. automodule:: viser.extras 5 | -------------------------------------------------------------------------------- /docs/source/gui_api.rst: -------------------------------------------------------------------------------- 1 | GUI API 2 | ======= 3 | 4 | .. autoclass:: viser.GuiApi 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/gui_handles.rst: -------------------------------------------------------------------------------- 1 | GUI Handles 2 | =========== 3 | 4 | .. autoclass:: viser.GuiInputHandle() 5 | 6 | .. autoclass:: viser.GuiButtonHandle() 7 | 8 | .. autoclass:: viser.GuiButtonGroupHandle() 9 | 10 | .. autoclass:: viser.GuiDropdownHandle() 11 | 12 | .. autoclass:: viser.GuiFolderHandle() 13 | 14 | .. autoclass:: viser.GuiMarkdownHandle() 15 | 16 | .. autoclass:: viser.GuiHtmlHandle() 17 | 18 | .. autoclass:: viser.GuiPlotlyHandle() 19 | 20 | .. autoclass:: viser.GuiTabGroupHandle() 21 | 22 | .. autoclass:: viser.GuiTabHandle() 23 | 24 | .. autoclass:: viser.GuiCheckboxHandle() 25 | 26 | .. autoclass:: viser.GuiMultiSliderHandle() 27 | 28 | .. autoclass:: viser.GuiNumberHandle() 29 | 30 | .. autoclass:: viser.GuiRgbaHandle() 31 | 32 | .. autoclass:: viser.GuiRgbHandle() 33 | 34 | .. autoclass:: viser.GuiSliderHandle() 35 | 36 | .. autoclass:: viser.GuiTextHandle() 37 | 38 | .. autoclass:: viser.GuiUploadButtonHandle() 39 | 40 | .. autoclass:: viser.UploadedFile() 41 | 42 | .. autoclass:: viser.GuiVector2Handle() 43 | 44 | .. autoclass:: viser.GuiVector3Handle() 45 | -------------------------------------------------------------------------------- /docs/source/icons.rst: -------------------------------------------------------------------------------- 1 | Icons 2 | ===== 3 | 4 | Icons for GUI elements (such as :meth:`GuiApi.add_button()`) can be 5 | specified using the :class:`viser.Icon` enum. 6 | 7 | .. autoclass:: viser.IconName 8 | .. autoclass:: viser.Icon 9 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | viser 2 | ===== 3 | 4 | |pyright| |nbsp| |typescript| |nbsp| |versions| 5 | 6 | **viser** is a library for interactive 3D visualization in Python. 7 | 8 | Features include: 9 | 10 | - API for visualizing 3D primitives 11 | - GUI building blocks: buttons, checkboxes, text inputs, sliders, etc. 12 | - Scene interaction tools (clicks, selection, transform gizmos) 13 | - Programmatic camera control and rendering 14 | - An entirely web-based client, for easy use over SSH! 15 | 16 | Installation 17 | ----------- 18 | 19 | You can install ``viser`` with ``pip``: 20 | 21 | .. code-block:: bash 22 | 23 | pip install viser 24 | 25 | To include example dependencies: 26 | 27 | .. code-block:: bash 28 | 29 | pip install viser[examples] 30 | 31 | After an example script is running, you can connect by navigating to the printed 32 | URL (default: ``http://localhost:8080``). 33 | 34 | .. toctree:: 35 | :caption: Notes 36 | :hidden: 37 | :maxdepth: 1 38 | :titlesonly: 39 | 40 | ./conventions.rst 41 | ./development.rst 42 | ./embedded_visualizations.rst 43 | 44 | .. toctree:: 45 | :caption: API (Basics) 46 | :hidden: 47 | :maxdepth: 1 48 | :titlesonly: 49 | 50 | ./server.rst 51 | ./scene_api.rst 52 | ./gui_api.rst 53 | ./state_serializer.rst 54 | 55 | 56 | .. toctree:: 57 | :caption: API (Advanced) 58 | :hidden: 59 | :maxdepth: 1 60 | :titlesonly: 61 | 62 | ./client_handles.rst 63 | ./camera_handles.rst 64 | ./gui_handles.rst 65 | ./scene_handles.rst 66 | ./events.rst 67 | ./icons.rst 68 | 69 | 70 | .. toctree:: 71 | :caption: API (Auxiliary) 72 | :hidden: 73 | :maxdepth: 1 74 | :titlesonly: 75 | 76 | ./transforms.rst 77 | ./infrastructure.rst 78 | ./extras.rst 79 | 80 | .. toctree:: 81 | :caption: Examples 82 | :hidden: 83 | :maxdepth: 1 84 | :titlesonly: 85 | :glob: 86 | 87 | examples/* 88 | 89 | 90 | .. |pyright| image:: https://github.com/nerfstudio-project/viser/actions/workflows/pyright.yml/badge.svg 91 | :alt: Pyright status icon 92 | :target: https://github.com/nerfstudio-project/viser 93 | .. |typescript| image:: https://github.com/nerfstudio-project/viser/actions/workflows/typescript-compile.yml/badge.svg 94 | :alt: TypeScript status icon 95 | :target: https://github.com/nerfstudio-project/viser 96 | .. |versions| image:: https://img.shields.io/pypi/pyversions/viser 97 | :alt: Version icon 98 | :target: https://pypi.org/project/viser/ 99 | .. |nbsp| unicode:: 0xA0 100 | :trim: -------------------------------------------------------------------------------- /docs/source/infrastructure.rst: -------------------------------------------------------------------------------- 1 | Communication 2 | ============= 3 | 4 | .. automodule:: viser.infra 5 | :show-inheritance: 6 | -------------------------------------------------------------------------------- /docs/source/scene_api.rst: -------------------------------------------------------------------------------- 1 | Scene API 2 | ========= 3 | 4 | .. autoclass:: viser.SceneApi 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/scene_handles.rst: -------------------------------------------------------------------------------- 1 | Scene Handles 2 | ============= 3 | 4 | A handle is created for each object that is added to the scene. These can be 5 | used to read and set state, as well as detect clicks. 6 | 7 | When a scene node is added to a server (for example, via 8 | :func:`viser.ViserServer.add_frame()`), state is synchronized between all 9 | connected clients. When a scene node is added to a client (for example, via 10 | :func:`viser.ClientHandle.add_frame()`), state is local to a specific client. 11 | 12 | The most common attributes to read and write here are 13 | :attr:`viser.SceneNodeHandle.wxyz` and :attr:`viser.SceneNodeHandle.position`. 14 | Each node type also has type-specific attributes that we can read and write. 15 | Many of these are lower-level than their equivalent arguments in factory 16 | methods like :func:`viser.ViserServer.add_frame()` or 17 | :func:`viser.ViserServer.add_image()`. 18 | 19 | .. autoclass:: viser.SceneNodeHandle 20 | 21 | .. autoclass:: viser.CameraFrustumHandle 22 | 23 | .. autoclass:: viser.FrameHandle 24 | 25 | .. autoclass:: viser.BatchedAxesHandle 26 | 27 | .. autoclass:: viser.GlbHandle 28 | 29 | .. autoclass:: viser.GridHandle 30 | 31 | .. autoclass:: viser.Gui3dContainerHandle 32 | 33 | .. autoclass:: viser.ImageHandle 34 | 35 | .. autoclass:: viser.LabelHandle 36 | 37 | .. autoclass:: viser.MeshHandle 38 | 39 | .. autoclass:: viser.BatchedMeshHandle 40 | 41 | .. autoclass:: viser.BatchedGlbHandle 42 | 43 | .. autoclass:: viser.MeshSkinnedHandle 44 | 45 | .. autoclass:: viser.MeshSkinnedBoneHandle 46 | 47 | .. autoclass:: viser.PointCloudHandle 48 | 49 | .. autoclass:: viser.SplineCatmullRomHandle 50 | 51 | .. autoclass:: viser.SplineCubicBezierHandle 52 | 53 | .. autoclass:: viser.TransformControlsHandle 54 | 55 | .. autoclass:: viser.GaussianSplatHandle 56 | 57 | .. autoclass:: viser.DirectionalLightHandle 58 | 59 | .. autoclass:: viser.AmbientLightHandle 60 | 61 | .. autoclass:: viser.HemisphereLightHandle 62 | 63 | .. autoclass:: viser.PointLightHandle 64 | 65 | .. autoclass:: viser.RectAreaLightHandle 66 | 67 | .. autoclass:: viser.SpotLightHandle -------------------------------------------------------------------------------- /docs/source/server.rst: -------------------------------------------------------------------------------- 1 | Viser Server 2 | ============ 3 | 4 | .. autoclass:: viser.ViserServer 5 | -------------------------------------------------------------------------------- /docs/source/state_serializer.rst: -------------------------------------------------------------------------------- 1 | State Serializer 2 | =============== 3 | 4 | .. autoclass:: viser.infra.StateSerializer 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/transforms.rst: -------------------------------------------------------------------------------- 1 | Transforms 2 | ========== 3 | 4 | .. automodule:: viser.transforms 5 | :show-inheritance: 6 | -------------------------------------------------------------------------------- /docs/update_example_docs.py: -------------------------------------------------------------------------------- 1 | """Helper script for updating the auto-generated examples pages in the documentation.""" 2 | 3 | from __future__ import annotations 4 | 5 | import dataclasses 6 | import pathlib 7 | import shutil 8 | from typing import Iterable 9 | 10 | import m2r2 11 | import tyro 12 | 13 | 14 | @dataclasses.dataclass 15 | class ExampleMetadata: 16 | index: str 17 | index_with_zero: str 18 | source: str 19 | title: str 20 | description: str 21 | 22 | @staticmethod 23 | def from_path(path: pathlib.Path) -> ExampleMetadata: 24 | # 01_functions -> 01, _, functions. 25 | index, _, _ = path.stem.partition("_") 26 | 27 | # 01 -> 1. 28 | index_with_zero = index 29 | index = str(int(index)) 30 | 31 | print("Parsing", path) 32 | source = path.read_text().strip() 33 | docstring = source.split('"""')[1].strip() 34 | 35 | title, _, description = docstring.partition("\n") 36 | 37 | return ExampleMetadata( 38 | index=index, 39 | index_with_zero=index_with_zero, 40 | source=source.partition('"""')[2].partition('"""')[2].strip(), 41 | title=title, 42 | description=description.strip(), 43 | ) 44 | 45 | 46 | def get_example_paths(examples_dir: pathlib.Path) -> Iterable[pathlib.Path]: 47 | return filter( 48 | lambda p: not p.name.startswith("_"), sorted(examples_dir.glob("*.py")) 49 | ) 50 | 51 | 52 | REPO_ROOT = pathlib.Path(__file__).absolute().parent.parent 53 | 54 | 55 | def main( 56 | examples_dir: pathlib.Path = REPO_ROOT / "examples", 57 | sphinx_source_dir: pathlib.Path = REPO_ROOT / "docs" / "source", 58 | ) -> None: 59 | example_doc_dir = sphinx_source_dir / "examples" 60 | shutil.rmtree(example_doc_dir) 61 | example_doc_dir.mkdir() 62 | 63 | for path in get_example_paths(examples_dir): 64 | ex = ExampleMetadata.from_path(path) 65 | 66 | relative_dir = path.parent.relative_to(examples_dir) 67 | target_dir = example_doc_dir / relative_dir 68 | target_dir.mkdir(exist_ok=True, parents=True) 69 | 70 | (target_dir / f"{path.stem}.rst").write_text( 71 | "\n".join( 72 | [ 73 | ( 74 | ".. Comment: this file is automatically generated by" 75 | " `update_example_docs.py`." 76 | ), 77 | " It should not be modified manually.", 78 | "", 79 | f"{ex.title}", 80 | "==========================================", 81 | "", 82 | m2r2.convert(ex.description), 83 | "", 84 | "", 85 | ".. code-block:: python", 86 | " :linenos:", 87 | "", 88 | "", 89 | "\n".join( 90 | f" {line}".rstrip() for line in ex.source.split("\n") 91 | ), 92 | "", 93 | ] 94 | ) 95 | ) 96 | 97 | 98 | if __name__ == "__main__": 99 | tyro.cli(main, description=__doc__) 100 | -------------------------------------------------------------------------------- /examples/00_coordinate_frames.py: -------------------------------------------------------------------------------- 1 | """Coordinate frames 2 | 3 | In this basic example, we visualize a set of coordinate frames. 4 | 5 | Naming for all scene nodes are hierarchical; /tree/branch, for example, is defined 6 | relative to /tree. 7 | """ 8 | 9 | import random 10 | import time 11 | 12 | import viser 13 | 14 | server = viser.ViserServer() 15 | 16 | while True: 17 | # Add some coordinate frames to the scene. These will be visualized in the viewer. 18 | server.scene.add_frame( 19 | "/tree", 20 | wxyz=(1.0, 0.0, 0.0, 0.0), 21 | position=(random.random() * 2.0, 2.0, 0.2), 22 | ) 23 | server.scene.add_frame( 24 | "/tree/branch", 25 | wxyz=(1.0, 0.0, 0.0, 0.0), 26 | position=(random.random() * 2.0, 2.0, 0.2), 27 | ) 28 | leaf = server.scene.add_frame( 29 | "/tree/branch/leaf", 30 | wxyz=(1.0, 0.0, 0.0, 0.0), 31 | position=(random.random() * 2.0, 2.0, 0.2), 32 | ) 33 | time.sleep(5.0) 34 | 35 | # Remove the leaf node from the scene. 36 | leaf.remove() 37 | time.sleep(0.5) 38 | -------------------------------------------------------------------------------- /examples/01_image.py: -------------------------------------------------------------------------------- 1 | """Images 2 | 3 | Example for sending images to the viewer. 4 | 5 | We can send backgrond images to display behind the viewer (useful for visualizing 6 | NeRFs), or images to render as 3D textures. 7 | """ 8 | 9 | import time 10 | from pathlib import Path 11 | 12 | import imageio.v3 as iio 13 | import numpy as np 14 | 15 | import viser 16 | 17 | 18 | def main() -> None: 19 | server = viser.ViserServer() 20 | 21 | # Add a background image. 22 | server.scene.set_background_image( 23 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), 24 | format="png", 25 | ) 26 | 27 | # Add main image. 28 | server.scene.add_image( 29 | "/img", 30 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), 31 | 4.0, 32 | 4.0, 33 | format="png", 34 | wxyz=(1.0, 0.0, 0.0, 0.0), 35 | position=(2.0, 2.0, 0.0), 36 | ) 37 | while True: 38 | server.scene.add_image( 39 | "/noise", 40 | np.random.randint(0, 256, size=(400, 400, 3), dtype=np.uint8), 41 | 4.0, 42 | 4.0, 43 | format="jpeg", 44 | wxyz=(1.0, 0.0, 0.0, 0.0), 45 | position=(2.0, 2.0, -1e-2), 46 | ) 47 | time.sleep(0.2) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /examples/04_camera_poses.py: -------------------------------------------------------------------------------- 1 | """Camera poses 2 | 3 | Example showing how we can detect new clients and read camera poses from them. 4 | """ 5 | 6 | import time 7 | 8 | import viser 9 | 10 | server = viser.ViserServer() 11 | server.scene.world_axes.visible = True 12 | 13 | 14 | @server.on_client_connect 15 | def _(client: viser.ClientHandle) -> None: 16 | print("new client!") 17 | 18 | # This will run whenever we get a new camera! 19 | @client.camera.on_update 20 | def _(_: viser.CameraHandle) -> None: 21 | print(f"New camera on client {client.client_id}!") 22 | 23 | # Show the client ID in the GUI. 24 | gui_info = client.gui.add_text("Client ID", initial_value=str(client.client_id)) 25 | gui_info.disabled = True 26 | 27 | 28 | while True: 29 | # Get all currently connected clients. 30 | clients = server.get_clients() 31 | print("Connected client IDs", clients.keys()) 32 | 33 | for id, client in clients.items(): 34 | print(f"Camera pose for client {id}") 35 | print(f"\twxyz: {client.camera.wxyz}") 36 | print(f"\tposition: {client.camera.position}") 37 | print(f"\tfov: {client.camera.fov}") 38 | print(f"\taspect: {client.camera.aspect}") 39 | print(f"\tlast update: {client.camera.update_timestamp}") 40 | 41 | time.sleep(2.0) 42 | -------------------------------------------------------------------------------- /examples/05_camera_commands.py: -------------------------------------------------------------------------------- 1 | """Camera commands 2 | 3 | In addition to reads, camera parameters also support writes. These are synced to the 4 | corresponding client automatically. 5 | """ 6 | 7 | import time 8 | 9 | import numpy as np 10 | 11 | import viser 12 | import viser.transforms as tf 13 | 14 | server = viser.ViserServer() 15 | num_frames = 20 16 | 17 | 18 | @server.on_client_connect 19 | def _(client: viser.ClientHandle) -> None: 20 | """For each client that connects, create GUI elements for adjusting the 21 | near/far clipping planes.""" 22 | 23 | client.camera.far = 10.0 24 | 25 | near_slider = client.gui.add_slider( 26 | "Near", min=0.01, max=10.0, step=0.001, initial_value=client.camera.near 27 | ) 28 | far_slider = client.gui.add_slider( 29 | "Far", min=1, max=20.0, step=0.001, initial_value=client.camera.far 30 | ) 31 | 32 | @near_slider.on_update 33 | def _(_) -> None: 34 | client.camera.near = near_slider.value 35 | 36 | @far_slider.on_update 37 | def _(_) -> None: 38 | client.camera.far = far_slider.value 39 | 40 | 41 | @server.on_client_connect 42 | def _(client: viser.ClientHandle) -> None: 43 | """For each client that connects, we create a set of random frames + a click handler for each frame. 44 | 45 | When a frame is clicked, we move the camera to the corresponding frame. 46 | """ 47 | 48 | rng = np.random.default_rng(0) 49 | 50 | def make_frame(i: int) -> None: 51 | # Sample a random orientation + position. 52 | wxyz = rng.normal(size=4) 53 | wxyz /= np.linalg.norm(wxyz) 54 | position = rng.uniform(-3.0, 3.0, size=(3,)) 55 | 56 | # Create a coordinate frame and label. 57 | frame = client.scene.add_frame(f"/frame_{i}", wxyz=wxyz, position=position) 58 | client.scene.add_label(f"/frame_{i}/label", text=f"Frame {i}") 59 | 60 | # Move the camera when we click a frame. 61 | @frame.on_click 62 | def _(_): 63 | T_world_current = tf.SE3.from_rotation_and_translation( 64 | tf.SO3(client.camera.wxyz), client.camera.position 65 | ) 66 | T_world_target = tf.SE3.from_rotation_and_translation( 67 | tf.SO3(frame.wxyz), frame.position 68 | ) @ tf.SE3.from_translation(np.array([0.0, 0.0, -0.5])) 69 | 70 | T_current_target = T_world_current.inverse() @ T_world_target 71 | 72 | for j in range(20): 73 | T_world_set = T_world_current @ tf.SE3.exp( 74 | T_current_target.log() * j / 19.0 75 | ) 76 | 77 | # We can atomically set the orientation and the position of the camera 78 | # together to prevent jitter that might happen if one was set before the 79 | # other. 80 | with client.atomic(): 81 | client.camera.wxyz = T_world_set.rotation().wxyz 82 | client.camera.position = T_world_set.translation() 83 | 84 | client.flush() # Optional! 85 | time.sleep(1.0 / 60.0) 86 | 87 | # Mouse interactions should orbit around the frame origin. 88 | client.camera.look_at = frame.position 89 | 90 | for i in range(num_frames): 91 | make_frame(i) 92 | 93 | 94 | while True: 95 | time.sleep(1.0) 96 | -------------------------------------------------------------------------------- /examples/06_mesh.py: -------------------------------------------------------------------------------- 1 | """Meshes 2 | 3 | Visualize a mesh. To get the demo data, see `./assets/download_dragon_mesh.sh`. 4 | """ 5 | 6 | import time 7 | from pathlib import Path 8 | 9 | import numpy as np 10 | import trimesh 11 | 12 | import viser 13 | import viser.transforms as tf 14 | 15 | mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj")) 16 | assert isinstance(mesh, trimesh.Trimesh) 17 | mesh.apply_scale(0.05) 18 | 19 | vertices = mesh.vertices 20 | faces = mesh.faces 21 | print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces") 22 | 23 | server = viser.ViserServer() 24 | server.scene.add_mesh_simple( 25 | name="/simple", 26 | vertices=vertices, 27 | faces=faces, 28 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz, 29 | position=(0.0, 0.0, 0.0), 30 | ) 31 | server.scene.add_mesh_trimesh( 32 | name="/trimesh", 33 | mesh=mesh, 34 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz, 35 | position=(0.0, 5.0, 0.0), 36 | ) 37 | grid = server.scene.add_grid( 38 | "grid", 39 | width=20.0, 40 | height=20.0, 41 | position=np.array([0.0, 0.0, -2.0]), 42 | ) 43 | 44 | while True: 45 | time.sleep(10.0) 46 | -------------------------------------------------------------------------------- /examples/12_click_meshes.py: -------------------------------------------------------------------------------- 1 | """Mesh click events 2 | 3 | Click on meshes to select them. The index of the last clicked mesh is displayed in the GUI. 4 | """ 5 | 6 | import time 7 | 8 | import matplotlib 9 | 10 | import viser 11 | 12 | 13 | def main() -> None: 14 | grid_shape = (4, 5) 15 | server = viser.ViserServer() 16 | 17 | with server.gui.add_folder("Last clicked"): 18 | x_value = server.gui.add_number( 19 | label="x", 20 | initial_value=0, 21 | disabled=True, 22 | hint="x coordinate of the last clicked mesh", 23 | ) 24 | y_value = server.gui.add_number( 25 | label="y", 26 | initial_value=0, 27 | disabled=True, 28 | hint="y coordinate of the last clicked mesh", 29 | ) 30 | 31 | def add_swappable_mesh(i: int, j: int) -> None: 32 | """Simple callback that swaps between: 33 | - a gray box 34 | - a colored box 35 | - a colored sphere 36 | 37 | Color is chosen based on the position (i, j) of the mesh in the grid. 38 | """ 39 | 40 | colormap = matplotlib.colormaps["tab20"] 41 | 42 | def create_mesh(counter: int) -> None: 43 | if counter == 0: 44 | color = (0.8, 0.8, 0.8) 45 | else: 46 | index = (i * grid_shape[1] + j) / (grid_shape[0] * grid_shape[1]) 47 | color = colormap(index)[:3] 48 | 49 | if counter in (0, 1): 50 | handle = server.scene.add_box( 51 | name=f"/sphere_{i}_{j}", 52 | position=(i, j, 0.0), 53 | color=color, 54 | dimensions=(0.5, 0.5, 0.5), 55 | ) 56 | else: 57 | handle = server.scene.add_icosphere( 58 | name=f"/sphere_{i}_{j}", 59 | radius=0.4, 60 | color=color, 61 | position=(i, j, 0.0), 62 | ) 63 | 64 | @handle.on_click 65 | def _(_) -> None: 66 | x_value.value = i 67 | y_value.value = j 68 | 69 | # The new mesh will replace the old one because the names 70 | # /sphere_{i}_{j} are the same. 71 | create_mesh((counter + 1) % 3) 72 | 73 | create_mesh(0) 74 | 75 | for i in range(grid_shape[0]): 76 | for j in range(grid_shape[1]): 77 | add_swappable_mesh(i, j) 78 | 79 | while True: 80 | time.sleep(10.0) 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /examples/14_markdown.py: -------------------------------------------------------------------------------- 1 | """Markdown demonstration 2 | 3 | Viser GUI has MDX 2 support. 4 | """ 5 | 6 | import time 7 | from pathlib import Path 8 | 9 | import viser 10 | 11 | server = viser.ViserServer() 12 | server.scene.world_axes.visible = True 13 | 14 | markdown_counter = server.gui.add_markdown("Counter: 0") 15 | 16 | here = Path(__file__).absolute().parent 17 | 18 | button = server.gui.add_button("Remove blurb") 19 | checkbox = server.gui.add_checkbox("Visibility", initial_value=True) 20 | 21 | markdown_source = (here / "./assets/mdx_example.mdx").read_text() 22 | markdown_blurb = server.gui.add_markdown( 23 | content=markdown_source, 24 | image_root=here, 25 | ) 26 | 27 | 28 | @button.on_click 29 | def _(_): 30 | markdown_blurb.remove() 31 | 32 | 33 | @checkbox.on_update 34 | def _(_): 35 | markdown_blurb.visible = checkbox.value 36 | 37 | 38 | counter = 0 39 | while True: 40 | markdown_counter.content = f"Counter: {counter}" 41 | counter += 1 42 | time.sleep(0.1) 43 | -------------------------------------------------------------------------------- /examples/16_modal.py: -------------------------------------------------------------------------------- 1 | """Modal basics 2 | 3 | Examples of using modals in Viser.""" 4 | 5 | import time 6 | 7 | import viser 8 | 9 | 10 | def main(): 11 | server = viser.ViserServer() 12 | 13 | @server.on_client_connect 14 | def _(client: viser.ClientHandle) -> None: 15 | with client.gui.add_modal("Modal example"): 16 | client.gui.add_markdown( 17 | "**The input below determines the title of the modal...**" 18 | ) 19 | 20 | gui_title = client.gui.add_text( 21 | "Title", 22 | initial_value="My Modal", 23 | ) 24 | 25 | modal_button = client.gui.add_button("Show more modals") 26 | 27 | @modal_button.on_click 28 | def _(_) -> None: 29 | with client.gui.add_modal(gui_title.value) as modal: 30 | client.gui.add_markdown("This is content inside the modal!") 31 | client.gui.add_button("Close").on_click(lambda _: modal.close()) 32 | 33 | while True: 34 | time.sleep(0.15) 35 | 36 | 37 | if __name__ == "__main__": 38 | main() 39 | -------------------------------------------------------------------------------- /examples/17_background_composite.py: -------------------------------------------------------------------------------- 1 | """Depth compositing 2 | 3 | In this example, we show how to use a background image with depth compositing. This can 4 | be useful when we want a 2D image to occlude 3D geometry, such as for NeRF rendering. 5 | """ 6 | 7 | import time 8 | 9 | import numpy as np 10 | import trimesh 11 | import trimesh.creation 12 | 13 | import viser 14 | 15 | server = viser.ViserServer() 16 | 17 | 18 | img = np.random.randint(0, 255, size=(1000, 1000, 3), dtype=np.uint8) 19 | depth = np.ones((1000, 1000, 1), dtype=np.float32) 20 | 21 | # Make a square middle portal. 22 | depth[250:750, 250:750, :] = 10.0 23 | img[250:750, 250:750, :] = 255 24 | 25 | mesh = trimesh.creation.box((0.5, 0.5, 0.5)) 26 | server.scene.add_mesh_trimesh( 27 | name="/cube", 28 | mesh=mesh, 29 | position=(0, 0, 0.0), 30 | ) 31 | server.scene.set_background_image(img, depth=depth) 32 | 33 | 34 | while True: 35 | time.sleep(1.0) 36 | -------------------------------------------------------------------------------- /examples/18_lines.py: -------------------------------------------------------------------------------- 1 | """Lines 2 | 3 | Make a ball with some random line segments and splines. 4 | """ 5 | 6 | import time 7 | 8 | import numpy as np 9 | 10 | import viser 11 | 12 | 13 | def main() -> None: 14 | server = viser.ViserServer() 15 | 16 | # Line segments. 17 | # 18 | # This will be much faster than creating separate scene objects for 19 | # individual line segments or splines. 20 | N = 2000 21 | points = np.random.normal(size=(N, 2, 3)) * 3.0 22 | colors = np.random.randint(0, 255, size=(N, 2, 3)) 23 | server.scene.add_line_segments( 24 | "/line_segments", 25 | points=points, 26 | colors=colors, 27 | line_width=3.0, 28 | ) 29 | 30 | # Spline helpers. 31 | # 32 | # If many lines are needed, it'll be more efficient to batch them in 33 | # `add_line_segments()`. 34 | for i in range(10): 35 | points = np.random.normal(size=(30, 3)) * 3.0 36 | server.scene.add_spline_catmull_rom( 37 | f"/catmull/{i}", 38 | positions=points, 39 | tension=0.5, 40 | line_width=3.0, 41 | color=np.random.uniform(size=3), 42 | segments=100, 43 | ) 44 | 45 | control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0 46 | server.scene.add_spline_cubic_bezier( 47 | f"/cubic_bezier/{i}", 48 | positions=points, 49 | control_points=control_points, 50 | line_width=3.0, 51 | color=np.random.uniform(size=3), 52 | segments=100, 53 | ) 54 | 55 | while True: 56 | time.sleep(10.0) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /examples/19_get_renders.py: -------------------------------------------------------------------------------- 1 | """Get renders 2 | 3 | Example for getting renders from a client's viewport to the Python API.""" 4 | 5 | import time 6 | 7 | import imageio.v3 as iio 8 | import numpy as np 9 | 10 | import viser 11 | 12 | 13 | def main(): 14 | server = viser.ViserServer() 15 | 16 | button = server.gui.add_button("Render a GIF") 17 | 18 | @button.on_click 19 | def _(event: viser.GuiEvent) -> None: 20 | client = event.client 21 | assert client is not None 22 | 23 | client.scene.reset() 24 | 25 | images = [] 26 | 27 | for i in range(20): 28 | positions = np.random.normal(size=(30, 3)) 29 | client.scene.add_spline_catmull_rom( 30 | f"/catmull_{i}", 31 | positions, 32 | tension=0.5, 33 | line_width=3.0, 34 | color=np.random.uniform(size=3), 35 | ) 36 | images.append(client.get_render(height=720, width=1280)) 37 | print("Got image with shape", images[-1].shape) 38 | 39 | print("Generating and sending GIF...") 40 | client.send_file_download( 41 | "image.gif", iio.imwrite("", images, extension=".gif", loop=0) 42 | ) 43 | print("Done!") 44 | 45 | while True: 46 | time.sleep(10.0) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /examples/21_set_up_direction.py: -------------------------------------------------------------------------------- 1 | """Set up direction 2 | 3 | `.set_up_direction()` can help us set the global up direction.""" 4 | 5 | import time 6 | 7 | import viser 8 | 9 | 10 | def main() -> None: 11 | server = viser.ViserServer() 12 | server.scene.world_axes.visible = True 13 | gui_up = server.gui.add_vector3( 14 | "Up Direction", 15 | initial_value=(0.0, 0.0, 1.0), 16 | step=0.01, 17 | ) 18 | 19 | @gui_up.on_update 20 | def _(_) -> None: 21 | server.scene.set_up_direction(gui_up.value) 22 | 23 | while True: 24 | time.sleep(1.0) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /examples/23_plotly.py: -------------------------------------------------------------------------------- 1 | """Plotly 2 | 3 | Examples of visualizing plotly plots in Viser.""" 4 | 5 | import time 6 | 7 | import numpy as np 8 | import plotly.express as px 9 | import plotly.graph_objects as go 10 | from PIL import Image 11 | 12 | import viser 13 | 14 | 15 | def create_sinusoidal_wave(t: float) -> go.Figure: 16 | """Create a sinusoidal wave plot, starting at time t.""" 17 | x_data = np.linspace(t, t + 6 * np.pi, 50) 18 | y_data = np.sin(x_data) * 10 19 | 20 | fig = px.line( 21 | x=list(x_data), 22 | y=list(y_data), 23 | labels={"x": "x", "y": "sin(x)"}, 24 | title="Sinusoidal Wave", 25 | ) 26 | 27 | # this sets the margins to be tight around the title. 28 | fig.layout.title.automargin = True # type: ignore 29 | fig.update_layout( 30 | margin=dict(l=20, r=20, t=20, b=20), 31 | ) # Reduce plot margins. 32 | 33 | return fig 34 | 35 | 36 | def main() -> None: 37 | server = viser.ViserServer() 38 | 39 | # Plot type 1: Line plot. 40 | line_plot_time = 0.0 41 | line_plot = server.gui.add_plotly(figure=create_sinusoidal_wave(line_plot_time)) 42 | 43 | # Plot type 2: Image plot. 44 | fig = px.imshow(Image.open("assets/Cal_logo.png")) 45 | fig.update_layout( 46 | margin=dict(l=20, r=20, t=20, b=20), 47 | ) 48 | server.gui.add_plotly(figure=fig, aspect=1.0) 49 | 50 | # Plot type 3: 3D Scatter plot. 51 | fig = px.scatter_3d( 52 | px.data.iris(), 53 | x="sepal_length", 54 | y="sepal_width", 55 | z="petal_width", 56 | color="species", 57 | ) 58 | fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)) 59 | fig.update_layout( 60 | margin=dict(l=20, r=20, t=20, b=20), 61 | ) 62 | server.gui.add_plotly(figure=fig, aspect=1.0) 63 | 64 | while True: 65 | # Update the line plot. 66 | line_plot_time += 0.1 67 | line_plot.figure = create_sinusoidal_wave(line_plot_time) 68 | 69 | time.sleep(0.01) 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /examples/27_notifications.py: -------------------------------------------------------------------------------- 1 | """Notifications 2 | 3 | Examples of adding notifications per client in Viser.""" 4 | 5 | import time 6 | 7 | import viser 8 | 9 | 10 | def main() -> None: 11 | server = viser.ViserServer() 12 | 13 | persistent_notif_button = server.gui.add_button( 14 | "Show persistent notification (default)" 15 | ) 16 | timed_notif_button = server.gui.add_button("Show timed notification") 17 | controlled_notif_button = server.gui.add_button("Show controlled notification") 18 | loading_notif_button = server.gui.add_button("Show loading notification") 19 | 20 | remove_controlled_notif = server.gui.add_button("Remove controlled notification") 21 | 22 | @persistent_notif_button.on_click 23 | def _(event: viser.GuiEvent) -> None: 24 | """Show persistent notification when the button is clicked.""" 25 | client = event.client 26 | assert client is not None 27 | 28 | client.add_notification( 29 | title="Persistent notification", 30 | body="This can be closed manually and does not disappear on its own!", 31 | loading=False, 32 | with_close_button=True, 33 | auto_close=False, 34 | ) 35 | 36 | @timed_notif_button.on_click 37 | def _(event: viser.GuiEvent) -> None: 38 | """Show timed notification when the button is clicked.""" 39 | client = event.client 40 | assert client is not None 41 | 42 | client.add_notification( 43 | title="Timed notification", 44 | body="This disappears automatically after 5 seconds!", 45 | loading=False, 46 | with_close_button=True, 47 | auto_close=5000, 48 | ) 49 | 50 | @controlled_notif_button.on_click 51 | def _(event: viser.GuiEvent) -> None: 52 | """Show controlled notification when the button is clicked.""" 53 | client = event.client 54 | assert client is not None 55 | 56 | controlled_notif = client.add_notification( 57 | title="Controlled notification", 58 | body="This cannot be closed by the user and is controlled in code only!", 59 | loading=False, 60 | with_close_button=False, 61 | auto_close=False, 62 | ) 63 | 64 | @remove_controlled_notif.on_click 65 | def _(_) -> None: 66 | """Remove controlled notification.""" 67 | controlled_notif.remove() 68 | 69 | @loading_notif_button.on_click 70 | def _(event: viser.GuiEvent) -> None: 71 | """Show loading notification when the button is clicked.""" 72 | client = event.client 73 | assert client is not None 74 | 75 | loading_notif = client.add_notification( 76 | title="Loading notification", 77 | body="This indicates that some action is in progress! It will be updated in 3 seconds.", 78 | loading=True, 79 | with_close_button=False, 80 | auto_close=False, 81 | ) 82 | 83 | time.sleep(3.0) 84 | 85 | loading_notif.title = "Updated notification" 86 | loading_notif.body = "This notification has been updated!" 87 | loading_notif.loading = False 88 | loading_notif.with_close_button = True 89 | loading_notif.auto_close = 5000 90 | loading_notif.color = "green" 91 | 92 | while True: 93 | time.sleep(1.0) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /examples/assets/.gitignore: -------------------------------------------------------------------------------- 1 | dragon.obj 2 | /record3d_dance/ 3 | /colmap_garden/ 4 | -------------------------------------------------------------------------------- /examples/assets/Cal_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/examples/assets/Cal_logo.png -------------------------------------------------------------------------------- /examples/assets/download_colmap_garden.sh: -------------------------------------------------------------------------------- 1 | # This downloads the COLMAP model for the MIP-NeRF garden dataset 2 | # with the images that are downscaled by a factor of 8. 3 | # The full dataset is available at https://jonbarron.info/mipnerf360/. 4 | 5 | set -e -x 6 | 7 | gdown "https://drive.google.com/uc?id=1wYHdrgwXPHtREdCjItvt4gqRQGISMade" 8 | 9 | mkdir -p colmap_garden 10 | # shellcheck disable=SC2035 11 | unzip *.zip && rm *.zip -------------------------------------------------------------------------------- /examples/assets/download_dragon_mesh.sh: -------------------------------------------------------------------------------- 1 | set -e -x 2 | 3 | gdown "https://drive.google.com/uc?id=1uRDvoS_l2Or8g8YDDPYV79K6_RfFYBeF" 4 | -------------------------------------------------------------------------------- /examples/assets/download_record3d_dance.sh: -------------------------------------------------------------------------------- 1 | set -e -x 2 | 3 | gdown "https://drive.google.com/uc?id=1_vd5bK_MhtlfisA6BkK1IgiJNfDbIntq" 4 | 5 | mkdir -p record3d_dance 6 | # shellcheck disable=SC2035 7 | unzip *.r3d -d record3d_dance && rm *.r3d 8 | -------------------------------------------------------------------------------- /examples/assets/mdx_example.mdx: -------------------------------------------------------------------------------- 1 | ## Markdown in Viser 2 | 3 | --- 4 | 5 | Viser has full support for the GFM markdown spec, including **bold**, _italics_, 6 | ~~strikethrough~~, and many other features. 7 | 8 | Here's a [masked link](https://github.com/nerfstudio-project/viser). Not a fan? 9 | Here's a normal one: https://pypi.org/project/viser/ 10 | 11 | Anywhere where you can insert GUI elements, you can also insert `images`, 12 | `blockquotes`, `lists`, `tables`, `task lists`, and `(unstyled) code blocks`. 13 | 14 | In inline code blocks, you can show off colors with color chips: `#FED363` 15 | `hsl(0, 0%, 82%)` `rgb(255, 255, 255)` 16 | 17 | Adding images from a remote origin is simple. 18 | 19 | ![Viser Logo](https://viser.studio/main/_static/logo.svg) 20 | 21 | For local images with relative paths, you can either directly use a 22 | [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) 23 | or set the `image_root` argument to the `Path` object that you'd like your paths 24 | relative to. If no such `image_root` is provided, the file system will be scoped 25 | to the directory that Viser is installed in. 26 | 27 | ![Cal Logo](../examples/assets/Cal_logo.png) 28 | 29 | Tables follow the standard markdown spec: 30 | 31 | | Application | Description | 32 | | ---------------------------------------------------- | -------------------------------------------------- | 33 | | [NS](https://nerf.studio) | A collaboration friendly studio for NeRFs | 34 | | [Viser](https://nerfstudio-project.github.io/viser/) | An interactive 3D visualization toolbox for Python | 35 | 36 | Code blocks, while being not nearly as exciting as some of the things presented, 37 | work as expected. Currently, while you can specify a language and metadata in 38 | your blocks, they will remain unused by the Markdown renderer. 39 | 40 | ```python 41 | """Markdown Demonstration 42 | 43 | Viser GUI has MDX 2 support. 44 | """ 45 | 46 | import time 47 | from pathlib import Path 48 | 49 | import viser 50 | 51 | server = viser.ViserServer() 52 | server.world_axes.visible = True 53 | 54 | 55 | @server.on_client_connect 56 | def _(client: viser.ClientHandle) -> None: 57 | with open("./assets/mdx_example.mdx", "r") as mkdn: 58 | markdown = client.gui.add_markdown( 59 | markdown=mkdn.read(), image_root=Path(__file__).parent 60 | ) 61 | 62 | button = client.gui.add_button("Remove Markdown") 63 | 64 | @button.on_click 65 | def _(_): 66 | markdown.remove() 67 | 68 | 69 | while True: 70 | time.sleep(10.0) 71 | ``` 72 | 73 | As a bonus, MDX is extensible and JS capable. This means that you have the 74 | freedom to do things like: 75 | 76 | This page loaded on {(new Date()).toString()} 77 | 78 | Or: 79 | 80 | > Oh yes, mdx PR would be exciting 81 | > 82 | > — Brent Yi 83 | 84 | **Note**: Be careful when playing with JSX, it's very easy to break markdown. 85 | 86 | So that's MDX in Viser. It has support for: 87 | 88 | - [x] CommonMark and GFM standards 89 | - bold, italics, strikethrough, images, blockquotes, tables, task lists, code 90 | blocks, inline code 91 | - [x] Color chips 92 | - [x] JSX enhanced components 93 | -------------------------------------------------------------------------------- /examples_dev/00_coordinate_frames.py: -------------------------------------------------------------------------------- 1 | """Coordinate frames 2 | 3 | In this basic example, we visualize a set of coordinate frames. 4 | 5 | Naming for all scene nodes are hierarchical; /tree/branch, for example, is defined 6 | relative to /tree. 7 | """ 8 | 9 | import random 10 | import time 11 | 12 | import viser 13 | 14 | server = viser.ViserServer() 15 | 16 | while True: 17 | # Add some coordinate frames to the scene. These will be visualized in the viewer. 18 | server.scene.add_frame( 19 | "/tree", 20 | wxyz=(1.0, 0.0, 0.0, 0.0), 21 | position=(random.random() * 2.0, 2.0, 0.2), 22 | ) 23 | server.scene.add_frame( 24 | "/tree/branch", 25 | wxyz=(1.0, 0.0, 0.0, 0.0), 26 | position=(random.random() * 2.0, 2.0, 0.2), 27 | ) 28 | leaf = server.scene.add_frame( 29 | "/tree/branch/leaf", 30 | wxyz=(1.0, 0.0, 0.0, 0.0), 31 | position=(random.random() * 2.0, 2.0, 0.2), 32 | ) 33 | time.sleep(5.0) 34 | 35 | # Remove the leaf node from the scene. 36 | leaf.remove() 37 | time.sleep(0.5) 38 | -------------------------------------------------------------------------------- /examples_dev/01_image.py: -------------------------------------------------------------------------------- 1 | """Images 2 | 3 | Example for sending images to the viewer. 4 | 5 | We can send backgrond images to display behind the viewer (useful for visualizing 6 | NeRFs), or images to render as 3D textures. 7 | """ 8 | 9 | import time 10 | from pathlib import Path 11 | 12 | import imageio.v3 as iio 13 | import numpy as np 14 | 15 | import viser 16 | 17 | 18 | def main() -> None: 19 | server = viser.ViserServer() 20 | 21 | # Add a background image. 22 | server.scene.set_background_image( 23 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), 24 | format="png", 25 | ) 26 | 27 | # Add main image. 28 | server.scene.add_image( 29 | "/img", 30 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), 31 | 4.0, 32 | 4.0, 33 | format="png", 34 | wxyz=(1.0, 0.0, 0.0, 0.0), 35 | position=(2.0, 2.0, 0.0), 36 | ) 37 | while True: 38 | server.scene.add_image( 39 | "/noise", 40 | np.random.randint(0, 256, size=(400, 400, 3), dtype=np.uint8), 41 | 4.0, 42 | 4.0, 43 | format="jpeg", 44 | wxyz=(1.0, 0.0, 0.0, 0.0), 45 | position=(2.0, 2.0, -1e-2), 46 | ) 47 | time.sleep(0.2) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /examples_dev/04_camera_poses.py: -------------------------------------------------------------------------------- 1 | """Camera poses 2 | 3 | Example showing how we can detect new clients and read camera poses from them. 4 | """ 5 | 6 | import time 7 | 8 | import viser 9 | 10 | server = viser.ViserServer() 11 | server.scene.world_axes.visible = True 12 | 13 | 14 | @server.on_client_connect 15 | def _(client: viser.ClientHandle) -> None: 16 | print("new client!") 17 | 18 | # This will run whenever we get a new camera! 19 | @client.camera.on_update 20 | def _(_: viser.CameraHandle) -> None: 21 | print(f"New camera on client {client.client_id}!") 22 | 23 | # Show the client ID in the GUI. 24 | gui_info = client.gui.add_text("Client ID", initial_value=str(client.client_id)) 25 | gui_info.disabled = True 26 | 27 | 28 | while True: 29 | # Get all currently connected clients. 30 | clients = server.get_clients() 31 | print("Connected client IDs", clients.keys()) 32 | 33 | for id, client in clients.items(): 34 | print(f"Camera pose for client {id}") 35 | print(f"\twxyz: {client.camera.wxyz}") 36 | print(f"\tposition: {client.camera.position}") 37 | print(f"\tfov: {client.camera.fov}") 38 | print(f"\taspect: {client.camera.aspect}") 39 | print(f"\tlast update: {client.camera.update_timestamp}") 40 | print( 41 | f"\tcanvas size: {client.camera.image_width}x{client.camera.image_height}" 42 | ) 43 | 44 | time.sleep(2.0) 45 | -------------------------------------------------------------------------------- /examples_dev/05_camera_commands.py: -------------------------------------------------------------------------------- 1 | """Camera commands 2 | 3 | In addition to reads, camera parameters also support writes. These are synced to the 4 | corresponding client automatically. 5 | """ 6 | 7 | import time 8 | 9 | import numpy as np 10 | 11 | import viser 12 | import viser.transforms as tf 13 | 14 | server = viser.ViserServer() 15 | num_frames = 20 16 | 17 | 18 | @server.on_client_connect 19 | def _(client: viser.ClientHandle) -> None: 20 | """For each client that connects, create GUI elements for adjusting the 21 | near/far clipping planes.""" 22 | 23 | client.camera.far = 10.0 24 | 25 | near_slider = client.gui.add_slider( 26 | "Near", min=0.01, max=10.0, step=0.001, initial_value=client.camera.near 27 | ) 28 | far_slider = client.gui.add_slider( 29 | "Far", min=1, max=20.0, step=0.001, initial_value=client.camera.far 30 | ) 31 | 32 | @near_slider.on_update 33 | def _(_) -> None: 34 | client.camera.near = near_slider.value 35 | 36 | @far_slider.on_update 37 | def _(_) -> None: 38 | client.camera.far = far_slider.value 39 | 40 | 41 | @server.on_client_connect 42 | def _(client: viser.ClientHandle) -> None: 43 | """For each client that connects, we create a set of random frames + a click handler for each frame. 44 | 45 | When a frame is clicked, we move the camera to the corresponding frame. 46 | """ 47 | 48 | rng = np.random.default_rng(0) 49 | 50 | def make_frame(i: int) -> None: 51 | # Sample a random orientation + position. 52 | wxyz = rng.normal(size=4) 53 | wxyz /= np.linalg.norm(wxyz) 54 | position = rng.uniform(-3.0, 3.0, size=(3,)) 55 | 56 | # Create a coordinate frame and label. 57 | frame = client.scene.add_frame(f"/frame_{i}", wxyz=wxyz, position=position) 58 | client.scene.add_label(f"/frame_{i}/label", text=f"Frame {i}") 59 | 60 | # Move the camera when we click a frame. 61 | @frame.on_click 62 | def _(_): 63 | T_world_current = tf.SE3.from_rotation_and_translation( 64 | tf.SO3(client.camera.wxyz), client.camera.position 65 | ) 66 | T_world_target = tf.SE3.from_rotation_and_translation( 67 | tf.SO3(frame.wxyz), frame.position 68 | ) @ tf.SE3.from_translation(np.array([0.0, 0.0, -0.5])) 69 | 70 | T_current_target = T_world_current.inverse() @ T_world_target 71 | 72 | for j in range(20): 73 | T_world_set = T_world_current @ tf.SE3.exp( 74 | T_current_target.log() * j / 19.0 75 | ) 76 | 77 | # We can atomically set the orientation and the position of the camera 78 | # together to prevent jitter that might happen if one was set before the 79 | # other. 80 | with client.atomic(): 81 | client.camera.wxyz = T_world_set.rotation().wxyz 82 | client.camera.position = T_world_set.translation() 83 | 84 | client.flush() # Optional! 85 | time.sleep(1.0 / 60.0) 86 | 87 | # Mouse interactions should orbit around the frame origin. 88 | client.camera.look_at = frame.position 89 | 90 | for i in range(num_frames): 91 | make_frame(i) 92 | 93 | 94 | while True: 95 | time.sleep(1.0) 96 | -------------------------------------------------------------------------------- /examples_dev/06_mesh.py: -------------------------------------------------------------------------------- 1 | """Meshes 2 | 3 | Visualize a mesh. To get the demo data, see `./assets/download_dragon_mesh.sh`. 4 | """ 5 | 6 | import time 7 | from pathlib import Path 8 | 9 | import numpy as np 10 | import trimesh 11 | 12 | import viser 13 | import viser.transforms as tf 14 | 15 | mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj")) 16 | assert isinstance(mesh, trimesh.Trimesh) 17 | mesh.apply_scale(0.05) 18 | 19 | vertices = mesh.vertices 20 | faces = mesh.faces 21 | print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces") 22 | 23 | server = viser.ViserServer() 24 | server.scene.add_mesh_simple( 25 | name="/simple", 26 | vertices=vertices, 27 | faces=faces, 28 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz, 29 | position=(0.0, 0.0, 0.0), 30 | ) 31 | server.scene.add_mesh_trimesh( 32 | name="/trimesh", 33 | mesh=mesh, 34 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz, 35 | position=(0.0, 5.0, 0.0), 36 | ) 37 | grid = server.scene.add_grid( 38 | "grid", 39 | width=20.0, 40 | height=20.0, 41 | position=np.array([0.0, 0.0, -2.0]), 42 | ) 43 | 44 | while True: 45 | time.sleep(10.0) 46 | -------------------------------------------------------------------------------- /examples_dev/12_click_meshes.py: -------------------------------------------------------------------------------- 1 | """Mesh click events 2 | 3 | Click on meshes to select them. The index of the last clicked mesh is displayed in the GUI. 4 | """ 5 | 6 | import time 7 | 8 | import matplotlib 9 | 10 | import viser 11 | 12 | 13 | def main() -> None: 14 | grid_shape = (4, 5) 15 | server = viser.ViserServer() 16 | 17 | with server.gui.add_folder("Last clicked"): 18 | x_value = server.gui.add_number( 19 | label="x", 20 | initial_value=0, 21 | disabled=True, 22 | hint="x coordinate of the last clicked mesh", 23 | ) 24 | y_value = server.gui.add_number( 25 | label="y", 26 | initial_value=0, 27 | disabled=True, 28 | hint="y coordinate of the last clicked mesh", 29 | ) 30 | 31 | def add_swappable_mesh(i: int, j: int) -> None: 32 | """Simple callback that swaps between: 33 | - a gray box 34 | - a colored box 35 | - a colored sphere 36 | 37 | Color is chosen based on the position (i, j) of the mesh in the grid. 38 | """ 39 | 40 | colormap = matplotlib.colormaps["tab20"] 41 | 42 | def create_mesh(counter: int) -> None: 43 | if counter == 0: 44 | color = (0.8, 0.8, 0.8) 45 | else: 46 | index = (i * grid_shape[1] + j) / (grid_shape[0] * grid_shape[1]) 47 | color = colormap(index)[:3] 48 | 49 | if counter in (0, 1): 50 | handle = server.scene.add_box( 51 | name=f"/sphere_{i}_{j}", 52 | position=(i, j, 0.0), 53 | color=color, 54 | dimensions=(0.5, 0.5, 0.5), 55 | ) 56 | else: 57 | handle = server.scene.add_icosphere( 58 | name=f"/sphere_{i}_{j}", 59 | radius=0.4, 60 | color=color, 61 | position=(i, j, 0.0), 62 | ) 63 | 64 | @handle.on_click 65 | def _(_) -> None: 66 | x_value.value = i 67 | y_value.value = j 68 | 69 | # The new mesh will replace the old one because the names 70 | # /sphere_{i}_{j} are the same. 71 | create_mesh((counter + 1) % 3) 72 | 73 | create_mesh(0) 74 | 75 | for i in range(grid_shape[0]): 76 | for j in range(grid_shape[1]): 77 | add_swappable_mesh(i, j) 78 | 79 | while True: 80 | time.sleep(10.0) 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /examples_dev/14_markdown.py: -------------------------------------------------------------------------------- 1 | """Markdown demonstration 2 | 3 | Viser GUI has MDX 2 support. 4 | """ 5 | 6 | import time 7 | from pathlib import Path 8 | 9 | import viser 10 | 11 | server = viser.ViserServer() 12 | server.scene.world_axes.visible = True 13 | 14 | markdown_counter = server.gui.add_markdown("Counter: 0") 15 | 16 | here = Path(__file__).absolute().parent 17 | 18 | button = server.gui.add_button("Remove blurb") 19 | checkbox = server.gui.add_checkbox("Visibility", initial_value=True) 20 | 21 | markdown_source = (here / "./assets/mdx_example.mdx").read_text() 22 | markdown_blurb = server.gui.add_markdown( 23 | content=markdown_source, 24 | image_root=here, 25 | ) 26 | 27 | 28 | @button.on_click 29 | def _(_): 30 | markdown_blurb.remove() 31 | 32 | 33 | @checkbox.on_update 34 | def _(_): 35 | markdown_blurb.visible = checkbox.value 36 | 37 | 38 | counter = 0 39 | while True: 40 | markdown_counter.content = f"Counter: {counter}" 41 | counter += 1 42 | time.sleep(0.1) 43 | -------------------------------------------------------------------------------- /examples_dev/16_modal.py: -------------------------------------------------------------------------------- 1 | """Modal basics 2 | 3 | Examples of using modals in Viser.""" 4 | 5 | import time 6 | 7 | import viser 8 | 9 | 10 | def main(): 11 | server = viser.ViserServer() 12 | 13 | @server.on_client_connect 14 | def _(client: viser.ClientHandle) -> None: 15 | with client.gui.add_modal("Modal example"): 16 | client.gui.add_markdown( 17 | "**The input below determines the title of the modal...**" 18 | ) 19 | 20 | gui_title = client.gui.add_text( 21 | "Title", 22 | initial_value="My Modal", 23 | ) 24 | 25 | modal_button = client.gui.add_button("Show more modals") 26 | 27 | @modal_button.on_click 28 | def _(_) -> None: 29 | with client.gui.add_modal(gui_title.value) as modal: 30 | client.gui.add_markdown("This is content inside the modal!") 31 | client.gui.add_button("Close").on_click(lambda _: modal.close()) 32 | 33 | while True: 34 | time.sleep(0.15) 35 | 36 | 37 | if __name__ == "__main__": 38 | main() 39 | -------------------------------------------------------------------------------- /examples_dev/17_background_composite.py: -------------------------------------------------------------------------------- 1 | """Depth compositing 2 | 3 | In this example, we show how to use a background image with depth compositing. This can 4 | be useful when we want a 2D image to occlude 3D geometry, such as for NeRF rendering. 5 | """ 6 | 7 | import time 8 | 9 | import numpy as np 10 | import trimesh 11 | import trimesh.creation 12 | 13 | import viser 14 | 15 | server = viser.ViserServer() 16 | 17 | 18 | img = np.random.randint(0, 255, size=(1000, 1000, 3), dtype=np.uint8) 19 | depth = np.ones((1000, 1000, 1), dtype=np.float32) 20 | 21 | # Make a square middle portal. 22 | depth[250:750, 250:750, :] = 10.0 23 | img[250:750, 250:750, :] = 255 24 | 25 | mesh = trimesh.creation.box((0.5, 0.5, 0.5)) 26 | server.scene.add_mesh_trimesh( 27 | name="/cube", 28 | mesh=mesh, 29 | position=(0, 0, 0.0), 30 | ) 31 | server.scene.set_background_image(img, depth=depth) 32 | 33 | 34 | while True: 35 | time.sleep(1.0) 36 | -------------------------------------------------------------------------------- /examples_dev/18_lines.py: -------------------------------------------------------------------------------- 1 | """Lines 2 | 3 | Make a ball with some random line segments and splines. 4 | """ 5 | 6 | import time 7 | 8 | import numpy as np 9 | 10 | import viser 11 | 12 | 13 | def main() -> None: 14 | server = viser.ViserServer() 15 | 16 | # Line segments. 17 | # 18 | # This will be much faster than creating separate scene objects for 19 | # individual line segments or splines. 20 | N = 2000 21 | points = np.random.normal(size=(N, 2, 3)) * 3.0 22 | colors = np.random.randint(0, 255, size=(N, 2, 3)) 23 | server.scene.add_line_segments( 24 | "/line_segments", 25 | points=points, 26 | colors=colors, 27 | line_width=3.0, 28 | ) 29 | 30 | # Spline helpers. 31 | # 32 | # If many lines are needed, it'll be more efficient to batch them in 33 | # `add_line_segments()`. 34 | for i in range(10): 35 | points = np.random.normal(size=(30, 3)) * 3.0 36 | server.scene.add_spline_catmull_rom( 37 | f"/catmull/{i}", 38 | positions=points, 39 | tension=0.5, 40 | line_width=3.0, 41 | color=np.random.uniform(size=3), 42 | segments=100, 43 | ) 44 | 45 | control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0 46 | server.scene.add_spline_cubic_bezier( 47 | f"/cubic_bezier/{i}", 48 | positions=points, 49 | control_points=control_points, 50 | line_width=3.0, 51 | color=np.random.uniform(size=3), 52 | segments=100, 53 | ) 54 | 55 | while True: 56 | time.sleep(10.0) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /examples_dev/19_get_renders.py: -------------------------------------------------------------------------------- 1 | """Get renders 2 | 3 | Example for getting renders from a client's viewport to the Python API.""" 4 | 5 | import time 6 | 7 | import imageio.v3 as iio 8 | import numpy as np 9 | 10 | import viser 11 | 12 | 13 | def main(): 14 | server = viser.ViserServer() 15 | 16 | button = server.gui.add_button("Render a GIF") 17 | 18 | @button.on_click 19 | def _(event: viser.GuiEvent) -> None: 20 | client = event.client 21 | assert client is not None 22 | 23 | client.scene.reset() 24 | 25 | images = [] 26 | 27 | for i in range(20): 28 | positions = np.random.normal(size=(30, 3)) 29 | client.scene.add_spline_catmull_rom( 30 | f"/catmull_{i}", 31 | positions, 32 | tension=0.5, 33 | line_width=3.0, 34 | color=np.random.uniform(size=3), 35 | ) 36 | images.append(client.get_render(height=720, width=1280)) 37 | print("Got image with shape", images[-1].shape) 38 | 39 | print("Generating and sending GIF...") 40 | client.send_file_download( 41 | "image.gif", iio.imwrite("", images, extension=".gif", loop=0) 42 | ) 43 | print("Done!") 44 | 45 | while True: 46 | time.sleep(10.0) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /examples_dev/21_set_up_direction.py: -------------------------------------------------------------------------------- 1 | """Set up direction 2 | 3 | `.set_up_direction()` can help us set the global up direction.""" 4 | 5 | import time 6 | 7 | import viser 8 | 9 | 10 | def main() -> None: 11 | server = viser.ViserServer() 12 | server.scene.world_axes.visible = True 13 | gui_up = server.gui.add_vector3( 14 | "Up Direction", 15 | initial_value=(0.0, 0.0, 1.0), 16 | step=0.01, 17 | ) 18 | 19 | @gui_up.on_update 20 | def _(_) -> None: 21 | server.scene.set_up_direction(gui_up.value) 22 | 23 | while True: 24 | time.sleep(1.0) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /examples_dev/23_plotly.py: -------------------------------------------------------------------------------- 1 | """Plotly 2 | 3 | Examples of visualizing plotly plots in Viser.""" 4 | 5 | import time 6 | 7 | import numpy as np 8 | import plotly.express as px 9 | import plotly.graph_objects as go 10 | from PIL import Image 11 | 12 | import viser 13 | 14 | 15 | def create_sinusoidal_wave(t: float) -> go.Figure: 16 | """Create a sinusoidal wave plot, starting at time t.""" 17 | x_data = np.linspace(t, t + 6 * np.pi, 50) 18 | y_data = np.sin(x_data) * 10 19 | 20 | fig = px.line( 21 | x=list(x_data), 22 | y=list(y_data), 23 | labels={"x": "x", "y": "sin(x)"}, 24 | title="Sinusoidal Wave", 25 | ) 26 | 27 | # this sets the margins to be tight around the title. 28 | fig.layout.title.automargin = True # type: ignore 29 | fig.update_layout( 30 | margin=dict(l=20, r=20, t=20, b=20), 31 | ) # Reduce plot margins. 32 | 33 | return fig 34 | 35 | 36 | def main() -> None: 37 | server = viser.ViserServer() 38 | 39 | # Plot type 1: Line plot. 40 | line_plot_time = 0.0 41 | line_plot = server.gui.add_plotly(figure=create_sinusoidal_wave(line_plot_time)) 42 | 43 | # Plot type 2: Image plot. 44 | fig = px.imshow(Image.open("assets/Cal_logo.png")) 45 | fig.update_layout( 46 | margin=dict(l=20, r=20, t=20, b=20), 47 | ) 48 | server.gui.add_plotly(figure=fig, aspect=1.0) 49 | 50 | # Plot type 3: 3D Scatter plot. 51 | fig = px.scatter_3d( 52 | px.data.iris(), 53 | x="sepal_length", 54 | y="sepal_width", 55 | z="petal_width", 56 | color="species", 57 | ) 58 | fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)) 59 | fig.update_layout( 60 | margin=dict(l=20, r=20, t=20, b=20), 61 | ) 62 | server.gui.add_plotly(figure=fig, aspect=1.0) 63 | 64 | while True: 65 | # Update the line plot. 66 | line_plot_time += 0.1 67 | line_plot.figure = create_sinusoidal_wave(line_plot_time) 68 | 69 | time.sleep(0.01) 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /examples_dev/27_notifications.py: -------------------------------------------------------------------------------- 1 | """Notifications 2 | 3 | Examples of adding notifications per client in Viser.""" 4 | 5 | import time 6 | 7 | import viser 8 | 9 | 10 | def main() -> None: 11 | server = viser.ViserServer() 12 | 13 | persistent_notif_button = server.gui.add_button( 14 | "Show persistent notification (default)" 15 | ) 16 | timed_notif_button = server.gui.add_button("Show timed notification") 17 | controlled_notif_button = server.gui.add_button("Show controlled notification") 18 | loading_notif_button = server.gui.add_button("Show loading notification") 19 | 20 | remove_controlled_notif = server.gui.add_button("Remove controlled notification") 21 | 22 | @persistent_notif_button.on_click 23 | def _(event: viser.GuiEvent) -> None: 24 | """Show persistent notification when the button is clicked.""" 25 | client = event.client 26 | assert client is not None 27 | 28 | client.add_notification( 29 | title="Persistent notification", 30 | body="This can be closed manually and does not disappear on its own!", 31 | loading=False, 32 | with_close_button=True, 33 | auto_close=False, 34 | ) 35 | 36 | @timed_notif_button.on_click 37 | def _(event: viser.GuiEvent) -> None: 38 | """Show timed notification when the button is clicked.""" 39 | client = event.client 40 | assert client is not None 41 | 42 | client.add_notification( 43 | title="Timed notification", 44 | body="This disappears automatically after 5 seconds!", 45 | loading=False, 46 | with_close_button=True, 47 | auto_close=5000, 48 | ) 49 | 50 | @controlled_notif_button.on_click 51 | def _(event: viser.GuiEvent) -> None: 52 | """Show controlled notification when the button is clicked.""" 53 | client = event.client 54 | assert client is not None 55 | 56 | controlled_notif = client.add_notification( 57 | title="Controlled notification", 58 | body="This cannot be closed by the user and is controlled in code only!", 59 | loading=False, 60 | with_close_button=False, 61 | auto_close=False, 62 | ) 63 | 64 | @remove_controlled_notif.on_click 65 | def _(_) -> None: 66 | """Remove controlled notification.""" 67 | controlled_notif.remove() 68 | 69 | @loading_notif_button.on_click 70 | def _(event: viser.GuiEvent) -> None: 71 | """Show loading notification when the button is clicked.""" 72 | client = event.client 73 | assert client is not None 74 | 75 | loading_notif = client.add_notification( 76 | title="Loading notification", 77 | body="This indicates that some action is in progress! It will be updated in 3 seconds.", 78 | loading=True, 79 | with_close_button=False, 80 | auto_close=False, 81 | ) 82 | 83 | time.sleep(3.0) 84 | 85 | loading_notif.title = "Updated notification" 86 | loading_notif.body = "This notification has been updated!" 87 | loading_notif.loading = False 88 | loading_notif.with_close_button = True 89 | loading_notif.auto_close = 5000 90 | loading_notif.color = "green" 91 | 92 | while True: 93 | time.sleep(1.0) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /examples_dev/README.md: -------------------------------------------------------------------------------- 1 | # Examples (development) 2 | 3 | This is the development version of examples for `viser`. This should be compatible with: 4 | 5 | - The latest development version of `viser` on GitHub. 6 | 7 | But not necessarily: 8 | 9 | - The latest release of `viser` on PyPI. 10 | -------------------------------------------------------------------------------- /src/viser/_icons.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | from functools import lru_cache 3 | from pathlib import Path 4 | 5 | from ._icons_enum import IconName 6 | 7 | ICONS_DIR = Path(__file__).absolute().parent / "_icons" 8 | 9 | 10 | @lru_cache(maxsize=32) 11 | def svg_from_icon(icon_name: IconName) -> str: 12 | """Read an icon and return it as a UTF string; we expect this to be an 13 | tag.""" 14 | assert isinstance(icon_name, str) 15 | icons_zipfile = ICONS_DIR / "tabler-icons.zip" 16 | 17 | with zipfile.ZipFile(icons_zipfile) as zip_file: 18 | with zip_file.open(f"{icon_name}.svg") as icon_file: 19 | out = icon_file.read() 20 | 21 | return out.decode("utf-8") 22 | -------------------------------------------------------------------------------- /src/viser/_icons/tabler-icons.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/_icons/tabler-icons.zip -------------------------------------------------------------------------------- /src/viser/_notification_handle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dataclasses 4 | from typing import Any, Literal 5 | 6 | from typing_extensions import override 7 | 8 | from viser._assignable_props_api import AssignablePropsBase 9 | 10 | from ._messages import NotificationMessage, NotificationProps, RemoveNotificationMessage 11 | from .infra._infra import WebsockClientConnection 12 | 13 | 14 | @dataclasses.dataclass 15 | class _NotificationHandleState: 16 | websock_interface: WebsockClientConnection 17 | uuid: str 18 | props: NotificationProps 19 | 20 | 21 | class NotificationHandle( 22 | NotificationProps, AssignablePropsBase[_NotificationHandleState] 23 | ): 24 | """Handle for a notification in our visualizer.""" 25 | 26 | def __init__(self, impl: _NotificationHandleState) -> None: 27 | self._impl = impl 28 | 29 | @override 30 | def _queue_update(self, name: str, value: Any) -> None: 31 | """Queue an update message with the property change.""" 32 | # For notifications, we'll just send the whole props object when a 33 | # property is reassigned. Deduplication in the message buffer will 34 | # debounce this when multiple properties are updated in succession. 35 | del name, value 36 | self._sync_with_client("update") 37 | 38 | def _sync_with_client(self, mode: Literal["show", "update"]) -> None: 39 | msg = NotificationMessage(mode, self._impl.uuid, self._impl.props) 40 | self._impl.websock_interface.queue_message(msg) 41 | 42 | def remove(self) -> None: 43 | self._impl.websock_interface.get_message_buffer().remove_from_buffer( 44 | # Don't send outdated GUI updates to new clients. 45 | # This is brittle... 46 | lambda message: getattr(message, "uuid", None) == self._impl.uuid 47 | ) 48 | msg = RemoveNotificationMessage(self._impl.uuid) 49 | self._impl.websock_interface.queue_message(msg) 50 | -------------------------------------------------------------------------------- /src/viser/_threadpool_exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import traceback 5 | from concurrent.futures import Future 6 | from typing import Any 7 | 8 | 9 | def print_threadpool_errors(future: Future[Any]) -> None: 10 | """Print errors from a Future in a ThreadPool, should be used with 11 | `add_done_callback`.""" 12 | if future.cancelled(): 13 | print("Task was cancelled", file=sys.stderr) 14 | return 15 | 16 | exc = future.exception() 17 | if exc is not None: 18 | print("Task failed with exception:", file=sys.stderr) 19 | traceback.print_exception(type(exc), exc, exc.__traceback__) 20 | -------------------------------------------------------------------------------- /src/viser/client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | settings: { 3 | react: { 4 | version: "detect", // React version. "detect" automatically picks the version you have installed. 5 | // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. 6 | // It will default to "latest" and warn if missing, and to "detect" in the future 7 | }, 8 | }, 9 | env: { 10 | browser: true, 11 | es2021: true, 12 | }, 13 | extends: [ 14 | "eslint:recommended", 15 | "plugin:react/recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | ], 18 | overrides: [], 19 | parser: "@typescript-eslint/parser", 20 | parserOptions: { 21 | ecmaVersion: "latest", 22 | sourceType: "module", 23 | }, 24 | plugins: ["react", "@typescript-eslint", "react-refresh"], 25 | ignorePatterns: ["build/", ".eslintrc.js", "src/csm"], 26 | rules: { 27 | // https://github.com/jsx-eslint/eslint-plugin-react/issues/3423 28 | "react/no-unknown-property": "off", 29 | "no-constant-condition": "off", 30 | // Suppress errors for missing 'import React' in files. 31 | "react/react-in-jsx-scope": "off", 32 | "@typescript-eslint/ban-ts-comment": "off", 33 | "@typescript-eslint/no-explicit-any": "off", 34 | "@typescript-eslint/no-non-null-assertion": "off", 35 | "react/prop-types": [ 36 | "error", 37 | { 38 | skipUndeclared: true, 39 | }, 40 | ], 41 | "react-refresh/only-export-components": "warn", 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/viser/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /src/viser/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | Viser 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/viser/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viser", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@mantine/core": "^8.0.0", 7 | "@mantine/form": "^8.0.0", 8 | "@mantine/hooks": "^8.0.0", 9 | "@mantine/notifications": "^8.0.0", 10 | "@mantine/vanilla-extract": "^8.0.0", 11 | "@mdx-js/mdx": "^3.0.1", 12 | "@mdx-js/react": "^3.0.1", 13 | "@msgpack/msgpack": "^3.0.0-beta2", 14 | "@react-three/drei": "^10.0.5", 15 | "@react-three/fiber": "9.1.0", 16 | "@tabler/icons-react": "^3.1.0", 17 | "@three.ez/instanced-mesh": "0.3.4", 18 | "@types/node": "^20.11.30", 19 | "@types/react": "^18.0.33", 20 | "@types/react-dom": "^18.0.11", 21 | "@types/three": "^0.174.0", 22 | "@vanilla-extract/css": "^1.14.1", 23 | "@vitejs/plugin-react": "^4.0.1", 24 | "acorn": "^8.0.0", 25 | "await-lock": "^2.2.2", 26 | "browserslist": "^4.24.4", 27 | "colortranslator": "^4.1.0", 28 | "detect-browser": "^5.3.0", 29 | "fflate": "^0.8.2", 30 | "hold-event": "^1.1.0", 31 | "immer": "^10.0.4", 32 | "its-fine": "^2.0.0", 33 | "postcss": "^8.4.38", 34 | "react": "^19.0.0", 35 | "react-dom": "^19.0.0", 36 | "react-error-boundary": "^4.0.10", 37 | "react-intersection-observer": "^9.13.1", 38 | "react-qr-code": "^2.0.12", 39 | "rehype-color-chips": "^0.1.3", 40 | "remark-gfm": "^4.0.0", 41 | "three": "^0.174.0", 42 | "uuid": "^11.0.5", 43 | "vite": "^5.2.6", 44 | "vite-plugin-svgr": "^4.2.0", 45 | "vite-tsconfig-paths": "^4.2.0", 46 | "zustand": "^4.3.7" 47 | }, 48 | "scripts": { 49 | "start": "vite --host", 50 | "build": "tsc && vite build", 51 | "serve": "vite preview" 52 | }, 53 | "eslintConfig": { 54 | "extends": [ 55 | "react-app" 56 | ] 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | "last 2 chrome versions", 61 | "last 2 firefox versions", 62 | "last 2 safari versions" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | }, 70 | "devDependencies": { 71 | "@types/uuid": "^9.0.8", 72 | "@types/wicg-file-system-access": "^2023.10.5", 73 | "@typescript-eslint/eslint-plugin": "^7.4.0", 74 | "@typescript-eslint/parser": "^7.4.0", 75 | "@vanilla-extract/vite-plugin": "^4.0.6", 76 | "browserslist-to-esbuild": "^2.1.1", 77 | "eslint": "^8.43.0", 78 | "eslint-plugin-react": "^7.32.2", 79 | "eslint-plugin-react-refresh": "^0.4.1", 80 | "typescript": "^5.0.4", 81 | "vite-plugin-eslint": "^1.8.1" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/viser/client/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: {}, 3 | }; 4 | -------------------------------------------------------------------------------- /src/viser/client/public/Inter-VariableFont_slnt,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/Inter-VariableFont_slnt,wght.ttf -------------------------------------------------------------------------------- /src/viser/client/public/hdri/dikhololo_night_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/dikhololo_night_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/empty_warehouse_01_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/empty_warehouse_01_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/forest_slope_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/forest_slope_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/kiara_1_dawn_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/kiara_1_dawn_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/lebombo_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/lebombo_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/potsdamer_platz_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/potsdamer_platz_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/rooitou_park_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/rooitou_park_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/st_fagans_interior_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/st_fagans_interior_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/studio_small_03_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/studio_small_03_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/hdri/venice_sunset_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/venice_sunset_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/viser/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Viser", 3 | "name": "Viser", 4 | "icons": [ 5 | { 6 | "src": "favicon.svg", 7 | "sizes": "any", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/viser/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/viser/client/src/App.css.ts: -------------------------------------------------------------------------------- 1 | import { globalStyle } from "@vanilla-extract/css"; 2 | 3 | globalStyle(".mantine-ScrollArea-scrollbar", { 4 | zIndex: 100, 5 | }); 6 | 7 | globalStyle("html", { 8 | fontSize: "92.5%", 9 | "@media": { 10 | "(max-width: 767px)": { 11 | fontSize: "83%", 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/viser/client/src/AppTheme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Checkbox, 3 | ColorInput, 4 | Select, 5 | TextInput, 6 | NumberInput, 7 | Paper, 8 | ActionIcon, 9 | Button, 10 | createTheme, 11 | Textarea, 12 | } from "@mantine/core"; 13 | import { themeToVars } from "@mantine/vanilla-extract"; 14 | 15 | export const theme = createTheme({ 16 | fontFamily: "Inter", 17 | autoContrast: true, 18 | components: { 19 | Checkbox: Checkbox.extend({ 20 | defaultProps: { 21 | radius: "xs", 22 | }, 23 | }), 24 | ColorInput: ColorInput.extend({ 25 | defaultProps: { 26 | radius: "xs", 27 | }, 28 | }), 29 | Select: Select.extend({ 30 | defaultProps: { 31 | radius: "sm", 32 | }, 33 | }), 34 | Textarea: Textarea.extend({ 35 | defaultProps: { 36 | radius: "xs", 37 | }, 38 | }), 39 | TextInput: TextInput.extend({ 40 | defaultProps: { 41 | radius: "xs", 42 | }, 43 | }), 44 | NumberInput: NumberInput.extend({ 45 | defaultProps: { 46 | radius: "xs", 47 | }, 48 | }), 49 | Paper: Paper.extend({ 50 | defaultProps: { 51 | radius: "xs", 52 | shadow: "0", 53 | }, 54 | }), 55 | ActionIcon: ActionIcon.extend({ 56 | defaultProps: { 57 | variant: "subtle", 58 | color: "gray", 59 | radius: "xs", 60 | }, 61 | }), 62 | Button: Button.extend({ 63 | defaultProps: { 64 | radius: "xs", 65 | styles: { 66 | label: { 67 | fontWeight: 450, 68 | }, 69 | }, 70 | }, 71 | }), 72 | }, 73 | }); 74 | 75 | export const vars = themeToVars(theme); 76 | -------------------------------------------------------------------------------- /src/viser/client/src/BrowserWarning.tsx: -------------------------------------------------------------------------------- 1 | import { notifications } from "@mantine/notifications"; 2 | import { detect } from "detect-browser"; 3 | import { useEffect } from "react"; 4 | 5 | export function BrowserWarning() { 6 | useEffect(() => { 7 | const browser = detect(); 8 | 9 | // Browser version are based loosely on support for SIMD, OffscreenCanvas. 10 | // 11 | // https://caniuse.com/?search=simd 12 | // https://caniuse.com/?search=OffscreenCanvas 13 | if (browser === null || browser.version === null) { 14 | console.log("Failed to detect browser"); 15 | notifications.show({ 16 | title: "Could not detect browser version", 17 | message: 18 | "Your browser version could not be detected. It may not be supported.", 19 | autoClose: false, 20 | color: "red", 21 | }); 22 | } else { 23 | const version = parseFloat(browser.version); 24 | console.log(`Detected ${browser.name} version ${version}`); 25 | if ( 26 | (browser.name === "chrome" && version < 91) || 27 | (browser.name === "edge" && version < 91) || 28 | (browser.name === "firefox" && version < 89) || 29 | (browser.name === "opera" && version < 77) || 30 | (browser.name === "safari" && version < 16.4) 31 | ) 32 | notifications.show({ 33 | title: "Unsuppported browser", 34 | message: `Your browser (${ 35 | browser.name.slice(0, 1).toUpperCase() + browser.name.slice(1) 36 | }/${ 37 | browser.version 38 | }) is outdated, which may cause problems. Consider updating.`, 39 | autoClose: false, 40 | color: "red", 41 | }); 42 | } 43 | }); 44 | return null; 45 | } 46 | -------------------------------------------------------------------------------- /src/viser/client/src/ControlPanel/GuiComponentContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as Messages from "../WebsocketMessages"; 3 | 4 | interface GuiComponentContext { 5 | folderDepth: number; 6 | setValue: (id: string, value: NonNullable) => void; 7 | messageSender: (message: Messages.Message) => void; 8 | GuiContainer: React.FC<{ containerUuid: string }>; 9 | } 10 | 11 | export const GuiComponentContext = React.createContext({ 12 | folderDepth: 0, 13 | setValue: () => undefined, 14 | messageSender: () => undefined, 15 | GuiContainer: () => { 16 | throw new Error("GuiComponentContext not initialized"); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/viser/client/src/ControlPanel/SceneTreeTable.css.ts: -------------------------------------------------------------------------------- 1 | import { globalStyle, style } from "@vanilla-extract/css"; 2 | import { vars } from "../AppTheme"; 3 | 4 | export const tableWrapper = style({ 5 | borderRadius: vars.radius.xs, 6 | padding: "0.1em 0", 7 | overflowX: "auto", 8 | display: "flex", 9 | flexDirection: "column", 10 | gap: "0", 11 | }); 12 | 13 | export const propsWrapper = style({ 14 | position: "relative", 15 | borderRadius: vars.radius.xs, 16 | border: "1px solid", 17 | borderColor: vars.colors.defaultBorder, 18 | padding: vars.spacing.xs, 19 | boxSizing: "border-box", 20 | margin: vars.spacing.xs, 21 | marginTop: "0.2em", 22 | marginBottom: "0.2em", 23 | overflowX: "auto", 24 | display: "flex", 25 | flexDirection: "column", 26 | gap: vars.spacing.xs, 27 | }); 28 | 29 | export const editIconWrapper = style({ 30 | opacity: "0", 31 | }); 32 | 33 | export const tableRow = style({ 34 | display: "flex", 35 | alignItems: "center", 36 | gap: "0.2em", 37 | padding: "0 0.25em", 38 | lineHeight: "2em", 39 | fontSize: "0.875em", 40 | ":hover": { 41 | [vars.lightSelector]: { 42 | backgroundColor: vars.colors.gray[1], 43 | }, 44 | [vars.darkSelector]: { 45 | backgroundColor: vars.colors.dark[6], 46 | }, 47 | }, 48 | }); 49 | 50 | export const tableHierarchyLine = style({ 51 | [vars.lightSelector]: { 52 | borderColor: vars.colors.gray[2], 53 | }, 54 | [vars.darkSelector]: { 55 | borderColor: vars.colors.dark[5], 56 | }, 57 | borderLeft: "0.3em solid", 58 | width: "0.2em", 59 | marginLeft: "0.375em", 60 | height: "2em", 61 | }); 62 | 63 | globalStyle(`${tableRow}:hover ${tableHierarchyLine}`, { 64 | [vars.lightSelector]: { 65 | borderColor: vars.colors.gray[3], 66 | }, 67 | [vars.darkSelector]: { 68 | borderColor: vars.colors.dark[4], 69 | }, 70 | }); 71 | 72 | globalStyle(`${tableRow}:hover ${editIconWrapper}`, { 73 | opacity: "1.0", 74 | }); 75 | -------------------------------------------------------------------------------- /src/viser/client/src/HoverContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Extended hover context to include instanceId for instanced meshes and clickable state 4 | export interface HoverState { 5 | isHovered: boolean; 6 | instanceId: number | null; 7 | clickable: boolean; 8 | } 9 | 10 | export const HoverableContext = 11 | React.createContext | null>(null); 12 | -------------------------------------------------------------------------------- /src/viser/client/src/MacWindowWrapper.tsx: -------------------------------------------------------------------------------- 1 | export function MacWindowWrapper({ 2 | children, 3 | title, 4 | width, 5 | height, 6 | }: { 7 | children: React.ReactNode; 8 | title: string; 9 | width: number; 10 | height: number; 11 | }) { 12 | const TITLEBAR_HEIGHT = 36; // px 13 | 14 | return ( 15 |
30 | {/* MacOS titlebar */} 31 |
42 | {/* Traffic light buttons */} 43 |
51 |
59 |
67 | {/* Window title */} 68 |
80 | {title} 81 |
82 |
83 | {/* Content */} 84 |
85 | {children} 86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/viser/client/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { ViewerContext } from "./ViewerContext"; 2 | import { GuiModalMessage } from "./WebsocketMessages"; 3 | import GeneratedGuiContainer from "./ControlPanel/Generated"; 4 | import { Modal } from "@mantine/core"; 5 | import { useContext } from "react"; 6 | 7 | export function ViserModal() { 8 | const viewer = useContext(ViewerContext)!; 9 | 10 | const modalList = viewer.useGui((state) => state.modals); 11 | const modals = modalList.map((conf, index) => { 12 | return ; 13 | }); 14 | 15 | return modals; 16 | } 17 | 18 | function GeneratedModal({ 19 | conf, 20 | index, 21 | }: { 22 | conf: GuiModalMessage; 23 | index: number; 24 | }) { 25 | return ( 26 | { 30 | // To make memory management easier, we should only close modals from 31 | // the server. 32 | // Otherwise, the client would need to communicate to the server that 33 | // the modal was deleted and contained GUI elements were cleared. 34 | }} 35 | withCloseButton={false} 36 | centered 37 | zIndex={100 + index} 38 | > 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/viser/client/src/OutlinesIfHovered.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { HoverableContext } from "./HoverContext"; 4 | import { Outlines } from "./Outlines"; 5 | import * as THREE from "three"; 6 | 7 | /** Outlines object, which should be placed as a child of all meshes that might 8 | * be clickable. */ 9 | export function OutlinesIfHovered( 10 | props: { alwaysMounted?: boolean; creaseAngle?: number } = { 11 | // Can be set to true for objects like meshes which may be slow to mount. 12 | // It seems better to set to False for instanced meshes, there may be some 13 | // drei or fiber-related race conditions... 14 | alwaysMounted: false, 15 | // Some thing just look better with no creasing, like camera frustum objects. 16 | creaseAngle: Math.PI, 17 | }, 18 | ) { 19 | const groupRef = React.useRef(null); 20 | const hoveredRef = React.useContext(HoverableContext); 21 | const [mounted, setMounted] = React.useState(true); 22 | 23 | useFrame(() => { 24 | if (hoveredRef === null) return; 25 | if (props.alwaysMounted) { 26 | if (groupRef.current === null) return; 27 | groupRef.current.visible = hoveredRef.current.isHovered; 28 | } else if (hoveredRef.current.isHovered != mounted) { 29 | setMounted(hoveredRef.current.isHovered); 30 | } 31 | }); 32 | return hoveredRef === null || !mounted ? null : ( 33 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/viser/client/src/SearchParamsUtils.tsx: -------------------------------------------------------------------------------- 1 | /** Utilities for interacting with the URL search parameters. 2 | * 3 | * This lets us specify the websocket server + port from the URL. */ 4 | 5 | export const searchParamKey = "websocket"; 6 | 7 | export function syncSearchParamServer(server: string) { 8 | const searchParams = new URLSearchParams(window.location.search); 9 | // No need to update the URL bar if the websocket port matches the HTTP port. 10 | // So if we navigate to http://localhost:8081, this should by default connect to ws://localhost:8081. 11 | const isDefaultServer = 12 | window.location.host.includes( 13 | server.replace("ws://", "").replace("/", ""), 14 | ) || 15 | window.location.host.includes( 16 | server.replace("wss://", "").replace("/", ""), 17 | ); 18 | if (isDefaultServer && searchParams.has(searchParamKey)) { 19 | searchParams.delete(searchParamKey); 20 | } else if (!isDefaultServer) { 21 | searchParams.set(searchParamKey, server); 22 | } 23 | window.history.replaceState( 24 | null, 25 | "Viser", 26 | // We could use URLSearchParams.toString() to build this string, but that 27 | // would escape it. We're going to just not escape the string. :) 28 | searchParams.size === 0 29 | ? window.location.href.split("?")[0] 30 | : "?" + 31 | Array.from(searchParams.entries()) 32 | .map(([k, v]) => `${k}=${v}`) 33 | .join("&"), 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/viser/client/src/ShadowArgs.tsx: -------------------------------------------------------------------------------- 1 | export const shadowArgs = { 2 | "shadow-camera-near": 0.001, 3 | "shadow-camera-far": 500.0, 4 | "shadow-bias": -0.00001, 5 | "shadow-mapSize-width": 1024, 6 | "shadow-mapSize-height": 1024, 7 | "shadow-radius": 4.0, 8 | }; 9 | -------------------------------------------------------------------------------- /src/viser/client/src/Splatting/SplatSortWorker.ts: -------------------------------------------------------------------------------- 1 | /** Worker for sorting splats. 2 | */ 3 | 4 | import MakeSorterModulePromise from "./WasmSorter/Sorter.mjs"; 5 | 6 | export type SorterWorkerIncoming = 7 | | { 8 | setBuffer: Uint32Array; 9 | setGroupIndices: Uint32Array; 10 | } 11 | | { 12 | setTz_camera_groups: Float32Array; 13 | } 14 | | { close: true }; 15 | 16 | { 17 | let sorter: any = null; 18 | let Tz_camera_groups: Float32Array | null = null; 19 | let sortRunning = false; 20 | const throttledSort = () => { 21 | if (sorter === null || Tz_camera_groups === null) { 22 | setTimeout(throttledSort, 1); 23 | return; 24 | } 25 | if (sortRunning) return; 26 | 27 | sortRunning = true; 28 | const lastView = Tz_camera_groups; 29 | 30 | // Important: we clone the output so we can transfer the buffer to the main 31 | // thread. Compared to relying on postMessage for copying, this reduces 32 | // backlog artifacts. 33 | const sortedIndices = ( 34 | sorter.sort(Tz_camera_groups) as Uint32Array 35 | ).slice(); 36 | 37 | // @ts-ignore 38 | self.postMessage({ sortedIndices: sortedIndices }, [sortedIndices.buffer]); 39 | 40 | setTimeout(() => { 41 | sortRunning = false; 42 | if (Tz_camera_groups === null) return; 43 | if ( 44 | !lastView.every( 45 | // Cast is needed because of closure... 46 | (val, i) => val === (Tz_camera_groups as Float32Array)[i], 47 | ) 48 | ) { 49 | throttledSort(); 50 | } 51 | }, 0); 52 | }; 53 | 54 | const SorterModulePromise = MakeSorterModulePromise(); 55 | 56 | self.onmessage = async (e) => { 57 | const data = e.data as SorterWorkerIncoming; 58 | if ("setBuffer" in data) { 59 | // Instantiate sorter with buffers populated. 60 | sorter = new (await SorterModulePromise).Sorter( 61 | data.setBuffer, 62 | data.setGroupIndices, 63 | ); 64 | } else if ("setTz_camera_groups" in data) { 65 | // Update object transforms. 66 | Tz_camera_groups = data.setTz_camera_groups; 67 | throttledSort(); 68 | } else if ("close" in data) { 69 | // Done! 70 | self.close(); 71 | } 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/viser/client/src/Splatting/WasmSorter/Sorter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/src/Splatting/WasmSorter/Sorter.wasm -------------------------------------------------------------------------------- /src/viser/client/src/Splatting/WasmSorter/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | emcc --bind -O3 sorter.cpp -o Sorter.mjs -s WASM=1 -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['addOnPostRun']" -s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=1GB -s STACK_SIZE=2097152 -msimd128; 4 | -------------------------------------------------------------------------------- /src/viser/client/src/VersionInfo.ts: -------------------------------------------------------------------------------- 1 | // Automatically generated file - do not edit manually. 2 | // This is synchronized with the Python package version in viser/__init__.py. 3 | export const VISER_VERSION = "0.2.23"; 4 | -------------------------------------------------------------------------------- /src/viser/client/src/ViewerContext.ts: -------------------------------------------------------------------------------- 1 | import "@mantine/core/styles.css"; 2 | import "@mantine/notifications/styles.css"; 3 | import "./App.css"; 4 | import "./index.css"; 5 | 6 | import { CameraControls } from "@react-three/drei"; 7 | import * as THREE from "three"; 8 | import React from "react"; 9 | import { UseSceneTree } from "./SceneTree"; 10 | 11 | import { UseGui } from "./ControlPanel/GuiState"; 12 | import { GetRenderRequestMessage, Message } from "./WebsocketMessages"; 13 | 14 | // Type definitions for all mutable state. 15 | export type ViewerMutable = { 16 | // Function references. 17 | sendMessage: (message: Message) => void; 18 | sendCamera: (() => void) | null; 19 | resetCameraView: (() => void) | null; 20 | 21 | // DOM/Three.js references. 22 | canvas: HTMLCanvasElement | null; 23 | canvas2d: HTMLCanvasElement | null; 24 | scene: THREE.Scene | null; 25 | camera: THREE.PerspectiveCamera | null; 26 | backgroundMaterial: THREE.ShaderMaterial | null; 27 | cameraControl: CameraControls | null; 28 | 29 | // Scene management. 30 | nodeAttributesFromName: { 31 | [name: string]: 32 | | undefined 33 | | { 34 | poseUpdateState?: "updated" | "needsUpdate" | "waitForMakeObject"; 35 | wxyz?: [number, number, number, number]; 36 | position?: [number, number, number]; 37 | visibility?: boolean; // Visibility state from the server. 38 | overrideVisibility?: boolean; // Override from the GUI. 39 | }; 40 | }; 41 | nodeRefFromName: { 42 | [name: string]: undefined | THREE.Object3D; 43 | }; 44 | 45 | // Message and rendering state. 46 | messageQueue: Message[]; 47 | getRenderRequestState: "ready" | "triggered" | "pause" | "in_progress"; 48 | getRenderRequest: null | GetRenderRequestMessage; 49 | 50 | // Interaction state. 51 | scenePointerInfo: { 52 | enabled: false | "click" | "rect-select"; // Enable box events. 53 | dragStart: [number, number]; // First mouse position. 54 | dragEnd: [number, number]; // Final mouse position. 55 | isDragging: boolean; 56 | }; 57 | 58 | // Skinned mesh state. 59 | skinnedMeshState: { 60 | [name: string]: { 61 | initialized: boolean; 62 | poses: { 63 | wxyz: [number, number, number, number]; 64 | position: [number, number, number]; 65 | }[]; 66 | }; 67 | }; 68 | 69 | // Global hover state tracking. 70 | hoveredElementsCount: number; 71 | }; 72 | 73 | export type ViewerContextContents = { 74 | // Non-mutable state. 75 | messageSource: "websocket" | "file_playback"; 76 | 77 | // Zustand state hooks. 78 | useSceneTree: UseSceneTree; 79 | useGui: UseGui; 80 | 81 | // Single reference to all mutable state. 82 | mutable: React.MutableRefObject; 83 | }; 84 | 85 | export const ViewerContext = React.createContext( 86 | null, 87 | ); 88 | -------------------------------------------------------------------------------- /src/viser/client/src/WebsocketInterface.tsx: -------------------------------------------------------------------------------- 1 | import WebsocketServerWorker from "./WebsocketServerWorker?worker"; 2 | import React, { useContext } from "react"; 3 | import { notifications } from "@mantine/notifications"; 4 | 5 | import { ViewerContext } from "./ViewerContext"; 6 | import { syncSearchParamServer } from "./SearchParamsUtils"; 7 | import { WsWorkerIncoming, WsWorkerOutgoing } from "./WebsocketServerWorker"; 8 | 9 | /** Component for handling websocket connections. */ 10 | export function WebsocketMessageProducer() { 11 | const viewer = useContext(ViewerContext)!; 12 | const viewerMutable = viewer.mutable.current; 13 | const server = viewer.useGui((state) => state.server); 14 | const resetGui = viewer.useGui((state) => state.resetGui); 15 | const resetScene = viewer.useSceneTree((state) => state.resetScene); 16 | 17 | syncSearchParamServer(server); 18 | 19 | React.useEffect(() => { 20 | const worker = new WebsocketServerWorker(); 21 | 22 | worker.onmessage = (event) => { 23 | const data: WsWorkerOutgoing = event.data; 24 | if (data.type === "connected") { 25 | resetGui(); 26 | resetScene(); 27 | viewer.useGui.setState({ websocketConnected: true }); 28 | viewerMutable.sendMessage = (message) => { 29 | postToWorker({ type: "send", message: message }); 30 | }; 31 | } else if (data.type === "closed") { 32 | resetGui(); 33 | viewer.useGui.setState({ websocketConnected: false }); 34 | viewerMutable.sendMessage = (message) => { 35 | console.log( 36 | `Tried to send ${message.type} but websocket is not connected!`, 37 | ); 38 | }; 39 | 40 | // Show notification for version mismatch. 41 | if (data.versionMismatch) { 42 | notifications.show({ 43 | id: "version-mismatch", 44 | title: "Connection rejected", 45 | message: `${data.closeReason}.`, 46 | color: "red", 47 | autoClose: 5000, 48 | withCloseButton: true, 49 | }); 50 | } 51 | } else if (data.type === "message_batch") { 52 | viewerMutable.messageQueue.push(...data.messages); 53 | } 54 | }; 55 | function postToWorker(data: WsWorkerIncoming) { 56 | worker.postMessage(data); 57 | } 58 | postToWorker({ type: "set_server", server: server }); 59 | return () => { 60 | postToWorker({ type: "close" }); 61 | viewerMutable.sendMessage = (message) => 62 | console.log( 63 | `Tried to send ${message.type} but websocket is not connected!`, 64 | ); 65 | viewer.useGui.setState({ websocketConnected: false }); 66 | }; 67 | }, [server, resetGui, resetScene]); 68 | 69 | return null; 70 | } 71 | -------------------------------------------------------------------------------- /src/viser/client/src/WebsocketUtils.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as THREE from "three"; 3 | import { Message } from "./WebsocketMessages"; 4 | import { ViewerContext, ViewerContextContents } from "./ViewerContext"; 5 | 6 | /** Easier, hook version of makeThrottledMessageSender. */ 7 | export function useThrottledMessageSender(throttleMilliseconds: number) { 8 | const viewer = React.useContext(ViewerContext)!; 9 | return makeThrottledMessageSender(viewer, throttleMilliseconds); 10 | } 11 | 12 | /** Returns a function for sending messages, with automatic throttling. */ 13 | export function makeThrottledMessageSender( 14 | viewer: ViewerContextContents, 15 | throttleMilliseconds: number, 16 | ) { 17 | let readyToSend = true; 18 | let stale = false; 19 | let latestMessage: Message | null = null; 20 | 21 | function send(message: Message) { 22 | const viewerMutable = viewer.mutable.current; 23 | if (viewerMutable.sendMessage === null) return; 24 | latestMessage = message; 25 | if (readyToSend) { 26 | viewerMutable.sendMessage(message); 27 | stale = false; 28 | readyToSend = false; 29 | 30 | setTimeout(() => { 31 | readyToSend = true; 32 | if (!stale) return; 33 | latestMessage && send(latestMessage); 34 | }, throttleMilliseconds); 35 | } else { 36 | stale = true; 37 | } 38 | } 39 | return send; 40 | } 41 | 42 | /** Type guard for threejs textures. Meant to be used with `scene.background`. */ 43 | export function isTexture( 44 | background: 45 | | THREE.Color 46 | | THREE.Texture 47 | | THREE.CubeTexture 48 | | null 49 | | undefined, 50 | ): background is THREE.Texture { 51 | return ( 52 | background !== null && 53 | background !== undefined && 54 | (background as THREE.Texture).isTexture !== undefined 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/viser/client/src/WorldTransformUtils.ts: -------------------------------------------------------------------------------- 1 | import { ViewerContextContents } from "./ViewerContext"; 2 | import * as THREE from "three"; 3 | 4 | /** Helper for computing the transformation between the three.js world and the 5 | * Python-exposed world frames. This is useful for things like switching 6 | * between +Y and +Z up directions for the world frame. */ 7 | export function computeT_threeworld_world(viewer: ViewerContextContents) { 8 | const wxyz = viewer.mutable.current.nodeAttributesFromName[""]!.wxyz!; 9 | const position = viewer.mutable.current.nodeAttributesFromName[""]! 10 | .position ?? [0, 0, 0]; 11 | return new THREE.Matrix4() 12 | .makeRotationFromQuaternion( 13 | new THREE.Quaternion(wxyz[1], wxyz[2], wxyz[3], wxyz[0]), 14 | ) 15 | .setPosition(position[0], position[1], position[2]); 16 | } 17 | 18 | /** Helper for converting a ray from the three.js world frame to the Python 19 | * world frame. Applies the transformation from computeT_threeworld_world. 20 | */ 21 | export function rayToViserCoords( 22 | viewer: ViewerContextContents, 23 | ray: THREE.Ray, 24 | ): THREE.Ray { 25 | const T_world_threeworld = computeT_threeworld_world(viewer).invert(); 26 | 27 | const origin = ray.origin.clone().applyMatrix4(T_world_threeworld); 28 | 29 | // Compute just the rotation term without new memory allocation; this 30 | // will mutate T_world_threeworld! 31 | const R_world_threeworld = T_world_threeworld.setPosition(0.0, 0.0, 0); 32 | const direction = ray.direction.clone().applyMatrix4(R_world_threeworld); 33 | 34 | return new THREE.Ray(origin, direction); 35 | } 36 | -------------------------------------------------------------------------------- /src/viser/client/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { GuiButtonMessage } from "../WebsocketMessages"; 2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 3 | import { Box } from "@mantine/core"; 4 | 5 | import { Button } from "@mantine/core"; 6 | import React from "react"; 7 | import { htmlIconWrapper } from "./ComponentStyles.css"; 8 | import { toMantineColor } from "./colorUtils"; 9 | 10 | export default function ButtonComponent({ 11 | uuid, 12 | props: { visible, disabled, label, color, _icon_html: icon_html }, 13 | }: GuiButtonMessage) { 14 | const { messageSender } = React.useContext(GuiComponentContext)!; 15 | if (!(visible ?? true)) return null; 16 | 17 | return ( 18 | 19 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/viser/client/src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Button, Flex } from "@mantine/core"; 3 | import { ViserInputComponent } from "./common"; 4 | import { GuiButtonGroupMessage } from "../WebsocketMessages"; 5 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 6 | 7 | export default function ButtonGroupComponent({ 8 | uuid, 9 | props: { hint, label, visible, disabled, options }, 10 | }: GuiButtonGroupMessage) { 11 | const { messageSender } = React.useContext(GuiComponentContext)!; 12 | if (!visible) return null; 13 | return ( 14 | 15 | 16 | {options.map((option, index) => ( 17 | 33 | ))} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/viser/client/src/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ViserInputComponent } from "./common"; 3 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 4 | import { GuiCheckboxMessage } from "../WebsocketMessages"; 5 | import { Box, Checkbox, Tooltip } from "@mantine/core"; 6 | 7 | export default function CheckboxComponent({ 8 | uuid, 9 | value, 10 | props: { disabled, visible, hint, label }, 11 | }: GuiCheckboxMessage) { 12 | const { setValue } = React.useContext(GuiComponentContext)!; 13 | if (!visible) return null; 14 | let input = ( 15 | { 20 | setValue(uuid, value.target.checked); 21 | }} 22 | disabled={disabled} 23 | /> 24 | ); 25 | if (hint !== null && hint !== undefined) { 26 | // For checkboxes, we want to make sure that the wrapper 27 | // doesn't expand to the full wuuidth of the parent. This will 28 | // de-center the tooltip. 29 | input = ( 30 | 39 | {input} 40 | 41 | ); 42 | } 43 | return ( 44 | {input} 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/viser/client/src/components/ComponentStyles.css.ts: -------------------------------------------------------------------------------- 1 | import { globalStyle, style } from "@vanilla-extract/css"; 2 | 3 | export const htmlIconWrapper = style({ 4 | height: "1em", 5 | width: "1em", 6 | position: "relative", 7 | }); 8 | 9 | globalStyle(`${htmlIconWrapper} svg`, { 10 | height: "auto", 11 | width: "1em", 12 | position: "absolute", 13 | top: "50%", 14 | transform: "translateY(-50%)", 15 | }); 16 | 17 | // Class for sliders with default min/max marks. We use this for aestheticn 18 | // its; global styles are used to shift the min/max mark labels to stay closer 19 | // within the bounds of the slider. 20 | export const sliderDefaultMarks = style({}); 21 | 22 | globalStyle( 23 | `${sliderDefaultMarks} .mantine-Slider-markWrapper:first-of-type div:nth-of-type(2)`, 24 | { 25 | transform: "translate(-0.1rem, 0.03rem) !important", 26 | }, 27 | ); 28 | 29 | globalStyle( 30 | `${sliderDefaultMarks} .mantine-Slider-markWrapper:last-of-type div:nth-of-type(2)`, 31 | { 32 | transform: "translate(-85%, 0.03rem) !important", 33 | }, 34 | ); 35 | 36 | // Style for filled slider marks - use primary color to match the active segment. 37 | globalStyle(".mantine-Slider-mark[data-filled]:not([data-disabled])", { 38 | background: "var(--mantine-primary-color-filled)", 39 | borderColor: "var(--mantine-primary-color-filled)", 40 | }); 41 | 42 | // Style for filled slider marks when disabled - use separate rules for light/dark. 43 | globalStyle(".mantine-Slider-mark[data-filled][data-disabled]", { 44 | background: "var(--mantine-color-gray-5)", 45 | borderColor: "var(--mantine-color-gray-5)", 46 | }); 47 | 48 | // Dark mode styles for filled marks when disabled. 49 | globalStyle( 50 | '[data-mantine-color-scheme="dark"] .mantine-Slider-mark[data-filled][data-disabled]', 51 | { 52 | background: "var(--mantine-color-dark-3)", 53 | borderColor: "var(--mantine-color-dark-3)", 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /src/viser/client/src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 3 | import { ViserInputComponent } from "./common"; 4 | import { GuiDropdownMessage } from "../WebsocketMessages"; 5 | import { Select } from "@mantine/core"; 6 | 7 | export default function DropdownComponent({ 8 | uuid, 9 | value, 10 | props: { hint, label, disabled, visible, options }, 11 | }: GuiDropdownMessage) { 12 | const { setValue } = React.useContext(GuiComponentContext)!; 13 | if (!visible) return null; 14 | return ( 15 | 16 |