Ambientes Virtuais Colaborativos


1. Introdução

Ambientes Virtuais Colaborativos (AVCs) são ambientes virtuais imersivos onde dois ou mais  usuários podem estar imersos, por meio de dispositivos especiais, como  óculos e rastreadores de posição, no qual podem comunicar-se e/ou trabalhar conjuntamente em uma ou mais tarefas. Nestes ambientes o computador passa a prover um espaço digital no qual se pode construir e utilizar espaços compartilhados onde são realizadas tarefas colaborativas ou individuais.

Para permitir o uso de Ambientes Virtuais Colaborativos é necessário que se utiliza algum tipo de ferramenta de comunicação entre os nodos.  Um das possibilidades existente é usar a biblioteca RemoteMemory.
Esta biblioteca foi originalmente com o apoio de Rafael S. Garcia ([email protected]). Esta biblioteca provê uma área de memória que pode ser compatilhada por várias máquinas.

O principal objetivo da biblioteca “Remote Memory” é permitir que o usuário tenha a sua disposição uma memória de dados que pode ser compartilhada entre programas que rodam em máquinas diferentes. A partir de uma aplicação servidora, rodando em uma máquina remota, o usuário pode criar aplicações clientes que se conectem a esse servidor e pode compartilhar dados entre as aplicações.

O diagrama da figura abaixo ilustra o funcionamento básico da biblioteca.


Figura - Arquitetura da Remote Memory



2. Aplicação-Servidor

Primeiramente vamos proceder a criação de um programa que leia e escreva dados em uma memória compartilhada, sem utilizar nenhum recurso gráfico. Este programa usará um sevidor de memória que pode ser copiado aqui. Para rodar o servidor, abra  uma janela DOS e execute-o com o comando

MemoryServer 1001

O número 1001 após o nome do programa determina a porta de comunicação a ser usada para receber as conexões dos clientes.

O código fonte deste servidor encontra-se neste link. Para o presente exercício não há a necessidade de recompilar o servidor. Para compilar a uma aplicação usando do DEVCPP, você terá de incluir no projeto a bibliteca de suporte a SOCKETS do DEVCPP: libwsock32.a. Esta biblioteca encontra-se no diretório LIB da instalação do DEVCPP.


3. Criação de uma Aplicação-Cliente

3.1 Conexão com o Servidor

Para a criação de uma aplicação-cliente, o programador deve incluir em seu projeto o arquivo com o código da biblioteca de compartilhamento de memória. Este arquivo se chama MemoryClient.cpp

No fonte onde será contruída a aplicação-cliente deve ser adicionado arquivo com os cabeçalhos das funções da biblioteca. Para fazer isso, basta-se adicionar a linha abaixo no começo do arquivo fonte:

#include "RemoteMemory.h"
Depois disto, a primeira etapa da aplicação-cliente deve ser a conexão com servidor da memória remota compartilhada. O trecho de código a seguir exemplifica como fica a função main.

Note que este código conecta com um servidor que está rodando na mesma máquina que o cliente, através da porta  número 1000.
 

#include <stdio.h>
#include <string.h>

#include "RemoteMemory.h" // Inclui a biblioteca de memória remota compartilhada

int  main ( int argc, char** argv )
{
         int result;
         char serverIP[30] = "127.0.0.1";  // Nro IP do servidor
         unsigned int port = 1000;  // porta de conexão com o servidor

         result = ConnectToRemoteMemory ( serverIP, port );  // conecta com o servidor
         if ( !result ) {
           printf ( "Unable to connect to Memory Server at %s.\n", serverIP );
           printf ( "Pressione uma tecla para encerrar.");
           getchar();
           return 0;
         }
         printf ("Conexão bem sucedida !\n");
         printf ( "Pressione uma tecla para encerrar");
         getchar();
         return 1;
}
 

Lembre-se de ativar o servidor, pelo DOS, com o comando:
MemoryServer 1000
O valor retornado pela função ConnectToRemoteMemory( serverIP, port ) é 1 quando for  possível estabelecer a conexão e 0 quando ocorrer um erro.
O parâmetro serverIP da função é uma “string” que indica o endereço ou o número IP da máquina onde a aplicação servidora deve estar rodando. Por exemplo, se a aplicação servidora está sendo executada na mesma máquina que a aplicação cliente, esse parâmetro será 127.0.0.1 ou localhost.
O segundo parâmetro (port) é um interiro sem sinal que define o número da porta através da qual será feita a conexão.

Esta função abre dois tipos de conexão com o servidor: uma conexão via TCP/IP e a outra via UDP. O usuário poderá usar funções dos dois tipos de conexão no mesmo programa, sem problemas.

3.2 Escrita de dados no Servidor

A partir desse ponto, o programador poderá desenvolver a aplicação da maneira que achar mais adequada para as suas necessidades.

Para gravar dados na memória remota compartilhada de dados, é possível usar uma das duas funções abaixo:

· void WriteOnRemoteMemoryIP  (void *message, WORD address, int size)
· void WriteOnRemoteMemoryUDP (void *message, WORD address, int size)
A primeira função grava o dado na memória remota compartilhada através da conexão TCP/IP enquanto que a segunda função grava o dado via UDP.

O Significado dos parâmetros nas duas funções é o seguinte:

· message: ponteiro para o dado que será enviado para o servidor e gravado na memória remota compartilhada
· address: endereço da memória remota compartilhada onde esse dado deve ser gravado
· size: o tamanho (em bytes) do dado que será gravado
ATENÇÃO:
É importante que o usuário da biblioteca entenda como se constitui a memória remota compartilhada para trabalhar corretamente com ela.
Cada posição da memória remota compartilhada equivale a 1 byte. Logo, para se gravar 3 valores inteiros em sequência não se pode utilizar, por exemplo, os endereços 3, 4 e 5.
Se o usuário fizer isso, não terá os dados corretos quando for feita uma leitura desses valores.
O mais indicado a fazer, nesse caso, é gravar os dados nos endereços 3 * sizeof(int), 4 * sizeof(int) e 5 * sizeof(int). Com isso, o programador garante que terá seus três valores gravados em sequência e os dados esterão corretos.
A título de exemplo, o código apresentado a seguir  faz uso destas funções para escrever um vetor de inteiros na memória compartilahda.
Este vetor será lido por um outro cliente que tambem estará conectado à memória remota.

Para evitar problemas de sincronização, antes de iniciar a gravação do vetor, a rotina grava uma flag "avisando" que os dados disponíveis na memória são antigos.
Ao concluir a gravação do vetor, a rotina atualiza a flag com um novo valor avisando que o vetor já foi atualizado. Este valor será testado pelo cliente que lê a memoria a fim de saber se pode ler o vetor.

/* ******************************************************
void GravaDadosNaMemoriaRemota()
    Esta rotina grava um vetor de 15 inteiros na
    memória remota.
    Antes de iniciar a gravação, a rotina escreve um
    número negativo (-111) na posição 10 da memória
    para indicar ao clinte-leitor que os dados ainda
    não estão disponíveis.
 ****************************************************** */
void GravaDadosNaMemoriaRemota()
{
    int dado, vetor[15], i;
 printf ("Gravando Dados na Memória...\n");

 // Grava A constante FLAG_DADOS_VELHOS na posição 10 usando uma
    // conexão TCP/IP, avisando que os dados gravados ainda não
    // foram gravados.
    // O Cliente Leitor deverá ficar lendo esta posição até que este valor
    // seja alterado para FLAG_DADOS_OK

 dado = FLAG_DADOS_VELHOS;
 WriteOnRemoteMemoryIP (&dado, 10*sizeof(int),  sizeof(int));

 // Grava um vetor de 15 números a partir da posição 12 usando uma
    // conexão TCP/IP
 for (i=0;i<15;i++)
    vetor[i] = (i+1)*10;
 WriteOnRemoteMemoryIP (&vetor[0], 12*sizeof(int),  15* sizeof(int));

 // Grava a constante FLAG_DADOS_OK na posição 10 usando uma conexão TCP/IP,
 // avisando que a gravação do vetor foi concluída
 dado = FLAG_DADOS_OK;
 WriteOnRemoteMemoryIP (&dado, 10*sizeof(int),  sizeof(int));
 }
 

3.3 Leitura de dados no Servidor

Para ler dados da memória remota compartilhada, as funções são as seguintes:
· void ReadFromRemoteMemoryIP (void *message, WORD address, int size)
·void ReadFromRemoteMemoryUDP(void *message, WORD address, int size)
Novamente, a primeira função lê o dado da memória remota compartilhada através ad conexão TCP/IP enquanto que a segunda lê o dado via TCP/UDP. Os parâmetros são os seguintes:
·message: onde o dado será armazenado quando for lido.
· address: o endereço da memória remota compartilhada onde esse dado deve ser gravado.
· size: o tamanho (em bytes) do dado que será gravado.
A seguir apresenta-se uma rotina que lê os dados de um vetor armazenado na memória remota. Note que, de acordo com o exemplo anterior, a rotiana fica em um loop testando uma flag para saber se os dados a serem lidos já estão atualizados.
 
/* ******************************************************
void LeDadosDaMemoriaRemota()
    Esta rotina lê os dados de um vetor de inteiros
    gravados na meória remota.
    Antes de iniciar a leitura a rotina testa se o
    cliente-escritor já terminou a gravação dos dados.
    Este teste é feito verificando o valor da posição
    de memória 10
 ****************************************************** */
void LeDadosDaMemoriaRemota()
 {
     int i, dado, vetor[15];

 // Este laço fica esperando que o cliente-escritor
 // grave uma flag avisando que os dados já estão
 // disponíveis
     printf("Tentando ler");
     do
     {
        ReadFromRemoteMemoryIP (&dado, 10*sizeof(int), sizeof(int));
        printf(".");
     } while (dado != FLAG_DADOS_OK);  // testa se os dados já são "novos"

     printf("Dados disponíveis !!\n");
     printf("\nDados Lidos:\n");

     // Lê o vetor de 15 números a partir da posição 12 usando uma
     // conexão TCP/IP
     ReadFromRemoteMemoryIP (&vetor[0], 12*sizeof(int),  15* sizeof(int));

     for (i=0;i<15;i++)
        printf("[%d]= %d\n", i, vetor[i]);

 }
 

Neste link SimpleMemoryClient está disponível um programa exemplo com estas rotinas. Note que ambas estão no mesmo fonte, permitindo que um mesmo executável seja usado como leitor ou escritor. No início da execução do programa o usuário escolhe se o programa é um leitor ou um escritor. Lembre-se de ativar o servidor antes de executar um cliente.


4. Exemplo

A seguir é apresentado um exemplo em que um AVC de dois usuários é criado.

Este ambiente é criado a partir de uma aplicação monousuário em que o observador pode:

Para a criação do AVC são tratados os seguintes aspectos: Cada um destes itens é detalhado na seções a seguir.

4.1 Conexão da aplicação-cliente com o servidor e Identificação dos Clientes

Para definir "quem é quem", foram criadas duas constantes usadas na identificação dos usuários:
#define ID_CLIENT_ONE 11
#define ID_CLIENT_TWO 22
Estas contantes alem de identificarem os usuários definem os endereços de duas posições de memória usadas para definir se o cliente é o primeiro ou o segundo a se conectar no servidor. O trecho de código que faz isto é apresentado a seguir.
Nele a aplicação-cliente lê a posição ID_CLIENT_ONE da memória remota e verifica se seu valor ainda é zero (valor inicial de todas as posições da memória remota). Se for, então, este é o primeiro cliente e o valor ID_CLIENT_ONE deve ser escrito na posição ID_CLIENT_ONE.

Caso não seja zero, o valor lido é comparado com ID_CLIENT_ONE. Se for igual a ID_CLIENT_ONE então este é o segundo cliente. O valor ID_CLIENT_TWO é gravado na posição ID_CLIENT_TWO.

// **********************************************************************
//  void ConnectToTheServer(int &ID)
//  faz a conexão com o servidor e cria um ID para o usuário
//    retirado do arquivo "AVC_CommFuncs.cpp"
// **********************************************************************
void ConnectToTheServer(int &ID)
{

...........
..............
...........

 printf ("Conexão bem sucedida !\n");
 ServerOK = 1;

 // verifica se já existe um cliente ONE
 MyID = 0;
 ReadFromRemoteMemoryIP (&dado, ID_CLIENT_ONE*sizeof(int),  sizeof(int));
 if (dado == 0) // Ainda não existe um cliente ONE !!
 {
      dado = ID_CLIENT_ONE;
      WriteOnRemoteMemoryIP (&dado, ID_CLIENT_ONE*sizeof(int),  sizeof(int));
      MyID = ID_CLIENT_ONE;
      printf("Sou o cliente UM!\n");
      setWindowTitles("Client ONE !!");
 }
 else
 {
  if (dado == ID_CLIENT_ONE) // Já existe um cliente ONE, logo este é o cliente DOIS
      {
       dado = ID_CLIENT_TWO;
       WriteOnRemoteMemoryIP (&dado, ID_CLIENT_TWO * sizeof(int),  sizeof(int));
       MyID = ID_CLIENT_TWO;
       printf("Sou o cliente DOIS!\n");
       setWindowTitles("Client TWO !!");
  }
 }
 if (MyID == 0)  // houve algum problema na identificação... (erro na comunicação ??)
 {
      printf("Identificação não definida !!\n");
      ServerOK =0;
      setWindowTitles("Identificação não definida !!");
 }
 ID = MyID;

}

A função de conexão é chamada pelo programa principal (arquivo AVC.cpp). Baseado na identificação obtida pela aplicação-cliente, o programa define a posição e a orientação iniciais do observador.
 ...........
 ..............
 ...........
 UserObject->TranslateBy(0,1.5,5);
 ...........
 ..............
 ...........
 if (MyID == ID_CLIENT_ONE)
 {
       PartnerObject->SetRenderFunctionData(Verde);
 }
 if (MyID == ID_CLIENT_TWO)
 {
      PartnerObject->SetRenderFunctionData(Rosa);
      // Move the user again if he is the second one
      // move the user forward
      UserObject->TranslateToOnOBJCS(0,0,-5,TargetObject);
      // Rotate it
      UserRotY = 180;
      UserObject->RotateBy(UserRotY, 0,1,0);
 }

4.2 Definição de áreas comuns de troca de informações

Para uma applicação colaborativa, é preciso definir quais serão os dados compartilhados pelas aplicações-clientes.
Neste exemplos, os dados a serem compartilhados serão a posição do observador e a orientação de seu corpo.
Para permitir que os dois clientes possam saber onde armazenar estes dados foram criadas constantes que definem a posição de cada informação na memória compartilhada. Esta definição encontra-se no arquivo "AVC_ClientFunc.h":
// endereço onde será guardada a posição do usuário ONE
#define POS_CLIENT_ONE 100
// endereço onde será guardada a posição do usuário TWO
#define POS_CLIENT_TWO 200

// endereço onde será guardada a rotação do usuário ONE
#define ROT_CLIENT_ONE 120
// endereço onde será guardada a rotação do usuário TWO
#define ROT_CLIENT_TWO 220

4.3 Gravação dos dados na área compartilhada

A escrita dos dados da memória compartilhada depende da identificação dos usuários.
No trecho abaixo, a rotina GravaDadosNaMemoriaRemota efetua a escrita da posição (x,y,z) e do ângulo de rotação do corpo do usuário.
// **********************************************************************
// void GravaDadosNaMemoriaRemota(double Pos[3], double Rot)
// **********************************************************************
void GravaDadosNaMemoriaRemota(double Pos[3], double Rot)
{

 if (!ServerOK)
  return;

 if (MyID == ID_CLIENT_ONE)
 {
  // Grava 3 variáveis "double" na memória, representando a posição do usuário
  WriteOnRemoteMemoryIP (&Pos[0], POS_CLIENT_ONE * sizeof(double), 3*sizeof(double));
  // Grava 1 variável "double" na memória, representando a orientação do usuário
  WriteOnRemoteMemoryIP (&Rot, ROT_CLIENT_ONE * sizeof(double), sizeof(double));
 }

 if (MyID == ID_CLIENT_TWO)
 {
  // Grava 3 variáveis "double" na memória, representando a posição do usuário
  WriteOnRemoteMemoryIP (&Pos[0], POS_CLIENT_TWO * sizeof(double), 3*sizeof(double));
  // Grava 1 variável "double" na memória, representando a orientação do usuário
  WriteOnRemoteMemoryIP (&Rot, ROT_CLIENT_TWO * sizeof(double), sizeof(double));
 }

}

4.4 Leitura dos dados da área compartilhada

A leitura dos dados da memória compartilhada depende da identificação dos usuários.
No trecho abaixo, a rotina LeDadosDaMemoriaRemotaefetua a leitura da posição (x,y,z) e do ângulo de rotação do corpo do usuário.
 
// **********************************************************************
// void LeDadosDaMemoriaRemota(double Pos[3], double &rotY)
// **********************************************************************
void LeDadosDaMemoriaRemota(double Pos[3], double &rotY)
{

  if (!ServerOK)
  return;

  if (MyID == ID_CLIENT_ONE)
  {
    // Le 3 variáveis "double" na memória, representando a posição do usuário
   ReadFromRemoteMemoryIP (&Pos[0], POS_CLIENT_TWO*sizeof(double),  3*sizeof(double));
    // Le 1 variável "double" na memória, representando a orientação do usuário
   ReadFromRemoteMemoryIP (&rotY, ROT_CLIENT_TWO*sizeof(double),  sizeof(double));
  }

  if (MyID == ID_CLIENT_TWO)
  {
    // Le 3 variáveis "double" na memória, representando a posição do usuário
    ReadFromRemoteMemoryIP (&Pos[0], POS_CLIENT_ONE*sizeof(double),  3*sizeof(double));
    // Le 3 variáveis "double" na memória, representando a posição do usuário
    ReadFromRemoteMemoryIP (&rotY, ROT_CLIENT_ONE*sizeof(double),  sizeof(double));
  }
 }

4.4 Atualização da Cena Gráfica

A escrita e a leitura dos dados é feita a cada frame, de forma a manter os dados dos clietes consistentes.
A rotina UpdateRemoteData, apresentada a seguir, mostra como é feita esta atualização. Esta rotina é chamada de dentro da rotian que redesenha a tela gráfica de OpenGL.
 
// **********************************************************************
//  void UpdateRemoteData()
//  Faz a leitura dos dados do parceiro no AVC a partir
// da RemoteMemory
//   >>>> Arquivo AVC.cpp <<<<
// **********************************************************************
void UpdateRemoteData()
{
 double PartnerPos[3], UserPos[3];
 SmVR_CPoint User, Zero (0,0,0);

 // verifica se há um parceiro
 if (IsPartnerOnLine())
 {
  // se houver, torna seu avatar visível
  PartnerObject->SetVisibility(TRUE);
  // Obtém os dados de sua posição
  LeDadosDaMemoriaRemota(PartnerPos, PartnerRotY);

  printf("Posicao do Parceiro: %7.2f, %7.2f, %7.2f\n", PartnerPos[0], PartnerPos[1], PartnerPos[2]);
  // Atualiza a posição/rotacão do avatar
  PartnerObject->SetLocalTransformationMatrixToIdentity();
  PartnerObject->TranslateBy(PartnerPos[0], PartnerPos[1], PartnerPos[2]);
  PartnerObject->RotateBy(PartnerRotY,0,1,0);
 }

 // Envia sua própria posição/rotacão para a RemoteMemoty
 UserObject->GetPointInOCS(Zero, &User, RootObject);
 UserPos[0] = User.X;
 UserPos[1] = User.Y;
 UserPos[2] = User.Z;
 GravaDadosNaMemoriaRemota(UserPos, UserRotY);

}


Exercício

Copie e descompacte o arquivo AVC.zip.
Abra o arquivo SmallVR_AVC.dev e altere o programa apresentado, de forma que seja possível criar objetos novos (cubos) durante a execução e que esta criação possa ser visualizada pelo paraceiro do usuário que o criar.

Para executar a aplicação-cliente (SmallVR_AVC.exe), rode primeiro o servidor de memória (arquivo RodaMemoryServer). .