Skip to content

A WebGL viewer for MDX and M3 files used by the games Warcraft 3 and Starcraft 2 respectively.

License

Notifications You must be signed in to change notification settings

435352980/mdx-m3-viewer

 
 

Repository files navigation

NO LONGER ACTIVELY MAINTAINED.

mdx-m3-viewer

A 3D model viewer for MDX and M3 models used by the games Warcraft 3 and Starcraft 2 respectively.

  • The src folder has in it the following:

    The viewer folder contains all of the 3D viewer specific functions and classes. Built-in handlers exist for the following formats:

    • MDX (Warcraft 3 model): extensive support, almost everything should work.
    • M3 (Starcraft 2 model): partial support.
    • W3M/W3X (Warcraft 3 map): partial support.
    • BLP1 (Warcraft 3 texture): extensive support, almost everything should work.
    • TGA (image): partial support, only simple 24bit images.
    • DDS (compressed texture): partial support - DXT1/DXT3/DXT5/RGTC.
    • PNG/JPG/GIF: supported as a wrapper around Image.
    • GEO (a simple JS format used for geometric shapes): note that this is solely a run-time handler.
    • OBJ: partial support (more of an example handler).
    • BMP: partial support (more of an example handler).

    The parsers folder contains classes that know how to read (and some how to write) different file formats. The parsers can be used also outside of the context of a web browser or the viewer itself. These include:

    • MDX/MDL: read/write.
    • M3: read.
    • BLP1: read.
    • INI: read.
    • SLK: read.
    • MPQ1: read/write.
    • W3M/W3X/W3N: read/write, including all of the internal files.
    • DDS: read (DXT1/DXT3/DXT5/RGTC).
  • The common folder contains functions and classes that are relatively general, and are used by all other parts.

  • The utils folder contains mostly interesting functions and classes that are built on top of the parsers.

  • The clients folder has in it a couple of clients using the library to perform different operations, including the unit tester.

  • Finally, the examples folder contains simple examples.


Building

  1. Download and install NodeJS from https://nodejs.org/en/.
  2. Open a command prompt in the viewer's directory, and run npm install.
  3. Run webpack-prod to generate dist/viewer.min.js, with the API given under the global object ModelViewer.

You can also require/import the library or anything in it directly in a webpack project.


Getting started

The examples directory has simple examples, I highly suggest looking at it first.

In case you don't have an HTTP server:

  1. Open a command prompt, run npm install http-server -g.
  2. Once it is done, at any time go to the viewer's folder, fire up the command prompt, and run http-server -p 80.
  3. In your browser, open http://localhost/examples/.

Usage

You can import the viewer in different ways:

// webpack export in the browser.
ModelViewer = ModelViewer.default;
new ModelViewer.viewer.ModelViewer(canvas);

// require/import the library.
const ModelViewer = require('mdx-m3-viewer'); // CommonJS.
import ModelViewer from 'mdx-m3-viewer'; // ES6.
new ModelViewer.viewer.ModelViewer(canvas);

// require/import something directly.
const ModelViewer = require('path_to_viewer/src/viewer/viewer'); // CommonJS.
import ModelViewer from 'path_to_viewer/src/viewer/viewer'; // ES6.
new ModelViewer(canvas);

All code snippets will use the names as if you imported them directly to avoid some mess. See the examples for actual namespacing.

First, let's create the viewer:

let canvas = ...; // A <canvas> aka HTMLCanvasElement object.

let viewer = new ModelViewer(canvas);

If the client doesn't have the WebGL requierments to run the viewer, an exception will be thrown when trying to create it.

When a new viewer instance is created, it doesn't yet support loading anything, since it has no handlers. Handlers are simple JS objects with a specific signature, that give information to the viewer (such as a file format(s), and the implementation objects). When you want to load something, the viewer will select the appropriate handler, if there is one, and use it to construct the object.

