Prechádzať zdrojové kódy

Adds Custom Render Pipeline Interface (#2304)

* added AlertArmatureMask

* fixed docs and added another helper

* made it easier to turn off priority checking

* removed copy methods

* jme3-core:
1. Added basic framework of framegraph to manage and organize new rendering passes;
2. Split existing rendering into several core RenderPasses;
3. Added multiple rendering paths (forward, deferred, tile based deferred);
4. Activated Framegraph system by useFramegraph, activated rendering paths by setRenderPath;

* jme3-examples:Added a set of test cases for renderPath.

* jme3-core:Improve deferred shading issues;
Fix flickering issues in tile based deferred shading;

* jme3-examples:Adjust deferred rendering test code

* jme3-core:Adjust GBuffer format, RT0(16F), RT1(16F), RT2(16F), RT3(32F), DEPTH

* jme3-core:Delete test code

* jme3-core:Fixed all known bugs in Tile-based DeferredShading.

* jme3-examples:update TestTileBasedDeferredShading

* jme3-core:update GLSLCompat.glsllib

* jme3-core:Fix frameGraph assertion error

* jme3-core:Add some core code comments, delete Chinese code comments

* jme3-core:Fix bugs existed in shadingModel

* jme3-examples:Add test case "TestShadingModel.java,TestRenderPathPointDirectionalAndSpotLightShadows.java"

* jme3-core:GBuffer data packing for terrain rendering compatibility (unlit, lighting)

* jme3-examples:
Added two renderPath sample codes.
Added terrain unlit shading model test code;
Added terrain lighting shading model test code;

* jme3-core:Add rendering path support for AdvancedPBRTerrain

* jme3-examples:Add a test code

* jme3-core:Supplement core code comments, add full JME license to each java file, fix a few minor issues

* jme3-terrain:Implement PBRTerrain compatibility code

* jme3-core:Fixed Tile-basedDeferred light culling issue, fixed some flickering problems, fixed attenuation issue with multiple PBR lights under deferred rendering.

* jme3-examples:update TestCode

* jme3-examples:update TestPBRTerrainRenderPath.java

* jme3-core:Fix TestTexture3D/TestTexture3DLoading

* jme3-core:fix lightTexSizeInv undefined

* jme3-core:
1.change java.com.jme3.renderer.renderPass=>java.com.jme3.renderer.pass
2.change IRenderGeometry.java=>RenderGeometry.java

* jme3-core:use g_ViewProjectionMatrixInverse

* jme3-core:use logger

* jme3-core:update javadoc(FastMath.nextRandomFlot)

* updated license

* updated javadoc

* renamed mask

* refactor FGPass, FGSink, and FGSource to interfaces

* fix method name collision for FGPass

* renamed FGPass reset method

* changes

* rebuilt API

* fixed framebuffer attachments bug

* deferred shading usable

* deferred and tiled deferred are working for TestShadingModel.java

* debugging

* debugging deferred pipeline

* more debugging

* more debugging

* cleanup code

* more cleanup

* remove reliance on J3mLoader and clean up code

* reorganized java files

* added support for cross-space parameter binding

* added optimizations

* recoding resource system

* redesigned resource system

* framegraph operational

* framegraph complete

* draft ready

* rename registration method

* renamed method

* delete personal changelog

* remove enums

* added information management passes

* migrated to new ticket protocol

* added resource extraction

* provide logical control of framegraph

* finalized user value sources and targets

* fixed framegraph use on multiple viewports binding textures too often

* fixed depth texture filtering

* added export and import methods

* javadoc for new classes

* licenses

* pass tests

* added framegraph opt-out for viewports

* added .j3g

* delete unused classes

* fixed background on deferred pipelines

* added basic opencl

* fixed tests

* verified fix for overlapping viewports

* added framegraph loading speed test

* added framegraph event capture

* fixed framebuffer update flag

* fixed broken light count

* revert debugging

* fix merge conflict

* start moving lighting logic outside matdef logic

* added ticket groups

* migrated lighting logic to render pass

* fixed light tex inverse type

* added light pack method junction

* deleted deferred lighting logic

* deleted draft files

* added group attribute pass

* added indefinite ticket groups

* finalized ticket lists

* porting filters

* narrowed PBR issue to metallic=1

* fixed functional array issues

* fixed build errors

* fixed depth of field filter import-export

* fixed deferred light probes

* fixed ambient and fixed buffered lights

* fixed gbuffer packing for advanced terrain

* fixed pbr terrain

* made render object map threadsafe

* added multithreading

* fix referencing bug

* added fix for missing defines

* test async

* fix dependency on java 9

* removed unnecessary atomic boolean

* fixed more async issues

* fixed camera size when rendering to smaller textures

* added addLoop

* add faze pass

* improved javadoc and fixed issue with deferred techniquedefs

* added pipeline interface and added render modules

* changes to factory methods

* deleted framegraph system

* removed gbuffer shaders

* restored deferred and gbuf shaders

* removed deferred materials

* removed effects passes

* deleted tests

* reverted unnecessary changes and moved pipeline classes to new package

* reverted more unnecessary changes

* small changes

* several changes

* revert changes in build file

* removed material adaptation code

* removed savable utilities

* removed math utility

* made render context implementation less confusing

* pass viewport to pipeline context

* fixed start method not being called

* remove miscellaneous

* removed miscellaneous

* remove miscellaneous

* remove miscellaneous

* changed render start order

* added license and attempted to revert changes

* remove target creation helpers

* added spacing

* several small changes

* added null pipeline and context creation helper

* delete GeometryRenderHandler and RenderUtils

---------

Co-authored-by: JohnKkk <[email protected]>
Co-authored-by: chenliming <[email protected]>
codex 10 mesiacov pred
rodič
commit
327426da51
23 zmenil súbory, kde vykonal 736 pridanie a 176 odobranie
  1. 1 1
      build.gradle
  2. 1 1
      jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
  3. 224 156
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  4. 29 1
      jme3-core/src/main/java/com/jme3/renderer/ViewPort.java
  5. 62 0
      jme3-core/src/main/java/com/jme3/renderer/pipeline/DefaultPipelineContext.java
  6. 159 0
      jme3-core/src/main/java/com/jme3/renderer/pipeline/ForwardPipeline.java
  7. 42 0
      jme3-core/src/main/java/com/jme3/renderer/pipeline/NullPipeline.java
  8. 70 0
      jme3-core/src/main/java/com/jme3/renderer/pipeline/PipelineContext.java
  9. 88 0
      jme3-core/src/main/java/com/jme3/renderer/pipeline/RenderPipeline.java
  10. 14 2
      jme3-core/src/main/java/com/jme3/renderer/queue/RenderQueue.java
  11. 33 2
      jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java
  12. 1 1
      jme3-core/src/main/resources/Common/MatDefs/Light/Deferred.j3md
  13. 1 1
      jme3-core/src/main/resources/Common/MatDefs/Light/Deferred.vert
  14. 1 1
      jme3-core/src/main/resources/Common/MatDefs/Light/GBuf.vert
  15. 1 1
      jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib
  16. 1 1
      jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java
  17. 1 1
      jme3-effects/src/main/java/com/jme3/post/filters/DepthOfFieldFilter.java
  18. 1 1
      jme3-effects/src/main/resources/Common/MatDefs/Post/CartoonEdge.frag
  19. 1 1
      jme3-effects/src/main/resources/Common/MatDefs/Post/CrossHatch.frag
  20. 1 1
      jme3-effects/src/main/resources/Common/MatDefs/Post/FXAA.frag
  21. 1 1
      jme3-examples/gradle.properties
  22. 1 1
      jme3-terrain/src/main/resources/Common/MatDefs/Terrain/HeightBasedTerrain.j3md
  23. 2 2
      jme3-terrain/src/main/resources/Common/MatDefs/Terrain/Terrain.j3md

+ 1 - 1
build.gradle

@@ -250,4 +250,4 @@ retrolambda {
   javaVersion JavaVersion.VERSION_1_7
   incremental true
     jvmArgs '-noverify'
-}
+}

