7

This was originally asked by @sydd here. I was curious about it so I try to code it but It was closed/deleted before I could answer so here it is.

Question: How to reproduce/implement this 2D ray casting lighting effect in GLSL?

The effect itself cast rays from mouse position to every direction, accumulating background map alpha and colors affecting the pixels strength.

So the input should be:

  • mouse position
  • background RGBA map texture
Community
  • 1
  • 1
Spektre
  • 49,595
  • 11
  • 110
  • 380

1 Answers1

8
  1. Background map

    Ok I created a test RGBA map as 2 images one containing RGB (on the left) and second with the alpha channel (on the right) so you can see them both. Of coarse they are combined to form single RGBA texture.

    RGB map alpha map

    I blurred them both a bit to obtain better visual effects on the edges.

  2. Ray casting

    As this should run in GLSL we need to cast the rays somewhere. I decided to do it in fragment shader. So the algo is like this:

    1. On GL side pass uniforms needed for shaders Here goes mouse position as texture coordinate, max resolution of texture and light transmition strength.
    2. On GL side draw quad covering whole screen with texture of background (o blending)
    3. On Vertex shader just pass the texture and fragment coordinates needed
    4. On Fragment shader per each fragment:

      • cast ray from mouse position to actual fragment position (in texture coordinates)
      • cumulate/integrate the light properties during the ray travel
      • stop if light strength near zero or target fragment position reached.

Vertex shader

// Vertex
#version 420 core
layout(location=0) in vec2 pos;     // glVertex2f <-1,+1>
layout(location=8) in vec2 txr;     // glTexCoord2f  Unit0 <0,1>
out smooth vec2 t1;                 // texture end point <0,1>
void main()
    {
    t1=txr;
    gl_Position=vec4(pos,0.0,1.0);
    }

Fragment shader

// Fragment
#version 420 core
uniform float transmit=0.99;// light transmition coeficient <0,1>
uniform int txrsiz=512;     // max texture size [pixels]
uniform sampler2D txrmap;   // texture unit for light map
uniform vec2 t0;            // texture start point (mouse position) <0,1>
in smooth vec2 t1;          // texture end point, direction <0,1>
out vec4 col;
void main()
    {
    int i;
    vec2 t,dt;
    vec4 c0,c1;
    dt=normalize(t1-t0)/float(txrsiz);
    c0=vec4(1.0,1.0,1.0,1.0);   // light ray strength
    t=t0;
    if (dot(t1-t,dt)>0.0)
     for (i=0;i<txrsiz;i++)
        {
        c1=texture2D(txrmap,t);
        c0.rgb*=((c1.a)*(c1.rgb))+((1.0f-c1.a)*transmit);
        if (dot(t1-t,dt)<=0.000f) break;
        if (c0.r+c0.g+c0.b<=0.001f) break;
        t+=dt;
        }
    col=0.90*c0+0.10*texture2D(txrmap,t1);  // render with ambient light
//  col=c0;                                 // render without ambient light
    }

And Finally the result:

output

Animated 256 colors GIF:

output animation

The colors in GIF are slightly distorted due to 8 bit truncation. Also if the animation stops refresh page or open in decend gfx viewer instead.

light inside semitransparent object

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • 1
    @sydd take a look at this. (Have re-asked your question to make it answerable). You need still to add that shimering noise texture if you want ... – Spektre Jan 10 '16 at 16:43
  • I think it might be faster to achieve this by using shadow mapping. It would work the same way as the 3D case, only in 2D. The shadow map itself would be 1 dimensional (you'd need 4 of them for a square cubemap). – Nicol Bolas Jan 10 '16 at 17:37
  • 1
    @NicolBolas good to know ... I just added a comment to one of his question to be sure. The shadows map could work for basic lighting but I can not imagine right now how to add the transparency and colors of the background (sub surface light) with such approach (the light changes colors and intensity dependent on what material it passes). – Spektre Jan 10 '16 at 18:45
  • @Spektre thanks, it looks great! With this approach my only concern is performance. How much is this killing the GPU? Would it work on mobile(esp. with multiple lights)? You are doing a lot of duplicate calculation if you are calculating a point far away from the light source. – sydd Jan 10 '16 at 20:58
  • 1
    @sydd Yes there is a big redundance of computations due to GLSL architecture limitations in comparison to CPU approach. But I think it is still fast enough I didn't benchmark it however. When I have time I will try. Anyway It is single QUAD only without any complicated computations. The slowest thing is the Texture fetch. You can add more lights by Blending or by adding more loops (one per light) summing the results together. You can speedup the process by enlarging the `dt` step or using small resolution map texture. I do not code on Mobile Phones so it is hard to say you need to try it... – Spektre Jan 10 '16 at 22:24
  • @Spektre Thanks, I will try it. I got another idea for optimization: Calculate the light values only on the edge of the texture. Write the luminosity values when doing the loop to an intermediate texture. This way the value for each pixel is calculated just ~2.5 times, instead of ~25 times as in your algo. On the downside there is the extra step of writing the shadow into an immediate texture (but I might need to do this anyway) But I have no idea whether this is faster, I will have to try. – sydd Jan 10 '16 at 22:49
  • 1
    @sydd ok on 512x512 window with 256x256 map, 0.97 transmitance I got the worst rendering time ~6.5ms on my GeForce GT440. On mobile there will be lesser resolution then that I think ... – Spektre Jan 10 '16 at 22:49
  • @sydd With the shadow map you will loose the SSS properties which make this so cool look at the effect when light is inside semitransparent object or near edge (last image). The shadow map will behave like solid I think or am I missing something? – Spektre Jan 10 '16 at 22:57
  • @Spektre Is not a shadow map, its your algorithm modified: The shader writes the output to a texture in the for loop. So for example if the light radius is 100, light is at (0,0) and it calculates the pixel at (100,0), it writes 100 pixels along the line it traces. And if you do this for all the pixels on the edge of the shadow (800 pixels for a 200x200 texture) then all pixels in the shadow are calculated. So for every pixel thats not at the edge we dont need to do anything. Im not sure if a fragment shader can modify the output at a different pixel, so I guess it needs another texture. – sydd Jan 10 '16 at 23:24
  • 1
    @sydd no that is not possible with current HW you can read as many texels as you need if have time for it but you can output only single pixel per fragment shader call. – Spektre Jan 11 '16 at 00:07
  • @Spektre thanks for all the help, this discussion was very helpful. I guess a fundamentally different solution would need CUDA/OpenCL then. – sydd Jan 11 '16 at 00:28
  • Sorry for bringing up this old topic, but i was thinking if its possible to implement multiple light sources with this method by sending an uniform buffer with multiple light positions and calculating them without looping though the sampling / light source. To avoid additional performance impact. – Playdome.io Nov 22 '16 at 02:22
  • @Azarus yes you can use multiple light sources but I see no option to avoid the looping through all of them. – Spektre Nov 22 '16 at 07:52
  • 1
    @Spektre Again thanks for the great tips. If anyone is interested I've implemented a modified version of this the way I wrote above using OpenGL compute shaders, its here: https://github.com/matyasf/sparrow-game/blob/master/SparrowGame.Shared/lightComputeShader.glsl Performance is pretty good: 50 FPS with 200 lights and a 512x512 texture on a Geforce 1050. Its not perfect yet, there is a slight moiré effect. – sydd Aug 31 '17 at 23:57