📜 ⬆️ ⬇️

VSDCT on OpenGL ES 3

I have long wanted to make a VSDCT demo on my mobile phone. VSDCT (Virtual Shadow Depth Cubemap Texture) is a representation of a cubemap texture, when instead of 6 separate faces, one regular 2D atlas texture is used, in which the original faces of the cube map are placed in the form of tightly packed tiles. Let's see how to make shadows from a point source of light using this technique.

image


')
Implementation


I will not dwell on the basic omni-directional shadow mapping algorithm, you can read the relevant resources in the links. We will use 6 linear projections and go straight to the VSDCT implementation.

Let's figure out what we want to do. We need to get 6 shadow maps that would cover all the directions visible from a point source of illumination. Those. one map for each of the directions ± X, ± Y, ± Z. So that there are no gaps between the cards, we will establish an FOV of 90 degrees for each of the projections:

image

6 model-view matrices are constructed in this way (P - coordinates of the light source):

Math::ViewMatrix( vec3( 0.0f, 0.0f, 1.0f ), vec3( 0.0f, 1.0f, 0.0f ), vec3( -1.0f, 0.0f, 0.0f ), P ); Math::ViewMatrix( vec3( 0.0f, 0.0f, -1.0f ), vec3( 0.0f, 1.0f, 0.0f ), vec3( 1.0f, 0.0f, 0.0f ), P ); Math::ViewMatrix( vec3( 1.0f, 0.0f, 0.0f ), vec3( 0.0f, 0.0f, 1.0f ), vec3( 0.0f, -1.0f, 0.0f ), P ); Math::ViewMatrix( vec3( 1.0f, 0.0f, 0.0f ), vec3( 0.0f, 0.0f, -1.0f ), vec3( 0.0f, 1.0f, 0.0f ), P ); Math::ViewMatrix( vec3( -1.0f, 0.0f, 0.0f ), vec3( 0.0f, 1.0f, 0.0f ), vec3( 0.0f, 0.0f, -1.0f ), P ); Math::ViewMatrix( vec3( 1.0f, 0.0f, 0.0f ), vec3( 0.0f, 1.0f, 0.0f ), vec3( 0.0f, 0.0f, 1.0f ), P ); LMatrix4 ViewMatrix( const LVector3& X, const LVector3& Y, const LVector3& Z, const LVector3& Position ) { LMatrix4 Matrix; Matrix[0][0] = Xx; Matrix[1][0] = Xy; Matrix[2][0] = Xz; Matrix[3][0] = -X.Dot( Position ); Matrix[0][1] = Yx; Matrix[1][1] = Yy; Matrix[2][1] = Yz; Matrix[3][1] = -Y.Dot( Position ); Matrix[0][2] = Zx; Matrix[1][2] = Zy; Matrix[2][2] = Zz; Matrix[3][2] = -Z.Dot( Position ); Matrix[0][3] = 0.0f; Matrix[1][3] = 0.0f; Matrix[2][3] = 0.0f; Matrix[3][3] = 1.0f; return Matrix; } 


All projections are identical, perspective with aspect 1: 1 and angle of 90 degrees:

 float NearCP = 0.5f; float FarCP = 512.0f; Math::Perspective( 90.0f, 1.0f, NearCP, FarCP ); 


When drawing each of the 6 shadow maps, we save the distance from the light source to the current pixel and package it in the 8-bit RGBA format.

 void main() { float D = distance( v_WorldPosition, u_LightPosition.xyz ); out_FragColor = Pack( D / 512.0 ); } vec4 Pack(float Value) { const vec4 BitSh = vec4( 256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0); const vec4 BitMsk = vec4( 0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 ); vec4 Comp = fract( Value * BitSh ); Comp -= Comp.xxyz * BitMsk; return Comp; } 


When drawing into separate regions of the atlas, you just need to set the corresponding viewport and scissor. To divide an arbitrary atlas into N identical regions (we do not always need exactly 6), we use the following code:

  LRectDivider( int Size, int NumSubRects ) : FSize( Size ) , FNumSubRects( NumSubRects ) , FCurrentX( 0 ) , FCurrentY( 0 ) { float Sqrt = sqrt( float( FNumSubRects ) ); FNumSlotsWidth = ( int )ceil( Sqrt ); FNumSlotsHeight = ( int )Sqrt; FSlotWidth = FSize / FNumSlotsWidth; FSlotHeight = FSize / FNumSlotsHeight; } void GetNextRect( int* X, int* Y, int* W, int* H ) { if ( X ) { *X = FCurrentX * FSlotWidth; } if ( Y ) { *Y = FCurrentY * FSlotHeight; } if ( W ) { *W = FSlotWidth; } if ( H ) { *H = FSlotHeight; } NextRect(); } private: void NextRect() { if ( ++FCurrentX >= FNumSlotsWidth ) { FCurrentX = 0; FCurrentY++; } } 


