#31600 closed defect (fixed)

threejs does not handle transparency correctly

Reported by: tmonteil Owned by:
Priority: blocker Milestone: sage-9.3
Component: graphics Keywords:
Cc: egourgoulhon, gh-jcamp0x2a, paulmasson, slelievre, gh-guenterrote Merged in:
Authors: Joshua Campbell Reviewers: Matthias Koeppe
Report Upstream: N/A Work issues:
Branch: 515bcf4 (Commits, GitHub, GitLab) Commit: 515bcf400723d186740db76a21d1613c1cf1ddf7
Dependencies: Stopgaps:

Status badges

Description (last modified by tmonteil)

Here is an example to reproduce it yourself:

theta,z=var('theta,z')
cb = cylindrical_plot3d(2,(theta,0,2*pi),(z,-5,5), alpha=0.3, color='blue')
cr = cylindrical_plot3d(1,(theta,0,2*pi),(z,-7,7), alpha=0.3, color='red')
cv = cylindrical_plot3d(0.5,(theta,0,2*pi),(z,-10,10), alpha=0.5, color='green')
p = cb + cr + cv

Compare

p.show(aspect_ratio=1)

with

p.show(viewer='jmol', aspect_ratio=1)

As you can see, with threejs, when you rotate the picture, on some angles, the transparency is correct, but sometimes there is no transparency at all, and sometimes there is a wrong transparency, see the attached pictures.

Attachments (4)

threejs.good.transparency.png (19.9 KB) - added by tmonteil 15 months ago.
threejs.no.transparency.png (17.3 KB) - added by tmonteil 15 months ago.
threejs.wrong.transparency.png (20.1 KB) - added by tmonteil 15 months ago.
noDepthWrite.html (483.9 KB) - added by gh-jcamp0x2a 15 months ago.
Example with depth write of text and transparent surfaces disabled

Download all attachments as: .zip

Change History (21)

Changed 15 months ago by tmonteil

Changed 15 months ago by tmonteil

Changed 15 months ago by tmonteil

comment:1 Changed 15 months ago by tmonteil

  • Cc egourgoulhon gh-jcamp0x2a paulmasson slelievre gh-guenterrote added
  • Description modified (diff)

comment:2 follow-up: Changed 15 months ago by gh-jcamp0x2a

  • Branch set to u/gh-jcamp0x2a/31600-transparency
  • Commit set to f52b0ad9946e9b1d47ec76fffe65717bd76ca524

I've used the render_order option in the past to help with nested transparent surfaces. So you'd make sure your cylinders are rendered innermost to outermost (green then red then blue):

theta,z=var('theta,z')
cb = cylindrical_plot3d(2,(theta,0,2*pi),(z,-5,5), alpha=0.3, color='blue', render_order=-1)
cr = cylindrical_plot3d(1,(theta,0,2*pi),(z,-7,7), alpha=0.3, color='red', render_order=-2)
cv = cylindrical_plot3d(0.5,(theta,0,2*pi),(z,-10,10), alpha=0.5, color='green', render_order=-3)
p = cb + cr + cv
show(p, viewer='threejs')

You'll still see issues with self-transparency due to the order in which the faces of each cylinder are drawn. Also, adjusting the render order can conflict with the text sprites, so I try to use negative values.


I don't know of a way to automatically set the render order or that it'd even be possible for some arbitrary set of transparent surfaces.

But for the self-transparency and text sprite problems I mentioned, perhaps it would be better to just disable writing to the depth-buffer entirely for transparent surfaces and text? I'm trying to think through whether this might result in other graphical glitches itself, since it seems so straight-forward that why wouldn't Three.js do this automatically when you make a material transparent?

I've pushed a commit with this change and will attach an example with the same cylinders plus an intersecting opaque surface for comparison.


New commits:

f52b0adDisable depth-buffer write for text and transparent surfaces

Changed 15 months ago by gh-jcamp0x2a

Example with depth write of text and transparent surfaces disabled

comment:3 in reply to: ↑ 2 ; follow-up: Changed 15 months ago by guenterrote

Replying to gh-jcamp0x2a:

But for the self-transparency and text sprite problems I mentioned, perhaps it would be better to just disable writing to the depth-buffer entirely for transparent surfaces and text? I'm trying to think through whether this might result in other graphical glitches itself, since it seems so straight-forward that why wouldn't Three.js do this automatically when you make a material transparent?

It might be for performance reasons. On such a small scene the difference might not be noticeable. (By the way, I noticed that on my little desktop, the noDepthWrite.html example ignites the cooling fan one second after I start rotating the scene, when viewing with the Chromium browser. But the situation is the same with the "normal" sage graphics output for this scene).

It is known that, in principle, a depth buffer cannot handle multiple transparent surfaces correctly unless they come in a consistent order (front-to-back or back-to-front).

Is there some documentation on what threejs does when setting depthWrite: false

Maybe the behaviour should be controllable by an option by the user.

I've pushed a commit with this change and will attach an example with the same cylinders plus an intersecting opaque surface for comparison.

The example looks good!

comment:4 in reply to: ↑ 3 ; follow-up: Changed 15 months ago by gh-jcamp0x2a

Replying to guenterrote:

Is there some documentation on what threejs does when setting depthWrite: false

The doc for it is basic, but it looks like it's only ever passed along as-is to WebGL's glDepthMask prior to rendering each object. A bit of the call stack in three.js:

Maybe the behaviour should be controllable by an option by the user.

Would want to default to disabling depth write I assume? So the user can toggle on the depth write only if they're having performance problems with lots of transparent surfaces. I'm still struggling to come up a non-performance reason for turning it on, though I have a limited imagination ;)

