Skip to main content
gamedev.net
🔒 Locked 🎮 Unity

OBJ Model does not render correctly

Started by Cortablo Sep 23, 2019 at 4:56 PM 31 replies 13.1k views
Original Post
Cortablo
Cortablo

Hello, if I want to load an OBJ model, it does not look like it should. I have created a template that should render a cube, but the result does not look like a cube. Since my project is very extensive I uploaded it to GitHub where everyone can read through the source code the important files are platform/opengl/OpenGLRenderer.cpp and src/renderer/ObjModel.cpp

Repo: GitHub.com/Cortablo/Cortablo-Engine

I hope you can help me.

greeting
Cortablo

Zakwayda
Zakwayda

You might get a few more eyes if you could provide clickable links, and/or include some code directly here in the forums.

I did take a quick look at the code. It's more than I'm prepared to try to analyze on sight, but here are some suggestions:

- Describe and/or post a screenshot of the rendering results so others can get an idea of how exactly it's not working.

- Manually examine the results of loading the OBJ model (e.g. via logging or the debugger). You might be able to spot obvious errors (especially since it's just a cube), which would indicate that the OBJ loading code might be the problem.

- Determine whether other rendering is working. (E.g. can you render a simple triangle? Another model? A hard-coded model? Etc.)

There are many debugging steps you could take to help isolate the problem. I think the top-level question to try to answer here (to begin with) is whether the problem most likely lies with the OBJ loading code, or with the OpenGL rendering code.

Cortablo
Cortablo

ObjModel::ObjModel(const std::string& filePath)
{
	std::ifstream in(filePath.c_str(), std::ios::in);
	std::string line;

	while (std::getline(in, line))
	{
		if (line.substr(0, 2) == "v ")
		{
			float vertex[3] = { };

			sscanf_s(line.substr(2).c_str(), "%f %f %f\n", &vertex[0], &vertex[1], &vertex[2]);

			m_Vertices.push_back(vertex[0]);
			m_Vertices.push_back(vertex[1]);
			m_Vertices.push_back(vertex[2]);
		}
		else if (line.substr(0, 3) == "vt ")
		{
			float uv[2] = { };

			sscanf_s(line.substr(2).c_str(), "%f %f\n", &uv[0], &uv[1]);

			m_UVs.push_back(uv[0]);
			m_UVs.push_back(uv[1]);
		}
		else if (line.substr(0, 3) == "vn ")
		{
			float normal[3] = { };

			sscanf_s(line.substr(2).c_str(), "%f %f %f\n", &normal[0], &normal[1], &normal[2]);

			m_Normals.push_back(normal[0]);
			m_Normals.push_back(normal[1]);
			m_Normals.push_back(normal[2]);
		}
		else if (line.substr(0, 2) == "f ")
		{
			unsigned int face[9] = { };

			sscanf_s(line.substr(2).c_str(), "%d/%d/%d %d/%d/%d %d/%d/%d\n", &face[0], &face[1], &face[2], &face[3], &face[4], &face[5], &face[6], &face[7], &face[8]);

			for (unsigned int i = 0; i < 9; i++)
			{
				face[i]--;
				m_Faces.push_back(face[i]);
			}
		}
	}
	printf("[Model] (%s) Model loaded!\n", __FUNCTION__);
	printf("[Model] (%s) -> File: %s\n", __FUNCTION__, filePath.c_str());
	printf("[Model] (%s) -> Vertices: %d\n", __FUNCTION__, m_Vertices.size());
	printf("[Model] (%s) -> UVs: %d\n", __FUNCTION__, m_UVs.size());
	printf("[Model] (%s) -> Normals: %d\n", __FUNCTION__, m_Normals.size());
	printf("[Model] (%s) -> Faces: %d\n", __FUNCTION__, m_Faces.size());

	if (m_Vertices.empty())
	{
		printf("[Model] (%s) This model has no vertices!\n", __FUNCTION__);
	}
	if (m_UVs.empty())
	{
		printf("[Model] (%s) This model has no UVs!\n", __FUNCTION__);
	}
	if (m_Normals.empty())
	{
		printf("[Model] (%s) This model has no Normals!\n", __FUNCTION__);
	}
	if (m_Faces.empty())
	{
		printf("[Model] (%s) This model has no Faces!\n", __FUNCTION__);
	}
}

void OpenGLRenderer::Render()
{
	if (!m_RendererQueue.empty())
	{
		for (unsigned int i = 0; i < m_RendererQueue.size(); i++)
		{
			m_Shader->Bind();
			m_RendererQueue[i]->GetTexture()->Bind();

			m_VAO->Bind();

			if (!m_RendererQueue[i]->GetModel()->GetVertices().empty())
			{
				m_VBO = VertexBuffer::Init(&m_RendererQueue[i]->GetModel()->GetVertices().front(), (unsigned int)m_RendererQueue[i]->GetModel()->GetVertices().size() * sizeof(float));
				glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (const void*)0);
			}

			if (!m_RendererQueue[i]->GetModel()->GetUVs().empty())
			{
				m_TexCoord = VertexBuffer::Init(&m_RendererQueue[i]->GetModel()->GetUVs().front(), (unsigned int)m_RendererQueue[i]->GetModel()->GetUVs().size() * sizeof(float));
				glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (const void*)0);
			}

			if (!m_RendererQueue[i]->GetModel()->GetNormals().empty())
			{
				m_Normal = VertexBuffer::Init(&m_RendererQueue[i]->GetModel()->GetNormals().front(), (unsigned int)m_RendererQueue[i]->GetModel()->GetNormals().size() * sizeof(float));
				glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (const void*)0);
			}

			glEnableVertexAttribArray(0);
			if (!m_RendererQueue[i]->GetModel()->GetUVs().empty())
				glEnableVertexAttribArray(1);
			if (!m_RendererQueue[i]->GetModel()->GetNormals().empty())
				glEnableVertexAttribArray(2);

#if 1
			m_IBO = IndexBuffer::Init(&m_RendererQueue[i]->GetModel()->GetFaces().front(), (unsigned int)m_RendererQueue[i]->GetModel()->GetFaces().size() * sizeof(unsigned int));
#endif

			m_ModelMatrix = glm::translate(glm::mat4(1.0f), m_RendererQueue[i]->GetPosition());
			m_ModelMatrix = glm::rotate(m_ModelMatrix, glm::radians(m_RendererQueue[i]->GetRotation().x), glm::vec3(0.0f, 1.0f, 0.0f));
			m_ModelMatrix = glm::rotate(m_ModelMatrix, glm::radians(m_RendererQueue[i]->GetRotation().y), glm::vec3(1.0f, 0.0f, 0.0f));
			m_ModelMatrix = glm::scale(m_ModelMatrix, m_RendererQueue[i]->GetScale());
			m_Shader->SetUniformMatrix4fv("modelMatrix", m_ModelMatrix);

#if 1
			glDrawElements(GL_TRIANGLES, (unsigned int)m_RendererQueue[i]->GetModel()->GetFaces().size(), GL_UNSIGNED_INT, NULL);
#else
			glDrawArrays(GL_TRIANGLES, 0, (unsigned int)m_RendererQueue[i]->GetModel()->GetVertices().size());
#endif

			glDisableVertexAttribArray(0);
			if (!m_RendererQueue[i]->GetModel()->GetUVs().empty())
				glDisableVertexAttribArray(1);
			if (!m_RendererQueue[i]->GetModel()->GetNormals().empty())
				glDisableVertexAttribArray(2);

#if 1
			m_IBO->Unbind();
#endif
			m_VBO->Unbind();
			m_VAO->Unbind();
		}
	}
}

Hello, I totally forgot about the screenshot. The important source code parts are embedded here.

2019-09-23 (16).png

Zakwayda
Zakwayda

Thanks for the screenshot.

Maybe someone else will analyze the code, but meanwhile I'll reiterate my answer from before. There are various steps you could take to debug this. I think a reasonable place to start would be:

- Debug the OBJ loading code in isolation, e.g. by manually checking the output for correctness.

- Check to see if your rendering works in general (e.g. for a simple triangle, another model, etc.).

If you're unsure as to whether the output of your OBJ loading code is correct, you could post it here for people to look at.

bioglaze
bioglaze

You don't seem to be processing the arrays (vertices, normals uvs) to have equal lengths. Also, have you checked that the .obj file has only 3 'f''s per line, meaning that it was exported as triangles?

Cortablo
Cortablo

Hello, I exported in Blender with the option Triangular Faces, but I have 9 "f" per line. What does not mean arrays of the same length?

bioglaze
bioglaze
19 minutes ago, Cortablo said:

What does not mean arrays of the same length?

I meant that in your code, these sizes must be equal:


m_RendererQueue[i]->GetModel()->GetNormals().size()

m_RendererQueue[i]->GetModel()->GetVertices().size()

m_RendererQueue[i]->GetModel()->GetUVs().size()
Green_Baron
Green_Baron

