細讀

《Ray tracing in one weekend》

,在我心中這本書可能是

入門圖形學

的最好方式,

作者PeterShirly

用C++語言加上一些簡單的初中物理知識,帶我們實現一個簡單的光線追蹤渲染器。這裡是我用

Java

復現的版本,以及記錄一些我的想法,已完工。每一章節工程程式碼已上傳至我的Github倉庫

有幫助的話求一個star,如果有意見和建議還請指出,感謝!

最後的最後實現的效果圖就是上方的題圖,其中所包含的所有實現細節(比如鏡面反射等)都會隨著章節的推進一一實現。文章中的貼出來的程式碼有所截選,請以Github倉庫裡的程式碼為準。

Chapter 0. 關於C++版本和Java版本執行時間的對比

在最前面這部分內容個,是因為評論區裡都是關於

C++和Java速度對比

的討論,當然這也確實是一個常年在討論的問題,這裡我做了測試對比。為了節省時間,測試條件都是

200*100的解析度

,每個畫素

100次取樣

。直接貼結果圖如下。C++的IDE是VS2017,Java的IDE是IDEA2018。

用Java實現一個光線追蹤渲染器

C++ 版本 Release下O3最佳化

用Java實現一個光線追蹤渲染器

Java版本執行時間

測試下的Java速度比C++稍微快一點點,其實是比較奇怪的,我打算最佳化下Cpp版本的code後面再測試一下~到時候繼續補充在這個Chapter裡。

Chapter 1. Output an image

這一章我們要了解

ppm影象檔案格式

自己畫出一張ppm圖

1。1 ppm格式檔案

PPM 是透過RGB三種顏色顯現的影象(pixmaps)。

PPM影象格式分為兩部分,分別為

頭部分

影象資料部分

頭部分:一共3部分,用換行或空格進行分割。

第1部分:P3或P6,指明PPM的編碼格式,這裡我們用P3。 第2部分:影象的寬度和高度,透過ASCII表示,這裡設定1980*1080。 第3部分:最大畫素值,0-255位元組表示,這裡設定255。

P3

1980 1080

255

影象資料部分: ASCII格式:按RGB的順序排列,RGB中間用空格隔開,圖片每一行用回車隔開。

//R G B

0 253 51

1 253 51

更多ppm格式介紹:

https://

blog。csdn。net/kinghzkin

gkkk/article/details/70226214

ppm格式的圖片使用的場景比較少,我們可以用XnView等軟體檢視,當然,我們也可以轉換ppm格式成jpg格式的圖片。下面是用ImageMagick實現的轉換例子(python語言),可以參考。

#ppm2jpg

import

PythonMagick

as

PM

img

=

PM

Image

r

“C:\Users\yh\Documents\CG\result\Chapter6_20_39_01。ppm”

img

write

r

“C:\Users\yh\Documents\CG\result\Cp6。jpg”

ppm2jpg:

https://

blog。csdn。net/y_jwei/ar

ticle/details/80502675

1。2 畫張ppm圖

寫一個我們最最最主要的Display類,我們最後顯示出來的結果(生成的圖片)都在這實現,Ch1裡我們先用Display類畫一張漸變圖。

程式碼見下方,透過兩個for迴圈,遍歷寬和高,最後繪畫整張圖。可以看到程式碼裡r變數從0到1,g是從1到0變化的。想象一下畫面應該是:從左往右紅色越來越深,從上到下綠色越來越淺,而藍色保持0。2*255灰階不變,結合RGB三種顏色可以想象下最後能得到什麼樣的影象?

FileWriter

fw

=

new

FileWriter

pictureName

);

//PPM格式頭部分

fw

write

“P3\n”

+

width

+

“ ”

+

height

+

“\n255\n”

);

int

index

=

0

//PPM格式影象資料部分

for

int

j

=

height

-

1

j

>=

0

j

——){

for

int

i

=

0

i

<

width

i

++){

float

r

=

float

i

/(

float

width

float

g

=

float

j

/(

float

height

float

b

=

0

2f

index

+=

1

int

ir

=

int

)(

255

59f

*

r

);

int

ig

=

int

)(

255

59f

*

g

);

int

ib

=

int

)(

255

59f

*

b

);

fw

write

ir

+

“ ”

+

ig

+

“ ”

+

ib

+

“\n”

);

if

index

%

100

==

0

){

cpb

show

index

);

}

}

}

fw

close

();

題外話:如果你看了我Github裡的工程檔案,你會發現我使用了一個在控制檯中即時顯示進度的小外掛,在後期渲染大圖的時候方便檢視渲染進度。

ConsoleProgress

cpb

=

new

ConsoleProgress

0

width

*

height

20

‘=’

);

最後我們寫個main呼叫一下Display類,解析度自己調整,得到我們繪製的一張圖。

public

class

RayTracing

{

public

static

void

main

String

[]

args

){

Display

TracingDisplay

=

new

Display

1980

1080

“Ray Tracer”

);

}

}

Ch1最後得到的結果:

用Java實現一個光線追蹤渲染器

Chapter 2. The vec3 class

這一章我們主要

寫一個Vec3類(它的作用將貫穿所有的章節)

2。1 Vec3類的作用

可以想象,我們會不斷和RGB這種三維資料打交道,因此我們要寫一個類來把RGB這樣的三維資料整合起來使用,表示一個顏色。但其實,這樣一個描述三維資料的類,只是用來表現顏色的話未免顯得太浪費了。仔細想想,這個類還可以用來表示一個向量的

三維向量

,也可以用來表示一個固定的

三維座標

,總之任意三維資料都可以表示。

2。2 Vec3類的基本操作

這個類也實現了一些Vec3類

基本操作

方法,包括以後會用到的

加(Add)、減(Subtract)、乘(Scale)、點乘(dot)、叉乘(cross)、按向量對應維度乘(Multiply)、求模(length)、歸一化(normalize)

。原著用到了C++過載運算子來實現這些操作,Java因為沒有運算子過載故用方法實現,舉個例子如下:

public

class

Vec3

{

//定義最基本的資料格式:三維的浮點型座標

public

float

[]

e

=

new

float

3

];

//建構函式

public

Vec3

()

{

}

public

Vec3

float

x

float

y

float

z

{

e

0

=

x

e

1

=

y

e

2

=

z

}

//向量求和

public

Vec3

Add

Vec3

v

{

return

new

Vec3

e

0

+

v

e

0

],

e

1

+

v

e

1

],

e

2

+

v

e

2

]);

}

//新增其他Vec3基本操作。。。

Chapter 3: Rays, a simple camera, and background

這一章我們才算正式開啟光線追蹤之路:

模擬一條光線

繪製天空(背景)

3。1 模擬一條光線

光是沿著直線傳播的,把光的運動想象成一條射線,那麼它就有兩個屬性射線的原點和射出的方向,因此我們設光線的方程式

p(t) = A + t*B

其中A是光線的

源點

(是一個固定的三維座標),B是光線的發射

方向

(還是一個三維資料代表方向,按照向量的定義,光線的

方向

就是

終點座標減去起點座標

),t是時間,隨著t的變化,光線到達的位置不同。

用Java實現一個光線追蹤渲染器

我們寫一個光線類,來模擬上面的光線表示式p(t),其實主要就是

記錄一下源點和方向

,此外還要寫一個方法叫point_at_parameter,返回t時刻下光線的座標位置。

public

class

Ray

{

public

Vec3

o

//源點

public

Vec3

d

//方向

public

Ray

()

{

}

public

Ray

Vec3

origin

Vec3

direction

{

o

=

origin

d

=

direction

}

public

Vec3

origin

()

{

return

o

}

public

Vec3

direction

()

{

return

d

}

//p(t)=A+t*B 即返回t時刻光線的位置

public

Vec3

point_at_parameter

float

t

{

return

o

Add

d

Scale

t

));

}

}

3。2 繪製天空(背景)

我們這次要用光線追蹤

渲染

一張圖,這和Ch1裡直接畫出來的圖是有一點區別的。

為了簡單起見,我們以(0,0,0)為

原點

,模擬我們

人眼

(也可以叫相機)來看世界。順便建立一個右手直角座標系,這樣整個世界就固定下來了。但是,畢竟人眼看到的

範圍

也是有限的,相機拍的照片大小也是有限的,所以我們把它控制在一個框內(畫布),如圖所示(右邊的矩形),畫布的位置在Z=-1的平面上,並且寬高固定的,這裡我們把

寬(horizontal)

設為4,

高(vertical)

設為2。

用Java實現一個光線追蹤渲染器

