• Check out the results of the Techtree Contest #19!
  • Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • Create a void inspired texture for Warcraft 3 and enter Hive's 34th Texturing Contest: Void! Click here to enter!
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!

Trouble calculating sequence extents (C++)

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 :p

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);
    }
}
The various helper functions used (saveTracks, insertTracks and applyTransformations):
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:
(i.e. ignore geosets not visible in the sequence)
do not ignore. Add empty sequence extent to the geoset and continue to the next sequence for the geoset.

Here is my revisited code in C# how i'm calculating all extents (geoset extent, geoset sequence extents, sequence extents, model's extent), with explaining comments. all is there.




If i made any mistakes someone could correct me tho.

And in case of Reforged models... you can add to the function a parameter [float percentageInfluence] and use that to protentially reduce the calculated values ( float factor = percentageInfluence / 100f; )
 
Last edited:
Back
Top