베지어 곡선을 동일한 세그먼트로 나누기

21329 단어 gamedevgotutorial
때때로 게임 개발에서 곡선 궤적을 따라 균일한 개체를 균등하게 배포해야 할 필요가 있습니다. 이에 대한 코드 솔루션을 찾을 수 없으므로 여기에 내 코드가 있습니다.

종속성



이 자습서에서는 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개의 제어점이 있는 3차 베지어 곡선을 사용합니다.

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)
}


메인 고루틴이 다른 스레드에 할당되지 않도록 하는 데 필요합니다.

이제 우리가 얻은 것을 보자:



보시다시피 곡선의 인접한 두 점 사이의 거리가 항상 같지는 않습니다. 또한 그래프는 곡선의 첫 번째 및 마지막 제어점에 가까워질수록 밀도가 특히 낮아집니다.

사실, 그것은 우리가 보고 싶은 것이 아닙니다. 모든 점이 곡선을 따라 균등하게 분산되어야 합니다. 이에 도달하려면 곡선을 동일한 세그먼트로 나누는 알고리즘을 도입해야 합니다.

이제 새 함수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.Vecgonum 벡터를 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)
    

    따라서 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 입니다.

    좋은 웹페이지 즐겨찾기