Backface Directional Lighting

So I have a bunch of instanced intersecting planes as a foliage system.

Looks pretty good, I’m mostly happy with it, however I have one issue. In it’s simplest form I use 2 intersecting square planes with a material set to THREE.DoubleSide. It seems that the directional light gets a little confused. If the directional light is hitting the back side of the plane the lighting is as if the geometry was rotated 180 degrees and is darker on the side facing the light. I was hoping since I set all the normals to (0,1,0) this wouldn’t be the case.

I’m assuming this is expected behavior but I’m not sure what the proper workaround is. Only thing I can think of is to set the material to THREE.FrontSide and create duplicate plane geometry facing the opposing way and merging. I am hesitant to do this as it will double the amount of geometry and also I’m sure there could be some z-fighting with planes overlapping like that.

Is there a better way?

4 Likes

Can you please share a screenshot that shows how the lighting looks like?

I believe TitansOfTime is talking about this:
image
Back side of the plane gets darker and I’ve ran into the same issue :sweat:
I feel like grass, regardless of the plane’s Y rotation or whether its frontface or backface, should have a unified lighting, like in this image:
image
or this:
image

Just like OP, I’ve tried setting all vertex normals to [0, 1, 0] (recommended in this nvidia page https://developer.nvidia.com/sites/all/modules/custom/gpugems/books/GPUGems/gpugems_ch07.html). I expected such normals to even out the lighting on both sides of the plane, but it didnt do much for me.

Using renderer.gammaInput = true; renderer.gammaOutput = true; renderer.gammaFactor = 2.2; settings gives me this:
image
These renderer settings makes it look much better, evens out the lighting by a lot, however it still feels like a hack rather than a solution.

Is there a better way?

3 Likes

Nice, that helps a lot!

1 Like

DolphinIQ, out of curiosity, are you using instancing in your example?

For this demo I made several thousands of plane buffer geometries in a loop and then merged them with BufferGeometryUtils. For serious apps I plan to use models made in 3D software.

I don’t really get instancing or why is it better than a big buffer geometry; would appreciate some directions/sources for dummies tho :sweat_smile:

Instancing will work in the end similar to merging but the memory footprint usage will be WAY smaller. When merging, my grass takes up about 600MB. With instancing it takes up about 8MB.

Pretty good explanation here: https://medium.com/@pailhead011/instancing-with-three-js-36b4b62bc127

If you want to go down this path (Recommended), I can probably help a bit. Learned the hard way.

2 Likes

As I was discussing privately with @titansoftime, I’ve modified Instancing Lambert example and the backface detection/rendering looks fine.

So it does not seem like a instancing related problem. Probably need to re-calculate normals after quaternion rotations, that would be my guess.

Here’s a simplified fiddle demonstrating the lighting issue I am experiencing (merged BufferGeometry).

https://jsfiddle.net/titansoftime/rosncw0m/

So, the “problem” here is that lambert shader reverts normal for backfaces. So, this is the expected behavior. lights_lambert_vertex - meshlambert_frag

The ideal workaround would be to create a custom shader adapting these to your needs. In this case, probably removing all the vLightBack calculation and just using vLightFront independant of which side is being drawn.

A hacky way to do it, is just replacing the reflectedLight.directDiffuse using onBeforeCompile to just account for vLightFront. Modified JSFiddle

But keep in mind that all the extra back light logic is still being calculated, and given the amount of instances… it would be worth removing all of that.

2 Likes

That’s it, perfect! Thank you!

Exactly what I was looking for.

I used this to remove the (now) unnecessary calculations:

shader.fragmentShader = shader.fragmentShader.replace('vLightBack += saturate( -dotNL ) * directLightColor_Diffuse;',"\n");

2 Likes

Just a thought…

Is something that maybe should be an option in the API itself?

Not sure how common this issue is.

Hi again :sweat_smile:
Do you perhaps know where in the meshStandardMaterial shader code can I find the equivalent of

#ifdef DOUBLE_SIDED
reflectedLight.directDiffuse = ( gl_FrontFacing ) ? vLightFront : vLightBack;
#else
reflectedLight.directDiffuse = vLightFront;
#endif

from the meshLambertMaterial? In other words, i’d like to apply the same logic to the standard material, but im getting lost in all the #includes :sweat:

normal_fragment_begin

Assuming you don’t need tangent and flat shading, just replace include with normal declaration:

shader.fragmentShader = shader.fragmentShader.replace(
  '#include <normal_fragment_begin>',
  'vec3 normal = normalize( vNormal );'
);

JSFiddle Example

3 Likes