mirror of
https://github.com/lubosz/overte.git
synced 2025-04-23 23:33:48 +02:00
Lots of work on metavoxel LODs and spanners.
This commit is contained in:
parent
7a182ed659
commit
63b4a2453b
10 changed files with 355 additions and 171 deletions
|
@ -111,13 +111,17 @@ int MetavoxelSession::parseData(const QByteArray& packet) {
|
|||
}
|
||||
|
||||
void MetavoxelSession::sendDelta() {
|
||||
// wait until we have a valid lod
|
||||
if (!_lod.isValid()) {
|
||||
return;
|
||||
}
|
||||
Bitstream& out = _sequencer.startPacket();
|
||||
out << QVariant::fromValue(MetavoxelDeltaMessage());
|
||||
_server->getData().writeDelta(_sendRecords.first().data, out);
|
||||
_server->getData().writeDelta(_sendRecords.first().data, _sendRecords.first().lod, out, _lod);
|
||||
_sequencer.endPacket();
|
||||
|
||||
// record the send
|
||||
SendRecord record = { _sequencer.getOutgoingPacketNumber(), _server->getData() };
|
||||
SendRecord record = { _sequencer.getOutgoingPacketNumber(), _server->getData(), _lod };
|
||||
_sendRecords.append(record);
|
||||
}
|
||||
|
||||
|
@ -139,7 +143,7 @@ void MetavoxelSession::handleMessage(const QVariant& message) {
|
|||
int userType = message.userType();
|
||||
if (userType == ClientStateMessage::Type) {
|
||||
ClientStateMessage state = message.value<ClientStateMessage>();
|
||||
_position = state.position;
|
||||
_lod = state.lod;
|
||||
|
||||
} else if (userType == MetavoxelEditMessage::Type) {
|
||||
_server->applyEdit(message.value<MetavoxelEditMessage>());
|
||||
|
|
|
@ -78,6 +78,7 @@ private:
|
|||
public:
|
||||
int packetNumber;
|
||||
MetavoxelData data;
|
||||
MetavoxelLOD lod;
|
||||
};
|
||||
|
||||
MetavoxelServer* _server;
|
||||
|
@ -86,7 +87,7 @@ private:
|
|||
|
||||
SharedNodePointer _node;
|
||||
|
||||
glm::vec3 _position;
|
||||
MetavoxelLOD _lod;
|
||||
|
||||
QList<SendRecord> _sendRecords;
|
||||
};
|
||||
|
|
|
@ -182,11 +182,16 @@ MetavoxelClient::MetavoxelClient(const SharedNodePointer& node) :
|
|||
|
||||
connect(&_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendData(const QByteArray&)));
|
||||
connect(&_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readPacket(Bitstream&)));
|
||||
connect(&_sequencer, SIGNAL(sendAcknowledged(int)), SLOT(clearSendRecordsBefore(int)));
|
||||
connect(&_sequencer, SIGNAL(receiveAcknowledged(int)), SLOT(clearReceiveRecordsBefore(int)));
|
||||
|
||||
// insert the baseline send record
|
||||
SendRecord sendRecord = { 0 };
|
||||
_sendRecords.append(sendRecord);
|
||||
|
||||
// insert the baseline receive record
|
||||
ReceiveRecord record = { 0, _data };
|
||||
_receiveRecords.append(record);
|
||||
ReceiveRecord receiveRecord = { 0, _data };
|
||||
_receiveRecords.append(receiveRecord);
|
||||
}
|
||||
|
||||
MetavoxelClient::~MetavoxelClient() {
|
||||
|
@ -206,9 +211,14 @@ void MetavoxelClient::applyEdit(const MetavoxelEditMessage& edit) {
|
|||
|
||||
void MetavoxelClient::simulate(float deltaTime) {
|
||||
Bitstream& out = _sequencer.startPacket();
|
||||
ClientStateMessage state = { Application::getInstance()->getCamera()->getPosition() };
|
||||
const float FIXED_LOD_THRESHOLD = 0.0001f;
|
||||
ClientStateMessage state = { MetavoxelLOD(Application::getInstance()->getCamera()->getPosition(), FIXED_LOD_THRESHOLD) };
|
||||
out << QVariant::fromValue(state);
|
||||
_sequencer.endPacket();
|
||||
|
||||
// record the send
|
||||
SendRecord record = { _sequencer.getOutgoingPacketNumber(), state.lod };
|
||||
_sendRecords.append(record);
|
||||
}
|
||||
|
||||
int MetavoxelClient::parseData(const QByteArray& packet) {
|
||||
|
@ -227,7 +237,7 @@ void MetavoxelClient::readPacket(Bitstream& in) {
|
|||
handleMessage(message, in);
|
||||
|
||||
// record the receipt
|
||||
ReceiveRecord record = { _sequencer.getIncomingPacketNumber(), _data };
|
||||
ReceiveRecord record = { _sequencer.getIncomingPacketNumber(), _data, _sendRecords.first().lod };
|
||||
_receiveRecords.append(record);
|
||||
|
||||
// reapply local edits
|
||||
|
@ -238,6 +248,10 @@ void MetavoxelClient::readPacket(Bitstream& in) {
|
|||
}
|
||||
}
|
||||
|
||||
void MetavoxelClient::clearSendRecordsBefore(int index) {
|
||||
_sendRecords.erase(_sendRecords.begin(), _sendRecords.begin() + index + 1);
|
||||
}
|
||||
|
||||
void MetavoxelClient::clearReceiveRecordsBefore(int index) {
|
||||
_receiveRecords.erase(_receiveRecords.begin(), _receiveRecords.begin() + index + 1);
|
||||
}
|
||||
|
@ -245,7 +259,7 @@ void MetavoxelClient::clearReceiveRecordsBefore(int index) {
|
|||
void MetavoxelClient::handleMessage(const QVariant& message, Bitstream& in) {
|
||||
int userType = message.userType();
|
||||
if (userType == MetavoxelDeltaMessage::Type) {
|
||||
_data.readDelta(_receiveRecords.first().data, in);
|
||||
_data.readDelta(_receiveRecords.first().data, _receiveRecords.first().lod, in, _sendRecords.first().lod);
|
||||
|
||||
} else if (userType == QMetaType::QVariantList) {
|
||||
foreach (const QVariant& element, message.toList()) {
|
||||
|
|
|
@ -103,16 +103,25 @@ private slots:
|
|||
|
||||
void readPacket(Bitstream& in);
|
||||
|
||||
void clearSendRecordsBefore(int index);
|
||||
|
||||
void clearReceiveRecordsBefore(int index);
|
||||
|
||||
private:
|
||||
|
||||
void handleMessage(const QVariant& message, Bitstream& in);
|
||||
|
||||
class SendRecord {
|
||||
public:
|
||||
int packetNumber;
|
||||
MetavoxelLOD lod;
|
||||
};
|
||||
|
||||
class ReceiveRecord {
|
||||
public:
|
||||
int packetNumber;
|
||||
MetavoxelData data;
|
||||
MetavoxelLOD lod;
|
||||
};
|
||||
|
||||
SharedNodePointer _node;
|
||||
|
@ -121,6 +130,7 @@ private:
|
|||
|
||||
MetavoxelData _data;
|
||||
|
||||
QList<SendRecord> _sendRecords;
|
||||
QList<ReceiveRecord> _receiveRecords;
|
||||
};
|
||||
|
||||
|
|
|
@ -49,6 +49,9 @@ void AttributeRegistry::configureScriptEngine(QScriptEngine* engine) {
|
|||
}
|
||||
|
||||
AttributePointer AttributeRegistry::registerAttribute(AttributePointer attribute) {
|
||||
if (!attribute) {
|
||||
return attribute;
|
||||
}
|
||||
AttributePointer& pointer = _attributes[attribute->getName()];
|
||||
if (!pointer) {
|
||||
pointer = attribute;
|
||||
|
@ -146,20 +149,20 @@ Attribute::Attribute(const QString& name) {
|
|||
Attribute::~Attribute() {
|
||||
}
|
||||
|
||||
void Attribute::read(MetavoxelNode& root, Bitstream& in) {
|
||||
root.read(this, in);
|
||||
void Attribute::read(MetavoxelData& data, MetavoxelStreamState& state) {
|
||||
data.createRoot(state.attribute)->read(state);
|
||||
}
|
||||
|
||||
void Attribute::write(const MetavoxelNode& root, Bitstream& out) {
|
||||
root.write(this, out);
|
||||
void Attribute::write(const MetavoxelNode& root, MetavoxelStreamState& state) {
|
||||
root.write(state);
|
||||
}
|
||||
|
||||
void Attribute::readDelta(MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& in) {
|
||||
root.readDelta(this, reference, in);
|
||||
void Attribute::readDelta(MetavoxelData& data, const MetavoxelNode& reference, MetavoxelStreamState& state) {
|
||||
data.createRoot(state.attribute)->readDelta(reference, state);
|
||||
}
|
||||
|
||||
void Attribute::writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& out) {
|
||||
root.writeDelta(this, reference, out);
|
||||
void Attribute::writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, MetavoxelStreamState& state) {
|
||||
root.writeDelta(reference, state);
|
||||
}
|
||||
|
||||
QRgbAttribute::QRgbAttribute(const QString& name, QRgb defaultValue) :
|
||||
|
@ -277,18 +280,36 @@ SpannerSetAttribute::SpannerSetAttribute(const QString& name, const QMetaObject*
|
|||
SharedObjectSetAttribute(name, metaObject) {
|
||||
}
|
||||
|
||||
void SpannerSetAttribute::read(MetavoxelNode& root, Bitstream& in) {
|
||||
root.read(this, in);
|
||||
void SpannerSetAttribute::read(MetavoxelData& data, MetavoxelStreamState& state) {
|
||||
forever {
|
||||
SharedObjectPointer object;
|
||||
state.stream >> object;
|
||||
if (!object) {
|
||||
break;
|
||||
}
|
||||
data.insert(state.attribute, object);
|
||||
}
|
||||
}
|
||||
|
||||
void SpannerSetAttribute::write(const MetavoxelNode& root, Bitstream& out) {
|
||||
root.write(this, out);
|
||||
void SpannerSetAttribute::write(const MetavoxelNode& root, MetavoxelStreamState& state) {
|
||||
Spanner::incrementVisit();
|
||||
root.writeSpanners(state);
|
||||
state.stream << SharedObjectPointer();
|
||||
}
|
||||
|
||||
void SpannerSetAttribute::readDelta(MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& in) {
|
||||
root.readDelta(this, reference, in);
|
||||
void SpannerSetAttribute::readDelta(MetavoxelData& data, const MetavoxelNode& reference, MetavoxelStreamState& state) {
|
||||
forever {
|
||||
SharedObjectPointer object;
|
||||
state.stream >> object;
|
||||
if (!object) {
|
||||
break;
|
||||
}
|
||||
data.toggle(state.attribute, object);
|
||||
}
|
||||
}
|
||||
|
||||
void SpannerSetAttribute::writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& out) {
|
||||
root.writeDelta(this, reference, out);
|
||||
void SpannerSetAttribute::writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, MetavoxelStreamState& state) {
|
||||
Spanner::incrementVisit();
|
||||
root.writeSpannerDelta(reference, state);
|
||||
state.stream << SharedObjectPointer();
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@ class QScriptEngine;
|
|||
class QScriptValue;
|
||||
|
||||
class Attribute;
|
||||
class MetavoxelData;
|
||||
class MetavoxelNode;
|
||||
class MetavoxelStreamState;
|
||||
|
||||
typedef SharedObjectPointerTemplate<Attribute> AttributePointer;
|
||||
|
||||
|
@ -171,11 +173,11 @@ public:
|
|||
virtual void readDelta(Bitstream& in, void*& value, void* reference, bool isLeaf) const { read(in, value, isLeaf); }
|
||||
virtual void writeDelta(Bitstream& out, void* value, void* reference, bool isLeaf) const { write(out, value, isLeaf); }
|
||||
|
||||
virtual void read(MetavoxelNode& root, Bitstream& in);
|
||||
virtual void write(const MetavoxelNode& root, Bitstream& out);
|
||||
virtual void read(MetavoxelData& data, MetavoxelStreamState& state);
|
||||
virtual void write(const MetavoxelNode& root, MetavoxelStreamState& state);
|
||||
|
||||
virtual void readDelta(MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& in);
|
||||
virtual void writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& out);
|
||||
virtual void readDelta(MetavoxelData& data, const MetavoxelNode& reference, MetavoxelStreamState& state);
|
||||
virtual void writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, MetavoxelStreamState& state);
|
||||
|
||||
virtual bool equal(void* first, void* second) const = 0;
|
||||
|
||||
|
@ -325,11 +327,11 @@ public:
|
|||
Q_INVOKABLE SpannerSetAttribute(const QString& name = QString(),
|
||||
const QMetaObject* metaObject = &SharedObject::staticMetaObject);
|
||||
|
||||
virtual void read(MetavoxelNode& root, Bitstream& in);
|
||||
virtual void write(const MetavoxelNode& root, Bitstream& out);
|
||||
virtual void read(MetavoxelData& data, MetavoxelStreamState& state);
|
||||
virtual void write(const MetavoxelNode& root, MetavoxelStreamState& state);
|
||||
|
||||
virtual void readDelta(MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& in);
|
||||
virtual void writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, Bitstream& out);
|
||||
virtual void readDelta(MetavoxelData& data, const MetavoxelNode& reference, MetavoxelStreamState& state);
|
||||
virtual void writeDelta(const MetavoxelNode& root, const MetavoxelNode& reference, MetavoxelStreamState& state);
|
||||
};
|
||||
|
||||
#endif /* defined(__interface__AttributeRegistry__) */
|
||||
|
|
|
@ -27,6 +27,10 @@ MetavoxelLOD::MetavoxelLOD(const glm::vec3& position, float threshold) :
|
|||
threshold(threshold) {
|
||||
}
|
||||
|
||||
bool MetavoxelLOD::shouldSubdivide(const glm::vec3& minimum, float size) const {
|
||||
return size >= glm::distance(position, minimum + glm::vec3(size, size, size) * 0.5f) * threshold;
|
||||
}
|
||||
|
||||
MetavoxelData::MetavoxelData() : _size(1.0f) {
|
||||
}
|
||||
|
||||
|
@ -62,7 +66,7 @@ void MetavoxelData::guide(MetavoxelVisitor& visitor) {
|
|||
const QVector<AttributePointer>& inputs = visitor.getInputs();
|
||||
const QVector<AttributePointer>& outputs = visitor.getOutputs();
|
||||
MetavoxelVisitation firstVisitation = { NULL, visitor, QVector<MetavoxelNode*>(inputs.size() + 1),
|
||||
QVector<MetavoxelNode*>(outputs.size()), { glm::vec3(_size, _size, _size) * -0.5f, _size,
|
||||
QVector<MetavoxelNode*>(outputs.size()), { getMinimum(), _size,
|
||||
QVector<AttributeValue>(inputs.size() + 1), QVector<OwnedAttributeValue>(outputs.size()) } };
|
||||
for (int i = 0; i < inputs.size(); i++) {
|
||||
MetavoxelNode* node = _roots.value(inputs.at(i));
|
||||
|
@ -98,10 +102,27 @@ void MetavoxelData::guide(MetavoxelVisitor& visitor) {
|
|||
}
|
||||
}
|
||||
|
||||
class InsertVisitor : public MetavoxelVisitor {
|
||||
typedef void (*SpannerUpdateFunction)(SharedObjectSet& set, const SharedObjectPointer& object);
|
||||
|
||||
void insertSpanner(SharedObjectSet& set, const SharedObjectPointer& object) {
|
||||
set.insert(object);
|
||||
}
|
||||
|
||||
void removeSpanner(SharedObjectSet& set, const SharedObjectPointer& object) {
|
||||
set.remove(object);
|
||||
}
|
||||
|
||||
void toggleSpanner(SharedObjectSet& set, const SharedObjectPointer& object) {
|
||||
if (!set.remove(object)) {
|
||||
set.insert(object);
|
||||
}
|
||||
}
|
||||
|
||||
template<SpannerUpdateFunction F> class SpannerUpdateVisitor : public MetavoxelVisitor {
|
||||
public:
|
||||
|
||||
InsertVisitor(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object);
|
||||
SpannerUpdateVisitor(const AttributePointer& attribute, const Box& bounds,
|
||||
float granularity, const SharedObjectPointer& object);
|
||||
|
||||
virtual bool visit(MetavoxelInfo& info);
|
||||
|
||||
|
@ -113,8 +134,8 @@ private:
|
|||
const SharedObjectPointer& _object;
|
||||
};
|
||||
|
||||
InsertVisitor::InsertVisitor(const AttributePointer& attribute, const Box& bounds,
|
||||
float granularity, const SharedObjectPointer& object) :
|
||||
template<SpannerUpdateFunction F> SpannerUpdateVisitor<F>::SpannerUpdateVisitor(const AttributePointer& attribute,
|
||||
const Box& bounds, float granularity, const SharedObjectPointer& object) :
|
||||
MetavoxelVisitor(QVector<AttributePointer>() << attribute, QVector<AttributePointer>() << attribute),
|
||||
_attribute(attribute),
|
||||
_bounds(bounds),
|
||||
|
@ -122,7 +143,7 @@ InsertVisitor::InsertVisitor(const AttributePointer& attribute, const Box& bound
|
|||
_object(object) {
|
||||
}
|
||||
|
||||
bool InsertVisitor::visit(MetavoxelInfo& info) {
|
||||
template<SpannerUpdateFunction F> bool SpannerUpdateVisitor<F>::visit(MetavoxelInfo& info) {
|
||||
if (!info.getBounds().intersects(_bounds)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -130,61 +151,45 @@ bool InsertVisitor::visit(MetavoxelInfo& info) {
|
|||
return true;
|
||||
}
|
||||
SharedObjectSet set = info.inputValues.at(0).getInlineValue<SharedObjectSet>();
|
||||
set.insert(_object);
|
||||
F(set, _object);
|
||||
info.outputValues[0] = AttributeValue(_attribute, encodeInline(set));
|
||||
return false;
|
||||
}
|
||||
|
||||
void MetavoxelData::insert(const AttributePointer& attribute, const SharedObjectPointer& object) {
|
||||
Spanner* spanner = static_cast<Spanner*>(object.data());
|
||||
insert(attribute, spanner->getBounds(), spanner->getGranularity(), object);
|
||||
}
|
||||
|
||||
void MetavoxelData::insert(const AttributePointer& attribute, const Box& bounds,
|
||||
float granularity, const SharedObjectPointer& object) {
|
||||
// expand to fit the entire bounds
|
||||
while (!getBounds().contains(bounds)) {
|
||||
expand();
|
||||
}
|
||||
InsertVisitor visitor(attribute, bounds, granularity, object);
|
||||
SpannerUpdateVisitor<insertSpanner> visitor(attribute, bounds, granularity, object);
|
||||
guide(visitor);
|
||||
}
|
||||
|
||||
class RemoveVisitor : public MetavoxelVisitor {
|
||||
public:
|
||||
|
||||
RemoveVisitor(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object);
|
||||
|
||||
virtual bool visit(MetavoxelInfo& info);
|
||||
|
||||
private:
|
||||
|
||||
const AttributePointer& _attribute;
|
||||
const Box& _bounds;
|
||||
float _longestSide;
|
||||
const SharedObjectPointer& _object;
|
||||
};
|
||||
|
||||
RemoveVisitor::RemoveVisitor(const AttributePointer& attribute, const Box& bounds,
|
||||
float granularity, const SharedObjectPointer& object) :
|
||||
MetavoxelVisitor(QVector<AttributePointer>() << attribute, QVector<AttributePointer>() << attribute),
|
||||
_attribute(attribute),
|
||||
_bounds(bounds),
|
||||
_longestSide(qMax(bounds.getLongestSide(), granularity)),
|
||||
_object(object) {
|
||||
}
|
||||
|
||||
bool RemoveVisitor::visit(MetavoxelInfo& info) {
|
||||
if (!info.getBounds().intersects(_bounds)) {
|
||||
return false;
|
||||
}
|
||||
if (info.size > _longestSide) {
|
||||
return true;
|
||||
}
|
||||
SharedObjectSet set = info.inputValues.at(0).getInlineValue<SharedObjectSet>();
|
||||
set.remove(_object);
|
||||
info.outputValues[0] = AttributeValue(_attribute, encodeInline(set));
|
||||
return false;
|
||||
void MetavoxelData::remove(const AttributePointer& attribute, const SharedObjectPointer& object) {
|
||||
Spanner* spanner = static_cast<Spanner*>(object.data());
|
||||
remove(attribute, spanner->getBounds(), spanner->getGranularity(), object);
|
||||
}
|
||||
|
||||
void MetavoxelData::remove(const AttributePointer& attribute, const Box& bounds,
|
||||
float granularity, const SharedObjectPointer& object) {
|
||||
RemoveVisitor visitor(attribute, bounds, granularity, object);
|
||||
SpannerUpdateVisitor<removeSpanner> visitor(attribute, bounds, granularity, object);
|
||||
guide(visitor);
|
||||
}
|
||||
|
||||
void MetavoxelData::toggle(const AttributePointer& attribute, const SharedObjectPointer& object) {
|
||||
Spanner* spanner = static_cast<Spanner*>(object.data());
|
||||
toggle(attribute, spanner->getBounds(), spanner->getGranularity(), object);
|
||||
}
|
||||
|
||||
void MetavoxelData::toggle(const AttributePointer& attribute, const Box& bounds,
|
||||
float granularity, const SharedObjectPointer& object) {
|
||||
SpannerUpdateVisitor<toggleSpanner> visitor(attribute, bounds, granularity, object);
|
||||
guide(visitor);
|
||||
}
|
||||
|
||||
|
@ -231,7 +236,7 @@ void MetavoxelData::expand() {
|
|||
_size *= 2.0f;
|
||||
}
|
||||
|
||||
void MetavoxelData::read(Bitstream& in) {
|
||||
void MetavoxelData::read(Bitstream& in, const MetavoxelLOD& lod) {
|
||||
// clear out any existing roots
|
||||
decrementRootReferenceCounts();
|
||||
_roots.clear();
|
||||
|
@ -239,27 +244,29 @@ void MetavoxelData::read(Bitstream& in) {
|
|||
in >> _size;
|
||||
|
||||
// read in the new roots
|
||||
int rootCount;
|
||||
in >> rootCount;
|
||||
for (int i = 0; i < rootCount; i++) {
|
||||
forever {
|
||||
AttributePointer attribute;
|
||||
in >> attribute;
|
||||
MetavoxelNode*& root = _roots[attribute];
|
||||
root = new MetavoxelNode(attribute);
|
||||
attribute->read(*root, in);
|
||||
if (!attribute) {
|
||||
break;
|
||||
}
|
||||
MetavoxelStreamState state = { getMinimum(), _size, attribute, in, lod, lod };
|
||||
attribute->read(*this, state);
|
||||
}
|
||||
}
|
||||
|
||||
void MetavoxelData::write(Bitstream& out) const {
|
||||
void MetavoxelData::write(Bitstream& out, const MetavoxelLOD& lod) const {
|
||||
out << _size;
|
||||
out << _roots.size();
|
||||
for (QHash<AttributePointer, MetavoxelNode*>::const_iterator it = _roots.constBegin(); it != _roots.constEnd(); it++) {
|
||||
out << it.key();
|
||||
it.key()->write(*it.value(), out);
|
||||
MetavoxelStreamState state = { getMinimum(), _size, it.key(), out, lod, lod };
|
||||
it.key()->write(*it.value(), state);
|
||||
}
|
||||
out << AttributePointer();
|
||||
}
|
||||
|
||||
void MetavoxelData::readDelta(const MetavoxelData& reference, Bitstream& in) {
|
||||
void MetavoxelData::readDelta(const MetavoxelData& reference, const MetavoxelLOD& referenceLOD,
|
||||
Bitstream& in, const MetavoxelLOD& lod) {
|
||||
// shallow copy the reference
|
||||
*this = reference;
|
||||
|
||||
|
@ -279,34 +286,36 @@ void MetavoxelData::readDelta(const MetavoxelData& reference, Bitstream& in) {
|
|||
}
|
||||
}
|
||||
|
||||
int changedCount;
|
||||
in >> changedCount;
|
||||
for (int i = 0; i < changedCount; i++) {
|
||||
forever {
|
||||
AttributePointer attribute;
|
||||
in >> attribute;
|
||||
MetavoxelNode*& root = _roots[attribute];
|
||||
if (root) {
|
||||
MetavoxelNode* oldRoot = root;
|
||||
root = new MetavoxelNode(attribute);
|
||||
attribute->readDelta(*root, *oldRoot, in);
|
||||
if (!attribute) {
|
||||
break;
|
||||
}
|
||||
MetavoxelStreamState state = { getMinimum(), _size, attribute, in, lod, referenceLOD };
|
||||
MetavoxelNode* oldRoot = _roots.value(attribute);
|
||||
if (oldRoot) {
|
||||
oldRoot->incrementReferenceCount();
|
||||
attribute->readDelta(*this, *oldRoot, state);
|
||||
oldRoot->decrementReferenceCount(attribute);
|
||||
|
||||
} else {
|
||||
root = new MetavoxelNode(attribute);
|
||||
attribute->read(*root, in);
|
||||
attribute->read(*this, state);
|
||||
}
|
||||
}
|
||||
|
||||
int removedCount;
|
||||
in >> removedCount;
|
||||
for (int i = 0; i < removedCount; i++) {
|
||||
forever {
|
||||
AttributePointer attribute;
|
||||
in >> attribute;
|
||||
if (!attribute) {
|
||||
break;
|
||||
}
|
||||
_roots.take(attribute)->decrementReferenceCount(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
void MetavoxelData::writeDelta(const MetavoxelData& reference, Bitstream& out) const {
|
||||
void MetavoxelData::writeDelta(const MetavoxelData& reference, const MetavoxelLOD& referenceLOD,
|
||||
Bitstream& out, const MetavoxelLOD& lod) const {
|
||||
// first things first: there might be no change whatsoever
|
||||
if (_size == reference._size && _roots == reference._roots) {
|
||||
out << false;
|
||||
|
@ -329,42 +338,29 @@ void MetavoxelData::writeDelta(const MetavoxelData& reference, Bitstream& out) c
|
|||
expandedReference = expanded;
|
||||
}
|
||||
|
||||
// count the number of roots added/changed, then write
|
||||
int changedCount = 0;
|
||||
for (QHash<AttributePointer, MetavoxelNode*>::const_iterator it = _roots.constBegin(); it != _roots.constEnd(); it++) {
|
||||
MetavoxelNode* referenceRoot = expandedReference->_roots.value(it.key());
|
||||
if (it.value() != referenceRoot) {
|
||||
changedCount++;
|
||||
}
|
||||
}
|
||||
out << changedCount;
|
||||
// write the added/changed roots
|
||||
for (QHash<AttributePointer, MetavoxelNode*>::const_iterator it = _roots.constBegin(); it != _roots.constEnd(); it++) {
|
||||
MetavoxelNode* referenceRoot = expandedReference->_roots.value(it.key());
|
||||
if (it.value() != referenceRoot) {
|
||||
out << it.key();
|
||||
MetavoxelStreamState state = { getMinimum(), _size, it.key(), out, lod, referenceLOD };
|
||||
if (referenceRoot) {
|
||||
it.key()->writeDelta(*it.value(), *referenceRoot, out);
|
||||
it.key()->writeDelta(*it.value(), *referenceRoot, state);
|
||||
} else {
|
||||
it.key()->write(*it.value(), out);
|
||||
it.key()->write(*it.value(), state);
|
||||
}
|
||||
}
|
||||
}
|
||||
out << AttributePointer();
|
||||
|
||||
// same with nodes removed
|
||||
int removedCount = 0;
|
||||
for (QHash<AttributePointer, MetavoxelNode*>::const_iterator it = expandedReference->_roots.constBegin();
|
||||
it != expandedReference->_roots.constEnd(); it++) {
|
||||
if (!_roots.contains(it.key())) {
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
out << removedCount;
|
||||
for (QHash<AttributePointer, MetavoxelNode*>::const_iterator it = expandedReference->_roots.constBegin();
|
||||
it != expandedReference->_roots.constEnd(); it++) {
|
||||
if (!_roots.contains(it.key())) {
|
||||
out << it.key();
|
||||
}
|
||||
}
|
||||
out << AttributePointer();
|
||||
|
||||
// delete the expanded reference if we had to expand
|
||||
if (expandedReference != &reference) {
|
||||
|
@ -372,6 +368,14 @@ void MetavoxelData::writeDelta(const MetavoxelData& reference, Bitstream& out) c
|
|||
}
|
||||
}
|
||||
|
||||
MetavoxelNode* MetavoxelData::createRoot(const AttributePointer& attribute) {
|
||||
MetavoxelNode*& root = _roots[attribute];
|
||||
if (root) {
|
||||
root->decrementReferenceCount(attribute);
|
||||
}
|
||||
return root = new MetavoxelNode(attribute);
|
||||
}
|
||||
|
||||
void MetavoxelData::incrementRootReferenceCounts() {
|
||||
for (QHash<AttributePointer, MetavoxelNode*>::const_iterator it = _roots.constBegin(); it != _roots.constEnd(); it++) {
|
||||
it.value()->incrementReferenceCount();
|
||||
|
@ -384,6 +388,17 @@ void MetavoxelData::decrementRootReferenceCounts() {
|
|||
}
|
||||
}
|
||||
|
||||
static glm::vec3 getNextMinimum(const glm::vec3& minimum, float nextSize, int index) {
|
||||
return minimum + glm::vec3(
|
||||
(index & X_MAXIMUM_FLAG) ? nextSize : 0.0f,
|
||||
(index & Y_MAXIMUM_FLAG) ? nextSize : 0.0f,
|
||||
(index & Z_MAXIMUM_FLAG) ? nextSize : 0.0f);
|
||||
}
|
||||
|
||||
void MetavoxelStreamState::setMinimum(const glm::vec3& lastMinimum, int index) {
|
||||
minimum = getNextMinimum(lastMinimum, size, index);
|
||||
}
|
||||
|
||||
MetavoxelNode::MetavoxelNode(const AttributeValue& attributeValue) : _referenceCount(1) {
|
||||
_attributeValue = attributeValue.copy();
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
|
@ -431,50 +446,72 @@ bool MetavoxelNode::isLeaf() const {
|
|||
return true;
|
||||
}
|
||||
|
||||
void MetavoxelNode::read(const AttributePointer& attribute, Bitstream& in) {
|
||||
clearChildren(attribute);
|
||||
void MetavoxelNode::read(MetavoxelStreamState& state) {
|
||||
clearChildren(state.attribute);
|
||||
|
||||
if (!state.shouldSubdivide()) {
|
||||
state.attribute->read(state.stream, _attributeValue, true);
|
||||
return;
|
||||
}
|
||||
bool leaf;
|
||||
in >> leaf;
|
||||
attribute->read(in, _attributeValue, leaf);
|
||||
state.stream >> leaf;
|
||||
state.attribute->read(state.stream, _attributeValue, leaf);
|
||||
if (!leaf) {
|
||||
MetavoxelStreamState nextState = { glm::vec3(), state.size * 0.5f, state.attribute,
|
||||
state.stream, state.lod, state.referenceLOD };
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
_children[i] = new MetavoxelNode(attribute);
|
||||
_children[i]->read(attribute, in);
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i] = new MetavoxelNode(state.attribute);
|
||||
_children[i]->read(nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MetavoxelNode::write(const AttributePointer& attribute, Bitstream& out) const {
|
||||
void MetavoxelNode::write(MetavoxelStreamState& state) const {
|
||||
if (!state.shouldSubdivide()) {
|
||||
state.attribute->write(state.stream, _attributeValue, true);
|
||||
return;
|
||||
}
|
||||
bool leaf = isLeaf();
|
||||
out << leaf;
|
||||
attribute->write(out, _attributeValue, leaf);
|
||||
state.stream << leaf;
|
||||
state.attribute->write(state.stream, _attributeValue, leaf);
|
||||
if (!leaf) {
|
||||
MetavoxelStreamState nextState = { glm::vec3(), state.size * 0.5f, state.attribute,
|
||||
state.stream, state.lod, state.referenceLOD };
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
_children[i]->write(attribute, out);
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i]->write(nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MetavoxelNode::readDelta(const AttributePointer& attribute, const MetavoxelNode& reference, Bitstream& in) {
|
||||
clearChildren(attribute);
|
||||
void MetavoxelNode::readDelta(const MetavoxelNode& reference, MetavoxelStreamState& state) {
|
||||
clearChildren(state.attribute);
|
||||
|
||||
if (!state.shouldSubdivide()) {
|
||||
state.attribute->readDelta(state.stream, _attributeValue, reference._attributeValue, true);
|
||||
return;
|
||||
}
|
||||
bool leaf;
|
||||
in >> leaf;
|
||||
attribute->readDelta(in, _attributeValue, reference._attributeValue, leaf);
|
||||
state.stream >> leaf;
|
||||
state.attribute->readDelta(state.stream, _attributeValue, reference._attributeValue, leaf);
|
||||
if (!leaf) {
|
||||
if (reference.isLeaf()) {
|
||||
MetavoxelStreamState nextState = { glm::vec3(), state.size * 0.5f, state.attribute,
|
||||
state.stream, state.lod, state.referenceLOD };
|
||||
if (reference.isLeaf() || !state.shouldSubdivideReference()) {
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
_children[i] = new MetavoxelNode(attribute);
|
||||
_children[i]->read(attribute, in);
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i] = new MetavoxelNode(state.attribute);
|
||||
_children[i]->read(nextState);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
bool changed;
|
||||
in >> changed;
|
||||
state.stream >> changed;
|
||||
if (changed) {
|
||||
_children[i] = new MetavoxelNode(attribute);
|
||||
_children[i]->readDelta(attribute, *reference._children[i], in);
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i] = new MetavoxelNode(state.attribute);
|
||||
_children[i]->readDelta(*reference._children[i], nextState);
|
||||
} else {
|
||||
_children[i] = reference._children[i];
|
||||
_children[i]->incrementReferenceCount();
|
||||
|
@ -484,28 +521,94 @@ void MetavoxelNode::readDelta(const AttributePointer& attribute, const Metavoxel
|
|||
}
|
||||
}
|
||||
|
||||
void MetavoxelNode::writeDelta(const AttributePointer& attribute, const MetavoxelNode& reference, Bitstream& out) const {
|
||||
void MetavoxelNode::writeDelta(const MetavoxelNode& reference, MetavoxelStreamState& state) const {
|
||||
if (!state.shouldSubdivide()) {
|
||||
state.attribute->writeDelta(state.stream, _attributeValue, reference._attributeValue, true);
|
||||
return;
|
||||
}
|
||||
bool leaf = isLeaf();
|
||||
out << leaf;
|
||||
attribute->writeDelta(out, _attributeValue, reference._attributeValue, leaf);
|
||||
state.stream << leaf;
|
||||
state.attribute->writeDelta(state.stream, _attributeValue, reference._attributeValue, leaf);
|
||||
if (!leaf) {
|
||||
if (reference.isLeaf()) {
|
||||
MetavoxelStreamState nextState = { glm::vec3(), state.size * 0.5f, state.attribute,
|
||||
state.stream, state.lod, state.referenceLOD };
|
||||
if (reference.isLeaf() || !state.shouldSubdivideReference()) {
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
_children[i]->write(attribute, out);
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i]->write(nextState);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
if (_children[i] == reference._children[i]) {
|
||||
out << false;
|
||||
state.stream << false;
|
||||
} else {
|
||||
out << true;
|
||||
_children[i]->writeDelta(attribute, *reference._children[i], out);
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
state.stream << true;
|
||||
_children[i]->writeDelta(*reference._children[i], nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MetavoxelNode::writeSpanners(MetavoxelStreamState& state) const {
|
||||
foreach (const SharedObjectPointer& object, decodeInline<SharedObjectSet>(_attributeValue)) {
|
||||
if (static_cast<Spanner*>(object.data())->testAndSetVisited()) {
|
||||
state.stream << object;
|
||||
}
|
||||
}
|
||||
if (!state.shouldSubdivide() || isLeaf()) {
|
||||
return;
|
||||
}
|
||||
MetavoxelStreamState nextState = { glm::vec3(), state.size * 0.5f, state.attribute,
|
||||
state.stream, state.lod, state.referenceLOD };
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i]->writeSpanners(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
void MetavoxelNode::writeSpannerDelta(const MetavoxelNode& reference, MetavoxelStreamState& state) const {
|
||||
SharedObjectSet oldSet = decodeInline<SharedObjectSet>(reference.getAttributeValue());
|
||||
SharedObjectSet newSet = decodeInline<SharedObjectSet>(_attributeValue);
|
||||
foreach (const SharedObjectPointer& object, oldSet) {
|
||||
if (static_cast<Spanner*>(object.data())->testAndSetVisited() && !newSet.contains(object)) {
|
||||
state.stream << object;
|
||||
}
|
||||
}
|
||||
foreach (const SharedObjectPointer& object, newSet) {
|
||||
if (static_cast<Spanner*>(object.data())->testAndSetVisited() && !oldSet.contains(object)) {
|
||||
state.stream << object;
|
||||
}
|
||||
}
|
||||
if (isLeaf() || !state.shouldSubdivide()) {
|
||||
if (!reference.isLeaf() && state.shouldSubdivideReference()) {
|
||||
MetavoxelStreamState nextState = { glm::vec3(), state.size * 0.5f, state.attribute,
|
||||
state.stream, state.lod, state.referenceLOD };
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
reference._children[i]->writeSpanners(nextState);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
MetavoxelStreamState nextState = { glm::vec3(), state.size * 0.5f, state.attribute,
|
||||
state.stream, state.lod, state.referenceLOD };
|
||||
if (reference.isLeaf() || !state.shouldSubdivideReference()) {
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i]->writeSpanners(nextState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < CHILD_COUNT; i++) {
|
||||
if (_children[i] != reference._children[i]) {
|
||||
nextState.setMinimum(state.minimum, i);
|
||||
_children[i]->writeSpannerDelta(*reference._children[i], nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MetavoxelNode::decrementReferenceCount(const AttributePointer& attribute) {
|
||||
if (--_referenceCount == 0) {
|
||||
destroy(attribute);
|
||||
|
@ -604,10 +707,7 @@ void DefaultMetavoxelGuide::guide(MetavoxelVisitation& visitation) {
|
|||
MetavoxelNode* child = node ? node->getChild(i) : NULL;
|
||||
nextVisitation.outputNodes[j] = child;
|
||||
}
|
||||
nextVisitation.info.minimum = visitation.info.minimum + glm::vec3(
|
||||
(i & X_MAXIMUM_FLAG) ? nextVisitation.info.size : 0.0f,
|
||||
(i & Y_MAXIMUM_FLAG) ? nextVisitation.info.size : 0.0f,
|
||||
(i & Z_MAXIMUM_FLAG) ? nextVisitation.info.size : 0.0f);
|
||||
nextVisitation.info.minimum = getNextMinimum(visitation.info.minimum, nextVisitation.info.size, i);
|
||||
static_cast<MetavoxelGuide*>(nextVisitation.info.inputValues.last().getInlineValue<
|
||||
SharedObjectPointer>().data())->guide(nextVisitation);
|
||||
for (int j = 0; j < nextVisitation.outputNodes.size(); j++) {
|
||||
|
|
|
@ -33,13 +33,21 @@ class SpannerRenderer;
|
|||
|
||||
/// Determines whether to subdivide each node when traversing.
|
||||
class MetavoxelLOD {
|
||||
STREAMABLE
|
||||
|
||||
public:
|
||||
glm::vec3 position;
|
||||
float threshold;
|
||||
STREAM glm::vec3 position;
|
||||
STREAM float threshold;
|
||||
|
||||
MetavoxelLOD(const glm::vec3& position = glm::vec3(), float threshold = 0.0f);
|
||||
|
||||
bool isValid() const { return threshold > 0.0f; }
|
||||
|
||||
bool shouldSubdivide(const glm::vec3& minimum, float size) const;
|
||||
};
|
||||
|
||||
DECLARE_STREAMABLE_METATYPE(MetavoxelLOD)
|
||||
|
||||
/// The base metavoxel representation shared between server and client.
|
||||
class MetavoxelData {
|
||||
public:
|
||||
|
@ -52,25 +60,35 @@ public:
|
|||
|
||||
float getSize() const { return _size; }
|
||||
|
||||
glm::vec3 getMinimum() const { return glm::vec3(_size, _size, _size) * -0.5f; }
|
||||
|
||||
Box getBounds() const;
|
||||
|
||||
/// Applies the specified visitor to the contained voxels.
|
||||
void guide(MetavoxelVisitor& visitor);
|
||||
|
||||
void insert(const AttributePointer& attribute, const SharedObjectPointer& object);
|
||||
void insert(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object);
|
||||
|
||||
void remove(const AttributePointer& attribute, const SharedObjectPointer& object);
|
||||
void remove(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object);
|
||||
|
||||
void toggle(const AttributePointer& attribute, const SharedObjectPointer& object);
|
||||
void toggle(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object);
|
||||
|
||||
void clear(const AttributePointer& attribute);
|
||||
|
||||
/// Expands the tree, increasing its capacity in all dimensions.
|
||||
void expand();
|
||||
|
||||
void read(Bitstream& in);
|
||||
void write(Bitstream& out) const;
|
||||
void read(Bitstream& in, const MetavoxelLOD& lod = MetavoxelLOD());
|
||||
void write(Bitstream& out, const MetavoxelLOD& lod = MetavoxelLOD()) const;
|
||||
|
||||
void readDelta(const MetavoxelData& reference, Bitstream& in);
|
||||
void writeDelta(const MetavoxelData& reference, Bitstream& out) const;
|
||||
void readDelta(const MetavoxelData& reference, const MetavoxelLOD& referenceLOD, Bitstream& in, const MetavoxelLOD& lod);
|
||||
void writeDelta(const MetavoxelData& reference, const MetavoxelLOD& referenceLOD,
|
||||
Bitstream& out, const MetavoxelLOD& lod) const;
|
||||
|
||||
MetavoxelNode* createRoot(const AttributePointer& attribute);
|
||||
|
||||
private:
|
||||
|
||||
|
@ -83,6 +101,22 @@ private:
|
|||
QHash<AttributePointer, MetavoxelNode*> _roots;
|
||||
};
|
||||
|
||||
/// Holds the state used in streaming metavoxel data.
|
||||
class MetavoxelStreamState {
|
||||
public:
|
||||
glm::vec3 minimum;
|
||||
float size;
|
||||
const AttributePointer& attribute;
|
||||
Bitstream& stream;
|
||||
const MetavoxelLOD& lod;
|
||||
const MetavoxelLOD& referenceLOD;
|
||||
|
||||
bool shouldSubdivide() const { return lod.shouldSubdivide(minimum, size); }
|
||||
bool shouldSubdivideReference() const { return referenceLOD.shouldSubdivide(minimum, size); }
|
||||
|
||||
void setMinimum(const glm::vec3& lastMinimum, int index);
|
||||
};
|
||||
|
||||
/// A single node within a metavoxel layer.
|
||||
class MetavoxelNode {
|
||||
public:
|
||||
|
@ -104,11 +138,14 @@ public:
|
|||
|
||||
bool isLeaf() const;
|
||||
|
||||
void read(const AttributePointer& attribute, Bitstream& in);
|
||||
void write(const AttributePointer& attribute, Bitstream& out) const;
|
||||
void read(MetavoxelStreamState& state);
|
||||
void write(MetavoxelStreamState& state) const;
|
||||
|
||||
void readDelta(const AttributePointer& attribute, const MetavoxelNode& reference, Bitstream& in);
|
||||
void writeDelta(const AttributePointer& attribute, const MetavoxelNode& reference, Bitstream& out) const;
|
||||
void readDelta(const MetavoxelNode& reference, MetavoxelStreamState& state);
|
||||
void writeDelta(const MetavoxelNode& reference, MetavoxelStreamState& state) const;
|
||||
|
||||
void writeSpanners(MetavoxelStreamState& state) const;
|
||||
void writeSpannerDelta(const MetavoxelNode& reference, MetavoxelStreamState& state) const;
|
||||
|
||||
/// Increments the node's reference count.
|
||||
void incrementReferenceCount() { _referenceCount++; }
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#include "MetavoxelData.h"
|
||||
#include "MetavoxelMessages.h"
|
||||
|
||||
void MetavoxelEditMessage::apply(MetavoxelData& data) const {
|
||||
|
@ -106,8 +105,7 @@ InsertSpannerEdit::InsertSpannerEdit(const AttributePointer& attribute, const Sh
|
|||
}
|
||||
|
||||
void InsertSpannerEdit::apply(MetavoxelData& data) const {
|
||||
Spanner* spanner = static_cast<Spanner*>(this->spanner.data());
|
||||
data.insert(attribute, spanner->getBounds(), spanner->getGranularity(), this->spanner);
|
||||
data.insert(attribute, spanner);
|
||||
}
|
||||
|
||||
RemoveSpannerEdit::RemoveSpannerEdit(const AttributePointer& attribute, int id) :
|
||||
|
|
|
@ -9,10 +9,7 @@
|
|||
#ifndef __interface__MetavoxelMessages__
|
||||
#define __interface__MetavoxelMessages__
|
||||
|
||||
#include "AttributeRegistry.h"
|
||||
#include "MetavoxelUtil.h"
|
||||
|
||||
class MetavoxelData;
|
||||
#include "MetavoxelData.h"
|
||||
|
||||
/// Requests to close the session.
|
||||
class CloseSessionMessage {
|
||||
|
@ -49,7 +46,7 @@ class ClientStateMessage {
|
|||
|
||||
public:
|
||||
|
||||
STREAM glm::vec3 position;
|
||||
STREAM MetavoxelLOD lod;
|
||||
};
|
||||
|
||||
DECLARE_STREAMABLE_METATYPE(ClientStateMessage)
|
||||
|
|
Loading…
Reference in a new issue