Source code for fury.gltf

# TODO: Materials, Lights
import base64
import copy
import os
from typing import Dict  # noqa

from PIL import Image
import numpy as np
import pygltflib as gltflib
from pygltflib.utils import glb2gltf, gltf2glb

from fury import actor, io, transform, utils
from fury.animation import Animation
from fury.animation.interpolator import (
    linear_interpolator,
    slerp,
    step_interpolator,
    tan_cubic_spline_interpolator,
)
from fury.decorators import warn_on_args_to_kwargs
from fury.lib import Camera, Matrix4x4, Texture, Transform, numpy_support

comp_type = {
    5120: {"size": 1, "dtype": np.byte},
    5121: {"size": 1, "dtype": np.ubyte},
    5122: {"size": 2, "dtype": np.short},
    5123: {"size": 2, "dtype": np.ushort},
    5125: {"size": 4, "dtype": np.uint},
    5126: {"size": 4, "dtype": np.float32},
}

acc_type = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}


[docs] class glTF: @warn_on_args_to_kwargs() def __init__(self, filename, *, apply_normals=False): """Read and generate actors from glTF files. Parameters ---------- filename : str Path of the gltf file apply_normals : bool, optional If `True` applies normals to the mesh. """ if filename in ["", None]: raise IOError("Filename cannot be empty or None!") name, extension = os.path.splitext(filename) if extension == ".glb": fname_gltf = f"{name}.gltf" if not os.path.exists(fname_gltf): glb2gltf(filename) filename = fname_gltf self.gltf = gltflib.GLTF2().load(filename) self.pwd = os.path.dirname(filename) self.apply_normals = apply_normals self.cameras = {} self.materials = [] self.nodes = [] self.transformations = [] self.polydatas = [] self.init_transform = np.identity(4) self.node_transform = [] self.animation_channels = {} self.sampler_matrices = {} # Skinning Information self.bone_tranforms = {} self.keyframe_transforms = [] self.joints_0 = [] self.weights_0 = [] self.bones = [] self.ibms = {} self._vertices = None self._vcopy = None self._bvertices = {} self._bvert_copy = {} self.show_bones = False # morphing inofrmations self.morph_vertices = [] self.morph_weights = [] self.inspect_scene(scene_id=0) self._actors = [] self._bactors = {}
[docs] def actors(self): """Generate actors from glTF file. Returns ------- actors : list List of vtkActors with texture. """ for i, polydata in enumerate(self.polydatas): actor = utils.get_actor_from_polydata(polydata) transform_mat = self.transformations[i] _transform = Transform() _matrix = Matrix4x4() _matrix.DeepCopy(transform_mat.ravel()) _transform.SetMatrix(_matrix) actor.SetUserTransform(_transform) if self.materials[i] is not None: base_col_tex = self.materials[i]["baseColorTexture"] actor.SetTexture(base_col_tex) base_color = self.materials[i]["baseColor"] actor.GetProperty().SetColor(tuple(base_color[:3])) self._actors.append(actor) return self._actors
[docs] @warn_on_args_to_kwargs() def inspect_scene(self, *, scene_id=0): """Loop over nodes in a scene. Parameters ---------- scene_id : int, optional scene index of the glTF. """ scene = self.gltf.scenes[scene_id] nodes = scene.nodes for node_id in nodes: self.transverse_node(node_id, self.init_transform) for i, animation in enumerate(self.gltf.animations): self.transverse_channels(animation, i)
[docs] @warn_on_args_to_kwargs() def transverse_node(self, nextnode_id, matrix, *, parent=None, is_joint=False): """Load mesh and generates transformation matrix. Parameters ---------- nextnode_id : int Index of the node matrix : ndarray (4, 4) Transformation matrix parent : list, optional List of indices of parent nodes Default: None. is_joint : Bool To determine if the current node is a joint/bone of skins. Default: False """ node = self.gltf.nodes[nextnode_id] if parent is None: parent = [nextnode_id] else: parent.append(nextnode_id) matnode = np.identity(4) if node.matrix is not None: matnode = np.array(node.matrix) matnode = matnode.reshape(-1, 4).T else: if node.translation is not None: trans = node.translation translate = transform.translate(trans) matnode = np.dot(matnode, translate) if node.rotation is not None: rot = node.rotation rotate = transform.rotate(rot) matnode = np.dot(matnode, rotate) if node.scale is not None: scales = node.scale scale = transform.scale(scales) matnode = np.dot(matnode, scale) next_matrix = np.dot(matrix, matnode) if node.skin is not None: if ( nextnode_id in self.gltf.skins[0].joints and nextnode_id not in self.bone_tranforms ): self.bone_tranforms[nextnode_id] = next_matrix[:] if is_joint: if nextnode_id not in self.bone_tranforms: self.bone_tranforms[nextnode_id] = next_matrix[:] if node.mesh is not None: mesh_id = node.mesh self.load_mesh(mesh_id, next_matrix, parent) if node.skin is not None: skin_id = node.skin joints, ibms = self.get_skin_data(skin_id) for bone, ibm in zip(joints, ibms): self.bones.append(bone) self.ibms[bone] = ibm self.transverse_node( joints[0], np.identity(4), parent=parent, is_joint=True ) if node.camera is not None: camera_id = node.camera self.load_camera(camera_id, next_matrix) if node.children: for child_id in node.children: self.transverse_node( child_id, next_matrix, parent=parent, is_joint=is_joint, )
[docs] def load_mesh(self, mesh_id, transform_mat, parent): """Load the mesh data from accessor and applies the transformation. Parameters ---------- mesh_id : int Mesh index to be loaded transform_mat : ndarray (4, 4) Transformation matrix. """ primitives = self.gltf.meshes[mesh_id].primitives for primitive in primitives: attributes = primitive.attributes vertices = self.get_acc_data(attributes.POSITION) self.transformations.append(transform_mat) polydata = utils.PolyData() utils.set_polydata_vertices(polydata, vertices) if attributes.NORMAL is not None and self.apply_normals: normals = self.get_acc_data(attributes.NORMAL) normals = transform.apply_transformation(normals, transform_mat) utils.set_polydata_normals(polydata, normals) if attributes.TEXCOORD_0 is not None: tcoords = self.get_acc_data(attributes.TEXCOORD_0) utils.set_polydata_tcoords(polydata, tcoords) if attributes.COLOR_0 is not None: color = self.get_acc_data(attributes.COLOR_0) color = color[:, :-1] * 255 utils.set_polydata_colors(polydata, color) if primitive.indices is not None: indices = self.get_acc_data(primitive.indices).reshape(-1, 3) else: indices = np.arange(0, len(vertices)).reshape((-1, 3)) utils.set_polydata_triangles(polydata, indices) if attributes.JOINTS_0 is not None: vertex_joints = self.get_acc_data(attributes.JOINTS_0) self.joints_0.append(vertex_joints) vertex_weight = self.get_acc_data(attributes.WEIGHTS_0) self.weights_0.append(vertex_weight) material = None if primitive.material is not None: material = self.get_materials(primitive.material) self.polydatas.append(polydata) self.nodes.append(parent[:]) self.materials.append(material) if primitive.targets is not None: prim_morphdata = [] for target in primitive.targets: prim_morphdata.append(self.get_morph_data(target, mesh_id)) self.morph_vertices.append(prim_morphdata)
[docs] def get_acc_data(self, acc_id): """Get the correct data from buffer using accessors and bufferviews. Parameters ---------- acc_id : int Accessor index Returns ------- buffer_array : ndarray Numpy array extracted from the buffer. """ accessor = self.gltf.accessors[acc_id] buffview_id = accessor.bufferView acc_byte_offset = accessor.byteOffset count = accessor.count d_type = comp_type.get(accessor.componentType) d_size = d_type["size"] a_type = acc_type.get(accessor.type) buffview = self.gltf.bufferViews[buffview_id] buff_id = buffview.buffer byte_offset = buffview.byteOffset byte_stride = buffview.byteStride byte_stride = byte_stride if byte_stride else (a_type * d_size) byte_length = count * byte_stride total_byte_offset = byte_offset + acc_byte_offset buff_array = self.get_buff_array( buff_id, d_type["dtype"], byte_length, total_byte_offset, byte_stride ) return buff_array[:, :a_type]
[docs] def get_buff_array(self, buff_id, d_type, byte_length, byte_offset, byte_stride): """Extract the mesh data from buffer. Parameters ---------- buff_id : int Buffer Index d_type : type Element data type byte_length : int The length of the buffer data byte_offset : int The offset into the buffer in bytes byte_stride : int The stride, in bytes Returns ------- out_arr : ndarray Numpy array of size byte_length from buffer. """ buffer = self.gltf.buffers[buff_id] uri = buffer.uri if d_type == np.short or d_type == np.ushort or d_type == np.uint16: byte_length = int(byte_length / 2) byte_stride = int(byte_stride / 2) elif d_type == np.float32: byte_length = int(byte_length / 4) byte_stride = int(byte_stride / 4) try: if uri.startswith("data:application/octet-stream;base64") or uri.startswith( "data:application/gltf-buffer;base64" ): buff_data = uri.split(",")[1] buff_data = base64.b64decode(buff_data) elif uri.endswith(".bin"): with open(os.path.join(self.pwd, uri), "rb") as f: buff_data = f.read(-1) out_arr = np.frombuffer( buff_data, dtype=d_type, count=byte_length, offset=byte_offset ) out_arr = out_arr.reshape(-1, byte_stride) return out_arr except IOError: print("Failed to read ! Error in opening file:")
[docs] def get_materials(self, mat_id): """Get the material data. Parameters ---------- mat_id : int Material index Returns ------- materials : dict Dictionary of all textures. """ material = self.gltf.materials[mat_id] bct = None pbr = material.pbrMetallicRoughness if pbr.baseColorTexture is not None: bct = pbr.baseColorTexture.index bct = self.get_texture(bct) colors = pbr.baseColorFactor return {"baseColorTexture": bct, "baseColor": colors}
[docs] def get_texture(self, tex_id): """Read and convert image into vtk texture. Parameters ---------- tex_id : int Texture index Returns ------- atexture : Texture Returns flipped vtk texture from image. """ texture = self.gltf.textures[tex_id].source image = self.gltf.images[texture] file = image.uri bv_index = image.bufferView if file is None: mimetype = image.mimeType if file is not None and file.startswith("data:image"): buff_data = file.split(",")[1] buff_data = base64.b64decode(buff_data) extension = ".png" if file.startswith("data:image/png") else ".jpg" image_path = os.path.join(self.pwd, str("b64texture" + extension)) with open(image_path, "wb") as image_file: image_file.write(buff_data) elif bv_index is not None: bv = self.gltf.bufferViews[bv_index] buffer = bv.buffer bo = bv.byteOffset bl = bv.byteLength uri = self.gltf.buffers[buffer].uri with open(os.path.join(self.pwd, uri), "rb") as f: f.seek(bo) img_binary = f.read(bl) extension = ".png" if mimetype == "images/png" else ".jpg" image_path = os.path.join(self.pwd, str("bvtexture" + extension)) with open(image_path, "wb") as image_file: image_file.write(img_binary) else: image_path = os.path.join(self.pwd, file) rgb = io.load_image(image_path) grid = utils.rgb_to_vtk(np.flipud(rgb)) atexture = Texture() atexture.InterpolateOn() atexture.EdgeClampOn() atexture.SetInputDataObject(grid) return atexture
[docs] def load_camera(self, camera_id, transform_mat): """Load the camera data of a node. Parameters ---------- camera_id : int Camera index of a node. transform_mat : ndarray (4, 4) Transformation matrix of the camera. """ camera = self.gltf.cameras[camera_id] vtk_cam = Camera() position = vtk_cam.GetPosition() position = np.asarray([position]) new_position = transform.apply_transformation(position, transform_mat) vtk_cam.SetPosition(tuple(new_position[0])) if camera.type == "orthographic": orthographic = camera.orthographic vtk_cam.ParallelProjectionOn() zfar = orthographic.zfar znear = orthographic.znear vtk_cam.SetClippingRange(znear, zfar) else: perspective = camera.perspective vtk_cam.ParallelProjectionOff() zfar = perspective.zfar if perspective.zfar else 1000.0 znear = perspective.znear vtk_cam.SetClippingRange(znear, zfar) angle = perspective.yfov * 180 / np.pi if perspective.yfov else 30.0 vtk_cam.SetViewAngle(angle) if perspective.aspectRatio: vtk_cam.SetExplicitAspectRatio(perspective.aspectRatio) self.cameras[camera_id] = vtk_cam
[docs] def transverse_channels(self, animation: gltflib.Animation, count: int): """Loop over animation channels and sets animation data. Parameters ---------- animation : glTflib.Animation pygltflib animation object. count : int Animation count. """ name = animation.name if name is None: name = str(f"anim_{count}") anim_channel = {} # type: Dict[int, np.ndarray] for channel in animation.channels: sampler = animation.samplers[channel.sampler] node_id = channel.target.node path = channel.target.path anim_data = self.get_sampler_data(sampler, node_id, path) self.node_transform.append(anim_data) sampler_data = self.get_matrix_from_sampler( path, node_id, anim_channel, sampler ) anim_channel[node_id] = sampler_data self.animation_channels[name] = anim_channel
[docs] def get_sampler_data(self, sampler: gltflib.Sampler, node_id: int, transform_type): """Get the animation and transformation data from sampler. Parameters ---------- sampler : glTFlib.Sampler pygltflib sampler object. node_id : int Node index of the current animation channel. transform_type : str Property of the node to be transformed. Returns ------- sampler_data : dict dictionary of data containing timestamps, node transformations and interpolation type. """ time_array = self.get_acc_data(sampler.input) transform_array = self.get_acc_data(sampler.output) interpolation = sampler.interpolation return { "node": node_id, "input": time_array, "output": transform_array, "interpolation": interpolation, "property": transform_type, }
[docs] def get_matrix_from_sampler( self, prop, node, anim_channel, sampler: gltflib.Sampler ): """Return transformation matrix for a given timestamp from Sampler data. Combine matrices for a given common timestamp. Parameters ---------- prop : str Property of the array ('translation', 'rotation' or 'scale') node : int Node index of the sampler data. anim_channel : dict Containing previous animations with node as keys. sampler : gltflib.Sampler Sampler object for an animation channel. """ time_array = self.get_acc_data(sampler.input) tran_array = self.get_acc_data(sampler.output) if prop == "weights": tran_array = tran_array.reshape( -1, ) tran_matrix = [] if node in anim_channel: prev_arr = anim_channel[node]["matrix"] else: prev_arr = [np.identity(4) for i in range(len(tran_array))] for i, arr in enumerate(tran_array): temp = self.generate_tmatrix(arr, prop) if temp.shape == (4, 4): tran_matrix.append(np.dot(prev_arr[i], temp)) else: tran_matrix.append(temp) data = {"timestamps": time_array, "matrix": tran_matrix} self.sampler_matrices[node] = data return data
[docs] def get_morph_data(self, target, mesh_id): weights_array = self.gltf.meshes[mesh_id].weights if target.get("POSITION") is not None: morphed_data = self.get_acc_data(target.get("POSITION")) self.morph_weights.append(weights_array) return morphed_data
[docs] def get_skin_data(self, skin_id): """Get the inverse bind matrix for each bone in the skin. Parameters ---------- skin_id : int Index of the skin. Returns ------- joint_nodes : list List of bones in the skin. inv_bind_matrix : ndarray Numpy array containing inverse bind pose for each bone. """ skin = self.gltf.skins[skin_id] inv_bind_matrix = self.get_acc_data(skin.inverseBindMatrices) inv_bind_matrix = inv_bind_matrix.reshape((-1, 4, 4)) joint_nodes = skin.joints return joint_nodes, inv_bind_matrix
[docs] def generate_tmatrix(self, transf, prop): """Create transformation matrix from TRS array. Parameters ---------- transf : ndarray Array containing translation, rotation or scale values. prop : str String that defines the type of array (values: translation, rotation or scale). Returns ------- matrix : ndarray (4, 4) ransformation matrix of shape (4, 4) with respective transforms. """ if prop == "translation": matrix = transform.translate(transf) elif prop == "rotation": matrix = transform.rotate(transf) elif prop == "scale": matrix = transform.scale(transf) else: matrix = transf return matrix
[docs] @warn_on_args_to_kwargs() def transverse_animations( self, animation, bone_id, timestamp, joint_matrices, *, parent_bone_deform=None, ): """Calculate skinning matrix (Joint Matrices) and transform bone for each animation. Parameters ---------- animation : Animation Animation object. bone_id : int Bone index of the current transform. timestamp : float Current timestamp of the animation. joint_matrices : dict Empty dictionary that will contain joint matrices. parent_bone_transform : ndarray (4, 4) Transformation matrix of the parent bone. (default=np.identity(4)) """ if parent_bone_deform is None: parent_bone_deform = np.identity(4) deform = animation.get_value("transform", timestamp) new_deform = np.dot(parent_bone_deform, deform) ibm = self.ibms[bone_id].T skin_matrix = np.dot(new_deform, ibm) joint_matrices[bone_id] = skin_matrix node = self.gltf.nodes[bone_id] if self.show_bones: actor_transform = self.transformations[0] bone_transform = np.dot(actor_transform, new_deform) self._bvertices[bone_id][:] = transform.apply_transformation( self._bvert_copy[bone_id], bone_transform ) utils.update_actor(self._bactors[bone_id]) if node.children: c_animations = animation.child_animations c_bones = node.children for c_anim, c_bone in zip(c_animations, c_bones): self.transverse_animations( c_anim, c_bone, timestamp, joint_matrices, parent_bone_deform=new_deform, )
[docs] def update_skin(self, animation): """Update the animation and actors with skinning data. Parameters ---------- animation : Animation Animation object. """ animation.update_animation() timestamp = animation.current_timestamp joint_matrices = {} root_bone = self.gltf.skins[0].skeleton root_bone = root_bone if root_bone else self.bones[0] if not root_bone == self.bones[0]: _animation = animation.child_animations[0] parent_transform = self.transformations[root_bone].T else: _animation = animation parent_transform = np.identity(4) for child in _animation.child_animations: self.transverse_animations( child, self.bones[0], timestamp, joint_matrices, parent_bone_deform=parent_transform, ) for i, vertex in enumerate(self._vertices): vertex[:] = self.apply_skin_matrix( self._vcopy[i], joint_matrices, actor_index=i, ) actor_transf = self.transformations[i] vertex[:] = transform.apply_transformation(vertex, actor_transf) utils.update_actor(self._actors[i]) utils.compute_bounds(self._actors[i])
[docs] @warn_on_args_to_kwargs() def initialize_skin(self, animation, *, bones=False, length=0.2): """Create bones and add to the animation and initialise `update_skin` Parameters ---------- animation : Animation Skin animation object. bones : bool Switches the visibility of bones in scene. (default=False) length : float Length of the bones. (default=0.2) """ self.show_bones = bones if bones: self.get_joint_actors(length=length, with_transforms=False) animation.add_actor(list(self._bactors.values())) self.update_skin(animation)
[docs] @warn_on_args_to_kwargs() def apply_skin_matrix(self, vertices, joint_matrices, *, actor_index=0): """Apply the skinnig matrix, that transform the vertices. Parameters ---------- vertices : ndarray Vertices of an actor. join_matrices : list List of skinning matrix to calculate the weighted transformation. Returns ------- vertices : ndarray Modified vertices. """ clone = np.copy(vertices) weights = self.weights_0[actor_index] joints = self.joints_0[actor_index] for i, xyz in enumerate(clone): a_joint = joints[i] a_joint = [self.bones[i] for i in a_joint] a_weight = weights[i] skin_mat = ( np.multiply(a_weight[0], joint_matrices[a_joint[0]]) + np.multiply(a_weight[1], joint_matrices[a_joint[1]]) + np.multiply(a_weight[2], joint_matrices[a_joint[2]]) + np.multiply(a_weight[3], joint_matrices[a_joint[3]]) ) xyz = np.dot(skin_mat, np.append(xyz, [1.0])) clone[i] = xyz[:3] return clone
[docs] def transverse_bones(self, bone_id, channel_name, parent_animation: Animation): """Loop over the bones and add child bone animation to their parent animation. Parameters ---------- bone_id : int Index of the bone. channel_name : str Animation name. parent_animation : Animation The animation of the parent bone. Should be `root_animation` by default. """ node = self.gltf.nodes[bone_id] animation = Animation() if bone_id in self.bone_tranforms.keys(): orig_transform = self.bone_tranforms[bone_id] else: orig_transform = np.identity(4) if bone_id in self.animation_channels[channel_name]: transforms = self.animation_channels[channel_name][bone_id] timestamps = transforms["timestamps"] matrices = transforms["matrix"] for time, matrix in zip(timestamps, matrices): animation.set_keyframe("transform", time[0], matrix) else: animation.set_keyframe("transform", 0.0, orig_transform) parent_animation.add(animation) if node.children: for child_bone in node.children: self.transverse_bones(child_bone, channel_name, animation)
[docs] def skin_animation(self): """One animation for each bone, contains parent transforms. Returns ------- root_animations : Dict An animation containing all the child animations for bones. """ root_animations = {} self._vertices = [utils.vertices_from_actor(act) for act in self.actors()] self._vcopy = [np.copy(vert) for vert in self._vertices] for name in self.animation_channels.keys(): root_animation = Animation() root_bone = self.gltf.skins[0].skeleton root_bone = root_bone if root_bone else self.bones[0] self.transverse_bones(root_bone, name, root_animation) root_animations[name] = root_animation root_animation.add_actor(self._actors) return root_animations
[docs] @warn_on_args_to_kwargs() def get_joint_actors(self, *, length=0.5, with_transforms=False): """Create an arrow actor for each bone in a skinned model. Parameters ---------- length : float (default = 0.5) Length of the arrow actor with_transforms : bool (default = False) Applies respective transformations to bone. Bones will be at origin if set to `False`. """ origin = np.zeros((3, 3)) parent_transforms = self.bone_tranforms for bone in self.bones: arrow = actor.arrow(origin, [0, 1, 0], [1, 1, 1], scales=length) verts = utils.vertices_from_actor(arrow) if with_transforms: verts[:] = transform.apply_transformation( verts, parent_transforms[bone] ) utils.update_actor(arrow) self._bactors[bone] = arrow self._bvertices[bone] = verts self._bvert_copy = copy.deepcopy(self._bvertices)
[docs] def update_morph(self, animation): """Update the animation and actors with morphing. Parameters ---------- animation : Animation Animation object. """ animation.update_animation() timestamp = animation.current_timestamp for i, vertex in enumerate(self._vertices): weights = animation.child_animations[0].get_value("morph", timestamp) vertex[:] = self.apply_morph_vertices(self._vcopy[i], weights, i) vertex[:] = transform.apply_transformation(vertex, self.transformations[i]) utils.update_actor(self._actors[i]) utils.compute_bounds(self._actors[i])
[docs] def apply_morph_vertices(self, vertices, weights, cnt): """Calculate weighted vertex from the morph data. Parameters ---------- vertices : ndarray Vertices of a actor. weights : ndarray Morphing weights used to calculate the weighted average of new vertex. cnt : int Count of the actor. """ clone = np.copy(vertices) target_vertices = np.copy(self.morph_vertices[cnt]) for i, weight in enumerate(weights): target_vertices[i][:] = np.multiply(weight, target_vertices[i]) new_verts = sum(target_vertices) for i, vertex in enumerate(clone): clone[i][:] = vertex + new_verts[i] return clone
[docs] def morph_animation(self): """Create animation for each channel in animations. Returns ------- root_animations : Dict A dictionary containing animations as values and animation name as keys. """ animations = {} self._vertices = [utils.vertices_from_actor(act) for act in self.actors()] self._vcopy = [np.copy(vert) for vert in self._vertices] for name, data in self.animation_channels.items(): root_animation = Animation() for i, transforms in enumerate(data.values()): weights = self.morph_weights[i] animation = Animation() timestamps = transforms["timestamps"] matrices = transforms["matrix"] matrices = np.array(matrices).reshape(-1, len(weights)) for time, weights in zip(timestamps, matrices): animation.set_keyframe("morph", time[0], weights) root_animation.add(animation) root_animation.add_actor(self._actors) animations[name] = root_animation return animations
[docs] def get_animations(self): """Return list of animations. Returns ------- animations: List List of animations containing actors. """ actors = self.actors() interpolators = { "LINEAR": linear_interpolator, "STEP": step_interpolator, "CUBICSPLINE": tan_cubic_spline_interpolator, } rotation_interpolators = { "LINEAR": slerp, "STEP": step_interpolator, "CUBICSPLINE": tan_cubic_spline_interpolator, } animations = [] for transforms in self.node_transform: target_node = transforms["node"] for i, nodes in enumerate(self.nodes): animation = Animation() transform_mat = self.transformations[i] position, rot, scale = transform.transform_from_matrix(transform_mat) animation.set_keyframe("position", 0.0, position) if target_node in nodes: animation.add_actor(actors[i]) timestamp = transforms["input"] node_transform = transforms["output"] prop = transforms["property"] interpolation_type = transforms["interpolation"] interpolator = interpolators.get(interpolation_type) rot_interp = rotation_interpolators.get(interpolation_type) timeshape = timestamp.shape transhape = node_transform.shape if transforms["interpolation"] == "CUBICSPLINE": node_transform = node_transform.reshape( (timeshape[0], -1, transhape[1]) ) for time, trs in zip(timestamp, node_transform): in_tan, out_tan = None, None if trs.ndim == 2: cubicspline = trs in_tan = cubicspline[0] trs = cubicspline[1] out_tan = cubicspline[2] if prop == "rotation": animation.set_rotation( time[0], trs, in_tangent=in_tan, out_tangent=out_tan ) animation.set_rotation_interpolator(rot_interp) if prop == "translation": animation.set_position( time[0], trs, in_tangent=in_tan, out_tangent=out_tan ) animation.set_position_interpolator(interpolator) if prop == "scale": animation.set_scale( time[0], trs, in_tangent=in_tan, out_tangent=out_tan ) animation.set_scale_interpolator(interpolator) else: animation.add_static_actor(actors[i]) animations.append(animation) return animations
[docs] def main_animation(self): """Return main animation with all glTF animations. Returns ------- main_animation : Animation A parent animation containing all child animations for simple animation. """ main_animation = Animation() animations = self.get_animations() for animation in animations: main_animation.add(animation) return main_animation
[docs] @warn_on_args_to_kwargs() def export_scene(scene, *, filename="default.gltf"): """Generate gltf from FURY scene. Parameters ---------- scene: Scene FURY scene object. filename: str, optional Name of the model to be saved """ gltf_obj = gltflib.GLTF2() name, extension = os.path.splitext(filename) if extension not in [".gltf", ".glb"]: raise IOError("Filename should be .gltf or .glb") buffer_file = open(f"{name}.bin", "wb") primitives = [] buffer_size = 0 bview_count = 0 for act in scene.GetActors(): prim, size, count = _connect_primitives( gltf_obj, act, buffer_file, buffer_size, bview_count, name ) primitives.append(prim) buffer_size = size bview_count = count buffer_file.close() write_mesh(gltf_obj, primitives) write_buffer(gltf_obj, size, f"{name}.bin") camera = scene.camera() cam_id = None if camera: write_camera(gltf_obj, camera) cam_id = 0 write_node(gltf_obj, mesh_id=0, camera_id=cam_id) write_scene(gltf_obj, [0]) gltf_obj.save(f"{name}.gltf") if extension == ".glb": gltf2glb(f"{name}.gltf", destination=filename)
def _connect_primitives(gltf, actor, buff_file, byteoffset, count, name): """Create Accessor, BufferViews and writes primitive data to a binary file Parameters ---------- gltf: Pygltflib.GLTF2 actor: Actor the fury actor buff_file: file filename.bin opened in `wb` mode byteoffset: int offset of the bufferview count: int BufferView count name: str Prefix of the gltf filename Returns ------- prim: Pygltflib.Primitive byteoffset: int Offset size of a primitive count: int BufferView count after adding the primitive. """ polydata = actor.GetMapper().GetInput() colors = utils.colors_from_actor(actor) if colors is not None: polydata = utils.set_polydata_colors(polydata, colors) vertices = utils.get_polydata_vertices(polydata) colors = utils.get_polydata_colors(polydata) normals = utils.get_polydata_normals(polydata) tcoords = utils.get_polydata_tcoord(polydata) try: indices = utils.get_polydata_triangles(polydata) except AssertionError as error: indices = None print(error) ispoints = polydata.GetNumberOfVerts() islines = polydata.GetNumberOfLines() istraingles = polydata.GetNumberOfPolys() if ispoints: mode = 0 elif islines: mode = 3 elif istraingles: mode = 4 vertex, index, normal, tcoord, color = (None, None, None, None, None) if indices is not None and len(indices) != 0: indices = indices.reshape((-1,)) amax = [np.max(indices)] amin = [np.min(indices)] ctype = comp_type.get(gltflib.UNSIGNED_SHORT) atype = acc_type.get(gltflib.SCALAR) indices = indices.astype(np.ushort) blength = len(indices) * ctype["size"] buff_file.write(indices.tobytes()) write_bufferview(gltf, 0, byteoffset, blength) write_accessor( gltf, count, 0, gltflib.UNSIGNED_SHORT, len(indices), gltflib.SCALAR ) byteoffset += blength index = count count += 1 if vertices is not None: amax = np.max(vertices, 0).tolist() amin = np.min(vertices, 0).tolist() ctype = comp_type.get(gltflib.FLOAT) atype = acc_type.get(gltflib.VEC3) vertices = vertices.reshape((-1,)).astype(ctype["dtype"]) blength = len(vertices) * ctype["size"] buff_file.write(vertices.tobytes()) write_bufferview(gltf, 0, byteoffset, blength) write_accessor( gltf, count, 0, gltflib.FLOAT, len(vertices) // atype, gltflib.VEC3, max=amax, min=amin, ) byteoffset += blength vertex = count count += 1 if normals is not None: amax = np.max(normals, 0).tolist() amin = np.min(normals, 0).tolist() ctype = comp_type.get(gltflib.FLOAT) atype = acc_type.get(gltflib.VEC3) normals = normals.reshape((-1,)) blength = len(normals) * ctype["size"] buff_file.write(normals.tobytes()) write_bufferview(gltf, 0, byteoffset, blength) write_accessor( gltf, count, 0, gltflib.FLOAT, len(normals) // atype, gltflib.VEC3, max=amax, min=amin, ) byteoffset += blength normal = count count += 1 if tcoords is not None: amax = np.max(tcoords, 0).tolist() amin = np.min(tcoords, 0).tolist() ctype = comp_type.get(gltflib.FLOAT) atype = acc_type.get(gltflib.VEC2) tcoords = tcoords.reshape((-1,)).astype(ctype["dtype"]) blength = len(tcoords) * ctype["size"] buff_file.write(tcoords.tobytes()) write_bufferview(gltf, 0, byteoffset, blength) write_accessor( gltf, count, 0, gltflib.FLOAT, len(tcoords) // atype, gltflib.VEC2 ) byteoffset += blength tcoord = count count += 1 vtk_image = actor.GetTexture().GetInput() rows, cols, _ = vtk_image.GetDimensions() scalars = vtk_image.GetPointData().GetScalars() np_im = numpy_support.vtk_to_numpy(scalars) np_im = np.reshape(np_im, (rows, cols, -1)) img = Image.fromarray(np_im) image_path = f"{name}BaseColorTexture.png" img.save(image_path) write_material(gltf, 0, image_path) if colors is not None: ctype = comp_type.get(gltflib.FLOAT) atype = acc_type.get(gltflib.VEC3) shape = colors.shape[0] colors = np.concatenate((colors, np.full((shape, 1), 255.0)), axis=1) colors = colors / 255 colors = colors.reshape((-1,)).astype(ctype["dtype"]) blength = len(colors) * ctype["size"] buff_file.write(colors.tobytes()) write_bufferview(gltf, 0, byteoffset, blength) write_accessor(gltf, count, 0, gltflib.FLOAT, shape, gltflib.VEC4) byteoffset += blength color = count count += 1 material = None if tcoords is None else 0 prim = get_prim(vertex, index, color, tcoord, normal, material, mode=mode) return prim, byteoffset, count
[docs] def write_scene(gltf, nodes): """Create scene Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 object nodes: list List of node indices. """ scene = gltflib.Scene() scene.nodes = nodes gltf.scenes.append(scene)
[docs] @warn_on_args_to_kwargs() def write_node(gltf, *, mesh_id=None, camera_id=None): """Create node Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 object mesh_id: int, optional Mesh index camera_id: int, optional Camera index. """ node = gltflib.Node() if mesh_id is not None: node.mesh = mesh_id if camera_id is not None: node.camera = camera_id gltf.nodes.append(node)
[docs] def write_mesh(gltf, primitives): """Create mesh and add primitive. Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 object. primitives: list List of Primitive object. """ mesh = gltflib.Mesh() for prim in primitives: mesh.primitives.append(prim) gltf.meshes.append(mesh)
[docs] def write_camera(gltf, camera): """Create and add camera. Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 object. camera: vtkCamera scene camera. """ orthographic = camera.GetParallelProjection() cam = gltflib.Camera() if orthographic: cam.type = "orthographic" else: clip_range = camera.GetClippingRange() angle = camera.GetViewAngle() ratio = camera.GetExplicitAspectRatio() aspect_ratio = ratio if ratio else 1.0 pers = gltflib.Perspective() pers.aspectRatio = aspect_ratio pers.znear, pers.zfar = clip_range pers.yfov = angle * np.pi / 180 cam.type = "perspective" cam.perspective = pers gltf.cameras.append(cam)
[docs] @warn_on_args_to_kwargs() def get_prim(vertex, index, color, tcoord, normal, material, *, mode=4): """Return a Primitive object. Parameters ---------- vertex: int Accessor index for the vertices data. index: int Accessor index for the triangles data. color: int Accessor index for the colors data. tcoord: int Accessor index for the texture coordinates data. normal: int Accessor index for the normals data. material: int Materials index. mode: int, optional The topology type of primitives to render. Default: 4 Returns ------- prim: Primitive pygltflib primitive object. """ prim = gltflib.Primitive() attr = gltflib.Attributes() attr.POSITION = vertex attr.NORMAL = normal attr.TEXCOORD_0 = tcoord attr.COLOR_0 = color prim.attributes = attr prim.indices = index if material is not None: prim.material = material prim.mode = mode return prim
[docs] def write_material(gltf, basecolortexture: int, uri: str): """Write Material, Images and Textures Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 object. basecolortexture: int BaseColorTexture index. uri: str BaseColorTexture uri. """ material = gltflib.Material() texture = gltflib.Texture() image = gltflib.Image() pbr = gltflib.PbrMetallicRoughness() tinfo = gltflib.TextureInfo() tinfo.index = basecolortexture pbr.baseColorTexture = tinfo pbr.metallicFactor = 0.0 material.pbrMetallicRoughness = pbr texture.source = basecolortexture image.uri = uri gltf.materials.append(material) gltf.textures.append(texture) gltf.images.append(image)
[docs] @warn_on_args_to_kwargs() def write_accessor( gltf, bufferview, byte_offset, comp_type, count, accssor_type, *, max=None, min=None ): """Write accessor in the gltf. Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 objecomp_type bufferview: int BufferView Index byte_offset: int ByteOffset of the accessor comp_type: type Type of a single component count: int Elements count of the accessor accssor_type: type Type of the accessor(SCALAR, VEC2, VEC3, VEC4) max: ndarray, optional Maximum elements of an array min: ndarray, optional Minimum elements of an array """ accessor = gltflib.Accessor() accessor.bufferView = bufferview accessor.byteOffset = byte_offset accessor.componentType = comp_type accessor.count = count accessor.type = accssor_type if (max is not None) and (min is not None): accessor.max = max accessor.min = min gltf.accessors.append(accessor)
[docs] @warn_on_args_to_kwargs() def write_bufferview(gltf, buffer, byte_offset, byte_length, *, byte_stride=None): """Write bufferview in the gltf. Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 object buffer: int Buffer index byte_offset: int Byte offset of the bufferview byte_length: int Byte length ie, Length of the data we want to get from the buffer byte_stride: int, optional Byte stride of the bufferview. """ buffer_view = gltflib.BufferView() buffer_view.buffer = buffer buffer_view.byteOffset = byte_offset buffer_view.byteLength = byte_length buffer_view.byteStride = byte_stride gltf.bufferViews.append(buffer_view)
[docs] def write_buffer(gltf, byte_length, uri): """Write buffer int the gltf Parameters ---------- gltf: GLTF2 Pygltflib GLTF2 object byte_length: int Length of the buffer uri: str Path to the external `.bin` file. """ buffer = gltflib.Buffer() buffer.uri = uri buffer.byteLength = byte_length gltf.buffers.append(buffer)