Gama C Library
Gama C API Documentation
obj.h
Go to the documentation of this file.
1/**
2 * @file obj.h
3 * @brief Implements a Wavefront OBJ 3D model file loader.
4 *
5 * This file provides functionality to parse .obj files and load their
6 * geometric and material data into a `gm3Mesh` structure. It also handles
7 * the parsing of associated .mtl files indirectly via `mtl.h`.
8 */
9#pragma once
10
11#include <ctype.h> // For isspace, isalpha
12#include <float.h> // For DBL_MAX
13#include <stddef.h> // For size_t
14#include <stdint.h> // For int32_t
15#include <stdio.h> // For snprintf
16#include <stdlib.h> // For malloc, realloc, free, strtod, strtoll
17#include <string.h> // For strcmp, strncmp, memset, strlen
18
19#include "mesh.h"
20#include "mtl.h"
21#include "position.h"
22
23// --- Structures ---
24
25/**
26 * @internal
27 * @brief Represents a single parsed line from an OBJ file.
28 */
29typedef struct {
30 char type; /**< The type of OBJ line (e.g., 'v', 'f', 'L' for mtllib, 'U' for usemtl). */
31 char name[128]; /**< For mtllib and usemtl names. */
32 double points[3]; /**< Stores vertex/normal/texcoord data. */
33 long long indices[64][3]; /**< Stores face indices [v, vt, vn] for up to 64 vertices. */
34 size_t n_indices; /**< Number of indices parsed for a face. */
36
37/**
38 * @internal
39 * @brief Skips leading whitespace characters in a string.
40 * @param s The string to process.
41 * @return A pointer to the first non-whitespace character in the string.
42 */
43static inline char *gmu_skip_ws(char *s) {
44 while (*s && *s == ' ')
45 s++;
46 return s;
47}
48
49/**
50 * @internal
51 * @brief Parses a single index value from an OBJ face line.
52 * @param ptr A pointer to the current position in the line string.
53 * @return The parsed index, or 0 if no valid index is found.
54 */
55static long long _gm3u_parse_idx(char **ptr) {
56 if (!ptr || !*ptr || isspace((unsigned char)**ptr) || **ptr == '/')
57 return 0;
58 char *end;
59 long long val = strtoll(*ptr, &end, 10);
60 *ptr = end;
61 return val;
62}
63
64// --- Parsing Logic ---
65
66/**
67 * @internal
68 * @brief Parses the next line from an OBJ file content string.
69 *
70 * This function identifies the type of OBJ line (vertex, face, normal,
71 * texture coordinate, material library, use material, object/group)
72 * and extracts its relevant data into a `gm3ObjLine` struct.
73 *
74 * @param end A pointer to the current read position in the OBJ content string.
75 * This will be updated to point to the start of the next line.
76 * @param ln A pointer to the `gm3ObjLine` struct to populate with parsed data.
77 * @return 1 if a line was successfully parsed, 0 if the end of content is reached.
78 */
79int gm3_obj_parse_next_line(char **end, gm3ObjLine *ln) {
80 memset(ln, 0, sizeof(*ln));
81 ln->type = '?';
82
83 char *p = gmu_skip_ws(*end);
84 if (*p == '\0')
85 return 0;
86
87 if (p[0] == 'v' && isspace((unsigned char)p[1])) {
88 ln->type = 'v';
89 p = gmu_skip_ws(p + 1);
90 ln->points[0] = strtod(p, &p);
91 ln->points[1] = strtod(p, &p);
92 ln->points[2] = strtod(p, &p);
93 } else if (p[0] == 'v' && p[1] == 'n') {
94 ln->type = 'n';
95 p = gmu_skip_ws(p + 2);
96 ln->points[0] = strtod(p, &p);
97 ln->points[1] = strtod(p, &p);
98 ln->points[2] = strtod(p, &p);
99 } else if (p[0] == 'v' && p[1] == 't') {
100 ln->type = 't';
101 p = gmu_skip_ws(p + 2);
102 ln->points[0] = strtod(p, &p);
103 ln->points[1] = strtod(p, &p);
104 } else if (p[0] == 'f' && isspace((unsigned char)p[1])) {
105 ln->type = 'f';
106 p = gmu_skip_ws(p + 1);
107 while (*p != '\0' && *p != '\n' && *p != '\r' && ln->n_indices < 64) {
108 ln->indices[ln->n_indices][0] = _gm3u_parse_idx(&p);
109 if (*p == '/') {
110 p++;
111 ln->indices[ln->n_indices][1] = _gm3u_parse_idx(&p);
112 if (*p == '/') {
113 p++;
114 ln->indices[ln->n_indices][2] = _gm3u_parse_idx(&p);
115 }
116 }
117 ln->n_indices++;
118 p = gmu_skip_ws(p);
119 }
120 } else if (strncmp(p, "mtllib", 6) == 0) {
121 ln->type = 'L';
122 gm3u_str_copy_eol(ln->name, p + 6, sizeof(ln->name));
123 } else if (strncmp(p, "usemtl", 6) == 0) {
124 ln->type = 'U';
125 gm3u_str_copy_eol(ln->name, p + 6, sizeof(ln->name));
126 } else if (p[0] == 'o' || p[0] == 'g') {
127 ln->type = p[0];
128 gm3u_str_copy_eol(ln->name, p + 1, sizeof(ln->name));
129 } else if (p[0] == '#') {
130 ln->type = '#';
131 }
132
133 while (*p && *p != '\n')
134 p++;
135 if (*p == '\n')
136 p++;
137 *end = p;
138 return 1;
139}
140
141/**
142 * @internal
143 * @brief Normalizes an index read from an OBJ file.
144 *
145 * OBJ indices can be 1-based (positive), relative to the end (-negative),
146 * or 0 (fallback). This converts them to 0-based absolute indices.
147 *
148 * @param idx A pointer to the index to normalize.
149 * @param total The total number of elements in the corresponding array (e.g., total vertices).
150 */
151static void _gm3u_normalize_idx(long long *idx, size_t total) {
152 if (*idx > 0)
153 *idx -= 1;
154 else if (*idx < 0)
155 *idx += (long long)total;
156 else
157 *idx = 0; // fallback
158}
159
160/**
161 * @internal
162 * @brief Parses the entire content of an OBJ file into an array of `gm3ObjLine` structs.
163 * @param content The null-terminated string content of the OBJ file.
164 * @param result A pointer to a `gm3ObjLine**` that will store the parsed lines. This memory
165 * is dynamically allocated and must be freed by the caller.
166 * @param n_lines A pointer to a `size_t` that will store the number of parsed lines.
167 * @return 0 on success, -1 on memory allocation failure.
168 */
169int gm3_obj_parse(char *content, gm3ObjLine **result, size_t *n_lines) {
170 char *pos = content;
171 size_t buffer_size = 128;
172 size_t buffer_index = 0;
173 gm3ObjLine *buffer = malloc(buffer_size * sizeof(gm3ObjLine));
174 if (!buffer)
175 return -1;
176
177 gm3ObjLine ln;
178 while (gm3_obj_parse_next_line(&pos, &ln)) {
179 if (buffer_index >= buffer_size) {
180 size_t new_size = buffer_size * 2;
181 gm3ObjLine *newbuff = realloc(buffer, new_size * sizeof(gm3ObjLine));
182 if (!newbuff) {
183 free(buffer);
184 return -1;
185 }
186 buffer = newbuff;
187 buffer_size = new_size;
188 }
189 buffer[buffer_index++] = ln;
190 }
191
192 *n_lines = buffer_index;
193 *result = buffer;
194 return 0;
195}
196
197/**
198 * @brief Loads a Wavefront OBJ 3D model from a file into a `gm3Mesh` structure.
199 *
200 * This function parses the `.obj` file, including vertices, normals, texture
201 * coordinates, and faces. It also loads associated `.mtl` material libraries.
202 * The `gm3Mesh` will be populated with all the data.
203 *
204 * @param m A pointer to the `gm3Mesh` structure to populate.
205 * @param path The file path to the .obj model.
206 * @param dir The directory containing the .obj file, used for resolving relative .mtl paths.
207 * @return 0 on success, -1 on file reading or parsing failure, or a negative
208 * value from `gm3_mtl_load` on material loading failure.
209 */
210int32_t gm3_obj_load(gm3Mesh *m, const char *path, const char *dir) {
211 char *content = NULL;
212 size_t content_len;
213 if (gmu_read_file(path, &content, &content_len) < 0)
214 return -1;
215
216 gm3ObjLine *parsed;
217 size_t n_lines;
218 memset(m, 0, sizeof(*m));
219
220 if (gm3_obj_parse(content, &parsed, &n_lines) < 0) {
221 free(content);
222 return -1;
223 }
224 free(content);
225
226 // 1. Load Material Libraries
227 for (size_t i = 0; i < n_lines; i++) {
228 if (parsed[i].type == 'L') {
229 char mtl_path[2048];
230 snprintf(mtl_path, sizeof(mtl_path), "%s/%s", dir, parsed[i].name);
231 gm3MtlLib mf;
232 memset(&mf, 0, sizeof(mf));
233 int ret = gm3_mtl_load(&mf, mtl_path, dir);
234 if (ret < 0)
235 return ret;
236 if (ret >= 0) {
237 gm3MtlLib *tmp =
238 realloc(m->mtllibs, sizeof(gm3MtlLib) * (m->n_mtllibs + 1));
239 if (tmp) {
240 m->mtllibs = tmp;
241 m->mtllibs[m->n_mtllibs++] = mf;
242 }
243 }
244 }
245 }
246
247 // 2. Pass: Count Totals
248 size_t total_v = 0, total_f = 0, total_n = 0, total_t = 0;
249 for (size_t i = 0; i < n_lines; i++) {
250 if (parsed[i].type == 'v')
251 total_v++;
252 else if (parsed[i].type == 'n')
253 total_n++;
254 else if (parsed[i].type == 't')
255 total_t++;
256 else if (parsed[i].type == 'f' && parsed[i].n_indices >= 3) {
257 total_f += (parsed[i].n_indices - 2); // Triangulation
258 }
259 }
260
261 m->n_vertices = total_v;
262 m->n_faces = total_f;
263 m->n_normals = total_n;
264 m->n_texs = total_t;
265 m->vertices = calloc(total_v, sizeof(gm3Pos));
266 m->faces = calloc(total_f, sizeof(gm3MeshFace));
267 m->normals = calloc(total_n, sizeof(gm3Pos));
268 m->texs = calloc(total_t, sizeof(gmPos));
269
270 // 3. Pass: Store Data
271 size_t cv = 0, cf = 0, cn = 0, ct = 0;
272 int active_mat_file = -1;
273 int active_mat = -1;
274
275 for (size_t i = 0; i < n_lines; i++) {
276 gm3ObjLine *ln = &parsed[i];
277 if (ln->type == 'L') {
278 for (size_t j = 0; j < m->n_mtllibs; j++)
279 if (strcmp(m->mtllibs[j].name, ln->name) == 0) {
280 active_mat_file = (int)j;
281 break;
282 }
283 } else if (ln->type == 'U') {
284 if (active_mat_file >= 0)
285 gm3_mtl_find_mat(&m->mtllibs[active_mat_file], ln->name, &active_mat);
286 } else if (ln->type == 'v') {
287 m->vertices[cv++] = (gm3Pos){ln->points[0], ln->points[1], ln->points[2]};
288 } else if (ln->type == 'n') {
289 m->normals[cn++] = (gm3Pos){ln->points[0], ln->points[1], ln->points[2]};
290 } else if (ln->type == 't') {
291 m->texs[ct++] = (gm3Tex){ln->points[0], ln->points[1]};
292 } else if (ln->type == 'f') {
293 // Normalize all indices for this face
294 for (size_t j = 0; j < ln->n_indices; j++) {
295 _gm3u_normalize_idx(&ln->indices[j][0], total_v);
296 _gm3u_normalize_idx(&ln->indices[j][1], total_t);
297 _gm3u_normalize_idx(&ln->indices[j][2], total_n);
298 }
299 // Triangulate (Fan)
300 for (size_t j = 0; j < ln->n_indices - 2; j++) {
301 gm3MeshFace *face = &m->faces[cf++];
302 face->vertices[0] = (size_t)ln->indices[0][0];
303 face->vertices[1] = (size_t)ln->indices[j + 1][0];
304 face->vertices[2] = (size_t)ln->indices[j + 2][0];
305 face->uvs[0] = (size_t)ln->indices[0][1];
306 face->uvs[1] = (size_t)ln->indices[j + 1][1];
307 face->uvs[2] = (size_t)ln->indices[j + 2][1];
308 face->material_file = active_mat_file;
309 face->material = active_mat;
310
311 // Normal calculation
312 gm3Pos e1 = m->vertices[face->vertices[1]];
313 gm3_pos_substract(&e1, &m->vertices[face->vertices[0]]);
314 gm3Pos e2 = m->vertices[face->vertices[2]];
315 gm3_pos_substract(&e2, &m->vertices[face->vertices[0]]);
316 gm3Pos n = gm3_pos_cross(e1, e2);
317 gm3_pos_normalize(&n);
318
319 face->normal = n;
320 }
321 }
322 }
323
325 free(parsed);
326 return 0;
327}
#define gm3_pos_cross(a, b)
Calculates the cross product of two gm3Pos vectors (a x b).
Definition position.h:146
void * malloc(size_t size)
Custom implementation of malloc using a static memory pool.
Definition malloc.h:144
void * realloc(void *ptr, size_t size)
Custom implementation of realloc for memory allocated by malloc (this custom version).
Definition malloc.h:236
void * calloc(size_t count, size_t size)
Custom implementation of calloc using a static memory pool.
Definition malloc.h:215
void free(void *ptr)
Custom implementation of free for memory allocated by malloc (this custom version).
Definition malloc.h:189
int gm3_mesh_center(gm3Mesh *m)
Centers the mesh geometry around the origin (0,0,0).
Definition mesh.h:86
Defines structures for 3D materials and material libraries, and functions for loading MTL files.
gm3Material * gm3_mtl_find_mat(gm3MtlLib *file, const char *name, int *index)
Finds a material by name within a loaded MTL file.
Definition mtl.h:219
int gm3_mtl_load(gm3MtlLib *mtl_lib, const char *path, const char *dir)
Loads materials and textures from an OBJ-style .mtl file.
Definition mtl.h:118
int gm3_obj_parse(char *content, gm3ObjLine **result, size_t *n_lines)
Definition obj.h:169
int32_t gm3_obj_load(gm3Mesh *m, const char *path, const char *dir)
Loads a Wavefront OBJ 3D model from a file into a gm3Mesh structure.
Definition obj.h:210
int gm3_obj_parse_next_line(char **end, gm3ObjLine *ln)
Definition obj.h:79
Represents a single face (triangle) in a 3D mesh.
Definition mesh.h:10
int material
Definition mesh.h:13
int material_file
Definition mesh.h:14
size_t vertices[3]
Definition mesh.h:11
long uvs[3]
Definition mesh.h:12
gm3Pos normal
Definition mesh.h:15
Represents a 3D mesh composed of vertices, faces, normals, and texture coordinates.
Definition mesh.h:30
size_t n_mtllibs
Definition mesh.h:44
size_t n_normals
Definition mesh.h:38
gm3Tex * texs
Definition mesh.h:40
size_t n_texs
Definition mesh.h:41
gm3MeshFace * faces
Definition mesh.h:34
size_t n_vertices
Definition mesh.h:32
gm3Pos * vertices
Definition mesh.h:31
gm3MtlLib * mtllibs
Definition mesh.h:43
gm3Pos * normals
Definition mesh.h:37
size_t n_faces
Definition mesh.h:35
Represents a material library, typically loaded from an .mtl file.
Definition mtl.h:53
char name[256]
Definition mtl.h:54
Definition obj.h:29
long long indices[64][3]
Definition obj.h:33
size_t n_indices
Definition obj.h:34
double points[3]
Definition obj.h:32
char name[128]
Definition obj.h:31
char type
Definition obj.h:30
Represents a 3D position or vector.
Definition position.h:11
Represents a 2D texture coordinate.
Definition mesh.h:21
Represents a 2D position or vector.
Definition position.h:8
int gmu_read_file(const char *path, char **content, size_t *size)
Reads the entire content of a file into a dynamically allocated buffer.
Definition utils.h:98