Skip to content

Commit 76d8a43

Browse files
authored
Added SoftBloomFilter (#2229)
* added pbr bloom filter * add javadoc and license * documented and tweaked test * added exception * various formatting fixes * fixed javadoc typo * fixed bug on applying glow factor * fix javadoc typo * fixed formatting issues * switched texture min/mag filters * rename filter * rename filter * improved test and capped number of passes * reformat test * serialize bilinear filtering * delete unrelated files * increase size limit to 2 * renamed shaders
1 parent 4152c1b commit 76d8a43

File tree

9 files changed

+779
-2
lines changed

9 files changed

+779
-2
lines changed
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/*
2+
* Copyright (c) 2024 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+
package com.jme3.post.filters;
33+
34+
import com.jme3.asset.AssetManager;
35+
import com.jme3.export.InputCapsule;
36+
import com.jme3.export.JmeExporter;
37+
import com.jme3.export.JmeImporter;
38+
import com.jme3.export.OutputCapsule;
39+
import com.jme3.material.Material;
40+
import com.jme3.math.FastMath;
41+
import com.jme3.math.Vector2f;
42+
import com.jme3.post.Filter;
43+
import com.jme3.renderer.RenderManager;
44+
import com.jme3.renderer.Renderer;
45+
import com.jme3.renderer.ViewPort;
46+
import com.jme3.texture.Image;
47+
import com.jme3.texture.Texture;
48+
import java.io.IOException;
49+
import java.util.logging.Logger;
50+
import java.util.logging.Level;
51+
import java.util.LinkedList;
52+
53+
/**
54+
* Adds a glow effect to the scene.
55+
* <p>
56+
* Compared to {@link BloomFilter}, this filter produces much higher quality
57+
* results that feel much more natural.
58+
* <p>
59+
* This implementation, unlike BloomFilter, has no brightness threshold,
60+
* meaning all aspects of the scene glow, although only very bright areas will
61+
* noticeably produce glow. For this reason, this filter should <em>only</em> be used
62+
* if HDR is also being utilized, otherwise BloomFilter should be preferred.
63+
* <p>
64+
* This filter uses the PBR bloom algorithm presented in
65+
* <a href="https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom">this article</a>.
66+
*
67+
* @author codex
68+
*/
69+
public class SoftBloomFilter extends Filter {
70+
71+
private static final Logger logger = Logger.getLogger(SoftBloomFilter.class.getName());
72+
73+
private AssetManager assetManager;
74+
private RenderManager renderManager;
75+
private ViewPort viewPort;
76+
private int width;
77+
private int height;
78+
private Pass[] downsamplingPasses;
79+
private Pass[] upsamplingPasses;
80+
private final Image.Format format = Image.Format.RGBA16F;
81+
private boolean initialized = false;
82+
private int numSamplingPasses = 5;
83+
private float glowFactor = 0.05f;
84+
private boolean bilinearFiltering = true;
85+
86+
/**
87+
* Creates filter with default settings.
88+
*/
89+
public SoftBloomFilter() {
90+
super("SoftBloomFilter");
91+
}
92+
93+
@Override
94+
protected void initFilter(AssetManager am, RenderManager rm, ViewPort vp, int w, int h) {
95+
96+
assetManager = am;
97+
renderManager = rm;
98+
viewPort = vp;
99+
postRenderPasses = new LinkedList<>();
100+
Renderer renderer = renderManager.getRenderer();
101+
this.width = w;
102+
this.height = h;
103+
104+
capPassesToSize(w, h);
105+
106+
downsamplingPasses = new Pass[numSamplingPasses];
107+
upsamplingPasses = new Pass[numSamplingPasses];
108+
109+
// downsampling passes
110+
Material downsampleMat = new Material(assetManager, "Common/MatDefs/Post/Downsample.j3md");
111+
Vector2f initTexelSize = new Vector2f(1f/w, 1f/h);
112+
w = w >> 1; h = h >> 1;
113+
Pass initialPass = new Pass() {
114+
@Override
115+
public boolean requiresSceneAsTexture() {
116+
return true;
117+
}
118+
@Override
119+
public void beforeRender() {
120+
downsampleMat.setVector2("TexelSize", initTexelSize);
121+
}
122+
};
123+
initialPass.init(renderer, w, h, format, Image.Format.Depth, 1, downsampleMat);
124+
postRenderPasses.add(initialPass);
125+
downsamplingPasses[0] = initialPass;
126+
for (int i = 1; i < downsamplingPasses.length; i++) {
127+
Vector2f texelSize = new Vector2f(1f/w, 1f/h);
128+
w = w >> 1; h = h >> 1;
129+
Pass prev = downsamplingPasses[i-1];
130+
Pass pass = new Pass() {
131+
@Override
132+
public void beforeRender() {
133+
downsampleMat.setTexture("Texture", prev.getRenderedTexture());
134+
downsampleMat.setVector2("TexelSize", texelSize);
135+
}
136+
};
137+
pass.init(renderer, w, h, format, Image.Format.Depth, 1, downsampleMat);
138+
if (bilinearFiltering) {
139+
pass.getRenderedTexture().setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
140+
}
141+
postRenderPasses.add(pass);
142+
downsamplingPasses[i] = pass;
143+
}
144+
145+
// upsampling passes
146+
Material upsampleMat = new Material(assetManager, "Common/MatDefs/Post/Upsample.j3md");
147+
for (int i = 0; i < upsamplingPasses.length; i++) {
148+
Vector2f texelSize = new Vector2f(1f/w, 1f/h);
149+
w = w << 1; h = h << 1;
150+
Pass prev;
151+
if (i == 0) {
152+
prev = downsamplingPasses[downsamplingPasses.length-1];
153+
} else {
154+
prev = upsamplingPasses[i-1];
155+
}
156+
Pass pass = new Pass() {
157+
@Override
158+
public void beforeRender() {
159+
upsampleMat.setTexture("Texture", prev.getRenderedTexture());
160+
upsampleMat.setVector2("TexelSize", texelSize);
161+
}
162+
};
163+
pass.init(renderer, w, h, format, Image.Format.Depth, 1, upsampleMat);
164+
if (bilinearFiltering) {
165+
pass.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
166+
}
167+
postRenderPasses.add(pass);
168+
upsamplingPasses[i] = pass;
169+
}
170+
171+
material = new Material(assetManager, "Common/MatDefs/Post/SoftBloomFinal.j3md");
172+
material.setTexture("GlowMap", upsamplingPasses[upsamplingPasses.length-1].getRenderedTexture());
173+
material.setFloat("GlowFactor", glowFactor);
174+
175+
initialized = true;
176+
177+
}
178+
179+
@Override
180+
protected Material getMaterial() {
181+
return material;
182+
}
183+
184+
/**
185+
* Sets the number of sampling passes in each step.
186+
* <p>
187+
* Higher values produce more glow with higher resolution, at the cost
188+
* of more passes. Lower values produce less glow with lower resolution.
189+
* <p>
190+
* The total number of passes is {@code 2n+1}: n passes for downsampling
191+
* (13 texture reads per pass per fragment), n passes for upsampling and blur
192+
* (9 texture reads per pass per fragment), and 1 pass for blending (2 texture reads
193+
* per fragment). Though, it should be noted that for each downsampling pass the
194+
* number of fragments decreases by 75%, and for each upsampling pass, the number
195+
* of fragments quadruples (which restores the number of fragments to the original
196+
* resolution).
197+
* <p>
198+
* Setting this after the filter has been initialized forces reinitialization.
199+
* <p>
200+
* default=5
201+
*
202+
* @param numSamplingPasses The number of passes per donwsampling/upsampling step. Must be greater than zero.
203+
* @throws IllegalArgumentException if argument is less than or equal to zero
204+
*/
205+
public void setNumSamplingPasses(int numSamplingPasses) {
206+
if (numSamplingPasses <= 0) {
207+
throw new IllegalArgumentException("Number of sampling passes must be greater than zero (found: " + numSamplingPasses + ").");
208+
}
209+
if (this.numSamplingPasses != numSamplingPasses) {
210+
this.numSamplingPasses = numSamplingPasses;
211+
if (initialized) {
212+
initFilter(assetManager, renderManager, viewPort, width, height);
213+
}
214+
}
215+
}
216+
217+
/**
218+
* Sets the factor at which the glow result texture is merged with
219+
* the scene texture.
220+
* <p>
221+
* Low values favor the scene texture more, while high values make
222+
* glow more noticeable. This value is clamped between 0 and 1.
223+
* <p>
224+
* default=0.05f
225+
*
226+
* @param factor
227+
*/
228+
public void setGlowFactor(float factor) {
229+
this.glowFactor = FastMath.clamp(factor, 0, 1);
230+
if (material != null) {
231+
material.setFloat("GlowFactor", glowFactor);
232+
}
233+
}
234+
235+
/**
236+
* Sets pass textures to use bilinear filtering.
237+
* <p>
238+
* If true, downsampling textures are set to {@code min=BilinearNoMipMaps} and
239+
* upsampling textures are set to {@code mag=Bilinear}, which produces better
240+
* quality glow. If false, textures use their default filters.
241+
* <p>
242+
* default=true
243+
*
244+
* @param bilinearFiltering true to use bilinear filtering
245+
*/
246+
public void setBilinearFiltering(boolean bilinearFiltering) {
247+
if (this.bilinearFiltering != bilinearFiltering) {
248+
this.bilinearFiltering = bilinearFiltering;
249+
if (initialized) {
250+
for (Pass p : downsamplingPasses) {
251+
if (this.bilinearFiltering) {
252+
p.getRenderedTexture().setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
253+
} else {
254+
p.getRenderedTexture().setMinFilter(Texture.MinFilter.NearestNoMipMaps);
255+
}
256+
}
257+
for (Pass p : upsamplingPasses) {
258+
if (this.bilinearFiltering) {
259+
p.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
260+
} else {
261+
p.getRenderedTexture().setMagFilter(Texture.MagFilter.Nearest);
262+
}
263+
}
264+
}
265+
}
266+
}
267+
268+
/**
269+
* Gets the number of downsampling/upsampling passes per step.
270+
*
271+
* @return number of downsampling/upsampling passes
272+
* @see #setNumSamplingPasses(int)
273+
*/
274+
public int getNumSamplingPasses() {
275+
return numSamplingPasses;
276+
}
277+
278+
/**
279+
* Gets the glow factor.
280+
*
281+
* @return glow factor
282+
* @see #setGlowFactor(float)
283+
*/
284+
public float getGlowFactor() {
285+
return glowFactor;
286+
}
287+
288+
/**
289+
* Returns true if pass textures use bilinear filtering.
290+
*
291+
* @return
292+
* @see #setBilinearFiltering(boolean)
293+
*/
294+
public boolean isBilinearFiltering() {
295+
return bilinearFiltering;
296+
}
297+
298+
/**
299+
* Caps the number of sampling passes so that texture size does
300+
* not go below 1 on any axis.
301+
* <p>
302+
* A message will be logged if the number of sampling passes is changed.
303+
*
304+
* @param w texture width
305+
* @param h texture height
306+
*/
307+
private void capPassesToSize(int w, int h) {
308+
int limit = Math.min(w, h);
309+
for (int i = 0; i < numSamplingPasses; i++) {
310+
limit = limit >> 1;
311+
if (limit <= 2) {
312+
numSamplingPasses = i;
313+
logger.log(Level.INFO, "Number of sampling passes capped at {0} due to texture size.", i);
314+
break;
315+
}
316+
}
317+
}
318+
319+
@Override
320+
public void write(JmeExporter ex) throws IOException {
321+
super.write(ex);
322+
OutputCapsule oc = ex.getCapsule(this);
323+
oc.write(numSamplingPasses, "numSamplingPasses", 5);
324+
oc.write(glowFactor, "glowFactor", 0.05f);
325+
oc.write(bilinearFiltering, "bilinearFiltering", true);
326+
}
327+
328+
@Override
329+
public void read(JmeImporter im) throws IOException {
330+
super.read(im);
331+
InputCapsule ic = im.getCapsule(this);
332+
numSamplingPasses = ic.readInt("numSamplingPasses", 5);
333+
glowFactor = ic.readFloat("glowFactor", 0.05f);
334+
bilinearFiltering = ic.readBoolean("bilinearFiltering", true);
335+
}
336+
337+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
#import "Common/ShaderLib/GLSLCompat.glsllib"
3+
#import "Common/ShaderLib/MultiSample.glsllib"
4+
5+
uniform COLORTEXTURE m_Texture;
6+
uniform vec2 m_TexelSize;
7+
varying vec2 texCoord;
8+
9+
void main() {
10+
11+
// downsampling code: https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom
12+
13+
float x = m_TexelSize.x;
14+
float y = m_TexelSize.y;
15+
16+
// Take 13 samples around current texel
17+
// a - b - c
18+
// - j - k -
19+
// d - e - f
20+
// - l - m -
21+
// g - h - i
22+
// === ('e' is the current texel) ===
23+
vec3 a = getColor(m_Texture, vec2(texCoord.x - 2*x, texCoord.y + 2*y)).rgb;
24+
vec3 b = getColor(m_Texture, vec2(texCoord.x, texCoord.y + 2*y)).rgb;
25+
vec3 c = getColor(m_Texture, vec2(texCoord.x + 2*x, texCoord.y + 2*y)).rgb;
26+
27+
vec3 d = getColor(m_Texture, vec2(texCoord.x - 2*x, texCoord.y)).rgb;
28+
vec3 e = getColor(m_Texture, vec2(texCoord.x, texCoord.y)).rgb;
29+
vec3 f = getColor(m_Texture, vec2(texCoord.x + 2*x, texCoord.y)).rgb;
30+
31+
vec3 g = getColor(m_Texture, vec2(texCoord.x - 2*x, texCoord.y - 2*y)).rgb;
32+
vec3 h = getColor(m_Texture, vec2(texCoord.x, texCoord.y - 2*y)).rgb;
33+
vec3 i = getColor(m_Texture, vec2(texCoord.x + 2*x, texCoord.y - 2*y)).rgb;
34+
35+
vec3 j = getColor(m_Texture, vec2(texCoord.x - x, texCoord.y + y)).rgb;
36+
vec3 k = getColor(m_Texture, vec2(texCoord.x + x, texCoord.y + y)).rgb;
37+
vec3 l = getColor(m_Texture, vec2(texCoord.x - x, texCoord.y - y)).rgb;
38+
vec3 m = getColor(m_Texture, vec2(texCoord.x + x, texCoord.y - y)).rgb;
39+
40+
// Apply weighted distribution:
41+
// 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1
42+
// a,b,d,e * 0.125
43+
// b,c,e,f * 0.125
44+
// d,e,g,h * 0.125
45+
// e,f,h,i * 0.125
46+
// j,k,l,m * 0.5
47+
// This shows 5 square areas that are being sampled. But some of them overlap,
48+
// so to have an energy preserving downsample we need to make some adjustments.
49+
// The weights are the distributed, so that the sum of j,k,l,m (e.g.)
50+
// contribute 0.5 to the final color output. The code below is written
51+
// to effectively yield this sum. We get:
52+
// 0.125*5 + 0.03125*4 + 0.0625*4 = 1
53+
vec3 downsample = e*0.125;
54+
downsample += (a+c+g+i)*0.03125;
55+
downsample += (b+d+f+h)*0.0625;
56+
downsample += (j+k+l+m)*0.125;
57+
58+
gl_FragColor = vec4(downsample, 1.0);
59+
60+
}

0 commit comments

Comments
 (0)