Seams in Transparent Edges of PNG Texture in BasicMeshMaterial and

Intro:
I need insights on how to achieve what is described below. I’ve tried many things but have found no perfect solution yet. I am not sure if I am struggling due to lack of knowledge about THREEJS, or some limitation, or some design decision of the framework, or a bug, or what not. Any help is appreciated.

Goal:
To be able to display (in the XY plane) a stack of plane meshes (not the same Z value), each containing a semi-transparent gray-scale texture map and a background color for tinting, such that viewing the entire stack of planes gives the impression that they form a single and more complex image.

Issue:
When viewing 2 partially overlapping plane meshes of the same background color and both having partially transparent PNG texture maps, the edges of the texture (where transparency transitions from opaque to fully transparent) produce a subtle yet awful looking seam between the two meshes.

Expected behavior:
Continuing from the description above. Since the material of the 2 planes will look like a partially transparent image of the same color, it is expected that there would be no seam when they overlap, as if the 2 planes were a single and bigger texture of a single color.

Example:
2 plane meshes with same background color (0xF1BDAE) and same semi-transparent gray-scale texture (from SymbolArtEditorOnline/241.png at 421a0379abb9916a20c4e0700b054c1fda2ba440 · malulleybovo/SymbolArtEditorOnline · GitHub). In it, the is a subtle seam on the right edge of the left disk. Since the texture is completely white + alpha, the plane should be entirely one color + alpha. On the edge of the texture, it should take up the color value of what is beneath (which is the exact same color. But it doesn’t do so and results in a seam.
image
This seam gets worse when zooming out as well.
image
This is the code for the mesh and the material. I tried using multiple types of custom blending, all of which just made it worse. The “best” result so far was this code, but this result is still unacceptable for the project it will go on since it is a premise that there will never be seams.

// mesh for each of the disks in the image.
new THREE.Mesh(/* geometry below */, /* material below */);

new THREE.ShapeGeometry(new THREE.Shape([
            new THREE.Vector2(-1, 1),
            new THREE.Vector2(1, 1),
            new THREE.Vector2(1, -1),
            new THREE.Vector2(-1, -1)
        ]))

new THREE.MeshBasicMaterial({
                    color: color.value,
                    transparent: true,
                    side: THREE.DoubleSide,
                    opacity: opacity.value
                })

Another example of the seams below, this time in a more complex image of an arm pointing to the side made of multiple meshes. Note that the skin color got really ugly due to the multiple seams that were never supposed to be there,
image

Details of the scenario of usage:

  • Uses new THREE.WebGLRenderer({ powerPreference: ‘high-performance’, antialias: true })
  • Uses new THREE.OrthographicCamera(-1, 1, -1, 1, 0.1, 1) for viewing (camera direction -Z)
  • Stacks meshes in the 3D scene with enough delta-Z to avoid aliasing and are displayed in the XY-plane at some Z.
  • meshes use the same code shared above for the mesh, geometry, and material. The geometry may be changed to stretch any corner of any mesh.

Conclusing:
The project is currently unreleased work, but will be open-source in the near future. It is highly desirable that this gets fixed before release, but I am running out of thoughts on how to address this.

  • I’ve spent a good amount of time trying to use THREE.CustomBlending options to no avail.
  • I’ve doubted it was the texture image at fault, but it wasn’t (I have another project using it with PixiJS and pixi does a wonderful job in this regard, though it is only 2D; plus the image is fully white and transparent so it cannot display more that one RGB color when tinting).
  • I’ve thought about changing the material to a THREE.ShaderMaterial and doing everything my way, but that’s the extreme end of solutions to me, which is why I am posting this question. I have not tried this yet and want to avoid it if at all possible.

Are there any leads on how to remove these seams from transparent textures?

Not really a solution - since it’ll introduce very hard edges to the images - but I have a slight feeling that setting both the texture.minFilter and texture.magFilter to THREE.NearestFilter will remove those seams :sweat_smile:

Looks more like an issue with blending linearly filtered textures, rather than just blending textures (see Magnification Filters part of the Texture docs, it explains how those filters work.)

I agree with you in this not really being a true solution. The answer after trying this out was a yes and a no. Yes, it works because the seams disappear. But no, because the lack of the linear filter is causing some aliasing and the low-res texture causes massive pixels to be rendered.

Evidence of texture with nearest filter applied.
image
image

I will keep this workaround in mind, but I still seek a legitimate solution to this since I doubt this is impossible to solve nicely.

Additionally, I have tried using a mix of material “alphaTest” values. For instance, alphaTest = 0.5 removes the seams. But I have textures that are entirely semi-transparent (like particles) that end up looking super weird with any alphaTest value I tried. It makes them look less transparent and less natural.

To me, the takeaway from this experiment was that for some reason, the linear filter may be causing darkening of the RGB on the transparent edges. It could be the blending, yet I can’t spot the exact cause yet.

Any other suggestions?

I think I found another workaround (that could be interpreted as a solution depending on how you see it).

Shout out to manthrax at three.js - ThreeJS: White PNG image loaded as texture, used as material and rendered as plane has grey edges - Stack Overflow for the insight.

As it turns out, the fully transparent portions of the PNG images I am using as textures are RGBA 0x00000000. They are black but fully transparent. For some reason, the fragment shader is using the black pixels to calculate the color even though the transparency is 0 (invisble). One would expect invisible pixels to be weightless is such calculations, but to my surprise they have weight for whatever reason…

In the end, the cleanest solution in my case will be to go through each of the many resources in the project and edit them to make sure that every pixel is white + transparency.

One more update on this.

Up until now the solution I reported above has worked on all but one browser: iOS Safari.

Verified working on: Chrome (win10), Firefox (win10), Opera (win10), Safari (MacOS Big Sur).
Not working on: iOS Safari.

No matter what I try, I cannot seem to get this working on all browsers. Before switching to Three.js, I used Pixi.js. It never had this issue, but I don’t know how it sets up blending and the fragment shader to make it work so I could replicate it in Three.js.

The closest alternative solution I found was White image is getting grey edges when rendered · Issue #14668 · mrdoob/three.js · GitHub. This got the PNG texture transparency working on all browsers, but its use of premultiplied alpha messed up something else that was working: the material opacity. Doing this made opacity < 1 result in a color blend with white, instead of just reducing the material transparency.
NearestFilters are also not solutions. Variations of CustomBlending each caused an undesirable effect (NormalBlending is the best one).

To get this to work 100%, I’d need PNG texture transparency (basically a sprite image) to not cause seams while also being able to further reduce the transparency of the whole material that uses the texture (to make the sprite fade away).

My question for anyone knowledgeable on this matter is: why would the code shared in the beginning of this issue using texture containing (white, alpha) pixels cause the seams exemplified before ONLY in iOS Safari? Could it be a difference in WebGL (doubt)? And any ideas what to try out?