F#でWPF --- CatmullRom曲線の描画

図形を描画する際に、指定した点を通る滑らかな曲線を描きたいことがあります。

f:id:any-programming:20170312220605p:plain

今回はCatmullRom曲線でこれを実現したいと思います。

CatmullRom曲線

2点間の補間は、更にその前後の点を利用して求められます。

2点(p1、p2)と、その前後の点(p0、p3)の4点から、下記の式にて任意の位置が求められます。

f:id:any-programming:20170312221749p:plain

Vector2定義

System.Windows.Vectorでは演算子が不十分であるため、独自のVector2を下記の様に定義しました。

type Vector2 =
    { X:double; Y:double }
    static member (~-) (v:Vector2)            = { X = -v.X;      Y = -v.Y }
    static member (+)  (a:Vector2, b:Vector2) = { X = a.X + b.X; Y = a.Y + b.Y }
    static member (-)  (a:Vector2, b:Vector2) = { X = a.X - b.X; Y = a.Y - b.Y }
    static member (*)  (s:double,  v:Vector2) = { X = s * v.X;   Y = s * v.Y }
    static member (*)  (v:Vector2, s:double)  = s * v
    static member (/)  (v:Vector2, s:double)  = { X = v.X / s;   Y = v.Y / s }


CatmullRom定義

Vector2 listを持つレコードと、interpolate関数を定義しています。

type CatmullRom = { Points : Vector2 list }

[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module CatmullRom =
    let interpolate (p0 : Vector2) (p1 : Vector2) (p2 : Vector2) (p3 : Vector2) t =
        let v0 = (p2 - p0) / 2.0
        let v1 = (p3 - p1) / 2.0
        (2.0 * p1 - 2.0 * p2 + v0 + v1) * t * t * t + (-3.0 * p1 + 3.0 * p2 - 2.0 * v0 - v1) * t * t + v0 * t + p1


Polylineで表示

CatmullRom曲線をPolylineで表示してみます。

4つの点の間を100分割して表示しています。(始点、終点は複製をしてあるため、入力は6点となっています。)

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="180" Width="160">
    <Canvas Margin="20">
        <Polyline Stroke="Red" StrokeThickness="10" Points="{Binding Points}" />
    </Canvas>
</Window>
open System
open System.Windows
open System.Windows.Media

type Model = { Points : PointCollection }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window

    let interpolate p0 p1 p2 p3 count =
        [ 0..count - 1 ]
        |> List.map (fun x -> double x / double count)
        |> List.map (CatmullRom.interpolate p0 p1 p2 p3)

    let points = 
        [ { X = 0.0;   Y = 0.0 }
          { X = 0.0;   Y = 0.0 }
          { X = 100.0; Y = 0.0 }
          { X = 0.0;   Y = 100.0 }
          { X = 100.0; Y = 100.0 }
          { X = 100.0; Y = 100.0 } ]
        |> List.windowed 4
        |> List.collect (fun x -> interpolate x.[0] x.[1] x.[2] x.[3] 100)

    let toPoint x = Point(x.X, x.Y)

    window.DataContext <- { Points = points |> List.map toPoint |> PointCollection }
    Application().Run(window) |> ignore    
    0

f:id:any-programming:20170312224702p:plain

ベジエ曲線で描画

上記でのPolyline利用では、ユーザーが補間したものを用意する必要があります。

そこでCatmullRom曲線をベジエ曲線に変換して描いてみます。

ベジエ曲線

2点間の補間は、その他2つのコントロールポイントを利用して求められます。

2点(p1、p2)と、コントロールポイント(c1、c2)の4点から、下記の式にて任意の位置が求められます。

f:id:any-programming:20170312222948p:plain

CatmullRom→ベジエ

CatmullRomの4点(p0、p1、p2、p3)が与えられたとき、ベジエ曲線のコントロールポイント(c1、c2)は下記の式で与えられます。
f:id:any-programming:20170312225322p:plain

Pathで描画

スタート位置をPathFigureで指定し、残りをPolyBezierSegmentのPointsにバインドすることによりベジエ曲線を描いています。

Pointsの中身は(p1、c1_1、c2_2、p2、c2_1、c2_2、p3...)のように、通過点の間にコントロールポイントが挟まっているものになります。

Polylineで描画したものと同じ図形であることが確認できます。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="180" Width="160">
    <Canvas Margin="20">
        <Path Stroke="Red" StrokeThickness="10">
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Figures>
                        <PathFigure StartPoint="{Binding BezierStartPoint}">
                            <PolyBezierSegment Points="{Binding BezierPoints}" />
                        </PathFigure>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>
    </Canvas>
</Window>
module CatmullRom =
    let bezierControlPoints (p0 : Vector2) (p1 : Vector2) (p2 : Vector2) (p3 : Vector2) =
        let c1 = p1 + (p2 - p0) / 6.0
        let c2 = p2 + (p1 - p3) / 6.0
        c1, c2

type Model = 
    { BezierStartPoint : Point
      BezierPoints : PointCollection }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window

    let points = 
        [ { X = 0.0;   Y = 0.0 }
          { X = 0.0;   Y = 0.0 }
          { X = 100.0; Y = 0.0 }
          { X = 0.0;   Y = 100.0 }
          { X = 100.0; Y = 100.0 }
          { X = 100.0; Y = 100.0 } ]
        |> List.windowed 4
        |> List.collect (fun x -> 
            let c1, c2 = CatmullRom.bezierControlPoints x.[0] x.[1] x.[2] x.[3]
            [ c1; c2; x.[2] ])

    let toPoint x = Point(x.X, x.Y)

    window.DataContext <- { BezierStartPoint = Point(0.0, 0.0)
                            BezierPoints = points |> List.map toPoint |> PointCollection }

    Application().Run(window) |> ignore    
    0

f:id:any-programming:20170312224702p:plain