Пользователь
0,0
рейтинг
28 января 2015 в 01:15

Разработка → Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 5 из 6 tutorial

Содержание основного курса




Улучшение кода






Official translation (with a bit of polishing) is available here.




Пришла пора веселья, давайте для начала смотреть размер текущего кода:
  • geometry.cpp+.h — 218 строк
  • model.cpp+.h — 139 строк
  • our_gl.cpp+.h — 102 строки
  • main.cpp — 66 строк


Итого 525 строк. Ровно то, что я обещал в самом начале курса. И заметьте, что отрисовкой мы занимаемся только в our_gl и main, а это всего 168 строк, и нигде мы не вызывали сторонних библиотек, вся отрисовка сделана нами с нуля!
Я напоминаю, что мой код нужен только для финального сравнения с вашим работающим кодом! По-хорошему, вы всё должны написать с нуля, если следуете этому циклу статей. Очень прошу, делайте самые безумные шейдеры и выкладывайте в комментарии картинки!!!



Чёрные треугольники на рогах — это модель битая слегка, просто мне надоела голова старого негра, а чинить не сильно хочется.



Рефакторим наш код, чтобы он походил на структуру OpenGL


Итак, наш main.cpp начинает слегка разрастаться, поэтому давайте его разделим на две части
  • our_gl.cpp + .h — это часть, которую мы программировать не можем, грубо говоря, бинарный файл библиотеки.
  • main.cpp — здесь мы можем программировать что хотим.


Давайте подробнее, что я вынес в our_gl? Фунции построения матриц проекции, вида и перехода к экранным координатам, а также сами матрицы просто глобальными переменными. Ну и функцию-растеризатор треугольника. Всё!

Вот содержимое файла our_gl.h (про назначение IShader чуть позже):
#include "tgaimage.h"
#include "geometry.h"

extern Matrix ModelView;
extern Matrix Viewport;
extern Matrix Projection;

void viewport(int x, int y, int w, int h);
void projection(float coeff=0.f); // coeff = -1/c
void lookat(Vec3f eye, Vec3f center, Vec3f up);

struct IShader {
    virtual ~IShader();
    virtual Vec3i vertex(int iface, int nthvert) = 0;
    virtual bool fragment(Vec3f bar, TGAColor &color) = 0;
};

void triangle(Vec4f *pts, IShader &shader, TGAImage &image, TGAImage &zbuffer);


В файле main.cpp осталось всего 66 строк, поэтому я его даю целиком (извините за простыню, но мне этот файл настолько нравится, что я не буду его прятать под спойлер):
#include <vector>
#include <iostream>

#include "tgaimage.h"
#include "model.h"
#include "geometry.h"
#include "our_gl.h"

Model *model     = NULL;
const int width  = 800;
const int height = 800;

Vec3f light_dir(1,1,1);
Vec3f       eye(1,1,3);
Vec3f    center(0,0,0);
Vec3f        up(0,1,0);

struct GouraudShader : public IShader {
    Vec3f varying_intensity; // written by vertex shader, read by fragment shader

    virtual Vec4f vertex(int iface, int nthvert) {
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
        return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
    }

    virtual bool fragment(Vec3f bar, TGAColor &color) {
        float intensity = varying_intensity*bar;   // interpolate intensity for the current pixel
        color = TGAColor(255, 255, 255)*intensity; // well duh
        return false;                              // no, we do not discard this pixel
    }
};

int main(int argc, char** argv) {
    if (2==argc) {
        model = new Model(argv[1]);
    } else {
        model = new Model("obj/african_head.obj");
    }

    lookat(eye, center, up);
    viewport(width/8, height/8, width*3/4, height*3/4);
    projection(-1.f/(eye-center).norm());
    light_dir.normalize();

    TGAImage image  (width, height, TGAImage::RGB);
    TGAImage zbuffer(width, height, TGAImage::GRAYSCALE);

    GouraudShader shader;
    for (int i=0; i<model->nfaces(); i++) {
        Vec4f screen_coords[3];
        for (int j=0; j<3; j++) {
            screen_coords[j] = shader.vertex(i, j);
        }
        triangle(screen_coords, shader, image, zbuffer);
    }

    image.  flip_vertically(); // to place the origin in the bottom left corner of the image
    zbuffer.flip_vertically();
    image.  write_tga_file("output.tga");
    zbuffer.write_tga_file("zbuffer.tga");

    delete model;
    return 0;
}


Давайте его разберём детально. Заголовки пропускаем, затем идут глобальные константы: размеры экрана, где находится камера и т.п.
Структуру GouraudShader разберём в следующем абзаце, пропускаем. Затем идёт непосредственно main():
  • Чтение модели из .obj файла
  • Инициализация матриц ModelView, Projection и Viewport (напоминаю, сами переменные хранятся в модуле our_gl)
  • Проход по модели и её отрисовка


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

Главное назначение вершинного шейдера — посчитать преобразованные координаты вершин. Второстепенное — подготовить данные для работы фрагментного шейдера.

Что происходит после того, как мы вызвали вершинный шейдер для всех вершин в треугольнике? Мы можем вызвать растеризатор нашего треугольника. Что происходит внутри него мы не знаем (не, ну, мы его сами написали, конечно). Кроме одной интересной вещи. Растеризатор треугольника вызывает нашу функцию, которую мы ему даём — фрагментный шейдер. То есть, ещё раз, для каждого пикселя внутри треугольника растеризатор вызывает фрагментный шейдер.

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

Пайплайн OpenGL 2 выглядит так:


Поскольку у нас краткий курс графики, пока ограничимся этими двумя шейдерами. В более новых версиях OpenGL появились новые виды шейдеров, которые позволяют создавать геометрию на лету. На этой картинке синим показаны этапы, которые мы программировать не можем, а рыжим те, что можем. По факту, наша main() — это primitive processing. Она вызывает вершинный шейдер. Сборщика примитивов у нас нет, т.к. мы рисуем тупые треугольники напрямую (он у нас склеился с primitive processing). Функция triangle() — это растеризатор, для каждой точки она вызывает фрагментный шейдер и затем делает проверки глубины в z-буфере и так далее.

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

Как работает моё воплощение шейдеров на примере тонировки Гуро



Давайте разберём те шейдеры, что я привёл в коде main.cpp. Как нетрудно догадаться, первый шейдер — это тонировка Гуро.

Скрытый текст


Вершинный шейдер читает вершину из .obj файла, погружает её в четырёхмерное пространство (см. предыдущую статью), находит её экранные координаты. Возвращает спроецированную в 3д точку, но перед этим считает коэффициент диффузной освещённости для данной вершины и сохраняет его в соответствующую компоненту вектора varying_intensity.

Ещё раз код для удобства:
Скрытый текст
    Vec3f varying_intensity; // written by vertex shader, read by fragment shader
    virtual Vec4f vertex(int iface, int nthvert) {
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
        return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
    }



varying — это зарезервированное слово в языке GLSL, я использовал varying_intensity в качестве имени просто чтобы подчеркнуть параллель между ними (о GLSL мы поговорим в седьмой статье). Мы сохраняем в структуре varying данные, которые будут интерполированы внутри треугольника, и фрагментный шейдер получит уже интерполированные данные.

Разберём фрагментный шейдер, ещё раз код для удобства:
Скрытый текст
    Vec3f varying_intensity; // written by vertex shader, read by fragment shader
// [...]
    virtual bool fragment(Vec3f bar, TGAColor &color) {
        float intensity = varying_intensity*bar;   // interpolate intensity for the current pixel
        color = TGAColor(255, 255, 255)*intensity; // well duh
        return false;                              // no, we do not discard this pixel
    }



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

