I'm trying to make a 3D game for mobile. When rendering a very simple scene, my performance drops from 60 fps to 30 fps when I use fragment shader lighting instead of vertex shader lighting.

I really hate the look of the vertex shader lighting, but I'm worried about whether or not I'm going to be able to run any game system and still get a reasonable framerate if I go with fragment shader lighting. I would like to seek advice as to what should I choose, or if there's any optimisation I could make to a shader to keep the good look and the framerate.

Here's my shader code (I'm using GLSLes 3.0):

vertex shader

#version 300 es

in mediump vec4 position;
in mediump vec2 i_UV;
in mediump vec4 i_Normal;

out mediump vec2 UV;
out mediump vec3 Position;
out mediump vec3 Normal;
out mediump vec3 CamDir;
out mediump float fogFactor;

uniform mat4 transformationMatrix;
uniform mat4 rotationMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;

uniform mediump float IOR;
uniform mediump float roughness;
uniform mediump float specular;

out mediump float Fresnel_O;

void main(){
    UV = i_UV;
    Position = (transformationMatrix*position).xyz;
    Normal = normalize((rotationMatrix*i_Normal).xyz);
    CamDir = normalize((inverse(viewMatrix) * vec4(0,0,0,1.0)).xyz - position.xyz);

    fogFactor = exp(-pow(distance((viewMatrix*transformationMatrix*position).xyz,vec3(0.0,0.0,0.0))*0.01,2.0));

    mat4 a = viewMatrix;

    float F_I = -1.0/IOR;
    float F_A = dot(refract(CamDir,Normal,F_I),Normal);
    float F_B = dot(Normal,CamDir);
    Fresnel_O = pow((F_B*F_I-F_A)/(F_B*F_I+F_A),2.0) * (1.0-roughness);

    vec4 finalPos = projectionMatrix*viewMatrix*transformationMatrix * position;
    gl_Position = finalPos;

fragment shader

#version 300 es
precision mediump float;

out highp vec4 Fragment;
in mediump vec2 UV;
in mediump vec3 Position;
in mediump vec3 Normal;
in mediump vec3 CamDir;
in mediump float fogFactor;

const lowp int NUMBER_OF_LIGHTS = 10;
in mediump float Fresnel_O;
uniform mediump vec3 LightPos[NUMBER_OF_LIGHTS];
uniform mediump vec3 LightColor[NUMBER_OF_LIGHTS];
uniform mediump float LightIntensity[NUMBER_OF_LIGHTS];

uniform mediump float roughness;
uniform mediump float Metaliness;
uniform mediump float specular;
uniform mediump sampler2D textureSampler;
uniform mediump vec3 emission;

const mediump vec3 envColor = vec3(0.0,0.1,0.1) * 0.0;

void main(){
    vec4 albedo = texture(textureSampler,UV) * color;
    if(texture(textureSampler,UV).a < 0.5){ discard; }

    vec3 diffColorMultiplier = vec3(0.0);
    vec3 specColor = vec3(0.0);

    for(int i = 0; i < NUMBER_OF_LIGHTS && LightPos[i] != vec3(0.0); i++){
        mediump float LightDist = length(LightPos[i]-Position);
        mediump vec3 LightDir = (LightPos[i] - Position)/LightDist;

        float diffLightAmount = max(dot(normalize(LightDir), Normal), 0.0);
        diffColorMultiplier += LightColor[i]*((diffLightAmount/(LightDist*LightDist))*LightIntensity[i]);

        //custom specular algorythim based on phong shading. It gives me the look I want.
        float specularAmount = ((pow(max(dot(reflect(-normalize(LightDir), Normal), CamDir), 0.0), (1.0-roughness)*(1.0-roughness)*300.0) + diffLightAmount*0.01)/(LightDist*LightDist))*Fresnel_O*(LightIntensity[i]*LightIntensity[i]) * specular;
        specularAmount = max(specularAmount, 0.0);
        specColor += LightColor[i] * specularAmount;

    vec4 finalColor = vec4(mix(mix((albedo.rgb * diffColorMultiplier) + specColor,specColor,Metaliness)+envColor+emission,envColor,1.0-fogFactor),albedo.a);

    Fragment = finalColor;
  • \$\begingroup\$ The most obvious optimization to make here is to reduce your number of lights, either by outright deleting/combining some lights, or by using a tile-based Forward+ style rendering where each tile of the output uses only a subset of the lights in the scene. To go deeper, it would help to see the results of the profiling you've done to identify where you're encountering bottlenecks in your current rendering loads. \$\endgroup\$
    – DMGregory
    Nov 27, 2021 at 2:22
  • \$\begingroup\$ The thing is with the lights, I only load one. \$\endgroup\$
    – Gyoo
    Nov 27, 2021 at 2:27
  • \$\begingroup\$ And how is it possible to profile a running shader? \$\endgroup\$
    – Gyoo
    Nov 27, 2021 at 2:28
  • 2
    \$\begingroup\$ "How to profile shader" sounds like a fruitful query to enter into your favourite search engine. \$\endgroup\$
    – DMGregory
    Nov 27, 2021 at 2:32
  • \$\begingroup\$ after many hours of research, I was not able to get a gpu profiler. I also found out that Iforgot to disable a heavy bloom post-processing effect, which destroyed much of my performances... But I'm still doubtfull of my code, is it unnecessarily slow? Is there some code that I could easily transfer to the vertex shader? \$\endgroup\$
    – Gyoo
    Nov 28, 2021 at 16:21

1 Answer 1


Is there some code that I could easily transfer to the vertex shader?

  • You're sampling your texture twice. if(texture(textureSampler,UV).a < 0.5) could just be if (albedo.a < 0.5) since you already stored the texture sample into albedo.

  • You're normalizing your LightDir three times, when once is enough. It's already normalized by this line: mediump vec3 LightDir = (LightPos[i] - Position)/LightDist;

  • You could pre-invert your roughness uniform to a smoothness to save inverting it in the shader. Similarly, any attribute that only ever appears as (1.0 - foo) in your fragment shader could be pre-inverted in the vertex shader before interpolation.

  • This line:

    vec4(mix(mix((albedo.rgb * diffColorMultiplier) + specColor,specColor,Metaliness)+envColor+emission,envColor,1.0-fogFactor),albedo.a);

    Can be written more simply as...

    vec3 surfaceColor = (1.0 - Metalness) * albedo.rgb * diffColorMultiplier
                         + specColor + emission;
    vec4 finalColor = envColor + fogFactor * surfaceColor;

Those are all micro-optimizations though, and it's likely that the shader compiler might already fix all but the third point for you.

The most expensive thing in this shader might be the discard instruction. The presence of a discard can disable some optimizations. If you have some objects that don't need per-pixel transparency, it would be worthwhile to make a pure opaque variant of this shader that does not include this alpha testing.

That's about as much as jumps out at me from the code alone. To go further, you'll need to do some profiling to identify which parts actually make your game slow in practice.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .