Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Material: Add "material.alphaHash" transparency mode #24271

Merged
merged 7 commits into from
Jun 12, 2023

Conversation

donmccurdy
Copy link
Collaborator

@donmccurdy donmccurdy commented Jun 24, 2022

Related issues:

Description:

Proposal for adding .alphaHash: boolean and .alphaHashScale: number properties to all three.js Material types. This is an alternative method of transparency distinct from alpha blending (.transparent=true) and physically-based transmission (.transmission > 0), which is not a panacea but has some significant benefits:

  1. Keeps objects in the 'opaque' render list
  2. Avoids all sorting issues associated with alpha blending
  3. Less overdraw than alpha blending
  4. Less performance cost than transmission
  5. Keeps objects visible through transmissive materials

The major drawback of the method is noise / grain, which can be reduced significantly with appropriate anti-aliasing. With or without AA, alpha hash is a good choice in several use cases:

  1. Transitioning visibility of opaque objects without triggering sorting and backface problems during the transition. E.g. switching between LODs should not cause distracting flicker.
  2. Hiding boundaries between two opaque objects, e.g. blades of grass fade into the ground, without requiring everything to be transparent.
  3. Scenes with so much overlapping transparency that sorting issues are intractable

In these cases a bit of grain might be a reasonable tradeoff to avoid alpha blending issues — particularly if the transparent region is temporary, small, or distant from the camera.

The effect closely resembles Blender's Blend Mode: Alpha Hashed material property. Blender enables multi-sampling by default, which hides most of the grain, but this can be disabled in its renderer settings. The .alphaHashScale: number parameter should be used to tune the hash function to fit the multi-sampling method, with values in the range of 0.1–0.5 working well in my tests. When multi-sampling is not used, 1.0 is fine.

# samples three.js blender
1 three_1 blender_1
16 three_16 blender_16

Questions:

  • Do we want to support material.alphaHash? We could also consider a NodeMaterial-only solution, as a separate node: material.alphaTest = new AlphaHashNode( opacityNode, scale );
  • TAARenderPass is brightening the scene, and I'm not sure why. I've omitted all color management settings for now, to isolate that underlying problem. Any ideas here? Is this an appropriate type of multi-sampling to be using for this example? /cc @bhouston See below.
  • Better example? I used this chloroplast scene from Sketchfab, mainly because it doesn't work well with alpha blending, for any choice of depthWrite.

References:


This contribution is funded by The New York Times.

@bhouston
Copy link
Contributor

This is really cool! It is fast and the results are nice!

With regards to TAA causing things to get brighter when it is enabled, it appears to do this with or without your changes. I checked here and it has that same bug in main: https://threejs.org/examples/?q=taa#webgl_postprocessing_taa. This wasn't the case a long time ago so some change in the last year or two must have broken it. But it is unrelated to your changes.

@donmccurdy
Copy link
Collaborator Author

Thanks @bhouston! I'll file that as a separate issue then, I'm having some other trouble with color management in post-processing (unrelated to TAARenderPass) that may be worth looking at.

Assuming we'll find a fix so that the scene brightness doesn't fluctuate, I would be interested in everyone's feedback on whether we want to support this. Happy to clean up the example and implementation more if so. :)

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 25, 2022

With regards to TAA causing things to get brighter when it is enabled,

@donmccurdy Can you please check if reverting #23671 fixes the issue? If so, SSAARenderPass might need a custom copy shader.

@donmccurdy
Copy link
Collaborator Author

Thanks @Mugen87! Unfortunately I don't see a difference after reverting #23671. The same thing happens in the official TAA example if I add this line:

scene.background = new THREE.Color( 0x505050 );

@donmccurdy
Copy link
Collaborator Author

Possibly #9051?

@donmccurdy
Copy link
Collaborator Author

donmccurdy commented Jun 26, 2022

Setting scene.background explicitly to black fixes the most noticeable flashing in the demo, although the model itself still shows a bit of flicker caused by the TAARenderPass brightening issue I think. Hopefully that is good enough to evaluate and consider the .alphaHash feature for now.

EDIT: Also the noise/grain is much less noticeable on higher DPI displays, in many cases TAA might not even be needed if the transparent areas aren't the focus of the scene.

@hybridherbst
Copy link
Contributor

Great to see! Typically transparency-through-noise can be improved with alpha to coverage, have you tried that? Could reduce the noise even more for low-ish AA values.

@donmccurdy
Copy link
Collaborator Author

Good point — to be honest I haven't really used alpha-to-coverage before. Assuming I've set that up right (alphaHash=true, alphaToCoverage=true, transparent=false, renderer.antialias=true) the difference is visible, but not in a way that seems obviously better or worse in this example:

alphaToCoverage=true alphaToCoverage=false
atc_on atc_off

Despite the checkbox in the screenshot, TAA is off for both. I'm not confident enough about setting up post-processing to combine TAA with WebGL2 anti-aliased render targets. So there's more grain than in the original screenshots — there are probably better ways to set this up and minimize noise.

@gkjohnson
Copy link
Collaborator

@hybridherbst can you explain how alpha to coverage should help here? A2C will only have any affect if MSAA is enabled and the written fragment is partially transparent. It doesn't guarantee correct depth order of fragments as far as I know, either, with or without depth write enabled. Alpha hash or dithered transparency only works because it reliably writes to the depth buffer.

@donmccurdy can you explain the value of the of the "alphaHashScale" parameter? I would have expected that it would be possible to hash such that the noise is always 1 px in size.

@donmccurdy
Copy link
Collaborator Author

donmccurdy commented Jun 28, 2022

@gkjohnson from the paper, emphasis mine —

User parameter g_HashScale controls the target noise scale in pixels (default 1.0). Changing g_HashScale is useful if the chosen hash outputs noise at another frequency. Also, when temporal antialiasing, using noise below pixel scale (e.g., 0.3–0.5) allows for temporal averaging.

With TAA, I'm finding 0.1 looks pretty good. This may depend on the number of samples. Without TAA, I also see an improvement when decreasing the scale from 1 to 0.5, but not much change below that — unsure why that would happen.

The paper also discusses "Applications to Alpha-to-Coverage" but I haven't gotten into that area yet, it may require more work.

I was also hoping to test with WEBGL_multisampled_render_to_texture but didn't realize that was really only supported in XR devices. 🙃

@donmccurdy
Copy link
Collaborator Author

Hmm, from one of the authors...

To move to alpha to coverage, you need to independently compute the binary visibility tests for each sample and explicitly output them, e.g., via gl_SampleMask. You cannot output an alpha value from the shader and let the hardware apply its default dithering.

https://twitter.com/_cwyman_/status/935743334244605953

I don't think WebGL 1.0 or 2.0 include gl_SampleMask, that's an OpenGL 4.0 feature. :/

@donmccurdy
Copy link
Collaborator Author

donmccurdy commented Jul 6, 2022

I've filed the issues about TAARenderPass at #24318 — it turns out we can work around them both by using a black scene background and keeping pass.sampleLevel = 0. Because gl_SampleMask is not available, alpha-to-coverage and multi-sampling can't reduce the noise associated with Hashed Alpha Testing, so TAA seems to be the best option for reducing that noise. Or that's my assumption, please correct me if there are other options! On higher-DPI displays or smaller surfaces, the noise is much less noticeable and TAA may not be needed. TRAA (#14050) could be expected to do better on dynamic scenes.

Here's the preview link once again:

Visually, this is comparable to Blender, so I probably won't make any further changes here unless we decide to move forward with the feature. If so, some further testing and cleanup would be needed.

@sdarpeng
Copy link
Contributor

sdarpeng commented Nov 9, 2022

You should have this model to process your branch test.
@donmccurdy
hair_test1.zip

@github-actions
Copy link

github-actions bot commented Jun 5, 2023

📦 Bundle size

Full ESM build, minified and gzipped.

Filesize dev Filesize PR Diff
640 kB (158.8 kB) 642.6 kB (159.3 kB) +2.52 kB

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Filesize dev Filesize PR Diff
433.4 kB (105 kB) 435.8 kB (105.4 kB) +2.47 kB

@donmccurdy
Copy link
Collaborator Author

Updates on TAARenderPass in #24318 (comment).

@donmccurdy
Copy link
Collaborator Author

The live demo is looking much better thanks to @Mugen87 fixes to #24318!

@donmccurdy donmccurdy marked this pull request as ready for review June 7, 2023 14:06
@donmccurdy
Copy link
Collaborator Author

I believe this is a useful improvement for the example shared by @sdarpeng as well. The appearance is stable (no sort issues) under different camera angles.

before after
Screenshot 2023-06-07 at 10 11 06 AM Screenshot 2023-06-07 at 10 11 04 AM

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 7, 2023

Better example? I used this chloroplast scene from Sketchfab, mainly because it doesn't work well with alpha blending, for any choice of depthWrite.

I think the example is ideal for demonstrating this technique 👍 .

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 7, 2023

Do we want to support material.alphaHash? We could also consider a NodeMaterial-only solution, as a separate node: material.alphaTest = new AlphaHashNode( opacityNode, scale );

I vote to add Material.alphaHash. In this way, we can provide at least one way in the core for addressing typical transparency artifacts related to alpha blending.

IMO, TAA should be used for acceptable image quality. But given the manageable amount of changes related to the core, I think it's worth an addition.

@donmccurdy
Copy link
Collaborator Author

I know it doesn't sound like much, but to me a really compelling use for alpha hash is being able to fade opaque objects in/out while avoiding "inside-out" display... cases like that — where the alpha hashed surface is temporary or small — do not necessarily require TAA. This has come up repeatedly in projects.

I'll work on cleaning up the remaining issues here, thanks!

@mrdoob
Copy link
Owner

mrdoob commented Jun 8, 2023

Yeah, this looks good to me 👍

As per the example, maybe something like this one but every sphere has a different color and alpha 0.5?

@mrdoob mrdoob modified the milestones: r???, r154 Jun 8, 2023
Material.alphaHash: Add example

Clean up.

Update builds

Examples: Clean up alpha hash params

Fix background color changes, add GammaCorrectionShader

TAARenderPass: Initialize .accumulateIndex

Alpha Hash: Initialize sampleLevel=0 in example

Avoids flashing from TAARenderPass

Clean up alpha hash example.

Lint

Update builds

Add screenshot
@donmccurdy
Copy link
Collaborator Author

As per the example, maybe something like this one but every sphere has a different color and alpha 0.5?

I might shuffle the order of instances to make the problems with alpha blending more obvious from any angle, but yes that sounds fine!

If there are no objections, I'd like to remove the .alphaHashScale property for now, using a hard-coded default instead. I haven't found any way to create a scene that benefits from changing its value, despite my earlier response, so including the property feels premature.

@donmccurdy
Copy link
Collaborator Author

donmccurdy commented Jun 8, 2023

To simplify things I've removed the demo, and will create a new demo in a separate PR.

I've also moved the alphaHash GLSL out of the alphaTest shader chunks, and into its own chunks. Happy to revert that if we'd prefer otherwise.

@donmccurdy donmccurdy changed the title Material: Add .alphaHash and .alphaHashScale Material: Add "material.alphaHash" transparency mode Jun 8, 2023
@mrdoob mrdoob merged commit 77ba743 into mrdoob:dev Jun 12, 2023
@donmccurdy donmccurdy deleted the feat-alphahash branch June 27, 2023 21:23
@donmccurdy
Copy link
Collaborator Author

PR for the example:

@sdarpeng
Copy link
Contributor

Greaaaaat Job, guys.

@sdarpeng
Copy link
Contributor

sdarpeng commented Sep 1, 2023

2023-09-01.12-12-07.mp4

The new three.js alphaHash transparency looks fucking good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants