Trying to draw specific meshes to texture using RenderingDevice

Godot Version

4.3 dev6

Question

Hello! I am trying to create a post-processing effect to make an underwater effect that corresponds to the vertex displacement of my water with the camera frustum.

I come from Unity and back then, I managed to do this by rendering the water meshes, with a set shader, to a texture in a “Render Pass” and then using that texture to enable the underwater shader per-fragment.
(Unity Render Pass code for reference)

I am pretty familiar with Godot’s Compute Shader system, as I have managed to make my own Compositor Effects, but it seems the API for drawing geometry is completely different from that and everything else I know about “Render passes” in other engines.

So here are my two questions:

  1. Is it possible (or feasible) to draw pre-existing geometry (preferably using a custom shader) in a Rendering device?
  2. Is it possible to store the results in a texture for post-processing purposes?
1 Like

Have you had any luck with this since posting? I am looking for this exact information as well. :sweat_smile:

1 Like

I have managed to draw triangles to a Texture, though I’ve been unable to find any real good way of getting mesh data without making a GDExtension plugin.
I’m also not quite sure on how you could get the texture data and use it as a texture buffer for a post-processing effect.

If you’re using C# you can pretty much copy-paste this, if you’re using GDScript, it should be easy enough to translate it (you can find a GDScript equivalent here)

I’ve also included a static Extensions class that I made for CompositorEffects, “CompositorExtensions”.

using System;
using Godot;
using Godot.Collections;

[Tool]
[GlobalClass]
public partial class DrawCompositorEffect : CompositorEffect, ISerializationListener {
	private RenderingDevice? renderingDevice;


	[Export] private RDShaderFile? DrawShaderFile {
		get => _drawShaderFile;
		set {
			_drawShaderFile = value;

			if (drawShader.IsValid && renderPipeline.IsValid) {
				Destruct();
				RenderingServer.CallOnRenderThread(Callable.From(Construct));
			}
		}
	}
	private RDShaderFile? _drawShaderFile;
	private Rid drawShader;

	private Rid frameBuffer;
	private Rid frameBufferTexture;

	private Rid indexBuffer;
	private Rid indexArray;

	private Rid vertexBuffer;
	private Rid vertexArray;

	private Rid renderPipeline;



	public DrawCompositorEffect() : base() {
		EffectCallbackType = EffectCallbackTypeEnum.PostTransparent;
	}


	private void Construct() {
		renderingDevice = RenderingServer.CreateLocalRenderingDevice();
		if (renderingDevice is null) return;

		if (DrawShaderFile is null) return;
		drawShader = renderingDevice.ShaderCreateFromSpirV(DrawShaderFile.GetSpirV());

		if (! drawShader.IsValid) {
			GD.Print("Shader is Invalid");
			return;
		}


		RDTextureFormat tex_format = new() {
			TextureType = RenderingDevice.TextureType.Type2D,
			Height = 256,
			Width = 256,
			Format = RenderingDevice.DataFormat.R8G8B8A8Uint,
			UsageBits = RenderingDevice.TextureUsageBits.ColorAttachmentBit | RenderingDevice.TextureUsageBits.CanCopyFromBit
		};
		RDTextureView tex_view = new();

		frameBufferTexture = renderingDevice.TextureCreate(tex_format, tex_view);
		if (! frameBufferTexture.IsValid) {
			GD.Print("Frame Buffer Texture is Invalid");
			return;
		}


		Array<RDAttachmentFormat> attachments = [
			new RDAttachmentFormat() {
				Format = tex_format.Format,
				Samples = RenderingDevice.TextureSamples.Samples1,
				UsageFlags = (uint)tex_format.UsageBits
			}
		];
		long frameBufferFormat = renderingDevice.FramebufferFormatCreate(attachments);
		frameBuffer = renderingDevice.FramebufferCreate([frameBufferTexture], frameBufferFormat);
		if (! frameBuffer.IsValid) {
			GD.Print("Frame Buffer is Invalid");
			return;
		}




		Array<RDVertexAttribute> vertexAttributes = [
			new RDVertexAttribute() {
				Format = RenderingDevice.DataFormat.R32G32B32Sfloat,
				Location = 0,
				Stride = 4*3,
			}
		];

		long vertexFormat = renderingDevice.VertexFormatCreate(vertexAttributes);

		float[] points = [
			0,		0,		0,
			-0.25f,	0f,		0,
			-0.25f,	0.25f,	0,
			0,		0.25f,	0,
			0.25f,	0.25f,	0,
			0.25f,	0,		0,
		];
		vertexBuffer = renderingDevice.VertexBufferCreate(points);
		if (! vertexBuffer.IsValid) {
			GD.Print("Vertex Buffer is Invalid");
			return;
		}
		vertexArray = renderingDevice.VertexArrayCreate((uint)(points.Length / 3), vertexFormat, [vertexBuffer]);
		if (! vertexArray.IsValid) {
			GD.Print("Vertex Array is Invalid");
			return;
		}



		ushort[] indices = [
			0, 1, 2,
			0, 2, 3,
			0, 3, 4,
			0, 4, 5,
		];
		indexBuffer = renderingDevice.IndexBufferCreate(indices);
		if (! indexBuffer.IsValid) {
			GD.Print("Index Buffer is Invalid");
			return;
		}
		indexArray = renderingDevice.IndexArrayCreate(indexBuffer, 0, (uint)indices.Length);
		if (! indexArray.IsValid) {
			GD.Print("Index Array is Invalid");
			return;
		}


		RDPipelineColorBlendState blend = new();
		blend.Attachments.Add(new RDPipelineColorBlendStateAttachment());



		renderPipeline = renderingDevice.RenderPipelineCreate(
			drawShader,
			renderingDevice.FramebufferGetFormat(frameBuffer),
			vertexFormat,
			RenderingDevice.RenderPrimitive.Triangles,
			new RDPipelineRasterizationState(),
			new RDPipelineMultisampleState(),
			new RDPipelineDepthStencilState(),
			blend
		);
		if (! renderPipeline.IsValid) {
			GD.Print("Render Pipeline is Invalid");
			return;
		}
	}
	private void Destruct() {
		if (renderingDevice is null) return;

		if (frameBuffer.IsValid) {
			renderingDevice.FreeRid(frameBuffer);
		}
		if (frameBufferTexture.IsValid) {
			renderingDevice.FreeRid(frameBufferTexture);
		}

		if (indexBuffer.IsValid) {
			renderingDevice.FreeRid(indexBuffer);
		}
		if (indexArray.IsValid) {
			renderingDevice.FreeRid(indexArray);
		}

		if (vertexBuffer.IsValid) {
			renderingDevice.FreeRid(vertexBuffer);
		}
		if (vertexArray.IsValid) {
			renderingDevice.FreeRid(vertexArray);
		}


		if (renderPipeline.IsValid) {
			renderingDevice.FreeRid(renderPipeline);
		}
		if (drawShader.IsValid) {
			renderingDevice.FreeRid(drawShader);
		}
	}

	public override void _RenderCallback(int effectCallbackType, RenderData renderData) {
		base._RenderCallback(effectCallbackType, renderData);
		if (renderingDevice is null || DrawShaderFile is null) return;
		if (! drawShader.IsValid) {
			GD.Print("Shader not valid");
			return;
		}
		if (! renderPipeline.IsValid) {
			GD.Print("Render Pipeline not valid");
			return;
		}


		renderingDevice.DrawCommandBeginLabel("Test Label", new Color(1f, 1f, 1f));

		Color[] clear_colors = [new Color(0, 0, 0, 0)];

		long drawList = renderingDevice.DrawListBegin(frameBuffer, RenderingDevice.InitialAction.Clear, RenderingDevice.FinalAction.Store, RenderingDevice.InitialAction.Clear, RenderingDevice.FinalAction.Discard, clear_colors);
		renderingDevice.DrawListBindRenderPipeline(drawList, renderPipeline);
		renderingDevice.DrawListBindVertexArray(drawList, vertexArray);
		renderingDevice.DrawListBindIndexArray(drawList, indexArray);
		renderingDevice.DrawListDraw(drawList, true, 2);
		renderingDevice.DrawListEnd(RenderingDevice.BarrierMask.AllBarriers);
		byte[] td = renderingDevice.TextureGetData(frameBufferTexture, 0);

		renderingDevice.DrawCommandEndLabel();
	}

	public void OnBeforeSerialize() {
		Destruct();
	}

	public void OnAfterDeserialize() {
		RenderingServer.CallOnRenderThread(Callable.From(Construct));
	}
}


public static class CompositorExtensions {

	public static Rid IndexBufferCreate(this RenderingDevice renderingDevice, ushort[] indices) {
		byte[] byteIndices = new byte[indices.Length * 2];
		for (int i = 0; i < indices.Length; i++) {
			byte[] bytes = BitConverter.GetBytes(indices[i]);
			int j = i * 2;
			byteIndices[j] = bytes[0];
			byteIndices[j+1] = bytes[1];
		}
		GD.Print("Index Buffer: uint16");
		GD.PrintS([.. byteIndices]);

		return renderingDevice.IndexBufferCreate((uint)indices.Length, RenderingDevice.IndexBufferFormat.Uint16, byteIndices);
	}
	public static Rid IndexBufferCreate(this RenderingDevice renderingDevice, uint[] indices) {
		byte[] byteIndices = new byte[indices.Length * 4];
		for (int i = 0; i < indices.Length; i++) {
			byte[] bytes = BitConverter.GetBytes(indices[i]);
			int j = i * 4;
			byteIndices[j] = bytes[0];
			byteIndices[j+1] = bytes[1];
			byteIndices[j+2] = bytes[2];
			byteIndices[j+3] = bytes[3];
		}
		GD.Print("Index Buffer: uint32");
		GD.PrintS([.. byteIndices]);

		return renderingDevice.IndexBufferCreate((uint)indices.Length, RenderingDevice.IndexBufferFormat.Uint32, byteIndices);
	}

	public static Rid VertexBufferCreate(this RenderingDevice renderingDevice, float[] vertices) {
		if (vertices.Length % 3 != 0) throw new ArgumentException("Invalid number of values in the points buffer, there should be three float value per point.", nameof(vertices));
		byte[] byteVertices = new byte[vertices.Length * 4];
		for (int i = 0; i < vertices.Length; i++) {
			byte[] bytes = BitConverter.GetBytes(vertices[i]);

			int j = i * 4;
			byteVertices[j] = bytes[0];
			byteVertices[j+1] = bytes[1];
			byteVertices[j+2] = bytes[2];
			byteVertices[j+3] = bytes[3];
		}
		GD.Print("Vertex Buffer");
		GD.PrintS([.. byteVertices]);

		return renderingDevice.VertexBufferCreate((uint)byteVertices.Length, byteVertices);
	}
}
1 Like

This is very helpful. Thank you so much for sharing! :grin:

As for getting the texture data, unless I misunderstood your meaning, this was the resource I was following: Everything About Textures in Compute Shaders! - NekotoArts. I gave up attempting to recreate the entire rendering process in the compute shader, at least for now due to time constraints. (This article seems a good resource for it though: Beating The GPU At Its Own Game (By Stacking the Deck) – The Burning Basis Vector)

I am also exploring a way to potentially solve my problem using this branch of the engine: Allow canvas_item shaders to access the screenspace depth buffer by BMagnu · Pull Request #89196 · godotengine/godot · GitHub, but I am not sure if it is relevant for you.

Note that I am only a 4th year university student at the moment and spending the summer solely on learning Godot and GDExtension, so I might be missing a few concepts at present. I’m not looking for help by sharing the links above, just wanting to share in case it is helpful to you!

Have a great day!

1 Like

After a bit of experimentation, I’ve managed to do everything I wanted…
Except the “getting the mesh data” part, I expect that there’s no function to do that outside of the C++ side so I guess I’ll have to stick to my GDExtension idea for that.

Anyways, for now, here’s the working solution for anyone who stumbles upon this, it should be pretty simple to convert to GDScript, but if you can’t manage to do it, feel free to contact me:

Abstract Compositor Effect class to simplify cleaning up:

using Godot;

public abstract partial class BaseCompositorEffect : CompositorEffect, ISerializationListener {
	protected RenderingDevice? RenderingDevice { get; private set; }



	public BaseCompositorEffect() : base() {
		RenderingDevice ??= RenderingServer.GetRenderingDevice();
		Callable.From(Construct).CallDeferred();
	}

	public override void _Notification(int what) {
		base._Notification(what);
		switch ((ulong)what) {
		case NotificationPredelete:
			Destruct();
			break;
		}
	}


	protected void Construct() {
		if (RenderingDevice is null) return;

		RenderingServer.CallOnRenderThread(Callable.From(() => ConstructBehaviour(RenderingDevice)));
	}
	protected void Destruct() {
		if (RenderingDevice is null) return;

		DestructBehaviour(RenderingDevice);
	}

	protected abstract void ConstructBehaviour(RenderingDevice renderingDevice);
	protected abstract void DestructBehaviour(RenderingDevice renderingDevice);

	public void OnBeforeSerialize() {
		Destruct();
	}
	public void OnAfterDeserialize() {
		Construct();
	}
}

Helper Methods:

using System;
using Godot;
using Godot.Collections;


public static class CompositorExtensions {

	public static Rid IndexBufferCreate(this RenderingDevice renderingDevice, ushort[] indices) {
		byte[] byteIndices = indices.ToByteArray();

		return renderingDevice.IndexBufferCreate((uint)indices.Length, RenderingDevice.IndexBufferFormat.Uint16, byteIndices);
	}
	public static Rid IndexBufferCreate(this RenderingDevice renderingDevice, uint[] indices) {
		byte[] byteIndices = indices.ToByteArray();

		return renderingDevice.IndexBufferCreate((uint)indices.Length, RenderingDevice.IndexBufferFormat.Uint32, byteIndices);
	}

	public static (Rid indexBuffer, Rid indexArray)? IndexArrayCreate(this RenderingDevice renderingDevice, ushort[] indices, uint indexOffset = 0) {
		Rid indexBuffer = renderingDevice.IndexBufferCreate(indices);
		if (!indexBuffer.IsValid) {
			throw new ArgumentException("Index Buffer is Invalid");
		}
		Rid indexArray = renderingDevice.IndexArrayCreate(indexBuffer, indexOffset, (uint)indices.Length);
		if (!indexArray.IsValid) {
			throw new ArgumentException("Index Array is Invalid");
		}

		return (indexBuffer, indexArray);
	}

