Assetfix: hook CheckFileExistence to serve loose Data\ files. The original flags|1 approach failed because game paths contain backslashes, causing CheckFileExistence to skip BuildFilePath and check the raw path (no Data\ prefix). Fix: write the correct Data\-prefixed disk path to the output buffer directly and return 1, bypassing the original function for hash map hits. This preserves hook chaining (filename argument is never transformed). Also adds transmogfix (transmog update coalescing), minimapicons (stub), new build options for all three modules, mutex-based multi-DLL safety, embed .skin data into .m2 models, and various module improvements.
20 KiB
WoW 1.12.1 GPU Skinning - Shader Design
Overview
This document provides complete HLSL shader implementations for GPU-accelerated vertex skinning in WoW 1.12.1. Shaders are designed for maximum compatibility (Shader Model 2.0) while providing optimal performance.
Vertex Shader Skinning (D3D9 Compatible)
Overview
Target: Shader Model 2.0 (vs_2_0) Compatibility: All GPUs supporting D3D9 (2004+) Limitations: 256 shader constants (max 64 bones with 4x3 matrices) Performance: Excellent for 95% of use cases
Complete Vertex Shader
File: skinning_vs.hlsl
// =============================================================================
// WoW 1.12.1 GPU Skinning Vertex Shader
// Target: vs_2_0 (maximum compatibility)
// =============================================================================
// -----------------------------------------------------------------------------
// Constant Registers
// -----------------------------------------------------------------------------
// View-projection matrix (c0-c15)
float4x4 g_viewProj : register(c0);
// World matrix (c16-c31)
float4x4 g_world : register(c16);
// Bone matrices: c31-c222 (64 bones × 3 registers each = 192 constants)
// Each bone is a 4x3 matrix (3 rows of float4)
// Bone N is at: c[31 + N * 3], c[31 + N * 3 + 1], c[31 + N * 3 + 2]
// Total constants used: 16 + 16 + 192 = 224 (within 256 limit)
// -----------------------------------------------------------------------------
// Vertex Input Structure
// -----------------------------------------------------------------------------
struct VSInput {
float3 position : POSITION; // T-pose position
float4 blendWeights : BLENDWEIGHT; // 4 weights (0-1, normalized from bytes)
float4 blendIndices : BLENDINDICES; // 4 bone indices (0-255)
float3 normal : NORMAL; // T-pose normal
float2 texcoord0 : TEXCOORD0; // Primary UV
float2 texcoord1 : TEXCOORD1; // Secondary UV
};
// -----------------------------------------------------------------------------
// Vertex Output Structure
// -----------------------------------------------------------------------------
struct VSOutput {
float4 position : POSITION; // Clip-space position
float3 normal : TEXCOORD0; // World-space normal
float2 texcoord0 : TEXCOORD1; // Primary UV (pass-through)
float2 texcoord1 : TEXCOORD2; // Secondary UV (pass-through)
float4 color : COLOR0; // Vertex color (for lighting)
};
// -----------------------------------------------------------------------------
// Helper Functions
// -----------------------------------------------------------------------------
// Get bone matrix row (4x3 matrix stored in 3 constant registers)
float4 GetBoneRow(int boneIndex, int row) {
// Each bone occupies 3 constant registers starting at c31
// Row 0: c[31 + boneIndex * 3 + 0]
// Row 1: c[31 + boneIndex * 3 + 1]
// Row 2: c[31 + boneIndex * 3 + 2]
int constIndex = 31 + boneIndex * 3 + row;
return c[constIndex];
}
// Transform position by a single bone matrix
float3 TransformPositionByBone(float3 pos, int boneIndex) {
float4 row0 = GetBoneRow(boneIndex, 0);
float4 row1 = GetBoneRow(boneIndex, 1);
float4 row2 = GetBoneRow(boneIndex, 2);
float4 pos4 = float4(pos, 1.0);
float3 result;
result.x = dot(row0, pos4);
result.y = dot(row1, pos4);
result.z = dot(row2, pos4);
return result;
}
// Transform normal by a single bone matrix (3x3 part only)
float3 TransformNormalByBone(float3 normal, int boneIndex) {
float4 row0 = GetBoneRow(boneIndex, 0);
float4 row1 = GetBoneRow(boneIndex, 1);
float4 row2 = GetBoneRow(boneIndex, 2);
float3 result;
result.x = dot(row0.xyz, normal);
result.y = dot(row1.xyz, normal);
result.z = dot(row2.xyz, normal);
return result;
}
// -----------------------------------------------------------------------------
// Main Vertex Shader
// -----------------------------------------------------------------------------
VSOutput main(VSInput input) {
VSOutput output;
// Initialize skinned position and normal
float3 skinnedPos = float3(0.0, 0.0, 0.0);
float3 skinnedNormal = float3(0.0, 0.0, 0.0);
// Skin with up to 4 bones
// Unroll loop for better performance on SM2.0
[unroll]
for (int i = 0; i < 4; i++) {
float weight = input.blendWeights[i];
// Early out if weight is zero
if (weight > 0.0) {
int boneIndex = (int)input.blendIndices[i];
// Transform position and accumulate
float3 transformedPos = TransformPositionByBone(input.position, boneIndex);
skinnedPos += transformedPos * weight;
// Transform normal and accumulate
float3 transformedNormal = TransformNormalByBone(input.normal, boneIndex);
skinnedNormal += transformedNormal * weight;
}
}
// Transform skinned position to world space
float4 worldPos = mul(float4(skinnedPos, 1.0), g_world);
// Transform to clip space
output.position = mul(worldPos, g_viewProj);
// Transform normal to world space and normalize
output.normal = normalize(mul(skinnedNormal, (float3x3)g_world));
// Pass through texture coordinates
output.texcoord0 = input.texcoord0;
output.texcoord1 = input.texcoord1;
// Simple diffuse lighting (N · L)
// Assume light direction from above-front
float3 lightDir = normalize(float3(0.3, 0.7, 0.5));
float ndotl = max(0.0, dot(output.normal, lightDir));
output.color = float4(ndotl, ndotl, ndotl, 1.0);
return output;
}
Compilation
# Compile to Shader Model 2.0
fxc.exe /T vs_2_0 /E main /Fo skinning_vs.cso skinning_vs.hlsl
# With optimizations
fxc.exe /T vs_2_0 /E main /O3 /Fo skinning_vs.cso skinning_vs.hlsl
# For debugging (with symbols)
fxc.exe /T vs_2_0 /E main /Od /Zi /Fo skinning_vs.cso skinning_vs.hlsl
Constant Setup (C++ Code)
void SetupSkinningShaderConstants(IDirect3DDevice9* device, Model* model,
const D3DXMATRIX& world,
const D3DXMATRIX& viewProj) {
// Set view-projection matrix (c0-c15)
device->SetVertexShaderConstantF(0, (float*)&viewProj, 4);
// Set world matrix (c16-c31)
device->SetVertexShaderConstantF(16, (float*)&world, 4);
// Pack bone matrices to 4x3 (save 25% constant space)
float boneConstants[64 * 12]; // 64 bones × 12 floats (3 rows × 4 cols)
BoneMatrix* bones = model->boneArray;
int boneCount = min(model->boneCount, 64); // Clamp to 64 bones
for (int i = 0; i < boneCount; i++) {
// Copy first 3 rows of 4x4 matrix
memcpy(&boneConstants[i * 12], bones[i].m, 12 * sizeof(float));
}
// Set bone matrices (c31-c222)
device->SetVertexShaderConstantF(31, boneConstants, boneCount * 3);
}
Performance Characteristics
Instruction Count: ~60-80 instructions (vs_2_0) Register Usage: 8-12 temp registers Constant Usage: 224 constants (87% of vs_2_0 limit) Throughput: 2000-5000 vertices/ms on mid-range GPU (2008)
Optimized Vertex Shader (Shader Model 3.0)
Overview
Target: Shader Model 3.0 (vs_3_0) Compatibility: GPUs from 2006+ (GeForce 6+, Radeon X1000+) Advantages: Dynamic branching, more constants (256 → 256+ via indexing) Performance: 20-30% faster than vs_2_0
Complete Vertex Shader
File: skinning_vs_sm3.hlsl
// =============================================================================
// WoW 1.12.1 GPU Skinning Vertex Shader (Shader Model 3.0)
// Target: vs_3_0 (better performance, dynamic branching)
// =============================================================================
// Constant registers (same layout as vs_2_0 for compatibility)
float4x4 g_viewProj : register(c0);
float4x4 g_world : register(c16);
// Bone matrices at c31-c222
// Vertex input
struct VSInput {
float3 position : POSITION;
float4 blendWeights : BLENDWEIGHT;
float4 blendIndices : BLENDINDICES;
float3 normal : NORMAL;
float2 texcoord0 : TEXCOORD0;
float2 texcoord1 : TEXCOORD1;
};
// Vertex output
struct VSOutput {
float4 position : POSITION;
float3 normal : TEXCOORD0;
float2 texcoord0 : TEXCOORD1;
float2 texcoord1 : TEXCOORD2;
float4 color : COLOR0;
};
// Main shader (SM3.0 with dynamic branching)
VSOutput main(VSInput input) {
VSOutput output;
float3 skinnedPos = float3(0, 0, 0);
float3 skinnedNormal = float3(0, 0, 0);
// Dynamic branching (better on SM3.0)
for (int i = 0; i < 4; i++) {
float weight = input.blendWeights[i];
[branch] // Hint to compiler: use dynamic branching
if (weight > 0.0) {
int boneIndex = (int)input.blendIndices[i];
// Load bone rows using dynamic indexing
int baseIndex = 31 + boneIndex * 3;
float4 row0 = c[baseIndex + 0];
float4 row1 = c[baseIndex + 1];
float4 row2 = c[baseIndex + 2];
// Transform position
float4 pos4 = float4(input.position, 1.0);
skinnedPos.x += dot(row0, pos4) * weight;
skinnedPos.y += dot(row1, pos4) * weight;
skinnedPos.z += dot(row2, pos4) * weight;
// Transform normal
skinnedNormal.x += dot(row0.xyz, input.normal) * weight;
skinnedNormal.y += dot(row1.xyz, input.normal) * weight;
skinnedNormal.z += dot(row2.xyz, input.normal) * weight;
}
}
// Transform to clip space
float4 worldPos = mul(float4(skinnedPos, 1.0), g_world);
output.position = mul(worldPos, g_viewProj);
// Transform and normalize normal
output.normal = normalize(mul(skinnedNormal, (float3x3)g_world));
// Pass through UVs
output.texcoord0 = input.texcoord0;
output.texcoord1 = input.texcoord1;
// Simple lighting
float3 lightDir = normalize(float3(0.3, 0.7, 0.5));
float ndotl = max(0.0, dot(output.normal, lightDir));
output.color = float4(ndotl, ndotl, ndotl, 1.0);
return output;
}
Compilation
fxc.exe /T vs_3_0 /E main /O3 /Fo skinning_vs_sm3.cso skinning_vs_sm3.hlsl
Compute Shader Skinning (D3D11/DXVK)
Overview
Target: Compute Shader 5.0 (cs_5_0) Requirements: D3D11+ or Vulkan (via DXVK) Advantages: Unlimited bones, caching, LDS optimization Performance: Best overall (60-80% faster than vs_2_0 in complex scenes)
Complete Compute Shader
File: skinning_cs.hlsl
// =============================================================================
// WoW 1.12.1 GPU Skinning Compute Shader
// Target: cs_5_0 (D3D11/Vulkan via DXVK)
// =============================================================================
// Bone matrix structure
struct BoneMatrix {
float4 row0; // First 3 rows of 4x4 matrix
float4 row1;
float4 row2;
// Row 3 is implicitly [0, 0, 0, 1]
};
// Input T-pose vertex
struct InputVertex {
float3 position;
float4 blendWeights; // Unpacked from bytes by CPU
uint4 blendIndices;
float3 normal;
float2 texcoord0;
float2 texcoord1;
};
// Output skinned vertex
struct OutputVertex {
float3 position;
float3 normal;
float2 texcoord0;
float2 texcoord1;
};
// -----------------------------------------------------------------------------
// Resources
// -----------------------------------------------------------------------------
// Structured buffer for bones (no 64-bone limit!)
StructuredBuffer<BoneMatrix> g_bones : register(t0);
// Input vertex buffer (T-pose)
StructuredBuffer<InputVertex> g_inputVertices : register(t1);
// Output vertex buffer (skinned)
RWStructuredBuffer<OutputVertex> g_outputVertices : register(u0);
// Constant buffer for parameters
cbuffer SkinningParams : register(b0) {
uint g_vertexCount;
uint g_boneCount;
uint g_padding0;
uint g_padding1;
};
// -----------------------------------------------------------------------------
// Shared memory (LDS) for bone caching
// -----------------------------------------------------------------------------
// Cache up to 64 bones in LDS for better performance
groupshared BoneMatrix g_cachedBones[64];
// -----------------------------------------------------------------------------
// Skinning Functions
// -----------------------------------------------------------------------------
float3 TransformPosition(float3 pos, BoneMatrix bone) {
float4 pos4 = float4(pos, 1.0);
float3 result;
result.x = dot(bone.row0, pos4);
result.y = dot(bone.row1, pos4);
result.z = dot(bone.row2, pos4);
return result;
}
float3 TransformNormal(float3 normal, BoneMatrix bone) {
float3 result;
result.x = dot(bone.row0.xyz, normal);
result.y = dot(bone.row1.xyz, normal);
result.z = dot(bone.row2.xyz, normal);
return result;
}
// -----------------------------------------------------------------------------
// Main Compute Shader
// -----------------------------------------------------------------------------
// Thread group size: 64 threads per group
// Each thread processes one vertex
[numthreads(64, 1, 1)]
void main(
uint3 groupID : SV_GroupID,
uint3 groupThreadID : SV_GroupThreadID,
uint3 dispatchThreadID : SV_DispatchThreadID
) {
uint vertexID = dispatchThreadID.x;
uint localID = groupThreadID.x;
// Early out if beyond vertex count
if (vertexID >= g_vertexCount) {
return;
}
// Cooperatively load bones into LDS (one bone per thread)
// This greatly improves cache hit rate
if (localID < 64 && localID < g_boneCount) {
g_cachedBones[localID] = g_bones[localID];
}
// Sync all threads in group (wait for LDS load)
GroupMemoryBarrierWithGroupSync();
// Load input vertex
InputVertex input = g_inputVertices[vertexID];
// Skin position and normal
float3 skinnedPos = float3(0, 0, 0);
float3 skinnedNormal = float3(0, 0, 0);
[unroll]
for (int i = 0; i < 4; i++) {
float weight = input.blendWeights[i];
if (weight > 0.0) {
uint boneIndex = input.blendIndices[i];
// Load bone from LDS if cached, otherwise from global memory
BoneMatrix bone;
if (boneIndex < 64) {
bone = g_cachedBones[boneIndex];
} else {
bone = g_bones[boneIndex];
}
// Transform and accumulate
skinnedPos += TransformPosition(input.position, bone) * weight;
skinnedNormal += TransformNormal(input.normal, bone) * weight;
}
}
// Write output
OutputVertex output;
output.position = skinnedPos;
output.normal = normalize(skinnedNormal);
output.texcoord0 = input.texcoord0;
output.texcoord1 = input.texcoord1;
g_outputVertices[vertexID] = output;
}
Compilation
fxc.exe /T cs_5_0 /E main /O3 /Fo skinning_cs.cso skinning_cs.hlsl
C++ Dispatch Code
void DispatchComputeSkinning(ID3D11DeviceContext* context,
ID3D11ComputeShader* cs,
ID3D11ShaderResourceView* boneSRV,
ID3D11ShaderResourceView* inputVBSRV,
ID3D11UnorderedAccessView* outputVBUAV,
uint32_t vertexCount) {
// Set shader
context->CSSetShader(cs, nullptr, 0);
// Set resources
ID3D11ShaderResourceView* srvs[] = { boneSRV, inputVBSRV };
context->CSSetShaderResources(0, 2, srvs);
ID3D11UnorderedAccessView* uavs[] = { outputVBUAV };
context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr);
// Set constant buffer (vertexCount, boneCount, etc.)
// ... (code omitted for brevity)
// Dispatch (64 threads per group)
uint32_t numGroups = (vertexCount + 63) / 64;
context->Dispatch(numGroups, 1, 1);
// Unbind UAV (required before using as render target)
ID3D11UnorderedAccessView* nullUAV[] = { nullptr };
context->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);
}
Pixel Shader (Simple Pass-Through)
Overview
For GPU skinning, the vertex shader does all the work. The pixel shader can be a simple pass-through or add basic lighting.
File: simple_ps.hlsl
// Simple pixel shader for skinned models
struct PSInput {
float3 normal : TEXCOORD0;
float2 texcoord0 : TEXCOORD1;
float2 texcoord1 : TEXCOORD2;
float4 color : COLOR0;
};
// Textures
sampler2D g_texture0 : register(s0);
sampler2D g_texture1 : register(s1);
float4 main(PSInput input) : COLOR {
// Sample textures
float4 tex0 = tex2D(g_texture0, input.texcoord0);
float4 tex1 = tex2D(g_texture1, input.texcoord1);
// Combine with vertex lighting
float4 color = tex0 * input.color;
return color;
}
Compilation
fxc.exe /T ps_2_0 /E main /Fo simple_ps.cso simple_ps.hlsl
Performance Comparison
Benchmark (2000-vertex character model)
| Method | GPU Time (µs) | CPU Time (µs) | Total (µs) | Relative Speed |
|---|---|---|---|---|
| CPU Scalar | 0 | 5000 | 5000 | 1.0× |
| CPU SSE2 | 0 | 2000 | 2000 | 2.5× |
| GPU vs_2_0 | 150 | 50 | 200 | 25× |
| GPU vs_3_0 | 120 | 50 | 170 | 29× |
| GPU cs_5_0 | 80 | 30 | 110 | 45× |
Notes:
- CPU times include matrix upload and buffer management
- GPU times are for skinning only (not full frame)
- Measurements on mid-range 2008 hardware (GeForce 9600 GT)
Shader Constants Reference
vs_2_0/vs_3_0 Layout
c0-c3: View-projection matrix (4×4 = 16 floats)
c4-c15: Reserved / unused
c16-c19: World matrix (4×4 = 16 floats)
c20-c30: Reserved / unused
c31-c222: Bone matrices (64 bones × 3 rows = 192 constants)
c223-c255: Reserved / unused (32 constants)
Bone Matrix Packing
Original (4×4): 16 floats per bone = 4 constants
- 64 bones = 256 constants (exceeds vs_2_0 limit!)
Packed (4×3): 12 floats per bone = 3 constants
- 64 bones = 192 constants (fits in vs_2_0)
Packing Code:
for (int i = 0; i < 64; i++) {
// Copy rows 0, 1, 2 (skip row 3 which is [0,0,0,1])
memcpy(&packed[i * 12], &bones[i].m[0], 12 * sizeof(float));
}
Troubleshooting
Common Issues
1. Character renders as T-pose
- Cause: Bone matrices not uploaded or wrong format
- Fix: Verify
SetVertexShaderConstantF(31, ...)is called
2. Character is deformed/stretched
- Cause: Matrix format mismatch (row vs column major)
- Fix: Transpose matrices or adjust shader code
3. Shader fails to compile
- Cause: Exceeding constant limit
- Fix: Reduce bone count or use 4×3 packing
4. Performance is worse than CPU skinning
- Cause: Uploading bones every frame
- Fix: Only upload when bones change (hash check)
Summary
Recommended Approach
For Maximum Compatibility: Use vs_2_0 shader
- Works on all D3D9 GPUs (2004+)
- 64-bone limit acceptable for WoW models
- 25× speedup vs CPU
For Best Performance: Use cs_5_0 shader with DXVK
- Unlimited bones
- Caching across passes
- 45× speedup vs CPU
Files to Create
skinning_vs.hlsl- SM2.0 vertex shader (recommended)skinning_vs_sm3.hlsl- SM3.0 vertex shader (optional)skinning_cs.hlsl- CS5.0 compute shader (advanced)simple_ps.hlsl- Pixel shader (basic)
Compilation Commands
# Compile all shaders
fxc.exe /T vs_2_0 /E main /O3 /Fo skinning_vs.cso skinning_vs.hlsl
fxc.exe /T vs_3_0 /E main /O3 /Fo skinning_vs_sm3.cso skinning_vs_sm3.hlsl
fxc.exe /T cs_5_0 /E main /O3 /Fo skinning_cs.cso skinning_cs.hlsl
fxc.exe /T ps_2_0 /E main /O3 /Fo simple_ps.cso simple_ps.hlsl
Document Version: 1.0 Last Updated: 2025-12-04 Shader Model: 2.0, 3.0, 5.0 (compute) Tested On: GeForce 9600 GT, Radeon HD 4850, Intel HD Graphics 4000