609 lines
16 KiB
Plaintext
609 lines
16 KiB
Plaintext
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 || []
|
|
|
|
for (var k in used_tex) {
|
|
var ti = number(k)
|
|
var t = textures[ti]
|
|
if (!t) continue
|
|
if (t.image == null) continue
|
|
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 |