Продолжаем писать рейтрейсер на D
Здравствуйте дорогие посетители!
Сегодня мы с вами продолжим писать рейтрейсер, попутно изучая язык D.
Для тех кто пропустил предыдущую часть - рекомендуем начать с нее. Учим язык D на примере рейтрейсинга.
И так, в прошлый раз мы с вами создали каркас нашего будущего рейтрейсера. Во всяком случае приложение запускается и заполняет окно оранжевым цветом Совсем неплохо.
Давайте выпишем основные блоки из которых должен состоять каждый уважающий себя рейтрейсер:
- Tracer - класс, который собственно и занимается трассировкой лучей и синтезом конечного изображения
- Scene - класс, представляющий трехмерную сцену, которую будет трассировать Tracer
- Primitive (Plane, Sphere) - классы, представляющие объекты сцены (пока простейшие геометрические фигуры)
Естественно что хотелось бы загружать сцену из файла. При этом, чтобы файл сцены можно было подправить вручную. Я остановил свой выбор на JSON. Простой текстовый формат, легок для чтения и редактирования. К тому же парсер JSON входит в стандартную библиотеку D.
Давайте создадим новый модуль. Файл модуля назовем scene.d, и положим его в папочку rayd. Создадим класс Primitive. Самый важный функционал каждого нашего примитива какой? Правильно - уметь определить пересечение луча с самим собой. Для этого добавим в наш новый класс метод Intersecion
abstract int Intersection(in Ray ray, ref float distance); |
Давайте немного рассмотрим это объявление метода. Метод принимает 2 параметра - луч и максимальное расстояние на котором можно искать пересечение. Возвращает результат пересечения как int (пока 0 или 1, задел на будущее). Однако нас интересуют фишки языка D. Ключевое слово abstract говорит компилятору что этот метод полностью абстрактный (pure virtual), и должен быть обязательно имплементирован во всех его наследующих классах. Как по мне намного симпатичнее чем
virtual void Foo() = 0; |
Для того чтобы перегрузить виртуальный метод (так как все методы в D по умолчанию виртуальны) - нужно указать ключевое слово override перед определением метода:
override int Intersection(in Ray ray, ref float distance) |
Параметр ray помечен как in - это означает что он не может быть изменен (т.е. константен) а так же назначен внутренней или глобальной переменной. Таким образом модификатор in равен по значению const scope, однако намного лаконичнее Второй параметр - distance - помечен модификатором ref, что означает что он будет передан по изменяемой ссылке. Таким образом мы можем передавать значение distance, равно как метод может его изменять. Полная аналогия с ссылками в С++.
Теперь рассмотрим еще одну "фишку" языка D - свойства (properties) объектов. Если не считать отсебятины от Microsoft (http://msdn.microsoft.com/en-us/library/yhfk0thd.aspx), в С++ свойств нет. Однако это очень удобная штука, и грех было бы ей не воспользоваться. Реализуются свойства в D посредством ключевого слова @property. Рассмотрим на примере:
@property const vec3 Position() { return m_Position; } @property void Position(vec3 value) { m_Position = value; } |
Я думаю что уже из самого кода понятно что здесь происходит. Мы объявили свойство Position и реализовали так называемые setter и getter. Причем не обязательно реализовывать оба - тогда свойство будет только read или только write. Нереально удобная вещь!
Как я уже упоминал - описание сцены у нас будет храниться в формате JSON. К счастью парсер включен в стандартную библиотеку D, так что для загрузки нам не понадобятся сторонние библиотеки. Вот ссылка на официальную документацию модуля json - http://dlang.org/phobos/std_json.html.
Во время написания загрузки сцены я наткнулся на недоработку парсера - при чтении чисел, парсер автоматически определяет тип числа, и для получения числа другого типа нужно вручную делать приведение типов. Так что написав значение "5" - парсер скорее всего решит что это unsigned int, и если вам потребуется float - будьте добры приводите тип вручную. Для этого мне пришлось написать вспомогательную функцию, а в данный момент я думаю над доработкой модуля и отправки патча разработчикам (все исходники модулей поставляются вместе с дистрибутивом языка, что очень удобно).
// iOrange - little function that helps us to get float value real GetJSONFloat(JSONValue v) { real ret = 0.0f; if (v.type == JSON_TYPE.FLOAT) ret = v.floating; else if (v.type == JSON_TYPE.INTEGER) ret = cast(real)v.integer; else if (v.type == JSON_TYPE.UINTEGER) ret = cast(real)v.uinteger; return ret; } |
Кстати - обратите внимание как сделаны приведения типов в языке D - очень наглядно и удобно. Приведение типов в стиле С здесь просто не работает, а в отличии от С++ с его const_cast, static_cast, reinterpret_cast, dynamic_cast - здесь мы имеем автоматическое определение компилятором. При приведении простых типов будет сгенерировано простое приведение, а при приведении более сложных типов (объектов классов) будет сгенерировано динамическое приведение, которое будет проверять приведение по иерархии вниз и в случае чего возвращать null. Гениально!
Последний класс который мы рассмотрим - Tracer. Тут все просто - самый главный метод класса
void Render(Scene scene, uint* image, int width, int height) |
То есть рендерим сцену в указанный нам буфер размером width на height пикселов. Зная размер выходного изображения, мы для каждого пиксела построим луч и пустим его гулять по сцене, пока он не пересечется с геометрией. Если пересечение найдено - запишем соответствующему пикселу цвет в точке пересечения. Вот так вот вкратце работает "обратная трассировка лучей". Пока нас нет камеры (черт, проговорился, да - камеру мы добавим в следующей части), мы параметры вьюпорта захардкодим немного. Вот так, схематически, выглядит процесс трассировки лучей:
Остальной код довольно прост и в особых комментариях не нуждается. В следующей статье мы добавим камеру и немного света, а пока скриншот того что получилось на текущий момент.
Как обычно - прикладываю исходный код к статье.
RayD_p2.zip (1490)