aboutsummaryrefslogtreecommitdiff
path: root/contrib/cgltf-tangents/cgltf_tangents.c
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-06-27 10:18:39 -0700
committer3gg <3gg@shellblade.net>2025-06-27 10:18:39 -0700
commitbd57f345ed9dbed1d81683e48199626de2ea9044 (patch)
tree4221f2f2a7ad2244d2e93052bd68187ec91b8ea9 /contrib/cgltf-tangents/cgltf_tangents.c
parent9a82ce0083437a4f9f58108b2c23b957d2249ad8 (diff)
Restructure projectHEADmain
Diffstat (limited to 'contrib/cgltf-tangents/cgltf_tangents.c')
-rw-r--r--contrib/cgltf-tangents/cgltf_tangents.c618
1 files changed, 618 insertions, 0 deletions
diff --git a/contrib/cgltf-tangents/cgltf_tangents.c b/contrib/cgltf-tangents/cgltf_tangents.c
new file mode 100644
index 0000000..80b1e56
--- /dev/null
+++ b/contrib/cgltf-tangents/cgltf_tangents.c
@@ -0,0 +1,618 @@
1/*
2Copyright 2022 Marc Sunet
3
4Redistribution and use in source and binary forms, with or without modification,
5are permitted provided that the following conditions are met:
6
71. Redistributions of source code must retain the above copyright notice, this
8list of conditions and the following disclaimer.
9
102. Redistributions in binary form must reproduce the above copyright notice,
11this list of conditions and the following disclaimer in the documentation and/or
12other materials provided with the distribution.
13
14THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24*/
25#include "cgltf_tangents.h"
26#include "cgltf.h"
27
28#include "MikkTSpace/mikktspace.h"
29
30#include <assert.h>
31#include <stdbool.h>
32#include <stdint.h>
33#include <stdlib.h>
34#include <string.h>
35
36#ifdef CGLTF_TANGENTS_DEBUG
37#include <stdio.h>
38#define DLOG printf
39#else
40#define DLOG(...)
41#endif
42
43#include <stdio.h> // TODO: Remove me.
44
45#define CGLTF_OPTIONS_MALLOC(size) \
46 options->memory.alloc(options->memory.user_data, size)
47
48#define CGLTF_OPTIONS_FREE(ptr) \
49 options->memory.free(options->memory.user_data, ptr)
50
51static void* cgltf_default_alloc(void* user, cgltf_size size) {
52 (void)user;
53 return malloc(size);
54}
55
56static void cgltf_default_free(void* user, void* ptr) {
57 (void)user;
58 free(ptr);
59}
60
61static const cgltf_size NUM_TANGENT_COMPONENTS = 4; // X,Y,Z,fSign
62
63static float normalize_i8(int8_t x) { return (float)x / 128.0; }
64static float normalize_u8(uint8_t x) { return (float)x / 255.0; }
65static float normalize_i16(int16_t x) { return (float)x / 32768.0; }
66static float normalize_u16(uint16_t x) { return (float)x / 65535.0; }
67static float normalize_u32(uint32_t x) { return (float)x / 4294967295.0; }
68
69static cgltf_size num_vertex_attrib_components(cgltf_type type) {
70 switch (type) {
71 case cgltf_type_scalar:
72 return 1;
73 case cgltf_type_vec2:
74 return 2;
75 case cgltf_type_vec3:
76 return 3;
77 case cgltf_type_vec4:
78 return 4;
79 default:
80 assert(false);
81 return 0;
82 }
83}
84
85static cgltf_size cgltf_component_type_size_bytes(cgltf_component_type type) {
86 switch (type) {
87 case cgltf_component_type_r_8:
88 return 1;
89 case cgltf_component_type_r_8u:
90 return 1;
91 case cgltf_component_type_r_16:
92 return 2;
93 case cgltf_component_type_r_16u:
94 return 2;
95 case cgltf_component_type_r_32u:
96 return 4;
97 case cgltf_component_type_r_32f:
98 return 4;
99 default:
100 assert(false);
101 return 0;
102 }
103}
104
105static cgltf_size default_stride(cgltf_type type,
106 cgltf_component_type component_type) {
107 return num_vertex_attrib_components(type) *
108 cgltf_component_type_size_bytes(component_type);
109}
110
111// -----------------------------------------------------------------------------
112// MikkTSpace interface
113
114// An array of values for a given vertex attribute or for vertex indices.
115// For positions and normals, glTF mandates floats.
116// Texcoords and indices can be different types and vary in size: 8-bit, 16-bit,
117// or 32-bit.
118// We store void* pointers so that we can do byte pointer arithmetic.
119typedef struct Buffer {
120 const void* start; // X-coordinate of the first attribute.
121 const void* end; // One byte past the end of the buffer.
122 cgltf_size stride_bytes; // Stride in bytes between each value.
123 cgltf_component_type type; // Type of each value in the buffer.
124} Buffer;
125
126// User data for mesh processing.
127// See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes
128// Buffer pointers have the accessor + view offsets baked in so that we do the
129// addition only once.
130typedef struct SMikkUserData {
131 const cgltf_primitive* primitive;
132 // Index buffer may be empty (mesh primitive has no indices).
133 Buffer indices;
134 // Vertex attributes.
135 Buffer positions;
136 Buffer normals;
137 Buffer texcoords;
138 // Output tangents.
139 void* tangents;
140} SMikkUserData;
141
142static cgltf_size get_vertex_index(const SMikkUserData* data, cgltf_size iFace,
143 cgltf_size iVert) {
144 const cgltf_primitive* primitive = data->primitive;
145
146 // First compute a vertex index as if the mesh primitive had no indices.
147 cgltf_size vertex_idx = 0;
148 switch (primitive->type) {
149 case cgltf_primitive_type_triangles:
150 vertex_idx = iFace * 3 + iVert;
151 break;
152 case cgltf_primitive_type_triangle_strip:
153 // For triangle strips:
154 // face 0 -> verts 0, 1, 2
155 // face 1 -> verts 1, 3, 2 (1, 2, 3 flipped)
156 // face 2 -> verts 2, 3, 4
157 // face 3 -> verts 3, 5, 4 (3, 4, 5 flipped)
158 // ...
159 // face N=2k -> verts N, N+1, N+2
160 // face N=2k+1 -> verts N, N+2, N+1
161 if (iFace & 1) {
162 // Flip the winding of odd faces so that the is consistent with the even
163 // ones.
164 // iVert = 0 -> vert 0
165 // iVert = 1 -> vert 2
166 // iVert = 2 -> vert 1
167 vertex_idx = iFace + (2 - iVert);
168 } else {
169 vertex_idx = iFace + iVert;
170 }
171 break;
172 case cgltf_primitive_type_triangle_fan:
173 // For triangle fans:
174 // face 0 -> verts 0, 1, 2
175 // face 1 -> verts 0, 2, 3
176 // face 2 -> verts 0, 3, 4
177 // face 3 -> verts 0, 4, 5
178 // ...
179 // face N -> verts 0, N=1, N=2
180 if (iVert == 0) {
181 vertex_idx = 0;
182 } else {
183 vertex_idx = iFace + iVert;
184 }
185 break;
186 default:
187 assert(false);
188 break;
189 }
190
191 // If the mesh primitive has vertex indices, then vertex_idx is actually the
192 // index of the index. Index the index buffer with vertex_idx to find the
193 // real vertex index.
194 if (primitive->indices != NULL) {
195 const void* p_idx =
196 data->indices.start + vertex_idx * data->indices.stride_bytes;
197 switch (data->indices.type) {
198 case cgltf_component_type_r_8:
199 vertex_idx = *((int8_t*)p_idx);
200 break;
201 case cgltf_component_type_r_8u:
202 vertex_idx = *((uint8_t*)p_idx);
203 break;
204 case cgltf_component_type_r_16:
205 vertex_idx = *((int16_t*)p_idx);
206 break;
207 case cgltf_component_type_r_16u:
208 vertex_idx = *((uint16_t*)p_idx);
209 break;
210 case cgltf_component_type_r_32u:
211 vertex_idx = *((uint32_t*)p_idx);
212 break;
213 default:
214 assert(false);
215 break;
216 }
217 }
218
219 return vertex_idx;
220}
221
222static const void* get_vertex(const Buffer* buffer, cgltf_size index) {
223 // Stride is the offset in bytes between vertex attributes.
224 const void* vertex = buffer->start + buffer->stride_bytes * index;
225 assert(vertex < buffer->end);
226 return vertex;
227}
228
229static const void* get_position(const SMikkUserData* data, cgltf_size index) {
230 return get_vertex(&data->positions, index);
231}
232
233static const void* get_normal(const SMikkUserData* data, cgltf_size index) {
234 return get_vertex(&data->normals, index);
235}
236
237static const void* get_texcoord(const SMikkUserData* data, cgltf_size index) {
238 return get_vertex(&data->texcoords, index);
239}
240
241static float* get_tangent(void* buffer, cgltf_size index) {
242 // Tangents are tightly packed.
243 return (float*)(buffer) + NUM_TANGENT_COMPONENTS * index;
244}
245
246static int SMikk_get_num_faces(const SMikkTSpaceContext* pContext) {
247 SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData;
248 const cgltf_primitive* primitive = data->primitive;
249
250 // Find the number of effective vertices (vertices or indices) in the mesh
251 // primitive.
252 //
253 // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes
254 //
255 // "All attribute accessors for a given primitive MUST have the same count.
256 // When indices property is not defined, attribute accessors' count indicates
257 // the number of vertices to render; when indices property is defined, it
258 // indicates the upper (exclusive) bound on the index values in the indices
259 // accessor, i.e., all index values MUST be less than attribute accessors'
260 // count."
261 const cgltf_size num_verts = (primitive->indices != NULL)
262 ? primitive->indices->count
263 : primitive->attributes_count;
264
265 // Determine the number of faces given the number of vertices.
266 //
267 // We use the fact that glTF only supports triangles for faces.
268 // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes
269 switch (primitive->type) {
270 case cgltf_primitive_type_triangles:
271 return (int)num_verts / 3;
272 case cgltf_primitive_type_triangle_strip:
273 case cgltf_primitive_type_triangle_fan:
274 return (int)num_verts - 2;
275 default:
276 return 0;
277 }
278}
279
280int SMikk_get_num_vertices_of_face(const SMikkTSpaceContext* pContext,
281 const int iFace) {
282 // Triangles are the only faces supported by glTF.
283 // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes
284 return 3;
285}
286
287void SMikk_get_position(const SMikkTSpaceContext* pContext, float fvPosOut[],
288 const int iFace, const int iVert) {
289 const SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData;
290 const cgltf_primitive* primitive = data->primitive;
291
292 const cgltf_size idx = get_vertex_index(data, iFace, iVert);
293 const float* coord = get_position(data, idx);
294 fvPosOut[0] = *coord++;
295 fvPosOut[1] = *coord++;
296 fvPosOut[2] = *coord;
297 DLOG("Position (face: %d, vert: %d): %f, %f, %f; idx: %lu\n", iFace, iVert,
298 fvPosOut[0], fvPosOut[1], fvPosOut[2], idx);
299}
300
301void SMikk_get_normal(const SMikkTSpaceContext* pContext, float fvNormOut[],
302 const int iFace, const int iVert) {
303 const SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData;
304 const cgltf_primitive* primitive = data->primitive;
305
306 const cgltf_size idx = get_vertex_index(data, iFace, iVert);
307 const float* coord = get_normal(data, idx);
308 fvNormOut[0] = *coord++;
309 fvNormOut[1] = *coord++;
310 fvNormOut[2] = *coord;
311 DLOG("Normal (face: %d, vert: %d): %f, %f, %f\n", iFace, iVert, fvNormOut[0],
312 fvNormOut[1], fvNormOut[2]);
313}
314
315void SMikk_get_texcoord(const SMikkTSpaceContext* pContext, float fvTexcOut[],
316 const int iFace, const int iVert) {
317 const SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData;
318 const cgltf_primitive* primitive = data->primitive;
319
320 const cgltf_size idx = get_vertex_index(data, iFace, iVert);
321 const void* coord = get_texcoord(data, idx);
322 switch (data->texcoords.type) {
323 case cgltf_component_type_r_8: {
324 const int8_t* c = coord;
325 fvTexcOut[0] = normalize_i8(*c++);
326 fvTexcOut[1] = normalize_i8(*c);
327 break;
328 }
329 case cgltf_component_type_r_8u: {
330 const uint8_t* c = coord;
331 fvTexcOut[0] = normalize_u8(*c++);
332 fvTexcOut[1] = normalize_u8(*c);
333 break;
334 }
335 case cgltf_component_type_r_16: {
336 const int16_t* c = coord;
337 fvTexcOut[0] = normalize_i16(*c++);
338 fvTexcOut[1] = normalize_i16(*c);
339 break;
340 }
341 case cgltf_component_type_r_16u: {
342 const uint16_t* c = coord;
343 fvTexcOut[0] = normalize_u16(*c++);
344 fvTexcOut[1] = normalize_u16(*c);
345 break;
346 }
347 case cgltf_component_type_r_32u: {
348 const uint32_t* c = coord;
349 fvTexcOut[0] = normalize_u32(*c++);
350 fvTexcOut[1] = normalize_u32(*c);
351 break;
352 }
353 case cgltf_component_type_r_32f: {
354 const float* c = coord;
355 fvTexcOut[0] = *c++;
356 fvTexcOut[1] = *c;
357 break;
358 }
359 default:
360 assert(false);
361 break;
362 }
363 DLOG("Texcoord (face: %d, vert: %d): %f, %f\n", iFace, iVert, fvTexcOut[0],
364 fvTexcOut[1]);
365}
366
367void SMikk_set_TSpace_basic(const SMikkTSpaceContext* pContext,
368 const float fvTangent[], const float fSign,
369 const int iFace, const int iVert) {
370 SMikkUserData* data = (SMikkUserData*)pContext->m_pUserData;
371 const cgltf_primitive* primitive = data->primitive;
372
373 const cgltf_size idx = get_vertex_index(data, iFace, iVert);
374 float* coord = get_tangent(data->tangents, idx);
375 *coord++ = fvTangent[0];
376 *coord++ = fvTangent[1];
377 *coord++ = fvTangent[2];
378 *coord = fSign;
379 DLOG("Tangent (face: %d, vert: %d): %f, %f, %f; sign: %f\n", iFace, iVert,
380 fvTangent[0], fvTangent[1], fvTangent[2], fSign);
381}
382
383// -----------------------------------------------------------------------------
384
385static bool has_normal_map(const cgltf_primitive* primitive) {
386 return (primitive->material != NULL) &&
387 (primitive->material->normal_texture.texture != NULL);
388}
389
390static const cgltf_attribute* find_attribute(const cgltf_primitive* primitive,
391 cgltf_attribute_type type) {
392 for (cgltf_size i = 0; i < primitive->attributes_count; ++i) {
393 const cgltf_attribute* attrib = &primitive->attributes[i];
394 if (attrib->type == type) {
395 return attrib;
396 }
397 }
398 return NULL;
399}
400
401static bool has_attribute(const cgltf_primitive* primitive,
402 cgltf_attribute_type type) {
403 return find_attribute(primitive, type) != NULL;
404}
405
406static bool has_positions3d(const cgltf_primitive* primitive) {
407 const cgltf_attribute* attrib =
408 find_attribute(primitive, cgltf_attribute_type_position);
409 if (attrib) {
410 return attrib->data->type == cgltf_type_vec3;
411 }
412 return false;
413}
414
415static bool has_normals(const cgltf_primitive* primitive) {
416 return has_attribute(primitive, cgltf_attribute_type_normal);
417}
418
419static bool has_texcoords(const cgltf_primitive* primitive) {
420 return has_attribute(primitive, cgltf_attribute_type_texcoord);
421}
422
423static bool has_tangents(const cgltf_primitive* primitive) {
424 return has_attribute(primitive, cgltf_attribute_type_tangent);
425}
426
427static bool has_indices(const cgltf_primitive* primitive) {
428 return primitive->indices != 0;
429}
430
431static cgltfTangentBuffer compute_tangents(const cgltf_options* options,
432 const cgltf_data* data,
433 cgltf_primitive* primitive) {
434 cgltfTangentBuffer buffer = {0};
435 SMikkUserData user = {0};
436 cgltf_size num_verts = 0;
437
438 user.primitive = primitive;
439
440 if (primitive->indices != NULL) {
441 const cgltf_accessor* accessor = primitive->indices;
442 const cgltf_buffer_view* view = accessor->buffer_view;
443 const cgltf_size offset_bytes = accessor->offset + view->offset;
444 const void* buffer_data = view->buffer->data + offset_bytes;
445 const void* buffer_end = view->buffer->data + view->offset + view->size;
446
447 user.indices.start = buffer_data;
448 user.indices.end = buffer_end;
449 // Indices are tightly packed, stride 0.
450 user.indices.stride_bytes =
451 default_stride(accessor->type, accessor->component_type);
452 user.indices.type = accessor->component_type;
453 }
454
455 for (cgltf_size i = 0; i < primitive->attributes_count; ++i) {
456 const cgltf_attribute* attrib = &primitive->attributes[i];
457
458 if ((attrib->type == cgltf_attribute_type_position) ||
459 (attrib->type == cgltf_attribute_type_normal) ||
460 (attrib->type == cgltf_attribute_type_texcoord)) {
461 const cgltf_accessor* accessor = attrib->data;
462 const cgltf_buffer_view* view = accessor->buffer_view;
463 const cgltf_buffer* buffer = view->buffer;
464 const cgltf_size offset_bytes = accessor->offset + view->offset;
465 const cgltf_size stride_bytes =
466 view->stride > 0
467 ? view->stride
468 : default_stride(accessor->type, accessor->component_type);
469 // const cgltf_size size_bytes = view->size;
470 const void* buffer_data = view->buffer->data + offset_bytes;
471 const void* buffer_end = view->buffer->data + view->offset + view->size;
472
473 Buffer* attrib_buffer = 0;
474
475 if (attrib->type == cgltf_attribute_type_position) {
476 // glTF currently mandates vec3 for positions. Caller should ensure
477 // this.
478 assert(accessor->type == cgltf_type_vec3);
479 num_verts = attrib->data->count;
480 attrib_buffer = &user.positions;
481 } else if (attrib->type == cgltf_attribute_type_normal) {
482 attrib_buffer = &user.normals;
483 } else if (attrib->type == cgltf_attribute_type_texcoord) {
484 attrib_buffer = &user.texcoords;
485 }
486
487 attrib_buffer->start = buffer_data;
488 attrib_buffer->end = buffer_end;
489 attrib_buffer->stride_bytes = stride_bytes;
490 attrib_buffer->type = accessor->component_type;
491 }
492 }
493
494 assert(user.positions.start);
495 assert(user.positions.end);
496 assert(user.normals.start);
497 assert(user.normals.end);
498 assert(user.texcoords.start);
499 assert(user.texcoords.end);
500 assert(num_verts > 0);
501
502 const cgltf_size tangents_size_bytes =
503 num_verts * NUM_TANGENT_COMPONENTS * sizeof(float);
504
505 user.tangents = CGLTF_OPTIONS_MALLOC(tangents_size_bytes);
506 if (!user.tangents) {
507 return buffer;
508 }
509
510 SMikkTSpaceInterface interface = (SMikkTSpaceInterface){
511 .m_getNumFaces = SMikk_get_num_faces,
512 .m_getNumVerticesOfFace = SMikk_get_num_vertices_of_face,
513 .m_getPosition = SMikk_get_position,
514 .m_getNormal = SMikk_get_normal,
515 .m_getTexCoord = SMikk_get_texcoord,
516 .m_setTSpaceBasic = SMikk_set_TSpace_basic,
517 };
518 const SMikkTSpaceContext context = (SMikkTSpaceContext){
519 .m_pInterface = &interface,
520 .m_pUserData = &user,
521 };
522 if (!genTangSpaceDefault(&context)) {
523 return buffer;
524 }
525
526 buffer.data = user.tangents;
527 buffer.size_bytes = tangents_size_bytes;
528 buffer.primitive = primitive;
529
530 return buffer;
531}
532
533static void process_primitive(const cgltf_options* options,
534 const cgltf_data* data,
535 cgltf_primitive* primitive,
536 cgltfTangentBuffer* tangent_buffers,
537 cgltf_size* num_tangent_buffers) {
538 DLOG("Processing primitive\n");
539 cgltf_size cur_buffer = 0;
540 // TODO: MikkTSpace should not be used with models with vertex indices. One
541 // workaround is to unindex the mesh, compute tangents, and then re-index it.
542 if (((primitive->type == cgltf_primitive_type_triangle_fan) ||
543 (primitive->type == cgltf_primitive_type_triangle_strip) ||
544 (primitive->type == cgltf_primitive_type_triangles)) &&
545 has_normal_map(primitive) && !has_tangents(primitive) &&
546 has_positions3d(primitive) && has_normals(primitive) &&
547 has_texcoords(primitive) && !has_indices(primitive)) {
548 *num_tangent_buffers += 1;
549 if (tangent_buffers) {
550 DLOG("Model with normal map missing tangents detected\n");
551 tangent_buffers[cur_buffer] = compute_tangents(options, data, primitive);
552 if (tangent_buffers[cur_buffer].data) {
553 DLOG("Tangents computed\n");
554 }
555 cur_buffer++;
556 }
557 }
558}
559
560cgltf_result cgltf_compute_tangents(const cgltf_options* input_options,
561 const cgltf_data* data,
562 cgltfTangentBuffer** tangent_buffers,
563 cgltf_size* num_tangent_buffers) {
564 if ((input_options == NULL) || (data == NULL)) {
565 return cgltf_result_invalid_options;
566 }
567
568 DLOG("cgltf_compute_tangents\n");
569
570 cgltf_options options = *input_options;
571 if (options.memory.alloc == NULL) {
572 options.memory.alloc = &cgltf_default_alloc;
573 }
574 if (options.memory.free == NULL) {
575 options.memory.free = &cgltf_default_free;
576 }
577
578 // First pass: compute the number of tangent buffers to be created.
579 *num_tangent_buffers = 0;
580 for (cgltf_size mesh_idx = 0; mesh_idx < data->meshes_count; ++mesh_idx) {
581 const cgltf_mesh* mesh = &data->meshes[mesh_idx];
582
583 for (cgltf_size prim_idx = 0; prim_idx < mesh->primitives_count;
584 ++prim_idx) {
585 // Pass in null for the tangent buffers to just compute the number of
586 // buffers.
587 process_primitive(&options, data, &mesh->primitives[prim_idx], 0,
588 num_tangent_buffers);
589 }
590 }
591 DLOG("Number of primitives to be patched: %lu\n", *num_tangent_buffers);
592
593 // Second pass: compute the tangents.
594 if (*num_tangent_buffers > 0) {
595 *tangent_buffers =
596 options.memory.alloc(options.memory.user_data,
597 *num_tangent_buffers * sizeof(cgltfTangentBuffer));
598 if (!*tangent_buffers) {
599 return cgltf_result_out_of_memory;
600 }
601
602 cgltf_size tangent_buffers_computed = 0;
603
604 for (cgltf_size mesh_idx = 0; mesh_idx < data->meshes_count; ++mesh_idx) {
605 const cgltf_mesh* mesh = &data->meshes[mesh_idx];
606
607 for (cgltf_size prim_idx = 0; prim_idx < mesh->primitives_count;
608 ++prim_idx) {
609 process_primitive(&options, data, &mesh->primitives[prim_idx],
610 *tangent_buffers, &tangent_buffers_computed);
611 }
612 }
613
614 assert(tangent_buffers_computed == *num_tangent_buffers);
615 }
616
617 return cgltf_result_success;
618}