最簡單的靜物是四面體。我曾用一份 OFF 檔案 foo.off 記錄了一個四面體,即
OFF
4 4 6
0 0 0
1 0 0
0 1 0
0 0 1
3 0 1 2
3 0 1 3
3 1 2 3
3 0 2 3
rskynet 專案的第一個使命,就是呈現該四面體的面目。
POV Ray 的網格模型
foo.off 所記錄的四面體資訊,在 POV Ray 場景裡可等價表述為
mesh2 {
vertex_vectors {
4,
<0, 0, 0>, <1, 0, 0>, <0, 1, 0>, <0, 0, -1>
}
face_indices {
4,
<0, 1, 2>, <0, 1, 3>, <1, 2, 3>, <0, 2, 3>
}
}
在 POV Ray 場景語言裡,mesh2
表示網格結構的第 2 種,至於第 1 種以及其他網格結構,在此不必深究。需要注意的是,POV Ray 的座標系是左手系,因此 mesh2
裡所有頂點的 z 座標(第三個座標)與 foo.off 裡的所有頂點的 z 座標相反。
圍繞上述網格結構,構造一份 POV Ray 場景檔案 foo.pov,其內容如下:
// 固定的檔案頭,適用 POV Ray 3.7 版本
#version 3.7;
#include "colors.inc"
global_settings {assumed_gamma 1.0}
// 四面體
mesh2 {
vertex_vectors {
4,
<0, 0, 0>, <1, 0, 0>, <0, 1, 0>, <0, 0, -1>
}
face_indices {
4,
<0, 1, 2>, <0, 1, 3>, <1, 2, 3>, <0, 2, 3>
}
texture {pigment {color Red}}
}
// 相機
camera {
location <-1, -1, 1>
look_at <0, 0, 0>
}
// 光源
light_source {
<0, -3, 10>
color White
}
使用 povray 解析 foo.pov:
$ povray +A +P foo.pov
所得結果為 foo.png,即下圖
模型與檢視
對上一節的 foo.pov 檔案內容稍作變化,首先將 mesh2
部分取出並將封存於變數 foo
:
#declare foo = mesh2 {
vertex_vectors {
4,
<0, 0, 0>, <1, 0, 0>, <0, 1, 0>, <0, 0, -1>
}
face_indices {
4,
<0, 1, 2>, <0, 1, 3>, <1, 2, 3>, <0, 2, 3>
}
}
將上述內容放入新建的檔案 foo.inc——與 foo.pov 位於同一目錄,然後將 foo.pov 修改為
// 固定的檔案頭,適用 POV Ray 3.7 版本
#version 3.7;
#include "colors.inc"
global_settings {
assumed_gamma 1.0
}
// 四面體
#include "foo.inc"
object {
foo
texture {pigment {color Red}}
}
// 相機
camera {
location <-1, -1, 1>
look_at <0, 0, 0>
}
// 光源
light_source {
<0, -3, 10>
color White
}
如此便實現了 POV Ray 場景的模型和檢視的分離,foo.inc 為模型檔案,foo.pov 為檢視檔案。
生成模型檔案
由於 rskynet 程式已經能夠解析 OFF 檔案,並將網格資訊儲存於 Mesh
結構體,因此只需為 Mesh
增加一個方法,便可生成模型檔案。例如,
fn mesh_fmt<T: Length + ops::Index<usize>>(v: &Vec<T>) -> String
where <T as ops::Index<usize>>::Output: fmt::Display,
<T as ops::Index<usize>>::Output: Sized {
let mut s = String::new();
let m = v.len();
s += format!(" {},\n", m).as_str();
for i in 0 .. m - 1 {
let n = v[i].len();
assert_eq!(n, 3);
s += " <";
for j in 0 .. n - 1 {
s += format!("{}, ", v[i][j]).as_str();
}
s += format!("{}>,\n", -v[i][ n - 1]).as_str();
}
let n = v[m - 1].len();
assert_eq!(n, 3);
s += " <";
for j in 0 .. n - 1 {
s += format!("{}, ", v[m - 1][j]).as_str();
}
s += format!("{}>\n }}\n", -v[m - 1][n - 1]).as_str();
return s;
}
impl<T: fmt::Display> Mesh<T> {
pub fn output_povray_model(&self, path: &str) {
assert_eq!(self.n, 3);
let path = Path::new(path);
let mut file = File::create(path).unwrap();
let name = path.file_stem().unwrap().to_str().unwrap();
file.write_all(format!("#declare {} = mesh2 {{\n", name).as_bytes()).unwrap();
// 輸出點表
file.write_all(" vertex_vectors {\n".as_bytes()).unwrap();
file.write_all(mesh_fmt(&self.points).as_bytes()).unwrap();
// 輸出面表
file.write_all(format!(" face_indices {{\n").as_bytes()).unwrap();
file.write_all(mesh_fmt(&self.facets).as_bytes()).unwrap();
file.write_all("}\n".as_bytes()).unwrap();
}
}
上述程式碼為 Mesh
增加了一個 output_povray_model
的方法,其用法如下:
let dim = 3;
let mut mesh: Mesh<f64> = Mesh::new(dim);
mesh.load("data/foo.off");
for x in &mut mesh.points { // 右手系 -> 左手系
x[2] *= -1.0;
}
mesh.output_povray_model("data/foo.inc");
for x in &mut mesh.points { // 左手系 -> 右手系
x[2] *= -1.0;
}
由於 Mesh
是泛型結構,我幾乎找不到好方法可以在 output_povray_model
中對 Mesh
頂點集合裡的每個頂點的第三個座標進行取反,因此只能針對泛型例項進行座標變換。於是,我又一次後悔將 Mesh
定義為泛型型別。
如無十足把握,請謹慎考慮使用 Rust 泛型。
小結
將 OFF 檔案轉化為 POV Ray 模型檔案是簡單的,因為二者的資訊等同。真正困難的是生成 POV Ray 檢視檔案。通過之前的例子可以看到,作為檢視檔案裡最重要的內容是相機和光源的設定,例如
// 相機
camera {
location <-1, -1, 1>
look_at <0, 0, 0>
}
// 光源
light_source {
<0, -3, 10>
color White
}
若想得到理想的場景渲染結果,相機和光源皆需要合適的定位。例如,倘若將上述光源修改為
light_source {
<-10, -10, 10>
color White
}
則四面體的渲染結果看上去是一個三角形,如下圖所示:
模型是客觀事物,是死的。檢視是主觀事物,是活的。要抓活物,最好是用天網。天網恢恢,疏而不漏。rskynet,是用 Rust 語言寫的 skynet。