flowtsohg / mdx-m3-viewer

A WebGL viewer for MDX and M3 files used by the games Warcraft 3 and Starcraft 2 respectively.
MIT License
130 stars 46 forks source link

Unknown tileset #54

Closed arcman7 closed 3 years ago

arcman7 commented 3 years ago

In viewer.ts line 282:

  async loadTerrainCliffsAndWater(w3e: War3MapW3e) {
    let texturesExt = this.solverParams.reforged ? '.dds' : '.blp';
    let tileset = w3e.tileset;

    let tilesetTextures = []; 
    let cliffTextures = [];
    let waterTextures = [];

    for (let groundTileset of w3e.groundTilesets) {
      let row = this.terrainData.getRow(groundTileset);

      this.tilesets.push(row);
      tilesetTextures.push(this.load(`${row.dir}\\${row.file}${texturesExt}`));
    }

This function works fine when loading up a map with the classic wc3 settings, but if I specify reforged, then I get an issue where the tileset strings inside of w3e.groundTilesets do not match whats available inside of this.terrainData. This happens for any frozenthrone map, or maps from hive. For example none of these values -

0: "Zdrt"
1: "Zdtr"
2: "Zdrg"
3: "Zbks"
4: "Zsan"
5: "Zbkl"
6: "Ztil"
7: "Zgrs"
8: "Zvin"

would be contained in this.terrainData's map object.

I'm not sure if this is a result of me missing using the library or something else.

flowtsohg commented 3 years ago

I was planning to add some basic reforged handling to maps, but never actually did, I don't quite remember what the differences are.

In truth, I got somewhat discouraged from working on the map viewer, because JS just isn't fast enough to actually handle maps. Small ones work fine, but open a big custom map and zoom out a little, and you'll quickly see how the browser handles it. There isn't much more to actually optimize on the code side that will speed it up in reality, because what really kills it are the simple things, like copying numbers into buffers every frame, and multiplying matrices for the skeleton animations.

With that being said, if it's important to you, I can perhaps update it a bit.

arcman7 commented 3 years ago

Do you have notes on where the current progress is? I happen to have access to some friends that might know a thing or two about this with regard to optimizations. Worst case scenario could we just use wasm scripts to get around this? Depending on the size of the calculations involved, wasm scripts can cut down the required number crunching time by about 3-4x. I tested this once when I was looking for a good rendering option in an ML project that trains sc2 bots. Here are the notes that I took.

Also, any chance you'd be interested in using a slack channel to facilitate these conversations? I feel like comment chains in the repo's issues might not be the best.

arcman7 commented 3 years ago

Another thing I noticed is that the moment you edit a .w3x map file using the blizzard map editor (for reforged) and save the file, it cannot be loaded in the browser anymore using your library. The memory just shoots up to about 4gigs on my computer before crashing. It's so odd lol.

flowtsohg commented 3 years ago

If you send me a map I can look.

The entire library should be written in C++ and compiled to WASM, but I am not going to do that.

Just to show the point, have a whole bunch of particles around, and the browser suffers. If you look at the particles code, you'll notice that...it does almost nothing, because it's mostly offloaded to the GPU. but even just copying a couple of numbers per particle every frame into a buffer can easily take >30% browser time given enough particles. If this was C++ I assume these types of things wouldn't take 0.1%, I doubt WASM will be that much better than JS in this regard, but I might be wrong (it can probably be somewhat faster on the matrix operations though).

arcman7 commented 3 years ago

We could use an already built wasm library for TensorFlow if it really is just basic number-crunching, we could even use the shader GPU portion of TensorFlow for the browser to speed up the matrix operations. I may not be understanding everything you're saying, but I specifically benchmarked various matrix operations using the wasm and gpu shader modules of the TensorFlow js libraries for rendering based operations.

As for a map to test out you can try this one: https://www.hiveworkshop.com/bundles/ancient-forest.167272/download?token=fe135ed61bd7fd929da2d8e95b64b5ed_167272_1607229237

I would start by testing it in your repos clients map example, and then afterward all you need to do is double click it and hit save once you're in the reforged editor.

flowtsohg commented 3 years ago

I only have 1.29.2 and 1.30.4 floating around, so no Reforged things, can you point me to a map?

And now the tl;dr part :)

Skeleton animations can't run on the GPU to the best of my knowledge, and I thought about this a lot in the past. There are many issues, like how would one even upload the animation keyframes to a shader, but even if you figure that out, ultimately we are talking about a hierarchy of nodes that need to be updated in sequence of parent->children->children->etc., while shaders and generally speaking GPGPU depends on random access and having no dependencies.

It's probably easy to write some WASM math functions that will run faster, but will that result in a real-world speed gain? without even trying I am incredibly doubtful, because the invocation itself needs to be taken into account, i.e. sending the arguments to WASM and getting back the result in JS. For an example, you can check how the BinaryStream class works...it's a huge mess of typecasting bytes rather than using the native DataView, because the latter, while being theoretically written in C++ and being a lot simpler and straightforward, is a lot slower because of the JS->C++->JS transitions. And this is still true to this day, after me and other people reported the slowness of DataView as issues to Firefox/Chrome, and the devs worked (took them a couple of years mind you...) on specifically optimizing these things. Maybe moving between JS and WASM costs less than JS and C++, but either way, the point stands - I am doubtful about optimizing small parts and hoping for any major change.

What does make sense is having the entire library as WASM by porting it to C++ and using emscripten, probably splitting out the loading code because that stuff can be in JS, especially with the recent change to promises where the viewer no longer needs to handle existing-but-not-loaded resources.

Am I going to bother doing that for a theoretical gain in performance when rendering whole maps of wc3 which is used by absolutely no one? nah.

flowtsohg commented 3 years ago

I also experimented with multithreading the viewer in the past, you can see that in the MT branch I never bothered to delete.

If I remember correctly, my conclusion from that time was that it's not really worth it without shared memory, and with shared memory it MIGHT be worth it. In theory it sounds like it could work well with shared memory, but shared buffers weren't implemented yet when I wrote that code, and shortly after the concept died due to that high precision timers and shared memory exploit, which resulted in both being killed by browser devs. If I am not mistaken both the high precision timer (performance.now() etc.) and shared buffers (new SharedArrayBuffer() etc.) are still not legit supported to this day, nor are devs interested in supporting them, but I didn't check in a while.

arcman7 commented 3 years ago

What do you mean by not legit supported? Both of those features are available in both Chrome and Firefox, although with Firefox the shared array buffer doesn't have the same signature in chrome. I've used the shared array buffer before in chrome a few years ago, worked fine then for me. That being said, I certainly don't want to push anything on you that you're not interested in, but I do really enjoy using it. It's my intention to continue building small games (just for fun) as a side hobby, and I'll be making extensive use of your library, but it's fair to question its value at large. I am going to spend some time to better understand what you're code does so that I can start digging into where the biggest costs/bottlenecks exist currently.

Edit: I just wanted to add that it's not that I'm expecting to find or think of anything you didn't already, it's just hard for me to visualize what you mean in terms of the node hierarchy and how it's hard to speed up the calculations involved.

arcman7 commented 3 years ago

Oh, and for the map, I just uploaded it here. The link will be valid for 7 days. It worked fine before I did anything in the reforged editor, and even then all I did was add one grunt unit and then remove it later. Still crashes in the browser.

flowtsohg commented 3 years ago

That map certainly crashes the browser, I'll check it later when I have time.

Looks like shared buffers were re-enabled this year, look at the "Security requierments" section here if you are interested. The high precision timer is still not actually a high precision timer since it was changed, it seems.

Either way it's not like having shared buffers makes this easy, the entire viewer part of the viewer has to be changed. The shared vs non-shared issue is mostly to keep everything clean (sending signals via the shared buffer, or constantly detaching buffers and moving them between threads), but it doesn't solve issues.

For instance, who loads models and how do you access them if they are on different WebWorkers.

Or consider the geometry particles. There you have one global object that updates all particles, and when rendering all that is required is to fill a buffer with a couple of numbers per particle. This is the perfect setup for multithreading, where a thread handles all of the particles and fills a shared buffer. But how can this actually happen, the second thread has no access to the model data, unless you somehow synchronize the entire viewer, which is not really feasible...and so the design issues begin.

Multithreading in JS is honestly terrible for real-world cases that are more complex than what you'd be able to use the GPU for. It's great for tech demos calculating fibonacci numbers as if that's relevant to the real world, and it can be a great boost for some image and compression algorithms (but really you'd probably be better off with the GPU for most common operations), but anything with any sort of shared state and logic in it, and it all goes down the dump.

tl;dr, maybe there is some brilliant way to easily e.g. run animations on N threads, but I definitely don't see how to do that in JS threads where every thread is its own environment, other than major mess and hacks that will make the code terrible and probably won't run that much faster either way.

flowtsohg commented 3 years ago

It looks like war3map.doo and war3mapUnits.doo changed to version 8. Can you give me a Reforged map with one doodad, one terrain/cliff doodad, and one unit?

arcman7 commented 3 years ago

Sure I'll get that map to you soon.

Is there an obvious reason as to why one can't use something like https://github.com/NectarJS/nectarjs to transpile your js code to c++ and then have that transpiled c++ be used for generating wasm? Sorry for the stupid questions, I just know of these tools but haven't really used them myself and so I keep thinking there's an easy solution here.

