As such, rendering the shadow map is done with an orthographic projection matrix. An orthographic matrix is just ... Here are two way to do this in GLSL.
Basicshadowmap
Renderingtheshadowmap
SettinguptherendertargetandtheMVPmatrix
Theshaders
Result
Usingtheshadowmap
Basicshader
Result-Shadowacne
Problems
Shadowacne
PeterPanning
Aliasing
PCF
PoissonSampling
StratifiedPoissonSampling
Goingfurther
Earlybailing
Spotlights
Pointlights
Combinationofseverallights
Automaticlightfrustum
Exponentialshadowmaps
Light-spaceperspectiveShadowMaps
Cascadedshadowmaps
Conclusion
InTutorial15welearnthowtocreatelightmaps,whichencompassesstaticlighting.Whileitproducesveryniceshadows,itdoesn’tdealwithanimatedmodels.
Shadowmapsarethecurrent(asof2016)waytomakedynamicshadows.Thegreatthingaboutthemisthatit’sfairlyeasytogettowork.Thebadthingisthatit’sterriblydifficulttogettoworkright.
Inthistutorial,we’llfirstintroducethebasicalgorithm,seeitsshortcomings,andthenimplementsometechniquestogetbetterresults.Sinceattimeofwriting(2012)shadowmapsarestillaheavilyresearchedtopic,we’llgiveyousomedirectionstofurtherimproveyourownshadowmap,dependingonyourneeds.
Basicshadowmap
Thebasicshadowmapalgorithmconsistsintwopasses.First,thesceneisrenderedfromthepointofviewofthelight.Onlythedepthofeachfragmentiscomputed.Next,thesceneisrenderedasusual,butwithanextratesttoseeitthecurrentfragmentisintheshadow.
The“beingintheshadow”testisactuallyquitesimple.Ifthecurrentsampleisfurtherfromthelightthantheshadowmapatthesamepoint,thismeansthatthescenecontainsanobjectthatisclosertothelight.Inotherwords,thecurrentfragmentisintheshadow.
Thefollowingimagemighthelpyouunderstandtheprinciple:
Renderingtheshadowmap
Inthistutorial,we’llonlyconsiderdirectionallights-lightsthataresofarawaythatallthelightrayscanbeconsideredparallel.Assuch,renderingtheshadowmapisdonewithanorthographicprojectionmatrix.Anorthographicmatrixisjustlikeausualperspectiveprojectionmatrix,exceptthatnoperspectiveistakenintoaccount-anobjectwilllookthesamewhetherit’sfarornearthecamera.
SettinguptherendertargetandtheMVPmatrix
SinceTutorial14,youknowhowtorenderthesceneintoatextureinordertoaccessitlaterfromashader.
Hereweusea1024x102416-bitdepthtexturetocontaintheshadowmap.16bitsareusuallyenoughforashadowmap.Feelfreetoexperimentwiththesevalues.Notethatweuseadepthtexture,notadepthrenderbuffer,sincewe’llneedtosampleitlater.
//Theframebuffer,whichregroups0,1,ormoretextures,and0or1depthbuffer.
GLuintFramebufferName=0;
glGenFramebuffers(1,&FramebufferName);
glBindFramebuffer(GL_FRAMEBUFFER,FramebufferName);
//Depthtexture.Slowerthanadepthbuffer,butyoucansampleitlaterinyourshader
GLuintdepthTexture;
glGenTextures(1,&depthTexture);
glBindTexture(GL_TEXTURE_2D,depthTexture);
glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT16,1024,1024,0,GL_DEPTH_COMPONENT,GL_FLOAT,0);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
glFramebufferTexture(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,depthTexture,0);
glDrawBuffer(GL_NONE);//Nocolorbufferisdrawnto.
//Alwayscheckthatourframebufferisok
if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE)
returnfalse;
TheMVPmatrixusedtorenderthescenefromthelight’spointofviewiscomputedasfollows:
TheProjectionmatrixisanorthographicmatrixwhichwillencompasseverythingintheaxis-alignedbox(-10,10),(-10,10),(-10,20)ontheX,YandZaxesrespectively.Thesevaluesaremadesothatourentire*visible*sceneisalwaysvisible;moreonthisintheGoingFurthersection.
TheViewmatrixrotatestheworldsothatincameraspace,thelightdirectionis-Z(wouldyouliketore-readTutorial3?)
TheModelmatrixiswhateveryouwant.
glm::vec3lightInvDir=glm::vec3(0.5f,2,2);
//ComputetheMVPmatrixfromthelight'spointofview
glm::mat4depthProjectionMatrix=glm::ortho(-10,10,-10,10,-10,20);
glm::mat4depthViewMatrix=glm::lookAt(lightInvDir,glm::vec3(0,0,0),glm::vec3(0,1,0));
glm::mat4depthModelMatrix=glm::mat4(1.0);
glm::mat4depthMVP=depthProjectionMatrix*depthViewMatrix*depthModelMatrix;
//Sendourtransformationtothecurrentlyboundshader,
//inthe"MVP"uniform
glUniformMatrix4fv(depthMatrixID,1,GL_FALSE,&depthMVP[0][0])
Theshaders
Theshadersusedduringthispassareverysimple.Thevertexshaderisapass-throughshaderwhichsimplycomputethevertex’positioninhomogeneouscoordinates:
#version330core
//Inputvertexdata,differentforallexecutionsofthisshader.
layout(location=0)invec3vertexPosition_modelspace;
//Valuesthatstayconstantforthewholemesh.
uniformmat4depthMVP;
voidmain(){
gl_Position=depthMVP*vec4(vertexPosition_modelspace,1);
}
Thefragmentshaderisjustassimple:itsimplywritesthedepthofthefragmentatlocation0(i.e.inourdepthtexture).
#version330core
//Ouputdata
layout(location=0)outfloatfragmentdepth;
voidmain(){
//Notreallyneeded,OpenGLdoesitanyway
fragmentdepth=gl_FragCoord.z;
}
Renderingashadowmapisusuallymorethantwiceasfastasthenormalrender,becauseonlylowprecisiondepthiswritten,insteadofboththedepthandthecolor;MemorybandwidthisoftenthebiggestperformanceissueonGPUs.
Result
Theresultingtexturelookslikethis:
Adarkcolourmeansasmallz;hence,theupper-rightcornerofthewallisnearthecamera.Attheopposite,whitemeansz=1(inhomogeneouscoordinates),sothisisveryfar.
Usingtheshadowmap
Basicshader
Nowwegobacktoourusualshader.Foreachfragmentthatwecompute,wemusttestwhetheritis“behind”theshadowmapornot.
Todothis,weneedtocomputethecurrentfragment’spositioninthesamespacethattheoneweusedwhencreatingtheshadowmap.SoweneedtotransformitoncewiththeusualMVPmatrix,andanothertimewiththedepthMVPmatrix.
Thereisalittletrick,though.Multiplyingthevertex’positionbydepthMVPwillgivehomogeneouscoordinates,whicharein[-1,1];buttexturesamplingmustbedonein[0,1].
Forinstance,afragmentinthemiddleofthescreenwillbein(0,0)inhomogeneouscoordinates;butsinceitwillhavetosamplethemiddleofthetexture,theUVswillhavetobe(0.5,0.5).
Thiscanbefixedbytweakingthefetchcoordinatesdirectlyinthefragmentshaderbutit’smoreefficienttomultiplythehomogeneouscoordinatesbythefollowingmatrix,whichsimplydividescoordinatesby2(thediagonal:[-1,1]->[-0.5,0.5])andtranslatesthem(thelowerrow:[-0.5,0.5]->[0,1]).
glm::mat4biasMatrix(
0.5,0.0,0.0,0.0,
0.0,0.5,0.0,0.0,
0.0,0.0,0.5,0.0,
0.5,0.5,0.5,1.0
);
glm::mat4depthBiasMVP=biasMatrix*depthMVP;
Wecannowwriteourvertexshader.It’sthesameasbefore,butweoutput2positionsinsteadof1:
gl_Positionisthepositionofthevertexasseenfromthecurrentcamera
ShadowCoordisthepositionofthevertexasseenfromthelastcamera(thelight)
//Outputpositionofthevertex,inclipspace:MVP*position
gl_Position=MVP*vec4(vertexPosition_modelspace,1);
//Same,butwiththelight'sviewmatrix
ShadowCoord=DepthBiasMVP*vec4(vertexPosition_modelspace,1);
Thefragmentshaderisthenverysimple:
texture(shadowMap,ShadowCoord.xy).zisthedistancebetweenthelightandthenearestoccluder
ShadowCoord.zisthedistancebetweenthelightandthecurrentfragment
…soifthecurrentfragmentisfurtherthanthenearestoccluder,thismeansweareintheshadow(ofsaidnearestoccluder):
floatvisibility=1.0;
if(texture(shadowMap,ShadowCoord.xy).zdrawonlyback-facingtriangles
Andwhenrenderingthescene,rendernormally(backfaceculling)
glCullFace(GL_BACK);//Cullback-facingtriangles->drawonlyfront-facingtriangles
Thismethodisusedinthecode,inadditiontothebias.
PeterPanning
Wehavenoshadowacneanymore,butwestillhavethiswrongshadingoftheground,makingthewalltolookasifit’sflying(hencetheterm“PeterPanning”).Infact,addingthebiasmadeitworse.
Thisoneisveryeasytofix:simplyavoidthingeometry.Thishastwoadvantages:
First,itsolvesPeterPanning:itthegeometryismoredeepthanyourbias,you’reallset.
Second,youcanturnonbackfacecullingwhenrenderingthelightmap,becausenow,thereisapolygonofthewallwhichisfacingthelight,whichwilloccludetheotherside,whichwouldn’tberenderedwithbackfaceculling.
Thedrawbackisthatyouhavemoretrianglestorender(twotimesperframe!)
Aliasing
Evenwiththesetwotricks,you’llnoticethatthereisstillaliasingontheborderoftheshadow.Inotherwords,onepixeliswhite,andthenextisblack,withoutasmoothtransitioninbetween.
PCF
Theeasiestwaytoimprovethisistochangetheshadowmap’ssamplertypetosampler2DShadow.Theconsequenceisthatwhenyousampletheshadowmaponce,thehardwarewillinfactalsosampletheneighboringtexels,dothecomparisonforallofthem,andreturnafloatin[0,1]withabilinearfilteringofthecomparisonresults.
Forinstance,0.5meansthat2samplesareintheshadow,and2samplesareinthelight.
Notethatit’snotthesamethanasinglesamplingofafiltereddepthmap!Acomparisonalwaysreturnstrueorfalse;PCFgivesainterpolationof4“trueorfalse”.
Asyoucansee,shadowbordersaresmooth,butshadowmap’stexelsarestillvisible.
PoissonSampling
AneasywaytodealwiththisistosampletheshadowmapNtimesinsteadofonce.UsedincombinationwithPCF,thiscangiveverygoodresults,evenwithasmallN.Here’sthecodefor4samples:
for(inti=0;i<4;i++){
if(texture(shadowMap,ShadowCoord.xy+poissonDisk[i]/700.0).z(glm::radians(45.0f),1.0f,2.0f,50.0f);
glm::mat4depthViewMatrix=glm::lookAt(lightPos,lightPos-lightInvDir,glm::vec3(0,1,0));
samething,butwithaperspectivefrustuminsteadofanorthographicfrustum.Usetexture2Dprojtoaccountforperspective-divide(seefootnotesintutorial4-Matrices)
Thesecondstepistotakeintoaccounttheperspectiveintheshader.(seefootnotesintutorial4-Matrices.Inanutshell,aperspectiveprojectionmatrixactuallydoesn’tdoanyperspectiveatall.Thisisdonebythehardware,bydividingtheprojectedcoordinatesbyw.Here,weemulatethetransformationintheshader,sowehavetodotheperspective-divideourselves.Bytheway,anorthographicmatrixalwaysgenerateshomogeneousvectorswithw=1,whichiswhytheydon’tproduceanyperspective)
HerearetwowaytodothisinGLSL.Thesecondusesthebuilt-intextureProjfunction,butbothmethodsproduceexactlythesameresult.
if(texture(shadowMap,(ShadowCoord.xy/ShadowCoord.w)).z