three.js: MeshPhysicalMaterial: clearcoat should not use underlying normal map
I have recognized that the PhyscialMaterialShader is taking the perturbed normal of the material into consideration. Seeing how the effect is implemented elsewhere (e.g. in Blender using the Principled BSDF shader node), I would expect the clearcoat to behave like a “flat additional coating” on top of the material and not being perturbed by the underlying normal map. Thus as a change request, I would suggest to conserve the “original” normal which should be fed into the calculation of the clearcoat reflection.
In order to achieve this getLightProbeIndirectRadiance needs to distinguish between specular and clearcoat reflection mapping; see Pseudocode:
vec3 getLightProbeIndirectRadiance( const in GeometricContext geometry, const in float blinnShininessExponent, const in int maxMIPLevel ) {
vec3 reflectVec;
if ( specular_reflections ) {
reflectVec = reflect( -geometry.viewDir, geometry.normal );
}
else { // clearcoat reflections
reflectVec = reflect( -geometry.viewDir, geometry.clearCoatNormal );
}
[...]
}
Alternativley passing the respective normals via the the function parameters (or adding the .originalNormal to the GeometricContext structure) could help too,
Cheers, Renate
img attachment: glossy leather with clearcoat on top
Three.js version
- Dev
- r88
- …
Browser
- All of them
- Chrome
- Firefox
- Internet Explorer
OS
- All of them
- Windows
- macOS

- Linux
- Android
- iOS
Hardware Requirements (graphics card, VR Device, …)
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 2
- Comments: 23 (5 by maintainers)
@WestLangley
How about we start by just making sure the clearCoat layer doesn’t have the geometry.normal perturbed? We can add parametrization later.
Can you hack a live example as a proof of concept?
Fixed by https://github.com/mrdoob/three.js/pull/17079. Thank you @arobertson0!
Agreed. That should be the objective.
Thing is, it seems to me the code needs to be significantly refactored to do that…
Hi! I’m also interested in a similar modification, and I am willing to implement it if the author of the first post is not interested anymore. The two demos now result in a 404, so I cannot see what has been done so far. However, I am not sure I understand why only getLightProbeIndirectRadiance() should be modified. Also direct light should take into account the double normal, right?
Thanks. I see your demo is working now.
No. Maybe you will fix your demo.
It is probably sufficient to keep it simple and just to add two properties:
If
clearCoatNormalMapis null, the clear coat uses no normal map.Unreal Engine 4 does this:
https://docs.unrealengine.com/latest/INT/Engine/Rendering/Materials/HowTo/ClearCoatDualNormal/
I’ve wanted this as well.
Basically the main way to do this is to add a second normal map and have it used for either the top layer or the bottom layer. I would prefer to have the second normal map be used for the surface because then we can call it clearCoatNormalMap and it is part of the clear coat top layer.
If it isn’t specified, then we would use the underlying normalMap for both? Or we could allow for a scalar parameter, clearCoatNormalWeight, that would scale between the clearCoatNormalMap and the underlying normalMap - normally it would be 0 and thus favor the underlying normalMap/bumpMap, but if you set it to 1 it would use the clearCoatNormalMap completely or it would be perfectly smooth if the clearCoatNormalMap isn’t set.
Does that logic work?
I think your demo is going to have to show a side-by-side comparison of two spheres to be able to see the subtlety you are trying to implement.
… and
normalScale.