Parte para fins de otimização, parte para fins de aprendizado, ousarei perguntar: Como posso verificar com mais eficiência se um ponto 2D P
está dentro de um retângulo girado em 2D XYZW
, usando C # ou C ++?
Atualmente, o que estou fazendo é usar o algoritmo "ponto no triângulo" encontrado no livro Detecção de colisão em tempo real e executá-lo duas vezes (para os dois triângulos que compõem o retângulo, digamos XYZ e XZW):
bool PointInTriangle(Vector2 A, Vector2 B, Vector2 C, Vector2 P)
{
// Compute vectors
Vector2 v0 = C - A;
Vector2 v1 = B - A;
Vector2 v2 = P - A;
// Compute dot products
float dot00 = Vector2.Dot(v0, v0);
float dot01 = Vector2.Dot(v0, v1);
float dot02 = Vector2.Dot(v0, v2);
float dot11 = Vector2.Dot(v1, v1);
float dot12 = Vector2.Dot(v1, v2);
// Compute barycentric coordinates
float invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
float v = (dot00 * dot12 - dot01 * dot02) * invDenom;
// Check if point is in triangle
if(u >= 0 && v >= 0 && (u + v) < 1)
{ return true; } else { return false; }
}
bool PointInRectangle(Vector2 X, Vector2 Y, Vector2 Z, Vector2 W, Vector2 P)
{
if(PointInTriangle(X,Y,Z,P)) return true;
if(PointInTriangle(X,Z,W,P)) return true;
return false;
}
No entanto, tenho a sensação de que pode haver uma maneira mais limpa e rápida. Especificamente, para reduzir o número de operações matemáticas.
c#
collision-detection
geometry
optimization
Louis15
fonte
fonte
Respostas:
Uma otimização fácil e direta seria alterar a condição final em
PointInTriangle
:o código
PointInRectangle
já estava praticamente , a condição(u + v) < 1
estava lá para verificar se não está no "segundo" triângulo do retângulo.Como alternativa, você também pode fazer uma
isLeft
(primeiro exemplo de código na página, também explicado em detalhes) quatro vezes e verificar se todos retornam resultados com o mesmo sinal (que depende de se os pontos foram dados no sentido horário ou anti-horário) para o ponto para estar dentro. Isso funciona para qualquer outro polígono convexo também.fonte
isLeft
método. Não requer funções trigonométricas (comoVector2.Dot
faz), o que acelera bastante as coisas.public static bool PointInRectangle(Vector2 P, Vector2 X, Vector2 Y, Vector2 Z, Vector2 W) { return !(( (Y.x - X.x) * (P.y - X.y) - (P.x - X.x) * (Y.y - X.y) ) < 0 || ( (Z.x - Y.x) * (P.y - Y.y) - (P.x - Y.x) * (Z.y - Y.y) ) < 0 || ( (W.x - Z.x) * (P.y - Z.y) - (P.x - Z.x) * (W.y - Z.y) ) < 0 || ( (X.x - W.x) * (P.y - W.y) - (P.x - W.x) * (X.y - W.y) ) < 0 ); }
isLeft
como inline o compilador fará algo semelhante para você (e provavelmente melhor do que você poderia, porque os engenheiros que escreviam o compilador conheciam melhor as CPUs, escolhendo qualquer opção mais rápida), tornando seu código mais legível com o mesmo ou melhor efeito.Edit: O comentário dos OPs tem sido cético sobre a eficiência da verificação do limite circular negativo sugerido para melhorar o algoritmo, a fim de verificar se um ponto 2D arbitrário se encontra dentro de um retângulo girado e / ou em movimento. Brincando um pouco no meu mecanismo de jogo em 2D (OpenGL / C ++), suplemento minha resposta fornecendo uma referência de desempenho do meu algoritmo com relação aos algoritmos (e variações) atuais dos pontos de verificação do retângulo no OP (e variações).
Originalmente, sugeri deixar o algoritmo no lugar (como é quase ideal), mas simplificá-lo através da mera lógica do jogo: (1) usando um círculo pré-processado em torno do retângulo original; (2) faça uma verificação de distância e se o ponto estiver dentro do círculo especificado; (3) use os OPs ou qualquer outro algoritmo direto (eu recomendo o algoritmo isLeft, conforme fornecido em outra resposta). A lógica por trás da minha sugestão é que verificar se um ponto está dentro de um círculo é consideravelmente mais eficiente do que uma verificação de limites de um retângulo girado ou qualquer outro polígono.
Meu cenário inicial para um teste de benchmark é executar um grande número de pontos que aparecem e desaparecem (cuja posição muda em cada ciclo do jogo) em um espaço restrito que será preenchido com cerca de 20 quadrados rotativos / móveis. Publiquei um vídeo ( link do youtube ) para fins ilustrativos. Observe os parâmetros: número de pontos que aparecem aleatoriamente, número ou retângulos. Vou comparar com os seguintes parâmetros:
OFF : Algoritmo simples, conforme fornecido pelo OP, sem verificações negativas nos limites do círculo
ATIVADO : usando círculos por processo (limite) em volta dos retângulos como uma primeira verificação de exclusão
ON + Stack : Criando limites de círculo em tempo de execução dentro do loop na pilha
ON + Distância quadrada : Usar distâncias quadradas como uma otimização adicional para evitar o algoritmo de raiz quadrada mais caro (Pieter Geerkens).
Aqui está um resumo dos vários desempenhos de diferentes algoritmos, mostrando o tempo necessário para percorrer o loop.
O eixo x mostra uma complexidade aumentada adicionando mais pontos (e diminuindo a velocidade do loop). (Por exemplo, em 1000 pontos de verificação aleatória em um espaço confidencial com 20 retângulos, o loop itera e chama o algoritmo 20000 vezes.) O eixo y mostra o tempo necessário (ms) para concluir o loop inteiro usando uma alta resolução temporizador de desempenho. Mais de 20 ms seriam problemáticos para um jogo decente, pois não tiraria proveito dos altos fps para interpolar uma animação suave e o jogo pode parecer assim 'áspero' às vezes.
Resultado 1 : um algoritmo de limite circular pré-processado com uma verificação negativa rápida dentro do loop melhora o desempenho em 1900% em comparação com o algoritmo regular (5% do tempo original do loop sem uma verificação). O resultado é aproximadamente proporcional ao número de iterações dentro de um loop, portanto, não importa se verificamos 10 ou 10000 pontos que aparecem aleatoriamente. Assim, nesta ilustração, é possível aumentar o número de objetos com segurança para 10k sem sentir uma perda de desempenho.
Resultado 2 : Foi sugerido por um comentário anterior que o algoritmo pode ser mais rápido, mas com muita memória. No entanto, observe que o armazenamento de um flutuador para o tamanho do círculo pré-processado leva apenas 4 bytes. Isso não deve representar problema real, a menos que o OP planeje executar simultaneamente mais de 100000 objetos. Uma abordagem alternativa e eficiente em termos de memória é calcular o tamanho máximo do círculo na pilha dentro do loop e deixá-lo fora do escopo a cada iteração e, portanto, praticamente não usa memória para um preço desconhecido da velocidade. De fato, o resultado mostra que essa abordagem é realmente mais lenta do que usar um tamanho de círculo pré-processado, mas ainda mostra uma melhoria considerável no desempenho em torno de 1150% (ou seja, 8% do tempo de processamento original).
Resultado 3 : eu melhoro ainda mais o algoritmo do resultado 1 usando distâncias quadradas em vez de distâncias reais e, assim, executando uma operação de raiz quadrada computacionalmente cara. Isso apenas aumenta levemente o desempenho (2400%). (Nota: eu também tento tabelas de hash para matrizes pré-processadas para aproximações de raízes quadradas com um resultado semelhante, mas um pouco pior)
Resultado 4 : Verifico mais a movimentação / colisão dos retângulos; no entanto, isso não altera os resultados básicos (conforme o esperado), pois a verificação lógica permanece essencialmente a mesma.
Resultado 5 : eu vario o número de retângulos e acho que o algoritmo se torna ainda mais eficiente quanto menos ocupado o espaço é preenchido (não mostrado na demonstração). O resultado também é algo esperado, pois a probabilidade diminui para que um ponto apareça dentro de um espaço minúsculo entre um círculo e os limites do objeto. Por outro lado, tento aumentar demais o número de retângulos em 100 dentro do mesmo espaço minúsculo confinado E variar dinamicamente em tamanho no tempo de execução no loop (sin (iterador)). Isso ainda apresenta um desempenho extremamente bom, com aumento no desempenho em 570% (ou 15% do tempo do loop original).
Resultado 6 : testei algoritmos alternativos sugeridos aqui e encontro uma diferença muito pequena, mas não significativa, no desempenho (2%). O interessante e mais simples algoritmo IsLeft funciona muito bem com um aumento de desempenho de 17% (85% do tempo de cálculo original), mas nem de longe com a eficiência de um algoritmo rápido de verificação negativa.
Meu objetivo é considerar primeiro o design enxuto e a lógica do jogo, especialmente ao lidar com limites e eventos de colisão. O algoritmo atual dos OPs já é bastante eficiente e uma otimização adicional não é tão crítica quanto otimizar o próprio conceito subjacente. Além disso, é bom comunicar o escopo e o objetivo do jogo, pois a eficiência de um algoritmo depende muito deles.
Sugiro sempre tentar fazer benchmark de qualquer algoritmo complexo durante o estágio de design do jogo, pois apenas olhar o código simples pode não revelar a verdade sobre o desempenho real do tempo de execução. O algoritmo sugerido pode não ser aqui necessário, se, por exemplo, se desejar apenas testar se o cursor do mouse está dentro de um retângulo ou não, ou quando a maioria dos objetos já está se tocando. Se a maioria das verificações de pontos estiver dentro do retângulo, o algoritmo será menos eficiente. (No entanto, seria possível estabelecer um limite de 'círculo interno' como uma verificação negativa secundária.) As verificações de limite de círculo / esfera são muito úteis para qualquer detecção de colisão decente de um grande número de objetos que naturalmente possuem algum espaço entre eles. .
fonte
Definir um retângulo com 4 pontos torna possível fazer um trapézio. Se, no entanto, você o definiria por x, y, largura, altura e uma rotação em torno do meio, basta girar o ponto que está verificando pela rotação inversa do seu retângulo (em torno da mesma origem) e depois verificar se está no retângulo original.
fonte
Não tive tempo de avaliar isso, mas minha sugestão seria armazenar a matriz de transformação que transforma o retângulo no quadrado alinhado ao eixo no intervalo x e y de 0 a 1. Em outras palavras, armazene a matriz que transforma um canto do retângulo em (0,0) e o oposto em (1,1).
Obviamente, isso seria mais caro se o retângulo for movido muito e a colisão for verificada raramente, mas se houver muito mais verificações do que atualizações no retângulo, isso seria pelo menos mais rápido que a abordagem original de testar contra dois triângulos, como os produtos de seis pontos seriam substituídos por uma multiplicação de matrizes.
Mas como sempre, a velocidade desse algoritmo depende muito do tipo de verificação que você espera que seja realizada. Se a maioria dos pontos não estiver nem perto do retângulo, executando uma verificação simples da distância (por exemplo, (point.x - firstCorner.x)> aLargeDistance) pode resultar em uma grande aceleração, enquanto pode até diminuir a velocidade, se quase todos os pontos estão dentro do retângulo.
EDIT: É assim que minha classe Rectangle seria:
Esta é a lista completa do meu benchmark:
O código certamente não é bonito, mas não vejo imediatamente nenhum erro grave. Com esse código, obtenho resultados que indicam que minha solução é duas vezes mais rápida se o retângulo for movido entre cada verificação. Se não se mover, meu código parece ser mais de cinco vezes mais rápido.
Se você souber como o código será usado, poderá até acelerar um pouco mais, separando a transformação e as verificações nas duas dimensões. Por exemplo, em um jogo de corrida, provavelmente seria mais rápido verificar primeiro as coordenadas que apontam na direção de direção, porque muitos obstáculos estarão na frente ou atrás do carro, mas dificilmente algum estará à direita ou à esquerda.
fonte