0

I have 2 meshes with each a shaderMaterial and each a different fragment shader. When I add both meshes to my scene, only one will show up. Below you can find my 2 fragment shaders (see both images to see what they look like). They're basically the same.

What I want to achieve: Use mesh1 as a mask and put the other one, mesh2 (purple blob) on top of the mask.

Purple blob:

// three.js code
const geometry1 = new THREE.PlaneBufferGeometry(1, 1, 1, 1);
const material1 = new THREE.ShaderMaterial({
  uniforms: this.uniforms,
  vertexShader,
  fragmentShader,
  defines: {
    PR: window.devicePixelRatio.toFixed(1)
  }
});

const mesh1 = new THREE.Mesh(geometry1, material1);
this.scene.add(mesh1);


// fragment shader
void main() {
  vec2 res = u_res * PR;
  vec2 st = gl_FragCoord.xy / res.xy - 0.5;

  st.y *= u_res.y / u_res.x * 0.8;

  vec2 circlePos = st;
  float c = circle(circlePos, 0.2 + 0. * 0.1, 1.) * 2.5;

  float offx = v_uv.x + sin(v_uv.y + u_time * .1);
  float offy = v_uv.y * .1 - u_time * 0.005 - cos(u_time * .001) * .01;
  float n = snoise3(vec3(offx, offy, .9) * 2.5) - 2.1;

  float finalMask = smoothstep(1., 0.99, n + pow(c, 1.5));

  vec4 bg = vec4(0.12, 0.07, 0.28, 1.0);
  vec4 bg2 = vec4(0., 0., 0., 0.);

  gl_FragColor = mix(bg, bg2, finalMask);
}

Blue mask

// three.js code
const geometry2 = new THREE.PlaneBufferGeometry(1, 1, 1, 1);
const material2 = new THREE.ShaderMaterial({
  uniforms,
  vertexShader,
  fragmentShader,
  defines: {
    PR: window.devicePixelRatio.toFixed(1)
  }
});

const mesh2 = new THREE.Mesh(geometry2, material2);
this.scene.add(mesh2);

// fragment shader
void main() {
  vec2 res = u_res * PR;
  vec2 st = gl_FragCoord.xy / res.xy - 0.5;

  st.y *= u_res.y / u_res.x * 0.8;

  vec2 circlePos = st;
  float c = circle(circlePos, 0.2 + 0. * 0.1, 1.) * 2.5;

  float offx = v_uv.x + sin(v_uv.y + u_time * .1);
  float offy = v_uv.y * .1 - u_time * 0.005 - cos(u_time * .001) * .01;
  float n = snoise3(vec3(offx, offy, .9) * 2.5) - 2.1;

  float finalMask = smoothstep(1., 0.99, n + pow(c, 1.5));

  vec4 bg = vec4(0.12, 0.07, 0.28, 1.0);
  vec4 bg2 = vec4(0., 0., 0., 0.);

  gl_FragColor = mix(bg, bg2, finalMask);
}

Render Target code

this.rtWidth = window.innerWidth;
this.rtHeight = window.innerHeight;
this.renderTarget = new THREE.WebGLRenderTarget(this.rtWidth, this.rtHeight);

this.rtCamera = new THREE.PerspectiveCamera(
  this.camera.settings.fov,
  this.camera.settings.aspect,
  this.camera.settings.near,
  this.camera.settings.far
);
this.rtCamera.position.set(0, 0, this.camera.settings.perspective);

this.rtScene = new THREE.Scene();
this.rtScene.add(this.purpleBlob);

const geometry = new THREE.PlaneGeometry(window.innerWidth, window.innerHeight, 1);
const material = new THREE.MeshPhongMaterial({
  map: this.renderTarget.texture,
});

this.mesh = new THREE.Mesh(geometry, material);

this.scene.add(this.mesh);

I'm still new to shaders so please be patient. :-)

blob purple blob blue

Ruud Luijten
  • 157
  • 4
  • 14
  • One solution is to use separate scenes and/or [render targets](https://threejsfundamentals.org/threejs/lessons/threejs-rendertargets.html) to achieve that. you can then render the mask to a texture via a render target and then render the second one passing the texture with the mask in it to the second shader similar to [how the EffectsComposer works](https://threejsfundamentals.org/threejs/lessons/threejs-post-processing.html) – gman Dec 04 '19 at 16:25
  • @gman thanks, I guess the easiest option would be creating an extra scene or am I wrong? Also, would the performance be worse if I have multiple scenes? – Ruud Luijten Dec 04 '19 at 16:34
  • Yes, create another scene (and a rendertarget so you can generate a mask texture from the first scene). No it will not be slower. Although it depends on what you're really trying to do. If you want to hard code things you can just combine both shaders into one shader. Change `main` in the mask to `mainMask` and have it return the last value instead of assigning it to `gl_FragColor`. Now call it from the second shader to get a mask. – gman Dec 04 '19 at 16:36
  • @gman I added my code for the render Target. Since I'm using the purple blob for page transitions this will be on every page, so I want to use this as my render target. Would you mind helping me a little with passing the texture in my mask shader? – Ruud Luijten Dec 05 '19 at 09:04
  • post runnable code in a [snippet](https://stackoverflow.blog/2014/09/16/introducing-runnable-javascript-css-and-html-code-snippets/) in your question. You can see lots of examples of runnable three.js snippets like [this one](https://stackoverflow.com/questions/24087757/three-js-and-loading-a-cross-domain-image/24103129#24103129). – gman Dec 05 '19 at 09:25
  • Also what do want to happen exactly? You say the purple blob should appear on top of the mask. You're calling it a "mask" but "on top of" doesn't sound like you want it to actually be "[masked](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Compositing#Clipping_paths)". – gman Dec 05 '19 at 09:25
  • @gman There is the blue mask and behind it there will be an image, so you can see part of the image behind the mask. The purple blob will be out of the viewport (right top, which is the initial state) and will expand on top of the blue mask when I click on a button. – Ruud Luijten Dec 05 '19 at 09:29

1 Answers1

0

There are probably infinite ways to mask in three.js. Here's a few

  1. Use the stencil buffer

    The stencil buffer is similar to the depth buffer in that it for every pixel in the canvas or render target there is a corresponding stencil pixel. You need to tell three.js you want a stencil buffer and then you can tell it when rendering what to do with the stencil buffer when you're drawing things.

    You the stencil settings on Material

    You tell three.js

    • what to do if the pixel you're drawing fails the stencil test
    • what to do if the pixel your drawing fails the depth test
    • what to do if the pixel you're drawing passes the depth test.

    The things you can tell it to do for each of those conditions are keep (do nothing), increment, decrement, increment wraparound, decrement wraparound, set to a specific value.

    You can also specify what the stencil test is by setting Material.stencilFunc

    So, for example you can clear the stencil buffer to 0 (the default?), set the stencil test so it always passes, and set the conditions so if the depth test passes you set the stencil to 1. You then draw a bunch of things. Everywhere they are drawn there will now be a 1 in then stencil buffer.

    Now you change the stencil test so it only passes if it equals 1 (or 0) and then draw more stuff, now things will only be drawn where the stencil equals the value you set

    This exmaple uses the stencil

  2. Mask with an alpha mask

    In this case you need 2 color textures and an alpha texture. How you get those is up to you. For example you could load all 3 from images. Or you could generate all 3 using 3 render targets. Finally you pass all 3 to a shader that mixes them as in

    gl_FragColor = mix(colorFromTexture1, colorFromTexture2, valueFromAlphaTexture);
    

    This example uses this alpha mixing method

    Note that if one of your 2 colors textures has an alpha channel you could use just 2 textures. You'd just pass one of the color textures as your mask.

    Or of course you could calculate a mask based on the colors in one image or the other or both. For example

    // assume you have function that converts from rgb to hue,saturation,value
    vec3 hsv = rgb2hsv(colorFromTexture1.rgb);
    float hue = hsv.x;
    // pick one or the other if color1 is close to green
    float mixAmount = step(abs(hue - 0.33), 0.05); 
    gl_FragColor = mix(colorFromTexture1, colorFromTexture2, mixAmount);
    

    The point here is not that exact code, it's that you can make any formula you want for the mask, based on whatever you want, color, position, random math, sine waves based on time, some formula that generates a blob, whatever. The most common is some code that just looks up a mixAmount from a texture which is what the linked example above does.

  3. ShaderToy style

    Your code above appears to be a shadertoy style shader which is drawing a fullscreen quad. Instead of drawing 2 separate things you can just draw them in the same shader

    vec4 computeBlueBlob() {
      ...
      return blueBlobColor;
    }
    
    vec4 computeWhiteBlob() {
      ...
      return whtieBlobColor;
    }
    
    vec4 main() {
      vec4 color1 = computeBlueBlob();
      vec4 color2 = computeWhiteBlob();
      float mixAmount = color.a;  // note: color2.a could be any
                                  // formula to decide which colors
                                  // to draw
      gl_FragColor = mix(color1, color2, mixAmount);
    }
    

    note just like above how you compute mixAmount is up to you. Based it off anything, color1.r, color2.r, some formula, some hue, some other blob generation function, whatever.

gman
  • 100,619
  • 31
  • 269
  • 393