jMonkeyEngine / jmonkeyengine

A complete 3-D game development suite written in Java.
http://jmonkeyengine.org
BSD 3-Clause "New" or "Revised" License
3.74k stars 1.12k forks source link

FrameGraph API #2256

Open codex128 opened 1 month ago

codex128 commented 1 month ago

This PR adds a FrameGraph rendering pipeline API that is similar in concept to shader nodes. This is based off work from #2090.

For more information: https://hub.jmonkeyengine.org/t/framegraph-api/47646?u=codex

To Do:

Issues to be investigated further:

Any testing is welcome!

codex128 commented 1 month ago

This PR is ready for review and testing.

To speed things along, I will try to explain how the resource handling works:

Each FrameGraph has a ResourceList, which handles RenderResources. RenderResources are not actual resources (which are called RenderObjects in the code) , they are simply promises of an actual resource (RenderObject) that will exist in the future. RenderPasses do not have direct access to the RenderResources, but can perform operations on them through ResourceTickets, which are essentially indices pointing to particular RenderResources within an array.

When a RenderPass makes a request for a RenderObject from the ResourceList using a ResourceTicket (called acquiring), the ResourceList will check if the RenderResource located by the ResourceTicket is virtual (is not associated with a RenderObject). If not, great! The associated RenderObject is simply returned. But if the RenderResource is virtual, the ResourceList asks the RenderObjectMap (held by the RenderManager) to either create a new RenderObject to use or find an existing one to reallocate.

The RenderObjectMap considers creating new RenderObjects expensive, so it tries to reallocate existing RenderObjects wherever it can. The ResourceDef (which defines the behavior of a RenderResource and the associated RenderObject) attached to the RenderResource is used to determine which RenderObjects are suitable for reallocation. Also the RenderObjectMap doesn't like to keep unused RenderObjects alive, so once a RenderObject has gone a number of frames without being used, it is disposed.

As an example to better understand how render passes and resource handling works, here is a basic render pass that performs downsampling. That is, it takes an input texture and renders to an output texture that is a quarter of the size.

public class MyRenderPass extends RenderPass {

    private ResourceTicket<Texture2D> inTex, outTex;
    private TextureDef<Texture2D> texDef;

    @Override
    protected void initialize(FrameGraph frameGraph) {

        // Initialize is called when the pass is assigned to the FrameGraph.

        // Add an input named "Texture". This allows this pass to access a texture
        // from another pass.
        inTex = addInput("Texture");

        // Add an output named "Texture". This allows other passes to access the
        // resulting texture.
        outTex = addOutput("Texture");

        // Declare the texture definition, defining how the output texture is created
        // and how it behaves.
        texDef = new TextureDef(Texture2D.class, img -> new Texture2D(img));

    }
    @Override
    protected void prepare(FGRenderContext context) {

        // Prepare is called before execution for every ViewPort rendered, and is used primarily to determine
        // what resources are referenced where, so that unused passes can be culled.

        // Note that a prepare call does not necessarily proceed a execute call, due to culling.

        // Declare a new resource as the output texture
        declare(texDef, outTex);

        // Reserve the output texture. This greatly increases the chances of getting
        // the same texture from frame to frame, and thus minimizes texture binds.
        reserve(outTex);

        // Reference the input texture, so this pass can safely access it
        reference(inTex);

    }
    @Override
    protected void execute(FGRenderContext context) {

        // Execute is called to perform the rendering operations. This is not guaranteed
        // to be called for every ViewPort rendered, because of culling.

        // Acquire the input texture provided by another pass
        Texture2D texture = resources.acquire(inTex);
        Image img = texture.getImage();

        // Set the size of the output texture as half the input texture on each demension
        int w = img.getWidth() / 2;
        int h = img.getHeight() / 2;
        texDef.setSize(w, h);

        // Set the format of the output texture to match the input texture
        texDef.setFormat(img.getFormat());

        // Get a framebuffer object matching the width, height, and samples. If no
        // such framebuffer exists, a new one will be created. This is handled only by
        // the RenderPass superclass and not the resource manager.
        FrameBuffer fb = getFrameBuffer(w, h, 1);

        // Acquire the output texture, which is immediately attached to the framebuffer.
        // This operation is specifically designed to limit texture binds and framebuffer updates.
        resources.acquireColorTargets(fb, outTex);

        // Attach the framebuffer for rendering
        context.getRenderer().setFrameBuffer(fb);
        context.getRenderer().clearBuffers(true, true, true);
        context.getRenderer().setBackgroundColor(ColorRGBA.BlackNoAlpha);

        // edit: set the camera width and height to the output texture width and height
        context.resizeCamera(w, h, false, false, false);

        // Render the input texture to the output texture on a fullscreen quad.
        // The depth texture is null, so the quad is rendered at a depth of one.
        context.renderTextures(texture, null);

    }
    @Override
    protected void reset(FGRenderContext context) {
        // Reset is called after execution. Passes cannot be culled from this step.
    }
    @Override
    protected void cleanup(FrameGraph frameGraph) {
        // Cleanup is called when this pass is removed from the FrameGraph.
    }

}

Further details for each function and class can be found in the relavent Javadoc.

codex128 commented 1 month ago

This branch does conflict with master, but fortunately it only is a small conflict in some Javadoc.

codex128 commented 1 week ago

After fixing several issues, the branch now builds successfully.

codex128 commented 1 week ago

Note: at this point, SceneProcessors are incompatible with the FrameGraph. This means no filters, shadows, etc.