5

In my open source project I have setup a deferred rendering pipeline using Qt3D. So far so good, but now I'd like to move forward by adding spotlights projection volume. (e.g. as if there is smoke in the scene) Like this:

enter image description here

The fragment shader I'm using is at the end of the question. I've read that for each fragment I should do ray marching from the light position and find the intersections with a cone, but I have no idea how to translate this into GLSL. I can easily add a uniform with the depth map (from camera point of view) coming from the GBuffer, but I don't know if that's of any help.

Since my GLSL knowledge is very limited, please reply with actual code, not a lengthy mathematical explanation that I won't be able to understand/translate into code. Please be patient with me.

uniform sampler2D color;
uniform sampler2D position;
uniform sampler2D normal;
uniform vec2 winSize;

out vec4 fragColor;

const int MAX_LIGHTS = 102;
const int TYPE_POINT = 0;
const int TYPE_DIRECTIONAL = 1;
const int TYPE_SPOT = 2;

struct Light {
    int   type;
    vec3  position;
    vec3  color;
    float intensity;
    vec3  direction;
    float constantAttenuation;
    float linearAttenuation;
    float quadraticAttenuation;
    float cutOffAngle;
};

uniform Light lightsArray[MAX_LIGHTS];
uniform int lightsNumber;

void main()
{
    vec2 texCoord = gl_FragCoord.xy / winSize;
    vec4 col = texture(color, texCoord);
    vec3 pos = texture(position, texCoord).xyz;
    vec3 norm = texture(normal, texCoord).xyz;

    vec3 lightColor = vec3(0.0);
    vec3 s;
    float att;

    for (int i = 0; i < lightsNumber; ++i) {
        att = 1.0;
        if ( lightsArray[i].type != TYPE_DIRECTIONAL ) {
            s = lightsArray[i].position - pos;
            if (lightsArray[i].constantAttenuation != 0.0
             || lightsArray[i].linearAttenuation != 0.0
             || lightsArray[i].quadraticAttenuation != 0.0) {
                float dist = length(s);
                att = 1.0 / (lightsArray[i].constantAttenuation + lightsArray[i].linearAttenuation * dist + lightsArray[i].quadraticAttenuation * dist * dist);
            }
            s = normalize( s );
            if ( lightsArray[i].type == TYPE_SPOT ) {
                if ( degrees(acos(dot(-s, normalize(lightsArray[i].direction))) ) > lightsArray[i].cutOffAngle)
                    att = 0.0;
            }
        } else {
            s = normalize(-lightsArray[i].direction);
        }

        float diffuse = max( dot( s, norm ), 0.0 );

        lightColor += att * lightsArray[i].intensity * diffuse * lightsArray[i].color;
    }
    fragColor = vec4(col.rgb * lightColor, col.a);
}

This is how a spotlight looks like with the original shader above: enter image description here

[EDIT - SOLVED] Thanks to Rabbid76 excellent answer and precious support

This is the modified code to see the cone projection:

#version 140

uniform sampler2D color;
uniform sampler2D position;
uniform sampler2D normal;
uniform vec2 winSize;

out vec4 fragColor;

const int MAX_LIGHTS = 102;
const int TYPE_POINT = 0;
const int TYPE_DIRECTIONAL = 1;
const int TYPE_SPOT = 2;

struct Light {
    int type;
    vec3 position;
    vec3 color;
    float intensity;
    vec3 direction;
    float constantAttenuation;
    float linearAttenuation;
    float quadraticAttenuation;
    float cutOffAngle;
};

uniform Light lightsArray[MAX_LIGHTS];
uniform int lightsNumber;

uniform mat4 inverseViewMatrix; // defined by camera position, camera target and up vector

void main()
{
    vec2 texCoord = gl_FragCoord.xy / winSize;
    vec4 col = texture(color, texCoord);
    vec3 pos = texture(position, texCoord).xyz;
    vec3 norm = texture(normal, texCoord).xyz;

    vec3 lightColor = vec3(0.0);
    vec3 s;

    // calculate unprojected fragment position on near plane and line of sight relative to view
    float nearZ  = -1.0;
    vec3 nearPos = vec3( (texCoord.x - 0.5) * winSize.x / winSize.y, texCoord.y - 0.5, nearZ ); // 1.0 is camera near
    vec3 los     = normalize( nearPos );

    // ray definition
    vec3 O = vec3( inverseViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 ) ); // translation part of the camera matrix, which is equal to the camera position
    vec3 D = (length(pos) > 0.0) ? normalize(pos - O) : (mat3(inverseViewMatrix) * los);

    for (int i = 0; i < lightsNumber; ++i)
    {
        float att = 1.0;
        if ( lightsArray[i].type == TYPE_DIRECTIONAL )
        {
            s = normalize( -lightsArray[i].direction );
        }
        else
        {
            s = lightsArray[i].position - pos;

            if (lightsArray[i].type != TYPE_SPOT
                && (lightsArray[i].constantAttenuation != 0.0
                || lightsArray[i].linearAttenuation != 0.0
                || lightsArray[i].quadraticAttenuation != 0.0))
            {
                float dist = length(s);
                att = 1.0 / (lightsArray[i].constantAttenuation + lightsArray[i].linearAttenuation * dist + lightsArray[i].quadraticAttenuation * dist * dist);
            }

            s = normalize( s );
            if ( lightsArray[i].type == TYPE_SPOT )
            {

                // cone definition
                vec3  C     = lightsArray[i].position;
                vec3  V     = normalize(lightsArray[i].direction);
                float cosTh = cos( radians(lightsArray[i].cutOffAngle) );

                // ray - cone intersection
                vec3  CO     = O - C;
                float DdotV  = dot( D, V );
                float COdotV = dot( CO, V );
                float a      = DdotV * DdotV - cosTh * cosTh;
                float b      = 2.0 * (DdotV * COdotV - dot( D, CO ) * cosTh * cosTh);
                float c      = COdotV * COdotV - dot( CO, CO ) * cosTh * cosTh;
                float det    = b * b - 4.0 * a * c;

                // find intersection
                float isIsect = 0.0;
                vec3  isectP  = vec3(0.0);
                if ( det >= 0.0 )
                {
                    vec3  P1 = O + (-b - sqrt(det)) / (2.0 * a) * D;
                    vec3  P2 = O + (-b + sqrt(det)) / (2.0 * a) * D;
                    float isect1 = step( 0.0, dot(normalize(P1 - C), V) );
                    float isect2 = step( 0.0, dot(normalize(P2 - C), V) );
                    if ( isect1 < 0.5 )
                    {
                        P1 = P2;
                        isect1 = isect2;
                    }
                    if ( isect2 < 0.5 )
                    {
                        P2 = P1;
                        isect2 = isect1;
                    }
                    isectP = (length(P1 - O) < length(P2 - O)) ? P1 : P2;
                    isIsect = mix( isect2, 1.0, isect1 );

                    if ( length(pos) != 0.0 && length(isectP - O) > length(pos - O))
                        isIsect = 0.0;
                }

                float dist = length( isectP - C.xyz );
                float limit = degrees(acos(dot(-s, normalize(lightsArray[i].direction))) );

                if (isIsect > 0 || limit <= lightsArray[i].cutOffAngle)
                {
                    att  = 1.0 / dot( vec3( 1.0, dist, dist * dist ),
                                      vec3(lightsArray[i].constantAttenuation,
                                           lightsArray[i].linearAttenuation,
                                           lightsArray[i].quadraticAttenuation) );
                }
                else
                    att = 0.0;
            }
        }

        float diffuse = max( dot( s, norm ), 0.0 );

        lightColor += att * lightsArray[i].intensity * diffuse * lightsArray[i].color;
    }
    fragColor = vec4(col.rgb * lightColor, col.a);
}

Uniforms passed to the shader are:

qml: lightsArray[0].type = 0
qml: lightsArray[0].position = QVector3D(0, 10, 0)
qml: lightsArray[0].color = #ffffff
qml: lightsArray[0].intensity = 0.8
qml: lightsArray[0].constantAttenuation = 1
qml: lightsArray[0].linearAttenuation = 0
qml: lightsArray[0].quadraticAttenuation = 0
qml: lightsArray[1].type = 2
qml: lightsArray[1].position = QVector3D(0, 3, 0)
qml: lightsArray[1].color = #008000
qml: lightsArray[1].intensity = 0.5
qml: lightsArray[1].constantAttenuation = 2
qml: lightsArray[1].linearAttenuation = 0
qml: lightsArray[1].quadraticAttenuation = 0
qml: lightsArray[1].direction = QVector3D(-0.573576, -0.819152, 0)
qml: lightsArray[1].cutOffAngle = 15
qml: lightsNumber = 2

Screenshot:

enter image description here

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
Massimo Callegari
  • 2,099
  • 1
  • 26
  • 39
  • 1
    If you have a shadow map of the light you want to raymarch from the worldspace fragment to the camera position and sample the shadow map at each step. Then just count the number of samples that are visible by the light and compute the intensity. – dari Aug 20 '17 at 15:12
  • @dari I don't have a shadow map for each light, and if possible I'd like to avoid that (ATM I don't need shadows). I'm just looking for a cheap way to see the light cone. More complexity could come later. – Massimo Callegari Aug 20 '17 at 15:16
  • 1
    If it should be really simple and cheap you could just render an actual cone with alpha blending after your lighting computations. In the fragment shader of the cone you would then make sure that it fades out at the edges and loses intensity with "cone height". – dari Aug 20 '17 at 15:38
  • @dari can't I just do it in the light pass shader above ? By the way, while I could understand the theory, I asked for some code cause I'm not capable of writing it myself, and this question could be useful to others researching on the same topic. – Massimo Callegari Aug 20 '17 at 15:50
  • @dari I'm interested to see how that turns out. – Robinson Aug 20 '17 at 15:55
  • So without rendering a cone you have to do the ray-cone intersection in the fragment shader yourself. After you clamped the near and far intersection point to the camera position and the current fragment depth you have integrate the spotlight intensity function over that line segment. I'm not sure if there is a closed form solution for that integral but a discrete iterative algorithm should work too. I can't give you any code examples right now, but i try to come back later. – dari Aug 20 '17 at 16:38
  • @dari much appreciated, thank you. That's exactly what I asked in my question. – Massimo Callegari Aug 20 '17 at 17:38
  • @Rabbid76 just tried. Unfortunately no joy. This is what I get: https://pasteboard.co/GGGLsgc.png – Massimo Callegari Aug 21 '17 at 14:34
  • @Rabbid76 this is with `C = lightsArray[i].position`: https://pasteboard.co/GGGV55P.png. and this is with `C = lightsArray[i].position - pos`: https://pasteboard.co/GGGVHax.png. I think the fragments I get are not in the coordinate system your code expects. My vertex shader is: `in vec4 vertexPosition; uniform mat4 modelMatrix; void main() { gl_Position = modelMatrix * vertexPosition; }` Thanks for your patience ! – Massimo Callegari Aug 21 '17 at 15:00
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152453/discussion-between-massimo-callegari-and-rabbid76). – Massimo Callegari Aug 21 '17 at 15:11

1 Answers1

6

For a primitive visualization of the light cone of a spot light, you have to do a intersection of the line of sight and the light cone.
The following algorithm works in a perpectiv view and the caluclations ar done in view (eye) sapce. The algorithm does not care about the geometry of the scene and does not do any depth test or shadow test, it only is a overlayerd visualization of the light cone.

The line of sight in a perspective view can be deifned by a points and a direction. Since the calculations is done in view (eye) space, the point is the point of view (the origin of the view frustum) which is vec3(0.0).
The direction can easily be determined, by the intersection of the line of sight and the near plane of the camera frustum. This can easily be calculated if the projected XY-coordinate of the fragment is known in normalized device coordinates (the lower left point is (-1,-1) and the upper right point is (1,1) see the answer to this question).

float aspect = .....; // ratio of the view port (widht/length)
float fov    = .....; // filed of view angle in radians (angle of the camera frustum on the Y-axis)
vec2  ndcPos = .....; // fragment position in NDC space from (-1,-1) to (1,1)

vec3 tanFov  = tan( fov * 0.5 );
vec3 los     = normalize( vec3( ndcPos.x * aspect * tanFov, ndcPos.y * tanFov, -1.0 ) );

The light cone is defined by the origin of the light source, the direction where the light source points to, and the full angle of the light cone. The position and the direction have to be up in view space. The angle has to be set up in radians.

vec3  vLightPos = .....; // position of the light source in view space
vec3  vLightDir = .....; // direction of the light in view space 
float coneAngle = .....; // full angle of the light cone in radians

How to calculate the intersection point(s) of a ray and a cone can be found in the answer to Stackoverflow question Points of intersection of vector with cone and in the following paper: Intersection of a ray and a cone.
The following code calculates a intersection of a ray and a cone as defined above. The result point is stored in isectP. The variable isIsect of the type float is set to 1.0 if there is a intersection, and is set to 0.0 else.

// ray definition
vec3 O = vec3(0.0);
vec3 D = los;

// cone definition
vec3  C     = vLightPos;
vec3  V     = vLightDir;
float cosTh = cos( coneAngle * 0.5 );

// ray - cone intersection
vec3  CO     = O - C;
float DdotV  = dot( D, V );
float COdotV = dot( CO, V );
float a      = DdotV*DdotV - cosTh*cosTh;
float b      = 2.0 * (DdotV*COdotV - dot( D, CO )*cosTh*cosTh);
float c      = COdotV*COdotV - dot( CO, CO )*cosTh*cosTh;
float det    = b*b - 4.0*a*c;

// find intersection
float isIsect = 0.0;
vec3  isectP  = vec3(0.0);
if ( det >= 0.0 )
{
    vec3  P1 = O + (-b-sqrt(det))/(2.0*a) * D;
    vec3  P2 = O + (-b+sqrt(det))/(2.0*a) * D;
    float isect1 = step( 0.0, dot(normalize(P1-C), V) );
    float isect2 = step( 0.0, dot(normalize(P2-C), V) );
    P1 = mix( P2, P1, isect1 );
    isectP = P2.z < 0.0 && P2.z > P1.z ? P2 : P1;
    isIsect = mix( isect2, 1.0, isect1 ) * step( isectP.z, 0.0 );
}

For the full GLSL code, see the following WebGL example:

(function loadscene() {

var sliderScale = 100.0
var gl, canvas, vp_size, camera, progDraw, progLightCone, bufTorus = {}, bufQuad = {}, drawFB;

function render(deltaMS) {

var ambient = document.getElementById( "ambient" ).value / sliderScale;
var diffuse = document.getElementById( "diffuse" ).value / sliderScale;
var specular = document.getElementById( "specular" ).value / sliderScale;
var shininess = document.getElementById( "shininess" ).value;
var cutOffAngle = document.getElementById( "cutOffAngle" ).value;

// setup view projection and model
vp_size = [canvas.width, canvas.height];
var prjMat = camera.Perspective();
var viewMat = camera.LookAt();
var modelMat = IdentM44();
modelMat = RotateAxis( modelMat, CalcAng( deltaMS, 13.0 ), 0 );
modelMat = RotateAxis( modelMat, CalcAng( deltaMS, 17.0 ), 1 );
    
var lightPos = [0.95, 0.95, -1.0];
var lightDir = [-1.0, -1.0, -3.0];
var lightCutOffAngleRad = cutOffAngle * Math.PI / 180.0;
var lightAtt = [0.7, 0.1, 0.5];

drawFB.Bind( true );    
gl.enable( gl.DEPTH_TEST );
gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );

ShProg.Use( progDraw );
ShProg.SetM44( progDraw, "u_projectionMat44", prjMat );
ShProg.SetM44( progDraw, "u_viewMat44", viewMat );
ShProg.SetF3( progDraw, "u_light.position", lightPos );
ShProg.SetF3( progDraw, "u_light.direction", lightDir );
ShProg.SetF1( progDraw, "u_light.ambient", ambient );
ShProg.SetF1( progDraw, "u_light.diffuse", diffuse );
ShProg.SetF1( progDraw, "u_light.specular", specular );
ShProg.SetF1( progDraw, "u_light.shininess", shininess );
ShProg.SetF3( progDraw, "u_light.attenuation", lightAtt );
ShProg.SetF1( progDraw, "u_light.cutOffAngle", lightCutOffAngleRad );
ShProg.SetM44( progDraw, "u_modelMat44", modelMat );

bufObj = bufTorus;
gl.enableVertexAttribArray( progDraw.inPos );
gl.enableVertexAttribArray( progDraw.inNV );
gl.enableVertexAttribArray( progDraw.inCol );
gl.bindBuffer( gl.ARRAY_BUFFER, bufObj.pos );
gl.vertexAttribPointer( progDraw.inPos, 3, gl.FLOAT, false, 0, 0 );
gl.bindBuffer( gl.ARRAY_BUFFER, bufObj.nv );
gl.vertexAttribPointer( progDraw.inNV, 3, gl.FLOAT, false, 0, 0 ); 
gl.bindBuffer( gl.ARRAY_BUFFER, bufObj.col );
gl.vertexAttribPointer( progDraw.inCol, 3, gl.FLOAT, false, 0, 0 );
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, bufObj.inx );
gl.drawElements( gl.TRIANGLES, bufObj.inxLen, gl.UNSIGNED_SHORT, 0 );
gl.disableVertexAttribArray( progDraw.pos );
gl.disableVertexAttribArray( progDraw.nv );
gl.disableVertexAttribArray( progDraw.col );

drawFB.Release( true );
gl.viewport( 0, 0, canvas.width, canvas.height );
var texUnitDraw = 2;
drawFB.BindTexture( texUnitDraw );
ShProg.Use( progLightCone );
ShProg.SetI1( progLightCone, "u_colorAttachment0", texUnitDraw );
ShProg.SetF2( progLightCone, "u_depthRange", [ camera.near, camera.far ] );
ShProg.SetF2( progLightCone, "u_vp", camera.vp );
ShProg.SetF1( progLightCone, "u_fov", camera.fov_y * Math.PI / 180.0 );
ShProg.SetF3( progLightCone, "u_light.position", lightPos );
ShProg.SetF3( progLightCone, "u_light.direction", lightDir );
ShProg.SetF3( progLightCone, "u_light.attenuation", lightAtt );
ShProg.SetF1( progLightCone, "u_light.cutOffAngle", lightCutOffAngleRad );

gl.enableVertexAttribArray( progLightCone.inPos );
gl.bindBuffer( gl.ARRAY_BUFFER, bufQuad.pos );
gl.vertexAttribPointer( progLightCone.inPos, 2, gl.FLOAT, false, 0, 0 );
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, bufQuad.inx );
gl.drawElements( gl.TRIANGLES, bufQuad.inxLen, gl.UNSIGNED_SHORT, 0 );
gl.disableVertexAttribArray( progLightCone.inPos );

requestAnimationFrame(render);
}

function initScene() {

canvas = document.getElementById( "glow-canvas");
vp_size = [canvas.width, canvas.height];
gl = canvas.getContext( "experimental-webgl" );
if ( !gl )
    return;

document.getElementById( "ambient" ).value = 0.25 * sliderScale;
document.getElementById( "diffuse" ).value = 1.0 * sliderScale;
document.getElementById( "specular" ).value = 1.0 * sliderScale;
document.getElementById( "shininess" ).value = 10.0;
document.getElementById( "cutOffAngle" ).value = 30.0;

progDraw = ShProg.Create( 
    [ { source : "draw-shader-vs", stage : gl.VERTEX_SHADER },
    { source : "draw-shader-fs", stage : gl.FRAGMENT_SHADER }
    ] );

progDraw.inPos = ShProg.AttrI( progDraw, "inPos" );
progDraw.inNV  = ShProg.AttrI( progDraw, "inNV" );
progDraw.inCol = ShProg.AttrI( progDraw, "inCol" );
if ( progDraw == 0 )
    return;

progLightCone = ShProg.Create( 
    [ { source : "light-cone-shader-vs", stage : gl.VERTEX_SHADER },
    { source : "light-cone-shader-fs", stage : gl.FRAGMENT_SHADER }
    ] );
progLightCone.inPos = ShProg.AttrI( progDraw, "inPos" );
if ( progDraw == 0 )
    return;

var circum_size = 32, tube_size = 32;
var rad_circum = 1.5;
var rad_tube = 0.8;
var torus_pts = [];
var torus_nv = [];
var torus_col = [];
var torus_inx = [];
var col = [1, 0.5, 0.0];
for ( var i_c = 0; i_c < circum_size; ++ i_c ) {
    var center = [
        Math.cos(2 * Math.PI * i_c / circum_size),
        Math.sin(2 * Math.PI * i_c / circum_size) ]
    for ( var i_t = 0; i_t < tube_size; ++ i_t ) {
        var tubeX = Math.cos(2 * Math.PI * i_t / tube_size)
        var tubeY = Math.sin(2 * Math.PI * i_t / tube_size)
        var pt = [
            center[0] * ( rad_circum + tubeX * rad_tube ),
            center[1] * ( rad_circum + tubeX * rad_tube ),
            tubeY * rad_tube ]
        var nv = [ pt[0] - center[0] * rad_tube, pt[1] - center[1] * rad_tube, tubeY * rad_tube ]
        torus_pts.push( pt[0], pt[1], pt[2] );
        torus_nv.push( nv[0], nv[1], nv[2] );
        torus_col.push( col[0], col[1], col[2] );
        var i_cn = (i_c+1) % circum_size
        var i_tn = (i_t+1) % tube_size
        var i_c0 = i_c * tube_size; 
        var i_c1 = i_cn * tube_size; 
        torus_inx.push( i_c0+i_t, i_c0+i_tn, i_c1+i_t, i_c0+i_tn, i_c1+i_t, i_c1+i_tn )
    }
}
bufTorus.pos = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufTorus.pos );
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( torus_pts ), gl.STATIC_DRAW );
bufTorus.nv = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufTorus.nv );
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( torus_nv ), gl.STATIC_DRAW );
bufTorus.col = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufTorus.col );
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( torus_col ), gl.STATIC_DRAW );
bufTorus.inx = gl.createBuffer();
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, bufTorus.inx );
gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( torus_inx ), gl.STATIC_DRAW );
bufTorus.inxLen = torus_inx.length;

bufQuad.pos = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufQuad.pos );
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( [ -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0 ] ), gl.STATIC_DRAW );
bufQuad.inx = gl.createBuffer();
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, bufQuad.inx );
gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( [ 0, 1, 2, 0, 2, 3 ] ), gl.STATIC_DRAW );
bufQuad.inxLen = 6;

camera = new Camera( [0, 4, 0.0], [0, 0, 0], [0, 0, 1], 90, vp_size, 0.5, 100 );

window.onresize = resize;
resize();
requestAnimationFrame(render);
}

function resize() {
//vp_size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
vp_size = [window.innerWidth, window.innerHeight]
//vp_size = [256, 256]
canvas.width = vp_size[0];
canvas.height = vp_size[1];

var fbsize = Math.max(vp_size[0], vp_size[1]);
fbsize = 1 << 31 - Math.clz32(fbsize); // nearest power of 2

var fb_rect = [fbsize, fbsize];
drawFB = FrameBuffer.Create( fb_rect );
}

function Fract( val ) { 
return val - Math.trunc( val );
}
function CalcAng( deltaMS, intervall ) {
return Fract( deltaMS / (1000*intervall) ) * 2.0 * Math.PI;
}
function CalcMove( deltaMS, intervall, range ) {
var pos = self.Fract( deltaMS / (1000*intervall) ) * 2.0
var pos = pos < 1.0 ? pos : (2.0-pos)
return range[0] + (range[1] - range[0]) * pos;
}    

function IdentM44() { return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; }

function RotateAxis(matA, angRad, axis) {
var aMap = [ [1, 2], [2, 0], [0, 1] ];
var a0 = aMap[axis][0], a1 = aMap[axis][1]; 
var sinAng = Math.sin(angRad), cosAng = Math.cos(angRad);
var matB = matA.slice(0);
for ( var i = 0; i < 3; ++ i ) {
    matB[a0*4+i] = matA[a0*4+i] * cosAng + matA[a1*4+i] * sinAng;
    matB[a1*4+i] = matA[a0*4+i] * -sinAng + matA[a1*4+i] * cosAng;
}
return matB;
}

function Cross( a, b ) { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], 0.0 ]; }
function Dot( a, b ) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }
function Normalize( v ) {
var len = Math.sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] );
return [ v[0] / len, v[1] / len, v[2] / len ];
}

Camera = function( pos, target, up, fov_y, vp, near, far ) {
this.Time = function() { return Date.now(); }
this.pos = pos;
this.target = target;
this.up = up;
this.fov_y = fov_y;
this.vp = vp;
this.near = near;
this.far = far;
this.orbit_mat = this.current_orbit_mat = this.model_mat = this.current_model_mat = IdentM44();
this.mouse_drag = this.auto_spin = false;
this.auto_rotate = true;
this.mouse_start = [0, 0];
this.mouse_drag_axis = [0, 0, 0];
this.mouse_drag_angle = 0;
this.mouse_drag_time = 0;
this.drag_start_T = this.rotate_start_T = this.Time();
this.Ortho = function() {
var fn = this.far + this.near;
var f_n = this.far - this.near;
var w = this.vp[0];
var h = this.vp[1];
return [
    2/w, 0,   0,       0,
    0,   2/h, 0,       0,
    0,   0,   -2/f_n,  0,
    0,   0,   -fn/f_n, 1 ];
};  
this.Perspective = function() {
var n = this.near;
var f = this.far;
var fn = f + n;
var f_n = f - n;
var r = this.vp[0] / this.vp[1];
var t = 1 / Math.tan( Math.PI * this.fov_y / 360 );
return [
    t/r, 0, 0,          0,
    0,   t, 0,          0,
    0,   0, -fn/f_n,   -1,
    0,   0, -2*f*n/f_n, 0 ];
}; 
this.LookAt = function() {
var mz = Normalize( [ this.pos[0]-this.target[0], this.pos[1]-this.target[1], this.pos[2]-this.target[2] ] );
var mx = Normalize( Cross( this.up, mz ) );
var my = Normalize( Cross( mz, mx ) );
var tx = Dot( mx, this.pos );
var ty = Dot( my, this.pos );
var tz = Dot( [-mz[0], -mz[1], -mz[2]], this.pos ); 
return [mx[0], my[0], mz[0], 0, mx[1], my[1], mz[1], 0, mx[2], my[2], mz[2], 0, tx, ty, tz, 1]; 
};
} 

var FrameBuffer = {};
FrameBuffer.Create = function( vp, texturePlan ) {
var texPlan = texturePlan ? new Uint8Array( texturePlan ) : null;
var fb = gl.createFramebuffer();
fb.width = vp[0];
fb.height = vp[1];
gl.bindFramebuffer( gl.FRAMEBUFFER, fb );
fb.color0_texture = gl.createTexture();
gl.bindTexture( gl.TEXTURE_2D, fb.color0_texture );
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, fb.width, fb.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, texPlan );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST );
fb.renderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer( gl.RENDERBUFFER, fb.renderbuffer );
gl.renderbufferStorage( gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, fb.width, fb.height );
gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fb.color0_texture, 0 );
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, fb.renderbuffer );
gl.bindTexture( gl.TEXTURE_2D, null );
gl.bindRenderbuffer( gl.RENDERBUFFER, null );
gl.bindFramebuffer( gl.FRAMEBUFFER, null );

fb.Bind = function( clear ) {
    gl.bindFramebuffer( gl.FRAMEBUFFER, this );
    if ( clear ) {
        gl.viewport( 0, 0, this.width, this.height );
        gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
        gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
    }
};

fb.Release = function( clear ) {
    gl.bindFramebuffer( gl.FRAMEBUFFER, null );
    if ( clear ) {
        gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
        gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
    }
};

fb.BindTexture = function( textureUnit ) {
    gl.activeTexture( gl.TEXTURE0 + textureUnit );
    gl.bindTexture( gl.TEXTURE_2D, this.color0_texture );
};

return fb;
}

var ShProg = {};
ShProg.Create = function( shaderList ) {
var shaderObjs = [];
for ( var i_sh = 0; i_sh < shaderList.length; ++ i_sh ) {
    var shderObj = this.Compile( shaderList[i_sh].source, shaderList[i_sh].stage );
    if ( shderObj == 0 )
        return 0;
    shaderObjs.push( shderObj );
}
var progObj = this.Link( shaderObjs )
if ( progObj != 0 ) {
    progObj.attrInx = {};
    var noOfAttributes = gl.getProgramParameter( progObj, gl.ACTIVE_ATTRIBUTES );
    for ( var i_n = 0; i_n < noOfAttributes; ++ i_n ) {
        var name = gl.getActiveAttrib( progObj, i_n ).name;
        progObj.attrInx[name] = gl.getAttribLocation( progObj, name );
    }
    progObj.uniLoc = {};
    var noOfUniforms = gl.getProgramParameter( progObj, gl.ACTIVE_UNIFORMS );
    for ( var i_n = 0; i_n < noOfUniforms; ++ i_n ) {
        var name = gl.getActiveUniform( progObj, i_n ).name;
        progObj.uniLoc[name] = gl.getUniformLocation( progObj, name );
    }
}
return progObj;
}
ShProg.AttrI = function( progObj, name ) { return progObj.attrInx[name]; } 
ShProg.UniformL = function( progObj, name ) { return progObj.uniLoc[name]; } 
ShProg.Use = function( progObj ) { gl.useProgram( progObj ); } 
ShProg.SetI1  = function( progObj, name, val ) { if(progObj.uniLoc[name]) gl.uniform1i( progObj.uniLoc[name], val ); }
ShProg.SetF1  = function( progObj, name, val ) { if(progObj.uniLoc[name]) gl.uniform1f( progObj.uniLoc[name], val ); }
ShProg.SetF2  = function( progObj, name, arr ) { if(progObj.uniLoc[name]) gl.uniform2fv( progObj.uniLoc[name], arr ); }
ShProg.SetF3  = function( progObj, name, arr ) { if(progObj.uniLoc[name]) gl.uniform3fv( progObj.uniLoc[name], arr ); }
ShProg.SetF4  = function( progObj, name, arr ) { if(progObj.uniLoc[name]) gl.uniform4fv( progObj.uniLoc[name], arr ); }
ShProg.SetM44 = function( progObj, name, mat ) { if(progObj.uniLoc[name]) gl.uniformMatrix4fv( progObj.uniLoc[name], false, mat ); }
ShProg.Compile = function( source, shaderStage ) {
var shaderScript = document.getElementById(source);
if (shaderScript) {
    source = "";
    var node = shaderScript.firstChild;
    while (node) {
    if (node.nodeType == 3) source += node.textContent;
    node = node.nextSibling;
    }
}
var shaderObj = gl.createShader( shaderStage );
gl.shaderSource( shaderObj, source );
gl.compileShader( shaderObj );
var status = gl.getShaderParameter( shaderObj, gl.COMPILE_STATUS );
if ( !status ) alert(gl.getShaderInfoLog(shaderObj));
return status ? shaderObj : 0;
} 
ShProg.Link = function( shaderObjs ) {
var prog = gl.createProgram();
for ( var i_sh = 0; i_sh < shaderObjs.length; ++ i_sh )
    gl.attachShader( prog, shaderObjs[i_sh] );
gl.linkProgram( prog );
status = gl.getProgramParameter( prog, gl.LINK_STATUS );
if ( !status ) alert("Could not initialise shaders");
gl.useProgram( null );
return status ? prog : 0;
}
    
initScene();

})();
html,body { margin: 0; overflow: hidden; }
#gui { position : absolute; top : 0; left : 0; }
<script id="draw-shader-vs" type="x-shader/x-vertex">
precision mediump float;

attribute vec3 inPos;
attribute vec3 inNV;
attribute vec3 inCol;

varying vec3 vertPos;
varying vec3 vertNV;
varying vec3 vertCol;
varying vec4 clip_space_pos;
    
uniform mat4 u_projectionMat44;
uniform mat4 u_viewMat44;
uniform mat4 u_modelMat44;

void main()
{
    vec3 modelNV  = mat3( u_modelMat44 ) * normalize( inNV );
    vertNV        = mat3( u_viewMat44 ) * modelNV;
    vertCol       = inCol;
    vec4 modelPos = u_modelMat44 * vec4( inPos, 1.0 );
    vec4 viewPos  = u_viewMat44 * modelPos;
    vertPos       = viewPos.xyz / viewPos.w;
    gl_Position   = u_projectionMat44 * viewPos;
}
</script>

<script id="draw-shader-fs" type="x-shader/x-fragment">
precision mediump float;

varying vec3 vertPos;
varying vec3 vertNV;
varying vec3 vertCol;

struct Light {
    vec3  position;
    vec3  direction;
    float ambient;
    float diffuse;
    float specular;
    float shininess;
    vec3  attenuation;
    float cutOffAngle;
};
uniform Light u_light;

void main()
{
    vec3  color     = vertCol;
    vec3  lightCol  = u_light.ambient * color;
    vec3  normalV   = normalize( vertNV );
    vec3  lightV    = normalize( u_light.position - vertPos );
    float lightD    = length( u_light.position - vertPos );
    float cosL      = dot( normalize( u_light.direction ), -lightV );
    float inCone    = step( cos( u_light.cutOffAngle * 0.5 ), cosL );
    float att       = 1.0 / dot( vec3( 1.0, lightD, lightD*lightD ), u_light.attenuation );
    float NdotL     = max( 0.0, dot( normalV, lightV ) );
    lightCol       += NdotL * u_light.diffuse * color * inCone * att;
    vec3  eyeV      = normalize( -vertPos );
    vec3  halfV     = normalize( eyeV + lightV );
    float NdotH     = max( 0.0, dot( normalV, halfV ) );
    float kSpecular = ( u_light.shininess + 2.0 ) * pow( NdotH, u_light.shininess ) / ( 2.0 * 3.14159265 );
    lightCol       += kSpecular * u_light.specular * color * inCone * att;
    gl_FragColor    = vec4( lightCol.rgb, 1.0 );
}
</script>

<script id="light-cone-shader-vs" type="x-shader/x-vertex">
precision mediump float;
attribute vec2 inPos;
varying vec2 vertPos;
void main()
{
    vertPos.xy  = inPos.xy;
    gl_Position = vec4( inPos, 0.0, 1.0 );
}
</script>

<script id="light-cone-shader-fs" type="x-shader/x-fragment">
precision mediump float;

varying vec2 vertPos;

uniform sampler2D u_colorAttachment0;
uniform vec2  u_depthRange;
uniform vec2  u_vp;
uniform float u_fov;

struct Light {
    vec3  position;
    vec3  direction;
    float ambient;
    float diffuse;
    float specular;
    float shininess;
    vec3  attenuation;
    float cutOffAngle;
};
uniform Light u_light;

void main()
{
    vec4 texCol = texture2D( u_colorAttachment0, vertPos.st * 0.5 + 0.5 );
    
    vec3 vLightPos  = u_light.position;
    vec3 vLightDir  = normalize( u_light.direction );
    float tanFOV    = tan(u_fov*0.5);
    vec3  nearPos   = vec3( vertPos.x * u_vp.x/u_vp.y * tanFOV, vertPos.y * tanFOV, -1.0 );
    //vec2 texCoord = gl_FragCoord.xy / u_vp;
    //vec3 nearPos  = vec3( (texCoord.x-0.5) * u_vp.x/u_vp.y, texCoord.y-0.5, -u_depthRange.x );
    vec3 los        = normalize( nearPos );
    
    // ray definition
    vec3 O = vec3(0.0);
    vec3 D = los;

    // cone definition
    vec3  C     = vLightPos;
    vec3  V     = vLightDir;
    float cosTh = cos( u_light.cutOffAngle * 0.5 );
    
    // ray - cone intersection
    vec3  CO     = O - C;
    float DdotV  = dot( D, V );
    float COdotV = dot( CO, V );
    float a      = DdotV*DdotV - cosTh*cosTh;
    float b      = 2.0 * (DdotV*COdotV - dot( D, CO )*cosTh*cosTh);
    float c      = COdotV*COdotV - dot( CO, CO )*cosTh*cosTh;
    float det    = b*b - 4.0*a*c;
    
    // find intersection
    float isIsect = 0.0;
    vec3  isectP  = vec3(0.0);
    if ( det >= 0.0 )
    {
        vec3  P1 = O + (-b-sqrt(det))/(2.0*a) * D;
        vec3  P2 = O + (-b+sqrt(det))/(2.0*a) * D;
        float isect1 = step( 0.0, dot(normalize(P1-C), V) );
        float isect2 = step( 0.0, dot(normalize(P2-C), V) );
        if ( isect1 < 0.5 )
        {
            P1 = P2;
            isect1 = isect2;
        }
        if ( isect2 < 0.5 )
        {
            P2 = P1;
            isect2 = isect1;
        }
        isectP = ( P1.z > -u_depthRange.x || (P2.z < -u_depthRange.x && P1.z < P2.z ) ) ? P2 : P1;
        isIsect = mix( isect2, 1.0, isect1 ) * step( isectP.z, -u_depthRange.x );
    }
    
    float dist = length( isectP - vLightPos.xyz );
    float att  = 1.0 / dot( vec3( 1.0, dist, dist*dist ), u_light.attenuation );        
    
    
    gl_FragColor = vec4( mix( texCol.rgb, vec3(1.0, 1.0, 1.0), isIsect * att * 0.5 ), 1.0 );
}
</script>

<div><form id="gui" name="inputs">
<table>
    <tr> <td> <font color=#40f040>ambient</font> </td> 
            <td> <input type="range" id="ambient" min="0" max="100" value="0"/></td> </tr>
    <tr> <td> <font color=#40f040>diffuse</font> </td> 
            <td> <input type="range" id="diffuse" min="0" max="100" value="0"/></td> </tr>
    <tr> <td> <font color=#40f040>specular</font> </td> 
            <td> <input type="range" id="specular" min="0" max="100" value="0"/></td> </tr>
    <tr> <td> <font color=#40f040>shininess</font> </td> 
            <td> <input type="range" id="shininess" min="1" max="100" value="0"/></td> </tr>
    <tr> <td> <font color=#40f040>cut off angle</font> </td> 
            <td> <input type="range" id="cutOffAngle" min="1" max="180" value="0"/></td> </tr>
</table>
</form>
</div>

<canvas id="glow-canvas" style="border: none;"></canvas>
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • 1
    THANKS a lot for your excellent answer ! I have updated my question with your changes. However I believe your code uses different coordinates than mine. I think in mine everything is in world space coordinates. I'm trying to figure out what makes the difference. – Massimo Callegari Aug 21 '17 at 13:33