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:
- No Member Functions: Structs cannot contain methods, member functions, or custom constructor logic.
- No Inheritance: Polymorphism, inheritance, and
access specifiers (such as
publicorprivate) do not exist in GLSL. - No Dynamic Memory: Structs cannot contain pointers, dynamic arrays, or references.
- Equality Comparisons: Structs of the same type can
be compared using
==and!=if all component types support equality comparison, but component-wise arithmetic cannot be performed on entire structs simultaneously without custom functions.