After a bit of research, here's the solution I found:
Below I show an example of offscreen rendering to a Frame Buffer with two texture color slots. Then, I render the texture from each color slot to the viewport.
import bpy
import gpu
from mathutils import Matrix
from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_texture_2d
# Create a custom Frame Buffer with two color slots
my_tex1 = gpu.types.GPUTexture((512, 512))
my_tex2 = gpu.types.GPUTexture((512, 512))
my_buff = gpu.types.GPUFrameBuffer(color_slots=(my_tex1, my_tex2))
# Define Vertex Shader
vertex_shader = '''
in vec3 position;
out vec3 pos;
void main()
{
gl_Position = vec4(position, 1.0f);
}
'''
# Define Fragment Shader
fragment_shader = '''
in vec3 pos;
out vec4[2] FragColor;
void main()
{
FragColor[0] = vec4(1.0, 0.0, 0.0, 1.0); //Red
FragColor[1] = vec4(0.0, 0.0, 1.0, 1.0); //Blue
}
'''
# Vertex Attributes: Positions
vertices = ((-1, 1, 0), (1, 1, 0), (-1, -1, 0), (1, -1, 0))
# Vertex Attributes: Indices
indices = ((0, 1, 2), (2, 1, 3))
# Create Shader
shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
# Create batch for rendering
batch = batch_for_shader(shader, 'TRIS', {"position": vertices}, indices=indices)
# Custom draw call to draw to the custom Frame Buffer
def my_draw():
# Bind custom Frame Buffer to be drawn
with my_buff.bind():
# Bind shader to be drawn
shader.bind()
# Draw to my_tex1 and my_tex2 offscreen
batch.draw(shader)
# Main draw call
def draw():
# Custom draw call
my_draw()
# Draw my_tex1 and my_tex2 seperately to the Viewport
draw_texture_2d(my_tex1, (0,0), 10, 10)
draw_texture_2d(my_tex2, (0,0), -10, 10)
# Add draw call to draw handler
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')
The result in Blender's viewport: