Como otimizar um mecanismo de colisão onde a ordem é significativa e a colisão é condicional com base no grupo de objetos?

14

Se esta é a primeira vez que você faz essa pergunta, sugiro ler a parte de pré-atualização abaixo, depois essa parte. Aqui está uma síntese do problema, no entanto:

Basicamente, eu tenho um mecanismo de detecção e resolução de colisões com um sistema de particionamento espacial de grade onde os grupos de ordem e colisão são importantes. Um corpo de cada vez deve se mover, detectar colisões e resolver colisões. Se eu mover todos os corpos de uma só vez, gerar possíveis pares de colisão, é obviamente mais rápido, mas a resolução é interrompida porque a ordem de colisão não é respeitada. Se eu mover um corpo por vez, sou forçado a fazer com que os corpos verifiquem colisões, e isso se torna um problema ^ 2. Coloque grupos na mistura e você pode imaginar por que fica muito lento muito rápido com muitos corpos.


Atualização: trabalhei muito nisso, mas não consegui otimizar nada.

Eu implementei com sucesso a "pintura" descrita por Will e mudei os grupos em bits, mas é uma aceleração muito menor.

Também descobri um grande problema: meu mecanismo depende da ordem de colisão.

Eu tentei uma implementação de geração de pares de colisão exclusiva , que definitivamente acelera muito tudo, mas quebrei a ordem de colisão .

Deixe-me explicar:

  • no meu design original (sem gerar pares), isso acontece:

    1. um único corpo se move
    2. depois que se move, atualiza suas células e obtém os corpos contra os quais colide
    3. se sobrepuser um corpo ao qual precisa resolver, resolva a colisão

    isso significa que, se um corpo se mover e atingir uma parede (ou qualquer outro corpo), apenas o corpo que se moveu resolverá sua colisão e o outro corpo não será afetado.

    Esse é o comportamento que desejo .

    Entendo que não é comum para motores de física, mas tem muitas vantagens para jogos em estilo retrô .

  • no design de grade usual (gerando pares únicos), isso acontece:

    1. todos os corpos se movem
    2. depois que todos os corpos se moverem, atualize todas as células
    3. gerar pares de colisão exclusivos
    4. para cada par, lide com a detecção e resolução de colisões

    nesse caso, um movimento simultâneo pode resultar na sobreposição de dois corpos e eles serão resolvidos ao mesmo tempo - isso efetivamente faz com que os corpos "se empurrem" e rompe a estabilidade de colisão com vários corpos

    Esse comportamento é comum para mecanismos de física, mas não é aceitável no meu caso .

Eu também encontrei outro problema, que é importante (mesmo que não seja provável que isso aconteça em uma situação do mundo real):

  • considerar órgãos dos grupos A, B e W
  • A colide e resolve contra W e A
  • B colide e resolve contra W e B
  • A não faz nada contra B
  • B não faz nada contra A

pode haver uma situação em que muitos corpos A e B ocupam a mesma célula - nesse caso, há muita iteração desnecessária entre os corpos que não devem reagir entre si (ou apenas detectar colisão, mas não resolvê-los) .

Para 100 corpos ocupando a mesma célula, são 100 ^ 100 iterações! Isso acontece porque pares únicos não estão sendo gerados - mas eu não posso gerar pares únicos , caso contrário, obteria um comportamento que não desejo.

Existe uma maneira de otimizar esse tipo de mecanismo de colisão?

Estas são as diretrizes que devem ser respeitadas:

  • A ordem de colisão é extremamente importante!

    • Os corpos devem se mover um de cada vez , depois verificar colisões, um de cada vez , e resolver após o movimento, um de cada vez .
  • Os corpos devem ter 3 conjuntos de bits de grupo

    • Grupos : grupos aos quais o corpo pertence
    • GroupsToCheck : agrupa o corpo deve detectar colisão contra
    • GroupsNoResolve : agrupa o corpo não deve resolver colisões contra
    • Pode haver situações em que eu só quero que uma colisão seja detectada, mas não resolvida



Pré-atualização:

Prefácio : sei que otimizar esse gargalo não é uma necessidade - o mecanismo já é muito rápido. Eu, no entanto, por motivos divertidos e educacionais, adoraria encontrar uma maneira de tornar o mecanismo ainda mais rápido.


Estou criando um mecanismo de detecção / resposta de colisão 2D C ++ de uso geral, com ênfase na flexibilidade e velocidade.

Aqui está um diagrama muito básico de sua arquitetura:

Arquitetura básica do mecanismo

Basicamente, a classe principal é World, que possui (gerencia a memória) de a ResolverBase*, a SpatialBase*e a vector<Body*>.

SpatialBase é uma classe virtual pura que lida com a detecção de colisão em fase ampla.

ResolverBase é uma classe virtual pura que lida com a resolução de colisões.

Os corpos comunicar à World::SpatialBase*com SpatialInfoobjetos, pertencentes aos próprios corpos.


Atualmente, existe uma classe espacial:, Grid : SpatialBaseque é uma grade 2D fixa básica. Tem sua própria classe de informação GridInfo : SpatialInfo,.

Veja como fica sua arquitetura:

Arquitetura do mecanismo com grade espacial

A Gridclasse possui uma matriz 2D de Cell*. A Cellclasse contém uma coleção de (não pertencente) Body*: a vector<Body*>que contém todos os corpos que estão na célula.

GridInfo os objetos também contêm ponteiros não proprietários para as células em que o corpo está.


Como eu disse anteriormente, o mecanismo é baseado em grupos.

  • Body::getGroups()retorna um std::bitsetde todos os grupos dos quais o corpo faz parte.
  • Body::getGroupsToCheck()retorna um std::bitsetde todos os grupos contra os quais o corpo tem que verificar a colisão.

Os corpos podem ocupar mais de uma única célula. O GridInfo sempre armazena ponteiros não proprietários nas células ocupadas.


Depois que um único corpo se move, a detecção de colisão acontece. Suponho que todos os corpos sejam caixas delimitadoras alinhadas ao eixo.

Como a detecção de colisão de fase ampla funciona:

Parte 1: atualização de informações espaciais

Para cada um Body body:

    • As células ocupadas no canto superior esquerdo e as células ocupadas no canto inferior direito são calculadas.
    • Se eles diferirem das células anteriores, body.gridInfo.cellssão limpos e preenchidos com todas as células que o corpo ocupa (2D para loop da célula superior esquerda para a célula inferior direita).
  1. body agora é garantido saber quais células ele ocupa.

Parte 2: verificações reais de colisão

Para cada um Body body:

  • body.gridInfo.handleCollisions é chamado:

void GridInfo::handleCollisions(float mFrameTime)
{
    static int paint{-1};
    ++paint;

    for(const auto& c : cells)
        for(const auto& b : c->getBodies())
        {
            if(b->paint == paint) continue;
            base.handleCollision(mFrameTime, b);
            b->paint = paint;
        }
}

void Body::handleCollision(float mFrameTime, Body* mBody)
    {
        if(mBody == this || !mustCheck(*mBody) || !shape.isOverlapping(mBody->getShape())) return;

        auto intersection(getMinIntersection(shape, mBody->getShape()));

        onDetection({*mBody, mFrameTime, mBody->getUserData(), intersection});
        mBody->onDetection({*this, mFrameTime, userData, -intersection});

        if(!resolve || mustIgnoreResolution(*mBody)) return;
        bodiesToResolve.push_back(mBody);
    }

  • A colisão é então resolvida para cada corpo em bodiesToResolve.

  • É isso aí.


Então, eu tenho tentado otimizar essa detecção de colisão de fase ampla por um bom tempo agora. Toda vez que tento algo diferente da arquitetura / configuração atual, algo não sai como o planejado ou assumo a simulação que mais tarde provou ser falsa.

Minha pergunta é: como posso otimizar a fase ampla do meu mecanismo de colisão ?

Existe algum tipo de otimização mágica de C ++ que pode ser aplicada aqui?

A arquitetura pode ser redesenhada para permitir mais desempenho?


Saída do Callgrind para a versão mais recente: http://txtup.co/rLJgz

Vittorio Romeo
fonte
Perfile e identifique gargalos. Deixe-nos saber onde eles estão, então temos algo para trabalhar.
Maik Semder
@MaikSemder: Eu fiz isso e escrevi no post. É o único trecho de código que é o gargalo. Desculpe se é longo e detalhado, mas isso faz parte da pergunta, porque tenho certeza de que esse gargalo só pode ser resolvido alterando algo no design do mecanismo.
Vittorio Romeo
Desculpe, foi difícil de encontrar. Você pode nos dar alguns números? O tempo da função e o número de objetos processados ​​nessa função?
Maik Semder
@MaikSemder: testado com Callgrind, em um binário compilado com Clang 3.4 SVN -O3: 10000 corpos dinâmicos - a função getBodiesToCheck()foi chamada 5462334 vezes e ocupou 35,1% de todo o tempo de criação de perfil (tempo de acesso de leitura da instrução)
Vittorio Romeo
2
@ Quonux: nenhuma ofensa tomada. Eu simplesmente amo "reinventar a roda". Eu poderia pegar o Bullet ou o Box2D e fazer um jogo com essas bibliotecas, mas esse não é realmente o meu objetivo. Sinto-me muito mais satisfeito e aprendo muito mais criando coisas do zero e tentando superar os obstáculos que aparecem - mesmo que isso signifique ficar frustrado e pedir ajuda. Além da minha convicção de que a codificação do zero é inestimável para fins de aprendizado, também acho muito divertido e um grande prazer passar o meu tempo livre.
Vittorio Romeo

Respostas:

14

getBodiesToCheck()

Pode haver dois problemas com a getBodiesToCheck()função; primeiro:

if(!contains(bodiesToCheck, b)) bodiesToCheck.push_back(b);

Esta parte é O (n 2 ), não é?

Em vez de verificar se o corpo já está na lista, use a pintura .

loop_count++;
if(!loop_count) { // if loop_count can wrap,
    // you just need to iterate all bodies to reset it here
}
bodiesToCheck.clear();
for(const auto& q : queries)
    for(const auto& b : *q)
        if(b->paint != loop_count) {
            bodiesToCheck.push_back(b);
            b->paint = loop_count;
        }
return bodiesToCheck;

Você está referenciando o ponteiro na fase de coleta, mas referenciando-o na fase de teste, portanto, se você tiver L1 suficiente, não é grande coisa. Você pode melhorar o desempenho adicionando dicas de pré-busca ao compilador também __builtin_prefetch, por exemplo , embora isso seja mais fácil com for(int i=q->length; i-->0; )loops clássicos e similares.

Essa é uma alteração simples, mas meu segundo pensamento é que poderia haver uma maneira mais rápida de organizar isso:

No entanto, você pode usar bitmaps e evitar o bodiesToCheckvetor inteiro . Aqui está uma abordagem:

Você já está usando chaves inteiras para corpos, mas depois as procura em mapas e outras coisas e mantém listas delas. Você pode mover para um alocador de slot, que é basicamente apenas uma matriz ou vetor. Por exemplo:

class TBodyImpl {
   public:
       virtual ~TBodyImpl() {}
       virtual void onHit(int other) {}
       virtual ....
       const int slot;
   protected:
      TBodyImpl(int slot): slot(slot_) {}
};

struct TBodyBase {
    enum ... type;
    ...
    rect_t rect;
    TQuadTreeNode *quadTreeNode; // see below
    TBodyImpl* imp; // often null
};

std::vector<TBodyBase> bodies; // not pointers to them

O que isso significa é que todo o material necessário para realizar as colisões reais está na memória linear compatível com o cache, e você só vai para o bit específico da implementação e o anexa a um desses slots se for necessário.

Para rastrear as alocações nesse vetor de corpos, você pode usar uma matriz de números inteiros como um bitmap e usar ajustes de bits ou __builtin_ffsetc. Isso é super eficiente para mover para os slots que estão ocupados no momento ou para encontrar um slot desocupado na matriz. Às vezes, você pode compactar a matriz, se ela crescer excessivamente grande e depois os lotes forem marcados como excluídos, movendo-os no final para preencher as lacunas.

verifique apenas para cada colisão uma vez

Se você verificou se a colide com b , não precisa verificar se b colide com a também.

