与 Rust 勾心斗角 · 最简单的静物

最简单的静物是四面体。我曾用一份 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,即下图
与 Rust 勾心斗角 · 最简单的静物
文章图片

模型与视图 对上一节的 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>(v: &Vec) -> String where >::Output: fmt::Display, >::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 Mesh { 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 = 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 }

则四面体的渲染结果看上去是一个三角形,如下图所示:
【与 Rust 勾心斗角 · 最简单的静物】与 Rust 勾心斗角 · 最简单的静物
文章图片

模型是客观事物,是死的。视图是主观事物,是活的。要抓活物,最好是用天网。天网恢恢,疏而不漏。rskynet,是用 Rust 语言写的 skynet。

    推荐阅读