comment:5 in reply to: ↑ 4 ; follow-up: Changed 15 months ago by paulmasson

Replying to gh-jcamp0x2a:

Replying to guenterrote:

Is there some documentation on what threejs does when setting depthWrite: false

The doc for it is basic, but it looks like it's only ever passed along as-is to WebGL's glDepthMask prior to rendering each object. A bit of the call stack in three.js:

Maybe the behaviour should be controllable by an option by the user.

Would want to default to disabling depth write I assume? So the user can toggle on the depth write only if they're having performance problems with lots of transparent surfaces. I'm still struggling to come up a non-performance reason for turning it on, though I have a limited imagination ;)

Joshua, why don’t you ask these questions in the Three.js forum: https://discourse.threejs.org/

comment:6 in reply to: ↑ 5 Changed 15 months ago by gh-jcamp0x2a

Replying to paulmasson:

Joshua, why don’t you ask these questions in the Three.js forum: https://discourse.threejs.org/

Hi Paul! Thanks for the link to the forum. I'll try to keep it in mind as an additional resource for questions in the future.

It looks like something similar to this issue been discussed there quite a bit already. I found ThreeJS and the transparent problem to be an interesting read. The rule of thumb indeed seems to be to disable depth write for transparent objects. I also found GLTFLoader: Use depthWrite=false for transparent materials which seems to support that and makes up my lack of imagination with some links to cases where you'd want the depth-write to remain enabled.

There's also a few interesting techniques mentioned in there like screendoor transparency and "Weighted Blended Order-Independent Transparency" that I didn't know about and will have to read up on now. :)

comment:7 follow-up: Changed 15 months ago by guenterrote

I checked those references, and I must correct myself. This has no effect on performance. (I thought the use of the framebuffer is completely abandoned when setting depthWrite:false.)

The effect of depthWrite:false is that the order of the transparent surfaces is ignored: A blue transparent surface in front of a red transparent surface will tint the objects behind them in the same way as in the opposite order. The difference would probably only be noticeable if the transparent surfaces have some shinyness of their own.

For example, the top of the cylinders gives no visual clue whether you can see "into the pipe" from above or whether you look "up" to the pipe from below. (If you view it with tachyon, it really looks much better. Look at the transition between the green and red cylinder in the upper half and in the lower half)

Another case is when you have two transparent surfaces (plane) that intersect. Then you probably would like to see the difference, which one is in front and in particular where they intersect and change the order. But depthWrite:true also does not resolve such a case correctly.

I would advocate to switch to disabling depth-write by default.

comment:8 follow-up: Changed 15 months ago by guenterrote

Here is an example where depthWrite:false would eliminate a visual distinction:

sage: var("u v")                                                                                                        
sage: red1 = parametric_plot3d([u,v,0.1-u-v],(u,0,2),(v,0,2),alpha=0.6, color='red',render_order=1)                     
sage: green= parametric_plot3d([u,v,0.2-u-v],(u,1,3),(v,1,3),alpha=0.6, color='green',render_order=2)                  
sage: red2 = parametric_plot3d([u,v,0.3-u-v],(u,0,2),(v,2.2,4.2),alpha=0.6, color='red',render_order=3)                 
sage: (red1+green+red2).show()                                                                                          

But surprise when looking from behind! (Why did I find no documentation on the render_order option?)

comment:9 in reply to: ↑ 7 Changed 15 months ago by gh-jcamp0x2a

Replying to guenterrote:

(If you view it with tachyon, it really looks much better. Look at the transition between the green and red cylinder in the upper half and in the lower half)

Ray-tracing is the future :)

comment:10 in reply to: ↑ 8 Changed 15 months ago by gh-jcamp0x2a

Replying to guenterrote:

(Why did I find no documentation on the render_order option?)

It's listed in the reference manual for the Three.js viewer: https://doc.sagemath.org/html/en/reference/plot3d/threejs.html

Cool example :)

comment:11 Changed 15 months ago by git

  • Commit changed from f52b0ad9946e9b1d47ec76fffe65717bd76ca524 to ec7cc0c3543267cd37ad6cf8505fcf30f2889d51

Branch pushed to git repo; I updated commit sha1. This was a forced push. New commits:

7f2eafeDisable depth-buffer write for text and transparent surfaces
ec7cc0cAdd option to disable depth-buffer write for surfaces

comment:12 Changed 15 months ago by git

  • Commit changed from ec7cc0c3543267cd37ad6cf8505fcf30f2889d51 to 515bcf400723d186740db76a21d1613c1cf1ddf7

Branch pushed to git repo; I updated commit sha1. New commits:

515bcf4Update three.js viewer doc with depth_write option

comment:13 Changed 15 months ago by gh-jcamp0x2a

  • Authors set to Joshua Campbell
  • Status changed from new to needs_review

I've added an option and documentation for controlling whether to do the depth-write for surfaces.

comment:14 Changed 15 months ago by mkoeppe

  • Priority changed from major to critical

comment:15 Changed 15 months ago by mkoeppe

  • Milestone changed from sage-9.4 to sage-9.3
  • Reviewers set to Matthias Koeppe
  • Status changed from needs_review to positive_review

Just ran into this problem myself. This fixes it.

comment:16 Changed 15 months ago by mkoeppe

  • Priority changed from critical to blocker

comment:17 Changed 14 months ago by vbraun

  • Branch changed from u/gh-jcamp0x2a/31600-transparency to 515bcf400723d186740db76a21d1613c1cf1ddf7
  • Resolution set to fixed
  • Status changed from positive_review to closed
Note: See TracTickets for help on using tickets.