-
Notifications
You must be signed in to change notification settings - Fork 52
Frequently Asked Questions
This is a place to ask any questions you might have as you're learning to build with JanusWeb.
- Client
- Room Building
- Server
The easiest way to do this is to pull in our hosted JS file and then use a <janus-viewer>
element.
<html>
<body>
<script src="https://web.janusvr.com/janusweb.js"></script>
<janus-viewer>
...your room code here...
</janus-viewer>
</body>
</html>
The following attributes are supported:
Name | Description | Default |
---|---|---|
fullscreen | Should the renderer take up the whole size of the page? | true |
autostart | Launch experience without requiring user interaction | true |
src | URL to load for first room | current page URL |
homepage | URL to take user to when they hit "home" button | same as src |
width | Width of render canvas if not fullscreen | 640 |
height | Height of render canvas if not fullscreen | 480 |
corsproxy | URL to an alternate CORS proxy server | |
networking | Enable networking | true |
avatarsrc | URL for an alternative avatar descriptor | |
shownavigation | Show the default JanusWeb UI | true |
You can also self-host your own version of JanusWeb if you want to change any of the default settings. Refer to the README for build instructions.
Yes, the JanusWeb UI is completely customizable, config-driven, and is built with a set of predefined custom element HTML UI elements like navigation, chat, inventory, and more. You can style these components by overriding their styles in a custom CSS file, or you can define new custom elements which can be slotted into the UI as needed.
The included UI is mostly built using Elation's included UI elements. These could do with improved documentation and example of how to use them, but for now you can find a list of available components in the Elation Elements submodule of Elation's GitHub repository.
If you only need the UI to appear for 2D monitors and don't need any of those UI elements to be available in VR mode, then you could likely build your UI using any other UI toolkit you'd like (React, etc). However, one of the primary advantages of using Elation Elements is that every custom element which extends the base Elation Element class can be rendered to a texture, which can then be used in the 3d world. This makes it possible to do things like have an inventory UI component that can be mapped to an in-game tablet, or bring settings menus into VR.
Ideally in order to avoid CORS issues, you should make sure that your host serves the Access-Control-Allow-Origin: *
HTTP header for all of your room's assets.
Knowing that this isn't always possible on every host, and in the interest of making it as easy as possible for people to create virtual worlds, JanusWeb also allows the client to specify a CORS proxy host which can be used to fetch the content from hosts which don't send the right headers. Please note that now that Janus is no longer a company, this service is provided as a convenience to the community. If you expect to send very large amounts of traffic through a Janus world and you need this functionality, it would be in your interest to run your own CORS proxy service instead of relying on ours.
Be aware of the security considerations of this when building your app - if you plan to use JanusWeb to perform some privileged actions using some authenticated API, it may be in your interest to disable the CORS proxy and restrict users to only loading authorized content.
There are several ways to build a room in JanusWeb, depending on what you are building. At its core, Janus rooms are a hierarchy of objects with various properties. The room itself is represented by an object, and all the objects contained within that room are represented as child objects of various types. These objects can in turn contain any number of objects, allowing you to define nested coordinate spaces in whatever configuration you need for your use case.
This object hierarchy can exist entirely in memory - for example, in the case of a scene that is generated entirely by a JS script, or in a sandbox room with multiple people editing - but generally it is loaded from markup, hosted in an HTML file that can be hosted anywhere you would normally host a website. You can write this markup yourself by hand if you feel comfortable with that, or you can use the client's built-in editing tools to build the room, and then save the markup from the "view source" window. You can also export the entire scene directly to glTF.
For more info on Janus markup, refer to the JanusVR Build Documentation.
The client's built-in edit mode lets you build your scene by dragging and dropping 3d models, images, sounds, and videos from your local hard drive, from remote websites, or from JanusWeb's inventory. When an object is selected in edit mode, the following controls are enabled:
-
right click
: select object, or cancel current selection -
left click
: place object or accept current edit -
ctrl+c
/ctrl+v
: copy/paste selected object -
tab
/shift+tab
: cycle between position, rotation, scaling, or color modes - Mouse wheel: behavior depends on current edit mode
- position:
mwheel
/alt+mwheel
/shift+mwheel
: translate on x/y/z axes, respectively - rotation:
mwheel
/alt+mwheel
/shift+mwheel
: rotate on y/z/x axes, respectively - scale:
mwheel
/alt+mwheel
/shift+mwheel
/shift+alt+mwheel
: scale on on all/y/z/x axes, respectively
- position:
-
delete
: removes the selected object from the scene -
i
/j
/k
/l
/u
/o
: manipulate object on +y/-x/-y/+x/-z/+z axis, respectively -
1
/2
/3
/4
: set snapping, depends on current edit mode- position: Snap position to nearest 1, .1, .01, or .001, respectively
- rotation: Snap position to nearest 45°, 15°, 5°, or 1.25°, respectively
- scale: Snap scale to nearest 1, .1, .01, or .001, respectively
The recommended way to add interactivity is to define custom elements which represent various entities in your world. For example, if you want to add monsters to your world, it might look something like this:
room.registerElement('monster', {
monstername: 'anonymous',
create() {
this.model = this.createObject('object', { id: 'zombie-model' });
this.attacking = false;
},
update(dt) {
if (this.findPlayerNearby()) {
this.beginAttack();
} else {
this.idle();
}
},
findPlayerNearby() {
// Fill this in, it depends on your game!
},
beginAttack() {
// This can do whatever you want it to
this.attacking = player;
// When we're done, we dispatch an event so that other parts of the script can know what we're doing
this.dispatchEvent({type: 'beginattack', target: this.attacking});
},
idle() {
// Here we just stand around and growl every once in a while until we see something
if (this.attacking)
this.attacking = false;
this.dispatchEvent({type: 'idle'});
}
}
});
You can then add as many <monster>
objects to your markup as you'd like:
<fireboxroom>
<assets>
<assetscript src="monster.js"/>
</assets>
<room>
<monster pos="-1 0 0" monstername="Fred" />
<monster pos="1 0 0" monstername="Quizblorg" />
</room>
</fireboxroom>
You can also spawn them from scripts:
let newMonster = room.createObject('monster', {
pos: V(Math.random() * 20, 0, Math.random() * 20)
});
You can also add event listeners to existing objects to add custom behavior in response to actions. The following event listeners are supported by all objects (some require colliders to be defined for the object):
-
onmouseover
onmouseout
onmousemove
onclick
onmousedown
onmouseout
onwheel
-
ontouchstart
ontouchmove
ontouchend
-
ongazeenter
ongazemove
ongazeleave
ongazeactivate
-
oncollision
ontrigger
-
oncreate
onupdate
onobjectchange
-
onloadstart
onloadprogress
onload
You can also respond to any of the custom elements that your element might emit using .dispatchEvent()
. For instance, we can add handlers for when our <monster>
objects start to attack, either in markup:
<fireboxroom>
<assets>
<assetscript src="monster.js"/>
<assetsound id="rawr" src="rawr.ogg"/>
<assetsound id="blargh" src="blargh.ogg"/>
</assets>
<room>
<monster pos="-1 0 0" monstername="Fred" onbeginattack="this.playSound({id: 'rawr', oneshot: true})"/>
<monster pos="1 0 0" monstername="Quizblorg" onbeginattack="this.playSound({id: 'blargh', oneshot: true})"/>
</room>
</fireboxroom>
Or in code:
let monster = room.createObject('monster', {
pos: V(Math.random() * 20, 0, Math.random() * 20)
});
// Somewhere in my application, I keep a list of monsters and who their friends are
// So if a monster starts to attack I can call his friends over to help out
monster.addEventListener('beginattack', (ev) => {
alertMonsterFriends(monster, monster.attacking)
});
JanusWeb has built-in support for an NPM-like package repository which offers up a library of custom elements and their dependencies. The default client is configured to load a default list of components, most of which are provided in the Janus Custom Elements repository. These include things like scenery (water, procedural trees/forests, etc), utility objects (instancedobjects, linesegments, labelsets), objects that work with certain types of media (recordplayer, tapedeck, projector), and more.
You can call these predefined elements into your page on the fly using either markup <room require="...">
or code room.require(...)
, then you can use them in your room as if they were any other built-in object type.
-
Markup:
<room require="water pushbutton slider"> <water js_id="ocean" /> <slider pos="1 2 3" min="0" max="1" name="opacity" onchange="room.objects['ocean'].opacity = this.value" /> </room>
-
Code:
room.require(['linesegments', 'labelset']).then(() => { ... });
- objects: glTF, OBJ, STL, PLY, DAE, FBX, VRML
- images: JPG, PNG, GIF, SVG, BMP
- sounds: mp3, ogg, wav
- video: mp4, webm
If you wish to generate some custom geometry, you can define a JanusWeb asset that refers to a any Three.js object. This can be used to import geometry from any existing Three.js project (like THREE.ExtrudeGeometry, THREE.Water, etc), parse data from different sources (like loading Doom maps from .wad files, or generating a point cloud based on some JSON data), or it can be used to procedurally generate geometry using libraries (like trees generated with proctree.js).
Here's an example of creating a simple triangle, most of this is just Three.js BufferGeometry boilerplate (see these Three.js BufferGeometry examples for more info)
room.registerElement('triangle', {
create() {
// Make a triangle with the points (-1,0,0), (0,0,1), (1, 0, 0)
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute([-1, 0, 0, 0, 0, 1, 1, 0, 0], 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute([ 0, 1, 0, 0, 1, 0, 0, 1, 0], 3));
let material = new THREE.MeshPhongMaterial({color: 0x00ff00});
let mesh = new THREE.Mesh(geometry, material);
// Tell the room about our new asset object
room.loadNewAsset('object', { id: 'mything', object: mesh });
// Create an instance of our new asset
this.triangle = this.createObject('object', { id: 'mything' });
}
});
Absolutely! There are several ways.
The one I prefer is to just define your application so that it all lives inside of one custom element. For example, we once built a client for the blockchain-based virtual world Decentraland, where you could pull in the whole Decentraland parcel system as an object in your world using the <dcl>
tag. This custom element would then make a number of API calls to fetch data, and use .createObject()
to spawn whatever it needed to represent that data. Roughly, that looks something like this:
room.registerElement('dcl', {
create() {
fetch('https://api.decentraland.org/v1/parcels')
.then(res => res.json())
.then(json => this.createParcels(json));
},
createParcels(json) {
if (json.ok) {
json.data.estates.forEach(estate => {
this.estates[estate.id] = this.createObject('dcl-estate', { estatedata: estate });
});
}
});
room.registerElement('dcl-estate', {
estatedata: null,
create() {
// Do whatever is needed to represent this estate, using the data provided in this.estatedata
}
});
You can use this.createObject()
to spawn objects underneath this top-level element (or any other element, for that matter), and they'll respect any position, orientation, and scaling that's applied to the nested objects' coordinate spaces. You can also use room.createObject()
to spawn objects at the top level of the room.
Some of our users in the past have done some very interesting experiments generating Janus markup on the server side. You can use whatever logic and data sources to generate a room that's as complex as you want it to be, and you can use links between those worlds to create a whole self-contained ecosystem of your own. Some people have hosted Janus worlds in MediaWiki, so that wiki edits get automatically translated into changes in the 3d world. The possibilities of this technique are incredibly powerful, but vastly underexplored.
It's also possible to generate a markup string on the client side, and load the room directly from that. This is a bit hacky, but it's certainly possible if you want things to be self contained.
// This string could be generated from a template, via string concatenation, fetched from a URL, or anything else
let myroomsource = `
<fireboxroom>
<room>
<object id="sphere" />
</room>
</fireboxroom>`;
janus.loadFromSource(myroomsource);
JanusWeb also has support for custom site translators, which can turn a URL hosted on an external site into a series of virtual rooms, without needing to place markup on those sites. The most fully-built-out translator we have in JanusWeb is the Reddit translator, which can take any URL for a subreddit, eg (https://reddit.com/r/popular), and instead of rendering the 2d site we make an XHR request to Reddit's JSON API, then use the data it returns to automatically generate a virtual world that represents the latest state of that subreddit.
There are a number of different reasons rooms might load slower than expected. The two main types of slowness come either from download size, and from parsing and processing all the data.
- Use HTTP/2 for optimum delivery of content
- If your server or file host supports Content-Encoding: gzip, you definitely want to use it!
- Otherwise, you can gzip your models and JanusWeb will automatically decompress them.
- JPG files tend to be smaller than PNG files. Basis Universal textures are somewhere in-between.
- Consider your texture resolution and the number of textures involved in each material. Maybe you don't need 4096x4096 diffuse+normal+roughness+metalness textures for every material that makes up an object, and can get away with 2048 or even 1024?
- Using a power-of-two size for your textures means less processing on the CPU, we can upload the texture directly to the GPU and use it as-is
- If your texture doesn't have alpha, consider using a JPG instead of a PNG
- Alternatively, if your pipeline supports it, consider using Basis Universal for all textures. Be sure to pre-generate mipmaps when generating the file.
- If you're using a PNG, consider using the
hasalpha="true"
orhasalpha="false"
on your<AssetImage>
to avoid costly alpha detection - glTF models are the fastest to parse. OBJ, STL, and PLY parse moderately quickly but use more memory. FBX is slow to parse and triggers lots of GC during load time.
Yes, and we encourage you to do so! The default presence server is community-run and can only support 20-30 people in a room before it starts reaching its limits. If you're planning on hosting worlds and expect to get a lot of traffic, we recommend you set up your own server. You can also add custom behavior to your server, see https://github.com/janusvr/janus-server for more details
You can override which server your room uses by using the server="my.host.name"
parameter. You can also override the port with port="12345"
, and the network sync rate with rate="50"
(default is 200ms, or 5 syncs per second)
<room server="my.host.name" port="12345" rate="50">
...
</room>
Please note, due to browser security restrictions, you almost certainly want to be running your presence server with a valid SSL certificate signed by a trusted authority. Browsers have moved lately to block "powerful features" like WebXR from being used on HTTP sites, citing concerns that HTTP might allow bad actors to "hijack your senses" and cause lasting physical and mental damage to users.
What this means for you is that, if you're hosting Janus content on your own servers and want your users to be able to use the content in VR or AR, you're going to need to ensure that the content is hosted on HTTPS. This means your presence server must also be set up to use SSL, and your client must be configured to connect via a wss:// port. Setting this up is a non-trivial task which varies based on your own hosting situation, so it's outside the scope of this document, but the two recommended free solutions are to use Lets Encrypt to get a free certificate then configure your presence server to use those certs, or to use a service like https://ngrok.com which acts as a public SSL tunnel to your private servers. If you're using a cloud provider they may offer their own certificate service, and if you've purchased or plan to purchase a commercial SSL certificate, you can use those as well.
This isn't an easy question to answer, it's easier to just say what we've observed from having run the service for several years now.
Janus has for as long as I can remember run its server on an AWS t2.micro instance (1 vCPU, 1GB ram, baseline 0.06 Gbit/s bandwidth, burstable to ~5 Gbit/s). The server is quite stable, I can only think of a small handful of times it needed rebooting over the past 5 years. Everything below applies to our experience running the service in that environment, but it is of course possible to run it in a more powerful environment and get better results. I'll come back to that, though.
Our most popular rooms have peaked at somewhere on the order of ~1500 users per day. The server starts to have problems if more than about 40 people are in one room, due to the nature of how the pubsub networking layer works.
When users load a room, we connect to the presence server that's being used for that room (we provide a default, but anyone can override per-room), and subscribe to a message channel associated with the URL of that room. From then on, the server will relay any of our presence packets to anyone else who's subscribed, and will send us all of the packets from each of those users as they come in.
This means that within a room, the traffic scales at a rate of b = p * (n - 1)^2
- where n
is the number of users, p
is average byte rate per second of presence packets for each user, and b
is the total bandwidth needed by the server to perform that type of syncing. So for 2 users, 10 users, even 20 users that's not too bad, but once you start hitting 30, 40, or 50+ users in the room, the bandwidth and memory requirements start to balloon fast enough that the t2.micro instance can't keep up.
Larger AWS instances would help with memory and available network bandwidth, and I'd expect the server software to be able to scale to somewhere in the 150+ range with that change alone. However, then you're starting to run into another set of limits: single-core capacity, and client rendering.
The presence server is a single threaded application, so while it would benefit from more memory and faster bandwidth, there's still a cap on how many packets one server instance can process per second. There is some experimental code where you can configure your presence server to run in a cluster, which uses a redis backend to communicate and synchronize state across any number of presence servers. Using this configuration, you could run multiple copies of the presence server software on your server (one per CPU core), or you could even expand this to have multiple server instances all running multiple cores worth of presence servers, and the redis backend would keep them all in sync with each other, allowing you to scale well past the limits of a single server or datacenter.
However, while that functionality exists, it hasn't seen anywhere near the type of real-world use that the single-threaded codepath has. We know that the cluster mode works in basic testing, but the nature of these sorts of things is that scaling them is a matter of running it until you find a new limit, and then fixing the problem or designing around it for your use case.
That being said, we were able to serve everyone's needs, hundreds of thousands of users navigating tens of thousands of worlds over 6 years, from the cheapest instance AWS has to offer and only rarely had this kind of issue. If you're ever using JanusWeb in a scenario where you need to unlock more scale, please do let me know :)
By default, only the player's position is synced over the network each frame. If your script makes any changes to objects in the world, or spawns new objects, it can set sync="true"
on the object which will cause the client to send the change over the network to the presence server, where it is duplicated out to everyone else.
let ball = room.createObject('object', {
id: 'sphere',
pos: V(player.pos),
vel: V(0,0,-5),
scale: V(.2),
sync: true
});
In dev builds there is also an autosync="true"
parameter which will continuously sync the object over the network, eg, so that physically-simulated objects get synced every frame. Use autosync="true"
instead of ```sync="true"```` to enable this behavior. This will be available in the next release after 1.5.2.