Как вычислить глубину шейдера пикселя, чтобы сделать круг, нарисованный на точном спрайте, в виде сферы, которая будет пересекаться с другими объектами?
Я пишу шейдер, чтобы отображать сферы на точечных спрайтах, рисуя заштрихованные круги, и вам нужно написать компонент глубины, а также цвет, чтобы сферы рядом друг с другом были правильно пересечены.
Я использую код, похожий на код, написанный Johna Holwerda:
void PS_ShowDepth(VS_OUTPUT input, out float4 color: COLOR0,out float depth : DEPTH)
{
float dist = length (input.uv - float2 (0.5f, 0.5f)); //get the distance form the center of the point-sprite
float alpha = saturate(sign (0.5f - dist));
sphereDepth = cos (dist * 3.14159) * sphereThickness * particleSize; //calculate how thick the sphere should be; sphereThickness is a variable.
depth = saturate (sphereDepth + input.color.w); //input.color.w represents the depth value of the pixel on the point-sprite
color = float4 (depth.xxx ,alpha ); //or anything else you might need in future passes
}
Видео по этой ссылке дает хорошее представление о влиянии, которое я получаю: те сферы, которые нарисованы на точечных спрайтах, пересекаются правильно. Я добавил изображения ниже, чтобы проиллюстрировать это.
Я могу точно рассчитать глубину точечного спрайта. Однако я не уверен, что рассчитает толщину сферы на пиксель, чтобы добавить ее к глубине спрайта, чтобы дать окончательное значение глубины. (Приведенный выше код использует переменную, а не вычисляет ее.)
Я работаю над этим в течение нескольких недель, но не понял это - я уверен, что это просто, но это то, что мой мозг не взмахнул.
Размеры спрайта Direct3D 9 точек вычисляются в пикселях, а мои спрайты имеют несколько размеров - как путем спада из-за расстояния (я реализовал тот же алгоритм, что и старый континент с фиксированной функцией, используемый для вычислений размера точки в моем вершинном шейдере), а также из-за того, что представляет спрайт.
Как я могу перейти от данных, которые у меня есть в пиксельном шейдере (расположение спрайтов, глубины спрайта, исходный радиус мирового пространства, радиус в пикселях на экране, нормализованное расстояние от пикселя, о котором идет речь, от центра спрайта ) до значения глубины? Частичное решение просто размера спрайта по толщине шара в координатах глубины было бы точным - это можно масштабировать по нормализованному расстоянию от центра, чтобы получить толщину сфера в пикселе.
Я использую Direct3D 9 и HLSL с шейдерной моделью 3 в качестве верхнего предела SM.
На снимках
Чтобы продемонстрировать технику и точку, в которой у меня возникают проблемы:
Начните с двух точечных спрайтов, а в пиксельном шейдере нарисуйте круг на каждом, используя clip, чтобы удалить фрагменты вне границы круга:
![enter image description here]()
Один будет изображать над другим, так как в конце концов они являются плоскими поверхностями.
Теперь сделайте шейдер более продвинутым и нарисуйте круг, как если бы он был сферой, с освещением. Обратите внимание, что даже если плоские спрайты выглядят 3D, они все еще рисуют один полностью перед другим, поскольку это иллюзия: они все еще плоские.
![enter image description here]()
(Это легко, это последний шаг, с которым у меня возникают проблемы, и я спрашиваю, как это сделать.)
Теперь вместо пиксельного шейдера, записывающего только значения цвета, он также должен написать глубину:
void SpherePS (...any parameters...
out float4 oBackBuffer : COLOR0,
out float oDepth : DEPTH0 <- now also writing depth
)
{
Заметим, что теперь сферы пересекаются, когда расстояние между ними меньше их радиусов:
![enter image description here]()
Как вычислить правильное значение глубины для достижения этого последнего шага?
Изменить/Примечания
Несколько человек отметили, что реальная сфера будет искажаться из-за перспективы, которая может быть особенно заметна по краям экрана, и поэтому я должен использовать другую технику. Во-первых, спасибо за указание, что это не обязательно очевидно и хорошо для будущих читателей! Во-вторых, моя цель состоит не в том, чтобы сделать перспективную сферу, а в том, чтобы быстро отображать миллионы точек данных, и визуально я думаю, что сфероподобный объект выглядит лучше, чем плоский спрайт, и лучше показывает пространственное положение. Небольшое искажение или отсутствие искажения не имеет значения. Если вы смотрите демонстрационное видео, вы можете увидеть, как это полезный визуальный инструмент. Я не хочу отображать реальные сетки сферы из-за большого количества треугольников по сравнению с простым аппаратным точечным спрайтом. Я действительно хочу использовать технику точечных спрайтов, и я просто хочу расширить существующую демонстрационную технику, чтобы вычислить правильное значение глубины, которое в демо было передан как переменная без источника для того, как она была получена.
Ответы
Ответ 1
Вчера я придумал решение, которое хорошо работает и дает желаемый результат сферы, нарисованной на спрайте, с правильным значением глубины, которое пересекается с другими объектами и сферами в сцене. Он может быть менее эффективным, чем он должен быть (он вычисляет и проектирует две вершины для спрайта, например) и, вероятно, не полностью корректен математически (требуется сокращение), но он дает визуально хорошие результаты.
Техника
Чтобы записать глубину "сферы", вам нужно вычислить радиус сферы в координатах глубины - т.е. насколько толстая половина сферы. Затем эту сумму можно масштабировать, когда вы пишете каждый пиксель на сфере, насколько далеко от центра вашей сферы.
Чтобы вычислить радиус в координатах глубины:
-
Vertex shader: в непрозрачных координатах сцены отбрасывает луч от глаза через центр сферы (то есть вершину, представляющую точечный спрайт) и добавляет радиус сферы. Это дает вам точку, лежащую на поверхности сферы. Проектируйте как вершину спрайта, так и вашу новую вершину поверхности сферы, и вычислите глубину (z/w) для каждого. Другое значение глубины вам нужно.
-
Pixel Shader:, чтобы нарисовать круг, вы уже рассчитали нормализованное расстояние от центра спрайта, используя clip
, чтобы не рисовать пиксели за пределами круга. Так как он нормирован (0-1), умножьте его на глубину сферы (это значение глубины радиуса, то есть пиксель в центре сферы) и добавьте к глубине самого плоского спрайта. Это дает глубину, наиболее большую в центре сферы, равную 0, и край, следующий за поверхностью сферы. (В зависимости от того, насколько точна она вам нужна, используйте косинус, чтобы получить изогнутую толщину. Я нашел, что линейные дали совершенно прекрасные результаты.)
код
Это не полный код, так как мои эффекты для моей компании, но код здесь переписан из моего фактического файла эффекта, опускающего ненужные/проприетарные вещи, и должен быть достаточно полным, чтобы продемонстрировать технику.
Vertex shader
void SphereVS(float4 vPos // Input vertex,
float fPointRadius, // Radius of circle / sphere in world coords
out float fDXScale, // Result of DirectX algorithm to scale the sprite size
out float fDepth, // Flat sprite depth
out float4 oPos : POSITION0, // Projected sprite position
out float fDiameter : PSIZE, // Sprite size in pixels (DX point sprites are sized in px)
out float fSphereRadiusDepth : TEXCOORDn // Radius of the sphere in depth coords
{
...
// Normal projection
oPos = mul(vPos, g_mWorldViewProj);
// DX depth (of the flat billboarded point sprite)
fDepth = oPos.z / oPos.w;
// Also scale the sprite size - DX specifies a point sprite size in pixels.
// One (old) algorithm is in http://msdn.microsoft.com/en-us/library/windows/desktop/bb147281(v=vs.85).aspx
fDXScale = ...;
fDiameter = fDXScale * fPointRadius;
// Finally, the key: what the depth coord to use for the thickness of the sphere?
fSphereRadiusDepth = CalculateSphereDepth(vPos, fPointRadius, fDepth, fDXScale);
...
}
Все стандартные материалы, но я включаю его, чтобы показать, как он используется.
Ключевой метод и ответ на вопрос:
float CalculateSphereDepth(float4 vPos, float fPointRadius, float fSphereCenterDepth, float fDXScale) {
// Calculate sphere depth. Do this by calculating a point on the
// far side of the sphere, ie cast a ray from the eye, through the
// point sprite vertex (the sphere center) and extend it by the radius
// of the sphere
// The difference in depths between the sphere center and the sphere
// edge is then used to write out sphere 'depth' on the sprite.
float4 vRayDir = vPos - g_vecEyePos;
float fLength = length(vRayDir);
vRayDir = normalize(vRayDir);
fLength = fLength + vPointRadius; // Distance from eye through sphere center to edge of sphere
float4 oSphereEdgePos = g_vecEyePos + (fLength * vRayDir); // Point on the edge of the sphere
oSphereEdgePos.w = 1.0;
oSphereEdgePos = mul(oSphereEdgePos, g_mWorldViewProj); // Project it
// DX depth calculation of the projected sphere-edge point
const float fSphereEdgeDepth = oSphereEdgePos.z / oSphereEdgePos.w;
float fSphereRadiusDepth = fSphereCenterDepth - fSphereEdgeDepth; // Difference between center and edge of sphere
fSphereRadiusDepth *= fDXScale; // Account for sphere scaling
return fSphereRadiusDepth;
}
Пиксельный шейдер
void SpherePS(
...
float fSpriteDepth : TEXCOORD0,
float fSphereRadiusDepth : TEXCOORD1,
out float4 oFragment : COLOR0,
out float fSphereDepth : DEPTH0
)
{
float fCircleDist = ...; // See example code in the question
// 0-1 value from the center of the sprite, use clip to form the sprite into a circle
clip(fCircleDist);
fSphereDepth = fSpriteDepth + (fCircleDist * fSphereRadiusDepth);
// And calculate a pixel color
oFragment = ...; // Add lighting etc here
}
Этот код пропускает освещение и т.д. Чтобы рассчитать, как далеко пиксель находится от центра спрайта (чтобы получить fCircleDist
), посмотрите пример кода в вопросе (вычисляет "float dist = ...
" ), который уже нарисовал круг.
Конечный результат:
Результат
![Intersecting spheres using point sprites]()
Воила, точки рисования сфер.
Примечания
- Алгоритм масштабирования для спрайтов может потребовать, чтобы глубина была
также масштабируется. Я не уверен, что строка верна.
- Это не полностью математически корректно (требуется сокращение)
но, как вы видите, результат визуально правильный.
- При использовании миллионов спрайтов я по-прежнему получаю хорошую скорость рендеринга (< 10ms на кадр для 3 миллионов спрайтов на эмуляторе DirectWD с эмуляцией VMWare Fusion).
Ответ 2
Первая большая ошибка заключается в том, что реальная 3d-область не будет проектироваться в круг под перспективной 3d-проекцией.
Это очень неинтуитивно, но посмотрите на некоторые фотографии, особенно с большим полем зрения и срединными сферами.
Во-вторых, я бы рекомендовал не использовать точечные спрайты в начале, это может сделать вещи сложнее, чем необходимо, особенно с учетом первой точки. Просто нарисуйте щедрый ограничивающий квадрат вокруг вашей сферы и идите оттуда.
В вашем шейдере вы должны иметь место на экране как вход. Из этого, преобразования представления и вашей матрицы проекции вы можете добраться до линии в пространстве глаз. Вам нужно пересечь эту линию со сферой в пространстве глаза (raytracing), получить точку пересечения глазного пространства и преобразовать ее обратно в пространство экрана. Затем выведите 1/w в качестве глубины. Я не делаю математику для вас здесь, потому что я немного пьян и ленив, и я не думаю, что вы действительно хотите все равно. Это отличное упражнение в линейной алгебре, хотя, возможно, вам стоит попробовать.:)
Эффект, который вы, вероятно, пытаетесь сделать, называется Depth Sprites и обычно используется только с орфографической проекцией и глубиной спрайта, хранящегося в текстуре. Просто храните глубину вместе с цветом, например, в альфа-канале, и просто выведите
eye.z + (storeddepth-0,5) * depthofsprite.
Ответ 3
Сфера не будет проектироваться в круг в общем случае. Вот решение.
Этот метод называется spherical billboards
. Подробное описание можно найти в этой статье:
Сферические рекламные щиты и их приложение для визуализации взрывов
Вы нарисовываете точечные спрайты как квадраты, а затем проецируете текстуру глубины, чтобы найти расстояние между значением Z-пикселя и текущей Z-координатой. Расстояние между выбранным Z-значением и током Z влияет на непрозрачность пикселя, чтобы он выглядел как сфера, пересекая основную геометрию. Авторы статьи предлагают следующий код для вычисления непрозрачности:
float Opacity(float3 P, float3 Q, float r, float2 scr)
{
float alpha = 0;
float d = length(P.xy - Q.xy);
if(d < r) {
float w = sqrt(r*r - d*d);
float F = P.z - w;
float B = P.z + w;
float Zs = tex2D(Depth, scr);
float ds = min(Zs, B) - max(f, F);
alpha = 1 - exp(-tau * (1-d/r) * ds);
}
return alpha;
}
Это предотвратит резкие перекрестки ваших рекламных щитов с геометрией сцены.
В случае, если конвейер с острием-спрайтами трудно контролировать (я могу сказать только об OpenGL, а не DirectX), лучше использовать биллинг с ускорением GPU: вы поставляете 4 равных трехмерных вершины, которые соответствуют центру частицы. Затем вы перемещаете их в соответствующие углы рекламного щита в вершинном шейдере, то есть:
if ( idx == 0 ) ParticlePos += (-X - Y);
if ( idx == 1 ) ParticlePos += (+X - Y);
if ( idx == 2 ) ParticlePos += (+X + Y);
if ( idx == 3 ) ParticlePos += (-X + Y);
Это больше ориентировано на современный конвейер GPU, а грубая работа будет работать с любой невырожденной перспективной проекцией.