M2/.skin/WotLK shader selection

From wowdev
Revision as of 23:21, 25 May 2016 by Fallenoak (talk | contribs) (Added example implementation of shader selection for WotLK)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The following is a mostly-working (knock on wood) implementation of the runtime logic necessary to identify shaders for M2s in Wrath of the Lich King.

Code is from the Wowser project: https://github.com/wowserhq/wowser

class BatchManager {
  constructor(data, skinData) {
    this.data = data;
    this.skinData = skinData;

    this.calculateRuntimeValues();
  }

  stubDef() {
    const def = {
      flags: null,
      shaderID: null,
      shaderNames: {
        vertex: null,
        fragment: null
      },
      opCount: null,
      textureMapping: null,
      renderFlags: null,
      blendingMode: null,
      textures: [],
      textureIndices: [],
      uvAnimations: [],
      uvAnimationIndices: [],
      transparencyAnimation: null,
      transparencyAnimationIndex: null,
      vertexColorAnimation: null,
      vertexColorAnimationIndex: null
    };

    return def;
  }

  createDefs() {
    const defs = [];

    this.skinData.batches.forEach((batchData) => {
      const def = this.createDef(batchData);
      defs.push(def);
      console.log(def.shaderNames);
    });

    return defs;
  }

  createDef(batchData) {
    const def = this.stubDef();

    const { textures } = this.data;
    const { vertexColorAnimations, transparencyAnimations, uvAnimations } = this.data;

    // Not everything has layered batches, so sub835E90 may not have run, and texures / uv
    // animations may still need to be resolved.
    if (!batchData.textureIndices) {
      batchData.textureIndices = [];

      for (let opIndex = 0; opIndex < batchData.runtimeOpCount; opIndex++) {
        const textureIndex = this.data.textureLookups[batchData.textureLookup + opIndex];
        batchData.textureIndices.push(textureIndex);
      }
    }

    if (!batchData.uvAnimationIndices) {
      batchData.uvAnimationIndices = [];

      for (let opIndex = 0; opIndex < batchData.runtimeOpCount; opIndex++) {
        const uvAnimationIndex = this.data.uvAnimationLookups[batchData.uvAnimationLookup + opIndex] + 1;
        batchData.uvAnimationIndices.push(uvAnimationIndex);
      }
    }

    const { runtimeOpCount } = batchData;
    const { textureMappingIndex, materialIndex } = batchData;
    const { vertexColorAnimationIndex, transparencyAnimationLookup } = batchData;
    const { textureIndices, uvAnimationIndices } = batchData;

    // Batch flags
    def.flags = batchData.flags;

    // Submesh index and batch layer
    def.submeshIndex = batchData.submeshIndex;
    def.layer = batchData.layer;

    // Op count and shader ID
    def.opCount = batchData.runtimeOpCount;
    def.shaderID = batchData.runtimeShaderID;

    // Texture mapping
    // -1 => Env; 0 => T1; 1 => T2
    if (textureMappingIndex >= 0) {
      const textureMapping = this.data.textureMappings[textureMappingIndex];
      def.textureMapping = textureMapping;
    }

    // Material (render flags and blending mode)
    const material = this.data.materials[materialIndex];
    def.renderFlags = material.renderFlags;
    def.blendingMode = material.blendingMode;

    // Determine names of vertex and fragment shader for this batch
    def.shaderNames = this.shaderNames(def.shaderID, def.opCount, def.textureMapping);

    // Vertex color animation block
    if (vertexColorAnimationIndex > -1 && vertexColorAnimations[vertexColorAnimationIndex]) {
      const vertexColorAnimation = vertexColorAnimations[vertexColorAnimationIndex];
      def.vertexColorAnimation = vertexColorAnimation;
      def.vertexColorAnimationIndex = vertexColorAnimationIndex;
    }

    // Transparency animation block
    // TODO: Do we load multiple values based on opCount?
    const transparencyAnimationIndex = this.data.transparencyAnimationLookups[transparencyAnimationLookup];
    if (transparencyAnimationIndex > -1 && transparencyAnimations[transparencyAnimationIndex]) {
      const transparencyAnimation = transparencyAnimations[transparencyAnimationIndex];
      def.transparencyAnimation = transparencyAnimation;
      def.transparencyAnimationIndex = transparencyAnimationIndex;
    }

    for (let opIndex = 0; opIndex < runtimeOpCount; ++opIndex) {
      // Texture
      // - already resolved to index in sub835E90
      const textureIndex = textureIndices[opIndex];
      const texture = textures[textureIndex];
      if (texture) {
        def.textures[opIndex] = texture;
        def.textureIndices[opIndex] = textureIndex;
      }

      // UV animation block
      // - already resolved to index in sub835E90
      // - decrement by 1 because sub835E90 increases by 1
      const uvAnimationIndex = uvAnimationIndices[opIndex] - 1;
      const uvAnimation = uvAnimations[uvAnimationIndex];
      if (uvAnimation) {
        def.uvAnimations[opIndex] = uvAnimation;
        def.uvAnimationIndices[opIndex] = uvAnimationIndex;
      }
    }

    return def;
  }

  calculateRuntimeValues() {
    this.skinData.batches.forEach((batchData, index) => {
      // Grab a fresh copy of the original on-disk shader ID and op count.
      batchData.runtimeShaderID = batchData.shaderID;
      batchData.runtimeOpCount = batchData.opCount;
    });

    this.sub836980();
    this.sub837680();
  }

  sub836980() {
    this.skinData.batches.forEach((batchData) => {
      // The shader ID is already 'modernized', so there's nothing to do.
      if (batchData.runtimeShaderID & 0x8000) {
        return;
      }

      const materialData = this.data.materials[batchData.materialIndex];

      if (!this.data.overrideBlending) {
        const textureMapping = this.data.textureMappings[batchData.textureMappingIndex];

        const envMapped = textureMapping === -1;
        const nonOpaqueBlendingMode = materialData.blendingMode !== 0;

        let newShaderID = 0;

        if (nonOpaqueBlendingMode) {
          newShaderID = 1;

          if (envMapped) {
            newShaderID |= 8;
          }
        }

        newShaderID *= 16;

        if (textureMapping === 1) {
          newShaderID |= 0x4000;
        }

        // Override shaderID
        batchData.runtimeShaderID = newShaderID;
      } else {
        // Shouldn't really happen, but client does check this, so it's perhaps a safeguard.
        if (batchData.runtimeOpCount === 0) {
          return;
        }

        const v19 = [0, 0];

        let blendingOverrideIndex = null;
        let blendingOverride = null;
        let textureMappingIndex = null;
        let textureMapping = null;
        let envMapped = null;

        let newShaderID = 0;

        for (let opIndex = 0; opIndex < batchData.runtimeOpCount; opIndex++) {
          blendingOverrideIndex = batchData.runtimeShaderID + opIndex;
          blendingOverride = this.data.blendingOverrides[blendingOverrideIndex];

          if (opIndex === 0 && materialData.blendingMode === 0) {
            blendingOverride = 0;
          }

          textureMappingIndex = batchData.textureMappingIndex + opIndex;
          textureMapping = this.data.textureMappings[textureMappingIndex];
          envMapped = textureMapping === -1;

          if (envMapped) {
            v19[opIndex] = blendingOverride | 8;
          } else {
            v19[opIndex] = blendingOverride;
          }

          if (textureMapping === 1 && opIndex + 1 === batchData.runtimeOpCount) {
            newShaderID |= 0x4000;
          }
        }

        // TODO: potentially need to LOWORD(v19[1])
        newShaderID |= v19[1] | (v19[0] * 16);

        // Override shaderID
        batchData.runtimeShaderID = newShaderID;
      }
    });
  }

