ベジェ曲線を等しいセグメントに分割する


時々ゲーム開発において、カーブした軌道に沿って均一なオブジェクトを均等に分配する必要があります.私は、これのためにどんなコード解決も見つけることができませんでした.

依存


このチュートリアルではgoプログラミング言語とPixel ゲームライブラリ.使いましょうBezier curves ここで記述されたアルゴリズムはどんな種類の曲がった軌道のためにも適用できるけれども、彼らはどんな軌道をつくって、それを手動で編集するのに十分柔軟であるので.ベジエ曲線を使用して作成することができますgonum パッケージ.インポートセクションです.
import (
    "fmt"
    "time"

    "github.com/faiface/pixel"
    "github.com/faiface/pixel/imdraw"
    "github.com/faiface/pixel/pixelgl"
    colors "golang.org/x/image/colornames"
    "gonum.org/v1/plot/plotter"
    "gonum.org/v1/plot/tools/bezier"
    "gonum.org/v1/plot/vg"
)
ご覧の通り、私も使用しますcolornames パッケージ定義済みの色.

曲線の作成


最初に新しい曲線を作成する必要があります.立方体のベジエ曲線を4つの制御点で使用します.
controlPoints := []vg.Point{
        {X: 0.45, Y: 0.328},
        {X: 1.403, Y: 0.12},
        {X: 0.62, Y: 1.255},
        {X: 1.521, Y: 0.593},
}
ご覧の通り、数字はかなり小さいです.心配しないでください、曲線が縮小され、画面上で良い見て翻訳されます.
また、後で必要とする定数もあります.
const (
    screenWidth              = 1280
    screenHeight             = 720
    offsetX          float64 = 400
    offsetY          float64 = 300
    scaleX           float64 = 300
    scaleY           float64 = 300
    numberOfSegments         = 10
    epsilon          float64 = 0.001
    dt               float64 = 0.5
)
コントロールポイントから新しい曲線を作成するには、次の手順に従います.
// Form the curve.
curve := bezier.New(controlPoints...)
今はカーブのポイントを計算する時間です.ベジエ曲線の単一点を得るには、パラメータt、0を使用する必要があります≤ t ≤ 1 .メソッド(c Curve) Point(t float64) vg.Point パラメータに対応する曲線のポイントを返します.たとえば、T = 0.5の場合、このメソッドは曲線の中点を返します.
それは多くのベジエ曲線ポイントをできるだけ取得する方が良いです.これを行うには、ステップを使用しますdt . より少ないステップ、あなたが得るより多くのポイント.
points := make(plotter.XYs, 0)

for t := 0.0; t < 100.0; t += dt {
    point := curve.Point(t / 100.0)

    points = append(points, plotter.XY{
        X: float64(point.X)*scaleX + offsetX,
        Y: float64(point.Y)*scaleY + offsetY})
}

曲線を描く


カーブのポイントを描くために、新しいウィンドウとIMDraw 対象:
cfg := pixelgl.WindowConfig{
    Title:  "Bezier curve",
    Bounds: pixel.R(0, 0, screenWidth, screenHeight),
}
win, err := pixelgl.NewWindow(cfg)
handleError(err)

imd := imdraw.New(nil)
また、FPSカウンタを設定したいです.
fps := 0
perSecond := time.Tick(time.Second)
今、私たちは、アプリケーションのメインループを入力し、曲線を各フレームを描画する準備が整いました
for !win.Closed() {
    win.Clear(colors.White)
    imd.Clear()

    // Draw the curve and other things.
    imd.Color = colors.Red

    for _, point := range points {
        imd.Push(gonumToPixel(point))
        imd.Circle(1, 1)
    }

    imd.Draw(win)

    win.Update()

    // Show FPS in the window title.
    fps++

    select {
    case <-perSecond:
        win.SetTitle(fmt.Sprintf("%s | FPS: %d", cfg.Title, fps))
        fps = 0

    default:
    }
}
ところで、上記のすべては、中に置かれなければなりませんrun() 関数main() :
func main() {
    pixelgl.Run(run)
}
メインGoroutineが別のスレッドに割り当てられないことを確認する必要があります.
では、私たちが得たものを見ましょう.

見ることができるように、曲線の任意の2つの隣接点間の距離は常に同じではありません.さらに、グラフは特に曲線の最初と最後の制御点に近いほど密になりません.
実は、それは私たちが見たいものではありません.我々は、カーブに沿って等しく散乱されるすべての点を必要とします.これに到達するためには,曲線を等分割するアルゴリズムを導入しなければならない.
では、新しい関数を定義しましょうgetSegmentPoints(points plotter.XYs, numberOfSegments int) []pixel.Vec :

  • 曲線点を線で接続します.
        // Create lines out of bezier
        // curve points.
        lines := []pixel.Line{}
    
        for i := 0; i < len(points)-1; i++ {
            line := pixel.L(gonumToPixel(points[i]),
                gonumToPixel(points[i+1]))
    
            lines = append(lines, line)
        }
    
    注意:関数gonumToPixel(xy plotter.XY) pixel.Vec
    Aを変換するgonum ベクトルにpixel ベクトル.

  • 行の合計長を計算します.
        // Compute the length
        // of the bezier curve
        // interpolated with lines.
        length := 0.0
    
        for _, line := range lines {
            length += line.Len()
        }
    

  • 単一のセグメントの長さであるステップを計算します.
        step := length / float64(numberOfSegments)
    

  • さて,多角形鎖をセグメント化するタスクに還元した.より多くの曲線点が得られると、より正確には、元の曲線のセグメント化になります.
    最初にいくつかの初期設定を行います.
        segmentPoints := []pixel.Vec{}
        lastLine := 0
        lastPoint := lines[0].A
        segmentPoints = append(segmentPoints, lastPoint)
    
    So lastPoint は最後に形成されたセグメントの最後のポイントである.lastLine は最後の点を含む行のインデックスです.ループへ
        for i := 0; i < numberOfSegments; i++ {
            subsegments := []pixel.Line{}
            startLine := pixel.L(lastPoint, lines[lastLine].B)
    
            subsegments = append(subsegments, startLine)
            localLength := startLine.Len()
    
            for step-localLength > epsilon {
                line := lines[lastLine+1]
                subsegments = append(subsegments, line)
    
                localLength += line.Len()
                lastLine++
            }
    
            line := lines[lastLine]
    
            if localLength > step {
                difference := localLength - step
                t := difference / line.Len()
    
                lastPoint = pixel.V(t*line.A.X+(1-t)*line.B.X,
                t*line.A.Y+(1-t)*line.B.Y)
            } else {
                lastPoint = line.B
                lastLine++
            }
    
            segmentPoints = append(segmentPoints, lastPoint)
        }
    
    このループでは、その長さがセグメントの長さを超えるまで、我々は線を拾い上げます.それが起こるとき、我々は線形補間を使っている最後の線に位置するセグメンテーションポイントを計算します.行の最後と一致する場合は、最後の行カウンタをインクリメントし、次の行で新しい反復を開始します.
  • では、カーブを10等分していきましょう.

    まあ、それはずっと良いです.お読みありがとうございます.ここではsource code .