Sunday, April 29, 2012

WebGL Texture Utils and Building Require.js libs

Two things to go over today:

One, I've got a new library out. WebGL Texture Utils. It's, as you might imagine, a collection of utilities to assist with loading WebGL textures of several different forms. Actually the repo for it has been available for a little bit now, but it's only today that I feel things have actually gotten to the point where it's really useful.

There's a lot of odds and ends squirreled away in there, and I'm definitely going to need to work on documenting it better in the near future, but for the time being the easiest way to use it is via the TextureLoader class. Usage is stupidly simple:
<script src="texture-util.js"></script>
<script>
    // Setup WebGL context, then...
    var textureLoader = new TextureUtil.TextureLoader(gl);
    
    // Simple texture load
    var texture1 = textureLoader.load("texture1.jpg");

    // Texture load with callback when finished
    var callback = function(texture) { /* Do something */};
    var texture2 = textureLoader.load("texture2.png", callback);

    // Overriding the extension
    var texture3 = textureLoader.load("texture3.php", callback, "gif");

    // Loading into an existing texture handle, control mipmap creation
    var texture4 = gl.createTexture();
    var buildMipmaps = true;
    textureLoader.loadEx("texture4.dds", texture4, buildMipmaps, callback);
</script>
And yes, it does support DDS. The full list of supported formats is currently JPG, PNG, GIF, BMP, DDS, CRN (Crunch), and some TGAs. I don't actually advocate using TGAs in your WebGL apps, but I had the code available so why not? I'd like that list to grow in the future, specifically to include things like PVR and possibly some video formats. There's also a lot more in the way of configuration options I'd like to introduce, but this is a good first step.

And that brings me around to the second topic I wanted to talk about: Packaging the library.

I've grown quite fond of require.js, and have made it my go-to solution for dependency management in all of my recent personal projects. When looking at a library like this, however, there's no way I could justify releasing something that's dependent on it. There's far too many javascript module solutions out there for me to mandate the use of a specific one in order to use my library. People simply wouldn't use it. Still, I wanted to use it during development. So I went searching for a way to compile require.js modules into a standalone file.

I immediately looked into the require.js optimization tool. Unfortunately while it did a great job of compiling the dependencies into a single file it didn't actually remove the requirement for require.js. The FAQ for the tool did mention a way to use a library called Almond to distribute libraries without requiring user to include require.js, but it still did require users to call a "require" function in their own code to access the library. While the removal of a dependency was a positive, I wanted a method that wouldn't require users to adhere to a certain code style to use the library.

I turned to Twitter and asked for suggestions, getting several good ones in return. One that particularly struck me was a suggestion from a former colleague, Tobias Klipstein, to use the Closure Compiler with some AMD specific flags to build the lib. I liked this option, but unfortunately the tool failed when compiling my library so this was a bit of a non-starter. (The author of the flags in question does warn that they're not well tested, so it wasn't terribly surprising)

That did give me an idea, however. In order for closure to expose the library properly you would have to "export" the symbols you want to be available by assigning them to a key on the window object:
window["MyClass"] = MyModule.MyClass;
It occurred to me that I could pull off the same trick in the Require.js/Almond version of the code by introducing a new "exports.js" file that required the necessary dependencies and exported them in the same way:
require(["texture-util/loader"], function(TextureUtil) {
    window['TextureUtil'] = TextureUtil;
}, "export", true);
The trick here is that "true" at the end, which indicates that the require is synchronous. By compiling the library with that file included, now it resolves all the dependencies internally and makes the TextureUtil namespace available globally. Hooray!

(Interestingly enough, Franck Lecollinet suggested exactly this approach on Twitter pretty much immediately after I got it working and posted to Github. He also pointed out that by checking for the existence of define.amd you can structure the built library so that it's can cleanly be included as a standard script OR a require.js lib. Thanks Franck!)

So that's my current solution for packaging the library. It's easily buildable with a single command line, and easy to use pretty much anywhere so I'm happy with it. If possible in the future I would like to use the Closure Compiler method, since that would eliminate the need for the Almond shim and cut the file size down a bit, but that's a minor optimization in the grand scheme of things.

Thanks to everyone on Twitter that provided suggestions for the packaging issue, and thank you to Evan Parker for putting together the Crunch emscripten build that inspired me to flesh out the texture library and for allowing me to use his work as part of the library.

4 comments:

  1. Is dds currently only working in canary?

    ReplyDelete
  2. At the moment, yes, but it sounds like Firefox Nightly support is coming soon as well according to this: https://bugzilla.mozilla.org/show_bug.cgi?id=728017

    The library does provide limited fallbacks for manually decoding DXT1 textures (Again, thanks to Evan Parker) so the compatibility is a little wider than normal, but I would say that if you are starting a WebGL game or similar large-ish app DDS support should be fairly ubiquitous by the time you release. The real thing to watch out for is mobile support. Essentially every WebGL-enabled desktop out there will support DXT compression, but mobiles support a whole mess of competing formats. (Example: iPhones only support PVR natively)

    ReplyDelete
  3. Thanks, I'll probably end up using 2 versions of the same texture for now then. I'm not too concerned about browsers other than Chrome/Firefox for this case, as it'll just be a tech experiment.

    ReplyDelete
  4. Does this play nicely with Three.js?

    ReplyDelete