#include "whiteout/models/wem/d3_converter.h"
#include "whiteout/models/wem/geometry/builder.h"
#include "whiteout/models/wem/geometry/render_view.h"
#include <whiteout/sno/d3/native/geometry.h>
#include "../materials/d3_core.h"
#include "../native/d3_copy.h"
#include "d3_anim.h"
#include "skin_skeleton.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstring>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace whiteout {
namespace models {
namespace wem {
namespace d3n = whiteout::sno::d3::native;
namespace {
constexpr ProfileId kD3Profiles[] = {ProfileId::Diablo3};
constexpr std::size_t kSnoHeaderSize = 16;
constexpr u32 kSnoMagic = 0xDEADBEEFu;
constexpr u32 kAppearanceVersion = 260;
constexpr u32 kActorVersion = 282;
constexpr const char* kDefaultLookName = "A";
constexpr i32 kTriggerSpawn = 0;
constexpr i32 kTriggerSpawnAttached = 25;
bool IEquals(const std::string& a, const std::string& b) {
if (a.size() != b.size()) {
return false;
}
for (std::size_t i = 0; i < a.size(); ++i) {
const auto lower = [](char c) {
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
};
if (lower(a[i]) != lower(b[i])) {
return false;
}
}
return true;
}
bool IsSpawn(const d3n::TriggerEvent& event) {
if ((event.tConditions.nChance & 0xFF) == 0) {
return false;
}
if (event.eTriggerType != kTriggerSpawn && event.eTriggerType != kTriggerSpawnAttached) {
return false;
}
if (event.tPayload.dwNameHandle == -1) {
return false;
}
const i32 group = event.tPayload.eSnoGroup;
return group == static_cast<i32>(d3n::Group::Actor) ||
group == static_cast<i32>(d3n::Group::Particle);
}
bool HasAttachedModel(const Model& model) {
for (const Node& node : model.nodes.nodes) {
const auto* payload = std::get_if<AttachmentPayload>(&node.payload);
if (payload != nullptr && (!payload->asset.empty() || payload->model != kInvalidIndex)) {
return true;
}
}
return false;
}
Transform ToTransform(const d3n::PRSTransform& source) {
Transform out;
out.translation = source.vTranslation;
out.rotation =
Quaternion{source.qRotation.x, source.qRotation.y, source.qRotation.z, source.qRotation.w};
out.scale = Vector3f{source.flScale, source.flScale, source.flScale};
return out;
}
Transform ToTransform(const d3n::PRTransform& source) {
Transform out;
out.translation = source.vTranslation;
out.rotation =
Quaternion{source.qRotation.x, source.qRotation.y, source.qRotation.z, source.qRotation.w};
return out;
}
Extent ToExtent(const d3n::AABB& box) {
Extent out;
out.minimum = Vector3f{box.vCenter.x - box.vHalfExtent.x, box.vCenter.y - box.vHalfExtent.y,
box.vCenter.z - box.vHalfExtent.z};
out.maximum = Vector3f{box.vCenter.x + box.vHalfExtent.x, box.vCenter.y + box.vHalfExtent.y,
box.vCenter.z + box.vHalfExtent.z};
FinishExtent(out);
return out;
}
Sphere ToSphere(const d3n::Sphere& source) {
Sphere out;
out.center = source.vCenter;
out.radius = source.flRadius;
return out;
}
void FillPoseSchema(NodeTree& tree) {
const auto add = [&tree](const char* name, PoseSpace space, bool inverse) {
PoseSchema entry;
entry.name = name;
entry.space = space;
entry.inverse = inverse;
tree.poseSchema.push_back(std::move(entry));
};
add("bindA", PoseSpace::Model, false);
add("bindAInverse", PoseSpace::Model, true);
add("local", PoseSpace::ParentRelative, false);
add("bindB", PoseSpace::Model, false);
add("bindBInverse", PoseSpace::Model, true);
tree.authoritativePose = 3;
tree.rig = RigConvention::ExplicitBind;
}
struct NodeBuild {
NodeTree tree;
std::vector<u32> boneToNode;
};
NodeBuild ImportNodes(const d3n::Appearances& source, Diagnostics& out) {
NodeBuild build;
NodeTree& tree = build.tree;
FillPoseSchema(tree);
const u32 boneCount = static_cast<u32>(source.arBones.size());
for (u32 b = 0; b < boneCount; ++b) {
const d3n::BoneStructure& bone = source.arBones[b];
Node node;
node.name = bone.szName;
node.kind = NodeKind::Bone;
node.resetPayloadForKind();
node.parent = bone.nParentIndex >= 0 && static_cast<u32>(bone.nParentIndex) < boneCount
? static_cast<u32>(bone.nParentIndex)
: kInvalidNode;
node.local = ToTransform(bone.tTransform2);
node.uniformScaleOnly = true;
node.poses = {ToTransform(bone.tTransform0), ToTransform(bone.tTransform1),
ToTransform(bone.tTransform2), ToTransform(bone.tTransform3),
ToTransform(bone.tTransform4)};
auto& payload = std::get<BonePayload>(node.payload);
payload.bounds = ToExtent(bone.tBounds);
payload.sphere = ToSphere(bone.tSphere);
if (!bone.arCollisionShapes.empty()) {
node.native.set("collisionShapeCount", static_cast<i64>(bone.arCollisionShapes.size()));
}
if (!bone.arConstraints.empty()) {
node.native.set("constraintCount", static_cast<i64>(bone.arConstraints.size()));
}
tree.add(std::move(node));
}
build.boneToNode.resize(boneCount);
for (u32 b = 0; b < boneCount; ++b) {
build.boneToNode[b] = b;
}
if (!tree.parentsPrecedeChildren()) {
const std::vector<u32> remap = tree.sortParentsFirst();
if (remap.size() == boneCount) {
build.boneToNode = remap;
} else {
out.warn(DiagCode::ConnectivityCorrupt,
"bone hierarchy has a cycle; the tree was left in file order");
}
}
for (u32 b = 0; b < boneCount; ++b) {
const d3n::BoneStructure& bone = source.arBones[b];
if (!bone.snoParticle.valid()) {
continue;
}
Node node;
node.name = bone.szName + "_particle";
node.kind = NodeKind::ParticleEmitter;
node.resetPayloadForKind();
node.parent = build.boneToNode[b];
auto& payload = std::get<ParticlePayload>(node.payload);
payload.system.group = static_cast<u32>(bone.snoParticle.group);
payload.system.id = static_cast<u32>(bone.snoParticle.id);
tree.add(std::move(node));
}
for (const d3n::Hardpoint& hardpoint : source.arHardpoints) {
Node node;
node.name = hardpoint.szName;
node.kind = NodeKind::Attachment;
node.resetPayloadForKind();
node.parent =
hardpoint.nBoneIndex >= 0 && static_cast<u32>(hardpoint.nBoneIndex) < boneCount
? build.boneToNode[static_cast<u32>(hardpoint.nBoneIndex)]
: kInvalidNode;
node.local = ToTransform(hardpoint.tTransform);
tree.add(std::move(node));
}
for (std::size_t l = 0; l < source.arStaticLights.size(); ++l) {
const d3n::StaticLight& light = source.arStaticLights[l];
Node node;
node.name = "light_" + std::to_string(l);
node.kind = NodeKind::Light;
node.resetPayloadForKind();
node.local.translation = light.vPosition;
auto& payload = std::get<LightPayload>(node.payload);
payload.kind = light.nType == 0 ? LightKind::Directional : LightKind::Omni;
payload.color = Vector3f{static_cast<f32>(light.dwColor & 0xFFu) / 255.0f,
static_cast<f32>((light.dwColor >> 8) & 0xFFu) / 255.0f,
static_cast<f32>((light.dwColor >> 16) & 0xFFu) / 255.0f};
payload.intensity = light.flIntensity;
payload.attenuationStart = light.flAttenuation0;
payload.attenuationEnd = light.flAttenuation1;
node.native.set("d3LightType", static_cast<i64>(light.nType));
node.native.set("d3LightFlags", static_cast<i64>(light.dwFlags));
tree.add(std::move(node));
}
return build;
}
bool WardrobeShows(const d3n::GeosetName& descriptor,
const std::vector<D3WardrobePiece>& wardrobe) {
if (!descriptor.parsed || descriptor.slot == d3n::LookSlot::Unknown) {
return true;
}
for (const D3WardrobePiece& piece : wardrobe) {
if (piece.slot != descriptor.slot) {
continue;
}
return piece.weight == descriptor.weight && piece.variant == descriptor.variant;
}
return descriptor.weight == d3n::ArmourWeight::Naked && descriptor.variant == 0;
}
Mesh ImportGeoSet(const d3n::GeoSet& geoSet, const std::string& name, Model& model,
const std::vector<u32>& boneToNode, const D3ImportOptions& options,
Diagnostics& out) {
geom::MeshBuilder builder;
struct Pending {
u32 section = 0;
u32 base = 0;
};
std::vector<Pending> pending;
pending.reserve(geoSet.arSubObjects.size());
for (std::size_t s = 0; s < geoSet.arSubObjects.size(); ++s) {
const d3n::SubObject& sub = geoSet.arSubObjects[s];
MeshSection section;
section.name = sub.szName;
section.materialSlot = model.slotIndex(sub.szName);
if (section.materialSlot == kInvalidIndex) {
section.materialSlot = model.addSlot(sub.szName);
out.warn(DiagCode::SlotNotBound,
"sub-object '" + sub.szName + "' names no appearance material",
ElementRef(ElementKind::Section, static_cast<u32>(s)), ProfileId::Diablo3);
}
section.bounds = ToExtent(sub.tBounds);
if (!sub.arClothData.empty()) {
section.flags |= SectionFlags::ClothSimulated;
}
if (sub.snoSurface.valid()) {
section.native.set("surfaceSno", static_cast<i64>(sub.snoSurface.id));
}
section.native.set("vertexFormat", static_cast<i64>(sub.dwVertexFormat));
section.native.set(
"boneNode", sub.nBoneIndex >= 0 && static_cast<u32>(sub.nBoneIndex) < boneToNode.size()
? static_cast<i64>(boneToNode[static_cast<u32>(sub.nBoneIndex)])
: -1);
if (!sub.szMaterialName.empty()) {
section.native.setText("materialName", sub.szMaterialName);
}
const d3n::GeosetName descriptor = d3n::parseGeosetName(sub);
section.native.set("descriptorParsed", descriptor.parsed ? 1 : 0);
if (descriptor.parsed) {
section.native.set("lookSlot", static_cast<i64>(descriptor.slot));
section.native.set("armourWeight", static_cast<i64>(descriptor.weight));
section.native.set("variant", static_cast<i64>(descriptor.variant));
section.native.set("cloth", descriptor.cloth ? 1 : 0);
}
if (!WardrobeShows(descriptor, options.wardrobe)) {
section.flags |= SectionFlags::Hidden;
}
if (sub.arVertexInfluences.empty() && sub.nBoneIndex >= 0 &&
static_cast<u32>(sub.nBoneIndex) < boneToNode.size()) {
section.rigidNode = boneToNode[static_cast<u32>(sub.nBoneIndex)];
}
Pending entry;
entry.base = builder.vertexCount();
entry.section = builder.addSection(std::move(section));
pending.push_back(entry);
for (const d3n::FatVertex& vertex : sub.arVertices) {
builder.addVertex(vertex.vPosition);
}
for (std::size_t v = 0; v < sub.arVertexInfluences.size() && v < sub.arVertices.size();
++v) {
const d3n::VertInfluences& influences = sub.arVertexInfluences[v];
const d3n::Influence* three[3] = {&influences.tInfluence0, &influences.tInfluence1,
&influences.tInfluence2};
for (const d3n::Influence* influence : three) {
if (influence->flWeight <= 0.0f || influence->nBoneIndex < 0) {
continue;
}
const u32 bone = static_cast<u32>(influence->nBoneIndex);
if (bone >= boneToNode.size()) {
out.warn(DiagCode::DanglingNodeReference,
"influence names bone " + std::to_string(bone));
continue;
}
builder.addInfluence(geom::VertexId(entry.base + static_cast<u32>(v)),
boneToNode[bone], influence->flWeight);
}
}
}
for (std::size_t s = 0; s < geoSet.arSubObjects.size(); ++s) {
const d3n::SubObject& sub = geoSet.arSubObjects[s];
const Pending& entry = pending[s];
for (std::size_t i = 0; i + 2 < sub.arIndices.size(); i += 3) {
const u32 corners[3] = {entry.base + sub.arIndices[i + 0],
entry.base + sub.arIndices[i + 1],
entry.base + sub.arIndices[i + 2]};
if (corners[0] >= builder.vertexCount() || corners[1] >= builder.vertexCount() ||
corners[2] >= builder.vertexCount()) {
out.warn(DiagCode::IndexOutOfRange,
"sub-object face corner past its own vertex array",
ElementRef(ElementKind::Section, static_cast<u32>(s)));
continue;
}
const geom::FaceId face =
builder.addTriangle(geom::VertexId(corners[0]), geom::VertexId(corners[1]),
geom::VertexId(corners[2]), entry.section);
for (u32 c = 0; c < 3; ++c) {
const d3n::FatVertex& vertex = sub.arVertices[corners[c] - entry.base];
builder.setCornerAttr(face, c, geom::names::kNormal, d3n::vertexNormal(vertex));
builder.setCornerAttr(face, c, geom::names::uv(0), d3n::vertexTexCoord0(vertex));
builder.setCornerAttr(face, c, geom::names::uv(1), d3n::vertexTexCoord1(vertex));
builder.setCornerAttr(face, c, geom::names::kTangent, d3n::vertexTangent(vertex));
builder.setCornerAttr(face, c, geom::names::kBinormal, d3n::vertexBinormal(vertex));
const d3n::VertexColor color = d3n::vertexColor(vertex);
const d3n::VertexColor aux = d3n::vertexAuxColor(vertex);
builder.setCornerAttr(face, c, geom::names::color(0),
std::array<u8, 4>{color.r, color.g, color.b, color.a});
builder.setCornerAttr(face, c, geom::names::color(1),
std::array<u8, 4>{aux.r, aux.g, aux.b, aux.a});
}
}
}
geom::MeshBuilder::BuildOutcome outcome = builder.build();
outcome.mesh.name = name;
outcome.mesh.recomputeBounds();
return std::move(outcome.mesh);
}
}
struct AssetSource::Impl {
sno::d3::native::AssetProvider& provider;
Stats stats;
std::unordered_map<i32, std::optional<d3n::Appearances>> appearances;
std::unordered_map<i32, std::optional<d3n::Actor>> actors;
std::unordered_map<i32, std::optional<d3n::Anim>> anims;
std::unordered_map<i32, std::optional<d3n::AnimSet>> animSets;
std::unordered_map<i32, std::optional<d3n::ShaderMap>> shaderMaps;
std::unordered_map<i32, std::optional<d3n::Shaders>> shaders;
std::unordered_map<i32, std::optional<d3n::Material>> materials;
explicit Impl(sno::d3::native::AssetProvider& source) : provider(source) {}
template <class T, class Parse>
const T* fetch(std::unordered_map<i32, std::optional<T>>& map, d3n::Group group, i32 snoId,
Parse parse) {
++stats.requests;
const auto found = map.find(snoId);
if (found != map.end()) {
++stats.hits;
return found->second.has_value() ? &*found->second : nullptr;
}
++stats.loads;
std::optional<T> parsed;
const std::vector<u8> bytes = provider.load(group, snoId);
if (!bytes.empty()) {
parsed = parse(bytes);
}
if (!parsed.has_value()) {
++stats.failures;
}
std::optional<T>& slot = map.emplace(snoId, std::move(parsed)).first->second;
return slot.has_value() ? &*slot : nullptr;
}
};
AssetSource::AssetSource(sno::d3::native::AssetProvider& provider)
: pImpl(std::make_unique<Impl>(provider)) {}
AssetSource::~AssetSource() = default;
const d3n::Appearances* AssetSource::appearance(i32 snoId) {
return pImpl->fetch(pImpl->appearances, d3n::Group::Appearance, snoId,
[](const std::vector<u8>& bytes) { return d3n::parseAppearances(bytes); });
}
const d3n::Actor* AssetSource::actor(i32 snoId) {
return pImpl->fetch(pImpl->actors, d3n::Group::Actor, snoId,
[](const std::vector<u8>& bytes) { return d3n::parseActor(bytes); });
}
const d3n::Anim* AssetSource::anim(i32 snoId) {
return pImpl->fetch(pImpl->anims, d3n::Group::Anim, snoId,
[](const std::vector<u8>& bytes) { return d3n::parseAnim(bytes); });
}
const d3n::AnimSet* AssetSource::animSet(i32 snoId) {
return pImpl->fetch(pImpl->animSets, d3n::Group::AnimSet, snoId,
[](const std::vector<u8>& bytes) { return d3n::parseAnimSet(bytes); });
}
const d3n::ShaderMap* AssetSource::shaderMap(i32 snoId) {
return pImpl->fetch(pImpl->shaderMaps, d3n::Group::ShaderMap, snoId,
[](const std::vector<u8>& bytes) { return d3n::parseShaderMap(bytes); });
}
const d3n::Shaders* AssetSource::shaders(i32 snoId) {
return pImpl->fetch(pImpl->shaders, d3n::Group::Shaders, snoId,
[](const std::vector<u8>& bytes) { return d3n::parseShaders(bytes); });
}
const d3n::Material* AssetSource::material(i32 snoId) {
return pImpl->fetch(pImpl->materials, d3n::Group::Material, snoId,
[](const std::vector<u8>& bytes) { return d3n::parseMaterial(bytes); });
}
const AssetSource::Stats& AssetSource::stats() const {
return pImpl->stats;
}
std::string D3Converter::formatId() const {
return "d3";
}
std::string D3Converter::formatName() const {
return "Diablo III";
}
std::span<const ProfileId> D3Converter::profiles() const {
return std::span<const ProfileId>(kD3Profiles, 1);
}
bool D3Converter::supportsImport() const {
return true;
}
bool D3Converter::supportsExport() const {
return false;
}
u32 D3Converter::defaultExportVersion() const {
return 0;
}
Result<std::vector<u8>> D3Converter::exportToBytes(const Document&, ProfileId, u32) const {
Result<std::vector<u8>> result;
result.diagnostics.error(DiagCode::OperationUnsupported,
"writing Diablo III SNO assets is out of scope for WEM v3 (§18)");
return result;
}
d3n::Group D3Converter::SniffGroup(std::span<const u8> data) {
if (data.size() < kSnoHeaderSize) {
return d3n::Group::Unknown;
}
u32 magic = 0;
u32 version = 0;
std::memcpy(&magic, data.data(), sizeof(magic));
std::memcpy(&version, data.data() + 4, sizeof(version));
if (magic != kSnoMagic) {
return d3n::Group::Unknown;
}
switch (version) {
case kAppearanceVersion:
return d3n::Group::Appearance;
case kActorVersion:
return d3n::Group::Actor;
default:
return d3n::Group::Unknown;
}
}
u32 D3Converter::FindAppearanceModel(const Document& document, i32 snoId) {
for (std::size_t m = 0; m < document.models.size(); ++m) {
const ProfileMaterialSet* set = document.models[m].setFor(ProfileId::Diablo3);
if (set == nullptr) {
continue;
}
const NativeBag::Entry* entry = set->native.find("appearanceSnoId");
if (entry != nullptr && entry->value == snoId) {
return static_cast<u32>(m);
}
}
return kInvalidIndex;
}
Result<Document> D3Converter::importFromBytes(std::span<const u8> data) const {
return importFromBytes(data, AssetHint::Unknown, nullptr, D3ImportOptions{});
}
Result<Document> D3Converter::importFromBytes(std::span<const u8> data, AssetHint hint,
AssetSource* assets,
const D3ImportOptions& options) const {
Result<Document> result;
const d3n::Group group = hint == AssetHint::Appearance ? d3n::Group::Appearance
: hint == AssetHint::Actor ? d3n::Group::Actor
: SniffGroup(data);
if (group == d3n::Group::Appearance) {
auto parsed = d3n::parseAppearances(data);
if (!parsed.has_value()) {
result.diagnostics.error(DiagCode::UnsupportedVersion, "not a readable `.app`");
return result;
}
return fromAppearance(*parsed, assets, options);
}
if (group == d3n::Group::Actor) {
auto parsed = d3n::parseActor(data);
if (!parsed.has_value()) {
result.diagnostics.error(DiagCode::UnsupportedVersion, "not a readable `.acr`");
return result;
}
if (assets == nullptr) {
result.diagnostics.error(DiagCode::AssetUnresolved,
"an actor names its appearance by SNO id; importing one "
"needs a provider");
return result;
}
return fromActor(*parsed, *assets, options);
}
result.diagnostics.error(DiagCode::UnsupportedVersion,
"bytes are neither a `.app` nor a `.acr`");
return result;
}
Result<Document> D3Converter::fromAppearance(const d3n::Appearances& source, AssetSource* assets,
const D3ImportOptions& options) const {
Result<Document> result;
Diagnostics& diagnostics = result.diagnostics;
Document document;
document.space = CoordSpace::Blizzard;
document.declare(ProfileId::Diablo3);
document.defaultProfile = ProfileId::Diablo3;
document.bounds = ToExtent(source.tBounds);
Model model;
model.bounds = document.bounds;
NodeBuild nodes = ImportNodes(source, diagnostics);
model.nodes = std::move(nodes.tree);
ProfileMaterialSet set;
set.profile = ProfileId::Diablo3;
for (const d3n::AppearanceLook& look : source.arLooks) {
set.looks.add(look.szName);
}
if (set.looks.empty()) {
set.looks = LookTable::Single();
}
const std::string wanted =
options.materialLook.empty() ? kDefaultLookName : options.materialLook;
const u32 chosen = set.looks.find(wanted);
if (chosen == kInvalidIndex) {
if (!options.materialLook.empty()) {
diagnostics.warn(DiagCode::LookDropped,
"look '" + options.materialLook + "' is not in this appearance",
ElementRef(), ProfileId::Diablo3);
}
set.defaultLook = 0;
} else {
set.defaultLook = chosen;
}
for (const d3n::AppearanceMaterial& material : source.arMaterials) {
model.addSlot(material.szName);
}
set.resizeBindings(model.materialSlots.size());
d3_core::Context context;
context.sourceVersion = kAppearanceVersion;
context.internUnknownIds = true;
context.assets = assets;
for (std::size_t m = 0; m < source.arMaterials.size(); ++m) {
const d3n::AppearanceMaterial& material = source.arMaterials[m];
const u32 slot = model.slotIndex(material.szName);
if (slot == kInvalidIndex) {
continue;
}
if (material.arVariants.size() != set.looks.size()) {
diagnostics.warn(DiagCode::LookBindingMalformed,
material.szName + " has " +
std::to_string(material.arVariants.size()) + " variants for " +
std::to_string(set.looks.size()) + " looks",
ElementRef(ElementKind::Slot, slot), ProfileId::Diablo3);
}
for (std::size_t look = 0; look < material.arVariants.size() && look < set.looks.size();
++look) {
Material imported = d3_core::ImportVariant(
material.arVariants[look], material.szName + "#" + set.looks.looks[look].name,
context, diagnostics);
set.slotBindings[slot].byLook[look] = static_cast<u32>(set.materials.size());
set.materials.push_back(std::move(imported));
}
}
document.textures.resize(context.texturesBySno.size());
for (const auto& [sno, index] : context.texturesBySno) {
TextureRef ref;
ref.key = TextureSnoId{static_cast<u32>(d3n::Group::Textures), sno};
document.textures[index] = std::move(ref);
}
model.meshes.push_back(
ImportGeoSet(source.tGeoSet0, "geoset0", model, nodes.boneToNode, options, diagnostics));
if (!source.tGeoSet1.arSubObjects.empty()) {
Mesh second =
ImportGeoSet(source.tGeoSet1, "geoset1", model, nodes.boneToNode, options, diagnostics);
second.lodLevel = 1;
model.meshes.push_back(std::move(second));
}
set.resizeBindings(model.materialSlots.size());
set.native.set("appearanceSnoId", static_cast<i64>(source.dwSnoId));
set.native.set("sourceVersion", static_cast<i64>(kAppearanceVersion));
set.native.set("objectType", static_cast<i64>(source.eObjectType));
if (!source.arCollisionCapsules.empty()) {
set.native.set("collisionCapsuleCount",
static_cast<i64>(source.arCollisionCapsules.size()));
}
if (!source.arConstraints.empty()) {
set.native.set("constraintCount", static_cast<i64>(source.arConstraints.size()));
}
model.profileSets.push_back(std::move(set));
document.models.push_back(std::move(model));
result.value = std::move(document);
return result;
}
Result<u32> D3Converter::appendActor(Document& document, const d3n::Actor& source,
AssetSource& assets, const D3ImportOptions& options) const {
Result<u32> result;
Diagnostics& diagnostics = result.diagnostics;
if (!source.snoAppearance.valid()) {
diagnostics.error(DiagCode::AssetUnresolved, "actor names no appearance");
return result;
}
std::vector<const d3n::TriggerEvent*> spawns;
if (options.attachmentDepth > 0) {
for (const d3n::MsgTriggeredEvent& message : source.arMsgTriggeredEvents) {
if (IsSpawn(message.tEvent)) {
spawns.push_back(&message.tEvent);
}
}
}
const std::string wantedLook =
!options.materialLook.empty() ? options.materialLook : source.tLook0.szName;
if (spawns.empty()) {
const u32 existing = FindAppearanceModel(document, source.snoAppearance.id);
if (existing != kInvalidIndex && !HasAttachedModel(document.models[existing])) {
const ProfileMaterialSet* set = document.models[existing].setFor(ProfileId::Diablo3);
const bool sameLook =
set != nullptr && (wantedLook.empty() ||
(set->defaultLook < set->looks.size() &&
IEquals(set->looks.looks[set->defaultLook].name, wantedLook)));
const NativeBag::Entry* animSet =
set != nullptr ? set->native.find("animSetSnoId") : nullptr;
const i64 wantedAnimSet = source.snoAnimSet.valid() ? source.snoAnimSet.id : -1;
const bool sameAnimSet = animSet != nullptr && animSet->value == wantedAnimSet;
if (sameLook && sameAnimSet) {
result.value = existing;
return result;
}
}
}
const d3n::Appearances* appearance = assets.appearance(source.snoAppearance.id);
if (appearance == nullptr) {
diagnostics.error(DiagCode::AssetUnresolved, "appearance " +
std::to_string(source.snoAppearance.id) +
" did not load");
return result;
}
D3ImportOptions resolved = options;
resolved.materialLook = wantedLook;
Result<Document> built = fromAppearance(*appearance, &assets, resolved);
diagnostics.append(built.diagnostics);
if (!built.ok() || built->models.empty()) {
diagnostics.error(DiagCode::AssetUnresolved, "appearance did not convert");
return result;
}
Document& builtDocument = *built.value;
Model model = std::move(builtDocument.models.front());
std::vector<u32> textureRemap(builtDocument.textures.size(), kInvalidIndex);
const auto snoOf = [](const TextureRef& texture) -> u32 {
const TextureSnoId* key = std::get_if<TextureSnoId>(&texture.key);
return key != nullptr ? key->id : kInvalidIndex;
};
for (std::size_t t = 0; t < builtDocument.textures.size(); ++t) {
const TextureRef& texture = builtDocument.textures[t];
u32 found = kInvalidIndex;
for (std::size_t e = 0; e < document.textures.size(); ++e) {
if (snoOf(document.textures[e]) == snoOf(texture)) {
found = static_cast<u32>(e);
break;
}
}
if (found == kInvalidIndex) {
found = static_cast<u32>(document.textures.size());
document.textures.push_back(texture);
}
textureRemap[t] = found;
}
for (ProfileMaterialSet& set : model.profileSets) {
for (Material& material : set.materials) {
CommonMaterial& common = material.InitCommon();
for (u32 ordinal = 0; ordinal < common.ordinalCount(); ++ordinal) {
TextureInput* input = common.inputAt(ordinal);
if (input != nullptr && input->texture < textureRemap.size()) {
input->texture = textureRemap[input->texture];
}
}
}
}
model.name = "actor_" + std::to_string(source.dwSnoId);
if (ProfileMaterialSet* set = model.setFor(ProfileId::Diablo3); set != nullptr) {
set->native.set("animSetSnoId",
static_cast<i64>(source.snoAnimSet.valid() ? source.snoAnimSet.id : -1));
}
const u32 index = static_cast<u32>(document.models.size());
document.declare(ProfileId::Diablo3);
document.models.push_back(std::move(model));
result.value = index;
if (options.importAnimation && source.snoAnimSet.valid()) {
Result<u32> animSet = importAnimSet(document, index, source.snoAnimSet.id, assets);
diagnostics.append(animSet.diagnostics);
}
for (const d3n::TriggerEvent* spawn : spawns) {
const d3n::TriggerEvent& event = *spawn;
const i32 group = event.tPayload.eSnoGroup;
const i32 payload = event.tPayload.dwNameHandle;
u32 node = kInvalidNode;
const std::string& wanted = event.tHardpoint0.szName;
if (!wanted.empty() && !IEquals(wanted, "Default") && !IEquals(wanted, "- None -") &&
!IEquals(wanted, "Don't Override")) {
const NodeTree& hostNodes = document.models[index].nodes;
for (u32 n = 0; n < hostNodes.size(); ++n) {
if (hostNodes.nodes[n].kind == NodeKind::Attachment &&
IEquals(hostNodes.nodes[n].name, wanted)) {
node = n;
break;
}
}
if (node == kInvalidNode) {
diagnostics.warn(DiagCode::HardpointUnresolved, "event names hardpoint '" + wanted +
"', which the model has no "
"attachment node for");
}
}
Node attach;
attach.name = wanted.empty() ? "attach" : wanted + "_attach";
attach.kind = NodeKind::Attachment;
attach.resetPayloadForKind();
attach.parent = node;
auto& attachPayload = std::get<AttachmentPayload>(attach.payload);
attachPayload.asset.group = static_cast<u32>(group);
attachPayload.asset.id = static_cast<u32>(payload);
if (group == static_cast<i32>(d3n::Group::Actor)) {
const d3n::Actor* child = assets.actor(payload);
if (child == nullptr) {
diagnostics.warn(DiagCode::AssetUnresolved,
"attached actor " + std::to_string(payload) + " did not load");
} else {
D3ImportOptions childOptions = options;
childOptions.attachmentDepth = options.attachmentDepth - 1;
childOptions.wardrobe.clear();
childOptions.materialLook.clear();
Result<u32> childModel = appendActor(document, *child, assets, childOptions);
diagnostics.append(childModel.diagnostics);
if (childModel.ok() && *childModel.value != index) {
attachPayload.model = *childModel.value;
} else if (childModel.ok()) {
diagnostics.info(DiagCode::AssetUnresolved,
"attached actor " + std::to_string(payload) +
" resolves to this model; left named, not linked");
}
}
}
document.models[index].nodes.add(std::move(attach));
}
return result;
}
namespace {
using TagMapField = std::vector<d3n::AnimSetTagMapEntry> d3n::AnimSet::*;
struct TagMap {
const char* name;
TagMapField field;
};
const TagMap kTagMaps[] = {
{"core", &d3n::AnimSet::tCoreTagMap},
{"hth", &d3n::AnimSet::tmHth},
{"1HSwing", &d3n::AnimSet::tm1HSwing},
{"1HThrust", &d3n::AnimSet::tm1HThrust},
{"2HSwing", &d3n::AnimSet::tm2HSwing},
{"2HThrust", &d3n::AnimSet::tm2HThrust},
{"staff", &d3n::AnimSet::tmStaff},
{"bow", &d3n::AnimSet::tmBow},
{"xbow", &d3n::AnimSet::tmXBow},
{"wand", &d3n::AnimSet::tmWand},
{"dualWield", &d3n::AnimSet::tmDualWield},
{"hthWithOrb", &d3n::AnimSet::tmHthWithOrb},
{"1HSwingWithOrb", &d3n::AnimSet::tm1HSwingWithOrb},
{"1HThrustWithOrb", &d3n::AnimSet::tm1HThrustWithOrb},
{"dualWieldSwordFist", &d3n::AnimSet::tmDualWieldSwordFist},
{"dualWieldFistFist", &d3n::AnimSet::tmDualWieldFistFist},
{"1HFist", &d3n::AnimSet::tm1HFist},
{"2HAxeMace", &d3n::AnimSet::tm2HAxeMace},
{"handXBow", &d3n::AnimSet::tmHandXBow},
{"wandWithOrb", &d3n::AnimSet::tmWandWithOrb},
{"1HSwingWithShield", &d3n::AnimSet::tm1HSwingWithShield},
{"1HThrustWithShield", &d3n::AnimSet::tm1HThrustWithShield},
{"hthWithShield", &d3n::AnimSet::tmHthWithShield},
{"2HSwingWithShield", &d3n::AnimSet::tm2HSwingWithShield},
{"2HThrustWithShield", &d3n::AnimSet::tm2HThrustWithShield},
{"staffWithShield", &d3n::AnimSet::tmStaffWithShield},
{"2HFlail", &d3n::AnimSet::tm2HFlail},
{"2HFlailWithShield", &d3n::AnimSet::tm2HFlailWithShield},
{"onHorse", &d3n::AnimSet::tmOnHorse},
};
}
Result<u32> D3Converter::importAnimSet(Document& document, u32 model, i32 snoId,
AssetSource& assets) const {
Result<u32> result;
if (model >= document.models.size()) {
result.diagnostics.error(DiagCode::ClipTargetMissing,
"model " + std::to_string(model) + " of " +
std::to_string(document.models.size()));
return result;
}
const d3n::AnimSet* source = assets.animSet(snoId);
if (source == nullptr) {
result.diagnostics.warn(DiagCode::AssetUnresolved,
"anim set " + std::to_string(snoId) + " did not load");
return result;
}
std::vector<std::pair<i32, u32>> clipOfAnim;
const auto clipFor = [&](i32 animSno) -> u32 {
for (const auto& entry : clipOfAnim) {
if (entry.first == animSno) {
return entry.second;
}
}
const d3n::Anim* anim = assets.anim(animSno);
if (anim == nullptr) {
clipOfAnim.emplace_back(animSno, kInvalidIndex);
return kInvalidIndex;
}
const std::vector<u32> clips =
d3_anim::ImportAnim(*anim, document, model, result.diagnostics);
const u32 first = clips.empty() ? kInvalidIndex : clips.front();
clipOfAnim.emplace_back(animSno, first);
return first;
};
const u32 core = static_cast<u32>(document.animSets.size());
u32 unresolved = 0;
for (std::size_t m = 0; m < sizeof(kTagMaps) / sizeof(kTagMaps[0]); ++m) {
const std::vector<d3n::AnimSetTagMapEntry>& rows = source->*(kTagMaps[m].field);
if (m != 0 && rows.empty()) {
continue; }
AnimSet set;
set.name = kTagMaps[m].name;
set.baseAnimSet = m == 0 ? kInvalidIndex : core;
for (const d3n::AnimSetTagMapEntry& row : rows) {
if (!row.snoAnim.valid()) {
continue;
}
const u32 clip = clipFor(row.snoAnim.id);
if (clip == kInvalidIndex) {
++unresolved;
continue;
}
set.byTag.push_back(AnimTag{static_cast<u32>(row.dwTagId), clip});
}
document.animSets.push_back(std::move(set));
}
if (unresolved != 0) {
result.diagnostics.warn(DiagCode::AssetUnresolved,
std::to_string(unresolved) +
" tags name an animation that did not load");
}
document.models[model].animSet = core;
result.value = core;
return result;
}
Result<Document> D3Converter::fromActor(const d3n::Actor& source, AssetSource& assets,
const D3ImportOptions& options) const {
Result<Document> result;
Document document;
document.space = CoordSpace::Blizzard;
document.defaultProfile = ProfileId::Diablo3;
document.declare(ProfileId::Diablo3);
Result<u32> root = appendActor(document, source, assets, options);
result.diagnostics = std::move(root.diagnostics);
if (!root.ok()) {
return result;
}
document.name = document.models[*root.value].name;
result.value = std::move(document);
return result;
}
namespace {
d3n::PRSTransform FromTransform(const Transform& source) {
d3n::PRSTransform out;
out.qRotation =
Vector4f{source.rotation.x, source.rotation.y, source.rotation.z, source.rotation.w};
out.vTranslation = source.translation;
out.flScale = source.scale.x;
return out;
}
d3n::PRTransform FromTransformPR(const Transform& source) {
d3n::PRTransform out;
out.qRotation =
Vector4f{source.rotation.x, source.rotation.y, source.rotation.z, source.rotation.w};
out.vTranslation = source.translation;
return out;
}
d3n::AABB FromExtent(const Extent& extent) {
d3n::AABB out;
out.vCenter = Vector3f{(extent.minimum.x + extent.maximum.x) * 0.5f,
(extent.minimum.y + extent.maximum.y) * 0.5f,
(extent.minimum.z + extent.maximum.z) * 0.5f};
out.vHalfExtent = Vector3f{(extent.maximum.x - extent.minimum.x) * 0.5f,
(extent.maximum.y - extent.minimum.y) * 0.5f,
(extent.maximum.z - extent.minimum.z) * 0.5f};
return out;
}
d3n::Sphere FromSphere(const Sphere& sphere) {
d3n::Sphere out;
out.vCenter = sphere.center;
out.flRadius = sphere.radius;
return out;
}
u32 PackVertexVector(const Vector3f& value) {
const auto channel = [](f32 v) -> u32 {
const f32 scaled = (std::clamp(v, -1.0f, 1.0f) + 1.0f) * (255.0f / 2.0f);
return static_cast<u32>(std::clamp(scaled + 0.5f, 0.0f, 255.0f));
};
return channel(value.x) | (channel(value.y) << 8) | (channel(value.z) << 16);
}
u32 PackTexCoord(const Vector2f& value, bool& clamped) {
const auto channel = [&clamped](f32 v) -> u32 {
const f32 scaled = (v + 64.0f) * 512.0f;
if (scaled < 0.0f || scaled > 65535.0f) {
clamped = true;
}
return static_cast<u32>(std::clamp(scaled + 0.5f, 0.0f, 65535.0f));
};
return channel(value.x) | (channel(value.y) << 16);
}
u32 PackVertexColor(const std::array<u8, 4>& rgba) {
return static_cast<u32>(rgba[0]) | (static_cast<u32>(rgba[1]) << 8) |
(static_cast<u32>(rgba[2]) << 16) | (static_cast<u32>(rgba[3]) << 24);
}
std::array<Transform, 5> FivePoses(const NodeTree& tree, u32 node, bool& derived) {
const Node& source = tree.nodes[node];
if (tree.poseSchema.size() == 5 && source.poses.size() == 5) {
return {source.poses[0], source.poses[1], source.poses[2], source.poses[3],
source.poses[4]};
}
derived = true;
const Transform world = tree.worldBind(node);
const Transform inverse = Inverse(world);
return {world, inverse, source.local, world, inverse};
}
const native::D3Material* D3BlockOf(const Material& material) {
return std::get_if<native::D3Material>(&material.Native());
}
d3n::SubObjectAppearance ExportVariant(const Material& material) {
d3n::SubObjectAppearance out;
const native::D3Material* block = D3BlockOf(material);
if (block == nullptr) {
out.dwUnknown00 = 1;
return out;
}
out.dwUnknown00 = block->variantFlags;
CopyFromNative(block->cloth, out.snoCloth);
CopyFromNative(block->baseMaterial, out.snoMaterial);
CopyFromNative(block->uber, out.tMaterial);
out.arShaderParams.reserve(block->shaderParams.size());
for (const native::D3TagValue& value : block->shaderParams) {
d3n::TagMapEntry entry;
CopyFromNative(value, entry);
out.arShaderParams.push_back(entry);
}
return out;
}
geom::RenderMeshDesc D3VertexDesc() {
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::uv(1), utils::AttributeClass::UV, utils::AttributeEncoding::Float32, 2, 0},
{geom::names::kTangent, utils::AttributeClass::Tangent, utils::AttributeEncoding::Float32,
3, 0},
{geom::names::kBinormal, utils::AttributeClass::Binormal, utils::AttributeEncoding::Float32,
3, 0},
{geom::names::color(0), utils::AttributeClass::Color, utils::AttributeEncoding::UInt8, 4,
0},
{geom::names::color(1), utils::AttributeClass::Color, utils::AttributeEncoding::UInt8, 4,
0},
};
desc.includeSkin = true;
desc.maxInfluences = Profile(ProfileId::Diablo3).maxBoneInfluences;
desc.splitBySection = true;
return desc;
}
class AppearanceWriter {
public:
AppearanceWriter(const Document& document, const Model& model, const ProfileMaterialSet& set,
Diagnostics& out)
: document_(document), model_(model), set_(set), out_(out) {}
D3AppearanceExport run(u32 look) {
D3AppearanceExport result;
result.look = look;
d3n::Appearances& appearance = result.appearance;
appearance.dwSnoId = static_cast<i32>(set_.native.value("appearanceSnoId", -1));
appearance.eObjectType = static_cast<i32>(set_.native.value("objectType", 0));
appearance.tBounds = FromExtent(model_.bounds.valid() ? model_.bounds : document_.bounds);
writeBones(appearance);
writeLooks(appearance);
writeGeometry(appearance, result);
if (overrides_ == 0) {
result.looks.clear();
}
if (dyed_ == 0) {
result.dyes.clear();
}
appearance.dwBoneCount = static_cast<i32>(appearance.arBones.size());
appearance.dwHardpointCount = static_cast<i32>(appearance.arHardpoints.size());
appearance.dwLookCount = static_cast<i32>(appearance.arLooks.size());
appearance.dwMaterialCount = static_cast<i32>(appearance.arMaterials.size());
appearance.dwStaticLightCount = static_cast<i32>(appearance.arStaticLights.size());
appearance.tGeoSet0.dwSubObjectCount =
static_cast<i32>(appearance.tGeoSet0.arSubObjects.size());
appearance.tGeoSet1.dwSubObjectCount =
static_cast<i32>(appearance.tGeoSet1.arSubObjects.size());
if (clothSections_ != 0) {
out_.warn(DiagCode::OperationUnsupported,
std::to_string(clothSections_) +
" sections are cloth-simulated and the simulation is not carried; they "
"draw skinned",
ElementRef(), ProfileId::Diablo3);
}
const i64 capsules = set_.native.value("collisionCapsuleCount", 0);
const i64 constraints = set_.native.value("constraintCount", 0);
if (capsules != 0 || constraints != 0 || droppedBoneShapes_ != 0) {
out_.info(DiagCode::OperationUnsupported,
"the ragdoll rig is not carried: " + std::to_string(droppedBoneShapes_) +
" bone collision shapes, " + std::to_string(capsules) +
" collision capsules and " + std::to_string(constraints) +
" constraints were counted on import and never stored",
ElementRef(), ProfileId::Diablo3);
}
return result;
}
private:
void writeBones(d3n::Appearances& appearance) {
const NodeTree& tree = model_.nodes;
boneOf_.assign(tree.size(), kInvalidIndex);
for (u32 n = 0; n < tree.size(); ++n) {
if (tree.nodes[n].kind == NodeKind::Bone) {
boneOf_[n] = static_cast<u32>(appearance.arBones.size());
appearance.arBones.emplace_back();
}
}
bool derivedAny = false;
u32 nonUniform = 0;
for (u32 n = 0; n < tree.size(); ++n) {
if (boneOf_[n] == kInvalidIndex) {
continue;
}
const Node& node = tree.nodes[n];
d3n::BoneStructure& bone = appearance.arBones[boneOf_[n]];
bone.szName = node.name;
bone.nParentIndex = static_cast<i32>(nearestBone(n));
const auto& payload = std::get<BonePayload>(node.payload);
bone.tBounds = FromExtent(payload.bounds);
bone.tSphere = FromSphere(payload.sphere);
const std::array<Transform, 5> poses = FivePoses(tree, n, derivedAny);
d3n::PRSTransform* slots[5] = {&bone.tTransform0, &bone.tTransform1, &bone.tTransform2,
&bone.tTransform3, &bone.tTransform4};
for (std::size_t p = 0; p < 5; ++p) {
*slots[p] = FromTransform(poses[p]);
if (!Uniform(poses[p].scale)) {
++nonUniform;
}
}
droppedBoneShapes_ += static_cast<u32>(node.native.value("collisionShapeCount", 0));
}
if (derivedAny) {
out_.warn(DiagCode::BindPoseRecomposed,
"the tree carries no five-entry D3 pose schema; bind poses A and B were "
"both composed from the local chain",
ElementRef(), ProfileId::Diablo3);
}
if (nonUniform != 0) {
out_.warn(DiagCode::NonUniformScaleFlattened,
std::to_string(nonUniform) +
" bind poses carry a non-uniform scale; a D3 `PRSTransform` holds one "
"float and the x component was written",
ElementRef(), ProfileId::Diablo3);
}
for (u32 n = 0; n < tree.size(); ++n) {
const Node& node = tree.nodes[n];
if (node.kind != NodeKind::Attachment) {
continue;
}
const auto& payload = std::get<AttachmentPayload>(node.payload);
if (!payload.asset.empty() || payload.model != kInvalidIndex) {
continue;
}
d3n::Hardpoint point;
point.szName = node.name;
point.nBoneIndex = static_cast<i32>(nearestBone(n));
point.tTransform = FromTransformPR(node.local);
appearance.arHardpoints.push_back(std::move(point));
}
for (u32 n = 0; n < tree.size(); ++n) {
const Node& node = tree.nodes[n];
if (node.kind == NodeKind::ParticleEmitter) {
const u32 bone = nearestBone(n);
if (bone < appearance.arBones.size()) {
const auto& payload = std::get<ParticlePayload>(node.payload);
appearance.arBones[bone].snoParticle.id = static_cast<i32>(payload.system.id);
appearance.arBones[bone].snoParticle.group =
static_cast<d3n::Group>(payload.system.group);
}
continue;
}
if (node.kind != NodeKind::Light) {
continue;
}
const auto& payload = std::get<LightPayload>(node.payload);
d3n::StaticLight light;
light.nType = static_cast<i32>(node.native.value("d3LightType", 0));
light.dwFlags = static_cast<i32>(node.native.value("d3LightFlags", 0));
light.vPosition = node.local.translation;
light.flIntensity = payload.intensity;
light.flAttenuation0 = payload.attenuationStart;
light.flAttenuation1 = payload.attenuationEnd;
const auto byte = [](f32 v) {
return static_cast<u32>(std::clamp(v * 255.0f + 0.5f, 0.0f, 255.0f));
};
light.dwColor = byte(payload.color.x) | (byte(payload.color.y) << 8) |
(byte(payload.color.z) << 16);
appearance.arStaticLights.push_back(light);
}
}
u32 nearestBone(u32 node) const {
for (u32 walk = model_.nodes.nodes[node].parent; walk != kInvalidNode;
walk = model_.nodes.nodes[walk].parent) {
if (boneOf_[walk] != kInvalidIndex) {
return boneOf_[walk];
}
}
return kInvalidIndex;
}
static bool Uniform(const Vector3f& scale) {
const f32 tolerance = 1e-5f;
return std::fabs(scale.x - scale.y) <= tolerance &&
std::fabs(scale.x - scale.z) <= tolerance;
}
void writeLooks(d3n::Appearances& appearance) {
for (const Look& entry : set_.looks.looks) {
d3n::AppearanceLook out;
out.szName = entry.name;
appearance.arLooks.push_back(std::move(out));
}
if (appearance.arLooks.empty()) {
appearance.arLooks.push_back(d3n::AppearanceLook{});
}
u32 improvised = 0;
for (std::size_t slot = 0; slot < model_.materialSlots.size(); ++slot) {
d3n::AppearanceMaterial material;
material.szName = model_.materialSlots[slot];
bool bound = false;
for (std::size_t l = 0; l < appearance.arLooks.size(); ++l) {
const Material* resolved = Resolve(model_, static_cast<u32>(slot),
ProfileId::Diablo3, static_cast<u32>(l));
if (resolved == nullptr) {
material.arVariants.push_back(d3n::SubObjectAppearance{});
continue;
}
bound = true;
if (D3BlockOf(*resolved) == nullptr) {
++improvised;
}
material.arVariants.push_back(ExportVariant(*resolved));
}
if (!bound) {
out_.warn(DiagCode::SlotNotBound,
"slot '" + material.szName + "' has no Diablo III material",
ElementRef(ElementKind::Slot, static_cast<u32>(slot)),
ProfileId::Diablo3);
}
appearance.arMaterials.push_back(std::move(material));
}
if (improvised != 0) {
out_.warn(DiagCode::DroppedNativeBlock,
std::to_string(improvised) +
" materials carry no Diablo III block; they were written empty rather "
"than derived, because a `UberMaterial` names textures by SNO id and "
"the common view holds document indices",
ElementRef(), ProfileId::Diablo3);
}
}
void writeGeometry(d3n::Appearances& appearance, D3AppearanceExport& result) {
geom::RenderMeshDesc desc = D3VertexDesc();
const SkinSkeleton skinSkeleton(model_.nodes);
skinSkeleton.describe(desc);
bool clamped = false;
for (std::size_t m = 0; m < model_.meshes.size(); ++m) {
const Mesh& mesh = model_.meshes[m];
d3n::GeoSet& target = mesh.lodLevel == 0 ? appearance.tGeoSet0 : appearance.tGeoSet1;
if (mesh.lodLevel > 1) {
out_.warn(DiagCode::LayerDropped,
"mesh '" + mesh.name + "' is LOD " + std::to_string(mesh.lodLevel) +
"; an appearance holds two geosets and it was written into the "
"second",
ElementRef(ElementKind::Mesh, static_cast<u32>(m)), ProfileId::Diablo3);
}
const geom::RenderMesh render = geom::BuildRenderMesh(mesh, desc);
out_.append(render.diagnostics);
for (std::size_t sec = 0; sec < mesh.sections.size(); ++sec) {
if (mesh.facesOfSection(static_cast<u32>(sec)).empty()) {
out_.warn(DiagCode::DegenerateFaceDropped,
"section '" + mesh.sections[sec].name +
"' has no faces left and writes no sub-object",
ElementRef(ElementKind::Section, static_cast<u32>(sec)),
ProfileId::Diablo3);
}
}
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 std::vector<Vector2f> uv1 = render.vertices.getUVs(1);
const std::vector<Vector4f> tangents = render.vertices.getTangents();
const std::vector<Vector3f> binormals = render.vertices.getBinormals();
const std::vector<Vector4f> color0 = render.vertices.getColors(0);
const std::vector<Vector4f> color1 = render.vertices.getColors(1);
const std::vector<std::array<u32, 4>> boneIndices = render.vertices.getBoneIndices();
const std::vector<std::array<f32, 4>> boneWeights = render.vertices.getBoneWeights();
for (const geom::RenderRange& range : render.ranges) {
if (range.indexCount == 0) {
continue;
}
const MeshSection* section =
range.section < mesh.sections.size() ? &mesh.sections[range.section] : nullptr;
d3n::SubObject sub;
sub.szName = section != nullptr ? section->name : std::string();
if (section != nullptr) {
sub.szMaterialName = section->native.text("materialName");
if (sub.szMaterialName.empty()) {
sub.szMaterialName = sub.szName + "Shape_" + sub.szName + "_001";
}
sub.dwVertexFormat = static_cast<i32>(section->native.value("vertexFormat", 0));
sub.tBounds = FromExtent(section->bounds);
const i64 surface = section->native.value("surfaceSno", -1);
if (surface >= 0) {
sub.snoSurface.id = static_cast<i32>(surface);
sub.snoSurface.group = d3n::Group::Surface;
}
}
const Slices slices{positions, normals, uv0, uv1, tangents,
binormals, color0, color1, boneIndices, boneWeights};
writeSubObject(render, range, slices, section, sub, clamped);
if (section != nullptr && hasFlag(section->flags, SectionFlags::ClothSimulated)) {
++clothSections_;
}
Emitted& emitted = &target == &appearance.tGeoSet0 ? first_ : second_;
emitted.hidden.push_back(
section != nullptr && hasFlag(section->flags, SectionFlags::Hidden) ? 1u : 0u);
const i64 look =
section != nullptr ? section->native.value("lookOverride", -1) : -1;
const i64 dye = section != nullptr ? section->native.value("dye", 0) : 0;
emitted.looks.push_back(look >= 0 ? static_cast<u32>(look) : kInvalidIndex);
emitted.dyes.push_back(static_cast<i32>(dye));
if (look >= 0) {
++overrides_;
}
if (dye != 0) {
++dyed_;
}
emitted.sections.emplace_back(static_cast<u32>(m), range.section);
target.arSubObjects.push_back(std::move(sub));
}
}
for (const Emitted* emitted : {&first_, &second_}) {
result.hidden.insert(result.hidden.end(), emitted->hidden.begin(),
emitted->hidden.end());
result.looks.insert(result.looks.end(), emitted->looks.begin(), emitted->looks.end());
result.dyes.insert(result.dyes.end(), emitted->dyes.begin(), emitted->dyes.end());
result.sourceSections.insert(result.sourceSections.end(), emitted->sections.begin(),
emitted->sections.end());
}
if (clamped) {
out_.warn(DiagCode::AttributeCountMismatch,
"a UV fell outside the [-64, 64) range a D3 texture coordinate encodes and "
"was clamped",
ElementRef(), ProfileId::Diablo3);
}
}
struct Slices {
const std::vector<Vector3f>& positions;
const std::vector<Vector3f>& normals;
const std::vector<Vector2f>& uv0;
const std::vector<Vector2f>& uv1;
const std::vector<Vector4f>& tangents;
const std::vector<Vector3f>& binormals;
const std::vector<Vector4f>& color0;
const std::vector<Vector4f>& color1;
const std::vector<std::array<u32, 4>>& boneIndices;
const std::vector<std::array<f32, 4>>& boneWeights;
};
void writeSubObject(const geom::RenderMesh& render, const geom::RenderRange& range,
const Slices& slices, const MeshSection* section, d3n::SubObject& sub,
bool& clamped) {
const std::vector<Vector3f>& positions = slices.positions;
const std::vector<std::array<u32, 4>>& boneIndices = slices.boneIndices;
const std::vector<std::array<f32, 4>>& boneWeights = slices.boneWeights;
std::vector<u32> localOf(positions.size(), kInvalidIndex);
std::vector<u32> sourceOf;
sub.arIndices.reserve(range.indexCount);
for (u32 i = 0; i < range.indexCount; ++i) {
const u32 index = render.indices[range.firstIndex + i];
if (index >= localOf.size()) {
sub.arIndices.push_back(0);
continue;
}
if (localOf[index] == kInvalidIndex) {
localOf[index] = static_cast<u32>(sourceOf.size());
sourceOf.push_back(index);
}
sub.arIndices.push_back(static_cast<u16>(localOf[index]));
}
if (sourceOf.size() > 0x10000u) {
out_.warn(DiagCode::IndexWidthExceeded,
"sub-object '" + sub.szName + "' needs " + std::to_string(sourceOf.size()) +
" vertices, past the u16 a D3 face corner is",
ElementRef(), ProfileId::Diablo3);
}
const bool rigid = section != nullptr && section->rigidNode.has_value();
const u32 node =
rigid ? *section->rigidNode
: (section != nullptr ? static_cast<u32>(section->native.value("boneNode", -1))
: kInvalidIndex);
sub.nBoneIndex = node < boneOf_.size() && boneOf_[node] != kInvalidIndex
? static_cast<i32>(boneOf_[node])
: -1;
const auto colorAt = [](const std::vector<Vector4f>& layer, u32 index) -> u32 {
if (index >= layer.size()) {
return 0xFFFFFFFFu;
}
std::array<u8, 4> bytes{};
for (std::size_t c = 0; c < 4; ++c) {
bytes[c] = static_cast<u8>(std::clamp(layer[index].data[c], 0.0f, 255.0f) + 0.5f);
}
return PackVertexColor(bytes);
};
sub.arVertices.reserve(sourceOf.size());
for (const u32 source : sourceOf) {
d3n::FatVertex vertex;
vertex.vPosition = positions[source];
vertex.dwNormal = PackVertexVector(
source < slices.normals.size() ? slices.normals[source] : Vector3f{0, 0, 1});
vertex.dwTexCoord0 = PackTexCoord(
source < slices.uv0.size() ? slices.uv0[source] : Vector2f{0, 0}, clamped);
vertex.dwTexCoord1 = PackTexCoord(
source < slices.uv1.size() ? slices.uv1[source] : Vector2f{0, 0}, clamped);
vertex.dwTangent = source < slices.tangents.size()
? PackVertexVector(Vector3f{slices.tangents[source].x,
slices.tangents[source].y,
slices.tangents[source].z})
: PackVertexVector(Vector3f{1, 0, 0});
vertex.dwBinormal = PackVertexVector(
source < slices.binormals.size() ? slices.binormals[source] : Vector3f{0, 1, 0});
vertex.dwColor = colorAt(slices.color0, source);
vertex.dwAuxColor = colorAt(slices.color1, source);
sub.arVertices.push_back(vertex);
}
if (!rigid) {
sub.arVertexInfluences.reserve(sourceOf.size());
for (const u32 source : sourceOf) {
d3n::VertInfluences influences;
d3n::Influence* three[3] = {&influences.tInfluence0, &influences.tInfluence1,
&influences.tInfluence2};
for (std::size_t k = 0; k < 3; ++k) {
three[k]->nBoneIndex = 0;
three[k]->flWeight = 0.0f;
if (source >= boneIndices.size() || source >= boneWeights.size() ||
boneWeights[source][k] <= 0.0f) {
continue;
}
const u32 influenceNode = boneIndices[source][k];
const u32 bone =
influenceNode < boneOf_.size() ? boneOf_[influenceNode] : kInvalidIndex;
if (bone == kInvalidIndex) {
continue;
}
three[k]->nBoneIndex = static_cast<i32>(bone);
three[k]->flWeight = boneWeights[source][k];
}
sub.arVertexInfluences.push_back(influences);
}
}
sub.dwVertexCount = static_cast<i32>(sub.arVertices.size());
sub.dwIndexCount = static_cast<i32>(sub.arIndices.size());
}
const Document& document_;
const Model& model_;
const ProfileMaterialSet& set_;
Diagnostics& out_;
struct Emitted {
std::vector<u8> hidden;
std::vector<u32> looks;
std::vector<i32> dyes;
std::vector<std::pair<u32, u32>> sections;
};
Emitted first_;
Emitted second_;
std::vector<u32> boneOf_;
u32 droppedBoneShapes_ = 0;
u32 clothSections_ = 0;
u32 overrides_ = 0;
u32 dyed_ = 0;
};
}
Result<D3AppearanceExport> D3Converter::toAppearance(const Document& document,
const D3ExportOptions& options) const {
Result<D3AppearanceExport> result;
if (options.model >= document.models.size()) {
result.diagnostics.error(DiagCode::ClipTargetMissing,
"model " + std::to_string(options.model) + " of " +
std::to_string(document.models.size()));
return result;
}
const Model& model = document.models[options.model];
const ProfileMaterialSet* set = model.setFor(ProfileId::Diablo3);
if (set == nullptr) {
result.diagnostics.error(DiagCode::ProfileNotCarried,
"the model carries no Diablo III material set; derive one first",
ElementRef(), ProfileId::Diablo3);
return result;
}
u32 look = set->defaultLook;
if (!options.look.empty()) {
const u32 named = set->looks.find(options.look);
if (named == kInvalidIndex) {
result.diagnostics.warn(DiagCode::LookDropped,
"look '" + options.look + "' is not in this set", ElementRef(),
ProfileId::Diablo3);
} else {
look = named;
}
}
result.value = AppearanceWriter(document, model, *set, result.diagnostics).run(look);
return result;
}
Result<D3Converter::D3AnimExport> D3Converter::toAnimSet(const Document& document,
u32 model) const {
Result<D3AnimExport> result;
if (model >= document.models.size()) {
result.diagnostics.error(DiagCode::ClipTargetMissing,
"model " + std::to_string(model) + " of " +
std::to_string(document.models.size()));
return result;
}
D3AnimExport out;
out.anims = d3_anim::ExportAnims(document, model, result.diagnostics);
std::vector<i32> animOfClip(document.clips.size(), -1);
{
i32 synthetic = d3_anim::kSyntheticAnimBase;
for (std::size_t c = 0; c < document.clips.size(); ++c) {
const Clip& clip = document.clips[c];
if (clip.model != model &&
!(clip.model == kInvalidIndex && document.models.size() == 1)) {
continue;
}
const i32 id = static_cast<i32>(clip.native.value("animSnoId", -1));
animOfClip[c] = id >= 0 ? id : synthetic++;
}
}
const Model& source = document.models[model];
const u32 core = source.animSet;
if (core == kInvalidIndex || core >= document.animSets.size()) {
result.value = std::move(out);
return result;
}
out.animSet.dwSnoId =
static_cast<i32>(source.setFor(ProfileId::Diablo3) != nullptr
? source.setFor(ProfileId::Diablo3)->native.value("animSetSnoId", -1)
: -1);
u32 unnamed = 0;
for (std::size_t s = 0; s < document.animSets.size(); ++s) {
const AnimSet& set = document.animSets[s];
if (s != core && set.baseAnimSet != core) {
continue; }
std::vector<d3n::AnimSetTagMapEntry>* target = nullptr;
for (const TagMap& map : kTagMaps) {
if (set.name == map.name) {
target = &(out.animSet.*(map.field));
break;
}
}
if (target == nullptr) {
++unnamed;
continue;
}
for (const AnimTag& tag : set.byTag) {
if (tag.clip >= animOfClip.size() || animOfClip[tag.clip] < 0) {
continue;
}
d3n::AnimSetTagMapEntry row;
row.dwTagId = static_cast<i32>(tag.tagId);
row.snoAnim.id = animOfClip[tag.clip];
row.snoAnim.group = d3n::Group::Anim;
target->push_back(row);
}
}
if (unnamed != 0) {
result.diagnostics.warn(DiagCode::AnimTrackDropped,
std::to_string(unnamed) +
" anim sets name no `.ans` tag map and were not written",
ElementRef(), ProfileId::Diablo3);
}
result.value = std::move(out);
return result;
}
} } }