Resulta do uso de IDs inteiros que você evita essas verificações com uma simples instrução if. Se o ID de uma colisão em potencial for menor ou igual ao ID atual que está sendo verificado, ele poderá ser ignorado! Dessa forma, você só verificará cada par possível uma vez; isso representa mais da metade do número de verificações de colisão.

unsigned * bitmap;
int bitmap_len;
...

for(int i=0; i<bitmap_len; i++) {
  unsigned mask = bitmap[i];
  while(mask) {
      const int j = __builtin_ffs(mask);
      const int slot = i*sizeof(unsigned)*8+j;
      for(int neighbour: get_neighbours(slot))
          if(neighbour > slot)
              check_for_collision(slot,neighbour);
      mask >>= j;
  }

respeitar a ordem das colisões

Em vez de avaliar uma colisão assim que um par for encontrado, calcule a distância para atingi-la e armazená-la em uma pilha binária . Esses montes são como você normalmente faz filas prioritárias na localização de caminhos, assim como um código utilitário muito útil.

Marque cada nó com um número de sequência, para poder dizer:

  • A 10 bate B 12 em 6
  • A 10 atinge C 12 em 3

Obviamente, depois de reunir todas as colisões, comece a saltar da fila de prioridade, o mais rápido primeiro. Portanto, o primeiro a ser obtido é A 10 e C 12 em 3. Você incrementa o número de sequência de cada objeto (o 10 bits), avalia a colisão, calcula seus novos caminhos e armazena suas novas colisões na mesma fila. A nova colisão é A 11 atinge B 12 em 7. A fila agora tem:

  • A 10 bate B 12 em 6
  • A 11 bate B 12 em 7

Então você sai da fila de prioridade e seu A 10 atinge B 12 em 6. Mas você vê que A 10 é obsoleto ; A está atualmente em 11. Portanto, você pode descartar essa colisão.

É importante não se preocupar em tentar excluir todas as colisões antigas da árvore; remover de um monte é caro. Simplesmente descarte-os quando você os estourar.

a grade

Você deve considerar usar um quadtree. É uma estrutura de dados muito simples de implementar. Muitas vezes, você vê implementações que armazenam pontos, mas eu prefiro armazenar rects e armazenar o elemento no nó que o contém. Isso significa que, para verificar colisões, você só precisa iterar sobre todos os corpos e, para cada um, verificar esses corpos no mesmo nó de quatro árvores (usando o truque de classificação descrito acima) e todos aqueles nos nós de quatro árvores principais. O quad-tree é ele próprio a lista de possíveis colisões.

Aqui está um Quadtree simples:

struct Object {
    Rect bounds;
    Point pos;
    Object * prev, * next;
    QuadTreeNode * parent;
};

struct QuadTreeNode {
    Rect bounds;
    Point centre;
    Object * staticObjects;
    Object * movableObjects;
    QuadTreeNode * parent; // null if root
    QuadTreeNode * children[4]; // null for unallocated children
};

Armazenamos os objetos móveis separadamente porque não precisamos verificar se os objetos estáticos vão colidir com alguma coisa.

Estamos modelando todos os objetos como caixas delimitadoras alinhadas ao eixo (AABB) e os colocamos no menor QuadTreeNode que os contém. Quando um QuadTreeNode muitos filhos, você pode subdividi-lo ainda mais (se esses objetos se distribuírem bem nos filhos).

Cada tick do jogo, você precisa recuar para o quadtree e calcular o movimento - e as colisões - de cada objeto móvel. Ele deve ser verificado quanto a colisões com:

  • todo objeto estático em seu nó
  • todo objeto móvel em seu nó que está antes dele (ou depois dele; basta escolher uma direção) na lista movableObjects
  • todo objeto móvel e estático em todos os nós pais

Isso irá gerar todas as colisões possíveis, sem ordem. Você então faz os movimentos. Você precisa priorizar esses movimentos à distância e 'quem se move primeiro' (qual é o seu requisito especial) e executá-los nessa ordem. Use um monte para isso.

Você pode otimizar este modelo quadtree; você não precisa realmente armazenar os limites e o ponto central; isso é totalmente derivável quando você anda na árvore. Você não precisa verificar se um modelo está dentro dos limites, apenas verifique de que lado está o ponto central (um teste de "eixo de separação").

Para modelar objetos voadores rápidos, como projéteis, em vez de movê-los a cada passo ou ter uma lista separada de 'marcadores' que você sempre verifica, basta colocá-los no quadtree com o reto do voo para várias etapas do jogo. Isso significa que eles se movem no quadtree muito mais raramente, mas você não está checando balas contra paredes distantes, por isso é uma boa escolha.

Objetos estáticos grandes devem ser divididos em partes componentes; um cubo grande deve ter cada face armazenada separadamente, por exemplo.

Vai
fonte
"Pintar" parece bom, vou tentar e relatar os resultados o mais rápido possível. Não entendo a segunda parte da sua resposta - tentarei ler algo sobre pré-busca.
Vittorio Romeo
Eu não recomendaria o QuadTree, é mais complicado do que fazer uma grade e, se não for feito corretamente, não funcionará com precisão e criará / removerá os nós com muita frequência.
ClickerMonkey 27/06
Sobre sua pilha: a ordem do movimento é respeitada? Considere corpo A eo corpo B . A se move para a direita em direção a B e B se move para a direita em direção a A. Agora - quando colidem simultaneamente, o que se moveu primeiro deve ser resolvido primeiro e o outro não será afetado.
Vittorio Romeo
@VittorioRomeo se A se move em direção a B e B se move em direção a A no mesmo tick e na mesma velocidade, eles se encontram no meio? Ou A, movendo-se primeiro, encontra B onde B começa?
Will
1
@Will youtube.com/watch?v=EExHVi8NMzA
Vittorio Romeo
3

Aposto que você tem uma tonelada de erros de cache ao percorrer os corpos. Você está reunindo todos os seus corpos usando algum esquema de design orientado a dados? Com uma fase larga N ^ 2, posso simular centenas e centenas , enquanto grava com fraps, de corpos sem queda de taxa de quadros nas regiões inferiores (menos de 60), e tudo isso sem um alocador personalizado. Imagine o que pode ser feito com o uso adequado do cache.

A pista está aqui:

const std::vector<Body *>

Isso imediatamente levanta uma enorme bandeira vermelha. Você está alocando esses corpos com novas chamadas brutas? Existe um alocador personalizado em uso? É mais importante que você tenha todos os seus corpos em uma enorme variedade na qual você atravessa linearmente . Se atravessar a memória linearmente não for algo que você consiga implementar, considere o uso de uma lista intrusivamente vinculada.

Além disso, você parece estar usando std :: map. Você sabe como a memória no std :: map é alocada? Você terá uma complexidade O (lg (N)) para cada consulta de mapa, e isso provavelmente pode ser aumentado para O (1) com uma tabela de hash. Além disso, a memória alocada pelo std :: map também afetará terrivelmente o seu cache.

Minha solução é usar uma tabela de hash intrusiva no lugar de std :: map. Um bom exemplo de listas intrusivamente vinculadas e tabelas de hash intrusivas está na base de Patrick Wyatt em seu projeto de coho: https://github.com/webcoyote/coho

Portanto, em resumo, você provavelmente precisará criar algumas ferramentas personalizadas para você, como um alocador e alguns contêineres intrusivos. É o melhor que posso fazer sem criar um perfil do código para mim.

RandyGaul
fonte
"Você está alocando esses corpos com novas chamadas brutas?" Não estou ligando explicitamente newao enviar corpos para o getBodiesToCheckvetor - você quer dizer que isso está acontecendo internamente? Existe uma maneira de evitar isso enquanto ainda temos uma coleção de corpos de tamanho dinâmico?
Vittorio Romeo
std::mapnão é um gargalo - também me lembro de tentar dense_hash_sete não obter nenhum tipo de desempenho.
Vittorio Romeo
@Vittorio, então qual parte do getBodiesToCheck é o gargalo? Precisamos de informações para ajudar.
Maik Semder
@MaikSemder: o criador de perfil não é mais profundo que a própria função. Toda a função é o gargalo, porque está sendo chamada uma vez por quadro por corpo. 10000 corpos = 10000 getBodiesToCheckchamadas por quadro. Suspeito que a constante limpeza / inserção do vetor seja o gargalo da própria função. O containsmétodo também faz parte da desaceleração, mas desde que bodiesToChecknão tem mais de 8-10 corpos nele, ele deve ser tão lento
Vittorio Romeo
@Vittorio seria bom se você colocar essas informações nas perguntas, isso é um divisor de águas;) Particularmente, quero dizer a parte que getBodiesToCheck é chamada para todos os corpos, portanto, 10000 vezes cada quadro. Gostaria de saber, você disse que eles estavam em grupos, então por que colocá-los no corpoToToCheck-array, se você já possui as informações do grupo. Você pode elaborar sobre essa parte, parece um ótimo candidato para otimização para mim.
Maik Semder
1

