Содержание основного курса
- Статья 1: алгоритм Брезенхэма
- Статья 2: растеризация треугольника + отсечение задних граней
- Статья 3: Удаление невидимых поверхностей: z-буфер
- Статья 4: Необходимая геометрия: фестиваль матриц
- Статья 5: Пишем шейдеры под нашу библиотеку
- Статья 6: Чуть больше, чем просто шейдер: просчёт теней
Улучшение кода
- Статья 3.1: Настала пора рефакторинга
- красивый класс матриц
- как работает новый растеризатор
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