mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-09-14 14:28:28 +00:00
Add Emscripten WASM build and three.js web demo
web/ compiles the v3 C runtime with emcc (Emscripten SDK from ~/work/emsdk by default) and ships a simple browser demo that loads glTF/GLB files via drag & drop and renders them with three.js: - loader.c: C bridge exporting flattened primitives, materials, textures (bufferView + data-URI paths) and the scene graph to JS - main.js/index.html/style.css: three.js viewer (OrbitControls, PBR materials, textures, node hierarchy, primitive modes) - gen_sample.py: generates a small self-contained textured Cube.glb sample from tracked assets (no large binaries committed) - Makefile: emcc build (MODULARIZE + ES6, no filesystem), sample generation and local http server targets Verified end-to-end: native bridge tests, Node harness exercising the WASM exports (parse, geometry, images, clear) and HTTP serving.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -81,6 +81,11 @@ tests/tester_v3_json_c
|
||||
tests/v3/fuzzer/fuzz_gltf_v3
|
||||
tests/v3/fuzzer/fuzz_gltf_v3_c
|
||||
|
||||
# Generated by web/Makefile (make / make sample)
|
||||
web/tinygltf_v3.js
|
||||
web/tinygltf_v3.wasm
|
||||
web/Cube.glb
|
||||
|
||||
# unignore
|
||||
!Makefile
|
||||
!tests/Makefile
|
||||
|
||||
@@ -67,8 +67,17 @@ $ cmake -B build && cmake --build build && ctest --test-dir build --output-on-fa
|
||||
|
||||
# Meson
|
||||
$ meson setup build && meson compile -C build && meson test -C build
|
||||
|
||||
# WebAssembly (requires an Emscripten SDK, defaults to ~/work/emsdk)
|
||||
$ make -C web && make -C web sample
|
||||
```
|
||||
|
||||
## Web/WASM demo
|
||||
|
||||
[`web/`](web/) contains a browser demo that compiles the v3 C runtime with
|
||||
Emscripten and renders glTF/GLB files with three.js (file picker + drag &
|
||||
drop). See [`web/README.md`](web/README.md) for build instructions.
|
||||
|
||||
## Legacy v1/v2 (C++)
|
||||
|
||||
The previous C++ implementation (`tiny_gltf.h`, `tiny_gltf.cc`,
|
||||
|
||||
44
web/Makefile
Normal file
44
web/Makefile
Normal file
@@ -0,0 +1,44 @@
|
||||
# Build the tinygltf v3 C WASM module for the three.js demo.
|
||||
#
|
||||
# Requires an Emscripten SDK. The default EMSDK path is $(HOME)/work/emsdk;
|
||||
# override with `make EMSDK=/path/to/emsdk`.
|
||||
|
||||
EMSDK ?= $(HOME)/work/emsdk
|
||||
ROOT := $(abspath ../)
|
||||
|
||||
SHELL := /bin/bash
|
||||
EMCC := source $(EMSDK)/emsdk_env.sh && emcc
|
||||
|
||||
CFLAGS := -O3 -std=c11 -Wall -Wextra -I$(ROOT)
|
||||
EMFLAGS := -sMODULARIZE=1 \
|
||||
-sEXPORT_ES6=1 \
|
||||
-sEXPORT_NAME=createTinyGLTF \
|
||||
-sENVIRONMENT=web \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sSTACK_SIZE=1048576 \
|
||||
-sFILESYSTEM=0 \
|
||||
-sEXPORTED_FUNCTIONS=_malloc,_free \
|
||||
-sEXPORTED_RUNTIME_METHODS=getValue,setValue,UTF8ToString,HEAP32,HEAPU32,HEAPF32,HEAPU8
|
||||
|
||||
SRC := $(ROOT)/tiny_gltf_v3.c loader.c
|
||||
OUT_JS := tinygltf_v3.js
|
||||
OUT_WASM := tinygltf_v3.wasm
|
||||
SAMPLE := Cube.glb
|
||||
|
||||
.PHONY: all sample serve clean
|
||||
|
||||
all: $(OUT_JS)
|
||||
|
||||
$(OUT_JS): loader.c Makefile $(ROOT)/tiny_gltf_v3.h $(ROOT)/tiny_gltf_v3.c $(ROOT)/tinygltf_json_c.h
|
||||
$(EMCC) $(CFLAGS) $(EMFLAGS) -o $(OUT_JS) $(SRC)
|
||||
|
||||
# Generate a small self-contained textured sample GLB (Cube.glb).
|
||||
sample:
|
||||
python3 gen_sample.py
|
||||
|
||||
# Serve the demo locally (http://localhost:8000)
|
||||
serve:
|
||||
python3 -m http.server 8000
|
||||
|
||||
clean:
|
||||
rm -f $(OUT_JS) $(OUT_WASM) $(SAMPLE)
|
||||
59
web/README.md
Normal file
59
web/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# tinygltf v3 C — Web/WASM demo (three.js)
|
||||
|
||||
A minimal single-page demo that parses glTF/GLB in the browser using the
|
||||
tinygltf v3 C runtime compiled to WebAssembly, then renders the result with
|
||||
[three.js](https://threejs.org).
|
||||
|
||||
```
|
||||
web/
|
||||
loader.c — C bridge: parse bytes, flatten primitives/materials/textures/nodes for JS
|
||||
index.html — demo page (file picker + drag & drop)
|
||||
main.js — three.js viewer consuming the WASM exports
|
||||
style.css
|
||||
Makefile — Emscripten build
|
||||
gen_sample.py — generates Cube.glb (small self-contained textured cube)
|
||||
Cube.glb — sample model, generated with `make sample`
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
* An Emscripten SDK. The Makefile defaults to `~/work/emsdk`
|
||||
(override with `make EMSDK=/path/to/emsdk`).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
$ cd web
|
||||
$ make # produces tinygltf_v3.js + tinygltf_v3.wasm
|
||||
$ make sample # generate Cube.glb (self-contained textured cube)
|
||||
$ make serve # python3 -m http.server 8000
|
||||
```
|
||||
|
||||
Then open http://localhost:8000 and drop a `.glb` / `.gltf` file onto the
|
||||
page, or click **Load sample**.
|
||||
|
||||
## Notes
|
||||
|
||||
* Assets must be **self-contained**: GLB binary chunk or embedded data-URI
|
||||
buffers/images. External `.bin` / image file references are not resolved
|
||||
(no filesystem is linked into the module: `-sFILESYSTEM=0`).
|
||||
* Primitives must use `float` `VEC3` positions (the common case); sparse
|
||||
accessors and `double` attributes are skipped with a warning message.
|
||||
* Image decoding happens client-side: raw image bytes are passed to
|
||||
`createImageBitmap()` and wrapped in a `THREE.CanvasTexture`.
|
||||
|
||||
## C exports (loader.c)
|
||||
|
||||
All functions are exported as `_tg3w_*` on the Emscripten module:
|
||||
|
||||
| Function | Description |
|
||||
| --- | --- |
|
||||
| `tg3w_parse(ptr, size)` | Parse GLB/glTF bytes (auto-detect), flatten model |
|
||||
| `tg3w_clear()` | Free the current model and flattened buffers |
|
||||
| `tg3w_last_error()` / `tg3w_error_message(i)` / `tg3w_error_count()` | Parse diagnostics |
|
||||
| `tg3w_prim(i)` | Primitive record (material, mode, counts, buffer offsets; `nrm_offset`/`uv_offset` are -1 when absent) |
|
||||
| `tg3w_positions()/normals()/uvs()/indices()` | Flattened vertex/index arrays |
|
||||
| `tg3w_mesh_prim_start/count(m)` | Primitive range of a mesh |
|
||||
| `tg3w_material_*` | PBR factors, alpha mode, base color texture |
|
||||
| `tg3w_texture_source(i)`, `tg3w_image_bytes/size/mime(i)` | Texture image bytes |
|
||||
| `tg3w_node_*`, `tg3w_scene_*`, `tg3w_default_scene()` | Scene graph |
|
||||
108
web/gen_sample.py
Normal file
108
web/gen_sample.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a small self-contained textured cube GLB (web/Cube.glb).
|
||||
|
||||
Uses the tracked Cube assets from models/ (Cube.gltf geometry in Cube.bin)
|
||||
plus a tiny checkerboard PNG generated with only the standard library, so
|
||||
the demo ships without large binary assets.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
BIN = ROOT / "models" / "Cube" / "Cube.bin"
|
||||
OUT = Path(__file__).resolve().parent / "Cube.glb"
|
||||
|
||||
|
||||
def make_png(width=64, height=64, rgb=(230, 140, 60), rgb2=(245, 245, 245)):
|
||||
"""Minimal zlib-based RGB PNG writer."""
|
||||
def chunk(tag, data):
|
||||
c = tag + data
|
||||
return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) # 8-bit RGB
|
||||
raw = bytearray()
|
||||
for y in range(height):
|
||||
raw.append(0) # filter: none
|
||||
for x in range(width):
|
||||
c = rgb if (x // 8 + y // 8) % 2 == 0 else rgb2
|
||||
raw += bytes(c)
|
||||
return (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", ihdr)
|
||||
+ chunk(b"IDAT", zlib.compress(bytes(raw), 9))
|
||||
+ chunk(b"IEND", b"")
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
bin_data = BIN.read_bytes()
|
||||
|
||||
png = make_png()
|
||||
total = len(bin_data) + len(png)
|
||||
|
||||
j = {
|
||||
"asset": {"version": "2.0", "generator": "tinygltf web demo (gen_sample.py)"},
|
||||
"scene": 0,
|
||||
"scenes": [{"nodes": [0]}],
|
||||
"nodes": [{"mesh": 0, "name": "Cube"}],
|
||||
"meshes": [{
|
||||
"primitives": [{
|
||||
"attributes": {
|
||||
"POSITION": 1, "NORMAL": 2, "TANGENT": 3, "TEXCOORD_0": 4,
|
||||
},
|
||||
"indices": 0,
|
||||
"material": 0,
|
||||
}]
|
||||
}],
|
||||
"materials": [{
|
||||
"name": "checker",
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorFactor": [1.0, 1.0, 1.0, 1.0],
|
||||
"baseColorTexture": {"index": 0},
|
||||
"metallicFactor": 0.0,
|
||||
"roughnessFactor": 1.0,
|
||||
},
|
||||
}],
|
||||
"textures": [{"source": 0, "sampler": 0}],
|
||||
"images": [{"bufferView": 5, "mimeType": "image/png"}],
|
||||
"samplers": [{"magFilter": 9729, "minFilter": 9987, "wrapS": 10497, "wrapT": 10497}],
|
||||
"buffers": [{"byteLength": total}],
|
||||
"bufferViews": [
|
||||
{"buffer": 0, "byteOffset": 0, "byteLength": 72, "target": 34963}, # indices
|
||||
{"buffer": 0, "byteOffset": 72, "byteLength": 432, "target": 34962}, # positions
|
||||
{"buffer": 0, "byteOffset": 504, "byteLength": 432, "target": 34962}, # normals
|
||||
{"buffer": 0, "byteOffset": 936, "byteLength": 576, "target": 34962}, # tangents
|
||||
{"buffer": 0, "byteOffset": 1512, "byteLength": 288, "target": 34962}, # uvs
|
||||
{"buffer": 0, "byteOffset": len(bin_data), "byteLength": len(png)}, # image
|
||||
],
|
||||
"accessors": [
|
||||
{"bufferView": 0, "componentType": 5123, "count": 36, "type": "SCALAR", "min": [0], "max": [35]},
|
||||
{"bufferView": 1, "componentType": 5126, "count": 36, "type": "VEC3", "min": [-1, -1, -1], "max": [1, 1, 1]},
|
||||
{"bufferView": 2, "componentType": 5126, "count": 36, "type": "VEC3"},
|
||||
{"bufferView": 3, "componentType": 5126, "count": 36, "type": "VEC4"},
|
||||
{"bufferView": 4, "componentType": 5126, "count": 36, "type": "VEC2"},
|
||||
],
|
||||
}
|
||||
|
||||
json_chunk = json.dumps(j, separators=(",", ":")).encode("utf-8")
|
||||
json_chunk += b" " * ((4 - len(json_chunk) % 4) % 4)
|
||||
bin_chunk = bin_data + png
|
||||
bin_chunk += b"\x00" * ((4 - len(bin_chunk) % 4) % 4)
|
||||
|
||||
total_len = 12 + 8 + len(json_chunk) + 8 + len(bin_chunk)
|
||||
glb = struct.pack("<4sII", b"glTF", 2, total_len)
|
||||
glb += struct.pack("<II", len(json_chunk), 0x4E4F534A) + json_chunk
|
||||
glb += struct.pack("<II", len(bin_chunk), 0x004E4942) + bin_chunk
|
||||
|
||||
OUT.write_bytes(glb)
|
||||
print(f"wrote {OUT} ({len(glb)} bytes, image {len(png)} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
28
web/index.html
Normal file
28
web/index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>tinygltf v3 — three.js WASM demo</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>tinygltf v3 (C) + three.js</h1>
|
||||
<div class="controls">
|
||||
<label class="btn" for="file-input">Open glTF/GLB…</label>
|
||||
<input type="file" id="file-input" accept=".gltf,.glb,model/gltf+json,model/gltf-binary" hidden>
|
||||
<button class="btn" id="sample-btn" hidden>Load sample (Cube.glb)</button>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div id="drop-zone">
|
||||
<p>Drop a <code>.glb</code> / <code>.gltf</code> file here, or use <b>Open glTF/GLB…</b></p>
|
||||
<p class="hint">Note: assets must be self-contained (GLB chunk or embedded data URI).</p>
|
||||
</div>
|
||||
<div id="viewer" hidden></div>
|
||||
</main>
|
||||
<footer id="status">Loading WASM module…</footer>
|
||||
<script type="module" src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
548
web/loader.c
Normal file
548
web/loader.c
Normal file
@@ -0,0 +1,548 @@
|
||||
/*
|
||||
* loader.c — tinygltf v3 C WASM bridge for the three.js demo.
|
||||
*
|
||||
* Parses glTF/GLB bytes passed in from JavaScript (Emscripten HEAPU8),
|
||||
* then exposes flattened per-primitive vertex/index data plus materials,
|
||||
* textures and the node hierarchy through EMSCRIPTEN_KEEPALIVE functions.
|
||||
*
|
||||
* Build with emcc (see Makefile). No filesystem or image decoding is
|
||||
* used: all assets must be embedded in the file (GLB chunk or data URI).
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "tiny_gltf_v3.h"
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
#include <emscripten.h>
|
||||
#define TG3W_EXPORT EMSCRIPTEN_KEEPALIVE
|
||||
#else
|
||||
#define TG3W_EXPORT
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported model info */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
int32_t material; /* material index or -1 */
|
||||
int32_t mode; /* TG3_MODE_* */
|
||||
uint32_t vertex_count;
|
||||
uint32_t index_count;
|
||||
uint32_t pos_offset; /* offset in tg3w_positions (floats, 3 * vertex_count) */
|
||||
int32_t nrm_offset; /* -1 = absent; floats, 3 * vertex_count */
|
||||
int32_t uv_offset; /* -1 = absent; floats, 2 * vertex_count */
|
||||
uint32_t idx_offset; /* offset in tg3w_indices (uint32) */
|
||||
} tg3w_prim;
|
||||
|
||||
typedef struct {
|
||||
float base_color[4];
|
||||
float metallic;
|
||||
float roughness;
|
||||
int32_t base_color_texture; /* texture index or -1 */
|
||||
int32_t alpha_mode; /* 0 = OPAQUE, 1 = MASK, 2 = BLEND */
|
||||
float alpha_cutoff;
|
||||
int32_t double_sided;
|
||||
} tg3w_mat;
|
||||
|
||||
typedef struct {
|
||||
int32_t mesh; /* mesh index or -1 */
|
||||
float translation[3];
|
||||
float rotation[4];
|
||||
float scale[3];
|
||||
} tg3w_node;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Module state (single parse at a time, like a simple loading screen) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static tg3_model g_model;
|
||||
static tg3_error_stack g_errors;
|
||||
static tg3_parse_options g_opts_;
|
||||
static tg3w_prim *g_prims = NULL;
|
||||
static uint32_t g_prim_count = 0;
|
||||
static uint32_t *g_mesh_prim_start = NULL;
|
||||
static uint32_t *g_mesh_prim_count = NULL;
|
||||
static float *g_positions = NULL;
|
||||
static float *g_normals = NULL;
|
||||
static float *g_uvs = NULL;
|
||||
static uint32_t *g_indices = NULL;
|
||||
static char g_last_error[512];
|
||||
|
||||
static int g_inited = 0;
|
||||
|
||||
/* Free the current model and all flattened buffers. */
|
||||
TG3W_EXPORT void tg3w_clear(void) {
|
||||
tg3_model_free(&g_model);
|
||||
free(g_prims); g_prims = NULL;
|
||||
free(g_positions); g_positions = NULL;
|
||||
free(g_normals); g_normals = NULL;
|
||||
free(g_uvs); g_uvs = NULL;
|
||||
free(g_indices); g_indices = NULL;
|
||||
free(g_mesh_prim_start); g_mesh_prim_start = NULL;
|
||||
free(g_mesh_prim_count); g_mesh_prim_count = NULL;
|
||||
g_prim_count = 0;
|
||||
}
|
||||
|
||||
static void tg3w_init_once(void) {
|
||||
if (!g_inited) {
|
||||
tg3_parse_options_init(&g_opts_);
|
||||
tg3_error_stack_init(&g_errors);
|
||||
g_inited = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Accessor helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static uint32_t tg3w_type_components(int32_t type) {
|
||||
switch (type) {
|
||||
case TG3_TYPE_SCALAR: return 1;
|
||||
case TG3_TYPE_VEC2: return 2;
|
||||
case TG3_TYPE_VEC3: return 3;
|
||||
case TG3_TYPE_VEC4: return 4;
|
||||
case TG3_TYPE_MAT4: return 16;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static const uint8_t *tg3w_accessor_ptr(const tg3_model *m,
|
||||
const tg3_accessor *a,
|
||||
uint64_t *stride_out) {
|
||||
if (a->buffer_view < 0 || a->sparse.is_sparse) {
|
||||
return NULL;
|
||||
}
|
||||
const tg3_buffer_view *bv = &m->buffer_views[a->buffer_view];
|
||||
if (bv->buffer < 0) {
|
||||
return NULL;
|
||||
}
|
||||
const tg3_buffer *b = &m->buffers[bv->buffer];
|
||||
if (!b->data.data || b->data.count < bv->byte_offset + bv->byte_length) {
|
||||
return NULL;
|
||||
}
|
||||
uint32_t comps = tg3w_type_components(a->type);
|
||||
uint64_t elem = 0;
|
||||
switch (a->component_type) {
|
||||
case TG3_COMPONENT_TYPE_FLOAT: elem = 4; break;
|
||||
case TG3_COMPONENT_TYPE_DOUBLE: elem = 8; break;
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_BYTE: elem = 1; break;
|
||||
case TG3_COMPONENT_TYPE_BYTE: elem = 1; break;
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_SHORT: elem = 2; break;
|
||||
case TG3_COMPONENT_TYPE_SHORT: elem = 2; break;
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_INT: elem = 4; break;
|
||||
case TG3_COMPONENT_TYPE_INT: elem = 4; break;
|
||||
default: return NULL;
|
||||
}
|
||||
uint64_t stride = bv->byte_stride ? bv->byte_stride : elem * comps;
|
||||
*stride_out = stride;
|
||||
return b->data.data + bv->byte_offset + a->byte_offset;
|
||||
}
|
||||
|
||||
static int tg3w_attr_index(const tg3_primitive *p, const char *name) {
|
||||
for (uint32_t i = 0; i < p->attributes_count; i++) {
|
||||
const tg3_str_int_pair *a = &p->attributes[i];
|
||||
if (a->key.len == (uint32_t)strlen(name) &&
|
||||
strncmp(a->key.data, name, a->key.len) == 0) {
|
||||
return a->value;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Parse + flatten */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
TG3W_EXPORT int tg3w_parse(const uint8_t *data, uint32_t size) {
|
||||
tg3w_init_once();
|
||||
|
||||
tg3_model_free(&g_model);
|
||||
tg3_error_stack_free(&g_errors);
|
||||
tg3_error_stack_init(&g_errors);
|
||||
tg3_parse_options_init(&g_opts_);
|
||||
|
||||
tg3_error_code err = tg3_parse_auto(&g_model, &g_errors, data, size,
|
||||
NULL, 0, &g_opts_);
|
||||
if (err != TG3_OK) {
|
||||
snprintf(g_last_error, sizeof(g_last_error), "parse failed: %d", (int)err);
|
||||
return (int)err;
|
||||
}
|
||||
|
||||
/* Pass 1: count vertices/indices per primitive. */
|
||||
uint32_t prims = 0;
|
||||
uint64_t total_pos = 0, total_nrm = 0, total_uv = 0, total_idx = 0;
|
||||
for (uint32_t mi = 0; mi < g_model.meshes_count; mi++) {
|
||||
const tg3_mesh *mesh = &g_model.meshes[mi];
|
||||
for (uint32_t pi = 0; pi < mesh->primitives_count; pi++) {
|
||||
const tg3_primitive *p = &mesh->primitives[pi];
|
||||
int pos_i = tg3w_attr_index(p, "POSITION");
|
||||
int nrm_i = tg3w_attr_index(p, "NORMAL");
|
||||
int uv_i = tg3w_attr_index(p, "TEXCOORD_0");
|
||||
uint64_t vcount = 0;
|
||||
if (pos_i >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[pos_i];
|
||||
if (a->component_type == TG3_COMPONENT_TYPE_FLOAT &&
|
||||
a->type == TG3_TYPE_VEC3 && !a->sparse.is_sparse) {
|
||||
vcount = a->count;
|
||||
}
|
||||
}
|
||||
uint64_t icount = 0;
|
||||
if (p->indices >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[p->indices];
|
||||
if (!a->sparse.is_sparse) {
|
||||
switch (a->component_type) {
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_BYTE:
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_SHORT:
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_INT:
|
||||
icount = a->count;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vcount == 0) {
|
||||
continue; /* unsupported primitive (e.g. sparse/double) */
|
||||
}
|
||||
total_pos += vcount * 3;
|
||||
if (nrm_i >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[nrm_i];
|
||||
if (a->component_type == TG3_COMPONENT_TYPE_FLOAT &&
|
||||
a->type == TG3_TYPE_VEC3 && !a->sparse.is_sparse) {
|
||||
total_nrm += vcount * 3;
|
||||
}
|
||||
}
|
||||
if (uv_i >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[uv_i];
|
||||
if (a->component_type == TG3_COMPONENT_TYPE_FLOAT &&
|
||||
a->type == TG3_TYPE_VEC2 && !a->sparse.is_sparse) {
|
||||
total_uv += vcount * 2;
|
||||
}
|
||||
}
|
||||
total_idx += icount;
|
||||
prims++;
|
||||
}
|
||||
}
|
||||
|
||||
if (prims == 0) {
|
||||
snprintf(g_last_error, sizeof(g_last_error),
|
||||
"no renderable primitives (float VEC3 POSITION required)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Allocate flattened arrays. */
|
||||
free(g_prims); free(g_positions); free(g_normals); free(g_uvs); free(g_indices);
|
||||
free(g_mesh_prim_start); free(g_mesh_prim_count);
|
||||
g_prims = (tg3w_prim *)malloc(prims * sizeof(tg3w_prim));
|
||||
g_positions = (float *)malloc(total_pos * sizeof(float));
|
||||
g_normals = total_nrm ? (float *)malloc(total_nrm * sizeof(float)) : NULL;
|
||||
g_uvs = total_uv ? (float *)malloc(total_uv * sizeof(float)) : NULL;
|
||||
g_indices = (uint32_t *)malloc(total_idx * sizeof(uint32_t));
|
||||
g_mesh_prim_start = (uint32_t *)malloc(g_model.meshes_count * sizeof(uint32_t));
|
||||
g_mesh_prim_count = (uint32_t *)malloc(g_model.meshes_count * sizeof(uint32_t));
|
||||
g_prim_count = 0;
|
||||
|
||||
if (!g_prims || (total_pos && !g_positions) || (total_nrm && !g_normals) ||
|
||||
(total_uv && !g_uvs) || (total_idx && !g_indices) ||
|
||||
!g_mesh_prim_start || !g_mesh_prim_count) {
|
||||
snprintf(g_last_error, sizeof(g_last_error), "out of memory");
|
||||
tg3w_clear();
|
||||
return -1;
|
||||
}
|
||||
memset(g_mesh_prim_start, 0, g_model.meshes_count * sizeof(uint32_t));
|
||||
memset(g_mesh_prim_count, 0, g_model.meshes_count * sizeof(uint32_t));
|
||||
|
||||
/* Pass 2: fill. */
|
||||
uint32_t p_off = 0, n_off = 0, u_off = 0, i_off = 0;
|
||||
for (uint32_t mi = 0; mi < g_model.meshes_count; mi++) {
|
||||
const tg3_mesh *mesh = &g_model.meshes[mi];
|
||||
g_mesh_prim_start[mi] = g_prim_count;
|
||||
for (uint32_t pi = 0; pi < mesh->primitives_count; pi++) {
|
||||
const tg3_primitive *p = &mesh->primitives[pi];
|
||||
int pos_i = tg3w_attr_index(p, "POSITION");
|
||||
uint64_t vcount = 0;
|
||||
uint64_t pstride = 0;
|
||||
const uint8_t *pp = NULL;
|
||||
if (pos_i >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[pos_i];
|
||||
if (a->component_type == TG3_COMPONENT_TYPE_FLOAT &&
|
||||
a->type == TG3_TYPE_VEC3 && !a->sparse.is_sparse) {
|
||||
vcount = a->count;
|
||||
pp = tg3w_accessor_ptr(&g_model, a, &pstride);
|
||||
}
|
||||
}
|
||||
if (!pp || vcount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tg3w_prim *out = &g_prims[g_prim_count];
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->nrm_offset = -1;
|
||||
out->uv_offset = -1;
|
||||
out->material = p->material;
|
||||
out->mode = (p->mode == -1) ? TG3_MODE_TRIANGLES : p->mode;
|
||||
out->vertex_count = (uint32_t)vcount;
|
||||
out->pos_offset = p_off;
|
||||
|
||||
for (uint64_t v = 0; v < vcount; v++) {
|
||||
const float *f = (const float *)(pp + v * pstride);
|
||||
g_positions[p_off + (uint32_t)v * 3 + 0] = f[0];
|
||||
g_positions[p_off + (uint32_t)v * 3 + 1] = f[1];
|
||||
g_positions[p_off + (uint32_t)v * 3 + 2] = f[2];
|
||||
}
|
||||
p_off += (uint32_t)vcount * 3;
|
||||
|
||||
int nrm_i = tg3w_attr_index(p, "NORMAL");
|
||||
if (nrm_i >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[nrm_i];
|
||||
uint64_t stride = 0;
|
||||
const uint8_t *np = tg3w_accessor_ptr(&g_model, a, &stride);
|
||||
if (np && a->component_type == TG3_COMPONENT_TYPE_FLOAT &&
|
||||
a->type == TG3_TYPE_VEC3 && a->count == vcount) {
|
||||
out->nrm_offset = (int32_t)n_off;
|
||||
for (uint64_t v = 0; v < vcount; v++) {
|
||||
const float *f = (const float *)(np + v * stride);
|
||||
g_normals[n_off + (uint32_t)v * 3 + 0] = f[0];
|
||||
g_normals[n_off + (uint32_t)v * 3 + 1] = f[1];
|
||||
g_normals[n_off + (uint32_t)v * 3 + 2] = f[2];
|
||||
}
|
||||
n_off += (uint32_t)vcount * 3;
|
||||
}
|
||||
}
|
||||
|
||||
int uv_i = tg3w_attr_index(p, "TEXCOORD_0");
|
||||
if (uv_i >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[uv_i];
|
||||
uint64_t stride = 0;
|
||||
const uint8_t *up = tg3w_accessor_ptr(&g_model, a, &stride);
|
||||
if (up && a->component_type == TG3_COMPONENT_TYPE_FLOAT &&
|
||||
a->type == TG3_TYPE_VEC2 && a->count == vcount) {
|
||||
out->uv_offset = (int32_t)u_off;
|
||||
for (uint64_t v = 0; v < vcount; v++) {
|
||||
const float *f = (const float *)(up + v * stride);
|
||||
g_uvs[u_off + (uint32_t)v * 2 + 0] = f[0];
|
||||
g_uvs[u_off + (uint32_t)v * 2 + 1] = f[1];
|
||||
}
|
||||
u_off += (uint32_t)vcount * 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (p->indices >= 0) {
|
||||
const tg3_accessor *a = &g_model.accessors[p->indices];
|
||||
uint64_t stride = 0;
|
||||
const uint8_t *ip = tg3w_accessor_ptr(&g_model, a, &stride);
|
||||
if (ip && !a->sparse.is_sparse) {
|
||||
out->index_count = (uint32_t)a->count;
|
||||
out->idx_offset = i_off;
|
||||
for (uint64_t v = 0; v < a->count; v++) {
|
||||
uint32_t idx = 0;
|
||||
switch (a->component_type) {
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_BYTE:
|
||||
idx = ((const uint8_t *)ip)[v * stride];
|
||||
break;
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_SHORT:
|
||||
idx = ((const uint16_t *)ip)[v * stride / 2];
|
||||
break;
|
||||
case TG3_COMPONENT_TYPE_UNSIGNED_INT:
|
||||
idx = ((const uint32_t *)ip)[v * stride / 4];
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
g_indices[i_off + (uint32_t)v] = idx;
|
||||
}
|
||||
i_off += (uint32_t)a->count;
|
||||
}
|
||||
}
|
||||
g_prim_count++;
|
||||
}
|
||||
g_mesh_prim_count[mi] = g_prim_count - g_mesh_prim_start[mi];
|
||||
}
|
||||
|
||||
g_last_error[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Getters */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
TG3W_EXPORT const char *tg3w_last_error(void) { return g_last_error; }
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_error_count(void) { return g_errors.count; }
|
||||
|
||||
TG3W_EXPORT const char *tg3w_error_message(uint32_t i) {
|
||||
if (i >= g_errors.count || !g_errors.entries[i].message) return "";
|
||||
return g_errors.entries[i].message;
|
||||
}
|
||||
|
||||
TG3W_EXPORT int tg3w_error_severity(uint32_t i) {
|
||||
if (i >= g_errors.count) return -1;
|
||||
return (int)g_errors.entries[i].severity;
|
||||
}
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_prim_count(void) { return g_prim_count; }
|
||||
|
||||
TG3W_EXPORT const tg3w_prim *tg3w_prim_at(uint32_t i) {
|
||||
if (i >= g_prim_count) return NULL;
|
||||
return &g_prims[i];
|
||||
}
|
||||
|
||||
TG3W_EXPORT const float *tg3w_positions(void) { return g_positions; }
|
||||
TG3W_EXPORT const float *tg3w_normals(void) { return g_normals; }
|
||||
TG3W_EXPORT const float *tg3w_uvs(void) { return g_uvs; }
|
||||
TG3W_EXPORT const uint32_t *tg3w_indices(void) { return g_indices; }
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_mesh_count(void) { return g_model.meshes_count; }
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_mesh_prim_start(uint32_t m) {
|
||||
if (m >= g_model.meshes_count) return 0;
|
||||
return g_mesh_prim_start[m];
|
||||
}
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_mesh_prim_count(uint32_t m) {
|
||||
if (m >= g_model.meshes_count) return 0;
|
||||
return g_mesh_prim_count[m];
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Materials */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_material_count(void) { return g_model.materials_count; }
|
||||
|
||||
TG3W_EXPORT const float *tg3w_material_base_color(uint32_t i) {
|
||||
static float c[4];
|
||||
if (i >= g_model.materials_count) { c[0]=1;c[1]=1;c[2]=1;c[3]=1; return c; }
|
||||
const tg3_material *m = &g_model.materials[i];
|
||||
for (int k = 0; k < 4; k++) c[k] = (float)m->pbr_metallic_roughness.base_color_factor[k];
|
||||
return c;
|
||||
}
|
||||
|
||||
TG3W_EXPORT float tg3w_material_metallic(uint32_t i) {
|
||||
if (i >= g_model.materials_count) return 1.0f;
|
||||
return (float)g_model.materials[i].pbr_metallic_roughness.metallic_factor;
|
||||
}
|
||||
|
||||
TG3W_EXPORT float tg3w_material_roughness(uint32_t i) {
|
||||
if (i >= g_model.materials_count) return 1.0f;
|
||||
return (float)g_model.materials[i].pbr_metallic_roughness.roughness_factor;
|
||||
}
|
||||
|
||||
TG3W_EXPORT int tg3w_material_base_color_texture(uint32_t i) {
|
||||
if (i >= g_model.materials_count) return -1;
|
||||
return g_model.materials[i].pbr_metallic_roughness.base_color_texture.index;
|
||||
}
|
||||
|
||||
TG3W_EXPORT int tg3w_material_alpha_mode(uint32_t i) {
|
||||
if (i >= g_model.materials_count) return 0;
|
||||
const tg3_str *a = &g_model.materials[i].alpha_mode;
|
||||
if (a->len == 5 && strncmp(a->data, "BLEND", 5) == 0) return 2;
|
||||
if (a->len == 4 && strncmp(a->data, "MASK", 4) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
TG3W_EXPORT float tg3w_material_alpha_cutoff(uint32_t i) {
|
||||
if (i >= g_model.materials_count) return 0.5f;
|
||||
return (float)g_model.materials[i].alpha_cutoff;
|
||||
}
|
||||
|
||||
TG3W_EXPORT int tg3w_material_double_sided(uint32_t i) {
|
||||
if (i >= g_model.materials_count) return 0;
|
||||
return g_model.materials[i].double_sided;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Textures (raw image bytes, decoded client-side) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_texture_count(void) { return g_model.textures_count; }
|
||||
TG3W_EXPORT uint32_t tg3w_image_count(void) { return g_model.images_count; }
|
||||
|
||||
TG3W_EXPORT int tg3w_texture_source(uint32_t i) {
|
||||
if (i >= g_model.textures_count) return -1;
|
||||
return g_model.textures[i].source;
|
||||
}
|
||||
|
||||
/* Image payload: either a bufferView (GLB / .bin) or a data URI in the
|
||||
* glTF JSON. The v3 parser does not decode image bytes itself, so expose
|
||||
* both paths to JS. */
|
||||
TG3W_EXPORT int tg3w_image_buffer_view(uint32_t i) {
|
||||
if (i >= g_model.images_count) return -1;
|
||||
return g_model.images[i].buffer_view;
|
||||
}
|
||||
|
||||
TG3W_EXPORT const char *tg3w_image_uri(uint32_t i) {
|
||||
if (i >= g_model.images_count) return NULL;
|
||||
return g_model.images[i].uri.data ? g_model.images[i].uri.data : NULL;
|
||||
}
|
||||
|
||||
TG3W_EXPORT const uint8_t *tg3w_image_bytes(uint32_t i) {
|
||||
if (i >= g_model.images_count) return NULL;
|
||||
int32_t bv = g_model.images[i].buffer_view;
|
||||
if (bv < 0 || bv >= (int32_t)g_model.buffer_views_count) return NULL;
|
||||
const tg3_buffer_view *v = &g_model.buffer_views[bv];
|
||||
if (v->buffer < 0 || v->buffer >= (int32_t)g_model.buffers_count) return NULL;
|
||||
const tg3_buffer *b = &g_model.buffers[v->buffer];
|
||||
if (!b->data.data || b->data.count < v->byte_offset + v->byte_length) return NULL;
|
||||
return b->data.data + v->byte_offset;
|
||||
}
|
||||
|
||||
TG3W_EXPORT uint64_t tg3w_image_size(uint32_t i) {
|
||||
if (i >= g_model.images_count) return 0;
|
||||
int32_t bv = g_model.images[i].buffer_view;
|
||||
if (bv < 0 || bv >= (int32_t)g_model.buffer_views_count) return 0;
|
||||
return g_model.buffer_views[bv].byte_length;
|
||||
}
|
||||
|
||||
TG3W_EXPORT const char *tg3w_image_mime(uint32_t i) {
|
||||
if (i >= g_model.images_count) return "";
|
||||
return g_model.images[i].mime_type.data ? g_model.images[i].mime_type.data : "";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Scene graph (default scene only) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_node_count(void) { return g_model.nodes_count; }
|
||||
|
||||
TG3W_EXPORT int32_t tg3w_node_mesh(uint32_t i) {
|
||||
if (i >= g_model.nodes_count) return -1;
|
||||
return g_model.nodes[i].mesh;
|
||||
}
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_node_child_count(uint32_t i) {
|
||||
if (i >= g_model.nodes_count) return 0;
|
||||
return g_model.nodes[i].children_count;
|
||||
}
|
||||
|
||||
TG3W_EXPORT const int32_t *tg3w_node_children(uint32_t i) {
|
||||
if (i >= g_model.nodes_count) return NULL;
|
||||
return g_model.nodes[i].children;
|
||||
}
|
||||
|
||||
TG3W_EXPORT const float *tg3w_node_trs(uint32_t i) {
|
||||
static float trs[10];
|
||||
if (i >= g_model.nodes_count) { memset(trs, 0, sizeof(trs)); trs[6] = 1.0f; return trs; }
|
||||
const tg3_node *n = &g_model.nodes[i];
|
||||
for (int k = 0; k < 3; k++) trs[k] = (float)n->translation[k];
|
||||
for (int k = 0; k < 4; k++) trs[3 + k] = (float)n->rotation[k];
|
||||
for (int k = 0; k < 3; k++) trs[7 + k] = (float)n->scale[k];
|
||||
return trs;
|
||||
}
|
||||
|
||||
TG3W_EXPORT int tg3w_default_scene(void) { return g_model.default_scene; }
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_scene_count(void) { return g_model.scenes_count; }
|
||||
|
||||
TG3W_EXPORT uint32_t tg3w_scene_node_count(uint32_t i) {
|
||||
if (i >= g_model.scenes_count) return 0;
|
||||
return g_model.scenes[i].nodes_count;
|
||||
}
|
||||
|
||||
TG3W_EXPORT const int32_t *tg3w_scene_nodes(uint32_t i) {
|
||||
if (i >= g_model.scenes_count) return NULL;
|
||||
return g_model.scenes[i].nodes;
|
||||
}
|
||||
309
web/main.js
Normal file
309
web/main.js
Normal file
@@ -0,0 +1,309 @@
|
||||
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
|
||||
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js';
|
||||
import createTinyGLTF from './tinygltf_v3.js';
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const viewerEl = document.getElementById('viewer');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const sampleBtn = document.getElementById('sample-btn');
|
||||
|
||||
function setStatus(msg) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.title = msg;
|
||||
}
|
||||
|
||||
let Module = null;
|
||||
let renderer = null;
|
||||
let scene = null;
|
||||
let camera = null;
|
||||
let controls = null;
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Scene construction */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
// tg3w_prim layout in bytes: material, mode, vertex_count, index_count,
|
||||
// pos_offset, nrm_offset, uv_offset, idx_offset (all 4-byte ints).
|
||||
function readPrim(i) {
|
||||
const p = Module._tg3w_prim(i);
|
||||
const get = (off) => Module.getValue(p + off, 'i32');
|
||||
return {
|
||||
material: get(0),
|
||||
mode: get(4),
|
||||
vertexCount: get(8),
|
||||
indexCount: get(12),
|
||||
posOffset: get(16),
|
||||
nrmOffset: get(20),
|
||||
uvOffset: get(24),
|
||||
idxOffset: get(28),
|
||||
};
|
||||
}
|
||||
|
||||
function copyF32(offsetFloats, count) {
|
||||
if (!offsetFloats || !count) return null;
|
||||
return Module.HEAPF32.subarray(offsetFloats, offsetFloats + count).slice();
|
||||
}
|
||||
|
||||
function makeTexture(texIdx) {
|
||||
const src = Module._tg3w_texture_source(texIdx);
|
||||
if (src < 0) return null;
|
||||
let blobPromise = null;
|
||||
|
||||
const bv = Module._tg3w_image_buffer_view(src);
|
||||
if (bv >= 0) {
|
||||
const ptr = Module._tg3w_image_bytes(src);
|
||||
const size = Module._tg3w_image_size(src);
|
||||
if (!ptr || !size) return null;
|
||||
const bytes = Module.HEAPU8.slice(ptr, ptr + size);
|
||||
const mime = Module.UTF8ToString(Module._tg3w_image_mime(src)) || 'image/png';
|
||||
blobPromise = Promise.resolve(new Blob([bytes], { type: mime }));
|
||||
} else {
|
||||
const uriPtr = Module._tg3w_image_uri(src);
|
||||
if (!uriPtr) return null;
|
||||
const uri = Module.UTF8ToString(uriPtr);
|
||||
const m = /^data:([^;,]+)(;base64)?,(.*)$/s.exec(uri);
|
||||
if (!m) return null; // external file URI — not resolvable without FS
|
||||
const bytes = m[2]
|
||||
? Uint8Array.from(atob(m[3]), (c) => c.charCodeAt(0))
|
||||
: new TextEncoder().encode(decodeURIComponent(m[3]));
|
||||
blobPromise = Promise.resolve(new Blob([bytes], { type: m[1] }));
|
||||
}
|
||||
|
||||
return blobPromise.then((blob) => createImageBitmap(blob)).then((bmp) => {
|
||||
const tex = new THREE.CanvasTexture(bmp);
|
||||
tex.colorSpace = THREE.SRGBColorSpace;
|
||||
return tex;
|
||||
});
|
||||
}
|
||||
|
||||
function makeMaterial(mi) {
|
||||
const color = copyF32(Module._tg3w_material_base_color(mi) / 4, 4);
|
||||
const metalness = Module._tg3w_material_metallic(mi);
|
||||
const roughness = Module._tg3w_material_roughness(mi);
|
||||
const alphaMode = Module._tg3w_material_alpha_mode(mi);
|
||||
const alphaCutoff = Module._tg3w_material_alpha_cutoff(mi);
|
||||
const doubleSided = Module._tg3w_material_double_sided(mi);
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(color[0], color[1], color[2]),
|
||||
metalness,
|
||||
roughness,
|
||||
transparent: alphaMode === 2,
|
||||
alphaTest: alphaMode === 1 ? alphaCutoff : 0,
|
||||
side: doubleSided ? THREE.DoubleSide : THREE.FrontSide,
|
||||
});
|
||||
const texIdx = Module._tg3w_material_base_color_texture(mi);
|
||||
if (texIdx >= 0) {
|
||||
return makeTexture(texIdx).then((tex) => {
|
||||
if (tex) mat.map = tex;
|
||||
return mat;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(mat);
|
||||
}
|
||||
|
||||
// Convert triangle strip/fan index buffers to plain triangles.
|
||||
function triangulate(indices, mode) {
|
||||
if (mode === 4) return indices;
|
||||
const out = [];
|
||||
const n = indices.length;
|
||||
if (mode === 5) { // TRIANGLE_STRIP
|
||||
for (let i = 2; i < n; i++) {
|
||||
if (i % 2 === 0) out.push(indices[i - 2], indices[i - 1], indices[i]);
|
||||
else out.push(indices[i - 1], indices[i - 2], indices[i]);
|
||||
}
|
||||
} else if (mode === 6) { // TRIANGLE_FAN
|
||||
for (let i = 2; i < n; i++) out.push(indices[0], indices[i - 1], indices[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function makePrimitive(i) {
|
||||
const p = readPrim(i);
|
||||
const positions = copyF32(p.posOffset, p.vertexCount * 3);
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
if (p.nrmOffset >= 0) {
|
||||
geometry.setAttribute('normal', new THREE.BufferAttribute(copyF32(p.nrmOffset, p.vertexCount * 3), 3));
|
||||
}
|
||||
if (p.uvOffset >= 0) {
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(copyF32(p.uvOffset, p.vertexCount * 2), 2));
|
||||
}
|
||||
if (p.indexCount) {
|
||||
const raw = Module.HEAPU32.subarray(p.idxOffset, p.idxOffset + p.indexCount).slice();
|
||||
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(triangulate(raw, p.mode)), 1));
|
||||
}
|
||||
|
||||
return makeMaterial(p.material).then((mat) => {
|
||||
if (p.mode === 0) return new THREE.Points(geometry, mat);
|
||||
if (p.mode === 1 || p.mode === 2 || p.mode === 3) {
|
||||
const cls = p.mode === 1 ? THREE.Line : p.mode === 2 ? THREE.LineLoop : THREE.LineSegments;
|
||||
return new cls(geometry, new THREE.LineBasicMaterial({ color: mat.color }));
|
||||
}
|
||||
return new THREE.Mesh(geometry, mat);
|
||||
});
|
||||
}
|
||||
|
||||
function buildNodeGraph(nodeIndices, parent) {
|
||||
const jobs = [];
|
||||
const visit = (nodeIdx, parentGroup) => {
|
||||
const trs = copyF32(Module._tg3w_node_trs(nodeIdx) / 4, 10);
|
||||
const group = new THREE.Group();
|
||||
group.position.set(trs[0], trs[1], trs[2]);
|
||||
group.quaternion.set(trs[3], trs[4], trs[5], trs[6]);
|
||||
group.scale.set(trs[7], trs[8], trs[9]);
|
||||
|
||||
const meshIdx = Module._tg3w_node_mesh(nodeIdx);
|
||||
if (meshIdx >= 0) {
|
||||
const start = Module._tg3w_mesh_prim_start(meshIdx);
|
||||
const count = Module._tg3w_mesh_prim_count(meshIdx);
|
||||
for (let k = 0; k < count; k++) {
|
||||
jobs.push(makePrimitive(start + k).then((mesh) => {
|
||||
mesh.name = `mesh${meshIdx}[${k}]`;
|
||||
group.add(mesh);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const childCount = Module._tg3w_node_child_count(nodeIdx);
|
||||
const childrenPtr = Module._tg3w_node_children(nodeIdx);
|
||||
for (let c = 0; c < childCount; c++) {
|
||||
const childIdx = Module.getValue(childrenPtr + c * 4, 'i32');
|
||||
visit(childIdx, group);
|
||||
}
|
||||
parentGroup.add(group);
|
||||
};
|
||||
for (const n of nodeIndices) visit(n, parent);
|
||||
return Promise.all(jobs).then(() => parent);
|
||||
}
|
||||
|
||||
async function buildScene() {
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x14161a);
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x223344, 1.0));
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
dirLight.position.set(5, 8, 6);
|
||||
scene.add(dirLight);
|
||||
|
||||
const root = new THREE.Group();
|
||||
scene.add(root);
|
||||
|
||||
const sceneCount = Module._tg3w_scene_count();
|
||||
let nodes = [];
|
||||
const defaultScene = Module._tg3w_default_scene();
|
||||
if (defaultScene >= 0 && defaultScene < sceneCount) {
|
||||
const n = Module._tg3w_scene_node_count(defaultScene);
|
||||
const p = Module._tg3w_scene_nodes(defaultScene);
|
||||
for (let i = 0; i < n; i++) nodes.push(Module.getValue(p + i * 4, 'i32'));
|
||||
} else {
|
||||
for (let i = 0; i < Module._tg3w_node_count(); i++) nodes.push(i);
|
||||
}
|
||||
await buildNodeGraph(nodes, root);
|
||||
|
||||
// Frame the camera on the geometry.
|
||||
const box = new THREE.Box3().setFromObject(root);
|
||||
if (!box.isEmpty()) {
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3()).length() || 1;
|
||||
camera.position.copy(center).add(new THREE.Vector3(size, size * 0.8, size));
|
||||
camera.lookAt(center);
|
||||
controls.target.copy(center);
|
||||
controls.update();
|
||||
}
|
||||
|
||||
viewerEl.hidden = false;
|
||||
dropZone.hidden = true;
|
||||
setStatus(`Rendered: ${Module._tg3w_prim_count()} primitives, ${Module._tg3w_node_count()} nodes`);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Loading */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
async function loadModel(bytes) {
|
||||
setStatus('Parsing with tinygltf v3 (C/WASM)…');
|
||||
const ptr = Module._malloc(bytes.length);
|
||||
Module.HEAPU8.set(bytes, ptr);
|
||||
const rc = Module._tg3w_parse(ptr, bytes.length);
|
||||
Module._free(ptr);
|
||||
|
||||
if (rc !== 0) {
|
||||
const first = Module.UTF8ToString(Module._tg3w_error_message(0));
|
||||
const last = Module.UTF8ToString(Module._tg3w_last_error());
|
||||
setStatus(`Parse failed (rc=${rc}): ${first || last}`);
|
||||
return;
|
||||
}
|
||||
const warn = Module._tg3w_error_count();
|
||||
await buildScene();
|
||||
if (warn > 0) {
|
||||
setStatus(`Loaded with ${warn} warning(s) — see console`);
|
||||
for (let i = 0; i < warn; i++) {
|
||||
console.warn(`[tg3] ${Module.UTF8ToString(Module._tg3w_error_message(i))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFile(file) {
|
||||
if (!file) return;
|
||||
const buf = await file.arrayBuffer();
|
||||
await loadModel(new Uint8Array(buf));
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Setup */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
async function init() {
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
|
||||
viewerEl.appendChild(renderer.domElement);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(50, viewerEl.clientWidth / viewerEl.clientHeight, 0.01, 10000);
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
const w = viewerEl.clientWidth;
|
||||
const h = viewerEl.clientHeight;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
});
|
||||
|
||||
renderer.setAnimationLoop(() => {
|
||||
if (scene) renderer.render(scene, camera);
|
||||
});
|
||||
|
||||
try {
|
||||
Module = await createTinyGLTF();
|
||||
} catch (e) {
|
||||
setStatus(`Failed to load WASM: ${e}`);
|
||||
return;
|
||||
}
|
||||
setStatus('WASM ready — drop a .glb/.gltf file');
|
||||
|
||||
fileInput.addEventListener('change', () => handleFile(fileInput.files[0]));
|
||||
sampleBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const res = await fetch('Cube.glb');
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
await loadModel(new Uint8Array(await res.arrayBuffer()));
|
||||
} catch (e) {
|
||||
setStatus(`Sample load failed: ${e}`);
|
||||
}
|
||||
});
|
||||
sampleBtn.hidden = false;
|
||||
|
||||
for (const ev of ['dragenter', 'dragover']) {
|
||||
dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
||||
}
|
||||
for (const ev of ['dragleave', 'drop']) {
|
||||
dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); });
|
||||
}
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
||||
if (f) handleFile(f);
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
91
web/style.css
Normal file
91
web/style.css
Normal file
@@ -0,0 +1,91 @@
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #14161a;
|
||||
color: #e8e8e8;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
background: #1c1f26;
|
||||
border-bottom: 1px solid #2c313a;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.controls { display: flex; gap: 8px; }
|
||||
|
||||
.btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: #e8e8e8;
|
||||
background: #2d66d6;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover { background: #3b76e8; }
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#drop-zone {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border: 2px dashed #3a4150;
|
||||
margin: 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
#drop-zone.dragover {
|
||||
border-color: #2d66d6;
|
||||
background: #1a2030;
|
||||
}
|
||||
|
||||
#drop-zone p { margin: 0; font-size: 15px; }
|
||||
#drop-zone .hint { font-size: 12px; color: #8a93a5; }
|
||||
#drop-zone code { background: #232833; padding: 2px 6px; border-radius: 3px; }
|
||||
|
||||
#viewer {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#viewer canvas { display: block; width: 100%; height: 100%; }
|
||||
|
||||
footer {
|
||||
padding: 6px 16px;
|
||||
font-size: 12px;
|
||||
color: #9aa3b4;
|
||||
background: #1c1f26;
|
||||
border-top: 1px solid #2c313a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
Reference in New Issue
Block a user