zturtleman / mm3d

Maverick Model 3D is a 3D model editor and animator for games.
https://clover.moe/mm3d
GNU General Public License v2.0
117 stars 24 forks source link

FYI: I'm starting work on Skeletal+Frame mode, rejiggering things some #149

Open m-7761 opened 3 years ago

m-7761 commented 3 years ago

Update: I haven't yet gone over the changes here to integrate into my fork (https://github.com/mick-p1982/mm3d) but I got to a stopping point on the project I was working on all year (https://swordofmoonlight.itch.io/k) so I have less time pressure right now to do some things.

Like today or tomorrow I'm breaking further ground on changing how animations are organized. The changes are pretty simple, but the biggest difference is instead using different indices for each mode I'm going to store the animations in the same array and use the same data structure (which is identical in my fork) so that the indices passed to the APIs can refer to any animation type by virtue of mapping onto an animation of a given type.

But I'm going to keep the modes partitioned so Skeletal is first, Frame is second, and the new mode will technically be Frame but with Joint keyframes allowed.

In my game project I have frame animation models that don't actually move center point wise (like a treadmill) so I'm faced with the option of "physically" moving their vertices, but this seems like a bad idea and throws out a lot of the benefits of the animation system (by having to provide express data-points for every single vertex just to walk) so what I really want is to parent the vertex animation to a basal joint/bone object. Anyway this is an example of why you'd want to combine these techniques.

Since I've already changed the frame-based point animations to work like the skeletal system instead most of the code is identical between these types and I've changed them to use the same APIs except I switch on the PositionTypeE type. In the new design that won't be necessary since the single index model will be able to fully identify an animation.

The new mode will be ANIMMODE_SKELETAL|ANIMMODE_FRAME (3) and I will switching to treating these like bitmasks. I've also planned for a UI facility that can edit/draw the animation with one or the other mode disabled for when you want to work with just one or the other. It only applies to the new animation type.

m-7761 commented 3 years ago

It took me 3 days of work to do the rewrite. I need to test it more but the old facilities are working. The new API is shown below. I changed "getAnimCount" to "getAnimationCount" because the other ones that use "getAnim" refer to Animation data members.

        //2021: Converts an animation type into the first valid
        //absolute index of that type. The number of animations
        //is got by getAnimationCount. (Which may be 0!)
        unsigned getAnimationIndex(AnimationModeE)const;
        unsigned getAnimationCount(AnimationModeE)const; //getAnimCount
        unsigned getAnimationCount()const{ return (unsigned)m_anims.size(); }
        //2021: Converts the old-style type-based index into an
        //absolute index, or -1 if this animation doesn't exist.
        int getAnim(AnimationModeE type, unsigned subindex)const;
        AnimationModeE getAnimType(unsigned anim)const;

getAnim (see C++ comments) is only used by the scritptif.cc file (IOW not used anywhere internally) and what I came up with for the new animation type is to use ANIMMODE as its constant (3) so to suggest the other modes are restrictions on it as opposed to features, which fundamentally they are. I felt that was best since there's no simple wording for it.

All of the "getAnimX" methods have their "mode" parameters ripped out, which generally makes code a lot simpler to deal with. (They were mostly busy work and duplication to keep track of that and draw from different memory pools, so I made no bones about keeping them.) For setCurrentAnimation I switched the parameters around so the mode parameter is optional. It's only used to restrict what edits can be made to the new animation type and for draw it means whether to apply the skeletal and frame data to the animation or to ignore it.

For operations like rotating the selection I chose to ignore the selection when there are joints selected in editing mode since I can foresee that rotating the joints and the vertex data attached to them simultaneously would be double rotating the vertices, so it's probably not what users would expect to happen.

So all and all the operation seems a success. Oh! also I changed the m_keyframes member to std::unordered_map. I didn't encounter any situations where ordering mattered (reordering was required) but when writing out the MM3D files the objectIndex fields are now in no particular order. (Edited: Although they probably are in order since the hash function just shifts the type 24bits and combines it with the index.)

m-7761 commented 3 years ago

Here is what I decided on for the layouts of these sections. I haven't run this code. I wonder about alignment. writeHeaderA and writeHeaderB have funky 2B alignment. Many of the sections put a 2B value after them but they're still internally unaligned. I wonder if a significant version could try to fix the alignment by changing how those functions are laid out, since that wouldn't be a burden to implement.

