Skip to content

Vertex Layouts

Vertex layouts are much like bind group layouts, in that they define the relationship between shaders and buffers. More precisely, they define what vertex attributes a shader expects, and how they are laid out in the corresponding vertex buffer.

Creating a vertex layout

To create a vertex layout, use the tgpu.vertexLayout function. It takes an array schema constructor, i.e., a function that returns an array schema given the number of elements to render (vertices/instances). To determine what each element of the array corresponds to, you can pass an optional stepMode argument, which can be either vertex (default) or instance.

import tgpu from 'typegpu';
import * as d from 'typegpu/data';
const ParticleGeometry = d.struct({
tilt: d.f32,
angle: d.f32,
color: d.vec4f,
});
const geometryLayout = tgpu
.vertexLayout((n: number) => d.arrayOf(ParticleGeometry, n), 'instance');

Utilizing loose schemas with vertex layouts

If the vertex buffer is not required to function as a storage or uniform buffer, a loose schema may be used to define the vertex data layout. Loose schemas are not subject to alignment restrictions and allow the use of various vertex formats.

To define a loose schema:

  • Use d.unstruct instead of d.struct.
  • Use d.disarrayOf instead of d.arrayOf.

Within a loose schema, both standard data types and vertex formats can be utilized.

const LooseParticleGeometry = d.unstruct({
tilt: d.f32,
angle: d.f32,
// four 8-bit values, unsigned & normalized
// i.e., four integers in (0, 255) represent four floats in the range of (0.0, 1.0)
color: d.unorm8x4,
});

The size of LooseParticleGeometry will be 12 bytes, compared to 32 bytes of ParticleGeometry. This can be useful when you’re working with large amounts of vertex data and want to save memory.

Using vertex layouts

You can utilize root.unwrap to get the raw GPUVertexBufferLayout from a typed vertex layout. It will automatically calculate the stride and attributes for you, according to the vertex layout you provided.

const ParticleGeometry = d.struct({
tilt: d.location(0, d.f32),
angle: d.location(1, d.f32),
color: d.location(2, d.vec4f),
});
const geometryLayout = tgpu
.vertexLayout((n: number) => d.arrayOf(ParticleGeometry, n), 'instance');
const geometry = root.unwrap(geometryLayout);
console.log(geometry);
//{
// "arrayStride": 32,
// "stepMode": "instance",
// "attributes": [
// {
// "format": "float32",
// "offset": 0,
// "shaderLocation": 0
// },
// {
// "format": "float32",
// "offset": 4,
// "shaderLocation": 1
// },
// {
// "format": "float32x4",
// "offset": 16,
// "shaderLocation": 2
// }
// ]
//}

This will return a GPUVertexBufferLayout that can be used when creating a render pipeline.

const renderPipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [root.unwrap(bindGroupLayout)],
}),
primitive: {
topology: 'triangle-strip',
},
vertex: {
module: renderShader,
buffers: [
{
arrayStride: 32,
stepMode: 'instance',
attributes: [
{
format: 'float32',
offset: 0,
shaderLocation: 0,
},
{
format: 'float32',
offset: 4,
shaderLocation: 1,
},
{
format: 'float32x4',
offset: 16,
shaderLocation: 2,
},
],
},
],
buffers: [root.unwrap(geometryLayout)],
},
fragment: {
...
},
});

Loose schemas can be interpreted in multiple ways within a shader. However, for convenience, they can be resolved to their default WGSL representation.

const LooseParticleGeometry = d.unstruct({
tilt: d.location(0, d.f32),
angle: d.location(1, d.f32),
color: d.location(2, d.unorm8x4),
});
const sampleShader = `
@vertex
fn main(particleGeometry: LooseParticleGeometry) -> @builtin(position) pos: vec4f {
return vec4f(
particleGeometry.tilt,
particleGeometry.angle,
particleGeometry.color.rgb,
1.0
);
}
`;
const wgslDefinition = tgpu.resolve({
template: sampleShader,
externals: { LooseParticleGeometry }
});
console.log(wgslDefinition);
// struct LooseParticleGeometry_0 {
// @location(0) tilt: f32,
// @location(1) angle: f32,
// @location(2) color: vec4f,
// }
//
// @vertex
// fn main(particleGeometry: LooseParticleGeometry_0) -> @builtin(position) pos: vec4f {
// return vec4f(
// particleGeometry.tilt,
// particleGeometry.angle,
// particleGeometry.color.rgb,
// 1.0
// );
// }