베 지어를 따라 두 행성 사이에서 배를 움직이면 가속에 대한 방정식이 누락됩니다.


48

좋아, 나는 이미 이것을 math.stackechange.com에 게시했지만 아무런 대답도 얻지 못했습니다 :(

먼저 여기 내 문제에 대한 그림이 있습니다.

대체 텍스트

그래서 모든 포인트와 값을 설정했습니다.

배는 왼쪽 행성 주위를 이동 밖으로 시작 P1과 함께 S=0.27 Degrees이 도달하면, gametick 당 Point A그것이 도달 할 때까지 베 지어 곡선을 다음으로 시작 Point D, 다음 오른쪽 행성 주위 여행 P2S=0.42 Degrees게임 틱 당. 차이점 S은 행성 주위에서 동일한 이동 속도로 이동하는 것입니다.

지금까지는 좋았어요. 이제 문제가 생겼습니다.

목적지 S P1S P2다를 때 배가 두 속도 사이를 뛰어 넘는 것은 꽤 나빠 보입니다. 그래서 사이에 배를 가속화 할 필요가 Point APoint D에서 S P1S P2.

내가 누락 된 것은 자주색입니다.

  • 진드기를 계산하는 방법은 선박이 가속을 고려하여 베 지어를 따라 이동하는 데 소요됩니다.

  • 그리고 가속도를 다시 고려하여 T를 기준으로 베 지어 곡선에서 위치를 찾는 방법입니다.

ATM N점 사이 의 거리를 계산하여 베 지어의 길이를 계산합니다 . 그래서 내가 무엇을 생각하는 내가 필요로의 확장하는 방법입니다 T내가 가속에 따라 내 베 지어 계산에 넣어야한다.


2
그것을 알아내는 데 좋은 일. 찾은 결과를 질문에 대한 답변으로 게시하는 것이 좋습니다.
bummzack

답변:


83

좋아, 나는 모든 것이 작동하고 있고, 영원히 걸렸으므로 여기에 자세한 솔루션을 게시 할 것입니다.
참고 : 모든 코드 샘플은 JavaScript로되어 있습니다.

따라서 문제를 기본 부분으로 나누겠습니다.

  1. 0..1베 지어 곡선 의 길이와 점 사이를 계산해야합니다.

  2. 이제 T배를 한 속도에서 다른 속도로 가속 하기 위해 스케일링을 조정해야합니다

베 지어 권리 얻기

베 지어 곡선을 그리는 코드를 찾는 것은 쉽지만, 여러 가지 접근 방식이 있지만 그중 하나는 DeCasteljau Algorithm 이지만 입방 베 지어 곡선에 대한 방정식 을 사용할 수도 있습니다 .

// Part of a class, a, b, c, d are the four control points of the curve
x: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
           + 3 * ((1 - t) * (1 - t)) * t * this.b.x
           + 3 * (1 - t) * (t * t) * this.c.x
           + (t * t * t) * this.d.x;
},

y: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
           + 3 * ((1 - t) * (1 - t)) * t * this.b.y
           + 3 * (1 - t) * (t * t) * this.c.y
           + (t * t * t) * this.d.y;
}

이와 함께, 하나는 지금 호출하여 베 지어 곡선을 그릴 수 xy함께 t범위하는 0 to 1, 살펴 보겠습니다 :

대체 텍스트

어 .. 그건 사실 균등 한 분포가 아니에요?
베 지어 곡선의 특성으로 인해 점의 점이 0...1서로 다르 arc lenghts므로 시작과 끝 근처의 선분이 곡선의 중간에있는 선분보다 길다.

곡선 AKA 호 길이 매개 변수화에서 T를 균등하게 매핑

그래서 뭐 할까? 그럼 간단한 측면에서 우리는 우리를 매핑하는 기능이 필요 Tt우리 있도록 곡선의 T 0.25결과 t그에서의 25%곡선의 길이.

우리는 어떻게합니까? 글쎄, 우리는 구글 ...하지만 그 용어는 googleable아니며 , 어느 시점 에서이 PDF 를 보게 될 것 입니다. 어느 쪽이 좋은지 잘 알지만 학교에서 배운 모든 수학 자료를 이미 잊어 버린 경우 (또는 수학 기호가 마음에 들지 않는 경우) 그것은 쓸모가 없습니다.

지금 무엇? 잘 가서 구글에 대해 더 읽어보십시오 (6 시간 읽기). 마침내 주제에 관한 훌륭한 기사 (멋진 그림 포함! ^ _ ^ ") :
http://www.planetclegg.com/projects/WarpingTextToSplines.html

실제 코드 수행

오래 전에 오래 전에 수학 지식을 잃어 버렸지 (그리고 훌륭한 기사 링크 를 건너 뛰어도) PDF를 다운로드하는 것을 막을 수없는 경우 , 이제 다음과 같이 생각할 수 있습니다. 수백 줄의 코드와 수많은 CPU "

아닙니다. 우리는 모든 프로그래머가하는 일을하기 때문에 수학에 관해서
는 간단히 속입니다.

게으른 방식으로 아크 길이 매개 변수화

우리는 게임에서 끝없는 정밀도가 필요하지 않습니까? 따라서 Nasa에서 일하고 화성을 사람들에게 보낼 계획이 아니라면 0.000001 pixel완벽한 솔루션이 필요하지 않습니다 .

그래서 우리는 어떻게지도 할 Tt? 간단하고 3 단계로만 구성됩니다.

  1. N사용하여 곡선의 점을 계산 하고 해당 위치 tarc-length(일명 곡선의 길이)를 배열에 저장

  2. 매핑하려면 Tt처음 곱셈, T얻을 수있는 곡선의 총 길이가 u다음보다 작의 가장 큰 값의 인덱스에 대한 길이의 배열을 검색u

  3. 우리가 정확하게 맞았다면, 그 인덱스의 배열 값을로 나눈 값을 반환하고 N, 찾은 점과 다음 점 사이의 비트를 보간하지 않으면 물건을 다시 한 번 나누고 N반환하십시오.

그게 다야! 이제 전체 코드를 살펴 보겠습니다.

function Bezier(a, b, c, d) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;

    this.len = 100;
    this.arcLengths = new Array(this.len + 1);
    this.arcLengths[0] = 0;

    var ox = this.x(0), oy = this.y(0), clen = 0;
    for(var i = 1; i <= this.len; i += 1) {
        var x = this.x(i * 0.05), y = this.y(i * 0.05);
        var dx = ox - x, dy = oy - y;        
        clen += Math.sqrt(dx * dx + dy * dy);
        this.arcLengths[i] = clen;
        ox = x, oy = y;
    }
    this.length = clen;    
}

이것은 우리의 새로운 곡선을 초기화하고를 계산하며 arg-lenghts, 또한 마지막 길이를 total length곡선 의 길이로 저장합니다 . 여기서 중요한 요소 this.len는 우리의 N입니다. 위의 그림에서 크기의 곡선 100 points이 충분하기 때문에 매핑이 높을수록 더 정확할 것입니다. 길이가 길면 25이미 1 픽셀 만 사용하여 작업을 수행 할 것입니다. 예,하지만 당신은 아니 그렇게 고른 분포가 발생합니다 덜 정확한 매핑해야합니다 T매핑 할 때를 t.

Bezier.prototype = {
    map: function(u) {
        var targetLength = u * this.arcLengths[this.len];
        var low = 0, high = this.len, index = 0;
        while (low < high) {
            index = low + (((high - low) / 2) | 0);
            if (this.arcLengths[index] < targetLength) {
                low = index + 1;

            } else {
                high = index;
            }
        }
        if (this.arcLengths[index] > targetLength) {
            index--;
        }

        var lengthBefore = this.arcLengths[index];
        if (lengthBefore === targetLength) {
            return index / this.len;

        } else {
            return (index + (targetLength - lengthBefore) / (this.arcLengths[index + 1] - lengthBefore)) / this.len;
        }
    },

    mx: function (u) {
        return this.x(this.map(u));
    },

    my: function (u) {
        return this.y(this.map(u));
    },

실제 매핑 코드는 먼저 binary search저장된 길이에 대해 간단한 것보다 작은 최대 길이를 찾은 targetLength다음 보간 또는 보간 및 반환을 수행합니다.

    x: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
               + 3 * ((1 - t) * (1 - t)) * t * this.b.x
               + 3 * (1 - t) * (t * t) * this.c.x
               + (t * t * t) * this.d.x;
    },

    y: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
               + 3 * ((1 - t) * (1 - t)) * t * this.b.y
               + 3 * (1 - t) * (t * t) * this.c.y
               + (t * t * t) * this.d.y;
    }
};

다시 t커브에서 계산 합니다.

결과 시간

대체 텍스트

지금 사용하여 mx그리고 my당신이 균등하게 분배받을 T곡선에 :)

그렇게 힘들지 않습니까? 다시 한 번, 단순한 (완벽한 해결책은 아니지만) 게임으로 충분하다는 것이 밝혀졌습니다.

완전한 코드를보고 싶다면 Gist를 사용할 수 있습니다 :
https://gist.github.com/670236

마지막으로 배를 가속

이제 남은 것은 곡선 T을 찾기 위해 사용 하는 위치를 매핑하여 경로를 따라 선박을 가속하는 t것입니다.

먼저 우리는 두 가지가 필요 운동 방정식 , 즉 ut + 1/2at²(v - u) / t

실제 코드에서는 다음과 같습니다.

startSpeed = getStartingSpeedInPixels() // Note: pixels
endSpeed = getFinalSpeedInPixels() // Note: pixels
acceleration = (endSpeed - startSpeed) // since we scale to 0...1 we can leave out the division by 1 here
position = 0.5 * acceleration * t * t + startSpeed * t;

그런 다음 다음 0...1을 수행 하여 축소합니다 .

maxPosition = 0.5 * acceleration + startSpeed;
newT = 1 / maxPosition * position;

그리고 거기서 당신은 배들이 길을 따라 부드럽게 움직이고 있습니다.

작동하지 않는 경우 ...

이 글을 읽을 때 모든 것이 잘 작동하고 멋지지만 처음에는 가속 부분에 문제가 있었는데 gamedev 대화방의 누군가에게 문제를 설명 할 때 내 생각에 최종 오류가 있음을 발견했습니다.

원래 질문의 그림에 대해 이미 잊어 버린 s경우, s속도가 도 단위로 밝혀 지지만 배는 경로를 따라 픽셀 로 이동 하고 그 사실을 잊어 버렸습니다. 그래서이 경우에해야 할 일은 픽셀 단위의 변위를 픽셀 단위의 변위로 변환하는 것이 었습니다.

function rotationToMovement(planetSize, rotationSpeed) {
    var r = shipAngle * Math.PI / 180;
    var rr = (shipAngle + rotationSpeed) * Math.PI / 180;
    var orbit = planetSize + shipOrbit;
    var dx = Math.cos(r) * orbit - Math.cos(rr) * orbit;
    var dy = Math.sin(r) * orbit - Math.sin(rr) * orbit;
    return Math.sqrt(dx * dx + dy * dy);
};

그리고 그게 다야! 읽어 주셔서 감사합니다 ;)


7
이 과정을 소화하는 데 시간이 걸립니다. 그러나 와우, 당신의 자신의 질문에 대한 놀라운 대답.
AttackingHobo

7
이 답변을 찬성하기 위해 계정을 만들었습니다.
아무도

내 친구 좀 알려줘 매력처럼 일했다. 질문과 답변 모두 공감.
Jace

2
'i'에 0.05을 곱한 반면 'len'은 100으로 설정되었습니다. '0-1'대신 '0-5'에 매핑되지 않습니다.
사악한 활동

1
@EvilActivity 그래, 나도 원래 길이가 20 이었음을 알았고 0.05에서 0.01로 변경하는 것을 잊었다. 따라서 동적 'len'(실제 아크 길이에 해당하거나 어쩌면 정확히 동일한 길이)을 사용하는 것이 더 좋으며 "단계"를 1 / 'len'로 계산하십시오. 나는 이것이 너무나도 이상하다는 것을 안다.
Bill Kotsias

4

문제는 배가 그 궤도를 자연스럽게 받아들이지 않을 것이라는 점입니다. 따라서 완벽하게 작동하더라도 여전히 올바르게 보이지 않습니다.

행성 사이의 부드러운 전환을 시뮬레이션하려면 실제로 모델링하는 것이 좋습니다. 중력과 추력의 두 가지 중요한 힘만 가지기 때문에 방정식은 매우 간단합니다.

상수를 설정하면됩니다 : P1, P2, 선박 질량

각 게임 틱 (시간 : t)마다 3 가지 일을하고 있습니다

  1. 선박의 p1과 선박의 p2의 중력을 계산하고 결과 벡터를 추력 벡터에 추가하십시오.

  2. 1 단계의 새로운 가속도에 따라 새로운 속도를 계산하십시오.

  3. 새로운 속도에 따라 배를 움직여

많은 작업처럼 보이지만 수십 줄의 코드로 수행 될 수 있으며 매우 자연스럽게 보일 것입니다.

물리학에 도움이 필요하면 알려주십시오.


당신이받는 함수 내에서이 작업을 수행하는 방법 제공 할 수 있는지 그 테스트를 고려해 볼 수 있습니다 t:
이보 Wetzel는

그러나 게임 프로그래밍에서는 t를 변수로 사용하지 않습니다. 당신은 단순히 선박에 대한 새로운 dx와 dy를 계산하기 때문에 이미 기본적으로 파라 메트릭 상황에 있습니다. 다음은 플래닛 에서 두 행성을 공전하는 예입니다 . aharrisbooks.net/flash/fg2r12/twoPlanets.html-
Two pi

2

자바 스크립트로 작성된 코드 예제 로이 문제에 대한 가능한 해결책을 설명 하는 훌륭한 기사를 찾았습니다 . "t"값을 올바른 방향으로 조정하여 작동합니다.

대신, 우리는 점 분포에 대한 평균 레그 길이 d_avg가 균일 한 간격의 도트가 생성하는 레그 길이와 거의 동일하다는 사실을 사용할 수 있습니다 (이 유사성은 n이 증가함에 따라 증가합니다). 실제 레그 길이 (d)와 평균 레그 길이 (d_avg) 사이의 차이 (d_err)를 계산하면,이 차이를 감소시키기 위해 각 포인트에 대응하는 시간 파라미터 (t)가 넛지 될 수있다.

이 질문에는 이미 멋진 답변이 많이 있지만이 솔루션은 주목할 가치가 있음을 알았습니다.


1

이 문제를 해결 한 방법을 설명하는 훌륭한 페이지에 감사드립니다. 메모리가 많이 제한되어 있기 때문에 나는 당신과 다소 다른 것을 한층 상세하게 수행했습니다. 배열을 만들지 않거나 이진 검색으로 올바른 '세그먼트'를 검색해야합니다. 베 지어 곡선의 한쪽 끝에서 다른 쪽 끝으로 이동하고 있다는 것을 항상 알고 있기 때문입니다. 따라서 단순히 '현재'세그먼트를 기억하고 해당 세그먼트의 경계를 벗어나서 다음 세그먼트를 계산할 것입니다 위치, 여행 방향에 따라 다음 또는 이전 세그먼트를 계산합니다. 이것은 내 응용 프로그램에 아주 잘 작동합니다. 내가 해결해야 할 유일한 결함은 일부 곡선에서 세그먼트의 크기가 너무 작아서 다음에 할 플롯이 드물게 현재 세그먼트보다 하나 이상의 세그먼트보다 많기 때문에 그냥가는 대신 로 '

이것이 의미가 있는지 모르겠지만 이것은 분명히 나에게 도움이되었습니다.


0

이런 종류의 모델링은 이상하며 이상한 비논리적 인 결과를 낳을 수 있습니다. 특히 시작 행성 속도가 정말 느린 경우.

추력으로 선박을 모델링하십시오.

배가 시작 행성에서 마지막 궤도에 올 때, 완전히 추력으로 가속하십시오.

배가 일정 거리 내에 도달하면 역 추력을 사용하여 배를 목표 행성의 궤도 속도로 늦 춥니 다.

편집 : 노드가 궤도를 떠나려고 할 때 전체 시뮬레이션을 한 번에 수행하십시오. 모든 데이터를 보내거나 간격을두고 몇 개의 이동 벡터를 보내서 보간합니다.


문제는 이것이 진드기 기반이며 중간 위치가 없다는 것입니다. 네트워킹 멀티 플레이어 게임이며 정식 게임에서 600 개 이상의 배를 모두 보내면 모든 네트워킹이 중단됩니다. tickOffset을 전송하는 이벤트 만 있고 나머지는 현재 월드 틱과 오프셋을 기반으로 계산됩니다.
Ivo Wetzel

응답을 편집했습니다.
AttackingHobo

0

올바르게 이해하면 문제가 지나치게 제한됩니다.

나는 당신이 우주선이 어떤 시간 t 에서 궤도 사이의 지정된 경로를 따라 이동하기를 원하며 , 또한 같은 시간 t 에서 속도 s1 에서 속도 s2 로 가속 하기를 원한다고 믿습니다 . 불행히도 (일반적으로) 두 제약 조건을 동시에 만족시키는 가속을 찾을 수 없습니다.

문제를 해결할 수 있도록 약간의 문제를 완화해야합니다.


2
그렇다면 휴식을 취하는 방법? 내가 상상할 수있는 것은 베 지어 경로에 연결하는 T를 수정하는 것입니다. 어떻게 든 먼저 0.5에서 느리게 성장한 다음 1로 빠르게 성장하도록 스케일을 조정해야합니다. 따라서 우주선은 원래 속도에서 곡선 중간의 고정 속도로 감속 한 다음이 속도에서 마지막 속도로 다시 가속됩니다. 곡선의?
Ivo Wetzel

1
우주선이 원래 속도에서 전송 중간 지점까지 가속 한 다음 새로운 궤도로 감속하면 더 현실적으로 보일 것입니다.
Gareth Rees

나는 아직도이 모든 일에 가속도를 연결하는 방법에 붙어있어, 나는 어떻게 든 T를 수정해야합니다 /
이보 Wetzel는

0

베 지어 곡선을 사용하는 svg 경로를 따라 점을 균등하게 분배하려고하기 때문에이 답변을 보았습니다.

MDN은 더 이상 사용되지 않는다고 말했지만을 사용 path.getPointAtLength하여 올바른 결과를 얻을 수 있습니다. https://developer.mozilla.org/en-US/docs/Web/API/SVGPathElement/getPointAtLength

현재 Chrome / Safari / Firefox에서 작동하며 IE / Edge에서도 작동하지만 2를 확인하지 않았습니다.


-1

받아 들여진 해결책의 문제

베 지어 지수 함수로서, 곡선의 다른 영역에서 다른 진행률을 기대합니다.

