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

Улучшение кода Ну вот наш краткий курс подходит к концу, задача на сегодня — научиться отрисовывать тени (внимание, просчёт полутеней — это отдельная тема): 50de2abe990efa345664f98c9464a4c8.pngКак всегда, код доступен на гитхабеДо сих пор мы умели затенять выпуклые объекты благодаря нормалям на поверхности, но для невыпуклых объектов наши рендеры давали неверный результат, почему правое (для нас левое) плечо демона освещено? Почему на левой щеке нет тени от рога? Непорядок.b4af24130ecb1536703e4793308af425.png

Идея очень простая: будем рендерить в два прохода. Если мы в первый раз отрендерим картинку, поставив камеру на место источника света, то мы будем точно знать, какие места освещены. А затем во второй проход мы будем использовать результат работы первого прохода. Трудностей тут почти нет. Давайте напишем вот такой шейдер:

Скрытый текст 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*¾, height*¾); projection (0);

DepthShader depthshader; Vec4f screen_coords[3]; for (int i=0; infaces (); 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. Обратите внимание, что самой последней строчкой я сохраняю матрицу перехода из координат объекта в экранные координаты.

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

Скрытый текст f743999b9d21aee9d0704c4036e18dce.png Второй проход я делаю при помощи другого шейдера:

Скрытый текст 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](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(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]

Как выглядит вызов второго шейдера в 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*¾, height*¾); 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; infaces (); 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().

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

Скрытый текст 164be1dce9e980d47a90159103b954a3.png Что это? Этот артефакт известен как борьба за z. Если пиксель должен быть освещён, то именно его z-координата должна быть в z-буфере теневого шейдера. Или это должно быть z-значение соседнего пикселя? В общем, разрешения нашего z-буфера не хватает, чтобы дать картинку без артефактов. Мы будем бороться с этой проблемой методом грубой силы:

float shadow = .3+.7*(shadowbuffer[idx]

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

© Habrahabr.ru