球との交差

球との交差まで実装。
ようやくコーディングが軌道に乗ってきた。
haskellの難しさというか取っ付きの悪さはシンタックスの問題だと思った。
関数型がどうとかモナドが云々という段階に達する前に
括弧の要不要、','の要不要、type constructorとdata constructorの違いとか
が分かっていないと単純にエラーが出まくってコンパイル通らない。
しかも慣れていないのでエラーから有効な情報を読み取れない。
慣れるしか無いのだが, 結構苦しんだ。

-- ray tracing
main=writeP3 "tmp.ppm" width height (rendering width height)
  where
    width=256
    height=256

-- write raw data to file as PPM text format.
writeP3::String->Int->Int->[RGB24]->IO()
writeP3 filepath width height rawdata=
  writeFile filepath 
    ("P3\n"
    ++(show width)++" "++(show height)++"\n"
    ++"255\n"
    ++(unlines $ map show rawdata))

-- Vector
instance (Num t)=>Num ([t]) where
  (+) lhs rhs=[x+y|(x, y)<-(zip lhs rhs)]
  (-) lhs rhs=[x-y|(x, y)<-(zip lhs rhs)]
-- dot product(inner product)
dot::Num t=>[t]->[t]->t
dot lhs rhs=foldr (+) 0 [x*y|(x,y)<-zip lhs rhs]
-- sqareNorm(length * length)
sqareNorm::(Num t)=>[t]->t
sqareNorm vector=dot vector vector
-- norm(length)
norm::(Floating f)=>[f]->f
norm vector=sqrt $ sqareNorm vector

-- rendering
rendering::Int->Int->[RGB24]
rendering width height=[shading $ intersect sphere $ getRay (x, y) 
  |x<-[0..width-1], y<-[0..height-1]] -- each pixel
  -- scene
  where
    sphere=Sphere [128, 128, 0] 100

-- get color
shading::Maybe Sphere->RGB24
shading intersection=
  case (intersection) of
    Just sphere -> RGB24 0 0 255
    Nothing -> RGB24 255 255 255

-- get ray from pixel coordinate
getRay::(Int, Int)->Ray
getRay (x, y)=Ray origin direction directionInverse
  where
    origin=[(fromIntegral x), (fromIntegral y), 0]
    direction=[0, 0, -1]
    directionInverse=[0, 0, -1]

-- test intersection
intersect::Sphere->Ray->Maybe Sphere
intersect sphere@(Sphere center radius) (Ray origin direction directionInverse)
  | discriminant<0 = Nothing
  | otherwise = Just sphere
  where
    centerToOrigin=origin-center
    a=(dot direction direction)
    b=2*(dot direction centerToOrigin)
    c=(dot centerToOrigin centerToOrigin)-(radius*radius)
    discriminant=(b*b)-(4*a*c)

-- rendering
data Ray=Ray
  [Float] -- origin
  [Float] -- direction
  [Float] -- direction inverse each value
instance Show Ray where
  show (Ray origin direction directionInverse)=
    "origin: "++(show origin)++", direction: "++(show direction)++", inverse: "++(show directionInverse)

data Sphere=Sphere
  [Float] -- center
  Float -- radius

data RGB24=RGB24
  Int -- r
  Int -- g
  Int -- b
instance Show RGB24 where
  show (RGB24 r g b)=(show r)++" "++(show g)++" "++(show b)

しかし、さすがhaskell。特に行数を節約したわけでも(むしろ無駄遣いしている)
ないのに100行足らずで書けるのは表現力の高さあらわしているか。

Vectorをどうするかについては少し試行錯誤したのだが、
新しい型を導入するよりはlistに必用な演算を定義するだけの方が楽(パターンマッチで中身を取り出す必用が無い)
になりそうなのでそのようにした。
あとtype constructorとdata constructorの名前を変える(type constructorの頭にTをつける)という
命名規則を一瞬取り入れたがださいので止めた。
ただ、はじめは別名をつけて意識的に区別するのもよいかもしれない。
しかし、もう慣れた。多分。

没案↓

data Vector3=Vector3
  Float -- x
  Float -- y
  Float -- z

要素を取り出すのが面倒くさい。
ラベル付きフィールドは自動定義のセレクタ関数の重複のことを考えると使う気がしなくなった。

newtype (Num t)=>Vector t= Vector [t]

数値のリストをベクトルと見なすというなんとなく数学的な定義を直接表現できていてよいかと思ったのだが、
これを使うところそれぞれでいちいち(Num t)というclass型を書かないといけなくなって面倒なことに。
class typeはなんかc++のテンプレートに似ているかも。
genericにしようとして面倒になるところを含めて。

メモ

instance使用時の"Could not deduce..."エラーの解決で助けになった。
http://itpro.nikkeibp.co.jp/article/COLUMN/20071204/288630/