How to approach custom render passes?

Godot Version

4.7

Question

I apologize as this one’s a bit of a doozy but I’ve been scratching my head for about a week now. For my project I’m looking to implement a screen space fluid effect as described here: https://developer.download.nvidia.com/presentations/2010/gdc/Direct3D_Effects.pdf. For this effect to work, I need a pair of dedicated framebuffers that contain the depth of the fluid particles and an additively blending thickness value (attached are examples of the sort of buffers I want to make). My question is how should I go about actually making these dedicated framebuffers and drawing to them? There are two approaches I’ve thought of but both feel really hacky and I’m really not sure how I should move forward.

Examples of what I’m trying to make:

SubViewports

I can have a camera in here that only draws the fluid particles and nothing else. I can render the particles using a custom material that writes the depth to the albedo, and then I can sample this output texture to later composite the final effect. Problem is I’d have to duplicate all the particle geometry with a different material in another SubViewport in order to do the additive blending for the thickness which feels super wasteful. This doesn’t really feel like two passes and more just like a workaround of carrying duplicate instances that are drawn differently. Another big problem with this is that it doesn’t play nice with other Subviewports I have for other effects (like a mirror) as the cameras drawing the fluid buffers can only be synced to one camera at a time (player cam or the mirror cam) meaning to actually get reflections I’d have to have duplicates of both the depth and thickness SubViewports for each possible render target which just seems like a nightmare of design and performance.

RenderingDevice API

I could explicitly bind my own framebuffers and build my own custom pipelines with the RenderingDevice API and draw the particles to the exact sort of F32 textures I need directly without the overhead of the SubViewports. The problem here is that I’m not entirely sure exactly how I should go about injecting these custom pipelines. Could I do this in a POST_OPAQUE compositor effect? I know the compositor is intended for full screenspace postprocesses but I’d be drawing geometry here, is that okay? The other big worry I have is that I’d essentially be skipping right over Godot’s normal rendering entirely for these meaning I’d be losing all the frustum and occlusion culling benefits and I’d have to manage that myself. Is there some kind of way I could get a list of like potentially visible instances from Godot before submitting my drawlist or would I have to reimplement these systems from scratch? I know what I’m trying to do is really fighting Godot’s typical rendering pipeline and might not be possible to do cleanly.

If there’s anybody that has ever tried doing something similar or knows of someone / a project that has tackled this sort of thing I’d really appreciate any pointers or ideas. I’m scared to dive into setting up a bunch of explicit Vulkan pipeline management just to find out after that what I’m doing is going to badly break something or invalidate some internal assumption the engine makes about when rendering code is run. If there are any rendering gurus or people who really know the renderer internals I’d really appreciate any help. Thanks!

Why not render in one pass into a hdr subviewport, each texture into a different color channel? Then depth composit with the rest of the scene.

Hey thanks a ton for the reply! If I were to go this route, I could pack the depth into r and g getting full precision for depth and a single 16 bit channel is plenty for the thickness. My question regarding this would be if this could be done with a next_pass material. I can’t do them at the exact same time because the depth values need to be written with depth testing enabled but the thickness just needs to blindly accumulate with an add blend mode. Could I set up my particle material to have the first pass write the depth into the r/g channels and then an additive pass to do the thickness in b with the testing disabled and the new blend mode? Again thanks a ton!

Each pass would output full rgba into the framebuffer. You can’t write only to specific channels.

You could do it in two passes like this: in the first pass render black color but keep the depth buffer functioning properly. In the second pass disable depth test/write, render additively into one channel, sample the depth texture and write its value into one or two other channels.

Ah I see, so have the first material just populate the depth texture and that’s it, and then the second reads from the hint_depth_texture to write all the depth and thickness values all at once? And the draw order shouldn’t matter at that point bc as long as they’re sampling the depth texture with the same SCREEN_UV it’s gonna be the same value right?

Actually now that I’m thinking about it, is that necessary? If the second pass is using add as the blend mode, can’t I just have the r/g channels be writing 0 and that wouldn’t be overwriting the first pass, just adding 0?

Could work. Try it out.


This is a really silly proxy for the real deal but yeah I think this can work! First pass is writing the “depth” (icon.svg) and the second is drawing (0, 0.2, 0) on all the fragments with add blending and you can see the green channel specifically stacking where they overlay. I’ll have to give this a more proper go when I’ve got more time but it looks like this will do it. Thank you so much!

One final question I have regarding this, will I be able to read the full 16 bit floats out of each color channel at full depth later or will the SubViewport crunch them down into 8 bits when I try to read from it?

If HDR 2D is enabled for the subviewport ist texture should be RGBA16F, so the depth precision you need will be preserved.