///|
/// glTF 2.0 container and metadata model. Platform I/O remains outside this
/// package; callers provide bytes and an optional URI resolver.
priv struct GltfAsset {
version : String
} derive(FromJson)
///|
priv struct GltfBuffer {
byteLength : Int
uri : String?
}
///|
priv struct GltfBufferView {
buffer : Int
byteOffset : Int?
byteLength : Int
byteStride : Int?
}
///|
priv struct GltfPrimitiveJson {
attributes : Map[String, Int]
indices : Int?
material : Int?
mode : Int?
targets : Array[Map[String, Int]]?
}
///|
priv struct GltfMeshJson {
primitives : Array[GltfPrimitiveJson]
name : String?
weights : Array[Double]?
}
///|
priv struct GltfNodeJson {
mesh : Int?
camera : Int?
skin : Int?
weights : Array[Double]?
children : Array[Int]?
name : String?
matrix : Array[Double]?
translation : Array[Double]?
rotation : Array[Double]?
scale : Array[Double]?
}
///|
priv struct GltfTextureInfo {
index : Int
} derive(FromJson)
///|
priv struct GltfPbrJson {
baseColorFactor : Array[Double]?
metallicFactor : Double?
roughnessFactor : Double?
baseColorTexture : GltfTextureInfo?
}
///|
priv struct GltfMaterialJson {
pbrMetallicRoughness : GltfPbrJson?
normalTexture : GltfTextureInfo?
occlusionTexture : GltfTextureInfo?
emissiveTexture : GltfTextureInfo?
emissiveFactor : Array[Double]?
alphaMode : String?
alphaCutoff : Double?
doubleSided : Bool?
}
///|
priv struct GltfImageJson {
uri : String?
mimeType : String?
bufferView : Int?
}
///|
priv struct GltfTextureJson {
source : Int?
sampler : Int?
}
///|
priv struct GltfSamplerJson {
magFilter : Int?
minFilter : Int?
wrapS : Int?
wrapT : Int?
}
///|
priv struct GltfPerspectiveJson {
aspectRatio : Double?
yfov : Double
zfar : Double?
znear : Double
}
///|
priv struct GltfOrthographicJson {
xmag : Double
ymag : Double
zfar : Double
znear : Double
}
///|
priv struct GltfCameraJson {
camera_type : String
perspective : GltfPerspectiveJson?
orthographic : GltfOrthographicJson?
name : String?
}
///|
priv struct GltfSceneJson {
nodes : Array[Int]?
name : String?
}
///|
priv struct GltfSparseIndicesJson {
bufferView : Int
byteOffset : Int?
componentType : Int
}
///|
priv struct GltfSparseValuesJson {
bufferView : Int
byteOffset : Int?
}
///|
priv struct GltfAccessorSparseJson {
count : Int
indices : GltfSparseIndicesJson
values : GltfSparseValuesJson
}
///|
priv struct GltfAnimationSamplerJson {
input : Int
output : Int
interpolation : String?
}
///|
priv struct GltfAnimationTargetJson {
node : Int?
path : String
}
///|
priv struct GltfAnimationChannelJson {
sampler : Int
target : GltfAnimationTargetJson
}
///|
priv struct GltfAnimationJson {
samplers : Array[GltfAnimationSamplerJson]
channels : Array[GltfAnimationChannelJson]
name : String?
}
///|
priv struct GltfRoot {
asset : GltfAsset
buffers : Array[GltfBuffer]?
bufferViews : Array[GltfBufferView]?
accessors : Array[GltfAccessorJson]?
meshes : Array[GltfMeshJson]?
nodes : Array[GltfNodeJson]?
materials : Array[GltfMaterialJson]?
images : Array[GltfImageJson]?
textures : Array[GltfTextureJson]?
samplers : Array[GltfSamplerJson]?
cameras : Array[GltfCameraJson]?
scenes : Array[GltfSceneJson]?
scene : Int?
skins : Array[GltfSkinJson]?
animations : Array[GltfAnimationJson]?
extensionsUsed : Array[String]?
extensionsRequired : Array[String]?
}
///|
priv struct GltfSkinJson {
inverseBindMatrices : Int?
skeleton : Int?
joints : Array[Int]
name : String?
}
///|
priv struct GltfAccessorJson {
bufferView : Int?
byteOffset : Int?
componentType : Int
normalized : Bool?
count : Int
accessor_kind : String
sparse : GltfAccessorSparseJson?
}
///|
impl @json.FromJson for GltfAccessorJson with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "accessor must be an object"))
}
let kind_json = match fields.get("type") {
Some(value) => value
None => raise JsonDecodeError((path, "accessor.type is required"))
}
{
bufferView: optional_field(fields, "bufferView", path),
byteOffset: optional_field(fields, "byteOffset", path),
componentType: required_field(fields, "componentType", path),
normalized: optional_field(fields, "normalized", path),
count: required_field(fields, "count", path),
accessor_kind: @json.from_json(kind_json, path~),
sparse: optional_field(fields, "sparse", path),
}
}
///|
fn object_fields(
json : Json,
path : @json.JsonPath,
) -> Map[String, Json] raise @json.JsonDecodeError {
match json {
Object(fields) => fields
_ => raise JsonDecodeError((path, "expected object"))
}
}
///|
fn[T : @json.FromJson] required_field(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> T raise @json.JsonDecodeError {
match fields.get(key) {
Some(value) => @json.from_json(value, path=path.add_key(key))
None => raise JsonDecodeError((path, "missing field " + key))
}
}
///|
fn[T : @json.FromJson] optional_field(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> T? raise @json.JsonDecodeError {
match fields.get(key) {
None => None
Some(Null) => None
Some(value) => Some(@json.from_json(value, path=path.add_key(key)))
}
}
///|
impl @json.FromJson for GltfBuffer with fn from_json(json, path) {
let fields = object_fields(json, path)
{
byteLength: required_field(fields, "byteLength", path),
uri: optional_field(fields, "uri", path),
}
}
///|
impl @json.FromJson for GltfBufferView with fn from_json(json, path) {
let fields = object_fields(json, path)
{
buffer: required_field(fields, "buffer", path),
byteOffset: optional_field(fields, "byteOffset", path),
byteLength: required_field(fields, "byteLength", path),
byteStride: optional_field(fields, "byteStride", path),
}
}
///|
impl @json.FromJson for GltfPrimitiveJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
attributes: required_field(fields, "attributes", path),
indices: optional_field(fields, "indices", path),
material: optional_field(fields, "material", path),
mode: optional_field(fields, "mode", path),
targets: optional_field(fields, "targets", path),
}
}
///|
impl @json.FromJson for GltfMeshJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
primitives: required_field(fields, "primitives", path),
name: optional_field(fields, "name", path),
weights: optional_field(fields, "weights", path),
}
}
///|
impl @json.FromJson for GltfNodeJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
mesh: optional_field(fields, "mesh", path),
camera: optional_field(fields, "camera", path),
skin: optional_field(fields, "skin", path),
weights: optional_field(fields, "weights", path),
children: optional_field(fields, "children", path),
name: optional_field(fields, "name", path),
matrix: optional_field(fields, "matrix", path),
translation: optional_field(fields, "translation", path),
rotation: optional_field(fields, "rotation", path),
scale: optional_field(fields, "scale", path),
}
}
///|
impl @json.FromJson for GltfPbrJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
baseColorFactor: optional_field(fields, "baseColorFactor", path),
metallicFactor: optional_field(fields, "metallicFactor", path),
roughnessFactor: optional_field(fields, "roughnessFactor", path),
baseColorTexture: optional_field(fields, "baseColorTexture", path),
}
}
///|
impl @json.FromJson for GltfMaterialJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
pbrMetallicRoughness: optional_field(fields, "pbrMetallicRoughness", path),
normalTexture: optional_field(fields, "normalTexture", path),
occlusionTexture: optional_field(fields, "occlusionTexture", path),
emissiveTexture: optional_field(fields, "emissiveTexture", path),
emissiveFactor: optional_field(fields, "emissiveFactor", path),
alphaMode: optional_field(fields, "alphaMode", path),
alphaCutoff: optional_field(fields, "alphaCutoff", path),
doubleSided: optional_field(fields, "doubleSided", path),
}
}
///|
impl @json.FromJson for GltfImageJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
uri: optional_field(fields, "uri", path),
mimeType: optional_field(fields, "mimeType", path),
bufferView: optional_field(fields, "bufferView", path),
}
}
///|
impl @json.FromJson for GltfTextureJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
source: optional_field(fields, "source", path),
sampler: optional_field(fields, "sampler", path),
}
}
///|
impl @json.FromJson for GltfSamplerJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
magFilter: optional_field(fields, "magFilter", path),
minFilter: optional_field(fields, "minFilter", path),
wrapS: optional_field(fields, "wrapS", path),
wrapT: optional_field(fields, "wrapT", path),
}
}
///|
impl @json.FromJson for GltfPerspectiveJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
aspectRatio: optional_field(fields, "aspectRatio", path),
yfov: required_field(fields, "yfov", path),
zfar: optional_field(fields, "zfar", path),
znear: required_field(fields, "znear", path),
}
}
///|
impl @json.FromJson for GltfOrthographicJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
xmag: required_field(fields, "xmag", path),
ymag: required_field(fields, "ymag", path),
zfar: required_field(fields, "zfar", path),
znear: required_field(fields, "znear", path),
}
}
///|
impl @json.FromJson for GltfCameraJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
camera_type: required_field(fields, "type", path),
perspective: optional_field(fields, "perspective", path),
orthographic: optional_field(fields, "orthographic", path),
name: optional_field(fields, "name", path),
}
}
///|
impl @json.FromJson for GltfSceneJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
nodes: optional_field(fields, "nodes", path),
name: optional_field(fields, "name", path),
}
}
///|
impl @json.FromJson for GltfSkinJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
inverseBindMatrices: optional_field(fields, "inverseBindMatrices", path),
skeleton: optional_field(fields, "skeleton", path),
joints: required_field(fields, "joints", path),
name: optional_field(fields, "name", path),
}
}
///|
impl @json.FromJson for GltfSparseIndicesJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
bufferView: required_field(fields, "bufferView", path),
byteOffset: optional_field(fields, "byteOffset", path),
componentType: required_field(fields, "componentType", path),
}
}
///|
impl @json.FromJson for GltfSparseValuesJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
bufferView: required_field(fields, "bufferView", path),
byteOffset: optional_field(fields, "byteOffset", path),
}
}
///|
impl @json.FromJson for GltfAccessorSparseJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
count: required_field(fields, "count", path),
indices: required_field(fields, "indices", path),
values: required_field(fields, "values", path),
}
}
///|
impl @json.FromJson for GltfAnimationSamplerJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
input: required_field(fields, "input", path),
output: required_field(fields, "output", path),
interpolation: optional_field(fields, "interpolation", path),
}
}
///|
impl @json.FromJson for GltfAnimationTargetJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
node: optional_field(fields, "node", path),
path: required_field(fields, "path", path),
}
}
///|
impl @json.FromJson for GltfAnimationChannelJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
sampler: required_field(fields, "sampler", path),
target: required_field(fields, "target", path),
}
}
///|
impl @json.FromJson for GltfAnimationJson with fn from_json(json, path) {
let fields = object_fields(json, path)
{
samplers: required_field(fields, "samplers", path),
channels: required_field(fields, "channels", path),
name: optional_field(fields, "name", path),
}
}
///|
impl @json.FromJson for GltfRoot with fn from_json(json, path) {
let fields = object_fields(json, path)
{
asset: required_field(fields, "asset", path),
buffers: optional_field(fields, "buffers", path),
bufferViews: optional_field(fields, "bufferViews", path),
accessors: optional_field(fields, "accessors", path),
meshes: optional_field(fields, "meshes", path),
nodes: optional_field(fields, "nodes", path),
materials: optional_field(fields, "materials", path),
images: optional_field(fields, "images", path),
textures: optional_field(fields, "textures", path),
samplers: optional_field(fields, "samplers", path),
cameras: optional_field(fields, "cameras", path),
scenes: optional_field(fields, "scenes", path),
scene: optional_field(fields, "scene", path),
skins: optional_field(fields, "skins", path),
animations: optional_field(fields, "animations", path),
extensionsUsed: optional_field(fields, "extensionsUsed", path),
extensionsRequired: optional_field(fields, "extensionsRequired", path),
}
}
///|
pub(all) suberror GltfError {
EmptyInput
InvalidMagic
UnsupportedVersion(Int)
InvalidDeclaredLength
TruncatedChunk
MissingJsonChunk
InvalidJson(String)
UnsupportedAssetVersion(String)
MissingBuffer(Int)
MissingBufferView(Int)
MissingAccessor(Int)
MissingAttribute(String)
InvalidAccessor(Int)
UnsupportedComponentType(Int)
UnsupportedAccessorType(String)
UnsupportedPrimitiveMode(Int)
UnsupportedExtension(String)
UnsupportedRequiredExtension(String)
ExternalResourceUnavailable(String)
InvalidTexture(Int)
InvalidSampler(Int)
InvalidCamera(Int)
InvalidScene(Int)
InvalidSkin(Int)
InvalidAnimation(Int)
UnsupportedAnimationPath(String)
InvalidNode(Int)
InvalidNodeCycle(Int)
} derive(Eq, Debug, ToJson)
///|
pub extend GltfError with Debug::{to_repr}
///|
pub extend GltfError with Eq::{not_equal, equal}
///|
pub extend GltfError with ToJson::{to_json}
///|
/// Resolves a URI referenced by a JSON `.gltf` document.
pub type GltfResourceResolver = (String) -> Bytes?
///|
pub(all) struct GltfDocument {
json : String
binary : Bytes?
mesh_count : Int
node_count : Int
material_count : Int
texture_count : Int
sampler_count : Int
camera_count : Int
scene_count : Int
} derive(Debug)
///|
pub extend GltfDocument with Debug::{to_repr}
///|
pub fn GltfDocument::json(self : GltfDocument) -> String {
self.json
}
///|
pub fn GltfDocument::binary(self : GltfDocument) -> Bytes? {
self.binary
}
///|
pub fn GltfDocument::mesh_count(self : GltfDocument) -> Int {
self.mesh_count
}
///|
pub fn GltfDocument::node_count(self : GltfDocument) -> Int {
self.node_count
}
///|
pub fn GltfDocument::material_count(self : GltfDocument) -> Int {
self.material_count
}
///|
pub fn GltfDocument::texture_count(self : GltfDocument) -> Int {
self.texture_count
}
///|
pub fn GltfDocument::sampler_count(self : GltfDocument) -> Int {
self.sampler_count
}
///|
pub fn GltfDocument::camera_count(self : GltfDocument) -> Int {
self.camera_count
}
///|
pub fn GltfDocument::scene_count(self : GltfDocument) -> Int {
self.scene_count
}
///|
pub fn parse_glb(bytes : Bytes) -> GltfDocument raise GltfError {
if bytes.length() < 12 {
raise EmptyInput
}
if bytes[0] != b'g' ||
bytes[1] != b'l' ||
bytes[2] != b'T' ||
bytes[3] != b'F' {
raise InvalidMagic
}
let version = read_u32_le(bytes, 4)
if version != 2 {
raise UnsupportedVersion(version)
}
let declared_length = read_u32_le(bytes, 8)
if declared_length < 12 || declared_length > bytes.length() {
raise InvalidDeclaredLength
}
let mut offset = 12
let mut json_bytes : Bytes? = None
let mut binary : Bytes? = None
while offset < declared_length {
if offset + 8 > declared_length {
raise TruncatedChunk
}
let chunk_length = read_u32_le(bytes, offset)
let chunk_type = read_u32_le(bytes, offset + 4)
if chunk_length % 4 != 0 {
raise TruncatedChunk
}
let chunk_start = offset + 8
let chunk_end = chunk_start + chunk_length
if chunk_end > declared_length || chunk_end > bytes.length() {
raise TruncatedChunk
}
if chunk_type == 0x4E4F534A {
if json_bytes is None {
json_bytes = Some(bytes[chunk_start:chunk_end].to_owned())
}
} else if chunk_type == 0x004E4942 {
if binary is None {
binary = Some(bytes[chunk_start:chunk_end].to_owned())
}
}
offset = chunk_end
}
let json_chunk = match json_bytes {
Some(value) => value
None => raise MissingJsonChunk
}
let json = @utf8.decode(json_chunk[:], ignore_bom=true) catch {
_ => raise InvalidJson("JSON chunk is not valid UTF-8")
}
parse_gltf_json(json, binary~)
}
///|
pub fn parse_gltf_json(
json : String,
binary? : Bytes? = None,
) -> GltfDocument raise GltfError {
let root : GltfRoot = @json.from_json(@json.parse(json)) catch {
err => raise InvalidJson(err.to_string())
}
if root.asset.version != "2.0" {
raise UnsupportedAssetVersion(root.asset.version)
}
{
json,
binary,
mesh_count: root.meshes.unwrap_or([]).length(),
node_count: root.nodes.unwrap_or([]).length(),
material_count: root.materials.unwrap_or([]).length(),
texture_count: root.textures.unwrap_or([]).length(),
sampler_count: root.samplers.unwrap_or([]).length(),
camera_count: root.cameras.unwrap_or([]).length(),
scene_count: root.scenes.unwrap_or([]).length(),
}
}
///|
fn read_u32_le(bytes : Bytes, offset : Int) -> Int {
if offset < 0 || offset + 3 >= bytes.length() {
return 0
}
bytes[offset].to_int() |
(bytes[offset + 1].to_int() << 8) |
(bytes[offset + 2].to_int() << 16) |
(bytes[offset + 3].to_int() << 24)
}