Skip to content
Stijn edited this page Nov 21, 2023 · 9 revisions

This document describes MDX rendering for both SD and HD modes. The MDX format is fairly complex to load and render. For a binary specification of MDX you can refer to this excellent description by GhostWolf (Note that it is not up-to-date for MDX version 1100 found in WC3 1.33).

The rendering mode (SD/HD) of a model can differ between

  • V900, V1000: geosets
  • V1100: layers

Thus, it is possible to have a model that is partially SD and partially HD such as the following ogre (image by Retera):

SD/HD fusion

(v900/v1000) If a material has a (valid?) shader then it is rendered with that shader, otherwise it is rendered in SD mode. (v1100) If a layer has the hd flag set, it will be rendered in HD mode, otherwise it is rendered in SD mode.

GeosetAnimation

Some rendering implementations use an alpha of 0.75 for a geoset as a cutoff where they hide the geoset entirely, but this is not correct. Geoset alphas do not have a cutoff.

Materials and Layers

SD

Every material can have any number of layers each with their own blendmode and properties such as TwoSided. Only the first layer is taken into account when sorting for transparency. So if the blendmode for layer 0 is either None or Transparent then the material is considered opaque. If the blendmode is any of the others such as Blend or Additive then it is considered transparent.

Afaict the only way to draw SD geosets is doing a draw call for every single layer in the geoset its material.

HD V900/V1000

In HD mode only the first 6 layers are used for actual rendering. They have to be in the right order which is

  1. Diffuse
  2. Normal
  3. ORM
  4. Emissive
  5. Teamcolor
  6. Environment

Except for layer 0, all blendmodes, static alphas and any other properties seem to be ignored. It seems that HD mode layer 0 supports all the flags such as TwoSided and NoDepthTest.

To efficiently render HD materials you should have a shader that takes in the 6 layer textures instead of doing 6 individual draw calls for every layer.

HD V1100

The new MDX version no longer separates the different PBR textures into layers. Every layer now has a list of textures (with id and optionally KMTF) instead of just one texture ID per layer. An SD layer will still only refer to a single texture. This allows HD models to have multiple layers per geoset like SD geosets do. The textures in the layer textures array are still in the following order:

  1. Diffuse
  2. Normal
  3. ORM
  4. Emissive
  5. Teamcolor
  6. Environment

Diffuse

The diffuse textures are simple S3TC DXT1/DXT5 textures and your image loading library should easily handle that. It is also possible to directly upload the texture data to the GPU without decompression as most GPUs can use the compressed data natively. The relevant OpenGL function for this is glCompressedTexImage2D

Normal

The normal maps are two channel red green 3Dc/DXN/BC5 textures. Again you can upload these directly to the GPU without decompression. Using the model tangents you can then construct a TBN matrix which you multiply with the light direction in the vertex shader to get the tangent light direction. vInstance being your mat4 with the object its translation/rotation/scale.

mat3 model = mat3(vInstance);
vec3 T = normalize(model * vTangent.xyz);
vec3 N = normalize(model * vNormal);
vec3 B = cross(N, T) * vTangent.w; // to fix handedness
mat3 TBN = transpose(mat3(T, B, N));

tangent_light_direction = normalize(TBN * light_direction);

Then in the fragment shader you sample the normal texture and reconstruct the third (blue) component using the fact that the 3D vector they form should have a total length of 1.

vec2 texel = texture(normal, UV).rg;
vec3 normal = vec3(texel, sqrt(1.f - texel.x * texel.x - texel.y * texel.y));

Then you can dot product this with the tangent light direction to get the final contribution. Note that we invert the tangent_light_direction. We add some ambient light because otherwise we would get totally dark sides.

float lambert = clamp(dot(normal, -tangent_light_direction), 0.f, 1.f);
color.rgb *= clamp(lambert + 0.1, 0.f, 1.f);

ORM

The orm map has three channels which are occlusion, roughness and metalness. Occlusion seems to be unused by the game at this time, while the other two channels are probably used like in standard OpenGL ORM maps.

Emissive

Regular emission map, just gets added to the color value

Teamcolor

A replaceable texture supposed to represent teamcolor. Since its a solid color you don't need to use a texture per se and can just use a uniform or pass it in a buffer.

Environment

A texture used for the reflective and shiny stuff when the ORM texture has metalness.

Open Questions

  • Does lighting apply to transparent layers?
  • Does the editor always play the stand animation and does it loop it even when it's set to non_looping