Trying fake light effect but cant get the directions right

Hope you are all doing well.
I am working on a fakelight effect with multiple light sources based on MeshStandardMaterial.

Here is the shader code below;

object.material.onBeforeCompile = function(shader) {
    shader.vertexShader = `
        varying vec3 vTransformedNormal;
    ` + shader.vertexShader;

    shader.vertexShader = shader.vertexShader.replace(
        `#include <worldpos_vertex>`,
        `
        #include <worldpos_vertex>
        vTransformedNormal = normalize(normalMatrix * normal);
        `
    );

    shader.fragmentShader = `
        uniform vec3 worldPosition;
        uniform vec3 lightDirections[3];
        uniform vec3 lightColors[3];
        uniform float lightIntensities[3];
        uniform float uBaseLight;
        uniform vec3 uAmbientLight;
        varying vec3 vTransformedNormal;
    ` + shader.fragmentShader;

    shader.fragmentShader = shader.fragmentShader.replace(
        `vec4 diffuseColor = vec4( diffuse, opacity );`,
        `
        vec4 diffuseColor = vec4( diffuse, opacity );
        vec3 totalLight = uAmbientLight;
        for(int i = 0; i < 3; i++) {
            float fakeDiffuse = max(dot(vTransformedNormal, lightDirections[i]), uBaseLight);
            totalLight += lightColors[i] * fakeDiffuse * lightIntensities[i];
        }
        diffuseColor.rgb = clamp(diffuseColor.rgb * totalLight, 0.0, 3.0);
        `
    );

    shader.uniforms.worldPosition = { value: new THREE.Vector3() };
    shader.uniforms.lightDirections = { value: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()] };
    shader.uniforms.lightColors = { value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)] };
    shader.uniforms.lightIntensities = { value: [1.0, 1.0, 4.0] };
    shader.uniforms.uAmbientLight = { value: new THREE.Vector3(ambientRate, ambientRate, ambientRate) };

    object.material.userData.shader = shader;
};

and i pass the light information like this

directionVec.set(lightPos.x - worldRef.position.x, lightPos.y - worldRef.position.y, lightPos.z - worldRef.position.z).normalize();
directionVec.applyQuaternion(worldRef.quaternion);
object.material.userData.shader.uniforms.lightDirections.value[i].copy(directionVec);

when i try like this;

directionVec.set(lightPos.x - worldRef.position.x, lightPos.y - worldRef.position.y, lightPos.z - worldRef.position.z).normalize();

results are like below;


and when i try with this;

directionVec.set(worldRef.position.x - lightPos.x, worldRef.position.y - lightPos.y, worldRef.position.z - lightPos.z).normalize();

this is the result below

I am stuck on this right now. I am not sure has anybody tried it before but it would really welcome if you have any clue how to solve it.

NOTES;

  • I tried inverting quaternion
  • I tried with EULER
  • I also played with the shader but no luck

Thanks !

While waiting for clues, you could try to make a minimal online-debuggable example (CodePen, JSFiddle, CodeSandBox, etc.) No need to replicate all the game, just one central light source + some simple meshes instead of characters around the light. Chances are that you may find the bug while making this minimal demo. If not, this will allow others to debug the code, instead of trying to guess from the code fragments.

BTW, just curious, why do you need directionVec.applyQuaternion(worldRef.quaternion)? If the light and the character are both inside worldRef (I don’t know what worldRef is, maybe this is the whole scene?!), then the orientation of worldRef should have no impact on the calculations. If you apply the quaternion, this may rotate the vector twice.

Edit: This is just an observation, it might be completely irrelevant. Whenever the character turns, the red area turns twice. This appears to happen to all positions in the image. I’ve marked a few: The black arrow is the rotation of the character, the red arrow is the rotation of the red-lit part.

worldref is the upper parent of the object (which have its material modified) attached to scene. I have 3-4-5-6 level child elements and changing. So in my game instead of getting worldposition and worldquaternion of the child object every time, i just reference the worldRef in the child ( top parent attached to scene - optimization ) so that i can use their rotations and positions.
I will check if i can make a quick demonstration… :thinking:

