В прошлой статье я показал как можно реализовать буфер глубины с помощью интерполяции значения 1/z. А так же сделал простейшую систему освещения, когда цвет рассчитывался целиком для треугольника. На низкополигональных моделях это выглядело ужасно, так рисовали много лет назад, когда производительности катастрофически не хватало. Сейчас даже ресурсов только процессора хватит, что бы сделать нормальное попиксельное освещение, чем мы сейчас и займёмся.
Напомню, что 3ds файл явно не сохраняет информацию о нормалях, но благодаря тому, что порядок обхода вершин там всегда одинаковый, нормали достаточно легко восстанавливаются для каждой грани отдельно и в прошлой статье я показывал как это делается. Но если рассчитывать одну нормаль для всей грани, то границы треугольников очень хорошо видны. Что бы обойти это обычно рассчитывают нормаль для каждой вершины в отдельности. Часто её считают как среднее от всех нормалей граней, куда входит вершина.
Тут есть одна тонкость, не все грани одинаково полезны, разные грани очевидно должны вносить разный вклад в результат. Способов реализовать такое можно придумать массу - учитывать площадь или размеры сторон треугольников, анализировать их форму и т.п. Можно ещё считать угол между нормалями и когда он больше 90 градусов - разбивать вершину на две, что бы допустим модель куба не сглаживалась до низкополигональной сферы. Впрочем последнее спорно, некоторые справедливо считают, что подобным должен заниматься экспортёр файлов, а не импортёр. Самым дешёвым способом будет учёт площади грани, он даже дешевле, чем просто считать среднее от всех нормалей граней. Напомню, что когда мы считали нормаль для грани, то использовали функцию cross, которая возвращала нормаль по длине равную удвоенной площади треугольника, т.е. если складывать не нормализованные нормали граней, то в результате как раз и получим что хотели. Вот как это можно сделать.
Ради полноты картины, отмечу, что формат 3ds хоть и подразумевает наличие групп сглаживания, для более точного расчёта нормалей, но экспортёр в blender их судя по всему не поддерживает, поэтому в парсере я их игнорирую.
Двигаемся дальше. Нормали мало рассчитать в каждой вершине, их ещё нужно уметь интерполировать. Если обобщить задачу, то для каждого числа возвращаемого вершинным шейдером, нужно уметь получать его значение в каждом пикселе внутри треугольника. Алгоритмов довольно много, попробуем выбрать самый подходящий:
Вот пример моей реализации. Вышло весьма запутанно, но к сожалению текущие знания Rust не позволяют написать понятнее без сильного ущерба для скорости. В качестве оптимизации кода можно пойти на компромисс и рассчитывать точные значения только в начальной и конечной точке scanline а между ними интерполировать линейно, что при грамотной реализации может дать неплохой прирост по скорости. Но пока скорость отрисовки остаётся приемлемой - я не буду этого делать.
Теперь порисуем. Для начала выведу шар с нормалью в каждом пикселе закодированной в цвет, что бы наглядно увидеть что нам наинтерполировалось (вершинный и пиксельный шейдера):
Дальше обещанное освещение, самое простое по Ламберту, оно не имеет specular составляющей (вершинный и пиксельный шейдера):
Усложняем, освещение по модели Фонга-Блина, тут уже есть блики (вершинный и пиксельный шейдера):
Ещё усложняем: упрощённый Cook-Torrance. Cчитается, что если предыдущая модель освещения больше подходит для пластика, то Cook-Torrance - для металлов, но я если честно особой разницы не вижу (а вот проседание fps существенное) (вершинный и пиксельный шейдера):
По идее кольцо должно быть серебряного цвета, а шар - золотого (коэффициенты брал тут), но к сожалению без текстур, карты нормалей, отражений и т.п. добиться реалистичности очень сложно.
И традиционная рубрика впечатлений о Rust (сегодня почему-то получился в основном негатив):
Продолжение
Напомню, что 3ds файл явно не сохраняет информацию о нормалях, но благодаря тому, что порядок обхода вершин там всегда одинаковый, нормали достаточно легко восстанавливаются для каждой грани отдельно и в прошлой статье я показывал как это делается. Но если рассчитывать одну нормаль для всей грани, то границы треугольников очень хорошо видны. Что бы обойти это обычно рассчитывают нормаль для каждой вершины в отдельности. Часто её считают как среднее от всех нормалей граней, куда входит вершина.
Тут есть одна тонкость, не все грани одинаково полезны, разные грани очевидно должны вносить разный вклад в результат. Способов реализовать такое можно придумать массу - учитывать площадь или размеры сторон треугольников, анализировать их форму и т.п. Можно ещё считать угол между нормалями и когда он больше 90 градусов - разбивать вершину на две, что бы допустим модель куба не сглаживалась до низкополигональной сферы. Впрочем последнее спорно, некоторые справедливо считают, что подобным должен заниматься экспортёр файлов, а не импортёр. Самым дешёвым способом будет учёт площади грани, он даже дешевле, чем просто считать среднее от всех нормалей граней. Напомню, что когда мы считали нормаль для грани, то использовали функцию cross, которая возвращала нормаль по длине равную удвоенной площади треугольника, т.е. если складывать не нормализованные нормали граней, то в результате как раз и получим что хотели. Вот как это можно сделать.
Ради полноты картины, отмечу, что формат 3ds хоть и подразумевает наличие групп сглаживания, для более точного расчёта нормалей, но экспортёр в blender их судя по всему не поддерживает, поэтому в парсере я их игнорирую.
Двигаемся дальше. Нормали мало рассчитать в каждой вершине, их ещё нужно уметь интерполировать. Если обобщить задачу, то для каждого числа возвращаемого вершинным шейдером, нужно уметь получать его значение в каждом пикселе внутри треугольника. Алгоритмов довольно много, попробуем выбрать самый подходящий:
- Можно линейно интерполировать нужный параметр в пространстве экрана, как это делалось в прошлой статье со значением 1/z. Но т.к. преобразование координат 3d треугольника в экранные координаты происходило не линейно, то подобная интерполяция даст весьма неточные результаты. Однако, этот способ вполне имеет право на жизнь для отрисовки маленьких треугольников, когда неточность будет незаметна.
- "Точная интерполяция" - подробное описание и вывод формул можно посмотреть тут. Краткий вывод: для каждого интерполируемого значения u можно получить формулу зависящую от экранных координат (sx, sy) и коэффициентов (C1-С6) зависящих от параметров грани, вот такого вида: u = (C1*sx+C2*sy+C3) / (C4*sx+C5*sy+C6). Тут мы получим честные значения.
- "Барицентрические координаты" - можно почитать вот тут, по моему вполне правильное объяснение: Они тоже дают правильный результат.
- "Перспективно-корректная интерполяция" - как выводятся формулы написано тут, в разделе "Интерполяция атрибутов". Вывод очень простой, если просто значение u интерполировать нельзя, то u/z уже можно. Т.е. пусть для каждой вершины треугольника задано некоторое значение u: Au, Bu, Cu, а так же известны координаты z: Az, Bz, Cz. Тогда нужно посчитать Au/Az, Bu/Bz, Cu/Cz, линейно их интерполировать и в каждом пикселе получившееся значение умножить на z. Значение z (ну если точнее то 1/z) мы все равно считаем для буфера глубины, т.е. оно нам достаётся "бесплатно", остаётся сделать линейную интерполяцию для каждого значения, что весьма не сложно.
Вот пример моей реализации. Вышло весьма запутанно, но к сожалению текущие знания Rust не позволяют написать понятнее без сильного ущерба для скорости. В качестве оптимизации кода можно пойти на компромисс и рассчитывать точные значения только в начальной и конечной точке scanline а между ними интерполировать линейно, что при грамотной реализации может дать неплохой прирост по скорости. Но пока скорость отрисовки остаётся приемлемой - я не буду этого делать.
Теперь порисуем. Для начала выведу шар с нормалью в каждом пикселе закодированной в цвет, что бы наглядно увидеть что нам наинтерполировалось (вершинный и пиксельный шейдера):
Дальше обещанное освещение, самое простое по Ламберту, оно не имеет specular составляющей (вершинный и пиксельный шейдера):
Усложняем, освещение по модели Фонга-Блина, тут уже есть блики (вершинный и пиксельный шейдера):
Ещё усложняем: упрощённый Cook-Torrance. Cчитается, что если предыдущая модель освещения больше подходит для пластика, то Cook-Torrance - для металлов, но я если честно особой разницы не вижу (а вот проседание fps существенное) (вершинный и пиксельный шейдера):
По идее кольцо должно быть серебряного цвета, а шар - золотого (коэффициенты брал тут), но к сожалению без текстур, карты нормалей, отражений и т.п. добиться реалистичности очень сложно.
И традиционная рубрика впечатлений о Rust (сегодня почему-то получился в основном негатив):
- ООП фактически на уровне старого доброго C - структура и набор методов для работы с ней, с небольшим синтаксическим сахаром в виде явно задаваемого self.
- Переопределения методов - нет, возможно это антипаттерн, поскольку делает код более запутанным, но я к ним настолько привык, что без них очень тяжело.
- Возможности задавать умолчательные значения для аргументов функций - тоже нет. В совокупности с предыдущим пунктом, раздражает особенно сильно. Приходится писать кучу функций для всех комбинаций аргументов. Есть альтернатива в виде Option, но работать с ними не очень удобно, хотя и более наглядно.
Продолжение
>> ООП фактически на уровне старого доброго C - структура и набор методов для работы с ней, с небольшим синтаксическим сахаром в виде явно задаваемого self.
ОтветитьУдалитьЭто спорный момент) Все-таки трейты дают полноценный полиморфизм.
Я вот все жду, чем кончатся срачи про добавление в ржавчину наследования или альтернативы ему. Надеюсь, что остановятся на анонимных полях, как в Go. Тогда с ООП проблем бы быть не должно, наоборот понятнее все станет из-за явного разделения полиморфизма и наследования реализации.
Может до меня просто медленно доходит, но только сейчас начинает постепенно вырисовываться их концепция ООП: и зачем нужны их трейты, и куда пристраивать типажи. Ощущение от всего этого - взяли старые знакомые концепции ООП, вывернули их наизнанку, назвали по другому и реализовали в языке.
Удалить>> зачем нужны их трейты, и куда пристраивать типажи
УдалитьЭто ж одно и то же)
>> Ощущение от всего этого - взяли старые знакомые концепции ООП, вывернули их наизнанку, назвали по другому и реализовали в языке.
Тут не бралось и мялось плюсовое ООП, а изначально было чего-то более похожее на функциональные хаскели всякие, но, по мере формирования конечного вида языка, оно стало больше напоминать плюсы, потому что требования близкие.
Ржавчина ранних версий - 0.2-0.3 очень сильно отличается от современной, туда интересно бывает заглянуть и попытаться проследить мысль авторов)
Эх тут бы с текущей версией разобраться. На то что бы заниматься археологией совсем времени не остается
УдалитьЕсли есть какие затыки по части языка - обращайся, буду рад помочь если смогу) Серия статей у тебя интересная, не хотелось бы что бы ты ее забросил.
УдалитьСпасибо. Там вроде нет ничего принципиально непонятного, нужно просто читать, заставлять кого-то перечитывать мануалы и делать выжимку - не удобно как-то )
УдалитьНасчет серии статей - я сейчас дописываю по текстурам пост, а потом если честно не знаю куда еще развивать и нужно ли вообще
Хм.. Скелетную анимацию добавить, например? )
УдалитьОт желания перегружать функции отвык относительно быстро.
ОтветитьУдалитьА вот про нехватку опциональных/умолчальных аргументов соглашусь. Года полтора активно ковыряю язык, а так и не привык к этому до конца. Можно, конечно, использовать шаблон Строитель, но это часто ощущается "стрельбой из пушки для воробьям".
Надеюсь, команда разработки в итоге это тоже добавит, благо обратно-совместимое изменение.
>> Переопределения методов - нет
Ну не то что бы совсем нет - http://is.gd/2STKxf
Я может неудачно выразился, но мне хочется 2 рядом стоящих метода с одинаковым названием, но с разным количеством аргументов или с разными типами. А компилятор пусть сам выбирает какой должен быть вызван в том или ином случае.
УдалитьНу это перегрузка тогда просто. Полноценную перегрузку почти наверняка не добавят, она в дизайн языка плохо укладывается, а вот опциональные параметры вполне могут появиться - их много кто просит и у ядра разработчиков, вроде, особых возражений нет.
Удалить