Reduza a contagem de corpos para verificar cada quadro:

Apenas verifique os corpos que realmente podem se mover. Objetos estáticos só precisam ser atribuídos às células de colisão uma vez depois de serem criados. Agora verifique apenas colisões para grupos que contenham pelo menos um objeto dinâmico. Isso deve reduzir o número de verificações em cada quadro.

Use um quadtree. Veja minha resposta detalhada aqui

Remova todas as alocações do seu código de física. Você pode querer usar um criador de perfil para isso. Mas eu analisei apenas a alocação de memória em C #, então não posso ajudar com C ++.

Boa sorte!

Stephen
fonte
0

Vejo dois candidatos a problemas em sua função de gargalo:

O primeiro é a parte "contém" - esta é provavelmente a principal razão do gargalo. Ele está interagindo através de corpos já encontrados para cada corpo. Talvez você deva usar algum tipo de hash_table / hash_map em vez de vetor. A inserção deve ser mais rápida (com busca de duplicidades). Mas não conheço números específicos - não tenho idéia de quantos corpos são iterados aqui.

O segundo problema pode ser vector :: clear e push_back. Clear pode ou não evocar realocação. Mas você pode evitá-lo. A solução pode ser uma matriz de sinalizadores. Mas você provavelmente pode ter muitos objetos; portanto, é ineficaz em termos de memória ter uma lista de todos os objetos para cada objeto. Outra abordagem poderia ser legal, mas não sei qual abordagem: /