То есть, интерполированная интенсивность может быть посчитана как varying_intensity[0]*bar[0]+varying_intensity[1]*bar[1]+varying_intensity[2]*bar[2] или просто-напросто скалярным произведением между векторами varying_intensity*bar. В настоящем GLSL, конечно, шейдер получает уже готовое значение.

Обратите внимание, что фрагментный шейдер возвращает булевское значение. Его значение легко понять, если посмотреть внуть растеризатора (our_gl.cpp, triangle()):
Скрытый текст
            TGAColor color;
            bool discard = shader.fragment(c, color);
            if (!discard) {
                zbuffer.set(P.x, P.y, TGAColor(P.z));
                image.set(P.x, P.y, color);
            }



Шейдер может отказаться рисовать данный пиксель, тогда растеризатор игнорирует и его z-координату, не обновляя z-буфер. Полезно если мы хотим делать бинарные маски или что ещё вам в голову придёт.

Разумеется, растеризатору и в голову не может прийти то, что придёт в голову вам, поэтому он компилироваться с вашим шейдером заранее не может. Тут нам приходит на помощь абстрактный класс IShader. Уфф, нечасто я пользуюсь абстрактными классами, но тут ровно случай, когда без него было бы плохо. Передавать указатели на функции мне не хочется совсем!




Первая модификация


Скрытый текст
    virtual bool fragment(Vec3f bar, TGAColor &color) {
        float intensity = varying_intensity*bar;
        if (intensity>.85) intensity = 1;
        else if (intensity>.60) intensity = .80;
        else if (intensity>.45) intensity = .60;
        else if (intensity>.30) intensity = .45;
        else if (intensity>.15) intensity = .30;
        else intensity = 0;
        color = TGAColor(255, 155, 0)*intensity;
        return false;
    }



Просто я разрешаю некий фиксированный набор интенсивностей освещения. Вот результат его работы:
Скрытый текст





Текстурируем модель


Тонировку Фонга пропускаем, её подробно разобрали в комментариях, давайте наложим текстуры. Для этого придётся интерполировать uv-координаты. Ничего нового, просто добавляем матрицу в две строки (uv) и три столбца (текстурные координаты трё вершин).
Скрытый текст
struct Shader : public IShader {
    Vec3f          varying_intensity; // written by vertex shader, read by fragment shader
    mat<2,3,float> varying_uv;        // same as above

    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
        return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
    }
    
    virtual bool fragment(Vec3f bar, TGAColor &color) {
        float intensity = varying_intensity*bar;   // interpolate intensity for the current pixel
        Vec2f uv = varying_uv*bar;                 // interpolate uv for the current pixel
        color = model->diffuse(uv)*intensity;      // well duh
        return false;                              // no, we do not discard this pixel
    }
};



Скрытый текст



Normalmapping



Окей, теперь у нас есть текстурные координаты. Но ведь в текстурах можно хранить не только цвет, RGB вполне хватает для представления xyz!
Давайте загрузим вот такую текстуру, которая для каждого пикселя нашей картинки (а не только для вершин, как раньше!) даст вектор нормали.
Скрытый текст

Кстати, сравните его с такой картинкой, это та же самая информация, но в другом репере:
Скрытый текст


Одна из этих картинок даёт нормальные векторы в глобальной системе координат, а другая в касательной, которая определяется для каждой точки нашего объекта. В этой текстуре вектор z — это нормаль к объекту, вектор x — это вектор главного направления кривизны поверхности, а y — это их векторное произведение.

Упражнение 1

Скажите, какая из этих текстур дана в глобальных координатах, а какая в касательных к объекту?

Упражнение 2

Какой формат текстуры предпочтительнее — касательный или глобальный? Почему?

Пожалуйста, не стесняйтесь (не читая комментариев заранее) дать ответы на эти вопросы в комментариях!

Скрытый текст
struct Shader : public IShader {
    mat<2,3,float> varying_uv;  // same as above
    mat<4,4,float> uniform_M;   //  Projection*ModelView
    mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose()

    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
        return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
   }

    virtual bool fragment(Vec3f bar, TGAColor &color) {
        Vec2f uv = varying_uv*bar;                 // interpolate uv for the current pixel
        Vec3f n = proj<3>(uniform_MIT*embed<4>(model->normal(uv))).normalize();
        Vec3f l = proj<3>(uniform_M  *embed<4>(light_dir        )).normalize();
        float intensity = std::max(0.f, n*l);
        color = model->diffuse(uv)*intensity;      // well duh
        return false;                              // no, we do not discard this pixel
    }
};
[...]
    Shader shader;
    shader.uniform_M   =  Projection*ModelView;
    shader.uniform_MIT = (Projection*ModelView).invert_transpose();
    for (int i=0; i<model->nfaces(); i++) {
        Vec4f screen_coords[3];
        for (int j=0; j<3; j++) {
            screen_coords[j] = shader.vertex(i, j);
        }
        triangle(screen_coords, shader, image, zbuffer);
    }



Ключевое слово uniform в GLSL позволяет передавать в шейдеры константы, здесь я в шейдер передал матрицу Projection*Modelview и её обратную транспонированную для того, чтобы преобразовать нормальные векторы (см. предыдущую статью).
То есть, всё то же, что и раньше, только вектор нормали мы не интерполируем, а берём из заготовленной текстуры, не забыв при этом вектор направления света и вектор нормали преобразовать надлежащим образом.

Скрытый текст





Блестящие поверхности или specular mapping



Продолжаем разговор! Для (дешёвого) обмана глаза мы используем приближение Фонга для освещения модели. Итоговая засвеченность данного участка составляется из постоянного освещения для всей сцены (ambient lighting), освещённости для матовых поверхностей, которые мы считали до сих пор (diffuse lighting) и освещённости для глянцевых поверхностей (specular lighting):



Засвеченность матовых поверхностей мы считали как косинус угла между вектором нормали и вектором света. То есть, мы предполагали, что поверхность рассеивает свет примерно во всех направлениях. Что происходит для глянцевых поверхностей? В предельном случе (для зеркальных поверхностей) мы имеем засвет если из этого пикселя видим источник света.

Вот картинка:

Если для данной точки освещённость для матовых поверхностей мы считали как косинус угла между векторами n и l, то теперь нам интересен косинус угла между векторами r (отражённый свет) и v (направление взгляда).

Упражнение 3: найдите вектор r, имея векторы n и l


Скрытый текст
если n и l нормализованы, то r = 2n <n,l> — l


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

Скрытый текст
struct Shader : public IShader {
    mat<2,3,float> varying_uv;  // same as above
    mat<4,4,float> uniform_M;   //  Projection*ModelView
    mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose()

    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
        return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
    }

    virtual bool fragment(Vec3f bar, TGAColor &color) {
        Vec2f uv = varying_uv*bar;
        Vec3f n = proj<3>(uniform_MIT*embed<4>(model->normal(uv))).normalize();
        Vec3f l = proj<3>(uniform_M  *embed<4>(light_dir        )).normalize();
        Vec3f r = (n*(n*l*2.f) - l).normalize();   // reflected light
        float spec = pow(std::max(r.z, 0.0f), model->specular(uv));
        float diff = std::max(0.f, n*l);
        TGAColor c = model->diffuse(uv);
        color = c;
        for (int i=0; i<3; i++) color[i] = std::min<float>(5 + c[i]*(diff + .6*spec), 255);
        return false;
    }
};



Собственно, тут и объяснять нечего, кроме как коэффициентов. В строчке
        for (int i=0; i<3; i++) color[i] = std::min<float>(5 + c[i]*(diff + .6*spec), 255);

Я взял 5 для ambient, 1 для diffuse и .6 для specular. Какие именно брать — вам решать. Это даёт впечатление разных материалов. Чаще всего они даны художником, но в данном случае у меня их нет, поэтому я взял примерно от балды.

