#include "mdx_anim.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <deque>
#include <optional>
#include <string>
#include <utility>
#include <whiteout/models/wem/anim/clip.h>
#include "mdx_track_slicer.h"
namespace whiteout {
namespace models {
namespace wem {
namespace mdx_anim {
namespace {
constexpr u32 kNoGlobalSequence = mdx::Track<f32>::kNoGlobalSequence;
bool HasExtent(const Extent& extent) {
return extent.sphereRadius > 0.0f || extent.maximum.x > extent.minimum.x ||
extent.maximum.y > extent.minimum.y || extent.maximum.z > extent.minimum.z;
}
constexpr f32 kMillisecondsPerSecond = 1000.0f;
constexpr f32 Seconds(f32 milliseconds) {
return milliseconds / kMillisecondsPerSecond;
}
using mdx_slice::InterpOf;
using mdx_slice::ValueTrait;
mdx::Track<Vector3f> SwapRedBlue(mdx::Track<Vector3f> track) {
for (Vector3f& value : track.keys_data) {
std::swap(value.x, value.z);
}
return track;
}
u32 FeatureIdFor(Material& material, FeatureKind kind, u32 ordinal) {
CommonMaterial& common = material.InitCommon();
for (const MaterialFeature& feature : common.features) {
if (feature.kind() == kind && feature.layer == ordinal) {
return feature.id;
}
}
if (kind != FeatureKind::UvAnimation) {
return kInvalidIndex;
}
MaterialFeature feature;
feature.id = NextFeatureId(common.features);
feature.layer = ordinal;
feature.payload = UvAnimationFeature{};
common.features.push_back(feature);
return feature.id;
}
class Builder {
public:
Builder(const mdx::Model& source, const Context& context, Document& document, u32 modelIndex,
Diagnostics& out)
: source_(source), context_(context), document_(document),
model_(document.models[modelIndex]), modelIndex_(modelIndex), out_(out) {}
void run() {
buildSequenceClips();
addNodeTracks();
addKindTracks();
addLayerTracks();
addGeosetAnimationTracks();
addEvents();
reserveEmitterClips();
commit();
}
private:
using Window = mdx_slice::Window;
void buildSequenceClips() {
windows_.reserve(source_.sequences.size());
for (const mdx::Sequence& sequence : source_.sequences) {
Clip clip;
clip.name = sequence.name;
clip.model = modelIndex_;
clip.duration =
Seconds(static_cast<f32>(sequence.intervalEnd - sequence.intervalStart));
clip.looping = !mdx::hasFlag(sequence.flags, mdx::Sequence::Flag::NonLooping);
clip.native.set("intervalStart", static_cast<i64>(sequence.intervalStart));
clip.native.set("intervalEnd", static_cast<i64>(sequence.intervalEnd));
SetClipMoveSpeed(clip, sequence.moveSpeed);
SetClipRarity(clip, sequence.rarity);
clip.native.set("syncPoint", static_cast<i64>(sequence.syncPoint));
clip.bounds.minimum = sequence.extent.minimum;
clip.bounds.maximum = sequence.extent.maximum;
clip.bounds.sphereRadius = sequence.extent.boundsRadius;
clip.containers.push_back(baseContainer());
Window window;
window.start = static_cast<f32>(sequence.intervalStart);
window.end = static_cast<f32>(sequence.intervalEnd);
window.clip = static_cast<u32>(clips_.size());
windows_.push_back(window);
clips_.push_back(std::move(clip));
}
}
static SubTrackContainer baseContainer() {
SubTrackContainer container;
container.name = "base";
container.priority = 0;
container.concurrent = false; return container;
}
u32 globalClipFor(u32 globalSequenceId) {
for (const auto& entry : globalClips_) {
if (entry.first == globalSequenceId) {
return entry.second;
}
}
if (globalSequenceId >= source_.globalSequences.size()) {
return kInvalidIndex;
}
Clip clip;
clip.name = "globalSequence_" + std::to_string(globalSequenceId);
clip.model = modelIndex_;
clip.duration = Seconds(static_cast<f32>(source_.globalSequences[globalSequenceId]));
clip.looping = true;
clip.flags = ClipFlags::AutoPlay | ClipFlags::WorldClocked;
clip.native.set("globalSequenceId", static_cast<i64>(globalSequenceId));
SubTrackContainer container = baseContainer();
container.concurrent = true;
clip.containers.push_back(std::move(container));
const u32 index = static_cast<u32>(clips_.size());
clips_.push_back(std::move(clip));
globalClips_.emplace_back(globalSequenceId, index);
return index;
}
template <class T>
void addTrack(const mdx::Track<T>& track, const TrackTarget& target) {
if (!track.isUsed || track.timestamps.empty()) {
return;
}
constexpr geom::AttrType kType = ValueTrait<T>::kType;
const Interpolation interp = InterpOf(track.interpolationType, kType);
const u32 perKey = ValuesPerKey(interp);
if (track.keys_data.size() < track.timestamps.size() * perKey) {
out_.warn(DiagCode::AnimTrackDropped,
std::string("a ") + ToString(interp) + " track holds " +
std::to_string(track.keys_data.size()) + " values for " +
std::to_string(track.timestamps.size()) + " keys",
ElementRef(ElementKind::Track, kInvalidIndex));
return;
}
const u32 id = model_.animChannels.nextFreeId();
bool used = false;
if (track.globalSequenceId != kNoGlobalSequence) {
const u32 clip = globalClipFor(track.globalSequenceId);
if (clip == kInvalidIndex) {
out_.warn(DiagCode::AnimTrackDropped,
"a track names global sequence " +
std::to_string(track.globalSequenceId) + ", which the model lacks",
ElementRef(ElementKind::Track, kInvalidIndex));
return;
}
clips_[clip].containers[0].subTracks.push_back(mdx_slice::WholeTrack(track, id));
used = true;
} else {
for (const Window& window : windows_) {
SubTrack sub;
if (mdx_slice::SliceWindow(track, id, window, sub)) {
clips_[window.clip].containers[0].subTracks.push_back(std::move(sub));
used = true;
}
}
}
if (!used) {
return;
}
AnimChannel channel;
channel.id = id;
channel.target = target;
channel.valueType = kType;
model_.animChannels.add(channel);
}
u32 nodeOf(u32 objectId) const {
if (context_.byObjectId == nullptr) {
return kInvalidNode;
}
const auto found = context_.byObjectId->find(objectId);
return found == context_.byObjectId->end() ? kInvalidNode : found->second;
}
static TrackTarget nodeTarget(u32 node, Channel channel, u32 sub = 0) {
TrackTarget target;
target.kind = TrackTarget::Kind::Node;
target.node = node;
target.channel = channel;
target.sub = sub;
return target;
}
void addNodeTracks() {
const auto visit = [this](const mdx::Node& node) {
const u32 index = nodeOf(node.objectId);
if (index == kInvalidNode) {
return;
}
addTrack(node.translationTracks, nodeTarget(index, Channel::Translation));
addTrack(node.rotationTracks, nodeTarget(index, Channel::Rotation));
addTrack(node.scalingTracks, nodeTarget(index, Channel::Scale));
};
for (const mdx::Bone& item : source_.bones) {
visit(item.node);
}
for (const mdx::Helper& item : source_.helpers) {
visit(item.node);
}
for (const mdx::Light& item : source_.lights) {
visit(item.node);
}
for (const mdx::Attachment& item : source_.attachments) {
visit(item.node);
}
for (const mdx::ParticleEmitter& item : source_.particleEmitters) {
visit(item.node);
}
for (const mdx::ParticleEmitter2& item : source_.particleEmitters2) {
visit(item.node);
}
for (const mdx::CornEmitter& item : source_.cornEmitters) {
visit(item.node);
}
for (const mdx::RibbonEmitter& item : source_.ribbonEmitters) {
visit(item.node);
}
for (const mdx::EventObject& item : source_.eventObjects) {
visit(item.node);
}
for (const mdx::CollisionShape& item : source_.collisionShapes) {
visit(item.node);
}
}
void addKindTracks() {
for (const mdx::Light& light : source_.lights) {
const u32 node = nodeOf(light.node.objectId);
if (node == kInvalidNode) {
continue;
}
addTrack(SwapRedBlue(light.colorTracks), nodeTarget(node, Channel::Color));
addTrack(light.intensityTracks, nodeTarget(node, Channel::Intensity));
addTrack(light.attenuationStartTracks, nodeTarget(node, Channel::AttenuationStart));
addTrack(light.attenuationEndTracks, nodeTarget(node, Channel::AttenuationEnd));
addTrack(light.visibilityTracks, nodeTarget(node, Channel::Visibility));
addTrack(SwapRedBlue(light.ambientColorTracks), nodeTarget(node, Channel::Color, 1));
addTrack(light.ambientIntensityTracks, nodeTarget(node, Channel::Intensity, 1));
}
for (const mdx::Attachment& attachment : source_.attachments) {
const u32 node = nodeOf(attachment.node.objectId);
if (node != kInvalidNode) {
addTrack(attachment.visibilityTracks, nodeTarget(node, Channel::Visibility));
}
}
for (const mdx::ParticleEmitter& emitter : source_.particleEmitters) {
const u32 node = nodeOf(emitter.node.objectId);
if (node != kInvalidNode) {
addTrack(emitter.visibilityTracks, nodeTarget(node, Channel::Visibility));
}
}
for (const mdx::ParticleEmitter2& emitter : source_.particleEmitters2) {
const u32 node = nodeOf(emitter.node.objectId);
if (node != kInvalidNode) {
addTrack(emitter.visibilityTracks, nodeTarget(node, Channel::Visibility));
}
}
for (const mdx::RibbonEmitter& ribbon : source_.ribbonEmitters) {
const u32 node = nodeOf(ribbon.node.objectId);
if (node == kInvalidNode) {
continue;
}
addTrack(SwapRedBlue(ribbon.colorTracks), nodeTarget(node, Channel::Color));
addTrack(ribbon.alphaTracks, nodeTarget(node, Channel::Alpha));
addTrack(ribbon.textureSlotTracks, nodeTarget(node, Channel::TextureIndex));
addTrack(ribbon.visibilityTracks, nodeTarget(node, Channel::Visibility));
}
for (std::size_t c = 0; c < source_.cameras.size(); ++c) {
if (c >= context_.cameraNodes.size() || context_.cameraNodes[c] == kInvalidNode) {
continue;
}
addTrack(source_.cameras[c].positionTracks,
nodeTarget(context_.cameraNodes[c], Channel::Translation));
}
}
void addLayerTracks() {
for (const Context::ProfileLayers& profileLayers : context_.layerOrdinals) {
ProfileMaterialSet* set = model_.setFor(profileLayers.profile);
if (set == nullptr) {
continue;
}
for (std::size_t m = 0; m < source_.materials.size(); ++m) {
if (m >= profileLayers.byMaterial.size() || m >= set->slotBindings.size()) {
continue;
}
const u32 material = set->slotBindings[m].byLook.empty()
? kInvalidIndex
: set->slotBindings[m].byLook[0];
if (material >= set->materials.size()) {
continue;
}
addOneMaterial(source_.materials[m], profileLayers.byMaterial[m],
profileLayers.profile, static_cast<u32>(m),
set->materials[material]);
}
}
}
void addOneMaterial(const mdx::Material& source, const std::vector<u32>& ordinals,
ProfileId profile, u32 slot, Material& material) {
for (std::size_t l = 0; l < source.layers.size() && l < ordinals.size(); ++l) {
const u32 ordinal = ordinals[l];
if (ordinal == kInvalidIndex) {
continue;
}
const mdx::Layer& layer = source.layers[l];
TrackTarget target;
target.kind = TrackTarget::Kind::MaterialLayer;
target.material.profile = profile;
target.material.slot = slot;
target.material.look = 0;
target.sub = ordinal;
target.channel = Channel::Alpha;
addTrack(layer.alphaTracks, target);
target.channel = Channel::TextureIndex;
addTrack(layer.textureIdTracks, target);
target.channel = Channel::Emissive;
addTrack(layer.emissiveGainTracks, target);
addFresnelTracks(layer, profile, slot, ordinal, material);
addUvTracks(layer, profile, slot, ordinal, material);
}
}
void addFresnelTracks(const mdx::Layer& layer, ProfileId profile, u32 slot, u32 ordinal,
Material& material) {
const bool keyed = layer.fresnelColorTracks.isUsed || layer.fresnelAlphaTracks.isUsed ||
layer.fresnelTeamColorTracks.isUsed;
if (!keyed) {
return;
}
const u32 feature = FeatureIdFor(material, FeatureKind::Fresnel, ordinal);
if (feature == kInvalidIndex) {
out_.warn(DiagCode::AnimTrackDropped,
"a fresnel track has no feature on layer " + std::to_string(ordinal),
ElementRef(ElementKind::Slot, slot), profile);
return;
}
TrackTarget target;
target.kind = TrackTarget::Kind::MaterialFeature;
target.material.profile = profile;
target.material.slot = slot;
target.material.look = 0;
target.sub = feature;
target.channel = Channel::Color;
addTrack(layer.fresnelColorTracks, target);
target.channel = Channel::Alpha;
addTrack(layer.fresnelAlphaTracks, target);
target.channel = Channel::Weight;
addTrack(layer.fresnelTeamColorTracks, target);
}
void addUvTracks(const mdx::Layer& layer, ProfileId profile, u32 slot, u32 ordinal,
Material& material) {
if (layer.textureAnimationId >= source_.textureAnimations.size()) {
return; }
const mdx::TextureAnimation& animation =
source_.textureAnimations[layer.textureAnimationId];
if (!animation.translationTracks.isUsed && !animation.rotationTracks.isUsed &&
!animation.scalingTracks.isUsed) {
return;
}
const u32 feature = FeatureIdFor(material, FeatureKind::UvAnimation, ordinal);
TrackTarget target;
target.kind = TrackTarget::Kind::MaterialFeature;
target.material.profile = profile;
target.material.slot = slot;
target.material.look = 0;
target.sub = feature;
target.channel = Channel::UvTranslate;
addTrack(animation.translationTracks, target);
target.channel = Channel::UvRotate;
addTrack(animation.rotationTracks, target);
target.channel = Channel::UvScale;
addTrack(animation.scalingTracks, target);
}
void addGeosetAnimationTracks() {
for (const mdx::GeosetAnimation& animation : source_.geosetAnimations) {
if (animation.geosetId >= model_.meshes.size() ||
model_.meshes[animation.geosetId].sections.empty()) {
out_.warn(DiagCode::AnimTrackDropped,
"a geoset animation names geoset " + std::to_string(animation.geosetId) +
", which the model does not have",
ElementRef(ElementKind::Mesh, animation.geosetId));
continue;
}
TrackTarget target;
target.kind = TrackTarget::Kind::Section;
target.mesh = animation.geosetId;
target.sub = 0;
target.channel = Channel::Alpha;
addTrack(animation.alphaTracks, target);
target.channel = Channel::Color;
addTrack(SwapRedBlue(animation.colorTracks), target);
}
}
void addEvents() {
for (const mdx::EventObject& event : source_.eventObjects) {
const u32 node = nodeOf(event.node.objectId);
if (node == kInvalidNode) {
continue;
}
if (event.globalSequenceId != kNoGlobalSequence) {
const u32 clip = globalClipFor(event.globalSequenceId);
if (clip == kInvalidIndex) {
continue;
}
for (u32 time : event.eventTrackTimes) {
clips_[clip].events.push_back(
ClipEvent{Seconds(static_cast<f32>(time)), node, event.node.name, 0});
}
continue;
}
for (u32 time : event.eventTrackTimes) {
for (const Window& window : windows_) {
if (static_cast<f32>(time) < window.start ||
static_cast<f32>(time) > window.end) {
continue;
}
clips_[window.clip].events.push_back(ClipEvent{
Seconds(static_cast<f32>(time) - window.start), node, event.node.name, 0});
}
}
}
}
void reserveEmitterClips() {
const auto reserve = [this](const auto& track) {
if (track.isUsed && !track.timestamps.empty() &&
track.globalSequenceId != kNoGlobalSequence &&
track.globalSequenceId < source_.globalSequences.size()) {
globalClipFor(track.globalSequenceId);
}
};
for (const mdx::ParticleEmitter& emitter : source_.particleEmitters) {
reserve(emitter.emissionRateTracks);
reserve(emitter.gravityTracks);
reserve(emitter.longitudeTracks);
reserve(emitter.latitudeTracks);
reserve(emitter.lifespanTracks);
reserve(emitter.speedTracks);
}
for (const mdx::ParticleEmitter2& emitter : source_.particleEmitters2) {
reserve(emitter.speedTracks);
reserve(emitter.variationTracks);
reserve(emitter.latitudeTracks);
reserve(emitter.gravityTracks);
reserve(emitter.emissionRateTracks);
reserve(emitter.lengthTracks);
reserve(emitter.widthTracks);
}
for (const mdx::RibbonEmitter& ribbon : source_.ribbonEmitters) {
reserve(ribbon.heightAboveTracks);
reserve(ribbon.heightBelowTracks);
}
}
void commit() {
for (Clip& clip : clips_) {
document_.clips.push_back(std::move(clip));
}
}
const mdx::Model& source_;
const Context& context_;
Document& document_;
Model& model_;
u32 modelIndex_;
Diagnostics& out_;
std::vector<Clip> clips_;
std::vector<Window> windows_;
std::vector<std::pair<u32, u32>> globalClips_;
};
}
void Import(const mdx::Model& source, const Context& context, Document& document, u32 model,
Diagnostics& out) {
if (model >= document.models.size()) {
return;
}
Builder(source, context, document, model, out).run();
}
namespace {
constexpr f32 kMilliseconds = 1000.0f;
u32 Milliseconds(f32 seconds) {
const f32 ms = seconds * kMilliseconds;
return ms <= 0.0f ? 0u : static_cast<u32>(ms + 0.5f);
}
mdx::InterpolationType MdxInterp(Interpolation interp) {
switch (interp) {
case Interpolation::Step:
return mdx::InterpolationType::None;
case Interpolation::Hermite:
return mdx::InterpolationType::Hermite;
case Interpolation::Bezier:
return mdx::InterpolationType::Bezier;
case Interpolation::Linear:
case Interpolation::Slerp:
case Interpolation::Count:
break;
}
return mdx::InterpolationType::Linear;
}
struct MergedTrack {
bool used = false;
Interpolation interp = Interpolation::Linear;
u32 globalSequenceId = mdx::Track<f32>::kNoGlobalSequence;
std::vector<u32> times; std::vector<const u8*> keys; u32 valuesPerKey = 1;
std::size_t valueSize = 0;
void add(u32 time, const u8* key) {
pending.emplace_back(time, key);
}
void finish() {
std::stable_sort(pending.begin(), pending.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
times.clear();
keys.clear();
for (const auto& [time, key] : pending) {
if (!times.empty() && times.back() == time) {
continue;
}
times.push_back(time);
keys.push_back(key);
}
used = !times.empty();
}
const u8* own(std::vector<u8> value) {
value.resize(valuesPerKey * valueSize, 0);
synthesized.push_back(std::move(value));
return synthesized.back().data();
}
std::vector<std::pair<u32, const u8*>> pending;
std::deque<std::vector<u8>> synthesized;
};
std::vector<u8> SampleValue(const SubTrack& track, geom::AttrType type, f32 time) {
const std::size_t size = geom::AttrTypeSize(type);
const std::size_t stride = ValuesPerKey(track.interp) * size;
const auto at = [&](std::size_t key) { return track.values.data() + key * stride; };
std::size_t after = 0;
while (after < track.times.size() && track.times[after] <= time) {
++after;
}
if (after == 0) {
return std::vector<u8>(at(0), at(0) + size);
}
const std::size_t before = after - 1;
if (after >= track.times.size() || track.interp == Interpolation::Step) {
return std::vector<u8>(at(before), at(before) + size);
}
const f32 span = track.times[after] - track.times[before];
const f32 alpha = span > 0.0f ? (time - track.times[before]) / span : 0.0f;
std::vector<u8> out(at(before), at(before) + size);
switch (type) {
case geom::AttrType::F32:
case geom::AttrType::F32x2:
case geom::AttrType::F32x3:
case geom::AttrType::F32x4: {
for (std::size_t i = 0; i < size / sizeof(f32); ++i) {
f32 a = 0, b = 0;
std::memcpy(&a, at(before) + i * sizeof(f32), sizeof(f32));
std::memcpy(&b, at(after) + i * sizeof(f32), sizeof(f32));
const f32 v = a + (b - a) * alpha;
std::memcpy(out.data() + i * sizeof(f32), &v, sizeof(f32));
}
break;
}
case geom::AttrType::Quat: {
f32 a[4], b[4], v[4];
std::memcpy(a, at(before), sizeof(a));
std::memcpy(b, at(after), sizeof(b));
const f32 sign = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3] < 0.0f ? -1.0f : 1.0f;
f32 length = 0.0f;
for (int i = 0; i < 4; ++i) {
v[i] = a[i] + (sign * b[i] - a[i]) * alpha;
length += v[i] * v[i];
}
length = std::sqrt(length);
for (int i = 0; i < 4; ++i) {
v[i] = length > 0.0f ? v[i] / length : v[i];
}
std::memcpy(out.data(), v, sizeof(v));
break;
}
default:
break;
}
return out;
}
bool IsM3UvSpelling(const AnimChannel& channel) {
if (channel.target.channel == Channel::UvRotate) {
return channel.valueType != geom::AttrType::Quat;
}
return channel.valueType != geom::AttrType::F32x3;
}
MergedTrack MdxUvTrack(const MergedTrack& source, const AnimChannel& channel) {
MergedTrack out;
out.used = source.used;
out.interp = ValuesPerKey(source.interp) > 1 ? Interpolation::Linear : source.interp;
out.globalSequenceId = source.globalSequenceId;
out.times = source.times;
out.valuesPerKey = 1;
out.valueSize = channel.target.channel == Channel::UvRotate ? sizeof(Quaternion)
: sizeof(Vector3f);
const std::size_t comps =
std::min<std::size_t>(geom::AttrTypeSize(channel.valueType) / sizeof(f32), 4);
for (const u8* key : source.keys) {
f32 in[4] = {0.0f, 0.0f, 0.0f, 0.0f};
std::memcpy(in, key, comps * sizeof(f32));
f32 written[4] = {0.0f, 0.0f, 0.0f, 0.0f};
switch (channel.target.channel) {
case Channel::UvTranslate:
written[0] = -in[0];
written[1] = -in[1];
break;
case Channel::UvRotate: {
const f32 half = in[2] * 0.5f;
written[2] = std::sin(half);
written[3] = std::cos(half);
break;
}
default:
written[0] = in[0];
written[1] = in[1];
written[2] = 1.0f;
break;
}
std::vector<u8> value(out.valueSize, 0);
std::memcpy(value.data(), written, out.valueSize);
out.keys.push_back(out.own(std::move(value)));
}
return out;
}
template <class T>
void Emit(const MergedTrack& merged, mdx::Track<T>& dst) {
if (!merged.used || merged.times.empty()) {
return;
}
dst.isUsed = true;
dst.interpolationType = MdxInterp(merged.interp);
dst.globalSequenceId = merged.globalSequenceId;
dst.timestamps = merged.times;
dst.keyCount = merged.times.size();
dst.keys_data.clear();
dst.keys_data.reserve(merged.times.size() * merged.valuesPerKey);
for (const u8* key : merged.keys) {
for (u32 v = 0; v < merged.valuesPerKey; ++v) {
T value{};
std::memcpy(&value, key + v * sizeof(T), sizeof(T));
dst.keys_data.push_back(value);
}
}
}
class Exporter {
public:
Exporter(const Document& document, u32 modelIndex, ProfileId profile,
const ExportContext& context, mdx::Model& out, Diagnostics& diagnostics)
: document_(document), model_(document.models[modelIndex]), modelIndex_(modelIndex),
profile_(profile), context_(context), out_(out), diagnostics_(diagnostics) {}
void run() {
buildWindows();
buildVisibilityGates();
for (const AnimChannel& channel : model_.animChannels.channels) {
emitChannel(channel);
}
emitStandingVisibilityGates();
emitStandingUvTransforms();
emitEvents();
}
private:
struct Window {
u32 clip = kInvalidIndex;
u32 start = 0; u32 end = 0; u32 globalSequenceId = mdx::Track<f32>::kNoGlobalSequence;
};
static bool IsGlobalClip(const Clip& clip) {
return hasFlag(clip.flags, ClipFlags::AutoPlay) &&
hasFlag(clip.flags, ClipFlags::WorldClocked);
}
void buildWindows() {
u32 nextFree = 0;
for (std::size_t c = 0; c < document_.clips.size(); ++c) {
const Clip& clip = document_.clips[c];
if (clip.model != modelIndex_) {
continue;
}
Window window;
window.clip = static_cast<u32>(c);
if (IsGlobalClip(clip)) {
const i64 stored = clip.native.value("globalSequenceId", -1);
const u32 id = stored >= 0 ? static_cast<u32>(stored)
: static_cast<u32>(out_.globalSequences.size());
if (out_.globalSequences.size() <= id) {
out_.globalSequences.resize(id + 1, 0);
}
out_.globalSequences[id] = Milliseconds(clip.duration);
window.globalSequenceId = id;
windows_.push_back(window);
continue;
}
mdx::Sequence sequence;
sequence.name = clip.name;
const i64 start = clip.native.value("intervalStart", -1);
const i64 end = clip.native.value("intervalEnd", -1);
if (start >= 0 && end >= start) {
sequence.intervalStart = static_cast<u32>(start);
sequence.intervalEnd = static_cast<u32>(end);
} else {
sequence.intervalStart = nextFree;
sequence.intervalEnd = nextFree + Milliseconds(clip.duration);
diagnostics_.info(DiagCode::AnimClipRetimed,
"clip '" + clip.name + "' had no MDX interval; placed at " +
std::to_string(sequence.intervalStart) + "ms",
ElementRef(ElementKind::Clip, static_cast<u32>(c)), profile_);
}
if (!clip.looping) {
sequence.flags = mdx::Sequence::Flag::NonLooping;
}
sequence.moveSpeed = ClipMoveSpeed(clip);
sequence.rarity = ClipRarity(clip);
sequence.syncPoint = static_cast<u32>(clip.native.value("syncPoint", 0));
const Extent& extent = HasExtent(clip.bounds) ? clip.bounds : model_.bounds;
sequence.extent.minimum = extent.minimum;
sequence.extent.maximum = extent.maximum;
sequence.extent.boundsRadius = extent.sphereRadius;
nextFree = std::max(nextFree, sequence.intervalEnd + 1000u);
window.start = sequence.intervalStart;
window.end = sequence.intervalEnd;
windows_.push_back(window);
out_.sequences.push_back(std::move(sequence));
}
}
MergedTrack gather(const AnimChannel& channel) const {
MergedTrack merged;
merged.valueSize = geom::AttrTypeSize(channel.valueType);
struct Part {
const Window* window;
const Clip* clip;
const SubTrack* track;
};
std::vector<Part> parts;
for (const Window& window : windows_) {
const Clip& clip = document_.clips[window.clip];
for (const SubTrackContainer& container : clip.containers) {
const SubTrack* track = container.find(channel.id);
if (track == nullptr || track->times.empty()) {
continue;
}
if (!track->wellSized(channel.valueType)) {
diagnostics_.warn(DiagCode::AnimTrackDropped,
"a sub-track of clip '" + clip.name +
"' is not sized for its channel",
ElementRef(ElementKind::Track, channel.id), profile_);
continue;
}
parts.push_back(Part{&window, &clip, track});
}
}
if (parts.empty()) {
return merged;
}
merged.interp = parts.front().track->interp;
for (const Part& part : parts) {
if (part.track->interp != Interpolation::Step) {
merged.interp = part.track->interp;
break;
}
}
for (const Part& part : parts) {
if (ValuesPerKey(part.track->interp) < ValuesPerKey(merged.interp)) {
merged.interp = Interpolation::Linear;
break;
}
}
merged.valuesPerKey = ValuesPerKey(merged.interp);
merged.globalSequenceId = parts.front().window->globalSequenceId;
for (const Part& part : parts) {
if (MdxInterp(part.track->interp) != MdxInterp(merged.interp)) {
diagnostics_.warn(DiagCode::AnimTrackApproximated,
"clip '" + part.clip->name +
"' interpolates a shared channel as " +
ToString(part.track->interp) + "; written as " +
ToString(merged.interp),
ElementRef(ElementKind::Track, channel.id), profile_);
}
}
for (const Part& part : parts) {
const Window& window = *part.window;
const Clip& clip = *part.clip;
const SubTrack& track = *part.track;
const u32 stride = ValuesPerKey(track.interp) * static_cast<u32>(merged.valueSize);
const bool global = window.globalSequenceId != kNoGlobalSequence;
const u32 start = global ? 0u : window.start;
const u32 end = global ? Milliseconds(clip.duration) : window.end;
const auto at = [&](std::size_t k) { return track.values.data() + k * stride; };
if (clip.native.value("intervalStart", -1) >= 0 ||
clip.native.value("globalSequenceId", -1) >= 0) {
for (std::size_t k = 0; k < track.times.size(); ++k) {
const f32 absolute =
track.times[k] * kMillisecondsPerSecond + static_cast<f32>(start);
merged.add(absolute <= 0.0f ? 0u : static_cast<u32>(absolute + 0.5f), at(k));
}
continue;
}
bool keyedStart = false;
bool keyedEnd = false;
for (std::size_t k = 0; k < track.times.size(); ++k) {
const f32 t = track.times[k];
if (t < -1e-4f || t > clip.duration + 1e-4f) {
continue;
}
const u32 time = std::clamp(
start + static_cast<u32>(std::max(t, 0.0f) * kMillisecondsPerSecond + 0.5f),
start, end);
keyedStart = keyedStart || time == start;
keyedEnd = keyedEnd || time == end;
merged.add(time, at(k));
}
if (!keyedStart) {
merged.add(start, merged.own(SampleValue(track, channel.valueType, 0.0f)));
}
if (!keyedEnd) {
merged.add(end, merged.own(SampleValue(track, channel.valueType, clip.duration)));
}
}
merged.finish();
return merged;
}
void emitChannel(const AnimChannel& channel) {
MergedTrack merged = gather(channel);
if (!merged.used) {
return;
}
switch (channel.target.kind) {
case TrackTarget::Kind::Node:
emitNodeChannel(channel, merged);
break;
case TrackTarget::Kind::MaterialLayer:
emitLayerChannel(channel, merged);
break;
case TrackTarget::Kind::MaterialFeature:
emitFeatureChannel(channel, merged);
break;
case TrackTarget::Kind::Section:
emitSectionChannel(channel, merged);
break;
case TrackTarget::Kind::Count:
break;
}
}
mdx::Node* nodeRecord(u32 wemNode) {
if (wemNode >= context_.nodeSlots.size()) {
return nullptr;
}
const ExportContext::NodeSlot& slot = context_.nodeSlots[wemNode];
const u32 i = slot.index;
switch (slot.slot) {
case ExportContext::Slot::Bone:
return i < out_.bones.size() ? &out_.bones[i].node : nullptr;
case ExportContext::Slot::Helper:
return i < out_.helpers.size() ? &out_.helpers[i].node : nullptr;
case ExportContext::Slot::Light:
return i < out_.lights.size() ? &out_.lights[i].node : nullptr;
case ExportContext::Slot::Attachment:
return i < out_.attachments.size() ? &out_.attachments[i].node : nullptr;
case ExportContext::Slot::ParticleEmitter:
return i < out_.particleEmitters.size() ? &out_.particleEmitters[i].node : nullptr;
case ExportContext::Slot::ParticleEmitter2:
return i < out_.particleEmitters2.size() ? &out_.particleEmitters2[i].node : nullptr;
case ExportContext::Slot::RibbonEmitter:
return i < out_.ribbonEmitters.size() ? &out_.ribbonEmitters[i].node : nullptr;
case ExportContext::Slot::CornEmitter:
return i < out_.cornEmitters.size() ? &out_.cornEmitters[i].node : nullptr;
case ExportContext::Slot::EventObject:
return i < out_.eventObjects.size() ? &out_.eventObjects[i].node : nullptr;
case ExportContext::Slot::CollisionShape:
return i < out_.collisionShapes.size() ? &out_.collisionShapes[i].node : nullptr;
case ExportContext::Slot::Camera:
case ExportContext::Slot::None:
break;
}
return nullptr;
}
void emitNodeChannel(const AnimChannel& channel, const MergedTrack& merged) {
const u32 wemNode = channel.target.node;
if (wemNode >= context_.nodeSlots.size()) {
return;
}
const ExportContext::NodeSlot& slot = context_.nodeSlots[wemNode];
if (slot.slot == ExportContext::Slot::Camera) {
if (slot.index < out_.cameras.size() &&
channel.target.channel == Channel::Translation) {
Emit(merged, out_.cameras[slot.index].positionTracks);
}
return;
}
if (channel.target.channel == Channel::Visibility) {
if (VisibilityGate* gate = gateFor(wemNode)) {
emitGate(*gate, channel, merged);
return;
}
}
if (mdx::Node* node = nodeRecord(wemNode)) {
switch (channel.target.channel) {
case Channel::Translation:
Emit(merged, node->translationTracks);
return;
case Channel::Rotation:
Emit(merged, node->rotationTracks);
return;
case Channel::Scale:
Emit(merged, node->scalingTracks);
return;
default:
break;
}
}
switch (slot.slot) {
case ExportContext::Slot::Light: {
if (slot.index >= out_.lights.size()) {
return;
}
mdx::Light& light = out_.lights[slot.index];
const bool ambient = channel.target.sub == 1;
switch (channel.target.channel) {
case Channel::Color: {
mdx::Track<Vector3f>& color = ambient ? light.ambientColorTracks : light.colorTracks;
Emit(merged, color);
color = SwapRedBlue(std::move(color));
return;
}
case Channel::Intensity:
Emit(merged, ambient ? light.ambientIntensityTracks : light.intensityTracks);
return;
case Channel::AttenuationStart:
Emit(merged, light.attenuationStartTracks);
return;
case Channel::AttenuationEnd:
Emit(merged, light.attenuationEndTracks);
return;
case Channel::Visibility:
Emit(merged, light.visibilityTracks);
return;
default:
break;
}
break;
}
case ExportContext::Slot::Attachment:
if (channel.target.channel == Channel::Visibility &&
slot.index < out_.attachments.size()) {
Emit(merged, out_.attachments[slot.index].visibilityTracks);
return;
}
break;
case ExportContext::Slot::ParticleEmitter:
if (channel.target.channel == Channel::Visibility &&
slot.index < out_.particleEmitters.size()) {
Emit(merged, out_.particleEmitters[slot.index].visibilityTracks);
return;
}
break;
case ExportContext::Slot::ParticleEmitter2:
if (channel.target.channel == Channel::Visibility &&
slot.index < out_.particleEmitters2.size()) {
Emit(merged, out_.particleEmitters2[slot.index].visibilityTracks);
return;
}
break;
case ExportContext::Slot::RibbonEmitter: {
if (slot.index >= out_.ribbonEmitters.size()) {
return;
}
mdx::RibbonEmitter& ribbon = out_.ribbonEmitters[slot.index];
switch (channel.target.channel) {
case Channel::Color:
Emit(merged, ribbon.colorTracks);
ribbon.colorTracks = SwapRedBlue(std::move(ribbon.colorTracks));
return;
case Channel::Alpha:
Emit(merged, ribbon.alphaTracks);
return;
case Channel::TextureIndex:
Emit(merged, ribbon.textureSlotTracks);
return;
case Channel::Visibility:
Emit(merged, ribbon.visibilityTracks);
return;
default:
break;
}
break;
}
default:
break;
}
diagnostics_.warn(DiagCode::AnimTrackDropped,
std::string("no MDX record animates ") +
ToString(channel.target.channel) + " on this node",
ElementRef(ElementKind::Node, wemNode), profile_);
}
mdx::Layer* layerRecord(const MaterialChannelRef& ref, u32 ordinal) {
if (ref.profile != profile_ || ref.slot >= out_.materials.size()) {
return nullptr;
}
mdx::Material& material = out_.materials[ref.slot];
u32 layer = ordinal;
if (ref.slot < context_.layerOfOrdinal.size()) {
const std::vector<u32>& map = context_.layerOfOrdinal[ref.slot];
if (ordinal < map.size()) {
layer = map[ordinal];
}
}
return layer < material.layers.size() ? &material.layers[layer] : nullptr;
}
void emitWholeMaterialChannel(const AnimChannel& channel, const MergedTrack& merged) {
const MaterialChannelRef& ref = channel.target.material;
if (ref.profile != profile_) {
return;
}
if (channel.target.channel != Channel::Alpha && channel.target.channel != Channel::Color) {
diagnostics_.warn(DiagCode::AnimTrackDropped,
std::string("a whole-material track is ") +
ToString(channel.target.channel) +
", and a geoset animation carries only colour and alpha",
ElementRef(ElementKind::Slot, ref.slot), profile_);
return;
}
bool drawn = false;
for (std::size_t g = 0; g < out_.geosets.size(); ++g) {
if (out_.geosets[g].materialId != ref.slot) {
continue;
}
drawn = true;
mdx::GeosetAnimation& animation = geosetAnimationFor(static_cast<u32>(g));
if (channel.target.channel == Channel::Alpha) {
Emit(merged, animation.alphaTracks);
} else {
Emit(merged, animation.colorTracks);
animation.colorTracks = SwapRedBlue(std::move(animation.colorTracks));
}
}
if (!drawn) {
diagnostics_.warn(DiagCode::AnimTrackDropped,
"no geoset draws this material slot, so its whole-material " +
std::string(ToString(channel.target.channel)) +
" track has nowhere to go",
ElementRef(ElementKind::Slot, ref.slot), profile_);
}
}
void emitLayerChannel(const AnimChannel& channel, const MergedTrack& merged) {
if (channel.target.sub == kWholeMaterial) {
emitWholeMaterialChannel(channel, merged);
return;
}
mdx::Layer* layer = layerRecord(channel.target.material, channel.target.sub);
if (layer == nullptr) {
if (channel.target.material.profile == profile_) {
diagnostics_.warn(
DiagCode::AnimTrackDropped,
"a layer track names ordinal " + std::to_string(channel.target.sub) +
", which this material "
"did not write",
ElementRef(ElementKind::Slot, channel.target.material.slot), profile_);
}
return;
}
switch (channel.target.channel) {
case Channel::Alpha:
Emit(merged, layer->alphaTracks);
return;
case Channel::TextureIndex:
Emit(merged, layer->textureIdTracks);
return;
case Channel::Emissive:
Emit(merged, layer->emissiveGainTracks);
return;
default:
break;
}
diagnostics_.warn(DiagCode::AnimTrackDropped,
std::string("an MDX layer has no ") + ToString(channel.target.channel) +
" track",
ElementRef(ElementKind::Slot, channel.target.material.slot), profile_);
}
void emitFeatureChannel(const AnimChannel& channel, const MergedTrack& merged) {
const MaterialChannelRef& ref = channel.target.material;
if (ref.profile != profile_) {
return;
}
const Material* material = Resolve(model_, ref.slot, ref.profile, ref.look);
if (material == nullptr) {
return;
}
const MaterialFeature* feature = nullptr;
for (const MaterialFeature& candidate : material->Common().features) {
if (candidate.id == channel.target.sub) {
feature = &candidate;
break;
}
}
if (feature == nullptr) {
diagnostics_.warn(DiagCode::AnimTrackDropped,
"a feature track names feature " +
std::to_string(channel.target.sub) +
", which the material "
"does not carry",
ElementRef(ElementKind::Slot, ref.slot), profile_);
return;
}
mdx::Layer* layer = layerRecord(ref, feature->layer);
if (layer == nullptr) {
return;
}
if (feature->kind() == FeatureKind::Fresnel) {
switch (channel.target.channel) {
case Channel::Color:
Emit(merged, layer->fresnelColorTracks);
return;
case Channel::Alpha:
Emit(merged, layer->fresnelAlphaTracks);
return;
case Channel::Weight:
Emit(merged, layer->fresnelTeamColorTracks);
return;
default:
break;
}
return;
}
if (feature->kind() != FeatureKind::UvAnimation) {
return;
}
if (layer->textureAnimationId >= out_.textureAnimations.size()) {
layer->textureAnimationId = static_cast<u32>(out_.textureAnimations.size());
out_.textureAnimations.emplace_back();
}
mdx::TextureAnimation& animation = out_.textureAnimations[layer->textureAnimationId];
const bool restate = IsM3UvSpelling(channel);
const MergedTrack converted = restate ? MdxUvTrack(merged, channel) : MergedTrack{};
const MergedTrack& uv = restate ? converted : merged;
switch (channel.target.channel) {
case Channel::UvTranslate:
Emit(uv, animation.translationTracks);
return;
case Channel::UvRotate:
Emit(uv, animation.rotationTracks);
return;
case Channel::UvScale:
Emit(uv, animation.scalingTracks);
return;
default:
break;
}
}
struct VisibilityGate {
u32 node = kInvalidNode;
std::vector<u32> geosets;
bool driven = false; };
VisibilityGate* gateFor(u32 node) {
for (VisibilityGate& gate : gates_) {
if (gate.node == node) {
return &gate;
}
}
return nullptr;
}
void buildVisibilityGates() {
for (std::size_t m = 0; m < model_.meshes.size() && m < context_.geosetsOfMesh.size();
++m) {
if (m >= context_.sectionOfGeoset.size()) {
break;
}
const Mesh& mesh = model_.meshes[m];
const std::vector<u32>& geosets = context_.geosetsOfMesh[m];
const std::vector<u32>& sections = context_.sectionOfGeoset[m];
for (std::size_t g = 0; g < geosets.size() && g < sections.size(); ++g) {
if (sections[g] >= mesh.sections.size()) {
continue;
}
if (hasFlag(mesh.sections[sections[g]].flags, SectionFlags::Hidden)) {
continue;
}
const i64 node =
mesh.sections[sections[g]].native.value(kSectionVisibilityNode, -1);
if (node < 0 || node == kSectionAlwaysDrawn ||
node >= static_cast<i64>(model_.nodes.size())) {
continue;
}
VisibilityGate* gate = gateFor(static_cast<u32>(node));
if (gate == nullptr) {
VisibilityGate created;
created.node = static_cast<u32>(node);
gates_.push_back(std::move(created));
gate = &gates_.back();
}
gate->geosets.push_back(geosets[g]);
}
}
}
const AnimChannel* visibilityChannelOf(u32 node) const {
for (const AnimChannel& channel : model_.animChannels.channels) {
if (channel.target.kind == TrackTarget::Kind::Node && channel.target.node == node &&
channel.target.channel == Channel::Visibility) {
return &channel;
}
}
return nullptr;
}
void emitGate(VisibilityGate& gate, const AnimChannel& channel, const MergedTrack& merged) {
gate.driven = true;
f32 rest = 1.0f;
if (channel.hasInitValue()) {
std::memcpy(&rest, channel.initValue.data(), sizeof(f32));
}
u8 restBytes[sizeof(f32)];
std::memcpy(restBytes, &rest, sizeof(f32));
MergedTrack stepped = merged;
stepped.interp = Interpolation::Step;
if (merged.globalSequenceId == kNoGlobalSequence) {
for (const Window& window : windows_) {
if (window.globalSequenceId != kNoGlobalSequence) {
continue;
}
const bool keyed =
std::any_of(merged.times.begin(), merged.times.end(),
[&](u32 t) { return t >= window.start && t <= window.end; });
if (!keyed) {
stepped.add(window.start, restBytes);
}
}
stepped.finish();
}
for (const u32 geoset : gate.geosets) {
if (geoset < out_.geosets.size()) {
Emit(stepped, geosetAnimationFor(geoset).alphaTracks);
}
}
}
void emitStandingVisibilityGates() {
for (const VisibilityGate& gate : gates_) {
if (gate.driven) {
continue;
}
const AnimChannel* channel = visibilityChannelOf(gate.node);
if (channel == nullptr || !channel->hasInitValue()) {
continue;
}
f32 rest = 1.0f;
std::memcpy(&rest, channel->initValue.data(), sizeof(f32));
if (rest != 0.0f) {
continue;
}
for (const u32 geoset : gate.geosets) {
if (geoset < out_.geosets.size()) {
geosetAnimationFor(geoset).alpha = 0.0f;
}
}
}
}
mdx::GeosetAnimation& geosetAnimationFor(u32 geoset) {
for (mdx::GeosetAnimation& existing : out_.geosetAnimations) {
if (existing.geosetId == geoset) {
return existing;
}
}
mdx::GeosetAnimation created;
created.geosetId = geoset;
created.flags = mdx::GeosetAnimation::Flag::Color;
out_.geosetAnimations.push_back(std::move(created));
return out_.geosetAnimations.back();
}
void emitSectionChannel(const AnimChannel& channel, const MergedTrack& merged) {
if (channel.target.mesh >= context_.geosetsOfMesh.size()) {
return;
}
for (const u32 geoset : context_.geosetsOfMesh[channel.target.mesh]) {
if (geoset >= out_.geosets.size()) {
continue;
}
mdx::GeosetAnimation& animation = geosetAnimationFor(geoset);
switch (channel.target.channel) {
case Channel::Alpha:
Emit(merged, animation.alphaTracks);
break;
case Channel::Color:
Emit(merged, animation.colorTracks);
animation.colorTracks = SwapRedBlue(std::move(animation.colorTracks));
break;
default:
break;
}
}
}
struct UvState {
Vector2f scale{1, 1};
f32 angle = 0; Vector2f column{0, 0}; };
static std::optional<UvState> standingUv(const Matrix3x2f& matrix) {
UvState state;
state.angle = std::atan2(matrix.m[1][0], matrix.m[0][0]);
const f32 c = std::cos(state.angle);
const f32 s = std::sin(state.angle);
state.scale = Vector2f{matrix.m[0][0] * c + matrix.m[1][0] * s,
matrix.m[1][1] * c - matrix.m[0][1] * s};
if (std::fabs(matrix.m[0][1] + state.scale.y * s) > 1e-3f ||
std::fabs(matrix.m[1][1] - state.scale.y * c) > 1e-3f) {
return std::nullopt;
}
state.column = Vector2f{matrix.m[0][2], matrix.m[1][2]};
return state;
}
static const UvAnimationFeature* constantRateUv(const CommonMaterial& common, u32 ordinal) {
for (const MaterialFeature& feature : common.features) {
if (feature.kind() != FeatureKind::UvAnimation || feature.layer != ordinal) {
continue;
}
const auto* body = std::get_if<UvAnimationFeature>(&feature.payload);
if (body != nullptr && body->isConstantRate()) {
return body;
}
}
return nullptr;
}
u32 globalSequenceOf(u32 milliseconds) {
for (std::size_t i = 0; i < out_.globalSequences.size(); ++i) {
if (out_.globalSequences[i] == milliseconds) {
return static_cast<u32>(i);
}
}
out_.globalSequences.push_back(milliseconds);
return static_cast<u32>(out_.globalSequences.size() - 1);
}
static Vector3f translationFor(const UvState& state, const Vector2f& column) {
const f32 c = std::cos(state.angle);
const f32 s = std::sin(state.angle);
const f32 vx = column.x - 0.5f;
const f32 vy = column.y - 0.5f;
const f32 wx = c * vx + s * vy;
const f32 wy = -s * vx + c * vy;
return Vector3f{state.scale.x != 0.0f ? wx / state.scale.x + 0.5f : column.x,
state.scale.y != 0.0f ? wy / state.scale.y + 0.5f : column.y, 0.0f};
}
static f32 seamlessPeriod(const Vector2f& rate) {
f32 period = 0.0f;
for (const f32 axis : {rate.x, rate.y}) {
if (axis != 0.0f) {
period = std::max(period, 1.0f / std::abs(axis));
}
}
return period;
}
void emitStandingUvTransforms() {
for (u32 slot = 0; slot < static_cast<u32>(model_.materialSlots.size()); ++slot) {
const Material* material = Resolve(model_, slot, profile_, 0);
if (material == nullptr) {
continue;
}
MaterialChannelRef ref;
ref.profile = profile_;
ref.slot = slot;
ref.look = 0;
const CommonMaterial& common = material->Common();
for (u32 ordinal = 0; ordinal < common.ordinalCount(); ++ordinal) {
mdx::Layer* layer = layerRecord(ref, ordinal);
if (layer == nullptr || layer->textureAnimationId < out_.textureAnimations.size()) {
continue;
}
const TextureInput* input = common.inputAt(ordinal);
const UvAnimationFeature* rate = constantRateUv(common, ordinal);
if (input == nullptr || (input->uvTransform.isIdentity() && rate == nullptr)) {
continue;
}
const std::optional<UvState> standing = standingUv(input->uvTransform);
if (!standing.has_value()) {
diagnostics_.warn(DiagCode::AnimTrackDropped,
"a UV transform with shear has no MDX spelling",
ElementRef(ElementKind::Layer, slot, ordinal), profile_);
continue;
}
emitOneUvAnimation(*layer, *standing, rate, slot, ordinal);
}
}
}
void emitOneUvAnimation(mdx::Layer& layer, const UvState& standing,
const UvAnimationFeature* rate, u32 slot, u32 ordinal) {
mdx::TextureAnimation animation;
const Vector3f start = translationFor(standing, standing.column);
constexpr f32 kRest = 1e-6f;
if (std::fabs(standing.scale.x - 1.0f) > kRest ||
std::fabs(standing.scale.y - 1.0f) > kRest) {
animation.scalingTracks.isUsed = true;
animation.scalingTracks.interpolationType = mdx::InterpolationType::None;
animation.scalingTracks.timestamps = {0};
animation.scalingTracks.keys_data = {
Vector3f{standing.scale.x, standing.scale.y, 1.0f}};
animation.scalingTracks.keyCount = 1;
}
if (std::fabs(standing.angle) > kRest) {
const f32 half = standing.angle * 0.5f;
animation.rotationTracks.isUsed = true;
animation.rotationTracks.interpolationType = mdx::InterpolationType::None;
animation.rotationTracks.timestamps = {0};
animation.rotationTracks.keys_data = {
Quaternion{0.0f, 0.0f, std::sin(half), std::cos(half)}};
animation.rotationTracks.keyCount = 1;
}
u32 globalSequence = kNoGlobalSequence;
if (rate != nullptr) {
if (rate->scaleRate.x != 0.0f || rate->scaleRate.y != 0.0f) {
diagnostics_.warn(DiagCode::AnimTrackDropped,
"a UV scale RATE has no MDX spelling; only the standing scale "
"was written",
ElementRef(ElementKind::Layer, slot, ordinal), profile_);
}
const f32 period = seamlessPeriod(rate->scrollRate);
const u32 milliseconds = Milliseconds(period);
if (milliseconds > 0) {
globalSequence = globalSequenceOf(milliseconds);
const Vector2f travelled{std::round(rate->scrollRate.x * period),
std::round(rate->scrollRate.y * period)};
const Vector2f end{standing.column.x + travelled.x,
standing.column.y + travelled.y};
animation.translationTracks.isUsed = true;
animation.translationTracks.interpolationType = mdx::InterpolationType::Linear;
animation.translationTracks.globalSequenceId = globalSequence;
animation.translationTracks.timestamps = {0, milliseconds};
animation.translationTracks.keys_data = {start,
translationFor(standing, end)};
animation.translationTracks.keyCount = 2;
}
if (rate->rotateRate != 0.0f) {
emitUvRotation(animation, rate->rotateRate, standing.angle, globalSequence);
}
}
if (!animation.translationTracks.isUsed &&
(std::fabs(start.x) > kRest || std::fabs(start.y) > kRest)) {
animation.translationTracks.isUsed = true;
animation.translationTracks.interpolationType = mdx::InterpolationType::None;
animation.translationTracks.timestamps = {0};
animation.translationTracks.keys_data = {start};
animation.translationTracks.keyCount = 1;
}
if (!animation.translationTracks.isUsed && !animation.scalingTracks.isUsed &&
!animation.rotationTracks.isUsed) {
return;
}
layer.textureAnimationId = static_cast<u32>(out_.textureAnimations.size());
out_.textureAnimations.push_back(std::move(animation));
}
void emitUvRotation(mdx::TextureAnimation& animation, f32 radiansPerSecond, f32 standingAngle,
u32 globalSequence) {
constexpr f32 kTwoPi = 6.283185307179586f;
if (globalSequence == kNoGlobalSequence) {
globalSequence = globalSequenceOf(Milliseconds(kTwoPi / std::abs(radiansPerSecond)));
}
const u32 milliseconds = out_.globalSequences[globalSequence];
const f32 period = static_cast<f32>(milliseconds) / kMilliseconds;
const f32 turns = std::max(1.0f, std::round(std::abs(radiansPerSecond) * period / kTwoPi));
const f32 total = turns * kTwoPi * (radiansPerSecond < 0.0f ? -1.0f : 1.0f);
animation.rotationTracks.isUsed = true;
animation.rotationTracks.interpolationType = mdx::InterpolationType::Linear;
animation.rotationTracks.globalSequenceId = globalSequence;
animation.rotationTracks.timestamps.clear();
animation.rotationTracks.keys_data.clear();
for (u32 step = 0; step <= 4; ++step) {
const f32 fraction = static_cast<f32>(step) / 4.0f;
const f32 half = (standingAngle + total * fraction) * 0.5f;
animation.rotationTracks.timestamps.push_back(
static_cast<u32>(static_cast<f32>(milliseconds) * fraction));
animation.rotationTracks.keys_data.push_back(
Quaternion{0.0f, 0.0f, std::sin(half), std::cos(half)});
}
animation.rotationTracks.keyCount = animation.rotationTracks.timestamps.size();
}
void emitEvents() {
for (const Window& window : windows_) {
const Clip& clip = document_.clips[window.clip];
for (const ClipEvent& event : clip.events) {
if (event.node >= context_.nodeSlots.size()) {
continue;
}
const ExportContext::NodeSlot& slot = context_.nodeSlots[event.node];
if (slot.slot != ExportContext::Slot::EventObject ||
slot.index >= out_.eventObjects.size()) {
diagnostics_.warn(DiagCode::AnimTrackDropped,
"event '" + event.name +
"' fires at a node that is not an MDX event object",
ElementRef(ElementKind::Node, event.node), profile_);
continue;
}
mdx::EventObject& object = out_.eventObjects[slot.index];
object.globalSequenceId = window.globalSequenceId;
const f32 absolute = event.time * kMilliseconds + static_cast<f32>(window.start);
object.eventTrackTimes.push_back(
absolute <= 0.0f ? 0u : static_cast<u32>(absolute + 0.5f));
}
}
for (mdx::EventObject& object : out_.eventObjects) {
std::sort(object.eventTrackTimes.begin(), object.eventTrackTimes.end());
object.eventTrackTimes.erase(
std::unique(object.eventTrackTimes.begin(), object.eventTrackTimes.end()),
object.eventTrackTimes.end());
}
}
const Document& document_;
const Model& model_;
u32 modelIndex_;
ProfileId profile_;
const ExportContext& context_;
mdx::Model& out_;
Diagnostics& diagnostics_;
std::vector<Window> windows_;
std::vector<VisibilityGate> gates_;
};
}
void Export(const Document& document, u32 model, ProfileId profile, const ExportContext& context,
mdx::Model& out, Diagnostics& diagnostics) {
if (model >= document.models.size()) {
return;
}
Exporter(document, model, profile, context, out, diagnostics).run();
}
} } } }