Understanding Two WebGL GPU Data Strategies used in xeokit
xeokit.io
xeokit Meetup #2, Nov 18, 2025
Lindsay Kay, Software Engineer @ Creoox AG
WebGL is a browser API for GPU-accelerated graphics.
• based on OpenGL ES
• runs inside an HTML canvas
• renders 2D and 3D graphics
• used by xeokit
xeokit.io
High-level flow:
WebGL is low-level: you manage most steps manually.
xeokit.io
CPU responsibilities:
• prepare buffers
• configure shaders
• manage state
• call draw commands
GPU responsibilities:
• run vertex + fragment shaders
• rasterize triangles
• write pixels to the screen
xeokit.io
Vertex Buffer Objects
xeokit.io
VBO = GPU array containing geometry data.
[x y z] [x y z] [x y z]
vertex vertex vertex
Common attributes:
• positions
• normals
• UVs
• colors
Uploaded once, reused many times.
xeokit.io
Basic usage:
gl.bindBuffer(ARRAY_BUFFER, vbo)
gl.bufferData(...)
gl.vertexAttribPointer(...)
gl.enableVertexAttribArray(...)
Vertex shader receives data via attributes.
Fast and ideal for traditional geometry.
xeokit.io
A More Flexible Data Source
xeokit.io
A data texture is a float/uint texture storing structured information.
Examples of what they can store:
• vertex positions
• colors
• transforms
• materials
• lookup tables
• indexing data
Textures can be randomly indexed in shaders, unlike VBOs which can only be accessed one vertex at a time.
xeokit.io
Flow:
This enables GPU-side indirection.
xeokit.io
The vertex shader might do:
pos = texelFetch(positionTex, vertexIndex)
This replaces attribute fetches from VBOs.
xeokit.io
• arbitrary attributes beyond VBO limits
• large or heterogeneous datasets
• GPU-driven rendering
• pointer-style indirection
• aggressive resource reuse
• BIM/IFC model rendering
• instancing with high data variety
They trade speed for flexibility.
xeokit.io
Key Differences
xeokit.io
• fastest vertex access
• simple API
• limited attribute count
• fixed layout
• ideal for conventional meshes
xeokit.io
• slower than VBO access
• unlimited attribute count
• random indexing
• pack many data types together
• great for complex pipelines
xeokit.io
Understanding how xeokit formats its data textures
xeokit.io
• Minimize GPU memory
• Avoid duplicated geometry
• Allow GPU-side indirection
• Enable random access into shared arrays
The format is designed for large BIM/IFC models.
xeokit.io
xeokit stores all raw geometry in large, shared arrays:
• positions
• normals
• colors
• indices
These arrays are uploaded into data textures.
positionTex: [x y z x y z ...]
normalTex: [nx ny nz ...]
colorTex: [r g b ...]
indexTex: [i0 i1 i2 ...]
Everything is global, not per-mesh.
All values are quantized to unsigned integers.
xeokit.io
• Only store each vertex once
• Enable reuse of identical shapes
• Reduce fragmentation
Meshes do not own memory.
They refer into shared memory.
xeokit.io
Each draw primitive corresponds to one mesh instance.
xeokit stores a table:
primitiveIndex → meshIndex
This table is a data texture.
The shader reads this to determine what to draw for each primitive.
xeokit.io
Each mesh index points into ranges in the shared global arrays:
meshIndex → {
vertexOffset
vertexCount
indexOffset
indexCount
colorOffset
pickColor
}
This is a second lookup texture.
xeokit.io
// Each vertex belongs to a triangle → 3 verts per primitive
int primitiveIndex = gl_VertexID / 3;
// Primitive → Mesh lookup (stored in a data texture)
int meshIndex = texelFetch(primitiveToMeshTex, primitiveIndex).r;
// Mesh → Vertex range lookup (also stored in a data texture)
Range range;
range.start = texelFetch(meshVertexRangesTex, meshIndex).r;
range.count = texelFetch(meshVertexRangesTex, meshIndex).g;
// Compute vertex index within packed attributes
int vertexIndex = range.start + (gl_VertexID % 3);
// Fetch packed attributes from data textures
vec3 position = texelFetch(positionsTex, vertexIndex).xyz;
vec3 normal = texelFetch(normalsTex, vertexIndex).xyz;
vec4 color = texelFetch(colorsTex, vertexIndex);
// Standard transform
gl_Position = projMat * viewMat * modelMat * vec4(position, 1.0);
Reading geometry from data textures in the vertex shader:
xeokit.io
Vertex_ID (WebGL built-in vertex index generated by draw call)
│
▼
primitiveId
│
▼
meshIndex (lookup texture)
│
▼
meshRanges (lookup texture)
│
▼
positionTex / normalTex / colorTex / indexTex
│
▼
gl_Position (WebGL built-in output)
All resolved inside the vertex shader.
xeokit.io
More abstractly:
meshIndex = texelFetch(meshTable, primitiveId)
ranges = texelFetch(rangeTable, meshIndex)
pos = texelFetch(positionTex, ranges.vertexOffset + vertexId)
color = texelFetch(colorTex, ranges.colorOffset)
Indices and normals resolve the same way.
xeokit.io
If two meshes share identical geometry:
• They share vertex ranges
• They share color ranges
• Only transforms differ
No duplicated vertices or indices.
xeokit.io
• Only a few textures allocated
• Shared data reused across many objects
• Mesh definitions cost only a few bytes
• Very low overhead per IFC element
The entire scene is stored compactly.
xeokit.io
• Large arrays uploaded once
• Lookup tables are small
• No thousands of buffer creations
• Faster load times for huge IFC models
Ideal for streaming.
xeokit.io
xeokit's draw loop is essentially:
gl.drawArrays( N )
All per-object state is fetched inside the shader via table lookups.
This batched rendering avoids expensive CPU state changes.
xeokit.io
The primitive->mesh lookup texture contains N / 3 items.
This structure gives xeokit a very small memory footprint and stable performance.
xeokit.io
xeokit.io
const viewer = new Viewer({
canvasId: "myCanvas",
dtxEnabled: true
});
const xktLoader = new XKTLoaderPlugin(viewer);
const sceneModel = xktLoader.load({
id: "myModel",
dtxEnabled: true,
src: "myModel.xkt"
});Text
Demo:
xeokit.io
This slide deck
xeokit Blog
Demo:
Original experiments by Toni Marti @ Tribia