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

Зміст основного курсу


Поліпшення коду


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;
}

віртуальний 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;
}

віртуальний 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.0 f), 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.