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 */
32
33package com.jme3.post;
34
35import com.jme3.asset.AssetManager;
36import com.jme3.material.Material;
37import com.jme3.math.Vector2f;
38import com.jme3.renderer.*;
39import com.jme3.renderer.queue.RenderQueue;
40import com.jme3.texture.FrameBuffer;
41import com.jme3.texture.Image;
42import com.jme3.texture.Image.Format;
43import com.jme3.texture.Texture;
44import com.jme3.texture.Texture.MagFilter;
45import com.jme3.texture.Texture.MinFilter;
46import com.jme3.texture.Texture2D;
47import com.jme3.ui.Picture;
48import java.util.Collection;
49import java.util.logging.Logger;
50
51public class HDRRenderer implements SceneProcessor {
52
53    private static final int LUMMODE_NONE = 0x1,
54                             LUMMODE_ENCODE_LUM = 0x2,
55                             LUMMODE_DECODE_LUM = 0x3;
56
57    private Renderer renderer;
58    private RenderManager renderManager;
59    private ViewPort viewPort;
60    private static final Logger logger = Logger.getLogger(HDRRenderer.class.getName());
61
62    private Camera fbCam = new Camera(1, 1);
63
64    private FrameBuffer msFB;
65
66    private FrameBuffer mainSceneFB;
67    private Texture2D mainScene;
68    private FrameBuffer scene64FB;
69    private Texture2D scene64;
70    private FrameBuffer scene8FB;
71    private Texture2D scene8;
72    private FrameBuffer scene1FB[] = new FrameBuffer[2];
73    private Texture2D scene1[] = new Texture2D[2];
74
75    private Material hdr64;
76    private Material hdr8;
77    private Material hdr1;
78    private Material tone;
79
80    private Picture fsQuad;
81    private float time = 0;
82    private int curSrc = -1;
83    private int oppSrc = -1;
84    private float blendFactor = 0;
85
86    private int numSamples = 0;
87    private float exposure = 0.18f;
88    private float whiteLevel = 100f;
89    private float throttle = -1;
90    private int maxIterations = -1;
91    private Image.Format bufFormat = Format.RGB16F;
92
93    private MinFilter fbMinFilter = MinFilter.BilinearNoMipMaps;
94    private MagFilter fbMagFilter = MagFilter.Bilinear;
95    private AssetManager manager;
96
97    private boolean enabled = true;
98
99    public HDRRenderer(AssetManager manager, Renderer renderer){
100        this.manager = manager;
101        this.renderer = renderer;
102
103        Collection<Caps> caps = renderer.getCaps();
104        if (caps.contains(Caps.PackedFloatColorBuffer))
105            bufFormat = Format.RGB111110F;
106        else if (caps.contains(Caps.FloatColorBuffer))
107            bufFormat = Format.RGB16F;
108        else{
109            enabled = false;
110            return;
111        }
112    }
113
114    public boolean isEnabled() {
115        return enabled;
116    }
117
118    public void setSamples(int samples){
119        this.numSamples = samples;
120    }
121
122    public void setExposure(float exp){
123        this.exposure = exp;
124    }
125
126    public void setWhiteLevel(float whiteLevel){
127        this.whiteLevel = whiteLevel;
128    }
129
130    public void setMaxIterations(int maxIterations){
131        this.maxIterations = maxIterations;
132
133        // regenerate shaders if needed
134        if (hdr64 != null)
135            createLumShaders();
136    }
137
138    public void setThrottle(float throttle){
139        this.throttle = throttle;
140    }
141
142    public void setUseFastFilter(boolean fastFilter){
143        if (fastFilter){
144            fbMagFilter = MagFilter.Nearest;
145            fbMinFilter = MinFilter.NearestNoMipMaps;
146        }else{
147            fbMagFilter = MagFilter.Bilinear;
148            fbMinFilter = MinFilter.BilinearNoMipMaps;
149        }
150    }
151
152    public Picture createDisplayQuad(/*int mode, Texture tex*/){
153        if (scene64 == null)
154            return null;
155
156        Material mat = new Material(manager, "Common/MatDefs/Hdr/LogLum.j3md");
157//        if (mode == LUMMODE_ENCODE_LUM)
158//            mat.setBoolean("EncodeLum", true);
159//        else if (mode == LUMMODE_DECODE_LUM)
160            mat.setBoolean("DecodeLum", true);
161            mat.setTexture("Texture", scene64);
162//        mat.setTexture("Texture", tex);
163
164        Picture dispQuad = new Picture("Luminance Display");
165        dispQuad.setMaterial(mat);
166        return dispQuad;
167    }
168
169    private Material createLumShader(int srcW, int srcH, int bufW, int bufH, int mode,
170                                int iters, Texture tex){
171        Material mat = new Material(manager, "Common/MatDefs/Hdr/LogLum.j3md");
172
173        Vector2f blockSize = new Vector2f(1f / bufW, 1f / bufH);
174        Vector2f pixelSize = new Vector2f(1f / srcW, 1f / srcH);
175        Vector2f blocks = new Vector2f();
176        float numPixels = Float.POSITIVE_INFINITY;
177        if (iters != -1){
178            do {
179                pixelSize.multLocal(2);
180                blocks.set(blockSize.x / pixelSize.x,
181                           blockSize.y / pixelSize.y);
182                numPixels = blocks.x * blocks.y;
183            } while (numPixels > iters);
184        }else{
185            blocks.set(blockSize.x / pixelSize.x,
186                       blockSize.y / pixelSize.y);
187            numPixels = blocks.x * blocks.y;
188        }
189        System.out.println(numPixels);
190
191        mat.setBoolean("Blocks", true);
192        if (mode == LUMMODE_ENCODE_LUM)
193            mat.setBoolean("EncodeLum", true);
194        else if (mode == LUMMODE_DECODE_LUM)
195            mat.setBoolean("DecodeLum", true);
196
197        mat.setTexture("Texture", tex);
198        mat.setVector2("BlockSize", blockSize);
199        mat.setVector2("PixelSize", pixelSize);
200        mat.setFloat("NumPixels", numPixels);
201
202        return mat;
203    }
204
205    private void createLumShaders(){
206        int w = mainSceneFB.getWidth();
207        int h = mainSceneFB.getHeight();
208        hdr64 = createLumShader(w,  h,  64, 64, LUMMODE_ENCODE_LUM, maxIterations, mainScene);
209        hdr8  = createLumShader(64, 64, 8,  8,  LUMMODE_NONE,       maxIterations, scene64);
210        hdr1  = createLumShader(8,  8,  1,  1,  LUMMODE_NONE,       maxIterations, scene8);
211    }
212
213    private int opposite(int i){
214        return i == 1 ? 0 : 1;
215    }
216
217    private void renderProcessing(Renderer r, FrameBuffer dst, Material mat){
218        if (dst == null){
219            fsQuad.setWidth(mainSceneFB.getWidth());
220            fsQuad.setHeight(mainSceneFB.getHeight());
221            fbCam.resize(mainSceneFB.getWidth(), mainSceneFB.getHeight(), true);
222        }else{
223            fsQuad.setWidth(dst.getWidth());
224            fsQuad.setHeight(dst.getHeight());
225            fbCam.resize(dst.getWidth(), dst.getHeight(), true);
226        }
227        fsQuad.setMaterial(mat);
228        fsQuad.updateGeometricState();
229        renderManager.setCamera(fbCam, true);
230
231        r.setFrameBuffer(dst);
232        r.clearBuffers(true, true, true);
233        renderManager.renderGeometry(fsQuad);
234    }
235
236    private void renderToneMap(Renderer r, FrameBuffer out){
237        tone.setFloat("A", exposure);
238        tone.setFloat("White", whiteLevel);
239        tone.setTexture("Lum", scene1[oppSrc]);
240        tone.setTexture("Lum2", scene1[curSrc]);
241        tone.setFloat("BlendFactor", blendFactor);
242        renderProcessing(r, out, tone);
243    }
244
245    private void updateAverageLuminance(Renderer r){
246        renderProcessing(r, scene64FB, hdr64);
247        renderProcessing(r, scene8FB, hdr8);
248        renderProcessing(r, scene1FB[curSrc], hdr1);
249    }
250
251    public boolean isInitialized(){
252        return viewPort != null;
253    }
254
255    public void reshape(ViewPort vp, int w, int h){
256        if (mainSceneFB != null){
257            renderer.deleteFrameBuffer(mainSceneFB);
258        }
259
260        mainSceneFB = new FrameBuffer(w, h, 1);
261        mainScene = new Texture2D(w, h, bufFormat);
262        mainSceneFB.setDepthBuffer(Format.Depth);
263        mainSceneFB.setColorTexture(mainScene);
264        mainScene.setMagFilter(fbMagFilter);
265        mainScene.setMinFilter(fbMinFilter);
266
267        if (msFB != null){
268            renderer.deleteFrameBuffer(msFB);
269        }
270
271        tone.setTexture("Texture", mainScene);
272
273        Collection<Caps> caps = renderer.getCaps();
274        if (numSamples > 1 && caps.contains(Caps.FrameBufferMultisample)){
275            msFB = new FrameBuffer(w, h, numSamples);
276            msFB.setDepthBuffer(Format.Depth);
277            msFB.setColorBuffer(bufFormat);
278            vp.setOutputFrameBuffer(msFB);
279        }else{
280            if (numSamples > 1)
281                logger.warning("FBO multisampling not supported on this GPU, request ignored.");
282
283            vp.setOutputFrameBuffer(mainSceneFB);
284        }
285
286        createLumShaders();
287    }
288
289    public void initialize(RenderManager rm, ViewPort vp){
290        if (!enabled)
291            return;
292
293        renderer = rm.getRenderer();
294        renderManager = rm;
295        viewPort = vp;
296
297        // loadInitial()
298        fsQuad = new Picture("HDR Fullscreen Quad");
299
300        Format lumFmt = Format.RGB8;
301        scene64FB = new FrameBuffer(64, 64, 1);
302        scene64 = new Texture2D(64, 64, lumFmt);
303        scene64FB.setColorTexture(scene64);
304        scene64.setMagFilter(fbMagFilter);
305        scene64.setMinFilter(fbMinFilter);
306
307        scene8FB = new FrameBuffer(8, 8, 1);
308        scene8 = new Texture2D(8, 8, lumFmt);
309        scene8FB.setColorTexture(scene8);
310        scene8.setMagFilter(fbMagFilter);
311        scene8.setMinFilter(fbMinFilter);
312
313        scene1FB[0] = new FrameBuffer(1, 1, 1);
314        scene1[0] = new Texture2D(1, 1, lumFmt);
315        scene1FB[0].setColorTexture(scene1[0]);
316
317        scene1FB[1] = new FrameBuffer(1, 1, 1);
318        scene1[1] = new Texture2D(1, 1, lumFmt);
319        scene1FB[1].setColorTexture(scene1[1]);
320
321        // prepare tonemap shader
322        tone = new Material(manager, "Common/MatDefs/Hdr/ToneMap.j3md");
323        tone.setFloat("A", 0.18f);
324        tone.setFloat("White", 100);
325
326        // load();
327        int w = vp.getCamera().getWidth();
328        int h = vp.getCamera().getHeight();
329        reshape(vp, w, h);
330
331
332    }
333
334    public void preFrame(float tpf) {
335        if (!enabled)
336            return;
337
338        time += tpf;
339        blendFactor = (time / throttle);
340    }
341
342    public void postQueue(RenderQueue rq) {
343    }
344
345    public void postFrame(FrameBuffer out) {
346        if (!enabled)
347            return;
348
349        if (msFB != null){
350            // first render to multisampled FB
351//            renderer.setFrameBuffer(msFB);
352//            renderer.clearBuffers(true,true,true);
353//
354//            renderManager.renderViewPortRaw(viewPort);
355
356            // render back to non-multisampled FB
357            renderer.copyFrameBuffer(msFB, mainSceneFB);
358        }else{
359//            renderer.setFrameBuffer(mainSceneFB);
360//            renderer.clearBuffers(true,true,false);
361//
362//            renderManager.renderViewPortRaw(viewPort);
363        }
364
365        // should we update avg lum?
366        if (throttle == -1){
367            // update every frame
368            curSrc = 0;
369            oppSrc = 0;
370            blendFactor = 0;
371            time = 0;
372            updateAverageLuminance(renderer);
373        }else{
374            if (curSrc == -1){
375                curSrc = 0;
376                oppSrc = 0;
377
378                // initial update
379                updateAverageLuminance(renderer);
380
381                blendFactor = 0;
382                time = 0;
383            }else if (time > throttle){
384
385                // time to switch
386                oppSrc = curSrc;
387                curSrc = opposite(curSrc);
388
389                updateAverageLuminance(renderer);
390
391                blendFactor = 0;
392                time = 0;
393            }
394        }
395
396        // since out == mainSceneFB, tonemap into the main screen instead
397        //renderToneMap(renderer, out);
398        renderToneMap(renderer, null);
399
400        renderManager.setCamera(viewPort.getCamera(), false);
401    }
402
403    public void cleanup() {
404        if (!enabled)
405            return;
406
407        if (msFB != null)
408            renderer.deleteFrameBuffer(msFB);
409        if (mainSceneFB != null)
410            renderer.deleteFrameBuffer(mainSceneFB);
411        if (scene64FB != null){
412            renderer.deleteFrameBuffer(scene64FB);
413            renderer.deleteFrameBuffer(scene8FB);
414            renderer.deleteFrameBuffer(scene1FB[0]);
415            renderer.deleteFrameBuffer(scene1FB[1]);
416        }
417    }
418
419}
420