var gltf = this var native_decode = gltf.decode var fd = use('fd') var blob = use('blob') var http = use('http') var json = use('json') var png = use('cell-image/png') var jpg = use('cell-image/jpg') function dirname(path) { var idx = path.lastIndexOf("/") if (idx == -1) return "" return text(path, 0, idx) } function join_paths(base, rel) { base = base.replace(/\/+$/, "") rel = rel.replace(/^\/+/, "") if (!base) return rel if (!rel) return base return base + "/" + rel } function ends_with(path, suffix) { return length(path) >= length(suffix) && text(path, length(path) - length(suffix)) == suffix } function spaces(count) { var s = "" for (var i = 0; i < count; i++) s += " " return s } function zeros_blob(bytes) { var b = blob(bytes * 8) for (var i = 0; i < bytes; i++) b.write_fit(0, 8) stone(b) return b } function u32(out, v) { out.write_fit(v, 32) } function warn(asset, msg, mode) { if (mode == null) mode = "collect" if (mode == "throw") throw Error(msg) asset.warnings.push(msg) } function parse_data_uri(uri) { var comma = uri.indexOf(",") if (comma == -1) return null var meta = text(uri, 5, comma) var data = text(uri, comma + 1) var mime = "text/plain" var is_base64 = false if (length(meta) > 0) { var parts = array(meta, ";") if (parts[0]) mime = parts[0] for (var i = 1; i < length(parts); i++) { if (parts[i] == "base64") is_base64 = true } } return { mime: mime, data: data, base64: is_base64 } } function load_uri_default(uri, base_dir) { if (uri.indexOf("://") != -1) return http.fetch(uri) if (!base_dir) throw Error("missing base_dir for relative uri: " + uri) return fd.slurp(join_paths(base_dir, uri)) } function load_uri(uri, base_dir, uri_loader) { if (uri == null) return null if (starts_with(uri, "data:")) { var parsed = parse_data_uri(uri) if (!parsed) throw Error("invalid data uri") if (!parsed.base64) throw Error("data uri is not base64") return text.base64_to_blob(parsed.data) } return uri_loader(uri, base_dir) } function parse_glb(glb_blob) { if (!stone.p(glb_blob)) stone(glb_blob) var magic = glb_blob.read_fit(0, 32) var version = glb_blob.read_fit(32, 32) var total_len = glb_blob.read_fit(64, 32) if (magic != 0x46546c67) throw Error("invalid glb") if (version != 2) throw Error("unsupported glb version") var json_len = glb_blob.read_fit(96, 32) var json_type = glb_blob.read_fit(128, 32) if (json_type != 0x4e4f534a) throw Error("invalid glb json chunk") var json_from = 160 var json_to = (20 + json_len) * 8 var json_blob = glb_blob.read_blob(json_from, json_to) stone(json_blob) var json_obj = json.decode(text(json_blob)) var offset = 20 + json_len if (offset + 8 > total_len) return { json: json_obj, bin: null } var bin_len = glb_blob.read_fit(offset * 8, 32) var bin_type = glb_blob.read_fit((offset + 4) * 8, 32) if (bin_type != 0x004e4942) return { json: json_obj, bin: null } var bin_from = (offset + 8) * 8 var bin_to = (offset + 8 + bin_len) * 8 var bin_blob = glb_blob.read_blob(bin_from, bin_to) stone(bin_blob) return { json: json_obj, bin: bin_blob } } function make_glb_from_json_and_bin(doc, bin_blob) { var json_text = json.encode(doc, null, 0) var json_blob = blob(json_text) var json_bytes = length(json_blob) / 8 var bin_bytes = length(bin_blob) / 8 var json_pad = (4 - (json_bytes % 4)) % 4 var bin_pad = (4 - (bin_bytes % 4)) % 4 var json_pad_blob = blob(spaces(json_pad)) var bin_pad_blob = zeros_blob(bin_pad) var json_chunk_len = json_bytes + json_pad var bin_chunk_len = bin_bytes + bin_pad var total_len = 12 + 8 + json_chunk_len + 8 + bin_chunk_len var out = blob(total_len * 8) u32(out, 0x46546c67) u32(out, 2) u32(out, total_len) u32(out, json_chunk_len) u32(out, 0x4e4f534a) out.write_blob(json_blob) out.write_blob(json_pad_blob) u32(out, bin_chunk_len) u32(out, 0x004e4942) out.write_blob(bin_blob) out.write_blob(bin_pad_blob) stone(out) return out } function ensure_image_slots(asset) { if (!asset.images) asset.images = [] for (var i = 0; i < length(asset.images); i++) { var im = asset.images[i] if (!im) continue if (im.encoded == null) im.encoded = null if (im.pixels == null) im.pixels = null } } function synthesize_glb_from_gltf_json(doc, base_dir, uri_loader) { if (!doc.buffers || length(doc.buffers) == 0) throw Error("gltf: .gltf has no buffers") if (length(doc.buffers) != 1) throw Error("gltf: only .gltf with exactly 1 buffer is supported") var uri = doc.buffers[0].uri if (!uri) throw Error("gltf: buffer[0] has no uri") var bin_blob = load_uri(uri, base_dir, uri_loader) var doc2 = json.decode(json.encode(doc, null, 0)) delete doc2.buffers[0].uri return make_glb_from_json_and_bin(doc2, bin_blob) } gltf.decode = function(input, opts) { if (opts == null) opts = {} var on_warning = opts.on_warning if (on_warning == null) on_warning = "collect" var uri_loader = opts.uri_loader if (uri_loader == null) uri_loader = load_uri_default if (is_text(input)) { var path = input if (ends_with(path, ".gltf")) { var gltf_blob = fd.slurp(path) var doc = json.decode(text(gltf_blob)) var base_dir = dirname(path) var glb = synthesize_glb_from_gltf_json(doc, base_dir, uri_loader) var asset = native_decode(glb) asset.warnings = [] asset.path = path asset.base_dir = base_dir asset.json = doc asset.extensions = opts.extensions || {} ensure_image_slots(asset) return asset } var glb_blob = fd.slurp(path) var asset = native_decode(glb_blob) asset.warnings = [] asset.path = path asset.base_dir = dirname(path) asset.extensions = opts.extensions || {} try { var parsed = parse_glb(glb_blob) asset.json = parsed.json asset.bin = parsed.bin } catch (e) { warn(asset, "gltf: could not parse glb json: " + e, on_warning) asset.json = null asset.bin = null } ensure_image_slots(asset) return asset } if (is_blob(input)) { var hint = opts.path_hint if (hint && ends_with(hint, ".gltf")) { var doc = json.decode(text(input)) var base_dir = dirname(hint) var glb = synthesize_glb_from_gltf_json(doc, base_dir, uri_loader) var asset = native_decode(glb) asset.warnings = [] asset.path = hint asset.base_dir = base_dir asset.json = doc asset.extensions = opts.extensions || {} ensure_image_slots(asset) return asset } var asset = native_decode(input) asset.warnings = [] asset.path = hint || null asset.base_dir = hint ? dirname(hint) : null asset.extensions = opts.extensions || {} try { var parsed = parse_glb(input) asset.json = parsed.json asset.bin = parsed.bin } catch (e) { asset.json = null asset.bin = null } ensure_image_slots(asset) return asset } throw Error("gltf.decode: input must be a string path or a blob") } function used_texture_indices(asset) { var used = {} var mats = asset.materials || [] function mark_texinfo(ti) { if (!ti) return if (ti.texture == null) return used[ti.texture] = true } for (var i = 0; i < length(mats); i++) { var m = mats[i] if (!m) continue if (m.pbr) { mark_texinfo(m.pbr.base_color_texture) mark_texinfo(m.pbr.metallic_roughness_texture) } mark_texinfo(m.normal_texture) mark_texinfo(m.occlusion_texture) mark_texinfo(m.emissive_texture) } return used } function used_image_indices(asset) { var used_imgs = {} var used_tex = used_texture_indices(asset) var textures = asset.textures || [] arrfor(array(used_tex), function(k) { var ti = number(k) var t = textures[ti] if (!t) return if (t.image == null) return used_imgs[t.image] = true }) return used_imgs } function extract_buffer_view(asset, view_index) { if (view_index == null) return null var view = asset.views[view_index] if (!view) return null if (view.buffer == null) return null var buf = asset.buffers[view.buffer] if (!buf || !buf.blob) return null var b = buf.blob if (!stone.p(b)) stone(b) var from = (view.byte_offset || 0) * 8 var to = ((view.byte_offset || 0) + (view.byte_length || 0)) * 8 var slice = b.read_blob(from, to) stone(slice) return slice } gltf.collect_dependencies = function(asset) { var deps = { buffers: [], images: [] } var doc = asset.json if (!doc) return deps if (doc.buffers) { for (var i = 0; i < length(doc.buffers); i++) { var b = doc.buffers[i] if (b && b.uri) deps.buffers.push(b.uri) } } if (doc.images) { for (var i = 0; i < length(doc.images); i++) { var im = doc.images[i] if (im && im.uri) deps.images.push(im.uri) } } return deps } gltf.pull_images = function(asset, opts) { if (opts == null) opts = {} var mode = opts.mode if (mode == null) mode = "all" var on_missing = opts.on_missing if (on_missing == null) on_missing = "warn" var uri_loader = opts.uri_loader if (uri_loader == null) uri_loader = load_uri_default var dedupe = opts.dedupe if (dedupe == null) dedupe = true var max_total_bytes = opts.max_total_bytes if (max_total_bytes == null) max_total_bytes = 0 ensure_image_slots(asset) var used = null if (mode == "used") used = used_image_indices(asset) var cache = {} var total = 0 for (var i = 0; i < length(asset.images); i++) { if (used && !used[i]) continue var img = asset.images[i] if (!img) continue if (img.encoded) continue if (img.kind == "buffer_view") { var encoded = extract_buffer_view(asset, img.view) if (!encoded) { if (on_missing == "throw") throw Error("missing embedded image bytes") if (on_missing == "warn") warn(asset, "missing embedded image bytes", "collect") continue } img.encoded = encoded total += length(encoded) / 8 continue } if (img.kind == "uri") { if (!img.uri) { if (on_missing == "throw") throw Error("image has null uri") if (on_missing == "warn") warn(asset, "image has null uri", "collect") continue } var key = img.uri if (dedupe && cache[key]) { img.encoded = cache[key] continue } try { var encoded = load_uri(img.uri, asset.base_dir, uri_loader) if (!stone.p(encoded)) stone(encoded) img.encoded = encoded if (dedupe) cache[key] = encoded total += length(encoded) / 8 if (max_total_bytes > 0 && total > max_total_bytes) throw Error("max_total_bytes exceeded") } catch (e) { if (on_missing == "throw") throw e if (on_missing == "warn") warn(asset, "missing image uri: " + img.uri, "collect") } continue } } return asset } function guess_mime(img) { if (img.mime) return img.mime if (img.uri) { var u = img.uri if (ends_with(u, ".png")) return "image/png" if (ends_with(u, ".jpg") || ends_with(u, ".jpeg")) return "image/jpeg" } return null } function premultiply_rgba(pixels_blob) { if (!stone.p(pixels_blob)) stone(pixels_blob) var bytes = length(pixels_blob) / 8 var out = blob(bytes * 8) for (var i = 0; i < bytes; i += 4) { var r = pixels_blob.read_fit((i + 0) * 8, 8) var g = pixels_blob.read_fit((i + 1) * 8, 8) var b = pixels_blob.read_fit((i + 2) * 8, 8) var a = pixels_blob.read_fit((i + 3) * 8, 8) var rr = floor((r * a) / 255) var gg = floor((g * a) / 255) var bb = floor((b * a) / 255) out.write_fit(rr, 8) out.write_fit(gg, 8) out.write_fit(bb, 8) out.write_fit(a, 8) } stone(out) return out } gltf.decode_images = function(asset, opts) { if (opts == null) opts = {} var mode = opts.mode if (mode == null) mode = "used" var format = opts.format if (format == null) format = "rgba8" var on_missing_encoded = opts.on_missing_encoded if (on_missing_encoded == null) on_missing_encoded = "pull" var decoder = opts.decoder if (decoder == null) decoder = function(mime, b) { if (mime == "image/png") return png.decode(b) if (mime == "image/jpeg") return jpg.decode(b) return null } var premultiply_alpha = opts.premultiply_alpha if (premultiply_alpha == null) premultiply_alpha = false ensure_image_slots(asset) if (on_missing_encoded == "pull") { gltf.pull_images(asset, { mode: mode }) } var used = null if (mode == "used") used = used_image_indices(asset) for (var i = 0; i < length(asset.images); i++) { if (used && !used[i]) continue var img = asset.images[i] if (!img) continue if (img.pixels) continue if (!img.encoded) { if (on_missing_encoded == "throw") throw Error("missing encoded image data") continue } var mime = guess_mime(img) if (!mime) { warn(asset, "unknown image mime", "collect") continue } var decoded = decoder(mime, img.encoded) if (!decoded) { warn(asset, "unsupported image mime: " + mime, "collect") continue } var pixels = decoded.pixels if (premultiply_alpha) pixels = premultiply_rgba(pixels) img.pixels = { width: decoded.width, height: decoded.height, pixels: pixels, format: format, pitch: decoded.pitch } } return asset } gltf.drop_images = function(asset, what) { if (what == null) what = "all" ensure_image_slots(asset) for (var i = 0; i < length(asset.images); i++) { var img = asset.images[i] if (!img) continue if (what == "encoded" || what == "all") img.encoded = null if (what == "pixels" || what == "all") img.pixels = null } return asset } gltf.stats = function(asset) { var stats = { meshes: (asset.meshes ? length(asset.meshes) : 0), nodes: (asset.nodes ? length(asset.nodes) : 0), images: (asset.images ? length(asset.images) : 0), textures: (asset.textures ? length(asset.textures) : 0), materials: (asset.materials ? length(asset.materials) : 0), animations: (asset.animations ? length(asset.animations) : 0), skins: (asset.skins ? length(asset.skins) : 0), bin_bytes: 0, encoded_image_bytes: 0, triangles: 0 } if (asset.buffers) { for (var i = 0; i < length(asset.buffers); i++) { var b = asset.buffers[i] if (b && b.blob) stats.bin_bytes += length(b.blob) / 8 } } if (asset.images) { for (var i = 0; i < length(asset.images); i++) { var im = asset.images[i] if (im && im.encoded) stats.encoded_image_bytes += length(im.encoded) / 8 } } if (asset.meshes) { for (var mi = 0; mi < length(asset.meshes); mi++) { var m = asset.meshes[mi] if (!m || !m.primitives) continue for (var pi = 0; pi < length(m.primitives); pi++) { var p = m.primitives[pi] if (!p) continue if (p.topology != "triangles") continue if (p.indices != null) { var acc = asset.accessors[p.indices] if (acc) stats.triangles += floor((acc.count || 0) / 3) } else if (p.attributes && p.attributes.POSITION != null) { var acc = asset.accessors[p.attributes.POSITION] if (acc) stats.triangles += floor((acc.count || 0) / 3) } } } } asset.stats = stats return asset } gltf.load = function(input, opts) { if (opts == null) opts = {} var pull = opts.pull_images if (pull == null) pull = true var decode = opts.decode_images if (decode == null) decode = false var mode = opts.mode if (mode == null) mode = "used" var asset = gltf.decode(input, opts) if (pull) gltf.pull_images(asset, { mode: mode }) if (decode) gltf.decode_images(asset, { mode: mode }) return asset } return gltf