Generating glTF files programmatically
While it's far more common to create 3D models in artist-friendly software like Blender, there are times when we want to create a model in code. For example, when converting from one 3D file format to another, or when creating geometry with procedural generation.
This will be a short, technical post explaining how to create a glTF 3D model programmatically, using the glTF Transform (gltf-transform.dev) JavaScript library. The library weighs about 20kb minzipped, and is much smaller than a full 3D engine like three.js or babylon.js.
First, we're going to need some vertex data! glTF supports points, lines, and triangle meshes. The details here depend entirely on what you're trying to create, but let's start with a torus (a.k.a. “donut”). three.js has open-source implementations of many common 3D shapes, and I've copied the relevant section of THREE.TorusGeometry
below, removing dependencies on three.js itself.
// MIT License. Copyright © 2010-2023 three.js authors.
function createTorus(radius = 1, tube = 0.4, radialSegments = 12,tubularSegments = 48, arc = Math.PI * 2) {
const indicesArray = [];
const positionArray = [];
const uvArray = [];
const vertex = [0, 0, 0];
// generate positions and uvs
for (let j = 0; j <= radialSegments; j++) {
for (let i = 0; i <= tubularSegments; i++) {
const u = (i / tubularSegments) * arc;
const v = (j / radialSegments) * Math.PI * 2;
// position
vertex[0] = (radius + tube * Math.cos(v)) * Math.cos(u);
vertex[1] = (radius + tube * Math.cos(v)) * Math.sin(u);
vertex[2] = tube * Math.sin(v);
positionArray.push(vertex[0], vertex[1], vertex[2]);
// uv
uvArray.push(i / tubularSegments);
uvArray.push(j / radialSegments);
}
}
// generate indices
for (let j = 1; j <= radialSegments; j++) {
for (let i = 1; i <= tubularSegments; i++) {
// indices
const a = (tubularSegments + 1) * j + i - 1;
const b = (tubularSegments + 1) * (j - 1) + i - 1;
const c = (tubularSegments + 1) * (j - 1) + i;
const d = (tubularSegments + 1) * j + i;
// faces
indicesArray.push(a, b, d);
indicesArray.push(b, c, d);
}
}
return { indicesArray, positionArray, uvArray };
}
The createTorus()
function above provides arrays of vertex positions and UVs, and indices for triangles connecting those vertices. Replace all this with your own geometry, if you want.
Next we need to assemble the vertex data into a glTF 2.0 file. First we'll create a new glTF Transform document, and a buffer to store our data.
import { Document } from '@gltf-transform/core';
const document = new Document();
const buffer = document.createBuffer();
Next we'll take the vertex data we generated above, and use it to create a mesh.
const { indicesArray, positionArray, uvArray } = createTorus();
// indices and vertex attributes
const indices = document
.createAccessor()
.setArray(new Uint16Array(indicesArray))
.setType('SCALAR')
.setBuffer(buffer);
const position = document
.createAccessor()
.setArray(new Float32Array(positionArray))
.setType('VEC3')
.setBuffer(buffer);
const texcoord = document
.createAccessor()
.setArray(new Float32Array(texcoordArray))
.setType('VEC2')
.setBuffer(buffer);
// material
const material = document.createMaterial()
.setBaseColorHex(0xD96459)
.setRoughnessFactor(1)
.setMetallicFactor(0);
// primitive and mesh
const prim = document
.createPrimitive()
.setMaterial(material)
.setIndices(indices)
.setAttribute('POSITION', position)
.setAttribute('TEXCOORD_0', texcoord);
const mesh = document.createMesh('MyMesh')
.addPrimitive(prim);
While glTF can be used to store a mesh all by itself, it's far more common to place the mesh within a default scene. This ensures everything shows up as expected in various 3D viewers. We'll add the mesh to a node, and put that node in a scene. If we had multiple meshes, we might assign different position/rotation/scale to each node.
const node = document.createNode('MyNode')
.setMesh(mesh)
.setTranslation([0, 0, 0]);
const scene = document.createScene('MyScene')
.addChild(node);
Finally, we're ready to save the result as a new glTF file. I/O depends on the environment where we're running the code, so select the appropriate option below.
Node.js
import { NodeIO } from '@gltf-transform/core';
const io = new NodeIO();
await io.write('./torus.glb', document);
Deno
import { DenoIO } from '@gltf-transform/core';
const io = new DenoIO();
await io.write('./torus.glb', document);
Web
import { WebIO } from '@gltf-transform/core';
const io = new WebIO();
const bytes = await io.writeBinary(document); // → Uint8Array
That's it — you've got a shiny new glTF 2.0 file:
The glTF Transform library, as the name hints, can do a lot more than create new files. It's more often used for optimizing existing glTF files, and we could do the same here by adding Draco compression to our model. Consider that an exercise for the reader.
Thanks for reading, and please reach out with any questions!