《一周學習光線追蹤》(四)光源和矩形

《一周學習光線追蹤》(四)光源和矩形

來自專欄從光柵到光線追蹤

第六章:矩形和光源

接下來就該製作光源了

我們首先需要一個發光的材質:

首先我們需要重寫Material基類,添加發光emitted函數

public abstract class Material { public virtual Color32 emitted(float u,float v,Vector3 p)=>new Color32(0,0,0); //...省略其他函數...// }

之後我們需要創建一個發光材質

public class DiffuseLight : Material { private readonly Texture texture; private float intensity; public DiffuseLight(Texture t,float i) { texture = t; intensity = i; } public override bool scatter(Ray rayIn, HitRecord record, ref Color32 attenuation, ref Ray scattered)=>false; public override Color32 emitted(float u, float v, Vector3 p)=>texture.value(u, v, p)*intensity; }

我們需要更改主渲染函數:添加emitted顏色的計算。

為了測試,我們先將環境顏色設置為黑色,稍後可以改過來

//Renderer.cs private Color32 Diffusing(Ray ray, HitableList hitableList, int depth) { var record = new HitRecord(); if (hitableList.Hit(ray, 0.0001f, float.MaxValue, ref record)) { var r = new Ray(Vector3.zero, Vector3.zero); var attenuation = Color32.black; var emitted = record.material.emitted(record.u, record.v, record.p); if (depth >= MAX_SCATTER_TIME || !record.material.scatter(ray, record, ref attenuation, ref r)) return emitted; var c = Diffusing(r, hitableList, depth + 1); return new Color32(c.r * attenuation.r, c.g * attenuation.g, c.b * attenuation.b)+emitted; } var t = 0.5f * ray.normalDirection.y + 1f; //為了測試,我們先將環境顏色設置為黑色,稍後可以改過來 //return (1 - t) * new Color32(1, 1, 1) + t * new Color32(0.5f, 0.7f, 1); return Color32.black; }

我們可以使用DiffuseLight材質來創建各種發光的物體

world.list.Add(new Sphere(new Vector3(0, 0, 0), 1f, new DiffuseLight(new ConstantTexture(new Color32(1, 1, 1, 1)), 10)));

各種效果

接下來我們要創建矩形了。

矩形只是一個平面,它的定義只包含空間的兩對角頂點的坐標。

我們 用x0 x1 y 0 y1 來定義這個平面的2D坐標,用k值確定這個平面在z軸的位置。

為了確定射線是否射中這個平面,運用之前的知識。

可知

p?(t) = a? + t*b

z(t) = az + t*bz.

反求t

t = (k-az) / bz

通過t可求在xy軸的平面的位置

x = ax + t*bx

y = ay + t*by

當 x0 < x < x1 並且 y0 < y < y1時

則發生碰撞。

下面是三種平面的代碼

class PlaneXY:Hitable { private float x0, x1, y0, y1, k; private Material material; public override bool BoundingBox(float t0, float t1, ref AABB box) { box=new AABB(new Vector3(x0,y0,k-0.0001f),new Vector3(x1,y1,k+0.0001f) ); return true; } public PlaneXY(float _x0, float _x1, float _y0, float _y1, float _k, Material mat) { material = mat; x0 = _x0; x1 = _x1; y0 = _y0; y1 = _y1; k = _k; } public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { var t = (k - ray.original.z) / ray.direction.z; if (t < t_min || t > t_max) return false; var x = ray.original.x + t * ray.direction.x; if (x < x0 || x > x1) return false; var y = ray.original.y + t * ray.direction.y; if (y < y0 || y > y1) return false; rec.u = (x - x0) / (x1 - x0); rec.v = (y - y0) / (y1 - y0); rec.t = t; rec.material = material; rec.normal=new Vector3(0,0,1); rec.p = ray.GetPoint(t); return true; } } class PlaneXZ : Hitable { private float x0, x1, z0, z1, k; private Material material; public PlaneXZ(float _x0, float _x1, float _z0, float _z1, float _k, Material mat) { material = mat; x0 = _x0; x1 = _x1; z0 = _z0; z1 = _z1; k = _k; } public override bool BoundingBox(float t0, float t1, ref AABB box) { box = new AABB(new Vector3(x0, k - 0.0001f,z0), new Vector3(x1, k + 0.0001f,z1 )); return true; } public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { var t = (k - ray.original.y) / ray.direction.y; if (t < t_min || t > t_max) return false; var x = ray.original.x + t * ray.direction.x; if (x < x0 || x > x1) return false; var z = ray.original.z + t * ray.direction.z; if (z < z0 || z > z1) return false; rec.u = (x - x0) / (x1 - x0); rec.v = (z - z0) / (z1 - z0); rec.t = t; rec.material = material; rec.normal = new Vector3(0, 1, 0); rec.p = ray.GetPoint(t); return true; } } class PlaneYZ : Hitable { private float z0, z1, y0, y1, k; private Material material; public PlaneYZ(float _y0, float _y1, float _z0, float _z1, float _k, Material mat) { material = mat; z0 = _z0; z1 = _z1; y0 = _y0; y1 = _y1; k = _k; } public override bool BoundingBox(float t0, float t1, ref AABB box) { box = new AABB(new Vector3(k - 0.0001f, y0, z0), new Vector3(k + 0.0001f, y1, z1)); return true; } public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { var t = (k - ray.original.x) / ray.direction.x; if (t < t_min || t > t_max) return false; var z = ray.original.z + t * ray.direction.z; if (z < z0 || z > z1) return false; var y = ray.original.y + t * ray.direction.y; if (y < y0 || y > y1) return false; rec.v = (z - z0) / (z1 - z0); rec.u = (y - y0) / (y1 - y0); rec.t = t; rec.material = material; rec.normal = new Vector3(1, 0, 0); rec.p = ray.GetPoint(t); return true; } }

通過這三種平面,我們可以構造一個 康奈爾盒子(Cornell Box)

康奈爾盒子是由康奈爾大學計算機圖形學組的Cindy M. Goral,Kenneth E. Torrance,Donald P. Greenberg和Bennett Battaile在SIGGRAPH84上發表並發表的論文「模擬漫反射表面之間光的相互作用」中創造的用來測試模擬不同材質如何反射光線的場景。

我們可以寫一個函數來創建康奈爾盒子

Hitable CornellBox() { camera = new Camera(new Vector3(278, 278, -800), new Vector3(278, 278, 0), new Vector3(0, 1, 0), 40, 1); var list=new List<Hitable>(); list.Add(new PlaneYZ(0, 555, 0, 555, 555, new Lambertian(new ConstantTexture(Color32.red)))); list.Add(new PlaneYZ(0, 555, 0, 555, 0, new Lambertian(new ConstantTexture(Color32.green)))); list.Add(new PlaneXZ(213, 343, 227, 332, 554,new DiffuseLight(new ConstantTexture(new Color32(1, 1, 1, 1)),15))); list.Add(new PlaneXZ(0, 555, 0, 555, 555, new Lambertian(new ConstantTexture(Color32.white)))); list.Add(new PlaneXZ(0, 555, 0, 555, 0, new Lambertian(new ConstantTexture(Color32.white)))); list.Add(new PlaneXY(0, 555, 0, 555, 555, new Lambertian(new ConstantTexture(Color32.white)))); return new BVHNode(list.ToArray(),list.Count,0,1); } //初始化場景 world.list.Add(CornellBox());

但是渲染結果是這樣的

原因是有三個面的法線並不是朝內的,我們需要寫一個新的類來翻轉法線

public class FilpNormals : Hitable { private readonly Hitable hitable; public FilpNormals(Hitable p) { hitable = p; } public override bool BoundingBox(float t0, float t1, ref AABB box) { return hitable.BoundingBox(t0, t1,ref box); } public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { if (!hitable.Hit(ray, t_min, t_max, ref rec)) return false; rec.normal = -rec.normal; return true; } }

然後修改康奈爾盒子的函數

Hitable CornellBox() { camera = new Camera(new Vector3(278, 278, -800), new Vector3(278, 278, 0), new Vector3(0, 1, 0), 40, 1); var list=new List<Hitable>(); list.Add(new FilpNormals(new PlaneYZ(0, 555, 0, 555, 555, new Lambertian(new ConstantTexture(Color32.Green))))); list.Add(new PlaneYZ(0, 555, 0, 555, 0, new Lambertian(new ConstantTexture(Color32.Red)))); list.Add( new PlaneXZ(213, 343, 227, 332, 554, new DiffuseLight(new ConstantTexture(new Color32(1, 1, 1, 1)), 35))))); list.Add(new FilpNormals(new PlaneXZ(0, 555, 0, 555, 555, new Lambertian(new ConstantTexture(Color32.White))))); list.Add(new PlaneXZ(0, 555, 0, 555, 0, new Lambertian(new ConstantTexture(Color32.White)))); list.Add(new FilpNormals(new PlaneXY(0, 555, 0, 555, 555, new Lambertian(new ConstantTexture(Color32.White))))); return new BVHNode(list.ToArray(),list.Count,0,1); }

這樣就可以正常渲染了。

值得注意的是,渲染康奈爾盒子會比之前的場景慢很多很多,原因是在之前的場景有幾乎一半的射線直接照到天上了,剩下的也差不多在2~5次反射之內射到天上。然而康奈爾盒子卻幾乎可讓每條射線跑滿最大反射次數。

效果如下,可以看到即使1000spp也依然會有十分多的噪點。


第七章:實例化

本章我們會創建一個Cube立方體。

立方體是由六個面組成的組合體,原理很簡單,跟之前的康奈爾盒子類似。

class Cube:Hitable { public Vector3 pmin, pmax; public HitableList list; public Cube(Vector3 p0, Vector3 p1, Material mat,Material m2=null) { pmax = p1; pmin = p0; if (m2 == null) m2 = mat; list=new HitableList(); list.list.Add(new PlaneXY(p0.x,p1.x,p0.y,p1.y,p1.z,m2)); list.list.Add(new FilpNormals(new PlaneXY(p0.x, p1.x, p0.y, p1.y, p0.z, m2))); list.list.Add(new PlaneXZ(p0.x,p1.x,p0.z,p1.z,p1.y,mat)); list.list.Add(new FilpNormals(new PlaneXZ(p0.x,p1.x,p0.z,p1.z,p0.y,mat))); list.list.Add(new PlaneYZ(p0.y,p1.y,p0.z,p1.z,p1.x,m2)); list.list.Add(new FilpNormals(new PlaneYZ(p0.y,p1.y,p0.z,p1.z,p0.x,m2))); } public override bool BoundingBox(float t0, float t1, ref AABB box) { box=new AABB(pmin,pmax); return true; } public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { return list.Hit(ray, t_min, t_max, ref rec); } }

我這裡寫的構造函數包含兩個材質,頂部和側面。因為這樣就可以渲染一些有趣的東西了。

接著,我們就可以繼續完善我們的康奈爾盒子了

為了實現真正的康奈爾盒子,我們需要在盒子內放置兩個立方體,並稍加旋轉。

我們需要編寫兩個新的類來完成對物體的移動和旋轉。

//首先我們重載Vector3的構造函數使之可以從另一個Vector3複製 //Vector3.cs public Vector3(Vector3 copy) { x = copy.x; y = copy.y; z = copy.z; } //位移類 public class Translate : Hitable { public Hitable Object; private Vector3 offset; public Translate(Hitable p, Vector3 displace) { offset = displace; Object = p; } public override bool BoundingBox(float t0, float t1, ref AABB box) { if (!Object.BoundingBox(t0, t1, ref box)) return false; box = new AABB(box.min + offset, box.max + offset); return true; } public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { var moved=new Ray(ray.original-offset,ray.direction); if (!Object.Hit(moved, t_min, t_max, ref rec)) return false; rec.p += offset; return true; } } //Y軸旋轉類 public class RotateY : Hitable { public AABB bbox = new AABB(); public bool hasbox; public Hitable Object; private float sin_theta, cos_theta; public override bool BoundingBox(float t0, float t1, ref AABB box) { box = bbox; return hasbox; } public RotateY(Hitable p, float angle) { Object = p; float radians = (Mathf.PI / 180f) * angle; sin_theta = Mathf.Sin(radians); cos_theta = Mathf.Cos(radians); hasbox = Object.BoundingBox(0, 1, ref bbox); var min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); var max = new Vector3(-float.MaxValue, -float.MaxValue, -float.MaxValue); for (var i = 0; i < 2; i++) for (var j = 0; j < 2; j++) for (var k = 0; k < 2; k++) { float x = i * bbox.max.x + (1 - i) * bbox.min.x; float y = j * bbox.max.y + (1 - j) * bbox.min.y; float z = k * bbox.max.z + (1 - k) * bbox.min.z; float newx = cos_theta * x + sin_theta * z; float newz = -sin_theta * x + cos_theta * z; var tester = new Vector3(newx, y, newz); for (int c = 0; c < 3; c++) { if (tester[c] > max[c]) max[c] = tester[c]; if (tester[c] < min[c]) min[c] = tester[c]; } } bbox = new AABB(min, max); } public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { var origin = new Vector3(ray.original); var direction = new Vector3(ray.direction); origin[0] = cos_theta * ray.original[0] - sin_theta * ray.original[2]; origin[2] = sin_theta * ray.original[0] + cos_theta * ray.original[2]; direction[0] = cos_theta * ray.direction[0] - sin_theta * ray.direction[2]; direction[2] = sin_theta * ray.direction[0] + cos_theta * ray.direction[2]; var rotatedR = new Ray(origin, direction, ray.time); var r = new HitRecord(rec); if (Object.Hit(rotatedR, t_min, t_max, ref rec)) { var p = new Vector3(rec.p); var normal = new Vector3(rec.normal); p[0] = cos_theta * rec.p[0] + sin_theta * rec.p[2]; p[2] = -sin_theta * rec.p[0] + cos_theta * rec.p[2]; normal[0] = cos_theta * rec.normal[0] + sin_theta * rec.normal[2]; normal[2] = -sin_theta * rec.normal[0] + cos_theta * rec.normal[2]; rec.p = p; rec.normal = normal; return true; } rec = r; return false; } }

Translate的代碼很簡單只是對射線的加一個位移,但是旋轉的代碼就不那麼容易理解了。

我們首先需要了解在2D坐標系內一個點相對原點旋轉theta度的公式:

x』 = cos(θ)*x - sin(θ)*y

y』 = sin(θ)*x + cos(θ)*y

這樣就實現了Z軸旋轉。

同樣

Y軸旋轉:

x』 = cos(θ)*x + sin(θ)*z

z』 = -sin(θ)*x + cos(θ)*z

X軸旋轉

y』 = cos(θ)*y - sin(θ)*z

z』 = sin(θ)*y + cos(θ)*z

書里只提供了簡單的旋轉Y軸的代碼,我近期會嘗試寫一個類似Unity的Transform組件的能夠實現三軸旋轉位移縮放的組件。如果成功的話也會發在這個系列裡。

然後為康奈爾盒子加入兩個方塊

world.list.Add(new Translate(new RotateY( new Cube( new Vector3(0, 0, 0), new Vector3(165, 330, 165), new Lambertian(new ConstantTexture(Color32.white))), 15) , new Vector3(265, 0, 295))); world.list.Add(new Translate(new RotateY( new Cube( new Vector3(0, 0, 0), new Vector3(165, 165, 165), new Lambertian(new ConstantTexture(Color32.white))), -18), new Vector3(130, 0, 65)));

渲染結果

推薦閱讀:

Artlantis材質細說(一)
【搶先看】VRay4.0 for 3DSMAX 新版本現已推出——VRay NEXT
關於Maxwell與Substance painter的一點嘗試

TAG:計算機圖形學 | 光線跟蹤 | 渲染器 |