Source code for py3dtiles.tileset.content.gltf_utils

from __future__ import annotations

from typing import Any, NamedTuple, cast

import numpy as np
import numpy.typing as npt
import pygltflib


[docs] class GltfAttribute(NamedTuple): name: str accessor_type: pygltflib.SCALAR | pygltflib.VEC2 | pygltflib.VEC3 component_type: pygltflib.UNSIGNED_BYTE | pygltflib.UNSIGNED_INT | pygltflib.FLOAT array: npt.NDArray[np.uint8 | np.uint16 | np.uint32 | np.float32]
[docs] def get_component_type_from_dtype(dt: np.dtype[Any]) -> int: val = None if dt == np.int8: val = pygltflib.BYTE elif dt == np.uint8: val = pygltflib.UNSIGNED_BYTE elif dt == np.int16: val = pygltflib.SHORT elif dt == np.uint16: val = pygltflib.UNSIGNED_SHORT elif dt == np.uint32: val = pygltflib.UNSIGNED_INT elif dt == np.float32: val = pygltflib.FLOAT else: raise ValueError(f"Cannot find a component type suitable for {dt}") return cast(int, val)
[docs] class GltfPrimitive: def __init__( self, points: npt.NDArray[np.float32], triangles: npt.NDArray[np.uint8 | np.uint16 | np.uint32] | None = None, normals: npt.NDArray[np.float32] | None = None, uvs: npt.NDArray[np.float32] | None = None, batchids: npt.NDArray[np.uint32] | None = None, additional_attributes: list[GltfAttribute] | None = None, texture_uri: str | None = None, material: pygltflib.Material | None = None, ) -> None: """ A data structure storing all information to create a glTF mesh's primitive. :param points: array of vertex positions, must have a (n, 3) shape. :param triangles: array of triangle indices, must have a (n, 3) shape. :param normals: array of vertex normals, must have a (n, 3) shape. :param uvs: array of texture coordinates, must have a (n, 2) shape. :param batchids: array of batch table IDs, must have a (n) shape. :param additional_attributes: additional attributes to add to the primitive. :param texture_uri: the URI of the texture image if the primitive is textured. :param material: a glTF material. If not set, a default material is created. """ self.points: GltfAttribute = GltfAttribute( "POSITION", pygltflib.VEC3, pygltflib.FLOAT, points ) self.triangles: GltfAttribute | None = ( GltfAttribute( "INDICE", pygltflib.SCALAR, get_component_type_from_dtype(triangles.dtype), triangles, ) if triangles is not None else None ) self.normals: GltfAttribute | None = ( GltfAttribute("NORMAL", pygltflib.VEC3, pygltflib.FLOAT, normals) if normals is not None else None ) self.uvs: GltfAttribute | None = ( GltfAttribute("TEXCOORD_0", pygltflib.VEC2, pygltflib.FLOAT, uvs) if uvs is not None else None ) self.batchids: GltfAttribute | None = ( GltfAttribute( "_BATCHID", pygltflib.SCALAR, pygltflib.UNSIGNED_INT, batchids ) if batchids is not None else None ) self.additional_attributes: list[GltfAttribute] = ( additional_attributes if additional_attributes is not None else [] ) self.texture_uri: str | None = texture_uri self.material: pygltflib.Material | None = material
[docs] def gltf_component_from_primitive( primitive: GltfPrimitive, byte_offset: int = 0, attribute_counter: int = 0, ) -> tuple[ pygltflib.Primitive, list[pygltflib.Accessor], list[pygltflib.BufferView], bytes ]: """Build the GlTF structure that corresponds to a triangulated mesh. The mesh is represented as numpy arrays as follows: - a 3D-point array; - a triangle array, where points are identified by their positional ID in the 3D-point array. See https://gitlab.com/dodgyville/pygltflib/ (section "Create a mesh, convert to bytes, convert back to mesh"). """ gltf_primitive = pygltflib.Primitive( attributes=pygltflib.Attributes(), ) gltf_binary_blob = b"" gltf_accessors = [] gltf_buffer_views = [] if primitive.triangles is not None: indice_blob, indice_accessor, indice_buffer_view = prepare_gltf_component( attribute_counter, primitive.triangles.array, byte_offset, primitive.triangles.array.size, primitive.triangles.accessor_type, primitive.triangles.component_type, pygltflib.ELEMENT_ARRAY_BUFFER, ) gltf_binary_blob += indice_blob gltf_accessors.append(indice_accessor) gltf_buffer_views.append(indice_buffer_view) gltf_primitive.indices = attribute_counter attribute_counter += 1 attributes_array: list[GltfAttribute | None] = [ primitive.points, primitive.normals, primitive.uvs, primitive.batchids, ] attributes_array.extend(primitive.additional_attributes) for attribute in attributes_array: if attribute is None: continue (array_blob, accessor, buffer_view,) = prepare_gltf_component( attribute_counter, attribute.array, byte_offset + len(gltf_binary_blob), len(attribute.array), attribute.accessor_type, attribute.component_type, ) gltf_binary_blob += array_blob gltf_accessors.append(accessor) gltf_buffer_views.append(buffer_view) setattr(gltf_primitive.attributes, attribute.name, attribute_counter) attribute_counter += 1 gltf_accessors[int(primitive.triangles is not None)].min = np.min( primitive.points.array, axis=0 ).tolist() gltf_accessors[int(primitive.triangles is not None)].max = np.max( primitive.points.array, axis=0 ).tolist() return gltf_primitive, gltf_accessors, gltf_buffer_views, gltf_binary_blob
[docs] def prepare_gltf_component( array_idx: int, array: npt.NDArray[np.uint8 | np.uint16 | np.uint32 | np.float32], byte_offset: int, count: int, accessor_type: str = pygltflib.VEC3, component_type: int = pygltflib.FLOAT, buffer_view_target: int = pygltflib.ARRAY_BUFFER, ) -> tuple[bytes, pygltflib.Accessor, pygltflib.BufferView]: array_blob = array.flatten().tobytes() BUFFER_INDEX = 0 # Everything is stored in the same buffer for sake of simplicity accessor = pygltflib.Accessor( bufferView=array_idx, componentType=component_type, count=count, type=accessor_type, ) buffer_view = pygltflib.BufferView( buffer=BUFFER_INDEX, byteOffset=byte_offset, byteLength=len(array_blob), target=buffer_view_target, ) return array_blob, accessor, buffer_view