當 Python 邂逅 POV-Ray

引言

POV-Ray 是一種專業的三維場景描述語言,它描述的三維場景可交由 POV-Ray 的解析器(或編譯器)採用光線跟蹤技術進行渲染,渲染結果爲位圖。html

POV-Ray 語言是圖靈完備的,亦即其餘編程語言能寫出來的程序,使用 POV-Ray 語言總能等價地寫出來。不過,這個世界上不會有人打算使用 POV-Ray 語言來編寫網絡服務程序、GUI 程序以及那些運行在手機上的 APP,更況且也寫不出來。兩個程序等價,是數學意義上的,而不是物理意義上的。許多時候,咱們是在編寫一些非圖形渲染類的程序時,須要繪製一些三維圖形,這時就能夠考慮如何用本身最熟悉的編程語言去驅動 POV-Ray 這支畫筆,即爲 POV-Ray 編寫代碼生成器。python

本文介紹了使用 Python 3 爲 POV-Ray 編寫代碼生成器的基本思路。所實現的代碼生成器重視 POV-Ray 的建模功能,而忽視其光線追蹤渲染功能。凡涉及渲染之處,僅僅使用 POV-Ray 的一些很是簡單的渲染語句,這種處理頗相似於繪畫藝術中的白描。由於我之因此有動力寫這份文檔,是由於我要使用 POV-Ray 來繪製個人一些論文與演示文檔裏的插圖。這些插圖以表意爲主,基本不須要考慮如何讓觀閱它們的人信覺得真。之因此選擇 Python 語言來驅動 POV-Ray,是由於它便於我在寫文檔的過程當中能夠忽略許多數據結構與內存管理上的細節。實際上,在寫這份文檔以前,我一直在用 C 語言生成 POV-Ray 代碼。git

我不會對 POV-Ray 與 Python 語言自己做太多介紹,由於我對它們也僅僅是略知一二。我在附錄中給出了我曾經粗略翻過的 POV-Ray 與 Python 的一些文檔的連接。github

模型、視圖與控制器

在計算機中繪製圖形,不管是二維仍是三維,不管是古代仍是現代,一直存在着一個基本範式,即:模型-視圖-控制器。在使用 Python 驅動 POV-Ray 的過程當中,這個範式依然有效。編程

直接使用 POV-Ray 語言,能夠像下面這樣描述這個範式:數組

@ model.inc # [POV-Ray]
... 定義一些模型 ...
@
@ view.inc # [POV-Ray]
... 設置光源與相機  ...

#include "model.inc" // 加載模型

... 繪製模型 ...
@
@ controller.ini # [POV-Ray]
... 設置動畫參數 ...
@

注:上述諸如 @ model.inc # [POV-Ray] 之類的語句,可理解爲註釋。位於符號 @# 之間的文本是文件名,位於 # 符號以後的 [...] 中的文本表示其後面的代碼段所用的語言。每一個代碼段的尾部都有一個 @ 符號,表示代碼段至此終止。這種註解形式來自我寫的一個文式編程工具所支持的標記,詳見 https://github.com/liyanrui/orez網絡

POV-Ray 語言並無刻意強調模型-視圖-控制器範式,但其語法可以很天然地描述這種範式。模型注重的是幾何形體,視圖注重的是如何觀察幾何形體,而控制器用於控制視角的變化。POV-Ray 雖然只能給出位圖形式的渲染結果,但因爲它提供了定時器功能,利用這一功能可生成一組視角有序變化的渲染結果,而後將它們組合爲 GIF 格式的動圖,我將這種方式稱爲 POV-Ray 視圖的控制器。數據結構

其實,這種範式無處不在。與其說是哪一個人發明了它,不如說這是人類處理複雜任務時的本能反應。簡單舉個餐飲業的例子,農民爲餐飲業提供了模型,廚師爲餐飲業創造了視圖,食客爲餐飲業創造了控制器。用經濟學的術語來講,就是「生產的社會化」,強調的是有規模的分工與合做。app

模型

下面從最簡單的任務開始,以點與點集的繪製爲例,講述如何用 Python 實現 POV-Ray 模型。less

POV-Ray 語言沒有提供點對象的繪製語法,可是可使用直徑很小的球體來表示點對象:

sphere {
  <x, y, z>, r
}

<x, y, z> 爲球心,r 爲半徑。

基於這一發現,就能夠構造點集模型:

#declare points = union {
  sphere {<x1, y1, z1>, r}
  sphere {<x2, y2, z2>, r}
  ... ... ...
}

