""" gltf.py ------------ Provides GLTF 2.0 exports of trimesh.Trimesh objects as GL_TRIANGLES, and trimesh.Path2D/Path3D as GL_LINES """ import json import base64 import collections import numpy as np from .. import util from .. import visual from .. import rendering from .. import resources from .. import transformations from ..constants import log, tol # magic numbers which have meaning in GLTF # most are uint32's of UTF-8 text _magic = {"gltf": 1179937895, "json": 1313821514, "bin": 5130562} # GLTF data type codes: little endian numpy dtypes _dtypes = {5120: " 0: tree['extras'] = collected except BaseException: log.warning('failed to export extras!', exc_info=True) # grab the flattened scene graph in GLTF's format nodes = scene.graph.to_gltf(scene=scene) tree.update(nodes) # store materials as {hash : index} to avoid duplicates mat_hashes = {} # store data from geometries buffer_items = [] # loop through every geometry for name, geometry in scene.geometry.items(): if util.is_instance_named(geometry, "Trimesh"): # add the mesh _append_mesh( mesh=geometry, name=name, tree=tree, buffer_items=buffer_items, include_normals=include_normals, mat_hashes=mat_hashes) elif util.is_instance_named(geometry, "Path"): # add Path2D and Path3D objects _append_path( path=geometry, name=name, tree=tree, buffer_items=buffer_items) # cull empty or unpopulated fields # check keys that might be empty so we can remove them check = ['textures', 'samplers', 'materials', 'images'] for key in check: if len(tree[key]) == 0: tree.pop(key) # in unit tests compare our header against the schema if tol.strict: validate(tree) return tree, buffer_items def _append_mesh(mesh, name, tree, buffer_items, include_normals, mat_hashes): """ Append a mesh to the scene structure and put the data into buffer_items. Parameters ------------- mesh : trimesh.Trimesh Source geometry name : str Name of geometry tree : dict Will be updated with data from mesh buffer_items Will have buffer appended with mesh data include_normals : bool Include vertex normals in export or not mat_hashes : dict Which materials have already been added """ # return early from empty meshes to avoid crashing later if len(mesh.faces) == 0: log.warning('skipping empty mesh!') return # meshes reference accessor indexes # mode 4 is GL_TRIANGLES tree["meshes"].append({ "name": name, "primitives": [{ "attributes": {"POSITION": len(tree["accessors"]) + 1}, "indices": len(tree["accessors"]), "mode": 4}]}) # if units are defined, store them as an extra # the GLTF spec says everything is implicit meters # we're not doing that as our unit conversions are expensive # although that might be better, implicit works for 3DXML # https://github.com/KhronosGroup/glTF/tree/master/extensions if mesh.units is not None and 'meter' not in mesh.units: tree["meshes"][-1]["extras"] = {"units": str(mesh.units)} # accessors refer to data locations # mesh faces are stored as flat list of integers tree["accessors"].append({ "bufferView": len(buffer_items), "componentType": 5125, "count": len(mesh.faces) * 3, "max": [int(mesh.faces.max())], "min": [0], "type": "SCALAR"}) # convert mesh data to the correct dtypes # faces: 5125 is an unsigned 32 bit integer buffer_items.append(_byte_pad( mesh.faces.astype(uint32).tobytes())) # the vertex accessor tree["accessors"].append({ "bufferView": len(buffer_items), "componentType": 5126, "count": len(mesh.vertices), "type": "VEC3", "byteOffset": 0, "max": mesh.vertices.max(axis=0).tolist(), "min": mesh.vertices.min(axis=0).tolist()}) # vertices: 5126 is a float32 buffer_items.append(_byte_pad( mesh.vertices.astype(float32).tobytes())) # make sure nothing fell off the truck assert len(buffer_items) >= tree['accessors'][-1]['bufferView'] # check to see if we have vertex or face colors if mesh.visual.kind in ['vertex', 'face']: # make sure colors are RGBA, this should always be true vertex_colors = mesh.visual.vertex_colors # add the reference for vertex color tree["meshes"][-1]["primitives"][0]["attributes"][ "COLOR_0"] = len(tree["accessors"]) # convert color data to bytes color_data = _byte_pad(vertex_colors.astype(uint8).tobytes()) # the vertex color accessor data tree["accessors"].append({ "bufferView": len(buffer_items), "componentType": 5121, "normalized": True, "count": len(vertex_colors), "type": "VEC4", "byteOffset": 0}) # the actual color data buffer_items.append(color_data) elif hasattr(mesh.visual, 'material'): # append the material and then set from returned index tree["meshes"][-1]["primitives"][0]["material"] = _append_material( mat=mesh.visual.material, tree=tree, buffer_items=buffer_items, mat_hashes=mat_hashes) # if mesh has UV coordinates defined export them has_uv = (hasattr(mesh.visual, 'uv') and mesh.visual.uv is not None and len(mesh.visual.uv) == len(mesh.vertices)) if has_uv: # add the reference for UV coordinates tree["meshes"][-1]["primitives"][0]["attributes"][ "TEXCOORD_0"] = len(tree["accessors"]) # slice off W if passed uv = mesh.visual.uv.copy()[:, :2] # reverse the Y for GLTF uv[:, 1] = 1.0 - uv[:, 1] # convert UV coordinate data to bytes and pad uv_data = _byte_pad(uv.astype(float32).tobytes()) # add an accessor describing the blob of UV's tree["accessors"].append({ "bufferView": len(buffer_items), "componentType": 5126, "count": len(mesh.visual.uv), "type": "VEC2", "byteOffset": 0}) # immediately add UV data so bufferView indices are correct buffer_items.append(uv_data) if (include_normals or (include_normals is None and 'vertex_normals' in mesh._cache.cache)): # add the reference for vertex color tree["meshes"][-1]["primitives"][0]["attributes"][ "NORMAL"] = len(tree["accessors"]) normal_data = _byte_pad(mesh.vertex_normals.astype( float32).tobytes()) # the vertex color accessor data tree["accessors"].append({ "bufferView": len(buffer_items), "componentType": 5126, "count": len(mesh.vertices), "type": "VEC3", "byteOffset": 0}) # the actual color data buffer_items.append(normal_data) # for each attribute with a leading underscore, assign them to trimesh # vertex_attributes for key in mesh.vertex_attributes: attribute_name = key # Application specific attributes must be prefixed with an underscore if not key.startswith("_"): attribute_name = "_" + key tree["meshes"][-1]["primitives"][0]["attributes"][attribute_name] = len( tree["accessors"]) attribute_data = _byte_pad(mesh.vertex_attributes[key].tobytes()) accessor = { "bufferView": len(buffer_items), "count": len(mesh.vertex_attributes[key]) } accessor.update(_build_accessor(mesh.vertex_attributes[key])) tree["accessors"].append(accessor) buffer_items.append(attribute_data) def _build_views(buffer_items): views = [] # create the buffer views current_pos = 0 for current_item in buffer_items: views.append( {"buffer": 0, "byteOffset": current_pos, "byteLength": len(current_item)}) current_pos += len(current_item) return views def _build_accessor(array): shape = array.shape data_type = "SCALAR" if len(shape) == 2: vec_length = shape[1] if vec_length > 4: raise ValueError("The GLTF spec does not support vectors larger than 4") if vec_length > 1: data_type = "VEC%d" % vec_length else: data_type = "SCALAR" if len(shape) == 3: if shape[2] not in [2, 3, 4]: raise ValueError("Matrix types must have 4, 9 or 16 components") data_type = "MAT%d" % shape[2] # get the array data type as a str, stripping off endian lookup = array.dtype.str[-2:] # map the numpy dtype to a GLTF code (i.e. 5121) componentType = _dtypes_lookup[lookup] accessor = { "componentType": componentType, "type": data_type, "byteOffset": 0} if len(shape) < 3: accessor["max"] = array.max(axis=0).tolist() accessor["min"] = array.min(axis=0).tolist() return accessor def _byte_pad(data, bound=4): """ GLTF wants chunks aligned with 4 byte boundaries. This function will add padding to the end of a chunk of bytes so that it aligns with the passed boundary size. Parameters -------------- data : bytes Data to be padded bound : int Length of desired boundary Returns -------------- padded : bytes Result where: (len(padded) % bound) == 0 """ bound = int(bound) if len(data) % bound != 0: # extra bytes to pad with count = bound - (len(data) % bound) # bytes(count) only works on Python 3 pad = (' ' * count).encode('utf-8') # combine the padding and data result = bytes().join([data, pad]) # we should always divide evenly if (len(result) % bound) != 0: raise ValueError( 'byte_pad failed! ori:{} res:{} pad:{} req:{}'.format( len(data), len(result), count, bound)) return result return data def _append_path(path, name, tree, buffer_items): """ Append a 2D or 3D path to the scene structure and put the data into buffer_items. Parameters ------------- path : trimesh.Path2D or trimesh.Path3D Source geometry name : str Name of geometry tree : dict Will be updated with data from path buffer_items Will have buffer appended with path data """ # convert the path to the unnamed args for # a pyglet vertex list vxlist = rendering.path_to_vertexlist(path) tree["meshes"].append({ "name": name, "primitives": [{ "attributes": {"POSITION": len(tree["accessors"])}, "mode": 1, # mode 1 is GL_LINES "material": len(tree["materials"])}]}) # if units are defined, store them as an extra: # https://github.com/KhronosGroup/glTF/tree/master/extensions if path.units is not None and 'meter' not in path.units: tree["meshes"][-1]["extras"] = {"units": str(path.units)} tree["accessors"].append( { "bufferView": len(buffer_items), "componentType": 5126, "count": vxlist[0], "type": "VEC3", "byteOffset": 0, "max": path.vertices.max(axis=0).tolist(), "min": path.vertices.min(axis=0).tolist()}) # TODO add color support to Path object # this is just exporting everying as black tree["materials"].append(_default_material) # data is the second value of the fourth field # which is a (data type, data) tuple buffer_items.append(_byte_pad( vxlist[4][1].astype(float32).tobytes())) def _parse_materials(header, views, resolver=None): """ Convert materials and images stored in a GLTF header and buffer views to PBRMaterial objects. Parameters ------------ header : dict Contains layout of file views : (n,) bytes Raw data Returns ------------ materials : list List of trimesh.visual.texture.Material objects """ try: import PIL.Image except ImportError: log.warning("unable to load textures without pillow!") return None # load any images images = None if "images" in header: # images are referenced by index images = [None] * len(header["images"]) # loop through images for i, img in enumerate(header["images"]): # get the bytes representing an image if 'bufferView' in img: blob = views[img["bufferView"]] elif 'uri' in img: # will get bytes from filesystem or base64 URI blob = _uri_to_bytes(uri=img['uri'], resolver=resolver) else: log.warning('unable to load image from: {}'.format( img.keys())) continue # i.e. 'image/jpeg' # mime = img['mimeType'] try: # load the buffer into a PIL image images[i] = PIL.Image.open(util.wrap_as_stream(blob)) except BaseException: log.error("failed to load image!", exc_info=True) # store materials which reference images materials = [] if "materials" in header: for mat in header["materials"]: # flatten key structure so we can loop it loopable = mat.copy() # this key stores another dict of crap if "pbrMetallicRoughness" in loopable: # add keys of keys to top level dict loopable.update(loopable.pop("pbrMetallicRoughness")) # save flattened keys we can use for kwargs pbr = {} for k, v in loopable.items(): if not isinstance(v, dict): pbr[k] = v elif "index" in v: # get the index of image for texture idx = header["textures"][v["index"]]["source"] # store the actual image as the value pbr[k] = images[idx] # create a PBR material object for the GLTF material materials.append(visual.material.PBRMaterial(**pbr)) return materials def _read_buffers(header, buffers, mesh_kwargs, merge_primitives=False, resolver=None): """ Given a list of binary data and a layout, return the kwargs to create a scene object. Parameters ----------- header : dict With GLTF keys buffers : list of bytes Stored data passed : dict Kwargs for mesh constructors Returns ----------- kwargs : dict Can be passed to load_kwargs for a trimesh.Scene """ # split buffer data into buffer views views = [None] * len(header["bufferViews"]) for i, view in enumerate(header["bufferViews"]): if "byteOffset" in view: start = view["byteOffset"] else: start = 0 end = start + view["byteLength"] views[i] = buffers[view["buffer"]][start:end] assert len(views[i]) == view["byteLength"] # load data from buffers into numpy arrays # using the layout described by accessors access = [None] * len(header['accessors']) for index, a in enumerate(header["accessors"]): # number of items count = a['count'] # what is the datatype dtype = _dtypes[a["componentType"]] # basically how many columns per_item = _shapes[a["type"]] # use reported count to generate shape shape = np.append(count, per_item) # number of items when flattened # i.e. a (4, 4) MAT4 has 16 per_count = np.abs(np.product(per_item)) if 'bufferView' in a: # data was stored in a buffer view so get raw bytes data = views[a["bufferView"]] # is the accessor offset in a buffer if "byteOffset" in a: start = a["byteOffset"] else: # otherwise assume we start at first byte start = 0 # length is the number of bytes per item times total length = np.dtype(dtype).itemsize * count * per_count # load the bytes data into correct dtype and shape access[index] = np.frombuffer( data[start:start + length], dtype=dtype).reshape(shape) else: # a "sparse" accessor should be initialized as zeros access[index] = np.zeros( count * per_count, dtype=dtype).reshape(shape) # load images and textures into material objects materials = _parse_materials( header, views=views, resolver=resolver) mesh_prim = collections.defaultdict(list) # load data from accessors into Trimesh objects meshes = collections.OrderedDict() for index, m in enumerate(header["meshes"]): metadata = {} try: # try loading units from the GLTF extra metadata['units'] = str(m["extras"]["units"]) except BaseException: # GLTF spec indicates the default units are meters metadata['units'] = 'meters' for j, p in enumerate(m["primitives"]): # if we don't have a triangular mesh continue # if not specified assume it is a mesh if "mode" in p and p["mode"] != 4: log.warning('skipping primitive with mode {}!'.format(p['mode'])) continue # store those units kwargs = {"metadata": {}} kwargs.update(mesh_kwargs) kwargs["metadata"].update(metadata) # get vertices from accessors kwargs["vertices"] = access[p["attributes"]["POSITION"]] # get faces from accessors if 'indices' in p: kwargs["faces"] = access[p["indices"]].reshape((-1, 3)) else: # indices are apparently optional and we are supposed to # do the same thing as webGL drawArrays? kwargs['faces'] = np.arange( len(kwargs['vertices']), dtype=np.int64).reshape((-1, 3)) # do we have UV coordinates if "material" in p: if materials is None: log.warning('no materials! `pip install pillow`') else: uv = None if "TEXCOORD_0" in p["attributes"]: # flip UV's top- bottom to move origin to lower-left: # https://github.com/KhronosGroup/glTF/issues/1021 uv = access[p["attributes"]["TEXCOORD_0"]].copy() uv[:, 1] = 1.0 - uv[:, 1] # create a texture visual kwargs["visual"] = visual.texture.TextureVisuals( uv=uv, material=materials[p["material"]]) # create a unique mesh name per- primitive if "name" in m: name = m["name"] else: name = "GLTF_geometry" # make name unique across multiple meshes if name in meshes: name += "_{}".format(util.unique_id()) # each primitive gets it's own Trimesh object if len(m["primitives"]) > 1: name += "_{}".format(j) custom_attrs = [attr for attr in p["attributes"] if attr.startswith("_")] if len(custom_attrs): vertex_attributes = {} for attr in custom_attrs: vertex_attributes[attr] = access[p["attributes"][attr]] kwargs["vertex_attributes"] = vertex_attributes kwargs["process"] = False meshes[name] = kwargs mesh_prim[index].append(name) # sometimes GLTF "meshes" come with multiple "primitives" # by default we return one Trimesh object per "primitive" # but if merge_primitives is True we combine the primitives # for the "mesh" into a single Trimesh object if merge_primitives: # if we are only returning one Trimesh object # replace `mesh_prim` with updated values mesh_prim_replace = dict() mesh_pop = [] for mesh_index, names in mesh_prim.items(): if len(names) <= 1: mesh_prim_replace[mesh_index] = names continue # use the first name name = names[0] # remove the other meshes after we're done looping mesh_pop.extend(names[1:]) # collect the meshes # TODO : use mesh concatenation with texture support current = [meshes[n] for n in names] v_seq = [p['vertices'] for p in current] f_seq = [p['faces'] for p in current] v, f = util.append_faces(v_seq, f_seq) if 'metadata' in meshes[names[0]]: metadata = meshes[names[0]]['metadata'] else: metadata = {} meshes[name] = { 'vertices': v, 'faces': f, 'metadata': metadata, 'process': False} mesh_prim_replace[mesh_index] = [name] # avoid altering inside loop mesh_prim = mesh_prim_replace # remove outdated meshes [meshes.pop(p, None) for p in mesh_pop] # make it easier to reference nodes nodes = header["nodes"] # nodes are referenced by index # save their string names if they have one # node index (int) : name (str) names = {} for i, n in enumerate(nodes): if "name" in n: if n["name"] in names.values(): names[i] = n["name"] + "_{}".format(util.unique_id()) else: names[i] = n["name"] else: names[i] = str(i) # make sure we have a unique base frame name base_frame = "world" if base_frame in names: base_frame = str(int(np.random.random() * 1e10)) names[base_frame] = base_frame # visited, kwargs for scene.graph.update graph = collections.deque() # unvisited, pairs of node indexes queue = collections.deque() if 'scene' in header: # specify the index of scenes if specified scene_index = header['scene'] else: # otherwise just use the first index scene_index = 0 # start the traversal from the base frame to the roots for root in header["scenes"][scene_index]["nodes"]: # add transform from base frame to these root nodes queue.append([base_frame, root]) # go through the nodes tree to populate # kwargs for scene graph loader while len(queue) > 0: # (int, int) pair of node indexes a, b = queue.pop() # dict of child node # parent = nodes[a] child = nodes[b] # add edges of children to be processed if "children" in child: queue.extend([[b, i] for i in child["children"]]) # kwargs to be passed to scene.graph.update kwargs = {"frame_from": names[a], "frame_to": names[b]} # grab matrix from child # parent -> child relationships have matrix stored in child # for the transform from parent to child if "matrix" in child: kwargs["matrix"] = np.array( child["matrix"], dtype=np.float64).reshape((4, 4)).T else: # if no matrix set identity kwargs["matrix"] = np.eye(4) # Now apply keyword translations # GLTF applies these in order: T * R * S if "translation" in child: kwargs["matrix"] = np.dot( kwargs["matrix"], transformations.translation_matrix(child["translation"])) if "rotation" in child: # GLTF rotations are stored as (4,) XYZW unit quaternions # we need to re- order to our quaternion style, WXYZ quat = np.reshape(child["rotation"], 4)[[3, 0, 1, 2]] # add the rotation to the matrix kwargs["matrix"] = np.dot( kwargs["matrix"], transformations.quaternion_matrix(quat)) if "scale" in child: # add scale to the matrix kwargs["matrix"] = np.dot( kwargs["matrix"], np.diag(np.concatenate((child['scale'], [1.0])))) if "mesh" in child: geometries = mesh_prim[child["mesh"]] # if the node has a mesh associated with it if len(geometries) > 1: # append root node graph.append(kwargs.copy()) # put primitives as children for i, geom_name in enumerate(geometries): # save the name of the geometry kwargs["geometry"] = geom_name # no transformations kwargs["matrix"] = np.eye(4) kwargs['frame_from'] = names[b] # if we have more than one primitive assign a new UUID # frame name for the primitives after the first one frame_to = '{}_{}'.format( names[b], util.unique_id(length=6)) kwargs['frame_to'] = frame_to # append the edge with the mesh frame graph.append(kwargs.copy()) else: kwargs["geometry"] = geometries[0] if 'name' in child: kwargs['frame_to'] = names[b] graph.append(kwargs.copy()) else: # if the node doesn't have any geometry just add graph.append(kwargs) # kwargs for load_kwargs result = {"class": "Scene", "geometry": meshes, "graph": graph, "base_frame": base_frame} # load any extras into scene.metadata result.update(_parse_extras(header)) return result def _parse_extras(header): """ Load any GLTF "extras" into scene.metadata['extras']. Parameters -------------- header : dict GLTF header Returns ------------- kwargs : dict Includes metadata """ if 'extras' not in header: return {} try: return {'metadata': {'extras': dict(header['extras'])}} except BaseException: log.warning('failed to load extras', exc_info=True) return {} def _convert_camera(camera): """ Convert a trimesh camera to a GLTF camera. Parameters ------------ camera : trimesh.scene.cameras.Camera Trimesh camera object Returns ------------- gltf_camera : dict Camera represented as a GLTF dict """ result = { "name": camera.name, "type": "perspective", "perspective": { "aspectRatio": camera.fov[0] / camera.fov[1], "yfov": np.radians(camera.fov[1]), "znear": float(camera.z_near)}} return result def _append_image(img, tree, buffer_items): """ Append a PIL image to a GLTF2.0 tree. Parameters ------------ img : PIL.Image Image object tree : dict GLTF 2.0 format tree buffer_items : (n,) bytes Binary blobs containing data Returns ----------- index : int or None The index of the image in the tree None if image append failed for any reason """ # probably not a PIL image so exit if not hasattr(img, 'format'): return None # don't re-encode JPEGs if img.format == 'JPEG': # no need to mangle JPEGs save_as = 'JPEG' else: # for everything else just use PNG save_as = 'png' # get the image data into a bytes object with util.BytesIO() as f: img.save(f, format=save_as) f.seek(0) data = f.read() # append buffer index and the GLTF-acceptable mimetype tree['images'].append({ 'bufferView': len(buffer_items), 'mimeType': 'image/{}'.format(save_as.lower())}) # append data so bufferView matches buffer_items.append(_byte_pad(data)) # index is length minus one return len(tree['images']) - 1 def _append_material(mat, tree, buffer_items, mat_hashes): """ Add passed PBRMaterial as GLTF 2.0 specification JSON serializable data: - images are added to `tree['images']` - texture is added to `tree['texture']` - material is added to `tree['materials']` Parameters ------------ mat : trimesh.visual.materials.PBRMaterials Source material to convert tree : dict GLTF header blob buffer_items : (n,) bytes Binary blobs with various data mat_hashes : dict Which materials have already been added Stored as { hashed : material index } Returns ------------- index : int Index at which material was added """ # materials are hashable hashed = hash(mat) # check stored material indexes to see if material # has already been added if mat_hashes is not None and hashed in mat_hashes: return mat_hashes[hashed] # convert passed input to PBR if necessary if hasattr(mat, 'to_pbr'): as_pbr = mat.to_pbr() else: as_pbr = mat # a default PBR metallic material result = {"pbrMetallicRoughness": {}} try: # try to convert base color to (4,) float color result['baseColorFactor'] = visual.color.to_float( as_pbr.baseColorFactor).reshape(4).tolist() except BaseException: pass try: result['emissiveFactor'] = as_pbr.emissiveFactor.reshape(3).tolist() except BaseException: pass # if name is defined, export if isinstance(as_pbr.name, str): result['name'] = as_pbr.name # if alphaMode is defined, export if isinstance(as_pbr.alphaMode, str): result['alphaMode'] = as_pbr.alphaMode # if doubleSided is defined, export if isinstance(as_pbr.doubleSided, bool): result['doubleSided'] = as_pbr.doubleSided # if scalars are defined correctly export if isinstance(as_pbr.metallicFactor, float): result['metallicFactor'] = as_pbr.metallicFactor if isinstance(as_pbr.roughnessFactor, float): result['roughnessFactor'] = as_pbr.roughnessFactor # which keys of the PBRMaterial are images image_mapping = { 'baseColorTexture': as_pbr.baseColorTexture, 'emissiveTexture': as_pbr.emissiveTexture, 'normalTexture': as_pbr.normalTexture, 'occlusionTexture': as_pbr.occlusionTexture, 'metallicRoughnessTexture': as_pbr.metallicRoughnessTexture} for key, img in image_mapping.items(): if img is None: continue # try adding the base image to the export object index = _append_image( img=img, tree=tree, buffer_items=buffer_items) # if the image was added successfully it will return index # if it failed for any reason, it will return None if index is not None: # add a reference to the base color texture result[key] = {'index': len(tree['textures'])} # add an object for the texture tree['textures'].append({'source': index, 'sampler': 0}) # for our PBRMaterial object we flatten all keys # however GLTF would like some of them under the # "pbrMetallicRoughness" key pbr_subset = ['baseColorTexture', 'baseColorFactor', 'roughnessFactor', 'metallicFactor', 'metallicRoughnessTexture'] # move keys down a level for key in pbr_subset: if key in result: result["pbrMetallicRoughness"][key] = result.pop(key) # if we didn't have any PBR keys remove the empty key if len(result['pbrMetallicRoughness']) == 0: result.pop('pbrMetallicRoughness') # which index are we inserting material at index = len(tree['materials']) # add the material to the data structure tree['materials'].append(result) # add the material index in-place mat_hashes[hashed] = index return index def validate(header): """ Validate a GLTF 2.0 header against the schema. Returns result from: `jsonschema.validate(header, schema=get_schema())` Parameters ------------- header : dict Populated GLTF 2.0 header Raises -------------- err : jsonschema.exceptions.ValidationError If the tree is an invalid GLTF2.0 header """ # a soft dependency import jsonschema # will do the reference replacement schema = get_schema() # validate the passed header against the schema return jsonschema.validate(header, schema=schema) def get_schema(): """ Get a copy of the GLTF 2.0 schema with references resolved. Returns ------------ schema : dict A copy of the GLTF 2.0 schema without external references. """ # replace references from ..schemas import resolve # get zip resolver to access referenced assets from ..resolvers import ZipResolver # get a blob of a zip file including the GLTF 2.0 schema blob = resources.get('gltf_2_schema.zip', decode=False) # get the zip file as a dict keyed by file name archive = util.decompress(util.wrap_as_stream(blob), 'zip') # get a resolver object for accessing the schema resolver = ZipResolver(archive) # get a loaded dict from the base file unresolved = json.loads(util.decode_text( resolver.get('glTF.schema.json'))) # remove references to other files in the schema schema = resolve(unresolved, resolver=resolver) return schema # exporters _gltf_loaders = {"glb": load_glb, "gltf": load_gltf}