-
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.
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>
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.
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' });
}
});
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.
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.
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.