Does it look like this ? (never mind the rounding)


# Blender v2.75 (sub 0) OBJ File: ''
# www.blender.org
o Cube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 1.000000 0.000000 0.000000
vn -0.000000 -0.000000 1.000000
vn -1.000000 -0.000000 -0.000000
vn 0.000000 0.000000 -1.000000
s off
f 1//1 2//1 3//1 4//1
f 5//2 8//2 7//2 6//2
f 1//3 5//3 6//3 2//3
f 2//4 6//4 7//4 3//4
f 3//5 7//5 8//5 4//5
f 5//6 1//6 4//6 8//6

Not sure about the triangulation, but uncheck "export edges" and "export materials". Only positions and normals are needed. Lower block are the indices. The cube has 8 vertices, 6 normals and 24 indices, 4 for each face.

I would calculate the uv coordinates in the shader and from the texture/face ratio. Less data is more speed ?


Edit: these positions and indices can be directly loaded into a vertex array and index buffer and drawn with drawelements(). One can also use 2 triangles per face and draw those, that'll just be more indices (6 per face instead of 4).


Cortablo
Cortablo

Here is my OBJ file, since Visual Studio got a median crunch at the file extension .obj, I just changed the file extension to .mdl.


# Blender v2.80 (sub 75) OBJ File: ''
# www.blender.org
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.375000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.500000
vt 0.375000 0.250000
vt 0.625000 0.500000
vt 0.375000 0.750000
vt 0.625000 0.750000
vt 0.375000 1.000000
vt 0.375000 0.500000
vt 0.125000 0.750000
vt 0.125000 0.500000
vt 0.875000 0.500000
vt 0.625000 0.500000
vt 0.625000 0.250000
vt 0.625000 0.750000
vt 0.625000 1.000000
vt 0.375000 0.750000
vt 0.875000 0.750000
vn 0.0000 1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
s off
f 5/1/1 3/2/1 1/3/1
f 3/4/2 8/5/2 4/6/2
f 7/7/3 6/8/3 8/5/3
f 2/9/4 8/10/4 6/8/4
f 1/11/5 4/12/5 2/13/5
f 5/14/6 2/9/6 6/15/6
f 5/1/1 7/16/1 3/2/1
f 3/4/2 7/7/2 8/5/2
f 7/7/3 5/17/3 6/8/3
f 2/9/4 4/18/4 8/10/4
f 1/11/5 3/19/5 4/12/5
f 5/14/6 1/20/6 2/9/6

EDIT:

I have exported with these options and still have the 9 "f" per line.

Green_Baron
Green_Baron

One group x/y/z is vertex coord. index/texture coord. index/normal index and 3 of these make up a triangle ... iirc ...

Edit: a random idea: do you subtract 1 from the indices ? obj starts at 1, while the rest of the reasonable world count arrays from 0 ...

Cortablo
Cortablo
94/5000
Now the cube renders normally, but how do you add UV indices and normal indices to the IBO?

2019-09-24.png

Green_Baron
Green_Baron

What have you changed ?