光線照射在畫布上,交點座標可以用一個

二維座標(u, v)

來表示。怎麼表示?記左下角點為lower_left,它的座標是(-2,-1,-1)。任一交點座標可以表示為

lowerleft + u*horizontal + v*vertical

horizotal是畫布長,vertical是畫布高。u和v是什麼呢?u是離lower_left點的橫向距離變數,v是離lower_left點的縱向距離變數,上圖已經詳細標明。並且除了u和v兩個變數以外,其他值都是固定的座標值。u和v這兩個變數唯一標識了畫布上的某一點,u和v的取值範圍都是[0,1),我們透過u和v的取值就可以遍歷整個畫布,並且u和v越精細,那麼圖畫就越精細(因為畫素的顏色越趨近於它原來該存在的位置)。

private

Vec3

lower_left

=

new

Vec3

(-

2

0f

-

1

0f

-

1

0f

);

private

Vec3

horizontal

=

new

Vec3

4

0f

0

0f

0

0f

);

private

Vec3

vertical

=

new

Vec3

0

0f

2

0f

0

0f

);

private

Vec3

origin

=

new

Vec3

0

0f

0

0f

0

0f

);

因為起點是(0,0,0),所以交點的向量也就是光線的方向向量。用上面的Ray類表示就是Ray(Vec3(0,0,0),

Vec3(畫布上的交點)-Vec3(0,0,0)

)。

發現了什麼?只要我們遍歷u和v,我們就能

把整個畫布用光線鋪滿

!而只要u和v越精細,整張圖的畫質就越高!

所以,我們要修改Display類了,我們不再單純地按畫素位置著色,而是對畫布上的每個畫素點都想象成是一束光投影到畫布上,然後返回那一點在畫布上的顏色。所以我們在遍歷畫布的時候,對每個畫素應該顯示的顏色都要經過計算,所以我們對每個畫素點都相當於new一個Ray物件,模擬每條光束,並

添加了一個color方法,每一個Ray物件都要經過color方法計算顯示的顏色,最後顯示出來。

因為目前的光線都是沒有阻擋的,直接投射到畫布上,所以想象一下,各畫素點應該是沒什麼區別的。不過為了模擬天空背景,我們在color方法裡我們做了個沿y軸的

線性插值

,讓我們的畫布看起來是藍色和白色的漸變。

try

{

FileWriter

fw

=

new

FileWriter

pictureName

);

fw

write

“P3\n”

+

width

+

“ ”

+

height

+

“\n255\n”

);

int

index

=

0

for

int

j

=

height

-

1

j

>=

0

j

——){

for

int

i

=

0

i

<

width

i

++){

float

u

=

float

i

/(

float

width

float

v

=

float

j

/(

float

height

Ray

r

=

new

Ray

origin

lower_left

Add

horizontal

Scale

u

))。

Add

vertical

Scale

v

)));

//模擬一條光線

Vec3

col

=

color

r

);

//根據每條光線(每個畫素點)上色

index

+=

1

int

ir

=

int

)(

255

59f

*

col

x

());

int

ig

=

int

)(

255

59f

*

col

y

());

int

ib

=

int

)(

255

59f

*

col

z

());

fw

write

ir

+

“ ”

+

ig

+

“ ”

+

ib

+

“\n”

);

if

index

%

100

==

0

){

cpb

show

index

);

}

}

}

fw

close

();

}

catch

Exception

e

){

System

out

println

“GG!”

);

}

public

Vec3

color

Ray

r

{

Vec3

unit_dir

=

r

direction

()。

normalize

();

//單位光線的方向 並歸一化

float

t

=

0

5f

*

unit_dir

y

()

+

1

0f

);

//取出y的座標值,原本範圍為[-1,1]調整為[0,1]

return

new

Vec3

1

0f

1

0f

1

0f

)。

Scale

1

0f

-

t

)。

Add

new

Vec3

0

5f

0

7f

1

0f

)。

Scale

t

));

//返回背景(1。0-t)*vec3(1。0, 1。0, 1。0) + t*vec3(0。5, 0。7, 1。0);

//沿著y軸線性插值,所以返回的顏色介於白色與天藍色之間,即模擬天空的顏色

}

執行結果如下:

用Java實現一個光線追蹤渲染器

Chapter 4: Adding a sphere

本章只有一個目標,思考下面的情況:

光線碰撞到障礙物

4。1 判斷光線是否碰撞障礙物的數學推導

光線不能總是無阻礙的傳播,遇到障礙物的時候,光線就會被攔截,我們設定的第一個障礙物就是個球體。光線和球體的相互關係,只有如下三種情況(假設光線能穿過物體)。

用Java實現一個光線追蹤渲染器

設球的球心是C(cx,cy,cz),半徑是R,球的方程式是

(x-cx)^2 +(y-cy)^2+(z-cz)^2=R^2

仔細看,左式完全可以

寫成是向量內積的形式

。也就是一個向量=(x,y,z)的點-球心座標,然後這個向量自己的內積就是左式。

我們要求的是光線和球的交點,所以我們把光線表示式帶入球的方程式。設p(x,y,z),結合p和C,上式不就可寫成這樣了嗎:

(p-C)(p-C)=R^2

把光線的方程p(t)=A+t*B帶入

(A+t*B-C)(A+t*B-C)=R^2

以t看做未知數,整理後如下:是一個二元一次方程

(B·B)t^2+2B(A-C)t+(A^2-C^2-R^2)=0

根據

求根公式

\Delta =  b^2 - 4ac

可知,如果

\Delta >0

說明和光線和球有交點,如果

\Delta < 0

說明光線和球沒有交點。

4。2 光線碰撞障礙物的程式碼實現

我們先寫一個hitSphere方法,返回光是否碰撞到障礙物。透過上述推導的公式計算出discriminant即判別式,即可實現這個功能。

/**

*

* @param center 球的圓心

* @param radius 球的半徑

* @param r 光線

* @return 光線是否碰到球

*/

public

boolean

hitSphere

final

Vec3

center

float

radius

final

Ray

r

{

Vec3

oc

=

r

origin

()。

Subtract

center

);

//oc = A-C

float

a

=

r

direction

()。

dot

r

direction

());

//a = B·B

float

b

=

2

0f

*

oc

dot

r

direction

());

//b = 2B·oc

float

c

=

oc

dot

oc

-

radius

*

radius

//c = oc^2 - R^2

float

discriminant

=

b

*

b

-

4

*

a

*

c

if

discriminant

<

0

{

return

false

}

else

{

return

true

}

}

然後修改color方法,我們設定:球的圓心位置在(0,0,-1),半徑為0。5。如果光碰到了那個球,就返回顏色(0,0,1)即藍色。否則返回上一章畫出來的天空背景。

/**

*

* @param r 光線

* @return 光線代表畫素的顏色

*/

public

Vec3

color

Ray

r

{

if

hitSphere

new

Vec3

0

0

,-

1

),

0

5f

r

)){

return

new

Vec3

0

0

1

);

}

else

{

Vec3

unit_dir

=

r

direction

()。

normalize

();

//單位方向向量

float

t

=

0

5f

*

unit_dir

y

()

+

1

0f

);

//原本範圍為[-1,1]調整為[0,1]

return

new

Vec3

1

0f

1

0f

1

0f

)。

Scale

1

0f

-

t

)。

Add

new

Vec3

0

5f

0

7f

1

0f

)。

Scale

t

));

//返回背景(1。0-t)*vec3(1。0, 1。0, 1。0) + t*vec3(0。5, 0。7, 1。0); 沿著y軸線性插值,返回的顏色介於白色與天藍色之間

}

}

Ch4最後顯示如圖:

用Java實現一個光線追蹤渲染器

Chapter 5: Surface normals and multiple objects.

這一章主要要實現

球體顏色漸變

多個球體共存

5。1 球體顏色最佳化

上圖的藍球實在是太醜了,看起來就是個圓,根本

看不出來是個球

好嗎?我們做個簡單的最佳化,我們要根據位置的不同,顯示出不同的顏色,比如不如用

法向量方向座標

來顯示成顏色?

首先hitable方法需要改變,返回值本來是boolean改成了float,因為我們不止是判斷是否碰撞了,我們還要拿到碰撞時的點。這裡返回的是第一個碰到的點的t值。

public

float

hitSphere

final

Vec3

center

float

radius

final

Ray

r

{

Vec3

oc

=

r

origin

()。

Subtract

center

);

//oc = A-C

float

a

=

r

direction

()。

dot

r

direction

());

//a = B·B

float

b

=

2

0f

*

oc

dot

r

direction

());

//b = 2B·oc

float

c

=

oc

dot

oc

-

radius

*

radius

//c = oc^2 - R^2

float

discriminant

=

b

*

b

-

4

*

a

*

c

if

discriminant

<

0

{

return

-

1

0f

}

else

{

return

(-

b

-

float

Math

sqrt

discriminant

))

/

2

0f

*

a

);

}

}

同時,color方法也得更改,只要t>0,就說明有撞點,我們求一下法向量方向,根據向量公式:

交點的座標P - 圓心座標C = 法向量(P-C)

,具體實現如下。

用Java實現一個光線追蹤渲染器

public

Vec3

color

Ray

r

{

float

t

=

hitSphere

new

Vec3

0

0

,-

1

),

0

5f

r

);

if

t

>

0

0

){

//t時刻(即交點)的座標 - 圓心座標 = 法向量 (並做了歸一化)

Vec3

N

=

r

point_at_parameter

t

)。

Subtract

new

Vec3

0

0

-

1

))。

normalize

();

return

new

Vec3

N

x

()+

1

N

y

()+

1

N

z

()+

1

)。

Scale

0

5f

);

}

//如果沒撞點就返回天空背景

Vec3

unit_dir

=

r

direction

()。

normalize

();

//單位方向向量

t

=

0

5f

*

unit_dir

y

()

+

1

0f

);

//原本範圍為[-1,1]調整為[0,1]

return

new

Vec3

1

0f

1

0f

1

0f

)。

Scale

1

0f

-

t

)。

Add

new

Vec3

0

5f

0

7f

1

0f

)。

Scale

t

));

//返回背景(1。0-t)*vec3(1。0, 1。0, 1。0) + t*vec3(0。5, 0。7, 1。0); 沿著y軸線性插值,返回的顏色介於白色與天藍色之間

}

實現效果如圖:

用Java實現一個光線追蹤渲染器

5。2 多個球體同時出現

對於多個球的情況,我們不可能去對每一個球單獨描述,我們當然是考慮面向物件的思想。寫個Hitable的抽象類,它代表著碰撞物,其中碰撞物可以是球,可以是方塊等等,後面我們例項的物體都要繼承它,並且必須重寫hit方法。

public

abstract

class

Hitable

{

public

abstract

boolean

hit

final

Ray

r

float

t_min

float

t_max

HitRecord

rec

);

}

注意這裡的引數,除了打架熟悉的Ray,還有

兩個t,一個HitRecord

。其中t

min和t

max是限定一個時間區間,也就是說在這個區間內發生的碰撞才是有效的。然後這個HitRecord是碰撞點的資訊,我們把有關碰撞點的資訊也封裝起來成一個單獨的類。

public

class

HitRecord

{

public

float

t

//相撞的時間

public

Vec3

p

//撞擊點的座標

public

Vec3

normal

//撞擊點的法向量

public

HitRecord

()

{

t

=

0

p

=

new

Vec3

0

0

0

);

normal

=

new

Vec3

0

0

0

);

}

}

然後,按計劃,當然是寫個球體類繼承這個Hitable,並

重寫hit方法

。首先當然還是判斷是否碰撞,如果t>0則代表相撞,根據求根公式算出碰撞時間t,如果t在時間範圍內,說明是有效碰撞,記錄下一些碰撞資訊。因為一般求根公式有兩個解,如果第一個點是有效碰撞,則不需要球第二個,因為第二個點更遠會被第一個點覆蓋。

public

class

Sphere

extends

Hitable

{

Vec3

center

float

radius

public

Sphere

()

{

}

public

Sphere

Vec3

center

float

radius

{

this

center

=

center

this

radius

=

radius

}

/**

* 判斷與球體是否有撞擊

* @param r 光線

* @param t_min 範圍

* @param t_max 範圍

* @param rec 撞擊點

* @return 是否有撞擊

*/

@Override

public

boolean

hit

Ray

r

float

t_min

float

t_max

HitRecord

rec

{

Vec3

oc

=

r

origin

()。

Subtract

center

);

float

a

=

r

direction

()。

dot

r

direction

());

float

b

=

2

*

oc

dot

r

direction

());

float

c

=

oc

dot

oc

-

radius

*

radius

float

discriminant

=

b

*

b

-

4

0f

*

a

*

c

if

discriminant

>

0