Скрытый текст


Заключение


Мы научились рендерить весьма правдобоподбные сцены, но освещение ещё далеко от идеала. В следующей статье я расскажу о том, что такое shadow mapping. В одной из ортогональных статей я расскажу о том, как работает новый растеризатор (ничто не мешает запустить этот же код на старом растеризаторе!).
@haqreu
карма
244,2
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (37)

  • +4
    Ура, снова торт!

    Скажите, какая из этих текстур дана в глобальных координатах, а какая в касательных к объекту?
    Первая в глобальных. В ней цвет (значения векторов) очень сильно изменяется от положения в пространстве.

    Какой формат текстуры предпочтительнее — касательный или глобальный? Почему?
    Касательный. Больше диапазон значений, хотя вроде бы для применения нужно на одно умножение векторов больше.
    • 0
      Действительно, синюшняя карта дана в касательной системе координат (репер Френе). А синюшняя она потому, что нормали в карте всё же обычно совпадают с нормалями к базовой поверхности (третья компонента репера).

      С обоснованием формата текстуры либо не понял, что вы имеете в виду, либо не согласен.
      • 0
        Наверно чушь. Идея была в том, что мы храним нормали в графическом файле в 8-битных каналах. Это значит, что на каждую координату у нас [0-255] значений. Если мы записываем координаты в глобальном формате, то теряется точность. Но теперь понимаю, что это просто направление, и кол-во возможны вариантов направления не меняется от точки отсчета нулевого направления.

        Тогда выгоднее в глобальном из-за лишенго умножения. Так?
        • +1
          Если использовать нормаль с единичной длиной, то имея 8 бит на канал вы не используете весь диапазон возможных значений, а только маленькую часть — поверхность сферы.
          Но есть простой хак: в шейдере распаковываем нормаль normalize(normal.xyz), сохраняя в текстуре полный диапазон значений из всех возможных векторов N^3.

          Геометрию модели в «глобальных координатах» (на самом деле в локальных координатах модели) сложно искажать — нормаль потребует коррекции. Отсюда вытекает дополнительная сложность при наложения декалей (например следов от пуль) и детальных текстур (высокочастотного шума).
        • –1
          Экономим умножение, да, но если будем использовать глобальную систему, то наткнемся на невозможность анимировать модель.
          • 0
            С чего это вдруг? При анимации просто умножаем на матрицу анимации 3х3 и всё (само собой если анимация в шейдере, сейчас трудно найти где она происходит не в шейдере). И при анимации в глобальных дополнительно выгоднее хранить т.к. ненужно хранить информацию о тангентах и бинормалях чтобы правильно развернуть нормаль. Хранить нормаль в виде касательных выгоднее только потому что точность выше.
            • +2
              Глобальные координаты проще, не надо считать репер Френе.

              Но текстуру в касательных координатах можно накладывать на любые модели. Например, если у вас есть кирпичная стена, то вы не будете хранить отдельную текстуру для всех возможных ориентаций стены в сцене. Вы сделаете один маленький кусочек касательной текстуры и все стены получат красивый нормалмап из одного и того же места.
              • 0
                Да точно забыл про такие варианты.
    • +1
      Все пишут, что касательный формат лучше, потому что глобальный не подразумевает искажение модели. Это так, но есть еще одна причина: касательный формат использует всего два цветовых канала — а это на треть меньше, чем глобальный. Одна из самых тяжелых операций на гпу — это чтение из текстуры, чем меньше данных — тем лучше. Особенно актуально для мобилок. И поэтому быстрее выполнить преобразование, чем читать дополнительную информацию.
      • 0
        Строго говоря, в глобальном случае тоже можно хранить только два числа, скажем фи и тета в обычных полярных координатах. Работать с этим, правда, не очень удобно.
      • 0
        Немного слукавил, на самом деле три канала. Просто в некоторых случаях синий можно опустить
  • –1
    Как модель в лице-то переменилась! А кстати всё равно узнал.
  • +2
    У вас ошибка в реализации работы с Z буфером.
    Вы делите вершины на [3] элемент сразу в вершинном шейдере. Этого делать нельзя, т.к. афинные преобразования линейны, а вот глубина у нас уже не линейна.
    Нужно интерполировать z и w отдельно, и делить попиксельно. То есть код вершинного шейдера переписать вот так:
        virtual Vec4f vertex(int iface, int nthvert) {
            Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
            gl_Vertex = Viewport*Projection*ModelView*gl_Vertex;     // transform it to screen coordinates
            varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity
            return gl_Vertex;                  // project homogenious coordinates to 3d
        }
    

    В коде функции triangle принимать Vec4f, и переделать проверку в буфере глубины как-то так:
    Vec3f c = barycentric(pts[0], pts[1], pts[2], P);
    float z = pts[0].z*c.x + pts[1].z*c.y + pts[2].z*c.z;
    float w = pts[0].w*c.x + pts[1].w*c.y + pts[2].w*c.z;
    P.z = std::max(0, std::min(255, z/w+.5));
    

    Случай, когда ваш подход с «сразу делением на w» не работает:
    Делаем 2 прямоугольника (горизонтальный и вертикальный) с вершинами:
    1 — Vec3f(-100.0, -1.0, 0.0) Vec3f(-100.0, 1.0, 0.0) Vec3f(100.0, -1.0, 0.0) Vec3f(100.0, 1.0, 0.0)
    2 — Vec3f(-1.0, -100.0, 0.1) Vec3f(-1.0, 100.0, 0.1) Vec3f(1.0, -100.0, 0.1) Vec3f(1.0, 100.0, 0.1)
    Крутим эту пару треугольников вокруг OY и наблюдаем артефакт.
    Поэтому все графические API получают 4 параметра, и сразу делить на W в вершинном шейдере ни в коем случае нельзя.

    p.s. Проблема растет оттуда же: habrahabr.ru/post/248611/#comment_8238683
    Хоть вы там и написали в ответе, что это «муторно» и ненужно, в реале же без этой коррекции мы получим некорректный буфер глубины. Так что коррекция нужна, и она не муторная.
    • 0
      Ммм, я ещё не проснулся, какой артефакт будет в вашей сцене из двух прямоугольников? Z-fighting?

      Что-то я не могу понять про нелинейность глубины.
      • 0
        Прямоугольник из-под низа будет плаво вылазить поверх вверхнего треугольника, когда будет становиться все параллельнее взгляду. Словами сложно описать, вы сделайте такую модельку, и покрутите, все сразу увидите.
        • 0
          Камера где должна быть?
        • 0
          Артефакта не вижу, но спасибо за замечание. Будет время — поправлю.
          Скрытый текст
          • 0
            Да, типа такого креста надо покрутить.
            У меня нет сейчас возможности показать на примере. Могу показать потерю перспективной информации на текстурах. Создаем пустой проект в RenderMonkey с текутрированный кубиком. Добавляем в вершинном шейдере gl_Position /= gl_Position.w;
            Результат такой. Слева без деления на w в вершинном. Справа — с делением на w:
            Скрытый текст


            p.s. Сейчас думаю, что насчет буфера глубины возможно был не прав (надо проверить), т.к. скорее всего у нас будет частный случай, при котором оно будет работать. Однако в общем случае нужно делать так, как я описал. Отсутствие деления на w в вершинном шейдере автоматом это решит проблему с текстурированием, т.к. текстурные координаты будут вычисляться по еще линейному xyz.
            • 0
              С глубиной всё в порядке, с текстурами согласен. Поправлю при случае, спасибо.

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

              • +2
                Выкладки у вас верные, но вы их делаете для вершин. И забываете что дальше у вас линейная интерполяция.
                После перспективного деления глубина должна меняться не линейно. Вот смотрите, слева сцена (так же без Y), а справа я нарисовал буфер глубины, какой он должен быть после перспективного деления:

                а зеленой линией нарисовано то, что сделает линейная интерполяция, т.е. что будет записано в буфере глубины в вашем случае.
                • 0
                  Да, вы правы. Если для отрезка (x0,y0,h0);(x1,y1,h1) сделать интерполяцию по-настоящему в 3d, то она будет прилично отличаться от интерполяции в 2d, если координаты h1 и h1 сильно отличаются.
                • 0
                  Поправил код, спасибо за замечание.
                  • 0
                    Так, задний ход, поправки неверные. Отдельная статья про коррекцию перспективного искажения на подходе.
  • +4
    Как странно у вас чайник отрендерился =)
  • 0
    На картинке с «формулой» Фонга справа ambient отсутствует.
    • 0
      Не совсем, засветы на правой картинке шире засветов на диффузной. Вопрос коэффициентов во взвешенной сумме.
  • +1
    Поменяйте, пожалуйста, «артиста» на «дизайнера» или «художника» хотя-бы… :)
  • +1
    Очень частая ошибка, которая повсеместно в интернете — забывают, что сумма коэффициентов Kd + Ks = 1. из-за этого модель начинает отражать больше света, чем получает от источника. У вас эта ошибка присутствует.
    • +2
      Я не делал вообще никаких предположений о моих материалах, поэтому это не ошибка.
      Более того, я ещё и модель с glow map выложу скоро, получил разрешение от художника.
  • +3
    Очень понравился ваш цикл статей, поэтому я, что называется, решил совместить полезное с полезным и написать рендерер на расте.

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



    Код выложен на гитхабе, может быть кому-то еще будет интересно. Или опытные «растаманы» подскажут мне, что можно улучшить :)
    • –1
      К тому же треугольник я отрисовываю не горизонтальными полосками, а вертикальными.
      Но зачем? Это может сильно ухудшить производительность.
      • +2
        Изначально начал так реализовывать, когда взялся самостоятельно писать метод. Производительность в принципе может пострадать, но массив с пикселями тоже можно перевернуть, да и задача здесь все-таки не в производительности.
    • 0
      т.к. раст не позволяет автоматически конвертировать числовые типы

      Раст позволяет это делать немного по-другому, но из-за бага в компиляторе, я до недавнего времени, пока не обошёл это, висел в первой части. Сегодня только закоммитил доделанные операции с числами, вот тут можно посмотреть: github.com/torkve/habropengl/blob/master/src/vec.rs

      В смысле, конвертировать конечно придётся, но всё в одном месте.
      • +1
        Спасибо, ваше решение весьма интересное и очень кстати, два unwrap без проверки, правда, немного смущают. Но вообще я больше жаловался на сложности работы с разными числовыми типами, где нужны постоянные касты. После других языков это кажется избыточным и некрасивым, хотя с другой стороны заставляет больше задумываться о дизайне кода.
        • 0
          Это на самом деле очень правильно, потому что в оригинальном коде, например, есть очень много кастов между u8/u32/i32, которые легко в определённых ситуациях могут выдать нам мусор, и единственное, что спасает — это допущение, что мы не будем рендерить картинки такого большого размера.
          Так что мои допущения с unwrap(), по большому счёту, такие же: я предполагаю, что мы будем инициализировать точки исключительно типами, которые можно свободно конвертировать в/из float, т.е. все те же стандартные численные типы с теми же условиями переполнения.
  • +1
    Касательно «тонировки Гуро» и вообще слова тонировка — оно в русском больше относится к колхозному тюнингу автомобилей, чем к графике. Стандартно это «закраска Гуро» или «модель освещения Гуро» или «затенение Гуро».
    • 0
      Мы друг друга поняли? Ну и славно. А слово я взял отсюда.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.