  sub837680() {
    let nonLayeredBatchCount = 0;

    this.skinData.batches.forEach((batchData) => {
      if (batchData.layer <= 0) {
        nonLayeredBatchCount++;
      }
    });

    // If no batches are layered, there's nothing to do.
    if (nonLayeredBatchCount === this.skinData.batches.length) {
      return;
    }

    // Resolve texture and UV animation lookups to indices.
    this.sub835E90();

    let v31 = [0, 0];
    let v33 = 0;

    let sharedMaterials = false;

    let currentLayer = null;
    let firstLayer = null;
    let currentBatch = null;
    let previousBatch = null;
    let currentMaterial = null;
    let firstMaterial = null;
    let currentTextureMapping = null;
    let nextTextureMapping = null;

    // Do a bunch of shader, texture index, and UV animation index manipulations based on blending
    // modes, layering, etc.
    this.skinData.batches.forEach((batchData) => {
      currentLayer = batchData;
      currentBatch = batchData;

      // If the material hasn't changed from the last batch, iterate. We'll copy over the relevant
      // data from the previous batch after this loop.
      if (previousBatch && currentBatch.materialIndex === previousBatch.materialIndex) {
        sharedMaterials = true;
        return;
      }

      previousBatch = batchData;

      currentTextureMapping = this.data.textureMappings[currentLayer.textureMappingIndex];
      nextTextureMapping = this.data.textureMappings[currentLayer.textureMappingIndex + 1];

      const v26 = currentLayer.runtimeShaderID & 0x07;
      currentMaterial = this.data.materials[currentLayer.materialIndex];

      // First layer, do a few resets.
      if (currentLayer.layer === 0) {
        v31 = [0, 0];

        if (currentLayer.runtimeOpCount >= 1 && currentMaterial.blendingMode === 0) {
          currentLayer.runtimeShaderID &= 0xFF8F;
        }

        firstLayer = currentLayer;
      }

      firstMaterial = this.data.materials[firstLayer.materialIndex];

      const firstLayerTransparencyIndex = this.data.transparencyAnimationLookups[firstLayer.transparencyAnimationLookup];
      const currentLayerTransparencyIndex = this.data.transparencyAnimationLookups[currentLayer.transparencyAnimationLookup];

      let skip1 = false;

      if (v31[0] !== 0) {
        if (v31[0] === 1) {
          if (
            (currentMaterial.blendingMode === 2 || currentMaterial.blendingMode === 1)
            && currentLayer.runtimeOpCount === 1
            && !(((currentMaterial.renderFlags & 0xFF) ^ (firstMaterial.renderFlags & 0xFF)) & 0x01)
            && (currentLayer.textureIndices[0] === firstLayer.textureIndices[0])
          ) {
            if (firstLayerTransparencyIndex === currentLayerTransparencyIndex) {
              currentLayer.runtimeShaderID = 0x8000;
              firstLayer.runtimeShaderID = 0x8001;

              v31[0] = 3;

              // goto 56
              return;
            }
          }

          v31[0] = 0;
        } else {
          // workaround for goto 34
          skip1 = true;
        }
      }

      // workaround for goto 34
      if (!skip1) {
        if (currentMaterial.blendingMode === 0 && currentLayer.runtimeOpCount === 2 && (v26 === 4 || v26 === 6)) {
          if (currentTextureMapping === 0 && nextTextureMapping === -1) {
            v31[0] = 1;
          }
        }
      }

      if (v31[1] !== 0) {
        if (v31[1] === 1) {
          if (
            (currentMaterial.blendingMode !== 4 && currentMaterial.blendingMode !== 6)
            || currentLayer.runtimeOpCount !== 1
            || (currentTextureMapping  >= 0 && currentTextureMapping <= 2)
          ) {
            v31[1] = 0;
          } else {
            // If transparencies match, override the textures, UV animations, op count, and shader
            // ID.
            if (firstLayerTransparencyIndex === currentLayerTransparencyIndex) {
              v31[1] = 2;

              v33 = 1;

              currentLayer.runtimeShaderID = 0x8000;

              if (currentMaterial.blendingMode !== 4) {
                firstLayer.runtimeShaderID = 0xE;
              } else {
                firstLayer.runtimeShaderID = 0x8002;
              }

              firstLayer.runtimeOpCount = 2;

              // Override texture indices
              const firstLayerTextureIndices = [];
              firstLayerTextureIndices[0] = firstLayer.textureIndices[0];
              firstLayerTextureIndices[1] = currentLayer.textureIndices[0];
              firstLayer.textureIndices = firstLayerTextureIndices;

              // Override UV animation indices
              const firstLayerUVAnimationIndices = [];
              firstLayerUVAnimationIndices[0] = firstLayer.uvAnimationIndices[0];
              firstLayerUVAnimationIndices[1] = currentLayer.uvAnimationIndices[0];
              firstLayer.uvAnimationIndices = firstLayerUVAnimationIndices;

              // Iterate to next batch / layer
              return;
            }
          }
        } else {
          if (v31[1] !== 2) {
            // Iterate to next batch / layer
            return;
          }

          if (
            (currentMaterial.blendingMode !== 2 && currentMaterial.blendingMode !== 1)
            || currentLayer.runtimeOpCount !== 1
            || (((currentMaterial.renderFlags & 0xFF) ^ (firstMaterial.renderFlags & 0xFF)) & 0x01)
            || (firstLayer.textureIndices[0] !== currentLayer.textureIndices[0])
          ) {
            v31[1] = 0;
          } else {
            if (firstLayerTransparencyIndex === currentLayerTransparencyIndex) {
              v31[1] = 3;

              currentLayer.runtimeShaderID = 0x8000;

              // TODO: signed? or something?
              firstLayer.runtimeShaderID = (2 * (firstLayer.runtimeShaderID === 0x8002 ? 1 : 0)) - 0x7FFF;

              // Iterate to next batch / layer
              return;
            }
          }
        }
      }

      if (currentMaterial.blendingMode === 0 && currentLayer.runtimeOpCount === 1 && currentTextureMapping === 0) {
        v31[1] = 1;
      }
    });

    if (v33 !== 0) {
      // TODO sub_837250(v1)
    }

    // TODO
    /*
    LABEL_59:
    sub_8374A0(v1);
    */

    if (sharedMaterials) {
      currentBatch = null;
      previousBatch = null;

      this.skinData.batches.forEach((batchData) => {
        currentBatch = batchData;

        if (previousBatch !== null && currentBatch.materialIndex === previousBatch.materialIndex) {
          currentBatch.runtimeShaderID = previousBatch.runtimeShaderID;
          currentBatch.runtimeOpCount = previousBatch.runtimeOpCount;
          currentBatch.textureIndices = previousBatch.textureIndices;
          currentBatch.uvAnimationIndices = previousBatch.uvAnimationIndices;
        } else {
          previousBatch = currentBatch;
        }
      });
    }
  }

  // Convert texture lookups and UV animation lookups into texture indices and UV animation
  // indices. Seems to be capped at opCount == 2 in WotLK. Weirdly, the UV animation indices are
  // incremented by 1, only to be later decremented by 1.
  sub835E90() {
    this.skinData.batches.forEach((batchData, index) => {
      const textureIndices = [];
      const uvAnimationIndices = [];

      for (let opIndex = 0; opIndex < batchData.runtimeOpCount; opIndex++) {
        const opTextureIndex = this.data.textureLookups[batchData.textureLookup + opIndex];
        const opUVAnimationIndex = this.data.uvAnimationLookups[batchData.uvAnimationLookup + opIndex];

        textureIndices[opIndex] = opTextureIndex;
        uvAnimationIndices[opIndex] = opUVAnimationIndex + 1; // is later decremented by 1 (before use)
      }

      batchData.textureIndices = textureIndices;
      batchData.uvAnimationIndices = uvAnimationIndices;
    });
  }

  shaderNames(shaderID, opCount, textureMapping) {
    let names = null;

    const tableLookup = !(shaderID & 0x8000);

    if (tableLookup) {
      names = this.shaderNamesFromTable(shaderID, opCount, textureMapping);

      if (!names) {
        names = this.shaderNamesFromTable(0x11, opCount, textureMapping);
      }
    } else {
      names = this.shaderNamesFromOther(shaderID);
    }

    return names;
  }

  shaderNamesFromOther(shaderID) {
    let names = null;

    switch (shaderID & 0x7FFF) {
      case 0:
        return null;

      case 1:
        names = {
          vertex: 'Diffuse_T1_Env',
          fragment: 'Combiners_Opaque_Mod2xNA_Alpha'
        };
        break;

      case 2:
        names = {
          vertex: 'Diffuse_T1_Env',
          fragment: 'Combiners_Opaque_AddAlpha'
        };
        break;

      case 3:
        names = {
          vertex: 'Diffuse_T1_Env',
          fragment: 'Combiners_Opaque_AddAlpha_Alpha'
        };
        break;

      default:
        break;
    }

    // todo: sub_876530 logic (which can throw back to the table based lookup)

    return names;
  }

  shaderNamesFromTable(shaderID, opCount, textureMapping) {
    let names = null;

    if (opCount === 1) {
      names = this.shaderNamesFromSingleOpTable(shaderID, textureMapping);
    } else {
      names = this.shaderNamesFromMultiOpTable(shaderID, textureMapping);
    }

    return names;
  }

  shaderNamesFromSingleOpTable(shaderID, textureMapping) {
    let names = null;
    let vertexName = null;
    let fragmentName = null;

    const t1FragmentMode = (shaderID >> 4) & 7;
    const t1EnvMapped = (shaderID >> 4) & 8;

    if (t1EnvMapped) {
      vertexName = 'Diffuse_Env';
    } else {
      if (textureMapping === 0) {
        vertexName = 'Diffuse_T1';
      } else {
        vertexName = 'Diffuse_T2';
      }
    }

    switch (t1FragmentMode) {
      case 0:
        fragmentName = 'Combiners_Opaque';
        break;

      case 1:
        fragmentName = 'Combiners_Mod';
        break;

      case 2:
        fragmentName = 'Combiners_Decal';
        break;

      case 3:
        fragmentName = 'Combiners_Add';
        break;

      case 4:
        fragmentName = 'Combiners_Mod2x';
        break;

      case 5:
        fragmentName = 'Combiners_Fade';
        break;

      default:
        fragmentName = 'Combiners_Mod';
        break;
    }

    names = {
      vertex: vertexName,
      fragment: fragmentName
    };

    return names;
  }

  shaderNamesFromMultiOpTable(shaderID, _textureMapping) {
    let names = null;
    let vertexName = null;
    let fragmentName = null;

    const t1FragmentMode = (shaderID >> 4) & 7;
    const t2FragmentMode = shaderID & 7;
    const t1EnvMapped = (shaderID >> 4) & 8;
    const t2EnvMapped = shaderID & 8;

    if (t1EnvMapped && t2EnvMapped) {
      vertexName = 'Diffuse_Env_Env';
    } else if (t1EnvMapped) {
      vertexName = 'Diffuse_Env_T2';
    } else if (t2EnvMapped) {
      vertexName = 'Diffuse_T1_Env';
    } else {
      vertexName = 'Diffuse_T1_T2';
    }

    if (t1FragmentMode === 0) {
      switch (t2FragmentMode) {
        case 0:
          fragmentName = 'Combiners_Opaque_Opaque';
          break;

        case 1:
          fragmentName = 'Combiners_Opaque_Mod';
          break;

        case 3:
          fragmentName = 'Combiners_Opaque_Add';
          break;

        case 4:
          fragmentName = 'Combiners_Opaque_Mod2x';
          break;

        case 6:
          fragmentName = 'Combiners_Opaque_Mod2xNA';
          break;

        case 7:
          fragmentName = 'Combiners_Opaque_AddNA';
          break;

        default:
          fragmentName = 'Combiners_Opaque_Mod';
          break;
      }
    } else if (t1FragmentMode === 1) {
      switch (t2FragmentMode) {
        case 0:
          fragmentName = 'Combiners_Mod_Opaque';
          break;

        case 3:
          fragmentName = 'Combiners_Mod_Add';
          break;

        case 4:
          fragmentName = 'Combiners_Mod_Mod2x';
          break;

        case 6:
          fragmentName = 'Combiners_Mod_Mod2xNA';
          break;

        case 7:
          fragmentName = 'Combiners_Mod_AddNA';
          break;

        default:
          fragmentName = 'Combiners_Mod_Mod';
          break;
      }
    } else if (t1FragmentMode === 3 && t2FragmentMode === 1) {
      fragmentName = 'Combiners_Add_Mod';
    } else if (t1FragmentMode === 4 && t2FragmentMode === 4) {
      fragmentName = 'Combiners_Mod2x_Mod2x';
    } else {
      return null;
    }

    names = {
      vertex: vertexName,
      fragment: fragmentName
    };

    return names;
  }
}

export default BatchManager;