Approximately (approximately, because in fact we packed a 32-bit float distance into 4 channels, including alpha), this will look like an atlas for this scene:

image

Now we can draw all this ourselves. In many implementations of VSDCT, an additional cubic map, indirection cubemap, is used, which converts 3D coordinates to 2D coordinates within a texture atlas. It was decided to do without it and convert the coordinates directly in the fragment shader. First we turn to paragraph 8.13 of the Cube Map Texture Selection from the OpenGL 4.4 Core Profile Specification . Table 8.18 tells us what to do with the 3D coordinates:

Major Axis DirectionTargetScTcMa
+ RxPOSITIVE_X-Rz-RyRx
-RxNEGATIVE_XRz-RyRx
+ RyPOSITIVE_YRxRzRy
-RyNEGATIVE_YRx-RzRy
+ RzPOSITIVE_ZRx-RyRz
-RzNEGATIVE_Z-Rx-RyRz


The resulting Sc, Tc and Ma are substituted into these formulas and we get the 2D coordinates s, t:

 s = 0.5 * ( Sc / abs(Ma) + 1 ) t = 0.5 * ( Tc / abs(Ma) + 1 ) 


Here is the shader code for GLSL, which performs all the transformations of the texture coordinates and drives the resulting s and t inside the atlas:

 vec2 GetShadowTC( vec3 Dir ) { float Sc; float Tc; float Ma; float FaceIndex; float rx = Dir.x; float ry = Dir.y; float rz = Dir.z; vec3 adir = abs(Dir); Ma = max( max( adir.x, adir.y ), adir.z ); if ( adir.x > adir.y && adir.x > adir.z ) { Sc = ( rx > 0.0 ) ? rz : -rz; Tc = ry; FaceIndex = ( rx > 0.0 ) ? 0.0 : 1.0; } else if ( adir.y > adir.x && adir.y > adir.z ) { Sc = rx; Tc = ( ry > 0.0 ) ? rz : -rz; FaceIndex = ( ry > 0.0 ) ? 2.0 : 3.0; } else { Sc = ( rz > 0.0 ) ? -rx : rx; Tc = ry; FaceIndex = ( rz > 0.0 ) ? 4.0 : 5.0; } float s = 0.5 * ( Sc / Ma + 1.0 ); float t = 0.5 * ( Tc / Ma + 1.0 ); //    s = s / 3.0; t = t / 2.0; float Flr = floor(FaceIndex / 3.0); float Rmd = FaceIndex - (3.0 * Flr); s += Rmd / 3.0; t += Flr / 2.0; return vec2( s, t ); } 


The very rendering of the shadow in the scene is extremely simple:

 float ComputePointLightShadow() { vec3 LightDirection = v_WorldPosition - u_LightPosition.xyz; vec2 IndirectTC = GetShadowTC( normalize( LightDirection ) ); vec4 Light = texture( Texture7, IndirectTC ); float LightD = Unpack( Light ) * 512.0; if ( LightD < length( LightDirection ) + u_ShadowDepthBias ) return u_ShadowIntensity; return 1.0; } float Unpack(vec4 Value) { const vec4 BitShifts = vec4( 1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0 ); return dot( Value, BitShifts ); } 


That's all!

This technique has a problem with artifacts on the edges of texture maps. They will be especially noticeable if you apply PCF shadow filtering or the like. To reduce such problems, you can add a black border between the individual textures inside the atlas.

Demo


If you have an Android device with OpenGL ES 3.0 and Android 4.4, then you can try running the application: play.google.com/store/apps/details?id=com.linderdaum.engine.vsdct

Links



Linderdaum engine
ShaderX3: Advanced Rendering with DirectX and OpenGL
VSDCT for omnidirectional shadow mapping
Omnidirectional shadows and VSDCT on OpenGL ES 3
Omni-directional shadow mapping

Source: https://habr.com/ru/post/219327/


All Articles