How to make a blurred background environment?

This is a common thing in sketchfab and i believe i have seen it in threejs examples as well but can’t remember where. Is there something in threejs that would allow me to do this with a regular environment map? I’m referring to the background only, i would still like objects to have reflections.

I don’t think it “just works” yet but there are a couple PRs that would enable it (#18241 and #20463). Alternatively you could generate a blurry background texture separately from the environment map and use them together. I believe you can do that on the fly with PREMGenerator, though you may have to use fromScene on a scene with the background texture to apply your own blur.

3 Likes

oh wow, thanks! can’t wait for those to come in! i tried fromscene, but it complains about the blurradius being too high due to max_samples set to 20, which seems to produce a very tame blur. i tried to fork it with 1000 samples to get close to what i see on sketchfab. it’s almost there, but i see a weird “pinch”, you see it when you move the camera around, is that normal? https://codesandbox.io/s/color-grading-forked-wtoe7?file=/src/App.js

i tried to fork it with 1000 samples to get close to what i see on sketchfab. it’s almost there, but i see a weird “pinch”, you see it when you move the camera around, is that normal?

I don’t use PMREMGenerator a lot so I’m less familiar with the details of it’s implementation. It’s possible it’s a bug but at the same time it sounds like it may not have been designed to handle so many samples.

1 Like

That sample either crashes CodeSandbox, or the preview crashes with Chrome sad face, on my macbook. I’m guessing it has something to do with the 1000 samples.

What’s the best way to blur a scene background these days?

I gave it a try with the blurTexture function from

and you’re right, higher amounts of passes don’t really do much. I updated the blurTexture to do a specified number of passes:

/** Adapted from https://discourse.threejs.org/t/39433 */
function blurTexture(renderer: WebGLRenderer, texture: Texture, blurAmount = 1) {
	console.log('blur amount', blurAmount)
	const width = texture.image.width
	const height = texture.image.height

	const cameraRTT = new OrthographicCamera(-1, 1, 1, -1, 0, 1)
	const sceneRTT = new ThreeScene()

	// render targets

	const renderTarget1 = new WebGLRenderTarget(width, height)
	const renderTarget2 = new WebGLRenderTarget(width, height)

	// shader materials

	const hBlurMaterial = new ShaderMaterial({
		vertexShader: HorizontalBlurShader.vertexShader,
		fragmentShader: HorizontalBlurShader.fragmentShader,
		uniforms: UniformsUtils.clone(HorizontalBlurShader.uniforms),
	})

	hBlurMaterial.uniforms.tDiffuse.value = texture
	hBlurMaterial.uniforms.h.value = 1 / width

	const vBlurMaterial = new ShaderMaterial({
		vertexShader: VerticalBlurShader.vertexShader,
		fragmentShader: VerticalBlurShader.fragmentShader,
		uniforms: UniformsUtils.clone(VerticalBlurShader.uniforms),
	})

	vBlurMaterial.uniforms.tDiffuse.value = renderTarget1.texture
	vBlurMaterial.uniforms.v.value = 1 / height

	// fullscreen quad

	const planeGeometry = new PlaneGeometry(2, 2)

	const fullScreenQuad = new Mesh(planeGeometry, hBlurMaterial)
	sceneRTT.add(fullScreenQuad)

	let lastTexture = texture

	// passes

	while (blurAmount--) {
		// horizontal pass

		fullScreenQuad.material = hBlurMaterial
		hBlurMaterial.uniforms.tDiffuse.value = lastTexture

		renderer.setRenderTarget(renderTarget1)
		renderer.render(sceneRTT, cameraRTT)
		renderer.setRenderTarget(null)

		lastTexture = renderTarget1.texture

		// vertical pass

		fullScreenQuad.material = vBlurMaterial
		vBlurMaterial.uniforms.tDiffuse.value = lastTexture

		renderer.setRenderTarget(renderTarget2)
		renderer.render(sceneRTT, cameraRTT)
		renderer.setRenderTarget(null)

		lastTexture = renderTarget2.texture
	}

	//

	return renderTarget2.texture
}

I think there’s a better way to do this…

three.js docs ?

That’s nice for values closer to 1.0, but it has unwanted pixelation for values like 0.3.

The TriangleBlurShader is the nicest so far:

but now the problem is there’s a seam on one side of the background:

Note how it is not too blurry, but also not pixelated. Here’s the same scene with same background, but using backgroundBlurriness 0.1:

It is not as smooth.

Now I need to study backgroundBlurriness to see how to make it seamless.

Three.js WebGLRenderer’s WebGLBackground is using the backgroundCube shader, and it uses a mipmap-based “blur” effect from here:

This is probably cheap, but the result is not very good, and it is not customizeable.

I’m not yet sure how to avoid the seam problem with the custom blur.

I was able to do this by applying the roughness logic to the output. Rather than sampling with reflection, i sample where the cubemap would normally be sampled. roughness of 0 makes it a clear cubemap, 1 makes it very blurry.

#define PI 3.14159265359
highp float rand( const in vec2 uv ) {
  const highp float a = 12.9898, b = 78.233, c = 43758.5453;
  highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
  return fract(sin(sn) * c);
}
vec3 dithering( vec3 color ) {
  //Calculate grid position
  float grid_position = rand( gl_FragCoord.xy );

  //Shift the individual colors differently, thus making it even harder to see the dithering pattern
  vec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );

  //modify shift acording to grid position.
  dither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );

  //shift the color by dither_shift
  return color + dither_shift_RGB;
}
uniform float envMapIntensity;
uniform float roughness;
#include <cube_uv_reflection_fragment>
varying vec3 vWorldView;
uniform sampler2D envMap;
void main(){
  vec3 world = normalize(vWorldView);
  world.y *=-1.;
  vec3 queryReflectVec = world;
  vec4 envMapColor = textureCubeUV( envMap, queryReflectVec, roughness ) * envMapIntensity;
  gl_FragColor = vec4(envMapColor.xyz,1.);
  gl_FragColor.xyz = pow(gl_FragColor.xyz, vec3(.6));
  gl_FragColor.rgb = dithering( gl_FragColor.rgb );
}

@dubois got a working sample?

@dubois So textureCubeUV is specifically for sampling across the seams, if I understand correctly? And then you’re saying you apply dither so that the mipmap-based roughness is not as pixelated?

If I understand what you’re suggesting, I should replace this line in TriangleBlurShader:

with the textureCubeUV and use the shader with a cube mesh?