HTML/DOM GUI instead of BABYLON.GUI

Hello, everybody,

I’m standing in front of a project with a very complex user interface and of course I know that BABYLON brings its own GUI system, but I’m afraid I can’t avoid building the UI in HTML.

But first I wanted to ask if there are any reasons against it. Performance slumps for example. Or maybe someone already has experience in combining Babylon-Canvas with the HTML DOM?

I have read some comments on the topic, in connection with other game engines, in which it was said that it was bad style. On the other hand the three.js community just dont gives a f*ck.js and uses ordanary dom elements for gui’s :smiley:

2 Likes

Hi!

I’m not sure if you can use HTML DOM UI with babylon.js, but I bet there is a way. I am currently using several HTML DOM events like mousemove, keyup, and keydown in my project, so I don’t see why you couldn’t.

I’ve seen tutorials on how to change html elements with js, and maybe that might work in your case.

Best of Luck,

Givo

EDIT: Do you mean three.js? That was the library I was using before babylon.js. I’ve stuck with babylon.js for a while now because of the community. Three.js had demos and a how to get started, and then nothing else except incomplete documentation. I hope you like it here!

PS. I’m working on a game, and the community has helped so much, I’m thinking about putting a couple people in the credits.

A few forum tips, too.

  1. us the playground, (babylonjs-playground.com) to replicate issues that you are having, unless it is like your question here. The playground can be played with, and eventually you might have an answer that is a playground. Also, the playground can auto-finish you syntax and stuff, so you can type everything correctly!

  2. the babylon.js documentation is amazing. It covers EVERYTHING. check out the api classes on the engine (Engine - Babylon.js Documentation) it really does have everything.

I hope to see you around more! Hope this helps!:smiley::smiley:

1 Like

Hi Steffen,

First of all if you want your UI to be available in VR mode Babylon GUI is the only choice you have.

Now if VR is not required there are 2 cases :

  1. Screen mode (= 2D)
    If you need simple UI - a menu for example - you can (should) use HTML/CSS. There’s no performance issue and it’s more easy to setup and maintain.
  2. Scene world (= 3D)
    If you want your GUI to match the 3D world, Babylon GUI is the only way.

So in this demo : Babylon.js - GUI demo

  • The right menu could be done with HTML/CSS…if you don’t need it in VR headsets.
  • Tooltips on the spheres + checkboxes for options => Babylon GUI (anyway it would be nearly impossible in HTML/CSS)
3 Likes

Thanks a lot for all the fast requests.
The UI iam planning is a tpical, as you called it, screenmode UI. The main menu and more or less complex diolog boxes ontop of the babylon canvas. So my plan was to do a wrapper div element for the ui and put it ontop with position absolute and an higher zindex. Than i can do all ordanary html/css stuff…

And yes Givo i meant three.js :slight_smile:

I know what you mean when you say

I would say there really isn’t a big community around three.js. just stackoverflow posts and this guy: https://www.youtube.com/channel/UCkJYfCcenyjHr3DZ9JWHbkQ.

Sure, there are others, but Three.js isn’t for beginners, like myself, and I’ve personally found more success with babylon.js than with three.js.

Good luck on the UI!

Givo

1 Like

Hello and welcome to the family @Steffen!

You can definitely use DOM element on top of Babylon.js canvas. There is no problem at all (@sharp describes it pretty well)

CSS+HTML+BABYLON is definitely your way to go :smiley:

1 Like

Hi there!
Sorry for picking up an old Topic, but I seem to be stuck.
While I get, that I can put HTML over the Canvas, I cant manage to overlay this with transparent Background. What i aim to achieve is Semi-Transparent Buttons and Text over the Babylon Canvas.

Has anybody a simple example for me maybe?

Thank you all so much in advance!

The Virtual Joysticks work like that but you have to be careful with the z-index because the upper canvas will catch all the events

Thanks! I’ll look into it!

the key is a little CSS to put any html element ontop of your canvas.
use an higher zindex than your canvas and position:absolute; to place elements in corners

another useful thing is PEP to catch inputs across all plattforms, special if you want to go mobile:
https://github.com/jquery/PEP

3 Likes

I use HTML in my UI and have not run into any issues - yet. If anything, I suspect using HTML for your UI will be faster for the canvas to render than using a 2d rendered UI framework inside the canvas.

I know this is a little bit old, but hopefully this might help someone else having the same question. While using the DOM is totally possible with BJS, BJS GUI is a real alternative. I have used it in all my projects and it is fairly easy to setup and behave as we want it to. The main issue I faced while using it, was building complex GUI’s. Not much in terms of performance but in terms of development complexity. Developing UI’s in Javascript is very non scalable, so I developed an XML loader to basically load GUI from XML. I am using it myself in a quite complex project. You can read more about it here : Use the Xml Loader - Babylon.js Documentation.

What motivated you to use BJ UI instead of HTML/CSS?

I’m also not sure I agree that building UIs in JS is not scaleable. Perhaps not as extensible as XML, but with extensibility often comes lack of flexibility.

I simply wanted to have the UI part of the same BJS “ecosystem”. BJS supported it so it felt right to use it. I would agree to the fact that the DOM is also a viable choice. I just went with BJS. Performance wise, with or without the GUI the performance does not seem to suffer, at least in Chrome. Firefox in the other hand, is not usable with BJS GUI. They do have some issues though in general support of WebGL.

Regarding the scalability of the programatically built UI, be it in JS or not, you can scale it of course, but it is a real pain in the ass doing so. If the UI would be built in a OOP style where you would keep controls as class attributes and build controls with functions, working on UI would be a very slow and tedious process, don’t you think? Just to add an attribute you would need to add a line of code. Adding a control would mean 4+ lines if you would like to style it. Imagine doing the same with XML or any Markup language. Achieving POC is way faster if the layout building is separate, done through a markup like XML, JSON or whatever it is.

Again, this depends on the UI’s complexity. I have had to build quite some complex interfaces with BJS and I would dread the moment when I had to add a new layout. But you are also using HTML, correct? You are not building everything through the DOM’s API, are you? The concept here is separation of concerns. Let JS handle business logic while XML/HTML/JSON handles the layout. Dynamic data could then be handled both in JS or Layout side.

class MENU{
	constructor(){		
		
		let dom = document.getElementById('injected-block')
		if(dom){
			dom.remove()
		}	
		
		dom = document.createElement('div')
		dom.classList.add('menu-block')
		dom.setAttribute('id', 'injected-block')
		document.body.appendChild(dom)
		
		let objectList = document.createElement('div')
		objectList.classList.add('menu-object-list')
		dom.appendChild(objectList)
		
		let objectOptions = document.createElement('div')
		objectOptions.classList.add('menu-object-options')
		dom.appendChild(objectOptions)
		
		let menuMain = document.createElement('div')
		menuMain.classList.add('menu-main-options')
		dom.appendChild(menuMain)

		this._dom = dom
		this._objectList = objectList
		this._objectOptions = objectOptions
		this._menuMain = menuMain
		
		this._objects = []
		
	}
	
	
	mapper(item){
		console.log('MAPPING', item)
		let mappedOut = document.createElement('div')
		mappedOut.classList.add('group')
		
		let map = item.map
		
		for(var k in map){
			if(map[k]['type']=="group"){
				this.grouper(map[k], item, mappedOut)
			}else{
				if(typeof map[k] === 'object' && map[k] !== null) {
					this.itemizer(map[k], item, mappedOut)
				} 
			}
		}		
		
		return mappedOut
	}	
	
	grouper(map, item, group){
		console.log('FOUND GROUP!', map)
	}
	
	itemizer(map, item, group){
		console.log('FOUND ITEM!', map)
		let target = item.target
		if(map.chain){
			console.log('Following Chain', map.chain)
			console.log('target was:', target)
			let chain = map.chain.split(',')
			for(let i=0; i<chain.length; i++){
				target = target[chain[i]]
			}
			console.log('target is Now:', target)                
        }
		
		let domObj = document.createElement('div')           
		   
		if(map.name && map.name != ''){
			let title = document.createElement('div')
			title.classList.add('item-title')
			title.innerHTML = map.name
			domObj.appendChild(title)			
		}		
		
		switch(map.type){
                case 'vector3' : 
                domObj.appendChild(new MENU.ITEMS.VECTOR3(map, item.target))
                break
				case 'number' : 
                domObj.appendChild(new MENU.ITEMS.NUMBER(map, item.target))
                break
				case 'bool' : 
                domObj.appendChild(new MENU.ITEMS.BOOL(map, item.target))
                break
        }
		
		domObj.classList.add('item')
		group.appendChild(domObj)		
		
	}
	
	addObject(target, map){
		let object = new MENU.OBJECT(target, map, this)
		console.log(object)
	}
	
	addPane(target, map){
		let pane = new MENU.PANE(target, map, this)
		console.log(pane)
	}
	
	
	get dom(){
		return this._dom
	}
	set dom(v){
		this._dom = v
	}	
	get objectList(){
		return this._objectList
	}
	set objectList(v){
		this._objectList = v
	}
	get objectOptions(){
		return this._objectOptions
	}
	set objectOptions(v){
		this._objectOptions = v
	}
	get menuMain(){
		return this._menuMain
	}
	set menuMain(v){
		this._menuMain = v
	}	
}

