Pull to refresh

VSDCT на OpenGL ES 3

Reading time 4 min
Views 9.5K
Давно хотел сделать демку VSDCT на мобильном телефоне. VSDCT (Virtual Shadow Depth Cubemap Texture) это представление cubemap текстуры, когда вместо 6 отдельных граней используется одна обычная 2D текстура-атлас, в которой исходные грани кубической карты помещены в виде плотно упакованных тайлов. Посмотрим, как сделать тени от точечного источника света, использую эту технику.

image



Реализация


Не буду останавливаться на базовом алгоритме omni-directional shadow mapping, в ссылках можно прочитать соответствующие ресурсы. Мы будем использовать 6 линейных проекций и сразу перейдём к реализации VSDCT.

Разберёмся, что же мы хотим сделать. Надо получить 6 теневых карт, которые покрыли бы все направления видимые из точечного источника освещения. Т.е. по одной карте на каждое из направлений ±X, ±Y, ±Z. Чтобы между картами не было разрывов, мы установим FOV 90 градусов для каждой из проекций:

image

6 model-view матриц строятся таким образом (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(  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] = X.x;
		Matrix[1][0] = X.y;
		Matrix[2][0] = X.z;
		Matrix[3][0] = -X.Dot( Position );
		Matrix[0][1] = Y.x;
		Matrix[1][1] = Y.y;
		Matrix[2][1] = Y.z;
		Matrix[3][1] = -Y.Dot( Position );
		Matrix[0][2] = Z.x;
		Matrix[1][2] = Z.y;
		Matrix[2][2] = Z.z;
		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;
	}


Все проекции одинаковые, перспективные с аспектом 1:1 и углом 90 градусов:

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


При отрисовке каждой из 6 теневых карт мы сохраняем расстояние от источника света до текущего пикселя и упаковываем его в формат 8-бит RGBA.

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;
}


При отрисовке в отдельные регионы атласа надо просто установить соответствующий viewport и scissor. Для разделения произвольного атласа на N одинаковых регионов (нам не всегда нужно именно 6) мы используем вот такой код:

	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++;
		}
	}



Примерно (примерно, потому что на самом деле мы упаковали 32-битное float расстояние в 4 канала, включая альфу) так будет выглядеть атлас для данной сцены:

image

Теперь мы можем всё это сами отрисовать. Во многих реализациях VSDCT применяется дополнительная кубическая карта, indirection cubemap, которая преобразовывает 3D координаты в 2D координаты внутри текстурного атласа. Было решено обойтись без неё и преобразовывать координаты прямо во фрагментном шейдере. Сначала обратимся к параграфу 8.13 Cube Map Texture Selection из OpenGL 4.4 Core Profile Specification. Таблица 8.18 говорит нам, что делать с 3D координатами:

Major Axis Direction Target Sc Tc Ma
+Rx POSITIVE_X -Rz -Ry Rx
-Rx NEGATIVE_X Rz -Ry Rx
+Ry POSITIVE_Y Rx Rz Ry
-Ry NEGATIVE_Y Rx -Rz Ry
+Rz POSITIVE_Z Rx -Ry Rz
-Rz NEGATIVE_Z -Rx -Ry Rz


Полученные Sc, Tc и Ma подставляем в эти формулы и получаем 2D-координаты s,t:

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


Вот код шейдера на GLSL, который вополняет все трансформации текстурных координат и загоняет полученные s и t внутрь атласа:

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 );
}


Сама отрисовка тени в сцене предельно простая:

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 );
}


Вот и всё!

У такой техники есть проблема с артефактами на границах текстурных карт. Они будут особенно заметны, если применять фильтрацию тени PCF или подобную. Чтобы уменьшить такие проблемы, можно добавлять чёрную рамку между отдельными текстурами внутри атласа.

Демка


Если у вас есть андроидный девайс с OpenGL ES 3.0 и Android 4.4, то вы можете попробовать запустить приложение: play.google.com/store/apps/details?id=com.linderdaum.engine.vsdct

Ссылки



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
Tags:
Hubs:
+21
Comments 5
Comments Comments 5

Articles