WMO: Unified Rendering

Blizzard debuted a new rendering path for WMOs in the Wrath of the Lich King beta: unified rendering.

This rendering path is only used when SMOHeader->flags F_UNIFIED (0x2) is set. When F_UNIFIED is not set, the classic rendering path is used.

From what I’ve observed, the unified rendering path was added to support using vertex coloring on WMO exteriors.

Prior to unified rendering being added, vertex colors were always baked with an interior ambient color term included. If these vertex colors were used in exterior batches, because the batches already receive ambient light from the exterior lighting source, the batches would have become overly bright.

Enter the unified rendering path.

Unified rendering involves a new suite of shaders (suffixed with U) and a new single rendering function that replaces the previously separate CMapObj::s_intFunc and CMapObj::s_extFunc functions.

In the unified rendering path, the ambient color term for interior batches is stored in SMOHeader->ambColor, and is not baked into the vertex colors in the MOCV chunk. This permits vertex colors to be used with exterior and interior batches alike. Exterior batches simply ignore the color present in SMOHeader->ambColor.

A full explanation of the unified rendering path follows.

Note: when illustrating the output of the unified rendering path, we’ll stick to a single WMO: ND_Human_Inn.wmo. This WMO was introduced in Wrath of the Lich King, and is the stock human inn used in various settlements around Northrend.

Lighting Formula #

Before diving into the guts of the rendering logic, let’s take a look at the standard lighting formula used for WMOs:

1
2
3
4
lightColor.rgb  = saturate((dirColor.rgb * dirFactor) + ambColor.rgb);
lightColor.rgb *= matDiffuseColor.rgb;
lightColor.rgb += vertexColor.rgb;
lightColor.rgb  = saturate(lightColor.rgb + matEmissiveColor.rgb)

This formula is found in the WMO vertex shaders, and its output is passed along to the WMO pixel shaders. Once in the pixel shader, lightColor is doubled prior to being multiplied against the sampled texture(s).

Lighting Modes #

As mentioned above, depending on the conditionals in the unified render path, the client sources dirColor and ambColor from various places.

These conditionals are distilled into 4 distinct lighting modes:

Mode Name dirColor ambColor
0 Unlit vec3(0.0, 0.0, 0.0) vec3(0.0, 0.0, 0.0)
1 Exterior DNInfo->lightInfo->dirColor DNInfo->lightInfo->ambColor
2 Window DNInfo->lightInfo->windowDirColor DNInfo->lightInfo->windowAmbColor
3 Interior vec3(0.0, 0.0, 0.0) SMOHeader->ambColor

Group Rendering Loop #

The core of the unified rendering path is a loop that iterates across all batches defined in the MOBA chunk for each WMO group.

This loop branches depending on the batch’s type. Batches are broken into three types based on the count values from the MOGP header: trans, int, and ext (in that order). int and ext batches are handled together in one branch, while trans batches are handled separately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void CMapObj::RenderGroupUnified(CMapObjGroup* group, uint32_t frustumCount) {

    // ... snip ...

    for (int batchIdx = 0; batchIdx < group->batchCount; batchIdx++) {

        SMOBatch *batch = group->batchList[batchIdx];

        // common rendering logic

        if (batchIdx >= group->transBatchCount) {

            // int / ext batch rendering logic

        } else {

            // trans batch rendering logic

        }

    }

    // ... snip ...

}

Common Rendering Logic #

Before branching for batch type specific logic, the client sets up general rendering state that applies to all batches.

WIP

Int / Ext Batch Rendering Logic #

Now that the basics are out of the way, let’s dive into the rendering logic for int / ext batch types.

Within int / ext batch type rendering, there are two major branches: exterior and interior.

Interestingly, although assets flagged for unified rendering still divide batches across trans, int, and ext batch types, the unified rendering path does not distinguish between int and ext batch types. Rather, the client relies on the presence or absence of group flags (SMOGroup::EXTERIOR, SMOGroup::EXTERIOR_LIT) to determine which branch to follow.

Branch Batch Type Group Flags Possible Lighting Modes
Exterior int / ext EXTERIOR | EXTERIOR_LIT Unlit, Exterior
Interior int / ext !(EXTERIOR | EXTERIOR_LIT) Window, Interior
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SMOGroup::EXTERIOR (0x8)
// SMOGroup::EXTERIOR_LIT (0x40)
if (group->flags & (SMOGroup::EXTERIOR | SMOGroup::EXTERIOR_LIT)) {

    // Exterior / Exterior Lit Groups
    // (see next sections)

} else {

    // Interior Groups
    // (see next sections)

}

// Set blending mode to mode specified by SMOMaterial
if (g_theGxDevicePtr->unk1[981]) {

    v41 = g_theGxDevicePtr->unk2[1595] + 144;

    if (*v41 != material->blendMode) {

        g_theGxDevicePtr->IRsDirty(GxRs_BlendingMode);
        *v41 = material->blendMode;

    }

}

CShaderEffect::SetAlphaRefDefault();

CMapObj::SetShaders();

// Draw batch
CGxBatch intExtBatch = {
    GxPrim_Triangles,
    batch->startIndex,
    batch->count,
    batch->minIndex,
    batch->maxIndex
};

g_theGxDevicePtr->BufRender(&intExtBatch, 1);

Exterior / Exterior Lit Groups

The exterior branch within int / ext batch type rendering deals with batches in groups flagged as either SMOGroup::EXTERIOR (0x8) or SMOGroup::EXTERIOR_LIT (0x40).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int32_t lightingMode;

// Choose the lighting mode
// F_UNLIT (0x1)
if (material->flags & F_UNLIT) {

    // Unlit (0)
    lightingMode = 0;

} else {

    // Exterior (1)
    lightingMode = 1;

}

// Set the lighting mode (dirColor, ambColor, etc)
CMapObj::SetLighting(group, lightingMode);

// Disable interior shadows (0)
CMapObj::SetShadow(0);

// Set fog mode to exterior (0x2)
CMapObj::SetFog(0x2);

Interior Groups

The interior branch within int / ext batch type rendering deals with batches in all remaining groups (ie. groups not flagged as SMOGroup::EXTERIOR or SMOGroup::EXTERIOR_LIT).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int32_t lightingMode;

// Choose the lighting mode
// F_WINDOW (0x20)
if (material->flags & F_WINDOW) {

    // Window (2)
    lightingMode = 2;

} else {

    // Interior (3)
    lightingMode = 3;

}

// Set lighting mode (dirColor, ambColor, etc)
CMapObj::SetLighting(group, lightingMode);

// Enable interior shadows (1)
CMapObj::SetShadow(1);

// Set fog mode to v81
CMapObj::SetFog(v81);

WIP

Trans Batch Rendering Logic #

trans type batches are rendered using two draw passes.

WIP

Draw Pass 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// Disable interior shadows
CMapObj::SetShadow(0);

int32_t lightingMode;

// Choose the lighting mode
// F_UNLIT (0x1)
if (material->flags & F_UNLIT) {

    // Unlit (0)
    lightingMode = 0;

// F_WINDOW (0x20)
} else if (material->flags & F_WINDOW) {

    // Window (2)
    lightingMode = 2;

} else {

    // Exterior (1)
    lightingMode = 1;

}

// Set lighting mode (dirColor, ambColor, etc)
CMapObj::SetLighting(group, lightingMode);

// Set fog mode
// F_UNFOGGED (0x2)
if (material->flags & F_UNFOGGED) {

    // Disabled (0x0)
    CMapObj::SetFog(0x0);

} else {

    // Exterior (0x2)
    CMapObj::SetFog(0x2);

}

// Set blending mode to GxBlend_SrcAlphaOpaque
if (g_theGxDevicePtr->unk1[981]) {

    curBlend = g_theGxDevicePtr->unk2[1595] + 144;

    if (*curBlend != GxBlend_SrcAlphaOpaque) {

        g_theGxDevicePtr->IRsDirty(GxRs_BlendingMode);
        *curBlend = GxBlend_SrcAlphaOpaque;

    }

}

CShaderEffect::SetAlphaRefDefault();

CMapObj::SetShaders();

// Draw batch
CGxBatch transBatchPass1 = {
    GxPrim_Triangles,
    batch->startIndex,
    batch->count,
    batch->minIndex,
    batch->maxIndex
};

g_theGxDevicePtr->BufRender(&transBatchPass1, 1);

Draw Pass 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Set lighting mode (dirColor, ambColor, etc) to interior (3)
CMapObj::SetLighting(group, 3);

// Set fog mode
// F_UNFOGGED (0x2)
if (material->flags & F_UNFOGGED) {

    // Disabled (0x0)
    CMapObj::SetFog(0x0);

} else {

    CMapObj::SetFog(v81);

}

// Enable interior shadows (1)
CMapObj::SetShadow(1);

// Set blending mode to GxBlend_InvSrcAlphaAdd
if (g_theGxDevicePtr->unk1[981]) {

    curBlend = g_theGxDevicePtr->unk2[1595] + 144;

    if (*curBlend != GxBlend_InvSrcAlphaAdd) {

        g_theGxDevicePtr->IRsDirty(GxRs_BlendingMode);
        *curBlend = GxBlend_InvSrcAlphaAdd;

    }

}

CShaderEffect::SetAlphaRefDefault();

CMapObj::SetShaders();

// Draw batch
CGxBatch transBatchPass2 = {
    GxPrim_Triangles,
    batch->startIndex,
    batch->count,
    batch->minIndex,
    batch->maxIndex
};

g_theGxDevicePtr->BufRender(&transBatchPass2, 1);