MENU.OBJECT = class{
	constructor(target, map, parent){
		this._map = map
		this._target = target
		this._parent = parent
		
		let domObj = document.createElement('a')
		domObj.setAttribute('href', '#')
		domObj.classList.add('object')		
		domObj.innerHTML = target.name+':'+target.id
		
		var self = this
		
		domObj.addEventListener('click', (e)=>{
			let mapped = parent.mapper(self)			
			
			while (parent.objectOptions.firstChild) {
				parent.objectOptions.removeChild(parent.objectOptions.firstChild)
			}
			
			let title = document.createElement('div')
			title.innerHTML = target.name+':'+target.id
			title.classList.add('object-title')
			
			parent.objectOptions.appendChild(title)	
			parent.objectOptions.appendChild(mapped)
		})		
		
	
		parent.objectList.appendChild(domObj)

		return this
	}
	
	get map(){
		return this._map
	}
	get target(){
		return this._target
	}
	get parent(){
		return this._parent
	}

}

MENU.PANE = class{
	constructor(target, map, parent){
		this._map = map
		this._target = target
		this._parent = parent
				
		let mapped = parent.mapper(this)
		mapped.classList.add('pane')	
	
		parent.menuMain.appendChild(mapped)	

		return this
	}
	
	get map(){
		return this._map
	}
	get target(){
		return this._target
	}
	get parent(){
		return this._parent
	}
}

MENU.ITEMS = {}

const cloneMap = (map)=>{
	let t = {}
	for (let prop in map) { 
		if (map.hasOwnProperty(prop)) { 
			t[prop] = map[prop]
		} 
	}
	return t	
}
MENU.ITEMS.VECTOR3 = class{
	constructor(map, target){
		let group = document.createElement('div')
		group.classList.add('vector3')
		
		let tx = cloneMap(map), ty = cloneMap(map), tz = cloneMap(map)	
		tx.name = 'x'		
		group.appendChild( new MENU.ITEMS.NUMBER(tx, target[map.name]))
		
		ty.name = 'y'
		group.appendChild( new MENU.ITEMS.NUMBER(ty, target[map.name]))
		
		tz.name = 'z'
		group.appendChild( new MENU.ITEMS.NUMBER(tz, target[map.name]))

		return group		
	}
}

MENU.ITEMS.NUMBER = class{
	constructor(map, target){
		console.log('Making Number Input:', map, target)
		
		let input = document.createElement('input')           
		input.setAttribute('type', 'number')
		
		if(map.values){
			if(map.values.min){
				input.setAttribute('min', map.values.min)
			}
			if(map.values.max){
				input.setAttribute('max', map.values.max)
			}
			if(map.values.step){
				input.setAttribute('step', map.values.step)
			}
		}
            
		input.setAttribute('value', target[map['name']])
		
		let update = ()=>{
			target[map['name']] = parseFloat(input.value)
		}
         
		input.addEventListener('change', update)
		input.addEventListener('input', update)
		input.addEventListener('wheel', update)
		
        return input
	}
}

MENU.ITEMS.BOOL = class{
	constructor(map, target){
		console.log('Making Bool Input:', map, target)
		
		let input = document.createElement('input')           
		input.setAttribute('type', 'checkbox')
		
		input.checked = target[map['name']]

		let update = ()=>{
			target[map['name']] = input.checked
		}
         
		input.addEventListener('change', update)
		
        return input
	}
}

MENU.MAPS = {
	'mesh' : {
		type: 'group',
		position : { name:"position", type:"vector3", values:{step:0.5} },
		rotation : { name:"rotation", type:"vector3", values:{step:0.1 } },
		scaling : { name:"scaling", type:"vector3", values:{step:0.1 } }		
	}
}

.menu-block{
	position: absolute;
    z-index: 1;
    width: auto;
    height: auto;
    background: rgba(180,180,180, 0.8);
    top: 1em;
    right: 0.8em;
}

.menu-object-list{
	position: relative;
    min-height: 1em;
    width: 320px;
    padding: 0.12em;
    margin: 0.2em;
	border:1px solid rgba(0,0,0,0.5);
	background: rgba(255,255,255, 0.5);
}

.menu-object-options{
	position: relative;
    padding: 0.12em;
    margin: 0.2em;
	border:1px solid rgba(255,255,255,0.5);
	background: rgba(0,0,0, 0.5);
}

.menu-main-options{
	position: relative;
	background: rgba(180,180,180, 0.2);
	border:1px solid rgba(255,255,255,0.5);
	min-height: 1em;
	padding: 0.12em;
    margin: 0.2em;
}

