與 Rust 勾心鬥角 · 作業

garfileo發表於2022-05-19

由於已經為網格模型(多面體)實現了計算包圍球的方法,基於網格的包圍球構造天球,使用經緯座標,便可將相機定位於天球球面上的任意一點,亦即將三維空間裡的相機定位問題轉化為二維球面定位問題,從而將問題的難度降低一個數量級。在解決該問題之前,需要先熱 POV Ray 場景語言的身

天球

天球是網格模型的外接球的放大。假設網格模型外接球的中心為 bs_center,半徑為 bs_r,放大倍數為 n,參考文獻 [1],可用 POV Ray 場景語言可將天球表示為

// 天球
sphere {
  bs_center, bs_r * n
  texture {pigment {color rgbt <0, 0, 1, 0.75>}}
  hollow
}

例如四面體

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}}
}

其包圍球中心為 <0.25, 0.25, -0.25>,半徑約為 0.829,若放大倍數為 3,則天球可表示為

#declare bs_center = <0.25, 0.25, -0.25>;
#declare bs_r = 0.829;
#declare n = 3;
#declare ss_r = bs_r * n;
sphere {
  bs_center, ss_r
  texture {pigment {color rgbt <0, 0, 1, 0.75>}}
  hollow
}

在場景中繪製天球,僅僅是便於直觀呈現網格模型的觀察空間。但是現在還不太好方便觀察天球以及網格,因為相機和光源的定位問題尚未得到妥善解決。

座標系

為了能在直覺上把握網格模型、相機和光源的相對位置,需要構建世界座標系。文 [1] 給出了使用 POV Ray 場景語言繪製三維座標系的方法,在此直接應用:

// 用於繪製任意軸向的巨集
#macro MakeAxis(Origin, Direction, Length, Thickness, Color)
  #local ArrowLength = 0.125 * Length;
  #local Direction = vnormalize(Direction);
  #local Begin = Origin;
  #local End = Begin + (Length - ArrowLength) * Direction;
  #local ArrowBegin = End;
  #local ArrowEnd = ArrowBegin + ArrowLength * Direction;
  object {
    union {
      cylinder {
        Begin, End
        Thickness
        texture { pigment { color Color } }
      }
      cone {
        ArrowBegin, 2 * Thickness
        ArrowEnd, 0
        texture { pigment { color Color } }
      }
    }
  }
#end
// 繪製世界座標系
object {
  union {
    #local Origin = <0, 0, 0>;
    sphere {
      Origin, 0.025 * ss_r
      texture { pigment { color Gray20 } }
    }
    #local Length = 0.5 * ss_r;
    #local Thickness = 0.0125 * ss_r;
    MakeAxis(Origin, x, Length, Thickness, Red)
    MakeAxis(Origin, y, Length, Thickness, Green)
    MakeAxis(Origin, z, Length, Thickness, Blue)
  }
}

隨便找個位置觀看

先較為隨意地將相機安放在能在頭腦中直接想出來的一個位置,只要保證這個位置距離四面體足夠遠即可,例如

camera {
  location <5, 5, 5>
  look_at <0, 0, 0>
}

相機的視點(拍攝位置)look_at 是世界座標系原點。

光源也可以隨意安放,只要保證四面體是在它的籠罩下即可,例如

light_source {
  <1, 3, 10>
  color White
}

現在看到的景象是

隨便看到的景象

現在將相機向四面體拉近一些,並將光源設成無影:

// 相機
camera {
  location 0.5 * <5, 5, 5>
  look_at bs_center
}

// 光源
light_source {
  <1, 3, 10>
  color White
  shadowless
}

看到的景象變為

更近了

日出東南隅

現在假設 <0, 0, -1> 為正南方向,<1, 0, 0> 為正東方向。將相機放在正南方位,視點為原點,光源放在東南方位的上空:

// 相機
camera {
  location 4 * <0, 0, -1>
  look_at <0, 0, 0>
}

// 光源
light_source {
  10 * <1, 1, 1>
  color White
  shadowless
}

相機拍攝的正北影像如下圖所示:

北望

此時,只能看到 x 軸(紅軸)和 y 軸(綠軸),看不到 z 軸,因為 z 軸垂直指向螢幕內部。將相機所在的這個位置定義為經度為 0 度, 維度為 0 度。相對於相機的位置,則光源所在的位置,經度為東經 45 度,維度為北緯 45 度。

若用 U 和 V 分別表示經緯度,則相機的位置可表示為

camera {
  // n 是調整相機與視點(拍攝位置)之間距離遠近的係數
  location n * vorate(vrotate(<0, 0, -1>, -U * y), V * x);
  look_at <0, 0, 0>
}

例如,倘若將相機放在西經 20 度,北緯 30 度的位置,只需

#declare U = -20; // 負數表示西經,正數表示東經
#declare V = 30;  // 負數表示南維,負數表示北緯
camera {
  location 5 * vrotate(vrotate(-z, -U * y), V * x)
  look_at <0, 0, 0>
}

注意,在 POV Ray 場景語言裡,<0, 0, -1> 可寫為 -z,即 -<0, 0, 1>vrotate(v, a * b) 是 POV Ray 場景語言內定的巨集,用於將向量 vxyz 軸轉動轉動角度 a

同理,光源也可以採用經緯度的方式設定,例如位於東經 45 度和北緯 45 度上空的光源,可定義為

light_source {
  10 * vrotate(vrotate(-z, -45 * y), 45 * x)
  color White
  shadowless
}

現在相機拍到的畫面如下圖所示。

從西南方的上空俯瞰

建立光源和相機的關係

相機和光源可以存在一個固定的關係。在 POV Ray 場景裡,相機本質上是如下圖所示的區域性座標系

相機座標系

將相機的 up 軸平移,令其過原點,然後將相機的位置繞 up 軸按左手定則轉 -45 度,所得位置可作為光源位置。

相機的 up 軸如何得到呢?由於相機一開始是在 -z 處,此時它的 up 軸與 y 軸平行,且方向相同。只需將 y 軸按經緯度旋轉,便可將其變換為相機所在經緯度的 up 軸,亦即

#declare up_dir = vrotate(vrotate(y, V * x), -U * y);

將相機位置繞 up_dir 方向按左手定則轉 -45 度

#declare sky_xyz = 5 * vrotate(vrotate(-z, -U * y), V * x);
#declare sun_xyz = vaxis_rotate(sky_xyz, up_dir, -45);

POV Ray 語言內定的巨集 vaxis_rotate(a, b, c) 用於將向量 a 圍繞向量 b 遵循左手定則轉動角度 c

上述過程構造的相機位置和光源位置之間的關係,猶如我們右手舉著手電筒觀察暗處的物品。

但是,上述過程構造的 up_dir 只是無數個方向中的一個。在日常中我們使用相機,倘若是正著拍照,上述的 up_dir 是合適的,但是倘若歪相機拍——類似於歪著頭看東西,可以讓 up_dir 繞視點到 sky_xyz 的方向構成的軸向一個角度來實現,例如向右側歪斜 15 度角:

#declare U = -20; // 負數表示西經,正數表示東經
#declare V = 30;  // 負數表示南緯,正數表示北緯
#declare up_dir = vrotate(vrotate(y, V * x), -U * y);
#declare sky_xyz = 5 * vrotate(vrotate(-z, V * x), -U * y);
#declare sun_xyz = vaxis_rotate(sky_xyz, up_dir, -45);

camera {
  location sky_xyz
  sky vaxis_rotate(up_dir, sky_xyz, 15)
  look_at <0, 0, 0>
}

上述程式碼中,sky 用於設定相機的 up 方向,拍到的景象如下圖所示:

相機向右歪了

平移變換

上述內容討論的相機設定,為了簡單起見,將視點設為世界座標系的原點,並圍繞該點設定相機和光源。實際上,若想讓網格模型出現在畫面中心區域,應當將視點視為網格模型包圍球的中心 bs_center。為此,需將上述設定的相機和光源位置進行平移變換:

// 相機和光源
#declare U = -20; // 負數表示西經,正數表示東經
#declare V = 30;  // 負數表示南緯,正數表示北緯
#declare up_dir = vrotate(vrotate(y, V * x), -U * y);
#declare sky_xyz = 5 * vrotate(vrotate(-z, V * x), -U * y);
#declare oblique_dir = vaxis_rotate(up_dir, sky_xyz, 15);
#declare sun_xyz = vaxis_rotate(sky_xyz, oblique_dir, -45);
#declare sky_xyz = sky_xyz + bs_center;
#declare sun_xyz = sun_xyz + bs_center;

camera {
  location sky_xyz
  sky oblique_dir
  look_at bs_center
}

light_source {
  sun_xyz
  color White
  shadowless
}

拍到的景象如下圖所示,與上一節相比,僅僅是檢視的中心有所變化。

視點略有變化

拍攝系統

經過上述一番「推導」,便可構造一個用於拍攝網格模型的系統了。該系統只需要三個引數:網格模型包圍球的放大倍數、相機的經緯度以及相機 up 軸向的偏轉角度。

作業

用 Rust 語言實現上述的拍攝系統。

我的實現見:https://gitee.com/garfileo/rskynet

作業完成後,我給我家的汗血寶馬拍了一張照片:

參考

[1] https://segmentfault.com/a/11...

[2] https://segmentfault.com/a/11...

附錄

foo.inc:

#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.pov:

#version 3.7;
#include "colors.inc"
global_settings {
  assumed_gamma 1.0
}

#macro MakeAxis(Origin, Direction, Length, Thickness, Color)
  #local ArrowLength = 0.125 * Length;
  #local Direction = vnormalize(Direction);
  #local Begin = Origin;
  #local End = Begin + (Length - ArrowLength) * Direction;
  #local ArrowBegin = End;
  #local ArrowEnd = ArrowBegin + ArrowLength * Direction;
  object {
    union {
      cylinder {
        Begin, End
        Thickness
        texture { pigment { color Color } }
      }
      cone {
        ArrowBegin, 2 * Thickness
        ArrowEnd, 0
        texture { pigment { color Color } }
      }
    }
  }
#end

// 四面體
#include "foo.inc"
object {
  foo
  texture {pigment {color Red}}
}

// 天球
#declare bs_center = <0.25, 0.25, -0.25>;
#declare bs_r = 0.829;
#declare sky_n = 3;
#declare sky_r = bs_r * sky_n;
sphere {
  bs_center,3 *  sky_r
  texture {pigment {color rgbt <0, 0, 1, 0.75>}}
  hollow
}

object {
  union {
    #local Origin = <0, 0, 0>;
    sphere {
      Origin, 0.025 * sky_r
      texture { pigment { color Gray20 } }
    }
    #local Length = 0.75 * sky_r;
    #local Thickness = 0.0125 * sky_r;
    MakeAxis(Origin, x, Length, Thickness, Red)
    MakeAxis(Origin, y, Length, Thickness, Green)
    MakeAxis(Origin, z, Length, Thickness, Blue)
  }
}

// 相機和光源
#declare U = -20; // 負數表示西經,正數表示東經
#declare V = 30;  // 負數表示南緯,正數表示北緯
#declare up_dir = vrotate(vrotate(y, V * x), -U * y);
#declare sky_xyz = 5 * vrotate(vrotate(-z, V * x), -U * y);
#declare oblique_dir = vaxis_rotate(up_dir, sky_xyz, 15);
#declare sun_xyz = vaxis_rotate(sky_xyz, oblique_dir, -45);
#declare sky_xyz = sky_xyz + bs_center;
#declare sun_xyz = sun_xyz + bs_center;


camera {
  location sky_xyz
  sky oblique_dir
  look_at bs_center
}

light_source {
  sun_xyz
  color White
  shadowless
}

相關文章