Skip to content

Instantly share code, notes, and snippets.

@wwwtyro
Last active November 7, 2025 16:22
Show Gist options
  • Select an option

  • Save wwwtyro/62b3d03c25602bca15dd2bf1967416fd to your computer and use it in GitHub Desktop.

Select an option

Save wwwtyro/62b3d03c25602bca15dd2bf1967416fd to your computer and use it in GitHub Desktop.
// MIT License
//
// Copyright 2025 Rye Terrell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { type TypedArray, WebIO } from "@gltf-transform/core";
import { vec3 } from "gl-matrix";
export interface Mesh {
positions: TypedArray;
normals: TypedArray | null | undefined;
colors: TypedArray;
count: number;
baseColorImage?: HTMLImageElement;
uvs?: TypedArray;
indices?: TypedArray;
}
export async function loadGLB(url: string) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const io = new WebIO({ credentials: "include" });
const doc = await io.readBinary(new Uint8Array(arrayBuffer));
const primitives: Mesh[] = [];
const imageCache = new Map<Uint8Array, HTMLImageElement>();
const meshes = doc.getRoot().listMeshes();
for (const mesh of meshes) {
for (const primitive of mesh.listPrimitives()) {
const positionsAttribute = primitive.getAttribute("POSITION");
if (!positionsAttribute) {
throw new Error(`Positions missing in asset ${url}`);
}
const positions = positionsAttribute.getArray();
if (positions === null) {
throw new Error(`Positions null in asset ${url}`);
}
const vertexCount = positionsAttribute.getCount();
const color0 = primitive.getAttribute("COLOR_0")?.getArray() ?? undefined;
const uvs = primitive.getAttribute("TEXCOORD_0")?.getArray() ?? undefined;
const indices = primitive.getIndices()?.getArray() ?? undefined;
const normals = primitive.getAttribute("NORMAL")?.getArray() ?? generateFlatNormals(positions, indices);
const material = primitive.getMaterial();
if (!material) {
throw new Error(`Material missing in asset ${url}`);
}
const baseColor = material.getBaseColorFactor();
const colors = color0 ?? new Float32Array(vertexCount * 3);
if (!color0) {
for (let i = 0; i < vertexCount; i++) {
colors[i * 3 + 0] = baseColor[0];
colors[i * 3 + 1] = baseColor[1];
colors[i * 3 + 2] = baseColor[2];
}
}
const texture = material.getBaseColorTexture();
let baseColorImage: HTMLImageElement | undefined;
if (texture) {
const image = texture.getImage();
if (image !== null) {
if (!imageCache.has(image)) {
const element = await createImageElementFromBuffer(image, texture.getMimeType());
imageCache.set(image, element);
}
baseColorImage = imageCache.get(image);
if (baseColorImage === undefined) {
throw new Error(`baseColorImage undefined for asset ${url}`);
}
}
}
primitives.push({
positions,
normals,
colors,
uvs,
count: indices ? indices.length : vertexCount,
baseColorImage,
indices,
});
}
}
return primitives;
}
function createImageElementFromBuffer(array: Uint8Array, mimeType: string) {
return new Promise<HTMLImageElement>((resolve) => {
const blob = new Blob([array as BlobPart], { type: mimeType });
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
resolve(img);
};
img.src = url;
});
}
function generateFlatNormals(positions: TypedArray, indices: TypedArray | undefined): Float32Array {
const normals = new Float32Array(positions.length);
if (indices === undefined) {
indices = generateSimpleIndices(positions.length / 3);
}
for (let i = 0; i < indices.length; i += 3) {
const i0 = indices[i] * 3;
const i1 = indices[i + 1] * 3;
const i2 = indices[i + 2] * 3;
const v0 = vec3.fromValues(positions[i0], positions[i0 + 1], positions[i0 + 2]);
const v1 = vec3.fromValues(positions[i1], positions[i1 + 1], positions[i1 + 2]);
const v2 = vec3.fromValues(positions[i2], positions[i2 + 1], positions[i2 + 2]);
const edge1 = vec3.subtract(vec3.create(), v1, v0);
const edge2 = vec3.subtract(vec3.create(), v2, v0);
const normal = vec3.cross(vec3.create(), edge1, edge2);
vec3.normalize(normal, normal);
normals.set(normal, i0);
}
return normals;
}
function generateSimpleIndices(count: number): TypedArray {
const indices = new Uint32Array(count);
for (let i = 0; i < count; i++) {
indices[i] = i;
}
return indices;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment