Sign in to follow this  

MD3loader last question about texturing!

This topic is 2048 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello once again people, I apologize for some much spam and stuff on the forums about my MD3loader but after weeks of coding i have it working nearly 100%, multiple surfaces, animation, loading the md3 file, loading a TGA texture, but im still having one small this step :(
I have the tga loaded but how do i get the texcoords from the md3 loader which i have stored in a vector to the vertex shader and fragment shader to finally complete my md3 loader :)

Here is my whole project
http://gitorious.org/md3simpleloader

But here is my fragment shader, vertex shader, here is my game asset cpp and game asset header and just my Md3loader.cpp

Fragment Shader
[CODE]
#version 120
varying vec2 texCoords;
uniform sampler2D texture1;
void main()
{
gl_FragColor = texture2D(texture1, vec2(texCoords.x,1 - texCoords.y) );
//gl_FragColor = vec4(0.0,1.0,0.0,1.0); // white
}
[/CODE]

Vertex Shader
[CODE]
#version 120
attribute vec3 position;
uniform mat4 mv_matrix;
varying vec2 texcoord;
mat4 view_frustum(
float angle_of_view,
float aspect_ratio,
float z_near,
float z_far
) {
return mat4(
vec4(1.0/tan(angle_of_view), 0.0, 0.0, 0.0),
vec4(0.0, aspect_ratio/tan(angle_of_view), 0.0, 0.0),
vec4(0.0, 0.0, (z_far+z_near)/(z_far-z_near), 1.0),
vec4(0.0, 0.0, -2.0*z_far*z_near/(z_far-z_near), 0.0)
);
}
mat4 translate(float x, float y, float z)
{
return mat4(
vec4(1.0, 0.0, 0.0, 0.0),
vec4(0.0, 1.0, 0.0, 0.0),
vec4(0.0, 0.0, 1.0, 0.0),
vec4(x, y, z, 1.0)
);
}
mat4 rotate_x(float theta)
{
return mat4(
vec4(1.0, 0.0, 0.0, 0.0),
vec4(0.0, cos(theta), sin(theta), 0.0),
vec4(0.0, -sin(theta), cos(theta), 0.0),
vec4(0.0, 0.0, 0.0, 1.0)
);
}
mat4 rotate_y(float theta)
{
return mat4(
vec4(cos(theta), 0.0, sin(theta), 0.0),
vec4(0.0, 1.0, 0.0, 0.0),
vec4(-sin(theta), 0.0, cos(theta), 0.0),
vec4(0.0, 0.0, 0.0, 1.0)
);
}
void main()
{
gl_Position = view_frustum(radians(45.0), 4.0/3.0, 0.5, 100.0)
* mv_matrix
* vec4(position.x, position.y, position.z, 1.0) ;
}
[/CODE]

GameAsset.h
[CODE]
/*
* GameObject.h
*
* Created on: 03-Mar-2009
* Author: balor
*/
#include <stdlib.h>
#include <GL/glew.h>
#include <GL/gl.h>
#include <stdlib.h>
#include <string>
#include <iostream>
#include <fstream>
#include "vectormath/scalar/cpp/vectormath_aos.h"
#include "Camera.h"
#include "BoundingBox.h"
using namespace std;
using namespace Vectormath::Aos;
#ifndef GAMEASSET_H_
#define GAMEASSET_H_
class GameAsset {
public:
GameAsset();
GameAsset(const string & v_shader, const string & f_shader);
virtual ~GameAsset();
bool collidesWith(GameAsset & a);
virtual void draw();
virtual void update()=0;
void updateVertexBuffer();
void *read_tga(const char *filename, int *width, int *height);
GLuint makeTexture(const char *filename,int width,int height);
protected:
/* functions */
int make_resources();
GLchar* shader_file_contents(const string &filename, GLint * length);
GLuint make_buffer(GLenum target, const void *buffer_data, GLsizei buffer_size);
GLuint make_shader(GLenum type, const char *filename);
GLuint make_program(GLuint vertex_shader, GLuint fragment_shader);
/* For keeping track of OpenGL VBOs */
GLuint vertex_buffer, element_buffer;
GLfloat texCoord_buffer;
GLuint vertex_shader, fragment_shader, program;
GLint position_attrib;
GLint mv_matrix_uniform;
GLfloat * g_vertex_buffer_data;
GLfloat * g_texCoord_buffer_data;
GLushort * g_element_buffer_data;
Matrix4 mv_matrix;

// How many vertices/triangles in this model
int num_vertices;
int num_triangles;
BoundingBox * bbox;
private:
void common_init(); // because we don't have delegating constructors yet (http://gcc.gnu.org/projects/cxx0x.html)
string v_shader;
string f_shader;
};
#endif /* GAMEASSET_H_ */
[/CODE]

