- Joined
- Nov 2, 2004
- Messages
- 2,229
I've written a system for calculating sequence extents. The sequence extents themselves take into account geoset animations (i.e. ignore geosets not visible in the sequence), and the geoset sequence extents take into account animations.
It works almost perfectly, but in some cases it goes slightly wrong. In particular, I'm testing it with the Knight model, and everything is fine, except in the Death animation and the Stand Victory animation the extent goes too far down (into the "ground"). In both animations, the horse rears up, which surely isn't a coincidence, so it must have something to do with rotation.
I've tried different orders of applying the transformations. I've also tried accumulating all the transformations of every node in a matrix group (instead of doing it separately per node like I'm doing now). Everything I tried either gave the same results or made it worse.
Would be grateful for any help with solving this
Edit: I think the issue is either something with the quaternionSlerp function or the way I calculate the ratio between two frames, or both.
Here's the main function for calculating geoset sequence extents:
The various helper functions used (saveTracks, insertTracks and applyTransformations):
And finally the functions applying the transformations (scaling, rotation, translation and SLERP):
It works almost perfectly, but in some cases it goes slightly wrong. In particular, I'm testing it with the Knight model, and everything is fine, except in the Death animation and the Stand Victory animation the extent goes too far down (into the "ground"). In both animations, the horse rears up, which surely isn't a coincidence, so it must have something to do with rotation.
I've tried different orders of applying the transformations. I've also tried accumulating all the transformations of every node in a matrix group (instead of doing it separately per node like I'm doing now). Everything I tried either gave the same results or made it worse.
Would be grateful for any help with solving this
Edit: I think the issue is either something with the quaternionSlerp function or the way I calculate the ratio between two frames, or both.
Here's the main function for calculating geoset sequence extents:
C++:
void ModelEdit::calculateGeosetSequenceExtent(uint32_t geosetIndex, uint32_t sequenceIndex)
{
if (geosetIndex < geosets.size() && sequenceIndex < sequences.size()) {
if (sequenceIndex+1 > geosets[geosetIndex]->sequenceExtents.size()) {
geosets[geosetIndex]->sequenceExtents.resize(sequenceIndex+1);
}
auto start = static_cast<int32_t>(sequences[sequenceIndex]->interval()[0]),
end = static_cast<int32_t>(sequences[sequenceIndex]->interval()[1]);
vec3 min = { 0.0F, 0.0F, 0.0F }, max = { 0.0F, 0.0F, 0.0F };
bool initialized = false;
// Check every matrix group
for (const auto &group : geosets[geosetIndex]->matrixGroups) {
// Collect all affected vertices
std::vector<vec3> vertices;
for (const auto &vertex : geosets[geosetIndex]->vertices) {
if (vertex->vertexGroup == group) {
vertices.push_back(vertex->vertex);
}
}
// Only continue if there are affected vertices
if (!vertices.empty()) {
// Go through every node in the group
for (const auto &node : group->objects) {
std::vector<int32_t> frames; // Collect all affected frames
std::vector<std::vector<Track<vec3>>> scalingTracks; // Collect all scaling tracks for each (parent) node
std::vector<std::vector<Track<vec4>>> rotationTracks; // Collect all rotation tracks for each (parent) node
std::vector<std::vector<Track<vec3>>> translationTracks; // Collect all translation tracks for each (parent) node
std::vector<bool> isScalingInterp, isRotationInterp, isTranslationInterp;
std::vector<vec3> pivotPoints; // Collect all pivot points for each (parent) node
bool inheritScaling = true;
bool inheritRotation = true;
bool inheritTranslation = true;
// Go through every parent node
for (auto currentNode=node; currentNode; currentNode=currentNode->parent) {
// Stop if the previous node doesn't inherit anything
if (!inheritScaling && !inheritRotation && !inheritTranslation) {
break;
}
std::vector<Track<vec3>> currentScalingTracks;
std::vector<Track<vec4>> currentRotationTracks;
std::vector<Track<vec3>> currentTranslationTracks;
bool isCurrentScInterp = false, isCurrentRoInterp = false, isCurrentTrInterp = false;
if (inheritScaling) {
isCurrentScInterp = saveTracks(currentNode->animations, KGSC, start, end, frames, currentScalingTracks);
}
if (inheritRotation) {
isCurrentRoInterp = saveTracks(currentNode->animations, KGRT, start, end, frames, currentRotationTracks);
}
if (inheritTranslation) {
isCurrentTrInterp = saveTracks(currentNode->animations, KGTR, start, end, frames, currentTranslationTracks);
}
// Save the pivot point and tracks
// Skip this node if there are no transformations
if (!currentScalingTracks.empty() || !currentRotationTracks.empty() || !currentTranslationTracks.empty()) {
pivotPoints.push_back(currentNode->pivotPoint);
scalingTracks.push_back(currentScalingTracks);
rotationTracks.push_back(currentRotationTracks);
translationTracks.push_back(currentTranslationTracks);
isScalingInterp.push_back(isCurrentScInterp);
isRotationInterp.push_back(isCurrentRoInterp);
isTranslationInterp.push_back(isCurrentTrInterp);
}
inheritScaling = !(currentNode->flags() & GenericObject::dontInheritFlagToMdx["Scaling"]);
inheritRotation = !(currentNode->flags() & GenericObject::dontInheritFlagToMdx["Rotation"]);
inheritTranslation = !(currentNode->flags() & GenericObject::dontInheritFlagToMdx["Translation"]);
}
// We've collected all the frames and tracks
// Sort frames, and insert opening and closing frame if necessary
std::sort(frames.begin(), frames.end());
if (frames.empty() || frames.front() != start) {
frames.insert(frames.begin(), start);
}
if (frames.back() != end) {
frames.push_back(end);
}
// Insert/calculate missing tracks
insertTracks(scalingTracks, frames, start, end, { 1.0F, 1.0F, 1.0F }, isScalingInterp);
insertTracks(rotationTracks, frames, start, end, { 0.0F, 0.0F, 0.0F, 1.0F }, isRotationInterp);
insertTracks(translationTracks, frames, start, end, { 0.0F, 0.0F, 0.0F }, isTranslationInterp);
// We've filled in all the frames, now we can apply the transformations
std::vector<std::vector<vec3>> trackVertices; // Save vertex positions for each track
trackVertices.resize(frames.size(), vertices); // Resize to number of tracks and fill with initial vertices
for (size_t i=0; i<pivotPoints.size(); i++) {
applyTransformations(scalingTracks[i], trackVertices, pivotPoints[i], "scale");
applyTransformations(rotationTracks[i], trackVertices, pivotPoints[i], "rotate");
applyTransformations(translationTracks[i], trackVertices, pivotPoints[i], "translate");
}
// Now we've transformed each vertex for each track, we can find the min/max positions
// Initialize to the first one
if (!initialized && !trackVertices.empty() && !trackVertices[0].empty()) {
min = trackVertices[0][0];
max = trackVertices[0][0];
initialized = true;
}
for (const auto &track : trackVertices) {
for (const auto &vertex : track) {
min = minvec3(min, vertex);
max = maxvec3(max, vertex);
}
}
}
}
}
geosets[geosetIndex]->sequenceExtents[sequenceIndex].min = min;
geosets[geosetIndex]->sequenceExtents[sequenceIndex].max = max;
geosets[geosetIndex]->sequenceExtents[sequenceIndex].boundsRadius = getBoundsRadius(min, max);
}
}
C++:
template<typename T>
bool ModelEdit::saveTracks(const animEditVec &animations, const uint32_t tag, const int32_t start, const int32_t end,
std::vector<int32_t> &frames, std::vector<Track<T>> &tracks)
{
bool isInterp = false;
// Find the correct animation
for (const auto &animation : animations) {
if (animation->tag() == tag) {
isInterp = animation->interpolationType() > 0;
for (const auto &track : dynamic_cast<AnimationEdit<T>*>(animation.get())->tracks) {
if (track.frame >= start && track.frame <= end) {
// Save the frame
if (std::find(frames.begin(), frames.end(), track.frame) == frames.end()) {
frames.push_back(track.frame);
}
// Save the track
tracks.push_back(track);
}
else if (track.frame > end) {
break;
}
}
break;
}
}
return isInterp;
}
template<typename T>
void ModelEdit::insertTracks(std::vector<std::vector<Track<T>>> &tracks, const std::vector<int32_t> frames,
const int32_t start, const int32_t end, const T defaultValue, const std::vector<bool> isInterp)
{
for (size_t i=0; i<tracks.size(); i++) {
// Insert opening and closing tracks first
if (tracks[i].empty() || tracks[i].front().frame != start) { // Default values
tracks[i].insert(tracks[i].begin(), Track<T>(start, defaultValue, defaultValue, defaultValue));
}
if (tracks[i].back().frame != end) { // Copy last available track
auto lastTrack = tracks[i].back();
tracks[i].push_back(Track<T>(end, lastTrack.value, lastTrack.inTan, lastTrack.outTan));
}
// We've already inserted opening and closing tracks so we can skip those
// Since every track's frame was saved in frames,
// the current frame is always smaller than or equal to the current track's frame
// So we can safely insert missing tracks at the current index
// We also have at least two tracks, and we insert any missing tracks,
// so the index will never be out of range for the tracks
for (size_t j=1; j<frames.size()-1; j++) {
if (tracks[i][j].frame != frames[j]) {
T newValue;
if (!isInterp[i] || tracks[i][j-1].value == tracks[i][j].value) {
newValue = tracks[i][j-1].value;
}
else {
float ratio = (frames[j]-tracks[i][j-1].frame)/(tracks[i][j].frame-tracks[i][j-1].frame);
if constexpr (std::is_same<T, vec4>()) {
newValue = quaternionSlerp(tracks[i][j-1].value, tracks[i][j].value, ratio);
}
else {
newValue = {
tracks[i][j-1].value[0] + (tracks[i][j].value[0]-tracks[i][j-1].value[0])*ratio,
tracks[i][j-1].value[1] + (tracks[i][j].value[1]-tracks[i][j-1].value[1])*ratio,
tracks[i][j-1].value[2] + (tracks[i][j].value[2]-tracks[i][j-1].value[2])*ratio
};
}
}
// We don't care about tangents when calulating vertex positions, so just insert default values
tracks[i].insert(tracks[i].begin()+j, Track<T>(frames[j], newValue, defaultValue, defaultValue));
}
}
}
}
template<typename T>
void ModelEdit::applyTransformations(const std::vector<Track<T>> tracks, std::vector<std::vector<vec3>> &vertices,
const vec3 pivotPoint, const std::string type)
{
for (size_t i=0; i<tracks.size(); i++) { // For every track
for (auto &vertex : vertices[i]) { // Transform each vertex for this track
if (type == "scale") {
if constexpr (std::is_same<T, vec3>()) {
vertex = scaleVector(pivotPoint, vertex, tracks[i].value);
}
}
else if (type == "rotate") {
if constexpr (std::is_same<T, vec4>()) {
vertex = rotateVector(pivotPoint, vertex, tracks[i].value);
}
}
else if (type == "translate") {
if constexpr (std::is_same<T, vec3>()) {
vertex = translateVector(vertex, tracks[i].value);
}
}
}
}
}
And finally the functions applying the transformations (scaling, rotation, translation and SLERP):
C++:
vec3 scaleVector(vec3 node, vec3 vector, vec3 scaling)
{
// Distance from node (vertex - node) multiplied by scaling, added to node
return {
node[0] + (vector[0] - node[0])*scaling[0],
node[1] + (vector[1] - node[1])*scaling[1],
node[2] + (vector[2] - node[2])*scaling[2]
};
}
vec3 rotateVector(vec3 node, vec3 vector, vec4 rotation)
{
vec3 relative = { vector[0] - node[0], vector[1] - node[1], vector[2] - node[2] };
// Convert quaternion to vector components
float x = rotation[0], y = rotation[1], z = rotation[2], w = rotation[3];
// Quaternion multiplication formula for rotating a vector
float num1 = 2*(y*relative[2] - z*relative[1]);
float num2 = 2*(z*relative[0] - x*relative[2]);
float num3 = 2*(x*relative[1] - y*relative[0]);
return {
node[0] + (relative[0] + w*num1 + (y*num3 - z*num2)),
node[1] + (relative[1] + w*num2 + (z*num1 - x*num3)),
node[2] + (relative[2] + w*num3 + (x*num2 - y*num1))
};
}
vec3 translateVector(vec3 vector, vec3 translation)
{
return {
vector[0] + translation[0],
vector[1] + translation[1],
vector[2] + translation[2],
};
}
// from https://github.com/MartinWeigel/Quaternion/blob/master/Quaternion.c
vec4 quaternionSlerp(vec4 q1, vec4 q2, float t)
{
// Based on http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/index.htm
float cosHalfTheta = q1[3]*q2[3] + q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2];
// if q1=q2 or q1=-q2 then theta = 0 and we can return q1
if (std::fabs(cosHalfTheta) >= 1.0F) {
return q1;
}
float halfTheta = std::acos(cosHalfTheta);
float sinHalfTheta = std::sqrt(1.0F - cosHalfTheta*cosHalfTheta);
// If theta = 180 degrees then result is not fully defined
// We could rotate around any axis normal to q1 or q2
if (std::fabs(sinHalfTheta) < 1e-4F) {
return {
q1[0] * 0.5F + q2[0] * 0.5F,
q1[1] * 0.5F + q2[1] * 0.5F,
q1[2] * 0.5F + q2[2] * 0.5F,
q1[3] * 0.5F + q2[3] * 0.5F
};
}
else {
// Default quaternion calculation
float ratioA = std::sin((1.0F - t) * halfTheta) / sinHalfTheta;
float ratioB = std::sin(t * halfTheta) / sinHalfTheta;
return {
q1[0] * ratioA + q2[0] * ratioB,
q1[1] * ratioA + q2[1] * ratioB,
q1[2] * ratioA + q2[2] * ratioB,
q1[3] * ratioA + q2[3] * ratioB
};
}
}
Last edited:

