Пользователь
0,0
рейтинг
30 января 2015 в 18:26

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

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




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






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




Shadow mapping


Ну вот наш краткий курс подходит к концу, задача на сегодня — научиться отрисовывать тени (внимание, просчёт полутеней — это отдельная тема):



Как всегда, код доступен на гитхабе

До сих пор мы умели затенять выпуклые объекты благодаря нормалям на поверхности, но для невыпуклых объектов наши рендеры давали неверный результат, почему правое (для нас левое) плечо демона освещено? Почему на левой щеке нет тени от рога? Непорядок.



Идея очень простая: будем рендерить в два прохода. Если мы в первый раз отрендерим картинку, поставив камеру на место источника света, то мы будем точно знать, какие места освещены. А затем во второй проход мы будем использовать результат работы первого прохода. Трудностей тут почти нет. Давайте напишем вот такой шейдер:
Скрытый текст
struct DepthShader : public IShader {
    mat<3,3,float> varying_tri;

    DepthShader() : varying_tri() {}

    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_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3]));
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor &color) {
        Vec3f p = varying_tri*bar;
        color = TGAColor(255, 255, 255)*(p.z/depth);
        return false;
    }
};


Этот шейдер просто рисует содержимое z-буфера во фрейм-буфере. Вызываю я этот шейдер из main():
Скрытый текст
    { // rendering the shadow buffer
        TGAImage depth(width, height, TGAImage::RGB);
        lookat(light_dir, center, up);
        viewport(width/8, height/8, width*3/4, height*3/4);
        projection(0);

        DepthShader depthshader;
        Vec4f screen_coords[3];
        for (int i=0; i<model->nfaces(); i++) {
            for (int j=0; j<3; j++) {
                screen_coords[j] = depthshader.vertex(i, j);
            }
            triangle(screen_coords, depthshader, depth, shadowbuffer);
        }
        depth.flip_vertically(); // to place the origin in the bottom left corner of the image
        depth.write_tga_file("depth.tga");
    }

    Matrix M = Viewport*Projection*ModelView;



Я ставлю камеру на место источника света (lookat(light_dir, center, up);) и делаю рендер. Z-буфер этого прохода рендеринга сохранён по указателю shadowbuffer. Обратите внимание, что самой последней строчкой я сохраняю матрицу перехода из координат объекта в экранные координаты.

Вот результат работы этого шейдера, первый проход рендеринга закончен.
Скрытый текст


Второй проход я делаю при помощи другого шейдера:

Скрытый текст
struct Shader : public IShader {
    mat<4,4,float> uniform_M;   //  Projection*ModelView
    mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose()
    mat<4,4,float> uniform_Mshadow; // transform framebuffer screen coordinates to shadowbuffer screen coordinates
    mat<2,3,float> varying_uv;  // triangle uv coordinates, written by the vertex shader, read by the fragment shader
    mat<3,3,float> varying_tri; // triangle coordinates before Viewport transform, written by VS, read by FS

    Shader(Matrix M, Matrix MIT, Matrix MS) : uniform_M(M), uniform_MIT(MIT), uniform_Mshadow(MS), varying_uv(), varying_tri() {}

    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        Vec4f gl_Vertex = Viewport*Projection*ModelView*embed<4>(model->vert(iface, nthvert));
        varying_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3]));
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor &color) {
        Vec4f sb_p = uniform_Mshadow*embed<4>(varying_tri*bar); // corresponding point in the shadow buffer
        sb_p = sb_p/sb_p[3];
        int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array
        float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]); 
        Vec2f uv = varying_uv*bar;                 // interpolate uv for the current pixel
        Vec3f n = proj<3>(uniform_MIT*embed<4>(model->normal(uv))).normalize(); // normal
        Vec3f l = proj<3>(uniform_M  *embed<4>(light_dir        )).normalize(); // light vector
        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);
        for (int i=0; i<3; i++) color[i] = std::min<float>(20 + c[i]*shadow*(1.2*diff + .6*spec), 255);
        return false;
    }
};



Это практически один-в-один шейдер из конца предыдущей статьи, за одним исключением:
я объявил константную матрицу, которая не меняется во время работы ни вершинного, ни фрагментного шейдеров mat<4,4,float> uniform_Mshadow.

Эта матрица позволит мне превратить экранные координаты текущего шейдера в экранные координаты уже отрисованного теневого буфера!

О том, как мы её считаем, в следующем абзаце. Давайте посмотрим, как мы её используем, обратим внимание на вот эти четыре строчки шейдера:

        Vec4f sb_p = uniform_Mshadow*embed<4>(varying_tri*bar); // corresponding point in the shadow buffer
        sb_p = sb_p/sb_p[3];
        int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array
        float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]);


varying_tri*bar даёт мне экранные координаты текущего фрагмента, что мы отрисовываем. Мы их погружаем в однородные координаты, преобразовываем нашей магической матрицей uniform_Mshadow и та-дам, мы знаем xyz координаты в пространстве теневого шейдера, что мы использовали для первого прохода. Теперь для того, чтобы понять, освещена данная точка или нет, нам достаточно сравнить её z-координату и значение z-буфера из первого прохода!

Как выглядит вызов второго шейдера в main()? Всё достаточно стандартно:

    Matrix M = Viewport*Projection*ModelView;

    { // rendering the frame buffer
        TGAImage frame(width, height, TGAImage::RGB);
        lookat(eye, center, up);
        viewport(width/8, height/8, width*3/4, height*3/4);
        projection(-1.f/(eye-center).norm());

        Shader shader(ModelView, (Projection*ModelView).invert_transpose(), M*(Viewport*Projection*ModelView).invert());
        Vec4f screen_coords[3];
        for (int i=0; i<model->nfaces(); i++) {
            for (int j=0; j<3; j++) {
                screen_coords[j] = shader.vertex(i, j);
            }
            triangle(screen_coords, shader, frame, zbuffer);
        }
        frame.flip_vertically(); // to place the origin in the bottom left corner of the image
        frame.write_tga_file("framebuffer.tga");
    }

Напоминаю, что матрица M — это матрица преобразования координат объекта в экранные координаты теневого буфера. Мы ставим камеру на место, где она и должна быть, настраиваем вьюпорт и параметры перспективной проекции, и объявляем шейдер второго прохода рендеринга.

Мы знаем, что Viewport*Projection*ModelView — это матрица преобразования координат объекта в экранные координаты второго шейдера. Но нам надо знать матрицу преобразования экрана второго шейдера в экран первого шейдера. Это просто: (Viewport*Projection*ModelView).invert() преобразует экран второго шейдера в объектные координаты, а затем умножим просто на М, получив финальную матрицу преобразования как M*(Viewport*Projection*ModelView).invert().

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


Что это? Этот артефакт известен как борьба за z. Если пиксель должен быть освещён, то именно его z-координата должна быть в z-буфере теневого шейдера. Или это должно быть z-значение соседнего пикселя? В общем, разрешения нашего z-буфера не хватает, чтобы дать картинку без артефактов. Мы будем бороться с этой проблемой методом грубой силы:
        float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]+43.34); // magic coeff to avoid z-fighting

Я сдвигаю один z-буфер относительно другого на некую константу, которой достаточно, чтобы этот артефакт исчез. Да, это порождает новые артефакты (какие?), но существенно менее заметные глазу. Всё, результат работы нашей программы виден на заглавной картинке.

Поздравляю, наш краткий курс подошёл к концу. Мы с нуля написали весьма неплохой, как мне кажется, аналог OpenGL.

А ты записался добровольцем?


В качестве бонуса к краткому курсу в следующий раз я покажу, как считать касательный базис к нашей поверхности (чтобы использовать текстуры, заданные в tangent space) и заодно напишем простой шейдер, умеющий работать со светящимися объектами (см. кристалл в голове у диаблы):



Samuel Sharit очень любезно предоставил нам эту модель, разумеется, её можно использовать без его специального разрешения только в рамках этого учебного курса, равно как и модель головы негра, сделанную Vidar Rapp.
@haqreu
карма
244,2
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • +9
    Когда же я привыкну к всяким страшным головам при открытии хабра)
    А по сабжу: спасибо за курс! Жду бонуса)
  • +3
    Огромное спасибо за основы основ!

    Надеюсь что это далеко не все, чем вы можете поделиться с сообществом ;) Это не приглашение сразу писать что-то новое, но робкая надежда.
  • 0
    В примере на гитхабе в Makefile в CPPFLAGS стоит также добавить -O3, иначе оптимизатор вхолостую работает.

    Эх, где вы были, когда я был студентом:)

    У меня не странный результат — где-то ошибки округления:

    • +1
      Проблема со specular map у диаблы, посмотрите его возведение неважно чего в степень ноль. Сколько моделей, столько коэффициентов. У нас неполные данные :)
  • 0
    Учу ваш курс на малинке. Сейчас на втором уроке и голову треугольниками рисует довольно быстро. С ужасом представляю, сколько времени будет рендериться чорт :)
    Спасибо за курс, кроме графики и математики разберусь немного в C++ и Makefile.
  • –13
    А можно прятать эти страшные рожи под кат?
    Надоело уже видеть их на главной какую неделю.
    • +2
      Когда будет урок про моделирование шерстяного покрова, тогда автор опубликует котиков. Потерпите года 2.
    • +5
      Я, конечно, знаю, что я не Аполлон, но все же я ещё не привык, чтобы в мой адрес говорили «страшная рожа».

      • 0
        Рогатые демоны и безглазые негры же в статьях, нет?
      • 0
        Между прочим, похоже на классическую римскую скульптуру :)
        Например,
        image
        image
  • +1
    Спасибо огромное за материал!

    А можно попросить еще объяснить алгоритм (ну или хотя-бы ссылки в инете на что посмотреть) для вот такой простой задачки 2D (не 3D):

    имеем выпуклый 4-х угольник и квадратную текстуру, надо натянуть текстуру на данный 4-х угольник причем с корректными искажениями:
    image
    • +1
      Вам нужно рисовать быстро или просто?
      • +1
        лучше просто, убыстрить я потом сам смогу, я думаю :)
        • +2
          Если просто, то проходите по каждому пикселю в текстуре, имеете его (u,v) координаты. Затем делаете билинейную интерполяцию в экранном прямоугольнике (находите две точки на противоположных рёбрах, что соответствуют координате u, две точки на двух других противолежащих, что соответствуют координате v). Считаете пересечение двух отрезков, это экранные коордианты пикселя из текстуры.

          Недостаток следующий: на экране вы можете несколько раз закрасить один и тот же пиксель.

          • 0
            Спасибо! Действительно не сложно. До этого что читал — там буфер строят какой-то специальный — видимо как раз чтобы не рисовать несколько раз один пиксель. Попробую реализовать.
            • +1
              Если четырёхугольник растягивает текстуру, то может понадобиться проходить с субпиксельным шагом по оригинальной текстуре, чтобы на экране не было дырок. В общем, преобразование uv->xy быстрое и безболезненное, xy->uv требует небольшой гимнастики.
    • +1
      Да просто работайте с квадратом, а не с треугольниками и все.
      Тот же сканлайн даст вам корректный результат в этом случае.
      Косяк из-за того, что два треугольника обрабатываются независимо.
      Если же вы будете работать с квадратом и использовать все четыре точки, то результат будет корректный.
  • 0
    Скажите, вот такой эффект справа (в углу левого глаза и в углу губ) — это из-за того, что я не считаю тень от головы, или у меня просто где-то косяк?
    • 0
      в тёмной части лица? это нормально, читайте тут: https://habrahabr.ru/post/249139/
  • 0
    Скажите, почему вы умножаете на матрицу Mshadow во втором фрагментном шейдере вместо того, чтобы заново считать значение shadow-буфера во втором вершинном шейдере (какой-нибудь varying_shadow) и потом интерполировать во втором фрагментном шейдере? Есть какая-нибудь причина для этого, или просто так было проще?

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