Pages: [1] 2
  Print  
Author Topic: [WIP] Collada (.dae) 3d Animated Model Loader  (Read 471 times)
Offline (Unknown gender) Solitudinal
Posted on: September 01, 2017, 03:48:24 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
Before I start, just let me say that I didn't see any existing code in enigma for loading Collada models, so I started writing my own. I also didn't see any in-built xml readers (aside from the skeletal remains of RapidXML), so I used pugixml. If I missed something, please be nice :-)
I'm aware that ENIGMA is already capable of reading .obj models, which is awesome, but Collada has animation and such.

Collada is an industry standard format (ISO/PAS 17506) for animated 3d models, utilizing skeletal joints for animation (it does some other things too, but this is what I'm using it for here). It stores data in XML.

This, then, is my ENIGMA/C++ reader for said format. It is a work in progress, so be patient and I'll continue to share my progress on it, and feel free to share feedback along the way (this is why I've shared it here). I'm developing this for a game idea that I'm poking at in ENIGMA.

I'm aware that other Collada readers exist, but they tend to be extremely bulky, easily reaching up into multiple MBs, so I figured something much smaller could be achieved and could easily be added to ENIGMA.


I'll provide steps for adding this to ENIGMA, but please ADD AT YOUR OWN RISK. Everything should be easily reversible by just undoing these changes (or if you're familiar with git, there's a git path).

First, pick up a copy of pugixml (not pugxml). Copy the .cpp and .hpp files into ENIGMAsystem/SHELL/Universal_System/ and rename the .hpp to .h
Edit both and add the following lines near the top:
Code: [Select]
#define PUGIXML_NO_EXCEPTIONS
#define PUGIXML_NO_STL

