In short
Enabling separateCullingPass
knocks out twoSidedLighting
setting in materials.
What we want to achieve
Render a double sided, semi-transparent material with proper lighting (flipped normals for back side facing triangles) while also using the separateCullingPass
option to first render the inside and then the outside of the mesh to minimize alpha z-issues.
Why do we want to do that
Imaging rendering a semitransparent open box. For alpha to work properly you need to render the inside of the box first, otherwise you will end up with the wrong z-order from some angles.
The meshes and materials come from an external source and we must be as smart as possible to make things work in the most number of scenarios, thus we can’t for example turn off the z-buffer or things like textures with semi-transparent holes in them will fail.
Suggestion for fix
- Change code so
twoSidedLighting
always flips the normals when the “back side” pass ofseparateCullingPass
is performed. - Either do this conditionally on
backfaceCulling
being false or update the documentation oftwoSidedLightning
so it applies both ifbackfaceCulling
is false and/or during the “back side” pass ofseparateCullingPass
.
Playground
A playground says more than a thousand words, so please have a look here:
https://www.babylonjs-playground.com/#GY9NG6#7
I commented the code quite a bit, here is most of the comments and a screenshot of how it looks. The boxes are numbered from left to right. All the boxes uses the same geometry which is a box with no top or bottom. The single light is indicated by the yellow arrow.
Box 1
The inside of the open box is rendered “incorrectly”:
- Back and left inside is black even though the light is shining on it.
- Right and front inside is lit even though it should be in shadow.
Box 2
We fix the lighting on the inside by enable twoSidedLighting
which flips the normals on the “back facing” triangles in the shader.
Box 3
We now enable transparency, but since the back of the box is drawn before the front, we get very incorrect rendering when the camera is in front of the boxes, for example looking straight along the z-axis.
Note 1: Simply changing the order of the indices isn’t enough, it will always be wrong from some camera angle, so that is not an workaround.
Note 2: We ignore facet sorting since it isn’t feasible in real life scenarios.
Box 4
A very good way to solve the issue of rendering both inside and outside of the meshes is to first render only the back faces then rendering only the front faces. Babylon.js has this built in via the separateCullingPass
option.
Enabling that fixes the alpha issues, however we are back at the lighting from box 1. Looking at the Babylon source code it appears that separateCullingPass
renders the mesh twice (as expected) by simply changing the orientation setting causing the back triangles to turn to front triangles etc.
However, looking at the source code for twoSidedLighting
in the shader code it only turns on if the triangles are back facing, which they never are due to the change of orientation by separateCullingPass
. Hence twoSidedLighting
is in practice disabled by separateCullingPass
.
Box 5
Scanning the shader source for a workaround I found forceNormalForward
which seems to be an effective workaround when separateCullingPass
is enabled. We now have a case where both the alpha and lighting issues are sorted! =)
However, forceNormalForward
has some drawbacks:
- It looks much more costly in the shader than
twoSidedLighting
. - Normals will not be flipped, but rather forced to always face the same direction on the triangle, no matter what they where to begin with. This can be correct, but might not be wanted/expected in edge cases where normals might intentionally be “the wrong way” in the loaded mesh.
- It doesn’t seem like this option is available on
StandardMaterial
, onlyPBRMaterial
?
Attempted workarounds
I have tried loads and loads of combinations and hacks to get this working before I found forceNormalForward
. I could not find a single one that worked without either manually modifying a clone of the Geometry to flip all the normals and manually rendering it first to get the inside. This however must be much more expensive compared to how separateCullingPass
works.
forceNormalForward
seems to do the trick for us, but see the drawbacks mentioned above. Also, I feel it lacks documentation to state exactly what it does. The shader code in pbrBlockNormalFix.fx
isn’t easy for a novice like myself to figure out, especially since it uses vEyePosition
which makes me a bit vary if it is really the right workaround for us.