Friday, March 1, 2013

Well Intentioned Three.js Constructor Monkey Patching

‹prev | My Chain | next›

I apologize in advance for this post. I am going to do a bad thing.

I rather like the plugin approach that I have used of late to add labels to Three.js objects. By simply constructing new Label objects for my plugin, I am able to add labels to a simple planetary simulation:



Construction is straight-forward and intuitive:
  new Label(earth, "Earth");
  new Label(mars, "Mars");
I can even retain references to the label objects for later manipulation such as removing them after a few seconds:
  var m_label = new Label(mars, "Mars");
  setTimeout(function() {m_label.remove();}, 5000);
It is all fairly nice. In fact only one thing bothers me. And that one thing is going to lead me to do a bad thing.

What is that one thing? To create labels, the programmer is forced to invoke addPostPlugin() on the Three.js renderer object:
  // This is normal renderer code
  var renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  // This is not
  renderer.addPostPlugin(LabelPlugin);
I can live with that under normal circumstances—this seems to be how Three.js wants it, after all. But I do not want to have to explain this to readers of 3D Game Programming for Kids.

And so, I monkey patch the Three.js constructor:
  var OriginalWebGLRenderer = THREE.WebGLRenderer;
  THREE.WebGLRenderer = function(parameters) {
     var orig = new OriginalWebGLRenderer(parameters);
     orig.addPostPlugin(LabelPlugin);
     return orig;
  };
That is a horrible thing to do and could lead to all sorts of trouble if many plugin programmers did this. But it works.

Under normal circumstances, I might try to override once of the methods defined in the THREE.WebGLRenderer prototype. Unfortunately, that is not an option in this case since Three.js defines no methods on the prototype. THREE.WebGLRenderer is nothing but pure constructor with object methods assigned inside the constructor. It is a 7500+ line file containing nothing but a single constructor. I presume it is done this way for speed and to discourage people from doing what I am doing. I am properly chagrined to be doing this, but I do it anyway.

To work this into a proper library for the book, I also need to do the same for the CanvasRenderer that is used for most of it:
  var OriginalCanvasRenderer = THREE.CanvasRenderer;
  THREE.CanvasRenderer = function(parameters) {
     var orig = new OriginalCanvasRenderer(parameters);
     orig.addPostPlugin(LabelPlugin);
     return orig;
  };
I also take some time to play with the API a bit so that delayed removal of labels can be done by the plugin:
  new Label(earth, "Earth");
  new Label(mars, "Mars", {remove: 5});
A few more changes like that and I will be ready to extract this out into its own library.


Day #677

4 comments:

  1. Really looking forward to this library!

    ReplyDelete
    Replies
    1. Silly me, I see it's already published on the next page! Trying it out.

      Delete
    2. I have no idea how well it works with more recent versions of Three.js. I don't think I ended up using it in the book, so I kinda forgot about this. I created a GitHub repo for it if you'd like to make some changes: https://github.com/eee-c/threejs-labels. Let me know how it goes!

      Delete
    3. Great. I submitted a pull request that makes it work with r66.

      Delete