Interpolating between mesh keyframes causes mesh to break

This has been solved! The issue wasn’t with my code! It was with Blender messing up indexes when the “Triangulate faces” option was checked. I instead selected all faces with Ctrl + A, then pressed CTRL + T to manually triangulate everything and then ignored the option in exporting. It fixed everything :slight_smile:

I’m trying to interpolate between two VBOs to create animation (I know this can be costly, not here to discuss that). It seems the in-between state of the “missing” frame is breaking. So if I have keyframe 1 and want to interp to keyframe 3, I want the interp to obviously “make” frame 2 by smoothing, but frame 2 ends up broken.

If I don’t use any interpolation, and simply “hard” render from frame to frame, none of the meshes break and everything renders fine besides the fact it’s not smooth, which obviously we want smooth, non choppy animation.

Heres a gif showing what it looks like:

Another gif showing the issue. You can see for some reason it’s almost “rotating” the verts in the leg.

Here’s an image with some more info too:

I don’t know if this is an issue with the same verts not being manipulated or what, I don’t even know how to go about a good way debugging it to see if it’s the proper vertex being moved.

I am using blender to export in obj format with these settings:

Vertex Shader:


    layout (location = 0) in vec3 a_pos;
    layout (location = 1) in vec2 a_tex_coord;
    layout (location = 2) in vec3 a_norm;
    layout (location = 3) in vec3 next_a_pos;
    layout (location = 4) in vec2 next_a_tex_coord;
    layout (location = 5) in vec3 next_a_norm;
    
    uniform float interpolation;
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    out vec2 tex_coord;
    
    
    void 
    main()
    {
    	
    	vec3 vert_interp = mix(a_pos, next_a_pos, interpolation);
    	vec3 norm_interp = mix(a_norm, next_a_norm, interpolation);
    	
    	vec4 world_pos = model * vec4(vert_interp, 1.0f);
    
    	gl_Position = projection * view * world_pos;
    	tex_coord = vec2(a_tex_coord.x, 1.0 - a_tex_coord.y);
    
    }

Could it be because of how I am loading in my .obj file? I have written a custom parser, maybe there is a chance I’ve messed something up? It’s confusing because without interpolating, the meshes are correct and working.

obj_mdl.h

    #pragma once
    
    #include <cglm/cglm.h>
    #include <stdint.h>
    
    #define VERTS_IN_BYTES(a) (a * sizeof(struct vertex))
    #define INDICES_IN_BYTES(a) (a * sizeof(uint32_t))
    
    struct
    vertex
    {
    	vec3 pos;
    	vec2 uv;
    	vec3 norm;
    };
    
    struct
    obj_mdl
    {
    	struct vertex *verts;
    	uint32_t verts_size;
    	uint32_t vert_count;
    
    	uint32_t *indices;
    	uint32_t indices_size;
    	uint32_t indice_count;
    };
    
    struct
    obj_mdl_face
    {
    	vec3 vert_index;
    	vec3 uv_index;
    	vec3 norm_index;
    };
    
    struct
    obj_mdl_data
    {
    	vec3 *verts;
    	uint32_t vert_count;
    	vec2 *uvs;
    	uint32_t uv_count;
    	vec3 *norms;
    	uint32_t norm_count;
    	
    	struct obj_mdl_face *faces;
    	uint32_t face_count;
    };
    
    bool obj_mdl_load(struct obj_mdl *mdl, const char *path);
    void obj_mdl_destroy(struct obj_mdl *mdl);