GameAsset.cpp
[CODE]
/*
* GameObject.cpp
*
* Created on: 03-Mar-2009
* Author: balor
*/
#include "GameAsset.h"
void GameAsset::common_init() {
mv_matrix = Matrix4::identity();
bbox = new BoundingBox(Point3(0,0,0), 1.0, 1.0, 1.0); // unit cube
}
GameAsset::GameAsset() {
common_init();
this->v_shader = "shaders/hello-gl.v.glsl";
this->f_shader = "shaders/hello-gl.f.glsl";
}
GameAsset::GameAsset(const string & v_shader, const string & f_shader) {
common_init();
this->v_shader = v_shader;
this->f_shader = f_shader;
}
GameAsset::~GameAsset() {
// TODO Auto-generated destructor stub
}
bool GameAsset::collidesWith(GameAsset & a ) {
return bbox->collidesWith((*a.bbox));
}
void GameAsset::draw() {
//Need to pass in a GLfloat array of texCoords,
//This gets passed into the vertex shader, and then the fragment shader
//Then the TGA loaded will be mapped around the model
//GLBufferData needs to take in the texCoords
glUseProgram(program);
mv_matrix = Camera::getInstance().getCameraM() * mv_matrix;
// horribe unpacking
GLfloat foo [16] = {
mv_matrix.getElem(0,0), mv_matrix.getElem(0,1), mv_matrix.getElem(0,2), mv_matrix.getElem(0,3),
mv_matrix.getElem(1,0), mv_matrix.getElem(1,1), mv_matrix.getElem(1,2), mv_matrix.getElem(1,3),
mv_matrix.getElem(2,0), mv_matrix.getElem(2,1), mv_matrix.getElem(2,2), mv_matrix.getElem(2,3),
mv_matrix.getElem(3,0), mv_matrix.getElem(3,1), mv_matrix.getElem(3,2), mv_matrix.getElem(3,3)
};
glUniformMatrix4fv(mv_matrix_uniform, 1, false, foo);
//glBindBuffer
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glVertexAttribPointer(
position_attrib, /* attribute */
3, /* size */
GL_FLOAT, /* type */
GL_FALSE, /* normalized? */
0, /* stride */
(void*)0 /* array buffer offset */
);
glEnableVertexAttribArray(position_attrib);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer);
glDrawElements(
GL_TRIANGLES,
3 * this->num_triangles,
GL_UNSIGNED_SHORT,
(GLvoid*) 0
);
glDisableVertexAttribArray(position_attrib);
}
/*
* Functions for general purpose stuff
*/
GLchar * GameAsset::shader_file_contents(const string &filename, GLint * length)
{
ifstream input_file;
input_file.open(filename.c_str(), ios::in);
input_file.seekg(0, ios::end); // go to the end of the file
*length = input_file.tellg(); // get length of the file
input_file.seekg(0, ios::beg); // go to beginning of the file
GLchar * buffer = new GLchar[(*length)+1];
input_file.read(buffer, *length);
buffer[(*length)+1]='\0'; // Ensure null terminated
input_file.close();
return buffer;
}
/*
* Functions for creating OpenGL objects:
*/
GLuint GameAsset::make_buffer(
GLenum target,
const void *buffer_data,
GLsizei buffer_size
) {
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(target, buffer);
glBufferData(target, buffer_size, buffer_data, GL_STATIC_DRAW);
return buffer;
}
GLuint GameAsset::make_shader(GLenum type, const char *filename)
{
GLint length;
GLchar *source = shader_file_contents(filename, &length);
GLuint shader;
GLint shader_ok;
if (!source)
return 0;
shader = glCreateShader(type);
glShaderSource(shader, 1, (const GLchar**)&source, &length);
delete(source);
glCompileShader(shader);
glGetShaderiv(shader, GL_COMPILE_STATUS, &shader_ok);
if (!shader_ok) {
cerr << "Failed to compile" << filename << " with error code " << shader_ok << endl;
glDeleteShader(shader);
return 0;
}
return shader;
}
GLuint GameAsset::make_program(GLuint vertex_shader, GLuint fragment_shader /*GLfloat texCoord*/)
{
GLint program_ok;
GLuint program = glCreateProgram();
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
//glAttachShader(program, texCoord);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &program_ok);
if (!program_ok) {
cerr<< "Failed to link shader program:" << endl;
glDeleteProgram(program);
return 0;
}
return program;
}
/*
* Load and create all of our resources:
*/
int GameAsset::make_resources(void)
{
vertex_buffer = make_buffer(
GL_ARRAY_BUFFER,
g_vertex_buffer_data,
3 * sizeof(GLfloat) * this->num_vertices
);
element_buffer = make_buffer(
GL_ELEMENT_ARRAY_BUFFER,
g_element_buffer_data,
3 * sizeof(GLushort) * this->num_triangles
);
//texcoord = texCoord_buffer;
/*
* texCoord_buffer = make_buffer(
* GL_ARRAY_BUFFER.
* g_texCoord_buffer_data,
* 2 * sizeof(GLfloat) * this->vertices
*/
vertex_shader = make_shader(
GL_VERTEX_SHADER,
this->v_shader.c_str()
);
if (vertex_shader == 0)
return 0;
fragment_shader = make_shader(
GL_FRAGMENT_SHADER,
this->f_shader.c_str()
);
if (fragment_shader == 0)
return 0;


program = make_program(vertex_shader, fragment_shader);
if (program == 0)
return 0;
position_attrib = glGetAttribLocation(program, "position");
mv_matrix_uniform = glGetUniformLocation(program, "mv_matrix");
return 1;
}
void GameAsset::updateVertexBuffer()
{
vertex_buffer = make_buffer(
GL_ARRAY_BUFFER,
g_vertex_buffer_data,
3 * sizeof(GLfloat) * this->num_vertices
);
}
/*
* TGA READER
*/
static short file_short(unsigned char *bytes)
{
return bytes[0] | ((char)bytes[1] << 8);
}
GLuint GameAsset::makeTexture(const char * filename,int width,int height)
{
GLuint texture;
void *pixels = read_tga(filename, &width, &height);
if(!pixels)
{
return 0;
}
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(
GL_TEXTURE_2D, 0,
GL_RGB8,
width, height, 0,
GL_BGR, GL_UNSIGNED_BYTE,
pixels
);
free(pixels);
return texture;
}
void * GameAsset::read_tga(const char *filename, int *width, int *height)
{
struct tga_header {
char id_length;
char color_map_type;
char data_type_code;
unsigned char color_map_origin[2];
unsigned char color_map_length[2];
char color_map_depth;
unsigned char x_origin[2];
unsigned char y_origin[2];
unsigned char width[2];
unsigned char height[2];
char bits_per_pixel;
char image_descriptor;
} header;
int i, color_map_size, pixels_size;
FILE *f;
size_t read;
void *pixels;
f = fopen(filename, "rb");
if (!f) {
fprintf(stderr, "Unable to open %s for reading\n", filename);
return NULL;
}
read = fread(&header, 1, sizeof(header), f);
if (read != sizeof(header)) {
fprintf(stderr, "%s has incomplete tga header\n", filename);
fclose(f);
return NULL;
}
if (header.data_type_code != 2) {
fprintf(stderr, "%s is not an uncompressed RGB tga file\n", filename);
fclose(f);
return NULL;
}
if (header.bits_per_pixel != 24) {
fprintf(stderr, "%s is not a 24-bit uncompressed RGB tga file\n", filename);
fclose(f);
return NULL;
}
for (i = 0; i < header.id_length; ++i)
if (getc(f) == EOF) {
fprintf(stderr, "%s has incomplete id string\n", filename);
fclose(f);
return NULL;
}
color_map_size = file_short(header.color_map_length) * (header.color_map_depth/8);
for (i = 0; i < color_map_size; ++i)
if (getc(f) == EOF) {
fprintf(stderr, "%s has incomplete color map\n", filename);
fclose(f);
return NULL;
}
*width = file_short(header.width); *height = file_short(header.height);
pixels_size = *width * *height * (header.bits_per_pixel/8);
pixels = malloc(pixels_size);
read = fread(pixels, 1, pixels_size, f);
if (read != pixels_size) {
fprintf(stderr, "%s has incomplete image\n", filename);
fclose(f);
free(pixels);
return NULL;
}
return pixels;
}
[/CODE]

