Пример. Масштабирование приложений на разных устройствах с разными возможностями

В этом примере описывается, как приложение Windows Mixed Reality может ориентироваться на различные платформы с разными аппаратными возможностями. Datascape — это Windows Mixed Reality приложение, которое отображает данные о погоде поверх данных о местности. Приложение окружает пользователей голографическими визуализациями данных. Пользователи могут изучать уникальные аналитические сведения, которые они получают, обнаруживая данные в смешанной реальности.

Приложение Datascape предназначено для Microsoft HoloLens, Windows Mixed Reality иммерсивных гарнитур, компьютеров с низким энергопотреблением и высокопроизводительных компьютеров. Main задачей было отрисовка визуально привлекательной сцены, выполняющейся с высокой частотой кадров, на устройствах с совершенно разными возможностями производительности.

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

Дополнительные сведения о рекомендациях по производительности для смешанной реальности и приложений Unity см. в разделе:

Обзор примера

Ниже приведены некоторые сведения о приложении Datascape и проблемах.

Прозрачность и перерисовка

Наши main рендеринга борются с прозрачностью, так как прозрачность может быть дорогостоящей.

Вы можете отрисовать сплошную геометрию спереди к спине при записи в буфер глубины, что останавливает отрисовку любых будущих пикселей, расположенных за этим пикселем. Эта операция предотвращает выполнение шейдера пикселей скрытыми пикселями и значительно ускоряет отрисовку. При оптимальной сортировке геометрии каждый пиксель на экране рисуется только один раз.

Прозрачная геометрия должна быть отсортирована по переднему краю и зависит от смешивания выходных данных шейдера пикселей с текущим пикселем на экране. Этот процесс может привести к тому, что каждый пиксель на экране рисуется несколько раз на кадр, что называется overdraw.

Для HoloLens и основных компьютеров можно заполнить экран только несколько раз, что делает прозрачную отрисовку проблематичной.

Компоненты сцены datascape

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

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

Для карты мы использовали пользовательский шейдер, который исключил стандартные функции Unity, такие как тени и сложное освещение. Пользовательский шейдер заменил эти функции простой моделью однолучного освещения солнца и пользовательским вычислением тумана. Этот простой пиксельный шейдер повысил производительность.

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

Облачные данные

Облачные данные, скачанные с серверов NOAA на трех разных двухD-уровнях. Каждый слой имел верхнюю и нижнюю высоту облака и плотность облака для каждой ячейки сетки. Мы обработали данные в облачную информационную текстуру, которая хранит каждый компонент в красном, зеленом и синем компонентах текстуры.

Создание геометрических облаков

Чтобы обеспечить отрисовку облаков на компьютерах с низким энергопотреблением, наш подход к резервному копированию использовал сплошную геометрию, чтобы свести к минимуму перерисовку.

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

Следующий код шейдера создает вершины:

v2g vert (appdata v)
{
    v2g o;
    o.height = tex2Dlod(_MainTex, float4(v.uv, 0, 0)).x;
    o.vertex = v.vertex;
    return o;
}
 
g2f GetOutput(v2g input, float heightDirection)
{
    g2f ret;
    float4 newBaseVert = input.vertex;
    newBaseVert.y += input.height * heightDirection * _HeigthScale;
    ret.vertex = UnityObjectToClipPos(newBaseVert);
    ret.height = input.height;
    return ret;
}
 
[maxvertexcount(6)]
void geo(triangle v2g p[3], inout TriangleStream<g2f> triStream)
{
    float heightTotal = p[0].height + p[1].height + p[2].height;
    if (heightTotal > 0)
    {
        triStream.Append(GetOutput(p[0], 1));
        triStream.Append(GetOutput(p[1], 1));
        triStream.Append(GetOutput(p[2], 1));
 
        triStream.RestartStrip();
 
        triStream.Append(GetOutput(p[2], -1));
        triStream.Append(GetOutput(p[1], -1));
        triStream.Append(GetOutput(p[0], -1));
    }
}
fixed4 frag (g2f i) : SV_Target
{
    clip(i.height - 0.1f);
 
    float3 finalColor = lerp(_LowColor, _HighColor, i.height);
    return float4(finalColor, 1);
}

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

Так как облака являются сплошной геометрией, они могут отображаться до отрисовки местности. Скрытие дорогих пикселей карты под облаками еще больше повышает частоту кадров. Из-за подхода к отрисовке сплошной геометрии это решение хорошо работало на всех графических картах, от минимальных до высококлассных графических карт, а также на HoloLens.

Изображение, показывающее геометрические облака.

Использование облаков твердых частиц

Наше решение создало достойное представление облачных данных, но было немного тусклым. Облачная отрисовка не передала объемное ощущение, что мы хотели для наших высококлассных компьютеров. Наш следующий шаг был в том, чтобы создать более органический и объемный вид, представляя облака примерно с 100000 частиц.

Если частицы остаются твердыми и сортируются спереди к спине, вы по-прежнему выигрываете отбраковки буфера глубины позади ранее преобразованных частиц, уменьшая переполнение. Кроме того, решение на основе частиц может изменять количество частиц, чтобы ориентироваться на другое оборудование. Однако все пиксели по-прежнему должны быть проверены на глубину, что приводит к дополнительным издержкам.

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

Вычислительный шейдер отбирал текстуру облачной информации, чтобы расположить каждую частицу на правильной высоте и окрасить ее в зависимости от плотности. Каждая частица содержала как высоту, так и радиус. Высота была основана на облачных данных, полученных из текстуры облачной информации. Радиус был основан на первоначальном распределении, которое вычислило и сохранило горизонтальное расстояние до ближайшего соседа.

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

Схема, показывающая форму и покрытие частиц.

В следующем коде шейдера показано распределение:

ComputeBuffer cloudPointBuffer = new ComputeBuffer(6, quadPointsStride);
cloudPointBuffer.SetData(new[]
{
    new Vector2(-.5f, .5f),
    new Vector2(.5f, .5f),
    new Vector2(.5f, -.5f),
    new Vector2(.5f, -.5f),
    new Vector2(-.5f, -.5f),
    new Vector2(-.5f, .5f)
});
 
StructuredBuffer<float2> quadPoints;
StructuredBuffer<float3> particlePositions;
v2f vert(uint id : SV_VertexID, uint inst : SV_InstanceID)
{
    // Find the center of the quad, from local to world space
    float4 centerPoint = mul(unity_ObjectToWorld, float4(particlePositions[inst], 1));
 
    // Calculate y offset for each quad point
    float3 cameraForward = normalize(centerPoint - _WorldSpaceCameraPos);
    float y = dot(quadPoints[id].xy, cameraForward.xz);
 
    // Read out the particle data
    float radius = ...;
    float height = ...;
 
    // Set the position of the vert
    float4 finalPos = centerPoint + float4(quadPoints[id].x, y * height, quadPoints[id].y, 0) * radius;
    o.pos = mul(UNITY_MATRIX_VP, float4(finalPos.xyz, 1));
    o.uv = quadPoints[id].xy + 0.5;
 
    return o;
}

Мы отсортировали частицы спереди к спине и по-прежнему использовали шейдер сплошного стиля, чтобы обрезать прозрачные пиксели, а не смешивать их. Этот метод обрабатывает большое количество частиц даже на машинах с низким энергопотреблением, избегая дорогостоящей перезарисовки.

Попробуйте прозрачные облака частиц

Твердые частицы обеспечивали органические ощущения к формам облака, но все еще нуждались в чем-то, чтобы захватить пушистость облаков. Мы решили попробовать пользовательское решение для высококлассных графических карт, которое предоставляет прозрачность. Мы просто переключили начальный порядок сортировки частиц и изменили шейдер для использования альфа-текстур.

Изображение, показывающее пушистые облака.

Это решение выглядело отлично, но оказалось слишком тяжелым для даже самых жестких машин. Каждый пиксель должен был отображаться на экране сотни раз.

Отрисовка вне экрана с более низким разрешением

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

В следующем коде показана отрисовка вне экрана:

cloudBlendingCommand = new CommandBuffer();
Camera.main.AddCommandBuffer(whenToComposite, cloudBlendingCommand);
 
cloudCamera.CopyFrom(Camera.main);
cloudCamera.rect = new Rect(0, 0, 1, 1);    //Adaptive rendering can set the main camera to a smaller rect
cloudCamera.clearFlags = CameraClearFlags.Color;
cloudCamera.backgroundColor = new Color(0, 0, 0, 1);
 
currentCloudTexture = RenderTexture.GetTemporary(Camera.main.pixelWidth / 2, Camera.main.pixelHeight / 2, 0);
cloudCamera.targetTexture = currentCloudTexture;
 
// Render clouds to the offscreen buffer
cloudCamera.Render();
cloudCamera.targetTexture = null;
 
// Blend low-res clouds to the main target
cloudBlendingCommand.Blit(currentCloudTexture, new RenderTargetIdentifier(BuiltinRenderTextureType.CurrentActive), blitMaterial);

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

Во-вторых, растяжение буфера ввело артефакты по краям облаков, где изменение разрешения было заметно. В следующих двух разделах описывается, как мы устранили эти проблемы.

Использование буфера глубины частиц

Мы должны были сделать частицы сосуществуют с геометрией мира, где гора или объект покрывали частицы позади него. Таким образом, мы заполнили буфер глубины, который содержал геометрию main сцены. Для создания буфера глубины мы создали вторую камеру, которая отрисовывает только сплошную геометрию и глубину сцены.

Мы использовали новую текстуру в облачном шейдере пикселей для заключения пикселей. Мы использовали ту же текстуру для вычисления расстояния до геометрии за облачным пикселем. Используя это расстояние и применив его к альфа-части пикселя, мы достигли эффекта облаков исчезает, когда они приближаются к местности. Этот эффект удаляет любые жесткие порезы там, где частицы и ландшафт встречаются.

Изображение, показывающее облака, смешанные с ландшафтом.

Заточить края

Растянутые облака выглядели почти идентичны облакам нормального размера в центрах частиц или там, где они перекрывались, но показали некоторые артефакты на краях облака. Острые края оказались размытыми, а движение камеры представило эффекты псевдонимов.

Чтобы решить эту проблему, мы:

  1. Запустите простой шейдер в буфере вне экрана, чтобы определить, где произошли значительные изменения контрастности.
  2. Поместите пиксели с большими изменениями в новый буфер трафарета.
  3. Используется буфер трафарета для маскирования этих областей высокой контрастности при применении буфера вне экрана к экрану, что приводит к отверстиям в облаках и вокруг них.
  4. Отрисовка всех частиц снова в полноэкранном режиме с помощью буфера трафарета для маскировки всего, кроме краев, что привело к минимальному набору пикселей. Так как мы уже создали буфер команд для отрисовки частиц, мы просто снова отрисовали его на новую камеру.

Изображение, показывающее ход отрисовки краев облака.

Конечным результатом были острые края с дешевыми центральных разделов облаков. Хотя это решение выполняется гораздо быстрее, чем отрисовка всех частиц в полноэкранном режиме, проверка пикселей на буфере трафарета по-прежнему стоит. Огромное количество overdraw по-прежнему дорого.

Частицы отвара

Для эффекта ветра мы создали длинные полоски треугольников в вычислительном шейдере, создав много кистей ветра в мире. Эффект ветра не был тяжелым на скорость заполнения, из-за узких полос. Однако многие сотни тысяч вершин вызвали большую нагрузку на вершинный шейдер.

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

В следующем коде показан буфер добавления.

Вычислительный шейдер:

AppendStructuredBuffer<int> culledParticleIdx;
 
if (show)
    culledParticleIdx.Append(id.x);

Код C#:

protected void Awake() 
{
    // Create an append buffer, setting the maximum size and the contents stride length
    culledParticlesIdxBuffer = new ComputeBuffer(ParticleCount, sizeof(int), ComputeBufferType.Append);
 
    // Set up Args Buffer for Draw Procedural Indirect
    argsBuffer = new ComputeBuffer(4, sizeof(int), ComputeBufferType.IndirectArguments);
    argsBuffer.SetData(new int[] { DataVertCount, 0, 0, 0 });
}
 
protected void Update()
{
    // Reset the append buffer, and dispatch the compute shader normally
    culledParticlesIdxBuffer.SetCounterValue(0);
 
    computer.Dispatch(...)
 
    // Copy the append buffer count into the args buffer used by the Draw Procedural Indirect call
    ComputeBuffer.CopyCount(culledParticlesIdxBuffer, argsBuffer, dstOffset: 1);
    ribbonRenderCommand.DrawProceduralIndirect(Matrix4x4.identity, renderMaterial, 0, MeshTopology.Triangles, dataBuffer);
}

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

Другая проблема заключалась в том, что буфер добавления заполнялся в случайном порядке из-за параллелизованного вычисления частиц. Отсортированные частицы стали несортированные, в результате чего мерцают облачные частицы. Существуют методы сортировки буфера отправки, но ограниченный объем повышения производительности от выбраковки частиц, вероятно, будет смещен другой сортировкой. Мы решили не проводить такую оптимизацию для облачных частиц.

Использование адаптивной отрисовки

Чтобы обеспечить стабильную частоту кадров в приложении с различными условиями отрисовки, такими как облачное и четкое представление, мы ввели адаптивную отрисовку.

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

Сравните время отрисовки с требуемой частотой обновления, чтобы показать, насколько близко вы подошли к удалению кадров. Когда вы приближаетесь к удалению кадров, вы можете адаптировать отрисовку, чтобы она была быстрее.

Одним из простых способов адаптации отрисовки является изменение размера окна просмотра экрана, чтобы для отрисовки было меньше пикселей. Система использует UnityEngine.XR.XRSettings.renderViewportScale для сжатия целевого окна просмотра и автоматически растягивает результат в соответствии с экраном. Небольшое изменение масштаба едва заметно в геометрии мира, а коэффициент масштабирования 0,7 требует половины числа пикселей для отрисовки.

Изображение с масштабом 70 % с половиной пикселей.

Когда мы обнаруживаем, что мы собираемся удалить кадры, мы уменьшаем масштаб на фиксированное соотношение и восстанавливаем его, когда мы снова будем работать достаточно быстро.

В этом примере мы решили, какой облачный метод использовать на основе графических возможностей оборудования при запуске. Вы также можете основывать это решение на данных из измерений производительности, чтобы предотвратить длительное пребывание системы с низким разрешением.

Рекомендации

Нацеливание на различные аппаратные возможности является сложной задачей и требует планирования. Вот несколько рекомендаций.

  • Начните ориентироваться на машины с низким энергопотреблением, чтобы ознакомиться с проблемным пространством.
  • Разработайте решение для резервного копирования, которое будет выполняться на всех компьютерах. Затем можно повысить сложность для высококлассных компьютеров или улучшить разрешение решения для резервного копирования.
  • Проектируйте решение с учетом скорости заполнения, так как пиксели являются самым ценным ресурсом.
  • Ориентация сплошной геометрии на прозрачность.
  • Проектируйте в наихудших сценариях и рассмотрите возможность использования адаптивной отрисовки для тяжелых ситуаций.

Об авторах

Фотография Роберта Феррезе Роберт Феррезе
@Microsoft инженера-программного обеспечения
Фотография Дэна Андерссона Дэн Андерссон
@Microsoft инженера-программного обеспечения

См. также раздел