Update:git地址 https://github.com/mahiru23/raytrace
本文的根本目標是在WebGL中使用GLSL實現光線追蹤,無圖(懶得放了),僅供參考。
在一切開始之前,我們預設對GLSL的基本語法有所瞭解,不理解請自行查詢。
一些需要重點關注的東西,請確認自己完全明白這一點再繼續:
MVP變換:模型座標空間 – 世界座標空間 – 相機座標空間 – 投影座標空間。
在工作的時候一定要明白自己當前是在哪個座標系下進行操作。
OpenGL 光線追蹤其一:Phong光照模型
在渲染管線中,模型轉換之後的工作就是Illumination(光照,這個過程我們也叫shading,著色)。簡單來說,本階段的工作就是對於光照的處理。
在計算機圖形學中,對於光源,我們模擬的是光子(photon)。
一些物理概念:
albedo(反照率):衡量物體對光線的反射水平,the average reflectivity of the surface ornaments
flux(通量) : Radiation flux (electromagnetic flux, radiant flux)
Radiance(輻射) – radiant flux per unit solid angle per unit projected area,在圖形學的角度上就是測量物體上某個點的反光量
Irradiance(輻照度) – differential flux falling onto differential area,理解為電磁輻射入射於曲面時每單位面積的功率
此處我們採用的Phong模型是BRDF反射模型,這一階段我們單純考慮反射,不考慮透明材質或次平面反射等。Phong光照模型在片元著色器中依賴於插值進行計算。
光源分為方向光源(平行光源不衰減)、點光源(隨距離衰減)、面光源(蒙特卡洛法)等,本文基本採用單個點光源的方式進行處理,其他型別的光源暫時不考慮。
Phong光照模型
跳過繁雜的理論推導,我們直接從實現層面簡單說明,Phong光照模型由以下三部分組成:
- Ambient:環境光源,不管什麼遮擋/陰影等效果,所有位置都需要附上。在實際的視覺效果上提供基礎照明,保證你不會得到一大片黑色。
- Specular Reflectance:鏡面反射,在實際的視覺效果上提供高光,表現為物體表面的亮斑(如果有的話),一般在鏡面材質上較強。
- Diffuse Reflectance:漫反射,一般是粗糙表面較多,在實際的視覺效果上表現為一大片暈染開的光。
最終得到的光照數值就是把以上三個光照加起來,簡單粗暴,但是有效。
在此處,我們使用vec4型別的變數進行處理,最後一位沒有實際意義,具體看個人實現。
假設n是平面法向量,l是光線方向,d是光源點到光照點的距離,v指向視點位置,r是反射後的光線。其中outNormal和outPosition從頂點著色器中傳過來。Ambient/ diffuse/ specular為三個提前設定的光照引數,按需調整。
程式碼如下:
vec3 ads()
{
vec3 n = outNormal;
vec3 l = normalize(lightPositionCam - outPosition);
float d = length(lightPositionCam - outPosition);
vec3 v = normalize(-outPosition);
vec3 r = reflect(-l, n);
float lightIntensity = light/(4.0*3.1415926*d*d);
vec4 res = ambient +
lightIntensity * (diffuse * max(dot(l, n), 0.0) +
specular * pow(max(dot(r,v), 0.0), shininess));
return vec3(res);
}
OpenGL 光線追蹤其二:Texture紋理
在接下來的實際光線追蹤中,我們期待能夠讓我們的玻璃球反射/折射出外部的貼圖。
但如果你只關心如何實現光線追蹤,而不考慮貼圖效果,那麼這一步可以跳過。
最簡單的貼圖只需要一張照片,使用六張照片構成一個Box,將我們的玻璃球放在其中。
請直接從這裡:https://github.com/JoeyDeVries/LearnOpenGL 獲取對應的texture資源。其中提供了很多基礎的材質貼圖和場景貼圖。
將貼圖應用到物體:如果是普通的model可以直接直接texture。如果你需要將一張貼圖應用到球體/非實體平面/……等幾何結構,或許需要做一些相應的幾何變換以適應。
此外,一些其他的貼圖(如凹凸貼圖等)也進行了實現,此處暫不說明。
這是一個對於球體的典型貼圖,textureSampler2自行匯入:
uniform sampler2D textureSampler2;
intersec.colour = mix(thisSphere.colour, vec3(texture(textureSampler2,vec2(0.5+asin(intersec.normal.x)/(3.14),0.5-asin(intersec.normal.y)/(3.14)))), 0.5);
這個是對於平面的,這裡直接使用了黑白格子:
if((modx<modbase/2.0 && modz<modbase/2.0) || (modx>=modbase/2.0 && modz>=modbase/2.0))
intersec.colour = 1.0 - thisPlane.colour;
else
intersec.colour = thisPlane.colour;
OpenGL 光線追蹤其三:反射(reflect)、折射(refract)和陰影(shadow)
光線追蹤就是從視點出發,逆向查詢投射進視點的光路。
這個過程是遞迴的,有對應的生成樹,每一層都對應著反射(reflect)、折射(refract)和陰影(shadow)三個操作。
實際實現中大部分邏輯在片元著色器中實現。
第一步:求交點
這裡我們只使用了球體和平面,
對於球體:顯然,解線和球體相交的方程會有兩個解,這裡我們暫且稱它們為u1和u2。這裡我們只取u2,原因是我們這裡不考慮一束光線在球體內部反覆反射/折射,只有與u1相交的那個點才是合法的。
首先判斷是否有交點(方程有解),已經交點是否合法(u1>0),在光線路徑上而不是反向延長線上。之後取對應位置的貼圖顏色。
Intersection sphereIntersection(vec3 p0, vec3 d, Sphere thisSphere) {
Intersection intersec;
vec3 ps = thisSphere.centre;
float r = thisSphere.radius;
vec3 delta_p = p0 - ps;
float temp = pow(dot(d, delta_p), 2.0) - pow(length(delta_p), 2.0) + r*r;
float u1 = -dot(d, delta_p) - sqrt(temp);
float u2 = -dot(d, delta_p) + sqrt(temp);
if((temp <= 0.001 || u1 <= 0.001)) {
intersec.flag = false;
return intersec;
}
// calculate the Intersection
intersec.flag = true;
intersec.u = u1;
intersec.position = p0 + u1 * d;
intersec.normal = normalize(intersec.position - ps);
intersec.colour = thisSphere.colour;
return intersec;
}
對於平面:與球體同理,但是隻可能有一個交點,所以只需要取唯一的交點即可。
Intersection planeIntersection(vec3 p0, vec3 d, Plane thisPlane) {
Intersection intersec;
vec3 p1 = thisPlane.point;
vec3 n = thisPlane.normal;
float u = -dot(p0-p1, n)/dot(d, n);
if(u <= 0.001) { // add offset to avoid self-shadowing
intersec.flag = false;
return intersec;
}
// calculate the Intersection
intersec.flag = true;
intersec.u = u;
intersec.position = p0 + u * d;
intersec.normal = n;
float modbase = 1.0;
float modx = mod(intersec.position.x, modbase);
float modz = mod(intersec.position.z, modbase);
// checkerboard pattern results even-odd cross grid
if((modx<modbase/2.0 && modz<modbase/2.0) || (modx>=modbase/2.0 && modz>=modbase/2.0))
intersec.colour = vec3(0.7, 0.7, 0.7);
else
intersec.colour = vec3(0.3, 0.3, 0.3);
return intersec;
}
這裡沒有使用貼圖,球體和平面均返回預設顏色。
第二步:找最近的交點
從第一步中返回的交點列表中選取最近的那個,若無交點則直接從該迴圈中返回。
for(int i=0; i<sphere_num+1; i++) {
if(intersectionList[i].flag == true) {
if(valid_flag == -1) {
closeIntersection = intersectionList[i];
min_u = intersectionList[i].u;
valid_flag = i;
}
else if(intersectionList[i].u < min_u) {
closeIntersection = intersectionList[i];
min_u = intersectionList[i].u;
valid_flag = i;
}
else {
;
}
}
}
if(valid_flag == -1) { // no valid intersection
Result[res_pos] = vec3(0.0);
res_pos++;
if(depth != maxRayTraceDepth-1) {
new_ray_list[now_list_size] = defalult_ray();
new_ray_list[now_list_size+1] = defalult_ray();
now_list_size += 2;
}
continue;
}
第三步:檢測陰影
陰影的檢測方法是做該點與光源點的連線,檢查中間是否有其他的物體,如果有則設為交點。
陰影僅考慮在單個點光源下的表現形式,不考慮多點光源帶來的軟陰影(soft shadow)問題。
// Shadow
bool shadow_check(Intersection intersection) {
// slightly move the ray origin outwards of the object along the surface normal
vec3 p0 = vec3(lightPosition);
vec3 d = normalize(intersection.position - vec3(lightPosition));
float u_this = length(intersection.position - vec3(lightPosition));
float min_u = 100000.0;
bool flag_sphere = false;
for(int i=0; i<sphere_num; i++) {
Intersection intersectionSphere = sphereIntersection(p0, d, sphere[i]);
if(intersectionSphere.flag == true) {
flag_sphere = true;
min_u = min(min_u, intersectionSphere.u);
//break;
}
}
Intersection intersectionPlane = planeIntersection(p0, d, plane);
if(intersectionPlane.flag == true) {
min_u = min(min_u, intersectionPlane.u);
}
if(intersectionPlane.flag == false && flag_sphere == false) {
return false;
}
if(min_u < u_this-0.001) {
return true;
}
return false;
}
第四步:反射/折射
反射和折射可以用glsl的內建函式實現。需要注意的是,對於反射和折射的光線佔比,可以根據物體折射率和光線角度,使用菲涅爾定律進行求解。
增加球體內部的反射和折射沒什麼必要,效能消耗過大了。
Ray Refract(Ray ray, Intersection intersection) {
Ray new_ray;
new_ray.p0 = intersection.position - intersection.normal*0.002; // another side
new_ray.d = normalize(refract(ray.d, intersection.normal, 0.6));
new_ray.intensity_k = ray.intensity_k*(1.0-k_fresnel);
return new_ray;
}
Ray Reflect(Ray ray, Intersection intersection) {
Ray new_ray;
new_ray.p0 = intersection.position + intersection.normal*0.001;
new_ray.d = normalize(reflect(ray.d, intersection.normal));
new_ray.intensity_k = ray.intensity_k * k_fresnel;
return new_ray;
}
影像效果
需要注意的問題:
在取交點的時候,記得沿法線外表面稍微延伸一小段距離(如0.001),否則由於浮點數精度丟失問題會出現粗糙表面。
在glsl中,由於該語言不支援遞迴,只能使用迴圈替代遞迴操作。因此需要使用額外的棧空間來進行過程中的中間值儲存,並在完成後統一累加結果。
個人實際測試,遞迴到三層就會有很好的視覺效果,且光線追蹤操作對於效能的消耗是隨著生成樹指數級遞增的,因此儘量減少額外空間的使用(否則會爆視訊記憶體),追蹤3-4次即可,實測RTX2070的最大追蹤深度在6-7層,謹慎使用過深。
final
毫無疑問這個實現效果是比較拉的,可以考慮增加菲涅爾效果/模糊陰影/抗鋸齒超取樣等等......理論確實是學了不少,但是限於工期,也不可能每一項都實現,畢竟也不打算從事圖形學,只能說是豐富一下眼界罷了。
感謝看到這裡,歡迎批評指正。