Okarin
Okarin

Reputation: 974

What's the simplest way to render overlapping transparent meshes in THREE.js?

I'd like to render transparent surfaces that partially overlap in THREE.js. I am not looking for perfect rendering of multiple overlapping surface, but would just like something that looks a little better than the current result - where for example if I have two ellipsoids, the one whose centre is behind gets rendered all with reference to that depth, even if parts of its surface are effectively in front of the other. I should also mention I'm using OrbitControls, so I can't simply order them statically once and forget about them.

I've seen that Depth Peeling is usually the technique for this, and even found someone who wrote an example doing it in THREE.js. Unfortunately it also looks rather complicated and possibly performance heavy. I was wondering if there were simpler alternatives. Something I've considered is just splitting each mesh in its fundamental triangles and pushing each as a separate geometry, but I don't know how much of a performance hit that would cause, and it would make all further rescaling/rotating operations messier. I saw that THREE's BufferGeometry class has an option for 'groups' that will be drawn in separate calls, but that alone doesn't seem to fix it. Is there anything else you think I could do? Maybe using a custom shader? Or should I really go with Depth Peeling?

Upvotes: 1

Views: 2757

Answers (1)

Garrett Johnson
Garrett Johnson

Reputation: 2534

enter image description here

Screendoor transparency can be used to create stable transparency overlap across frames but comes with a few artifacts that may be undesireable. This gist of the technique is to discard pixels in a screen space pattern depending on how opaque the object is supposed to be. The pattern can be derived from a texture or generated in the shader. The fragments that aren't discarded still write to depth and no alpha blending is used.

Here's a bit of code to get started.

Generating a DataTexture with the dither pattern:

const data = new Float32Array(16);
data[0] = 1.0 / 17.0;
data[1] = 9.0 / 17.0;
data[2] = 3.0 / 17.0;
data[3] = 11.0 / 17.0;

data[4] = 13.0 / 17.0;
data[5] = 5.0 / 17.0;
data[6] = 15.0 / 17.0;
data[7] = 7.0 / 17.0;

data[8] = 4.0 / 17.0;
data[9] = 12.0 / 17.0;
data[10] = 2.0 / 17.0;
data[11] = 10.0 / 17.0;

data[12] = 16.0 / 17.0;
data[13] = 8.0 / 17.0;
data[14] = 14.0 / 17.0;
data[15] = 6.0 / 17.0;

ditherTex = new THREE.DataTexture(data, 4, 4, THREE.LuminanceFormat, THREE.FloatType);
ditherTex.minFilter = THREE.NearestFilter;
ditherTex.magFilter = THREE.NearestFilter;
ditherTex.anisotropy = 1;
ditherTex.wrapS = THREE.RepeatWrapping;
ditherTex.wrapT = THREE.RepeatWrapping;

And some shader code to discard the fragments by:

// ...
uniform sampler2D ditherTex;    
void main() {

    // ...

    // get the color of the surface and discard pixels based on the dither pattern       
    vec4 texColor = texture2D(diffuseTex, vUv);
    vec4 color = texColor * vec4(color.rgb, opacity);

    if(texture2D(ditherTex, gl_FragCoord.xy / 4.0).r > color.a) discard;

    // ...

}

You'll want to set the data texture to the ditherTex uniform used in the shader. You can use a different texture, as well, if you want to use something stylized or less regular.

Lastly some things to keep in mind:

  • Texture opacity should be used when comparing to the dither texture.

  • The screen door artifacts can be mitigated with some antialiasing approaches to blend surrounding pixels.

  • Material.transparent should be false.

  • Because fragments are not blended objects with the same opacity will not show visible overlap.

Hopefully that helps! Let me know if you have other questions.

Upvotes: 3

Related Questions