Ainda não resolvi as equações completas para isso, mas aqui estão alguns recursos visuais para ajudar a entender o problema. Tudo se resume a alguma geometria:
( Ícones de carros via Kenney )
A partir de qualquer ponto de partida e orientação, podemos desenhar dois círculos com o raio de viragem mínimo - um à esquerda e outro à direita. Eles descrevem os pontos do começo mais estreito possível ao nosso caminho.
Podemos fazer o mesmo para qualquer posição final e orientação desejadas. Esses círculos descrevem o fim mais apertado possível do nosso caminho.
Agora, o problema se reduz a encontrar um caminho que une um dos círculos iniciais a um dos círculos finais, beijando cada um ao longo de sua tangente.
(Isso pressupõe que não precisamos encontrar obstáculos intermediários, que não foram mencionados na pergunta. A resposta do Stormwind aborda como podemos usar as informações do gráfico de navegação para esses tipos de problemas. Depois que tivermos a sequência de nós para passar, podemos aplicar o método abaixo para cada segmento do plano.)
Se, por simplicidade, usamos linhas retas, obtemos algo assim:
Isso nos dá o caso limitante. Depois de encontrar um caminho por esse método, você pode aumentar artificialmente um ou ambos os círculos inicial e final para obter um caminho menos direto, porém mais suave, até o ponto em que os dois círculos se beijam.
Computando esses caminhos
Vamos elaborar os casos para uma direção de virada - digamos que começamos nosso caminho virando à direita.
O centro do nosso círculo de viragem à direita é:
startRightCenter = carStart.position + carStart.right * minRadius
Vamos chamar o ângulo da seção reta do nosso caminho (medido a partir do eixo x positivo) pathAngle
Se desenharmos um vetor a partir rightCenter
do ponto em que deixamos o círculo giratório (nesse ponto, devemos estar de frente para pathAngle), então esse vetor será ...
startOffset = minRadius * (-cos(pathAngle), sin(pathAngle))
Isso significa que o ponto em que deixamos o círculo deve ser ...
departure = startRightCenter + startOffset
O ponto em que reintroduzimos um círculo de viragem depende se pretendemos terminar com uma curva à esquerda ou à direita:
// To end with a right turn:
reentry = endRightCenter + startOffset
// To end with a left turn: (crossover)
reentry = endLeftCenter - startOffset
Agora, se nós fizemos nosso trabalho direito, a linha que une departure
a reentry
deveria ser perpendicular à startOffset
:
dot(reentry - departure, startOffset) = 0
E resolver esta equação nos dará o (s) ângulo (s) em que isso é verdade. (Eu uso um plural aqui, porque tecnicamente existem dois desses ângulos, mas um deles envolve dirigir em marcha à ré, o que geralmente não é o que queremos)
Vamos substituir a curva da direita para a direita como exemplo:
dot(endRightCenter + startOffset - startRightCenter - startOffset, startOffset) = 0
dot(endRightCenter - startRightCenter, startOffset) = 0
pathAngle = atan2(endRightCenter - startRightCenter)
O caso do crossover é mais complicado - ainda não resolvi toda a matemática. Vou postar a resposta sem por enquanto, caso seja útil para você enquanto trabalho nos detalhes restantes.
Edit: Destino dentro do raio de giro mínimo
Acontece que esse método geralmente funciona imediatamente, mesmo quando o destino está mais próximo do que a nossa distância mínima de giro. Pelo menos parte de um dos círculos de reentrada acaba fora do raio de virada, permitindo-nos encontrar um caminho viável, desde que não nos importemos com a aparência de um pretzel ...
Se não gostarmos do caminho que seguimos dessa maneira (ou se não for viável - não verifiquei todos os casos exaustivamente - talvez existam impossíveis), sempre podemos seguir em frente ou voltar até encontrar uma solução adequada. beijando o contato entre um círculo inicial e final, conforme diagrama acima.