	public static (Rid indexBuffer, Rid indexArray) IndexArrayCreate(this RenderingDevice renderingDevice, uint[] indices, uint indexOffset = 0) {
		Rid indexBuffer = renderingDevice.IndexBufferCreate(indices);
		if (!indexBuffer.IsValid) {
			throw new ArgumentException("Index Buffer is Invalid");
		}
		Rid indexArray = renderingDevice.IndexArrayCreate(indexBuffer, indexOffset, (uint)indices.Length);
		if (!indexArray.IsValid) {
			throw new ArgumentException("Index Array is Invalid");
		}

		return (indexBuffer, indexArray);
	}

	public static Rid VertexBufferCreate(this RenderingDevice renderingDevice, float[] vertices) {
		if (vertices.Length % 3 != 0) throw new ArgumentException("Invalid number of values in the points buffer, there should be three float values per point.", nameof(vertices));
		byte[] byteVertices = vertices.ToByteArray();

		return renderingDevice.VertexBufferCreate((uint)byteVertices.Length, byteVertices);
	}

	public static (Rid vertexBuffer, Rid vertexArray) VertexArrayCreate(this RenderingDevice renderingDevice, float[] points, long vertexFormat) {
		Rid vertexBuffer = renderingDevice.VertexBufferCreate(points);
		if (!vertexBuffer.IsValid) {
			throw new ArgumentException("Vertex Buffer is Invalid");
		}
		Rid vertexArray = renderingDevice.VertexArrayCreate((uint)(points.Length / 3), vertexFormat, [vertexBuffer]);
		if (!vertexArray.IsValid) {
			throw new ArgumentException("Vertex Array is Invalid");
		}

		return (vertexBuffer, vertexArray);
	}

	public static (Rid framebufferTexture, Rid framebuffer) FramebufferCreate(this RenderingDevice renderingDevice, RDTextureFormat textureFormat, RDTextureView textureView, RenderingDevice.TextureSamples textureSamples = RenderingDevice.TextureSamples.Samples1) {
		Rid frameBufferTexture = renderingDevice.TextureCreate(textureFormat, textureView);
		if (!frameBufferTexture.IsValid) {
			throw new ArgumentException("Frame Buffer Texture is Invalid");
		}

		Array<RDAttachmentFormat> attachments = [
			new RDAttachmentFormat() {
				Format = textureFormat.Format,
				Samples = textureSamples,
				UsageFlags = (uint)textureFormat.UsageBits
			}
		];
		long frameBufferFormat = renderingDevice.FramebufferFormatCreate(attachments);
		Rid frameBuffer = renderingDevice.FramebufferCreate([frameBufferTexture], frameBufferFormat);
		if (!frameBuffer.IsValid) {
			throw new ArgumentException("Frame Buffer is Invalid");
		}

		return (frameBufferTexture, frameBuffer);
	}

	public static RDUniform AddIds(this RDUniform uniform, Span<Rid> ids) {
		for (int i = 0; i < ids.Length; i++) {
			uniform.AddId(ids[i]);
		}

		return uniform;
	}

	public static void ComputeListBindImage(this RenderingDevice device, long computeList, Rid shaderRid, Rid image, uint setIndex, int binding = 0) {
		RDUniform uniform = new RDUniform() {
			UniformType = RenderingDevice.UniformType.Image,
			Binding = binding,
		}.AddIds([image]);

		device.ComputeListBindUniform(computeList, uniform, shaderRid, setIndex);
	}
	public static void ComputeListBindSampler(this RenderingDevice device, long computeList, Rid shaderRid, Rid image, Rid sampler, uint setIndex, int binding = 0) {
		RDUniform uniform = new RDUniform() {
			UniformType = RenderingDevice.UniformType.SamplerWithTexture,
			Binding = binding,
		}.AddIds([sampler, image]);

		device.ComputeListBindUniform(computeList, uniform, shaderRid, setIndex);
	}

	public static void ComputeListBindColor(this RenderingDevice device, long computeList, Rid shaderRid, RenderSceneBuffersRD sceneBuffers, uint view, uint setIndex, int binding = 0) =>
		device.ComputeListBindImage(computeList, shaderRid, sceneBuffers.GetColorLayer(view), setIndex, binding);

	public static void ComputeListBindDepth(this RenderingDevice device, long computeList, Rid shaderRid, RenderSceneBuffersRD sceneBuffers, uint view, Rid sampler, uint setIndex, int binding = 0) =>
		device.ComputeListBindSampler(computeList, shaderRid, sceneBuffers.GetDepthLayer(view), sampler, setIndex, binding);


	public static void ComputeListBindUniform(this RenderingDevice device, long computeList, RDUniform uniform, Rid shaderRid, uint setIndex) {
		Rid set = UniformSetCacheRD.GetCache(shaderRid, setIndex, [uniform]);

		device.ComputeListBindUniformSet(computeList, set, setIndex);
	}

	public static byte[] ToByteArray(this Half[] halfs) {
		byte[] bytes = new byte[halfs.Length * 2];
		for (int i = 0; i < halfs.Length; i++) {
			byte[] halfBytes = BitConverter.GetBytes(halfs[i]);

			int j = i * 2;
			bytes[j] = halfBytes[0];
			bytes[j+1] = halfBytes[1];
		}

		return bytes;
	}
	public static byte[] ToByteArray(this float[] floats) {
		byte[] bytes = new byte[floats.Length * 4];
		for (int i = 0; i < floats.Length; i++) {
			byte[] floatBytes = BitConverter.GetBytes(floats[i]);

			int j = i * 4;
			bytes[j] = floatBytes[0];
			bytes[j+1] = floatBytes[1];
			bytes[j+2] = floatBytes[2];
			bytes[j+3] = floatBytes[3];
		}

		return bytes;
	}
	public static byte[] ToByteArray(this double[] doubles) {
		byte[] bytes = new byte[doubles.Length * 8];
		for (int i = 0; i < doubles.Length; i++) {
			byte[] doubleBytes = BitConverter.GetBytes(doubles[i]);

			int j = i * 8;
			bytes[j] = doubleBytes[0];
			bytes[j+1] = doubleBytes[1];
			bytes[j+2] = doubleBytes[2];
			bytes[j+3] = doubleBytes[3];
			bytes[j+4] = doubleBytes[4];
			bytes[j+5] = doubleBytes[5];
			bytes[j+6] = doubleBytes[6];
			bytes[j+7] = doubleBytes[7];
		}

		return bytes;
	}


	public static byte[] ToByteArray(this ushort[] ushorts) {
		byte[] bytes = new byte[ushorts.Length * 2];
		for (int i = 0; i < ushorts.Length; i++) {
			byte[] ushortBytes = BitConverter.GetBytes(ushorts[i]);

			int j = i * 2;
			bytes[j] = ushortBytes[0];
			bytes[j+1] = ushortBytes[1];
		}

		return bytes;
	}
	public static byte[] ToByteArray(this short[] shorts) {
		byte[] bytes = new byte[shorts.Length * 2];
		for (int i = 0; i < shorts.Length; i++) {
			byte[] shortBytes = BitConverter.GetBytes(shorts[i]);

			int j = i * 2;
			bytes[j] = shortBytes[0];
			bytes[j+1] = shortBytes[1];
		}

		return bytes;
	}

	public static byte[] ToByteArray(this uint[] uints) {
		byte[] bytes = new byte[uints.Length * 4];
		for (int i = 0; i < uints.Length; i++) {
			byte[] uintBytes = BitConverter.GetBytes(uints[i]);

			int j = i * 4;
			bytes[j] = uintBytes[0];
			bytes[j+1] = uintBytes[1];
			bytes[j+2] = uintBytes[2];
			bytes[j+3] = uintBytes[3];
		}

		return bytes;
	}
	public static byte[] ToByteArray(this int[] ints) {
		byte[] bytes = new byte[ints.Length * 4];
		for (int i = 0; i < ints.Length; i++) {
			byte[] intBytes = BitConverter.GetBytes(ints[i]);

			int j = i * 4;
			bytes[j] = intBytes[0];
			bytes[j+1] = intBytes[1];
			bytes[j+2] = intBytes[2];
			bytes[j+3] = intBytes[3];
		}

		return bytes;
	}

	public static byte[] ToByteArray(this ulong[] ulongs) {
		byte[] bytes = new byte[ulongs.Length * 8];
		for (int i = 0; i < ulongs.Length; i++) {
			byte[] ulongBytes = BitConverter.GetBytes(ulongs[i]);

			int j = i * 8;
			bytes[j] = ulongBytes[0];
			bytes[j+1] = ulongBytes[1];
			bytes[j+2] = ulongBytes[2];
			bytes[j+3] = ulongBytes[3];
			bytes[j+4] = ulongBytes[4];
			bytes[j+5] = ulongBytes[5];
			bytes[j+6] = ulongBytes[6];
			bytes[j+7] = ulongBytes[7];
		}

		return bytes;
	}
	public static byte[] ToByteArray(this long[] longs) {
		byte[] bytes = new byte[longs.Length * 8];
		for (int i = 0; i < longs.Length; i++) {
			byte[] longBytes = BitConverter.GetBytes(longs[i]);

			int j = i * 8;
			bytes[j] = longBytes[0];
			bytes[j+1] = longBytes[1];
			bytes[j+2] = longBytes[2];
			bytes[j+3] = longBytes[3];
			bytes[j+4] = longBytes[4];
			bytes[j+5] = longBytes[5];
			bytes[j+6] = longBytes[6];
			bytes[j+7] = longBytes[7];
		}

		return bytes;
	}
}

