Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 6 из 6
Улучшение кода Ну вот наш краткий курс подходит к концу, задача на сегодня — научиться отрисовывать тени (внимание, просчёт полутеней — это отдельная тема): Как всегда, код доступен на гитхабеДо сих пор мы умели затенять выпуклые объекты благодаря нормалям на поверхности, но для невыпуклых объектов наши рендеры давали неверный результат, почему правое (для нас левое) плечо демона освещено? Почему на левой щеке нет тени от рога? Непорядок.
Идея очень простая: будем рендерить в два прохода. Если мы в первый раз отрендерим картинку, поставив камеру на место источника света, то мы будем точно знать, какие места освещены. А затем во второй проход мы будем использовать результат работы первого прохода. Трудностей тут почти нет. Давайте напишем вот такой шейдер:
Скрытый текст 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; i
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]
Эта матрица позволит мне превратить экранные координаты текущего шейдера в экранные координаты уже отрисованного теневого буфера! О том, как мы её считаем, в следующем абзаце. Давайте посмотрим, как мы её используем, обратим внимание на вот эти четыре строчки шейдера:
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; i Всё бы было хорошо, если б не безделица: девятнадцать пополам, кажется, не делится. Вот результат работы нашего двухпроходного рендера: Скрытый текст
Что это? Этот артефакт известен как борьба за z. Если пиксель должен быть освещён, то именно его z-координата должна быть в z-буфере теневого шейдера. Или это должно быть z-значение соседнего пикселя? В общем, разрешения нашего z-буфера не хватает, чтобы дать картинку без артефактов. Мы будем бороться с этой проблемой методом грубой силы:
float shadow = .3+.7*(shadowbuffer[idx]
В качестве бонуса к краткому курсу в следующий раз я покажу, как считать касательный базис к нашей поверхности (чтобы использовать текстуры, заданные в tangent space) и заодно напишем простой шейдер, умеющий работать со светящимися объектами (см. кристалл в голове у диаблы): Samuel Sharit очень любезно предоставил нам эту модель, разумеется, её можно использовать без его специального разрешения только в рамках этого учебного курса, равно как и модель головы негра, сделанную Vidar Rapp.