Go で Ray tracing やってみる ~ラスタライズ編~

2022/08/01に公開

Go で Ray tracing やってみる ~ラスタライズ編~

あらまし

Ray cast 並びに Ray tracing についてもっと理解を深めたい。
「Ray Tracing in One Weekend」 という資料に沿って Go 言語で実装してみる。

https://raytracing.github.io/

今回はラスタライズまでを解説する。

画像を出力してみよう

PNM フォーマットについて

PNM (Portable Any Map 形式[1][2]、Netpbm 形式) は簡素な 2 次元ラスター画像形式のひとつである。PNM は特定の 1 種類の画像フォーマットではなく、異なるカラーモードをサポートするための 3 種類の画像形式をまとめて呼ぶときに使われる総称である[3]。これらの形式は、それぞれ portable pixmap format (PPM 形式)、portable graymap format (PGM 形式)、portable bitmap format (PBM 形式) と呼ばれ、いずれも異なるプラットフォーム間でも高い互換性を保てる画像形式として開発されたものである。

https://en.wikipedia.org/wiki/Netpbm

例えば、PBM はこのように書ける。

P1
# comment
6 8
0 0 0 0 0 0
0 1 0 0 1 0
0 1 0 1 0 0
0 1 1 0 0 0
0 1 1 0 0 0
0 1 0 1 0 0
0 1 0 0 1 0
0 0 0 0 0 0

スクリーンショット 2022-05-26 12.45.19.png

今回は PPM を使って実装する。

PPM について

P3(PPM 形式)
フルカラーのテキスト形式。指定した最大値までの数値で、RGB の順に 10 進数のテキストで順に格納する。 数値の桁数は決まっていないため、数値と数値の間にはデリミタが必要である。

RGB について

https://ja.wikipedia.org/wiki/RGB#:~:text=RGB(またはRGBカラーモデル,混合の一種である。

8 bit color
8 * 3 = 24 bit
R 8 bit -> 256 パターン
G 8 bit -> 256 パターン
B 8 bit -> 256 パターン

PPM では RGB を 24bit で表現

PPM のヘッダー

  • P3 # フォーマット
  • 200 100 # x y サイズ
  • 255 # 最大輝度

スクリーンショット 2022-05-26 12.39.21.png

実装例

package main

import (
	"fmt"
	"os"
	"strings"
)

type RGB struct {
	R int
	G int
	B int
}

const (
	HeaderFormat = "%s\n%d %d\n%d\n"
	BodyFormat   = "%d %d %d\n"
)

type Image struct {
	Format    string
	X         int
	Y         int
	MaxBright int
	Header    string
	Body      strings.Builder
	Color     RGB
}

func (img Image) CreateHeader() string {
	return fmt.Sprintf(HeaderFormat, img.Format, img.X, img.Y, img.MaxBright)
}

func (img Image) CreateP3Data() Image {
	img.Header = img.CreateHeader()
	for j := 0; j < img.Y; j++ {
		for i := 0; i < img.X; i++ {
			img.Color.R = 173
			img.Color.G = 255
			img.Color.B = 47
			img.Body.WriteString(fmt.Sprintf(BodyFormat, img.Color.R, img.Color.G, img.Color.B))
		}
	}
	return img
}

func (img Image) CreateFile(filename string, header string, body string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.WriteString(header)
	if err != nil {
		return err
	}
	_, err = file.WriteString(body)
	if err != nil {
		return err
	}
	return nil
}

func main() {
	img := Image{}
	img.Format = "P3"
	img.X = 200
	img.Y = 100
	img.MaxBright = 255
	result := img.CreateP3Data()

	filename := "test.ppm"
	img.CreateFile(filename, result.Header, result.Body.String())
}

以下の画像が生成される

test1.png

グラデーションなどやってみるとこんなかんじ

test.png

Ray とは

直訳すると 「光線」

このようなイメージ
画面収録_2022-05-26_12_56_45_AdobeCreativeCloudExpress.gif

この緑の線を Ray と呼ぶ

プログラムで Ray を表現

// You can edit this code!
// Click here and start typing.
package main

import "fmt"

type Ray struct {
	Origin    float64
	Direction float64
}

func (r Ray) At(t float64) float64 {
	return r.Origin + t*r.Direction
}

func main() {
	ray := Ray{}
	ray.Origin = 0.0
	ray.Direction = 1.0
	fmt.Println(ray.At(5.0))
}

https://go.dev/play/p/08a0uDYqtPA

At 関数は、ベクトル \vec{A} から ベクトル \vec{B} への任意の点を線形補完を用いてパラメータ t を指定して求める。

スクリーンショット 2022-05-26 13.12.42.png

3 次元の場合

Go で 3 次元ベクトルを扱うのに良い r3 というライブラリがあるのでこちらを利用する。

https://pkg.go.dev/github.com/golang/geo/r3

package main

import (
	"fmt"

	"github.com/golang/geo/r3"
)

type Ray struct {
	Origin    r3.Vector
	Direction r3.Vector
}

func NewVector(x, y, z float64) r3.Vector {
	return r3.Vector{
		X: x,
		Y: y,
		Z: z,
	}
}

func NewRay(origin r3.Vector, direction r3.Vector) Ray {
	return Ray{Origin: origin, Direction: direction}
}

func (r Ray) At(t float64) r3.Vector {
	return r3.Vector{
		X: r.Origin.X + t*r.Direction.X,
		Y: r.Origin.Y + t*r.Direction.Y,
		Z: r.Origin.Z + t*r.Direction.Z,
	}
}

func main() {
	ray := Ray{}
	ray.Origin = NewVector(0.0, 0.0, 0.0)
	ray.Direction = NewVector(0.0, 0.0, 0.1)
	fmt.Println(ray.At(5.0))
}

https://go.dev/play/p/-Po4ihi2dqs

Ray を Scene(スクリーン) に送る

レイトレーシングではスクリーン上のピクセルごとに Ray を飛ばす。
Ray は Camera (デスクトップモニターの任意の点)の位置から発生し、スクリーンの向きに飛んでいく。

Camera はスクリーンの X 軸と Y 軸と Z 軸の直交基底ベクトルを持つ。

スクリーンショット 2022-05-26 13.50.00.png

C++ の実装例メモ

https://raytracing.github.io/books/RayTracingInOneWeekend.html#rays,asimplecamera,andbackground/sendingraysintothescene

package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/golang/geo/r3"
)

type RGB struct {
	R int
	G int
	B int
}

const (
	HeaderFormat = "%s\n%d %d\n%d\n"
	BodyFormat   = "%d %d %d\n"
)

type Image struct {
	Format    string
	X         int
	Y         int
	MaxBright int
	Header    string
	Body      strings.Builder
	Color     RGB
}

type Ray struct {
	Origin    r3.Vector
	Direction r3.Vector
}

func NewRay(origin r3.Vector, direction r3.Vector) Ray {
	return Ray{Origin: origin, Direction: direction}
}

func Color(ray Ray) r3.Vector {
	result := r3.Vector{
		X: 0.8,
		Y: 0.7,
		Z: 0.8,
	}
	return result
}

func (img Image) CreateHeader() string {
	return fmt.Sprintf(HeaderFormat, img.Format, img.X, img.Y, img.MaxBright)
}

func (img Image) CreateP3Data() Image {
	img.Header = img.CreateHeader()
	lowerLeftCorner := r3.Vector{
		X: -4,
		Y: -2,
		Z: -1.0,
	}
	horizontal := r3.Vector{
		X: 8.0,
		Y: 0.0,
		Z: 0.0,
	}
	vertical := r3.Vector{
		X: 0.0,
		Y: 4.0,
		Z: 0.0,
	}
	origin := r3.Vector{
		X: 0.0,
		Y: 0.0,
		Z: 0.0,
	}
	for j := 0; j < img.Y; j++ {
		for i := 0; i < img.X; i++ {
			u := float64(i) / float64(img.X)
			v := float64(j) / float64(img.Y)
			fmt.Println("p:", lowerLeftCorner.Add(
				horizontal.Mul(u).Add(vertical.Mul(v)),
			),
			)
			ray := NewRay(
				origin,
				lowerLeftCorner.Add(
					horizontal.Mul(u).Add(vertical.Mul(v)),
				).Sub(origin),
			)
			fmt.Println("ray:", ray)
			col := Color(ray)
			img.Color.R = int(255.99 * col.X)
			img.Color.G = int(255.99 * col.Y)
			img.Color.B = int(255.99 * col.Z)
			img.Body.WriteString(fmt.Sprintf(BodyFormat, img.Color.R, img.Color.G, img.Color.B))
		}
	}
	return img
}

func (img Image) CreateFile(filename string, header string, body string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.WriteString(header)
	if err != nil {
		return err
	}
	_, err = file.WriteString(body)
	if err != nil {
		return err
	}
	return nil
}

func main() {
	img := Image{}
	img.Format = "P3"
	img.X = 800
	img.Y = 400
	img.MaxBright = 255
	result := img.CreateP3Data()

	filename := "test.ppm"
	img.CreateFile(filename, result.Header, result.Body.String())
}

https://go.dev/play/p/8Rs0K53aj7T

こんな画像が出てきます。

test2.png

球体を出してみよう

球は中心を原点とすると半径 r を使って

x^2+y^2+z^2 = r^2

という式が成り立つ。

中心= Cx,Cy,Cz

(x-Cx)^2+(y-Cy)^2+(z-Cz)^2 = r^2

ベクトルの中心位置が \vec{C} で位置が \vec{P}

(\vec{P}-\vec{C})\cdot(\vec{P}-\vec{C}) = r^2

Ray と球体の衝突した位置

ここで、先程学んだ、 Ray の方程式を用いる

\vec{P}(t) = \vec{A} + t\vec{B}

代入すると

(\vec{P}(t)-\vec{C})\cdot (\vec{P}(t)-\vec{C}) = r^2

つまり

(\vec{A} + t\vec{B} - \vec{C})\cdot (\vec{A} + t\vec{B} - \vec{C}) = r^2

ここで

AC = \vec{A} - \vec{C}

とすると

(AC + t\vec{B})\cdot (AC + t\vec{B}) - r^2 = 0

展開すると

(\vec{B}\cdot\vec{B})t^2 + 2(\vec{B}\cdot AC)t + (AC\cdot AC) - r^2 = 0

「解の公式」が使えます。

2次方程式

ax^2 + bx + c = 0
の解は

x = \frac{-b\pm\sqrt{b^2-4ac}}{2a}

AC を展開して,

ax^2 + bx + c = 0

a = (\vec{B}\cdot\vec{B})
b = 2(\vec{B}\cdot (\vec{A} - \vec{C}))
c = ((\vec{A} - \vec{C})\cdot (\vec{A} - \vec{C})) - r^2

ここで,

D = b^2-4ac

判別式といい、解がいくつあるかわかります。

D>0

なら、2つの解がある

D=0

なら、1つの解が存在

x = -\frac{b}{2a}


また、

D < 0

に解は存在しない。

Ray と球体が衝突したかを今回は知りたいので、判別式を使うことができる。

メモ
https://raytracing.github.io/books/RayTracingInOneWeekend.html#addingasphere/ray-sphereintersection

Go で実装してみる。

package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/golang/geo/r3"
)

type RGB struct {
	R int
	G int
	B int
}

const (
	HeaderFormat = "%s\n%d %d\n%d\n"
	BodyFormat   = "%d %d %d\n"
)

type Image struct {
	Format    string
	X         int
	Y         int
	MaxBright int
	Header    string
	Body      strings.Builder
	Color     RGB
}

type Ray struct {
	Origin    r3.Vector
	Direction r3.Vector
}

func NewRay(origin r3.Vector, direction r3.Vector) Ray {
	return Ray{Origin: origin, Direction: direction}
}

func HitSphere(center r3.Vector, radius float64, ray Ray) bool {
	oc := ray.Origin.Sub(center)
	a := ray.Direction.Dot(ray.Direction)
	b := 2.0 * oc.Dot(ray.Direction)
	c := oc.Dot(oc) - radius*radius
	detect := b*b-4*a*c >= 0
	return detect
}

func Color(ray Ray) r3.Vector {
	center := r3.Vector{
		X: 0.0,
		Y: 0.0,
		Z: -1.0,
	}
	if HitSphere(center, 0.7, ray) {
		return r3.Vector{
			X: 0.2,
			Y: 1.0,
			Z: 0.2,
		}
	}
	unit := ray.Direction.Normalize()
	t := unit.Y + 1
	result := r3.Vector{
		X: 0.2,
		Y: 0.3,
		Z: 0.2,
	}.Mul(t)
	return result
}

func (img Image) CreateHeader() string {
	return fmt.Sprintf(HeaderFormat, img.Format, img.X, img.Y, img.MaxBright)
}

func (img Image) CreateP3Data() Image {
	img.Header = img.CreateHeader()
	lowerLeftCorner := r3.Vector{
		X: -4.0,
		Y: -2.0,
		Z: -1.0,
	}
	horizontal := r3.Vector{
		X: 8.0,
		Y: 0.0,
		Z: 0.0,
	}
	vertical := r3.Vector{
		X: 0.0,
		Y: 4.0,
		Z: 0.0,
	}
	origin := r3.Vector{
		X: 0.0,
		Y: 0.0,
		Z: 0.0,
	}
	for j := 0; j < img.Y; j++ {
		for i := 0; i < img.X; i++ {
			u := float64(i) / float64(img.X)
			v := float64(j) / float64(img.Y)
			fmt.Println("p:", lowerLeftCorner.Add(
				horizontal.Mul(u).Add(vertical.Mul(v)),
			),
			)
			ray := NewRay(
				origin,
				lowerLeftCorner.Add(
					horizontal.Mul(u).Add(vertical.Mul(v)),
				).Sub(origin),
			)
			fmt.Println("ray:", ray)
			col := Color(ray)
			img.Color.R = int(255.99 * col.X)
			img.Color.G = int(255.99 * col.Y)
			img.Color.B = int(255.99 * col.Z)
			img.Body.WriteString(fmt.Sprintf(BodyFormat, img.Color.R, img.Color.G, img.Color.B))
		}
	}
	return img
}

func (img Image) CreateFile(filename string, header string, body string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.WriteString(header)
	if err != nil {
		return err
	}
	_, err = file.WriteString(body)
	if err != nil {
		return err
	}
	return nil
}

func main() {
	img := Image{}
	img.Format = "P3"
	img.X = 800
	img.Y = 400
	img.MaxBright = 255
	result := img.CreateP3Data()

	filename := "test.ppm"
	img.CreateFile(filename, result.Header, result.Body.String())
}

https://go.dev/play/p/8Rs0K53aj7T

生成された画像

test3.png

発光している風笑

スクリーンショット 2022-05-26 16.19.23.png

まとめ

今回は単純なラスタライズまでをやりました。
現状だと、影の表現がなく、面白みがないのと、球体のジャギーが目立つので、次回は拡散反射とアンチエイリアシングをやりたいと思います。

参考

https://ja.wikipedia.org/wiki/レイトレーシング

https://en.wikipedia.org/wiki/Netpbm

https://raytracing.github.io/

GitHubで編集を提案

Discussion