I was pleased to discover the power of Three.js plugins for myself last night. I do not think that I have them quite right. Or more precisely, I hope that I can improve upon the way in which I am using them.
The particular problem that I am trying to solve is a way to label three dimensional Three.js objects using two dimensional DOM elements. I have the plugin approach working to the point where I have to add each object/label pair to the renderer:
renderer.addPostPlugin(new Label(earth, "Earth"));
renderer.addPostPlugin(new Label(mars, "Mars"));
This gives me nice labels as the planets make their way around in their orbits:There are two related problems with this approach. First, adding a new label is a two step process: the programmer has to construct a label object and add with the
addPostPlugin()
method. Second, there is no way to remove a plugin—at least there is no easy way to do so.It occurs to me that Three.js plugins need simply respond to
init()
and render()
. In other words, I can create a global object to hold all labels: var LabelPlugin = {
labels: [],
add: function(l) {this.labels.push(l);},
init: function() {},
render: function() {
for (var i=0; i<this.labels.length; i++) {
var args = Array.prototype.slice.call(arguments);
this.labels[i].render.apply(this.labels[i], args);
}
}
};
The benefit of this approach is that I can add the LabelPlugin
as a Three.js plugin immediately after constructing the renderer and then forget about it: // This will draw what the camera sees onto the screen:
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.addPostPlugin(LabelPlugin);
It is still a bit of a bummer that there is no way to auto-register the plugin. Even after I extract this out into a separate library, users of this library will still have to manually renderer.addPostPlugin(LabelPlugin)
.Anyhow, none of this is of any use unless I add some labels to the
LabelPlugin.labels
property. I can update yesterday's Label
constructor to do just that: function Label(object, content) {
this.object = object;
this.content = content;
this.el = this.buildElement();
LabelPlugin.add(this);
}
Each Label
already has a render()
method. So, when these are added to LabelPlugin.labels
, LabelPlugin.render()
will in turn call each Label
's render()
method. In other words, to add a label to an element, all that I need do is construct a Label
object: new Label(earth, "Earth");
new Label(mars, "Mars");
The main benefit of this approach, aside from being a little cleaner, is that I can now remove labels. I add a remove()
method to the LabelPlugin
object to prevent the plugin from attempting to render removed labels: var LabelPlugin = {
labels: [],
init: function() {},
add: function(l) {this.labels.push(l);},
remove: function(l) {
this.labels = this.labels.filter(function (label) {
return label != l;
});
},
render: function() { /* ... */ }
};
Similarly, I define a remove()
method on the Label
class so that the DOM label is removed as well: Label.prototype.remove = function() {
this.el.style.display = 'none';
LabelPlugin.remove(this);
};
So, when my space simulation first starts, I can have labels on planets. I can then remove them 5 seconds later: new Label(earth, "Earth");
var m_label = new Label(mars, "Mars");
setTimeout(function() {m_label.remove();}, 5000);
That is of limited use with this particular simulation. I suppose that I could add a keyboard listener to toggle the display of labels—that might be of some benefit. But if these labels were used as speech bubbles for game characters, then the ability to remove them would be quite handy.(live code demo)
Day #676