Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleaner API for saved/embedded visualizations + initial documentation #357

Merged
merged 11 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ jobs:
# Get version from pyproject.toml.
- name: Get version + subdirectory
run: |
VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['version'])")
VERSION=$(python -c "import viser; print(viser.__version__)")
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOCS_SUBDIR=versions/$VERSION" >> $GITHUB_ENV

# Hack to overwrite version.
- name: Set version to 'latest' for pushes (this will appear in the doc banner)
- name: Set version to 'main' for pushes (this will appear in the doc banner)
run: |
python -c "import toml; conf = toml.load('pyproject.toml'); conf['project']['version'] = 'latest'; toml.dump(conf, open('pyproject.toml', 'w'))"
echo "VISER_VERSION_STR_OVERRIDE=main" >> $GITHUB_ENV
if: github.event_name == 'push'

# Build documentation.
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ jobs:
- name: Only bundle client build for PyPI release
run: |
# This should delete everything in src/viser/client except for the
# build folder.
# build folder + original source files. We don't want to package
# .nodeenv, node_modules, etc in the release.
mv src/viser/client/build ./__built_client
rm -rf src/viser/client/*
git checkout src/viser/client
mv ./__built_client src/viser/client/build
- name: Build and publish
env:
Expand Down
10 changes: 4 additions & 6 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
# full list see the documentation:
# http://www.sphinx-doc.org/en/stable/config

from pathlib import Path
import os
from typing import Dict, List

import m2r2
import toml

import viser

# -- Path setup --------------------------------------------------------------

Expand All @@ -25,10 +26,7 @@
copyright = "2024"
author = "brentyi"

# The short X.Y version
version: str = toml.load(
Path(__file__).absolute().parent.parent.parent / "pyproject.toml"
)["project"]["version"]
version: str = os.environ.get("VISER_VERSION_STR_OVERRIDE", viser.__version__)

# Formatting!
# 0.1.30 => v0.1.30
Expand Down
11 changes: 7 additions & 4 deletions docs/source/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ pip install -e .[dev]
pre-commit install
```

It would be hard to write unit tests for `viser`. We rely on static typing for
robustness. To check your code, you can run the following:
For code quality, rely primarily on `pyright` and `ruff`:

```bash
# runs linting, formatting, and type-checking
viser-dev-checks
# Check static types.
pyright

# Lint and format.
ruff check --fix .
ruff format .
```

## Message updates
Expand Down
198 changes: 198 additions & 0 deletions docs/source/embedded_visualizations.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
Embedding Visualizations
===============================================

This guide describes how to export 3D visualizations from Viser and embed them into static webpages. The process involves three main steps: exporting scene state, creating a client build, and hosting the visualization.

.. warning::

This workflow is experimental and not yet polished. We're documenting it
nonetheless, since we think it's quite useful! If you have suggestions or
improvements, issues and PRs are welcome.


Step 1: Exporting Scene State
----------------------------

You can export static or dynamic 3D data from a Viser scene using the scene
serializer. :func:`ViserServer.get_scene_serializer` returns a serializer
object that can serialize the current scene state to a binary format.

Static Scene Export
~~~~~~~~~~~~~~~~~~~

For static 3D visualizations, use the following code to save the scene state:

.. code-block:: python

import viser
from pathlib import Path

server = viser.ViserServer()

# Add objects to the scene via server.scene
# For example:
# server.scene.add_mesh(...)
# server.scene.add_point_cloud(...)
server.scene.add_box("/box", color=(255, 0, 0), dimensions=(1, 1, 1))

# Serialize and save the scene state
data = server.get_scene_serializer().serialize() # Returns bytes
Path("recording.viser").write_bytes(data)


As a suggestion, you can also add a button for exporting the scene state:

.. code-block:: python

import viser
server = viser.ViserServer()

# Add objects to the scene via server.scene.
# For example:
# server.scene.add_mesh(...)
# server.scene.add_point_cloud(...)
server.scene.add_box("/box", color=(255, 0, 0), dimensions=(1, 1, 1))

save_button = server.gui.add_button("Save Scene")

@save_button.on_click
def _(event: viser.GuiEvent) -> None:
assert event.client is not None
event.client.send_file_download("recording.viser", server.get_scene_serializer().serialize())

server.sleep_forever()

Dynamic Scene Export
~~~~~~~~~~~~~~~~~~~~

For dynamic visualizations with animation, you can create a "3D video" by inserting sleep commands between frames:

.. code-block:: python

import viser
import numpy as np
from pathlib import Path

server = viser.ViserServer()

# Add objects to the scene via server.scene
# For example:
# server.scene.add_mesh(...)
# server.scene.add_point_cloud(...)
box = server.scene.add_box("/box", color=(255, 0, 0), dimensions=(1, 1, 1))

# Create serializer.
serializer = server.get_scene_serializer()

num_frames = 100
for t in range(num_frames):
# Update existing scene objects or add new ones.
box.position = (0.0, 0.0, np.sin(t / num_frames * 2 * np.pi))

# Add a frame delay.
serializer.insert_sleep(1.0 / 30.0)

# Save the complete animation.
data = serializer.serialize() # Returns bytes
Path("recording.viser").write_bytes(data)

.. note::
Always add scene elements using :attr:`ViserServer.scene`, not :attr:`ClientHandle.scene`.

.. note::
The ``.viser`` file is a binary format containing scene state data and is not meant to be human-readable.

Step 2: Creating a Viser Client Build
-----------------------------------

To serve the 3D visualization, you'll need two things:

1. The ``.viser`` file containing your scene data
2. A build of the Viser client (static HTML/JS/CSS files)

With Viser installed, create the Viser client build using the command-line tool:

.. code-block:: bash

# View available options
viser-build-client --help

# Build to a specific directory
viser-build-client --output-dir viser-client/


Step 3: Hosting
---------------

Directory Structure
~~~~~~~~~~~~~~~~~~~

For our hosting instructions, we're going to assume the following directory structure:

.. code-block::

.
├── recordings/
│ └── recording.viser # Your exported scene data
└── viser-client/
├── index.html # Generated client files
├── assets/
└── ...

This is just a suggestion; you can structure your files however you like.

Local Development Server
~~~~~~~~~~~~~~~~~~~~~~~~

For testing locally, you can use Python's built-in HTTP server:

.. code-block:: bash

# Navigate to the parent directory containing both folders
cd /path/to/parent/dir

# Start the server (default port 8000)
python -m http.server 8000

Then open your browser and navigate to:

* ``http://localhost:8000/viser-client/`` (default port)

This should show the a standard Viser client. To visualize the exported scene, you'll need to specify a URL via the ``?playbackPath=`` parameter:

* ``http://localhost:8000/viser-client/?playbackPath=http://localhost:8000/recordings/recording.viser``


GitHub Pages Deployment
~~~~~~~~~~~~~~~~~~~~~~~

To host your visualization on GitHub Pages:

1. Create a new repository or use an existing one
2. Create a ``gh-pages`` branch or enable GitHub Pages on your main branch
3. Push your directory structure to the repository:

.. code-block:: bash

git add recordings/ viser-client/
git commit -m "Add Viser visualization"
git push origin main # or gh-pages

Your visualization will be available at: ``https://user.github.io/repo/viser-client/?playbackPath=https://user.github.io/repo/recordings/recording.viser``

You can embed this into other webpages using an HTML ``<iframe />`` tag.


Step 4: Setting the initial camera pose
-----------------------------------------------

To set the initial camera pose, you can add a ``&logCamera`` parameter to the URL:

* ``http://localhost:8000/viser-client/?playbackPath=http://localhost:8000/recordings/recording.viser&logCamera``

Then, open your Javascript console. You should see the camera pose printed
whenever you move the camera. It should look something like this:

* ``&initialCameraPosition=2.216,-4.233,-0.947&initialCameraLookAt=-0.115,0.346,-0.192&initialCameraUp=0.329,-0.904,0.272``

You can then add this string to the URL to set the initial camera pose.
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ URL (default: `http://localhost:8080`).

./conventions.md
./development.md
./embedded_visualizations.rst

.. toctree::
:caption: API (Basics)
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ exclude = ["src/viser/client/.nodeenv", "src/viser/client/node_modules", "**/__p
# Client build is in the gitignore, but we still want it in the distribution.
ignore-vcs = true

[tool.hatch.version]
path = "src/viser/__init__.py"

[tool.hatch.build.targets.sdist]
only-include = ["src/viser"]

Expand All @@ -15,7 +18,7 @@ packages = ["src/viser"]

[project]
name = "viser"
version = "0.2.19"
dynamic = ["version"]
description = "3D visualization + Python"
readme = "README.md"
license = { text="MIT" }
Expand Down Expand Up @@ -70,7 +73,7 @@ examples = [
"GitHub" = "https://github.com/nerfstudio-project/viser"

[project.scripts]
viser-dev-checks = "viser.scripts.dev_checks:entrypoint"
viser-build-client = "viser._client_autobuild:build_client_entrypoint"

[tool.pyright]
exclude = ["./docs/**/*", "./examples/assets/**/*", "./src/viser/client/.nodeenv", "./build"]
Expand All @@ -86,6 +89,7 @@ lint.select = [
"I", # Import sorting.
]
lint.ignore = [
"E731", # Do not assign a lambda expression, use a def.
"E741", # Ambiguous variable name. (l, O, or I)
"E501", # Line too long.
"E721", # Do not compare types, use `isinstance()`.
Expand Down
2 changes: 2 additions & 0 deletions src/viser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@
from ._viser import CameraHandle as CameraHandle
from ._viser import ClientHandle as ClientHandle
from ._viser import ViserServer as ViserServer

__version__ = "0.2.20"
Loading
Loading