The index buffer (ibo) is for drawing only. It defines the order in which the vertices are drawn.

UV coords and normals are grouped together in the vertex array. They are vertex attributes, because they belong to a single vertex (uv and vertex normals) or a group of vertices that form a face (face normals).

Cortablo
Cortablo

Before:


sscanf_s(line.substr(2).c_str(), "%d/%d/%d %d/%d/%d %d/%d/%d\n", &face[0], &face[1], &face[2], &face[3], &face[4], &face[5], &face[6], &face[7], &face[8]);

After:


if (sscanf_s(line.substr(2).c_str(), "%d/%d/%d %d/%d/%d %d/%d/%d\n", &vertexIndex[0], &uvIndex[0], &normalIndex[0], &vertexIndex[1], &uvIndex[1], &normalIndex[1], &vertexIndex[2], &uvIndex[2], &normalIndex[2]) == 9);

Now the question is why the textures are not yet rendered correctly. I have set as a texture a black pink chessboard.

NubDevice
NubDevice

tldr; How do you add UV or normal indices to the IBO? You don't. All attribute arrays must have the same ordering.


nice. very good. I suspect what has changed is, using only index 0, 3 and 6 per face input.

16 hours ago, bioglaze said:

You don't seem to be processing the arrays

I would suggest to go back to this statement from @bioglaze.

What needs to happen now is to rebuild the vert/(uv)/normal arrays and indices. Currently, from these lists, there is not an association with each other. Meaning the vertex at slot n does not automatically mean the uv at slot n are related. Same with the normal list at slot n does not automatically mean it's associated with the vertex at slot n. After load, you need to rebuild the lists per face making a "close enough" comparison from the face list. The problem is a uv seam will force duplicate vertices that may not be in the .obj file lists but is required because the rendering api expects only one list of indices. Vertices with the same exact position, but uv or normal may be different. This explains what @bioglaze is suggesting when mentioning the lists are of same length after processing. Another way to look at it would be, rearrange the lists so that a single index list can be used that correctly points into all array lists.

Dev careful. Pixel on board.
Buckle up. Everything will be revealed.
Green_Baron
Green_Baron

It is probably rendered correctly, but not as expected.

The uvs you get from blender are for a single texture to wrap around the whole cube (look at them, they represent the fractions that correspond to each face.

If you want to map a texture to one face at a time, you must provide (or better calculate in the shader !) texture coords for one face: 1/1, 0/1, 0/0, 1/0 (all floats).

This explains it better than i ever could.

Cortablo
Cortablo
2 minutes ago, Green_Baron said:

calculate in the shader !

Whats the best way to do this?

Green_Baron
Green_Baron
11 minutes ago, Cortablo said:

Whats the best way to do this?

Totally depends on your application's needs and where you have which information available. Pointing out the possibility. A static cube face has fixed uv indices and the face normals are implicitly derived from the winding order, iow it won't change in this case if you just dropped them and set winding order and face culling.

But sorry for muddying the waters.

Do you have a texture that represents a (1) face of a cube and want to project that on each face ? Or do you have a texture that has been unwrapped from a cube and needs to be wrapped around it again ?


Cortablo
Cortablo

Currently the cube should have the same texture on all surfaces.

Green_Baron
Green_Baron

Ok. In this case it looks like the texture is stretched over the whole cube, but it should be projected on each of the faces.

In this case you need to repeat the uv coordinates between 0 and 1 (float) over all of the faces, to tell opengl that this texture spreads as a whole over the four corners of a single face. Face by face.

From this site i stole an example, the six faces of a cube (this one is a unit cube at the centre, reaching from -0.5 to 0.5 on each axis) and the uv coords for a single texture projected on each cube face. A cube face is represented as a quad of 2 triangles. Just to show what i mean, it must be adapted to your needs and it comes with a pair of simple shaders you can find on the page. Hope it helps ?


float vertices[] = {
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
     0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

    -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
    -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
    -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
};


Cortablo
Cortablo

I do not understand how this works, what I'm asking is why in the .obj file are the "vt" lines entered if they do not work?

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.