horde3d / Horde3D

Horde3D is a small 3D rendering and animation engine. It is written in an effort to create an engine being as lightweight and conceptually clean as possible.
http://horde3d.org/
1.54k stars 308 forks source link

how to program morph target animation? #226

Closed gwald closed 9 months ago

gwald commented 9 months ago

I can't figure it out.... I'm following the code as per the knight sample, assuming morph targets are programmed the same? I can't seem to find any example of morph animation, the closes was the alfred code, but that looks very old or incomplete. https://hcm-lab.de/public/Horde3D/trunk/Tools/GameEngine/Samples/AlfredXpad It would be nice if the old uni students made their code available to have more samples! Very cool stuff! https://www.youtube.com/@rolla3d/videos

Anyway, I tried swapping out h3dSetModelAnimParams with h3dSetModelMorpher, but not seeing anything exempt the model without any morphing.

Slowly going crazy trying and searching, trying and search :(

The main h3d functions look like this


//loading

//HD3D LOADING MODEL
    animRes->model_res =  modelRes = h3dAddResource( H3D_ResTypes_SceneGraph, filename, 0 );
    h3dutLoadResourcesFromDisk( "./Content" );

//HD3D LOADING ANIM
    sprintf(filename, "Models/%s.anim", g_RAM_to_filename[i].filename);// g_RAM_to_filename[i].filename);
    animRes->anim_res  = animID =   h3dAddResource( H3D_ResTypes_Animation, filename, 0 );
    h3dutLoadResourcesFromDisk( "./Content" );

// animate and render

    real_node =  h3dAddNodes( g_y3d_node_buff[node].h3d_node,  res );
    h3dSetupModelAnimStage( real_node, 0, anim_p->anim_res, 0, "", false );

#if 1
        //Cube-mesh_morph_Bottom Cube-mesh_morph_Top
        if(anim_p->cur_frame < 8)
        {
            test = h3dSetModelMorpher(      real_node,
                    "Cube-mesh_morph_Top",
                    0.001f //anim_p->weight
            );

            LOG("h3dSetModelMorpher test %d\n ", test);

            test= h3dSetModelMorpher(       real_node,
                    "Cube-mesh_morph_Bottom",
                    anim_p->weight  );

            LOG(" h3dSetModelMorpher test %d\n ", test);
        }
        else
        {
            test = h3dSetModelMorpher(      real_node,
                    "Cube-mesh_morph_Top",
                    anim_p->weight
            );

            LOG("h3dSetModelMorpher test %d\n ", test);

            test = h3dSetModelMorpher(      real_node,
                    "Cube-mesh_morph_Bottom",
                    0.0001f//anim_p->weight
            );

            LOG("h3dSetModelMorpher test %d\n ", test);
        }
#else

         h3dSetModelAnimParams( real_node, 0,  2.50f, 1.0f);
#endif

     h3dUpdateModel( real_node,   H3D_ModelUpdateFlags_Animation | H3D_ModelUpdateFlags_Geometry | H3D_ModelUpdateFlags_ChildNodes );

//main loop - screen render

    clearAllModels();

    g_y3d_gameloop_function();

    h3dUpdateModel( 1,  H3D_ModelUpdateFlags_Animation | H3D_ModelUpdateFlags_Geometry | H3D_ModelUpdateFlags_ChildNodes );

    // Render scene
    h3dRender( g_cam );

    // Finish rendering of frame
    h3dFinalizeFrame();

    SDL_GL_SwapWindow(win);
algts commented 9 months ago

Hello. I've never used morph targets myself, so I may not be of help here, but here are some guidelines:

gwald commented 9 months ago

Thanks for the reply, great tips! But I've removed my project from the situation, and made my first sample test app, to try do the morph animation, I couldn't figure it out, it's using the code base that loads the TrueSpace 7 animated horse dae export and that works fine, the cube converts and loads fine, but the morph animations doesnt seem to work, and h3dSetModelMorpher always returns 0.

The cube with morphing is the best I could find, I posted in the other issue: https://github.com/KhronosGroup/COLLADA2GLTF/issues/166#issuecomment-385405062

There was one here but it's gone: http://www.horde3d.org/forums/viewtopic.php?f=2&t=1024 (archive.org doesn't have it either)

I would like to rule out either the code side or the morphing model/anim, but I cant find one either one to validate the other, and I dont think I can do it without one known to work :'(

