4. ソリッドテクスチャ
Ray Tracing: The Next Week (v3.2.3): 4 Solid Textures / 2.4 ソリッドテクスチャ
グラフィックスにおけるテクスチャとは,物体表面の色をプロシージャルに(あるいは画像参照によって)決める関数の総称です。ソリッドテクスチャは入力に世界座標
本章では「色」をテクスチャに格上げし,マテリアルが定数色だけでなく任意のテクスチャを参照できるようにします。新規の道具立ては次の 4 つです。
Textureトレイト:value(u, v, p)で点に色を返すSolidColor:定数色のテクスチャCheckerTexture:3 次元チェッカーHitRecordにを追加し, Sphere/MovingSphereで計算する
Lambertian のメンバを Color から Arc<dyn Texture> に切り替えますが,Lambertian::new(Color) のシグネチャは互換維持します(内部で SolidColor に包む)。これにより第 1 編の r1XX クレートと第 2 編の r201 / r203 はそのままビルドできます。
Texture トレイトと SolidColor
pub trait Texture: Send + Sync {
fn value(&self, u: f64, v: f64, p: Point3) -> Color;
}
pub struct SolidColor {
pub color_value: Color,
}
impl SolidColor {
pub fn new(color_value: Color) -> Self {
SolidColor { color_value }
}
}
impl Texture for SolidColor {
fn value(&self, _u: f64, _v: f64, _p: Point3) -> Color {
self.color_value
}
}Send + Sync を要求するのは,Arc<dyn Texture> を将来的に別スレッドへ渡したり並列レンダリングへ拡張する余地を確保するためです。Material トレイトと同じ流儀です。
HitRecord に を加える
テクスチャは表面座標 HitRecord にこの 2 値を保存します。
pub struct HitRecord {
pub p: Point3,
pub normal: Vec3,
pub t: f64,
pub u: f64, // ← 追加
pub v: f64, // ← 追加
pub front_face: bool,
pub mat: Option<Arc<dyn Material>>,
}HitRecord を構築するのは Sphere::hit と MovingSphere::hit の 2 箇所だけです。両方とも更新します。
球の 座標
単位球上の点
球面上の点
と置くと,逆解は
で,
と正規化します。これを単純な自由関数として実装します。
pub fn get_sphere_uv(p: Point3) -> (f64, f64) {
// p.y() is in [-1, 1]; theta is in [0, pi]; phi is in [0, 2*pi].
let theta = (-p.y()).acos();
let phi = (-p.z()).atan2(p.x()) + PI;
(phi / (2.0 * PI), theta / PI)
}Sphere::hit では outward_normal(中心から表面への単位ベクトル)が単位球上の点に他ならないので,そのまま渡せます。
let outward_normal = (p - self.center) / self.radius;
let (u, v) = get_sphere_uv(outward_normal);
let mut rec = HitRecord {
t: root,
u, v,
p,
normal: outward_normal,
front_face: false,
mat: self.material.clone(),
};MovingSphere::hit も同じく outward_normal から
C++ と Rust の違い
C++ 原典では get_sphere_uv を sphere クラスの private static メソッドとして書き,出力引数 double& u, double& v で値を返します。Rust ではタプル (f64, f64) を返すフリー関数として実装し,MovingSphere からも pub use 経由で再利用できるようにしました。出力引数は Rust ではあまり使われず,値を返すスタイルが標準的です。
Lambertian のテクスチャ化
Lambertian が保持するのは「色」ではなく「テクスチャ」になります。
pub struct Lambertian {
pub albedo: Arc<dyn Texture>,
}
impl Lambertian {
/// Compatibility constructor: wraps a constant color in `SolidColor`.
pub fn new(albedo: Color) -> Self {
Lambertian {
albedo: Arc::new(SolidColor::new(albedo)),
}
}
/// Variant that accepts any texture directly.
pub fn with_texture(albedo: Arc<dyn Texture>) -> Self {
Lambertian { albedo }
}
}
impl Material for Lambertian {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
let mut scatter_direction = rec.normal + random_unit_vector();
if scatter_direction.near_zero() {
scatter_direction = rec.normal;
}
let scattered = Ray::with_time(rec.p, scatter_direction, r_in.time());
let attenuation = self.albedo.value(rec.u, rec.v, rec.p);
Some((attenuation, scattered))
}
}Lambertian::new(Color) の呼び出し箇所はすべて従来どおり動作し,テクスチャを使いたい新コードは Lambertian::with_texture(...) を呼びます。これで第 1 編・第 2 編前半のクレート群はそのままビルドできます。
C++ と Rust の違い
C++ 原典は lambertian のメンバを shared_ptr<texture> albedo にしてコンストラクタを 2 つオーバーロードします。Rust にはコンストラクタも関数オーバーロードもないので,前章までの Vec::new / Vec::with_capacity 流儀に合わせて new と with_texture で名前を分けました。Material::scatter の中で albedo->value(...) を呼ぶ流れは原典と同じです。
Metal や Dielectric には今のところテクスチャ化の必要がない(金属は単色アルベド,誘電体はそもそもアルベドを持たない)ので変更しません。
チェッカーテクスチャ
3 軸の正弦の積
pub struct CheckerTexture {
pub even: Arc<dyn Texture>,
pub odd: Arc<dyn Texture>,
}
impl CheckerTexture {
pub fn new(even: Arc<dyn Texture>, odd: Arc<dyn Texture>) -> Self {
CheckerTexture { even, odd }
}
/// Convenience constructor from two solid colors.
pub fn from_colors(c1: Color, c2: Color) -> Self {
CheckerTexture {
even: Arc::new(SolidColor::new(c1)),
odd: Arc::new(SolidColor::new(c2)),
}
}
}
impl Texture for CheckerTexture {
fn value(&self, u: f64, v: f64, p: Point3) -> Color {
let sines = (10.0 * p.x()).sin() * (10.0 * p.y()).sin() * (10.0 * p.z()).sin();
if sines < 0.0 {
self.odd.value(u, v, p)
} else {
self.even.value(u, v, p)
}
}
}even / odd を Arc<dyn Texture> にしているので,子テクスチャに別の CheckerTexture を入れたり,将来のパーリンノイズや画像テクスチャを混ぜたりできます。Pat Hanrahan が 1980 年代に提案したシェーダーネットワークの発想です。
デモ 1:r204-checker-ground
2.3 の random_scene() の地面の球(半径 1000 の灰色球)だけを,CheckerTexture::from_colors で作ったチェッカーに差し替えます。
let checker = Arc::new(CheckerTexture::from_colors(
Color::new(0.2, 0.3, 0.1),
Color::new(0.9, 0.9, 0.9),
));
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -1000.0, 0.0),
1000.0,
Arc::new(Lambertian::with_texture(checker)),
)));シーンの残り(小さな球とその上の 3 つの大きな球,BVH,シャッター区間)はすべて 2.3 と同じです。BVH のおかげで 100 spp でも妥当な時間で描画できます。
ページ冒頭の WASM デモで確認できます。地面は球なので,チェッカーは大きく湾曲して見えます。
デモ 2:r204-checker-spheres
原典の Image 3 に対応する two_spheres() シーンを書き起こします。同じチェッカーテクスチャを共有する半径 10 の 2 球(中心
fn two_spheres() -> HittableList {
let mut objects = HittableList::new();
let checker = Arc::new(CheckerTexture::from_colors(
Color::new(0.2, 0.3, 0.1),
Color::new(0.9, 0.9, 0.9),
));
objects.add(Box::new(Sphere::with_material(
Point3::new(0.0, -10.0, 0.0),
10.0,
Arc::new(Lambertian::with_texture(checker.clone())),
)));
objects.add(Box::new(Sphere::with_material(
Point3::new(0.0, 10.0, 0.0),
10.0,
Arc::new(Lambertian::with_texture(checker)),
)));
objects
}カメラパラメータは lookfrom = (13, 2, 3),lookat = (0, 0, 0),vfov = 20°,絞り 0(被写界深度なし),シャッター区間
checker.clone() で 2 つの Lambertian に同じ Arc を共有させているのは,テクスチャ実体を 1 つに保ちたいからです。Arc の参照カウントが増えるだけで,テクスチャ自体は複製されません。
まとめ
Textureトレイト・SolidColor・CheckerTextureをcommon/に追加した。HitRecordにを加え, Sphere/MovingSphereでget_sphere_uvから計算するようにした。Lambertianを「色」ではなく「テクスチャ」を持つマテリアルに変更した。Lambertian::new(Color)の互換性は維持し,新たにLambertian::with_texture(Arc<dyn Texture>)を追加した。r204-checker-groundとr204-checker-spheresの 2 つのデモで効果を確認した。
これで「色をテクスチャとして抽象化する」基盤が整いました。次章ではこの上にパーリンノイズを載せ,木目や大理石のような自然な質感の生成へ進みます。