Tutorial 23 - Shadow Mapping - Part 1 - OGLdev

文章推薦指數: 80 %
投票人數:10人

The results of the 3D pipeline in OpenGL end up in something which is called a 'framebuffer object' (a.k.a FBO). This concept wraps within it the color ... Background Theconceptofshadowisinseparablefromtheconceptoflight,asyouneedlightinordertocast ashadow.Therearemanytechniquesthatgenerateshadowsandinthistwoparttutorialwearegoing tostudyoneofthemorebasicandsimpleones-shadowmapping. Whenitcomestorasterizationandshadowsthequestionthatyouaskyourselfis-isthispixellocated inshadowornot?Let'saskthisdifferently-doesthepathfromthelightsourcetothepixelgoes throughanotherobjectornot?Ifitdoes-thepixelisprobablyinshadow(assumingtheotherobject isnottransparent...),andifnot-thepixelisnotinshadow.Inaway,thisquestionissimilartothe questionweaskedourselvesintheprevioustutorial-howtomakesurethatwhentwoobjectsoverlapeach otherwewillseethecloserone.Ifweplacethecameraforamomentatthelightoriginthetwoquestions becomeone.Wewantthepixelsthatfailthedepthtest(i.e.theonesthatarefurtherawayandhavepixels beforethem)tobeinshadow.Onlythepixelsthatwinthedepthtestmustbeinlight.Theyaretheones thatareindirectcontactwiththelightsourceandthereisnothinginbetweenthatconcealsthem.Inanutshell, thisistheideabehindshadowmapping. Soitlookslikethedepthtestcanhelpusdetectwhetherapixelisinshadowornotbutthereisaproblem. Thecameraandthelightarenotalwayspositionedinthesameplace.Thedepthtestisnormallyusedto solvethevisibilityproblemfromthecamerapointofview,sohowcanweharnessitforshadowdetectionwhen thelightislocatedfurtheraway?Thesolutionistorenderthescenetwice.Firstfromthelightpointof view.Theresultsofthisrenderpassdon'treachthecolorbuffer.Instead,theclosestdepthvaluesare renderedintoanapplicationcreateddepthbuffer(insteadoftheonethatisautomaticallygeneratedbyGLUT). Inthesecondpassthesceneisrenderedasusualfromthecamerapointofview.Thedepthbufferthatwe've createdisboundtothefragmentshaderforreading.Foreachpixelwefetchthecorrespondingdepthfromthat depthbuffer.Wealsocalculatethedepthofthispixelfromthelightpointofview.Sometimesthetwodepth valueswillbeidentical.Thisisthecasewherethispixelwasclosesttothelightsoitsdepthvalueended upinthedepthbuffer.Ifthathappenweconsiderthepixelasifitisinlightandcalculateitscolorasusual. Ifthedepthvaluesaredifferentitmeansthereisanotherpixelthatcoversthispixelwhenlookingatitfrom thelightposition.Inthiscaseweaddsomeshadowfactortothecolorcalculationinordertosimulatethe shadoweffect.Takealookatthefollowingpicture: Oursceneismadeupoftwoobjects-thesurfaceandthecube.Thelightsourceislocatedatthetopleftcorner andispointingatthecube.Inthefirstpasswerenderintothedepthbuffer fromthepointofviewofthelightsource.Let'sfocusonthethreepointsA,BandC.WhenBisrendereditsdepth valuegoesintothedepthbuffer.Thereasonisthatthereisnothinginbetweenthepointandthelight.Bydefault, itistheclosestpointtothelightonthatline.However,whenAandCarerenderedthey"compete"ontheexactsame spotinthedepthbuffer.Bothpointsareonthesamestraightlinefromthelightsourcesoafterperspectiveprojection takesplacetherasterizerfindsoutbothpointsneedtogotothesamepixelonthescreen.Thisisthedepthtestand pointC"wins"it. Inthesecondpasswerenderthesurfaceandthecubefromthecamerapointofview.Inadditiontoeverythingwehave doneinourlightingshaderperpixelwealsocalculatethedistancefromthelightsourcetothepixelandcompare ittothecorrespondingvalueinthedepthbuffer.WhenwerasterizepointBthetwovaluesshouldroughlybesame (somedifferencesareexpectedduetodifferencesininterpolationandfloatingpointprecisionissues).Therefore, wedecidethatBisnotinshadowandactaccordingly.WhenwerasterizepointAwefindoutthatthestoreddepth valueisclearlysmallerthanthedepthofA.Therefore,wedecidethatAisinshadowandapplysomeshadowfactortoit inordertogetitdarkerthanusual. This,inanutshell,istheshadowmappingalgorithm(thedepthbufferthatwerendertointhefirstpass iscalledthe"shadowmap").Wearegoingtostudyitintwostages.Inthefirststage(thistutorial)wewill learnhowtorenderintotheshadowmap.Theprocessofrenderingsomething(depth,color,etc)intoan applicationcreatedtextureisknownas'rendertotexture'.Wewilldisplaytheshadowmaponthescreen usingasimpletexturemappingtechniquethatwearealreadyfamiliarwith.Thisisagooddebuggingstep asgettingtheshadowmapcorrectiscrucialinordertogetthecompleteshadoweffectworkingcorrectly. Inthenexttutorialwewillseehowtousetheshadowmapinordertodothe"inshadow/notinshadow"decision. Thesourcesofthistutorialincludeasimplequadmeshthatcanbeusedtodisplaytheshadowmap.Thequadismade upoftwotrianglesandthetexturecoordinatesaresetupsuchthattheycovertheentiretexturespace.When thequadisrenderedthetexturecoordinatesareinterpolatedbytherasterizer,allowingyoutosampleanentire textureanddisplayitonscreen. Sourcewalkthru (shadow_map_fbo.h:50) classShadowMapFBO {   public:     ShadowMapFBO();     ~ShadowMapFBO();     boolInit(unsignedintWindowWidth,unsignedintWindowHeight);     voidBindForWriting();     voidBindForReading(GLenumTextureUnit);   private:     GLuintm_fbo;     GLuintm_shadowMap; }; Theresultsofthe3DpipelineinOpenGLendupinsomethingwhichiscalleda'framebufferobject'(a.k.aFBO).This conceptwrapswithinitthecolorbuffer(whichisdisplayedonscreen),thedepthbufferaswell asafewotherbuffersforadditionalusages.WhenglutInitDisplayMode()iscalleditcreatesthe defaultframebufferusingthespecifiedparameters.Thisframebufferismanagedbythewindowing systemandcannotbedeletedbyOpenGL.Inadditiontothedefaultframebuffer,anapplicationcan createFBOsofitsown.Theseobjectscanbemanipulatedandusedforvarious techniquesunderthecontroloftheapplication.TheShadowMapFBOclassprovidesaneasytouse interfacetoaFBOwhichwillbeusedfortheshadowmappingtechnique.Internally, thisclasscontainstwoOpenGLhandles.Thehandle'm_fbo'representstheactualFBO. TheFBOencapsulateswithinittheentirestateoftheframebuffer.Oncethisobject iscreatedandconfiguredproperlywecanchangeframebuffersbysimplybindingadifferentobject. Notethatonlythedefaultframebuffercanbeusedtodisplaysomethingonthescreen.Theframebuffers createdbytheapplicationcanonlybeusedfor"offscreenrendering".Thiscanbeanintermediate renderingpass(e.g.ourshadowmappingbuffer)whichcanlaterbeusedforthe"real"renderingpass thatgoestothescreen. Initself,theframebufferisjustaplaceholder.Tomakeitusableweneedtoattachtexturesto oneormoreoftheavailableattachmentpoints.Thetexturescontaintheactualstoragespaceoftheframebuffer. OpenGLdefinesthefollowingattachmentpoints: COLOR_ATTACHMENTi-thetexturethatwillbeattachedherewillreceivethecolorthatcomesoutof thefragmentshader.The'i'suffixmeansthattherecanbemultipletexturesattachedascolor attachmentssimultaneously.Thereisamechanisminthefragmentshaderthatenablesrendering intoseveralcolorbuffersatthesametime. DEPTH_ATTACHMENT-thetexturethatwillbeattachedherewillreceivetheresultsofthedepthtest. STENCIL_ATTACHMENT-thetexturethatwillbeattachedherewillserveasthestencilbuffer.The stencilbufferenableslimitingtheareaofrasterizationandcanbeusedforvarioustechniques. DEPTH_STENCIL_ATTACHMENT-thisoneissimplyacombinationofdepthandstencilbuffersas thetwoareoftenusedtogether. Fortheshadowmappingtechniquewewillonlyneedadepthbuffer.Thememberattribute'm_shadowMap'is thehandleofthetexturethatwillbeattachedtotheDEPTH_ATTACHMENTattachmentpoint.TheShadowMapFBO alsoprovidesacoupleofmethodsthatwillbeusedinthemainrenderfunction.Wewillcall BindForWriting()beforerenderingintotheshadowmapandBindForReading()whenstartingthesecondrenderingpass. (shadow_map_fbo.cpp:43) glGenFramebuffers(1,&m_fbo); HerewecreatetheFBO.Sameasintexturesandbuffers,wespecifytheaddressofan arrayofGLuintsanditssize.Thearrayispopulatedwiththehandles. (shadow_map_fbo.cpp:46) glGenTextures(1,&m_shadowMap); glBindTexture(GL_TEXTURE_2D,m_shadowMap); glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT,WindowWidth,WindowHeight,0,GL_DEPTH_COMPONENT,GL_FLOAT,NULL); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE); Nextwecreatethetexturethatwillserveastheshadowmap.Ingeneral,thisisastandard2Dtexturewith somespecificconfigurationtomakeitsuitableforitspurpose: TheinternalformatisGL_DEPTH_COMPONENT.Thisisdifferentfromtheprevioususeofthisfunction wheretheinternalformatwasusuallyoneofthecolortypes(e.g.GL_RGB).GL_DEPTH_COMPONENTmeansasingle floatingpointnumberthatrepresentsthenormalizeddepth. ThelastparameterofglTexImage2Disnull.Thismeansthatwearenotsupplyinganydatabywhichto initializethebuffer.Thismakessenseknowingthatwewantthebuffertocontainthedepthvaluesof eachframeandeachframeisabitdifferent.WheneverwestartanewframewewilluseglClear()to clearoutthebuffer.Thisisalltheinitializationthatweneedforthecontent. WetellOpenGLthatincaseatexturecoordinategoesoutofbounditneedstoclampittothe[0,1] range.Thiscanhappenwhentheprojectionwindowfromthecamerapointofviewcontainsmorethan theprojectionwindowfromthelightpointofview.Toavoidstrangeartifactssuchastheshadowrepeating itselfelsewhere(duetowraparound)weclampthetexturecoordinates. (shadow_map_fbo.cpp:54) glBindFramebuffer(GL_FRAMEBUFFER,m_fbo); WehavegeneratedtheFBO,thetextureobjectandalsoconfiguredthetextureobjectforshadowmapping. NowweneedtoattachthetextureobjecttotheFBO.Thefirstthingweneedtodoistobindthe FBO.Thiswillmakeit"current"andthenallfutureFBOoperationswillapplytoit.Thisfunction takestheFBOhandleandthedesiredtarget.ThetargetcanbeGL_FRAMEBUFFER,GL_DRAW_FRAMEBUFFERorGL_READ_FRAMEBUFFER. GL_READ_FRAMEBUFFERisusedwhenwewanttoreadfromtheFBOusingglReadPixels(notinthis tutorial).GL_DRAW_FRAMEBUFFERisusedwhenwewanttorenderintotheFBO.WhenweuseGL_FRAMEBUFFERboth thereadingandwritingstateisupdatedandthisistherecommendedwayforinitializingtheFBO.Wewill useGL_DRAW_FRAMEBUFFERwhenweactuallystarttorender. (shadow_map_fbo.cpp:55) glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_TEXTURE_2D,m_shadowMap,0); HereweattachtheshadowmaptexturetothedepthattachmentpointoftheFBO.Thelastparameter tothisfunctionindicatesthemipmaplayertouse.Mipmappingisatexturemappingfeaturewhere atextureisrepresentedatdifferentresolutions,startingfromthehighestresolutionatmipmap0 anddecreasingresolutionsinmipmaps1-N.Thecombinationofamipmappedtextureandtrilinearfiltering providesmorepleasantresultsbycombiningtexelsfromneighboringmipmaplevels(whennosingle levelisperfect).Herewehaveasinglemipmaplevelsoweuse0.Weprovidetheshadowmaphandle asthefourthparameter.Ifweuse0hereitwilldetachthecurrenttexturefromthespecified attachmentpoint(depthinthecaseabove). (shadow_map_fbo.cpp:58) glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); Sincewearenotgoingtorenderintothecolorbuffer(onlyintothedepth)weexplicitlyspecify itusingtheabovecall.Bydefault,thecolorbuffertargetissettoGL_COLOR_ATTACHMENT0,butourFBO isn'tevengoingtocontainacolorbuffer.Therefore,itisbettertotellOpenGLourintentionsexplicitly. ThevalidparameterstothisfunctionsareGL_NONEandGL_COLOR_ATTACHMENT0toGL_COLOR_ATTACHMENTmwhere 'm'isGL_MAX_COLOR_ATTACHMENTS-1.TheseparametersarevalidonlyforFBOs.Ifthedefaultframebuffer isusedthevalidparametersareGL_NONE,GL_FRONT_LEFT,GL_FRONT_RIGHT,GL_BACK_LEFTandGL_BACK_RIGHT. Theseallowyoutorenderdirectlyintothefrontorbackbuffers(whereeachonehasaleftandrightbuffer). WealsosetthereadbuffertoGL_NONE(remember,wearenotgoingtocalloneoftheglReadPixelAPIs). ThisismainlytoavoidproblemswithGPUsthatsupportonlyOpenGL3.xandnot4.x. (shadow_map_fbo.cpp:61) GLenumStatus=glCheckFramebufferStatus(GL_FRAMEBUFFER); if(Status!=GL_FRAMEBUFFER_COMPLETE){   printf("FBerror,status:0x%x\n",Status);   returnfalse; } WhentheconfigurationoftheFBOisfinisheditisveryimportanttoverifythatitsstateiswhatthe OpenGLspecdefinesas"complete".Thismeansthatnoerrorwasdetectedandthattheframebuffer cannowbeused.Thecodeabovechecksthat. (shadow_map_fbo.cpp:72) voidShadowMapFBO::BindForWriting() {   glBindFramebuffer(GL_DRAW_FRAMEBUFFER,m_fbo); } Wewillneedtotogglebetweenrenderingintotheshadowmapandrenderingintothedefaultframebuffer. Inthesecondpasswewillalsoneedtobindourshadowmapforinput.Thisfunctionandthenext oneprovideeasytousewrapperstodothat.TheabovefunctionsimplybindstheFBOforwritingas wedidearlier.Wewillcallitbeforethefirstrenderpass... (shadow_map_fbo.cpp:78) voidShadowMapFBO::BindForReading(GLenumTextureUnit) {   glActiveTexture(TextureUnit);   glBindTexture(GL_TEXTURE_2D,m_shadowMap); } ...andthisfunctionwillbeusedbeforethesecondrenderpasstobindtheshadowmapforreading. Notethatwebindspecificallythetextureobject,ratherthantheFBOitself.Thisfunctiontakes thetextureunittowhichtheshadowmapwillbebound.Thetextureunitindexmustbesynchronized withtheshader(sincetheshaderhasasampler2Duniformvariabletoaccessthetexture).Itisvery importanttonotethatwhileglActiveTexturetakesthetextureindexasanenum(e.g.GL_TEXTURE0,GL_TEXTURE1,etc), theshaderneedssimplytheindexitself(0,1,etc).Thiscanbethesourceofmanybugs(believeme,Iknow). (shadow_map.vs) #version330 layout(location=0)invec3Position; layout(location=1)invec2TexCoord; layout(location=2)invec3Normal; uniformmat4gWVP; outvec2TexCoordOut; voidmain() {   gl_Position=gWVP*vec4(Position,1.0);   TexCoordOut=TexCoord; } Wearegoingtousethesameshaderprogramforbothrenderpasses.Thevertexshaderwillbeusedby bothpasseswhilethefragmentshaderwillbeusedonlybythesecondpass.Sincewearedisabling writingtothecolorbufferinthefirstpassthefragmentshaderwillsimplybeleftunusedthere.Thevertexshader aboveisverysimple.Itgeneratestheclipspacecoordinatebymultiplyingthelocalspaceposition bytheWVPmatrixandpassesthroughthetexturecoordinates.Inthefirstpassthetexturecoordinates areredundant(nofragmentshader).However,thereisnorealimpactanditissimplertosharethe vertexshader.Asyoucansee,fromthepointofviewoftheshaderitmakesnodifferencewhether thisisaZpassorarealrenderpass.Whatmakesthedifferenceisthattheapplicationpassesa lightpointofviewWVPmatrixinthefirstpassandacamerapointofviewWVPmatrixinthesecondpass.Inthefirst passtheZbufferwillbepopulatedbytheclosestZvaluesfromthelightpointofviewandonthesecond passfromthecamerapointofview.Inthesecondpasswealsoneedthetexturecoordinatesinthefragment shaderbecausewewillsamplefromtheshadowmap(whichisnowinputtotheshader). (shadow_map.fs) #version330 invec2TexCoordOut; uniformsampler2DgShadowMap; outvec4FragColor; voidmain() {   floatDepth=texture(gShadowMap,TexCoordOut).x;   Depth=1.0-(1.0-Depth)*25.0;   FragColor=vec4(Depth); } Thisisthefragmentshaderthatisusedtodisplaytheshadowmapintherenderpass.The2Dtexturecoordinates areusedtofetchthedepthvaluefromtheshadowmap.Theshadowmaptexturewascreatedwiththetype GL_DEPTH_COMPONENTasitsinternalformat.Thismeansthatthebasictexelisasinglefloatingpointvalueand notacolor.Thisiswhy'.x'isusedduringsampling. TheperspectiveprojectionmatrixhasaknownbehaviorthatwhenitnormalizestheZinthepositionvectorit reservesmorevaluesinthe[0,1]rangetothecloserlocationsratherthanthelocationsthatarefurtheraway fromthecamera.TherationalistoallowgreaterZprecisionaswegetclosertothecamerabecauseerrorshere aremorenoticeable.Whenwedisplaythecontentsofthedepthbufferwemayrunintoacasewheretheresulting imageisnotclearenough.Therefore,afterwesamplethedepthfromtheshadowmapwesharpenitbyscaling thedistanceofthecurrentpointtothefaredge(whereZis1.0)andthensubstractingtheresultfrom1.0again. Thisamplifiestherangeandimprovesthefinalimage.Weusethenewdepthvaluetocreateacolorbybroadcasting itacrossallthecolorchannels.Thismeanswewillgetsomevariationofgray(whiteatthefarclippingplaneand blackatthenearclippingplane). Nowlet'sseehowtocombinethepiecesofcodeaboveandcreatetheapplication. (tutorial23.cpp:106) virtualvoidRenderSceneCB() {   m_pGameCamera->OnRender();   m_scale+=0.05f;   ShadowMapPass();   RenderPass();   glutSwapBuffers(); } Themainrenderfunctionhasbecomemuchsimplerasmostfunctionalitymovedtootherfunctions.Firstwetake careofthe"global"stufflikeupdatingthepositionofthecameraandtheclassmemberwhichisusedto rotatetheobject.Thenwecallafunctiontorenderintotheshadowmaptexturefollowedbyafunctionto displaytheresults.Finally,glutSwapBuffer()iscalledtodisplayittothescreen. (tutorial23.cpp:117) virtualvoidShadowMapPass() {   m_shadowMapFBO.BindForWriting();   glClear(GL_DEPTH_BUFFER_BIT);   Pipelinep;   p.Scale(0.1f,0.1f,0.1f);   p.Rotate(0.0f,m_scale,0.0f);   p.WorldPos(0.0f,0.0f,5.0f);   p.SetCamera(m_spotLight.Position,m_spotLight.Direction,Vector3f(0.0f,1.0f,0.0f));   p.SetPerspectiveProj(20.0f,WINDOW_WIDTH,WINDOW_HEIGHT,1.0f,50.0f);   m_pShadowMapTech->SetWVP(p.GetWVPTrans());   m_pMesh->Render();   glBindFramebuffer(GL_FRAMEBUFFER,0); } WestarttheshadowmappassbybindingintheshadowmapFBO.Fromnowonallthedepthvalueswillgointo ourshadowmaptextureandcolorwriteswillbediscarded.Weclearthedepthbuffer(only)beforewestart doinganything.Thenwesetupthepipelineclassinordertorenderthemesh(atankfromQuake2issupplied withthetutorialsource).Thesinglepointworthnoticing hereisthatthecameraisupdatedbasedonthepositionanddirectionofthespotlight.Werenderthemesh andthenswitchbacktothedefaultframebufferbybindingFBOzero. (tutorial23.cpp:135) virtualvoidRenderPass() {   glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);   m_pShadowMapTech->SetTextureUnit(0);   m_shadowMapFBO.BindForReading(GL_TEXTURE0);   Pipelinep;   p.Scale(5.0f,5.0f,5.0f);   p.WorldPos(0.0f,0.0f,10.0f);   p.SetCamera(m_pGameCamera->GetPos(),m_pGameCamera->GetTarget(),m_pGameCamera->GetUp());   p.SetPerspectiveProj(30.0f,WINDOW_WIDTH,WINDOW_HEIGHT,1.0f,50.0f);   m_pShadowMapTech->SetWVP(p.GetWVPTrans());   m_pQuad->Render(); } Therenderpassstartsbyclearingbothcoloranddepthbuffers.Thesebuffersbelondtothedefaultframebuffer. Wetelltheshadertousetextureunit0andbindtheshadowmaptextureforreadingontextureunit0. Fromhereoneverythingisasusual.Wescalethequadup,placeitdirectlyinfrontofthecameraandrenderit. Duringrasterizationtheshadowmapissampledanddisplayed. Note:inthistutorial'scodewenolongerautomaticallyloadawhitetexturewhenthemeshfiledoes notspecifyone.Thereasonistobeabletobindtheshadowmapinstead.Ifameshdoesnotcontaina texturewesimplybindnoneandthisallowsthecallingcodetobinditsowntexture. Nexttutorial commentspoweredbyDisqus



請為這篇文章評分?