This is my test app anyway (horse & morph). h3d-test.zip

It's a shame so much time and inactivity has gone by :/

I appreciate your help anyway.

gwald commented 9 months ago

It looks like that blender and collada export doesn't have animation, I should have read that !!! https://github.com/KhronosGroup/COLLADA2GLTF/issues/166#issuecomment-420366128 image I thought it did, 8 floats on each target... anyway, I'll experiment with the latest blender collada export.

And there was no error when creating the animation image

algts commented 9 months ago

It think that this information is rather old, judging from the info I found, at least in 2021 people successfully exported animation via collada in blender: https://forum.defold.com/t/solved-how-to-export-blender-animation-to-dae-and-import-into-defold/68699.

gwald commented 9 months ago

Oh GOD! it worked :+1: :+1: :+1: Thank you so much!!! :) :D :)

image

I read that thread, and at the end he recommends a github: https://github.com/yeqwep/Toufu-3D-project

I converted the .dae file and it just worked! :) Excellent :)

It worked with the boning animation code... // Play boned animation if(g_animRes) { h3dSetModelAnimParams( g_model, 0, _anim_count, 1.0f ); h3dUpdateModel( g_model, H3D_ModelUpdateFlags_Animation | H3D_ModelUpdateFlags_Geometry ); }

And not using the h3dSetModelMorpher function.

algts commented 9 months ago

I've checked the resources from the test you've uploaded. The box resource (dae) seems to be correct, because it has morph controller inside. Horse dae does not have morph targets, only joints for skinning (check for word "morph" in dae file). So, using horse model for morphing won't lead to any results. But the result animation for box model is too small in my opinion, will have to check that. Box model also has 0 for both weights as default value. You should also check horde logs, I've found out that you have messages about incorrect access to resources. That could also lead to some problems, like no/incorrect animation.

P.S. You can try to use premake if you are not fond of Cmake. It is way easier to write a small project and you also get cross-platform build practically for free (if you have low number of dependencies and they are not hard to find/get). https://github.com/premake/premake-core, https://premake.github.io/

algts commented 9 months ago

After analyzing the test more, I think that converter probably could not get all required information from dae file and did not write morphers name to geo file (if you look at it via hex editor, you won't find any letters except for the header). And all calls to h3dSetModelMorpher fails because of incorrect names. It is strange that horde does not allow to get morpher names from model or geometry resource, I should probably add these calls.

EDIT: confirmed that collada converter fails to process box dae file correctly. It parses dae file correctly but then does not create morph targets because it cannot find morph. And it cannot find morph because it sends name "Cube-mesh", but morph is registered as "Cube-morph". So, nothing is written to geometry resource. It seems that either the exporter does something incorrect and you should try to reexport with newer blender/other DCC tool (have not tried myself yet), or converter should be updated to support this situation. For converter, Converter::validateInstance and Converter::processMeshes have to be updated.

EDIT 2: exporting via blender 4 yields the same results, so converter should be fixed. Ideally export from 3dmax or maya should be checked with new opencollada exporter, but blender is rather popular now so this should be fixed. I think that probably we should fix DaeLibControllers::findSkin and findMorph functions to check passed id with names other than requested controller, probably parent object name should be checked.

gwald commented 9 months ago

Yes, the horse is bone/skin animation, to test if the morph would work using that code, it did :) I will look at premake, thanks for that! :+1: :)

The animation also works with nodes being deleted image

I am looking at the converter and I will try to fix it, I will let you know if I can not tho :) And then I will try to make the .anim from multiple single dea files.

gwald commented 9 months ago

Well... I thought it was too good to be true!! :laughing: :laughing: :laughing: It looks to me that Toufo3D .dae isn't morphing, but it's skinned :/ I thought it was strange that it didn't require using h3dSetModelMorpher I will keep looking for a .dae with morph targets to test.

I'm not familia with the collada format, I just downloaded the 1.4.1 spec. I've stepped through the converter with the cube dae, it looks to me it has an empty tag and it doesn't have any tags.

Is this what you mean, that the 2 tags: Cube-mesh_morph_Bottom Cube-mesh_morph_Top is the animation frames, stored as keyframes in library_geometries?

How would you suggest this case to be handled? I was thinking of copying the parse function in DaeAnimation but with looping in for tags, I dont know if that would work tho.

This is similar to what I want to do, instead of being in different sections, the data is in separate files.


<library_controllers>
    <controller id="Cube-morph" name="Cube-morph">
      <morph source="#Cube-mesh" method="NORMALIZED">
        <source id="Cube-targets">
          <IDREF_array id="Cube-targets-array" count="2">Cube-mesh_morph_Bottom Cube-mesh_morph_Top</IDREF_array>
          <technique_common>
            <accessor source="#Cube-targets-array" count="2" stride="1">
              <param name="IDREF" type="IDREF"/>
            </accessor>
          </technique_common>
        </source>
        <source id="Cube-weights">
          <float_array id="Cube-weights-array" count="2">0 0</float_array>
          <technique_common>
            <accessor source="#Cube-weights-array" count="2" stride="1">
              <param name="MORPH_WEIGHT" type="float"/>
            </accessor>
          </technique_common>
        </source>
        <targets>
          <input semantic="MORPH_TARGET" source="#Cube-targets"/>
          <input semantic="MORPH_WEIGHT" source="#Cube-weights"/>
        </targets>
      </morph>
    </controller>
algts commented 9 months ago

Morphing is not treated as animation by collada or by blender exporter, it seems. At least the cube blend file could be exported to dae with animation and without morphs, or without animation and with morphs. So either animation, or morph targets. Morphing is done in controllers. Then later, during model conversion, during processNode function we try to find DaeMorph and cannot find it, because morph controller has name "cube-morph" and converter tries to find "cube-mesh" morph controller. It fails and no morphing is written to geometry (morph targets are written to geometry files, not animation).

Collada (and more so GLTF) are rather cryptic formats that constantly point from one point of document to another, and you have to constantly jump here and there to get actual data. So yes, all morph positions are actually in library_geometry, and its metadata is in library_controllers. Arrays have ids, in this case Cube-mesh_morph_Bottom and Cube-mesh_morph_Top are arrays with vertex data.

algts commented 9 months ago

As for unifying data from several files - it may be much easier to do it in blender or other dcc that supports animation. Bring all posibles poses to blender, set it to different keyframes, and export shape blends and no animation. That way you should have one dae file with many morph targets. Or, if you wish, you could just export animations and forget about morphing. Unifying several dae files in converter would probably be a serious programming task.

gwald commented 9 months ago

You are right, I would be very happy just having a simple working .dae with per vector morphing working :)

I think the converter code is expecting the animation tag and lib controller morph tags in a .dae. I was able to create them in blender following: https://blender.stackexchange.com/questions/34758/is-it-possible-to-create-duplicates-of-a-mesh-that-can-be-deformed-and-used-as/34764#34764

If you're a noob at blender like me you need to right click the shape keys and insert keyframe image Actually, do NOT create any keyframes! create your base model and morph targets, then follow the tutorial to create the shape keys, then to export, select the base model (the shape key object) and export.

Collada export has to include the "shape keys" image btest.zip The names of the morph target frames will be written to the scene.xml file

But the convert still has the same problems that you describable, the converter isn't creating Morph targets information in the .geo file.

I tried changing the controller tag from 'Icosphere-morph' to 'Icosphere-mesh', visa-versa and just 'Icosphere' also but still not creating the data.

How is the converter expecting this section for it to create a per-vertext morphing .dae? Is the problem just this section or is it a bad tag name somewhere else?


<library_controllers>
    <controller id="Icosphere-morph" name="Icosphere-morph">
      <morph source="#Icosphere-mesh" method="NORMALIZED">
        <source id="Icosphere-targets">
          <IDREF_array id="Icosphere-targets-array" count="3">Icosphere-mesh_morph_Icosphere_001 Icosphere-mesh_morph_Icosphere_002 Icosphere-mesh_morph_Icosphere_003</IDREF_array>
          <technique_common>
            <accessor source="#Icosphere-targets-array" count="3" stride="1">
              <param name="IDREF" type="IDREF"/>
            </accessor>
          </technique_common>
        </source>
        <source id="Icosphere-weights">
          <float_array id="Icosphere-weights-array" count="3">1 0 1</float_array>
          <technique_common>
            <accessor source="#Icosphere-weights-array" count="3" stride="1">
              <param name="MORPH_WEIGHT" type="float"/>
            </accessor>
          </technique_common>
        </source>
        <targets>
          <input semantic="MORPH_TARGET" source="#Icosphere-targets"/>
          <input semantic="MORPH_WEIGHT" source="#Icosphere-weights"/>
        </targets>
      </morph>
    </controller>
  </library_controllers>
  <library_animations>
    <animation id="action_container-Icosphere" name="Icosphere"/>
  </library_animations>

_anim_count is a range from 0 to 1.0f

test= h3dSetModelMorpher(       g_model,
                "",
                _anim_count );
        LOG("2 h3dSetModelMorpher test %d %f\n ", test, _anim_count);

But always returns 0

No errors or warnings in the log HTML.

algts commented 9 months ago

How is the converter expecting this section for it to create a per-vertext morphing .dae? Is the problem just this section or is it a bad tag name somewhere else?

Converter is searching for Icosphere-mesh, because it searches for morph when it processes collada nodes. So, it just passes current node id to search for morph controller. And morph controller has name Icosphere-morph, so no morph controller is found, therefore, nothing is written to geometry.

You should try to modify DaeMorph *DaeLibControllers::findMorph( const std::string &id ) (daeLibControllers.h file) to search for morphs not only based on passed id, but also to get model/mesh name (Icosphere) and manually add "-morph" to it and check morphcontroller id against generated string.

algts commented 9 months ago

As a hotfix, replace findMorph function with the following. It works correctly and I get morphing cube (modified your test a bit to increase and lower morph value through keyboard keys). For cube model morph target names are "Top" and "Bottom". I'll check if skinning works correctly on horse (finds controllers) and also make a fix if needed. Then I'll commit to develop branch. I thinks I'll also add querying morph targets for models or geometry resources.

DaeMorph *findMorph( const std::string &id ) const
    {
        if( id == "" ) return 0x0;

        for( unsigned int i = 0; i < morphControllers.size(); ++i )
        {
            if( morphControllers[i]->id == id ) return morphControllers[i];
            else
            {
                                // Check for special cases. For example, Blender now creates <model-name>-morph, and id is <model-name>-mesh.
                auto morphId = morphControllers[i]->id;
                if (morphId.find("-morph") != std::string::npos && id.find("-mesh") != std::string::npos)
                    return morphControllers[i];
            }
        }

        return 0x0;
    }

Edit: skinning seems to be working correctly, no patches currently needed.

gwald commented 9 months ago

Thank very much @algts :) :+1: I tested with cube and my btest above and both worked for me :+1: With the morphing function as expected :+1: .

    test= h3dSetModelMorpher(       g_model,
                "",
                _anim_count );

        LOG("2 h3dSetModelMorpher test %d %f\n ", test, _anim_count);

I think querying is a good idea and useful. I would also suggest, having more descriptive output on the converter which shows/confirms the names of references (object names), would help in debugging etc, just simple printf statements:

    DaeMorph *findMorph( const std::string &id ) const
    {
        if( id == "" ) return 0x0;

        for( unsigned int i = 0; i < morphControllers.size(); ++i )
        {
            if( morphControllers[i]->id == id )
                        {
                                printf("Animation: %s\n", id.c_str() ); fflush(stdout);
                               return morphControllers[i];
                        }
            else
            {
                // Check for special cases. For example, Blender now creates <model-name>-morph, and id is <model-name>-mesh.
                auto morphId = morphControllers[i]->id;
                if (morphId.find("-morph") != std::string::npos && id.find("-mesh") != std::string::npos)
                {
                    printf("Animation: %s\n", id.c_str() ); fflush(stdout);
                    return morphControllers[i];
                }
            }
        }

        return 0x0;
    }

I think this is solved and can be closed at your convenience.

Regarding my single file animation functionality, I still haven't given up!!!! :laughing: If I can find a solution in a few days, I will post my solution on the other issue. :crossed_fingers:

algts commented 9 months ago

Ok, great. I thinks I'll make additional prints under verbose flag in converter. As for getting morph target information, it will be from geometry resource. Node interface does not allow get string parameters for different elements (no elemIdx is passed to function). I'll commit today and then close this.

algts commented 9 months ago

Pushed to develop branch. Added ability to get morph target count and morph target names from geometry resource. Fix converter to find morph targets that have "-morph" instead of "-mesh" in name.
Reopen if issues occur.