rlkw1024
rlkw1024

Reputation: 6515

Z-sorted list of 3d objects

In a C++/OpenGL app, I have a bunch of translucent objects arranged in 3d space. Because of the translucency, the objects must be drawn in order from furthest to nearest. (For the reasons described in "Transparency Sorting.")

Luckily, the camera is fixed. So I plan to maintain a collection of pointers to the 3d objects, sorted by camera Z. Each frame, I'll iterate over the collection, drawing each object.

Fast insertion and deletion are important, because the objects in existence change frequently.

I'm considering using a std::list as the container. To insert, I'll use std::lower_bound to determine where the new object goes. Then I'll insert at the iterator returned by lower_bound.

Does this sound like a sane approach? Given the details I've provided, do you foresee any major performance issues I've overlooked?

Upvotes: 0

Views: 120

Answers (3)

Reto Koradi
Reto Koradi

Reputation: 54592

I don't think a std::list would ever be a good choice for this use case. While insertion is very inefficient, you need to iterate through the list to find the right place for the insertion, which makes it O(n) complexity.

If you want to keep it simple, a std::set would already be much better, and even simpler to apply than std::list. It's implemented as a balanced tree, so insertion is O(log n) complexity, and done by simply calling the insert() method on the container. The iterator gives you the elements in sorted order. It does have the downside of non-local memory access patterns during iteration, which makes it not cache friendly.

Another approach comes to mind that intuitively should be very efficient. Its basic idea is similar to what @ratchet_freak already proposed, but it does not copy the entire vector on each iteration:

  • The container that contains the main part of the data is a std::vector, which is always kept sorted.
  • New elements are added to an "overflow" container, which could be a std::set, or another std::vector that is kept sorted. This is only allowed to reach a certain size.
  • While iterating, traverse the main and overflow containers simultaneously, using similar logic to a merge sort.
  • When the overflow container reaches the size limit, merge it with the main container, resulting in a new main container.

A rough sketch of the code for this:

const size_t OVERFLOW_SIZE = 32;

// Ping pong between two vectors when merging.
std::vector<Entry> mainVecs[2];
unsigned activeIdx = 0;

std::vector<Entry> overflowVec;
overflowVec.reserve(OVERFLOW_SIZE);

void insert(const Entry& entry) {
    std::vector<Entry>::iterator pos =
        std::upper_bound(overflowVec.begin(), overflowVec.end(), entry);
    overflowVec.insert(pos, 1, entry);

    if (overflowVec.size() == OVERFLOW_SIZE) {
        std::merge(mainVecs[activeIdx].begin(), mainVecs[activeIdx].end(),
                   overflowVec.begin(), overflowVec.end(),
                   mainVecs[1 - activeIdx].begin());

        mainVecs[activeIdx].clear();
        overflowVec.clear();
        activeIdx = 1 - activeIdx;
    }
}

void draw() {
    std::vector<Entry>::const_iterator mainIt = mainVecs[activeIdx].begin();
    std::vector<Entry>::const_iterator mainEndIt = mainVecs[activeIdx].begin();

    std::vector<Entry>::const_iterator overflowIt = overflowVec.begin();
    std::vector<Entry>::const_iterator overflowEndIt = overflowVec.end();

    for (;;) {
        if (overflowIt == overflowEndIt) {
            if (mainIt == mainEndIt) {
                break;
            }
            draw(*mainIt);
            ++mainIt;
        } else if (mainIt == mainEndIt) {
            if (overflowIt == overflowEndIt) {
                break;
            }
            draw(*overflowIt);
            ++overflowIt;
        } else if (*mainIt < *overflowIt) {
            draw(*mainIt);
            ++mainIt;
        } else {
            draw(*overflowIt);
            ++overflowIt;
        }
    }
}

Upvotes: 1

ratchet freak
ratchet freak

Reputation: 48196

Depending on how big the list is you can keep a smaller "mutation set" for the objects that got added/changed the last frame and a big existing sorted set.

Then each frame you do a merge while drawing:

vector<GameObject*> newList;
newList.reserve(mutationSet.size()+ExistingSet.size();
sort(mutationSet.begin(), mutationSet.end(), byZCoord);//small list -> faster sort
auto mutationIt = mutationSet.begin();
for(auto it = ExistingSet.begin(); it != ExistingSet.end(); ++it){
    if(*it->isRemoved()){
        //release to pool and 
        continue;
    }

    while(mutationIt != mutationSet.end() && *mutationIt->getZ() < *it->getZ()){
        *mutationIt->render();
        newList.pushBack(*mutationIt);

    }
    *it->render();
    newList.pushBack(*iIt);
}
while(mutationIt != mutationSet.end()){
    *mutationIt->render();
    newList.pushBack(*mutationIt);

}

mutationSet.clear();
ExistingSet.clear();
swap(ExistingSet, newList);

You will be doing the iteration anyway and sorting a small list is faster than appending the new list and sorting everything O(n + k + k log k) vs. O( (n+k)log(n+k))

Upvotes: 0

Jarod42
Jarod42

Reputation: 217293

std::list is a non-random-access container,

Complexity of lower_bound.

On average, logarithmic in the distance between first and last: Performs approximately log2(N)+1 element comparisons (where N is this distance). On non-random-access iterators, the iterator advances produce themselves an additional linear complexity in N on average

So it seems not a good idea.

Using std::vector, you will have correct complexity for lower_bound.

And you may have better performance too for inserting/removing element(but lower complexity).

Upvotes: 0

Related Questions