Render Pipelines

Since JMonkeyEngine 3.8, ViewPorts are rendered using render pipelines. Before, the RenderManager was entirely responsible for processing each ViewPort’s scenes and rendering their geometries. Only basic forward rendering could really be used without great difficulty, so render pipelines were introduced to allow developers to implement whatever rendering techniques their game demands.

The sky is the limit with render pipelines, however, because they control every aspect of rendering a ViewPort (including SceneProcessors and profiling), a great deal goes into implementing a pipeline.

PipelineContext

Before diving into building render pipelines, one must first have at least a rudimentary knowledge of PipelineContexts, which are responsible for handling global objects for pipelines. Pipelines themselves cannot manage these global objects because they are localized to specific ViewPorts. PipelineContexts are stored directly by the RenderManager, so they are better suited for this task.

public class MyPipelineContext implements PipelineContext {

    // an example of a global object to manage
    private GlobalResources resources;

    @Override
    public boolean startViewPortRender(RenderManager rm, ViewPort vp) {
        // Called when a ViewPort begins rendering that
        // this context is involved in.
        // Must return true if this context was already involved
        // in a ViewPort rendering this frame.
    }

    @Override
    public void endViewPortRender(RenderManager rm, ViewPort vp) {
        // Called when a ViewPort ends rendering that
        // this context was involved in.
    }

    @Override
    public void endContextRenderFrame(RenderManager rm) {
        // Called after all rendering this frame is complete AND this
        // context was involved in rendering a ViewPort this frame.
    }

    // give pipelines access to the example global object
    public GlobalResources getResources() {
        return resources;
    }

}

Note that PipelineContexts get run only if they become involved in rendering a ViewPort. They do so when a pipeline specifically selects them for rendering, which will be covered later.

In order to be selected at all, PipelineContexts must be registered with the RenderManager at the time. This can either be done manually, or the pipeline itself can create and register the context if it does not yet exist.

// register context manually
renderManager.registerContext(MyPipelineContext.class, new MyPipelineContext());

Contexts are registered by a class type by which they can then be retrieved.

RenderPipeline

The RenderPipeline interface is the primary element of the pipeline system, as it is directly responsible for rendering a ViewPort. RenderPipeline provides five methods to implement:

public class MyPipeline implements RenderPipeline<MyPipelineContext> {

    @Override
    public MyPipelineContext fetchPipelineContext(RenderManager rm) {
        // Returns a PipelineContext from the RenderManager
        // that handles global objects for this pipeline. The
        // returned context is passed to pipelineRender.
    }

    @Override
    public boolean hasRenderedThisFrame() {
        // Returns true if this context has performed any
        // rendering previously on this frame.
    }

    @Override
    public void startRenderFrame() {
        // Called before pipelineRender on the first rendering
        // this pipeline is to perform this frame.
    }

    @Override
    public void pipelineRender(RenderManager rm, MyPipelineContext context, ViewPort vp, float tpf) {
        // Does the actual rendering of the ViewPort.
    }

    @Override
    public void endRenderFrame(RenderManager rm) {
        // Called after all rendering is complete in a frame in
        // which this pipeline rendered a ViewPort.
    }

}

The pipelineRender method can get quite complicated, as rendering is a complicated process. Fortunately, the pipeline system imposes little to no restriction on what pipelines actually do during rendering. Here is a quick renderPipeline implementation to get started with:

@Override
public void pipelineRender(RenderManager rm, MyPipelineContext context, ViewPort vp, float tpf) {
    // apply viewport to rendering context
    rm.getRenderer().setFrameBuffer(vp.getOutputFrameBuffer());
    rm.getRenderer().clearBuffers(true, true, true);
    rm.getRenderer().setBackgroundColor(vp.getBackgroundColor());
    rm.setCamera(vp.getCamera(), false);
    // render each geometry in all viewport scenes
    for (Spatial scene : vp.getScenes()) {
        scene.depthFirstTraversal(s -> {
            if (s instanceof Geometry) {
                rm.renderGeometry((Geometry)s);
            }
        });
    }
    // reset clip rect
    rm.getRenderer().clearClipRect();
}

As previously mentioned, there is very little restriction over what pipelineRender does, so following the above example is not required. In fact, JMonkeyEngine’s default renderer, ForwardPipeline, looks vastly different.

Fetching Contexts to Use

Since pipelines often depend on global objects (as stated before), the RenderPipeline interface has a generic specifying the type of context to be expected (set as MyPipelineContext in the example above), and the interface provides the fetchPipelineContext method to select the context to use during rendering. The context returned by fetchPipelineContext will then be passed to pipelineRender to actually be used.

For example, if the pipeline wanted to select MyPipelineContext that is already registered with the RenderManager:

@Override
public MyPipelineContext fetchPipelineContext(RenderManager rm) {
    // assuming MyPipelineContext is registered under MyPipelineContext.class
    return rm.getContext(MyPipelineContext.class);
}

Even if a RenderPipeline does not need to use a PipelineContext, it is still required that fetchPipelineContext return a non-null context. For such cases, returning rm.getDefaultContext() is acceptable.

Usage

In order to get a RenderPipeline to render a ViewPort, simply assign the pipeline to the ViewPort. When the rendering step occurs, the RenderManager uses each ViewPort’s assigned pipeline to render the ViewPort.

viewPort.setPipeline(new MyRenderPipeline());

Note that RenderPipelines (unless otherwise specified) can be assigned to multiple ViewPorts at once.

MyRenderPipeline p = new MyRenderPipeline();
viewPort.setPipeline(p);
guiViewPort.setPipeline(p);

If no pipeline is assigned to a ViewPort, the RenderManager uses a default pipeline to render that ViewPort. The default pipeline can be set as so:

renderManager.setPipeline(new MyRenderPipeline());