mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-06-08 11:13:50 +00:00
Compare commits
9 Commits
v3
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb9c9421d | ||
|
|
ebc9f765c5 | ||
|
|
5a3f56d43c | ||
|
|
577f08a680 | ||
|
|
aa41d25054 | ||
|
|
721430aa5f | ||
|
|
87b0d175c6 | ||
|
|
9b3947c976 | ||
|
|
9bcfcea6e0 |
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
|
||||
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_V3_VALIDATOR_TOOL=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
@@ -37,6 +37,17 @@ jobs:
|
||||
- name: Run tests
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Run v3 validator on tracked glTF files
|
||||
continue-on-error: true
|
||||
run: |
|
||||
rc=0
|
||||
while IFS= read -r path; do
|
||||
echo "::group::Validate ${path}"
|
||||
./build/tools/validator/tinygltf3-validator "${path}" || rc=1
|
||||
echo "::endgroup::"
|
||||
done < <(git ls-files '*.gltf')
|
||||
exit $rc
|
||||
|
||||
# Linux x64 - Clang 21
|
||||
linux-clang-x64:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -13,6 +13,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
option(TINYGLTF_BUILD_LOADER_EXAMPLE "Build loader_example(load glTF and dump infos)" ON)
|
||||
option(TINYGLTF_BUILD_GL_EXAMPLES "Build GL exampels(requires glfw, OpenGL, etc)" OFF)
|
||||
option(TINYGLTF_BUILD_VALIDATOR_EXAMPLE "Build validator exampe" OFF)
|
||||
option(TINYGLTF_BUILD_V3_VALIDATOR_TOOL "Build tg3_validate CLI tool" OFF)
|
||||
option(TINYGLTF_BUILD_BUILDER_EXAMPLE "Build glTF builder example" OFF)
|
||||
option(TINYGLTF_BUILD_TESTS "Build unit tests" OFF)
|
||||
option(TINYGLTF_HEADER_ONLY "On: header-only mode. Off: create tinygltf library(No TINYGLTF_IMPLEMENTATION required in your project)" OFF)
|
||||
@@ -35,6 +36,10 @@ if (TINYGLTF_BUILD_VALIDATOR_EXAMPLE)
|
||||
add_subdirectory( examples/validator )
|
||||
endif (TINYGLTF_BUILD_VALIDATOR_EXAMPLE)
|
||||
|
||||
if (TINYGLTF_BUILD_V3_VALIDATOR_TOOL)
|
||||
add_subdirectory( tools/validator )
|
||||
endif (TINYGLTF_BUILD_V3_VALIDATOR_TOOL)
|
||||
|
||||
if (TINYGLTF_BUILD_BUILDER_EXAMPLE)
|
||||
add_subdirectory ( examples/build-gltf )
|
||||
endif (TINYGLTF_BUILD_BUILDER_EXAMPLE)
|
||||
@@ -65,6 +70,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)
|
||||
|
||||
#
|
||||
|
||||
1079
tests/v3/tester_v3.cc
Normal file
1079
tests/v3/tester_v3.cc
Normal file
File diff suppressed because it is too large
Load Diff
908
tiny_gltf_v3.h
908
tiny_gltf_v3.h
@@ -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 */
|
||||
@@ -388,6 +392,7 @@ typedef struct tg3_asset {
|
||||
typedef struct tg3_buffer {
|
||||
tg3_str name;
|
||||
tg3_span_u8 data;
|
||||
uint64_t byte_length; /* Declared glTF buffer.byteLength */
|
||||
tg3_str uri;
|
||||
tg3_extras_ext ext;
|
||||
} tg3_buffer;
|
||||
@@ -623,6 +628,8 @@ typedef struct tg3_perspective_camera {
|
||||
double yfov;
|
||||
double zfar; /* 0 = infinite */
|
||||
double znear;
|
||||
int32_t has_aspect_ratio;
|
||||
int32_t has_zfar;
|
||||
tg3_extras_ext ext;
|
||||
} tg3_perspective_camera;
|
||||
|
||||
@@ -970,6 +977,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 +1131,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 +1493,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));
|
||||
}
|
||||
|
||||
@@ -2333,6 +2360,7 @@ static int tg3__parse_buffer(tg3__parse_ctx *ctx, const tg3__json &o,
|
||||
|
||||
uint64_t byte_length = 0;
|
||||
tg3__parse_uint64(ctx, o, "byteLength", &byte_length, 1, "/buffer");
|
||||
buf->byte_length = byte_length;
|
||||
|
||||
/* Load buffer data */
|
||||
if (ctx->is_binary && buf_idx == 0 && buf->uri.len == 0) {
|
||||
@@ -2801,6 +2829,10 @@ static int tg3__parse_camera(tg3__parse_ctx *ctx, const tg3__json &o,
|
||||
if (cam->type.data && tg3_str_equals_cstr(cam->type, "perspective")) {
|
||||
auto p_it = o.find("perspective");
|
||||
if (p_it != o.end() && p_it->is_object()) {
|
||||
cam->perspective.has_aspect_ratio =
|
||||
((*p_it).find("aspectRatio") != (*p_it).end()) ? 1 : 0;
|
||||
cam->perspective.has_zfar =
|
||||
((*p_it).find("zfar") != (*p_it).end()) ? 1 : 0;
|
||||
tg3__parse_double(ctx, *p_it, "aspectRatio",
|
||||
&cam->perspective.aspect_ratio, 0, "/camera/perspective");
|
||||
tg3__parse_double(ctx, *p_it, "yfov",
|
||||
@@ -3717,7 +3749,7 @@ static tg3__json tg3__serialize_buffer(const tg3_buffer *b, int wd,
|
||||
(void)wd;
|
||||
tg3__json o = tg3__json::object();
|
||||
tg3__serialize_str(o, "name", b->name);
|
||||
o["byteLength"] = (int64_t)b->data.count;
|
||||
o["byteLength"] = (int64_t)(b->byte_length ? b->byte_length : b->data.count);
|
||||
|
||||
if (b->uri.data && b->uri.len > 0) {
|
||||
tg3__serialize_str(o, "uri", b->uri);
|
||||
@@ -4054,10 +4086,10 @@ static tg3__json tg3__serialize_camera(const tg3_camera *c, int wd) {
|
||||
|
||||
if (c->type.data && tg3_str_equals_cstr(c->type, "perspective")) {
|
||||
tg3__json p = tg3__json::object();
|
||||
if (c->perspective.aspect_ratio > 0)
|
||||
if (c->perspective.has_aspect_ratio)
|
||||
p["aspectRatio"] = c->perspective.aspect_ratio;
|
||||
p["yfov"] = c->perspective.yfov;
|
||||
if (c->perspective.zfar > 0) p["zfar"] = c->perspective.zfar;
|
||||
if (c->perspective.has_zfar) p["zfar"] = c->perspective.zfar;
|
||||
p["znear"] = c->perspective.znear;
|
||||
tg3__serialize_extras_ext(p, &c->perspective.ext);
|
||||
o["perspective"] = static_cast<tg3__json&&>(p);
|
||||
@@ -4431,6 +4463,868 @@ 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.
|
||||
* If arena allocation fails (OOM) the error entry is silently dropped
|
||||
* rather than storing dangling stack pointers. */
|
||||
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 = NULL;
|
||||
const char *path = NULL;
|
||||
|
||||
if (arena) {
|
||||
msg = tg3__arena_strdup(arena, msg_buf, (size_t)n);
|
||||
if (json_path_buf) {
|
||||
path = tg3__arena_strdup(arena, json_path_buf,
|
||||
strlen(json_path_buf));
|
||||
}
|
||||
}
|
||||
|
||||
/* If arena duplication failed (OOM), drop the entry to avoid storing
|
||||
* dangling stack pointers. */
|
||||
if (!msg) return;
|
||||
|
||||
tg3__error_push(es, sev, code, msg, path, -1);
|
||||
}
|
||||
|
||||
/* Helper: validate a tg3_texture_info index.
|
||||
* Returns 1 if an error was pushed, 0 otherwise. */
|
||||
static int 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 0; /* 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);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t tg3__val_accessor_elem_size_with_padding(
|
||||
int32_t component_type,
|
||||
int32_t type) {
|
||||
int32_t comp_sz = tg3_component_size(component_type);
|
||||
if (comp_sz <= 0) return 0;
|
||||
|
||||
switch (type) {
|
||||
case TG3_TYPE_MAT2: {
|
||||
uint64_t column_stride = ((uint64_t)(2 * comp_sz) + 3ull) & ~3ull;
|
||||
return 2ull * column_stride;
|
||||
}
|
||||
case TG3_TYPE_MAT3: {
|
||||
uint64_t column_stride = ((uint64_t)(3 * comp_sz) + 3ull) & ~3ull;
|
||||
return 3ull * column_stride;
|
||||
}
|
||||
case TG3_TYPE_MAT4: {
|
||||
uint64_t column_stride = ((uint64_t)(4 * comp_sz) + 3ull) & ~3ull;
|
||||
return 4ull * column_stride;
|
||||
}
|
||||
default: {
|
||||
int32_t num_comp = tg3_num_components(type);
|
||||
if (num_comp <= 0) return 0;
|
||||
return (uint64_t)comp_sz * (uint64_t)num_comp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
uint64_t declared_len =
|
||||
(buf->byte_length > 0) ? buf->byte_length : buf->data.count;
|
||||
uint64_t end = bv->byte_offset + bv->byte_length;
|
||||
if (end > declared_len) {
|
||||
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
|
||||
"bufferView[%u] byteOffset+byteLength (%llu) exceeds "
|
||||
"buffer[%d].byteLength (%llu)",
|
||||
i,
|
||||
(unsigned long long)end,
|
||||
bv->buffer,
|
||||
(unsigned long long)declared_len);
|
||||
} else if (buf->data.data != NULL && end > buf->data.count) {
|
||||
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
|
||||
"bufferView[%u] byteOffset+byteLength (%llu) exceeds "
|
||||
"loaded buffer[%d] size (%llu)",
|
||||
i,
|
||||
(unsigned long long)end,
|
||||
bv->buffer,
|
||||
(unsigned long long)buf->data.count);
|
||||
}
|
||||
}
|
||||
|
||||
if (bv->byte_length == 0) {
|
||||
TG3__VERR(TG3_ERR_INVALID_BUFFER_VIEW, path,
|
||||
"bufferView[%u].byteLength must be > 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);
|
||||
uint64_t elem_sz =
|
||||
tg3__val_accessor_elem_size_with_padding(
|
||||
acc->component_type, acc->type);
|
||||
if (comp_sz > 0 && elem_sz > 0) {
|
||||
uint64_t stride = (bv->byte_stride > 0)
|
||||
? (uint64_t)bv->byte_stride
|
||||
: elem_sz;
|
||||
uint64_t max_byte =
|
||||
acc->byte_offset +
|
||||
(uint64_t)(acc->count - 1) * stride +
|
||||
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);
|
||||
}
|
||||
/* glTF also requires the effective offset into the
|
||||
* underlying buffer (bufferView.byteOffset +
|
||||
* accessor.byteOffset) to be component-size aligned. */
|
||||
if (((bv->byte_offset + acc->byte_offset) %
|
||||
(uint64_t)comp_sz) != 0) {
|
||||
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
|
||||
"accessor[%u] effective byte offset %llu "
|
||||
"(bufferView[%d].byteOffset %llu + "
|
||||
"accessor.byteOffset %llu) is not aligned "
|
||||
"to componentType size %d",
|
||||
i,
|
||||
(unsigned long long)
|
||||
(bv->byte_offset + acc->byte_offset),
|
||||
acc->buffer_view,
|
||||
(unsigned long long)bv->byte_offset,
|
||||
(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 ((uint64_t)acc->sparse.count > acc->count) {
|
||||
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
|
||||
"accessor[%u].sparse.count %d exceeds accessor.count %llu",
|
||||
i, acc->sparse.count,
|
||||
(unsigned long long)acc->count);
|
||||
}
|
||||
if (acc->sparse.indices.component_type !=
|
||||
TG3_COMPONENT_TYPE_UNSIGNED_BYTE &&
|
||||
acc->sparse.indices.component_type !=
|
||||
TG3_COMPONENT_TYPE_UNSIGNED_SHORT &&
|
||||
acc->sparse.indices.component_type !=
|
||||
TG3_COMPONENT_TYPE_UNSIGNED_INT) {
|
||||
TG3__VERR(TG3_ERR_INVALID_ACCESSOR, path,
|
||||
"accessor[%u].sparse.indices.componentType %d must be "
|
||||
"UNSIGNED_BYTE, UNSIGNED_SHORT, or UNSIGNED_INT",
|
||||
i, acc->sparse.indices.component_type);
|
||||
}
|
||||
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
|
||||
* ------------------------------------------------------------------ */
|
||||
uint32_t *node_parent_counts = NULL;
|
||||
if (model->nodes_count > 0) {
|
||||
node_parent_counts = (uint32_t *)calloc(model->nodes_count,
|
||||
sizeof(uint32_t));
|
||||
if (!node_parent_counts) {
|
||||
TG3__VERR(TG3_ERR_OUT_OF_MEMORY, "/nodes",
|
||||
"OOM while validating node graph");
|
||||
}
|
||||
}
|
||||
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);
|
||||
} else if (node_parent_counts) {
|
||||
uint32_t child_u = (uint32_t)child_idx;
|
||||
node_parent_counts[child_u] += 1;
|
||||
if (node_parent_counts[child_u] > 1) {
|
||||
char cpath[256];
|
||||
snprintf(cpath, sizeof(cpath), "/nodes/%u", child_u);
|
||||
TG3__VERR(TG3_ERR_INVALID_NODE, cpath,
|
||||
"node[%u] has multiple parents", child_u);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node_parent_counts) {
|
||||
/* Queue entries are written before they are read, so zero-init is not
|
||||
* required here. */
|
||||
uint32_t *queue = (uint32_t *)malloc(sizeof(uint32_t) * model->nodes_count);
|
||||
if (!queue) {
|
||||
TG3__VERR(TG3_ERR_OUT_OF_MEMORY, "/nodes",
|
||||
"OOM while validating node graph");
|
||||
} else {
|
||||
uint32_t queue_head = 0;
|
||||
uint32_t queue_tail = 0;
|
||||
uint32_t visited = 0;
|
||||
|
||||
for (uint32_t i = 0; i < model->nodes_count; i++) {
|
||||
if (node_parent_counts[i] == 0) {
|
||||
queue[queue_tail++] = i;
|
||||
}
|
||||
}
|
||||
|
||||
while (queue_head < queue_tail) {
|
||||
uint32_t node_idx = queue[queue_head++];
|
||||
const tg3_node *node = &model->nodes[node_idx];
|
||||
visited++;
|
||||
|
||||
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 ||
|
||||
(uint32_t)child_idx == node_idx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node_parent_counts[(uint32_t)child_idx] > 0) {
|
||||
node_parent_counts[(uint32_t)child_idx]--;
|
||||
if (node_parent_counts[(uint32_t)child_idx] == 0) {
|
||||
queue[queue_tail++] = (uint32_t)child_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (visited != model->nodes_count) {
|
||||
for (uint32_t i = 0; i < model->nodes_count; i++) {
|
||||
if (node_parent_counts[i] > 0) {
|
||||
snprintf(path, sizeof(path), "/nodes/%u", i);
|
||||
TG3__VERR(TG3_ERR_INVALID_NODE, path,
|
||||
"node[%u] participates in a cycle", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(queue);
|
||||
}
|
||||
free(node_parent_counts);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 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) {
|
||||
TG3__VERR(TG3_ERR_MISSING_REQUIRED, ipath,
|
||||
"animation[%u].channels[%u].target.node is required",
|
||||
i, ci);
|
||||
} else if ((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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Texture index checks: update first_err when any out-of-range
|
||||
* index is detected (the helper bypasses TG3__VERR). */
|
||||
if (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")) {
|
||||
if (first_err == TG3_OK) first_err = TG3_ERR_INVALID_INDEX;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
if (cam->perspective.has_aspect_ratio &&
|
||||
cam->perspective.aspect_ratio <= 0.0) {
|
||||
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
|
||||
"camera[%u].perspective.aspectRatio must be > 0", i);
|
||||
}
|
||||
if (cam->perspective.has_zfar &&
|
||||
cam->perspective.zfar <= 0.0) {
|
||||
TG3__VERR(TG3_ERR_INVALID_CAMERA, path,
|
||||
"camera[%u].perspective.zfar 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);
|
||||
}
|
||||
|
||||
if (img->buffer_view >= 0 &&
|
||||
(img->mime_type.data == NULL || img->mime_type.len == 0)) {
|
||||
TG3__VERR(TG3_ERR_INVALID_IMAGE, path,
|
||||
"image[%u] uses bufferView but is missing mimeType",
|
||||
i);
|
||||
}
|
||||
|
||||
/* 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__VERR(TG3_ERR_INVALID_IMAGE, path,
|
||||
"image[%u] must not have both URI and bufferView",
|
||||
i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef TG3__VERR
|
||||
#undef TG3__VWARN
|
||||
|
||||
return first_err;
|
||||
}
|
||||
|
||||
#endif /* TINYGLTF3_IMPLEMENTATION */
|
||||
|
||||
#endif /* TINY_GLTF_V3_H_ */
|
||||
|
||||
22
tools/validator/CMakeLists.txt
Normal file
22
tools/validator/CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
add_executable(tinygltf3_validator
|
||||
validator.cc
|
||||
)
|
||||
|
||||
target_include_directories(tinygltf3_validator PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../..
|
||||
)
|
||||
|
||||
if (TINYGLTF_USE_CUSTOM_JSON)
|
||||
target_compile_definitions(tinygltf3_validator PRIVATE TINYGLTF_USE_CUSTOM_JSON)
|
||||
endif ()
|
||||
|
||||
set_target_properties(tinygltf3_validator PROPERTIES
|
||||
CXX_STANDARD 11
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
CXX_EXTENSIONS OFF
|
||||
OUTPUT_NAME tinygltf3-validator
|
||||
)
|
||||
|
||||
install(TARGETS tinygltf3_validator
|
||||
DESTINATION bin
|
||||
)
|
||||
95
tools/validator/validator.cc
Normal file
95
tools/validator/validator.cc
Normal file
@@ -0,0 +1,95 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#define TINYGLTF3_IMPLEMENTATION
|
||||
#define TINYGLTF3_ENABLE_FS
|
||||
#include "tiny_gltf_v3.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char *severity_name(tg3_severity severity) {
|
||||
switch (severity) {
|
||||
case TG3_SEVERITY_WARNING:
|
||||
return "warning";
|
||||
case TG3_SEVERITY_ERROR:
|
||||
return "error";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
void print_errors(std::ostream &os, const tg3_error_stack *errors) {
|
||||
const uint32_t count = tg3_errors_count(errors);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const tg3_error_entry *entry = tg3_errors_get(errors, i);
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
os << severity_name(entry->severity);
|
||||
if (entry->json_path && entry->json_path[0] != '\0') {
|
||||
os << " " << entry->json_path;
|
||||
}
|
||||
if (entry->message && entry->message[0] != '\0') {
|
||||
os << ": " << entry->message;
|
||||
}
|
||||
os << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
int usage(const char *name) {
|
||||
std::cerr << "Usage: " << name << " <path/to/model.gltf|model.glb>\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) {
|
||||
return usage(argv[0]);
|
||||
}
|
||||
|
||||
const char *filename = argv[1];
|
||||
|
||||
tg3_model model;
|
||||
tg3_error_stack errors;
|
||||
tg3_parse_options options;
|
||||
|
||||
std::memset(&model, 0, sizeof(model));
|
||||
model.default_scene = -1;
|
||||
tg3_error_stack_init(&errors);
|
||||
tg3_parse_options_init(&options);
|
||||
|
||||
tg3_error_code rc =
|
||||
tg3_parse_file(&model, &errors, filename,
|
||||
static_cast<uint32_t>(std::strlen(filename)), &options);
|
||||
|
||||
if (tg3_errors_count(&errors) > 0) {
|
||||
print_errors(std::cerr, &errors);
|
||||
}
|
||||
|
||||
if (rc != TG3_OK) {
|
||||
tg3_model_free(&model);
|
||||
tg3_error_stack_free(&errors);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
tg3_error_stack_free(&errors);
|
||||
tg3_error_stack_init(&errors);
|
||||
|
||||
rc = tg3_validate(&model, &errors);
|
||||
|
||||
if (tg3_errors_count(&errors) > 0) {
|
||||
print_errors(rc == TG3_OK ? std::cout : std::cerr, &errors);
|
||||
}
|
||||
|
||||
if (rc == TG3_OK) {
|
||||
std::cout << filename << ": valid glTF 2.0\n";
|
||||
}
|
||||
|
||||
tg3_model_free(&model);
|
||||
tg3_error_stack_free(&errors);
|
||||
|
||||
return (rc == TG3_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
Reference in New Issue
Block a user