var gltf = use('internal/gltf') 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) { var _base = base.replace(/\/+$/, "") var _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 = "" var i = 0 for (i = 0; i < count; i++) s += " " return s } function zeros_blob(bytes) { var b = blob(bytes * 8) var i = 0 for (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) { var _mode = mode if (_mode == null) _mode = "collect" if (_mode == "throw") disrupt asset.warnings[] = 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 var parts = null var i = 0 if (length(meta) > 0) { parts = array(meta, ";") if (parts[0]) mime = parts[0] for (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) disrupt return fd.slurp(join_paths(base_dir, uri)) } function load_uri(uri, base_dir, uri_loader) { if (uri == null) return null var parsed = null if (starts_with(uri, "data:")) { parsed = parse_data_uri(uri) if (!parsed) disrupt if (!parsed.base64) disrupt 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) disrupt if (version != 2) disrupt var json_len = glb_blob.read_fit(96, 32) var json_type = glb_blob.read_fit(128, 32) if (json_type != 0x4e4f534a) disrupt 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 = [] var i = 0 var im = null for (i = 0; i < length(asset.images); i++) { 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) disrupt if (length(doc.buffers) != 1) disrupt var uri = doc.buffers[0].uri if (!uri) disrupt 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) { var _opts = 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 var path = null var gltf_blob = null var doc = null var base_dir = null var glb = null var asset = null var glb_blob = null var parse_fn = null if (is_text(input)) { path = input if (ends_with(path, ".gltf")) { gltf_blob = fd.slurp(path) doc = json.decode(text(gltf_blob)) base_dir = dirname(path) glb = synthesize_glb_from_gltf_json(doc, base_dir, uri_loader) 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 } glb_blob = fd.slurp(path) asset = native_decode(glb_blob) asset.warnings = [] asset.path = path asset.base_dir = dirname(path) asset.extensions = _opts.extensions || {} parse_fn = function() { var parsed = parse_glb(glb_blob) asset.json = parsed.json asset.bin = parsed.bin } disruption { warn(asset, "gltf: could not parse glb json", on_warning) asset.json = null asset.bin = null } parse_fn() ensure_image_slots(asset) return asset } var hint = null if (is_blob(input)) { hint = _opts.path_hint if (hint && ends_with(hint, ".gltf")) { doc = json.decode(text(input)) base_dir = dirname(hint) glb = synthesize_glb_from_gltf_json(doc, base_dir, uri_loader) 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 } asset = native_decode(input) asset.warnings = [] asset.path = hint || null asset.base_dir = hint ? dirname(hint) : null asset.extensions = _opts.extensions || {} parse_fn = function() { var parsed = parse_glb(input) asset.json = parsed.json asset.bin = parsed.bin } disruption { asset.json = null asset.bin = null } parse_fn() ensure_image_slots(asset) return asset } disrupt } 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 } var i = 0 var m = null for (i = 0; i < length(mats); i++) { 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 var i = 0 var b = null if (doc.buffers) { for (i = 0; i < length(doc.buffers); i++) { b = doc.buffers[i] if (b && b.uri) deps.buffers[] = b.uri } } var im2 = null if (doc.images) { for (i = 0; i < length(doc.images); i++) { im2 = doc.images[i] if (im2 && im2.uri) deps.images[] = im2.uri } } return deps } gltf.pull_images = function(asset, opts) { var _opts = 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 var i = 0 var img = null var encoded = null var key = null var load_fn = function() { 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) disrupt } disruption { if (on_missing == "throw") disrupt if (on_missing == "warn") warn(asset, "missing image uri: " + img.uri, "collect") } for (i = 0; i < length(asset.images); i++) { if (used && !used[i]) continue img = asset.images[i] if (!img) continue if (img.encoded) continue if (img.kind == "buffer_view") { encoded = extract_buffer_view(asset, img.view) if (!encoded) { if (on_missing == "throw") disrupt 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") disrupt if (on_missing == "warn") warn(asset, "image has null uri", "collect") continue } key = img.uri if (dedupe && cache[key]) { img.encoded = cache[key] continue } load_fn() continue } } return asset } function guess_mime(img) { if (img.mime) return img.mime var u = null if (img.uri) { 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) var i = 0 var r = null var g = null var b = null var a = null var rr = null var gg = null var bb = null for (i = 0; i < bytes; i += 4) { r = pixels_blob.read_fit((i + 0) * 8, 8) g = pixels_blob.read_fit((i + 1) * 8, 8) b = pixels_blob.read_fit((i + 2) * 8, 8) a = pixels_blob.read_fit((i + 3) * 8, 8) rr = floor((r * a) / 255) gg = floor((g * a) / 255) 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) { var _opts = 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) var i = 0 var img = null var mime = null var decoded = null var pixels = null for (i = 0; i < length(asset.images); i++) { if (used && !used[i]) continue img = asset.images[i] if (!img) continue if (img.pixels) continue if (!img.encoded) { if (on_missing_encoded == "throw") disrupt continue } mime = guess_mime(img) if (!mime) { warn(asset, "unknown image mime", "collect") continue } decoded = decoder(mime, img.encoded) if (!decoded) { warn(asset, "unsupported image mime: " + mime, "collect") continue } 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) { var _what = what if (_what == null) _what = "all" ensure_image_slots(asset) var i = 0 var img = null for (i = 0; i < length(asset.images); i++) { 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 } var i = 0 var b = null var im = null var mi = 0 var m = null var pi = 0 var p = null var acc = null if (asset.buffers) { for (i = 0; i < length(asset.buffers); i++) { b = asset.buffers[i] if (b && b.blob) stats.bin_bytes += length(b.blob) / 8 } } if (asset.images) { for (i = 0; i < length(asset.images); i++) { im = asset.images[i] if (im && im.encoded) stats.encoded_image_bytes += length(im.encoded) / 8 } } if (asset.meshes) { for (mi = 0; mi < length(asset.meshes); mi++) { m = asset.meshes[mi] if (!m || !m.primitives) continue for (pi = 0; pi < length(m.primitives); pi++) { p = m.primitives[pi] if (!p) continue if (p.topology != "triangles") continue if (p.indices != null) { acc = asset.accessors[p.indices] if (acc) stats.triangles += floor((acc.count || 0) / 3) } else if (p.attributes && p.attributes.POSITION != null) { 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) { var _opts = 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