/*
* Copyright (c) 2009-2012 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.post;
import com.jme3.asset.AssetManager;
import com.jme3.export.*;
import com.jme3.material.Material;
import com.jme3.profile.*;
import com.jme3.renderer.*;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.ui.Picture;
import com.jme3.util.SafeArrayList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* A FilterPostProcessor is a processor that can apply several {@link Filter}s to a rendered scene<br>
* It manages a list of filters that will be applied in the order in which they've been added to the list
* @author Rémy Bouquet aka Nehon
*/
public class FilterPostProcessor implements SceneProcessor, Savable {
public static final String FPP = FilterPostProcessor.class.getSimpleName();
private RenderManager renderManager;
private Renderer renderer;
private ViewPort viewPort;
private FrameBuffer renderFrameBufferMS;
private int numSamples = 1;
private FrameBuffer renderFrameBuffer;
private Texture2D filterTexture;
private Texture2D depthTexture;
private SafeArrayList<Filter> filters = new SafeArrayList<Filter>(Filter.class);
private AssetManager assetManager;
private Picture fsQuad;
private boolean computeDepth = false;
private FrameBuffer outputBuffer;
private int width;
private int height;
private float bottom;
private float left;
private float right;
private float top;
private int originalWidth;
private int originalHeight;
private int lastFilterIndex = -1;
private boolean cameraInit = false;
private boolean multiView = false;
private AppProfiler prof;
private Format fbFormat = Format.RGB111110F;
/**
* Create a FilterProcessor
* @param assetManager the assetManager
*/
public FilterPostProcessor(AssetManager assetManager) {
this.assetManager = assetManager;
}
/**
* Don't use this constructor, use {@link #FilterPostProcessor(AssetManager assetManager)}<br>
* This constructor is used for serialization only
*/
public FilterPostProcessor() {
}
/**
* Adds a filter to the filters list<br>
* @param filter the filter to add
*/
public void addFilter(Filter filter) {
if (filter == null) {
throw new IllegalArgumentException("Filter cannot be null.");
}
filters.add(filter);
if (isInitialized()) {
initFilter(filter, viewPort);
}
setFilterState(filter, filter.isEnabled());
}
/**
* removes this filters from the filters list
* @param filter
*/
public void removeFilter(Filter filter) {
if (filter == null) {
throw new IllegalArgumentException("Filter cannot be null.");
}
filters.remove(filter);
filter.cleanup(renderer);
updateLastFilterIndex();
}
public Iterator<Filter> getFilterIterator() {
return filters.iterator();
}
public void initialize(RenderManager rm, ViewPort vp) {
renderManager = rm;
renderer = rm.getRenderer();
viewPort = vp;
fsQuad = new Picture("filter full screen quad");
fsQuad.setWidth(1);
fsQuad.setHeight(1);
if (fbFormat == Format.RGB111110F && !renderer.getCaps().contains(Caps.PackedFloatTexture)) {
fbFormat = Format.RGB8;
}
Camera cam = vp.getCamera();
//save view port diensions
left = cam.getViewPortLeft();
right = cam.getViewPortRight();
top = cam.getViewPortTop();
bottom = cam.getViewPortBottom();
originalWidth = cam.getWidth();
originalHeight = cam.getHeight();
//first call to reshape
reshape(vp, cam.getWidth(), cam.getHeight());
}
/**
* init the given filter
* @param filter
* @param vp
*/
private void initFilter(Filter filter, ViewPort vp) {
filter.setProcessor(this);
if (filter.isRequiresDepthTexture()) {
if (!computeDepth && renderFrameBuffer != null) {
depthTexture = new Texture2D(width, height, Format.Depth24);
renderFrameBuffer.setDepthTexture(depthTexture);
}
computeDepth = true;
filter.init(assetManager, renderManager, vp, width, height);
filter.setDepthTexture(depthTexture);
} else {
filter.init(assetManager, renderManager, vp, width, height);
}
}
/**
* renders a filter on a fullscreen quad
* @param r
* @param buff
* @param mat
*/
private void renderProcessing(Renderer r, FrameBuffer buff, Material mat) {
if (buff == outputBuffer) {
viewPort.getCamera().resize(originalWidth, originalHeight, false);
viewPort.getCamera().setViewPort(left, right, bottom, top);
// update is redundant because resize and setViewPort will both
// run the appropriate (and same) onXXXChange methods.
// Also, update() updates some things that don't need to be updated.
//viewPort.getCamera().update();
renderManager.setCamera( viewPort.getCamera(), false);
if (mat.getAdditionalRenderState().isDepthWrite()) {
mat.getAdditionalRenderState().setDepthTest(false);
mat.getAdditionalRenderState().setDepthWrite(false);
}
}else{
viewPort.getCamera().resize(buff.getWidth(), buff.getHeight(), false);
viewPort.getCamera().setViewPort(0, 1, 0, 1);
// update is redundant because resize and setViewPort will both
// run the appropriate (and same) onXXXChange methods.
// Also, update() updates some things that don't need to be updated.
//viewPort.getCamera().update();
renderManager.setCamera( viewPort.getCamera(), false);
mat.getAdditionalRenderState().setDepthTest(true);
mat.getAdditionalRenderState().setDepthWrite(true);
}
fsQuad.setMaterial(mat);
fsQuad.updateGeometricState();
r.setFrameBuffer(buff);
r.clearBuffers(true, true, true);
renderManager.renderGeometry(fsQuad);
}
public boolean isInitialized() {
return viewPort != null;
}
public void postQueue(RenderQueue rq) {
for (Filter filter : filters.getArray()) {
if (filter.isEnabled()) {
if (prof != null) prof.spStep(SpStep.ProcPostQueue, FPP, filter.getName());
filter.postQueue(rq);
}
}
}
/**
* iterate through the filter list and renders filters
* @param r
* @param sceneFb
*/
private void renderFilterChain(Renderer r, FrameBuffer sceneFb) {
Texture2D tex = filterTexture;
FrameBuffer buff = sceneFb;
boolean msDepth = depthTexture != null && depthTexture.getImage().getMultiSamples() > 1;
for (int i = 0; i < filters.size(); i++) {
Filter filter = filters.get(i);
if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName());
if (filter.isEnabled()) {
if (filter.getPostRenderPasses() != null) {
for (Iterator<Filter.Pass> it1 = filter.getPostRenderPasses().iterator(); it1.hasNext();) {
Filter.Pass pass = it1.next();
if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), pass.toString());
pass.beforeRender();
if (pass.requiresSceneAsTexture()) {
pass.getPassMaterial().setTexture("Texture", tex);
if (tex.getImage().getMultiSamples() > 1) {
pass.getPassMaterial().setInt("NumSamples", tex.getImage().getMultiSamples());
} else {
pass.getPassMaterial().clearParam("NumSamples");
}
}
if (pass.requiresDepthAsTexture()) {
pass.getPassMaterial().setTexture("DepthTexture", depthTexture);
if (msDepth) {
pass.getPassMaterial().setInt("NumSamplesDepth", depthTexture.getImage().getMultiSamples());
} else {
pass.getPassMaterial().clearParam("NumSamplesDepth");
}
}
renderProcessing(r, pass.getRenderFrameBuffer(), pass.getPassMaterial());
}
}
if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "postFrame");
filter.postFrame(renderManager, viewPort, buff, sceneFb);
Material mat = filter.getMaterial();
if (msDepth && filter.isRequiresDepthTexture()) {
mat.setInt("NumSamplesDepth", depthTexture.getImage().getMultiSamples());
}
if (filter.isRequiresSceneTexture()) {
mat.setTexture("Texture", tex);
if (tex.getImage().getMultiSamples() > 1) {
mat.setInt("NumSamples", tex.getImage().getMultiSamples());
} else {
mat.clearParam("NumSamples");
}
}
boolean wantsBilinear = filter.isRequiresBilinear();
if (wantsBilinear) {
tex.setMagFilter(Texture.MagFilter.Bilinear);
tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
}
buff = outputBuffer;
if (i != lastFilterIndex) {
buff = filter.getRenderFrameBuffer();
tex = filter.getRenderedTexture();
}
if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "render");
renderProcessing(r, buff, mat);
if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "postFilter");
filter.postFilter(r, buff);
if (wantsBilinear) {
tex.setMagFilter(Texture.MagFilter.Nearest);
tex.setMinFilter(Texture.MinFilter.NearestNoMipMaps);
}
}
}
}
public void postFrame(FrameBuffer out) {
FrameBuffer sceneBuffer = renderFrameBuffer;
if (renderFrameBufferMS != null && !renderer.getCaps().contains(Caps.OpenGL32)) {
renderer.copyFrameBuffer(renderFrameBufferMS, renderFrameBuffer, true);
} else if (renderFrameBufferMS != null) {
sceneBuffer = renderFrameBufferMS;
}
renderFilterChain(renderer, sceneBuffer);
renderer.setFrameBuffer(outputBuffer);
//viewport can be null if no filters are enabled
if (viewPort != null) {
renderManager.setCamera(viewPort.getCamera(), false);
}
}
public void preFrame(float tpf) {
if (filters.isEmpty() || lastFilterIndex == -1) {
//If the camera is initialized and there are no filter to render, the camera viewport is restored as it was
if (cameraInit) {
viewPort.getCamera().resize(originalWidth, originalHeight, true);
viewPort.getCamera().setViewPort(left, right, bottom, top);
viewPort.setOutputFrameBuffer(outputBuffer);
cameraInit = false;
}
} else {
setupViewPortFrameBuffer();
//if we are ina multiview situation we need to resize the camera
//to the viewportsize so that the backbuffer is rendered correctly
if (multiView) {
viewPort.getCamera().resize(width, height, false);
viewPort.getCamera().setViewPort(0, 1, 0, 1);
viewPort.getCamera().update();
renderManager.setCamera(viewPort.getCamera(), false);
}
}
for (Filter filter : filters.getArray()) {
if (filter.isEnabled()) {
if (prof != null) prof.spStep(SpStep.ProcPreFrame, FPP, filter.getName());
filter.preFrame(tpf);
}
}
}
/**
* sets the filter to enabled or disabled
* @param filter
* @param enabled
*/
protected void setFilterState(Filter filter, boolean enabled) {
if (filters.contains(filter)) {
filter.enabled = enabled;
updateLastFilterIndex();
}
}
/**
* compute the index of the last filter to render
*/
private void updateLastFilterIndex() {
lastFilterIndex = -1;
for (int i = filters.size() - 1; i >= 0 && lastFilterIndex == -1; i--) {
if (filters.get(i).isEnabled()) {
lastFilterIndex = i;
//the Fpp is initialized, but the viwport framebuffer is the
//original out framebuffer so we must recover from a situation
//where no filter was enabled. So we set th correc framebuffer
//on the viewport
if(isInitialized() && viewPort.getOutputFrameBuffer()==outputBuffer){
setupViewPortFrameBuffer();
}
return;
}
}
if (isInitialized() && lastFilterIndex == -1) {
//There is no enabled filter, we restore the original framebuffer
//to the viewport to bypass the fpp.
viewPort.setOutputFrameBuffer(outputBuffer);
}
}
public void cleanup() {
if (viewPort != null) {
//reseting the viewport camera viewport to its initial value
viewPort.getCamera().resize(originalWidth, originalHeight, true);
viewPort.getCamera().setViewPort(left, right, bottom, top);
viewPort.setOutputFrameBuffer(outputBuffer);
viewPort = null;
if(renderFrameBuffer != null){
renderFrameBuffer.dispose();
}
if(depthTexture!=null){
depthTexture.getImage().dispose();
}
filterTexture.getImage().dispose();
if(renderFrameBufferMS != null){
renderFrameBufferMS.dispose();
}
for (Filter filter : filters.getArray()) {
filter.cleanup(renderer);
}
}
}
@Override
public void setProfiler(AppProfiler profiler) {
this.prof = profiler;
}
public void reshape(ViewPort vp, int w, int h) {
Camera cam = vp.getCamera();
//this has no effect at first init but is useful when resizing the canvas with multi views
cam.setViewPort(left, right, bottom, top);
//resizing the camera to fit the new viewport and saving original dimensions
cam.resize(w, h, false);
left = cam.getViewPortLeft();
right = cam.getViewPortRight();
top = cam.getViewPortTop();
bottom = cam.getViewPortBottom();
originalWidth = w;
originalHeight = h;
//computing real dimension of the viewport and resizing the camera
width = (int) (w * (Math.abs(right - left)));
height = (int) (h * (Math.abs(bottom - top)));
width = Math.max(1, width);
height = Math.max(1, height);
//Testing original versus actual viewport dimension.
//If they are different we are in a multiview situation and
//camera must be handled differently
if(originalWidth!=width || originalHeight!=height){
multiView = true;
}
cameraInit = true;
computeDepth = false;
if (renderFrameBuffer == null && renderFrameBufferMS == null) {
outputBuffer = viewPort.getOutputFrameBuffer();
}
Collection<Caps> caps = renderer.getCaps();
//antialiasing on filters only supported in opengl 3 due to depth read problem
if (numSamples > 1 && caps.contains(Caps.FrameBufferMultisample)) {
renderFrameBufferMS = new FrameBuffer(width, height, numSamples);
if (caps.contains(Caps.OpenGL32)) {
Texture2D msColor = new Texture2D(width, height, numSamples, fbFormat);
Texture2D msDepth = new Texture2D(width, height, numSamples, Format.Depth);
renderFrameBufferMS.setDepthTexture(msDepth);
renderFrameBufferMS.setColorTexture(msColor);
filterTexture = msColor;
depthTexture = msDepth;
} else {
renderFrameBufferMS.setDepthBuffer(Format.Depth);
renderFrameBufferMS.setColorBuffer(fbFormat);
}
}
if (numSamples <= 1 || !caps.contains(Caps.OpenGL32)) {
renderFrameBuffer = new FrameBuffer(width, height, 1);
renderFrameBuffer.setDepthBuffer(Format.Depth);
filterTexture = new Texture2D(width, height, fbFormat);
renderFrameBuffer.setColorTexture(filterTexture);
}
for (Filter filter : filters.getArray()) {
initFilter(filter, vp);
}
setupViewPortFrameBuffer();
}
/**
* return the number of samples for antialiasing
* @return numSamples
*/
public int getNumSamples() {
return numSamples;
}
/**
*
* Removes all the filters from this processor
*/
public void removeAllFilters() {
filters.clear();
updateLastFilterIndex();
}
/**
* Sets the number of samples for antialiasing
* @param numSamples the number of Samples
*/
public void setNumSamples(int numSamples) {
if (numSamples <= 0) {
throw new IllegalArgumentException("numSamples must be > 0");
}
this.numSamples = numSamples;
}
/**
* Sets the asset manager for this processor
* @param assetManager
*/
public void setAssetManager(AssetManager assetManager) {
this.assetManager = assetManager;
}
public void setFrameBufferFormat(Format fbFormat) {
this.fbFormat = fbFormat;
}
public void write(JmeExporter ex) throws IOException {
OutputCapsule oc = ex.getCapsule(this);
oc.write(numSamples, "numSamples", 0);
oc.writeSavableArrayList(new ArrayList(filters), "filters", null);
}
public void read(JmeImporter im) throws IOException {
InputCapsule ic = im.getCapsule(this);
numSamples = ic.readInt("numSamples", 0);
filters = new SafeArrayList<Filter>(Filter.class, ic.readSavableArrayList("filters", null));
for (Filter filter : filters.getArray()) {
filter.setProcessor(this);
setFilterState(filter, filter.isEnabled());
}
assetManager = im.getAssetManager();
}
/**
* For internal use only<br>
* returns the depth texture of the scene
* @return the depth texture
*/
public Texture2D getDepthTexture() {
return depthTexture;
}
/**
* For internal use only<br>
* returns the rendered texture of the scene
* @return the filter texture
*/
public Texture2D getFilterTexture() {
return filterTexture;
}
/**
* returns the first filter in the list assignable form the given type
* @param <T>
* @param filterType the filter type
* @return a filter assignable form the given type
*/
public <T extends Filter> T getFilter(Class<T> filterType) {
for (Filter c : filters.getArray()) {
if (filterType.isAssignableFrom(c.getClass())) {
return (T) c;
}
}
return null;
}
/**
* returns an unmodifiable version of the filter list.
* @return the filters list
*/
public List<Filter> getFilterList(){
return Collections.unmodifiableList(filters);
}
private void setupViewPortFrameBuffer() {
if (renderFrameBufferMS != null) {
viewPort.setOutputFrameBuffer(renderFrameBufferMS);
} else {
viewPort.setOutputFrameBuffer(renderFrameBuffer);
}
}
}