Ivo의 솔루션 은 이러한 초기 지수 샘플 간에 선형 보간 되기 때문에 , 이러한 델타가 가장 큰 (일반적으로 입방) 곡선의 끝 / 중간으로 부정확성이 크게 바이어스됩니다. 따라서 그가 제안한대로 샘플 속도 가 크게 증가 하지 않는 한 , 오차는 명백하며, 어떤 줌 수준에서는 항상 주어진 경우에 대해 분명합니다. 즉, 바이어스는 해당 알고리즘에 내재적입니다. 줌이 무제한 일 수있는 벡터 기반 그래픽에는 적합하지 않습니다.NN

유도 샘플링을 통한 카운터 링 바이어스

대안적인 해결책은 선형 매핑하는 distancet베 지어 함수는 생산되는 천연 바이어스에 대항 이후.

이것이 우리가 이상적으로 원하는 것이라고 가정합니다.

curve length = 10

t      distance
0.2    2
0.4    4
0.6    6
0.8    8
1.0    10

그러나 이것은 베 지어 위치 함수에서 얻는 것입니다.

t      distance
0.2    0.12
0.4    1.22
0.6    2.45
0.8    5.81
1.0    10.00

N수집 된 샘플 을 보면 거리 델타가 가장 큰 위치를 확인할 수 있고 인접한 두 거리 사이의 중간에 리샘플링 ( "분할") N하여 1 씩 증가 합니다. 예를 들어 t=0.9(가장 큰 델타의 중간에 있음)에서 분할 하면 가져 오기:

0.8    5.81
0.9    7.39
1.0    10.00

전체 세트에서 두 거리 사이의 최대 델타가 일부 minDistanceDelta보다 낮을 때 epsilon까지 ,보다 구체적으로, 특정 거리에서 멀어 질 때까지이 단계를 다음 가장 큰 거리 간격 동안 반복합니다 t. 그런 다음 원하는 t단계를 해당 distances에 선형으로 매핑 할 수 있습니다 . 이렇게하면 해시 테이블 / 맵이 생성되어 값 싸게 액세스 할 수 있으며 런타임에 바이어스없이 값을 뛰어 넘을 수 있습니다.

세트가 증가함에 따라 N이를 반복하는 비용이 증가하므로 사전 프로세스로 이상적으로 수행하십시오. N증가 할 때마다 두 개의 새로운 결과 간격을 intervals콜렉션에 추가하고 교체 한 이전 단일 간격을 제거하십시오. 이것은 다음으로 가장 큰 간격을 두 개로 나누는 구조입니다. 유지 intervals거리상으로 분류하면 단지 등 마지막을 지나고 다음 작업 항목을 팝업 및 분할 수있는 상황이 쉽게 유지

우리는 이상적으로 원하는 것과 같은 결과를 얻습니다.

epsilon: 0.01

t            distance
0.200417     2.00417
0.3998132    3.9998132
0.600703     6.00703
0.800001     8.00001
0.9995309    9.995309

우리는 모든 단계에서 추측을 가지고 있기 때문에, 우리는 정확한 정확한 거리를 얻을 수 없습니다 2, 4우리가 원하는 등,하지만, 이러한 반복 반복을 통해 당신이 당신에 매핑 할 수 있도록 원하는 거리 값에 충분히 가까이 t인해 편견을 제거하는 공정 정확도 단계 거의 등거리 샘플링에.

그런 다음 t=0.5Ivo가 답변에서하는 것처럼 예를 들어 위의 두 가장 가까운 값 ( 3.99981326.00703) 사이에 보간하여을 검색 할 수 있습니다 .

결론

대부분의 경우, Ivo의 솔루션은 효과가 있지만, 모든 비용으로 편견을 피해야하는 경우 distance가능한 한 고르게 분산 시킨 다음에 선형으로 매핑해야 t합니다.

분할은 매번 중간을 분할하는 대신 확률 적으로 수행 할 수 있습니다. 예를 들어, 첫 번째 예제 간격을 at가 t=0.827아닌 분할 할 수 있습니다 t=0.9.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.