-
Notifications
You must be signed in to change notification settings - Fork 60
war3map.w3e Terrain
This is the tileset file. It contains all the data about the tilesets of the map. Let's say the map is divided into squares called "tiles". Each tile has 4 corners. In 3D, we define surfaces using points and in this case tiles are defined by their corners. I call one tile corner a "tilepoint". So if you want a 256x256 map, you'll have 257x257 tilepoints. That's also why a tile texture is defined by each of its four tilepoints. A tile can be half dirt, one quarter grass and one quarter rock for example. The first tilepoint defined in the file stands for the lower left corner of the map when looking from the top, then it goes row by row. Tilesets are the group of textures used for the ground.
Here is the file format:
char[4] magic_number
int32 format_version
char main_tileset
int32 uses_custom_tileset
int32 ground_tilesets
char[4][a] ground_tileset_ids
int32 cliff_tilesets
char[4][b] cliff_tilesets_ids
int32 width
int32 height
float horizontal_offset
float vertical_offset
Then width * height tilepoints, each 7 bytes long:
int16 ground_height
int16 water_height + boundary_flag
4bits flags
4bits ground_texture
5bits ground_variation
3bits cliff_variation
4bits cliff_texture
4bits layer_height
char[4] magic_number
A constant string at the start of the war3map.w3e file used to identify it.
int32 format_version
The version. Seems to always be 11? Needs more investigation
char main_tileset
This is the main tileset for the map. Each tileset has an .mpq in the game mpqs which contains some specific textures for this tileset like water or cliff textures. It can be one of the following:
- A Ashenvale
- B Barrens
- C Felwood
- D Dungeon
- F Lordaeron Fall
- G Underground
- L Lordaeron Summer
- N Northrend
- Q Village Fall
- V Village
- W Lordaeron Winter
- X Dalaran
- Y Cityscape
- Z Sunken Ruins
- I Icecrown
- J Dalaran Ruins
- O Outland
- K Black Citadel
int32 uses_custom_tileset
Indicates whether this map uses a combination of textures from different tilesets or a predefined one from the list above.
(1 = custom tileset, 0 = no custom tileset)
int32 ground_tilesets
Indicates how many ground textures there are. There is no maximum except that a tilepoint only allocates 4 bits and thus can save only 16 different values and so only the first 16 tilesets are usable.
char[a][4] ground_tileset_ids
A list of strings of size 4. Example: "Ldrt" stands for "Lordaeron Summer Dirt"
Refer to "TerrainArt\Terrain.slk" located in War3.mpq or War3x.mpq for more details.
int32 cliff_tilesets
Indicates how many cliff textures there are. There is no maximum except that a tilepoint only allocates 4 bits and thus can save only 16 different values and so only the first 16 cliffs are usable.
char[b][4] cliff_tilesets_ids
A list of strings of size 4. Example: "CLdi" stands for Lordaeron Cliff Dirt
Refer to "TerrainArt\CliffTypes.slk" located in War3.mpq or War3x.mpq for more details.
The cliff tile list is actually ignored, the World Editor will simply add the cliff tiles for each tile in the ground tile list, if a cliff version of this ground tile exists.
int32 Width of the map + 1
int32 Height of the map + 1
Width and Height of the map in tiles. We add 1 because a map of 256 x 256 tiles will have 257 x 257 corners.
float horizontal_offset
float vertical_offset
These 2 offsets are used in the scripts files, doodads and more.
The original (0,0) coordinate is at the bottom left of the map (looking from the top), but it's easier to work with (0,0) in the middle of the map.
These offsets are:
-1*(Mx - 1) * 128 / 2 and -1 * (My - 1) * 128 / 2
where:
(Width - 1) and (Height - 1) are the width and the height of the map in tiles 128 is the size of a tile on the map
/ 2 because we don't want the length, but the middle.
-1 * because we are "translating" the centre of the map, not giving it's new coordinates
int16 ground_height
Minimum height (-8192)
Normal height (ground level 0)
Maximum height (+8192)
The last two bits are unused.
The tilepoint "final height" you see on the WE is given by:
(ground_height - 8192 + (layer - 2) * 512) / 4
Where 8192 is the "ground zero" level, 2 the "layer zero" level and 512 the layer height
int16 water_height + boundary_flag
The highest bit (bit 15) is used for the boundary flag 1.
0x4000 --> boundary flag 1 (shadow generated by the world editor on the edge of the map)
The tilepoint "water level" you see on the WE is given by:
(water_level - 8192) / 4 - 89.6
Where 8192 is the "ground zero" level, -89.6 is the water zero level. The water zero level is a constant value found in Water.slk * 128. So height = -0,7 --> water_zero_level = -0,7 * 128 = -89.6.
4bits flags
Flags values (shown as big endian):
0x0010 --> ramp flag (used to set a ramp between two layers)
0x0020 --> blight flag (ground will look like Undead's ground)
0x0040 --> water flag (enable water)
0x0080 --> boundary flag 2 (used on "camera bounds" area. Usually set by the World Editor "boundary" tool.)
4bits ground_texture
Which ground textures is used (dirt, grass, rock, etc...). This refers to one of the ground tilesets discussed earlier.
5bits ground_variation
Which variation of the texture to use (bones, holes, etc...). This is to reduce the amount of repetition.
3bits cliff_variation
? (Which model to use? Values seen are 0, 1, 2)
4bits cliff_texture
Which cliff texture to use (dirt, grass, snow, etc...). While technically this should refer to one of the cliff tilesets discussed earlier the cliff tile list is actually ignored, the World Editor will simply add the cliff tiles for each tile in the ground tile list, if a cliff version of this ground tile exists.
4bits layer_height
The layer height is changed when using cliffs.
A reference implementation in C++ is available here and a JavaScript implementation here.
Ground Textures are either square or extended. Extended textures have extra variations for the "full" tile to reduce the amount of repetition when you have big patches of a single tile. Here you see a square (non extended) and extended texture.
The ground_variation data in the tilepoints determines which of the variations to use. When the texture is square it will be either the top left variation or the bottom right variation. If the texture is extended then one of the variations from the extended part is chosen in the following order:
As you may have noticed the square texture and the left part of the extended texture have all kinds of variations for different texture combinations. Based on how many tilepoints in a tile share the same texture a variation is chosen. This is done for every texture that the tilepoints of that tile have. Let's take an example where the bottom left, top left and top right are all dirt and only the bottom right is grass.
Now there is a nice formula to figure out which variation we need to pick. It requires that we number all the variations from 0 to 15 (just like we did with the extended variations). Then for every different texture in the tile (in this case dirt and grass) we make a bitstring (a sequence of 0's and 1's) and convert that to a number. Here is an example:
You will have to do this from the lowest ground_texture number to the highest since tiles can overlap each other, thus we do dirt first.
1 1
1 0
We then place the top row in front of the bottom row which produces:
1 1 1 0
Which if we convert it from binary to a number gives us 14 which is the following variation:
Now we repeat this for the grass texture:
0 0
0 1
Place the top row in front of the bottom row.
0 0 0 1
Which converted from binary is the number 1 which is the following variation:
And finally if we combine the two we get:
This will work for any combination of texture tiles. Now in the example we calculated what dirt should be, but in reality the lowest ground_texture number is always the full tile since it always gets placed on the bottom. Here is an example implementation bit in C++.
std::vector<std::tuple<int, int>> tileVariations;
std::set<Corner> set({ topL, topR, bottomL, bottomR });
Corner first = *set.begin();
tileVariations.push_back({ get_tile_variation(first), first });
set.erase(set.begin());
std::bitset<4> index;
for (auto&& texture : set) {
index[0] = bottomR == texture;
index[1] = bottomL == texture;
index[2] = topR == texture;
index[3] = topL == texture;
tileVariations.push_back({ index.to_ulong(), texture });
}
What the code does is first get all the unique textures used by the four corners and sort them from lowest to highest (this is what the std::set does automatically). Then we take out the smallest ground_texture number and we add this to our vector (basically an array/list). After that we repeat the aforementioned process for every remaining texture.
Note: In C++ true is implicitly converted to 1 and false to 0
Blight is not a texture defined in the map file, but instead each tilepoint has a Boolean flag indicating whether they are blight or not. If a tilepoint has the blight flag set to true then instead of using its original ground_texture it should use the blight texture. The blight texture is always on top of all other textures with the exception of cliff textures.
When a tilepoint has a cliff within its Moore neighborhood (a square around the tilepoint) then it should use the ground texture which belong to that cliff. These are defined in "TerrainArt\CliffTypes.slk" which can be found in the game mpq's.
Water consists of 45 textures which are looped to animate the water. These can be found in the tileset specific mpqs. The animation speed is tileset specific and can be found in TerrainArt\Water.slk.
Water changes colour and transparency based on the distance to the ground. The colours to interpolate between can be found in the TerrainArt\Water.slk. The heights used to interpolate can be found in UI\MiscData.txt:
// Depth-based colors
// The water plane is vertex colored based on the water "depth": the distance
// from the water plane to the ground. These values define the colors for two
// distinct ranges: "shallow" (MinDepth to DeepLevel) and "deep" (DeepLevel to MaxDepth).
// where the color is found by interpolating between the corresponding colors given
// in TerrainArt/Water.slk.MinDepth=10
DeepLevel=64
MaxDepth=72
Note that to have 100% matching results you should use the same index order as the world editor so that the diagonal line goes from topleft to bottomright.
Cliffs in Warcraft 3 are made out of models. Each tilepoint chooses a model for its top right, top left, bottom right and bottom left based on the height differences in those tiles. When 2 tilepoints of a tile are both cliffs then a different model is used. This means that a tile will have always only 1 cliff model on it. All 94 of these models can be found in Doodads/Terrain/Cliffs and Doodads/Terrain/CityCliffs (which we will come to later). Here is an example of such a cliff model.
Left you can see a single cliff model and to the right you see a cliff model where the top-left tilepoint and bottom-right tilepoint are both cliffs.
The formula for determining which cliff model to use depends on the relative layer_height difference. You take the lowest layer_height and then calculate the differences. You then add the difference to the char 'A' which gives you the filename. So for the left cliff above the layer_heights are (imaginary):
13 12
12 12
The lowest layer height is then 12. This means that the differences in height are layer_height - 12:
1 0
0 0
So only the top-left is different from the base height. We then add the differences to the character 'A':
'A' + 1 = B
'A' + 0 = A
'A' + 0 = A
'A' + 0 = A
We then concatenate which gives us BAAA. Finally we simply add "Cliffs" in front of it and ".mdx" at the back to get the correct filename. Except that some cliffs have variations to reduce visible repetition when you have a row of cliffs. This variation number can be 0, 1, and 2 which depends on the cliff_variation value you loaded for each tilepoint. The final filename is thus: "CliffsBAAAx.mdx" where x is the cliff_variation. The same can be repeated for the cliff shown to the right which yields "CliffsBABAx.mdx". Here is an example implementation in C++:
int base = std::min({ bottomLeft.layer_height, bottomRight.layer_height, topLeft.layer_height, topRight.layer_height });
std::string file_name = ""s + (char)('A' + topLeft.layer_height - base)
+ (char)('A' + topRight.layer_height - base)
+ (char)('A' + bottomRight.layer_height - base)
+ (char)('A' + bottomLeft.layer_height - base);
CityCliffs follow the same exact ordering and naming pattern except that their folder is "Doodads/Terrain/CityCliffs" and their filenames are prefixed by "CityCliffs" instead of just "Cliffs". Which type of cliff model to load can be found in "TerrainArt\CliffTypes.slk"