{

//優先選取符合範圍的根較小的撞擊點,若沒有再選取另一個根

float

discFactor

=

float

Math

sqrt

discriminant

);

float

temp

=

(-

b

-

discFactor

/

2

0f

*

a

);

if

temp

<

t_max

&&

temp

>

t_min

{

rec

t

=

temp

rec

p

=

r

point_at_parameter

rec

t

);

rec

normal

=

rec

p

Subtract

center

))。

Scale

1

0f

/

radius

);

return

true

}

temp

=

(-

b

+

discFactor

/

2

0f

*

a

);

if

temp

<

t_max

&&

temp

>

t_min

{

rec

t

=

temp

rec

p

=

r

point_at_parameter

rec

t

);

rec

normal

=

rec

p

Subtract

center

))。

Scale

1

0f

/

radius

);

return

true

}

}

return

false

}

}

但是,但是,但是我們這次明明要解決的是多個球體共存的場面,因此還不夠。我們怎麼解決這個問題呢?我們再寫一個類,把對所有的球的判斷封裝起來,

其實也就是一個for迴圈的事罷了,每一條光線都會遍歷所有的球判斷是否有交點

(當然這是個比較低效,並且蠢的寫法,後面第二本書作者提到了

層次包圍盒

就是最佳化這個的)。寫個HitableList類,仍是繼承Hitable類。這裡有個closestSoFar要注意下,是這樣的,對於一條光線來說,如果有更近的碰撞點出現的話,那麼遠的碰撞點是要捨棄的。我們可以想象,很多球出現的時候,可能一條光射過去,會有很多球與光線相交,但作為人眼,我們只能看到前面的那個。所以,我們維護closestSoFar這個變數,每次有新撞擊點(更近的)記錄下來,就會更新它。

public

class

HitableList

extends

Hitable

{

List

<

Hitable

>

list

public

HitableList

()

{

list

=

new

ArrayList

<

Hitable

>();

}

public

HitableList

List

<

Hitable

>

list

{

this

list

=

list

}

/**

* 判斷列表裡的任一個球是否撞擊

* @param r 光線

* @param t_min 範圍

* @param t_max 範圍

* @param rec 撞擊點

* @return 任意一個球是否撞擊

*/

@Override

public

boolean

hit

Ray

r

float

t_min

float

t_max

HitRecord

rec

{

HitRecord

tempRec

=

new

HitRecord

();

boolean

hitAnything

=

false

float

closestSoFar

=

t_max

for

int

i

=

0

i

<

list

size

();

i

++)

{

if

list

get

i

)。

hit

r

t_min

closestSoFar

tempRec

))

{

hitAnything

=

true

closestSoFar

=

tempRec

t

//每當有撞擊點,tmax就會減為最近的一次撞擊點

rec

t

=

tempRec

t

rec

normal

=

tempRec

normal

rec

p

=

tempRec

p

}

}

return

hitAnything

}

}

至此,我們的準備工作都做好了。可以開始使用上述新寫的類開始實現多個球共存下的光線追蹤了。如下,我們自己弄3個球出來,並串成一個HitableList。

//多個球體的資訊

List

<

Hitable

>

objList

=

new

ArrayList

<

Hitable

>();

objList

add

new

Sphere

new

Vec3

0

0f

0

0f

,-

1

0f

),

0

5f

));

objList

add

new

Sphere

new

Vec3

0

3f

0

0f

,-

1

0f

),

0

3f

));

objList

add

new

Sphere

new

Vec3

0

0f

,-

100

5f

,-

1

0f

),

100f

));

world

=

new

HitableList

objList

);

然後,修改color方法。判斷整個List裡是否有任意一個球發生了碰撞。如果有就返回法向量代表的顏色。

public

Vec3

color

Ray

r

{

HitRecord

rec

=

new

HitRecord

();

if

world

hit

r

0

0f

Float

MAX_VALUE

rec

)){

//有撞擊點,按撞擊點法向量代表的顏色繪製

return

new

Vec3

rec

normal

x

()+

1

rec

normal

y

()+

1

rec

normal

z

()+

1

)。

Scale

0

5f

);

}

else

{

//沒有撞擊點,繪製背景

Vec3

unit_dir

=

r

direction

()。

normalize

();

//單位方向向量

float

t

=

0

5f

*

unit_dir

y

()

+

1

0f

);

//原本範圍為[-1,1]調整為[0,1]

return

new

Vec3

1

0f

1

0f

1

0f

)。

Scale

1

0f

-

t

)。

Add

new

Vec3

0

5f

0

7f

1

0f

)。

Scale

t

));

//返回背景(1。0-t)*vec3(1。0, 1。0, 1。0) + t*vec3(0。5, 0。7, 1。0); 沿著y軸線性插值,返回的顏色介於白色與天藍色之間

}

}

用Java實現一個光線追蹤渲染器

發現了嗎,地面其實是一個半徑為100的大球,它之所以是綠色的原因是,它上方的法向量幾乎都是保持(0,1,0)這樣的,所以對應起來就顯示的是綠色。

Chapter 6: Antialiasing

這一章兩個小目標~

寫個Camera類,把和相機以及光線有關的資訊封裝起來

抗鋸齒的實現

6。1 寫一個Camera類

如果我們把第五章的output圖點開,不斷放大,會發現物體的交界處是有明顯的鋸齒的。比如球的下面部分。

用Java實現一個光線追蹤渲染器

作為精緻的圖形學男孩一定要最佳化這個抗鋸齒,但是我們先來做下準備工作:寫一個Camera類,

封裝一下有關Camera的資訊

,方便以後使用,都是我們之前設定好的資訊。

public

class

Camera

{

private

Vec3

lower_left

//畫布左下角點

private

Vec3

horizontal

//寬

private

Vec3

vertical

//高

private

Vec3

origin

//相機原點

public

Camera

()

{

lower_left

=

new

Vec3

(-

2

0f

-

1

0f

-

1

0f

);

horizontal

=

new

Vec3

4

0f

0

0f

0

0f

);

vertical

=

new

Vec3

0

0f

2

0f

0

0f

);

origin

=

new

Vec3

0

0f

0

0f

0

0f

);

}

/**

*

* @param u 距離lower_left的橫向距離

* @param v 距離lower_left的縱向距離

* @return 光線向量

*/

public

Ray

GetRay

float

u

float

v

{

return

new

Ray

origin

lower_left

Add

horizontal

Scale

u

))。

Add

vertical

Scale

v

)));

}

}

6。2 抗鋸齒的實現

抗鋸齒的原理其實很簡單,我們先設定一個取樣次數,比如100,意思就是同一個點取樣100次,當然後面要再除以100,這樣我們獲得的最後的值就是這

100次取樣後得出的平均值

。然後在之前u和v上都新增一個[0,1)的

隨機數

,讓100次取樣的點不是固定一點,而是取原點附近的一個隨機的點(由於Math。random()的範圍限制,這裡其實只是取原來點右上角小正方形內的隨機點,但效果其實已經很不錯了),用來代替原來的點。這樣取樣100次後,得出的點在邊界上會明顯平滑。實際上,最好是把取樣的範圍拉成如下圖所示的範圍(即原來點往外擴散成一個正方形),聰明的你知道該怎麼改嗎?

用Java實現一個光線追蹤渲染器

int

ns

=

100

//取樣次數 消鋸齒

for

int

j

=

height

-

1

j

>=

0

j

——){

for

int

i

=

0

i

<

width

i

++){

Vec3

col

=

new

Vec3

0

0

0

);

//初始化該點的畫素

for

int

s

=

0

s

<

ns

s

++){

float

u

=

float

)(

i

+

Math

random

())/(

float

width

//新增隨機數 消鋸齒

float

v

=

float

)(

j

+

Math

random

())/(

float

height

Ray

r

=

camera

GetRay

u

v

);

//根據uv得出光線向量

col

=

col

Add

color

r

world

));

//根據每個畫素點上色 累加

}

col

=

col

Scale

1

0f

/(

float

ns

);

//除以取樣次數 求平均

int

ir

=

int

)(

255

59f

*

col

x

());

int

ig

=

int

)(

255

59f

*

col

y

());

int

ib

=

int

)(

255

59f

*

col

z

());

fw

write

ir

+

“ ”

+

ig

+

“ ”

+

ib

+

“\n”

);

}

}

看效果,還是很明顯的:

用Java實現一個光線追蹤渲染器

Chapter 7: Diffuse Materials

從這一章開始,我們終於能渲染出好看點的圖片了,也就是有點貼近真實的水平了,這一章講的是

漫反射。