Next, edit ENIGMAsystem/SHELL/Graphics_Systems/General/GSmodel.h and add the following function def:
Code: [Select]
void d3d_collada_load(int id, std::string fname);
Finally, edit ENIGMAsystem/SHELL/Graphics_Systems/OpenGL1/GSmodel.cpp (only tested for OpenGL1, but shouldn't require much if any modification for other systems) and on line 41 add the following includes:
Code: [Select]
#include <sstream> //used to split strings
#include <algorithm> //needed for find(), used kinda like indexof
#include "Universal_System/pugixml.h"
And on line 107 or thereabouts (I chose between d3d_model_save and d3d_model_load) add the following code:
Code: [Select]
//temporarily stores the vertex data with texture/normal ids to construct unique VNTs
class Vertex {
public:
  unsigned int index;
  float x, y, z;
  int texId = -1;
  int normId = -1;
  Vertex *next = NULL;

  Vertex(int id, float x, float y, float z) :
      index(id), x(x), y(y), z(z) {}
};

// I don't like enigma's split functions, so I made my own
template <class T>
std::vector<T> cppsplit(const pugi::char_t* slist) {
  std::stringstream ss(slist);
  std::vector<T> tlist;
  T token;
  while (ss >> token)
    tlist.push_back(token);
  return tlist;
}

//This workhorse reads a collada xml file (.dae) into an enigma model "mesh"
void d3d_collada_load(int id, string fname)
{
    pugi::xml_document doc;
    doc.load_file(fname.c_str());
    pugi::xml_node rootNode = doc.child("COLLADA");

    //////////////////
    //GeometryLoader//
    //////////////////
    pugi::xml_node meshData = rootNode.child("library_geometries").child("geometry").child("mesh");
    //readPositions
    const pugi::char_t* posId = meshData.child("vertices").child("input").attribute("source").value();
    pugi::xml_node posData = meshData.find_child_by_attribute("source","id",posId + 1).child("float_array");
    int posCount = posData.attribute("count").as_int();
    const pugi::char_t* sposList = posData.text().get();
    std::stringstream sspos(sposList);
    std::vector<Vertex*> vertices;
    for(int i = 0; i < posCount / 3; i++) {
        float x,y,z;
        sspos >> x >> y >> z;
        //correct Collada's z-up to y-up with (x,z,-y)
        vertices.push_back(new Vertex(i, x,y,z));
    }
    //readNormals
    pugi::xml_node poly = meshData.child("polylist");
    const pugi::char_t* normId = poly.find_child_by_attribute("input","semantic","NORMAL").attribute("source").value();
    pugi::xml_node normData = meshData.find_child_by_attribute("source","id",normId + 1).child("float_array");
    int normCount = normData.attribute("count").as_int();
    const pugi::char_t* snormList = normData.text().get();
    std::stringstream ssnorm(snormList);
    std::vector<std::array<float,3>> normList;
    for(int i = 0; i < normCount / 3; i++) {
        float x,y,z;
        ssnorm >> x >> y >> z;
        std::array<float,3> row = {x,y,z}; //correct Collada's z-up to y-up
        normList.push_back(row);
    }
    //readTextureCoords
    const pugi::char_t* texcId = poly.find_child_by_attribute("input","semantic","TEXCOORD").attribute("source").value();
    pugi::xml_node texcData = meshData.find_child_by_attribute("source","id",texcId + 1).child("float_array");
    int texcCount = texcData.attribute("count").as_int();
    const pugi::char_t* stexcList = texcData.text().get();
    std::stringstream sstexc(stexcList);
    std::vector<std::array<float,2>> texcList;
    for(int i = 0; i < texcCount / 2; i++) {
        float s,t;
        sstexc >> s >> t;
        std::array<float,2> row = {s, 1.0f - t}; //in OpenGL, texture's y coordinate "t" is inverted
        texcList.push_back(row);
    }
    //assembleVertices
    const pugi::char_t* indexData = poly.child("p").text().get();
    std::stringstream ssp(indexData);
    std::vector<unsigned int> indices;
    int idVert, idNorm, idTexc, idCol; //I'm assuming it's just these 4
    while (ssp >> idVert >> idNorm >> idTexc >> idCol) {
        //processVertex
        Vertex *v = vertices[idVert];
        if (v->texId == -1 || v->normId == -1) {
            v->texId = idTexc;
            v->normId = idNorm;
        } else if (v->texId != idTexc || v->normId != idNorm) {
            //modified dealWithAlreadyProcessedVertex
            Vertex *newv = v;
            while (newv != NULL) {
                if (newv->texId == idTexc && newv->normId == idNorm)
                    break;
                v = newv;
                newv = newv->next;
            }
            if (newv == NULL) {
                Vertex *nv = new Vertex(vertices.size(), v->x,v->y,v->z);
                nv->texId = idTexc;
                nv->normId = idNorm;
                vertices.push_back(nv); //this is why we can't just vector<tex,norm>
                newv = nv;
                v->next = newv;
                v = newv;
            }
        }
        indices.push_back(v->index);
    }

    // Close the xml file
//    delete doc;

    //"removeUnusedVertices", or in this case we just link them to first texture and normal
    for (Vertex *vertex : vertices) {
        //tangents aren't used, otherwise we'd average them
        if (vertex->normId == -1) vertex->normId = 0;
        if (vertex->texId == -1) vertex->texId = 0;
    }

    // Convert to ENIGMA model/mesh.
    meshes[id]->Begin(pr_trianglelist);
    for (const Vertex *ov : vertices) {
        meshes[id]->AddVertex(ov->x, ov->y, ov->z);
        meshes[id]->AddNormal(normList[ov->normId][0],normListov->normId][1],normList[ov->normId][2]);
        meshes[id]->AddTexture(texcList[ov->texId][0],texcList[ov->texId][1]);
        delete ov;
    }
    for (unsigned int i = 0; i < indices.size(); i++) {
        meshes[id]->AddIndex(indices[i]);
    }
    meshes[id]->End();
}

So the code will only read the base model right now. Animation will come eventually. It will then push the base model VNT (Vertex, Normal, Texture) and indices into an ENIGMA mesh. Code ported from this java example and AnimatedModelLoader.java
Also, if you need a model/texture to test with, it has one.


Here's an example of it used in a game, if you can get it to compile:
Create a background called tex_dude and load in the texture (diffuse.png in the java example)
Create event:
Code: [Select]
file = "C:/path/to/model.dae" //please point this to a valid collada .dae file
d3d_start();
d3d_set_perspective(true);
d3d_set_hidden(true);
d3d_set_lighting(false);
draw_set_color(c_white);
d3d_set_fog(true,c_white,1,1024);
d3d_set_culling(false)
d3d_set_shading(false);
texture_set_interpolation(true);

//here we load in the model and store it in the "mdl" variable
mdl = d3d_model_create(0); //0 means its static, for now
d3d_collada_load(mdl,file);

Draw event:
Code: [Select]
d3d_set_projection(0,-20,0, 0,0,0, 0,0,1); //feel free to tweak these numbers as needed to point the camera at your model
d3d_model_draw(mdl,0,0,0,background_get_texture(tex_dude));

Updates:
Sept 2 @ 1500est: included a few missing functions
Sept 2 @ 1600est: and a few missing structs
Sept 4 @ 2200est: replaced with instructions for inserting into ENIGMA and an example
Sept 5 @ 1900est: textures now apply correctly
Sept 16 @ 2300est: removed unused skeletal data and recursive function. Should now load a little faster and static models no longer have a dependency on having a skeleton.
Sept 29 @ 1800est: fixed memory leak
Oct 7 @ 1500est: removed unused joint weight data. Should now load a little faster and static models no longer have a dependency on having joint weights.
« Last Edit: October 07, 2017, 02:10:05 PM by Solitudinal » Logged
Offline (Male) hpg678
Reply #1 Posted on: September 02, 2017, 12:48:51 AM

Member
Location: Barbados
Joined: Mar 2017
Posts: 117

View Profile Email
sounds interesting, could you do an example and post it please?
Logged
Hugar
Offline (Unknown gender) Solitudinal
Reply #2 Posted on: September 02, 2017, 01:01:39 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
Like I said, I hacked it into ENIGMA, so it's not as simple as posting a .gmk file. In fact, for testing/developing it, I've actually got my own independent C++ program running on glew and glfw3. I may post that, but you'd need to set up glew and glfw (which is not fun).

UPDATE OP updated to include instructions, so the rest of this post is outdated.


As far as the ENIGMA route goes, typically I just wrap that code in a function and stick it in one of the files in enigma-dev\ENIGMAsystem\SHELL\Graphics_Systems\OpenGL1, call the function something like
int readCollada(std::string fname)
and have it return indices.size();
Then another function:
Code: [Select]
void drawCollada(int inds) {
glDrawElements(GL_TRIANGLES, inds, GL_UNSIGNED_INT, 0);
}

In a create event:
//General d3d setup code goes here.
vertNum = readCollada("C:/model.dae") //or wherever you put your model

In the draw event:
drawCollada(vertNum)
//don't forget to set up the camera/projection. For the model from the java example, I found the best camera position/orientation to be 0,5,15, 0,5,0, 0,1,0
« Last Edit: September 04, 2017, 09:18:18 PM by Solitudinal » Logged
Offline (Male) hpg678
Reply #3 Posted on: September 03, 2017, 03:43:14 AM

Member
Location: Barbados
Joined: Mar 2017
Posts: 117

View Profile Email
ok.....I see. It still is a great achievement and I congratulate you on your efforts.

I have dabbled in loading .obj objects into a 3D Breakout project I was working on but for some reason it was rotated at a right angle. Whether it was due to the modeler i used or perhaps ENIGMA reads it that way. I didn't investigate any further, but am planning to at some later date.

Anyway,  Keep up the good work. :) (Y) (Y)
Logged
Hugar
Offline (Unknown gender) Solitudinal
Reply #4 Posted on: September 03, 2017, 12:05:02 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
Yeah, that's what happens here too. As you see in the GeometryLoader->readPositions loop, I correct the Z-up from the modeller to Y-up. To do this, we can either multiply each vertex vector by a rotated correction matrix (as we kinda do here, I just simplified all the matrix math to y = -z and z = y) OR we can perform a rotational transform of the model after it is set up.
The correction matrix used in the java program is:
Code: [Select]
new Matrix4f().rotate((float) Math.toRadians(-90), new Vector3f(1, 0, 0))which I believe simplifies to:
Code: [Select]
{ 1,0,0,0,
  0,0,-1,0,
  0,1,0,0,
  0,0,0,1 }
which again simplifies to:
Code: [Select]
//x = x;
tmp = y;
y = -z;
z = tmp;
//w = w;

And then I set the camera up to be 0,5,15, 0,5,0, 0,1,0 (looking down the Z-axis with Y-up).

I'm currently struggling to get the texture applied. Here's the code I use, which is pretty standard:
Code: [Select]
GLuint textureID;
glGenTextures(1, &textureID);

// "Bind" the newly created texture : all future texture functions will modify this texture
glBindTexture(GL_TEXTURE_2D, textureID);

// Give the image to OpenGL
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGBA, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, &img[0]);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
but it doesn't apply

I'm also poking at porting this to Enigma's "mesh" used by other models.
Logged
Offline (Unknown gender) Solitudinal
Reply #5 Posted on: September 05, 2017, 06:03:38 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
I've updated the first post. You'll find that it's now uses ENIGMA's built in mesh/modelling system, and is able to utilize textures. I've also made it a lot easier to stick into ENIGMA with instructions.
(I also found out the texture was applying incorrectly because OpenGL inverts the y coordinates "t" of the texture, so once I accounted for that in the code with 1.0f - t, it worked amazing)

Next I'll be working on animation. This may take a while, but in the meantime, enjoy the ability to read in static collada models!

And if anybody who works on ENIGMA wants to either make this official or figure out how to make this an Extension, that'd be awesome.
Logged
Offline (Male) time-killer-games
Reply #6 Posted on: September 05, 2017, 11:04:50 PM

Member
Location: Virginia Beach
Joined: Jan 2013
Posts: 1067

View Profile WWW Email
This looks great! :D Is it platform-specific functionality or is it portable?
Logged
Offline (Unknown gender) Solitudinal
Reply #7 Posted on: September 06, 2017, 02:39:28 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
To answer that, let's break it down into 4 parts:
1: Pugixml
Pugixml is its own thing, so you'd have to consult with their website or such for more information, but since all you have to include is just two source files (.cpp and .h) and don't need a dll or anything, it should be relatively platform independent, aside from Windows Line Endings1
2: Interpreting the XML nodes
As far as I can tell, all of my code is standard C++ stuff, nothing fancy or platform dependant. I don't make any direct GL calls, window calls, or filesystem calls, or anything of the sort. Just be platform aware with the path/filename that you pass in.
3: Transferring that data into an ENIGMA mesh
This is just transferring memory to memory, and only depends on ENIGMA's Mesh struct, which appears to have an equivalent in DirectX9, OpenGL1, and so on
4: ENIGMA's GL calls
Again, this is up to ENIGMA. I don't make any GL calls myself, I just offload the Mesh to ENIGMA, and leave it up to the selected GL to make the appropriate calls. I'm on Windows, and ENIGMA is defaulting to OpenGL1, and it works fine.

1 Windows Line Endings don't even seem to play any part in this. I opened up a collada file in notepad and saw no rhyme or reason to line endings - I kinda don't think it uses any. My code also doesn't do anything that would necessarily care about line endings. The only strings I deal with are node names - which should be short identifiers - and space-separated number lists which I simply split on any whitespace.


tl;dr: It's as portable as ENIGMA's GL and Pugixml are. So yes, it should be quite portable. Just copy the code to each GL you intend to use it on.
« Last Edit: September 06, 2017, 11:10:02 PM by Solitudinal » Logged
Offline (Male) Goombert
Reply #8 Posted on: September 08, 2017, 05:26:53 AM

Contributor
Location: Cappuccino, CA
Joined: Jan 2013
Posts: 2988

View Profile
Hey, this is really awesome, keep up the good work!
Logged
Welcome to ENIGMO, the game engine built by fucking aliens.

Offline (Unknown gender) Solitudinal
Reply #9 Posted on: September 12, 2017, 05:21:15 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
Just wanted to give a quick little update.
The way that the java project that I'm porting this from does the animation is by using shaders, so I've taken a bit of time to learn about GLSL and have a pretty good grasp on it now, and also understand why it does this.
I believe shaders are GL2+ (mainly GL3), meaning that if I were to do a direct port, I'd probably have to drop OpenGL1 and target OpenGL3 in ENIGMA? Since everything is shaders these days, this seems like the way to go.
There's 3 alternatives I can think of:
0) The aforementioned shaders path.
1) Generate vertex models for every animation frame. This would use a huge amount of memory.
2) Handle all the vertex calculations through the CPU. This would require porting the shader's responsibilities into C++ code. Not sure how much ENIGMA's engine can support me along this way, but if I end up writing my own GL calls, GL3.1+ isn't going to like it unless you stick it in compat mode.

So yeah. For the time being, I'm forging ahead and porting option 0.
Logged
Offline (Male) Goombert
Reply #10 Posted on: September 14, 2017, 02:15:08 PM

Contributor
Location: Cappuccino, CA
Joined: Jan 2013
Posts: 2988

View Profile
No you can use shaders in OpenGL1, just not all kinds of shaders and not all shader functions. Well technically you can use them and they won't fail on your computer because I don't think we are setting the context to actually use a specific core, but it will just fail if you release the game and somebody else's computer only supports OpenGL1. You probably don't need to worry about that though because Windows XP is almost dead (hard to kill, but it's almost there). SFML or SDL or something (I forget) is only OpenGL2 but it still has vertex and fragment shaders too.

https://www.khronos.org/opengl/wiki/Core_Language_(GLSL)#OpenGL_and_GLSL_versions
Logged
Welcome to ENIGMO, the game engine built by fucking aliens.

Offline (Unknown gender) Solitudinal
Reply #11 Posted on: September 14, 2017, 04:11:19 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
Yeah, for the purpose of the game I'm making, I don't care about dropping GL1 and older computer support. But for the purpose of making the code accessible to other ENIGMA users, I'm trying to be mindful of it. I'm assuming shader words like "in/out/texture" are ok to use in place of "attribute/varying/texture2d", too?
Logged
Offline (Male) Goombert
Reply #12 Posted on: September 15, 2017, 12:34:49 PM

Contributor
Location: Cappuccino, CA
Joined: Jan 2013
Posts: 2988

View Profile
Yes, but I think you have it backwards, this post over on the OpenGL forums suggests that in and out actually replace varying, meaning varying is the one that's deprecated:
https://www.opengl.org/discussion_boards/showthread.php/174424-varying-in-out

The Wiki also says varying is removed:
https://www.khronos.org/opengl/wiki/Type_Qualifier_(GLSL)#Removed_qualifiers

Forgive me, it's been a while since I've worked with GLSL, been doing a lot of UI and higher-level things lately.
Logged
Welcome to ENIGMO, the game engine built by fucking aliens.

Offline (Unknown gender) Solitudinal
Reply #13 Posted on: September 15, 2017, 02:23:05 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
Nope, that's what I said, and that's why I said it :)

Great, I'll do that then. Had to check cuz it's a little hard to find info on GL1 and GLSL. I just find a bunch of stuff for GL3+ and lots of deprecated functions without replacements (frustum? I 'ardly knew 'em). (and before you say anything, yes, I know you're responsible for your own matrices now. Just find it amusing that GL decided to nix all the useful functions)
« Last Edit: September 15, 2017, 02:24:46 PM by Solitudinal » Logged
Offline (Unknown gender) Solitudinal
Reply #14 Posted on: September 16, 2017, 02:29:58 PM
Member
Joined: Aug 2017
Posts: 17

View Profile
According to this:
https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/texture.xhtml
texture() was not introduced until GLSL 130, so I guess I'll have to stick with the deprecated texture2d for OpenGL1, but that's easy enough to swap out for OpenGL3/etc.

In my test program I've switched over to shaders without too much trouble. Also decided to use GLM, since handling my own matrices was becoming too much of a burden (much as I enjoyed doing matrix math, I could never get GL to render right). Hopefully this shouldn't be too hard to port back to ENIGMA.

Come to think of it, are there any examples using ENIGMA's shaders, especially in a 3d environment? I'm wondering how the MVP (Model-View-Projection) is handled.
Logged
Pages: [1] 2
  Print