Go で Ray tracing やってみる ~ラスタライズ編~
Go で Ray tracing やってみる ~ラスタライズ編~
あらまし
Ray cast 並びに Ray tracing についてもっと理解を深めたい。
「Ray Tracing in One Weekend」 という資料に沿って Go 言語で実装してみる。
今回はラスタライズまでを解説する。
画像を出力してみよう
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 形式) と呼ばれ、いずれも異なるプラットフォーム間でも高い互換性を保てる画像形式として開発されたものである。
例えば、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
今回は PPM を使って実装する。
PPM について
P3(PPM 形式)
フルカラーのテキスト形式。指定した最大値までの数値で、RGB の順に 10 進数のテキストで順に格納する。 数値の桁数は決まっていないため、数値と数値の間にはデリミタが必要である。
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 # 最大輝度
実装例
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())
}
以下の画像が生成される
グラデーションなどやってみるとこんなかんじ
Ray とは
直訳すると 「光線」
このようなイメージ
この緑の線を 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))
}
At 関数は、ベクトル
3 次元の場合
Go で 3 次元ベクトルを扱うのに良い 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))
}
Ray を Scene(スクリーン) に送る
レイトレーシングではスクリーン上のピクセルごとに Ray を飛ばす。
Ray は Camera (デスクトップモニターの任意の点)の位置から発生し、スクリーンの向きに飛んでいく。
Camera はスクリーンの X 軸と Y 軸と Z 軸の直交基底ベクトルを持つ。
C++ の実装例メモ
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())
}
こんな画像が出てきます。
球体を出してみよう
球は中心を原点とすると半径
という式が成り立つ。
中心=
ベクトルの中心位置が
Ray と球体の衝突した位置
ここで、先程学んだ、 Ray の方程式を用いる
代入すると
つまり
ここで
とすると
展開すると
「解の公式」が使えます。
2次方程式
ここで,
判別式といい、解がいくつあるかわかります。
なら、2つの解がある
なら、1つの解が存在
また、
に解は存在しない。
Ray と球体が衝突したかを今回は知りたいので、判別式を使うことができる。
メモ
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())
}
生成された画像
発光している風笑
まとめ
今回は単純なラスタライズまでをやりました。
現状だと、影の表現がなく、面白みがないのと、球体のジャギーが目立つので、次回は拡散反射とアンチエイリアシングをやりたいと思います。
参考
Discussion