日常生活中,有的物體表面是粗糙的,光線照射過去的時候,將按照不規則的方向反射,如下圖所示。

用Java實現一個光線追蹤渲染器

下面

見下圖

,我們現在就是要

模擬漫反射光線的路線

。設我們入射光線是V,我們先忽略不看上面的那個小球(那個小球是我們虛擬出來的),V射到了如下的P點上,假設它隨機反射的光線是射線S方向。那我們怎麼模擬這個方向PS呢,我們假設一個單位球(就是那個小球)是與大球相切的,切點是P。在這個小球裡,我們任取一點,作為反射的方向,用來貼近真實世界漫反射的方向機率。

用Java實現一個光線追蹤渲染器

這兩個紅色的箭頭是光漫反射的路徑

那我們來寫一下這個

隨機點的生成

,寫成一個方法需要的時候直接呼叫即可,我們用一個三維隨機數可以生成一個

單位立方體

,但是這樣生成的點並不都滿足在一個

單位球

內,所以我們要判斷一下,如果不在球內的話就迴圈再隨機一次,直到那個隨機點是球內的隨機一點。(這裡隨機數範圍調整了一下成[-1,1],大家應該都看的懂哈~)

public

Vec3

randomInUnitSphere

(){

Vec3

p

do

{

p

=

new

Vec3

((

float

)(

Math

random

()),

float

)(

Math

random

()),

float

)(

Math

random

()))。

Scale

2

0f

)。

Subtract

new

Vec3

1

0f

1

0f

1

0f

));

}

while

p

dot

p

>=

1

0f

);

return

p

}

我們上面這個方法得出的三維向量是基於原點C到S的方向,即向量CS,但我們真正要的是向量PS,所以我們可以用

向量相加

的方式,即PS=PC+CS,PC是碰撞點P點的法向量,我們之前已經存在HitRecord了,直接獲得即可~

也可以用我下面寫的方法,直接算出S點的座標,P點的座標我們也可以在HitRecord獲得,再加上單位法向量,再加上剛才求得相對隨機點,得到S座標,這裡用target表示。

然後,我們調整下color方法,如果碰到了障礙物的話,算出

新的反射光線

,新光線的原點是P,方向是PS,然後新光線又可能再次發生反射,我們假設每次反射會吸收50%的能量,對新光線返回的顏色乘以係數0。5,這樣遞迴,直到最後的光線射到天空背景上。

HitRecord

rec

=

new

HitRecord

();

//如果有撞擊點

if

world

hit

r

0

0f

Float

MAX_VALUE

rec

)){

//求出反射方向的座標

Vec3

target

=

rec

p

Add

rec

normal

)。

Add

randomInUnitSphere

());

//遞迴,每次吸收50%的能量

return

color

new

Ray

rec

p

target

Subtract

rec

p

)),

world

)。

Scale

0

5f

);

}

我們想象一條光,經過k次漫反射,最後投射到背景上,抽象出color的計算式,最後得到的大概是這樣:

PixelColor = BackgroundColor*(0.5)^k

也就是說,

物體的顏色其實是和背景有關的

,有點背景烘托物體的感覺,不然你試著把背景顏色改成紅色(渲染出來還有點恐怖呢~)。如果經過的漫反射次數太多會發生什麼呢,會使color返回的數趨向於0,最後顯示成

黑色的陰影

哦對了,這一章開始作者還進行了一個gamma最佳化處理。我們不是透過color函式返回了每個畫素(光線)的顏色,即得到了畫素RGB比例值嗎?返回的值是[0,1]之間的資料,還要乘以255才是最後的RGB值。作者進行了一個操作,把這些畫素值,進行了一個開根號的處理,因為本來的值是小於1的,開根號以後反而變大了,因此最後的畫素值會更白(畫素趨於0是黑色,趨於255是白色),因此我們經過這個gamma最佳化後,整體效果會變的更明亮。

//gamma矯正

col

=

new

Vec3

((

float

Math

sqrt

col

x

()),

float

Math

sqrt

col

y

()),

float

Math

sqrt

col

z

()));

如果沒有進行這個gamma最佳化處理的話,得出來的整個圖片是比較暗的~

這一章極其優雅結果圖如下:

用Java實現一個光線追蹤渲染器

好了,本篇文章到此為止,已經達到專欄文章字數上限了,下半部分請見: