《一周學習光線追蹤》(三)紋理和柏林雜訊

《一周學習光線追蹤》(三)紋理和柏林雜訊

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

上一章:《一周學習光線追蹤》(二)層次包圍盒BVH


第三章:純色貼圖

接下來就要給渲染加上紋理了。

首先我們需要創建Texture類,這是一切紋理的基類:

public abstract class Texture { public abstract Color32 value(float u, float v, Vector3 p); }

我們先從最簡單的純色紋理開始。

public class ConstantTexture : Texture { private Color32 color; public ConstantTexture(Color32 c)=>color = c; public override Color32 value(float u, float v, Vector3 p)=>color; }

value函數是得到某個點的顏色的函數,會在不同紋理上重寫。

之後我們需要將改進所有材質Material類,使之初始化時賦值一個紋理而不是一個顏色。

public class Lambertian : Material { //Color32 albedo; //啟用顏色,使用紋理 //public Lambertian(Color32 a) => albedo = a; public Texture texture;//[NEW] public Lambertian(Texture t) => texture = t;//[NEW] public override bool scatter(Ray rayIn, HitRecord record, ref Color32 attenuation, ref Ray scattered) { var target = record.p + record.normal + GetRandomPointInUnitSphere(); scattered = new Ray(record.p, target - record.p); //attenuation = albedo;//刪除舊的顏色返回,使之返回紋理的顏色。 attenuation = texture.value(u, v, record.p);//[NEW] return true; } }

只有是純色紋理是不夠的,我們可以寫一個棋盤紋理。這個紋理會按棋盤格的方式分割兩個不同的紋理。

public class CheckerTexture : Texture { public Texture odd, even; public CheckerTexture(Texture t0, Texture t1) { even = t0; odd = t1; } public override Color32 value(float u, float v, Vector3 p)=> Mathf.Sin(10 * p.x) * Mathf.Sin(10 * p.y) * Mathf.Sin(10 * p.z) <= 0?odd.value(u,v,p):even.value(u,v,p); }

我們將地面設置成一個棋盤格

world.list.Add(new Sphere(new Vector3(0, -100.5f, -1), 100f, new Lambertian(new CheckerTexture(new ConstantTexture(new Color32(0, 0, 0)), new ConstantTexture(Color32.white)))));//地面

渲染結果


第四章:柏林雜訊

柏林雜訊 ( Perlin noise )指由Ken Perlin發明的自然雜訊生成演算法 。

除了簡單迅速以外,柏林雜訊的另一個關鍵特性是它對於相同的輸入永遠返回相同的

隨機數字,輸入點附近的點返回近似的數字。

public class Perlin { public static float Noise(Vector3 p) { int i = Mathf.Floor2int(p.x)&255; int j = Mathf.Floor2int(p.y)&255; int k = Mathf.Floor2int(p.z)&255; return ranfloats[perm_x[i] ^ perm_y[j] ^ perm_z[k]]; } private static float[] ranfloats= perlin_generate(); static readonly int[] perm_x=perlin_generate_perm(); static readonly int[] perm_y=perlin_generate_perm(); static readonly int[] perm_z=perlin_generate_perm(); private static float[] perlin_generate() { var p = new float[256]; for (var i = 0; i < 256; ++i)p[i] = Random.Get(); return p; } static void permute(int[] p, int n) { for (var i = n - 1; i > 0; i--) { var target = (int)(Random.Get() * (i + 1)); var tmp = p[i]; p[i] = p[target]; p[target] = tmp; } } private static int[] perlin_generate_perm() { var p = new int[256]; for (var i = 0; i < 256; i++)p[i] = i; permute(p, 256); return p; } }

我們可以用此雜訊生成我們的雜訊紋理

public class NoiseTexture:Texture { public override Color32 value(float u, float v, Vector3 p)=>Color32.white * Perlin.Noise(p); }

我們將地面設置為雜訊紋理。

world.list.Add(new Sphere(new Vector3(0, -100.5f, -1), 100f, new Lambertian(new ImageTexture.NoiseTexture())));//地面

渲染結果

我們可以使用線性差值使材質更加平滑。

static float TrillinearInterp(float[,,] c, float u, float v, float w) { float accum = 0; for (var i = 0; i < 2; i++) for (var j = 0; j < 2; j++) for (var k = 0; k < 2; k++) { accum += (i * u + (1 - i) * (1 - u)) * (j * v + (1 - j) * (1 - v)) * (k * w + (1 - k) * (1 - w)) * c[i, j, k]; } return accum; } public static float Noise(Vector3 p) { float u = p.x - Mathf.Floor(p.x); float v = p.y - Mathf.Floor(p.y); float w = p.z - Mathf.Floor(p.z); int i = Mathf.Floor2int(p.x); int j = Mathf.Floor2int(p.y); int k = Mathf.Floor2int(p.z); var c=new float[2,2,2]; for (int di = 0; di < 2; di++) for (int dj = 0; dj < 2; dj++) for (int dk = 0; dk < 2; dk++) c[di,dj,dk] = ranfloats[perm_x[(i + di) & 255] ^ perm_y[(j + dj) & 255] ^ perm_z[(k + dk) & 255]]; return TrillinearInterp(c, u, v, w); }

線性差值結果仍然看起來非常的具有網格感,我們可以使用埃爾米特立方體(hermite cubic)來插值:

public static float Noise(Vector3 p) { float u = p.x - Mathf.Floor(p.x); float v = p.y - Mathf.Floor(p.y); float w = p.z - Mathf.Floor(p.z); u = u * u * (3 - 2 * u); //[NEW] v = v * v * (3 - 2 * v); //[NEW] w = w * w * (3 - 2 * w); //[NEW] int i = Mathf.Floor2int(p.x); int j = Mathf.Floor2int(p.y); int k = Mathf.Floor2int(p.z); var c = new float[2, 2, 2]; for (int di = 0; di < 2; di++) for (int dj = 0; dj < 2; dj++) for (int dk = 0; dk < 2; dk++) c[di, dj, dk] = ranfloats[perm_x[(i + di) & 255] ^ perm_y[(j + dj) & 255] ^ perm_z[(k + dk) & 255]]; return TrillinearInterp(c, u, v, w); }

接下來我們要使這個雜訊紋理可以調節大小

public class NoiseTexture:Texture { public NoiseTexture() { } public NoiseTexture(float s) => scale = s; public float scale; public override Color32 value(float u, float v, Vector3 p)=>Color32.white * Perlin.Noise(scale*p); }

但是依然看起來非常網格化,Ken Perlin 的演算法非常巧妙的地方是他沒有直接將單位隨機向量輸出到點,而是使用點積來移動最大和最小值。

接下來我們需要將隨機浮點序列改成隨機矢量序列。

static readonly Vector3[] ranvec=perlin_generate(); //private static float[] ranfloats= perlin_generate(); private static Vector3[] perlin_generate() { var p = new Vector3[256]; for (var i = 0; i < 256; ++i)p[i] = new Vector3(-1 + 2 * Random.Get(), -1 + 2 * Random.Get(), -1 + 2 * Random.Get()).Normalized(); return p; } public static float Noise(Vector3 p) { float u = p.x - Mathf.Floor(p.x); float v = p.y - Mathf.Floor(p.y); float w = p.z - Mathf.Floor(p.z); int i = Mathf.Floor2int(p.x); int j = Mathf.Floor2int(p.y); int k = Mathf.Floor2int(p.z); var c = new Vector3[2, 2, 2]; for (int di = 0; di < 2; di++) for (int dj = 0; dj < 2; dj++) for (int dk = 0; dk < 2; dk++) c[di, dj, dk] = ranvec[perm_x[(i + di) & 255] ^ perm_y[(j + dj) & 255] ^ perm_z[(k + dk) & 255]]; return TrillinearInterp(c, u, v, w); }

然後將差值變得更複雜一些

static float TrillinearInterp(Vector3[,,] c, float u, float v, float w) { float uu = u * u * (3 - 2 * u); float vv = v * v * (3 - 2 * v); float ww = w * w * (3 - 2 * w); float accum = 0; for (int i = 0; i < 2; i++) for (int j = 0; j < 2; j++) for (int k = 0; k < 2; k++) { accum += (i * uu + (1 - i) * (1 - uu)) * (j * vv + (1 - j) * (1 - vv)) * (k * ww + (1 - k) * (1 - ww)) * Vector3.Dot(c[i,j,k], new Vector3(u - i, v - j, w - k)); } return accum; }

渲染結果

太黑了,但只需要簡單修改NoiseTexture即可

public class NoiseTexture:Texture { /*...省略其他代碼...*/ public override Color32 value(float u, float v, Vector3 p) => Color32.white * 0.5f * (1 + Perlin.Noise(scale * p)); }

這樣就好多了。

我們通常使用具有多個相加頻率的複合雜訊。 這稱為Turbulence,是對噪音重複調用的總統稱:

public static float Turb(Vector3 p, int depth = 7) { float accum = 0; var temp_p = p; var weight = 1.0f; for (var i = 0; i<depth; i++) { accum += weight* Noise(temp_p); weight *= 0.5f; temp_p *= 2; } return Mathf.Abs(accum); }

通過修改NoiseTexture可以達到不同的效果

public class NoiseTexture:Texture { /*...省略其他代碼...*/ public override Color32 value(float u, float v, Vector3 p) => Color32.white * 0.5f * (1 + (Mathf.Sin(scale * p.z) + 10 * Perlin.Turb(p))); }

(偏藍了,因為反射了些天空色)


第五章:圖像紋理映射

是時候給我們的模型加上貼圖了

我們之前使用一個向量p為純色紋理建立索引。如果我們想要使用貼圖紋理,需要使用一個2D坐標(u,v)來獲取此位置的像素的顏色。我們同樣需要設置一個值來調整貼圖的縮放。

u和v的值是範圍0~1的浮點數。我們需要輸入(i,j)這是像素坐標。公式如下

u = i/(nx-1)

v = j/(nx-1)

對於球體,我們需要想三維矢量轉換為極坐標再轉換為u,v

從向量的

x = cos(phi) cos(theta)

y = sin(phi) cos(theta)

z = sin(theta)

可以反求

phi = atan2(y, x)

phi的值在(-π,π)區間內。

theta = asin(z)

theta 的值在(-π/2,π/2)區間內。

我們要重寫Sphere類的Hit函數使之計算正確的u,v

public override bool Hit(Ray ray, float t_min, float t_max, ref HitRecord rec) { //加入嵌套函數來計算u,v void GetSphereUV(ref HitRecord record) { float phi = Mathf.Atan2(record.p.z, record.p.x); float theta = Mathf.Asin(record.p.y); record.u = 1 - (phi + Mathf.PI) / (2 * Mathf.PI); record.v = (theta + Mathf.PI / 2) / Mathf.PI; } var oc = ray.original - center; var a = Vector3.Dot(ray.direction, ray.direction); var b = 2f * Vector3.Dot(oc, ray.direction); var c = Vector3.Dot(oc, oc) - radius * radius; var discriminant = b * b - 4 * a * c; if (!(discriminant > 0)) return false; var temp = (-b - Mathf.Sqrt(discriminant)) / a * 0.5f; if (temp < t_max && temp > t_min) { rec.material = material; rec.t = temp; rec.p = ray.GetPoint(rec.t); rec.normal = (rec.p - center).Normalized(); GetSphereUV(ref rec);//計算UV return true; } temp = (-b + Mathf.Sqrt(discriminant)) / a * 0.5f; if (!(temp < t_max) || !(temp > t_min)) return false; rec.material = material; rec.t = temp; rec.p = ray.GetPoint(rec.t); rec.normal = (rec.p - center).Normalized(); GetSphereUV(ref rec);//計算UV return true; }

接下來我們需要創建一個新的Texture類來儲存貼圖紋理:

我這裡寫了一些新的東西,使這個對象在構造的函數直接讀取一張圖片並轉換為RGB緩衝。

RGB緩衝是一個byte[]數組,這個數組的長度是 圖片的長度*圖片的寬度*3

每個像素佔byte[]的三個元素來儲存RGB顏色信息(0~255)。有的緩衝還需要多一個元素來儲存Alpha信息,目前我們不需要。

將一張圖轉換為RGB緩衝有很多種方法,在這裡我的方法並不是很高效但是可以支持多種圖片格式。這個方法不會影響渲染速度因為這隻在程序一開始運行。

public class ImageTexture : Texture { private byte[] data; private int w, h; private float scale = 1; //構造函數直接讀取圖片 public ImageTexture(string file,float s=1) { scale = s; var bitmap = new Bitmap(Image.FromFile(file)); data=new byte[bitmap.Width*bitmap.Height*3]; w = bitmap.Width; h = bitmap.Height; for (var i = 0; i < bitmap.Height; i++) { for (var j = 0; j < bitmap.Width; j++) { var c = bitmap.GetPixel(j, i); data[3 * j + 3 * w * i] = c.R; data[3 * j + 3 * w * i+1] = c.G; data[3 * j + 3 * w * i+2] = c.B; } } } //構造函數賦予RGB緩衝 public ImageTexture(byte[] p, int x, int y) { data = p; w = x; h = y; } //取得某UV的顏色值。 public override Color32 value(float u, float v, Vector3 p) { u = u * scale % 1; v = v * scale % 1; var i = Mathf.Range((int) (u * w), 0, w - 1); var j= Mathf.Range((int) ((1 - v) * h - 0.001f), 0, h - 1); return new Color32( data[3 * i + 3 * w * j] / 255f, data[3 * i + 3 * w * j+1] / 255f, data[3 * i + 3 * w * j+2] / 255f); } }

接下來我們創建一個帶有貼圖的球形

world.list.Add(new Sphere(new Vector3(0, 0f, 0), 0.5f, new Metal(new ImageTexture("texture.jpg", 10),0.9f)));


推薦閱讀:

渲染的未來 - MAXON宣布與AMD合作,C4D將提供原生GPU渲染引擎
Maxwell 4這次真的來了
pbrt中為什麼認為光線傳遞是平穩馬爾可夫過程?
用多線程為光線追蹤提速

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