與 Rust 勾心鬥角 · 最簡單的靜物

garfileo發表於2022-05-17

最簡單的靜物是四面體。我曾用一份 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。

相關文章