Я уже давно хотел разобраться получше с Rust, что бы составить впечатление о языке. У них есть достаточно хорошая документация, которая к тому же переведена на русский, в интернете - куча статей. Но видно я не из тех, кто способен понять язык по книжкам. Поэтому было решено написать на нем, что-то более или менее крупное, в качестве задачи решил взять Software rendering, заодно университетский курс линейной алгебры вспомню. Тем более я уже когда-то давно писал его для конкурса (я там под ником Rean), правда исходники не сохранились, но зато в памяти остались общие моменты. Сразу оговорюсь: я не буду пошагово описывать все алгоритмы, которые я использую, иначе это будет не серия постов, а объёмная книга. Но постараюсь давать ссылки на материалы, которые, на мой взгляд, хорошо описывают что я делаю и комментаровать некоторе моменты. Так же буду выкладывать ссылки на мою реализацию в GitHub, местами корявую, частично из-за незнаний языка, частично из-за подзабытых алгоритмов. Всего этого должно быть в принципе достаточно для воспроизведения, при желании, моих результатов на любом другом языке. А для тех кому лень - будут картинки, по которым можно оценить прогресс.
Сразу обещанная ссылка на мою реализацию.
А теперь приступим: как установить rust и сделать минимальную IDE для него я уже писал раньше. Минимальный проект можно создать при помощи cargo:
Что бы как-то отправлять мои художества на отрисовку, понадобиться sdl2, для archlinux уставить можно вот так:
Для ubuntu вот так:
На сайте можно поискать как установить для других OS, библиотека вполне кроссплатформенная. Обёртку для неё писать не пришлось, её уже сделали добрые люди. Что бы её подключить не пришлось ничего ни выкачивать, ни компилировать, ни как-то особо прописывать пути, достаточно в файле проекта указать какие библиотеки и каких версий нужны и пакетный менеджер всё грязную работу сделает за нас. Что порадовало - в репозитории выложено уже около 3 000 готовых библиотек.
От sdl2 мне много не нужно, достаточно, что бы она умела сгенерированную мной текстуру быстро отобразить на экране. Поэтому я долго разбираться с документацией не стал, а просто посмотрел на примеры и на их основе сделал свой класс.
Дальше нужно научиться рисовать одноцветный треугольник на плоскости. Воспользуемся самым простым алгоритмом на основе scanline, отмечу что существуют и другие алгоритмы (кому интересно может поискать в google "traversal rasterization algorithm"), но они сложнее для понимания и заточены под SIMD и многопоточную отрисовку. Главное не забыть, что треугольник может выходить за границу экрана и учесть вырожденные случаи - вроде треугольников у которых 2 вершины имеют одинаковые координаты. Вообще конечно эту часть лучше сделать никуда не подглядывая, т.к. там математика на уровне школы, достаточно знать уравнение прямой и подумать немного. Немного нетривиальным является момент с тем - рисовать или нет пиксель, если треугольник "залезает" на него лишь частично. Для решения этой проблемы есть так называемые rasterization rules, реализуются они если упрощенно - проверкой того, "вылезает" ли начальный и конечный пиксель каждой рисуемой линии за половину пикселя или нет. В конце концов должно получиться что-то похожее на на вот это. Ну и что бы было не совсем скучно будем рисовать сразу много треугольников со случайным цветом и координатами. Осталось собрать и запустить:
В итоге получим 16 000 треугольников и всего 1 FPS:
Можно конечно попробовать это все ускорить, но функция отрисовки будет ещё сильно меняться, поэтому смысла нет.
Наверное не стоит даже упоминать, что первый правильный "на глаз" результат содержал кучу ошибок. Причём даже мощный статический анализ кода от компилятора rust их не обнаруживал, ибо они были в алгоритме. Однако в арсенале языка есть встроенный framework для unit-testing. Он довольно простой, но на удивление самодостаточный и неплохо описанный. В качестве тестовых данных я взял отличную картинку от Microsoft, которая показывает результаты растеризации для "особых" случаев:
И для каждого треугольника из рисунка я сделал отдельный тест и почти каждый выявлял одну, а то и парочку проблем в моей реализации. Тесты, кстати, запускаются вот так:
Ну и напоследок первые впечатления о rust:
Сразу обещанная ссылка на мою реализацию.
А теперь приступим: как установить rust и сделать минимальную IDE для него я уже писал раньше. Минимальный проект можно создать при помощи cargo:
cargo new rust-software-render --bin
sudo pacman -S sdl2 sdl2_image
sudo add-apt-repository ppa:team-xbmc/ppa -y sudo apt-get update -q sudo apt-get install libsdl2-dev
От sdl2 мне много не нужно, достаточно, что бы она умела сгенерированную мной текстуру быстро отобразить на экране. Поэтому я долго разбираться с документацией не стал, а просто посмотрел на примеры и на их основе сделал свой класс.
Дальше нужно научиться рисовать одноцветный треугольник на плоскости. Воспользуемся самым простым алгоритмом на основе scanline, отмечу что существуют и другие алгоритмы (кому интересно может поискать в google "traversal rasterization algorithm"), но они сложнее для понимания и заточены под SIMD и многопоточную отрисовку. Главное не забыть, что треугольник может выходить за границу экрана и учесть вырожденные случаи - вроде треугольников у которых 2 вершины имеют одинаковые координаты. Вообще конечно эту часть лучше сделать никуда не подглядывая, т.к. там математика на уровне школы, достаточно знать уравнение прямой и подумать немного. Немного нетривиальным является момент с тем - рисовать или нет пиксель, если треугольник "залезает" на него лишь частично. Для решения этой проблемы есть так называемые rasterization rules, реализуются они если упрощенно - проверкой того, "вылезает" ли начальный и конечный пиксель каждой рисуемой линии за половину пикселя или нет. В конце концов должно получиться что-то похожее на на вот это. Ну и что бы было не совсем скучно будем рисовать сразу много треугольников со случайным цветом и координатами. Осталось собрать и запустить:
cargo run --release
Можно конечно попробовать это все ускорить, но функция отрисовки будет ещё сильно меняться, поэтому смысла нет.
Наверное не стоит даже упоминать, что первый правильный "на глаз" результат содержал кучу ошибок. Причём даже мощный статический анализ кода от компилятора rust их не обнаруживал, ибо они были в алгоритме. Однако в арсенале языка есть встроенный framework для unit-testing. Он довольно простой, но на удивление самодостаточный и неплохо описанный. В качестве тестовых данных я взял отличную картинку от Microsoft, которая показывает результаты растеризации для "особых" случаев:
И для каждого треугольника из рисунка я сделал отдельный тест и почти каждый выявлял одну, а то и парочку проблем в моей реализации. Тесты, кстати, запускаются вот так:
cargo test
- Писать методом copy-paste со stackoverflow почти невозможно, требуется более глубокое понимание концепций языка. Например python, я первые пол года использовал не заглядывая в книги, тут так не выйдет.
- Компилятор очень дотошный, заставляет исправлять все, начиная от неявного приведения типов вплоть до того, что ругается на лишние скобки в выражении, неиспользуемые переменные, функции и т.п. Впрочем надеюсь это окупиться меньшим количеством ошибок в runtime.
- Сообщения об ошибках компиляции радуют своей внятностью, особенно после C++.
- Менеджер пакетов (cargo) после многих лет использования языков без оного - порадовал. Если создатели языка не озаботились подобным, то обязательно вокруг вырастает куча костылей разной степени убогости. И вообще управление проектом и тестами в rust радует простотой и удобством.
- Сам язык создаёт впечатление этакого C++ спроектированного с нуля, без оглядки на совместимость. По крайней мере сейчас я не вижу в нем каких-то нелогичных моментов, любая строчка ясно говорит, что будет сделано. Н-р когда передаёшь переменную в функцию, то без заглядывания в определение функции понятно, как будет передана переменная и будет ли она модифицирована.