Files
cell-model/gltf.cm
2025-12-13 16:13:32 -06:00

612 lines
16 KiB
Plaintext

var gltf = this
var native_decode = gltf.decode
var fd = use('fd')
var utf8 = use('utf8')
var blob = use('blob')
var http = use('http')
var text = use('internal/text')
var png = use('cell-image/png')
var jpg = use('cell-image/jpg')
function dirname(path) {
var idx = path.lastIndexOf("/")
if (idx == -1) return ""
return path.substring(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) {
path = path.toLowerCase()
suffix = suffix.toLowerCase()
return path.length >= suffix.length && path.substring(path.length - suffix.length) == suffix
}
function spaces(count) {
var s = ""
for (var i = 0; i < count; i++) s += " "
return s
}
function zeros_blob(bytes) {
var b = new 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 new Error(msg)
asset.warnings.push(msg)
}
function parse_data_uri(uri) {
var comma = uri.indexOf(",")
if (comma == -1) return null
var meta = uri.substring(5, comma)
var data = uri.substring(comma + 1)
var mime = "text/plain"
var is_base64 = false
if (meta.length > 0) {
var parts = meta.split(";")
if (parts[0]) mime = parts[0]
for (var i = 1; i < parts.length; i++) {
if (parts[i].toLowerCase() == "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 new 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 (uri.startsWith("data:")) {
var parsed = parse_data_uri(uri)
if (!parsed) throw new Error("invalid data uri")
if (!parsed.base64) throw new 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 new Error("invalid glb")
if (version != 2) throw new 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 new 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(utf8.decode(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 = utf8.encode(json_text)
var json_bytes = json_blob.length / 8
var bin_bytes = bin_blob.length / 8
var json_pad = (4 - (json_bytes % 4)) % 4
var bin_pad = (4 - (bin_bytes % 4)) % 4
var json_pad_blob = utf8.encode(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 = new 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 < asset.images.length; 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 || doc.buffers.length == 0) throw new Error("gltf: .gltf has no buffers")
if (doc.buffers.length != 1) throw new Error("gltf: only .gltf with exactly 1 buffer is supported")
var uri = doc.buffers[0].uri
if (!uri) throw new 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 (typeof input == 'string') {
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 (input instanceof blob) {
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 new 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 < mats.length; 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 = parseInt(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 < doc.buffers.length; i++) {
var b = doc.buffers[i]
if (b && b.uri) deps.buffers.push(b.uri)
}
}
if (doc.images) {
for (var i = 0; i < doc.images.length; 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 < asset.images.length; 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 new Error("missing embedded image bytes")
if (on_missing == "warn") warn(asset, "missing embedded image bytes", "collect")
continue
}
img.encoded = encoded
total += encoded.length / 8
continue
}
if (img.kind == "uri") {
if (!img.uri) {
if (on_missing == "throw") throw new 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 += encoded.length / 8
if (max_total_bytes > 0 && total > max_total_bytes) throw new 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.toLowerCase()
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 = pixels_blob.length / 8
var out = new 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 = Math.floor((r * a) / 255)
var gg = Math.floor((g * a) / 255)
var bb = Math.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 < asset.images.length; 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 new 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 < asset.images.length; 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 ? asset.meshes.length : 0),
nodes: (asset.nodes ? asset.nodes.length : 0),
images: (asset.images ? asset.images.length : 0),
textures: (asset.textures ? asset.textures.length : 0),
materials: (asset.materials ? asset.materials.length : 0),
animations: (asset.animations ? asset.animations.length : 0),
skins: (asset.skins ? asset.skins.length : 0),
bin_bytes: 0,
encoded_image_bytes: 0,
triangles: 0
}
if (asset.buffers) {
for (var i = 0; i < asset.buffers.length; i++) {
var b = asset.buffers[i]
if (b && b.blob) stats.bin_bytes += b.blob.length / 8
}
}
if (asset.images) {
for (var i = 0; i < asset.images.length; i++) {
var im = asset.images[i]
if (im && im.encoded) stats.encoded_image_bytes += im.encoded.length / 8
}
}
if (asset.meshes) {
for (var mi = 0; mi < asset.meshes.length; mi++) {
var m = asset.meshes[mi]
if (!m || !m.primitives) continue
for (var pi = 0; pi < m.primitives.length; 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 += Math.floor((acc.count || 0) / 3)
} else if (p.attributes && p.attributes.POSITION != null) {
var acc = asset.accessors[p.attributes.POSITION]
if (acc) stats.triangles += Math.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