Elmでゆる系レイトレーシング

Haskell風言語であり、コンパイルするとJSを出してくれるElmで簡単なレイトレーシングをしてみました。
0.17でだいぶ変わってしまいましたが、使いやすくなったかなーと思います。

# 実行方法

Elm自体のインストール方法は省きます。npmなりなんなりで入れてあげてください。

以下、コンソール。

```bash
$ mkdir ray && cd ray

$ vim Main.elm

$ elm-package install elm-lang/html -y

$ elm-package install evancz/elm-graphics -y

$ cat elm-package.json
{
    "version": "1.0.0",
    "summary": "helpful summary of your project, less than 80 characters",
    "repository": "https://github.com/user/project.git",
    "license": "BSD3",
    "source-directories": [
        "."
    ],
    "exposed-modules": [],
    "dependencies": {
        "elm-lang/core": "4.0.5 <= v < 5.0.0",
        "elm-lang/html": "1.1.0 <= v < 2.0.0",
        "evancz/elm-graphics": "1.0.0 <= v < 2.0.0"
    },
    "elm-version": "0.17.0 <= v < 0.18.0"
}

$ elm reactor
elm reactor 0.17.0
Listening on http://localhost:8000/

```
（Main.elmは下のコードをコピペしてください。）

elm reactorのあと、ブラウザで http://localhost:8000/Main.elm にアクセスすると、
なんか球っぽいのが表示されると思います。

# コード

```Elm
module Main exposing (main)

import Html.App as App
import Html exposing (div, Html)
import Color

import Element as E
import Collage as C


type alias Vec = { x: Float , y: Float , z: Float }
type alias RGB = { r: Float , g: Float , b: Float }

type alias Sphere = { pos: Vec , rad: Float , color: RGB }
type alias Light = { pos: Vec , color: RGB}
type alias Ray = { dir: Vec , pos: Vec }
type alias Model = { width: Float , height: Float , sphere: Sphere , lights: List Light }

type Msg = Init


init : {width: Float, height: Float} -> (Model, Cmd Msg)
init {width, height} = 
  let 
    sphere = { pos = initV, rad = 1.0, color = white }
    light1 = { pos = {x = 3.0, y = -2.0, z = -2.0}, color = blue }
    light2 = { pos = {x = -3.0, y = 2.0, z = -2.0}, color = red }
    light3 = { pos = {x = 3.0, y = 2.0, z = -2.0}, color = green }
    model =  { width = width , height = height , sphere = sphere , lights = [ light1, light2, light3 ] }
  in (model, Cmd.none)

update : Msg -> Model -> (Model, Cmd a)
update msg model = (model, Cmd.none)

-- 1pxの線で代用
point : Int -> Int -> RGB -> C.Form
point x y c = 
  let mkColor = Color.rgb (round<|c.r*255) (round<|c.g*255) (round<|c.b*255)
  in (C.solid mkColor |> C.traced) 
    <| C.path [(toFloat x, toFloat y), (toFloat <|x+1, toFloat y)] 

view : Model -> Html.Html a
view model = 
  let 
    toInt = round
    ih = toInt model.height
    iw = toInt model.width
  in E.toHtml 
    <| C.collage ((*) 2 <| iw) ((*) 2 <| ih) 
    <| List.concat 
    <| List.map2 (\ y row -> List.map2  (\ x c -> point x y c) [0..iw] row) [0..ih]
    <| scene model 

subscriptions: Model -> Sub a
subscriptions model = Sub.none

main : Program Never
main =  
  App.program { 
      init = (init {width = 800, height = 600})
    , update = update
    , view = view
    , subscriptions = subscriptions
    }

scene : Model -> List (List RGB)
scene model = 
  let 
    ray x y = { 
        dir = normalize { x = x, y = y, z = 5.0 }
      , pos = { x = 0.0, y = 0.0, z = -5.0 }
      }
    way r = (flip (-) <| r / 2) >> flip (/) 100
    listThen = flip List.map
  in 
    [0..model.height-1] `listThen` \y ->
    [0..model.width-1] `listThen` \x -> 
      render model.sphere model.lights <| ray (way model.width x) (way model.height y)

intersect : Sphere -> Ray -> Maybe Float
intersect sph ray = 
  let 
    m = ray.pos `subV` sph.pos
    b = m `dot` ray.dir
    c = b ^ 2 - magnitude2 m + sph.rad ^ 2
    t = -b - sqrt c
  in if c < 0 then Nothing else if t > 0 then Just t else Nothing

render : Sphere -> List Light -> Ray -> RGB
render sphere lights ray = 
  case intersect sphere ray of 
    Just dist -> 
      let 
        diffuse light acc = trace dist sphere light ray |> flip (updateCC (+)) acc
        result = List.foldr diffuse black lights
      in updateC min result 1.0
    Nothing -> black

trace : Float -> Sphere -> Light -> Ray -> RGB
trace dist sphere light ray = 
  let
    r = addV ray.pos <| updateV (*) ray.dir dist
    n = updateV (/) (r `subV` sphere.pos) sphere.rad
    l = light.pos `subV` r |> normalize 
    cos = n `dot` l |> max 0 
  in updateCC (*) sphere.color light.color |> flip (updateC (*)) cos

-- Vec
addV : Vec -> Vec -> Vec
addV = updateVV (+)

subV : Vec -> Vec -> Vec
subV = updateVV (-)

dot : Vec -> Vec -> Float
dot v1 v2 = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z

updateV : (Float-> Float -> Float) -> Vec -> Float -> Vec
updateV f2 vec v = { x = f2 vec.x v , y = f2 vec.y v , z = f2 vec.z v }

updateVV : (Float-> Float -> Float) -> Vec -> Vec -> Vec
updateVV f2 vec1 vec2 = 
  { x = f2 vec1.x vec2.x , y = f2 vec1.y vec2.y , z = f2 vec1.z vec2.z }

initV : Vec
initV = { x = 0.0, y = 0.0, z = 0.0 }

magnitude : Vec -> Float
magnitude {x, y, z}  = sqrt <| x^2 + y^2 + z^2

magnitude2 : Vec -> Float
magnitude2 {x, y, z}  = x^2 + y^2 + z^2

normalize : Vec -> Vec
normalize vec =
  let m = magnitude vec in updateV (/) vec <| if m == 0 then 1 else m

-- Color
updateC : (Float-> Float -> Float) -> RGB -> Float -> RGB
updateC f2 a v = 
  { r = f2 a.r v , g = f2 a.g v , b = f2 a.b v }

updateCC : (Float -> Float -> Float) -> RGB -> RGB -> RGB
updateCC f2 a b = 
  { r = f2 a.r b.r , g = f2 a.g b.g , b = f2 a.b b.b }

addC = updateCC (+)

black = { r = 0.0, g = 0.0, b = 0.0 }
white = { r = 1.0, g = 1.0, b = 1.0 }
red = { r = 1.0, g = 0.0, b = 0.0 }
green = { r = 0.0, g = 1.0, b = 0.0 }
blue = { r = 0.0, g = 0.0, b = 1.0 }
```

# まとめ

コンパイラに怒られましょう。