最简略的静物是四面体。我曾用一份 OFF 文件 foo.off 记录了一个四面体,即

OFF4 4 60 0 01 0 00 1 00 0 13 0 1 23 0 1 33 1 2 33 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>) -> Stringwhere <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。