Md3asset.cpp
[CODE]
#include "Md3Asset.h"
Md3Asset::Md3Asset(const char * filename, const char * textureFile, bool animated, float x,float y,float z)
{
animation = animated;
md3ModelLoad(filename,textureFile);
//Position
mv_matrix = mv_matrix.translation( Vector3(x,y,z));
}
void Md3Asset::update() {
GameAsset::draw();
}

Md3Asset::~Md3Asset() {
}

void Md3Asset::md3ModelLoad(const char * filename,const char * textureFile)
{
ifstream md3model;
md3model.open(filename, ios::in|ios::binary);
md3_header * md3Header = new md3_header;
md3model.read((char*) md3Header, sizeof (struct md3_header));
// check for the correct MD3 header and version number 15
if ((md3Header->ident != 860898377 )||(md3Header->version < 15))
{
cout << "Error: bad version or identifier" << endl;
}
int totalVertices = 0;
int totalTriangles = 0;
this->num_triangles = 0;
this->num_vertices = 0;
this->num_surfaces = md3Header->num_surfaces;
frameState = 0;
int surface_offsets[num_surfaces];
int offset = md3Header->ofs_surfaces;
surface_offsets[0] = md3Header->ofs_surfaces;
//Multiple surfaces
for(int i = 0;i < num_surfaces; i++)
{
//Get surface data
this->surfaces = new md3_surface;
md3model.seekg(surface_offsets[i], ios::beg);
md3model.read((char *) surfaces, sizeof (struct md3_surface));
offset = surface_offsets[i] + surfaces->ofs_triangles;
if(i < num_surfaces - 1)
{
surface_offsets[i+1] = surface_offsets[i] + surfaces->ofs_end;
}//End of num surfaces
md3model.seekg(offset); //seek to triangles;
totalVertices += surfaces->num_verts;
//Get triangle data
for (int j = 0; j < surfaces->num_triangles; j++)
{
this->triangles = new md3_triangle;
md3model.read((char *) triangles, sizeof (struct md3_triangle));
vecTriangles.push_back(triangles);
totalTriangles++;
}//End of triangle data
//Get shader data
offset = surface_offsets[i] + surfaces->ofs_shaders;
md3model.seekg(offset);
for (int j = 0;j < surfaces->num_shaders; j++)
{
this->shaders = new md3_shader;
md3model.read((char *)shaders, sizeof (struct md3_shader));
vecShaders.push_back(shaders);
}//End of shader data
//Get texCoord data
offset = surface_offsets[i] + surfaces->ofs_st;
md3model.seekg(offset);
for (int j = 0;j < surfaces->num_verts; j++)
{
this->coords = new md3_texcoord;
md3model.read((char *)coords, sizeof (struct md3_texcoord));
coords->st[1] = 1 - coords->st[1];
vecTexCoords.push_back(coords);
}//End of texCoord data
//Get vertex data
offset = surface_offsets[i] + surfaces->ofs_xyznormal;
md3model.seekg(offset);
for (int j = 0;j < (surfaces->num_verts * surfaces->num_frames); j++)
{
this->vertices = new md3_vertex;
md3model.read((char *)vertices, sizeof(struct md3_vertex));
vecVertices.push_back(vertices);
}//End of vertex data
cout << "Frames :" << surfaces->num_frames << endl;
vec_Surfaces.push_back(surfaces);
cout << "Triangles :" << vecTriangles.at(0)->indexes[0] << endl;
cout << "Triangles :" << vecTriangles.at(0)->indexes[1] << endl;
cout << "Triangles :" << vecTriangles.at(0)->indexes[2] << endl;
vec_Triangles.push_back(vecTriangles);
vecTriangles.clear();
vec_Shaders.push_back(vecShaders);
vecShaders.clear();
vec_Vertices.push_back(vecVertices);
vecVertices.clear();
vec_TexCoords.push_back(vecTexCoords);
vecTexCoords.clear();
}//End of surfaces
//Tag Data
offset = md3Header->ofs_tags;
md3model.seekg(offset);
for (int i = 0; i < md3Header->num_frames; i++) {
for (int j = 0; j < md3Header->num_tags; j++) {
this->tags = new md3_tag;
md3model.read((char *) tags, sizeof (struct md3_tag));
vecTags.push_back(tags);
}
vec_Tags.push_back(vecTags);
vecTags.clear();
}
this->num_triangles = totalTriangles;
this->num_vertices = totalVertices;
//Update vertex and element buffer
this->g_element_buffer_data = new GLushort[totalTriangles * 3];
this->g_vertex_buffer_data = new GLfloat[totalVertices * 3];
float md3_xyzScale = 1.0/64;
int aa = 0;
int dd = 0;
int bb = 0;
for (int i = 0; i < md3Header->num_surfaces; i++)
{
totalTriangles = vec_Surfaces.at(i)->num_triangles;
totalVertices = vec_Surfaces.at(i)->num_verts;
//element buffer
for (int j = 0; j < totalTriangles; j++)
{
this->g_element_buffer_data[aa++] = vec_Triangles.at(i).at(j)->indexes[0] + dd;
this->g_element_buffer_data[aa++] = vec_Triangles.at(i).at(j)->indexes[1] + dd;
this->g_element_buffer_data[aa++] = vec_Triangles.at(i).at(j)->indexes[2] + dd;
}
dd += vec_Surfaces.at(i)->num_verts;
for (int j = 0; j < totalVertices; j++)
{
this->g_vertex_buffer_data[bb++]= /*vec_Tags.at(0).at(0)->origin[0] +*/ vec_Vertices.at(i).at(j)->coord[0] * md3_xyzScale;
this->g_vertex_buffer_data[bb++]= vec_Vertices.at(i).at(j)->coord[1] * md3_xyzScale;
this->g_vertex_buffer_data[bb++]= vec_Vertices.at(i).at(j)->coord[2] * md3_xyzScale;
}
}
cout << "Vertex buffer updated & Element buffer updated" << endl;
//Position
//BoundingBox * pos_box = bbox;
// bbox = new BoundingBox(Point3(x, y, z), 1.0, 1.0, 1.0);
makeTexture(textureFile, 256, 256);
make_resources();
}
void Md3Asset::animationUpdate()
{
//FRAME INTERPOLATION not implemented yet!!!,
//Animation will look wrong or out of place with multiple surfaces
float md3_xyzScale = 1.0/64;
int cc = 0;
if(animation == true){
int totalAniVerts = 0;
for(int i = 0;i < vec_Surfaces.size(); i++)
{
totalVertices = vec_Surfaces.at(i)->num_verts;
if(frameState >= (vec_Surfaces.at(i)->num_frames -1))
{
frameState = 0;
}
frameState++;
for (int j = 0; j < totalVertices; j++)
{
this->g_vertex_buffer_data[cc++]= vec_Vertices.at(i).at(j + (totalVertices * frameState))->coord[0] * md3_xyzScale;
this->g_vertex_buffer_data[cc++]= vec_Vertices.at(i).at(j + (totalVertices * frameState))->coord[1] * md3_xyzScale;
this->g_vertex_buffer_data[cc++]= vec_Vertices.at(i).at(j + (totalVertices * frameState))->coord[2] * md3_xyzScale;
}
}
make_resources();
}
//updateVertexBuffer();
}
[/CODE]

