1 | **NO LONGER ACTIVELY MAINTAINED.**
|
2 |
|
3 | mdx-m3-viewer
|
4 | =============
|
5 |
|
6 | At the core of it, a 3D model viewer for MDX and M3 models used by the games Warcraft 3 and Starcraft 2 respectively.
|
7 |
|
8 | The viewer part handles the following formats:
|
9 | * MDX (Warcraft 3 model): extensive support, almost everything should work.
|
10 | * M3 (Starcraft 2 model): partial support.
|
11 | * W3M/W3X (Warcraft 3 map): partial support.
|
12 | * BLP1 (Warcraft 3 texture): extensive support, almost everything should work.
|
13 | * TGA (image): partial support, only simple 24bit images.
|
14 | * DDS (compressed texture): partial support - DXT1/DXT3/DXT5/RGTC.
|
15 | * PNG/JPG/GIF/WebP: supported by the browser.
|
16 |
|
17 | There are file parsers that the viewer depends on.\
|
18 | These don't rely on the viewer or indeed on even running in a browser.\
|
19 | They include:
|
20 | * MDX/MDL: read/write.
|
21 | * M3: read.
|
22 | * BLP1: read.
|
23 | * INI: read.
|
24 | * SLK: read.
|
25 | * MPQ1: read/write.
|
26 | * W3M/W3X/W3N: read/write, including all of the internal files.
|
27 | * DDS: read (DXT1/DXT3/DXT5/RGTC).
|
28 |
|
29 | There are all sorts of utilities that were made over the years.\
|
30 | These include things like...
|
31 | * The library's unit tester, which compares rendered results against stored images that were generated in the same way.
|
32 | * The MDX sanity test, which looks for errors and weird things in MDX models.
|
33 | * A Jass context that can...well, run Jass code. That being said, it really runs Lua code converted from Jass, on a JS Lua VM. What a tongue twiser. While it supports some Warcraft 3 natives, don't expect it run whole maps. Maybe in the future 😉
|
34 | * A utility that makes it possible to open Warcraft 3 maps in the vanilla World Editor, in cases were said maps used a non-vanilla editor with extended GUI, in which case they crash upon opening in the official editor.
|
35 | * etc.
|
36 |
|
37 | Finally, the library also comes with a bunch of clients.\
|
38 | A "client" in this context means external code that uses the library.\
|
39 | Most of these clients are simple and messy, since they were made as side projects while working on the library.\
|
40 | Most of these clients are also just wrappers around the viewer and the utilities, merely giving them an interface on a web page.\
|
41 | These include things like...
|
42 | * A simple example client.
|
43 | * The unit tester's page, which allows to run the unit tests, and to download the results.
|
44 | * The MDX sanity test's page, which visually shows the results of sanity tests, and other nifty things.
|
45 | * etc.
|
46 |
|
47 | ------------------------
|
48 |
|
49 | #### Building
|
50 |
|
51 | ```
|
52 | npm install mdx-m3-viewer
|
53 | npm run build
|
54 | ```
|
55 | This will generate `dist/viewer.min.js`.
|
56 |
|
57 | If you are using Typescript, you can also import anything in the library directly.\
|
58 | For example, if you only care about an MDX viewer, you could do the following:
|
59 | ```javascript
|
60 | import ModelViewer from 'mdx-m3-viewer/src/viewer/viewer';
|
61 | import mdxHandler from 'mdx-m3-viewer/src/viewer/handlers/mdx/handler';
|
62 | ```
|
63 | This way, you don't import any parsers/handlers/utilities that are not needed.
|
64 |
|
65 | ------------------------
|
66 |
|
67 | #### Getting started
|
68 |
|
69 | 1. Run the given demo HTTP server `npm run serve`.
|
70 | 2. In your browser, open `http://localhost/clients/example`.
|
71 | 3. You can also check the other more advanced clients.
|
72 |
|
73 | ------------------------
|
74 |
|
75 | #### Usage
|
76 |
|
77 | You can import the viewer in different ways:
|
78 | ```javascript
|
79 | // webpack export in the browser.
|
80 | new ModelViewer.default.viewer.ModelViewer(canvas);
|
81 |
|
82 | // require/import the library.
|
83 | const ModelViewer = require('mdx-m3-viewer'); // CommonJS.
|
84 | import ModelViewer from 'mdx-m3-viewer'; // ES6.
|
85 | new ModelViewer.viewer.ModelViewer(canvas);
|
86 |
|
87 | // require/import something directly.
|
88 | const ModelViewer = require('mdx-m3-viewer/src/viewer/viewer'); // CommonJS.
|
89 | import ModelViewer from 'mdx-m3-viewer/src/viewer/viewer'; // ES6.
|
90 | new ModelViewer(canvas);
|
91 |
|
92 | ```
|
93 |
|
94 | All code snippets will use the names as if you imported them directly to avoid some mess. See the examples for actual namespacing.
|
95 |
|
96 | First, let's create the viewer:
|
97 | ```javascript
|
98 | let canvas = ...; // A <canvas> aka HTMLCanvasElement object.
|
99 |
|
100 | let viewer = new ModelViewer(canvas);
|
101 | ```
|
102 |
|
103 | If the client doesn't have the WebGL requierments to run the viewer, an exception will be thrown when trying to create it.
|
104 |
|
105 | When a new viewer instance is created, it doesn't yet support loading anything, since it has no handlers.\
|
106 | 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).\
|
107 | When you want to load something, the viewer will select the appropriate handler, if there is one, and use it to construct the object.
|
108 |
|
109 | Let's add the MDX and BLP handlers.
|
110 |
|
111 | ```javascript
|
112 | viewer.addHandler(handlers.mdx);
|
113 | viewer.addHandler(handlers.blp);
|
114 | ```
|
115 |
|
116 | 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.
|
117 | ```javascript
|
118 | let scene = viewer.addScene();
|
119 | ```
|
120 |
|
121 | Finally, let's move the scene's camera backwards a bit.
|
122 | ```javascript
|
123 | scene.camera.move([0, 0, 500]);
|
124 | ```
|
125 |
|
126 | The viewer class acts as a sort-of resource manager.\
|
127 | Loading models and textures happens by using handlers and `load`, while other files are loaded generically with `loadGeneric`.
|
128 |
|
129 | For handlers, the viewer uses path solving functions.\
|
130 | You supply a function that takes a source you want to load, such as an url, and you need to return whatever you want loaded by the viewer.\
|
131 | The load function itself looks like this:
|
132 |
|
133 | ```javascript
|
134 | let resourcePromise = viewer.load(src, pathSolver[, solverParams])
|
135 | ```
|
136 |
|
137 | In other words, you give it a source, and a promise to a resource is returned, where a resource in this context means a model or a texture.
|
138 |
|
139 | The source can be anything - a string, an object, a typed array, something else - it highly depends on your code, and on the path solver.
|
140 |
|
141 | The path solver is a function with this signature: `function(src[, solverParams]) => finalSrc`, where:
|
142 | * `src` is the source you gave the load call, or one given by the resource itself when loading internal resources.
|
143 | * `finalSrc` 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, typically an ArrayBuffer or a string.
|
144 |
|
145 | Generally speaking, you'll need a simple path solver that expects urls and prepends them by some base directory or API url.\
|
146 | There are however times when this is not the case, such as loading models with custom textures, and handling both in-memory and fetches in the same solver as done in the map viewer.
|
147 |
|
148 | For information about `solverParams`, see the [solver parameters section](#solver-params-reforged-and-map-loading).
|
149 |
|
150 | So let's use an example.
|
151 |
|
152 | Suppose we have the following directory structure:
|
153 |
|
154 | ```
|
155 | ├── index.html
|
156 | └── Resources
|
157 | ├── model.mdx
|
158 | └── texture.blp
|
159 | ```
|
160 |
|
161 | Where `model.mdx` uses the texture `texture.blp`.
|
162 |
|
163 | Let's see how a possible path solver could look.\
|
164 | I'll make it assume it's getting urls, and automatically prepend "Resources/" to sources.
|
165 |
|
166 | ```javascript
|
167 | function myPathSolver(path) {
|
168 | return "Resources/" + path;
|
169 | }
|
170 | ```
|
171 |
|
172 | Now let's try to load the model.
|
173 |
|
174 | ```javascript
|
175 | let modelPromise = viewer.load("model.mdx", myPathSolver);
|
176 | ```
|
177 |
|
178 | This function call results in the following:
|
179 |
|
180 | 1. myPathSolver is called with `"model.mdx"` and returns `"Resources/model.mdx"`.
|
181 | 2. The viewer starts the fetch, and emits the `loadstart` event.
|
182 | 3. A promise is returned.
|
183 | 4. ...time passes until the file finishes loading...
|
184 | 5. The viewer detects the format as MDX.
|
185 | 6. The model is constructed successfuly, or not, and sends a `load` or `error` event respectively, followed by the `loadend` event.
|
186 | 7. In the case of an MDX model, the previous step will also cause it to load its textures, in this case `texture.blp`.
|
187 | 8. myPathSolver is called with `"texture.blp"`, which returns `"Resources/texture.blp"`, and we loop back to step 2, but with a texture this time.
|
188 |
|
189 | Once the promise is resolved, we have a model, however a model in this context is simply a source of data, not something that you see.\
|
190 | The next step is to create an instance of this model.\
|
191 | Instances can be rendered, moved, rotated, scaled, parented to other instances or nodes, play animations, and so on.
|
192 | ```javascript
|
193 | let instance = model.addInstance();
|
194 | ```
|
195 |
|
196 | Let's add the instance to the scene, so it's rendered:
|
197 | ```javascript
|
198 | scene.addInstance(instance);
|
199 | // Equivalent to:
|
200 | instance.setScene(scene);
|
201 | ```
|
202 |
|
203 | Finally, we need to actually let the viewer update and render:
|
204 | ```javascript
|
205 | (function step() {
|
206 | requestAnimationFrame(step);
|
207 |
|
208 | viewer.updateAndRender();
|
209 | }());
|
210 | ```
|
211 |
|
212 | ---
|
213 |
|
214 | Loading other files is simpler:
|
215 | ```javascript
|
216 | let resourcePromise = viewer.loadGeneric(path, dataType[, callback]);
|
217 | ```
|
218 |
|
219 | Where:
|
220 | * `path` is an url string.
|
221 | * `dataType` is a string with one of these values: `text`, `arrayBuffer`, `blob`, or `image`.
|
222 | * `callback` is a function that will be called with the data once the fetch is complete, and should return the resource's data.
|
223 |
|
224 | If a callback is given, `resource.data` will be whatever the callback returns.\
|
225 | If a promise is returned, the loader waits for it to resolve, and uses whatever it resolved to.\
|
226 | If no callback is given, the data will be the fetch data itself, according to the given data type.
|
227 |
|
228 | `loadGeneric` is a simple layer above the standard `fetch` function.\
|
229 | 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`.
|
230 |
|
231 | ------------------------
|
232 |
|
233 | #### Events and Promises
|
234 |
|
235 | As mentioned above, there are emitted events, and they can be listened to, using the NodeJS EventEmitter API:
|
236 | ```javascript
|
237 | viewer.on(eventName, listener)
|
238 | viewer.off(eventName, listener)
|
239 | viewer.once(eventName, listener)
|
240 | viewer.emit(eventName[, ...args])
|
241 | ```
|
242 |
|
243 | The event name can be one of:
|
244 | * `loadstart` - a resource started loading.
|
245 | * `load` - a resource successfully loaded.
|
246 | * `error` - something bad happened.
|
247 | * `loadend` - a resource finished loading, follows both `load` and `error` when loading a resource.
|
248 | * `idle` - all loads finished for now.
|
249 |
|
250 | For example:
|
251 | ```javascript
|
252 | viewer.on('error', (viewer, error, reason) => {
|
253 | console.log(`Error: ${error}, Reason: ${reason}`);
|
254 | });
|
255 | ```
|
256 |
|
257 | In addition there is `ModelViewer.whenAllLoaded()` which will trigger both on the `idle` event, and if there is currently nothing loading.
|
258 |
|
259 | ------------------------
|
260 |
|
261 | #### Solver Params, Reforged, and map loading
|
262 |
|
263 | As mentioned above, when loading resources, the `solverParams` parameter can be supplied.
|
264 | Solver parameters allow to give additional information about a load to the path solver.
|
265 | A client can supply its own solver parameters, and the handler implementations can supply their own parameters for internal resources.
|
266 |
|
267 | This is used by the MDX handler to request SD/HD resources from Reforged.
|
268 |
|
269 | It's also used by the map viewer to request SD/HD resources from Reforged, and to select the tileset.
|
270 |
|
271 | For example, let's suppose we want to load the Warcraft 3 Footman model, but with a twist - we want all three versions of it - TFT, Reforged SD, and Reforged HD.
|
272 |
|
273 | The MDX handler defines the parameters as such: `{reforged?: boolean, hd?: boolean}`.
|
274 |
|
275 | If `reforged` is falsy or doesn't exist, then it wants a TFT resource.\
|
276 | If `reforged` is true, then it wants a Reforged SD resource and...\
|
277 | If `hd` and `reforged` are true, then it wants a Reforged HD resource.
|
278 |
|
279 | Following this, the loading code can be something along these lines:
|
280 | ```js
|
281 | let modelTFT = viewer.load('Units/Human/Footman/Footman.mdx', mySolver);
|
282 | let modelReforged = viewer.load('Units/Human/Footman/Footman.mdx', mySolver, {reforged: true});
|
283 | let modelHD = viewer.load('Units/Human/Footman/Footman.mdx', mySolver, {reforged: true, hd: true});
|
284 | ```
|
285 |
|
286 | Note that you don't have to use these exact values.
|
287 | Rather, these are the values that will be sent by the MDX handler or map viewer, when loading internal textures, models, and generic files.
|
288 |
|
289 | What does the path solver do, then?
|
290 | As always, that depends on the client and the server.
|
291 | For example, the client may append the parameters as url parameters, which can be seen in the existing clients.
|
292 | The client can also completely ignore these parameters and return whatever resources it wants.
|