aardvark

Let’s Make A Watch Gadget!

This tutorial is intended to follow the first one, where you’ve learned about the prerequisites for developing AardvarkXR gadgets, gadget initialization, and some scene graph anatomy. If you haven’t worked through that tutorial yet, please do so and come back when you’re ready for more - we’ll be building on that information and assuming you’ve mastered the basics.

In this guide we’ll be building a simple digital watch; we’ll utilize a panel and a 3D model, and implement a a little interactivity. Once you’ve worked through this guide, you should have two things:

Step 1: The First Time - Tooling and debugging

If you’ve worked on wearable VR/AR experiences, you’re already familiar with the ergonomic difficulties of iterating on development and alternating between coding (headset off) and testing (headset on). You might already have a workflow you’re comfortable with that mitigates this, in which case feel free to skim or skip this section. In any case, here’s my typical workflow in the hope that it might be of use to you too:

  1. I like to stay in VR and work in the virtual desktop to minimize headset fidgeting time. It’s worth knowing there are several good VR desktop solutions available like this one, this one, this one and this one that play well with SteamVR, some of which look and feel much better than the built-in one.

  2. Coding is done in VS Code, with the npm start script running in VS Code’s terminal (you can launch a new terminal instance from VS Code’s Terminal tab):

workflow-1

Remember you’ll also have to be serving the gadget locally during iteration as described in the first tutorial.

Note that the script watches the files and recompiles on change, so you don’t have to “refresh” anything aside from hitting save. When iterating, the Aardvark renderer does not have to be restarted to reflect gadget updates but you do have to instantiate a new gadget every time you want to see the latest compiled changes (and probably want to discard that gadget after testing). This is what my workflow typically looks like - IDE, test in SteamVR, back to IDE:

