I'm trying to create an API to describe an OpenGL Shader Pipeline in Zig. The goal of all this is to have type-safe functions for e.g. setting uniforms and automatically generating variable declarations based on the types.
My current (semi-working) implementation looks like this:
Edit: Actually, it's working, but there's still a lot of stuff done by manually calling the OpenGL functions (like setting uniforms). So basically only the code generation is being used right now.
/// For now it only supports vertex and fragment shaders.
/// At some point a generic Stage type could be created which would
/// contain the information about input and output. This allows
/// for creating arbitrary pipelines where Tesselation/Geometry shaders
/// can easily be added or removed.
pub const Pipeline = struct {
Vertex: type,
Uniforms: type,
Varyings: type,
/// These contain the source code of the shader
/// as written by the user
vertex_shader: []const u8,
fragment_shader: []const u8,
/// This function puts together the actual code that gets passed to glShaderSource from
/// the user code, Vertex-Type, Uniform-Type, etc. and does the OpenGL setup.
pub fn compile(comptime self: Pipeline, allocator: *Allocator) ShaderProgram
};
// As you can see, this is rather sparse right now, which is the reason
// so much stuff still has to be done manually.
pub const ShaderProgram = struct {
gl_id: u32, // OpenGL GLuint
};
The result of calling Pipeline.compile
can then be used to create a Mesh that is rendered with the compiled program.
pub const Mesh = struct {
pub fn create(program: ShaderProgram) Mesh
// Functions to draw the mesh or use it in some other way. You get the idea
pub fn draw() void
};
I'm not really experienced with OpenGL so it could very well be that I am trying to use it in a way it's not supposed to be used.
A small, non-working excerpt from my code :)
/// The Pipeline
const test_pipeline = Pipeline {
.Vertex = struct {
aPosition: Vec2,
aColor: Vec3,
},
.Uniforms = struct {
uProjection: Mat3,
uModel: Mat3,
uScale: Vec2
},
.Varyings = struct {
fColor: Vec3
},
vertex_shader =
\\void main()
\\ vec3 position = uProjection * uModel * vec3(aPosition * uScale, 1.0);
\\
\\ fColor = aColor;
\\
\\ gl_Position = vec4(position, 1.0)
\\}
,
.fragment_shader =
\\void main() {
\\ outColor = vec4(fColor, 1.0);
\\}
};
// Later, when creating the mesh
const program = test_pipeline.compile(std.debug.global_allocator);
const mesh = Mesh.create(program);
// Set uniforms (projection matrix, etc.)
// ...
// Render the mesh
mesh.draw();
Here's a gif of the running application, which is just a triangle that can be moved around using WASD.
The generated shader code looks like this:
#version 330
in vec2 aPosition;
in vec3 aColor;
uniform mat3 uProjection;
uniform mat3 uModel;
uniform vec2 uScale;
out vec3 fColor;
void main() {
vec3 position = uProjection * uModel * vec3(aPosition * uScale, 1.0);
fColor = aColor;
gl_Position = vec4(position, 1.0);
}
#version 330
in vec3 fColor;
uniform mat3 uProjection;
uniform mat3 uModel;
uniform vec2 uScale;
out vec4 outColor;
void main() {
outColor = vec4(fColor, 1.0);
}