#unity3d #shader #unity3d_faq
Версия Unity 2018.1, шейдеры создавались в Shader Graph для Lightweight Render Pipeline. При рендеринге с использованием полупрозрачного материала возникла проблема странной отрисовки некоторых частей меши: Opaque PBR Transparent PBR, alpha = 1 Master node в Shader Graph Объясните пожалуйста, почему так происходит, и как можно от этого избавиться?
Ответы
Ответ 1
Почему так происходит? Непрозрачные (и не только, но это уже другая тема) шейдеры перед "основной" отрисовкой пишут глубину пикселя в буфер, как ни странно, глубины - depth buffer, также его называют z-buffer. Логика достаточно простая: объекты отрисовываются от самых близких к самым дальним - такой подход позволят отсекать невидимые пиксели при пересечении какой-либо геометрии в сцене. Перед обработкой очередного фрагмента (пикселя) в определенный буфер GPU сравнивает значение глубины этого самого фрагмента с z-buffer`ом, чтобы решить, нужно ли вообще обрабатывать этот пиксель. Вся эта предыстория нужна для того, чтобы понять, почему полупрозрачные объекты являются головной болью при рендеринге. Для примера возьмем обычный куб с полупрозрачным материалом и двусторонней отрисовкой и посмотрим, какая потенциальная проблема возникает в полупрозрачных объектах. Чтобы рисунок был попроще, допустим, что конкретно этот куб состоит из четырехугольников, а не треугольников. Для наглядности я продемонстрировал, в каком порядке GPU, например, отрисует полигоны (вообще, это зависит от того, как полигоны индексированы в index buffer), и сразу ниже я нарисовал, что мы ожидаем увидеть от GPU: Даже без отрисовки 2х последних граней видно, что что-то пошло не так: пиксели более "поздних" в плане отрисовки полигонов почему-то пишут поверх уже отрендеренных пикселей, даже не смотря на то, что они изначально должны были провалить ZTest - передний полигон, очевидно, ближе, чем все остальные полигоны нашего куба. Давайте вернемся к непрозрачным объектам. Почему буфер глубины работает? Все просто, из-за системы отрисовки front-to-back (от ближних объектов к дальним) значения в буфере глубины могут меняться только на более "близкие" (зависит от устройства буфера, иногда самый близкий пиксель отмечается 1, иногда 0). В Transparent объектах все наоборот: Они отрисовываются по системе back-to-front, тут стоит сделать оговорку: так происходит не везде, такой подход на сегодняшний день используется в Unity на данный момент, с добавлением Scriptable Render Pipeline появилась возможность переписать рендер как душе угодно, в том числе можно применить front-to-back transparent rendering, подробнее про это здесь. Для чего это нужно? Если вы расположите 2 полупрозрачных объекта с, к примеру, alpha = 0.5, то вы ожидаете, что сначала произойдет блендинг первого объекта с непрозрачным фоном, после произойдет блендинг обновленного фона и второго объекта. Такой подход обязывает не обновлять информацию в буфере глубины, опять же, почему? На то есть 2 причины: первая - мы все еще проверяем наш пиксель на глубину, чтобы не рисовать лишний раз перекрытый пиксель, вторая - если мы начнем обновлять буфер глубины, возникнут артефакты при частичном перекрытии друг другом двух полупрозрачных объектов: В данном случае я поместил один полупрозрачный объект (каска) в другой (стена) так, чтобы каска была по обе стороны стены. Без обновления буфера глубины видно, что оба объекта нормально смешиваются между собой, но стоит включить обновление буфера глубины, как мы сразу видим, что половина каски "за стеной" не отрисовывается. P.S. Каска при ZWrite Off выглядит, как будто непрозрачная - это из-за того, что у стены и у каски цвет материала белый, в обычных условиях все будет выглядеть хорошо. Решение данной проблемы Для начала нужно понять, что панацеи от этого не существует. Вся проблема заключается в том, что пикселям полупрозрачных объектов не очень нравится бороться за право быть отрисованным - поэтому они вообще этого не делают. Ладно бы это бы разные объекты - там все просто разрешается сортировкой полупрозрачных объектов по глубине, но когда эти пиксели принадлежать одному объекту - возникает довольно сложная в решении проблема, описанная выше. И вот, после горы теории, мы подобрались к способам решения такой проблемы: Обновлять буфер глубины или что такое ZWrite On Радикально писать в буфер я не советую, в идеале нужно просто написать shader variant (нужно писать свой шейдер с нуля), либо же сделать 2 разных шейдера в Shader Graph, один с ZWrite On при alpha = [1..0.5) и ZWrite Off при alpha = [0.5..0]. Уж совсем в идеале можно слегка переписать Lightweight Render Pipeline, проверяя, пересекаются ли полупрозрачные объекты при их сортировке. В общем, вариантов при данном подходе много. Самый дешевый способ - один шейдер с ZWrite On, но и рисует он только "внешнюю оболочку" объекта, если внутри что-то есть - нужно его комбинировать с ZWrite Off для разных значений alpha: Как такое сделать в Shader Graph? К сожалению, Shader Graph, в отличии от его аналога - Amplify Shader Editor, не дает вот так просто менять ZWrite и прочие прелести сабшейдеров, ну... на то она и бета, когда-нибудь добавят :) А мы не брезгуя создаем новый файл шейдера, подойдет любой, лишь бы не Graph - нам этот дурацкий редактор не нужен, например - Standard Surface Shader. Называем, как хотим, открываем, удаляем все содержимое, сохраняем и пока что оставляем в покое. Создаем любой нужный нам шейдер в Shader Graph, ставим Surface - Transparent. Создали, протестировали, все работает - кликаем правой кнопкой мыши на Master Node и выбираем Show Generated Code. Ждем, пока прогрузится выбранный редактор кода, видим наш шейдер, выделяем его и копируем. Далее открываем наш первый файл шейдера, из которого мы все удалили, и вставляем скопированный код. Далее через Ctrl + F находим ZWrite Off и меняем его на ZWrite On. Будьте внимательны, это нужно заменить в Forward проходе сабшейдера, это важно: А важно, потому что проходов у сабшейдера может быть несколько, и каждый из них будет содержать параметр ZWrite: P.S. Вообще есть еще несколько вариантов, например, - Cutout шейдер и постоянная смена индексирования вершин для разного положения и поворота камеры. Первый вариант не подходит, т.к. нужен именно полупрозрачный объект, а второй не подходит сразу по двум причинам: во-первых слишком сложно реализовать без хоть каких-то знаний в области GPU и конвейера рендеринга, во-вторых такая методика явно не подходит для runtime вычислений.
Комментариев нет:
Отправить комментарий