arcman7 commented 3 years ago

Here's a w3m map with the requested doodads and one orc grunt unit.

flowtsohg commented 3 years ago

Actually version 8 is for TFT, I forgot. Blizzard did it again. They changed the data format, without changing versions, because only the most brilliant people work there.

For doodads, there's an extra ID that is the same as the doodad ID. For units, there's an extra ID that is the same as the unit ID, and then some other changes.

It's hard to tell exactly what's going on without being able to edit stuff in WE and check what changes.

I suppose this is one reason to actually do boundary checks in BinaryStream (better to get an exception than crash the VM), but do I really want to copy-paste the same condition 50 times? lol

flowtsohg commented 3 years ago

I skimmed over and added boundary checks, and while it allows these maps to load (without doodads or units), it also seems like there are some models that now fail to load because they don't have enough bytes for their collision shapes. I didn't pinpoint the models yet (they are in a big map I use for testing the viewer), I am assuming they are custom models exported badly, I'll see later.

I am not really sure how to proceed from here to support these maps. The doodads only require the extra ID, but I never looked what information actually tells me a map is Reforged or not. I am assuming it's just the version in war3map.w3i, but I'll need to look.

As to units, I will need more examples to really be able to tell what's going on. For example, a map with a couple of units of the same type, with some different properties between them, like different players, one without item drops, one with item drops, and such, where I need to know all the numbers and differences. Then I can look at the differences in the binary data and attempt to make sense of the changes.

It could also be someone already figured this stuff out, but I am not really a part of the modding scene nowadays to know.

arcman7 commented 3 years ago

I was looking for documentation regarding the reforged file format earlier and this was the best I could find so far: https://xgm.guru/p/wc3/warcraft-3-map-files-format

arcman7 commented 3 years ago

I'd be happy to give you the necessary maps with whatever differences between them, just let me know what you need.

flowtsohg commented 3 years ago

Honestly this almost makes me want to finally connect the Jass context and the viewer, and run war3map.j/war3map.lua instead of loading in the editor files, I never really liked the latter. But...probably not worth the effort(?) I suppose that will come with its own issues, like models not yet existing when creating doodads/units of them.

arcman7 commented 3 years ago

I don't know what it entails to connect the jass context and the viewer so it's hard for me to give an opinion :| Still reading through the code. I had to start on some basics of webgl to understand all those shaders you have written.

flowtsohg commented 3 years ago

Well, for example, right now JassUnit is an object with just a couple of properties, by connecting I mean storing also a model instance in it, but yeah there aren't models at that point, so it's back to figuring a design that works async...

arcman7 commented 3 years ago

typically when I have the need to do some asynchronous setup work and want to stick it in the constructor, but can't since javascript will not allow constructors to be async I settle for this pattern:

class Omelette {
   constructor(egg, cheese) {
        this.egg = egg,  this.cheese = cheese;
        this.status = 'raw';
    }
    aysnc init() {
        await Promise.all([this.cook(egg), this.cook(cheese)])
        this.status = 'done';
    }
    cook(ingredient) {
        let resolve;
        const prom = new Promise((res) => { resolve = res; })
        setTimeout(() => { resolve('cooked ' + ingredient), 100)
        return prom
    }
}

Nothing novel here, I just think it's convenient to have the initialization work be done in a method called "init" or something else that implies constructor like behavior. Ofc the downside is that once you start awaiting promises to make sync-like syntax, everything else that uses it also has to be async.

flowtsohg commented 3 years ago

Think for example of code that creates a unit, and then sets its owner. The model instance needs to have its team color changed, but there cannot be a model instance yet since the model doesn't exist yet since it's being fetched. One solution is to make units a lot more complex, in that they need to have local data that can be changed freely, like a player owner, a position, a facing angle, etc., and this object then syncs itself with the model instance if it exists, and has to sync with the instance initially when it comes to existence after the model is loaded. In a way. that is how things already worked, right before this new change to promises. Of course, this is not ideal, because you end up with a lot of duplicate data that needs to be sync'd and such annoyances, but that's what you get when you take a simple model viewer and put it in the context of a game engine, lol.

arcman7 commented 3 years ago

Let's say you did take the complex approach and you had a buffer of data that's going to be used for the final rendering of the model:

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(javascriptArrayValues), gl.STATIC_DRAW);

The team value/player owner will not change often - if this is the full frozen throne game you have three possibilities where those particular values might change; banshees, Sylvanas, spell breakers (taking control of summoned units). If it were a custom game, then we could have more or less. In any case, I think it's safe to assume that particular bit of state won't be changing by the second or anywhere close. And if it did, simply have each unit pre-rendered in the various colors (memory vs speed trade-offs). Would this be so bad?

flowtsohg commented 3 years ago