zacharmarz
fonte
Sobre o primeiro problema: tentei usar um dense_hash_set em vez do vetor + contém e foi mais lento. Tentei preencher o vetor e, em seguida, remover todas as duplicatas, e ficou mais lento.
Vittorio Romeo
0

Nota: Não sei nada de C ++, apenas Java, mas você deve conseguir descobrir o código. Física é linguagem universal, certo? Também percebo que este é um post de um ano, mas eu só queria compartilhar isso com todos.

Eu tenho um padrão de observador que, basicamente, depois que a entidade se move, ele retorna o objeto com o qual colidiu, incluindo um objeto NULL. Simplificando:

( Estou refazendo minecraft )

public Block collided(){
   return World.getBlock(getLocation());
}

Então diga que você está vagando pelo seu mundo. sempre que você ligar move(1), ligue collided(). se você conseguir o bloco desejado, talvez as partículas voem e você possa se mover para a esquerda direita e para trás, mas não para frente.

Usando isso de forma mais genérica do que apenas minecraft como exemplo:

public Object collided(){
   return threeDarray[3][2][3];
}

Simplesmente, tenha uma matriz para apontar as coordenadas que, literalmente, como Java faz isso, usam ponteiros.

O uso desse método ainda requer algo diferente do método a priori de detecção de colisão. Você pode repetir isso, mas isso anula o objetivo. Você pode aplicar isso às técnicas de colisão ampla, média e estreita, mas por si só é uma fera, especialmente quando funciona muito bem para jogos 3D e 2D.

Agora, dando mais uma olhada, isso significa que, de acordo com o meu método minecraft collide (), eu terminarei dentro do bloco, então terei que mover o jogador para fora dele. Em vez de verificar o jogador, preciso adicionar uma caixa delimitadora que verifique qual bloco está atingindo cada lado da caixa. Problema resolvido.

o parágrafo acima pode não ser tão fácil com polígonos se você quiser precisão. Para maior precisão, sugiro definir uma caixa delimitadora de polígono que não seja um quadrado, mas não em mosaico. se não, então um retângulo está ótimo.

AMDG
fonte