Source code for openalea.plantconvert.gltf.writer

import base64
from math import isnan
from warnings import warn

import numpy.linalg as npl
import pygltflib as gltf

import openalea.mtg.mtg as mtg
from openalea.mtg import traversal

from .. import binary_tools, matrix
from ..geometry import (
    mat_from_transformed,
    tapering_radius_from_transformed,
)


[docs] class gltf_builder(object): """Class used to build a gltf object from the mtg. The gltf object is implemented in pygltflib : A Python library for reading, writing and handling GLTF files. link : https://github.com/sergkr/gltflib. basic usage : builder = gltf_builder(g, features = ["Length", "Width"]) # you should initiate an instnace with your mtg g and a list of features to be written in the gltf object. builder.build() #by calling this method, the build.gltf object will be created according to the mtg g. #Finally, use the gltf's method to output a gltf file Here we choose : builder.gltf.save_json("test.gltf") #here we choose to output a simple json file. The meshes are written in an embedded (when we construct the gltf object). You can choose also to output in a more compact file type. Please refer to the pygltflib's documentation :param g: the mtg object :type g: mtg.MTG :param features: a list of features to be written in the gltf object. Default is None. :type features: list or None """ def __init__(self, g: mtg.MTG, features: list = None): """Create a new instance of gltf_builder from mtg and list of features to be written in the gltf.""" if not features: self.features = [] else: self.features = features self.g = g # property vid is added after using reindex # it's a mapping from old vid to the new id self.g.reindex() self.gltf = gltf.GLTF2() self.gltf_meshes = {}
[docs] def build(self): """ Build the gltf object with embedded mesh information. Uses the methods implemented in pygltflib to output it into a file of a given type. """ self._add_meshes() self._add_topology()
# validator.validate(self.gltf) def _add_meshes(self, meshes=None): """Process a reference mesh by adding one more buffer.""" if meshes is None: # get the reference meshes ref_meshes = getattr(self.g.node(0), "ref_meshes", None) if ref_meshes is None: raise ValueError("the mtg doesnot contain reference meshes.") if isinstance(ref_meshes, dict): meshes = [ref_mesh[0] for ref_mesh in ref_meshes.values()] mesh_id = list(ref_meshes.keys()) elif isinstance(ref_meshes, list): meshes = ref_meshes mesh_id = range(len(ref_meshes)) for triset, id in zip(meshes, mesh_id): points = binary_tools.pack_vec_array(triset.pointList, "float") normals = binary_tools.pack_vec_array(triset.normalList, "float") try: tcoords = binary_tools.pack_vec_array(triset.texCoordList, "float") except TypeError: tcoords = b"" warn("No tex coordinates are defined !") indices = binary_tools.pack_vec_array(triset.indexList, "ushort") all_data = points + normals + tcoords + indices encoded = str(base64.b64encode(all_data).decode("utf-8")) byte_length = len(all_data) # instantiate a buffer and update its value buffer = gltf.Buffer() buffer.uri = gltf.DATA_URI_HEADER + encoded buffer.byteLength = byte_length # update buffers self.gltf.buffers.append(buffer) buffer_ind = ( len(self.gltf.buffers) - 1 ) # get the index of the last added buffer # instantiate bufferviews and update their value buffer_view_ind = gltf.BufferView() # indices buffer view buffer_view_vec3 = ( gltf.BufferView() ) # vec 3 buffer view (normals and points) if len(tcoords) > 0: # if t coords exist, create the bufferviews buffer_view_vec2 = gltf.BufferView() # vec 2 buffer view (tex coord) buffer_view_vec2.buffer = buffer_ind buffer_view_vec2.byteLength = len(tcoords) buffer_view_vec2.byteOffset = len(points) + len(normals) buffer_view_vec2.target = gltf.ARRAY_BUFFER self.gltf.bufferViews.append(buffer_view_vec2) tcoords_ind = len(self.gltf.bufferViews) - 1 buffer_view_vec3.buffer = buffer_ind buffer_view_vec3.byteLength = len(points) + len(normals) buffer_view_vec3.byteStride = 12 buffer_view_vec3.target = gltf.ARRAY_BUFFER buffer_view_ind.buffer = buffer_ind buffer_view_ind.byteLength = len(indices) buffer_view_ind.byteOffset = len(points) + len(normals) + len(tcoords) buffer_view_ind.target = gltf.ELEMENT_ARRAY_BUFFER # add bufferviews to the object # and keep the buffer view's index self.gltf.bufferViews.append(buffer_view_vec3) points_ind = len(self.gltf.bufferViews) - 1 normals_ind = len(self.gltf.bufferViews) - 1 self.gltf.bufferViews.append(buffer_view_ind) indices_ind = len(self.gltf.bufferViews) - 1 # instantiate accessors aces_ind = gltf.Accessor() aces_points = gltf.Accessor() aces_normals = gltf.Accessor() aces_ind.bufferView = indices_ind aces_ind.byteOffset = 0 aces_ind.componentType = gltf.UNSIGNED_SHORT aces_ind.count = len(indices) // 2 aces_ind.type = gltf.SCALAR aces_points.bufferView = points_ind aces_points.byteOffset = 0 aces_points.componentType = gltf.FLOAT aces_points.count = len(points) // (4 * 3) aces_points.type = gltf.VEC3 min_bounds, max_bounds = triset.pointList.getBounds() aces_points.max = list(max_bounds) aces_points.min = list(min_bounds) aces_normals.bufferView = normals_ind aces_normals.byteOffset = len(points) aces_normals.componentType = gltf.FLOAT aces_normals.count = len(normals) // (4 * 3) aces_normals.type = gltf.VEC3 if len(tcoords) > 0: # instantiate tcoords accessor only if they exist aces_tcoords = gltf.Accessor() aces_tcoords.bufferView = tcoords_ind aces_tcoords.byteOffset = 0 aces_tcoords.componentType = gltf.FLOAT aces_tcoords.count = len(tcoords) // (4 * 2) aces_tcoords.type = gltf.VEC2 self.gltf.accessors.append(aces_tcoords) tcoords_ind = len(self.gltf.accessors) - 1 # add all other accessors self.gltf.accessors.append(aces_points) self.gltf.accessors.append(aces_normals) self.gltf.accessors.append(aces_ind) n = len(self.gltf.accessors) points_ind = n - 3 normals_ind = n - 2 indices_ind = n - 1 mesh = gltf.Mesh() primitive = gltf.Primitive() primitive.attributes.NORMAL = normals_ind primitive.attributes.POSITION = points_ind if len(tcoords) > 0: primitive.attributes.TEXCOORD_0 = tcoords_ind primitive.indices = indices_ind primitive.mode = gltf.TRIANGLES mesh.primitives.append(primitive) self.gltf.meshes.append(mesh) self.gltf_meshes[id] = len(self.gltf.meshes) - 1 def _add_topology(self): """Translate the mtg into the scene graph of gltf.""" iter_mtg = traversal.iter_mtg2(self.g, self.g.root) n = len(self.g) nodes = [gltf.Node() for i in range(n)] for vid in iter_mtg: parent_id = self.g.parent(vid) # set the label nodes[vid].name = self.g.label(vid) # this function add mtg node attribute into the gltf node self._add_extra2node(vid, nodes[vid]) # here we solve the topology children = self.g.children(vid) nodes[vid].children.extend(children) nodes[vid].extras["scale"] = self.g.scale(vid) nodes[vid].extras["edge_type"] = self.g.edge_type(vid) # if the mtg node is not at the finest scale nodes[vid].extras["component_roots"] = self.g.component_roots(vid) # try to solve geometry geo = getattr(self.g.node(vid), "geometry", None) shapeIndex = self.g.node(vid).shapeIndex parent_geo = getattr(self.g.node(parent_id), "geometry", None) if geo is not None: mat_c = mat_from_transformed(geo) if parent_geo is None: mat_p = None else: mat_p = mat_from_transformed(parent_geo) try: # translation,rotation,scaling = maths.global_to_local_no_scale(mat_c,mat_p) # translation,rotation,scaling = maths.global_to_local(mat_c,mat_p, use_quaternion=False) # nodes[vid].scale = scaling.tolist() # nodes[vid].rotation = rotation # nodes[vid].translation = translation.tolist() nodes[vid].matrix = ( matrix.global_to_local_matrix(mat_c, mat_p) .flatten("F") .tolist() ) top, base = tapering_radius_from_transformed(geo) if not isnan(top) and not isnan(base): nodes[vid].extras["top"] = top nodes[vid].extras["base"] = base else: warn("nan value found in tapering") except ZeroDivisionError: warn( "singular matrix found in the mtg, we ignore the geometry of the corresponding node" ) except npl.LinAlgError: warn( "singular matrix found in the mtg, we ignore the geometry of the corresponding node" ) except matrix.TRSError: warn( "singular matrix found in the mtg, we ignore the geometry of the corresponding node" ) else: mesh_id = self.g[0]["shapes"][shapeIndex]["meshIndex"] mesh_gltf_id = self.gltf_meshes[mesh_id] nodes[vid].mesh = mesh_gltf_id # nodes[vid].matrix = list(np.identity(4).flatten()) self.gltf.nodes = nodes for s in self.g.scales_iter(): scene = gltf.Scene() scene.name = "Scene of scale %s" % (s) scene.nodes = self.g.roots(s) self.gltf.scenes.append(scene) self.gltf.scene = s def _add_extra2node(self, i, gnode: gltf.Node): """ Add the attributes of ith mtg node to a gltf node. input : i : the index of the mtg node gnode : the gltf node to be updated. """ for f in self.features: value = getattr(self.g.node(i), f, None) if value is not None: gnode.extras[f] = value