При преобразовании текстур (нарисованных как плоские 3D-объекты) для имитации глубины черные линии появляются случайным образом

Мы разрабатываем нисходящую RPG с использованием XNA. Недавно мы столкнулись с неудачей при написании кода для отображения наших карт. При рисовании карты сверху вниз с нормальной матрицей трансформации вроде все нормально. При использовании неплоской матрицы преобразования например, сжимая верх или низ до имитации глубины, появляются черные линии (строки, когда сверху или снизу, столбец, когда сжимается слева или справа), которые перемещаются при изменении положения камеры. Движение и размещение кажутся случайными. (Изображение предоставлено ниже.)

Справочная информация

Карты состоят из плиток. Исходная текстура имеет тайлы размером 32x32 пикселя. Мы рисуем плитки, создавая 2 треугольника и отображая часть исходной текстуры на этих треугольниках. Шейдер делает это за нас. Есть три слоя треугольников. Сначала мы рисуем все непрозрачные плитки и все непрозрачные пиксели всех полупрозрачных и частично прозрачных плиток, затем все полупрозрачные и частично прозрачные плитки и пиксели. Это отлично работает (но когда мы увеличиваем масштаб с плавающей запятой, иногда между строками плитки и / или столбцами находятся смешанные по цвету линии).

Renderstates

Мы используем одно и то же rasterizerState для всех плиток и переключаемся между двумя при рисовании сплошных или полупрозрачных плиток.

_rasterizerState = new RasterizerState();
_rasterizerState.CullMode = CullMode.CullCounterClockwiseFace;

_solidDepthState = new DepthStencilState();
_solidDepthState.DepthBufferEnable = true;
_solidDepthState.DepthBufferWriteEnable = true;

_alphaDepthState = new DepthStencilState();
_alphaDepthState.DepthBufferEnable = true;
_alphaDepthState.DepthBufferWriteEnable = false;

В тени мы устанавливаем SpriteBlendMode следующим образом:

Первый твердый слой 1 использует

AlphaBlendEnable = False; 
SrcBlend = One; 
DestBlend = Zero; 

Все остальные твердые и прозрачные слои (нарисованные позже) используют

AlphaBlendEnable = True; 
SrcBlend = SrcAlpha;
DestBlend = InvSrcAlpha; 

Другие шейдеры тоже используют это. В SpriteBatch для SpriteFonts используются настройки по умолчанию.

Сгенерированная текстура

Некоторые плитки создаются на лету и сохраняются в файл. Файл загружается при загрузке карты. Это делается с помощью RenderTarget , созданного следующим образом:

RenderTarget2D rt = new RenderTarget2D(sb.GraphicsDevice, 768, 1792, false, 
    SurfaceFormat.Color, DepthFormat.None);
    sb.GraphicsDevice.SetRenderTarget(rt);

После создания файл сохраняется и загружается (поэтому мы не потеряем его при перезагрузке устройства, потому что его больше не будет в RenderTarget ). Я пробовал использовать mipmapping, но это спрайт-лист. Нет информации о том, где размещены плитки, поэтому MIP-отображение бесполезно и не решило проблему.

Vertices

Мы перебираем каждую позицию. Здесь пока нет плавающих точек, но позиция - это Vector3 (Float3).

for (UInt16 x = 0; x < _width;  x++)
{
    for (UInt16 y = 0; y < _heigth; y++)
    {
        [...]
        position.z = priority; // this is a byte 0-5

Для позиционирования тайлов используется следующий код:

tilePosition.X = position.X;
tilePosition.Y = position.Y + position.Z;
tilePosition.Z = position.Z;

Как вы знаете, числа с плавающей запятой 32-битные, с 24-битными для точности. Максимальное битовое значение z составляет 8 бит (5 = 00000101). Максимальные значения для X и Y - 16 бит соответственно. 24 бита. Я предполагал, что с плавающей точкой ничего не может пойти не так.

this.Position = tilePosition;

Когда вершины установлены, это происходит следующим образом (так что все они имеют одинаковую позицию тайла)

Vector3[] offsets  = new Vector3[] { Vector3.Zero, Vector3.Right, 
    Vector3.Right + (this.IsVertical ? Vector3.Forward : Vector3.Up), 
    (this.IsVertical ? Vector3.Forward : Vector3.Up) };
Vector2[] texOffset = new Vector2[] { Vector2.Zero, Vector2.UnitX, 
    Vector2.One, Vector2.UnitY };

for (int i = 0; i < 4; i++)
{
    SetVertex(out arr[start + i]);
    arr[start + i].vertexPosition = Position + offsets[i];

    if (this.Tiles[0] != null)
        arr[start + i].texturePos1 += texOffset[i] * this.Tiles[0].TextureWidth;
    if (this.Tiles[1] != null)
        arr[start + i].texturePos2 += texOffset[i] * this.Tiles[1].TextureWidth;
    if (this.Tiles[2] != null)
        arr[start + i].texturePos3 += texOffset[i] * this.Tiles[2].TextureWidth;
}

Шейдер

Шейдер может рисовать анимированные тайлы и статическая плитка. Оба используют следующее состояние сэмплера:

sampler2D staticTilesSampler = sampler_state { 
    texture = <staticTiles> ; magfilter = POINT; minfilter = POINT; 
    mipfilter = POINT; AddressU = clamp; AddressV = clamp;};

Шейдер не устанавливает никаких других состояний сэмплера, мы также не устанавливаем его в нашем коде.

Каждый проход мы обрезаем значение альфа (чтобы не получить черные пиксели), используя следующую строку:

clip(color.a - alpha)

Альфа равна 1 для твердого слоя 1 и почти 0 для любого другого слоя. . Это означает, что если есть доля альфы, она будет отрисована, за исключением нижнего слоя (потому что мы не знаем, что с ним делать).

Камера

Мы используем камеру, чтобы имитировать поиск из сверху вниз на плитках, заставляя их казаться плоскими, используя значение z для наложения их на внешние данные наложения (3 слоя не всегда расположены в правильном порядке). Это тоже отлично работает. Камера обновляет матрицу преобразования. Если вам интересно, почему у него такая странная структура, как эта: AddChange - код с двойной буферизацией (это тоже работает). Матрица преобразования формируется следующим образом:

// First get the position we will be looking at. Zoom is normally 32
Single x = (Single)Math.Round((newPosition.X + newShakeOffset.X) * 
    this.Zoom) / this.Zoom;
Single y = (Single)Math.Round((newPosition.Y + newShakeOffset.Y) * 
    this.Zoom) / this.Zoom;

// Translation
Matrix translation = Matrix.CreateTranslation(-x, -y, 0);

// Projection
Matrix obliqueProjection = new Matrix(1, 0, 0, 0,
                                      0, 1, 1, 0,
                                      0, -1, 0, 0,
                                      0, 0, 0, 1);

Matrix taper = Matrix.Identity; 

// Base it of center screen
Matrix orthographic = Matrix.CreateOrthographicOffCenter(
    -_resolution.X / this.Zoom / 2, 
     _resolution.X / this.Zoom / 2, 
     _resolution.Y / this.Zoom / 2, 
    -_resolution.Y / this.Zoom / 2, 
    -10000, 10000);

// Shake rotation. This works fine       
Matrix shakeRotation = Matrix.CreateRotationZ(
    newShakeOffset.Z > 0.01 ? newShakeOffset.Z / 20 : 0);

// Projection is used in Draw/Render
this.AddChange(() => { 
    this.Projection = translation * obliqueProjection * 
    orthographic * taper * shakeRotation; }); 

Обоснование и поток

Имеется 3 уровня данных тайла. Каждая плитка определяется IsSemiTransparent . Когда тайл имеет вид IsSemiTransparent , его нужно рисовать после чего-то не IsSemiTransparent . Данные мозаики складываются при загрузке в экземпляр SplattedTile . Таким образом, даже если уровень один из данных тайлов пуст, слой один из SplattedTile будет иметь данные тайлов в первом слое (при условии, что по крайней мере один слой имеет данные тайлов). Причина в том, что Z-буфер не знает, с чем смешивать, если они нарисованы по порядку, так как за ним могут не быть сплошных пикселей.

Слои НЕ имеют значения z, в отличие от данных отдельных элементов мозаичного изображения. Когда это тайл земли, он имеет приоритет = 0 . Таким образом, плитки с одинаковым приоритетом мы упорядочиваем по слою (порядок рисования) и непрозрачности (полупрозрачный, затем непрозрачный). Плитки с разным приоритетом будут отображаться в соответствии с их приоритетом.

Первый сплошной слой не имеет целевых пикселей, поэтому я установил для него значение DestinationBlend.Zero . Также не требуется AlphaBlending , так как нет ничего, с чем можно было бы использовать альфа-смешение. Другие слои (5, 2 сплошных, 3 прозрачных) могут быть нарисованы, когда уже есть данные о цвете и их необходимо соответствующим образом смешать.

Перед повторением через 6 проходов устанавливается матрица проекции . При использовании без конуса это работает. При использовании конуса это не так.

Проблема

Мы хотим имитировать некоторую глубину, применяя конус, используя некоторую матрицу. Мы пробовали несколько значений, но это пример:

new Matrix(1, 0, 0, 0,
           0, 1, 0, 0.1f,
           0, 0, 1, 0,
           0, 0, 0, 1);

Экран (все со значением высоты 0, все плоские) будет сжат. Чем ниже y (выше на экране), тем сильнее он сжат. Это действительно работает, но теперь почти везде появляются случайные черные линии. Кажется, что исключает несколько плиток, но я не вижу, какова корреляция. Мы думаем, что это могло иметь какое-то отношение к интерполяции или MIP-картам.

А вот изображение, чтобы показать вам, о чем я говорю: Screenshot with lines.

Не затронутые плитки кажутся статическими плитками НЕ на нижнем слое. Однако прозрачные плитки поверх них показывают другие графические артефакты. Они пропускают строки (поэтому строки просто удаляются). Я отметил этот текст, потому что считаю, что это намек на то, что происходит. вертикальные линии появляются, если я помещаю mip mag и minfilter в Linear .

Вот изображение, увеличенное (в увеличении игры), показывающее артефакт на плитках на слое 2 или 3

  • Удаление клипа (непрозрачность) в шейдере . Это удаляет только некоторые строки. Мы исследуем это дальше.
  • Ищем ту же проблему на msdn, stackoverflow и с помощью google (безуспешно).
  • Кто-нибудь узнал об этой проблеме? В заключение, мы вызываем SpriteBatch ПОСЛЕ рисования тайлов и используем другой шейдер для аватарок (не показывать проблем, потому что у них высота> 0). Отменяет ли это состояние сэмплера ? Или ...?

    50
    задан Community 8 February 2017 в 04:32
    поделиться