workflow.gif

  1. For debugging, remember you can use Chrome’s devtools on the Aardvark Server (http://localhost:8042/), as well as the Gadget Monitor (http://localhost:23842/gadgets/aardvark_monitor/index.html). You can also debug your gadget from VS Code, provided you’ve installed the Debugger for Chrome extension and answered “yes” to the Do you want to debug with VS Code? question when instantiating your gadget. You can then attach the debugger in VS Code and reload with the debugger widget button or ctrl-shift-F5. The create script will make a launch.json that attached to avrenderer to enable this.

Step 2: Time of the Season - Adding a time/date panel

Alright, now that we’ve got a dev environment and a nice workflow, we can jump into the code.

You can find the following code and the assets we’ll be using here.

Let’s start by instantiating a new gadget. Give it any name you want, and select the defaults for everything (check out the previous tutorial if you’d like a recap).

We’ll be working primarily in main.tsx and styles.css located in the src/ folder, which (after running npm start as described in the previous tutorial) get recompiled to the dist/ folder on every save. Remember that dist/ will be the root of your local references - where the compiled index.js will be solving paths from.

Now, let’s tweak main.tsx to a minimum, so that it only contains the bare bones:

import { AvPanel, AvStandardGrabbable, AvTransform, DefaultLanding } from '@aardvarkxr/aardvark-react';
import { Av, g_builtinModelBox } from '@aardvarkxr/aardvark-shared';
import bind from 'bind-decorator';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

interface WatchState {}

class WatchGadget extends React.Component< {}, WatchState >
{
	constructor( props: any ) {
		super( props );
	}

	public render()
	{
		return (
				<div>
					<AvStandardGrabbable modelUri={ g_builtinModelBox } modelScale={ 0.03 }
						style={ GrabbableStyle.Gadget }>
						<AvTransform translateY={ 0.08 } >
							<AvPanel interactive={true} widthInMeters={ 0.1 }></AvPanel>
						</AvTransform>
					</AvStandardGrabbable>
				</div>
		);
	}
}

let main = Av() ? <WatchGadget/> : <DefaultLanding/>
ReactDOM.render( main, document.getElementById( "root" ) );

Note we’re using the following nodes:

We’ve timmed much more: note the fewer imported modules, the removal of methods for networking-related state-dependent rendering, etc. Finally, we added the WatchState interface that will hold the gadget state. All the pieces are there - let’s get to work!

First, let’s add a simple sanity check to make sure our dev environment is fully functional and the gadget is being updated - put this in your <AvPanel>:

<div className="watch">Hello Aardvark!</div>

Upon saving and instantiating a new gadget, you should be able to see immediate results, but they’ll default to the size of fairly small chicken scratch. In styles.css, let’s fix that!

.watch {
  font-size: 3rem;
}

hello-aardvark

Now we’re cooking with fire. Let’s get the text looking like a proper digital watch digits. We’ll tweak the CSS of the .watch class so that the numbers utilize a nice digital font (I picked the freely available Digital 7, and position it somewhere that’ll make sense once we add the digital watch model.

A nice trick here is making the panel background transparent, because we don’t want a “page” as much as “digits displayed on a watch’s screen”. Here’s styles.css:

@font-face {
  font-family: "digital-7";
  src: url("./fonts/digital-7\ \(mono\).ttf");
}

body,
html {
  background-color: transparent;
}

.watch {
  font-family: "digital-7", monospace;
	font-size: 2.65rem;
  color: #4af626;
  margin-top: 47%;
  margin-left: 48%;
  max-width: 1.5rem;
}

et voilà!

hello-aardvark-digital

Step 3: Time After Time - Adding the Watch model

After scouring Poly and Sketchfab for good, free digital watch models, I found this great-looking, downloadable photogrammetized model of a Casio G-Shock by jeandiz. Clocking in at ~25MB, its textures are way too nice and detailed for our purposes, so I’ve resized them in imagemagick and did some model cleanup in Blender; you can find the optimized, <1MB gltf file here.

Casio

Let’s make this the gadget preview model while we’re at it! in manifest.webmanifest, under icons, change to:

	'icons': [
		{
			"src": "models/watch.glb",
			"type": "model/gltf-binary"
		}
	],

important - assets don’t get automagically copied over in our webpack script. Let’s head over to webpack.config.js and in plugins, under the CopyPlugin settings, add:

({ "from": "./src/models/", "to": "models/" },
{ "from": "./src/fonts/", "to": "fonts/" })

to ensure that our model and font get properly copied over to the dist folder, which is our root, on every compile.

Now that our gadget preview is all set up, let’s add the watch to the gadget itself. Here’s our revised render function:

	public render()
	{
		return (
      <div>
        <AvStandardGrabbable modelUri={"./models/watch.glb"} modelScale={0.03}>
          <AvTransform
            translateX={-0.012}
            translateY={0.063}
            translateZ={0.00375}
            rotateX={-90}
            uniformScale={0.2}
          >
            <AvPanel widthInMeters={1.23} interactive={true}>
								<div className="watch">Hello Aardvark!</div>
            </AvPanel>
          </AvTransform>
        </AvStandardGrabbable>
      </div>
		);
	}

Not too much happening here. First, note that we’ve changed the model in <AvStandardGrabbable> to be our watch. We’re using the <AvTransform> component to place the text (which will soon be the time/date) relatively to the model. The values are a result of trial and error - our main concern is having the watch in a reasonable place and setting the digits’ size and location with the <AvTransform> values and the .watch class’ CSS.

Step 4: Time for Action - Making things interactive

Alright, so we’ve got a watch; time (heh) to make it tick! In the spirit of old school digital watches, let’s have a time display and - when the screen is “clicked” with a VR controller - switch to a date display. We’ll start by adding a “watchDisplay” string and an instance of a new watchDisplayType enum to our state interface, to manage the display state:

enum watchDisplayType {
  Time,
  Date,
}

interface WatchState {
  watchDisplayTypeInstance: watchDisplayType;
  watchDisplay: string;
}

In the WatchGadget class, we’ll:

class WatchGadget extends React.Component<{}, WatchState> {
  constructor(props: any) {
    super(props);
    this.state = {
      watchDisplayTypeInstance: watchDisplayType.Time,
      watchDisplay: "",
    };
  }

  componentDidMount() {
    setInterval(this.getTime, 1000);
  }

  @bind
  private getTime() {
    switch (this.state.watchDisplayTypeInstance) {
      case watchDisplayType.Time:
        this.setState({ watchDisplay: new Date().toLocaleTimeString() });
        break;

      case watchDisplayType.Date:
        this.setState({
          watchDisplay: new Date().toLocaleDateString(undefined, {
            month: "2-digit",
            day: "2-digit",
            year: "2-digit",
          }),
        });
        break;

      default:
        break;
    }
  }

  @bind
  public changeDisplay() {
    this.state.watchDisplayTypeInstance == watchDisplayType.Date
      ? this.setState({ watchDisplayTypeInstance: watchDisplayType.Time })
      : this.setState({ watchDisplayTypeInstance: watchDisplayType.Date });
  }
//  (the rest of our code...)

finally, in our render method, we’ll update what we’re displaying in the watch div and add an onMouseDown event handler, in order to trigger the display change:

<div className="watch" onMouseDown={this.changeDisplay}>
  {this.state.watchDisplay}
</div>

Here’s main.tsx after all our additions:

import { AvPanel, AvStandardGrabbable, AvTransform, DefaultLanding } from '@aardvarkxr/aardvark-react';
import { Av } from '@aardvarkxr/aardvark-shared';
import bind from 'bind-decorator';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

enum watchDisplayType {
	Time,
	Date,
  }
  
interface WatchState {
watchDisplayTypeInstance: watchDisplayType;
watchDisplay: string;
}

class WatchGadget extends React.Component< {}, WatchState >
{
  constructor(props: any) {
    super(props);
    this.state = {
      watchDisplayTypeInstance: watchDisplayType.Time,
      watchDisplay: "",
    };
  }

  componentDidMount() {
    setInterval(this.getTime, 1000);
  }

  @bind
  private getTime() {
    switch (this.state.watchDisplayTypeInstance) {
      case watchDisplayType.Time:
        this.setState({ watchDisplay: new Date().toLocaleTimeString() });
        break;

      case watchDisplayType.Date:
        this.setState({
          watchDisplay: new Date().toLocaleDateString(undefined, {
            month: "2-digit",
            day: "2-digit",
            year: "2-digit",
          }),
        });
        break;

      default:
        break;
    }
  }

  @bind
  public changeDisplay() {
    this.state.watchDisplayTypeInstance == watchDisplayType.Date
      ? this.setState({ watchDisplayTypeInstance: watchDisplayType.Time })
      : this.setState({ watchDisplayTypeInstance: watchDisplayType.Date });
  }

	public render()
	{
		return (
			<div>
			<AvStandardGrabbable modelUri={"./models/watch.glb"} modelScale={0.03}>
			  <AvTransform
				translateX={-0.012}
				translateY={0.056}
				translateZ={0.00375}
				rotateX={-90}
				uniformScale={0.2}
			  >
				<AvPanel widthInMeters={1.23} interactive={true}>
				  <div className="watch" onMouseDown={this.changeDisplay}>
					{this.state.watchDisplay}
				  </div>
				</AvPanel>
			  </AvTransform>
			</AvStandardGrabbable>
		  </div>
		);
	}
}

let main = Av() ? <WatchGadget/> : <DefaultLanding/>
ReactDOM.render( main, document.getElementById( "root" ) );

And here’s our gadget in action:

final

Step 5: Our Time Together - sharing your gadget in a multiuser experience

VR can feel lonely if you’re all by yourself… Why not do it together?

PlutoVR is a spatial computing communications service available on Steam. You can use it to hang out with friends or coworkers in VR, and With the addition of a single prop, you can share your gadget in PlutoVR conversations!

Add the prop remoteInterfaceLocks={[]} to your <AvStandardGrabbable>:

<AvStandardGrabbable modelUri={"./models/watch.glb"} modelScale={0.03} remoteInterfaceLocks={[]}>

Remote Interface Locks handle communication between networked gadgets - we’re just defining an empty Remote Interface Lock to make the gadget appear across pluto conversations. You can learn more about them in the docs and check out the default gadget (that gets created when you run npm init @aardvarkxr) for a sample use. In addition, here is a quick, helpful video on enabling Aardvark’s multi-user functionality in Pluto.

Now that you’ve enabled multiuser functionality in your gadget, you can show it off to a friend!

multiuser-opt.gif

There you have it. Your own Casio Watch gadget. Now go wander the metaverse in style!

Step 6: Closing Time - a few tips for the road:

Thanks for reading, and happy hacking!