oohh and for luck Md3Asset.h
[CODE]
#include <GL/glew.h>
#include <GL/gl.h>
#include <stdlib.h>
#include <iostream>
#include <fstream>
#include <vector>
#include "GameAsset.h"
using namespace std;
#ifndef Md3Asset_H_
#define Md3Asset_H_
class Md3Asset : public GameAsset {
public:
Md3Asset();
Md3Asset(const char * filename, const char * textureFile, bool animated, float x,float y,float z);
virtual ~Md3Asset();
virtual void update();
void animationUpdate();
private:
void md3ModelLoad(const char * filename,const char * textureFile);
bool animation;
//Defines
typedef float vec3[3];
//MD3 Structures
struct md3_header
{
int ident; //IDP3
int version; //Version of MD3
char name[64]; //Name of MD3
int flags;
int num_frames; //Number of frames in animation
int num_tags; //Number of tags for attachment
int num_surfaces; //Number of animated textures
int num_skins; //Number of skins
int ofs_frames; //Offset where information for frames start
int ofs_tags; //Offset where information for tags start
int ofs_surfaces; //Offset where information for surfaces start
int ofs_eofs; //Offset where information for file ends
};
// Frame info
struct md3_frame
{
vec3 min_bounds; //First corner of bounding box
vec3 max_bounds; //Second corner of bounding box
vec3 local_origin; //Local origin
float radius; //Radius of bounding sphere
char name[16]; //Name of the frame
};
// Tag info
struct md3_tag
{
char name[64]; //Name of the tag
vec3 origin; //Coordinates of tag object
vec3 axis[3]; //3x3 rotational matrix
};
// Surface info
struct md3_surface
{
int ident; //IDP3
char name[64]; //Name of the surface
int flags; //Flags
int num_frames; //Number of frames in surface
int num_shaders; //Number of shaders in surface
int num_verts; //Number of vertices in surface
int num_triangles; //Number of triangles in surface
int ofs_triangles; //Offset of triangles in surface
int ofs_shaders; //Offset of shaders in surface
int ofs_st; //Offset of where s&t texture coordinates start
int ofs_xyznormal; //Offset where vertex object starts
int ofs_end; //Offset where surface ends
};
// Shader info
struct md3_shader
{
char name[64]; //Name of the shader
int shader_index; //Shader index number
};
// Triangle info
struct md3_triangle
{
int indexes[3]; //Index of triangle vertices
};
// Texture coordinate info
struct md3_texcoord
{
float st[3]; //S&T texture coordinates
};
// Vertex info
struct md3_vertex
{
short coord[3]; //x,y,z coordinate
char normal[2]; //Angle of normal vector
};

md3_frame * frames;
md3_tag * tags;
md3_surface * surfaces;
md3_triangle * triangles;
md3_vertex * vertices;
md3_shader * shaders;
md3_texcoord * coords;
int num_frames;
int num_tags;
int num_surfaces;
int num_shaders;
int totalVertices;
int totalTriangles;
//Animation
int frameState;
//Testing multiple surfaces
vector<md3_surface *> vec_Surfaces;
vector<vector<md3_triangle *> >vec_Triangles;
vector<md3_triangle *> vecTriangles;
vector<vector<md3_shader *> > vec_Shaders;
vector<md3_shader *> vecShaders;
vector<vector<md3_texcoord *> > vec_TexCoords;
vector<md3_texcoord *> vecTexCoords;
vector<vector<md3_vertex *> >vec_Vertices;
vector<md3_tag *> vecTags;
vector<vector<md3_tag *> > vec_Tags;
vector<md3_vertex *>vecVertices;
};
#endif

[/CODE]

Hope to hear from you soon ppl

Canvas

Share this post


Link to post
Share on other sites

This topic is 2048 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this