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

MeshPhysicalMaterial: Update version for certain transmission changes. #22379

Merged
merged 1 commit into from
Aug 23, 2021

Conversation

Mugen87
Copy link
Collaborator

@Mugen87 Mugen87 commented Aug 20, 2021

Related issue: Fixed #22365.

Description

If you setup a material with zero transmission and increase it at a later point, nothing will happen. That's because the transmission code path of MeshPhysicalMaterial is only used when transmission > 0. A recompile is required when USE_TRANSMISSION should be defined in the shader (or removed).

The only way to detect this with the current transmission implementation is to check each frame the transmission value in setProgram() and then set needsProgramChange if necessary.

If we don't want this code section for performance reasons, it's up to the user to set Material.needsUpdate to true. However, I think the behavior reported in #22365 is buggy and should be fixed.

@WestLangley
Copy link
Collaborator

If you setup a material with zero transmission and increase it at a later point, nothing will happen

That is not exactly true. It depends on the value of material.transparent.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 20, 2021

Thanks for the clarification but this does not affect the change.

@mrdoob
Copy link
Owner

mrdoob commented Aug 20, 2021

If we do this we'll have to do the same for sheen and clearcoat. What do you think?

@mrdoob
Copy link
Owner

mrdoob commented Aug 20, 2021

We can probably group them though:

if ( material.isMeshPhysicalMaterial ) {

	if ( materialProperties.clearcoat !== material.clearcoat > 0 ) {

		needsProgramChange = true;

	} else if ( materialProperties.sheen !== material.sheen > 0 ) {

		needsProgramChange = true;

	} else if( materialProperties.transmission !== material.transmission > 0 ) {

		needsProgramChange = true;

	}

}

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 20, 2021

It seems clearcoat is implemented in a way that does not require such checks. The respective GLSL defines (USE_*) only depend on the maps. And certain clear coat related GLSL code is always executed when using MeshPhysicalMaterial. That means you can change clearcoat from 0 to >0 without settings needsUpdate to true.

sheen on the other hand does it similar like transmission without this PR. If you change the initial value after the first render, you have to set needsUpdate to true. However, I think it's less error prone if sheen is also changed.

@mrdoob
Copy link
Owner

mrdoob commented Aug 20, 2021

Actually, I was meant to try this for a while...

Seems like we can use Proxy and have this logic in the material instead:

class Material {

	constructor() {

		this.transmission = 0;
		this.version = 0;  
		
		return new Proxy( this, {

			set: function ( target, property, value ) {
      			
				if ( property === 'transmission' ) {
				
					if ( target[ property ] > 0 !== value > 0 ) {

						target.version ++;

					}
				
				}

				target[ property ] = value;
        
				return true;

			}

		} );
		
	}

}
const material1 = new Material();
material1.transmission = 1;
console.log( material1.version ); // 1;

const material2 = new Material();
material1.copy( material2 );
console.log( material1.version ); // 2;

https://caniuse.com/proxy

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 20, 2021

Interesting! I clearly favor this approach instead of enhancing setProgram(). Should we give Proxy a try in context of MeshPhysicalMaterial? If this works, we can utilize it for other use cases, too.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 20, 2021

Do you think this could irritate users^^?

image

@takahirox
Copy link
Collaborator

takahirox commented Aug 20, 2021

BTW, if I understand correctly a material can support multiple programs now (ex: a program with skinning and another program without skinning). So I expected that if user changes .transmission from or to 0 an appropriate program (one using USE_TRANSMISSION or another one not using it) will be selected. But it seems I expected too much or my understanding is wrong. (I haven't read through the multiple programs code path yet.) Sorry, I should have tested the case when I made a transmission PR.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 20, 2021

@takahirox For performance reasons the getProgram() function of the renderer is not executed per frame. That means getParameters() and the related computation of the program cache key does also not happen per frame.

This circumstance makes it impossible to detect the case reported in #22365. It has to be manually checked in setProgram(). Such checks are not necessary if the execution in the shader does not rely on defines. Meaning a transmission of 0 produces a different shader program than >0. That is similar to how sheen is implemented. Theoretically, it is possible to write shaders in a way such that multiple permutations do no occur. However, I'm not sure if the transmission and sheen code can be refactored in this fashion.

@donmccurdy
Copy link
Collaborator

donmccurdy commented Aug 20, 2021

However, I'm not sure if the transmission and sheen code can be refactored in a similar fashion.

Difficulty of refactoring aside, these are expensive effects. We probably do not want them enabled in the shader if they aren't being used... To expand on @takahirox's question a bit, suppose we have three materials in the scene:

  • (1) MeshPhysicalMaterial w/ .transmission=0
  • (2) MeshPhysicalMaterial w/ .transmission=0.5
  • (3) MeshPhysicalMaterial w/ .transmission=1

If we modify material (2) and set .transmission=0, also setting .needsUpdate=true if necessary, does this incur a recompile? Or does it reuse the underlying program from (1)?


I am a bit nervous of trading an unintuitive "transmission does not update" case for an unintuitive "frame dropped during recompile" case. Knowing that .needsUpdate=true incurs a recompile at least makes the performance predictable... users will probably complain less about the performance, but I think that's mostly because it's easier to miss the lag and let it slip into your application. Users may want the ability to animate properties smoothly, without a recompile when the value reaches 0.

Sorry I believe this was already discussed, but had we already decided against using .transmission = null to disable it?

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 20, 2021

If we modify material (2) and set .transmission=0, also setting .needsUpdate=true if necessary, does this incur a recompile? Or does it reuse the underlying program from (1)?

It will reuse (1). There will be no recompile.

We probably do not want them enabled in the shader if they aren't being used...

I was originally hoping we could opt-out expensive code sections without defines. But it seems there is no way around different shader permutations...

@takahirox
Copy link
Collaborator

had we already decided against using .transmission = null to disable it?

I don't think we discussed it because I assumed as #22379 (comment) and thought zero or non-zero approach worked well (but it was wrong).

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 20, 2021

Sorry I believe this was already discussed, but had we already decided against using .transmission = null to disable it?

I think similar to sheen it is more clear when optional shader features are null by default.

Related #17700 (comment).

@mrdoob
Copy link
Owner

mrdoob commented Aug 20, 2021

@Mugen87

Do you think this could irritate users^^?

image

That's annoying indeed...

There's this option:

class Material {

	constructor() {

		let transmission = 0;

		Object.defineProperty( this, 'transmission', {

			enumerable: true,

			get: () => {

				return transmission;

			},
			set: ( value ) => {

				if ( transmission > 0 !== value > 0 ) {

					this.version ++;

				}

				transmission = value;

			}

		} );

		this.version = 0;

	}

}

But with this approach transmission doesn't show up 😬

Screen Shot 2021-08-20 at 11 35 28 PM

There's also this option:

class Material {

	#transmission = 0;

	constructor() {

		this.version = 0;
		
	}

	get transmission() {

		return this.#transmission;
	
	}

	set transmission( value ) {

		if ( this.#transmission > 0 !== value > 0 ) {

			this.version ++;

		}

		this.#transmission = value;
	
	}

}

But transmission still doesn't show up. Yet #transmission does... 😬😬

Screen Shot 2021-08-20 at 11 37 55 PM

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 22, 2021

I favor the private instance field approach. Besides, we already use getter/setters for certain properties like e.g. PointLight.power. And private instance fields could be a nice replacement for the previous underscore syntax in order to declare private properties.

@Mugen87 Mugen87 changed the title WebGLRenderer: Detect recompile when transmission changes. MeshPhysicalMaterial: Update version for certain transmission changes. Aug 22, 2021
@Mugen87
Copy link
Collaborator Author

Mugen87 commented Aug 22, 2021

Browser support seems to be sufficient, too: https://caniuse.com/?search=private%20class%20fields

@mrdoob
Copy link
Owner

mrdoob commented Aug 23, 2021

Oh! I did not realize Safari 14.1 added support for private class fields. We live in the future! 😁

@mrdoob mrdoob added this to the r132 milestone Aug 23, 2021
@mrdoob mrdoob merged commit fcf526d into mrdoob:dev Aug 23, 2021
@mrdoob
Copy link
Owner

mrdoob commented Aug 23, 2021

Thanks!

@joshuaellis joshuaellis mentioned this pull request Aug 30, 2021
10 tasks
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.

MeshPhysicalMaterial: Transmission not working if initialized with 0.
5 participants