War3map.w3e - Terrain

From HIVE
Revision as of 20:33, 15 February 2018 by Selaya (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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.

Format

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

Explanation

	char[4]		magic_number

The constant string "W3E!" 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.

Note that while the tilepoints allocate 4 bits for which cliff_tilesets_ids to use, the value 15 is reserved for an unknown. This means that while you can define as many cliff_tilesets as you want the maximum of usable ones is 15.

	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 tilepoints. We add 1 because a map of 256 x 256 tiles will have 257 x 257 tilepoints.

	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  

Tilepoint

	int16		ground_height

Minimum height (-8192)
Normal height (ground level 0)
Maximum height (+8192)
The maximum height (and in effect the minimum height) are defined in "UI/MiscData.txt".

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 variable value found in Water.slk * 128. So if it is -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

Cliff variations seem to be allowed to be between the range 0 to 7. If the cliff variation model isn't available then the game will take the highest available one.

	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.

Note that the value 15 is reserved for unknown reasons.

	4bits		layer_height

The layer height is changed when using cliffs.

Implementation

A reference implementation in C++ is available [here](https://github.com/stijnherfst/HiveWE/blob/master/HiveWE/Terrain.cpp) and a JavaScript implementation [here](https://github.com/flowtsohg/mdx-m3-viewer/tree/master/src/viewer/handlers/w3x).

Heightmap

While the specification above sprinkles some 8192 here and there in the height calculations you won't actually need those if you divide by 512 when parsing war3map.w3e. You will then get the values in the range needed if you assume a tile is 1 unit wide.

	ground_height = (ground_height - 8192.f) / 512.f

Textures

Ground Textures

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. ![Non extended texture](http://g2f.nl/0jjp6uk) ![Extended texture](http://g2f.nl/0mn95zq) 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: ![Dirt Numbered](http://g2f.nl/0odnxad)

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. ![Example Tile](http://g2f.nl/0nlnf18) 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: ![Dirt Variation 14](http://g2f.nl/0l11gax) 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: ![Grass Variation 1](http://g2f.nl/0k4nji6) And finally if we combine the two we get: ![Combined Tile](http://g2f.nl/09cnrow)

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

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.

Cliff Textures

When a tilepoint has a cliff within its [Moore neighborhood](https://en.wikipedia.org/wiki/Moore_neighborhood) (a square around the tilepoint) then it should use the ground texture which belongs to that cliff. These are defined in "TerrainArt\CliffTypes.slk" which can be found in the game mpq's.

Thus the final order of importance is: cliff texture -> blight -> ground_texture.

Water

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. ![Diagonal Line](http://g2f.nl/03ybj1h)

Cliffs

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. ![Cliff 1](http://g2f.nl/0moq8vl) ![Cliff 2](http://g2f.nl/08eeqie) 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. To determine whether a tilepoint is a cliff you only need to check if it's top, top-right and right neighbour have a different layer_height than you.

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"

Deformation

When one corner of a cliff has a different height than the others the cliff has to be deformed appropriately. This is simply adding the bilinear interpolations of the different heights to the current vertex it's z position. Some raw GLSL code to illustrate:

	float bottom = mix(topRight, topLeft, -(vertex.x / 128.f));
	float top = mix(bottomRight, bottomLeft, -(vertex.x / 128.f));
	float value = mix(bottom, top, vertex.y / 128.f);

	gl_Position = MVP * vec4(vertex.xy, vertex.z + value * 128.f, 1);
 Oftentimes in my C++ implementation bottom and top are reversed since OpenGL flips textures vertically. I should fix it sometime

Ramps

Ramps are a little different from cliffs in that they take up 2 tiles of space. The right texture to use (grass, dirt, etc) can be determined from the cliffs around the ramp.

Just like cliffs their file names contain the letters A, B and C, but they now also contain H, L and X. The side containing the actual slope always uses HLX and the other side ABC. ![Ramps slope side](http://g2f.nl/03hzgdd) Where H seems to be the base for the slope side. The ASCII values of the characters uses for the slope side are:

	H - 72  
	L - 76  
	X - 88  

Where the algorithm seems to be 'H' + 4^difference_to_base.

Curiously enough the editor and game dont actually load any of the .mdx models containing X or C. This is visible sometimes when creating a ramp near a 2 height cliff. ![Missing ramp model](http://g2f.nl/01yq4w1)