Martin Schuhfuß
Martin Schuhfuß

Reputation: 6986

Texture from WebGLRenderTarget not rendered with StereoEffect

I have a panoramic view and want to blur it in order to present a user-interface in front of the blurred view. The blurred image has to be computed on the client so I decided to use a fragment-shader-based implementation to do that.

This works very nice as long as I only use only the regular renderer. But when the scene is rendered using the THREE.StereoEffect instead, the blurred image doesn't appear on screen.

You can see this in the attached snippet (jsfiddle here: https://jsfiddle.net/n988sg96/3/): If you press "toggle blur" everything looks like it should. But if you press "toggle stereo" and then activate the blur, the screen just turns black (so basically, the blurred image will not render).

The generation of the blurred image is implemented in createBlurredTexture() using the same renderer that is also used for the scene and two render-targets for the vertical and horizontal pass of the blur.

I already verified (by exporting the framebuffers as image via renderer.readRenderTargetPixels()) that both render-targets contain correct images in both cases (so independent of wether the stereo-mode is on or not).

So the questions I have are:

const panoUrl = 'https://farm9.staticflickr.com/8652/29593302665_9e747048f7_k_d.jpg';

const panoTexture = new THREE.Texture();
const image = new Image();
image.crossOrigin = 'Anonymous';
image.onload = () => {
  panoTexture.image = image;
  panoTexture.format = THREE.RGBFormat;
  panoTexture.needsUpdate = true;
};
image.src = panoUrl;

const blurButton = document.querySelector('.blur-btn');
const stereoButton = document.querySelector('.stereo-btn');

// creates meshes
function initScene(scene, renderer) {
  const panoSphere = new THREE.Mesh(
    new THREE.SphereGeometry(100, 36, 18),
    new THREE.MeshBasicMaterial({
      depthWrite: false,
      map: panoTexture
    }));

  const blurSphere = new THREE.Mesh(
    new THREE.SphereGeometry(80, 36, 18),
    new THREE.MeshBasicMaterial({
      color: 0x666666
    })
  );

  // flip normals
  blurSphere.scale.x = panoSphere.scale.x = -1;
  blurSphere.visible = false;

  scene.add(panoSphere, blurSphere);

  blurButton.addEventListener('click', ev => {
    if (blurSphere.visible) {
      blurSphere.visible = false;
    } else {
      blurSphere.material.map = createBlurredTexture(
        renderer, panoSphere.material.map.image);
      blurSphere.material.needsUpdate = true;
      blurSphere.visible = true;
    }
  });
}


// creates a blurred image-texture from the given image
function createBlurredTexture(renderer, img, prescale = 0.25) {
  const width = img.width * prescale;
  const height = img.height * prescale;
  const material = blurPassMaterial;

  const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
  const scene = new THREE.Scene()
    .add(new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), material));

  const renderTargetOpts = {
    depthBuffer: false,
    stencilBuffer: false
  };
  const rt1 = new THREE.WebGLRenderTarget(width, height, renderTargetOpts);
  const rt2 = new THREE.WebGLRenderTarget(width, height, renderTargetOpts);

  material.uniforms.resolution.value.set(width, height);

  // prepare: downscale source-image
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  canvas.getContext('2d').drawImage(img, 0, 0, width, height);
  const texture = new THREE.CanvasTexture(canvas);
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;

  // pass 1: vertical blur, texture -> rt1
  material.uniforms.image.value = texture;
  material.uniforms.direction.value.set(0, 1);
  renderer.render(scene, camera, rt1);

  // pass 2: horizontal blur, rt1 -> rt2
  material.uniforms.image.value = rt1.texture;
  material.uniforms.direction.value.set(1, 0);
  renderer.render(scene, camera, rt2);

  // cleanup
  texture.dispose();
  rt1.texture.dispose();
  rt1.dispose();

  return rt2.texture;
}


// simple material for a fast 5px blur pass
const blurPassMaterial = new THREE.ShaderMaterial({
  uniforms: {
    image: {type: 't', value: null},
    resolution: {type: 'v2', value: new THREE.Vector2()},
    direction: {type: 'v2', value: new THREE.Vector2(1, 0)}
  },

  vertexShader: `
      varying vec2 vUv;
      void main() {
        vUv = uv; 
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,

  fragmentShader: `
      varying vec2 vUv;

      uniform vec2 direction;
      uniform vec2 resolution;
      uniform sampler2D image;

      // based on https://github.com/Jam3/glsl-fast-gaussian-blur
      vec4 blur5(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
        vec2 offset = (vec2(1.3333333333333333) * direction) / resolution;
        
        return texture2D(image, uv) * 0.29411764705882354
          + texture2D(image, uv + offset) * 0.35294117647058826
          + texture2D(image, uv - offset) * 0.35294117647058826;
      }

      void main() {
        gl_FragColor = blur5(image, vUv, resolution, direction); 
      }
    `
});



// ---- boilerplate-code

// .... setup renderer and stereo-effect
let isStereoMode = false;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const effect = new THREE.StereoEffect(renderer);

// .... setup scene
const scene = window.scene = new THREE.Scene();

// .... setup camera and controls
const camera = new THREE.PerspectiveCamera(
  70, window.innerWidth / window.innerHeight, 1, 1000);
const controls = new THREE.OrbitControls(camera);
controls.enableZoom = false;
controls.enableDamping = true;
controls.dampingFactor = .15;

camera.position.set(0, 0, .1);
camera.lookAt(new THREE.Vector3(0, 0, 0));

// .... setup and run
initScene(scene, renderer);

requestAnimationFrame(function loop(time) {
  controls.update();

  if (isStereoMode) {
    effect.render(scene, camera);
  } else {
    renderer.render(scene, camera);
  }

  requestAnimationFrame(loop);
});

// .... bind events
stereoButton.addEventListener('click', ev => {
  isStereoMode = !isStereoMode;

  if (!isStereoMode) {
    renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
  }
});

window.addEventListener('resize', ev => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
});

document.body.appendChild(renderer.domElement);
body {
  margin: 0;
  overflow: hidden;
}
canvas {
  width: 100vw;
  height: 100vh;
}
.buttons {
  position: absolute;
  top: 10px;
  left: 0;
  right: 0;
  text-align: center;
}
button {
  display: inline-block;
}
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/build/three.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/effects/StereoEffect.js"></script>

<div class="buttons">
  <button class="blur-btn">toggle blur</button>
  <button class="stereo-btn">toggle stereo</button>
</div>

Upvotes: 0

Views: 525

Answers (1)

Martin Schuhfu&#223;
Martin Schuhfu&#223;

Reputation: 6986

Found a solution reading this Question: three.js - THREE.StereoEffect / webVR-boilerplate + THREE.Mirror

And I just needed to add a single line to the createBlurredTexture()-function. When cleaning up, it is neccessary to manually unset the renderTarget by calling

renderer.setRenderTarget(null);

The reason for this is that the rendering of the stereo-effect will call renderer.clear(), which will - without unsetting the rendertarget - clear the renderTarget instead of the screen framebuffer.

So thanks a lot stackoverflow <3

Upvotes: 1

Related Questions