obj_mdl.c

    #include <stdio.h>
    #include <string.h>
    #include "../io/io.h"
    #include "obj_mdl.h"
    
    static void
    obj_mdl_load_prepare_data(struct obj_mdl_data *data, const char *path)
    {	
    	FILE *fp = fopen(path, "r");
    	
    	while (1)
    	{
    		char line[256];
    
    		int32_t res = fscanf(fp, "%s", line);
    		if (res == EOF) break;
    
    		if (strcmp(line, "v") == 0) data->vert_count++;
    		else if (strcmp(line, "vt") == 0) data->uv_count++;
    		else if (strcmp(line, "vn") == 0) data->norm_count++;
    		else if (strcmp(line, "f") == 0) data->face_count++;
    	}
    
    	data->verts = malloc(sizeof(vec3) * data->vert_count); 
    	data->uvs = malloc(sizeof(vec2) * data->uv_count);
    	data->norms = malloc(sizeof(vec3) * data->norm_count);
    	data->faces = malloc(sizeof(struct obj_mdl_face) * data->face_count);
    
    	fclose(fp);
    }
    
    static void
    obj_mdl_load_process_data(struct obj_mdl *mdl, struct obj_mdl_data *data)
    {
    	
    	struct vertex *verts = malloc(sizeof(struct vertex) * (data->face_count * 3));
    	uint32_t vert_count = 0;
    	uint32_t *indices = malloc(sizeof(struct vertex) * (data->face_count * 3));
    
    	for (uint32_t i = 0; i < data->face_count; i++)
    	{
    		for (int j = 0; j < 3; j++)
    		{
    			struct vertex v;	
    			int32_t vert_index = (int32_t)(data->faces[i].vert_index[j]-1);
    			int32_t uv_index = (int32_t)(data->faces[i].uv_index[j]-1);
    			int32_t norm_index = (int32_t)(data->faces[i].norm_index[j]-1);
    			memcpy(v.pos, data->verts[vert_index], sizeof(vec3));
    			memcpy(v.uv, data->uvs[uv_index], sizeof(vec2));
    			memcpy(v.norm, data->norms[norm_index], sizeof(vec3));
    			indices[vert_count] = vert_count;
    			verts[vert_count] = v;
    			vert_count++;
    		}
    	}
    
    	uint32_t indice_count = vert_count;
    
    	uint32_t verts_size = VERTS_IN_BYTES(vert_count);
    	mdl->verts = malloc(verts_size);
    	mdl->verts_size = verts_size;
    	mdl->vert_count = vert_count;
    	memcpy(mdl->verts, verts, verts_size);
    	
    	uint32_t indices_size = INDICES_IN_BYTES(indice_count);
    	mdl->indices = malloc(indices_size);
    	mdl->indices_size = indices_size;
    	mdl->indice_count = indice_count;
    	memcpy(mdl->indices, indices, indices_size);
    
    	free(verts);
    	free(indices);
    }
    
    bool
    obj_mdl_load(struct obj_mdl *mdl, const char *path)
    {
    	/* Currently we assume that faces will always have the 3 values, but it could be 
    	 * "f 4//5" for example if theres no tex coords, so we should eventually check that */
    
    	FILE *fp = fopen(path, "r");
    	if (fp == NULL)
    	{
    		 return false;
    	}
    
    	struct obj_mdl_data data = {NULL, 0, NULL, 0, NULL, 0, NULL, 0};
    	obj_mdl_load_prepare_data(&data, path);
    
    	uint32_t v_i = 0, u_i = 0, n_i = 0, f_i = 0;
    
    	while (1)
    	{
    		char line[256];
    
    		int32_t res = fscanf(fp, "%s", line);
    		if (res == EOF) break;
    
    		if (strcmp(line, "v") == 0)
    		{
    			fscanf(fp, "%f %f %f
", &data.verts[v_i][0], &data.verts[v_i][1], &data.verts[v_i][2]);	
    			v_i++;
    		}
    		else if (strcmp(line, "vt") == 0)
    		{
    			fscanf(fp, "%f %f
", &data.uvs[u_i][0], &data.uvs[u_i][1]);
    			u_i++;
    		}
    		else if (strcmp(line, "vn") == 0)
    		{
    			fscanf(fp, "%f %f %f
", &data.norms[n_i][0], &data.norms[n_i][1], &data.norms[n_i][2]);	
    			n_i++;
    		}
    		else if (strcmp(line, "f") == 0)
    		{
    			/* vert / uv / norm */
    			fscanf(fp, "%f/%f/%f %f/%f/%f %f/%f/%f
", 
    					&data.faces[f_i].vert_index[0], 
    					&data.faces[f_i].uv_index[0], 
    					&data.faces[f_i].norm_index[0], 
    					&data.faces[f_i].vert_index[1], 
    					&data.faces[f_i].uv_index[1], 
    					&data.faces[f_i].norm_index[1], 
    					&data.faces[f_i].vert_index[2], 
    					&data.faces[f_i].uv_index[2], 
    					&data.faces[f_i].norm_index[2]);
    
    			f_i++;
    		}
    	}
    	
    	fclose(fp);
    
    	obj_mdl_load_process_data(mdl, &data);
    
    	free(data.verts);
    	free(data.uvs);
    	free(data.norms);
    	free(data.faces);
    
    	printf("Verts: %u UV: %u Normals: %u Faces: %u
", 
    			data.vert_count, data.uv_count, data.norm_count, data.face_count);
    	
    
    	return true;
    }
    
    void 
    obj_mdl_destroy(struct obj_mdl *mdl)
    {
    	free(mdl->verts);
    	free(mdl->indices);
    }

To render, I simply generate VAO, VBO, EBO out of obj_mdl struct verts and indices, and then use this:


	glBindVertexArray(a->vaos[a->cur_frame]);

	glBindBuffer(GL_ARRAY_BUFFER, a->vbos[a->cur_frame]);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)0);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)(3 * sizeof(float)));
	glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)(5 * sizeof(float)));
			
	glBindBuffer(GL_ARRAY_BUFFER, a->vbos[a->next_frame]);
	glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)0);
	glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)(3 * sizeof(float)));
	glVertexAttribPointer(5, 3, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)(5 * sizeof(float)));

	for (uint32_t i = 0; i < 6; i++) glEnableVertexAttribArray(i);

	glBindVertexArray(0);

	a->interp += perc; 

	shader_set_float(SHADER_DEFAULT, "interpolation", a->interp);

I suspect that the different frames have different topology (the way that the quad rotates in the second GIF supports this theory). IOW, a given vertex index refers to different vertices in different frames. In order to interpolate meshes, each vertex must have the same index in every frame. If you’re saving each frame as a separate OBJ file, you may not be able to guarantee this. And even if you can, there are issue with your loader which mean that this isn’t enough.

Colour vertices based upon their index. Step through the frames and see if the colours change for a particular vertex.

First, I’d suggest turning off the “Triangulate Faces” option. If you have non-triangular faces, triangulate them in the OBJ importer. It’s quite possible that Blender takes the shape of the face into account when triangulating, meaning that the triangulation will change between frames. This would be particularly significant in this case as your loader is creating 3 vertices for each triangle based upon their order in the file. So even if Blender maintains consistent indices between frames, these won’t necessarily be preserved by your loader.

Ideally, the loader should create one vertex for each distinct combination of position, normal and texture-coordinate indices, and do so in a way that’s deterministic (i.e. two OBJ files with identical sets of vertices will get identical indices). In C++, you’d typically use std::map or std::unordered_map; in C, you’ll need to either implement a similar structure (e.g. btree or hashtable) or find a library which provides it. For determinism, you’d create the mapping from <v,vn,vt> to the index while loading the first frame, and re-use it for subsequent frames.

Vertices which share a position usually share texture coordinates (the exception being vertices on a texture seam. If you’re using smooth shading (rather than flat shading), vertices not on a sharp edge (crease) will share the normal. So creating 3 distinct vertices for each triangle enlarges the size of the vertex arrays. Even more so if you’re triangulating faces, as all but 2 vertices for each face will be duplicated, and when you duplicate a vertex you’re guaranteed to be able to share all of the attributes between the duplicates.

I have solved the issue… it turned out to be something you had mentioned… Blender triangulating faces!

So instead of using the option in the exporter, I instead manually triangluated the faces and skipped using the option on export… and it all works now!

Thanks for all your help regardless, I greatly appreciate it!