レイトレーサーを書く
少し前にFということになる.1つの言語を学ぶ必要があるだけでなく、その全体の巨大な生態系があるので、ネット体験は少し困難です.だから大きなプロジェクトでいくつかの経験を得るために自分自身をコピージェイムズバックの優れた本を得たThe Ray Tracer Challenge そしてゆっくりと私の道を通っています.祝日のために、私はまだ期待していたのと全く同じではありませんでしたGitLab repository . ここで私が学んだ/これまで実装.
これは、おそらく味付けのために明らかであるものの一つです.ネット開発者は、プロジェクトとソリューションの違いを把握するのに時間がかかりました.私は、主なコードのための1つの2つのプロジェクトに定住しました
本は、単一のタプルタイプを定義し、両方のポイントとベクトルの両方を使用して起動します.閉じるこの動画はお気に入りから削除されています
The 私はあまりにも多くのカスタム演算子とモジュール関数を定義したくなかったので、私は算術演算のオーバーライドを提供するための静的メソッドを使用します. 浮動小数点演算は乱雑になるので、カスタム等値演算子があります
この単純なモジュールはRGBカラーを表して、非常に類似したアプローチ
私たちはポイント、ベクトル、色を持っているので、私たちはほとんど物事を描く準備ができています.以下のモジュールは、1を表し、PPM イメージ.
私はまだまだ私の行列の実装では完了していないが、これは現在のように基づいている
最初の章では、開始位置と速度を与えられた発射体の飛行経路を計算するための少しのスクリプトを書いて、重力と風から成る環境を終えます.第2章の終わりに、このスクリプトは、キャンバスに軌道をプロットし、PPM画像として保存するために強化されます.
それは見た目にはあまり意味がないが、私のコードから生成されたイメージを見ることは非常に満足していた.
これまでのところ、これは本当に楽しい挑戦と良い学習経験されています.ご質問やご提案があれば私に知らせてください、私はコードが様々な場所で改善される可能性があることを確認します.それはまた、ソリューションを比較するのも素晴らしいでしょうので、もしあなたがコードを参照してくださいしたいのです.
プロジェクトレイアウト
これは、おそらく味付けのために明らかであるものの一つです.ネット開発者は、プロジェクトとソリューションの違いを把握するのに時間がかかりました.私は、主なコードのための1つの2つのプロジェクトに定住しました
Raytracer
), テストのための1つRaytracer.Tests
, 使用FsUnit.Xunit ).├── LICENSE
├── README.md
├── Raytracer
│ ├── Canvas.fs
│ ├── Color.fs
│ ├── Constants.fs
│ ├── Matrix.fs
│ ├── Program.fs
│ ├── Raytracer.fsproj
│ ├── Tuples.fs
│ ├── bin
│ └── obj
├── Raytracer.Tests
│ ├── CanvasTests.fs
│ ├── ColorTests.fs
│ ├── MatrixTests.fs
│ ├── Program.fs
│ ├── Raytracer.Tests.fsproj
│ ├── Scripts
│ ├── TestUtilities.fs
│ ├── TuplesTests.fs
│ ├── bin
│ └── obj
├── Raytracer.sln
└── img
└── ch02-projectile.ppm
注:あなたはScripts
テストプロジェクト内のフォルダ.本の最初の2つの章では、その点まで書かれたコードを行使するための小さなスクリプトを書いて終了します.私は彼らのために3番目のプロジェクトを加えることを考えました、しかし、それがoverkillであると決めたので、ちょうど既存のテストプロジェクトに彼らを加えました:種類
ベクトルと点
本は、単一のタプルタイプを定義し、両方のポイントとベクトルの両方を使用して起動します.閉じるこの動画はお気に入りから削除されています
magnitude
だけでなく、ベクトルの意味を確認します.私は何度か実装を行ったり来たりしていましたが、F - CHERHIで初めてクラスを使用していましたが、結局は少しの重複は間違った抽象化より有害ではないと判断しました.このようにして、1つのタイプの実装を変更することができました Vector4
他に影響しないでください.namespace Raytracer.Types
open Raytracer.Constants
module rec Tuples =
module Point =
type T =
{ X : float
Y : float
Z : float
W : float }
static member (+) (p, v : Vector.T) =
{ X = p.X + v.X
Y = p.Y + v.Y
Z = p.Z + v.Z
W = p.W + v.W }
static member (-) (p, v : Vector.T) =
{ X = p.X - v.X
Y = p.Y - v.Y
Z = p.Z - v.Z
W = p.W - v.W }
static member (-) (p1, p2) : Vector.T =
{ X = p1.X - p2.X
Y = p1.Y - p2.Y
Z = p1.Z - p2.Z
W = p1.W - p2.W }
static member (.=) (p1, p2) =
abs (p1.X - p2.X) < epsilon && abs (p1.Y - p2.Y) < epsilon
&& abs (p1.Z - p2.Z) < epsilon && abs (p1.W - p2.W) < epsilon
let make x y z =
{ X = x
Y = y
Z = z
W = 1.0 }
module Vector =
type T =
{ X : float
Y : float
Z : float
W : float }
static member (+) (v1, v2) =
{ X = v1.X + v2.X
Y = v1.Y + v2.Y
Z = v1.Z + v2.Z
W = v1.W + v2.W }
static member (-) (v1, v2) =
{ X = v1.X - v2.X
Y = v1.Y - v2.Y
Z = v1.Z - v2.Z
W = v1.W - v2.W }
static member (~-) (v) =
{ X = -v.X
Y = -v.Y
Z = -v.Z
W = -v.W }
static member ( * ) (v, scalar) =
make (v.X * scalar) (v.Y * scalar) (v.Z * scalar)
static member (/) (v, scalar) =
{ X = v.X / scalar
Y = v.Y / scalar
Z = v.Z / scalar
W = v.W / scalar }
static member (.=) (v1, v2) =
abs (v1.X - v2.X) < epsilon && abs (v1.Y - v2.Y) < epsilon
&& abs (v1.Z - v2.Z) < epsilon && abs (v1.W - v2.W) < epsilon
let make x y z =
{ X = x
Y = y
Z = z
W = 0.0 }
let magnitude v = sqrt (v.X * v.X + v.Y * v.Y + v.Z * v.Z)
let normalize v =
let mag = magnitude v
make (v.X / mag) (v.Y / mag) (v.Z / mag)
let dot v1 v2 = v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z
let cross v1 v2 =
make (v1.Y * v2.Z - v1.Z * v2.Y) (v1.Z * v2.X - v1.X * v2.Z)
(v1.X * v2.Y - v1.Y * v2.X)
これはすべてかなり標準ですが、いくつかのことが指摘する価値があります.Tuples
モジュールはrec
キーワードは、2つの含まれるタイプを参照できるように..=
) 指定したEpsilon内で値が等しいかどうかをチェックするにはRaytracer.Constants
モジュール).色
この単純なモジュールはRGBカラーを表して、非常に類似したアプローチ
Point
and Vector
:namespace Raytracer.Types
module rec Color =
type T =
{ Red : float
Green : float
Blue : float }
static member (+) (c1, c2) =
make (c1.Red + c2.Red) (c1.Green + c2.Green) (c1.Blue + c2.Blue)
static member (-) (c1, c2) =
make (c1.Red - c2.Red) (c1.Green - c2.Green) (c1.Blue - c2.Blue)
static member ( * ) (c, scalar) =
make (c.Red * scalar) (c.Green * scalar) (c.Blue * scalar)
static member ( * ) (c1, c2) =
make (c1.Red * c2.Red) (c1.Green * c2.Green) (c1.Blue * c2.Blue)
let make r g b =
{ Red = r
Green = g
Blue = b }
let black = make 0.0 0.0 0.0
let red = make 1.0 0.0 0.0
キャンバス
私たちはポイント、ベクトル、色を持っているので、私たちはほとんど物事を描く準備ができています.以下のモジュールは、1を表し、PPM イメージ.
namespace Raytracer.Types
open System
open System.Text.RegularExpressions
module Canvas =
type T =
{ Width : int
Height : int
Pixels : Color.T [,] }
let make width height =
{ Width = width
Height = height
Pixels = Array2D.create height width Color.black }
let writePixel canvas x y color = Array2D.set canvas.Pixels y x color
let pixelAt canvas x y = Array2D.get canvas.Pixels y x
let toPpm canvas =
let clamp f =
let rgbVal = 255.0 * f |> round
Math.Clamp(int rgbVal, 0, 255)
let colorToRgb (c : Color.T) =
sprintf "%d %d %d" (clamp c.Red) (clamp c.Green) (clamp c.Blue)
let splitLongLines (rgbs : seq<string>) =
let row = String.Join(" ", rgbs)
Regex.Replace(row, "[\s\S]{1,69}(?!\S)",
(fun m -> m.Value.TrimStart(' ') + "\n"))
.TrimEnd('\n')
let pixelsToString canvas =
canvas.Pixels
|> Array2D.map colorToRgb
|> Seq.cast<string>
|> Seq.chunkBySize canvas.Width
|> Seq.map splitLongLines
|> String.concat "\n"
let header =
sprintf "P3\n%d %d\n255" canvas.Width canvas.Height
let pixels = pixelsToString canvas
sprintf "%s\n%s\n" header pixels
ビルトインArray2D クラスは便利ですが、驚くほど、多くの高次の機能を欠いています Seq.cast
. 私はまだギザギザの配列かどうか考えていますstring[][]
) 多次元配列よりも好ましいstring[,]
) ここでは、現在のままにすることにしました.マトリックス
私はまだまだ私の行列の実装では完了していないが、これは現在のように基づいている
Array2D
それで、コードは場所において非常に不可欠ですnamespace Raytracer.Types
open Raytracer.Constants
open Raytracer.Types.Tuples
module Matrix =
type T =
{ Dimension : int
Entries : float [,] }
static member (.=) (m1, m2 : T) =
let allWithinEpsilon =
let len = m1.Dimension - 1
seq {
for r in 0 .. len do
for c in 0 .. len do
if abs (m1.[r, c] - m2.[r, c]) > epsilon then
yield false
}
|> Seq.forall id
m1.Dimension = m2.Dimension && allWithinEpsilon
static member ( * ) (m1, m2) =
let len = m1.Dimension - 1
let result = Array2D.zeroCreate m1.Dimension m1.Dimension
for r in 0 .. len do
for c in 0 .. len do
let row = m1.Entries.[r, *]
let col = m2.Entries.[*, c]
Array.fold2 (fun sum r c -> sum + r * c) 0.0 row col
|> Array2D.set result r c
{ Dimension = m1.Dimension
Entries = result }
static member ( * ) (m, v : Vector.T) : Vector.T =
let len = m.Dimension - 1
let result =
seq {
for r in 0 .. len ->
let row = m.Entries.[r, *]
let vArray = [| v.X; v.Y; v.Z; v.W |]
Array.fold2 (fun sum r c -> sum + r * c) 0.0 row vArray
}
|> Seq.toArray
Vector.make result.[0] result.[1] result.[2]
member x.Item
with get (r, c) = x.Entries.[r, c]
let make rows =
let dim = List.length rows
if dim >= 2 && dim <= 4
&& List.forall (fun l -> List.length l = dim) rows then
{ Dimension = dim
Entries = array2D rows }
else
failwith "Matrix must be square with dimension 2, 3 or 4"
let identity =
make
[ [ 1.; 0.; 0.; 0. ]
[ 0.; 1.; 0.; 0. ]
[ 0.; 0.; 1.; 0. ]
[ 0.; 0.; 0.; 1. ] ]
let transpose m =
[ for c in [ 0 .. m.Dimension - 1 ] do
yield m.Entries.[*, c] |> List.ofArray ]
|> make
私は簡単にSIMDMatrix4x4 タイプが、その本が必要な2 x 2と3 x 3のマトリックスに対応していません.もちろん、私はまた、いくつかの外部マトリックスライブラリを使用することができましたが、私はそれが自分ですべてを実装するより楽しく、より良い学習経験であると思いました.第一の像
最初の章では、開始位置と速度を与えられた発射体の飛行経路を計算するための少しのスクリプトを書いて、重力と風から成る環境を終えます.第2章の終わりに、このスクリプトは、キャンバスに軌道をプロットし、PPM画像として保存するために強化されます.
#load "../../Raytracer/Color.fs"
#load "../../Raytracer/Canvas.fs"
#load "../../Raytracer/Tuples.fs"
open System.IO
open Raytracer.Types
open Raytracer.Types.Tuples
type Projectile =
{ position : Point.T
velocity : Vector.T }
type Environment =
{ gravity : Vector.T
wind : Vector.T }
let tick env p =
{ position = p.position + p.velocity
velocity = p.velocity + env.gravity + env.wind }
// Projectile starts one unit above the origin.
// Velocity is normalized and multiplied to increase its magnitude.
let mutable projectile =
{ position = Point.make 0.0 1.0 0.0
velocity = (Vector.make 1.0 1.8 0.0 |> Vector.normalize) * 11.25 }
// gravity -0.1 unit/tick, and wind is -0.01 unit/tick.
let env =
{ gravity = Vector.make 0.0 -0.1 0.0
wind = Vector.make -0.01 0.0 0.0 }
let canvas = Canvas.make 900 550
let color = Color.make 1.0 0.7 0.7
while projectile.position.Y > 0.0 do
let canvasX =
projectile.position.X
|> round
|> int
let canvasY = canvas.Height - (int <| round projectile.position.Y)
if canvasX >= 0 && canvasX < canvas.Width && canvasY >= 0
&& canvasY < canvas.Height then
Canvas.writePixel canvas canvasX canvasY color
projectile <- tick env projectile
File.WriteAllText("img/ch02-projectile.ppm", Canvas.toPpm canvas)
結果を以下に示します.それは見た目にはあまり意味がないが、私のコードから生成されたイメージを見ることは非常に満足していた.
ラップです
これまでのところ、これは本当に楽しい挑戦と良い学習経験されています.ご質問やご提案があれば私に知らせてください、私はコードが様々な場所で改善される可能性があることを確認します.それはまた、ソリューションを比較するのも素晴らしいでしょうので、もしあなたがコードを参照してくださいしたいのです.
Reference
この問題について(レイトレーサーを書く), 我々は、より多くの情報をここで見つけました https://dev.to/citizen428/learning-f-writing-a-ray-tracer-36laテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol