I've been writing a software 3D renderer for a few months. The API is designed after OpenGL ES2.0, for better or worse. It has traits for vertex shader and fragment shader. The entire fixed function pipeline works well, and performance is excellent for what it is. The trouble I ran into is when I tried making the type system generic to allow executing the fixed function pipeline with arbitrary shader implementations (which are different types, not known at compile time).
I wrote about the design on my blog; relevant code snippets are included.
It appeared simple enough; global state like a vector of shader programs, render target, depth buffer, viewport matrix, etc. The shader programs are different types, e.g. one that does Gouraud lighting, and another that does water simulation. So I tried a vector of trait objects. But the shader program trait has associated types. AFAICT, the associated types are concrete types, so I wasn't able to populate the vector dynamically.
The complexity was too high with the trait object design, so I scrapped it. I would love to keep the API simple. I really just want to share code and state within the fixed function pipeline across many shader programs. It's easy enough to create multiple instances of my base struct, but then I can't easily share internet state (render target, depth buffer, viewport matrix, ...) between them.
Here's some pseudo code to show the kind of API I wanted:
let SAR = 16.0 / 9.0; // Scene Aspect Ratio
let diffuse_texture = Texture::new( ... );
let uniform = gouraud_shader::Uniform::new(
cgmath::perspective(cgmath::Deg(60.0_f32), SAR, 1.0, 100.0),
cgmath::Vector3::new(-1.0, 1.0, 1.0).normalize(),
diffuse_texture,
);
let vertex_shader = gouraud_shader::Vertex::new();
let shader_program = gouraud_shader::ShaderProgram::new(uniform, vertex_shader);
let mut gl = SoftwareGL::new(shader_program);
loop {
// Input handling ...
gl.draw_arrays();
// Copy the render target to the physical display ...
// Wait for V-Sync ...
}
This works great. But now I want to swap shader_program
at runtime with a method like gl.use_program(program_index)
. I got it working trivially when shader_program
was a new instance of the same type. But using a different type like water_shader::ShaderProgram::new( ... )
has so far tripped me up.
The second attempt I tried was rebinding my gl
variable with different instances of SoftwareGL
. This only works if I recreate all of the internal state every time I rebind the variable.
The third attempt was to create a separate variable for two different SoftwareGL
instances. I was able to move state between them with a method that moves the struct fields, and then some fancy juggling with variable bindings. It ends up looking like this:
// Create gl1
let mut gouraud_shader_program = ...
let mut gl1 = SoftwareGL::new(gouraud_shader_program);
// Swap shaders :(
let (pixels, zbuffer, viewport, shader_tmp) = gl1.move_state();
gouraud_shader_program = shader_tmp; // Save shader program
// Create gl2
let mut water_shader_program = ...
let mut gl2 = SoftwareGL::new_from(pixels, zbuffer, viewport, water_shader_program);
loop {
// Swap shader programs
let (pixels, zbuffer, viewport, shader_tmp) = gl2.move_state();
water_shader_program = shader_tmp; // Save shader program
gl1 = SoftwareGL::new_from(pixels, zbuffer, viewport, gouraud_shader_program);
gl1.draw_arrays();
// Swap shader programs
let (pixels, zbuffer, viewport, shader_tmp) = gl1.move_state();
gouraud_shader_program = shader_tmp; // Save shader program
gl2 = SoftwareGL::new_from(pixels, zbuffer, viewport, water_shader_program);
gl2.draw_arrays();
}
This syntax is in need of some love. Any ideas?
BTW, I'm trying to keep up periodic updates on my blog for anyone interested. And why not, here's some #screenshotsaturday tax: