Source code for py3dtiles.tileset.content.gltf
from __future__ import annotations
import json
import struct
from typing import Any, Sequence
import numpy as np
import numpy.typing as npt
[docs]
class GlTF:
HEADER_LENGTH = 12
CHUNK_HEADER_LENGTH = 8
def __init__(self) -> None:
self.header = {}
self.body = None
[docs]
def to_array(self) -> npt.NDArray[np.uint8]: # glb
scene = json.dumps(self.header, separators=(",", ":"))
# body must start with 4-byte boundary
scene += " " * ((4 - len(scene) % 4) % 4)
length = GlTF.HEADER_LENGTH + (2 * GlTF.CHUNK_HEADER_LENGTH)
length += len(self.body) + len(scene)
# gltf must end with an 8-byte boundary
# the padding is added inside the gltf body
# https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel#padding
body_padding = np.zeros((8 - length % 8) % 8, dtype=np.uint8)
length += len(body_padding)
binary_header = np.array(
[0x46546C67, 2, length], dtype=np.uint32 # "glTF" magic # version
)
json_chunk_header = np.array(
[len(scene), 0x4E4F534A], dtype=np.uint32 # JSON chunck length
) # "JSON"
bin_chunk_header = np.array(
[
len(self.body) + len(body_padding),
# BIN chunck length
0x004E4942,
],
dtype=np.uint32,
) # "BIN"
return np.concatenate(
(
binary_header.view(np.uint8),
json_chunk_header.view(np.uint8),
np.frombuffer(scene.encode("utf-8"), dtype=np.uint8),
bin_chunk_header.view(np.uint8),
self.body,
body_padding,
)
)
[docs]
@staticmethod
def from_array(array: npt.NDArray[np.uint8]) -> GlTF:
gltf = GlTF()
if struct.unpack("4s", array[0:4])[0] != b"glTF":
raise RuntimeError("Array does not contain a binary glTF")
version = struct.unpack("i", array[4:8])[0]
if version != 1 and version != 2:
raise RuntimeError("Unsupported glTF version")
length = struct.unpack("i", array[8:12])[0]
json_chunk_length = struct.unpack("i", array[12:16])[0]
chunk_type = struct.unpack("i", array[16:20])[0]
if chunk_type != 0 and chunk_type != 1313821514: # 1313821514 => 'JSON'
raise RuntimeError("Unsupported binary glTF content type")
index = (
GlTF.HEADER_LENGTH + GlTF.CHUNK_HEADER_LENGTH
) # Skip the header and the JSON chunk header
header = struct.unpack(
str(json_chunk_length) + "s", array[index : index + json_chunk_length]
)[0]
gltf.header = json.loads(header.decode("ascii"))
index += (
json_chunk_length + GlTF.CHUNK_HEADER_LENGTH
) # Skip the JSON chunk data and the binary chunk header
gltf.body = array[index:length]
return gltf
[docs]
@staticmethod
def from_binary_arrays(
arrays: list[dict[str, Sequence[Any]]],
transform: npt.NDArray[np.float64],
batched: bool = True,
uri: str | None = None,
texture_uri: str | None = None,
) -> GlTF:
"""
Parameters
----------
arrays : array of dictionaries
Each dictionary has the data for one geometry
arrays['position']: binary array of vertex positions
arrays['normal']: binary array of vertex normals
arrays['uv']: binary array of vertex texture coordinates
(Not implemented yet)
arrays['bbox']: geometry bounding box (numpy.array)
transform : numpy.array
World coordinates transformation flattend matrix
Returns
-------
gltf : GlTF
"""
gltf = GlTF()
textured = "uv" in arrays[0]
bin_vertices = []
bin_normals = []
bin_ids = []
bin_uvs = []
n_vertices = []
bb = []
batch_length = 0
for i, geometry in enumerate(arrays):
bin_vertices.append(geometry["position"])
bin_normals.append(geometry["normal"])
n = round(len(geometry["position"]) / 12)
n_vertices.append(n)
bb.append(geometry["bbox"])
if batched:
bin_ids.append(np.full(n, i, dtype=np.float32))
if textured:
bin_uvs.append(geometry["uv"])
if batched:
bin_vertices = [b"".join(bin_vertices)]
bin_normals = [b"".join(bin_normals)]
bin_uvs = [b"".join(bin_uvs)]
bin_ids = [b"".join(bin_ids)]
n_vertices = [sum(n_vertices)]
batch_length = len(arrays)
[minx, miny, minz] = bb[0][0]
[maxx, maxy, maxz] = bb[0][1]
for box in bb[1:]:
minx = min(minx, box[0][0])
miny = min(miny, box[0][1])
minz = min(minz, box[0][2])
maxx = max(maxx, box[1][0])
maxy = max(maxy, box[1][1])
maxz = max(maxz, box[1][2])
bb = [[[minx, miny, minz], [maxx, maxy, maxz]]]
gltf.header = compute_header(
bin_vertices,
n_vertices,
bb,
transform,
textured,
batched,
batch_length,
uri,
texture_uri,
)
gltf.body = np.frombuffer(
compute_binary(bin_vertices, bin_normals, bin_ids, bin_uvs), dtype=np.uint8
)
return gltf
[docs]
def compute_binary(bin_vertices, bin_normals, bin_ids, bin_uvs):
bv = b"".join(bin_vertices)
bn = b"".join(bin_normals)
bid = b"".join(bin_ids)
buv = b"".join(bin_uvs)
return bv + bn + buv + bid
[docs]
def compute_header(
bin_vertices,
n_vertices,
bb,
transform,
textured,
batched,
batch_length,
uri,
texture_uri,
):
# Buffer
mesh_nb = len(bin_vertices)
size_vce = []
for i in range(mesh_nb):
size_vce.append(len(bin_vertices[i]))
byte_length = 2 * sum(size_vce)
if textured:
byte_length += int(round(2 * sum(size_vce) / 3))
if batched:
byte_length += int(round(sum(size_vce) / 3))
buffers = [{"byteLength": byte_length}]
if uri is not None:
buffers.append({"binary_glTF": {"uri": uri}})
# Buffer view
buffer_views = [
{"buffer": 0, "byteLength": sum(size_vce), "byteOffset": 0, "target": 34962},
{
"buffer": 0,
"byteLength": sum(size_vce),
"byteOffset": sum(size_vce),
"target": 34962,
},
]
# vertices
if textured:
buffer_views.append(
{
"buffer": 0,
"byteLength": int(round(2 * sum(size_vce) / 3)),
"byteOffset": 2 * sum(size_vce),
"target": 34962,
}
)
if batched:
buffer_views.append(
{
"buffer": 0,
"byteLength": int(round(sum(size_vce) / 3)),
"byteOffset": int(round(8 / 3 * sum(size_vce)))
if textured
else 2 * sum(size_vce),
"target": 34962,
}
)
# Accessor
accessors = []
for i in range(mesh_nb):
# vertices
accessors.append(
{
"bufferView": 0,
"byteOffset": sum(size_vce[0:i]),
"componentType": 5126,
"count": n_vertices[i],
"min": [bb[i][0][0], bb[i][0][1], bb[i][0][2]],
"max": [bb[i][1][0], bb[i][1][1], bb[i][1][2]],
"type": "VEC3",
}
)
# normals
accessors.append(
{
"bufferView": 1,
"byteOffset": sum(size_vce[0:i]),
"componentType": 5126,
"count": n_vertices[i],
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3",
}
)
if textured:
accessors.append(
{
"bufferView": 2,
"byteOffset": int(round(2 / 3 * sum(size_vce[0:i]))),
"componentType": 5126,
"count": sum(n_vertices),
"max": [1, 1],
"min": [0, 0],
"type": "VEC2",
}
)
if batched:
accessors.append(
{
"bufferView": 3 if textured else 2,
"byteOffset": 0,
"componentType": 5126,
"count": n_vertices[0],
"max": [batch_length],
"min": [0],
"type": "SCALAR",
}
)
# Meshes
meshes = []
n_attributes = 3 if textured else 2
for i in range(mesh_nb):
meshes.append(
{
"primitives": [
{
"attributes": {
"POSITION": n_attributes * i,
"NORMAL": n_attributes * i + 1,
},
"material": 0,
"mode": 4,
}
]
}
)
if textured:
meshes[i]["primitives"][0]["attributes"]["TEXCOORD_0"] = (
n_attributes * i + 2
)
if batched:
meshes[0]["primitives"][0]["attributes"]["_BATCHID"] = n_attributes
# Nodes
nodes = []
for i in range(mesh_nb):
nodes.append({"matrix": [float(e) for e in transform], "mesh": i})
# Materials
materials = [
{
"pbrMetallicRoughness": {"metallicFactor": 0},
"name": "Material",
}
]
# Final glTF
header = {
"asset": {"generator": "py3dtiles", "version": "2.0"},
"scene": 0,
"scenes": [{"nodes": list(range(len(nodes)))}],
"nodes": nodes,
"meshes": meshes,
"materials": materials,
"accessors": accessors,
"bufferViews": buffer_views,
"buffers": buffers,
}
# Texture data
if textured:
header["textures"] = [{"sampler": 0, "source": 0}]
header["images"] = [{"uri": texture_uri}]
header["samplers"] = [
{"magFilter": 9729, "minFilter": 9987, "wrapS": 10497, "wrapT": 10497}
]
header["materials"][0]["pbrMetallicRoughness"]["baseColorTexture"] = {
"index": 0
}
return header