.menu-object-list .object{
	text-decoration: none;
    width: 100%;
    display: inline-block;
    border: 1px solid rgba(0,0,0,0.2);
    padding: 0.1em;
	cursor:pointer;
}

.menu-object-list .object:hover, .menu-object-list .object:active{
	text-decoration: none;
    width: 100%;
    display: inline-block;
    border: 1px solid rgba(255,255,255,0.2);
	background-color:rgba(0,0,0,0.2);
}

.menu-block input[type=number] {
    display: block;
    width:100%;
}

.vector3{
	width: 320px;
}

.vector3 input{
    width: 32% !important;
    display: inline-block !important;
}

Usage:

var menu = new MENU()
var box = BABYLON.Mesh.CreateBox("box", 1, scene)

menu.addObject(cloudBox, MENU.MAPS['mesh'])

let options = {
    op1: 0,
    something: BABYLON.Vector3.Zero(),
    something2: false
}

menu.addPane(options , {
					type:'group',
					name:'Options',
					op1: { name:"Option1", type:"number", values:{ min: 0, step:0.1 } },
					something: { name:"something1", type:"vector3", values:{step:0.1 } },
					something2 : { name:"something2", type:"bool" },
				})

If you need an explanation let me know.

1 Like

Thanks for the snippet. I am aware of this kind of development and UI creation. Still too tedious if you’d ask me. Is this only the menu? Let’s suppose you would use reusable components ( been there, done that ). How readable would that be for someone else entering the project. Just because we can do something doesn’t mean we should. :slight_smile: . I agree it varies on context and application though.

Its reusable. You provide the “map” and the script does the rest. It’s not super dynamic but it handles most development stuff I have needed when creating scenes.

I’m sure it can be tailored to your needs.

Thanks again, I am pretty sure this will prove useful to someone :slight_smile: . However, I had the same kind of system in place with BJS GUI and switched it all in XML.

I’ve abstracted away most of the DOM-altering portions of my GUI components. For example, in my game, I obviously have many different windows floating around the screen: target health bar, self healthbar, a spellbar, inventory screen, etc. So when I instance a new GUI window (let’s take the inventory window for example), I of course instance a new JS class (let’s just call it InvWindow), one of the first things it does is

// "this" is the InvWindow class or function/object, whatever you want to call it.
parts.integrate.apply(this, "part[for=inventory]");
this.divContent.appendTo(document.body);

The “parts” object is just a simple dictionary of various portions of HTML, each “class” having its own .HTML file, all of which get compiled into one massive HTML parts dictionary which gets read by the client upon page load, looking something like:

<parts>
    <part for="InvWindow">
        <div data-js-name="divContent">
            <div title="whatever">Hello</div>
            <div class="body">
                ... a bunch more html in here for inventory slots, bag slots, money, etc ....
            </div>
            <div class="btn">Close Window</div>
        </div>
    </part>
</parts>

event handlers are added with other special keywords:

<div class="button" data-js-name="btnCloseWindow" data-js-events="click">Close Window</div>

Which, during “integrating” in the first code block in this post, automatically binds an event handler to the pre-determined function handler name:

this.onBtnCloseWindowClicked = function(event) {
    this.divContent.remove();
}

Any “dynamic” content in a window should be represented in its own class as well, and integrated just like above. For example, I have a window called WndTrack which shows me every entity in a zone. Clearly I’ll need to be adding and removing DIVs from a “list” DIV in WndTrack, so my WndTrack window needs a child class/object called TrackItem which, of course, does something along the lines of parts.integrate.apply(this, [for=“WndTrackListitem”]). The collection is managed by the parent WndTrack class (this.allListItems / this.allListItemsByEntityID), but there should be no DOM add/removes being done outside of the “parts” system. I will admit that I do some CSS manipulation and .hide() and .show() (jQuery) on elements for visual effects, but again, all structure-based operations are off the table in these classes.

Anyway, It’s just my home-grown way of repeating some of the current trends in template-based web development. One thing I hate about the modern frameworks is that, to build anything of reasonable complexity, you end up writing so much code/logic into the HTML template: When should an element have this css class vs that css class? Oh just add some special magic attribute to the HTML and write some code inside the my-attribute="…" portion. Like wtf? My attributes above are just connectors, no logic, no code.

Despite me lobbying for HTML based GUIs, I have not written off using BJS GUI at some point, simply because I know people love modding UIs and I’m not sure how far my current design will go in terms of letting people design their own UIs with different physical structures. Being a one-man-band, it would be a significant detour to table the game development and learn the BJS GUI.

Main reason for BJS GUI are embedding it in the 3d scene like on character infos in game and also VR where html is simply not available.

3 Likes