How Do Structs and Custom Types Work in GLSL?

In OpenGL Shading Language (GLSL), structures allow developers to group multiple variables of different types into a single user-defined composite data type. This article explores how to declare structs, instantiate them using constructors, pass them through shader functions and interface blocks, and manage their layout and performance constraints within the rendering pipeline.

Declaring Structs and Custom Types

A structure in GLSL is declared using the struct keyword, followed by the type name and a block defining member variables. Struct members can include basic scalar types (such as float, int, bool), vector and matrix types (such as vec3, mat4), fixed-size arrays, and other previously defined struct types.

struct Light {
    vec3 position;
    vec3 color;
    float intensity;
    float radius;
};

You can define a struct globally or inside a function, though global definitions are standard when sharing types across multiple shader functions. Struct declarations can also declare variable instances immediately after the closing brace:

struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
} primaryMaterial;

Instantiation and Constructors

GLSL automatically creates a constructor for every declared struct. The constructor takes the exact name of the struct and expects arguments for every field in the exact order they were declared.

// Constructing an instance of Light
Light mainLight = Light(
    vec3(0.0, 10.0, 5.0), // position
    vec3(1.0, 0.95, 0.8), // color
    2.5,                  // intensity
    50.0                  // radius
);

Accessing and modifying members uses the standard dot operator (.):

mainLight.intensity = 3.0;
vec3 lightDir = normalize(mainLight.position - fragPosition);

GLSL does not support default values inside the struct definition, so every field must be supplied when using the constructor.

Structs in Functions and Shading Logic

Structs can serve as parameter types and return types for user-defined functions. By default, structs are passed by value, but GLSL parameter qualifiers (in, out, inout) dictate how data flows into and out of functions.

vec3 calculateDirectLight(in Light light, in Material mat, in vec3 normal) {
    float nDotL = max(dot(normal, normalize(light.position)), 0.0);
    return mat.diffuse * light.color * (nDotL * light.intensity);
}

Passing large structs by value can impact register usage in GPU threads, so grouping only closely related attributes is best practice.

Using Structs in Uniforms and Interface Blocks

Custom types are frequently used to organize uniform inputs, keeping related state together:

uniform Material uMaterial;
uniform Light uLights[4];

When using structs inside Uniform Buffer Objects (UBOs) or Shader Storage Buffer Objects (SSBOs), memory layout rules (such as std140 or std430) govern byte offsets and padding:

layout(std140) uniform LightingBlock {
    Light directionalLight;
    Light pointLights[8];
    int activeLightCount;
};

In std140, struct members are aligned according to base alignment rules, and the entire struct is padded to a multiple of 16 bytes (the size of a vec4). Understanding these alignment rules ensures memory matches between the host CPU application and the GLSL shader.

Nesting and Limitations

GLSL supports nesting one struct inside another, enabling complex hierarchical data models:

struct Surface {
    vec3 worldPos;
    vec3 normal;
    Material material;
};

However, GLSL structs are strictly data containers and differ from object-oriented classes: