Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GLSL spotlight projection volume

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

like image 388
Massimo Callegari Avatar asked Aug 20 '17 14:08

Massimo Callegari


1 Answers

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>
like image 185
Rabbid76 Avatar answered Oct 19 '22 07:10

Rabbid76