Let's add the MDX handler. This handler handles MDX files, unsurprisingly. It also adds the BLP and TGA handlers automatically, since it requires them.

viewer.addHandler(handlers.mdx);
// Or if you want to be explicit:
viewer.addHandler(handlers.mdx.handler);

Next, let's add a new scene to the viewer. Each scene has its own camera and viewport, and holds a list of things to render.

let scene = viewer.addScene();

Finally, let's move the scene's camera backwards a bit.

scene.camera.move([0, 0, 500]);

The viewer class acts as a sort-of resource manager. Loading models and textures happens by using handlers and load, while other files are loaded generically with loadGeneric.

For handlers, the viewer uses path solving functions. You supply a function that takes a source you want to load, such as an url, and you need to return specific results so the viewer knows what to do. The load function itself looks like this:

let resource = viewer.load(source, pathSolver)

In other words, you give it a source, and a resource is returned. A resource in this context means a model or a texture.

The source here can be anything - a string, an object, a typed array, something else - it highly depends on your code, and on the path solver. Generally speaking though, the source will probably be a an url string.

The path solver is a function with this signature: function(source) => [finalSource, ext, isFetch], where:

  • source is the source you gave the load call.
  • finalSource is the actual source to load from. If this is a server fetch, then this is the url to fetch from. If it's an in-memory load, it depends on what each handler expects.
  • ext is the extension of the resource you are loading, which selects the handler to use. The extension is given in a ".ext" format. That is, a string that contains a dot, followed by the extension. This will usually be the extension of an url.
  • isFetch is a boolean, and will determine if this is an in-memory load, or a server fetch. This will usually be true.

So let's use an example.

Suppose we have the following directory structure:

index.html
Resources
	model.mdx
	texture.blp

Where model.mdx uses the texture texture.blp.

Let's see how a possible path solver could look. I'll make it assume it's getting urls, and automatically prepend "Resources/" to sources.

function myPathSolver(path) {
  // Prepend Resources/, and get the extension from the path.
  return ["Resources/" + path, path.slice(path.lastIndexOf('.')), true];
}

Now let's try to load the model.

let model = viewer.load("model.mdx", myPathSolver);

This function call results in the following:

  1. myPathSolver is called with "model.mdx" and returns ["Resources/model.mdx", ".mdx", true].
  2. The viewer chooses the correct handler based on the extension - in this case the MDX handler - sees this is a server fetch, and uses the final source for the fetch.
  3. A new MDX model is created and starts loading (at this point the viewer gets a loadstart event from the model).
  4. The model is returned.
  5. ...time passes until the model finishes loading...
  6. The model is constructed successfuly, or not, and sends a load or error event respectively, followed by the loadend event.
  7. In the case of an MDX model, the previous step will also cause it to load its textures, in this case texture.blp.
  8. myPathSolver is called with texture.blp, which returns ["Resources/texture.blp", ".blp", true], and we loop back to step 2, but with a texture this time.

Generally speaking, you'll need a simple path solver that returns the source assuming its an url but prepended by some directory, its extension, and true for server fetch. There are, however, times when this is not the case, such as loading models with custom textures, handling both in-memory and server-fetches in the same solver (used by the W3X handler), etc.

We now have a model, however a model in this context is simply a source of data, not something that you see. The next step is to create an instance of this model. Instances can be rendered, moved, rotated, scaled, parented to other instances or nodes, play animations, and so on.

let instance = model.addInstance();

Let's add the instance to the scene, so it's rendered:

scene.addInstance(instance);
// Equivalent to:
instance.setScene(scene);

Finally, we need to actually let the viewer update and render:

(function step() {
  requestAnimationFrame(step);

  viewer.updateAndRender();
}());

Loading other files is simpler:

let resource = viewer.loadGeneric(path, dataType[, callback]);

Where:

  • path is an url string.
  • dataType is a string with one of these values: text, arrayBuffer, blob, or image.
  • callback is a function that will be called with the data once the fetch is complete, and should return the resource's data.

If a callback is given, resource.data will be whatever the callback returns. If a promise is returned, the loader waits for it to resolve, and uses whatever it resolved to. If no callback is given, the data will be the fetch data itself, according to the given data type.

loadGeneric is a simple layer above the standard fetch function. The purpose of loading other files through the viewer is to cache the results and avoid multiple loads, while also allowing the viewer itself to handle events correctly, such as whenAllLoaded.


Async everywhere I go

The viewer tries to allow you to write linear code, even though many things are asyncronious.

Sometimes this is not possible, for example when you want to get the list of animations a model has. If the model wasn't loaded yet, the list will be empty.

There are two ways to react to resources being loaded: promises/callbacks, and events.

In addition, every resource has two loading hints: loaded, and ok. When a resource is loaded, it means that it doesn't need further processing by the viewer. It doesn't however neccessarily mean the resource loaded successfully. When a resource is ok, it means it actually loaded successfully and is ready for use.

Promises/Callbacks

Every resource has a whenLoaded([callback]) method that waits for it to load. If a callback is given, it will be used. Otherwise, a promise is returned. If the resource was already loaded, the callback/promise is immediately called/resolved.

The viewer itself has whenLoaded(resources[, callback]) which does the same when all of the given resources in an iterable have been loaded, and also whenAllLoaded([callback]), to check when there are no longer resources being loaded.

Some examples of callbacks/promises:

model.whenLoaded()
  .then((model) => {
    // Must be true!
    console.assert(model.loaded);

    // May be true.
    console.log(model.ok);

    // Assuming this is an MDX/M3 model, let's print all of its animation names.
    // If model.ok is false, this will print an empty line, since sequences is an empty array.
    console.log(model.sequences.map((sequence) => sequence.name));
  });

viewer.whenLoaded([model, otherModel])
  .then(([model, otherModel]) => {
    // Do something.
  });

viewer.whenAllLoaded((viewer) => {
  // Everything loaded
})
Events

Events are done with the NodeJS EventEmitter API:

resource.on(eventName, listener)
resource.off(eventName, listener)
resource.once(eventName, listener)
resource.emit(eventName[, ...args])

If a listener is attached to a specific resource, such as a model, it only gets events from that resource. If a listener is attached to the viewer itself, it will receive events for all resources.

The event name can be one of:

  • loadstart - a resource started loading.
  • load - a resource successfully loaded.
  • error - something bad happened.
  • loadend - a resource finished loading, follows both load and error when loading a resource.

Note that attaching a loadstart listener to a resource is pointless, since the listener is registered after the resource started loading. Attach it to the viewer instead.

Some examples of event listeners:

model.on('load', (model) => {
  // Must be true!
  console.assert(model.loaded);

  // Must be true!
  console.assert(model.ok);
  
  // Assuming this is an MDX/M3 model, let's print all of its animation names.
  console.log(model.sequences.map((sequence) => sequence.name));
});

model.on('loadend', (model) => {
  // Must be true!
  console.assert(model.loaded);

  // May be true.
  console.log(model.ok);

  // Assuming this is an MDX/M3 model, let's print all of its animation names.
  // If model.ok is false, this will print an empty line, since sequences is an empty array.
  console.log(model.sequences.map((sequence) => sequence.name));
});

// target is the resource for which the error occured.
// For global errors, this will be the viewer itself.
viewer.on('error', (target, error, reason) => {
  console.log(`Error: ${error}, Reason: ${reason}`, target);
});

Instances are also event emitters. An MDX/M3 instance will emit the seqend event every time it finishes its current animation.

instance.on('seqend', (instance) => {
  console.log(`Finished running sequence ${instance.model.sequences[instance.sequence].name}`);
});

About

A WebGL viewer for MDX and M3 files used by the games Warcraft 3 and Starcraft 2 respectively.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 99.4%
  • Other 0.6%