Add tg3_validate() glTF 2.0 validation API to tiny_gltf_v3.h with tests

Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/5e03ce1e-cc19-46d5-a9ae-04c373be3919
This commit is contained in:
copilot-swe-agent[bot]
2026-03-25 02:32:29 +00:00
parent 9bcfcea6e0
commit 9b3947c976
3 changed files with 1334 additions and 4 deletions

View File

@@ -65,6 +65,15 @@ if (TINYGLTF_BUILD_TESTS)
)
target_compile_definitions(tester_intensive_customjson PRIVATE TINYGLTF_USE_CUSTOM_JSON)
add_test(NAME tester_intensive_customjson COMMAND tester_intensive_customjson WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
# v3 API tests (parser + validator)
add_executable(tester_v3 tests/v3/tester_v3.cc)
target_include_directories(tester_v3 PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
)
set_target_properties(tester_v3 PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON)
add_test(NAME tester_v3 COMMAND tester_v3 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
endif (TINYGLTF_BUILD_TESTS)
#

612
tests/v3/tester_v3.cc Normal file
View File

@@ -0,0 +1,612 @@
/*
* tester_v3.cc — Unit tests for tiny_gltf_v3.h (v3 API), including
* the tg3_validate() validation feature.
*/
#define TINYGLTF3_IMPLEMENTATION
#include "tiny_gltf_v3.h"
#define CATCH_CONFIG_MAIN
#include "../catch.hpp"
#include <cstring>
#include <cstdio>
/* ======================================================================
* Helpers
* ====================================================================== */
/* Build a minimal valid glTF 2.0 JSON string */
static const char *MINIMAL_GLTF_JSON =
"{\"asset\":{\"version\":\"2.0\"}}";
/* Parse a JSON string into a tg3_model, returns error code. */
static tg3_error_code parse_json(tg3_model *model, tg3_error_stack *errors,
const char *json) {
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.required_sections = TG3_NO_REQUIRE;
return tg3_parse(model, errors,
(const uint8_t *)json, (uint64_t)strlen(json),
"", 0, &opts);
}
/* ======================================================================
* Tests: tg3_validate() — basic API behaviour
* ====================================================================== */
TEST_CASE("v3-validate-null-model", "[v3][validate]") {
tg3_error_stack errors;
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(NULL, &errors);
REQUIRE(rc == TG3_ERR_INVALID_VALUE);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-null-errors", "[v3][validate]") {
tg3_model model;
memset(&model, 0, sizeof(model));
model.default_scene = -1;
/* errors may be NULL — results are silently discarded */
tg3_error_code rc = tg3_validate(&model, NULL);
/* Empty model (no asset.version) should still return an error code */
REQUIRE(rc != TG3_OK); /* missing asset.version */
tg3_model_free(&model);
}
TEST_CASE("v3-validate-minimal-valid", "[v3][validate]") {
tg3_model model;
tg3_error_stack errors;
memset(&model, 0, sizeof(model));
tg3_error_stack_init(&errors);
tg3_error_code rc = parse_json(&model, &errors, MINIMAL_GLTF_JSON);
REQUIRE(rc == TG3_OK);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_OK);
REQUIRE(tg3_errors_has_error(&errors) == 0);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: asset.version validation
* ====================================================================== */
TEST_CASE("v3-validate-missing-asset-version", "[v3][validate][asset]") {
tg3_model model;
tg3_error_stack errors;
memset(&model, 0, sizeof(model));
tg3_error_stack_init(&errors);
/* asset.version.data == NULL means absent */
model.default_scene = -1;
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_MISSING_REQUIRED);
REQUIRE(tg3_errors_has_error(&errors) == 1);
bool found = false;
for (uint32_t i = 0; i < tg3_errors_count(&errors); i++) {
const tg3_error_entry *e = tg3_errors_get(&errors, i);
if (e->code == TG3_ERR_MISSING_REQUIRED) { found = true; break; }
}
REQUIRE(found);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-wrong-asset-version-warn", "[v3][validate][asset]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
/* Parse a model with version "1.0" */
const char *json = "{\"asset\":{\"version\":\"1.0\"}}";
tg3_error_code rc = parse_json(&model, &errors, json);
REQUIRE(rc == TG3_OK);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
rc = tg3_validate(&model, &errors);
/* Wrong version emits a WARNING, not an error */
REQUIRE(rc == TG3_OK);
REQUIRE(tg3_errors_has_error(&errors) == 0);
bool found_warn = false;
for (uint32_t i = 0; i < tg3_errors_count(&errors); i++) {
const tg3_error_entry *e = tg3_errors_get(&errors, i);
if (e->severity == TG3_SEVERITY_WARNING &&
e->code == TG3_ERR_INVALID_VALUE) {
found_warn = true; break;
}
}
REQUIRE(found_warn);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: default scene index
* ====================================================================== */
TEST_CASE("v3-validate-invalid-default-scene", "[v3][validate][scene]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
parse_json(&model, &errors, MINIMAL_GLTF_JSON);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
/* Point default_scene to a non-existent scene */
model.default_scene = 5;
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: accessor validation
* ====================================================================== */
TEST_CASE("v3-validate-accessor-invalid-component-type", "[v3][validate][accessor]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":[{"
" \"componentType\":9999,"
" \"count\":1,"
" \"type\":\"SCALAR\""
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_ACCESSOR);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-accessor-zero-count", "[v3][validate][accessor]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
/* Build model directly */
parse_json(&model, &errors, MINIMAL_GLTF_JSON);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
/* Inject a fake accessor with count=0 */
tg3_accessor *acc = (tg3_accessor *)calloc(1, sizeof(tg3_accessor));
acc->buffer_view = -1;
acc->component_type = TG3_COMPONENT_TYPE_FLOAT;
acc->type = TG3_TYPE_SCALAR;
acc->count = 0; /* invalid */
model.accessors = acc;
model.accessors_count = 1;
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_ACCESSOR);
/* Restore so tg3_model_free doesn't double-free */
model.accessors = NULL;
model.accessors_count = 0;
free(acc);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-accessor-invalid-bufferView-index", "[v3][validate][accessor]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":[{"
" \"bufferView\":99,"
" \"componentType\":5126,"
" \"count\":1,"
" \"type\":\"SCALAR\""
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: mesh validation
* ====================================================================== */
TEST_CASE("v3-validate-mesh-no-primitives", "[v3][validate][mesh]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"meshes\":[{\"primitives\":[]}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_MESH);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-mesh-indices-out-of-range", "[v3][validate][mesh]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":["
" {\"componentType\":5126,\"count\":3,\"type\":\"VEC3\"}"
"],"
"\"meshes\":[{"
" \"primitives\":[{"
" \"attributes\":{\"POSITION\":0},"
" \"indices\":99"
" }]"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: node validation
* ====================================================================== */
TEST_CASE("v3-validate-node-invalid-mesh-index", "[v3][validate][node]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"mesh\":42}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-node-self-reference", "[v3][validate][node]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"children\":[0]}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_NODE);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: texture validation
* ====================================================================== */
TEST_CASE("v3-validate-texture-invalid-source", "[v3][validate][texture]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"textures\":[{\"source\":99}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: material validation
* ====================================================================== */
TEST_CASE("v3-validate-material-invalid-alpha-mode", "[v3][validate][material]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"materials\":[{\"alphaMode\":\"INVALID\"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_MATERIAL);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: camera validation
* ====================================================================== */
TEST_CASE("v3-validate-camera-perspective-bad-yfov", "[v3][validate][camera]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"cameras\":[{"
" \"type\":\"perspective\","
" \"perspective\":{\"yfov\":0.0,\"znear\":0.01,\"zfar\":100.0,"
" \"aspectRatio\":1.0}"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_CAMERA);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-camera-invalid-type", "[v3][validate][camera]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"cameras\":[{\"type\":\"unknown\"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_CAMERA);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: scene validation
* ====================================================================== */
TEST_CASE("v3-validate-scene-invalid-node-index", "[v3][validate][scene]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"scenes\":[{\"nodes\":[99]}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_SCENE);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: animation validation
* ====================================================================== */
TEST_CASE("v3-validate-animation-invalid-sampler-ref", "[v3][validate][animation]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":["
" {\"componentType\":5126,\"count\":2,\"type\":\"SCALAR\"},"
" {\"componentType\":5126,\"count\":2,\"type\":\"VEC3\"}"
"],"
"\"animations\":[{"
" \"samplers\":[{\"input\":0,\"output\":1,\"interpolation\":\"LINEAR\"}],"
" \"channels\":[{"
" \"sampler\":99,"
" \"target\":{\"path\":\"translation\"}"
" }]"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-animation-invalid-target-path", "[v3][validate][animation]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":["
" {\"componentType\":5126,\"count\":2,\"type\":\"SCALAR\"},"
" {\"componentType\":5126,\"count\":2,\"type\":\"VEC3\"}"
"],"
"\"animations\":[{"
" \"samplers\":[{\"input\":0,\"output\":1,\"interpolation\":\"LINEAR\"}],"
" \"channels\":[{"
" \"sampler\":0,"
" \"target\":{\"path\":\"bogus\"}"
" }]"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_VALUE);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: error stack msg_arena_ lifetime
* ====================================================================== */
TEST_CASE("v3-validate-error-messages-persist", "[v3][validate][errorstack]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
/* Trigger multiple validation errors */
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"mesh\":99,\"camera\":88}],"
"\"textures\":[{\"source\":77}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_validate(&model, &errors);
uint32_t count = tg3_errors_count(&errors);
REQUIRE(count >= 3);
/* All message and path pointers must be non-NULL and readable */
for (uint32_t i = 0; i < count; i++) {
const tg3_error_entry *e = tg3_errors_get(&errors, i);
REQUIRE(e != NULL);
REQUIRE(e->message != NULL);
/* Sanity: message should not be empty */
REQUIRE(strlen(e->message) > 0);
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: C++ wrapper
* ====================================================================== */
TEST_CASE("v3-validate-cpp-wrapper", "[v3][validate][cpp]") {
tinygltf3::Model model;
tinygltf3::ErrorStack errors;
const char *json = "{\"asset\":{\"version\":\"2.0\"}}";
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.required_sections = TG3_NO_REQUIRE;
tg3_parse_auto(model.get(), errors.get(),
(const uint8_t *)json, strlen(json),
"", 0, &opts);
tg3_error_code rc = tinygltf3::validate(model, errors);
REQUIRE(rc == TG3_OK);
REQUIRE(errors.has_error() == false);
}

View File

@@ -231,6 +231,9 @@ typedef struct tg3_allocator {
void *user_data;
} tg3_allocator;
/* Forward declaration of arena type (defined in the implementation section) */
struct tg3_arena;
/* ======================================================================
* Section 7: Error Reporting
* ====================================================================== */
@@ -309,10 +312,11 @@ typedef struct tg3_error_entry {
} tg3_error_entry;
typedef struct tg3_error_stack {
tg3_error_entry *entries;
uint32_t count;
uint32_t capacity;
int32_t has_error; /* 1 if any entry with severity == ERROR */
tg3_error_entry *entries;
uint32_t count;
uint32_t capacity;
int32_t has_error; /* 1 if any entry with severity == ERROR */
struct tg3_arena *msg_arena_; /* Internal: arena for dynamic validation message strings */
} tg3_error_stack;
/* Error stack query functions */
@@ -970,6 +974,19 @@ TINYGLTF3_API void tg3_write_options_init(tg3_write_options *options);
TINYGLTF3_API void tg3_error_stack_init(tg3_error_stack *es);
TINYGLTF3_API void tg3_error_stack_free(tg3_error_stack *es);
/* ======================================================================
* Section 13.5: Validation API
* ====================================================================== */
/* Validate a parsed model against the glTF 2.0 specification.
* Errors and warnings are appended to 'errors'.
* Returns TG3_OK if no validation errors are found, or the first error
* code encountered otherwise.
* 'model' must not be NULL; 'errors' may be NULL (results discarded). */
TINYGLTF3_API tg3_error_code tg3_validate(
const tg3_model *model,
tg3_error_stack *errors);
/* ======================================================================
* Section 14: Writer API
* ====================================================================== */
@@ -1111,6 +1128,10 @@ inline tg3_error_code parse(Model &model, ErrorStack &errors,
options);
}
inline tg3_error_code validate(const Model &model, ErrorStack &errors) {
return tg3_validate(model.get(), errors.get());
}
} /* namespace tinygltf3 */
#endif /* __cplusplus */
@@ -1469,6 +1490,9 @@ TINYGLTF3_API void tg3_error_stack_init(tg3_error_stack *es) {
TINYGLTF3_API void tg3_error_stack_free(tg3_error_stack *es) {
if (!es) return;
free(es->entries);
if (es->msg_arena_) {
tg3__arena_destroy(es->msg_arena_);
}
memset(es, 0, sizeof(tg3_error_stack));
}
@@ -4431,6 +4455,691 @@ TINYGLTF3_API void tg3_writer_destroy(tg3_writer *w) {
delete w;
}
/* ======================================================================
* Public: Validation API Implementation
* ====================================================================== */
/* Internal helper: push a validation error/warning with arena-duped
* message and json_path strings so they outlive the validate function. */
static void tg3__val_push(tg3_error_stack *es, tg3_arena *arena,
tg3_severity sev, tg3_error_code code,
const char *json_path_buf, const char *fmt, ...) {
if (!es) return;
char msg_buf[1024];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(msg_buf, sizeof(msg_buf), fmt, ap);
va_end(ap);
if (n < 0) n = 0;
if ((size_t)n >= sizeof(msg_buf)) n = (int)(sizeof(msg_buf) - 1);
const char *msg = msg_buf;
const char *path = json_path_buf;
if (arena) {
char *m = tg3__arena_strdup(arena, msg_buf, (size_t)n);
if (m) msg = m;
if (json_path_buf) {
char *p = tg3__arena_strdup(arena, json_path_buf, strlen(json_path_buf));
if (p) path = p;
}
}
tg3__error_push(es, sev, code, msg, path, -1);
}
/* Helper: validate a tg3_texture_info index. */
static void tg3__val_check_tex_index(tg3_error_stack *es, tg3_arena *arena,
int32_t tex_idx, uint32_t textures_count,
uint32_t mat_idx, const char *field_name) {
if (tex_idx < 0) return; /* absent */
if ((uint32_t)tex_idx >= textures_count) {
char path[128];
snprintf(path, sizeof(path), "/materials/%u", mat_idx);
tg3__val_push(es, arena, TG3_SEVERITY_ERROR, TG3_ERR_INVALID_INDEX,
path,
"material[%u].%s texture index %d out of range [0, %u)",
mat_idx, field_name, tex_idx, textures_count);
}
}
TINYGLTF3_API tg3_error_code tg3_validate(
const tg3_model *model,
tg3_error_stack *errors) {
if (!model) return TG3_ERR_INVALID_VALUE;
/* Set up arena for message and path strings */
tg3_arena *arena = NULL;
if (errors) {
if (!errors->msg_arena_) {
tg3_memory_config cfg;
memset(&cfg, 0, sizeof(cfg));
errors->msg_arena_ = tg3__arena_create(&cfg);
}
arena = errors->msg_arena_;
}
tg3_error_code first_err = TG3_OK;
/* Push an error and record the first error code seen */
#define TG3__VERR(code, path_buf, ...) \
do { \
tg3__val_push(errors, arena, TG3_SEVERITY_ERROR, (code), (path_buf), __VA_ARGS__); \
if (first_err == TG3_OK) first_err = (code); \
} while (0)
/* Push a warning (does not set first_err) */
#define TG3__VWARN(code, path_buf, ...) \
tg3__val_push(errors, arena, TG3_SEVERITY_WARNING, (code), (path_buf), __VA_ARGS__)
/* Temporary path buffer used in all per-entity loops.
* Content is always arena-duped inside tg3__val_push before returning. */
char path[256];
char ipath[512]; /* inner path for nested entities */
/* ------------------------------------------------------------------
* 1. Asset
* ------------------------------------------------------------------ */
if (!model->asset.version.data) {
TG3__VERR(TG3_ERR_MISSING_REQUIRED, "/asset",
"asset.version is required");
} else if (!tg3_str_equals_cstr(model->asset.version, "2.0")) {
TG3__VWARN(TG3_ERR_INVALID_VALUE, "/asset/version",
"asset.version is \"%.*s\", expected \"2.0\"",
(int)model->asset.version.len, model->asset.version.data);
}
/* ------------------------------------------------------------------
* 2. Default scene index
* ------------------------------------------------------------------ */
if (model->default_scene >= 0 &&
(uint32_t)model->default_scene >= model->scenes_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, "/scene",
"default scene index %d out of range [0, %u)",
model->default_scene, model->scenes_count);
}
/* ------------------------------------------------------------------
* 3. Buffer views
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->buffer_views_count; i++) {
const tg3_buffer_view *bv = &model->buffer_views[i];
snprintf(path, sizeof(path), "/bufferViews/%u", i);
if (bv->buffer < 0 || (uint32_t)bv->buffer >= model->buffers_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"bufferView[%u].buffer index %d out of range [0, %u)",
i, bv->buffer, model->buffers_count);
} else {
const tg3_buffer *buf = &model->buffers[(uint32_t)bv->buffer];
if (bv->byte_offset + bv->byte_length > buf->data.count) {
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
"bufferView[%u] byteOffset+byteLength (%llu) exceeds "
"buffer[%d].byteLength (%llu)",
i,
(unsigned long long)(bv->byte_offset + bv->byte_length),
bv->buffer,
(unsigned long long)buf->data.count);
}
}
if (bv->byte_length == 0) {
TG3__VWARN(TG3_ERR_INVALID_BUFFER_VIEW, path,
"bufferView[%u].byteLength is 0", i);
}
if (bv->byte_stride != 0) {
/* glTF 2.0 spec §3.6.2.4: byteStride must be in [4, 252] and
* a multiple of 4. */
if (bv->byte_stride < 4 || bv->byte_stride > 252) {
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
"bufferView[%u].byteStride %u must be in [4, 252]",
i, bv->byte_stride);
} else if (bv->byte_stride % 4 != 0) {
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
"bufferView[%u].byteStride %u must be a multiple of 4",
i, bv->byte_stride);
}
}
}
/* ------------------------------------------------------------------
* 4. Accessors
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->accessors_count; i++) {
const tg3_accessor *acc = &model->accessors[i];
snprintf(path, sizeof(path), "/accessors/%u", i);
/* Validate componentType */
int valid_ct = (acc->component_type == TG3_COMPONENT_TYPE_BYTE ||
acc->component_type == TG3_COMPONENT_TYPE_UNSIGNED_BYTE ||
acc->component_type == TG3_COMPONENT_TYPE_SHORT ||
acc->component_type == TG3_COMPONENT_TYPE_UNSIGNED_SHORT ||
acc->component_type == TG3_COMPONENT_TYPE_UNSIGNED_INT ||
acc->component_type == TG3_COMPONENT_TYPE_FLOAT);
if (!valid_ct) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u].componentType %d is not a valid glTF 2.0 componentType",
i, acc->component_type);
}
/* Validate type */
int valid_type = (acc->type == TG3_TYPE_SCALAR ||
acc->type == TG3_TYPE_VEC2 ||
acc->type == TG3_TYPE_VEC3 ||
acc->type == TG3_TYPE_VEC4 ||
acc->type == TG3_TYPE_MAT2 ||
acc->type == TG3_TYPE_MAT3 ||
acc->type == TG3_TYPE_MAT4);
if (!valid_type) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u].type %d is not a valid glTF 2.0 accessor type",
i, acc->type);
}
/* count >= 1 */
if (acc->count == 0) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u].count must be >= 1", i);
}
/* bufferView cross-reference and byte bounds */
if (acc->buffer_view >= 0) {
if ((uint32_t)acc->buffer_view >= model->buffer_views_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"accessor[%u].bufferView index %d out of range [0, %u)",
i, acc->buffer_view, model->buffer_views_count);
} else if (valid_ct && valid_type && acc->count > 0) {
const tg3_buffer_view *bv =
&model->buffer_views[(uint32_t)acc->buffer_view];
int32_t comp_sz = tg3_component_size(acc->component_type);
int32_t num_comp = tg3_num_components(acc->type);
if (comp_sz > 0 && num_comp > 0) {
int32_t elem_sz = comp_sz * num_comp;
int32_t stride = (bv->byte_stride > 0)
? (int32_t)bv->byte_stride
: elem_sz;
uint64_t max_byte =
acc->byte_offset +
(uint64_t)(acc->count - 1) * (uint64_t)stride +
(uint64_t)elem_sz;
if (max_byte > bv->byte_length) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u] byte range exceeds "
"bufferView[%d].byteLength (%llu): "
"needs %llu bytes",
i, acc->buffer_view,
(unsigned long long)bv->byte_length,
(unsigned long long)max_byte);
}
/* byteOffset alignment */
if (acc->byte_offset % (uint64_t)comp_sz != 0) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u].byteOffset %llu is not aligned "
"to componentType size %d",
i, (unsigned long long)acc->byte_offset,
comp_sz);
}
}
}
}
/* Sparse accessor sub-structure */
if (acc->sparse.is_sparse) {
if (acc->sparse.count <= 0) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"accessor[%u].sparse.count must be > 0", i);
}
if (acc->sparse.indices.buffer_view < 0 ||
(uint32_t)acc->sparse.indices.buffer_view >=
model->buffer_views_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"accessor[%u].sparse.indices.bufferView index %d "
"out of range [0, %u)",
i, acc->sparse.indices.buffer_view,
model->buffer_views_count);
}
if (acc->sparse.values.buffer_view < 0 ||
(uint32_t)acc->sparse.values.buffer_view >=
model->buffer_views_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"accessor[%u].sparse.values.bufferView index %d "
"out of range [0, %u)",
i, acc->sparse.values.buffer_view,
model->buffer_views_count);
}
}
}
/* ------------------------------------------------------------------
* 5. Meshes
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->meshes_count; i++) {
const tg3_mesh *mesh = &model->meshes[i];
snprintf(path, sizeof(path), "/meshes/%u", i);
if (mesh->primitives_count == 0) {
TG3__VERR(TG3_ERR_INVALID_MESH, path,
"mesh[%u] must have at least one primitive", i);
}
for (uint32_t pi = 0; pi < mesh->primitives_count; pi++) {
const tg3_primitive *prim = &mesh->primitives[pi];
snprintf(ipath, sizeof(ipath), "/meshes/%u/primitives/%u", i, pi);
if (prim->attributes_count == 0) {
TG3__VERR(TG3_ERR_INVALID_MESH, ipath,
"mesh[%u].primitives[%u] must have at least one attribute",
i, pi);
}
/* Validate attribute accessor indices */
for (uint32_t ai = 0; ai < prim->attributes_count; ai++) {
int32_t acc_idx = prim->attributes[ai].value;
if (acc_idx < 0 ||
(uint32_t)acc_idx >= model->accessors_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, ipath,
"mesh[%u].primitives[%u] attribute \"%.*s\" "
"accessor index %d out of range [0, %u)",
i, pi,
(int)prim->attributes[ai].key.len,
prim->attributes[ai].key.data,
acc_idx, model->accessors_count);
}
}
/* Validate indices accessor */
if (prim->indices >= 0) {
if ((uint32_t)prim->indices >= model->accessors_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, ipath,
"mesh[%u].primitives[%u].indices accessor index %d "
"out of range [0, %u)",
i, pi, prim->indices, model->accessors_count);
} else {
const tg3_accessor *idx_acc =
&model->accessors[(uint32_t)prim->indices];
if (idx_acc->type != TG3_TYPE_SCALAR) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, ipath,
"mesh[%u].primitives[%u].indices accessor "
"must be SCALAR type",
i, pi);
}
if (idx_acc->component_type !=
TG3_COMPONENT_TYPE_UNSIGNED_BYTE &&
idx_acc->component_type !=
TG3_COMPONENT_TYPE_UNSIGNED_SHORT &&
idx_acc->component_type !=
TG3_COMPONENT_TYPE_UNSIGNED_INT) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, ipath,
"mesh[%u].primitives[%u].indices accessor "
"componentType must be UNSIGNED_BYTE, "
"UNSIGNED_SHORT, or UNSIGNED_INT",
i, pi);
}
}
}
/* Validate material index */
if (prim->material >= 0 &&
(uint32_t)prim->material >= model->materials_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, ipath,
"mesh[%u].primitives[%u].material index %d "
"out of range [0, %u)",
i, pi, prim->material, model->materials_count);
}
/* Validate primitive mode */
if (prim->mode < TG3_MODE_POINTS || prim->mode > TG3_MODE_TRIANGLE_FAN) {
TG3__VERR(TG3_ERR_INVALID_MESH, ipath,
"mesh[%u].primitives[%u].mode %d is not valid "
"(must be TG3_MODE_POINTS..TG3_MODE_TRIANGLE_FAN)",
i, pi, prim->mode);
}
}
}
/* ------------------------------------------------------------------
* 6. Nodes
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->nodes_count; i++) {
const tg3_node *node = &model->nodes[i];
snprintf(path, sizeof(path), "/nodes/%u", i);
if (node->camera >= 0 && (uint32_t)node->camera >= model->cameras_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"node[%u].camera index %d out of range [0, %u)",
i, node->camera, model->cameras_count);
}
if (node->skin >= 0 && (uint32_t)node->skin >= model->skins_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"node[%u].skin index %d out of range [0, %u)",
i, node->skin, model->skins_count);
}
if (node->mesh >= 0 && (uint32_t)node->mesh >= model->meshes_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"node[%u].mesh index %d out of range [0, %u)",
i, node->mesh, model->meshes_count);
}
if (node->light >= 0 && (uint32_t)node->light >= model->lights_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"node[%u].light index %d out of range [0, %u)",
i, node->light, model->lights_count);
}
for (uint32_t ci = 0; ci < node->children_count; ci++) {
int32_t child_idx = node->children[ci];
if (child_idx < 0 || (uint32_t)child_idx >= model->nodes_count) {
TG3__VERR(TG3_ERR_INVALID_NODE, path,
"node[%u].children[%u] = %d is out of range [0, %u)",
i, ci, child_idx, model->nodes_count);
} else if ((uint32_t)child_idx == i) {
TG3__VERR(TG3_ERR_INVALID_NODE, path,
"node[%u].children[%u] = %d creates a self-reference",
i, ci, child_idx);
}
}
}
/* ------------------------------------------------------------------
* 7. Skins
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->skins_count; i++) {
const tg3_skin *skin = &model->skins[i];
snprintf(path, sizeof(path), "/skins/%u", i);
if (skin->joints_count == 0) {
TG3__VERR(TG3_ERR_INVALID_SKIN, path,
"skin[%u] must have at least one joint", i);
}
for (uint32_t ji = 0; ji < skin->joints_count; ji++) {
int32_t j = skin->joints[ji];
if (j < 0 || (uint32_t)j >= model->nodes_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"skin[%u].joints[%u] = %d out of range [0, %u)",
i, ji, j, model->nodes_count);
}
}
if (skin->inverse_bind_matrices >= 0) {
if ((uint32_t)skin->inverse_bind_matrices >= model->accessors_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"skin[%u].inverseBindMatrices index %d "
"out of range [0, %u)",
i, skin->inverse_bind_matrices,
model->accessors_count);
} else {
const tg3_accessor *ibm =
&model->accessors[(uint32_t)skin->inverse_bind_matrices];
if (ibm->type != TG3_TYPE_MAT4) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"skin[%u].inverseBindMatrices accessor "
"must be MAT4 type", i);
}
if (ibm->component_type != TG3_COMPONENT_TYPE_FLOAT) {
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
"skin[%u].inverseBindMatrices accessor "
"must have FLOAT componentType", i);
}
if (ibm->count < (uint64_t)skin->joints_count) {
TG3__VERR(TG3_ERR_INVALID_SKIN, path,
"skin[%u].inverseBindMatrices count %llu < "
"joints count %u",
i, (unsigned long long)ibm->count,
skin->joints_count);
}
}
}
if (skin->skeleton >= 0 &&
(uint32_t)skin->skeleton >= model->nodes_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"skin[%u].skeleton index %d out of range [0, %u)",
i, skin->skeleton, model->nodes_count);
}
}
/* ------------------------------------------------------------------
* 8. Animations
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->animations_count; i++) {
const tg3_animation *anim = &model->animations[i];
snprintf(path, sizeof(path), "/animations/%u", i);
for (uint32_t si = 0; si < anim->samplers_count; si++) {
const tg3_animation_sampler *samp = &anim->samplers[si];
snprintf(ipath, sizeof(ipath),
"/animations/%u/samplers/%u", i, si);
if (samp->input < 0 ||
(uint32_t)samp->input >= model->accessors_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, ipath,
"animation[%u].samplers[%u].input accessor index %d "
"out of range [0, %u)",
i, si, samp->input, model->accessors_count);
}
if (samp->output < 0 ||
(uint32_t)samp->output >= model->accessors_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, ipath,
"animation[%u].samplers[%u].output accessor index %d "
"out of range [0, %u)",
i, si, samp->output, model->accessors_count);
}
if (samp->interpolation.data) {
if (!tg3_str_equals_cstr(samp->interpolation, "LINEAR") &&
!tg3_str_equals_cstr(samp->interpolation, "STEP") &&
!tg3_str_equals_cstr(samp->interpolation, "CUBICSPLINE")) {
TG3__VWARN(TG3_ERR_INVALID_VALUE, ipath,
"animation[%u].samplers[%u].interpolation "
"\"%.*s\" is not a recognised value",
i, si,
(int)samp->interpolation.len,
samp->interpolation.data);
}
}
}
for (uint32_t ci = 0; ci < anim->channels_count; ci++) {
const tg3_animation_channel *chan = &anim->channels[ci];
snprintf(ipath, sizeof(ipath),
"/animations/%u/channels/%u", i, ci);
if (chan->sampler < 0 ||
(uint32_t)chan->sampler >= anim->samplers_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, ipath,
"animation[%u].channels[%u].sampler index %d "
"out of range [0, %u)",
i, ci, chan->sampler, anim->samplers_count);
}
if (chan->target.node >= 0 &&
(uint32_t)chan->target.node >= model->nodes_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, ipath,
"animation[%u].channels[%u].target.node index %d "
"out of range [0, %u)",
i, ci, chan->target.node, model->nodes_count);
}
if (!chan->target.path.data || chan->target.path.len == 0) {
TG3__VERR(TG3_ERR_MISSING_REQUIRED, ipath,
"animation[%u].channels[%u].target.path is required",
i, ci);
} else if (
!tg3_str_equals_cstr(chan->target.path, "translation") &&
!tg3_str_equals_cstr(chan->target.path, "rotation") &&
!tg3_str_equals_cstr(chan->target.path, "scale") &&
!tg3_str_equals_cstr(chan->target.path, "weights")) {
TG3__VERR(TG3_ERR_INVALID_VALUE, ipath,
"animation[%u].channels[%u].target.path \"%.*s\" "
"must be \"translation\", \"rotation\", "
"\"scale\", or \"weights\"",
i, ci,
(int)chan->target.path.len,
chan->target.path.data);
}
}
}
/* ------------------------------------------------------------------
* 9. Textures
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->textures_count; i++) {
const tg3_texture *tex = &model->textures[i];
snprintf(path, sizeof(path), "/textures/%u", i);
if (tex->sampler >= 0 &&
(uint32_t)tex->sampler >= model->samplers_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"texture[%u].sampler index %d out of range [0, %u)",
i, tex->sampler, model->samplers_count);
}
if (tex->source >= 0 &&
(uint32_t)tex->source >= model->images_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"texture[%u].source index %d out of range [0, %u)",
i, tex->source, model->images_count);
}
}
/* ------------------------------------------------------------------
* 10. Materials
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->materials_count; i++) {
const tg3_material *mat = &model->materials[i];
snprintf(path, sizeof(path), "/materials/%u", i);
if (mat->alpha_mode.data) {
if (!tg3_str_equals_cstr(mat->alpha_mode, "OPAQUE") &&
!tg3_str_equals_cstr(mat->alpha_mode, "MASK") &&
!tg3_str_equals_cstr(mat->alpha_mode, "BLEND")) {
TG3__VERR(TG3_ERR_INVALID_MATERIAL, path,
"material[%u].alphaMode \"%.*s\" must be "
"\"OPAQUE\", \"MASK\", or \"BLEND\"",
i, (int)mat->alpha_mode.len, mat->alpha_mode.data);
}
}
tg3__val_check_tex_index(errors, arena,
mat->pbr_metallic_roughness.base_color_texture.index,
model->textures_count, i,
"pbrMetallicRoughness.baseColorTexture");
tg3__val_check_tex_index(errors, arena,
mat->pbr_metallic_roughness.metallic_roughness_texture.index,
model->textures_count, i,
"pbrMetallicRoughness.metallicRoughnessTexture");
tg3__val_check_tex_index(errors, arena,
mat->normal_texture.index,
model->textures_count, i, "normalTexture");
tg3__val_check_tex_index(errors, arena,
mat->occlusion_texture.index,
model->textures_count, i, "occlusionTexture");
tg3__val_check_tex_index(errors, arena,
mat->emissive_texture.index,
model->textures_count, i, "emissiveTexture");
}
/* ------------------------------------------------------------------
* 11. Cameras
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->cameras_count; i++) {
const tg3_camera *cam = &model->cameras[i];
snprintf(path, sizeof(path), "/cameras/%u", i);
if (!cam->type.data || cam->type.len == 0) {
TG3__VERR(TG3_ERR_MISSING_REQUIRED, path,
"camera[%u].type is required", i);
} else if (tg3_str_equals_cstr(cam->type, "perspective")) {
if (cam->perspective.yfov <= 0.0) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].perspective.yfov must be > 0", i);
}
if (cam->perspective.znear <= 0.0) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].perspective.znear must be > 0", i);
}
} else if (tg3_str_equals_cstr(cam->type, "orthographic")) {
if (cam->orthographic.xmag == 0.0) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].orthographic.xmag must not be 0", i);
}
if (cam->orthographic.ymag == 0.0) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].orthographic.ymag must not be 0", i);
}
if (cam->orthographic.znear < 0.0) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].orthographic.znear must be >= 0", i);
}
if (cam->orthographic.zfar <= cam->orthographic.znear) {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].orthographic.zfar must be > znear", i);
}
} else {
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
"camera[%u].type \"%.*s\" must be "
"\"perspective\" or \"orthographic\"",
i, (int)cam->type.len, cam->type.data);
}
}
/* ------------------------------------------------------------------
* 12. Scenes
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->scenes_count; i++) {
const tg3_scene *scene = &model->scenes[i];
snprintf(path, sizeof(path), "/scenes/%u", i);
for (uint32_t ni = 0; ni < scene->nodes_count; ni++) {
int32_t n = scene->nodes[ni];
if (n < 0 || (uint32_t)n >= model->nodes_count) {
TG3__VERR(TG3_ERR_INVALID_SCENE, path,
"scene[%u].nodes[%u] = %d out of range [0, %u)",
i, ni, n, model->nodes_count);
}
}
}
/* ------------------------------------------------------------------
* 13. Images
* ------------------------------------------------------------------ */
for (uint32_t i = 0; i < model->images_count; i++) {
const tg3_image *img = &model->images[i];
snprintf(path, sizeof(path), "/images/%u", i);
if (img->buffer_view >= 0 &&
(uint32_t)img->buffer_view >= model->buffer_views_count) {
TG3__VERR(TG3_ERR_INVALID_INDEX, path,
"image[%u].bufferView index %d out of range [0, %u)",
i, img->buffer_view, model->buffer_views_count);
}
/* An image should have either a URI or a bufferView */
{
int has_uri = (img->uri.data != NULL && img->uri.len > 0);
int has_bv = (img->buffer_view >= 0);
if (!has_uri && !has_bv && img->image.count == 0) {
TG3__VWARN(TG3_ERR_INVALID_IMAGE, path,
"image[%u] has no URI, bufferView, or decoded data",
i);
}
if (has_uri && has_bv) {
TG3__VWARN(TG3_ERR_INVALID_IMAGE, path,
"image[%u] has both URI and bufferView; "
"bufferView takes precedence",
i);
}
}
}
#undef TG3__VERR
#undef TG3__VWARN
return first_err;
}
#endif /* TINYGLTF3_IMPLEMENTATION */
#endif /* TINY_GLTF_V3_H_ */