mirror of
https://github.com/overte-org/overte.git
synced 2025-04-15 22:07:34 +02:00
Merge pull request #5103 from samcake/punk
TEAM TEACHING : Repair the Overlay 3D rendering and introduce the concept of layer in the scene
This commit is contained in:
commit
a1ee339765
8 changed files with 115 additions and 106 deletions
|
@ -304,7 +304,8 @@ SelectionDisplay = (function () {
|
|||
visible: false,
|
||||
dashed: true,
|
||||
lineWidth: 2.0,
|
||||
ignoreRayIntersection: true // this never ray intersects
|
||||
ignoreRayIntersection: true, // this never ray intersects
|
||||
drawInFront: true
|
||||
});
|
||||
|
||||
var selectionBox = Overlays.addOverlay("cube", {
|
||||
|
|
|
@ -123,6 +123,7 @@ protected:
|
|||
namespace render {
|
||||
template <> const ItemKey payloadGetKey(const Overlay::Pointer& overlay);
|
||||
template <> const Item::Bound payloadGetBound(const Overlay::Pointer& overlay);
|
||||
template <> int payloadGetLayer(const Overlay::Pointer& overlay);
|
||||
template <> void payloadRender(const Overlay::Pointer& overlay, RenderArgs* args);
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace render {
|
|||
template <> const ItemKey payloadGetKey(const Overlay::Pointer& overlay) {
|
||||
if (overlay->is3D() && !static_cast<Base3DOverlay*>(overlay.get())->getDrawOnHUD()) {
|
||||
if (static_cast<Base3DOverlay*>(overlay.get())->getDrawInFront()) {
|
||||
return ItemKey::Builder().withTypeShape().withNoDepthSort().build();
|
||||
return ItemKey::Builder().withTypeShape().withLayered().build();
|
||||
} else {
|
||||
return ItemKey::Builder::opaqueShape();
|
||||
}
|
||||
|
@ -53,6 +53,17 @@ namespace render {
|
|||
return AABox(glm::vec3(bounds.x(), bounds.y(), 0.0f), glm::vec3(bounds.width(), bounds.height(), 0.1f));
|
||||
}
|
||||
}
|
||||
template <> int payloadGetLayer(const Overlay::Pointer& overlay) {
|
||||
// MAgic number while we are defining the layering mechanism:
|
||||
const int LAYER_2D = 2;
|
||||
const int LAYER_3D_FRONT = 1;
|
||||
const int LAYER_3D = 0;
|
||||
if (overlay->is3D()) {
|
||||
return (static_cast<Base3DOverlay*>(overlay.get())->getDrawInFront() ? LAYER_3D_FRONT : LAYER_3D);
|
||||
} else {
|
||||
return LAYER_2D;
|
||||
}
|
||||
}
|
||||
template <> void payloadRender(const Overlay::Pointer& overlay, RenderArgs* args) {
|
||||
if (args) {
|
||||
glPushMatrix();
|
||||
|
|
|
@ -50,6 +50,7 @@ RenderDeferredTask::RenderDeferredTask() : Task() {
|
|||
_jobs.push_back(Job(RenderDeferred()));
|
||||
_jobs.push_back(Job(ResolveDeferred()));
|
||||
_jobs.push_back(Job(DrawTransparentDeferred()));
|
||||
_jobs.push_back(Job(DrawPostLayered()));
|
||||
_jobs.push_back(Job(ResetGLState()));
|
||||
}
|
||||
|
||||
|
@ -85,7 +86,7 @@ template <> void render::jobRun(const DrawOpaqueDeferred& job, const SceneContex
|
|||
|
||||
// render opaques
|
||||
auto& scene = sceneContext->_scene;
|
||||
auto& items = scene->getMasterBucket().at(ItemFilter::Builder::opaqueShape());
|
||||
auto& items = scene->getMasterBucket().at(ItemFilter::Builder::opaqueShape().withoutLayered());
|
||||
auto& renderDetails = renderContext->args->_details;
|
||||
|
||||
ItemIDsBounds inItems;
|
||||
|
@ -163,7 +164,7 @@ template <> void render::jobRun(const DrawTransparentDeferred& job, const SceneC
|
|||
|
||||
// render transparents
|
||||
auto& scene = sceneContext->_scene;
|
||||
auto& items = scene->getMasterBucket().at(ItemFilter::Builder::transparentShape());
|
||||
auto& items = scene->getMasterBucket().at(ItemFilter::Builder::transparentShape().withoutLayered());
|
||||
auto& renderDetails = renderContext->args->_details;
|
||||
|
||||
ItemIDsBounds inItems;
|
||||
|
|
|
@ -192,7 +192,6 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void addClearStateCommands(gpu::Batch& batch) {
|
||||
batch._glDepthMask(true);
|
||||
batch._glDepthFunc(GL_LESS);
|
||||
|
@ -446,6 +445,48 @@ template <> void render::jobRun(const DrawBackground& job, const SceneContextPoi
|
|||
args->_context->syncCache();
|
||||
}
|
||||
|
||||
template <> void render::jobRun(const DrawPostLayered& job, const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) {
|
||||
PerformanceTimer perfTimer("DrawPostLayered");
|
||||
assert(renderContext->args);
|
||||
assert(renderContext->args->_viewFrustum);
|
||||
|
||||
// render backgrounds
|
||||
auto& scene = sceneContext->_scene;
|
||||
auto& items = scene->getMasterBucket().at(ItemFilter::Builder::opaqueShape().withLayered());
|
||||
|
||||
|
||||
ItemIDsBounds inItems;
|
||||
inItems.reserve(items.size());
|
||||
for (auto id : items) {
|
||||
auto& item = scene->getItem(id);
|
||||
if (item.getKey().isVisible() && (item.getLayer() > 0)) {
|
||||
inItems.emplace_back(id);
|
||||
}
|
||||
}
|
||||
if (inItems.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RenderArgs* args = renderContext->args;
|
||||
gpu::Batch batch;
|
||||
args->_batch = &batch;
|
||||
|
||||
glm::mat4 projMat;
|
||||
Transform viewMat;
|
||||
args->_viewFrustum->evalProjectionMatrix(projMat);
|
||||
args->_viewFrustum->evalViewTransform(viewMat);
|
||||
batch.setProjectionTransform(projMat);
|
||||
batch.setViewTransform(viewMat);
|
||||
|
||||
batch.clearFramebuffer(gpu::Framebuffer::BUFFER_DEPTH, glm::vec4(), 1.f, 0);
|
||||
|
||||
renderItems(sceneContext, renderContext, inItems);
|
||||
args->_context->render((*args->_batch));
|
||||
args->_batch = nullptr;
|
||||
|
||||
// Force the context sync
|
||||
args->_context->syncCache();
|
||||
}
|
||||
|
||||
|
||||
void ItemMaterialBucketMap::insert(const ItemID& id, const model::MaterialKey& key) {
|
||||
|
|
|
@ -87,6 +87,14 @@ public:
|
|||
};
|
||||
template <> void jobRun(const DrawBackground& job, const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext);
|
||||
|
||||
|
||||
class DrawPostLayered {
|
||||
public:
|
||||
};
|
||||
template <> void jobRun(const DrawPostLayered& job, const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext);
|
||||
|
||||
|
||||
|
||||
class ResetGLState {
|
||||
public:
|
||||
};
|
||||
|
|
|
@ -45,10 +45,12 @@ void ItemBucketMap::reset(const ItemID& id, const ItemKey& oldKey, const ItemKey
|
|||
}
|
||||
|
||||
void ItemBucketMap::allocateStandardOpaqueTranparentBuckets() {
|
||||
(*this)[ItemFilter::Builder::opaqueShape()];
|
||||
(*this)[ItemFilter::Builder::transparentShape()];
|
||||
(*this)[ItemFilter::Builder::opaqueShape().withoutLayered()];
|
||||
(*this)[ItemFilter::Builder::transparentShape().withoutLayered()];
|
||||
(*this)[ItemFilter::Builder::light()];
|
||||
(*this)[ItemFilter::Builder::background()];
|
||||
(*this)[ItemFilter::Builder::opaqueShape().withLayered()];
|
||||
(*this)[ItemFilter::Builder::transparentShape().withLayered()];
|
||||
}
|
||||
|
||||
|
||||
|
@ -61,11 +63,6 @@ void Item::resetPayload(const PayloadPointer& payload) {
|
|||
}
|
||||
}
|
||||
|
||||
void Item::kill() {
|
||||
_payload.reset();
|
||||
_key._flags.reset();
|
||||
}
|
||||
|
||||
void PendingChanges::resetItem(ItemID id, const PayloadPointer& payload) {
|
||||
_resetItems.push_back(id);
|
||||
_resetPayloads.push_back(payload);
|
||||
|
@ -164,27 +161,3 @@ void Scene::updateItems(const ItemIDs& ids, UpdateFunctors& functors) {
|
|||
_items[(*updateID)].update((*updateFunctor));
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::registerObserver(ObserverPointer& observer) {
|
||||
// make sure it's a valid observer
|
||||
if (observer && (observer->getScene() == nullptr)) {
|
||||
// Then register the observer
|
||||
_observers.push_back(observer);
|
||||
|
||||
// And let it do what it wants to do
|
||||
observer->registerScene(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::unregisterObserver(ObserverPointer& observer) {
|
||||
// make sure it's a valid observer currently registered
|
||||
if (observer && (observer->getScene() == this)) {
|
||||
// let it do what it wants to do
|
||||
observer->unregisterScene();
|
||||
|
||||
// Then unregister the observer
|
||||
auto it = std::find(_observers.begin(), _observers.end(), observer);
|
||||
_observers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ public:
|
|||
enum FlagBit {
|
||||
TYPE_SHAPE = 0, // Item is a Shape
|
||||
TYPE_LIGHT, // Item is a Light
|
||||
TYPE_BACKGROUND, // Item is a Background
|
||||
TRANSLUCENT, // Transparent and not opaque, for some odd reason TRANSPARENCY doesn't work...
|
||||
VIEW_SPACE, // Transformed in view space, and not in world space
|
||||
DYNAMIC, // Dynamic and bound will change unlike static item
|
||||
|
@ -44,7 +43,7 @@ public:
|
|||
INVISIBLE, // Visible or not? could be just here to cast shadow
|
||||
SHADOW_CASTER, // Item cast shadows
|
||||
PICKABLE, // Item can be picked/selected
|
||||
NO_DEPTH_SORT, // Item should not be depth sorted
|
||||
LAYERED, // Item belongs to one of the layers different from the default layer
|
||||
|
||||
NUM_FLAGS, // Not a valid flag
|
||||
};
|
||||
|
@ -57,6 +56,7 @@ public:
|
|||
ItemKey(const Flags& flags) : _flags(flags) {}
|
||||
|
||||
class Builder {
|
||||
friend class ItemKey;
|
||||
Flags _flags{ 0 };
|
||||
public:
|
||||
Builder() {}
|
||||
|
@ -65,7 +65,6 @@ public:
|
|||
|
||||
Builder& withTypeShape() { _flags.set(TYPE_SHAPE); return (*this); }
|
||||
Builder& withTypeLight() { _flags.set(TYPE_LIGHT); return (*this); }
|
||||
Builder& withTypeBackground() { _flags.set(TYPE_BACKGROUND); return (*this); }
|
||||
Builder& withTransparent() { _flags.set(TRANSLUCENT); return (*this); }
|
||||
Builder& withViewSpace() { _flags.set(VIEW_SPACE); return (*this); }
|
||||
Builder& withDynamic() { _flags.set(DYNAMIC); return (*this); }
|
||||
|
@ -73,14 +72,15 @@ public:
|
|||
Builder& withInvisible() { _flags.set(INVISIBLE); return (*this); }
|
||||
Builder& withShadowCaster() { _flags.set(SHADOW_CASTER); return (*this); }
|
||||
Builder& withPickable() { _flags.set(PICKABLE); return (*this); }
|
||||
Builder& withNoDepthSort() { _flags.set(NO_DEPTH_SORT); return (*this); }
|
||||
Builder& withLayered() { _flags.set(LAYERED); return (*this); }
|
||||
|
||||
// Convenient standard keys that we will keep on using all over the place
|
||||
static ItemKey opaqueShape() { return Builder().withTypeShape().build(); }
|
||||
static ItemKey transparentShape() { return Builder().withTypeShape().withTransparent().build(); }
|
||||
static ItemKey light() { return Builder().withTypeLight().build(); }
|
||||
static ItemKey background() { return Builder().withTypeBackground().build(); }
|
||||
static Builder opaqueShape() { return Builder().withTypeShape(); }
|
||||
static Builder transparentShape() { return Builder().withTypeShape().withTransparent(); }
|
||||
static Builder light() { return Builder().withTypeLight(); }
|
||||
static Builder background() { return Builder().withViewSpace().withLayered(); }
|
||||
};
|
||||
ItemKey(const Builder& builder) : ItemKey(builder._flags) {}
|
||||
|
||||
bool isOpaque() const { return !_flags[TRANSLUCENT]; }
|
||||
bool isTransparent() const { return _flags[TRANSLUCENT]; }
|
||||
|
@ -101,8 +101,7 @@ public:
|
|||
|
||||
bool isPickable() const { return _flags[PICKABLE]; }
|
||||
|
||||
bool isDepthSort() const { return !_flags[NO_DEPTH_SORT]; }
|
||||
bool isNoDepthSort() const { return _flags[NO_DEPTH_SORT]; }
|
||||
bool isLayered() const { return _flags[LAYERED]; }
|
||||
};
|
||||
|
||||
inline QDebug operator<<(QDebug debug, const ItemKey& itemKey) {
|
||||
|
@ -122,6 +121,7 @@ public:
|
|||
ItemFilter(const ItemKey::Flags& value = ItemKey::Flags(0), const ItemKey::Flags& mask = ItemKey::Flags(0)) : _value(value), _mask(mask) {}
|
||||
|
||||
class Builder {
|
||||
friend class ItemFilter;
|
||||
ItemKey::Flags _value{ 0 };
|
||||
ItemKey::Flags _mask{ 0 };
|
||||
public:
|
||||
|
@ -131,7 +131,6 @@ public:
|
|||
|
||||
Builder& withTypeShape() { _value.set(ItemKey::TYPE_SHAPE); _mask.set(ItemKey::TYPE_SHAPE); return (*this); }
|
||||
Builder& withTypeLight() { _value.set(ItemKey::TYPE_LIGHT); _mask.set(ItemKey::TYPE_LIGHT); return (*this); }
|
||||
Builder& withTypeBackground() { _value.set(ItemKey::TYPE_BACKGROUND); _mask.set(ItemKey::TYPE_BACKGROUND); return (*this); }
|
||||
|
||||
Builder& withOpaque() { _value.reset(ItemKey::TRANSLUCENT); _mask.set(ItemKey::TRANSLUCENT); return (*this); }
|
||||
Builder& withTransparent() { _value.set(ItemKey::TRANSLUCENT); _mask.set(ItemKey::TRANSLUCENT); return (*this); }
|
||||
|
@ -153,16 +152,18 @@ public:
|
|||
|
||||
Builder& withPickable() { _value.set(ItemKey::PICKABLE); _mask.set(ItemKey::PICKABLE); return (*this); }
|
||||
|
||||
Builder& withDepthSort() { _value.reset(ItemKey::NO_DEPTH_SORT); _mask.set(ItemKey::NO_DEPTH_SORT); return (*this); }
|
||||
Builder& withNotDepthSort() { _value.set(ItemKey::NO_DEPTH_SORT); _mask.set(ItemKey::NO_DEPTH_SORT); return (*this); }
|
||||
Builder& withoutLayered() { _value.reset(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); }
|
||||
Builder& withLayered() { _value.set(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); }
|
||||
|
||||
// Convenient standard keys that we will keep on using all over the place
|
||||
static ItemFilter opaqueShape() { return Builder().withTypeShape().withOpaque().withWorldSpace().build(); }
|
||||
static ItemFilter transparentShape() { return Builder().withTypeShape().withTransparent().withWorldSpace().build(); }
|
||||
static ItemFilter light() { return Builder().withTypeLight().build(); }
|
||||
static ItemFilter background() { return Builder().withTypeBackground().build(); }
|
||||
static Builder opaqueShape() { return Builder().withTypeShape().withOpaque().withWorldSpace(); }
|
||||
static Builder transparentShape() { return Builder().withTypeShape().withTransparent().withWorldSpace(); }
|
||||
static Builder light() { return Builder().withTypeLight(); }
|
||||
static Builder background() { return Builder().withViewSpace().withLayered(); }
|
||||
};
|
||||
|
||||
ItemFilter(const Builder& builder) : ItemFilter(builder._value, builder._mask) {}
|
||||
|
||||
// Item Filter operator testing if a key pass the filter
|
||||
bool test(const ItemKey& key) const { return (key._flags & _mask) == (_value & _mask); }
|
||||
|
||||
|
@ -179,7 +180,7 @@ public:
|
|||
};
|
||||
|
||||
inline QDebug operator<<(QDebug debug, const ItemFilter& me) {
|
||||
debug << "[ItemFilter: opaqueShape:" << me.test(ItemKey::Builder::opaqueShape())
|
||||
debug << "[ItemFilter: opaqueShape:" << me.test(ItemKey::Builder::opaqueShape().build())
|
||||
<< "]";
|
||||
return debug;
|
||||
}
|
||||
|
@ -214,35 +215,40 @@ public:
|
|||
public:
|
||||
virtual const ItemKey getKey() const = 0;
|
||||
virtual const Bound getBound() const = 0;
|
||||
virtual void render(RenderArgs* args) = 0;
|
||||
virtual int getLayer() const = 0;
|
||||
|
||||
virtual void update(const UpdateFunctorPointer& functor) = 0;
|
||||
virtual void render(RenderArgs* args) = 0;
|
||||
|
||||
virtual const model::MaterialKey getMaterialKey() const = 0;
|
||||
|
||||
~PayloadInterface() {}
|
||||
protected:
|
||||
friend class Item;
|
||||
virtual void update(const UpdateFunctorPointer& functor) = 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
typedef std::shared_ptr<PayloadInterface> PayloadPointer;
|
||||
|
||||
|
||||
|
||||
Item() {}
|
||||
~Item() {}
|
||||
|
||||
// Main scene / item managment interface Reset/Update/Kill
|
||||
void resetPayload(const PayloadPointer& payload);
|
||||
void kill();
|
||||
void update(const UpdateFunctorPointer& updateFunctor) { _payload->update(updateFunctor); } // Communicate update to the payload
|
||||
void kill() { _payload.reset(); _key._flags.reset(); } // Kill means forget the payload and key
|
||||
|
||||
// Check heuristic key
|
||||
const ItemKey& getKey() const { return _key; }
|
||||
|
||||
// Payload Interface
|
||||
|
||||
// Get the bound of the item expressed in world space (or eye space depending on the key.isWorldSpace())
|
||||
const Bound getBound() const { return _payload->getBound(); }
|
||||
|
||||
// Get the layer where the item belongs. 0 by default meaning NOT LAYERED
|
||||
int getLayer() const { return _payload->getLayer(); }
|
||||
|
||||
// Render call for the item
|
||||
void render(RenderArgs* args) { _payload->render(args); }
|
||||
void update(const UpdateFunctorPointer& updateFunctor) { _payload->update(updateFunctor); }
|
||||
|
||||
// Shape Type Interface
|
||||
const model::MaterialKey getMaterialKey() const { return _payload->getMaterialKey(); }
|
||||
|
@ -280,6 +286,7 @@ inline QDebug operator<<(QDebug debug, const Item& item) {
|
|||
// of the Payload interface
|
||||
template <class T> const ItemKey payloadGetKey(const std::shared_ptr<T>& payloadData) { return ItemKey(); }
|
||||
template <class T> const Item::Bound payloadGetBound(const std::shared_ptr<T>& payloadData) { return Item::Bound(); }
|
||||
template <class T> int payloadGetLayer(const std::shared_ptr<T>& payloadData) { return 0; }
|
||||
template <class T> void payloadRender(const std::shared_ptr<T>& payloadData, RenderArgs* args) { }
|
||||
|
||||
// Shape type interface
|
||||
|
@ -290,19 +297,25 @@ public:
|
|||
typedef std::shared_ptr<T> DataPointer;
|
||||
typedef UpdateFunctor<T> Updater;
|
||||
|
||||
virtual void update(const UpdateFunctorPointer& functor) { static_cast<Updater*>(functor.get())->_func((*_data)); }
|
||||
Payload(const DataPointer& data) : _data(data) {}
|
||||
|
||||
// Payload general interface
|
||||
virtual const ItemKey getKey() const { return payloadGetKey<T>(_data); }
|
||||
virtual const Item::Bound getBound() const { return payloadGetBound<T>(_data); }
|
||||
virtual int getLayer() const { return payloadGetLayer<T>(_data); }
|
||||
|
||||
|
||||
virtual void render(RenderArgs* args) { payloadRender<T>(_data, args); }
|
||||
|
||||
// Shape Type interface
|
||||
virtual const model::MaterialKey getMaterialKey() const { return shapeGetMaterialKey<T>(_data); }
|
||||
|
||||
Payload(const DataPointer& data) : _data(data) {}
|
||||
protected:
|
||||
DataPointer _data;
|
||||
|
||||
// Update mechanics
|
||||
virtual void update(const UpdateFunctorPointer& functor) { static_cast<Updater*>(functor.get())->_func((*_data)); }
|
||||
friend class Item;
|
||||
};
|
||||
|
||||
// Let's show how to make a simple FooPayload example:
|
||||
|
@ -358,6 +371,7 @@ public:
|
|||
|
||||
typedef std::vector< ItemIDAndBounds > ItemIDsBounds;
|
||||
|
||||
|
||||
// A map of ItemIDSets allowing to create bucket lists of items which are filtering correctly
|
||||
class ItemBucketMap : public std::map<ItemFilter, ItemIDSet, ItemFilter::Less> {
|
||||
public:
|
||||
|
@ -374,8 +388,6 @@ public:
|
|||
};
|
||||
|
||||
class Engine;
|
||||
class Observer;
|
||||
|
||||
|
||||
class PendingChanges {
|
||||
public:
|
||||
|
@ -411,36 +423,6 @@ typedef std::queue<PendingChanges> PendingChangesQueue;
|
|||
// Items are notified accordingly on any update message happening
|
||||
class Scene {
|
||||
public:
|
||||
|
||||
class Observer {
|
||||
public:
|
||||
Observer(Scene* scene) {
|
||||
|
||||
}
|
||||
~Observer() {}
|
||||
|
||||
const Scene* getScene() const { return _scene; }
|
||||
Scene* editScene() { return _scene; }
|
||||
|
||||
protected:
|
||||
Scene* _scene = nullptr;
|
||||
virtual void onRegisterScene() {}
|
||||
virtual void onUnregisterScene() {}
|
||||
|
||||
friend class Scene;
|
||||
void registerScene(Scene* scene) {
|
||||
_scene = scene;
|
||||
onRegisterScene();
|
||||
}
|
||||
|
||||
void unregisterScene() {
|
||||
onUnregisterScene();
|
||||
_scene = 0;
|
||||
}
|
||||
};
|
||||
typedef std::shared_ptr< Observer > ObserverPointer;
|
||||
typedef std::vector< ObserverPointer > Observers;
|
||||
|
||||
Scene();
|
||||
~Scene() {}
|
||||
|
||||
|
@ -450,11 +432,6 @@ public:
|
|||
/// Enqueue change batch to the scene
|
||||
void enqueuePendingChanges(const PendingChanges& pendingChanges);
|
||||
|
||||
/// Scene Observer listen to any change and get notified
|
||||
void registerObserver(ObserverPointer& observer);
|
||||
void unregisterObserver(ObserverPointer& observer);
|
||||
|
||||
|
||||
/// Access the main bucketmap of items
|
||||
const ItemBucketMap& getMasterBucket() const { return _masterBucketMap; }
|
||||
|
||||
|
@ -483,10 +460,6 @@ protected:
|
|||
void removeItems(const ItemIDs& ids);
|
||||
void updateItems(const ItemIDs& ids, UpdateFunctors& functors);
|
||||
|
||||
|
||||
// The scene context listening for any change to the database
|
||||
Observers _observers;
|
||||
|
||||
friend class Engine;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue