レイトレーサーを書く


少し前にFということになる.1つの言語を学ぶ必要があるだけでなく、その全体の巨大な生態系があるので、ネット体験は少し困難です.だから大きなプロジェクトでいくつかの経験を得るために自分自身をコピージェイムズバックの優れた本を得たThe Ray Tracer Challenge そしてゆっくりと私の道を通っています.祝日のために、私はまだ期待していたのと全く同じではありませんでしたGitLab repository . ここで私が学んだ/これまで実装.

プロジェクトレイアウト


これは、おそらく味付けのために明らかであるものの一つです.ネット開発者は、プロジェクトとソリューションの違いを把握するのに時間がかかりました.私は、主なコードのための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)
これはすべてかなり標準ですが、いくつかのことが指摘する価値があります.
  • The 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)
    
    結果を以下に示します.

    それは見た目にはあまり意味がないが、私のコードから生成されたイメージを見ることは非常に満足していた.

    ラップです


    これまでのところ、これは本当に楽しい挑戦と良い学習経験されています.ご質問やご提案があれば私に知らせてください、私はコードが様々な場所で改善される可能性があることを確認します.それはまた、ソリューションを比較するのも素晴らしいでしょうので、もしあなたがコードを参照してくださいしたいのです.