- PVSM.RU - https://www.pvsm.ru -
Привет! Мой предыдущий пост [1], посвященный программированию графики, был благодушно воспринят сообществом, и я отважился ещё на один. Сегодня я расскажу об алгоритме рендеринга теней Parallel-Split Shadow Mapping (PSSM), с которым я впервые столкнулся, когда возникла рабочая необходимость отображать тени на большом расстоянии от игрока. Тогда я был ограничен набором возможностей Direct3D 10, сейчас я реализовал алгоритм на Direct3D 11 и OpenGL 4.3. Подробнее алгоритм PSSM описывается в GPU Gems 3 [2] как с математической точки зрения, так и с точки зрения реализации на Direct3D 9 и 10. За подробностями прошу под кат.
Демку можно найти здесь [3]. Проект называется Demo_PSSM. Для сборки вам понадобятся Visual Studio 2012/2013 и CMake [4].
Оригинальный алгоритм теневых карт (shadow mapping [5]) придумали уже достаточно давно. Принцип работы его заключается в следующем:
Shadow mapping на сегодняшний день является, наверное, самым распространенным алгоритмом для рендеринга динамических теней. Реализацию той или иной модификации алгоритма можно найти практически в любом графическом движке. Главным достоинством этого алгоритма является то, что он обеспечивает быстрое формирование теней от сколь угодно геометрически сложных объектов. Вместе с тем, существование широкого спектра вариаций алгоритма объясняется во многом его недостатками, которые могут приводить к очень неприятным графическим артефактам. Проблемы, характерные для PPSM, и пути их преодоления будут рассмотрены ниже.
Рассмотрим следующую задачу: необходимо рисовать динамические тени от объектов, находящихся на значительном удалении от игрока без ущерба для теней от близко расположенных объектов. Ограничимся направленным солнечным светом.
Задача такого рода может быть особенно актуальна в outdoor-играх, где в некоторых ситуациях игрок может видеть ландшафт на сотни метров перед собой. При этом, чем дальше мы хотим видеть тень, тем большее пространство должно попадать в теневую карту. Чтобы сохранить должное разрешение объектов в теневой карте, мы вынуждены увеличивать разрешение самой карты, что сначала приводит к снижению производительности, затем мы упираемся в ограничение на максимальный размер render target’а. В итоге, балансируя между производительностью и качеством тени, мы получим тени с хорошо заметным эффектом алиасинга, который плохо маскируется даже размытием. Понятно, что такое решение нас не может удовлетворить.
Для решения данной проблемы мы можем придумать такую матрицу проекции, чтобы близко расположенные к игроку объекты получали в карте теней площадь больше, чем объекты, которые расположены далеко. В этом заключается основная идея алгоритма Perspective Shadow Mapping (PSM) и ряда других алгоритмов. Главным преимуществом такого подхода является тот факт, что мы практически не изменили процесс рендеринга сцены, изменился лишь способ расчёта матрицы вида-проекции. Такой подход может быть легко встроен в существующую игру или движок без необходимости серьезных доработок последних. Главный недостаток такого рода подходов – граничные условия. Представим себе ситуацию, что мы рисуем тени от Солнца на закате. Когда Солнце приближается к горизонту, объекты в теневой карте начинают сильно перекрывать друг друга. В этом случае нетипичная проекционная матрица может усугубить ситуацию. Иными словами, алгоритмы класса PSM неплохо работают в определённых ситуациях, например, когда в игре рисуются тени от «неподвижного Солнца» близкого к зениту.
Принципиально другой подход предлагается в алгоритме PSSM. Некоторым данный алгоритм может быть известен под названием Cascaded Shadow Mapping [12] (CSM). Формально, это разные алгоритмы, я бы даже сказал, что PSSM является частным случаем CSM. В этом алгоритме предлагается разделить пирамиду видимости (frustum) основной камеры на сегменты. В случае PSSM – с границами параллельными ближней и дальней плоскостям отсечения, в случае CSM – вид разделения жестко не регламентирован. Для каждого сегмента (split в терминологии алгоритма) строится своя теневая карта. Пример разделения приведен на рисунке ниже.
На рисунке можно видеть разбиение пирамиды видимости на 3 сегмента. Каждый из сегментов выделен ограничивающим прямоугольником (в трёхмерном пространстве будет параллелепипед, bounding box). Для каждой из этих ограниченных частей пространства будет строиться своя теневая карта. Внимательный читатель обратит внимание, что здесь я использовал выравненные по осям ограничивающие параллелепипеды. Можно использовать и невыравненные, это добавит дополнительную сложность в алгоритм отсечения объектов и несколько изменит способ формирования матрицы вида из позиции источника света. Так как пирамида видимости расширяется, площадь сегментов более близких к камере может быть существенно меньше площади более дальних. При одинаковом разрешении теневых карт это означает большее разрешение для тени от близко расположенных объектов. В упомянутой выше статье в GPU Gems 3 [2] предложена следующая схема для вычисления расстояний разбиения пирамиды видимости:
где i – индекс разбиения, m – количество разбиений, n – расстояние до ближней плоскости отсечения, f – расстояние до дальней плоскости отсечения, λ – коэффициент, определяющий интерполяцию между логарифмической и равномерной шкалой разбиения.
Алгоритм PSSM в реализации на Direct3D 11 и OpenGL имеет много общего. Для реализации алгоритма необходимо подготовить следующее:
В итоге, получим следующий алгоритм формирования матриц вида-проекции для рендеринга карт теней:
void calculateMaxSplitDistances()
{
float nearPlane = m_camera.getInternalCamera().GetNearPlane();
float farPlane = m_camera.getInternalCamera().GetFarPlane();
for (int i = 1; i < m_splitCount; i++)
{
float f = (float)i / (float)m_splitCount;
float l = nearPlane * pow(farPlane / nearPlane, f);
float u = nearPlane + (farPlane - nearPlane) * f;
m_maxSplitDistances[i - 1] = l * m_splitLambda + u * (1.0f - m_splitLambda);
}
m_farPlane = farPlane + m_splitShift;
}
float calculateFurthestPointInCamera(const matrix44& cameraView)
{
bbox3 scenebox;
scenebox.begin_extend();
for (size_t i = 0; i < m_entitiesData.size(); i++)
{
if (m_entitiesData[i].isShadowCaster)
{
bbox3 b = m_entitiesData[i].geometry.lock()->getBoundingBox();
b.transform(m_entitiesData[i].model);
scenebox.extend(b);
}
}
scenebox.end_extend();
float maxZ = m_camera.getInternalCamera().GetNearPlane();
for (int i = 0; i < 8; i++)
{
vector3 corner = scenebox.corner_point(i);
float z = -cameraView.transform_coord(corner).z;
if (z > maxZ) maxZ = z;
}
return std::min(maxZ, m_farPlane);
}
void calculateSplitDistances()
{
// calculate how many shadow maps do we really need
m_currentSplitCount = 1;
if (!m_maxSplitDistances.empty())
{
for (size_t i = 0; i < m_maxSplitDistances.size(); i++)
{
float d = m_maxSplitDistances[i] - m_splitShift;
if (m_furthestPointInCamera >= d) m_currentSplitCount++;
}
}
float nearPlane = m_camera.getInternalCamera().GetNearPlane();
for (int i = 0; i < m_currentSplitCount; i++)
{
float f = (float)i / (float)m_currentSplitCount;
float l = nearPlane * pow(m_furthestPointInCamera / nearPlane, f);
float u = nearPlane + (m_furthestPointInCamera - nearPlane) * f;
m_splitDistances[i] = l * m_splitLambda + u * (1.0f - m_splitLambda);
}
m_splitDistances[0] = nearPlane;
m_splitDistances[m_currentSplitCount] = m_furthestPointInCamera;
}
bbox3 calculateFrustumBox(float nearPlane, float farPlane)
{
vector3 eye = m_camera.getPosition();
vector3 vZ = m_camera.getOrientation().z_direction();
vector3 vX = m_camera.getOrientation().x_direction();
vector3 vY = m_camera.getOrientation().y_direction();
float fov = n_deg2rad(m_camera.getInternalCamera().GetAngleOfView());
float aspect = m_camera.getInternalCamera().GetAspectRatio();
float nearPlaneHeight = n_tan(fov * 0.5f) * nearPlane;
float nearPlaneWidth = nearPlaneHeight * aspect;
float farPlaneHeight = n_tan(fov * 0.5f) * farPlane;
float farPlaneWidth = farPlaneHeight * aspect;
vector3 nearPlaneCenter = eye + vZ * nearPlane;
vector3 farPlaneCenter = eye + vZ * farPlane;
bbox3 box;
box.begin_extend();
box.extend(vector3(nearPlaneCenter - vX * nearPlaneWidth - vY * nearPlaneHeight));
box.extend(vector3(nearPlaneCenter - vX * nearPlaneWidth + vY * nearPlaneHeight));
box.extend(vector3(nearPlaneCenter + vX * nearPlaneWidth + vY * nearPlaneHeight));
box.extend(vector3(nearPlaneCenter + vX * nearPlaneWidth - vY * nearPlaneHeight));
box.extend(vector3(farPlaneCenter - vX * farPlaneWidth - vY * farPlaneHeight));
box.extend(vector3(farPlaneCenter - vX * farPlaneWidth + vY * farPlaneHeight));
box.extend(vector3(farPlaneCenter + vX * farPlaneWidth + vY * farPlaneHeight));
box.extend(vector3(farPlaneCenter + vX * farPlaneWidth - vY * farPlaneHeight));
box.end_extend();
return box;
}
matrix44 calculateShadowViewProjection(const bbox3& frustumBox)
{
const float LIGHT_SOURCE_HEIGHT = 500.0f;
vector3 viewDir = m_camera.getOrientation().z_direction();
vector3 size = frustumBox.size();
vector3 center = frustumBox.center() - viewDir * m_splitShift;
center.y = 0;
auto lightSource = m_lightManager.getLightSource(0);
vector3 lightDir = lightSource.orientation.z_direction();
matrix44 shadowView;
shadowView.pos_component() = center - lightDir * LIGHT_SOURCE_HEIGHT;
shadowView.lookatRh(shadowView.pos_component() + lightDir, lightSource.orientation.y_direction());
shadowView.invert_simple();
matrix44 shadowProj;
float d = std::max(size.x, size.z);
shadowProj.orthoRh(d, d, 0.1f, 2000.0f);
return shadowView * shadowProj;
}
Отсечение объектов реализуем при помощи простого теста на пересечение двух ограничивающих параллелепипедов (объекта и сегмента пирамиды видимости). Здесь есть одна особенность, которую важно учесть. Мы можем не видеть объект, но видеть тень от него. Нетрудно догадаться, что при описанном выше подходе мы отсечём все объекты, которые не видны в основной камере, и теней от них не будет. Чтобы этого не происходило, я использовал довольно распространенный приём – вытянул ограничивающий параллелепипед объекта вдоль направления распространения света, что дало грубое приближение области пространства, в которой видна тень от объекта. В итоге, для каждого объекта был сформирован массив индексов теневых карт, в которые необходимо рисовать этот объект.
void updateShadowVisibilityMask(const bbox3& frustumBox, const std::shared_ptr<framework::Geometry3D>& entity, EntityData& entityData, int splitIndex)
{
bbox3 b = entity->getBoundingBox();
b.transform(entityData.model);
// shadow box computation
auto lightSource = m_lightManager.getLightSource(0);
vector3 lightDir = lightSource.orientation.z_direction();
float shadowBoxL = fabs(lightDir.z) < 1e-5 ? 1000.0f : (b.size().y / -lightDir.z);
bbox3 shadowBox;
shadowBox.begin_extend();
for (int i = 0; i < 8; i++)
{
shadowBox.extend(b.corner_point(i));
shadowBox.extend(b.corner_point(i) + lightDir * shadowBoxL);
}
shadowBox.end_extend();
if (frustumBox.clipstatus(shadowBox) != bbox3::Outside)
{
int i = entityData.shadowInstancesCount;
entityData.shadowIndices[i] = splitIndex;
entityData.shadowInstancesCount++;
}
}
Теперь рассмотрим процесс рендеринга и специфичные для Direct3D 11 и OpenGL 4.3 части.
Для реализации алгоритма на Direct3D 11 нам понадобятся:
D3D11_TEXTURE2D_DESC
есть поле ArraySize
. Таким образом, в коде на C++ у нас не будет ничего похожего на ID3D11Texture2D* array[N]
. С точки зрения Direct3D API массив текстур слабо отличается от единственной текстуры. Важной особенностью при использовании такого массива в шейдере является то, что мы можем определить, в какую именно текстуру в массиве будем рисовать тот или иной объект (семантика SV_RenderTargetArrayIndex
в HLSL). В этом заключается и главное отличие этого подхода от MRT (multiple render targets), при котором один объект рисуется сразу во все заданные текстуры. Для объектов, которые необходимо нарисовать сразу в несколько теневых карт, мы воспользуемся аппаратным инстансингом, позволяющим клонировать объекты на уровне GPU. При этом объект может быть нарисован в одну текстуру в массиве, а его клоны в другие. В картах теней мы будем хранить только значение глубины, поэтому воспользуемся текстурным форматом DXGI_FORMAT_R32_FLOAT
.D3D11_SAMPLER_DESC
зададим следующие параметры:
samplerDesc.Filter = D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
samplerDesc.ComparisonFunc = D3D11_COMPARISON_LESS;
samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER;
samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER;
samplerDesc.BorderColor[0] = 1.0f;
samplerDesc.BorderColor[1] = 1.0f;
samplerDesc.BorderColor[2] = 1.0f;
samplerDesc.BorderColor[3] = 1.0f;
Таким образом, у нас будет билинейная фильтрация, сравнение функцией «меньше», а выборка из текстуры по координатам вне диапазона [0;1] вернет 1 (т.е. отсутствие тени).
Рендеринг будем осуществлять по следующей схеме:
FLT_MAX
.DrawIndexedInstanced
. Шейдеры на HLSL для формирования карт теней приведены ниже.
#include <common.h.hlsl>
struct VS_OUTPUT
{
float4 position : SV_POSITION;
float depth : TEXCOORD0;
uint instanceID : SV_InstanceID;
};
VS_OUTPUT main(VS_INPUT input, unsigned int instanceID : SV_InstanceID)
{
VS_OUTPUT output;
float4 pos = mul(float4(input.position, 1), model);
output.position = mul(pos, shadowViewProjection[shadowIndices[instanceID]]);
output.depth = output.position.z;
output.instanceID = instanceID;
return output;
}
#include <common.h.hlsl>
struct GS_INPUT
{
float4 position : SV_POSITION;
float depth : TEXCOORD0;
uint instanceID : SV_InstanceID;
};
struct GS_OUTPUT
{
float4 position : SV_POSITION;
float depth : TEXCOORD0;
uint index : SV_RenderTargetArrayIndex;
};
[maxvertexcount(3)]
void main(triangle GS_INPUT pnt[3], inout TriangleStream<GS_OUTPUT> triStream)
{
GS_OUTPUT p = (GS_OUTPUT)pnt[0];
p.index = shadowIndices[pnt[0].instanceID];
triStream.Append(p);
p = (GS_OUTPUT)pnt[1];
p.index = shadowIndices[pnt[1].instanceID];
triStream.Append(p);
p = (GS_OUTPUT)pnt[2];
p.index = shadowIndices[pnt[2].instanceID];
triStream.Append(p);
triStream.RestartStrip();
}
struct PS_INPUT
{
float4 position : SV_POSITION;
float depth : TEXCOORD0;
uint index : SV_RenderTargetArrayIndex;
};
float main(PS_INPUT input) : SV_TARGET
{
return input.depth;
}
В результате, массив карт теней будет выглядеть примерно так.
float3 getShadowCoords(int splitIndex, float3 worldPos)
{
float4 coords = mul(float4(worldPos, 1), shadowViewProjection[splitIndex]);
coords.xy = (coords.xy / coords.ww) * float2(0.5, -0.5) + float2(0.5, 0.5);
return coords.xyz;
}
float sampleShadowMap(int index, float3 coords, float bias)
{
if (coords.x < 0 || coords.x > 1 || coords.y < 0 || coords.y > 1) return 1.0f;
float3 uv = float3(coords.xy, index);
float receiver = coords.z;
float sum = 0.0;
const int FILTER_SIZE = 3;
const float HALF_FILTER_SIZE = 0.5 * float(FILTER_SIZE);
for (int i = 0; i < FILTER_SIZE; i++)
{
for (int j = 0; j < FILTER_SIZE; j++)
{
float3 offset = float3(shadowBlurStep * (float2(i, j) - HALF_FILTER_SIZE) / HALF_FILTER_SIZE, 0);
sum += shadowMap.SampleCmpLevelZero(shadowMapSampler, uv + offset, receiver - bias);
}
}
return sum / (FILTER_SIZE * FILTER_SIZE);
}
float shadow(float3 worldPos)
{
float shadowValue = 0;
[unroll(MAX_SPLITS)]
for (int i = 0; i < splitsCount; i++)
{
float3 coords = getShadowCoords(i, worldPos);
shadowValue += (1.0 - sampleShadowMap(i, coords, SHADOW_BIASES[i]));
}
return 1.0 - saturate(shadowValue);
}
Наконец, необходимо учесть тот факт, что помимо теней существует еще затенение от алгоритма освещения (я использовал модель освещения Блинна-Фонга [14]). Чтобы не происходило двойного затенения, я добавил в пиксельный шейдер следующий код.
float shadowValue = shadow(input.worldPos);
shadowValue = lerp(1, shadowValue, ndol);
Здесь преимущество отдается модели освещения, т.е. там, где темно согласно модели Блинна-Фонга, тень дополнительно накладываться не будет. Решение не претендует на идеальность, но проблему в некоторой степени устраняет.
В результате, мы получим следующую картинку.
Для реализации алгоритма на OpenGL 4.3 нам понадобится все то же самое, что и для Direct3D 11, однако есть тонкости. В OpenGL мы можем делать совмещённую со сравнением выборку только для текстур, содержащих значение глубины (например, в формате GL_DEPTH_COMPONENT32F
). Следовательно, рендеринг мы будем осуществлять только в буфер глубины, а запись в цвет уберём (точнее, привяжем к framebuffer’у только массив текстур для хранения буфера глубины). Это, с одной стороны, немного сэкономит нам видеопамять и облегчит графический конвейер, с другой, вынудит работать с нормализованными значениями глубины.
Параметры выборки в OpenGL можно привязать прямо к текстуре. Они будут идентичны тем, что были рассмотрены ранее для Direct3D 11.
const float BORDER_COLOR[] = { 1.0f, 1.0f, 1.0f, 1.0f };
glBindTexture(m_shadowMap->getTargetType(), m_shadowMap->getDepthBuffer());
glTexParameteri(m_shadowMap->getTargetType(), GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(m_shadowMap->getTargetType(), GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(m_shadowMap->getTargetType(), GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(m_shadowMap->getTargetType(), GL_TEXTURE_COMPARE_FUNC, GL_LESS);
glTexParameteri(m_shadowMap->getTargetType(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(m_shadowMap->getTargetType(), GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameterfv(m_shadowMap->getTargetType(), GL_TEXTURE_BORDER_COLOR, BORDER_COLOR);
glBindTexture(m_shadowMap->getTargetType(), 0);
Интересен процесс создания массива текстур, который внутри OpenGL представлен трёхмерной текстурой. Для его создания не сделали специальной функции, и то и другое создается при помощи glTexStorage3D
. Аналогом SV_RenderTargetArrayIndex
в GLSL является встроенная переменная gl_Layer
.
Схема рендеринга также осталась прежней:
#version 430 core
const int MAX_SPLITS = 4;
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 uv0;
layout(location = 3) in vec3 tangent;
layout(location = 4) in vec3 binormal;
out VS_OUTPUT
{
float depth;
flat int instanceID;
} vsoutput;
uniform mat4 modelMatrix;
uniform mat4 shadowViewProjection[MAX_SPLITS];
uniform int shadowIndices[MAX_SPLITS];
void main()
{
vec4 wpos = modelMatrix * vec4(position, 1);
vec4 pos = shadowViewProjection[shadowIndices[gl_InstanceID]] * vec4(wpos.xyz, 1);
gl_Position = pos;
vsoutput.depth = pos.z;
vsoutput.instanceID = gl_InstanceID;
}
#version 430 core
const int MAX_SPLITS = 4;
layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;
in VS_OUTPUT
{
float depth;
flat int instanceID;
} gsinput[];
uniform int shadowIndices[MAX_SPLITS];
void main()
{
for (int i = 0; i < gl_in.length(); i++)
{
gl_Position = gl_in[i].gl_Position;
gl_Layer = shadowIndices[gsinput[i].instanceID];
EmitVertex();
}
EndPrimitive();
}
coords.z
в коде) в диапазон [0;1].
vec3 getShadowCoords(int splitIndex, vec3 worldPos)
{
vec4 coords = shadowViewProjection[splitIndex] * vec4(worldPos, 1);
coords.xyz = (coords.xyz / coords.w) * vec3(0.5) + vec3(0.5);
return coords.xyz;
}
В результате получим слабо отличающуюся от Direct3D 11 картину (ради интереса покажу с другого ракурса).
Проблем у алгоритма shadow mapping и его модификаций много. Зачастую алгоритм приходится тщательно настраивать под конкретную игру или даже конкретную сцену. Список наиболее частых проблем и путей их решения можно найти здесь [15]. При реализации PSSM я столкнулся со следующим:
Данный артефакт иногда достаточно сложно заметить, особенно на объектах сложной формы, но он почти всегда есть.
Замеры производительности велись на компьютере следующей конфигурации: AMD Phenom II X4 970 3.79GHz, 16Gb RAM, AMD Radeon HD 7700 Series, под управлением ОС Windows 8.1.
Среднее время кадра. Direct3D 11 / 1920x1080 / MSAA 8x / полный экран / маленькая сцена (~12к полигонов в кадре, ~20 объектов)
Кол-во разбиений / Размер карты теней (N x N пикселей) | 1024 | 2048 | 4096 |
---|---|---|---|
2 | 4.5546ms | 5.07555ms | 7.1661ms |
3 | 5.50837ms | 6.18023ms | 9.75103ms |
4 | 6.00958ms | 7.23269ms | 12.1952ms |
Среднее время кадра. OpenGL 4.3 / 1920x1080 / MSAA 8x / полный экран / маленькая сцена (~12к полигонов в кадре, ~20 объектов)
Кол-во разбиений / Размер карты теней (N x N пикселей) | 1024 | 2048 | 4096 |
---|---|---|---|
2 | 3.2095ms | 4.05457ms | 6.06558ms |
3 | 3.9968ms | 4.87389ms | 8.65781ms |
4 | 4.68831ms | 5.93709ms | 10.4345ms |
Среднее время кадра. 4 разбиения / 1920x1080 / MSAA 8x / полный экран / большая сцена (~1000к полигонов в кадре, ~1000 объектов, ~500 инстансов объектов)
API/ Размер карты теней (N x N пикселей) | 1024 | 2048 | 4096 |
---|---|---|---|
Direct3D 11 | 29.2031ms | 33.3434ms | 40.5429ms |
OpenGL 4.3 | 21.0032ms | 26.4095ms | 41.8098ms |
Результаты показали, что на больших и малых сценах реализация на OpenGL 4.3 работает, в целом, быстрее. С увеличением нагрузки на графический конвейер (увеличение количества объектов и их инстансов, увеличение размера карт теней) разница по скорости работы между реализациями сокращается. Преимущество реализации на OpenGL я связываю с отличным от Direct3D 11 способом формирования карты теней (мы использовали только буфер глубины без записи в цвет). Ничего нам не мешает сделать то же самое на Direct3D 11, смирившись при этом с использованием нормализованных значений глубины. Однако такой подход будет работать только до тех пор, пока мы не захотим хранить в карте теней какие дополнительные данные или функцию от значения глубины вместо значения глубины. И некоторые улучшения алгоритма (например, Variance Shadow Mapping [16]) окажутся для нас сложно реализуемыми.
Алгоритм PSSM является одним их самых удачных способов для создания теней на больших открытых пространствах. В его основе лежит простой и понятный принцип разбиения, который можно легко масштабировать, увеличивая или уменьшая качество теней. Данный алгоритм можно объединять с другими алгоритмами shadow mapping для получения более красивых мягких теней или физически более правильных. Вместе с тем, алгоритмы класса shadow mapping часто приводят к появлению неприятных графических артефактов, которые необходимо устранять путем тонкой настройки алгоритма под конкретную игру.
Автор: rokuz
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/algoritmy/62824
Ссылки в тексте:
[1] предыдущий пост: http://habrahabr.ru/post/224003/
[2] GPU Gems 3: http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html
[3] здесь: https://github.com/rokuz/GraphicsDemo
[4] CMake: http://www.cmake.org/
[5] shadow mapping: http://en.wikipedia.org/wiki/Shadow_mapping
[6] ортографическую проекцию: http://en.wikipedia.org/wiki/Orthographic_projection
[7] перспективная проекционная матрица: http://www.scratchapixel.com/lessons/3d-advanced-lessons/perspective-and-orthographic-projection-matrix/perspective-projection-matrix
[8] старая статья: http://http.developer.nvidia.com/GPUGems/gpugems_ch12.html
[9] LiSPSM: http://www.cg.tuwien.ac.at/~scherzer/files/papers/LispSM_survey.pdf
[10] TSM: http://www.comp.nus.edu.sg/~tants/tsm.html
[11] PSM: http://www-sop.inria.fr/reves/Marc.Stamminger/psm/
[12] Cascaded Shadow Mapping: http://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf
[13] Percentage Closer Filtering: http://http.developer.nvidia.com/GPUGems/gpugems_ch11.html
[14] Блинна-Фонга: http://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_shading_model
[15] здесь: http://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx
[16] Variance Shadow Mapping: http://steps3d.narod.ru/tutorials/vsm-tutorial.html
[17] Источник: http://habrahabr.ru/post/226421/
Нажмите здесь для печати.