+ 1 - 1
jme3-core/src/main/java/com/jme3/material/TechniqueDef.java

@@ -824,4 +824,4 @@ public class TechniqueDef implements Savable, Cloneable {
 
         return clone;
     }
-}
+}

+ 224 - 156
jme3-core/src/main/java/com/jme3/renderer/RenderManager.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,6 +31,10 @@
  */
 package com.jme3.renderer;
 
+import com.jme3.renderer.pipeline.ForwardPipeline;
+import com.jme3.renderer.pipeline.DefaultPipelineContext;
+import com.jme3.renderer.pipeline.RenderPipeline;
+import com.jme3.renderer.pipeline.PipelineContext;
 import com.jme3.light.DefaultLightFilter;
 import com.jme3.light.LightFilter;
 import com.jme3.light.LightList;
@@ -44,7 +48,6 @@ import com.jme3.math.Matrix4f;
 import com.jme3.post.SceneProcessor;
 import com.jme3.profile.AppProfiler;
 import com.jme3.profile.AppStep;
-import com.jme3.profile.SpStep;
 import com.jme3.profile.VpStep;
 import com.jme3.renderer.queue.GeometryList;
 import com.jme3.renderer.queue.RenderQueue;
@@ -65,8 +68,12 @@ import com.jme3.texture.FrameBuffer;
 import com.jme3.util.SafeArrayList;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.logging.Logger;
 
 /**
@@ -87,6 +94,10 @@ public class RenderManager {
     private final ArrayList<ViewPort> preViewPorts = new ArrayList<>();
     private final ArrayList<ViewPort> viewPorts = new ArrayList<>();
     private final ArrayList<ViewPort> postViewPorts = new ArrayList<>();
+    private final HashMap<Class, PipelineContext> contexts = new HashMap<>();
+    private final LinkedList<PipelineContext> usedContexts = new LinkedList<>();
+    private final LinkedList<RenderPipeline> usedPipelines = new LinkedList<>();
+    private RenderPipeline defaultPipeline = new ForwardPipeline();
     private Camera prevCam = null;
     private Material forcedMaterial = null;
     private String forcedTechnique = null;
@@ -104,7 +115,7 @@ public class RenderManager {
     private LightFilter lightFilter = new DefaultLightFilter();
     private TechniqueDef.LightMode preferredLightMode = TechniqueDef.LightMode.MultiPass;
     private int singlePassLightBatchSize = 1;
-    private MatParamOverride boundDrawBufferId=new MatParamOverride(VarType.Int,"BoundDrawBuffer",0);
+    private MatParamOverride boundDrawBufferId=new MatParamOverride(VarType.Int, "BoundDrawBuffer", 0);
     private Predicate<Geometry> renderFilter;
 
 
@@ -117,6 +128,115 @@ public class RenderManager {
     public RenderManager(Renderer renderer) {
         this.renderer = renderer;
         this.forcedOverrides.add(boundDrawBufferId);
+        // register default pipeline context
+        contexts.put(PipelineContext.class, new DefaultPipelineContext());
+    }
+    
+    /**
+     * Gets the default pipeline used when a ViewPort does not have a
+     * pipeline already assigned to it.
+     * 
+     * @return 
+     */
+    public RenderPipeline getPipeline() {
+        return defaultPipeline;
+    }
+    
+    /**
+     * Sets the default pipeline used when a ViewPort does not have a
+     * pipeline already assigned to it.
+     * <p>
+     * default={@link ForwardPipeline}
+     * 
+     * @param pipeline default pipeline (not null)
+     */
+    public void setPipeline(RenderPipeline pipeline) {
+        assert pipeline != null;
+        this.defaultPipeline = pipeline;
+    }
+    
+    /**
+     * Gets the default pipeline context registered under
+     * {@link PipelineContext#getClass()}.
+     * 
+     * @return 
+     */
+    public PipelineContext getDefaultContext() {
+        return getContext(PipelineContext.class);
+    }
+    
+    /**
+     * Gets the pipeline context registered under the class.
+     * 
+     * @param <T>
+     * @param type
+     * @return registered context or null
+     */
+    public <T extends PipelineContext> T getContext(Class<T> type) {
+        return (T)contexts.get(type);
+    }
+    
+    /**
+     * Gets the pipeline context registered under the class or creates
+     * and registers a new context from the supplier.
+     * 
+     * @param <T>
+     * @param type
+     * @param supplier interface for creating a new context if necessary
+     * @return registered or newly created context
+     */
+    public <T extends PipelineContext> T getOrCreateContext(Class<T> type, Supplier<T> supplier) {
+        T c = getContext(type);
+        if (c == null) {
+            c = supplier.get();
+            registerContext(type, c);
+        }
+        return c;
+    }
+    
+    /**
+     * Gets the pipeline context registered under the class or creates
+     * and registers a new context from the function.
+     * 
+     * @param <T>
+     * @param type
+     * @param function interface for creating a new context if necessary
+     * @return registered or newly created context
+     */
+    public <T extends PipelineContext> T getOrCreateContext(Class<T> type, Function<RenderManager, T> function) {
+        T c = getContext(type);
+        if (c == null) {
+            c = function.apply(this);
+            registerContext(type, c);
+        }
+        return c;
+    }
+    
+    /**
+     * Registers the pipeline context under the class.
+     * <p>
+     * If another context is already registered under the class, that
+     * context will be replaced by the given context.
+     * 
+     * @param <T>
+     * @param type class type to register the context under (not null)
+     * @param context context to register (not null)
+     */
+    public <T extends PipelineContext> void registerContext(Class<T> type, T context) {
+        assert type != null;
+        if (context == null) {
+            throw new NullPointerException("Context to register cannot be null.");
+        }
+        contexts.put(type, context);
+    }
+    
+    /**
+     * Gets the application profiler.
+     * 
+     * @return 
+     */
+    public AppProfiler getProfiler() {
+        return prof;
     }
 
     /**
@@ -402,7 +522,7 @@ public class RenderManager {
         for (ViewPort vp : preViewPorts) {
             notifyRescale(vp, x, y);
         }
-        for (ViewPort vp : viewPorts) {        
+        for (ViewPort vp : viewPorts) {      
             notifyRescale(vp, x, y);
         }
         for (ViewPort vp : postViewPorts) {
@@ -422,6 +542,15 @@ public class RenderManager {
     public void setForcedMaterial(Material mat) {
         forcedMaterial = mat;
     }
+    
+    /**
+     * Gets the forced material.
+     * 
+     * @return 
+     */
+    public Material getForcedMaterial() {
+        return forcedMaterial;
+    }
 
     /**
      * Returns the forced render state previously set with
@@ -628,7 +757,33 @@ public class RenderManager {
      * @see com.jme3.material.Material#render(com.jme3.scene.Geometry, com.jme3.renderer.RenderManager)
      */
     public void renderGeometry(Geometry geom) {
-        if (renderFilter != null && !renderFilter.test(geom)) return;
+        
+        if (renderFilter != null && !renderFilter.test(geom)) {
+            return;
+        }
+        
+        LightList lightList = geom.getWorldLightList();
+        if (lightFilter != null) {
+            filteredLightList.clear();
+            lightFilter.filterLights(geom, filteredLightList);
+            lightList = filteredLightList;
+        }
+        
+        renderGeometry(geom, lightList);
+        
+    }
+    
+    /**
+     * 
+     * @param geom
+     * @param lightList 
+     */
+    public void renderGeometry(Geometry geom, LightList lightList) {
+        
+        if (renderFilter != null && !renderFilter.test(geom)) {
+            return;
+        }
+        
         this.renderer.pushDebugGroup(geom.getName());
         if (geom.isIgnoreTransform()) {
             setWorldMatrix(Matrix4f.IDENTITY);
@@ -636,23 +791,15 @@ public class RenderManager {
             setWorldMatrix(geom.getWorldMatrix());
         }
 
-        // Use material override to pass the current target index (used in api such as GL ES that do not support glDrawBuffer)
+        // Use material override to pass the current target index (used in api such as GL ES
+        // that do not support glDrawBuffer)
         FrameBuffer currentFb = this.renderer.getCurrentFrameBuffer();
         if (currentFb != null && !currentFb.isMultiTarget()) {
             this.boundDrawBufferId.setValue(currentFb.getTargetIndex());
         }
 
-        // Perform light filtering if we have a light filter.
-        LightList lightList = geom.getWorldLightList();
-
-        if (lightFilter != null) {
-            filteredLightList.clear();
-            lightFilter.filterLights(geom, filteredLightList);
-            lightList = filteredLightList;
-        }
-
         Material material = geom.getMaterial();
-        
+
         // If forcedTechnique exists, we try to force it for the render.
         // If it does not exist in the mat def, we check for forcedMaterial and render the geom if not null.
         // Otherwise, the geometry is not rendered.
@@ -736,7 +883,7 @@ public class RenderManager {
                 preloadScene(children.get(i));
             }
         } else if (scene instanceof Geometry) {
-            // add to the render queue
+            // addUserEvent to the render queue
             Geometry gm = (Geometry) scene;
             if (gm.getMaterial() == null) {
                 throw new IllegalStateException("No material is set for Geometry: " + gm.getName());
@@ -755,7 +902,7 @@ public class RenderManager {
             }
         }
     }
-
+    
     /**
      * Flattens the given scene graph into the ViewPort's RenderQueue,
      * checking for culling as the call goes down the graph recursively.
@@ -786,10 +933,10 @@ public class RenderManager {
      *     contain the flattened scene graph.
      */
     public void renderScene(Spatial scene, ViewPort vp) {
-        //reset of the camera plane state for proper culling
-        //(must be 0 for the first note of the scene to be rendered)
+        // reset of the camera plane state for proper culling
+        // (must be 0 for the first note of the scene to be rendered)
         vp.getCamera().setPlaneState(0);
-        //rendering the scene
+        // queue the scene for rendering
         renderSubScene(scene, vp);
     }
 
@@ -800,12 +947,10 @@ public class RenderManager {
      * @param vp the ViewPort to render in (not null)
      */
     private void renderSubScene(Spatial scene, ViewPort vp) {
-
-        // check culling first.
+        // check culling first
         if (!scene.checkCulling(vp.getCamera())) {
             return;
         }
-
         scene.runControlRender(this, vp);
         if (scene instanceof Node) {
             // Recurse for all children
@@ -819,12 +964,11 @@ public class RenderManager {
                 renderSubScene(children.get(i), vp);
             }
         } else if (scene instanceof Geometry) {
-            // add to the render queue
+            // addUserEvent to the render queue
             Geometry gm = (Geometry) scene;
             if (gm.getMaterial() == null) {
                 throw new IllegalStateException("No material is set for Geometry: " + gm.getName());
             }
-
             vp.getQueue().addToQueue(gm, scene.getQueueBucket());
         }
     }
@@ -1038,7 +1182,7 @@ public class RenderManager {
     }
 
     private void setViewPort(Camera cam) {
-        // this will make sure to update viewport only if needed
+        // this will make sure to clearReservations viewport only if needed
         if (cam != prevCam || cam.isViewportChanged()) {
             viewX      = (int) (cam.getViewPortLeft() * cam.getWidth());
             viewY      = (int) (cam.getViewPortBottom() * cam.getHeight());
@@ -1120,141 +1264,54 @@ public class RenderManager {
     }
 
     /**
-     * Renders the {@link ViewPort}.
-     *
-     * <p>If the ViewPort is {@link ViewPort#isEnabled() disabled}, this method
-     * returns immediately. Otherwise, the ViewPort is rendered by
-     * the following process:<br>
-     * <ul>
-     * <li>All {@link SceneProcessor scene processors} that are attached
-     * to the ViewPort are {@link SceneProcessor#initialize(com.jme3.renderer.RenderManager,
-     * com.jme3.renderer.ViewPort) initialized}.
-     * </li>
-     * <li>The SceneProcessors' {@link SceneProcessor#preFrame(float) } method
-     * is called.</li>
-     * <li>The ViewPort's {@link ViewPort#getOutputFrameBuffer() output framebuffer}
-     * is set on the Renderer</li>
-     * <li>The camera is set on the renderer, including its view port parameters.
-     * (see {@link #setCamera(com.jme3.renderer.Camera, boolean) })</li>
-     * <li>Any buffers that the ViewPort requests to be cleared are cleared
-     * and the {@link ViewPort#getBackgroundColor() background color} is set</li>
-     * <li>Every scene that is attached to the ViewPort is flattened into
-     * the ViewPort's render queue
-     * (see {@link #renderViewPortQueues(com.jme3.renderer.ViewPort, boolean) })
-     * </li>
-     * <li>The SceneProcessors' {@link SceneProcessor#postQueue(com.jme3.renderer.queue.RenderQueue) }
-     * method is called.</li>
-     * <li>The render queue is sorted and then flushed, sending
-     * rendering commands to the underlying Renderer implementation.
-     * (see {@link #flushQueue(com.jme3.renderer.ViewPort) })</li>
-     * <li>The SceneProcessors' {@link SceneProcessor#postFrame(com.jme3.texture.FrameBuffer) }
-     * method is called.</li>
-     * <li>The translucent queue of the ViewPort is sorted and then flushed
-     * (see {@link #renderTranslucentQueue(com.jme3.renderer.ViewPort) })</li>
-     * <li>If any objects remained in the render queue, they are removed
-     * from the queue. This is generally objects added to the
-     * {@link RenderQueue#renderQueue(com.jme3.renderer.queue.RenderQueue.Bucket,
-     * com.jme3.renderer.RenderManager, com.jme3.renderer.Camera)
-     * shadow queue}
-     * which were not rendered because of a missing shadow renderer.</li>
-     * </ul>
-     *
-     * @param vp View port to render
-     * @param tpf Time per frame value
+     * Applies the ViewPort's Camera and FrameBuffer in preparation
+     * for rendering.
+     * 
+     * @param vp 
      */
-    public void renderViewPort(ViewPort vp, float tpf) {
-        if (!vp.isEnabled()) {
-            return;
-        }
-        if (prof != null) {
-            prof.vpStep(VpStep.BeginRender, vp, null);
-        }
-
-        SafeArrayList<SceneProcessor> processors = vp.getProcessors();
-        if (processors.isEmpty()) {
-            processors = null;
-        }
-
-        if (processors != null) {
-            if (prof != null) {
-                prof.vpStep(VpStep.PreFrame, vp, null);
-            }
-            for (SceneProcessor proc : processors.getArray()) {
-                if (!proc.isInitialized()) {
-                    proc.initialize(this, vp);
-                }
-                proc.setProfiler(this.prof);
-                if (prof != null) {
-                    prof.spStep(SpStep.ProcPreFrame, proc.getClass().getSimpleName());
-                }
-                proc.preFrame(tpf);
-            }
-        }
-
+    public void applyViewPort(ViewPort vp) {
         renderer.setFrameBuffer(vp.getOutputFrameBuffer());
         setCamera(vp.getCamera(), false);
         if (vp.isClearDepth() || vp.isClearColor() || vp.isClearStencil()) {
             if (vp.isClearColor()) {
                 renderer.setBackgroundColor(vp.getBackgroundColor());
             }
-            renderer.clearBuffers(vp.isClearColor(),
-                    vp.isClearDepth(),
-                    vp.isClearStencil());
-        }
-
-        if (prof != null) {
-            prof.vpStep(VpStep.RenderScene, vp, null);
+            renderer.clearBuffers(vp.isClearColor(), vp.isClearDepth(), vp.isClearStencil());
         }
-        List<Spatial> scenes = vp.getScenes();
-        for (int i = scenes.size() - 1; i >= 0; i--) {
-            renderScene(scenes.get(i), vp);
-        }
-
-        if (processors != null) {
-            if (prof != null) {
-                prof.vpStep(VpStep.PostQueue, vp, null);
-            }
-            for (SceneProcessor proc : processors.getArray()) {
-                if (prof != null) {
-                    prof.spStep(SpStep.ProcPostQueue, proc.getClass().getSimpleName());
-                }
-                proc.postQueue(vp.getQueue());
-            }
+    }
+    
+    /**
+     * Renders the {@link ViewPort} using the ViewPort's {@link RenderPipeline}.
+     * <p>
+     * If the ViewPort's RenderPipeline is null, the pipeline returned by
+     * {@link #getPipeline()} is used instead.
+     * <p>
+     * If the ViewPort is disabled, no rendering will occur.
+     *
+     * @param vp View port to render
+     * @param tpf Time per frame value
+     */
+    public void renderViewPort(ViewPort vp, float tpf) {
+        if (!vp.isEnabled()) {
+            return;
+        }        
+        RenderPipeline pipeline = vp.getPipeline();
+        if (pipeline == null) {
+            pipeline = defaultPipeline;
         }
-
-        if (prof != null) {
-            prof.vpStep(VpStep.FlushQueue, vp, null);
+        PipelineContext context = pipeline.fetchPipelineContext(this);
+        if (context == null) {
+            throw new NullPointerException("Failed to fetch pipeline context.");
         }
-        flushQueue(vp);
-
-        if (processors != null) {
-            if (prof != null) {
-                prof.vpStep(VpStep.PostFrame, vp, null);
-            }
-            for (SceneProcessor proc : processors.getArray()) {
-                if (prof != null) {
-                    prof.spStep(SpStep.ProcPostFrame, proc.getClass().getSimpleName());
-                }
-                proc.postFrame(vp.getOutputFrameBuffer());
-            }
-            if (prof != null) {
-                prof.vpStep(VpStep.ProcEndRender, vp, null);
-            }
+        if (!context.startViewPortRender(this, vp)) {
+            usedContexts.add(context);
         }
-        //renders the translucent objects queue after processors have been rendered
-        renderTranslucentQueue(vp);
-        // clear any remaining spatials that were not rendered.
-        clearQueue(vp);
-
-        /*
-         * the call to setCamera will indirectly cause a clipRect to be set, must be cleared to avoid surprising results
-         * if renderer#copyFrameBuffer is used later
-         */
-        renderer.clearClipRect();
-
-        if (prof != null) {
-            prof.vpStep(VpStep.EndRender, vp, null);
+        if (!pipeline.hasRenderedThisFrame()) {
+            usedPipelines.add(pipeline);
+            pipeline.startRenderFrame(this);
         }
+        pipeline.pipelineRender(this, context, vp, tpf);
+        context.endViewPortRender(this, vp);
     }
 
     /**
@@ -1276,7 +1333,7 @@ public class RenderManager {
         if (renderer instanceof NullRenderer) {
             return;
         }
-
+        
         uniformBindingManager.newFrame();
 
         if (prof != null) {
@@ -1308,12 +1365,22 @@ public class RenderManager {
                 renderViewPort(vp, tpf);
             }
         }
+        
+        // cleanup for used render pipelines and pipeline contexts only
+        for (PipelineContext c : usedContexts) {
+            c.endContextRenderFrame(this);
+        }
+        for (RenderPipeline p : usedPipelines) {
+            p.endRenderFrame(this);
+        }
+        usedContexts.clear();
+        usedPipelines.clear();
+        
     }
 
-
     /**
      * Returns true if the draw buffer target id is passed to the shader.
-     * 
+     *
      * @return True if the draw buffer target id is passed to the shaders.
      */
     public boolean getPassDrawBufferTargetIdToShaders() {
@@ -1324,7 +1391,7 @@ public class RenderManager {
      * Enable or disable passing the draw buffer target id to the shaders. This
      * is needed to handle FrameBuffer.setTargetIndex correctly in some
      * backends.
-     * 
+     *
      * @param v
      *            True to enable, false to disable (default is true)
      */
@@ -1337,11 +1404,12 @@ public class RenderManager {
             this.forcedOverrides.remove(boundDrawBufferId);
         }
     }
+    
     /**
      * Set a render filter. Every geometry will be tested against this filter
      * before rendering and will only be rendered if the filter returns true.
      * 
-     * @param filter
+     * @param filter the render filter
      */
     public void setRenderFilter(Predicate<Geometry> filter) {
         renderFilter = filter;
@@ -1350,7 +1418,7 @@ public class RenderManager {
     /**
      * Returns the render filter that the RenderManager is currently using
      * 
-     * @return the render filter
+     * @return the render filter 
      */
     public Predicate<Geometry> getRenderFilter() {
         return renderFilter;

+ 29 - 1
jme3-core/src/main/java/com/jme3/renderer/ViewPort.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,6 +31,7 @@
  */
 package com.jme3.renderer;
 
+import com.jme3.renderer.pipeline.RenderPipeline;
 import com.jme3.math.ColorRGBA;
 import com.jme3.post.SceneProcessor;
 import com.jme3.renderer.queue.RenderQueue;
@@ -84,6 +85,10 @@ public class ViewPort {
      * Scene processors currently applied.
      */
     protected final SafeArrayList<SceneProcessor> processors = new SafeArrayList<>(SceneProcessor.class);
+    /**
+     * Dedicated pipeline.
+     */
+    protected RenderPipeline pipeline;
     /**
      * FrameBuffer for output.
      */
@@ -424,5 +429,28 @@ public class ViewPort {
     public boolean isEnabled() {
         return enabled;
     }
+    
+    /**
+     * Sets the pipeline used by this viewport for rendering.
+     * <p>
+     * If null, the render manager's default pipeline will be used
+     * to render this viewport.
+     * <p>
+     * default=null
+     * 
+     * @param pipeline pipeline, or null to use render manager's pipeline
+     */
+    public void setPipeline(RenderPipeline pipeline) {
+        this.pipeline = pipeline;
+    }
+    
+    /**
+     * Gets the framegraph used by this viewport for rendering.
+     * 
+     * @return 
+     */
+    public RenderPipeline getPipeline() {
+        return pipeline;
+    }
 
 }

+ 62 - 0
jme3-core/src/main/java/com/jme3/renderer/pipeline/DefaultPipelineContext.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.renderer.pipeline;
+
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Default implementation of AbstractPipelineContext that
+ * does nothing extra.
+ * 
+ * @author codex
+ */
+public class DefaultPipelineContext implements PipelineContext {
+
+    // decided to use an atomic boolean, since it is less hassle
+    private final AtomicBoolean rendered = new AtomicBoolean(false);
+    
+    @Override
+    public boolean startViewPortRender(RenderManager rm, ViewPort vp) {
+        return rendered.getAndSet(true);
+    }
+    
+    @Override
+    public void endViewPortRender(RenderManager rm, ViewPort vp) {}
+    
+    @Override
+    public void endContextRenderFrame(RenderManager rm) {
+        rendered.set(false);
+    }
+    
+}

+ 159 - 0
jme3-core/src/main/java/com/jme3/renderer/pipeline/ForwardPipeline.java

@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.renderer.pipeline;
+
+import com.jme3.post.SceneProcessor;
+import com.jme3.profile.AppProfiler;
+import com.jme3.profile.SpStep;
+import com.jme3.profile.VpStep;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Spatial;
+import com.jme3.util.SafeArrayList;
+import java.util.List;
+
+/**
+ * Port of the standard forward renderer to a pipeline.
+ * 
+ * @author codex
+ */
+public class ForwardPipeline implements RenderPipeline {
+
+    private boolean rendered = false;
+    
+    @Override
+    public PipelineContext fetchPipelineContext(RenderManager rm) {
+        return rm.getDefaultContext();
+    }
+    
+    @Override
+    public boolean hasRenderedThisFrame() {
+        return rendered;
+    }
+    
+    @Override
+    public void startRenderFrame(RenderManager rm) {}
+    
+    @Override
+    public void pipelineRender(RenderManager rm, PipelineContext context, ViewPort vp, float tpf) {
+        
+        AppProfiler prof = rm.getProfiler();
+        
+        SafeArrayList<SceneProcessor> processors = vp.getProcessors();
+        if (processors.isEmpty()) {
+            processors = null;
+        }
+        
+        if (processors != null) {
+            if (prof != null) {
+                prof.vpStep(VpStep.PreFrame, vp, null);
+            }
+            for (SceneProcessor p : processors.getArray()) {
+                if (!p.isInitialized()) {
+                    p.initialize(rm, vp);
+                }
+                p.setProfiler(prof);
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPreFrame, p.getClass().getSimpleName());
+                }
+                p.preFrame(tpf);
+            }
+        }
+        
+        rm.applyViewPort(vp);
+        
+        if (prof != null) {
+            prof.vpStep(VpStep.RenderScene, vp, null);
+        }
+        // flatten scenes into render queue
+        List<Spatial> scenes = vp.getScenes();
+        for (int i = scenes.size() - 1; i >= 0; i--) {
+            rm.renderScene(scenes.get(i), vp);
+        }
+        if (processors != null) {
+            if (prof != null) {
+                prof.vpStep(VpStep.PostQueue, vp, null);
+            }
+            for (SceneProcessor p : processors.getArray()) {
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPostQueue, p.getClass().getSimpleName());
+                }
+                p.postQueue(vp.getQueue());
+            }
+        }
+
+        if (prof != null) {
+            prof.vpStep(VpStep.FlushQueue, vp, null);
+        }
+        rm.flushQueue(vp);
+
+        if (processors != null) {
+            if (prof != null) {
+                prof.vpStep(VpStep.PostFrame, vp, null);
+            }
+            for (SceneProcessor proc : processors.getArray()) {
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPostFrame, proc.getClass().getSimpleName());
+                }
+                proc.postFrame(vp.getOutputFrameBuffer());
+            }
+            if (prof != null) {
+                prof.vpStep(VpStep.ProcEndRender, vp, null);
+            }
+        }
+
+        // render the translucent objects queue after processors have been rendered
+        rm.renderTranslucentQueue(vp);
+
+        // clear any remaining spatials that were not rendered.
+        rm.clearQueue(vp);
+        
+        rendered = true;
+
+        /*
+         * the call to setCamera will indirectly cause a clipRect to be set, must be cleared to avoid surprising results
+         * if renderer#copyFrameBuffer is used later
+         */
+        rm.getRenderer().clearClipRect();
+
+        if (prof != null) {
+            prof.vpStep(VpStep.EndRender, vp, null);
+        }
+        
+    }
+    
+    @Override
+    public void endRenderFrame(RenderManager rm) {
+        rendered = false;
+    }
+    
+}

+ 42 - 0
jme3-core/src/main/java/com/jme3/renderer/pipeline/NullPipeline.java

@@ -0,0 +1,42 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.jme3.renderer.pipeline;
+
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+
+/**
+ * Render pipeline that performs no rendering.
+ * 
+ * @author codex
+ */
+public class NullPipeline implements RenderPipeline {
+    
+    private boolean rendered = false;
+    
+    @Override
+    public PipelineContext fetchPipelineContext(RenderManager rm) {
+        return rm.getDefaultContext();
+    }
+
+    @Override
+    public boolean hasRenderedThisFrame() {
+        return rendered;
+    }
+
+    @Override
+    public void startRenderFrame(RenderManager rm) {}
+
+    @Override
+    public void pipelineRender(RenderManager rm, PipelineContext context, ViewPort vp, float tpf) {
+        rendered = true;
+    }
+
+    @Override
+    public void endRenderFrame(RenderManager rm) {
+        rendered = false;
+    }
+    
+}

+ 70 - 0
jme3-core/src/main/java/com/jme3/renderer/pipeline/PipelineContext.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.renderer.pipeline;
+
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+
+/**
+ * Handles objects globally for a single type of RenderPipeline.
+ * 
+ * @author codex
+ */
+public interface PipelineContext {
+    
+    /**
+     * Called when a ViewPort rendering session starts that this context
+     * is participating in.
+     * 
+     * @param rm
+     * @param vp viewport being rendered
+     * @return true if this context has already rendered a viewport this frame
+     */
+    public boolean startViewPortRender(RenderManager rm, ViewPort vp);
+    
+    /**
+     * Called when viewport rendering session ends that this context
+     * is participating in.
+     * 
+     * @param rm 
+     * @param vp viewport being rendered
+     */
+    public void endViewPortRender(RenderManager rm, ViewPort vp);
+    
+    /**
+     * Called at the end of a render frame this context participated in.
+     * 
+     * @param rm 
+     */
+    public void endContextRenderFrame(RenderManager rm);
+    
+}

+ 88 - 0
jme3-core/src/main/java/com/jme3/renderer/pipeline/RenderPipeline.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.renderer.pipeline;
+
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+
+/**
+ * Pipeline for rendering a ViewPort.
+ * 
+ * @author codex
+ * @param <T>
+ */
+public interface RenderPipeline <T extends PipelineContext> {
+    
+    /**
+     * Fetches the PipelineContext this pipeline requires for rendering
+     * from the RenderManager.
+     * 
+     * @param rm
+     * @return pipeline context (not null)
+     */
+    public T fetchPipelineContext(RenderManager rm);
+    
+    /**
+     * Returns true if this pipeline has rendered a viewport this render frame.
+     * 
+     * @return 
+     */
+    public boolean hasRenderedThisFrame();
+    
+    /**
+     * Called before this pipeline is rendered for the first time this frame.
+     * <p>
+     * Only called if the pipeline will actually be rendered.
+     * 
+     * @param rm 
+     */
+    public void startRenderFrame(RenderManager rm);
+    
+    /**
+     * Renders the pipeline.
+     * 
+     * @param rm
+     * @param context
+     * @param vp
+     * @param tpf 
+     */
+    public void pipelineRender(RenderManager rm, T context, ViewPort vp, float tpf);
+    
+    /**
+     * Called after all rendering is complete in a rendering frame this
+     * pipeline participated in.
+     * 
+     * @param rm 
+     */
+    public void endRenderFrame(RenderManager rm);
+    
+}

+ 14 - 2
jme3-core/src/main/java/com/jme3/renderer/queue/RenderQueue.java

@@ -264,7 +264,7 @@ public class RenderQueue {
         }
     }
 
-    private void renderGeometryList(GeometryList list, RenderManager rm, Camera cam, boolean clear) {
+    private void renderGeometryList(GeometryList list, RenderManager rm, Camera cam, boolean flush) {
         list.setCamera(cam); // select camera for sorting
         list.sort();
         for (int i = 0; i < list.size(); i++) {
@@ -273,7 +273,7 @@ public class RenderQueue {
             rm.renderGeometry(obj);
             obj.queueDistance = Float.NEGATIVE_INFINITY;
         }
-        if (clear) {
+        if (flush) {
             list.clear();
         }
     }
@@ -329,6 +329,18 @@ public class RenderQueue {
         }
         rm.getRenderer().popDebugGroup();
     }
+    
+    public GeometryList getList(Bucket bucket) {
+        switch (bucket) {
+            case Opaque: return opaqueList;
+            case Gui: return guiList;
+            case Transparent: return transparentList;
+            case Translucent: return translucentList;
+            case Sky: return skyList;
+            default:
+                throw new UnsupportedOperationException();
+        }
+    }
 
     public void clear() {
         opaqueList.clear();

+ 33 - 2
jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java

@@ -179,7 +179,6 @@ public class FrameBuffer extends NativeObject {
             return this.layer;
         }
     }
-
     
     public static class FrameBufferTextureTarget extends RenderBuffer {
         private FrameBufferTextureTarget(){}
@@ -260,12 +259,44 @@ public class FrameBuffer extends NativeObject {
         colorBuf.slot=colorBufs.size();
         colorBufs.add(colorBuf);
     }
-
+    
     public void addColorTarget(FrameBufferTextureTarget colorBuf){
         // checkSetTexture(colorBuf.getTexture(), false);  // TODO: this won't work for levels.
         colorBuf.slot=colorBufs.size();
         colorBufs.add(colorBuf);
     }
+    
+    /**
+     * Replaces the color target at the index.
+     * <p>
+     * A color target must already exist at the index, otherwise
+     * an exception will be thrown.
+     * 
+     * @param i index of color target to replace
+     * @param colorBuf color target to replace with
+     */
+    public void replaceColorTarget(int i, FrameBufferTextureTarget colorBuf) {
+        if (i < 0 || i >= colorBufs.size()) {
+            throw new IndexOutOfBoundsException("No color target exists to replace at index=" + i);
+        }
+        colorBuf.slot = i;
+        colorBufs.set(i, colorBuf);
+    }
+    
+    /**
+     * Removes the color target at the index.
+     * <p>
+     * Color targets above the removed target will have their
+     * slot indices shifted accordingly.
+     * 
+     * @param i 
+     */
+    public void removeColorTarget(int i) {
+        colorBufs.remove(i);
+        for (; i < colorBufs.size(); i++) {
+            colorBufs.get(i).slot = i;
+        }
+    }
 
     /**
      * Adds a texture to one of the color Buffers Array. It uses {@link TextureCubeMap} ordinal number for the

+ 1 - 1
jme3-core/src/main/resources/Common/MatDefs/Light/Deferred.j3md

@@ -61,4 +61,4 @@ MaterialDef Phong Lighting Deferred {
     Technique {
     }
 
-}
+}

+ 1 - 1
jme3-core/src/main/resources/Common/MatDefs/Light/Deferred.vert

@@ -9,4 +9,4 @@ void main(){
    texCoord = inTexCoord;
    vec4 pos = vec4(inPosition, 1.0);
    gl_Position = vec4(sign(pos.xy-vec2(0.5)), 0.0, 1.0);
-}
+}

+ 1 - 1
jme3-core/src/main/resources/Common/MatDefs/Light/GBuf.vert

@@ -70,4 +70,4 @@ void main(){
     #ifdef VERTEX_COLOR
       DiffuseSum *= inColor;
     #endif
-}
+}

+ 1 - 1
jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib

@@ -106,4 +106,4 @@ vec3 TransformWorldNormal(vec3 normal) {
 }
 
  
-#endif
+#endif

+ 1 - 1
jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java

@@ -968,4 +968,4 @@ public class J3MLoader implements AssetLoader {
             textureOption.applyToTexture(value, texture);
         }
     }
-}
+}

+ 1 - 1
jme3-effects/src/main/java/com/jme3/post/filters/DepthOfFieldFilter.java

@@ -243,4 +243,4 @@ public class DepthOfFieldFilter extends Filter {
         focusRange = ic.readFloat("focusRange", 10f);
         debugUnfocus = ic.readBoolean("debugUnfocus", false);
     }
-}
+}

+ 1 - 1
jme3-effects/src/main/resources/Common/MatDefs/Post/CartoonEdge.frag

@@ -55,4 +55,4 @@ void main(){
     color = mix (color,m_EdgeColor.rgb,edgeAmount);
    
     gl_FragColor = vec4(color, 1.0);
-}
+}

+ 1 - 1
jme3-effects/src/main/resources/Common/MatDefs/Post/CrossHatch.frag

@@ -51,4 +51,4 @@ void main() {
     vec4 paperColor = mix(m_PaperColor, texVal, m_ColorInfluencePaper);
  
     gl_FragColor = mix(paperColor, lineColor, linePixel);
-}
+}

+ 1 - 1
jme3-effects/src/main/resources/Common/MatDefs/Post/FXAA.frag

@@ -86,4 +86,4 @@ void main()
 {
     vec4 texVal = texture2D(m_Texture, texCoord);
     gl_FragColor = vec4(FxaaPixelShader(posPos, m_Texture, g_ResolutionInverse), texVal.a);
-}
+}

+ 1 - 1
jme3-examples/gradle.properties

@@ -2,4 +2,4 @@
 assertions = true
 
 # Build javadoc per Github issue #1366
-buildJavaDoc = true
+buildJavaDoc = true

+ 1 - 1
jme3-terrain/src/main/resources/Common/MatDefs/Terrain/HeightBasedTerrain.j3md

@@ -42,4 +42,4 @@ MaterialDef Terrain {
 
     Technique {
     }
-}
+}

+ 2 - 2
jme3-terrain/src/main/resources/Common/MatDefs/Terrain/Terrain.j3md

@@ -18,7 +18,7 @@ MaterialDef Terrain {
 	Technique {
 		VertexShader    GLSL300 GLSL150 GLSL100:   Common/MatDefs/Terrain/Terrain.vert
 		FragmentShader  GLSL300 GLSL150 GLSL100: Common/MatDefs/Terrain/Terrain.frag
-		
+            
 		WorldParameters {
 			WorldViewProjectionMatrix
 		}
@@ -31,4 +31,4 @@ MaterialDef Terrain {
 	
     Technique {
     }
-}
+}