1/*
2 * Copyright (c) 2009-2010 jMonkeyEngine
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * * Redistributions of source code must retain the above copyright
10 *   notice, this list of conditions and the following disclaimer.
11 *
12 * * Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 *
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 *   may be used to endorse or promote products derived from this software
18 *   without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32package com.jme3.post;
33
34import com.jme3.asset.AssetManager;
35import com.jme3.export.*;
36import com.jme3.material.Material;
37import com.jme3.renderer.*;
38import com.jme3.renderer.queue.RenderQueue;
39import com.jme3.texture.FrameBuffer;
40import com.jme3.texture.Image.Format;
41import com.jme3.texture.Texture2D;
42import com.jme3.ui.Picture;
43import java.io.IOException;
44import java.util.ArrayList;
45import java.util.Collection;
46import java.util.Iterator;
47import java.util.List;
48
49/**
50 * A FilterPostProcessor is a processor that can apply several {@link Filter}s to a rendered scene<br>
51 * It manages a list of filters that will be applied in the order in which they've been added to the list
52 * @author Rémy Bouquet aka Nehon
53 */
54public class FilterPostProcessor implements SceneProcessor, Savable {
55
56    private RenderManager renderManager;
57    private Renderer renderer;
58    private ViewPort viewPort;
59    private FrameBuffer renderFrameBufferMS;
60    private int numSamples = 1;
61    private FrameBuffer renderFrameBuffer;
62    private Texture2D filterTexture;
63    private Texture2D depthTexture;
64    private List<Filter> filters = new ArrayList<Filter>();
65    private AssetManager assetManager;
66    private Camera filterCam = new Camera(1, 1);
67    private Picture fsQuad;
68    private boolean computeDepth = false;
69    private FrameBuffer outputBuffer;
70    private int width;
71    private int height;
72    private float bottom;
73    private float left;
74    private float right;
75    private float top;
76    private int originalWidth;
77    private int originalHeight;
78    private int lastFilterIndex = -1;
79    private boolean cameraInit = false;
80
81    /**
82     * Create a FilterProcessor
83     * @param assetManager the assetManager
84     */
85    public FilterPostProcessor(AssetManager assetManager) {
86        this.assetManager = assetManager;
87    }
88
89    /**
90     * Don't use this constructor use {@link FilterPostProcessor(AssetManager assetManager)}<br>
91     * This constructor is used for serialization only
92     */
93    public FilterPostProcessor() {
94    }
95
96    /**
97     * Adds a filter to the filters list<br>
98     * @param filter the filter to add
99     */
100    public void addFilter(Filter filter) {
101        filters.add(filter);
102
103        if (isInitialized()) {
104            initFilter(filter, viewPort);
105        }
106
107        setFilterState(filter, filter.isEnabled());
108
109    }
110
111    /**
112     * removes this filters from the filters list
113     * @param filter
114     */
115    public void removeFilter(Filter filter) {
116        filters.remove(filter);
117        filter.cleanup(renderer);
118        updateLastFilterIndex();
119    }
120
121    public Iterator<Filter> getFilterIterator() {
122        return filters.iterator();
123    }
124
125    public void initialize(RenderManager rm, ViewPort vp) {
126        renderManager = rm;
127        renderer = rm.getRenderer();
128        viewPort = vp;
129        fsQuad = new Picture("filter full screen quad");
130
131        Camera cam = vp.getCamera();
132
133        //save view port diensions
134        left = cam.getViewPortLeft();
135        right = cam.getViewPortRight();
136        top = cam.getViewPortTop();
137        bottom = cam.getViewPortBottom();
138        originalWidth = cam.getWidth();
139        originalHeight = cam.getHeight();
140        //first call to reshape
141        reshape(vp, cam.getWidth(), cam.getHeight());
142    }
143
144    /**
145     * init the given filter
146     * @param filter
147     * @param vp
148     */
149    private void initFilter(Filter filter, ViewPort vp) {
150        filter.setProcessor(this);
151        if (filter.isRequiresDepthTexture()) {
152            if (!computeDepth && renderFrameBuffer != null) {
153                depthTexture = new Texture2D(width, height, Format.Depth24);
154                renderFrameBuffer.setDepthTexture(depthTexture);
155            }
156            computeDepth = true;
157            filter.init(assetManager, renderManager, vp, width, height);
158            filter.setDepthTexture(depthTexture);
159        } else {
160            filter.init(assetManager, renderManager, vp, width, height);
161        }
162    }
163
164    /**
165     * renders a filter on a fullscreen quad
166     * @param r
167     * @param buff
168     * @param mat
169     */
170    private void renderProcessing(Renderer r, FrameBuffer buff, Material mat) {
171        if (buff == outputBuffer) {
172            fsQuad.setWidth(width);
173            fsQuad.setHeight(height);
174            filterCam.resize(originalWidth, originalHeight, true);
175            fsQuad.setPosition(left * originalWidth, bottom * originalHeight);
176        } else {
177            fsQuad.setWidth(buff.getWidth());
178            fsQuad.setHeight(buff.getHeight());
179            filterCam.resize(buff.getWidth(), buff.getHeight(), true);
180            fsQuad.setPosition(0, 0);
181        }
182
183        if (mat.getAdditionalRenderState().isDepthWrite()) {
184            mat.getAdditionalRenderState().setDepthTest(false);
185            mat.getAdditionalRenderState().setDepthWrite(false);
186        }
187
188        fsQuad.setMaterial(mat);
189        fsQuad.updateGeometricState();
190
191        renderManager.setCamera(filterCam, true);
192        r.setFrameBuffer(buff);
193        r.clearBuffers(false, true, true);
194        renderManager.renderGeometry(fsQuad);
195
196    }
197
198    public boolean isInitialized() {
199        return viewPort != null;
200    }
201
202    public void postQueue(RenderQueue rq) {
203
204        for (Iterator<Filter> it = filters.iterator(); it.hasNext();) {
205            Filter filter = it.next();
206            if (filter.isEnabled()) {
207                filter.postQueue(renderManager, viewPort);
208            }
209        }
210
211    }
212    Picture pic = new Picture("debug");
213
214    /**
215     * iterate through the filter list and renders filters
216     * @param r
217     * @param sceneFb
218     */
219    private void renderFilterChain(Renderer r, FrameBuffer sceneFb) {
220        Texture2D tex = filterTexture;
221        FrameBuffer buff = sceneFb;
222        boolean msDepth = depthTexture != null && depthTexture.getImage().getMultiSamples() > 1;
223        for (int i = 0; i < filters.size(); i++) {
224            Filter filter = filters.get(i);
225            if (filter.isEnabled()) {
226                if (filter.getPostRenderPasses() != null) {
227                    for (Iterator<Filter.Pass> it1 = filter.getPostRenderPasses().iterator(); it1.hasNext();) {
228                        Filter.Pass pass = it1.next();
229                        pass.beforeRender();
230                        if (pass.requiresSceneAsTexture()) {
231                            pass.getPassMaterial().setTexture("Texture", tex);
232                            if (tex.getImage().getMultiSamples() > 1) {
233                                pass.getPassMaterial().setInt("NumSamples", tex.getImage().getMultiSamples());
234                            } else {
235                                pass.getPassMaterial().clearParam("NumSamples");
236
237                            }
238                        }
239                        if (pass.requiresDepthAsTexture()) {
240                            pass.getPassMaterial().setTexture("DepthTexture", depthTexture);
241                            if (msDepth) {
242                                pass.getPassMaterial().setInt("NumSamplesDepth", depthTexture.getImage().getMultiSamples());
243                            } else {
244                                pass.getPassMaterial().clearParam("NumSamplesDepth");
245                            }
246                        }
247                        renderProcessing(r, pass.getRenderFrameBuffer(), pass.getPassMaterial());
248                    }
249                }
250
251                filter.postFrame(renderManager, viewPort, buff, sceneFb);
252
253                Material mat = filter.getMaterial();
254                if (msDepth && filter.isRequiresDepthTexture()) {
255                    mat.setInt("NumSamplesDepth", depthTexture.getImage().getMultiSamples());
256                }
257
258                if (filter.isRequiresSceneTexture()) {
259                    mat.setTexture("Texture", tex);
260                    if (tex.getImage().getMultiSamples() > 1) {
261                        mat.setInt("NumSamples", tex.getImage().getMultiSamples());
262                    } else {
263                        mat.clearParam("NumSamples");
264                    }
265                }
266
267                buff = outputBuffer;
268                if (i != lastFilterIndex) {
269                    buff = filter.getRenderFrameBuffer();
270                    tex = filter.getRenderedTexture();
271
272                }
273                renderProcessing(r, buff, mat);
274            }
275        }
276    }
277
278    public void postFrame(FrameBuffer out) {
279
280        FrameBuffer sceneBuffer = renderFrameBuffer;
281        if (renderFrameBufferMS != null && !renderer.getCaps().contains(Caps.OpenGL31)) {
282            renderer.copyFrameBuffer(renderFrameBufferMS, renderFrameBuffer);
283        } else if (renderFrameBufferMS != null) {
284            sceneBuffer = renderFrameBufferMS;
285        }
286        renderFilterChain(renderer, sceneBuffer);
287        renderer.setFrameBuffer(outputBuffer);
288
289        //viewport can be null if no filters are enabled
290        if (viewPort != null) {
291            renderManager.setCamera(viewPort.getCamera(), false);
292        }
293
294    }
295
296    public void preFrame(float tpf) {
297        if (filters.isEmpty() || lastFilterIndex == -1) {
298            //If the camera is initialized and there are no filter to render, the camera viewport is restored as it was
299            if (cameraInit) {
300                viewPort.getCamera().resize(originalWidth, originalHeight, true);
301                viewPort.getCamera().setViewPort(left, right, bottom, top);
302                viewPort.setOutputFrameBuffer(outputBuffer);
303                cameraInit = false;
304            }
305
306        } else {
307            if (renderFrameBufferMS != null) {
308                viewPort.setOutputFrameBuffer(renderFrameBufferMS);
309            } else {
310                viewPort.setOutputFrameBuffer(renderFrameBuffer);
311            }
312            //init of the camera if it wasn't already
313            if (!cameraInit) {
314                viewPort.getCamera().resize(width, height, true);
315                viewPort.getCamera().setViewPort(0, 1, 0, 1);
316            }
317        }
318
319        for (Iterator<Filter> it = filters.iterator(); it.hasNext();) {
320            Filter filter = it.next();
321            if (filter.isEnabled()) {
322                filter.preFrame(tpf);
323            }
324        }
325
326    }
327
328    /**
329     * sets the filter to enabled or disabled
330     * @param filter
331     * @param enabled
332     */
333    protected void setFilterState(Filter filter, boolean enabled) {
334        if (filters.contains(filter)) {
335            filter.enabled = enabled;
336            updateLastFilterIndex();
337        }
338    }
339
340    /**
341     * compute the index of the last filter to render
342     */
343    private void updateLastFilterIndex() {
344        lastFilterIndex = -1;
345        for (int i = filters.size() - 1; i >= 0 && lastFilterIndex == -1; i--) {
346            if (filters.get(i).isEnabled()) {
347                lastFilterIndex = i;
348                return;
349            }
350        }
351        if (lastFilterIndex == -1) {
352            cleanup();
353        }
354    }
355
356    public void cleanup() {
357        if (viewPort != null) {
358            //reseting the viewport camera viewport to its initial value
359            viewPort.getCamera().resize(originalWidth, originalHeight, true);
360            viewPort.getCamera().setViewPort(left, right, bottom, top);
361            viewPort.setOutputFrameBuffer(outputBuffer);
362            viewPort = null;
363            for (Filter filter : filters) {
364                filter.cleanup(renderer);
365            }
366        }
367
368    }
369
370    public void reshape(ViewPort vp, int w, int h) {
371        //this has no effect at first init but is useful when resizing the canvas with multi views
372        Camera cam = vp.getCamera();
373        cam.setViewPort(left, right, bottom, top);
374        //resizing the camera to fit the new viewport and saving original dimensions
375        cam.resize(w, h, false);
376        left = cam.getViewPortLeft();
377        right = cam.getViewPortRight();
378        top = cam.getViewPortTop();
379        bottom = cam.getViewPortBottom();
380        originalWidth = w;
381        originalHeight = h;
382        cam.setViewPort(0, 1, 0, 1);
383
384        //computing real dimension of the viewport and resizing he camera
385        width = (int) (w * (Math.abs(right - left)));
386        height = (int) (h * (Math.abs(bottom - top)));
387        width = Math.max(1, width);
388        height = Math.max(1, height);
389        cam.resize(width, height, false);
390        cameraInit = true;
391        computeDepth = false;
392
393        if (renderFrameBuffer == null) {
394            outputBuffer = viewPort.getOutputFrameBuffer();
395        }
396
397        Collection<Caps> caps = renderer.getCaps();
398
399        //antialiasing on filters only supported in opengl 3 due to depth read problem
400        if (numSamples > 1 && caps.contains(Caps.FrameBufferMultisample)) {
401            renderFrameBufferMS = new FrameBuffer(width, height, numSamples);
402            if (caps.contains(Caps.OpenGL31)) {
403                Texture2D msColor = new Texture2D(width, height, numSamples, Format.RGBA8);
404                Texture2D msDepth = new Texture2D(width, height, numSamples, Format.Depth);
405                renderFrameBufferMS.setDepthTexture(msDepth);
406                renderFrameBufferMS.setColorTexture(msColor);
407                filterTexture = msColor;
408                depthTexture = msDepth;
409            } else {
410                renderFrameBufferMS.setDepthBuffer(Format.Depth);
411                renderFrameBufferMS.setColorBuffer(Format.RGBA8);
412            }
413        }
414
415        if (numSamples <= 1 || !caps.contains(Caps.OpenGL31)) {
416            renderFrameBuffer = new FrameBuffer(width, height, 1);
417            renderFrameBuffer.setDepthBuffer(Format.Depth);
418            filterTexture = new Texture2D(width, height, Format.RGBA8);
419            renderFrameBuffer.setColorTexture(filterTexture);
420        }
421
422        for (Iterator<Filter> it = filters.iterator(); it.hasNext();) {
423            Filter filter = it.next();
424            initFilter(filter, vp);
425        }
426
427        if (renderFrameBufferMS != null) {
428            viewPort.setOutputFrameBuffer(renderFrameBufferMS);
429        } else {
430            viewPort.setOutputFrameBuffer(renderFrameBuffer);
431        }
432    }
433
434    /**
435     * return the number of samples for antialiasing
436     * @return numSamples
437     */
438    public int getNumSamples() {
439        return numSamples;
440    }
441
442    /**
443     *
444     * Removes all the filters from this processor
445     */
446    public void removeAllFilters() {
447        filters.clear();
448        updateLastFilterIndex();
449    }
450
451    /**
452     * Sets the number of samples for antialiasing
453     * @param numSamples the number of Samples
454     */
455    public void setNumSamples(int numSamples) {
456        if (numSamples <= 0) {
457            throw new IllegalArgumentException("numSamples must be > 0");
458        }
459
460        this.numSamples = numSamples;
461    }
462
463    /**
464     * Sets the asset manager for this processor
465     * @param assetManager
466     */
467    public void setAssetManager(AssetManager assetManager) {
468        this.assetManager = assetManager;
469    }
470
471    public void write(JmeExporter ex) throws IOException {
472        OutputCapsule oc = ex.getCapsule(this);
473        oc.write(numSamples, "numSamples", 0);
474        oc.writeSavableArrayList((ArrayList) filters, "filters", null);
475    }
476
477    public void read(JmeImporter im) throws IOException {
478        InputCapsule ic = im.getCapsule(this);
479        numSamples = ic.readInt("numSamples", 0);
480        filters = ic.readSavableArrayList("filters", null);
481        for (Filter filter : filters) {
482            filter.setProcessor(this);
483            setFilterState(filter, filter.isEnabled());
484        }
485        assetManager = im.getAssetManager();
486    }
487
488    /**
489     * For internal use only<br>
490     * returns the depth texture of the scene
491     * @return
492     */
493    public Texture2D getDepthTexture() {
494        return depthTexture;
495    }
496
497    /**
498     * For internal use only<br>
499     * returns the rendered texture of the scene
500     * @return
501     */
502    public Texture2D getFilterTexture() {
503        return filterTexture;
504    }
505}
506