摘要:本文介紹了使用為編寫代碼生成器的基本思路。所實現的代碼生成器重視的建模功能,而忽視其光線追蹤渲染功能。至于點的尺寸,可將其視為包圍盒外接球空間的最小長度單位,并使之與包圍盒外接球半徑成固定比例。
引言
POV-Ray 是一種專業的三維場景描述語言,它描述的三維場景可交由 POV-Ray 的解析器(或編譯器)采用光線跟蹤技術進行渲染,渲染結果為位圖。
POV-Ray 語言是圖靈完備的,亦即其他編程語言能寫出來的程序,使用 POV-Ray 語言總能等價地寫出來。不過,這個世界上不會有人打算使用 POV-Ray 語言來編寫網絡服務程序、GUI 程序以及那些運行在手機上的 APP,更何況也寫不出來。兩個程序等價,是數學意義上的,而不是物理意義上的。許多時候,我們是在編寫一些非圖形渲染類的程序時,需要繪制一些三維圖形,這時就可以考慮如何用自己最熟悉的編程語言去驅動 POV-Ray 這支畫筆,即為 POV-Ray 編寫代碼生成器。
本文介紹了使用 Python 3 為 POV-Ray 編寫代碼生成器的基本思路。所實現的代碼生成器重視 POV-Ray 的建模功能,而忽視其光線追蹤渲染功能。凡涉及渲染之處,僅僅使用 POV-Ray 的一些非常簡單的渲染語句,這種處理頗類似于繪畫藝術中的白描。因為我之所以有動力寫這份文檔,是因為我要使用 POV-Ray 來繪制我的一些論文與演示文檔里的插圖。這些插圖以表意為主,基本不需要考慮如何讓觀閱它們的人信以為真。之所以選擇 Python 語言來驅動 POV-Ray,是因為它便于我在寫文檔的過程中可以忽略許多數據結構與內存管理上的細節。實際上,在寫這份文檔之前,我一直在用 C 語言生成 POV-Ray 代碼。
我不會對 POV-Ray 與 Python 語言本身作太多介紹,因為我對它們也僅僅是略知一二。我在附錄中給出了我曾經粗略翻過的 POV-Ray 與 Python 的一些文檔的鏈接。
模型、視圖與控制器在計算機中繪制圖形,無論是二維還是三維,無論是古代還是現代,一直存在著一個基本范式,即:模型-視圖-控制器。在使用 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 視圖的控制器。
其實,這種范式無處不在。與其說是哪個人發明了它,不如說這是人類處理復雜任務時的本能反應。簡單舉個餐飲業的例子,農民為餐飲業提供了模型,廚師為餐飲業創造了視圖,食客為餐飲業創造了控制器。用經濟學的術語來說,就是「生產的社會化」,強調的是有規模的分工與合作。
模型下面從最簡單的任務開始,以點與點集的繪制為例,講述如何用 Python 實現 POV-Ray 模型。
POV-Ray 語言沒有提供點對象的繪制語法,但是可以使用直徑很小的球體來表示點對象:
sphere {, r }
基于這一發現,就可以構造點集模型:
#declare points = union { sphere {, r} sphere { , 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 { ") 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 + "} ") model.write("} // " + model_name + " end ") model.close() @
注:上述代碼片段首部的 @ points-to-pov.py # + 的 + 符號表示在已存在的 points-to-pov.py 代碼片段之后追加一段代碼。
現在,只需將 points_parser 與 output_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 { ") 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 + "} ") model.write("} // " + model_name + " end ") 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 { locationlook_at }
location 參數定義了相機的位置,look_at 參數定義的是相機待拍攝的三維場景的中心,即相機鏡頭光心要對準的位置。
對于拍攝點集模型而言,通常會希望點集能夠完整且最大化的出現在所拍攝照片中,因此相機參數的設定依賴點集模型的位置與尺寸。
除了相機之外,POV-Ray 視圖還需要光源。沒有光,就沒有圖像。在 POV-Ray 視圖里像上帝那樣創造一個太陽,并不困難:
light_source {color White }
light_source { <0, 5000, -5000> color White }
這樣的光源就類似于在一個位于 (0, 0, 0) 的物體的正前上方高掛著一個太陽。只要沒有物體比這樣的光源更高遠,就無需考慮物體的位置與尺寸。
現在,待繪制的點集模型、相機與光源均已出現,它們構成了一幅完整的 POV-Ray 視圖:
camera { locationlook_at } light_source { 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; ", "#include "colors.inc" ", "background {color White} ", "global_settings {assumed_gamma 1.0} " ] view.writelines(prelude) @
下面開始考慮如何構造視圖的基本要素。
首先給出點集包圍球的中心與半徑,并將包圍球的中心作為視圖的初始中心:
@ points-to-pov.py # + def space_of_scene(view, x, r): view.write("#declare model_center = <%f, %f, %f>; " % ((x[0], x[1], x[2]))) view.write("#declare model_radius = %f; " % (r)) view.write("#declare view_center = model_center; ") @
然后擺放相機:
@ points-to-pov.py # + def place_camera(view): view.write("camera { ") view.write(" location <0, 0, -model_radius> + model_center * z ") view.write(" look_at <0, 0, 0> ") view.write(" translate view_center ") view.write("} ") @
上述代碼可將相機擺放點集模型的正前方,光心對準點集包圍球的中心,并且相機的光心到點集包圍球中心的距離與包圍球半徑相同。
注:POV-Ray 的坐標系是左手系,z 軸的正方向指向屏幕內部。因此,相機相對于點集的位移為負值,意味著沿 z 軸負方向移動。
再來看光源的設定:
@ points-to-pov.py # + def place_light_source(view, color = [1.0, 1.0, 1.0]): view.write("light_source { ") view.write(" model_center + <0, 0, -10 * model_radius> ") view.write(" color rgb <%f, %f, %f> " % (color[0], color[1], color[2])) view.write(" shadowless // 無影光源 ") view.write("} ") @
光源的位置被設定在相機的正上方,與相機的距離為 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; " % (r * s)) view.write("#include "" + model_name + ".inc" ") view.write("object { ") view.write(" " + model_name + " ") view.write(" pigment { ") view.write(" color rgb <%f, %f, %f> " % (color[0], color[1], color[2])) view.write(" } ") view.write("} ") @
將上述函數組合起來,便可得到視圖文件生成器:
@ 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 的組合。
附錄POV-Ray 3.7 指南
更豐富的 POV-Ray 指南
Dive Into Python 3
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/40985.html
摘要:微信小程序之與唯品會來場粉紅色的邂逅買買買,雖然雙十二剛過,可是唯品會的折扣卻是依然火爆。一打開頁面,便是粉色的主頁映入眼簾,琳瑯滿目的商品,讓我這個月光族過了把眼癮。 Welcome to miaomiaoXiongs segmentfault 微信小程序之--(與唯品會來場粉紅色的邂逅 ???) 買買買,雖然雙十二剛過,可是唯品會的折扣卻是依然火爆。一打開頁面,便是粉色的主頁映入...
摘要:微信小程序之與唯品會來場粉紅色的邂逅買買買,雖然雙十二剛過,可是唯品會的折扣卻是依然火爆。一打開頁面,便是粉色的主頁映入眼簾,琳瑯滿目的商品,讓我這個月光族過了把眼癮。 Welcome to miaomiaoXiongs segmentfault 微信小程序之--(與唯品會來場粉紅色的邂逅 ???) 買買買,雖然雙十二剛過,可是唯品會的折扣卻是依然火爆。一打開頁面,便是粉色的主頁映入...
摘要:微信小程序之與唯品會來場粉紅色的邂逅買買買,雖然雙十二剛過,可是唯品會的折扣卻是依然火爆。一打開頁面,便是粉色的主頁映入眼簾,琳瑯滿目的商品,讓我這個月光族過了把眼癮。 Welcome to miaomiaoXiongs segmentfault 微信小程序之--(與唯品會來場粉紅色的邂逅 ???) 買買買,雖然雙十二剛過,可是唯品會的折扣卻是依然火爆。一打開頁面,便是粉色的主頁映入...
閱讀 1088·2021-11-16 11:44
閱讀 1373·2019-08-30 13:12
閱讀 2411·2019-08-29 16:05
閱讀 3076·2019-08-28 18:29
閱讀 912·2019-08-26 13:41
閱讀 3233·2019-08-26 13:34
閱讀 2602·2019-08-26 10:35
閱讀 939·2019-08-26 10:28