#declare 是 POV-Ray 提供的用於定義局部變量的指令。points 是變量的名字。union 是 POV-Ray 的三維實體模型的布爾運算符,它可將一組三維實體合併爲一個對象。上述代碼中,全部小球的半徑相同。

如今開始考慮,如何使用 Python 語言自動生成上述的點集模型的 POV-Ray 描述。假設有一份名爲 points.asc 的文本文件,其中的每一行文本存儲一個三維點的座標值,例如:

2.3 4.5 1.1
3.0 8.7 11.3
... ... ...

若使用 Python 語言寫一個程序,讓它來解析 points.asc 文件,而後再基於解析到的點集數據生成 POV-Ray 模型文件,這樣就能夠避免手動去寫一大堆 sphere 語句。更重要的是,多數狀況下,像 points.asc 這樣的文件是現有的,例如一些程序輸出的數據與三維掃描設備從實物表面上採集到的數據等等。

下面是一份從 points.asc 這樣的文件解析三維點集數據的 Python 代碼:

@ points-to-pov.py # [Python]
def points_parser(src_file_name, right_handed = True, dim = 3):
    points = []
    with open(src_file_name, 'r') as src:
        for line in src:
            coords = line.split()
            if len(coords) != dim:
                continue
            x = []
            for t in coords:
                x.append(float(t))
            if right_handed: 
                x[dim - 1] = -x[dim - 1] # 將 x 的座標從右手座標系變換到左手座標系
            points.append(x)
    src.close()
    return points

@

注:POV-Ray 的座標系是左手系。若待解析的三維點集數據位於右手系,那麼在解析過程當中須要對每一個點的最後一個維度的座標取相反數。

像這樣的功能,用 C/C++ 之類的語言來寫,會比較繁瑣;用 POV-Ray 語言也能寫得出來,依然會比較繁瑣。

繼續用 Python 語言將解析所得點集轉化爲 POV-Ray 模型,並以文件的形式保存:

@ points-to-pov.py # +
def output_points_model(points, model_name, dim = 3):
    model = open(model_name + '.inc', 'w')
    model.write('#declare ' + model_name + ' = union {\n') 
    for x in points:
        model.write('  sphere {<')
        for i in range(0, dim):
            if i < dim - 1:
                model.write('%f, ' % (x[i]))
            else:
                model.write('%f' % (x[i]))
        model.write('>, point_size_of_' + model_name + '}\n')
    model.write('} // ' + model_name + ' end\n') 
    model.close()

@

注:上述代碼片斷首部的 @ points-to-pov.py # ++ 符號表示在已存在的 points-to-pov.py 代碼片斷以後追加一段代碼。

如今,只需將 points_parseroutput_points_model 組合起來即可將一份點集數據文件轉化爲 POV-Ray 點集模型文件。例如將 foo.asc 轉化爲 foo.inc 文件,而且兩者位於同一目錄:

points = points_parser('foo.asc')
output_points_model(points, 'foo')

若結合 Python 的命令行參數方式,即可寫出一個可將任意一份三維點集數據轉化爲 POV-Ray 模型文件的小工具:

@ test.py # [Python]
# points-to-pov.py @

import os
import sys
if __name__=="__main__":
    path = sys.argv[1]
    (parent, file_name) = os.path.split(path)
    (model_name, ext_name) = os.path.splitext(file_name)
    points = points_parser(path)
    output_points_model(points, model_name)
@

注:上述代碼片斷中的 # points_model.py @ 表示將全部名爲 points_model.py 的代碼片斷聚集於該語句出現之處。

爲了照顧一下至此還沒有看懂上述代碼片斷首部與尾部的那些 @ 代碼片斷 # [語言標記] + 或 ^+ 運算 之類符號的人,下面給出 test.py 的完整代碼:

def points_parser(src_file_name, right_handed = True, dim = 3):
    points = []
    with open(src_file_name, 'r') as src:
        for line in src:
            coords = line.split()
            if len(coords) != dim:
                continue
            x = []
            for t in coords:
                x.append(float(t))
            if right_handed: 
                x[dim - 1] = -x[dim - 1] # 將 x 的座標從右手座標系變換到左手座標系
            points.append(x)
    src.close()
    return points

def output_points_model(points, model_name, dim = 3):
    model = open(model_name + '.inc', 'w')
    model.write('#declare ' + model_name + ' = union {\n') 
    for x in points:
        model.write('  sphere {<')
        for i in range(0, dim):
            if i < dim - 1:
                model.write('%f, ' % (x[i]))
            else:
                model.write('%f' % (x[i]))
        model.write('>, point_size_of_' + model_name + '}\n')
    model.write('} // ' + model_name + ' end\n') 
    model.close()

import sys
if __name__=="__main__":
    path = sys.argv[1]
    (parent, file_name) = os.path.split(path)
    (model_name, ext_name) = os.path.splitext(file_name)
    points = points_parser(path)
    output_points_model(points, model_name)

實際上在我這裏,上述代碼是經過我寫的一個名字叫 orez 的工具直接從這份文檔中提取獲得:

$ orez -t -e test.py python-meeting-povray.md

其中,python-meeting-povray.md 即是這份文檔的名字。

試着讓 Python 解釋器執行

$ python3 test.py foo.asc

結果會在 src.asc 所在的目錄中產生 foo.inc 文件,其內容相似:

#declare foo = union {
  sphere { <3.554705, 199.173300, 8.394049>, point_size_of_foo}
  sphere { <3.667395, 198.429900, 10.576820>, point_size_of_foo}
  ... ... ...
} // foo end

其中,foo_size 是小球的半徑值,可是如今它只是一個還沒有定義的變量,它的值須要在在視圖中進行肯定。

在視圖看來,模型是什麼?

前面說過,模型-視圖-控制器這個範式,各個部分是分工合做的關係,而不是隻分工不合做的關係。在上述的點集模型構造過程當中,用於表示點的小球的半徑是一個未定義的變量,它須要在視圖中進行定義。所以,對於點集的繪製這一任務而言,視圖與模型之間最基本的合做是視圖須要爲點集模型肯定小球的半徑。

下面是針對點集的視圖與模型之間一種很是簡單又粗暴的合做方式:

#declare foo_size = 0.1;
#include "foo.inc"
object {
  foo
  pigment {
    color rgb <0.5, 0.5, 0.5>
  }
}

這樣,在視圖文件中載入 foo.inc 時,foo_size 的值就是 0.1

這種簡單粗暴的合做方式帶來的問題是,foo_size 的取值有時會不合適,過小了,會致使點集不可見,太大了,看到的又每每是一堆相交的球體,以至看不清點集的形貌。

如今,姑且容忍這種簡單又粗暴的方式,繼續考慮爲點集模型與視圖之間是否還存在其餘方面的聯繫,這須要從 POV-Ray 視圖的基本結構開始考慮。

在 POV-Ray 視圖結構中,首先要考慮相機的擺放,例如:

camera {
  location <x, y, z>
  look_at <x, y, z>
}

location 參數定義了相機的位置,look_at 參數定義的是相機待拍攝的三維場景的中心,即相機鏡頭光心要對準的位置。

對於拍攝點集模型而言,一般會但願點集可以完整且最大化的出如今所拍攝照片中,所以相機參數的設定依賴點集模型的位置與尺寸。

除了相機以外,POV-Ray 視圖還須要光源。沒有光,就沒有圖像。在 POV-Ray 視圖裏像上帝那樣創造一個太陽,並不困難:

light_source {
  <x, y, z>
  color White
}

<x, y, z> 表示光源的位置。color White 表示光源的顏色是白光。光源的位置也依賴於點集模型的位置與尺寸,只是不像相機那樣敏感。一般狀況下,只要光源的位置足夠高遠,它老是可以照到待渲染的模型上的,例如:

light_source {
  <0, 5000, -5000>
  color White
}

這樣的光源就相似於在一個位於 (0, 0, 0) 的物體的正前上方高掛着一個太陽。只要沒有物體比這樣的光源更高遠,就無需考慮物體的位置與尺寸。

如今,待繪製的點集模型、相機與光源均已出現,它們構成了一幅完整的 POV-Ray 視圖:

camera {
  location <x, y, z>
  look_at <x, y, z>
}

light_source {
  <x, y, z>
  color White
}

#declare foo_size = 0.1;
#include "foo.inc"
object {
  foo
  pigment {
    color rgb <0.5, 0.5, 0.5>
  }
}