I tried to demonstrate here: https://jsfiddle.net/jamirgame/0tw8c9mx/473/
hope this can help :thinking:
i am not sure if i could make a test environment because it is my first fiddle, if you have any questions i can answer.

UPDATED FIDDLE: https://jsfiddle.net/jamirgame/0tw8c9mx/631/
I commented out applyQuaternion. I am not sure what i do wrong but the directions are still not working right. It must be something that i am doing wrong, i will try to fix it on the game. Thanks!

I’m still struggling to understand the logic in the code (it looks more complex than I’d expect it should be) and I still cannot understand what is the expected behaviour. This is a snapshot, what is wrong with the light in it? What direction is wrong? The head of the character looks OK, and the bluish color of the head is always towards the light (even while I rotate the human with the mouse).

image

Edit:

BTW, when I change the color in the shader code (e.g. to be red, independent on the fake lights and the distances to them), the final color is very dark red. I’d expect to be bright red. Because you modify part of the shader, are you sure that this modified part goes well with the rest of the shader?

Additional question: why do you need to customize the shader? Could you go with three lights and just update their positions, colors and intensities based on the closest 3 lights from the array of fake lights?

2 Likes

Yeah it looks like it is working on the fiddle, i think the increasing intensity made me confused.

This is the problem i am actually facing in the game: JAMIR - Online sci fi browser FPS Game - Google Chrome 2023-10-21 15-18-38
When i rotate the character, light stays the same but i think this is an issue about me so as i said i will try to find a way out directly on the game.

I tried with pointlights before but wasnt happy with the performance. Fiddle code let object to take up to 3 light sources but in my code it will be 5 or maybe even more. I just I am trying to imitate light on moving objects because in a secenario where there are lots of light objects like in this one

it is impossible for me to make it work with pointlights ( all the plants are light sources )

Here is my attempt of fake lights without modifying shaders. In the demo there are a total of 961 lights, but only 4 of them are actually active spot lights (the number is configurable, see line 124).

The initial creation of fake lights is in lines 105-120, and the real lights are in lines 124-131. The update of lights in real time is in lines 134-146 – the closest lights are identified and their parameters are copied into the real lights.

https://codepen.io/boytchev/full/KKJKdrQ

image

1 Like

I have an extra lights option in the graphic settings which is doing the same as your code. I cant enable it by default because it is too costly and prevent most of the players play the game in a high fps rate.

Stable objects already have their own lightmaps, even grasses and some other custom ones gets lightened in an artificial way ( that took me a lot of days )

And i already have a pool of 2 pointlights in the scene for shooting lights of my player, other players and also for the effect where bullets hits on a metal surface which fills my light limit.

Thats why i am trying to modify the shaders of only moving objects in the game and i am trying to make a general and an easy setup for passing all the moving objects. I tried this approach with a lot of lights today and i didnt face any bottleneck instead of finding the first three lights function. It needs to be optimized and search for the nearest lights based on avoiding too far lights check.

Shouldn’t point lights without shadow be theoretically as fast as fake lights? They both would calculate a color multiplier based on angle. What’s the difference? Or is it that the point lights affect other stuff too (not baked) and we need selective lighting?

Or is it that the point lights affect other stuff too (not baked) and we need selective lighting?

I have lightmaps for non-moving objects like every game (including floor) and im able to lighten up the instanced grases, stones etc with a custom shader cheaply so i dont need all objects to get effected by a light source which makes it more expensive. In my case it is impossible for me to use stable pointlights.

And when i try the current approach in gameplay, the results looking nice altough the directions are not right. One thing left is to fix the directions…

1 Like

Looks like selective lighting is coming out with WebGPURenderer. I wish selective lighting were out of the box with WebGLRenderer like other engines (f.e. Babylon’s).

I had some fun playing Jamir btw. Looking forward to what’s next!

Yeah i feel like a child looking to a candy shop through glass when i saw the webgpu examples, especially this: WebGPU Clustered Forward Shading

Right now it is impossible to transfer everything to webgpu because there are many custom shaders in the game and it was really hard to create them and i didnt read or tried anything with wgsl yet.

Thank you for playing i am glad you had fun :heart_eyes:

1 Like

Here is the latest version which is looking more natural.

You can use upY for;
Shining of the passed light to every direction => 0
Shining of the passed light if the object’s position y is higher then light => 1
Shining of the passed light if the object’s position y is lower then light => -1
You can change 5000.0 in the fragment to fit your requirements.

I have made it for max 4 lights.

I have tested it with MeshPhongMaterial and MeshStandardMaterial and the results are fine.

material.onBeforeCompile = function(shader) {
    shader.vertexShader = `
        varying vec3 vTransformedNormal;
        varying vec4 vLocalPosition;
    ` + shader.vertexShader;

    shader.vertexShader = shader.vertexShader.replace(
        `#include <worldpos_vertex>`,
        `
        #include <worldpos_vertex>
        vTransformedNormal = normalize(normalMatrix * normal);
        vLocalPosition = modelMatrix * vec4(position, 1.0);
        `
    );

    shader.fragmentShader = `
        uniform vec3 lightDirections[4];
        uniform vec3 lightColors[4];
        uniform float lightIntensities[4];
        uniform float lightMaxDistances[4];
        uniform float uBaseLight;
        uniform vec3 uAmbientLight;
        uniform float upY[4];
        uniform vec3 lightWorldPositions[4];

        varying vec3 vTransformedNormal;
        varying vec4 vLocalPosition;
    ` + shader.fragmentShader;

    shader.fragmentShader = shader.fragmentShader.replace(
        `vec4 diffuseColor = vec4( diffuse, opacity );`,
        `
        vec4 diffuseColor = vec4( diffuse, opacity );
        vec3 totalLight = uAmbientLight;
        vec3 worldPosition2 = vLocalPosition.xyz;

        for(int i = 0; i < 4; i++) {
            vec3 lightToFragment = normalize(worldPosition2 - lightWorldPositions[i]);
            float distanceToLight = length(lightWorldPositions[i] - worldPosition2);
            float attenuation = 1.0 - clamp(distanceToLight / lightMaxDistances[i], 0.0, 1.0);
            float lightToFragmentDotNormal = dot(lightToFragment, vTransformedNormal);
            float yDifference = abs(worldPosition2.y - lightWorldPositions[i].y);
            float intensityFactor = clamp(1.0 - yDifference / 5000.0, 0.0, 1.0); 
            bool applyLighting = true;
            if (upY[i] < -0.5 && worldPosition2.y > lightWorldPositions[i].y) {
                applyLighting = false; 
            } else if (upY[i] > 0.5 && worldPosition2.y < lightWorldPositions[i].y) {
                applyLighting = false;
            }
            if (applyLighting) {
                float fakeDiffuse = max(dot(vTransformedNormal, lightDirections[i]), uBaseLight);
                totalLight += lightColors[i] * fakeDiffuse * lightIntensities[i] * intensityFactor * attenuation;
            }
        }
        diffuseColor.rgb = clamp(diffuseColor.rgb * totalLight, 0.0, 3.0);
        `
    );

    shader.uniforms.upY = { value: [0.0, 0.0, 0.0, 0.0] };
    shader.uniforms.lightWorldPositions = { value: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()] };
    shader.uniforms.lightDirections = { value: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)] };
    shader.uniforms.lightColors = { value: [new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, 1)] };
    shader.uniforms.lightIntensities = { value: [1.0, 1.0, 1.0, 1.0] };
    shader.uniforms.lightMaxDistances = { value: [5000.0, 5000.0, 5000.0, 5000.0] };  
    shader.uniforms.uAmbientLight = { value: new THREE.Vector3(ambientRate, ambientRate, ambientRate) };
    material.userData.shader = shader;
};