Actual code:

using System;
using Godot;

[Tool]
[GlobalClass]
public partial class DrawAndComputeCompositorEffect : BaseCompositorEffect {
	public static readonly StringName Context = "UnderwaterEffect";
	public static readonly StringName WaterMapName = "water_map";
	public static readonly StringName WaterDepthName = "water_depth";


	[Export] private RDShaderFile? RenderShaderFile {
		get => _renderShaderFile;
		set {
			_renderShaderFile = value;

			if (RenderingDevice is not null) {
				Destruct();
				Construct();
			}
		}
	}
	private RDShaderFile? _renderShaderFile;
	private Rid renderShader;

	[Export] private RDShaderFile? ComputeShaderFile {
		get => _computeShaderFile;
		set {
			_computeShaderFile = value;

			if (RenderingDevice is not null) {
				Destruct();
				Construct();
			}
		}
	}
	private RDShaderFile? _computeShaderFile;
	private Rid computeShader;

	private Rid nearestSampler;

	private readonly RDAttachmentFormat waterMapAttachmentFormat = new() {
		Format = RenderingDevice.DataFormat.R16G16B16A16Unorm,
		Samples = RenderingDevice.TextureSamples.Samples1,
		UsageFlags = (uint)(RenderingDevice.TextureUsageBits.ColorAttachmentBit | RenderingDevice.TextureUsageBits.StorageBit)
	};
	private readonly RDAttachmentFormat waterDepthAttachmentFormat = new() {
		Format = RenderingDevice.DataFormat.D32Sfloat,
		Samples = RenderingDevice.TextureSamples.Samples1,
		UsageFlags = (uint)RenderingDevice.TextureUsageBits.DepthStencilAttachmentBit
	};
	private long framebufferFormat;

	private readonly RDVertexAttribute vertexAttribute = new() {
		Format = RenderingDevice.DataFormat.R32G32B32Sfloat,
		Location = 0,
		Stride = sizeof(float) * 3,
	};
	private long vertexFormat;

	private Rid renderPipeline;
	private Rid computePipeline;



	public DrawAndComputeCompositorEffect() : base() {
		EffectCallbackType = EffectCallbackTypeEnum.PostTransparent;
	}



	public override void _RenderCallback(int effectCallbackType, RenderData renderData) {
		base._RenderCallback(effectCallbackType, renderData);

		// if (effectCallbackType != (long)EffectCallbackTypeEnum.PostTransparent) return;

		if (RenderingDevice is null || _renderShaderFile is null || _computeShaderFile is null) return;


		RenderSceneBuffers renderSceneBuffers = renderData.GetRenderSceneBuffers();
		if (renderSceneBuffers is not RenderSceneBuffersRD sceneBuffers) return;
		RenderSceneData renderSceneData = renderData.GetRenderSceneData();
		if (renderSceneData is not RenderSceneDataRD sceneData) return;

		uint viewCount = sceneBuffers.GetViewCount();


		Vector2I renderSize = sceneBuffers.GetInternalSize();
		if (renderSize.X == 0.0 && renderSize.Y == 0.0) {
			throw new ArgumentException("Render size is incorrect");
		}

		uint xGroups = (uint)((renderSize.X - 1) / 8) + 1;
		uint yGroups = (uint)((renderSize.Y - 1) / 8) + 1;


		if (sceneBuffers.HasTexture(Context, WaterMapName)) {
			// Reset the Color and Depth textures if their sizes are wrong
			RDTextureFormat textureFormat = sceneBuffers.GetTextureFormat(Context, WaterMapName);
			if (textureFormat.Width != renderSize.X || textureFormat.Height != renderSize.Y) {
				sceneBuffers.ClearContext(Context);
			}
		}

		if (! sceneBuffers.HasTexture(Context, WaterMapName)) {
			// Create and cache the Map and Depth to create the Water Buffer
			sceneBuffers.CreateTexture(Context, WaterMapName, waterMapAttachmentFormat.Format, waterMapAttachmentFormat.UsageFlags, waterMapAttachmentFormat.Samples, renderSize, viewCount, 1, true);
			sceneBuffers.CreateTexture(Context, WaterDepthName, waterDepthAttachmentFormat.Format, waterDepthAttachmentFormat.UsageFlags, waterDepthAttachmentFormat.Samples, renderSize, viewCount, 1, true);
		}


		Color[] clearColors = [new Color(0, 0, 0, 0)];


		for (uint view = 0; view < viewCount; view++) {
			Rid waterMap = sceneBuffers.GetTextureSlice(Context, WaterMapName, view, 0, 1, 1);
			Rid waterDepth = sceneBuffers.GetTextureSlice(Context, WaterDepthName, view, 0, 1, 1);

			// Include the Map and Depth from earlier
			Rid waterBuffer = RenderingDevice.FramebufferCreate([waterMap, waterDepth], framebufferFormat);
			if (! waterBuffer.IsValid) {
				throw new ArgumentException("Water Mask Frame Buffer is Invalid");
			}

			// TODO: get the mesh data instead of this hard-coded data
			float[] vertCoords = [
				5f,	0f,	0f,
				0f,	0f,	0f,
				0f,	5f,	0f,
				5f,	5f,	0f,
				5f,	5f,	-5f,
				5f,	0f,	-5f,
			];
			uint[] vertIndices = [
				0, 1, 2,
				0, 2, 3,
				0, 3, 4,
				0, 4, 5,
			];

			(_, Rid vertexArray) = RenderingDevice.VertexArrayCreate(vertCoords, vertexFormat);
			(_, Rid indexArray) = RenderingDevice.IndexArrayCreate(vertIndices);


			Projection projection = sceneData.GetViewProjection(view);
			Projection viewMatrix = new(sceneData.GetCamTransform().Inverse());

			// World-space -> Clip-space Matrix to be used in the rendering shader
			Projection WorldToClip = projection * viewMatrix;
			// Eye Offset for fancy VR multi-view
			Vector3 eyeOffset = sceneData.GetViewEyeOffset(view);

			// Unfolding into a push constant
			float[] renderPushConstant = [
				WorldToClip.X.X, WorldToClip.X.Y, WorldToClip.X.Z, WorldToClip.X.W,
				WorldToClip.Y.X, WorldToClip.Y.Y, WorldToClip.Y.Z, WorldToClip.Y.W,
				WorldToClip.Z.X, WorldToClip.Z.Y, WorldToClip.Z.Z, WorldToClip.Z.W,
				WorldToClip.W.X, WorldToClip.W.Y, WorldToClip.W.Z, WorldToClip.W.W,

				eyeOffset.X, eyeOffset.Y, eyeOffset.Z, 0, // Pad with a zero, because the push constant needs to contain a multiple of 16 bytes (4 floats)
			];
			byte[] renderPushConstantBytes = renderPushConstant.ToByteArray();


			// Render the Geometry (see vertCoords and vertIndices) to an intermediate framebuffer To use later
			RenderingDevice.DrawCommandBeginLabel("Render Water Mask", new Color(1f, 1f, 1f));
			long drawList = RenderingDevice.DrawListBegin(waterBuffer, RenderingDevice.InitialAction.Clear, RenderingDevice.FinalAction.Store, RenderingDevice.InitialAction.Clear, RenderingDevice.FinalAction.Discard, clearColors);
			RenderingDevice.DrawListBindRenderPipeline(drawList, renderPipeline);
			RenderingDevice.DrawListBindVertexArray(drawList, vertexArray);
			RenderingDevice.DrawListBindIndexArray(drawList, indexArray);
			RenderingDevice.DrawListSetPushConstant(drawList, renderPushConstantBytes, (uint)renderPushConstantBytes.Length);
			RenderingDevice.DrawListDraw(drawList, true, 2);
			RenderingDevice.DrawListEnd();
			RenderingDevice.DrawCommandEndLabel();

			// Unfolding into a push constant
			float[] computePushConstant = [
				renderSize.X, renderSize.Y, 0, 0, // Pad instead with two zeroes here
			];
			byte[] computePushConstantBytes = computePushConstant.ToByteArray();

			// Here we draw the Underwater effect, using the waterBuffer to know where there is water geometry
			RenderingDevice.DrawCommandBeginLabel("Render Underwater Effect", new Color(1f, 1f, 1f));
			long computeList = RenderingDevice.ComputeListBegin();
			RenderingDevice.ComputeListBindComputePipeline(computeList, computePipeline);
			RenderingDevice.ComputeListBindColor(computeList, computeShader, sceneBuffers, view, 0);
			RenderingDevice.ComputeListBindDepth(computeList, computeShader, sceneBuffers, view, nearestSampler, 1);
			RenderingDevice.ComputeListBindImage(computeList, computeShader, waterMap, 2);
			RenderingDevice.ComputeListSetPushConstant(computeList, computePushConstantBytes, (uint)computePushConstantBytes.Length);
			RenderingDevice.ComputeListDispatch(computeList, xGroups, yGroups, 1);
			RenderingDevice.ComputeListEnd();
			RenderingDevice.DrawCommandEndLabel();


			RenderingDevice.FreeRid(vertexArray);
			RenderingDevice.FreeRid(indexArray);

			RenderingDevice.FreeRid(waterBuffer);
		}
	}




	protected override void ConstructBehaviour(RenderingDevice renderingDevice) {
		// Framebuffer Format includes a depth attachment to Self-occlude
		framebufferFormat = renderingDevice.FramebufferFormatCreate([waterMapAttachmentFormat, waterDepthAttachmentFormat]);
		vertexFormat = renderingDevice.VertexFormatCreate([vertexAttribute]);

		nearestSampler = renderingDevice.SamplerCreate(new() {
			MinFilter = RenderingDevice.SamplerFilter.Nearest,
			MagFilter = RenderingDevice.SamplerFilter.Nearest
		});

		ConstructRenderPipeline(renderingDevice);
		ConstructComputePipeline(renderingDevice);
	}

	private void ConstructRenderPipeline(RenderingDevice renderingDevice) {
		if (RenderShaderFile is null) return;

		renderShader = renderingDevice.ShaderCreateFromSpirV(RenderShaderFile.GetSpirV());
		if (! renderShader.IsValid) {
			throw new ArgumentException("Render Shader is Invalid");
		}


		RDPipelineColorBlendState blend = new() {
			Attachments = [new RDPipelineColorBlendStateAttachment()]
		};

		renderPipeline = renderingDevice.RenderPipelineCreate(
			renderShader,
			framebufferFormat,
			vertexFormat,
			RenderingDevice.RenderPrimitive.Triangles,
			new RDPipelineRasterizationState(),
			new RDPipelineMultisampleState(),
			new RDPipelineDepthStencilState() {
				// Enable Self-occlusion via Depth Test (see ConstructBehaviour(RenderingDevice))
				EnableDepthTest = true,
				EnableDepthWrite = true,
				DepthCompareOperator = RenderingDevice.CompareOperator.LessOrEqual
			},
			blend
		);
		if (! renderPipeline.IsValid) {
			throw new ArgumentException("Render Pipeline is Invalid");
		}
	}


	private void ConstructComputePipeline(RenderingDevice renderingDevice) {
		if (ComputeShaderFile is null) return;

		computeShader = renderingDevice.ShaderCreateFromSpirV(ComputeShaderFile.GetSpirV());
		if (! computeShader.IsValid) {
			throw new ArgumentException("Compute Shader is Invalid");
		}

		computePipeline = renderingDevice.ComputePipelineCreate(computeShader);
		if (! computePipeline.IsValid) {
			throw new ArgumentException("Compute Pipeline is Invalid");
		}
	}


	protected override void DestructBehaviour(RenderingDevice renderingDevice) {
		if (nearestSampler.IsValid) {
			renderingDevice.FreeRid(nearestSampler);
		}

		if (renderShader.IsValid) {
			renderingDevice.FreeRid(renderShader);
			renderShader = default;
		}
		// Don't need to free the pipeline as freeing the shader does that for us.
		renderPipeline = default;

		if (computeShader.IsValid) {
			renderingDevice.FreeRid(computeShader);
			computeShader = default;
		}
		// Same as above
		computePipeline = default;
	}
}

Render Shader:

#[vertex]
#version 450 core

layout(location = 0) in vec3 vertex;

layout(push_constant, std430) uniform Params {
	mat4 world_to_clip; // World-space -> Clip-space Matrix to transform the mesh
	vec3 eye_offset; // Eye offset from Multi-view
} params;


void main()
{
	vec4 pos = params.world_to_clip * vec4(vertex, 1.0);
	gl_Position = vec4(vec3(pos.x, -pos.y, pos.z) + params.eye_offset, pos.w);
}


#[fragment]
#version 450 core

layout(location = 0) out vec4 frag_color;


void main() {
	frag_color = vec4(gl_FragCoord.xyw, 1 - float(gl_FrontFacing));
}

Compute Shader:

#[compute]
#version 450

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(rgba8, set = 0, binding = 0) uniform image2D color_image; // Color of the screen pre post-processing
layout(set = 1, binding = 0) uniform sampler2D depth_image; // Depth buffer of the screen
layout(r16f, set = 2, binding = 0) uniform image2D water_map_image; // Water Map that was rendered in the Render Shader

layout(push_constant, std430) uniform Params {
	vec2 render_size; // Size of the Screen
} params;


void main() {
	ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
	vec2 depth_uv = vec2(uv) / params.render_size;

	vec4 color = imageLoad(color_image, uv);
	vec4 water_map = imageLoad(water_map_image, uv);
	float depth = texture(depth_image, depth_uv).r;

	// The amount of water we are looking through is either the end of the water volume (water_map.z) or the closest surface (depth)
	float max_depth = max(water_map.z, depth);

	// Actual water color calculation
	vec4 water_color = mix(color, vec4(0, 0.1, 0.75, 1), 1 - max_depth);

	imageStore(color_image, uv, mix(color, water_color, water_map.a));
}

Alright I’ve found how to get mesh data:

for surface_index in range(mesh.get_surface_count()):
	var arrays = mesh.surface_get_arrays(surface_index)
	var verts = arrays[Mesh.ARRAY_VERTEX]
	var idx = arrays[Mesh.ARRAY_INDEX]