#include "whiteout/models/mdx/parser.h"
#include "whiteout/models/mdx/writer.h"
#include "whiteout/models/wem/converters.h"
#include "whiteout/models/wem/geometry/builder.h"
#include "whiteout/models/wem/geometry/render_view.h"
#include "../materials/mdx_core.h"
#include "mdx_anim.h"
#include "skin_skeleton.h"
#include <algorithm>
#include <array>
#include <bit>
#include <map>
#include <string>
#include <unordered_map>
namespace whiteout {
namespace models {
namespace wem {
namespace {
constexpr ProfileId kMdxProfiles[] = {ProfileId::Wc3Classic, ProfileId::Wc3Reforged};
Extent ToExtent(const mdx::Extent& source) {
Extent out;
out.minimum = source.minimum;
out.maximum = source.maximum;
out.sphereRadius = source.boundsRadius;
return out;
}
mdx::Extent FromExtent(const Extent& source) {
mdx::Extent out;
out.minimum = source.minimum;
out.maximum = source.maximum;
out.boundsRadius = source.sphereRadius;
return out;
}
std::string SlotName(std::size_t materialIndex) {
return "material_" + std::to_string(materialIndex);
}
enum class Origin : u8 {
Helper,
Bone,
Light,
Attachment,
ParticleEmitter,
ParticleEmitter2,
CornEmitter,
RibbonEmitter,
Event,
Collision,
};
struct PendingNode {
const mdx::Node* source = nullptr;
Origin origin = Origin::Helper;
u32 sourceIndex = 0;
};
NodeKind KindOf(Origin origin) {
switch (origin) {
case Origin::Bone:
return NodeKind::Bone;
case Origin::Light:
return NodeKind::Light;
case Origin::Attachment:
return NodeKind::Attachment;
case Origin::ParticleEmitter:
case Origin::ParticleEmitter2:
case Origin::CornEmitter:
return NodeKind::ParticleEmitter;
case Origin::RibbonEmitter:
return NodeKind::RibbonEmitter;
case Origin::Event:
return NodeKind::Event;
case Origin::Collision:
return NodeKind::CollisionShape;
case Origin::Helper:
break;
}
return NodeKind::Helper;
}
NodeFlags ToNodeFlags(mdx::Node::NodeFlag source) {
const u32 bits = static_cast<u32>(source);
NodeFlags out = NodeFlags::None;
if (bits & static_cast<u32>(mdx::Node::NodeFlag::DontInheritTranslation)) {
out |= NodeFlags::DontInheritTranslation;
}
if (bits & static_cast<u32>(mdx::Node::NodeFlag::DontInheritRotation)) {
out |= NodeFlags::DontInheritRotation;
}
if (bits & static_cast<u32>(mdx::Node::NodeFlag::DontInheritScaling)) {
out |= NodeFlags::DontInheritScale;
}
if (bits & static_cast<u32>(mdx::Node::NodeFlag::Billboarded)) {
out |= NodeFlags::Billboarded;
}
if (bits & static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockX)) {
out |= NodeFlags::BillboardLockX;
}
if (bits & static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockY)) {
out |= NodeFlags::BillboardLockY;
}
if (bits & static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockZ)) {
out |= NodeFlags::BillboardLockZ;
}
if (bits & static_cast<u32>(mdx::Node::NodeFlag::ModelSpace)) {
out |= NodeFlags::ModelSpace;
}
return out;
}
mdx::Node::NodeFlag FromNodeFlags(NodeFlags source, u32 rawFallback) {
u32 bits = rawFallback;
constexpr u32 kShared = static_cast<u32>(mdx::Node::NodeFlag::DontInheritTranslation) |
static_cast<u32>(mdx::Node::NodeFlag::DontInheritRotation) |
static_cast<u32>(mdx::Node::NodeFlag::DontInheritScaling) |
static_cast<u32>(mdx::Node::NodeFlag::Billboarded) |
static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockX) |
static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockY) |
static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockZ) |
static_cast<u32>(mdx::Node::NodeFlag::ModelSpace);
bits &= ~kShared;
if (hasFlag(source, NodeFlags::DontInheritTranslation)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::DontInheritTranslation);
}
if (hasFlag(source, NodeFlags::DontInheritRotation)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::DontInheritRotation);
}
if (hasFlag(source, NodeFlags::DontInheritScale)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::DontInheritScaling);
}
if (hasFlag(source, NodeFlags::Billboarded)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::Billboarded);
}
if (hasFlag(source, NodeFlags::BillboardLockX)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockX);
}
if (hasFlag(source, NodeFlags::BillboardLockY)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockY);
}
if (hasFlag(source, NodeFlags::BillboardLockZ)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::BillboardedLockZ);
}
if (hasFlag(source, NodeFlags::ModelSpace)) {
bits |= static_cast<u32>(mdx::Node::NodeFlag::ModelSpace);
}
return static_cast<mdx::Node::NodeFlag>(bits);
}
std::vector<PendingNode> CollectNodes(const mdx::Model& source) {
std::vector<PendingNode> pending;
const auto push = [&pending](const mdx::Node& node, Origin origin, std::size_t index) {
pending.push_back({&node, origin, static_cast<u32>(index)});
};
for (std::size_t i = 0; i < source.bones.size(); ++i) {
push(source.bones[i].node, Origin::Bone, i);
}
for (std::size_t i = 0; i < source.helpers.size(); ++i) {
push(source.helpers[i].node, Origin::Helper, i);
}
for (std::size_t i = 0; i < source.lights.size(); ++i) {
push(source.lights[i].node, Origin::Light, i);
}
for (std::size_t i = 0; i < source.attachments.size(); ++i) {
push(source.attachments[i].node, Origin::Attachment, i);
}
for (std::size_t i = 0; i < source.particleEmitters.size(); ++i) {
push(source.particleEmitters[i].node, Origin::ParticleEmitter, i);
}
for (std::size_t i = 0; i < source.particleEmitters2.size(); ++i) {
push(source.particleEmitters2[i].node, Origin::ParticleEmitter2, i);
}
for (std::size_t i = 0; i < source.cornEmitters.size(); ++i) {
push(source.cornEmitters[i].node, Origin::CornEmitter, i);
}
for (std::size_t i = 0; i < source.ribbonEmitters.size(); ++i) {
push(source.ribbonEmitters[i].node, Origin::RibbonEmitter, i);
}
for (std::size_t i = 0; i < source.eventObjects.size(); ++i) {
push(source.eventObjects[i].node, Origin::Event, i);
}
for (std::size_t i = 0; i < source.collisionShapes.size(); ++i) {
push(source.collisionShapes[i].node, Origin::Collision, i);
}
std::stable_sort(pending.begin(), pending.end(),
[](const PendingNode& a, const PendingNode& b) {
return a.source->objectId < b.source->objectId;
});
return pending;
}
void FillPayload(const mdx::Model& source, const PendingNode& pending, Node& node) {
node.kind = KindOf(pending.origin);
node.resetPayloadForKind();
switch (pending.origin) {
case Origin::Bone: {
const mdx::Bone& bone = source.bones[pending.sourceIndex];
node.native.set("geosetId", static_cast<i64>(bone.geosetId));
node.native.set("geosetAnimationId", static_cast<i64>(bone.geosetAnimationId));
break;
}
case Origin::Light: {
const mdx::Light& light = source.lights[pending.sourceIndex];
auto& payload = std::get<LightPayload>(node.payload);
switch (light.type) {
case mdx::Light::LightType::Directional:
payload.kind = LightKind::Directional;
break;
case mdx::Light::LightType::Ambient:
payload.kind = LightKind::Ambient;
break;
case mdx::Light::LightType::Omni:
default:
payload.kind = LightKind::Omni;
break;
}
payload.color = light.color;
payload.intensity = light.intensity;
payload.attenuationStart = light.attenuationStart;
payload.attenuationEnd = light.attenuationEnd;
node.native.set("ambientIntensity", static_cast<i64>(light.ambientIntensity * 1000.0f));
node.native.set("shadowIntensity", static_cast<i64>(light.shadowIntensity * 1000.0f));
break;
}
case Origin::Attachment: {
const mdx::Attachment& attachment = source.attachments[pending.sourceIndex];
node.native.set("mdxAttachmentId", static_cast<i64>(attachment.attachmentId));
std::get<AttachmentPayload>(node.payload).asset.path = attachment.path;
break;
}
case Origin::ParticleEmitter: {
const mdx::ParticleEmitter& emitter = source.particleEmitters[pending.sourceIndex];
std::get<ParticlePayload>(node.payload).system.path = emitter.spawnModelFileName;
break;
}
case Origin::ParticleEmitter2: {
const mdx::ParticleEmitter2& emitter = source.particleEmitters2[pending.sourceIndex];
std::get<ParticlePayload>(node.payload).system.id = emitter.textureId;
break;
}
case Origin::CornEmitter: {
const mdx::CornEmitter& emitter = source.cornEmitters[pending.sourceIndex];
std::get<ParticlePayload>(node.payload).system.path = emitter.path;
break;
}
case Origin::RibbonEmitter: {
const mdx::RibbonEmitter& emitter = source.ribbonEmitters[pending.sourceIndex];
std::get<RibbonPayload>(node.payload).system.id = emitter.materialId;
node.native.set("textureSlot", static_cast<i64>(emitter.textureSlot));
break;
}
case Origin::Event: {
const mdx::EventObject& event = source.eventObjects[pending.sourceIndex];
std::get<EventPayload>(node.payload).id = event.globalSequenceId;
break;
}
case Origin::Collision: {
const mdx::CollisionShape& shape = source.collisionShapes[pending.sourceIndex];
auto& payload = std::get<CollisionPayload>(node.payload);
switch (shape.type) {
case mdx::CollisionShape::ShapeType::Sphere:
payload.shape.kind = CollisionShapeKind::Sphere;
payload.shape.sphere.radius = shape.radius;
if (!shape.vertices.empty()) {
payload.shape.sphere.center = shape.vertices[0];
}
break;
case mdx::CollisionShape::ShapeType::Plane:
payload.shape.kind = CollisionShapeKind::Plane;
break;
case mdx::CollisionShape::ShapeType::Cylinder:
payload.shape.kind = CollisionShapeKind::Cylinder;
payload.shape.sphere.radius = shape.radius;
break;
case mdx::CollisionShape::ShapeType::Box:
default:
payload.shape.kind = CollisionShapeKind::Box;
break;
}
if (shape.vertices.size() >= 2) {
payload.shape.box.minimum = shape.vertices[0];
payload.shape.box.maximum = shape.vertices[1];
}
break;
}
case Origin::Helper:
break;
}
}
struct NodeImport {
NodeTree tree;
std::unordered_map<u32, u32> byObjectId;
std::vector<u32> cameraNodes;
u32 resolve(u32 objectId) const {
const auto it = byObjectId.find(objectId);
return it == byObjectId.end() ? kInvalidNode : it->second;
}
};
NodeImport ImportNodes(const mdx::Model& source) {
NodeImport out;
const std::vector<PendingNode> pending = CollectNodes(source);
out.tree.poseSchema.push_back(PoseSchema{});
out.tree.authoritativePose = 0;
out.tree.rig = RigConvention::PivotRelative;
for (std::size_t i = 0; i < pending.size(); ++i) {
out.byObjectId.emplace(pending[i].source->objectId, static_cast<u32>(i));
}
for (const PendingNode& item : pending) {
const mdx::Node& mdxNode = *item.source;
Node node;
node.name = mdxNode.name;
node.flags = ToNodeFlags(mdxNode.flags);
if (mdxNode.objectId < source.pivotPoints.size()) {
node.pivot = source.pivotPoints[mdxNode.objectId];
}
node.parent =
mdxNode.parentId == mdx::Node::NO_PARENT ? kInvalidNode : out.resolve(mdxNode.parentId);
node.native.set("objectId", static_cast<i64>(mdxNode.objectId));
node.native.set("nodeFamilyId", static_cast<i64>(mdxNode.nodeFamilyId));
node.native.set("mdxFlagBits", static_cast<i64>(static_cast<u32>(mdxNode.flags)));
FillPayload(source, item, node);
Vector3f parentPivot{0, 0, 0};
if (node.parent != kInvalidNode && node.parent < pending.size()) {
const mdx::Node& parentNode = *pending[node.parent].source;
if (parentNode.objectId < source.pivotPoints.size()) {
parentPivot = source.pivotPoints[parentNode.objectId];
}
}
node.local.translation =
Vector3f{node.pivot.x - parentPivot.x, node.pivot.y - parentPivot.y,
node.pivot.z - parentPivot.z};
node.poses.push_back(node.local);
out.tree.add(std::move(node));
}
for (const mdx::Camera& camera : source.cameras) {
Node node;
node.name = camera.name;
node.kind = NodeKind::Camera;
node.resetPayloadForKind();
auto& payload = std::get<CameraPayload>(node.payload);
payload.fov = camera.fieldOfView;
payload.nearClip = camera.nearClippingPlane;
payload.farClip = camera.farClippingPlane;
node.pivot = camera.position;
node.local.translation = camera.position;
node.poses.push_back(node.local);
out.cameraNodes.push_back(out.tree.size());
out.tree.add(std::move(node));
}
return out;
}
int ChunkRank(NodeKind kind) {
switch (kind) {
case NodeKind::Bone:
return 0;
case NodeKind::Light:
return 1;
case NodeKind::Helper:
return 2;
case NodeKind::Attachment:
return 3;
case NodeKind::ParticleEmitter:
return 4;
case NodeKind::RibbonEmitter:
return 5;
case NodeKind::Event:
return 6;
case NodeKind::CollisionShape:
return 7;
case NodeKind::Camera:
case NodeKind::Count:
break;
}
return -1;
}
constexpr int kLastChunkRank = 7;
void ImportSkin(const mdx::Geoset& geoset, const NodeImport& nodes, geom::MeshBuilder& builder,
std::size_t vertexCount, Diagnostics& out, std::size_t geosetIndex) {
const auto resolveMatrix = [&](u32 raw) -> u32 {
const u32 objectId = raw < geoset.matrixIndices.size() ? geoset.matrixIndices[raw] : raw;
const u32 node = nodes.resolve(objectId);
if (node == kInvalidNode) {
out.warn(DiagCode::DanglingNodeReference,
"geoset matrix names object id " + std::to_string(objectId) +
", which is not a node",
ElementRef(ElementKind::Mesh, static_cast<u32>(geosetIndex)));
}
return node;
};
if (!geoset.skinData.empty()) {
for (std::size_t v = 0; v < vertexCount; ++v) {
const std::size_t base = v * 8;
if (base + 7 >= geoset.skinData.size()) {
break;
}
for (std::size_t k = 0; k < 4; ++k) {
const f32 weight = static_cast<f32>(geoset.skinData[base + 4 + k]) / 255.0f;
if (weight <= 0.0f) {
continue;
}
const u32 node = resolveMatrix(geoset.skinData[base + k]);
if (node != kInvalidNode) {
builder.addInfluence(geom::VertexId(static_cast<u32>(v)), node, weight);
}
}
}
return;
}
if (geoset.vertexGroups.empty() || geoset.matrixGroups.empty()) {
return;
}
std::vector<u32> groupStart(geoset.matrixGroups.size() + 1, 0);
for (std::size_t g = 0; g < geoset.matrixGroups.size(); ++g) {
groupStart[g + 1] = groupStart[g] + geoset.matrixGroups[g];
}
for (std::size_t v = 0; v < vertexCount && v < geoset.vertexGroups.size(); ++v) {
const u32 group = geoset.vertexGroups[v];
if (group >= geoset.matrixGroups.size()) {
continue;
}
const u32 count = geoset.matrixGroups[group];
if (count == 0) {
continue;
}
const f32 weight = 1.0f / static_cast<f32>(count);
for (u32 k = 0; k < count && (groupStart[group] + k) < geoset.matrixIndices.size(); ++k) {
const u32 node = resolveMatrix(groupStart[group] + k);
if (node != kInvalidNode) {
builder.addInfluence(geom::VertexId(static_cast<u32>(v)), node, weight);
}
}
}
}
std::vector<u32> TextureWrapBits(const Document& document, const ProfileMaterialSet* set) {
std::vector<u32> bits(document.textures.size(), 0u);
if (set == nullptr) {
return bits;
}
const auto note = [&bits](const TextureInput& input) {
if (!input.hasTexture() || input.texture >= bits.size()) {
return;
}
bits[input.texture] |= (input.wrapU == WrapMode::Repeat ? 0x1u : 0u) |
(input.wrapV == WrapMode::Repeat ? 0x2u : 0u);
};
for (const Material& material : set->materials) {
const CommonMaterial& common = material.Common();
if (const CompositeBody* composite = common.composite()) {
for (const CompositeLayer& layer : composite->layers) {
note(layer.input);
}
} else if (const CombinersBody* combiners = common.combiners()) {
for (const CombinerStage& stage : combiners->stages) {
note(stage.input);
}
} else if (const PbrDeferredBody* pbr = common.pbr()) {
for (const auto& [slot, input] : pbr->slots) {
note(input);
}
} else if (const LegacyDeferredBody* legacy = common.legacy()) {
for (const auto& [slot, input] : legacy->slots) {
note(input);
}
}
}
return bits;
}
}
Result<Document> MdxConverter::fromMdx(const mdx::Model& source) const {
Result<Document> result;
Document document;
Diagnostics& diagnostics = result.diagnostics;
document.name = source.modelName;
document.bounds = ToExtent(source.modelExtent);
document.space = CoordSpace::Blizzard;
mdx_core::Context context;
context.modelVersion = source.version;
document.textures.reserve(source.textures.size());
for (const mdx::Texture& texture : source.textures) {
TextureRef ref;
ref.path = texture.fileName;
ref.flags = static_cast<u32>(texture.flags);
ref.replaceableId = texture.replaceableId;
context.textureIndexMap.push_back(static_cast<u32>(document.textures.size()));
document.textures.push_back(std::move(ref));
}
context.textureRefs = &document.textures;
Model model;
model.name = source.modelName;
model.bounds = document.bounds;
const NodeImport nodes = ImportNodes(source);
model.nodes = nodes.tree;
model.materialSlots.reserve(source.materials.size());
for (std::size_t m = 0; m < source.materials.size(); ++m) {
model.materialSlots.push_back(SlotName(m));
}
std::vector<ProfileMask> slotProfiles(source.materials.size(), kNoProfiles);
ProfileMask documentMask = kNoProfiles;
for (std::size_t m = 0; m < source.materials.size(); ++m) {
for (ProfileId profile : kMdxProfiles) {
if (mdx_core::HasLayersFor(source.materials[m], profile, context)) {
slotProfiles[m] |= ProfileBit(profile);
documentMask |= ProfileBit(profile);
}
}
}
if (documentMask == kNoProfiles) {
documentMask = ProfileBit(ProfileId::Wc3Classic);
}
for (ProfileId profile : kMdxProfiles) {
if (HasProfile(documentMask, profile)) {
document.declare(profile);
}
}
document.defaultProfile = document.profiles.front();
model.meshes.reserve(source.geosets.size());
for (std::size_t g = 0; g < source.geosets.size(); ++g) {
const mdx::Geoset& geoset = source.geosets[g];
geom::MeshBuilder builder;
MeshSection section;
section.name = geoset.lodName.empty() ? "geoset_" + std::to_string(g) : geoset.lodName;
section.selectionGroup = static_cast<u16>(geoset.selectionGroup);
section.bounds = ToExtent(geoset.extent);
if (geoset.materialId < model.materialSlots.size()) {
section.materialSlot = geoset.materialId;
section.profiles = slotProfiles[geoset.materialId];
if (section.profiles == kNoProfiles) {
section.profiles = documentMask;
}
} else {
section.profiles = documentMask;
diagnostics.warn(DiagCode::IndexOutOfRange,
"geoset names material " + std::to_string(geoset.materialId) +
", past the end of the material array",
ElementRef(ElementKind::Mesh, static_cast<u32>(g)));
}
section.native.set("selectionFlags", static_cast<i64>(geoset.selectionFlags));
for (const mdx::GeosetAnimation& animation : source.geosetAnimations) {
if (animation.geosetId == g && !animation.alphaTracks.isUsed &&
animation.alpha <= 0.0f) {
section.flags |= SectionFlags::Hidden;
break;
}
}
for (const mdx::GeosetAnimation& animation : source.geosetAnimations) {
if (animation.geosetId != g) {
continue;
}
const auto bits = [](f32 value) { return static_cast<i64>(std::bit_cast<u32>(value)); };
const Vector3f& c = animation.color;
if (c.x != 1.0f || c.y != 1.0f || c.z != 1.0f) {
section.native.set("geosetColorR", bits(c.x));
section.native.set("geosetColorG", bits(c.y));
section.native.set("geosetColorB", bits(c.z));
}
if (animation.alpha > 0.0f && animation.alpha != 1.0f) {
section.native.set("geosetAlpha", bits(animation.alpha));
}
break;
}
const u32 sectionIndex = builder.addSection(std::move(section));
for (const Vector3f& position : geoset.vertexPositions) {
builder.addVertex(position);
}
const std::size_t vertexCount = geoset.vertexPositions.size();
const std::size_t triangles = geoset.faces.size() / 3;
for (std::size_t t = 0; t < triangles; ++t) {
const std::array<u32, 3> corners = {geoset.faces[t * 3 + 0], geoset.faces[t * 3 + 1],
geoset.faces[t * 3 + 2]};
if (corners[0] >= vertexCount || corners[1] >= vertexCount ||
corners[2] >= vertexCount) {
diagnostics.warn(DiagCode::IndexOutOfRange, "face corner past the vertex array",
ElementRef(ElementKind::Mesh, static_cast<u32>(g)));
continue;
}
const geom::FaceId face =
builder.addTriangle(geom::VertexId(corners[0]), geom::VertexId(corners[1]),
geom::VertexId(corners[2]), sectionIndex);
for (u32 c = 0; c < 3; ++c) {
const u32 vertex = corners[c];
if (vertex < geoset.vertexNormals.size()) {
builder.setCornerAttr(face, c, geom::names::kNormal,
geoset.vertexNormals[vertex]);
}
if (vertex < geoset.tangents.size()) {
builder.setCornerAttr(face, c, geom::names::kTangent, geoset.tangents[vertex]);
}
for (std::size_t uv = 0; uv < geoset.textureCoordinateSets.size(); ++uv) {
if (vertex < geoset.textureCoordinateSets[uv].size()) {
builder.setCornerAttr(face, c, geom::names::uv(static_cast<u32>(uv)),
geoset.textureCoordinateSets[uv][vertex]);
}
}
}
}
ImportSkin(geoset, nodes, builder, vertexCount, diagnostics, g);
geom::MeshBuilder::BuildOutcome outcome = builder.build();
outcome.mesh.name = outcome.mesh.sections.empty() ? "geoset_" + std::to_string(g)
: outcome.mesh.sections[0].name;
outcome.mesh.lodLevel = geoset.lod;
outcome.mesh.bounds = ToExtent(geoset.extent);
model.meshes.push_back(std::move(outcome.mesh));
}
mdx_anim::Context animContext;
animContext.byObjectId = &nodes.byObjectId;
animContext.cameraNodes = nodes.cameraNodes;
for (ProfileId profile : document.profiles) {
ProfileMaterialSet set;
set.profile = profile;
set.looks.looks.push_back(Look{});
set.resizeBindings(model.materialSlots.size());
mdx_anim::Context::ProfileLayers layers;
layers.profile = profile;
layers.byMaterial.resize(source.materials.size());
for (std::size_t m = 0; m < source.materials.size(); ++m) {
if (!HasProfile(slotProfiles[m], profile)) {
continue;
}
const u32 index = static_cast<u32>(set.materials.size());
set.materials.push_back(mdx_core::ImportMaterial(source.materials[m], profile, context,
diagnostics, &layers.byMaterial[m]));
set.materials.back().name = SlotName(m);
set.slotBindings[m].byLook[0] = index;
}
model.profileSets.push_back(std::move(set));
animContext.layerOrdinals.push_back(std::move(layers));
}
const u32 modelIndex = static_cast<u32>(document.models.size());
document.models.push_back(std::move(model));
mdx_anim::Import(source, animContext, document, modelIndex, diagnostics);
result.value = std::move(document);
return result;
}
void WriteGeosetSkin(mdx::Geoset& geoset, const std::vector<u32>& sourceOf,
const std::vector<std::array<u32, 4>>& boneIndices,
const std::vector<std::array<f32, 4>>& boneWeights,
const std::vector<u32>& objectIdOf, bool skinChunk, u32 mesh,
Diagnostics& diagnostics) {
if (boneIndices.empty() || boneWeights.empty()) {
return;
}
struct Bound {
std::vector<u32> ids;
std::vector<f32> weights;
u32 dominant = 0; };
std::vector<Bound> bound(sourceOf.size());
std::vector<u32> palette;
std::unordered_map<u32, u32> paletteOf;
for (std::size_t v = 0; v < sourceOf.size(); ++v) {
const u32 source = sourceOf[v];
if (source >= boneIndices.size() || source >= boneWeights.size()) {
continue;
}
Bound& entry = bound[v];
f32 best = -1.0f;
for (std::size_t k = 0; k < boneIndices[source].size(); ++k) {
const f32 weight = k < boneWeights[source].size() ? boneWeights[source][k] : 0.0f;
if (weight <= 0.0f) {
continue;
}
const u32 node = boneIndices[source][k];
const u32 objectId = node < objectIdOf.size() ? objectIdOf[node] : mdx::Node::NO_PARENT;
if (objectId == mdx::Node::NO_PARENT) {
continue;
}
if (weight > best) {
best = weight;
entry.dominant = static_cast<u32>(entry.ids.size());
}
entry.ids.push_back(objectId);
entry.weights.push_back(weight);
if (paletteOf.try_emplace(objectId, static_cast<u32>(palette.size())).second) {
palette.push_back(objectId);
}
}
}
if (palette.empty()) {
return;
}
std::vector<u32> groupOf(sourceOf.size(), 0);
std::vector<std::vector<u32>> groups;
std::map<std::vector<u32>, u32> groupIndex;
bool sets = true;
for (std::size_t v = 0; v < bound.size() && sets; ++v) {
std::vector<u32> key = bound[v].ids;
std::sort(key.begin(), key.end());
key.erase(std::unique(key.begin(), key.end()), key.end());
if (key.empty()) {
key.push_back(palette.front());
}
const auto [entry, inserted] = groupIndex.try_emplace(key, static_cast<u32>(groups.size()));
if (inserted) {
if (groups.size() >= 256) {
sets = false;
break;
}
groups.push_back(key);
}
groupOf[v] = entry->second;
}
if (!sets) {
diagnostics.warn(DiagCode::BonePaletteLimit,
"section binds more than 256 distinct bone sets; the group encoding "
"keeps only the heaviest bone per vertex",
ElementRef(ElementKind::Mesh, mesh));
}
if (sets && !skinChunk) {
geoset.matrixGroups.reserve(groups.size());
for (const std::vector<u32>& group : groups) {
geoset.matrixGroups.push_back(static_cast<u32>(group.size()));
geoset.matrixIndices.insert(geoset.matrixIndices.end(), group.begin(), group.end());
}
geoset.vertexGroups.reserve(sourceOf.size());
for (const u32 group : groupOf) {
geoset.vertexGroups.push_back(static_cast<u8>(group));
}
return;
}
if (palette.size() > 256) {
diagnostics.warn(DiagCode::BonePaletteLimit,
"section binds " + std::to_string(palette.size()) +
" bones; a geoset palette holds 256",
ElementRef(ElementKind::Mesh, mesh));
}
geoset.matrixIndices = palette;
geoset.matrixGroups.assign(palette.size(), 1u);
geoset.vertexGroups.reserve(sourceOf.size());
for (const Bound& entry : bound) {
const u32 slot =
entry.dominant < entry.ids.size() ? paletteOf[entry.ids[entry.dominant]] : 0u;
geoset.vertexGroups.push_back(static_cast<u8>(std::min<u32>(slot, 0xFFu)));
}
if (!skinChunk) {
return;
}
geoset.skinData.assign(sourceOf.size() * 8, 0);
for (std::size_t v = 0; v < bound.size(); ++v) {
const Bound& entry = bound[v];
for (std::size_t k = 0; k < entry.ids.size() && k < 4; ++k) {
const u32 slot = paletteOf[entry.ids[k]];
if (slot > 0xFFu) {
continue;
}
geoset.skinData[v * 8 + k] = static_cast<u8>(slot);
geoset.skinData[v * 8 + 4 + k] =
static_cast<u8>(std::clamp(entry.weights[k], 0.0f, 1.0f) * 255.0f + 0.5f);
}
}
}
Result<mdx::Model> MdxConverter::toMdx(const Document& document, ProfileId profile,
u32 targetVersion) const {
Result<mdx::Model> result;
if (!checkExportProfile(document, profile, result.diagnostics)) {
return result;
}
checkRigConvention(document, profile, result.diagnostics);
if (document.models.empty()) {
result.value = mdx::Model{};
result.value->version = targetVersion;
return result;
}
Diagnostics& diagnostics = result.diagnostics;
const Model& model = document.models.front();
const ProfileMaterialSet* set = model.setFor(profile);
mdx::Model out;
out.version = targetVersion;
out.modelName = document.name.empty() ? model.name : document.name;
out.modelExtent = FromExtent(model.bounds);
mdx_core::Context context;
context.modelVersion = targetVersion;
out.textures.reserve(document.textures.size());
const bool authoredAsMdx =
Profile(document.defaultProfile).nativeMaterialKind == NativeKind::Mdx;
const std::vector<u32> wrapBits = TextureWrapBits(document, set);
for (std::size_t t = 0; t < document.textures.size(); ++t) {
const TextureRef& ref = document.textures[t];
mdx::Texture texture;
texture.fileName = ref.path;
texture.flags = static_cast<mdx::Texture::Flag>(authoredAsMdx ? ref.flags : wrapBits[t]);
texture.replaceableId = authoredAsMdx ? ref.replaceableId : 0u;
context.textureIndexMap.push_back(static_cast<u32>(out.textures.size()));
out.textures.push_back(std::move(texture));
}
std::vector<mdx::Texture> stockTextures;
context.stockTextures = &stockTextures;
context.stockBase = static_cast<u32>(out.textures.size());
mdx_anim::ExportContext animContext;
animContext.nodeSlots.assign(model.nodes.size(), mdx_anim::ExportContext::NodeSlot{});
const auto claim = [&animContext](std::size_t node, mdx_anim::ExportContext::Slot slot,
std::size_t index) {
animContext.nodeSlots[node] = {slot, static_cast<u32>(index)};
};
std::vector<u32> objectIdOf(model.nodes.size(), mdx::Node::NO_PARENT);
u32 nextObjectId = 0;
for (int rank = 0; rank <= kLastChunkRank; ++rank) {
for (std::size_t i = 0; i < model.nodes.size(); ++i) {
if (ChunkRank(model.nodes.nodes[i].kind) == rank) {
objectIdOf[i] = nextObjectId++;
}
}
}
out.pivotPoints.assign(nextObjectId, Vector3f{0, 0, 0});
const auto buildNode = [&](std::size_t index) {
const Node& node = model.nodes.nodes[index];
mdx::Node out_node;
out_node.name = node.name;
out_node.objectId = objectIdOf[index];
out_node.parentId = node.parent == kInvalidNode || node.parent >= objectIdOf.size()
? mdx::Node::NO_PARENT
: objectIdOf[node.parent];
const NodeNative::Entry* raw = node.native.find("mdxFlagBits");
out_node.flags = FromNodeFlags(node.flags, raw ? static_cast<u32>(raw->value) : 0u);
const NodeNative::Entry* family = node.native.find("nodeFamilyId");
out_node.nodeFamilyId = family ? static_cast<u32>(family->value) : 0u;
return out_node;
};
for (std::size_t i = 0; i < model.nodes.size(); ++i) {
const Node& node = model.nodes.nodes[i];
if (node.kind == NodeKind::Camera) {
mdx::Camera camera;
camera.name = node.name;
camera.position = node.local.translation;
if (const auto* payload = std::get_if<CameraPayload>(&node.payload)) {
camera.fieldOfView = payload->fov;
camera.nearClippingPlane = payload->nearClip;
camera.farClippingPlane = payload->farClip;
}
claim(i, mdx_anim::ExportContext::Slot::Camera, out.cameras.size());
out.cameras.push_back(std::move(camera));
continue;
}
out.pivotPoints[objectIdOf[i]] =
model.nodes.rig == RigConvention::PivotRelative
? node.pivot
: model.nodes.worldBind(static_cast<u32>(i)).translation;
switch (node.kind) {
case NodeKind::Bone: {
mdx::Bone bone;
bone.node = buildNode(i);
if (const auto* geosetId = node.native.find("geosetId")) {
bone.geosetId = static_cast<u32>(geosetId->value);
}
if (const auto* animId = node.native.find("geosetAnimationId")) {
bone.geosetAnimationId = static_cast<u32>(animId->value);
}
claim(i, mdx_anim::ExportContext::Slot::Bone, out.bones.size());
out.bones.push_back(std::move(bone));
break;
}
case NodeKind::Light: {
mdx::Light light;
light.node = buildNode(i);
if (const auto* payload = std::get_if<LightPayload>(&node.payload)) {
switch (payload->kind) {
case LightKind::Directional:
light.type = mdx::Light::LightType::Directional;
break;
case LightKind::Ambient:
light.type = mdx::Light::LightType::Ambient;
break;
case LightKind::Omni:
case LightKind::Spot:
default:
light.type = mdx::Light::LightType::Omni;
break;
}
light.color = payload->color;
light.intensity = payload->intensity;
light.attenuationStart = payload->attenuationStart;
light.attenuationEnd = payload->attenuationEnd;
if (payload->kind == LightKind::Spot) {
diagnostics.warn(DiagCode::FeatureDropped,
"WC3 has no spot light; written as omni",
ElementRef(ElementKind::Node, static_cast<u32>(i)));
}
}
claim(i, mdx_anim::ExportContext::Slot::Light, out.lights.size());
out.lights.push_back(std::move(light));
break;
}
case NodeKind::Attachment: {
mdx::Attachment attachment;
attachment.node = buildNode(i);
if (const auto* id = node.native.find("mdxAttachmentId")) {
attachment.attachmentId = static_cast<u32>(id->value);
}
if (const auto* payload = std::get_if<AttachmentPayload>(&node.payload)) {
attachment.path = payload->asset.path;
}
claim(i, mdx_anim::ExportContext::Slot::Attachment, out.attachments.size());
out.attachments.push_back(std::move(attachment));
break;
}
case NodeKind::ParticleEmitter: {
mdx::ParticleEmitter2 emitter;
emitter.node = buildNode(i);
if (const auto* payload = std::get_if<ParticlePayload>(&node.payload)) {
if (payload->system.id != AssetKey::kNoId) {
emitter.textureId = payload->system.id;
}
}
claim(i, mdx_anim::ExportContext::Slot::ParticleEmitter2, out.particleEmitters2.size());
out.particleEmitters2.push_back(std::move(emitter));
break;
}
case NodeKind::RibbonEmitter: {
mdx::RibbonEmitter emitter;
emitter.node = buildNode(i);
if (const auto* payload = std::get_if<RibbonPayload>(&node.payload)) {
if (payload->system.id != AssetKey::kNoId) {
emitter.materialId = payload->system.id;
}
}
if (const auto* slot = node.native.find("textureSlot")) {
emitter.textureSlot = static_cast<u32>(slot->value);
}
claim(i, mdx_anim::ExportContext::Slot::RibbonEmitter, out.ribbonEmitters.size());
out.ribbonEmitters.push_back(std::move(emitter));
break;
}
case NodeKind::Event: {
mdx::EventObject event;
event.node = buildNode(i);
if (const auto* payload = std::get_if<EventPayload>(&node.payload)) {
event.globalSequenceId = payload->id;
}
claim(i, mdx_anim::ExportContext::Slot::EventObject, out.eventObjects.size());
out.eventObjects.push_back(std::move(event));
break;
}
case NodeKind::CollisionShape: {
mdx::CollisionShape shape;
shape.node = buildNode(i);
if (const auto* payload = std::get_if<CollisionPayload>(&node.payload)) {
switch (payload->shape.kind) {
case CollisionShapeKind::Sphere:
shape.type = mdx::CollisionShape::ShapeType::Sphere;
shape.radius = payload->shape.sphere.radius;
shape.vertices.push_back(payload->shape.sphere.center);
break;
case CollisionShapeKind::Plane:
shape.type = mdx::CollisionShape::ShapeType::Plane;
break;
case CollisionShapeKind::Cylinder:
shape.type = mdx::CollisionShape::ShapeType::Cylinder;
shape.radius = payload->shape.sphere.radius;
break;
case CollisionShapeKind::Capsule:
case CollisionShapeKind::Hull:
diagnostics.warn(DiagCode::FeatureDropped,
"WC3 has no capsule or hull collision shape; written as box",
ElementRef(ElementKind::Node, static_cast<u32>(i)));
[[fallthrough]];
case CollisionShapeKind::Box:
default:
shape.type = mdx::CollisionShape::ShapeType::Box;
shape.vertices.push_back(payload->shape.box.minimum);
shape.vertices.push_back(payload->shape.box.maximum);
break;
}
}
claim(i, mdx_anim::ExportContext::Slot::CollisionShape, out.collisionShapes.size());
out.collisionShapes.push_back(std::move(shape));
break;
}
case NodeKind::Helper:
default: {
mdx::Helper helper;
helper.node = buildNode(i);
claim(i, mdx_anim::ExportContext::Slot::Helper, out.helpers.size());
out.helpers.push_back(std::move(helper));
break;
}
}
}
out.materials.reserve(model.materialSlots.size());
animContext.layerOfOrdinal.resize(model.materialSlots.size());
for (std::size_t slot = 0; slot < model.materialSlots.size(); ++slot) {
const Material* material = set ? Resolve(model, static_cast<u32>(slot), profile) : nullptr;
if (material == nullptr) {
out.materials.push_back(mdx::Material{});
continue;
}
out.materials.push_back(mdx_core::ExportMaterial(*material, profile, context, diagnostics,
&animContext.layerOfOrdinal[slot]));
}
for (mdx::Texture& texture : stockTextures) {
out.textures.push_back(std::move(texture));
}
geom::RenderMeshDesc desc;
desc.attributes = {
{geom::names::kPosition, utils::AttributeClass::Position, utils::AttributeEncoding::Float32,
3, 0},
{geom::names::kNormal, utils::AttributeClass::Normal, utils::AttributeEncoding::Float32, 3,
0},
{geom::names::uv(0), utils::AttributeClass::UV, utils::AttributeEncoding::Float32, 2, 0},
{geom::names::kTangent, utils::AttributeClass::Tangent, utils::AttributeEncoding::Float32,
4, 0},
};
desc.includeSkin = true;
desc.maxInfluences = Profile(profile).maxBoneInfluences;
desc.splitBySection = true;
const SkinSkeleton skinSkeleton(model.nodes);
skinSkeleton.describe(desc);
const bool skinChunk = targetVersion > 800;
animContext.geosetsOfMesh.assign(model.meshes.size(), {});
animContext.sectionOfGeoset.assign(model.meshes.size(), {});
std::vector<u32> hiddenGeosets;
std::vector<std::pair<u32, const MeshSection*>> tintedGeosets;
constexpr u32 kUnmapped = ~0u;
for (std::size_t m = 0; m < model.meshes.size(); ++m) {
const Mesh& mesh = model.meshes[m];
const geom::RenderMesh render = geom::BuildRenderMesh(mesh, desc);
diagnostics.append(render.diagnostics);
const std::vector<Vector3f> positions = render.vertices.getPositions();
const std::vector<Vector3f> normals = render.vertices.getNormals();
const std::vector<Vector2f> uv0 = render.vertices.getUVs(0);
const bool hasTangents = targetVersion > 800 &&
mesh.attributes.has(geom::names::kTangent, geom::Domain::Halfedge);
const std::vector<Vector4f> tangents =
hasTangents ? render.vertices.getTangents() : std::vector<Vector4f>();
const std::vector<std::array<u32, 4>> boneIndices = render.vertices.getBoneIndices();
const std::vector<std::array<f32, 4>> boneWeights = render.vertices.getBoneWeights();
std::vector<u32> localOf(render.vertexCount(), kUnmapped);
for (const geom::RenderRange& range : render.ranges) {
const MeshSection* section =
range.section < mesh.sections.size() ? &mesh.sections[range.section] : nullptr;
mdx::Geoset geoset;
geoset.lod = mesh.lodLevel;
geoset.lodName =
section != nullptr && !section->name.empty() ? section->name : mesh.name;
std::vector<u32> sourceOf;
sourceOf.reserve(range.indexCount);
geoset.faces.reserve(range.indexCount);
bool wide = false;
const u32 end = range.firstIndex + range.indexCount;
for (u32 i = range.firstIndex; i < end && i < render.indices.size(); ++i) {
const u32 source = render.indices[i];
if (source >= localOf.size()) {
geoset.faces.push_back(0);
continue;
}
if (localOf[source] == kUnmapped) {
localOf[source] = static_cast<u32>(sourceOf.size());
sourceOf.push_back(source);
}
const u32 local = localOf[source];
wide = wide || local > 0xFFFFu;
geoset.faces.push_back(static_cast<u16>(local & 0xFFFFu));
}
for (const u32 source : sourceOf) {
localOf[source] = kUnmapped;
}
if (wide) {
diagnostics.warn(DiagCode::IndexWidthExceeded,
"section needs more than 65535 vertices for one geoset",
ElementRef(ElementKind::Mesh, static_cast<u32>(m)));
}
geoset.faceTypeGroups.push_back(4);
geoset.faceGroups.push_back(static_cast<u32>(geoset.faces.size()));
geoset.vertexPositions.reserve(sourceOf.size());
geoset.vertexNormals.reserve(sourceOf.size());
if (!tangents.empty()) {
geoset.tangents.reserve(sourceOf.size());
}
std::vector<Vector2f> uvs;
if (!uv0.empty()) {
uvs.reserve(sourceOf.size());
}
Extent bounds;
ResetExtent(bounds);
for (const u32 source : sourceOf) {
const Vector3f position =
source < positions.size() ? positions[source] : Vector3f(0, 0, 0);
geoset.vertexPositions.push_back(position);
GrowExtent(bounds, position);
geoset.vertexNormals.push_back(source < normals.size() ? normals[source]
: Vector3f(0, 0, 1));
if (!tangents.empty()) {
geoset.tangents.push_back(source < tangents.size() ? tangents[source]
: Vector4f(1, 0, 0, 1));
}
if (!uv0.empty()) {
uvs.push_back(source < uv0.size() ? uv0[source] : Vector2f(0, 0));
}
}
if (!uvs.empty()) {
geoset.textureCoordinateSets.push_back(std::move(uvs));
}
if (!sourceOf.empty()) {
FinishExtent(bounds);
geoset.extent = FromExtent(bounds);
} else {
geoset.extent = FromExtent(mesh.bounds);
}
WriteGeosetSkin(geoset, sourceOf, boneIndices, boneWeights, objectIdOf, skinChunk,
static_cast<u32>(m), diagnostics);
if (section != nullptr) {
geoset.materialId = section->materialSlot;
geoset.selectionGroup = section->selectionGroup;
if (const auto* flags = section->native.find("selectionFlags")) {
geoset.selectionFlags = static_cast<u32>(flags->value);
}
if (hasFlag(section->flags, SectionFlags::Hidden)) {
hiddenGeosets.push_back(static_cast<u32>(out.geosets.size()));
}
if (section->native.find("geosetColorR") != nullptr ||
section->native.find("geosetAlpha") != nullptr) {
tintedGeosets.emplace_back(static_cast<u32>(out.geosets.size()), section);
}
}
animContext.geosetsOfMesh[m].push_back(static_cast<u32>(out.geosets.size()));
animContext.sectionOfGeoset[m].push_back(range.section);
out.geosets.push_back(std::move(geoset));
}
}
for (const u32 geoset : hiddenGeosets) {
mdx::GeosetAnimation animation;
animation.geosetId = geoset;
animation.alpha = 0.0f;
animation.flags = mdx::GeosetAnimation::Flag::Color;
out.geosetAnimations.push_back(std::move(animation));
}
for (const auto& [geoset, section] : tintedGeosets) {
const NativeBag::Entry* r = section->native.find("geosetColorR");
const NativeBag::Entry* a = section->native.find("geosetAlpha");
mdx::GeosetAnimation* animation = nullptr;
for (mdx::GeosetAnimation& existing : out.geosetAnimations) {
if (existing.geosetId == geoset) {
animation = &existing;
}
}
if (animation == nullptr) {
mdx::GeosetAnimation created;
created.geosetId = geoset;
created.flags = mdx::GeosetAnimation::Flag::Color;
out.geosetAnimations.push_back(std::move(created));
animation = &out.geosetAnimations.back();
}
const auto value = [](const NativeBag::Entry* entry) {
return std::bit_cast<f32>(static_cast<u32>(entry->value));
};
if (r != nullptr) {
const NativeBag::Entry* g = section->native.find("geosetColorG");
const NativeBag::Entry* b = section->native.find("geosetColorB");
if (g != nullptr && b != nullptr) {
animation->color = Vector3f{value(r), value(g), value(b)};
}
}
if (a != nullptr && !hasFlag(section->flags, SectionFlags::Hidden)) {
animation->alpha = value(a);
}
}
mdx_anim::Export(document, 0, profile, animContext, out, diagnostics);
result.value = std::move(out);
return result;
}
std::string MdxConverter::formatId() const {
return "mdx";
}
std::string MdxConverter::formatName() const {
return "Warcraft III MDX";
}
std::span<const ProfileId> MdxConverter::profiles() const {
return kMdxProfiles;
}
bool MdxConverter::supportsImport() const {
return true;
}
bool MdxConverter::supportsExport() const {
return true;
}
u32 MdxConverter::defaultExportVersion() const {
return 800;
}
Result<Document> MdxConverter::importFromBytes(std::span<const u8> data) const {
mdx::Parser parser;
const mdx::Model source = parser.parse(data);
Result<Document> result = fromMdx(source);
for (const std::string& issue : parser.getIssues()) {
result.diagnostics.warn(DiagCode::Unspecified, issue);
}
return result;
}
Result<std::vector<u8>> MdxConverter::exportToBytes(const Document& document, ProfileId profile,
u32 version) const {
Result<mdx::Model> converted =
toMdx(document, profile, version == 0 ? defaultExportVersion() : version);
Result<std::vector<u8>> result;
result.diagnostics = std::move(converted.diagnostics);
if (!converted.ok()) {
return result;
}
mdx::Writer writer;
result.value = writer.write(*converted);
return result;
}
} } }