GLSL Sprite-Kollision [SFML]



  • Hallo,

    Mir ist eben eine Idee für schnelle Kollisionsprüfung in SFML gekommen. Die Idee wäre, wenn ein Sprite gezeichnet wird in einem Fragment Shader zu prüfen, ob der Framebuffer an der Stelle eine bestimmte Farbe hat, also der Sprite auf einen anderen gezeichnet wird.
    Dazu müsste folgendes möglich sein:

    - Farbe vom Framebuffer auslesen
    - Boolscher wert vom Shader aus an die App zurückschreiben (Kollision)

    Weiss jemand ob diese Dinge prinzipiell möglich sind? Danke!


  • Mod

    beides geht normalerweise nicht, was vermutlich der grund ist weshalb niemand bisher diese idee genutzt hat.
    die waffe die du hier anwendest ist auch ein bissl gross fuer die problemstellung.



  • Das mit den Farben halte ich für halbwegs unflexibel.
    Vielleicht könnte man statt dessen mit dem Stencil-Buffer und/oder Depth-Buffer arbeiten?

    Wobei: Pixelgenaue Kollisionserkennung ist sowieso nicht das was man normalerweise haben will. Zumindest nicht wenn die Kollisionsmasken 1:1 mit der grafischen Darstellung eines Objekts verknüpft sind.

    Und wenn man sich von pixelgenau verabschiedet landet man schnell bei dem was die meisten Spiele sowieso schon machen: irgendwelche einfach zu testenden Bounding-Volumes (Kreise, Boxen), und das ganze dann über diverse Tricks optimiert damit man nicht so viele Tests machen muss (hierarchisch Bounding-Volumes, Quadtrees, Grids).



  • Ich hätte mir das so vorgestellt, die beiden zu prüfenden Sprites auf ein separates render Target zu zeichnen und dabei nur den Alphawert zu verwenden. Der Vorteil wäre, dass die Transformation des Sprites automatisch berücksichtigt worden wäre.
    Ich glaube aber, so könnte es funktionieren:
    1. Erstelle sf::RenderTexture in Grösse des Fensters.
    2. Zeichen 1. Sprite in Farbe Weiss mit Alphakanal.
    3. Zeichne 2. Sprite, aber prüfe in Shader nur ob an der Stelle schon ein weisses Pixel vorhanden ist und der 2. Sprite an der Stelle auch opak ist. Dafür kann die RenderTexture dem Shader übergeben werden.
    4. Übergebe Position irgendwie der App...



  • Es gibt da aber einige fundamentale Probleme mit diesem Ansatz; allem voran, dass du dadurch schonmal rein prinzpiell gezwungen bist, alle Sprites einzeln hintereinander zu rendern, was bei vielen Sprites so ziemlich das maximal ineffiziente Vorgehen ist. Weiters musst du das Ergebnis des Tests irgendwie von der Grafikkarte zurück zur CPU bringen, was dazu führt, dass CPU und GPU noch öfter aufeinander warten müssen. Für einige wenige Sprites könnte das funktionieren; sofern ich den Ort der Kollision nicht kennen muss, würd ich es wohl mit Occlusion Queries versuchen. Aber selbst vom Standpunkt des Softwaredesign würde man es in der Regel wohl eigentlich anstreben, Rendering und Spiellogik möchst voneinander entkoppelt zu halten...



  • @stefan666
    Man kann es auf jeden Fall irgendwie auf der Grafikkarte machen. Die Frage ist nur wie schnell das dann läuft und ob es Sinn macht.

    Und überleg dir nochmal eins: stell dir ein klassisches 2D Raumschiff-Ballerspiel vor. Wie willst du checken welcher Schuss des Spielers welchen Gegner getroffen hat?
    Dazu müsstest du entweder pro Schuss die Textur löschen und alle Gegner zeichnen oder pro Gegner die Textur löschen und alle Schüsse zeichnen.
    Vom Aufwand her auf jeden Fall O(N*M).

    Ein naiver Algorithmus mit Bounding-Box Tests ist genauso O(N*M), nur linear viel viel schneller.

    Und wenn du es wirklich pixelgenau haben willst, kannst du immer noch einen Feintest nachschieben. Theoretisch könnte man den dann wieder auf der Grafikkarte machen, aber vermutlich ist auch der schneller wenn man ihn einfach schnell CPU-seitig macht.

    Und ganz wichtig: du kannst den CPU-seitig laufenden O(N*M) Algorithmus mit Bounding-Box Tests mit relativ geringem Aufwand optimieren.



  • Ich habe jetzt eine elegante, pixelgenaue Lösung gefunden, wo auch die Transformation der Sprites berücksichtigt wird. War wohl doch einiges einfacher als es auf der GPU zu machen 😉
    Nur das Auslesen der Texturen könnte noch etwas Overhead machen, wobei man die Masken natürlich auch separat zur Verfügung stellen könnte. Danke, dass ihr mich von der dummen Idee abgebracht habt 😉
    Falls jemand am Code interessiert ist:

    #ifndef COLLISION_HPP_INCLUDED
    #define COLLISION_HPP_INCLUDED
    
    #include <algorithm>
    #include <SFML/Graphics.hpp>
    
    inline bool Collides(const sf::Sprite& spr1, const sf::Sprite& spr2, sf::Vector2f& location)
    {
        sf::FloatRect rect1 = spr1.getGlobalBounds();
        sf::FloatRect rect2 = spr2.getGlobalBounds();
    
        // if bounding boxes don't intersect no further check is needed
        if(!rect1.intersects(rect2)) return false;
    
        // calculate intersecting rect
        float left = std::max(rect1.left, rect2.left);
        float top = std::max(rect1.top, rect2.top);
        float right = std::min(rect1.left + rect1.width, rect2.left + rect2.width);
        float bottom = std::min(rect1.top + rect1.height, rect2.top + rect2.height);
    
        // copy textures to images for pixel access
        sf::Image img1 = spr1.getTexture()->copyToImage();
        sf::Image img2 = spr2.getTexture()->copyToImage();
    
        // sample image rows and columns for collision
        for(int i = top; i < bottom; ++i)
        {
            for(int j = left; j < right; ++j)
            {
                // apply reverse transformation to sample points
                sf::Vector2f samp1 = spr1.getInverseTransform() * sf::Vector2f(j, i);
                sf::Vector2f samp2 = spr2.getInverseTransform() * sf::Vector2f(j, i);
                int x1 = samp1.x;
                int y1 = samp1.y;
                int x2 = samp2.x;
                int y2 = samp2.y;
    
                // check if both samples are on the image
                if( x1 >= 0 && y1 >= 0 && x1 < img1.getSize().x && y1 < img1.getSize().y &&
                    x2 >= 0 && y2 >= 0 && x2 < img2.getSize().x && y2 < img2.getSize().y )
                {
                    // if two pixels with alpha > 127 overlap we detect a collision
                    if( reinterpret_cast<const sf::Color*>(img1.getPixelsPtr())[x1 + y1 * img1.getSize().x].a > 127 &&
                        reinterpret_cast<const sf::Color*>(img2.getPixelsPtr())[x2 + y2 * img2.getSize().x].a > 127 )
                    {
                        // set collision location on screen
                        location.x = j;
                        location.y = i;
                        return true;
                    }
                }
            }
        }
    
        return false;
    }
    
    #endif // COLLISION_HPP_INCLUDED
    

Anmelden zum Antworten