Jump to content

Bem vindo à Unidev
Registre para ter acesso a todos os recursos do site. Uma vez registrado e logado, você poderá criar tópicos, postar em tópicos já existentes, gerenciar seu perfil e muito mais. Se você já tem uma conta, faça login aqui - ou então crie aqui uma conta agora mesmo!
- - - - -
Photo

Carregando e exibindo arquivos BVH


por Edin Mujagic

Neste artigo iremos ver o mais comum formato de captura de movimento: BVH. BVH é um acrônimo para BioVision Hierarchical data e é usado para armazenar dados de captura de movimento. É simples e fácil de entender. Escreveremos uma classe simples que pode carregar, exibir e executar dados do arquivo.

FORMATO BVH

Pode-se achar muito sobre esse formato nesses dois links:
http://www.cs.wisc.e...9/Jeff/BVH.html
http://www.dcs.shef....smes/CS0111.pdf

Basicamente, ele tem duas partes, Hierarquia e Movimento. Como os nomes sugerem, estas duas partes contém apenas isto: hierarquias dos esqueletos e dados de movimento. Dentro da parte de hierarquia temos a descrição dos esqueletos. Mesmo que o formato permita ter várias definições de esqueletos, raramente ele vai conter mais do que uma. Esqueletos são definidos ao definir os ossos, que são definidos ao definir as juntas; significando que definimos um esqueleto definindo as juntas. Mas se a junta do cotovelo é filha da junta do ombro, como sabemos o comprimento do braço? Definindo um offset.
Dê uma olhada no exemplo:


HIERARCHY
ROOT Hips
{
	OFFSET 0.00 0.00 0.00
	CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
	JOINT Chest
	{
		OFFSET 5.00 0.00 0.00
		CHANNELS 3 Zrotation Xrotation Yrotation
		End Site
		{
			OFFSET 0.00 5.00 0.00
		}
	}
	JOINT Leg
	{
		OFFSET -5.0 0.0 0.0
		CHANNELS 3 Zrotation Xrotation Yrotation
		End Site
		{
			OFFSET 0.0 5.0 0.0
		}
	}
}
MOTION
Frames:		2
Frame Time: 0.033333
 0.00 0.00 0.00 0.00 0.00 0.00  0.00 0.00 0.00  0.00 0.00 0.00
 0.00 0.00 0.00 0.00 0.00 0.00  0.00 45.00 0.00  0.00 0.00 0.00


A primeira junta da hierarquia é uma junta raiz, então é definida usando a palavra-chave ROOT. Todas as outras juntas que são descendentes são definidas usando a palavra-chave JOINT seguida pelo nome da junta. Juntas especiais são juntas de End Site, que são juntas sem filhas ou nome.

O conteúdo de uma junta são OFFSET e CHANNELS. Usamos um offset para saber o comprimento dos ossos entre as juntas de uma junta-pai e ela mesma. Comumente, uma junta ROOT vai ter um offset de (0,0,0) (perceba que esses são os eixos x, y, z). Linhas CHANNELS definem o número de canais a seguir quais canais cujas partes de MOTION (movimento) contém dados de animação. Novamente, o uso mais comum é uma junta ROOT que tenha 6 canais (posição em xyz e rotação em xyz) enquanto as outras juntas terão 3. Juntas de End Site não tem dados de animação, então não precisam ter dados de CHANNELS. Elas terão apenas dados de OFFSET para sabermos seu comprimento.

A parte de MOTION contém duas linhas (frames definindo o número de frames... e frame time que é a taxa de frames; bvh motion FPS = 1/frame_time) seguida pelas linhas para cada frame, que tem dados float para cada combinação de junta/canal(especificado) começando do pai até os nós filhos, na mesma ordem em que foram especificados na parte de hierarquia, do topo até o final. O exemplo é meio besta e só tem zeros, mas você entendeu onde quero chegar. Quando fizermos o loader você pode mudar os valores e ir brincando com isso.

Interpretar o MOTION e realmente mudar a posição das juntas é descrito depois. Primeiro, faremos o loading.

CÓDIGO

Definiremos algumas estruturas que precisaremos para armazenar dados:


#define Xposition 0x01
#define Yposition 0x02
#define Zposition 0x04
#define Zrotation 0x10
#define Xrotation 0x20
#define Yrotation 0x40
    
typedef struct
{
    float x, y, z;
} OFFSET;


typedef struct JOINT JOINT;


struct JOINT
{
    const char* name = NULL;        // joint name
    JOINT* parent = NULL;           // joint parent
    OFFSET offset;                  // offset data
    unsigned int num_channels = 0;  // num of channels joint has
    short* channels_order = NULL;   // ordered list of channels
    std::vector<JOINT*> children;   // joint's children
    glm::mat4 matrix;               // local transofrmation matrix (premultiplied with parents'
    unsigned int channel_start = 0; // index of joint's channel data in motion array
};


typedef struct
{
    JOINT* rootJoint;
    int num_channels;
} HIERARCHY;


typedef struct
{
    unsigned int num_frames;              // number of frames
    unsigned int num_motion_channels = 0; // number of motion channels 
    float* data = NULL;                   // motion float data array
    unsigned* joint_channel_offsets;      // number of channels from beggining of hierarchy for i-th joint
} MOTION;


A maior parte destes parâmetros é auto explicativo. Para cada junta precisaremos de uma lista de filhas, matriz de transformação local e ordem de canais no mínimo.

CLASSE BVH


class Bvh
{
    JOINT* loadJoint(std::istream& stream, JOINT* parent = NULL);
    void loadHierarchy(std::istream& stream);
    void loadMotion(std::istream& stream);
public:
    Bvh();
    ~Bvh();


    // loading 
    void load(const std::string& filename);


    /** Loads motion data from a frame into local matrices */
    void moveTo(unsigned frame);


    const JOINT* getRootJoint() const { return rootJoint; }
    unsigned getNumFrames() const { return motionData.num_frames; }
private:
    JOINT* rootJoint;
    MOTION motionData;
};


Esta é uma classe simples e abaixo estão as funções para loading:


void Bvh::load(const std::string& filename)
{
    std::fstream file;
    file.open(filename.c_str(), std::ios_base::in);


    if( file.is_open() )
    {
        std::string line;


        while( file.good() )
        {
            file >> line;
            if( trim(line) == "HIERARCHY" )
                loadHierarchy(file);
            break;
        }


        file.close();
    }
}


void Bvh::loadHierarchy(std::istream& stream)
{
    std::string tmp;


    while( stream.good() )
    {
        stream >> tmp;


        if( trim(tmp) == "ROOT" )
            rootJoint = loadJoint(stream);
        else if( trim(tmp) == "MOTION" )
            loadMotion(stream);
    }
}


JOINT* Bvh::loadJoint(std::istream& stream, JOINT* parent)
{
    JOINT* joint = new JOINT;
    joint->parent = parent;
    
    // load joint name
    std::string* name = new std::string;
    stream >> *name;
    joint->name = name->c_str();


    std::string tmp;
    
    // setting local matrix to identity
    joint->matrix = glm::mat4(1.0);


    static int _channel_start = 0;
    unsigned channel_order_index = 0;
    
    while( stream.good() )
    {
        stream >> tmp;
        tmp = trim(tmp);


        // loading channels
        char c = tmp.at(0);
        if( c == 'X' || c == 'Y' || c == 'Z' )
        {
            if( tmp == "Xposition" )
            {
                joint->channels_order[channel_order_index++] = Xposition;
            }
            if( tmp == "Yposition" )
            {
                joint->channels_order[channel_order_index++] = Yposition;
            }
            if( tmp == "Zposition" )
            {
                joint->channels_order[channel_order_index++] = Zposition;
            }


            if( tmp == "Xrotation" )
            {
                joint->channels_order[channel_order_index++] = Xrotation;
            }
            if( tmp == "Yrotation" )
            {
                joint->channels_order[channel_order_index++] = Yrotation;
            }
            if( tmp == "Zrotation" )
            {
                joint->channels_order[channel_order_index++] = Zrotation;
            }
        }


        if( tmp == "OFFSET" )
        {
            // reading an offset values
            stream  >> joint->offset.x
                    >> joint->offset.y
                    >> joint->offset.z;
        }
        else if( tmp == "CHANNELS" )
        {
            // loading num of channels
            stream >> joint->num_channels;


            // adding to motiondata
            motionData.num_motion_channels += joint->num_channels;


            // increasing static counter of channel index starting motion section
            joint->channel_start = _channel_start;
            _channel_start += joint->num_channels;


            // creating array for channel order specification
            joint->channels_order = new short[joint->num_channels];


        }
        else if( tmp == "JOINT" )
        {
            // loading child joint and setting this as a parent
            JOINT* tmp_joint = loadJoint(stream, joint);


            tmp_joint->parent = joint;
            joint->children.push_back(tmp_joint);
        }
        else if( tmp == "End" )
        {
            // loading End Site joint
            stream >> tmp >> tmp; // Site {


            JOINT* tmp_joint = new JOINT;


            tmp_joint->parent = joint;
            tmp_joint->num_channels = 0;
            tmp_joint->name = "EndSite";
            joint->children.push_back(tmp_joint);


            stream >> tmp;
            if( tmp == "OFFSET" )
                stream >> tmp_joint->offset.x
                       >> tmp_joint->offset.y
                       >> tmp_joint->offset.z;


            stream >> tmp;
        }
        else if( tmp == "}" )
            return joint;


    }
}


void Bvh::loadMotion(std::istream& stream)
{
    std::string tmp;


    while( stream.good() )
    {
        stream >> tmp;


        if( trim(tmp) == "Frames:" )
        {
            // loading frame number
            stream >> motionData.num_frames;
        }
        else if( trim(tmp) == "Frame" )
        {
            // loading frame time
            float frame_time;
            stream >> tmp >> frame_time;


            int num_frames   = motionData.num_frames;
            int num_channels = motionData.num_motion_channels;


            // creating motion data array
            motionData.data = new float[num_frames * num_channels];


            // foreach frame read and store floats
            for( int frame = 0; frame < num_frames; frame++ )
            {
                for( int channel = 0; channel < num_channels; channel++)
                {
                    // reading float
                    float x;
                    std::stringstream ss;
                    stream >> tmp;
                    ss << tmp;
                    ss >> x;


                    // calculating index for storage
                    int index = frame * num_channels + channel;
                    motionData.data[index] = x;
                }
            }
        }
    }
}


O código para loading deve ser fácil de ler. A função load() chama a loadHierarchy(), que chama a loadRoot() para a junta raiz (ROOT) e loadMotion() quando necessário. A função loadJoint() carrega a junta e todos aqueles ifs apenas tentam cuidar da ordenação de canais. A função loadMotion() somente carrega o número de frames e o frame time, e então itera por todos os canais, lê os floats, calcula onde armazenar um float e então o armazena.

Esta versão não dá suporte a múltiplas hierarquias, mas pode ser facilmente adicionada.


TRANSFORMAÇÕES DE JUNTA (JOINT TRANSFORMATIONS)

Se imaginarmos um esqueleto humano simplificado, a mão seria filha do braço, que seria filho do ombro, etc. Podemos ir todo o caminho até a junta raiz que pode ser, por exemplo, quadris (o que realmente é na maioria dos arquivos). Para saber a posição absoluta de todos os descendentes da junta raiz, teremos que aplicar a transformação do pai nelas. Você provavelmente sabe que isso pode ser alcançado usando matrizes. É por isso que temos a "matriz de transformação local" da junta.

Basicamente, a matriz de transformação é composta de parâmetros de rotação e translação (BVH não dá suporte a escalas de ossos, por isso não temos uma). Ela pode ser representada usando uma matrix padrão 4x4 onde os parâmetros de translação estão presentes na 4ª coluna. Perceba que OpenGL usa ordenação column-major, que se parece como uma transposição de uma matriz ordenada row-major. Já que OpenGL usa isso, GLSL usa isso e também GLM que é baseado em GLSL que usamos aqui. Dizemos isso porque precisaremos saber isso e precisaremos disto mais tarde.

A função que faz o posicionamento é a moveTo() e usa uma função helper estática dentro do arquivo .cpp (não pode ser usada fora, e nem precisa também):


/** 
	Calculates JOINT's local transformation matrix for 
	specified frame starting index 
*/
static void moveJoint(JOINT* joint, MOTION* motionData, int frame_starts_index)
{
    // we'll need index of motion data's array with start of this specific joint
    int start_index = frame_starts_index + joint->channel_start;


    // translate indetity matrix to this joint's offset parameters
    joint->matrix = glm::translate(glm::mat4(1.0),
                                   glm::vec3(joint->offset.x,
                                             joint->offset.y,
                                             joint->offset.z));


    // here we transform joint's local matrix with each specified channel's values
    // which are read from motion data
    for(int i = 0; i < joint->num_channels; i++)
    {
        // channel alias
        const short& channel = joint->channels_order[i];


        // extract value from motion data
        float value = motionData->data[start_index + i];
        
        if( channel & Xposition )
        {
            joint->matrix = glm::translate(joint->matrix, glm::vec3(value, 0, 0));
        }
        if( channel & Yposition )
        {
            joint->matrix = glm::translate(joint->matrix, glm::vec3(0, value, 0));
        }
        if( channel & Zposition )
        {
            joint->matrix = glm::translate(joint->matrix, glm::vec3(0, 0, value));
        }


        if( channel & Xrotation )
        {
            joint->matrix = glm::rotate(joint->matrix, value, glm::vec3(1, 0, 0));
        }
        if( channel & Yrotation )
        {
            joint->matrix = glm::rotate(joint->matrix, value, glm::vec3(0, 1, 0));
        }
        if( channel & Zrotation )
        {
            joint->matrix = glm::rotate(joint->matrix, value, glm::vec3(0, 0, 1));
        }
    }


    // then we apply parent's local transfomation matrix to this joint's LTM (local tr. mtx. :)
    if( joint->parent != NULL )
        joint->matrix = joint->parent->matrix * joint->matrix;


    // when we have calculated parent's matrix do the same to all children
    for(auto& child : joint->children)
        moveJoint(child, motionData, frame_starts_index);
}


void Bvh::moveTo(unsigned frame)
{
    // we calculate motion data's array start index for a frame
    unsigned start_index = frame * motionData.num_motion_channels;


    // recursively transform skeleton
    moveJoint(rootJoint, &motionData, start_index);
}


O que precisamos (para cada junta, começando da raiz) é pegar o valor dos dados de movimento e aplicar nele para que ele fosse carregado/definido no arquivo com ambas as funções glm::translate() e glm::rotate(). Usamos a função helper estática moveJoint() para nos ajudar com a transformação de juntas usando recursão.

O que precisamos fazer é exibi-las.

USANDO A CLASSE E EXIBINDO O ESQUELETO

Construir array de vértices dos dados de juntas de um esqueleto não é uma tarefa da classe BVH. Usando recursão e std::vector() podemos facilmente construir o array de vértices:


std::vector<glm::vec4> vertices;
std::vector<GLshort>   indices;


GLuint bvhVAO;
GLuint bvhVBO;
Bvh* bvh = NULL;


/** put translated joint vertices into array */
void bvh_to_vertices(JOINT*                  joint,
                     std::vector<glm::vec4>& vertices,
                     std::vector<GLshort>&   indices,
                     GLshort                 parentIndex = 0)
{
    // vertex from current joint is in 4-th ROW (column-major ordering)
    glm::vec4 translatedVertex = joint->matrix[3];


    // pushing current 
    vertices.push_back(translatedVertex);


    // avoid putting root twice
    GLshort myindex = vertices.size() - 1;
    if( parentIndex != myindex )
    {
        indices.push_back(parentIndex);
        indices.push_back(myindex);
    }


    // foreach child same thing
    for(auto& child : joint->children)
        tmpProcess(child, vertices, indices, myindex);
}


void bvh_load_upload(int frame = 1)
{    
    // using Bvh class
    if( bvh == NULL )
    {
        bvh = new Bvh;
        bvh->load("file.bvh");
    }
    
    bvh->moveTo(frame);
    
    JOINT* rootJoint = (JOINT*) bvh->getRootJoint();
    bvh_to_vertices(rootJoint, vertices, indices);
    
    // here goes OpenGL stuff with gen/bind buffer and sending data
    // basically you want to use GL_DYNAMIC_DRAW so you can update same VBO
}


Note que há algumas características do C++11. Se você usar o GCC, você deve adicionar alguns switches do C++11 como o -std=c++11 e o -std=gnu++11 para faze-lo compilar. A função bvh_tovertices() nos ajuda a reconstruir vértices usando a informação de um esqueleto.

FINALIZANDO

Então vimos o formato BVH e como carrega-lo e exibi-lo. Este é apenas um loader básico, que pode ser a base de coisas mais complicadas, como blending e mixing de animação.
Isto é tudo. Espero que tenham gostado.

Artigo originalmente postado no Gamedev.net:
http://www.gamedev.n...isplaying-r3295



0 Comments