NVIDIA前段時間推出了NVIDIA RTX,使得光線追蹤這一古老而又年輕的技術再次進入人們的視野,相比傳統的光柵化

演算法,光線追蹤更加符合數學家對物理世界的描述,得益於這個技術我們能在遊戲或者電影裡面看到更加炫酷,更加真實,更激動人心的畫面。本文立足於Unity渲染API,以及Real Time Rending等參考資料,意在說明實現一個簡單的光線追蹤渲染器需要注意的點和主要的模塊,主要用於技術探索和交流。(文章所用原理圖片來自網路)

1.光線追蹤原理

1.1渲染方程

渲染方程可以簡單理解為:在一個光照環境中,一個物體w在不同觀察方向上的光照強度分布情況。本文後面的光照模型,phone模型等,都是對這個方程的簡單模擬。

渲染方程中,x為當前的物體,L(x,w) 表示,在x處,沿著w方向射出的光照強度,Le為自發光,後半部分是反射光。方程裡面的坐標表示都是球坐標。,球坐標的解釋如下,具體的計算和推導細節請讀者參考其他資料。

物理學中通常使用的球坐標(r, θ, φ)(ISO 約定):徑向距離 r,極角θ (theta) 與方位角φ (phi)。在數學裡,球坐標系(spherical coordinate system)是一種利用球坐標表示一個點P在三維空間的位置的三維正交坐標系。右圖顯示了球坐標的幾何意義:原點與點P之間的徑向距離(radial distance),原點到點P的連線與正z-軸之間的極角(polar angle),以及原點到點P的連線在xy-平面的投影線,與正x-軸之間的方位角(azimuth angle)。它可以被視為極坐標系的三維版本。

1.2光線追蹤過程

嚴格來講,光線追蹤應該叫做視線追蹤,其本質是逆物理過程,這裡的物理過程指的是:光線從光源發出,照射到物體上面通過屏幕進入相機,從而能夠成像。光線追蹤就是假設從攝像機發射一束視線穿過屏幕到物體上,觀察這個地方的光照結果,並且把這些結果疊加從而作為屏幕上當前點的顏色信息。這裡光照結果大體分為兩個部分,第一個部分指的是光照直接投射到物體上產生的結果,第二個部分指的是其他物體發出的光線對該物體的影響,這時候就需要對其他物體遞歸使用視線追蹤,最終把結果疊加。在本文中,光線的產生和相交運算採用Unity Api,主要是Physica.Raycast模塊,為了更好的性能這一部分操作理應在GPU中進行,這也是筆者後面的優化方向。

2.Unity常見API和概念

  • onRenderImage

當圖像已經渲染完成,相當於後期處理,這裡能夠拿到原始的圖片,然後通過3d數據進行圖像的修正。這個函數可以訪問當前正在渲染的圖像。這個函數只出現在Camera上的腳本

  • Graphics.Blit

後期處理的過程:在onRenderImage裡面拿到當前的渲染圖像,然後Blit傳遞給shader進行渲染,渲染完成之後會再次通過onRenderImage進行回調,這時候可以通過Blit函數渲染到屏幕上。sourceTexture會成為material的mainTexture。Blit(sourceTarget,des,material),渲染的時候會使用material裡面的shader。

  • Mesh

Mesh,網格 。 MeshFilter 用於獲取網格數據的組件。MeshRender,用於渲染網格的組件。因為mesh可能被多個模型使用,mesh是每個模型單獨的,sharedMesh則指的是共享的mesh,修改之後所有使用的mesh都會進行改變。Mesh實際上是三角形的點和邊的集合。

  • ComputeShader

需要判斷ComputeShader是否支持,ComputeShader不能掛在mesh上面,只能在腳本裡面調用。SetTexture 傳遞數據,Dispatch 函數名字,線程組個數,每個線程組的線程數目,每次處理的像素個數,StructBuffer可以雙向傳遞數據albedo

  • Awake

創建之後立即調用

  • Start

update之前調用

  • 部分Shader內置函數

UnityObjectToClipPos

本地坐標轉換成相機空間的坐標,光線追蹤裡面都是以相機空間作為基準。在頂點著色器拿到的都是本地坐標,計算的時候需要轉換。

unity_CameraInvProjection,攝像機投影矩陣的逆矩陣

  • 馮氏光照模型材質

l Albedo Color 反照顏色

l Metalness 金屬性

l Fresnel Color 菲涅爾顏色,反射顏色

l Roughness 粗糙程度 光滑程度

馮氏光照模型決定了光照的顏色

Albedo定義了物體的整體顏色

3.Unity光照系統參數

Unity的光照模型至關重要。Unity裡面有四種光源類型

  • Directional Light 方向光,類似太陽光的效果,消耗的系統資源最少
  • Point Light 點光源,類似蠟燭
  • Spotlight 聚光燈,蕾絲手電筒
  • Area Light 區域光,一般用於烘焙貼圖,在光線追蹤裡面可以用來產生軟陰影,這裡會涉及對光源進行採樣,但是不規則的光源計算起來太複雜,所以後面都會把光源模擬成球體來簡化計算。

4.參考資料

  • github.com/SIZMW/unity-

CPU計算,只計算了反射,但是對從模型獲取三角形,光源的處理有很強的借鑒意義。

  • blog.three-eyed-games.com

實現了GPU求交,採樣等等,優點是能夠借鑒GPU處理光線追蹤的框架。缺點是求交運算太簡單,材質處理也太簡單

  • 《Ray Trace In One Week》

對光線追蹤和圖像渲染有一個高屋建瓴的認識,能夠快速建立起一個大概的印象,但是只能渲染球體,其他進階的處理還需要額外看資料

  • 《Real Time Rending》

既有基本框架,也有高深的數據描述,但是沒有多少實踐部分

5.光線追蹤模型框架

參照前文所述的光線追蹤原理,我們可以得出一個光線追蹤模型的基本框架。

//參考 real time rending
rayTrace(){
for( p in pixels){
color of p = trace(eye ray through p);
}
}

trace(){
pt = find last intersection;
return shade(pt);
}

shade(point){
color = 0;
for(L in light sources){
trace(shadow ray to L);
color += evaluate BRDF;
}
color += trace(reflection ray);
color += trace(refraction ray);
return color;
}

6.法線插值計算

當渲染一個三角形的時候整個面的法線方向都是一致的,因此會出現稜角分明的效果,需要對法線進行插值。Mesh裡面包含了每個頂點的法線信息,頂點法線其實也是通過對面的計算然後加權平均計算的,raycast會返回重心坐標,這個坐標反應了重心分割的時候三塊面積的大小。以此對三角形面的法線進行插值。需要注意的是如何獲取頂點index,根據三角形的index,去mesh.triangles裡面查找。mesh裡面的信息都是本地坐標系統需要用transform進行轉換。

public void GetFixedNormal(Mesh mesh,ref Vector3 normal,int hIndex,Transform transform,Vector3 barycentricCoordinate)
{
Vector3[] normals = mesh.normals;
int[] triangles = mesh.triangles;
int trianglesLength = triangles.Length;
int normalLength = normals.Length;
int tIndex0 = hIndex * 3 + 0;
int tIndex1 = hIndex * 3 + 1;
int tIndex2 = hIndex * 3 + 2;
if (tIndex0 >= 0 && tIndex0 < trianglesLength && tIndex1 < trianglesLength && tIndex2 < trianglesLength)
{
int vIndex0 = triangles[tIndex0];
int vIndex1 = triangles[tIndex1];
int vIndex2 = triangles[tIndex2];
// Extract local space normals of the triangle we hit
if (vIndex0 < normalLength && vIndex1 < normalLength && vIndex2 < normalLength)
{
Vector3 n0 = normals[vIndex0];
Vector3 n1 = normals[vIndex1];
Vector3 n2 = normals[vIndex2];
// interpolate using the barycentric coordinate of the hitpoint
Vector3 baryCenter = barycentricCoordinate;
// Use barycentric coordinate to interpolate normal
Vector3 interpolatedNormal = n0 * baryCenter.x + n1 * baryCenter.y + n2 * baryCenter.z;
// normalize the interpolated normal
interpolatedNormal = interpolatedNormal.normalized;

// Transform local space normals to world space
Transform hitTransform = transform;
interpolatedNormal = hitTransform.TransformDirection(interpolatedNormal);

normal = interpolatedNormal;
}
}
}

7.局部光照模型簡單實現

using UnityEngine;
using System.Collections;
//光線處理工具
public class RayUtil
{

/*--------------------------------------局部光照模型------------------------------------
* 單一光源,特定BRDF下的推導。需要光線方向,光照強度,視線方向
*/

/*
* 反射光線在視線上的投影
* Phone模型 ks*ls*(v dot r)^n 高光係數*光照強度*(反射光線 點乘 視線 )^高光指數
*/
public static Vector3 PhoneLight(Vector3 lightDirection, Vector3 lightColor, float specular, float lightStrength, Vector3 normal, Vector3 viewDirection, float alpha)
{

return lightColor * specular * lightStrength *
Mathf.Pow(
(Vector3.Dot(Vector3.Normalize(Vector3.Reflect(lightDirection, normal)), Vector3.Normalize(viewDirection))),
alpha);
}

/*
* 漫反射,光照射到粗糙的表面的時候,均勻向四周反射,漫反射的光強與入射方向與法線的夾角餘弦成正比,因此此模型不涉及視線
*
* Lambert模型 kd*ld*(n dot l) 漫反射屬性*入射光強度*(入射單位法向量 dot 入射點指向光源的單位向量)
*/
public static Vector3 LambertLight(Vector3 lightDirection, Vector3 lightColor, float albedo, Vector3 normal,float ligthStrength)
{

return lightColor * albedo * Mathf.Max((Vector3.Dot(Vector3.Normalize(normal), Vector3.Normalize(lightDirection)))*0.5f+0.5f,0.0f);

}

public static Vector3 BlinnPhong(Vector3 lightDirection, Vector3 lightColor, float specular, float lightStrength, Vector3 normal, Vector3 viewDirection, float alpha)
{

return lightColor * specular * lightStrength * Mathf.Pow(Mathf.Max(Vector3.Dot(Vector3.Normalize(lightDirection + viewDirection ), Vector3.Normalize(normal)),0.0f), alpha);

}

public static float SmoothnessToAlpha(float s)
{
return Mathf.Pow(1000.0f, s * s);
}
}

8.軟陰影

在光線追蹤裡面,陰影的產生和判斷都是通過發射shadow ray判斷是否有障礙物來計算的,這樣的話其實陰影會有一個很明顯的邊界,在現實世界,光源不可能是一個點,方向光也不可能絕對平行,因此陰影會有一個平滑的過渡過程,這樣的陰影就是軟陰影。在光線追蹤裡面,軟陰影的實現採用發射shadow ray的時候在光源表面隨機採樣來實現,按理說應該用光源上的每個點進行計算,但是這樣計算量太大了。這裡光源就不是一個點(其他部分為了方便計算都抽象成一個點)。

Vector3 shadowRayDirection = ligthDirection + Random.onUnitSphere * 0.009f;
if (Physics.Raycast(hit.point, shadowRayDirection, 500))
{
return new Vector3(0.0f, 0.0f, 0.0f);
}

9.體積光渲染

體積光使用RayMarch的方式渲染,測試每一條光和光源之間的距離,採用合適的衰減和採樣函數來確定當前方向上的光線表現。實現如下:

目前光線的衰減函數是和距離平方成反比,這樣的函數衰減特別厲害,會導致光線到不了物體,後面需要優化。下圖是函數衰減曲線。

當物體在光線的陰影裡面的時候需要進行相交判斷,具體做法是從當前位置向光源發射一束光線,如果沒有碰撞說明沒有遮擋,這裡又個需要注意的地方,有可能會導致光源前後都形成遮擋區域這時候要根據碰撞距離來保留合適的那一個。

using System;
using System.Collections.Generic;
using UnityEngine;
/*
* 處理髮光物的丁達爾效應,默認為球體光源
*/
public class LightObject
{
public BoundingSphere boundSphere;
public static float MAX_LIGHT_DISTANCE = 2.0f;
//物體最靠近的顏色
public static float MIN_DISTANCE = 1.0E-2f;

public Vector3 mLightColor;

public static int MAX_SAMPLE_COUNT =180;

public float mStep = 0.0f;

public float mInnerDistance = MIN_DISTANCE;
public Vector3 mPosition;

public void Init(Vector3 color,Vector3 position,float length)
{
mPosition = position;
mLightColor = color;
mStep = 0.1f;
mInnerDistance = length;
}
/*
* 光線步進的方式獲取光照顏色
*/
public Vector3 RayMarch(Ray ray,float minDistance,float maxDistance)
{
float t = minDistance;
Vector3 result = new Vector3(0.0f,0.0f,0.0f);
int realCount = 0;
for(int i = 0; i < MAX_SAMPLE_COUNT && t >= minDistance && t <= maxDistance; i++)
{
Vector3 p = ray.GetPoint(t);
if (!InShadow(p))
{
result += GetSampleColor(Vector3.Distance(p, GetPosition()));
realCount++;
}
t += GetStep();
}
result /= (realCount + 0.0f);
return result;
}
public Vector3 GetSampleColor(float distance)
{
return mLightColor * (mInnerDistance / (distance + 0.0f));
}
public Vector3 GetPosition()
{
return mPosition;
}
public float GetStep()
{
return mStep;
}
public Vector3 GetLightColor()
{
return mLightColor;
}
public bool InShadow(Vector3 point)
{

Vector3 direction = (mPosition - point).normalized;
RaycastHit hit;
if (Physics.Raycast(point ,direction ,out hit,100f))
{
return hit.distance >= 0.0f;
}
else
{
return false;
}
}
}

11.初步結果

  • 採用圖像二次採樣,即關閉所有光照,環境光調整為1,然後在需要圖像顏色的時候對原始圖片進行採樣,這樣其實不正確,但是也能得到一個近似結果

  • 上面那種方法,對陰影採樣進行收斂之後的結果

  • 上面的方法,材質全光滑的結果

  • 下面都是正常處理,採用Unity的材質系統處理顏色。單一方向光

  • alpha = 3.2f,specular = 1.1f,可以看到出現了光斑

  • 修改成B-Phong模型

  • 根據Mesh讀取顏色

  • 法線插值效果

  • 一些模型

  • 軟陰影,50條光線

  • 軟陰影 400條光線

  • 無限制體積光

12.後期的構想

一方面需要提升性能,因此我打算在GPU裡面實現相交,採樣等運算,Unity仍然負責提供材質的解析,一些初始化的工作。另一方面需要完善自發光物體的效果,因此引入了體積光渲染,但是要渲染一個不規則的自發光物體,還需要對體積光渲染進行修正,比如可以對發光物體先進行一次發光物體的求交運算。細節問題歡迎私信交流討論。

推薦閱讀:

相关文章