Another source of alignment trouble is the std::string fields. Those could be padded with or something, I don't know. Wouldn't be too hard to remove the padding. I'm thinking of fixes that would be unintrusive.

    //2021: Scale
    if(setOffset(MDT_ScaleFactors,true))
    {
        size_t sz = 2*sizeof(int16_t)+3*sizeof(float32_t);

        writeHeaderB(0x0000,basescaled,sz);

        Model::Position pos;
        auto f = [&](const Model::Object2020 *ea)
        {
            if(1!=ea->m_xyz[0]||1!=ea->m_xyz[1]||1!=ea->m_xyz[2])
            {
                m_dst->write((int16_t)pos.type);
                m_dst->write((int16_t)pos.index);
                for(unsigned i=0;i<3;i++)
                m_dst->write((float32_t)ea->m_xyz[i]);
            }
            pos.index++;
        };
        pos = {Model::PT_Joint,0}; for(auto*ea:modelJoints) f(ea);
        pos = {Model::PT_Point,0}; for(auto*ea:modelPoints) f(ea);
    }

    //2021: Animations
    if(setOffset(MDT_Animations,false))
    {
        writeHeaderA(0x0000,modelAnims.size());

        int anim = -1; for(auto*ab:modelAnims)
        {
            anim++;

            //2020: Filled in afterward.
            auto off1 = m_dst->offset(); 
            m_dst->write((uint32_t)0); //animSize

            uint32_t frameCount = ab->_frame_count();

            uint16_t flags = ab->_type;
            if(ab->m_wrap) flags|=MAF_ANIM_LOOP; //8

            m_dst->write(flags);
            m_dst->writeBytes(ab->m_name.c_str(),ab->m_name.size()+1);
            m_dst->write((float32_t)ab->m_fps);
            m_dst->write(frameCount);           
            for(uint32_t f=0;f<frameCount;f++)
            m_dst->write((float32_t)ab->m_timetable2020[f]);
            m_dst->write((float32_t)ab->_time_frame());

            uint32_t keyframeMask = model->hasKeyframeData
            (anim,Model::PM_Vertex|Model::PM_Joint|Model::PM_Point);

            m_dst->write(keyframeMask);

            if(keyframeMask&Model::PM_Vertex)
            {
                unsigned fp = ab->m_frame0;
                size_t vcount = modelVerts.size();
                auto *vdata = modelVerts.data();
                for(unsigned f=0;f<frameCount;f++,fp++)             
                for(size_t w=0,v=0;v<vcount;)
                {
                    auto cmp = vdata[v]->m_frames[fp]->m_interp2020;
                    for(w++;w<vcount&&cmp==vdata[w]->m_frames[fp]->m_interp2020;)
                    w++;
                    m_dst->write((uint32_t)cmp);
                    m_dst->write((uint32_t)(w-v));
                    if(cmp>Model::InterpolateCopy) for(;v<w;v++)
                    {
                        //WARNING: This depends on the interpolation model.
                        double *coord = vdata[v]->m_frames[fp]->m_coord;
                        for(unsigned i=0;i<3;i++) m_dst->write((float32_t)coord[i]);
                    }
                    else v = w;
                }
            }

            for(int i=1;i<=2;i++) if(keyframeMask&1<<i)
            {               
                uint32_t objectKeyframes = 0;
                for(auto&ea:ab->m_keyframes)
                if(i==ea.first.type)
                objectKeyframes+=ea.second.size();

                m_dst->write(objectKeyframes);

                for(auto&ea:ab->m_keyframes)
                if(i==ea.first.type) for(auto*kf:ea.second)
                {
                    //A MOMENTARY FIX
                    //MM3D doesn't need this but it's helpful so third-party 
                    //loaders don't have to compute it.
                    if(kf->m_interp2020==Model::InterpolateCopy)
                    {
                        double *x[3] = {}; x[kf->m_isRotation>>1] = kf->m_parameter;
                        model->interpKeyframe(anim,kf->m_frame,ea.first,x[0],x[1],x[2]);
                    }

                    //EXTENSIBLE DATA
                    //This is an extensible data strategy.
                    //Byte 4 is how many 4B words comprise 
                    //the rest of the keyframe object. The 
                    //rest of the keyframe object's format 
                    //is described by Byte 1 & 2 as a unit.
                    uint8_t format_descriptor[4] = 
                    {
                        kf->m_interp2020,0,kf->m_isRotation,4
                    };
                    m_dst->writeBytes(format_descriptor,4);
                    m_dst->write((uint16_t)ea.first.index);
                    m_dst->write((uint16_t)kf->m_frame);                    
                    for(int i=0;i<3;i++) 
                    m_dst->write((float32_t)kf->m_parameter[i]);
                }           
            }

            //TODO: Can MisfitOffsetList do this?
            auto off2 = m_dst->offset();
            uint32_t animSize = off2-off1-sizeof(animSize);
            m_dst->seek(off1);
            m_dst->write(animSize);
            m_dst->seek(off2);
        }
        log_debug("wrote %d animations\n",modelAnims.size());
    }
m-7761 commented 3 years ago

Published: https://github.com/mick-p1982/mm3d/releases/tag/win32-demo2c (It also lets vertices be moved in skeletal only animation mode.)