I am not talking here about actually implementing the game :P There's war3mapUnits.doo which tells the editor about all preplaced units/items. The editor then creates Jass code that results in the same units/items being created, which is what the game runs. Probably the strongest example is item drops that you add in the editor, which become Jass functions with event handling and everything. Either way, the point is that when this initialization phase occurs, you will have lots of functions that modify the spawned units/items/doodads/whatever, because that's how wc3 initialization works. It doesn't matter how frequent it is becase - like you mentioned before - once one thing is async, everything involved need to be async in one way or another. It was easy to do this without promises because models existed immediately and instances of them could be made and modified, whether the models were actually loaded or not, but if resources are promise based and you have no concrete object to work with synchronously, you need to move the async logic one level up, e.g. to JassUnit (or more likely JassWidget to share the same code between the different object types).

arcman7 commented 3 years ago

Okay, I sort of follow you so far. Are there any resources you would recommend for getting caught up in terms of a high-level overview of the process you use to load up MDX models? I figured I should start there before moving onto how the maps are done.

flowtsohg commented 3 years ago

There isn't much to overview. The loader loads whatever data based on the arguments it gets, and when the data is available it creates a resource. Loading an MDX model is effectively the same as the following, with a lot more complexity:

fetch('my/file.mdx')
    .then((response) => response.arrayBuffer())
    .then((buffer) => new MdxModel(buffer));

Previously you could do something like so:

let model = viewer.load(...);
let instance = model.addInstance();

and you could follow that by e.g. setting team colors, animations, and what not for the instance. Obviously it was still async if you needed actual model data, but it allowed to call setters and such and they would automatically sync when the model loaded. This cannot work with automatic format detection, which is a requirement to support Reforged models, since it can attempt to load textures from a format not known to the client (e.g. you request a BLP but there's only a DDS with the same name, only the server/game know what actual resource needs to load).

Now that the loader returns promises, you don't have this synchronous model object and so you can't make an instance of it, and so you can't e.g. set the team color if the Jass requires it. That is unless the same kind of code is written again, but in JassWidget or some such. For example, the JassWidget constructor can take a model promise, and internally create an instance and do stuff with it when the promise is resolved. In addition rather than setting fields directly, some would have to be methods that update the internal instance if it exists. E.g.

setPlayer(player: JassPlayer) {
    this.player = player;

    if (this.instance) {
        this.instance.setTeamColor(player.index);
    }
}

It's not hard to do so, just pretty ugly, but I am not sure how async loads in a context that was designed for lazy loading local files can be handled nicely...web code issues 🤷

arcman7 commented 3 years ago

Still thinking about your last comment.

On another note, do you by chance know of any code in the repo that might be constantly setting a loaded model instance.sequence value to 0 or 1? It seems to be happening at a frequency close to the frequency that the rendering is done. This only happens when the unit is loaded in a map.

flowtsohg commented 3 years ago

The map viewer sets sequences in update to have all the instances run a random stand animation every time the previous one finished. Obviously if this was game code the units/doodads/whatever themselves would handle this based on their state when they update, but again, this code was never designed to be a game engine 🤷

arcman7 commented 3 years ago

Oh for sure, I was thinking about it, and in no way does this seem like a bug on your end. In fact, I think it's really cool you went that far to have the units perform their in-game animations in the rendered map. Like I said before, I'll be making good use of your library :D. Thanks for the pointer though - I'm using vs code as my editor, and searching 'setSequence(' definitely does not pull up every instance of the function being used.

flowtsohg commented 3 years ago

Not sure in what context you can use the library, but good luck :)

I also use vs code, and it does find the call in standsequence.ts (albeit probably harder to see if you are not familiar with the files). Be sure to open the project, or open the folder as a project. Then you can do a full search with ctrl+shift+f. Another option is to find the function itself (e.g. if you are looking at a call, right click the call and "Go to Definition"), right click its name, and "Go to References".

flowtsohg commented 3 years ago

I started working on a little example how the VM and viewer can be connected in a decent way, but then I remembered doodads aren't even converted to Jass, just units and items do, lol. There are also new Reforged natives I don't have, but adding those should be fairly easy (without an actual implementation). Not sure if it's worth the effort though.

arcman7 commented 3 years ago

Yeah, I just switched over from sublime. That's my bad then. I'll try using the references feature you mentioned.

doodads aren't even converted to Jass, just units and items do, lol.

Are the doodads done in a distinctly different way?

There are also new Reforged natives I don't have,

What are these natives you found?

arcman7 commented 3 years ago

I also just started implementing some very basic controls just to demo walking around in one of the maps https://www.loom.com/share/19fd544187a54492bd5f365054395985

It's suuuper rough lol.

flowtsohg commented 3 years ago

I added the map-viewer-jass branch, it's just a quick example though, and if you open the console you'll most likely be bombarded by warnings. Really all it took is to have the map viewer reference a VM and the VM reference the viewer. It's not even that terrible, because the viewer is conditional to the VM, so the VM can still run without a viewer when one isn't needed.

flowtsohg commented 3 years ago

If you are going for a serious project, warsmash (talk to Retera) is probably more relevant, unless you really like the web for some reason. This library doesn't really have much in the direction of a game engine other than being able to render models, it was never designed with that in mind, the map viewer and MPQ parser started as a half joke question of whether I can render whole maps, the Jass context started as a half joke question of whether I can run Jass, etc. If I were to make an actual game engine, which I thought about doing in the past but dismissed, I would rewrite the entire thing in a different way.

arcman7 commented 3 years ago

I think there's a certain power of accessibility by having it run in the web. There are a million JS developers out there, but no so many that know the languages/frameworks behind real modern game engines. Also, I find it kind of fun to push the envelope of browser performance. IF this were to ever evolve into something more serious then I 100% agree with you. I'll check out your branch in a bit as well as warsmash.

arcman7 commented 3 years ago

Hey, so with your branch map-viewer-jass I was able to open a map modified by the reforged editor! There were quite a few warnings in the console as you said:

3viewer.min.js:24 Error while converting Jass to Lua: Error: End of stream
t.default @ viewer.min.js:24
viewer.min.js:24 SetMapName was called but is not implemented :(
Ce @ viewer.min.js:24
viewer.min.js:24 SetMapDescription was called but is not implemented :(
Be @ viewer.min.js:24
viewer.min.js:24 SetPlayers was called but is not implemented :(
De @ viewer.min.js:24
viewer.min.js:24 SetTeams was called but is not implemented :(
Fe @ viewer.min.js:24
viewer.min.js:24 SetGamePlacement was called but is not implemented :(
Xe @ viewer.min.js:24
5viewer.min.js:24 SetPlayerRacePreference was called but is not implemented :(
gt @ viewer.min.js:24
4viewer.min.js:24 SetPlayerTeam was called but is not implemented :(
dt @ viewer.min.js:24
72viewer.min.js:24 SetPlayerAlliance was called but is not implemented :(
mt @ viewer.min.js:24
viewer.min.js:24 SetPlayerTeam was called but is not implemented :(
dt @ viewer.min.js:24
viewer.min.js:24 SetStartLocPrioCount was called but is not implemented :(
Ve @ viewer.min.js:24
viewer.min.js:24 SetStartLocPrio was called but is not implemented :(
He @ viewer.min.js:24
viewer.min.js:24 SetStartLocPrioCount was called but is not implemented :(
Ve @ viewer.min.js:24
viewer.min.js:24 SetStartLocPrio was called but is not implemented :(
He @ viewer.min.js:24
viewer.min.js:24 SetStartLocPrioCount was called but is not implemented :(
Ve @ viewer.min.js:24
2viewer.min.js:24 SetStartLocPrio was called but is not implemented :(
He @ viewer.min.js:24
viewer.min.js:24 SetStartLocPrioCount was called but is not implemented :(
Ve @ viewer.min.js:24
2viewer.min.js:24 SetStartLocPrio was called but is not implemented :(
He @ viewer.min.js:24
8viewer.min.js:24 GetCameraMargin was called but is not implemented :(
ub @ viewer.min.js:24
viewer.min.js:24 SetCameraBounds was called but is not implemented :(
Ag @ viewer.min.js:24
viewer.min.js:24 SetDayNightModels was called but is not implemented :(
X_ @ viewer.min.js:24
viewer.min.js:24 NewSoundEnvironment was called but is not implemented :(
Lb @ viewer.min.js:24
viewer.min.js:24 CreateMIDISound was called but is not implemented :(
kb @ viewer.min.js:24
viewer.min.js:24 GetFloatGameState was called but is not implemented :(
Cd @ viewer.min.js:24
viewer.min.js:24 CreateMIDISound was called but is not implemented :(
kb @ viewer.min.js:24
viewer.min.js:24 GetFloatGameState was called but is not implemented :(
Cd @ viewer.min.js:24
viewer.min.js:24 StartSound was called but is not implemented :(
Nb @ viewer.min.js:24
viewer.min.js:24 SetMapMusic was called but is not implemented :(
jb @ viewer.min.js:24
viewer.min.js:24 Rect was called but is not implemented :(
yn @ viewer.min.js:24
26viewer.min.js:24 SetPlayerAlliance was called but is not implemented :(
mt @ viewer.min.js:24
viewer.min.js:24 SetPlayerState was called but is not implemented :(
td @ viewer.min.js:24
7viewer.min.js:24 Filter was called but is not implemented :(
Ta @ viewer.min.js:24
viewer.min.js:24 ForceEnumPlayers was called but is not implemented :(
gn @ viewer.min.js:24
viewer.min.js:24 GetGameSpeed was called but is not implemented :(
rt @ viewer.min.js:24
viewer.min.js:24 IsFogEnabled was called but is not implemented :(
cd @ viewer.min.js:24
viewer.min.js:24 IsFogMaskEnabled was called but is not implemented :(
od @ viewer.min.js:24
4viewer.min.js:24 GetPlayerSlotState was called but is not implemented :(
kt @ viewer.min.js:24
11viewer.min.js:24 CreateSoundFromLabel was called but is not implemented :(
Eb @ viewer.min.js:24
viewer.min.js:24 TriggerRegisterTimerExpireEvent was called but is not implemented :(
Ma @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24
viewer.min.js:24 VersionGet was called but is not implemented :(
gd @ viewer.min.js:24
viewer.min.js:24 TriggerRegisterTimerExpireEvent was called but is not implemented :(
Ma @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24
2viewer.min.js:24 CreateSoundFromLabel was called but is not implemented :(
Eb @ viewer.min.js:24
viewer.min.js:24 TriggerRegisterGameStateEvent was called but is not implemented :(
Pa @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24
viewer.min.js:24 TriggerRegisterGameStateEvent was called but is not implemented :(
Pa @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24
2viewer.min.js:24 TriggerRegisterGameStateEvent was called but is not implemented :(
Pa @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24
2viewer.min.js:24 TriggerRegisterGameStateEvent was called but is not implemented :(
Pa @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMinX was called but is not implemented :(
cb @ viewer.min.js:24
viewer.min.js:24 GetCameraMargin was called but is not implemented :(
ub @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMinY was called but is not implemented :(
db @ viewer.min.js:24
viewer.min.js:24 GetCameraMargin was called but is not implemented :(
ub @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMaxX was called but is not implemented :(
hb @ viewer.min.js:24
viewer.min.js:24 GetCameraMargin was called but is not implemented :(
ub @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMaxY was called but is not implemented :(
fb @ viewer.min.js:24
viewer.min.js:24 GetCameraMargin was called but is not implemented :(
ub @ viewer.min.js:24
viewer.min.js:24 Rect was called but is not implemented :(
yn @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMinX was called but is not implemented :(
cb @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMinY was called but is not implemented :(
db @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMaxX was called but is not implemented :(
hb @ viewer.min.js:24
viewer.min.js:24 GetCameraBoundMaxY was called but is not implemented :(
fb @ viewer.min.js:24
viewer.min.js:24 Rect was called but is not implemented :(
yn @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 GetPlayerTechResearched was called but is not implemented :(
Wc @ viewer.min.js:24
2viewer.min.js:24 SetPlayerTechMaxAllowed was called but is not implemented :(
Jc @ viewer.min.js:24
viewer.min.js:24 SetAllItemTypeSlots was called but is not implemented :(
pc @ viewer.min.js:24
viewer.min.js:24 SetAllUnitTypeSlots was called but is not implemented :(
gc @ viewer.min.js:24
viewer.min.js:24 TriggerRegisterPlayerUnitEvent was called but is not implemented :(
er @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24
viewer.min.js:24 TriggerRegisterTimerEvent was called but is not implemented :(
Ia @ viewer.min.js:24
viewer.min.js:24 TriggerAddAction was called but is not implemented :(
pi @ viewer.min.js:24

I'm not sure if those warnings are related to why not much other than the ground plus the ground's texture gets rendered or if that's something else. But in any case - great job getting a quick fix to work like that!

flowtsohg commented 3 years ago

Theoretically units should load and do for the TFT maps I checked. Reforged maps likely fail at runtime because the VM tries to get new natives and my code has no implementation for them, because they didn't exist when I ran the natives generator last. For example, if you try to load reforged_doodads.w3m and look in the console, the last message should say it failed to call BlzCreateUnitWithSkin. Updating the natives is just a matter of running the generator with an updated common.j (specifically the above native needs to be implemented as well, guess that's the normal way to create units in Reforged?), however I never implemented anything that helps in copying over natives that I actually implemented and aren't stubs 🤔

This isn't much of a fix, more of a small experiment. war3map.doo (trees and other doodads/destructibles) is used also by the game, so it needs to be corrected anyway. It would also be preferable to correct war3mapUnits.doo. With that being said, it's also somewhat neat to be able to run a map, but as you saw most of the natives aren't implemented and thus you got all of those warnings. Most Jass/Lua should still run, because all of the natives that return things return default values, but it obviously won't run correctly.

arcman7 commented 3 years ago

Have these guys already done some of this necessary work? https://github.com/lep/jassdoc

flowtsohg commented 3 years ago

There aren't many questions when it comes to how most natives do/should work. But consider the fact that if you want to implement all of the natives, well, you effectively need to recreate WC3. There is a start with all sorts of natives being implemented, and some objects like timers and triggers that are half implemented, but ultimately it's not something you go and do in a couple of hours for fun :P

arcman7 commented 3 years ago

I see. I guess I can't be of much help until I have a fuller understanding of this entire project. I'm working my way up from just rendering 3d shapes using only webgl shader programs. Putting that together with how your parsers interpret the various blizzard file formats and turn those into rendered objects within the scene of a full map (with lighting, and animations) will take some time - and then more time to understand your workflow process with this common.j file. I imagine that last part isn't hard to grasp, but requires some context I don't have yet. I'm splitting my free time between learning the above-mentioned topics, to figuring out how to make the high-level js functions you've provided work for my gaming context. I'm experimenting with using ammo.js as a physics engine to drive interactions between objects in a loaded/rendered map. It's honestly a lot of fun lol.

flowtsohg commented 3 years ago

common.j and all of this Jass running isn't really required, unless you want to actually run maps which is a different thing altogether. For simple loading and rendering static maps like the code already does, it's a lot less complicated to load in the editor files, I just don't really want to spend time on them without having access to the Reforged editor so I can instantly change stuff and check. It could be someone else already updated some code/specs somewhere with the changes, like the Hive, HiveWE, etc., I didn't bother checking yet.

There actually isn't really lighting, not dynamic nor static. Both are possible to implement of course, but require a bunch of work.

As far as animations go, it's just your typical skeletal animation code. You have a hierarchy of bones, where every parent bone affects its children. Every frame you calculate the transformation (translation/rotation/scale) of the top most root bone, then go to every child and do the same, and to their children, etc. Every vertex in the geometry has a list of bones it is attached to (and in the case of more modern things than TFT, e.g. in Reforged, there is also a list of numerical weights which affect how much each bone affects the vertex). Finally, in the shader (or not), for every vertex, take all the bones, and add the transformations together (perhaps weighted). This is really basic animation code, however if you never dealt with this sort of code and want a more in-depth explanation, I can write more, albeit I am not really sure why you are looking at this stuff :P

arcman7 commented 3 years ago

This is really basic animation code, however if you never dealt with this sort of code and want a more in-depth explanation, I can write more, albeit I am not really sure why you are looking at this stuff :P

Learning to do this has been on my coding wish list for quite a while. If there's more info you can give me I'd gladly take it if it's not too much trouble.

I just don't really want to spend time on them without having access to the Reforged editor so I can instantly change stuff and check

If all you need is a reforged editor I'd be happy to get ahold of a new copy for you

flowtsohg commented 3 years ago

Do you have some experience with shaders in general? In particular the vertex stage and its inputs.

I was offered copies of Reforged in the past, I don't want it.

flowtsohg commented 3 years ago

Right, skeletal animation.

First we start with a skeleton, which works much like how you'd imagine a real skeleton. You have a bunch of bones attached at the ends with joints, linked in chains - like your hands and legs - with bones affecting each other.

When it comes to programming, typically you will store bones in a tree-like structure, where you have a root bone that has children, and the children in turn have their own children, and so on. Each bone stores a transformation, such as a position, rotation, and scaling. Generally, and also in WC3, the bone transformations are relative to their parents, rather than absolute units in the world. For example, if a bone is moved by 50 on the x axis, and its child is moved 50 on the x axis, the child's absolute movement is 100 on the x axis.

Typically each bone will store a local transformation which is relative, and a global transformation which is in absolute world units. To get the latter, the skeleton needs to be visited in its hierarchy. Starting from the root bone, its local transformation is its global transformation, because it has no parent. Then we go over all of the root's children, and have their global transformation as their parent's global transformation plus their own local transformation. Then we go to the children's children, and do the same, and the process goes on until all the skeleton is visited.

The actual transformations are usually stored in a matrix, most commonly a 4x4 one when it comes to 3D. I don't know what's your background with linear algebra, but without going too much into it, a 4x4 (and in fact 4x3) matrix allows to store a 3D translation+rotation+scaling, and if you have two matrices and multiply them, you get the transformation of both of them, in the order of multiplication, and if you multiply a vector by a matrix, the transformation of the matrix will be applied to the vector.

The animation itself is generally stored in some form of timelines with keyframes that give information such as "at frame N my relative position should be X". Every time you go over a bone to get its absolute transformation, you add the animation data to the local transformation first.

Now let's move to the geometry. When creating models in 3D software, a skeleton is made, and the vertices are attached to bones, which is also called skinning. Usually a vertex can be attached to some maximum amount of bones, typically 4 (this isn't true to TFT which can have presumably any number of bones per vertex). Usually a vertex will have also a weight per bone it's attached to, controlling how much "strength" the bone has over the vertex (this isn't true for TFT, more on this below), where the sum of all weights must be 1, unless you want some very funky animations.

So what is animating? First step, update the skeleton so you have all of the absolute transformations of the bones. Now for each vertex you want to render, get all of the bones it is attached to, possibly scaled by a weight, add them up via multiplication, and finally transform the vertex. You can see this being done in the HD shader. TFT has no weights, and is a bit old and weird, so instead the vertex is transformed by each bone, all the results are added together, and finally divided by the number of bones the vertex is attached to. You can see this in the SD shader, but it's a bit messy. This effectively mimics a perfectly split weight.

We can do all of this on the CPU, but it's a bit silly to use a few cores for perhaps millions of vertices, rather than the highly concurrent and optimized cores of the GPU of course. Doing this in a vertex shader is actually more or less as straightforward as doing it on the CPU. There are two differences that I'll note though. 1) You can't support any number of bones per vertex, because shaders don't support such stuff, which is indeed why typically games go for 4 (more than 4 is barely necessary for actual models). 2) There are only so many uniform variables you can fill in a vertex shader. I don't know what the stats are nowadays, but last time I checked for WebGL, almost everyone had 128 slots, most people had 256, but over that and the support dropped by a lot. We need to upload N bones, every bone is a 4x4 matrix, and every uniform slot contains a 4D vector, so each bone takes 4 slots. That means that if we want to support most people, we don't actually support many bones per model. This is fine for smaller models, but bigger ones will break, especially with Reforged where every model has like 5 times the bones they should. The more scalable solution is to use a texture for data rather than for an image. For example, I have a 2D texture where each pixel is 4 floats, where each row is big enough to contain all of the matrices of a certain instance, essentially rows of matrices stacked on top of each other. In the shader I then get 4 pixels for every matrix that I need and reconstruct the matrix from them. This limits the number of bones to the maximum texture size, which practically means you can support any number of bones you want. A thing to note is that outside of WebGL (or if using WebGL2) using textures as data is a lot more simple and straightforward, with things like texture buffers and texelFetch, and probably many more things I didn't use in years and forgot about.

Finally a note on FK and IK, even though it's not relevant to WC3. If you ever animated with 3D software you'll probably be aware of the differences. FK, or Forward Kinematics, is what WC3 uses. It means that when animating, both on the code level and in the 3D software, the animation starts from the root of a bone chain, and moves down the hierarchy up to the last leaves in the tree. This is very simple to code, and the result is 100% consistent - the animation data you put is the animation data. On the other hand, it's very rigid, you can never do anything dynamic with the animation, it is what it is. It's also a lot of extra work and not very intuitive to work with in the 3D software. IK, or Inverse Kinematics, means the opposite, where you say you want some bone to move in a certain way, and all the bones up the hierarchy have to follow the best they can. When you move your hand to grab something, you don't first rotate and twist your upper arm which causes it to extend forward , then rotate the lower arm, then rotate the hand, right? you "just move your hand" and the rest follows. This is what IK tries to mimic, based on similar constraints that exist in real life, such as "you can only rotate this in this direction between X and Y degrees", don't wanna twist your neck 360 degrees :P Once you have a properly constrained skeleton with IK chains that make sense, making animations in 3D software becomes a lot faster and more intuitive, I suggest you to find some video on youtube of advanced 3D model rigging and you'll see what I mean. The upside of IK in games is that you are basing your animations on a real-time math simulation, which means suddenly your animations can react in real time to the world around, such as feet matching the ground height when walking up a hill, or a fist stopping moving forward when it reaches its target (and this goes way cooler the more you think about how much you can connect between character animations and the world around them). The downside of IK in games is that you are basing your animations on a real-time math simulation, and things can sometimes go crazy with the math. I am sure you saw plenty of examples in games of animation simulations going out of hand. Practically speaking most modern games most likely have animations that are a mix between the two, for example a predefined walk animation that is then affected in real time by the terrain and physics around them.

flowtsohg commented 3 years ago

I just checked HiveWE, and the new data is there, apparently the extra ID lets you select the skin, which I suppose is then used with the previously mentioned CreateUnitWithSkin. Not that I know what skin means in this context because I never checked how Reforged works (skin = skeleton? texture? sound set? other?) I'll try to update the doodads/units parsing later.

flowtsohg commented 3 years ago

The maps you gave me load properly now, and this issue is killing my browser because for some reason rendering simple text is really heavy on github 🤔 so I'll close this issue, open a new one please if you want to continue discussing something.

arcman7 commented 3 years ago

Cool checking out the commits. At the airport right now, I'll try out the changes soon!