Reasonable rendering abstraction for simple engine
What's a good way of thinking of abstraction in simple 3d renderers? To be clear, I'm not thinking abstraction of the api itself (e.g. supporting Vulkan + DX12), but mostly level higher than that? Assume I'm only targeting one API, e.g. vulkan or webgpu?
It feels like most examples are find either have no abstraction at all (basically single file hello triangle) or they are enormously complex 1000-file engines with render graphs and kitchen sinks. I'm aiming for a pretty simple engine to experiment with various techniques such as adding a pass for GI etc, without that in itself making everything else break.
I have made a renderer now which manages to draw a gltf scene with a simple forward renderer. But it has several pipelines/passes (a depth prepass, an AO compute pass, then a forward render pass, and now I want to add e.g. shadows, an ImGui pass and so on). But thus far I have only managed to implement all this in one big type with dozens of textures and samplers and constant buffers. There are only two methods: the initialization where every buffer and pipeline layout is created. Then the render method where everything is bound and executed once per frame. And it's a complete mess and adding one more pass is beginning to be harder and harder - already at 3 passes.
So presumably what one does here is some sort of abstraction. I know you can go all "render graph" and make it super abstract and flexible, but I also see people claim that's overkill in most scenarios. So what's a good middle ground for a "simple" engine which just has one renderer path with a fixed number of passes. Say I want to keep some sort of sanity with shadow rendering, UI rendering and so on somewhat separate. What are the main abstractions or functions you'd use (no need to be language specific here, I think this would work the same in Rust/C++/C#/TS...)? What are the terms usually given to these building blocks in order to make it understandable? Do you make a "pass" type which holds some resources (pipelines, buffers) needed to run that render pass? or is "pass" the wrong abstraction (seeing as renderpass is already a term in many of the apis)?
Are there any good OSS examples that are more complex than a hard coded hello triangle, but still short of things like a big game engine renderer or full on render graph? Something in the sort of 10-50 source file range?
22 comments · 100% upvoted · other discussions
You may want to start on a render pass type and a compositor class which is responsible for coordinating your render passes.
Try and see how "data driven" you can make your render passes - these will usually need to know about the frame buffer targets, input textures and buffers, and shaders. They may also take a callback to your engine which iterates over game objects to draw them.
The render pass ends up serving as a node in your render graph, and these nodes are typically linked by the textures they read or write to.