Skip to content

Instantly share code, notes, and snippets.

@aniongithub
Last active November 24, 2025 01:29
Show Gist options
  • Select an option

  • Save aniongithub/961bfcc07e0d89db511be3df4131db64 to your computer and use it in GitHub Desktop.

Select an option

Save aniongithub/961bfcc07e0d89db511be3df4131db64 to your computer and use it in GitHub Desktop.

Revisions

  1. aniongithub revised this gist Sep 6, 2023. No changes.
  2. aniongithub revised this gist Sep 6, 2023. 1 changed file with 32 additions and 15 deletions.
    47 changes: 32 additions & 15 deletions load_model_from_gltf.ipynb
    Original file line number Diff line number Diff line change
    @@ -2,7 +2,7 @@
    "cells": [
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 15,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -11,16 +11,17 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 16,
    "metadata": {},
    "outputs": [],
    "source": [
    "# Download this file from https://paste.c-net.org/ScreechGrubby\n",
    "gltf = GLTF2.load(\"assets/X Bot.glb\")"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 17,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -83,7 +84,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 18,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -100,7 +101,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 19,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -110,7 +111,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 20,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -129,7 +130,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 21,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -146,7 +147,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 22,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -155,7 +156,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 23,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -170,7 +171,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 24,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -195,7 +196,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 25,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -206,7 +207,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 26,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -219,7 +220,7 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 27,
    "metadata": {},
    "outputs": [],
    "source": [
    @@ -231,9 +232,25 @@
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "execution_count": 28,
    "metadata": {},
    "outputs": [],
    "outputs": [
    {
    "data": {
    "application/vnd.jupyter.widget-view+json": {
    "model_id": "c8293ea23cf64061be2e4ae2b5b95b9a",
    "version_major": 2,
    "version_minor": 0
    },
    "text/plain": [
    "Renderer(camera=PerspectiveCamera(aspect=1.3333333333333333, position=(10.0, 6.0, 10.0), projectionMatrix=(1.0…"
    ]
    },
    "execution_count": 28,
    "metadata": {},
    "output_type": "execute_result"
    }
    ],
    "source": [
    "view_width = 800\n",
    "view_height = 600\n",
  3. aniongithub revised this gist Sep 6, 2023. No changes.
  4. aniongithub created this gist Sep 6, 2023.
    282 changes: 282 additions & 0 deletions load_model_from_gltf.ipynb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,282 @@
    {
    "cells": [
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "from pygltflib import GLTF2, Accessor, Skin, Node"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "gltf = GLTF2.load(\"assets/X Bot.glb\")"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "from enum import IntEnum\n",
    "import struct\n",
    "\n",
    "class GLTFComponentType(IntEnum):\n",
    " BYTE = 5120\n",
    " UNSIGNED_BYTE = 5121\n",
    " SHORT = 5122\n",
    " UNSIGNED_SHORT = 5123\n",
    " UNSIGNED_INT = 5125\n",
    " FLOAT = 5126\n",
    "\n",
    "GLTF_COMPONENTTYPE_SIZES = {\n",
    " GLTFComponentType.BYTE: 1,\n",
    " GLTFComponentType.UNSIGNED_BYTE: 1,\n",
    " GLTFComponentType.SHORT: 2,\n",
    " GLTFComponentType.UNSIGNED_SHORT: 2,\n",
    " GLTFComponentType.UNSIGNED_INT: 4,\n",
    " GLTFComponentType.FLOAT: 4\n",
    "}\n",
    "\n",
    "GLTF_ACCESSORTYPE_COUNTS = {\n",
    " \"SCALAR\": 1,\n",
    " \"VEC2\": 2,\n",
    " \"VEC3\": 3,\n",
    " \"VEC4\": 4,\n",
    " \"MAT2\": 4,\n",
    " \"MAT3\": 9,\n",
    " \"MAT4\": 16\n",
    "}\n",
    "\n",
    "GLTF_COMPONENT_UNPACK_FORMATS = {\n",
    " GLTFComponentType.BYTE: \"b\",\n",
    " GLTFComponentType.UNSIGNED_BYTE: \"B\",\n",
    " GLTFComponentType.SHORT: \"h\",\n",
    " GLTFComponentType.UNSIGNED_SHORT: \"H\",\n",
    " GLTFComponentType.UNSIGNED_INT: \"I\",\n",
    " GLTFComponentType.FLOAT: \"f\"\n",
    "}\n",
    "\n",
    "def get_dense_data(gltf: GLTF2, accessor: Accessor):\n",
    " bufferView = gltf.bufferViews[accessor.bufferView]\n",
    " buffer = gltf.buffers[bufferView.buffer]\n",
    " buffer_data = gltf.get_data_from_buffer_uri(buffer.uri)\n",
    " result = []\n",
    " elem_stride = GLTF_ACCESSORTYPE_COUNTS[accessor.type] * GLTF_COMPONENTTYPE_SIZES[int(accessor.componentType)]\n",
    " for i in range(accessor.count):\n",
    " index = bufferView.byteOffset + accessor.byteOffset + i * elem_stride\n",
    " base64_elem_data = buffer_data[index:index + elem_stride]\n",
    " elem_data = struct.unpack(f\"<{GLTF_COMPONENT_UNPACK_FORMATS[accessor.componentType] * GLTF_ACCESSORTYPE_COUNTS[accessor.type]}\", base64_elem_data)\n",
    " if len(elem_data) == 1:\n",
    " result.append(elem_data[0])\n",
    " else:\n",
    " result.append(elem_data)\n",
    " \n",
    " return result"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "primitive = gltf.meshes[gltf.scenes[gltf.scene].nodes[0]].primitives[0]\n",
    "\n",
    "indices = get_dense_data(gltf, gltf.accessors[primitive.indices])\n",
    "positions = get_dense_data(gltf, gltf.accessors[primitive.attributes.POSITION])\n",
    "normals = get_dense_data(gltf, gltf.accessors[primitive.attributes.NORMAL])\n",
    "texcoords = get_dense_data(gltf, gltf.accessors[primitive.attributes.TEXCOORD_0])\n",
    "joints = get_dense_data(gltf, gltf.accessors[primitive.attributes.JOINTS_0])\n",
    "joint_weights = get_dense_data(gltf, gltf.accessors[primitive.attributes.WEIGHTS_0])\n",
    "inverse_bind_matrices = get_dense_data(gltf, gltf.accessors[gltf.skins[0].inverseBindMatrices])\n"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "from pythreejs import *\n",
    "from IPython.display import display"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "def rgba_to_html_color(rgba_tuple):\n",
    " # Convert each RGBA value to its corresponding 8-bit integer representation\n",
    " r = int(rgba_tuple[0] * 255)\n",
    " g = int(rgba_tuple[1] * 255)\n",
    " b = int(rgba_tuple[2] * 255)\n",
    " a = int(rgba_tuple[3] * 255)\n",
    "\n",
    " # Format the values as a hexadecimal color string\n",
    " color_string = \"#{:02X}{:02X}{:02X}{:02X}\".format(r, g, b, a)\n",
    "\n",
    " return color_string"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "def create_material(material: Material):\n",
    " result = MeshPhongMaterial(\n",
    " color = rgba_to_html_color(material.pbrMetallicRoughness.baseColorFactor),\n",
    " metallicFactor = material.pbrMetallicRoughness.metallicFactor,\n",
    " roughnessFactor = material.pbrMetallicRoughness.roughnessFactor,\n",
    " emissiveFactor = material.emissiveFactor,\n",
    " skinning = True)\n",
    "\n",
    " return result"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "mesh_material = create_material(gltf.materials[0])"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "mesh_geometry = BufferGeometry()\n",
    "mesh_geometry.attributes[\"position\"] = BufferAttribute(positions)\n",
    "mesh_geometry.attributes[\"index\"] = BufferAttribute(indices)\n",
    "mesh_geometry.attributes[\"uv\"] = BufferAttribute(texcoords)\n",
    "mesh_geometry.attributes[\"normal\"] = BufferAttribute(normals)\n",
    "mesh_geometry.attributes[\"skinIndex\"] = BufferAttribute(joints)\n",
    "mesh_geometry.attributes[\"skinWeight\"] = BufferAttribute(joint_weights)"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "from typing import List\n",
    "\n",
    "def create_skeleton(gltf: GLTF2, skin: Skin, bones: List[Bone]):\n",
    " def create_bone(gltf: GLTF2, node: Node):\n",
    " bone = Bone(name = node.name, \n",
    " position = node.translation, \n",
    " scale = node.scale, \n",
    " quaternion = node.rotation)\n",
    " if bones is not None:\n",
    " bones.insert(0, bone)\n",
    " for child_node in node.children:\n",
    " child_bone = create_bone(gltf, gltf.nodes[child_node])\n",
    " bone.add(child_bone)\n",
    "\n",
    " return bone\n",
    "\n",
    " return create_bone(gltf, gltf.nodes[skin.skeleton])"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "bones = []\n",
    "root_bone = create_skeleton(gltf, gltf.skins[0], bones)\n",
    "skeleton = Skeleton(bones = bones, boneInverses = inverse_bind_matrices)"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "mesh = SkinnedMesh(mesh_geometry, mesh_material)\n",
    "mesh.add(root_bone)\n",
    "mesh.skeleton = skeleton\n",
    "\n",
    "helper = SkeletonHelper(mesh)"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "sphere = Mesh(\n",
    " SphereBufferGeometry(1, 32, 16),\n",
    " MeshStandardMaterial(color='red')\n",
    ")"
    ]
    },
    {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
    "view_width = 800\n",
    "view_height = 600\n",
    "\n",
    "key_light = DirectionalLight(position=[0, 10, 10], intensity=0.6)\n",
    "ambient_light = AmbientLight()\n",
    "\n",
    "camera = PerspectiveCamera(position=[10, 6, 10], aspect=view_width/view_height)\n",
    "\n",
    "# This line doesn't work, draws a small white rectangle\n",
    "scene_elems = [camera, key_light, ambient_light, mesh, helper]\n",
    "\n",
    "# This line works, draws a red sphere\n",
    "# scene_elems = [camera, key_light, ambient_light, sphere]\n",
    "\n",
    "scene = Scene(children = scene_elems)\n",
    "renderer = Renderer(camera=camera, scene=scene,\n",
    " controls=[OrbitControls(controlling=camera)],\n",
    " width=view_width, height=view_height)\n",
    "renderer"
    ]
    }
    ],
    "metadata": {
    "kernelspec": {
    "display_name": "base",
    "language": "python",
    "name": "python3"
    },
    "language_info": {
    "codemirror_mode": {
    "name": "ipython",
    "version": 3
    },
    "file_extension": ".py",
    "mimetype": "text/x-python",
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
    "version": "3.11.5"
    },
    "orig_nbformat": 4
    },
    "nbformat": 4,
    "nbformat_minor": 2
    }