相機、光源以及點的尺寸等參數的肯定皆與繪製的點集模型的位置與尺寸相關。那麼,點集模型的位置與尺寸該如何給出?一個簡單又有效的方法是計算點集模型的軸向最小包圍盒,以包圍盒的中心做爲點集的中心。相機與光源若都位於包圍盒的外接球空間以外,而且相機的光心對準包圍盒的中心,那麼就能夠保證點集模型可見而且老是位於相機的拍攝範圍以內。至於點的尺寸,可將其視爲包圍盒外接球空間的最小長度單位,並使之與包圍盒外接球半徑成固定比例。

點集的包圍球

下面的代碼可計算基於點集的軸向最小包圍盒的外接球的中心與半徑:

@ points-to-pov.py # +
import math
def space_of_points(points, dim = 3):
    llc = []
    urc = []
    for i in range(0, dim):
        llc.append(sys.float_info.max)
        urc.append(-sys.float_info.max)
    for x in points:
        for i in range(0, dim):
            if llc[i] > x[i]:
                llc[i] = x[i]
            if urc[i] < x[i]:
                urc[i] = x[i]
    center = []
    squared_d = 0.0
    for i in range(0, dim):
        center.append(0.5 *  (urc[i] + llc[i]))
        t = urc[i] - llc[i]
        squared_d += t * t
    r = 0.5 * math.sqrt(squared_d)
    return (center, r)

@

生成 POV-Ray 視圖文件

如上文所述,一旦得到了點集的包圍球的中心與半徑,即可進行相機、光源以及點的尺寸等參數的設定,從而生成 POV-Ray 視圖文件。有了視圖文件,即可由 POV-Ray 解析器生成渲染結果。不過,事情沒那麼簡單。固然也沒那麼複雜。POV-Ray 解析器(至少 3.7 版本)對一些 POV-Ray 代碼有一些硬性要求,即一些代碼必須提供,不然就會給出警告信息。這部分代碼與繪製點集模型基本上沒有太大關係,所以下面將其隔離對待。此外,爲了能讓基本的渲染功能正常進行,也須要載入 POV-Ray 的一些預約義文件,例如顏色的預約義文件。可將這些代碼看做是視圖文件的「導言」:

@ points-to-pov.py # +
def view_prelude(view):
    prelude = [
        '#version 3.7;\n',
        '#include "colors.inc"\n',
        'background {color White}\n',
        'global_settings {assumed_gamma 1.0}\n\n'
    ]
    view.writelines(prelude)

@

下面開始考慮如何構造視圖的基本要素。

首先給出點集包圍球的中心與半徑,並將包圍球的中心做爲視圖的初始中心:

@ points-to-pov.py # +
def space_of_scene(view, x, r):
    view.write('#declare model_center = <%f, %f, %f>;\n' % ((x[0], x[1], x[2])))
    view.write('#declare model_radius = %f;\n' % (r))
    view.write('#declare view_center = model_center;\n\n')

@

而後擺放相機:

@ points-to-pov.py # +
def place_camera(view):
    view.write('camera {\n')
    view.write('  location <0, 0, -model_radius> + model_center * z\n')
    view.write('  look_at <0, 0, 0>\n')
    view.write('  translate view_center\n')
    view.write('}\n\n')

@

上述代碼可將相機擺放點集模型的正前方,光心對準點集包圍球的中心,而且相機的光心到點集包圍球中心的距離與包圍球半徑相同。

注:POV-Ray 的座標系是左手系,z 軸的正方向指向屏幕內部。所以,相機相對於點集的位移爲負值,意味着沿 z 軸負方向移動。

再來看光源的設定:

@ points-to-pov.py # +
def place_light_source(view, color = [1.0, 1.0, 1.0]):
    view.write('light_source {\n')
    view.write('  model_center + <0, 0, -10 * model_radius>\n')
    view.write('  color rgb <%f, %f, %f>\n'  % (color[0], color[1], color[2]))
    view.write('    shadowless // 無影光源\n')
    view.write('}\n\n')

@

光源的位置被設定在相機的正上方,與相機的距離爲 10 * model_radius

最後將點集模型放到三維場景中:

@ points-to-pov.py # +
def place_model(view, model_name, s, color = [0.5, 0.5, 0.5]):
    view.write('#declare point_size_of_' + model_name + ' = %f;\n' % (r * s))
    view.write('#include "' + model_name + '.inc"\n')
    view.write('object {\n')
    view.write('  ' + model_name + '\n')
    view.write('  pigment {\n')
    view.write('    color rgb <%f, %f, %f>\n'  % (color[0], color[1], color[2]))
    view.write('  }\n')
    view.write('}\n')

@

將上述函數組合起來,即可獲得視圖文件生成器:

@ test-2.py # [Python]
# points-to-pov.py @

import os
import sys
if __name__=="__main__":
    point_size = float(sys.argv[1])
    path = sys.argv[2]
    (parent, file_name) = os.path.split(path)
    (model_name, ext_name) = os.path.splitext(file_name)
    
    # 生成模型文件 
    points = points_parser(path)
    output_points_model(points, model_name)
    
    # 生成視圖文件
    (center, r) = space_of_points(points)
    with open(model_name + '.pov', 'w') as view:
        view_prelude(view)
        space_of_scene(view, center, r)
        place_camera(view)
        place_light_source(view)
        place_model(view, model_name, point_size)
    view.close()
@

測試:

$ python3 test-2.py 0.003 foo.asc
$ povray +P foo.pov

0.003 是點的尺寸係數,它與點集的包圍半徑的積即是點的實際尺寸。

折騰到這裏,終於能看到一張圖了。上述命令最終獲得的渲染結果爲 foo.png:

點集模型的白描

產生上述渲染結果的視圖文件以下:

#version 3.7;
#include "colors.inc"
background {color White}
global_settings {assumed_gamma 1.0}

#declare model_center = <2.413898, 15.227750, 1.339995>;
#declare model_radius = 3.807916;
#declare view_center = model_center;

camera {
  location <0, 0, -model_radius> + model_center * z
  look_at <0, 0, 0>
  translate view_center
}

light_source {
  model_center + <0, 0, -10 * model_radius>
  color rgb <1.000000, 1.000000, 1.000000>
  shadowless
}

#declare point_size_of_points = 0.011424;
#include "points.inc"
object {
  points
  pigment {
    color rgb <0.500000, 0.500000, 0.500000>
  }
}

控制器

一番辛苦,看到的只是一幅簡單的點集圖像,的確很丟 POV-Ray 的臉,然而這就是所謂的「白描」。若想獲得美侖美奐的渲染結果,不只須要對 POV-Ray 足夠熟悉,也須要具有必定的美術功底。不過,全部的修飾都集中在視圖部分。模型是不變的。事實上能獲得這種白描的結果,已是邁出了一大步。

如今來考慮控制器的構建。與模型、視圖的代碼生成器相比,POV-Ray 的控制器更簡單一些,由於根本不須要爲它編寫代碼生成器。就像要獲得精美的渲染結果只須要修改視圖部分,控制器也是如此,一切只須要動手去修改視圖文件。值得一提的是,POV-Ray 提供了製做動畫的功能。利用這一功能,可讓上面的白描渲染結果動起來。老話說,一動不如一靜,然而現代人看書的人少啊,看片的人多。

我要讓上面所繪製的模型向左偏移 15 度角,而後再向右偏移 15 度角,即讓它搖晃一個角度,輕輕搖晃,一點一點搖晃。要實現這一想法,只需將上述的視圖文件 foo.pov 的 object 部分做如下修改:

#declare joggle = 30;
object {
  foo
  translate -model_center
  rotate #if (clock < 0.51) clock #else (1 - clock) #end * joggle * y
  translate model_center
  pigment {
    color rgb <0.300000, 0.300000, 0.300000>
  }
}

上述代碼只是對點集模型增長了平移與旋轉變換:(1) 平移點集模型,使其中心與座標系原點重合;(2) 將點集模型向左緩慢偏移 15 度角,再向右緩慢偏移 15 度角;(3) 將點集的中心恢復到原來的位置。

而後在 foo.pov 的同一目錄增長 foo.ini 文件,內容以下:

Input_File_Name = foo.pov
Initial_Frame = 1
Final_Frame = 30

接下來,將 POV-Ray 解析器做用於 foo.ini:

$ povray foo.ini

上述命令須要一些時間,待其運行結束後,會產生 30 幅圖片,名稱爲 foo01.png, foo02.png, ... foo30.png。使用 imagemagick 工具箱的 convert 命令可將這組圖片合成爲 GIF 動圖 foo.gif:

$ convert -delay 10 -loop 0 foo*.png foo.gif

結果以下圖所示:

白描的動圖

結語

雖然本文檔僅介紹了點集模型的繪製,可是對於更復雜的圖形繪製而言, 0 和 1 已經有了,剩下的事情是 0 和 1 的組合。

附錄

  1. POV-Ray 3.7 指南
  2. 更豐富的 POV-Ray 指南
  3. Dive Into Python 3
相關文章
相關標籤/搜索