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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

Image that shows geometry clouds.

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

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

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

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

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

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

Diagram that shows particle shape and coverage.

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

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

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

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

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

Image that shows fluffy clouds.

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

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

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

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

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

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

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

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

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

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

Image that shows clouds blended into terrain.

Заточение ребер

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

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

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

Image showing the progression of rendering cloud edges.

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

Частицы кулли

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

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

Следующий код демонстрирует буфер добавления.

Шейдер вычислений:

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 требует половины отрисовки пикселей.

Image showing 70% scale, with half the pixels.

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

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

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

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

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

Об авторах

Picture of Robert Ferrese Роберт Феррезе
@Microsoft инженера по программному обеспечению
Picture of Dan Andersson Дэн Андерссон
@Microsoft инженера по программному обеспечению

См. также