Merge branch 'master' of github.com:worklist/hifi into assignment

This commit is contained in:
Stephen Birarda 2013-11-07 12:43:58 -08:00
commit 72b2309dc0
28 changed files with 400 additions and 144 deletions

View file

@ -71,6 +71,9 @@ We have successfully built on OS X 10.8, Ubuntu and a few other modern Linux
distributions. A Windows build is planned for the future, but not currently in
development.
On a fresh Ubuntu 13.10 install, these are all the packages you need to grab and build the hifi project:
<pre>sudo apt-get install build-essential cmake git libcurl4-openssl-dev libqt5scripttools5 libqt5svg5-dev libqt5webkit5-dev libqt5location5 qtlocation5-dev qtdeclarative5-dev qtscript5-dev qtsensors5-dev qtmultimedia5-dev qtquick1-5-dev libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack-dev</pre>
Running Interface
-----
@ -147,4 +150,4 @@ To access your local domain in Interface, open your Preferences -- on OS X this
If everything worked you should see "Servers: 3" in the upper right. Nice work!
In the voxel-server/src directory you will find a README that explains in
further detail how to setup and administer a voxel-server.
further detail how to setup and administer a voxel-server.

View file

@ -29,7 +29,11 @@ link_hifi_library(voxel-server-library ${TARGET_NAME} ${ROOT_DIR})
include_directories(${ROOT_DIR}/externals/civetweb/include)
if (UNIX)
target_link_libraries(${TARGET_NAME} ${CMAKE_DL_LIBS})
endif (UNIX)
# link curl for synchronous script downloads
find_package(CURL REQUIRED)
include_directories(${CURL_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${CURL_LIBRARY})
target_link_libraries(${TARGET_NAME} ${CURL_LIBRARY})

View file

@ -124,6 +124,7 @@ Application::Application(int& argc, char** argv, timeval &startup_time) :
_isHoverVoxel(false),
_isHoverVoxelSounding(false),
_mouseVoxelScale(1.0f / 1024.0f),
_mouseVoxelScaleInitialized(false),
_justEditedVoxel(false),
_nudgeStarted(false),
_lookingAlongX(false),
@ -1782,7 +1783,7 @@ void Application::shrinkMirrorView() {
}
const float MAX_AVATAR_EDIT_VELOCITY = 1.0f;
const float MAX_VOXEL_EDIT_DISTANCE = 20.0f;
const float MAX_VOXEL_EDIT_DISTANCE = 50.0f;
const float HEAD_SPHERE_RADIUS = 0.07;
static QUuid DEFAULT_NODE_ID_REF;
@ -1987,8 +1988,8 @@ void Application::updateMyAvatarLookAtPosition(glm::vec3& lookAtSpot, glm::vec3&
// deflect using Faceshift gaze data
glm::vec3 origin = _myAvatar.getHead().calculateAverageEyePosition();
float pitchSign = (_myCamera.getMode() == CAMERA_MODE_MIRROR) ? -1.0f : 1.0f;
const float PITCH_SCALE = 0.5f;
const float YAW_SCALE = 0.5f;
const float PITCH_SCALE = 0.25f;
const float YAW_SCALE = 0.25f;
lookAtSpot = origin + _myCamera.getRotation() * glm::quat(glm::radians(glm::vec3(
_faceshift.getEstimatedEyePitch() * pitchSign * PITCH_SCALE, _faceshift.getEstimatedEyeYaw() * YAW_SCALE, 0.0f))) *
glm::inverse(_myCamera.getRotation()) * (lookAtSpot - origin);
@ -2051,6 +2052,8 @@ void Application::updateMouseVoxels(float deltaTime, glm::vec3& mouseRayOrigin,
PerformanceWarning warn(showWarnings, "Application::updateMouseVoxels()");
_mouseVoxel.s = 0.0f;
bool wasInitialized = _mouseVoxelScaleInitialized;
_mouseVoxelScaleInitialized = false;
if (Menu::getInstance()->isVoxelModeActionChecked() &&
(fabs(_myAvatar.getVelocity().x) +
fabs(_myAvatar.getVelocity().y) +
@ -2058,6 +2061,12 @@ void Application::updateMouseVoxels(float deltaTime, glm::vec3& mouseRayOrigin,
if (_voxels.findRayIntersection(mouseRayOrigin, mouseRayDirection, _mouseVoxel, distance, face)) {
if (distance < MAX_VOXEL_EDIT_DISTANCE) {
// set the voxel scale to that of the first moused-over voxel
if (!wasInitialized) {
_mouseVoxelScale = _mouseVoxel.s;
}
_mouseVoxelScaleInitialized = true;
// find the nearest voxel with the desired scale
if (_mouseVoxelScale > _mouseVoxel.s) {
// choose the larger voxel that encompasses the one selected
@ -2990,6 +2999,7 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) {
glDisable(GL_LIGHTING);
glPushMatrix();
glScalef(TREE_SCALE, TREE_SCALE, TREE_SCALE);
const float CUBE_EXPANSION = 1.01f;
if (_nudgeStarted) {
renderNudgeGuide(_nudgeGuidePosition.x, _nudgeGuidePosition.y, _nudgeGuidePosition.z, _nudgeVoxel.s);
renderNudgeGrid(_nudgeVoxel.x, _nudgeVoxel.y, _nudgeVoxel.z, _nudgeVoxel.s, _mouseVoxel.s);
@ -2999,7 +3009,7 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) {
_nudgeVoxel.z + _nudgeVoxel.s * 0.5f);
glColor3ub(255, 255, 255);
glLineWidth(4.0f);
glutWireCube(_nudgeVoxel.s);
glutWireCube(_nudgeVoxel.s * CUBE_EXPANSION);
glPopMatrix();
} else {
renderMouseVoxelGrid(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s);
@ -3018,13 +3028,13 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) {
_nudgeGuidePosition.y + _nudgeVoxel.s*0.5f,
_nudgeGuidePosition.z + _nudgeVoxel.s*0.5f);
glLineWidth(4.0f);
glutWireCube(_nudgeVoxel.s);
glutWireCube(_nudgeVoxel.s * CUBE_EXPANSION);
} else {
glTranslatef(_mouseVoxel.x + _mouseVoxel.s*0.5f,
_mouseVoxel.y + _mouseVoxel.s*0.5f,
_mouseVoxel.z + _mouseVoxel.s*0.5f);
glLineWidth(4.0f);
glutWireCube(_mouseVoxel.s);
glutWireCube(_mouseVoxel.s * CUBE_EXPANSION);
}
glLineWidth(1.0f);
glPopMatrix();

View file

@ -378,6 +378,7 @@ private:
VoxelDetail _mouseVoxel; // details of the voxel to be edited
float _mouseVoxelScale; // the scale for adding/removing voxels
bool _mouseVoxelScaleInitialized;
glm::vec3 _lastMouseVoxelPos; // the position of the last mouse voxel edit
bool _justEditedVoxel; // set when we've just added/deleted/colored a voxel

View file

@ -22,6 +22,7 @@
#include <QSlider>
#include <QStandardPaths>
#include <QUuid>
#include <QWindow>
#include <UUID.h>
@ -326,6 +327,7 @@ Menu::Menu() :
false,
appInstance->getFaceshift(),
SLOT(setTCPEnabled(bool)));
addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::ChatCircling, 0, true);
QMenu* webcamOptionsMenu = developerMenu->addMenu("Webcam Options");
@ -717,16 +719,13 @@ void Menu::aboutApp() {
InfoView::forcedShow();
}
void updateDSHostname(const QString& domainServerHostname) {
QString newHostname(DEFAULT_DOMAIN_HOSTNAME);
void sendFakeEnterEvent() {
QPoint lastCursorPosition = QCursor::pos();
QGLWidget* glWidget = Application::getInstance()->getGLWidget();
if (domainServerHostname.size() > 0) {
// the user input a new hostname, use that
newHostname = domainServerHostname;
}
// give our nodeList the new domain-server hostname
NodeList::getInstance()->setDomainHostname(newHostname);
QPoint windowPosition = glWidget->mapFromGlobal(lastCursorPosition);
QEnterEvent enterEvent = QEnterEvent(windowPosition, windowPosition, lastCursorPosition);
QCoreApplication::sendEvent(glWidget, &enterEvent);
}
const int QLINE_MINIMUM_WIDTH = 400;
@ -742,11 +741,15 @@ void Menu::login() {
loginDialog.resize(loginDialog.parentWidget()->size().width() * DIALOG_RATIO_OF_WINDOW, loginDialog.size().height());
int dialogReturn = loginDialog.exec();
if (dialogReturn == QDialog::Accepted && !loginDialog.textValue().isEmpty() && loginDialog.textValue() != username) {
// there has been a username change
// ask for a profile reset with the new username
Application::getInstance()->resetProfile(loginDialog.textValue());
}
sendFakeEnterEvent();
}
void Menu::editPreferences() {
@ -810,52 +813,52 @@ void Menu::editPreferences() {
layout->addWidget(buttons);
int ret = dialog.exec();
if (ret != QDialog::Accepted) {
return;
}
QUrl faceModelURL(faceURLEdit->text());
if (faceModelURL.toString() != faceURLString) {
// change the faceModelURL in the profile, it will also update this user's BlendFace
applicationInstance->getProfile()->setFaceModelURL(faceModelURL);
if (ret == QDialog::Accepted) {
QUrl faceModelURL(faceURLEdit->text());
// send the new face mesh URL to the data-server (if we have a client UUID)
DataServerClient::putValueForKey(DataServerKey::FaceMeshURL,
faceModelURL.toString().toLocal8Bit().constData());
}
QUrl skeletonModelURL(skeletonURLEdit->text());
if (skeletonModelURL.toString() != skeletonURLString) {
// change the skeletonModelURL in the profile, it will also update this user's Body
applicationInstance->getProfile()->setSkeletonModelURL(skeletonModelURL);
if (faceModelURL.toString() != faceURLString) {
// change the faceModelURL in the profile, it will also update this user's BlendFace
applicationInstance->getProfile()->setFaceModelURL(faceModelURL);
// send the new face mesh URL to the data-server (if we have a client UUID)
DataServerClient::putValueForKey(DataServerKey::FaceMeshURL,
faceModelURL.toString().toLocal8Bit().constData());
}
// send the new skeleton model URL to the data-server (if we have a client UUID)
DataServerClient::putValueForKey(DataServerKey::SkeletonURL,
skeletonModelURL.toString().toLocal8Bit().constData());
QUrl skeletonModelURL(skeletonURLEdit->text());
if (skeletonModelURL.toString() != skeletonURLString) {
// change the skeletonModelURL in the profile, it will also update this user's Body
applicationInstance->getProfile()->setSkeletonModelURL(skeletonModelURL);
// send the new skeleton model URL to the data-server (if we have a client UUID)
DataServerClient::putValueForKey(DataServerKey::SkeletonURL,
skeletonModelURL.toString().toLocal8Bit().constData());
}
QUrl avatarVoxelURL(avatarURL->text());
applicationInstance->getAvatar()->getVoxels()->setVoxelURL(avatarVoxelURL);
Avatar::sendAvatarURLsMessage(avatarVoxelURL);
applicationInstance->getAvatar()->getHead().setPupilDilation(pupilDilation->value() / (float)pupilDilation->maximum());
_maxVoxels = maxVoxels->value();
applicationInstance->getVoxels()->setMaxVoxels(_maxVoxels);
applicationInstance->getAvatar()->setLeanScale(leanScale->value());
_audioJitterBufferSamples = audioJitterBufferSamples->value();
if (_audioJitterBufferSamples != 0) {
applicationInstance->getAudio()->setJitterBufferSamples(_audioJitterBufferSamples);
}
_fieldOfView = fieldOfView->value();
applicationInstance->resizeGL(applicationInstance->getGLWidget()->width(), applicationInstance->getGLWidget()->height());
}
QUrl avatarVoxelURL(avatarURL->text());
applicationInstance->getAvatar()->getVoxels()->setVoxelURL(avatarVoxelURL);
Avatar::sendAvatarURLsMessage(avatarVoxelURL);
applicationInstance->getAvatar()->getHead().setPupilDilation(pupilDilation->value() / (float)pupilDilation->maximum());
_maxVoxels = maxVoxels->value();
applicationInstance->getVoxels()->setMaxVoxels(_maxVoxels);
applicationInstance->getAvatar()->setLeanScale(leanScale->value());
_audioJitterBufferSamples = audioJitterBufferSamples->value();
if (_audioJitterBufferSamples != 0) {
applicationInstance->getAudio()->setJitterBufferSamples(_audioJitterBufferSamples);
}
_fieldOfView = fieldOfView->value();
applicationInstance->resizeGL(applicationInstance->getGLWidget()->width(), applicationInstance->getGLWidget()->height());
sendFakeEnterEvent();
}
void Menu::goToDomain() {
@ -875,9 +878,19 @@ void Menu::goToDomain() {
domainDialog.resize(domainDialog.parentWidget()->size().width() * DIALOG_RATIO_OF_WINDOW, domainDialog.size().height());
int dialogReturn = domainDialog.exec();
if (dialogReturn == QDialog::Accepted && !domainDialog.textValue().isEmpty()) {
updateDSHostname(domainDialog.textValue());
if (dialogReturn == QDialog::Accepted) {
QString newHostname(DEFAULT_DOMAIN_HOSTNAME);
if (domainDialog.textValue().size() > 0) {
// the user input a new hostname, use that
newHostname = domainDialog.textValue();
}
// give our nodeList the new domain-server hostname
NodeList::getInstance()->setDomainHostname(domainDialog.textValue());
}
sendFakeEnterEvent();
}
void Menu::goToLocation() {
@ -917,6 +930,8 @@ void Menu::goToLocation() {
}
}
}
sendFakeEnterEvent();
}
void Menu::goToUser() {
@ -934,6 +949,8 @@ void Menu::goToUser() {
DataServerClient::getValuesForKeysAndUserString((QStringList() << DataServerKey::Domain << DataServerKey::Position),
userDialog.textValue());
}
sendFakeEnterEvent();
}
void Menu::bandwidthDetails() {

View file

@ -146,6 +146,7 @@ namespace MenuOption {
const QString AutomaticallyAuditTree = "Automatically Audit Tree Stats";
const QString Bandwidth = "Bandwidth Display";
const QString BandwidthDetails = "Bandwidth Details";
const QString ChatCircling = "Chat Circling";
const QString Collisions = "Collisions";
const QString CopyVoxels = "Copy";
const QString CoverageMap = "Render Coverage Map";

View file

@ -96,8 +96,6 @@ Avatar::Avatar(Node* owningNode) :
_leadingAvatar(NULL),
_voxels(this),
_moving(false),
_hoverOnDuration(0.0f),
_hoverOffDuration(0.0f),
_initialized(false),
_handHoldingPosition(0.0f, 0.0f, 0.0f),
_maxArmLength(0.0f),
@ -387,30 +385,6 @@ void Avatar::simulate(float deltaTime, Transmitter* transmitter) {
}
}
// head scale grows when avatar is looked at
const float BASE_MAX_SCALE = 3.0f;
float maxScale = BASE_MAX_SCALE * glm::distance(_position, Application::getInstance()->getCamera()->getPosition());
if (Application::getInstance()->getLookatTargetAvatar() == this) {
_hoverOnDuration += deltaTime;
_hoverOffDuration = 0.0f;
const float GROW_DELAY = 1.0f;
const float GROW_RATE = 0.25f;
if (_hoverOnDuration > GROW_DELAY) {
_head.setScale(glm::mix(_head.getScale(), maxScale, GROW_RATE));
}
} else {
_hoverOnDuration = 0.0f;
_hoverOffDuration += deltaTime;
const float SHRINK_DELAY = 1.0f;
const float SHRINK_RATE = 0.25f;
if (_hoverOffDuration > SHRINK_DELAY) {
_head.setScale(glm::mix(_head.getScale(), 1.0f, SHRINK_RATE));
}
}
_skeletonModel.simulate(deltaTime);
_head.setBodyRotation(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll));
glm::vec3 headPosition;

View file

@ -227,8 +227,6 @@ protected:
AvatarVoxelSystem _voxels;
bool _moving; ///< set when position is changing
float _hoverOnDuration;
float _hoverOffDuration;
// protected methods...
glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; }

View file

@ -17,6 +17,7 @@
#include "Application.h"
#include "DataServerClient.h"
#include "Menu.h"
#include "MyAvatar.h"
#include "Physics.h"
#include "devices/OculusManager.h"
@ -830,6 +831,7 @@ void MyAvatar::updateHandMovementAndTouching(float deltaTime, bool enableHandMov
// reset hand and arm positions according to hand movement
glm::vec3 up = orientation * IDENTITY_UP;
bool pointing = false;
if (enableHandMovement && glm::length(_mouseRayDirection) > EPSILON && !Application::getInstance()->isMouseHidden()) {
// confine to the approximate shoulder plane
glm::vec3 pointDirection = _mouseRayDirection;
@ -841,6 +843,7 @@ void MyAvatar::updateHandMovementAndTouching(float deltaTime, bool enableHandMov
}
const float FAR_AWAY_POINT = TREE_SCALE;
_skeleton.joint[AVATAR_JOINT_RIGHT_FINGERTIPS].position = _mouseRayOrigin + pointDirection * FAR_AWAY_POINT;
pointing = true;
}
_avatarTouch.setMyBodyPosition(_position);
@ -932,6 +935,8 @@ void MyAvatar::updateHandMovementAndTouching(float deltaTime, bool enableHandMov
if (_mousePressed) {
_handState = HAND_STATE_GRASPING;
} else if (pointing) {
_handState = HAND_STATE_POINTING;
} else {
_handState = HAND_STATE_NULL;
}
@ -1059,6 +1064,11 @@ void MyAvatar::updateAvatarCollisions(float deltaTime) {
// detect collisions with other avatars and respond
void MyAvatar::applyCollisionWithOtherAvatar(Avatar * otherAvatar, float deltaTime) {
// for now, don't collide if we have a new skeleton
if (_skeletonModel.isActive()) {
return;
}
glm::vec3 bodyPushForce = glm::vec3(0.0f, 0.0f, 0.0f);
// loop through the body balls of each avatar to check for every possible collision
@ -1110,6 +1120,10 @@ bool operator<(const SortedAvatar& s1, const SortedAvatar& s2) {
}
void MyAvatar::updateChatCircle(float deltaTime) {
if (!Menu::getInstance()->isOptionChecked(MenuOption::ChatCircling)) {
return;
}
// find all members and sort by distance
QVector<SortedAvatar> sortedAvatars;
NodeList* nodeList = NodeList::getInstance();

View file

@ -28,7 +28,12 @@ void SkeletonModel::simulate(float deltaTime) {
Model::simulate(deltaTime);
setRightHandPosition(_owningAvatar->getHandPosition());
if (_owningAvatar->getHandState() == HAND_STATE_NULL) {
const float HAND_RESTORATION_RATE = 0.25f;
restoreRightHandPosition(HAND_RESTORATION_RATE);
} else {
setRightHandPosition(_owningAvatar->getHandPosition());
}
}
bool SkeletonModel::render(float alpha) {

View file

@ -68,7 +68,7 @@ void Faceshift::update() {
(_eyeGazeLeftPitch + _eyeGazeRightPitch) / 2.0f, (_eyeGazeLeftYaw + _eyeGazeRightYaw) / 2.0f, 0.0f))));
// compute and subtract the long term average
const float LONG_TERM_AVERAGE_SMOOTHING = 0.9999f;
const float LONG_TERM_AVERAGE_SMOOTHING = 0.999f;
if (!_longTermAverageInitialized) {
_longTermAverageEyePitch = eulers.x;
_longTermAverageEyeYaw = eulers.y;

View file

@ -477,6 +477,10 @@ bool Model::setLeftHandPosition(const glm::vec3& position) {
return isActive() && setJointPosition(_geometry->getFBXGeometry().leftHandJointIndex, position);
}
bool Model::restoreLeftHandPosition(float percent) {
return isActive() && restoreJointPosition(_geometry->getFBXGeometry().leftHandJointIndex, percent);
}
bool Model::setLeftHandRotation(const glm::quat& rotation) {
return isActive() && setJointRotation(_geometry->getFBXGeometry().leftHandJointIndex, rotation);
}
@ -485,6 +489,10 @@ bool Model::setRightHandPosition(const glm::vec3& position) {
return isActive() && setJointPosition(_geometry->getFBXGeometry().rightHandJointIndex, position);
}
bool Model::restoreRightHandPosition(float percent) {
return isActive() && restoreJointPosition(_geometry->getFBXGeometry().rightHandJointIndex, percent);
}
bool Model::setRightHandRotation(const glm::quat& rotation) {
return isActive() && setJointRotation(_geometry->getFBXGeometry().rightHandJointIndex, rotation);
}
@ -617,6 +625,19 @@ bool Model::setJointRotation(int jointIndex, const glm::quat& rotation) {
return true;
}
bool Model::restoreJointPosition(int jointIndex, float percent) {
if (jointIndex == -1 || _jointStates.isEmpty()) {
return false;
}
const FBXGeometry& geometry = _geometry->getFBXGeometry();
const QVector<int>& freeLineage = geometry.joints.at(jointIndex).freeLineage;
foreach (int index, freeLineage) {
_jointStates[index].rotation = safeMix(_jointStates[index].rotation, geometry.joints.at(index).rotation, percent);
}
return true;
}
void Model::deleteGeometry() {
foreach (Model* attachment, _attachments) {
delete attachment;

View file

@ -74,6 +74,11 @@ public:
/// \return whether or not the left hand joint was found
bool setLeftHandPosition(const glm::vec3& position);
/// Restores some percentage of the default position of the left hand.
/// \param percent the percentage of the default position to restore
/// \return whether or not the left hand joint was found
bool restoreLeftHandPosition(float percent = 1.0f);
/// Sets the rotation of the left hand.
/// \return whether or not the left hand joint was found
bool setLeftHandRotation(const glm::quat& rotation);
@ -82,6 +87,11 @@ public:
/// \return whether or not the right hand joint was found
bool setRightHandPosition(const glm::vec3& position);
/// Restores some percentage of the default position of the right hand.
/// \param percent the percentage of the default position to restore
/// \return whether or not the right hand joint was found
bool restoreRightHandPosition(float percent = 1.0f);
/// Sets the rotation of the right hand.
/// \return whether or not the right hand joint was found
bool setRightHandRotation(const glm::quat& rotation);
@ -130,6 +140,12 @@ protected:
bool setJointPosition(int jointIndex, const glm::vec3& position);
bool setJointRotation(int jointIndex, const glm::quat& rotation);
/// Restores the indexed joint to its default position.
/// \param percent the percentage of the default position to apply (i.e., 0.25f to slerp one fourth of the way to
/// the original position
/// \return true if the joint was found
bool restoreJointPosition(int jointIndex, float percent = 1.0f);
private:
void deleteGeometry();

View file

@ -16,10 +16,15 @@
#include "SharedUtil.h"
#include "OctalCode.h"
int numberOfThreeBitSectionsInCode(const unsigned char* octalCode) {
int numberOfThreeBitSectionsInCode(const unsigned char* octalCode, int maxBytes) {
if (maxBytes == OVERFLOWED_OCTCODE_BUFFER) {
return OVERFLOWED_OCTCODE_BUFFER;
}
assert(octalCode);
if (*octalCode == 255) {
return *octalCode + numberOfThreeBitSectionsInCode(octalCode + 1);
int newMaxBytes = (maxBytes == UNKNOWN_OCTCODE_LENGTH) ? UNKNOWN_OCTCODE_LENGTH : maxBytes - 1;
return *octalCode + numberOfThreeBitSectionsInCode(octalCode + 1, newMaxBytes);
} else {
return *octalCode;
}

View file

@ -24,7 +24,15 @@ void printOctalCode(const unsigned char* octalCode);
int bytesRequiredForCodeLength(unsigned char threeBitCodes);
int branchIndexWithDescendant(const unsigned char* ancestorOctalCode, const unsigned char* descendantOctalCode);
unsigned char* childOctalCode(const unsigned char* parentOctalCode, char childNumber);
int numberOfThreeBitSectionsInCode(const unsigned char* octalCode);
const int OVERFLOWED_OCTCODE_BUFFER = -1;
const int UNKNOWN_OCTCODE_LENGTH = -2;
/// will return -1 if there is an error in the octcode encoding, or it would overflow maxBytes
/// \param const unsigned char* octalCode the octalcode to decode
/// \param int maxBytes number of bytes that octalCode is expected to be, -1 if unknown
int numberOfThreeBitSectionsInCode(const unsigned char* octalCode, int maxBytes = UNKNOWN_OCTCODE_LENGTH);
unsigned char* chopOctalCode(const unsigned char* originalOctalCode, int chopLevels);
unsigned char* rebaseOctalCode(const unsigned char* originalOctalCode, const unsigned char* newParentOctalCode,
bool includeColorSpace = false);

View file

@ -91,23 +91,23 @@ bool PacketSender::process() {
while (keepGoing) {
uint64_t SEND_INTERVAL_USECS = (_packetsPerSecond == 0) ? USECS_PER_SECOND : (USECS_PER_SECOND / _packetsPerSecond);
lock();
NetworkPacket& packet = _packets.front();
NetworkPacket temporary = packet; // make a copy
_packets.erase(_packets.begin());
packetsLeft = _packets.size();
unlock();
// send the packet through the NodeList...
UDPSocket* nodeSocket = NodeList::getInstance()->getNodeSocket();
nodeSocket->send(&packet.getAddress(), packet.getData(), packet.getLength());
nodeSocket->send(&temporary.getAddress(), temporary.getData(), temporary.getLength());
packetsThisCall++;
if (_notify) {
_notify->packetSentNotification(packet.getLength());
_notify->packetSentNotification(temporary.getLength());
}
lock();
_packets.erase(_packets.begin());
unlock();
packetsLeft = _packets.size();
// in threaded mode, we go till we're empty
if (isThreaded()) {

View file

@ -31,12 +31,13 @@ bool ReceivedPacketProcessor::process() {
usleep(RECEIVED_THREAD_SLEEP_INTERVAL);
}
while (_packets.size() > 0) {
NetworkPacket& packet = _packets.front();
processPacket(packet.getAddress(), packet.getData(), packet.getLength());
lock();
_packets.erase(_packets.begin());
unlock();
lock(); // lock to make sure nothing changes on us
NetworkPacket& packet = _packets.front(); // get the oldest packet
NetworkPacket temporary = packet; // make a copy of the packet in case the vector is resized on us
_packets.erase(_packets.begin()); // remove the oldest packet
unlock(); // let others add to the packets
processPacket(temporary.getAddress(), temporary.getData(), temporary.getLength()); // process our temporary copy
}
return isStillRunning(); // keep running till they terminate us
}

View file

@ -35,6 +35,8 @@ VoxelNodeData::VoxelNodeData(Node* owningNode) :
_lastVoxelPacketLength = 0;
_duplicatePacketCount = 0;
resetVoxelPacket();
qDebug("VoxelNodeData::VoxelNodeData() this=%p owningNode=%p\n", this, owningNode);
}
void VoxelNodeData::initializeVoxelSendThread(VoxelServer* voxelServer) {
@ -42,6 +44,10 @@ void VoxelNodeData::initializeVoxelSendThread(VoxelServer* voxelServer) {
QUuid nodeUUID = getOwningNode()->getUUID();
_voxelSendThread = new VoxelSendThread(nodeUUID, voxelServer);
_voxelSendThread->initialize(true);
qDebug("VoxelNodeData::initializeVoxelSendThread() this=%p owningNode=%p _voxelSendThread=%p\n",
this, getOwningNode(), _voxelSendThread);
qDebug() << "VoxelNodeData::initializeVoxelSendThread() nodeUUID=" << nodeUUID << "\n";
}
bool VoxelNodeData::packetIsDuplicate() const {
@ -111,6 +117,12 @@ void VoxelNodeData::writeToPacket(unsigned char* buffer, int bytes) {
}
VoxelNodeData::~VoxelNodeData() {
qDebug("VoxelNodeData::~VoxelNodeData() this=%p owningNode=%p _voxelSendThread=%p\n",
this, getOwningNode(), _voxelSendThread);
QUuid nodeUUID = getOwningNode()->getUUID();
qDebug() << "VoxelNodeData::~VoxelNodeData() nodeUUID=" << nodeUUID << "\n";
delete[] _voxelPacket;
delete[] _lastVoxelPacket;

View file

@ -33,7 +33,9 @@ bool VoxelPersistThread::process() {
{
PerformanceWarning warn(true, "Loading Voxel File", true);
_tree->lockForRead();
persistantFileRead = _tree->readFromSVOFile(_filename);
_tree->unlock();
}
if (persistantFileRead) {
@ -41,7 +43,9 @@ bool VoxelPersistThread::process() {
// after done inserting all these voxels, then reaverage colors
qDebug("BEGIN Voxels Re-Averaging\n");
_tree->lockForWrite();
_tree->reaverageVoxelColors(_tree->rootNode);
_tree->unlock();
qDebug("DONE WITH Voxels Re-Averaging\n");
}
@ -70,7 +74,9 @@ bool VoxelPersistThread::process() {
// check the dirty bit and persist here...
if (_tree->isDirty()) {
qDebug("saving voxels to file %s...\n",_filename);
_tree->lockForRead();
_tree->writeToSVOFile(_filename);
_tree->unlock();
_tree->clearDirtyBit(); // tree is clean after saving
qDebug("DONE saving voxels to file...\n");
}

View file

@ -27,27 +27,31 @@ bool VoxelSendThread::process() {
uint64_t start = usecTimestampNow();
Node* node = NodeList::getInstance()->nodeWithUUID(_nodeUUID);
VoxelNodeData* nodeData = NULL;
if (node) {
nodeData = (VoxelNodeData*) node->getLinkedData();
}
node->lock(); // make sure the node list doesn't kill our node while we're using it
VoxelNodeData* nodeData = NULL;
int packetsSent = 0;
nodeData = (VoxelNodeData*) node->getLinkedData();
int packetsSent = 0;
// Sometimes the node data has not yet been linked, in which case we can't really do anything
if (nodeData) {
bool viewFrustumChanged = nodeData->updateCurrentViewFrustum();
if (_myServer->wantsDebugVoxelSending()) {
printf("nodeData->updateCurrentViewFrustum() changed=%s\n", debug::valueOf(viewFrustumChanged));
// Sometimes the node data has not yet been linked, in which case we can't really do anything
if (nodeData) {
bool viewFrustumChanged = nodeData->updateCurrentViewFrustum();
if (_myServer->wantsDebugVoxelSending()) {
printf("nodeData->updateCurrentViewFrustum() changed=%s\n", debug::valueOf(viewFrustumChanged));
}
packetsSent = deepestLevelVoxelDistributor(node, nodeData, viewFrustumChanged);
}
packetsSent = deepestLevelVoxelDistributor(node, nodeData, viewFrustumChanged);
node->unlock(); // we're done with this node for now.
}
// dynamically sleep until we need to fire off the next set of voxels
int elapsed = (usecTimestampNow() - start);
int usecToSleep = VOXEL_SEND_INTERVAL_USECS - elapsed;
if (usecToSleep > 0) {
usleep(usecToSleep);
} else {
@ -55,6 +59,7 @@ bool VoxelSendThread::process() {
std::cout << "Last send took too much time, not sleeping!\n";
}
}
return isStillRunning(); // keep running till they terminate us
}
@ -112,7 +117,7 @@ int VoxelSendThread::handlePacketSend(Node* node, VoxelNodeData* nodeData, int&
/// Version of voxel distributor that sends the deepest LOD level at once
int VoxelSendThread::deepestLevelVoxelDistributor(Node* node, VoxelNodeData* nodeData, bool viewFrustumChanged) {
_myServer->lockTree();
_myServer->getServerTree().lockForRead();
int truePacketsSent = 0;
int trueBytesSent = 0;
@ -355,7 +360,7 @@ int VoxelSendThread::deepestLevelVoxelDistributor(Node* node, VoxelNodeData* nod
} // end if bag wasn't empty, and so we sent stuff...
_myServer->unlockTree();
_myServer->getServerTree().unlock();
return truePacketsSent;
}

View file

@ -10,6 +10,7 @@
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <time.h>
#include <QtCore/QDebug>
#include <QtCore/QString>
@ -45,7 +46,11 @@ const char* VOXELS_PERSIST_FILE = "/etc/highfidelity/voxel-server/resources/voxe
void attachVoxelNodeDataToNode(Node* newNode) {
if (newNode->getLinkedData() == NULL) {
newNode->setLinkedData(new VoxelNodeData(newNode));
VoxelNodeData* voxelNodeData = new VoxelNodeData(newNode);
QUuid nodeUUID = newNode->getUUID();
qDebug("attachVoxelNodeDataToNode() newNode=%p voxelNodeData=%p\n", newNode, voxelNodeData);
qDebug() << "attachVoxelNodeDataToNode() node UUID:" << nodeUUID << "\n";
newNode->setLinkedData(voxelNodeData);
}
}
@ -71,7 +76,10 @@ VoxelServer::VoxelServer(const unsigned char* dataBuffer, int numBytes) : Assign
_voxelServerPacketProcessor = NULL;
_voxelPersistThread = NULL;
_parsedArgV = NULL;
_started = time(0);
_startedUSecs = usecTimestampNow();
_theInstance = this;
}
@ -106,6 +114,19 @@ void VoxelServer::initMongoose(int port) {
int VoxelServer::civetwebRequestHandler(struct mg_connection* connection) {
const struct mg_request_info* ri = mg_get_request_info(connection);
#ifdef FORCE_CRASH
if (strcmp(ri->uri, "/force_crash") == 0 && strcmp(ri->request_method, "GET") == 0) {
qDebug() << "About to force a crash!\n";
int foo;
int* forceCrash = &foo;
mg_printf(connection, "%s", "HTTP/1.0 200 OK\r\n\r\n");
mg_printf(connection, "%s", "forcing a crash....\r\n");
delete[] forceCrash;
mg_printf(connection, "%s", "did it crash....\r\n");
return 1;
}
#endif
if (strcmp(ri->uri, "/") == 0 && strcmp(ri->request_method, "GET") == 0) {
uint64_t checkSum;
@ -114,6 +135,45 @@ int VoxelServer::civetwebRequestHandler(struct mg_connection* connection) {
mg_printf(connection, "%s", "Your Voxel Server is running.\r\n");
mg_printf(connection, "%s", "\r\n");
tm* localtm = localtime(&GetInstance()->_started);
const int MAX_TIME_LENGTH = 128;
char buffer[MAX_TIME_LENGTH];
strftime(buffer, MAX_TIME_LENGTH, "%m/%d/%Y %X", localtm);
mg_printf(connection, "Running since: %s", buffer);
// Convert now to tm struct for UTC
tm* gmtm = gmtime(&GetInstance()->_started);
if (gmtm != NULL) {
strftime(buffer, MAX_TIME_LENGTH, "%m/%d/%Y %X", gmtm);
mg_printf(connection, " [%s UTM] ", buffer);
}
mg_printf(connection, "%s", "\r\n");
uint64_t now = usecTimestampNow();
const int USECS_PER_MSEC = 1000;
uint64_t msecsElapsed = (now - GetInstance()->_startedUSecs) / USECS_PER_MSEC;
const int MSECS_PER_SEC = 1000;
const int SECS_PER_MIN = 60;
const int MIN_PER_HOUR = 60;
const int MSECS_PER_MIN = MSECS_PER_SEC * SECS_PER_MIN;
float seconds = (msecsElapsed % MSECS_PER_MIN)/(float)MSECS_PER_SEC;
int minutes = (msecsElapsed/(MSECS_PER_MIN)) % MIN_PER_HOUR;
int hours = (msecsElapsed/(MSECS_PER_MIN * MIN_PER_HOUR));
mg_printf(connection, "%s", "Uptime: ");
if (hours > 0) {
mg_printf(connection, "%d hour%s ", hours, (hours > 1) ? "s" : "" );
}
if (minutes > 0) {
mg_printf(connection, "%d minute%s ", minutes, (minutes > 1) ? "s" : "");
}
if (seconds > 0) {
mg_printf(connection, "%.3f seconds ", seconds);
}
mg_printf(connection, "%s", "\r\n");
mg_printf(connection, "%s", "\r\n");
mg_printf(connection, "%s", "\r\n");
mg_printf(connection, "%s", "Configuration: \r\n ");
@ -283,8 +343,6 @@ void VoxelServer::run() {
parsePayload();
}
pthread_mutex_init(&_treeLock, NULL);
qInstallMessageHandler(Logging::verboseMessageHandler);
const char* STATUS_PORT = "--statusPort";
@ -438,7 +496,19 @@ void VoxelServer::run() {
_voxelServerPacketProcessor->initialize(true);
}
qDebug("Now running...\n");
// Convert now to tm struct for local timezone
tm* localtm = localtime(&_started);
const int MAX_TIME_LENGTH = 128;
char localBuffer[MAX_TIME_LENGTH] = { 0 };
char utcBuffer[MAX_TIME_LENGTH] = { 0 };
strftime(localBuffer, MAX_TIME_LENGTH, "%m/%d/%Y %X", localtm);
// Convert now to tm struct for UTC
tm* gmtm = gmtime(&_started);
if (gmtm != NULL) {
strftime(utcBuffer, MAX_TIME_LENGTH, " [%m/%d/%Y %X UTC]", gmtm);
}
qDebug() << "Now running... started at: " << localBuffer << utcBuffer << "\n";
// loop to send to nodes requesting data
while (true) {
@ -519,8 +589,6 @@ void VoxelServer::run() {
// tell our NodeList we're done with notifications
nodeList->removeHook(&_nodeWatcher);
pthread_mutex_destroy(&_treeLock);
}

View file

@ -11,6 +11,7 @@
#define __voxel_server__VoxelServer__
#include <QStringList>
#include <QDateTime>
#include <QtCore/QCoreApplication>
#include <Assignment.h>
@ -47,10 +48,6 @@ public:
VoxelTree& getServerTree() { return _serverTree; }
JurisdictionMap* getJurisdiction() { return _jurisdiction; }
void lockTree() { pthread_mutex_lock(&_treeLock); }
void unlockTree() { pthread_mutex_unlock(&_treeLock); }
VoxelTree* getTree() { return &_serverTree; }
int getPacketsPerClientPerInterval() const { return _packetsPerClientPerInterval; }
bool getSendMinimalEnvironment() const { return _sendMinimalEnvironment; }
EnvironmentData* getEnvironmentData(int i) { return &_environmentData[i]; }
@ -79,7 +76,6 @@ private:
JurisdictionSender* _jurisdictionSender;
VoxelServerPacketProcessor* _voxelServerPacketProcessor;
VoxelPersistThread* _voxelPersistThread;
pthread_mutex_t _treeLock;
EnvironmentData _environmentData[3];
NodeWatcher _nodeWatcher; // used to cleanup AGENT data when agents are killed
@ -91,6 +87,8 @@ private:
static int civetwebRequestHandler(struct mg_connection *connection);
static VoxelServer* _theInstance;
time_t _started;
uint64_t _startedUSecs;
};
#endif // __voxel_server__VoxelServer__

View file

@ -24,6 +24,12 @@ VoxelServerPacketProcessor::VoxelServerPacketProcessor(VoxelServer* myServer) :
void VoxelServerPacketProcessor::processPacket(sockaddr& senderAddress, unsigned char* packetData, ssize_t packetLength) {
bool debugProcessPacket = _myServer->wantsDebugVoxelReceiving();
if (debugProcessPacket) {
printf("VoxelServerPacketProcessor::processPacket(() packetData=%p packetLength=%ld\n", packetData, packetLength);
}
int numBytesPacketHeader = numBytesForPacketHeader(packetData);
if (packetData[0] == PACKET_TYPE_SET_VOXEL || packetData[0] == PACKET_TYPE_SET_VOXEL_DESTRUCTIVE) {
@ -49,7 +55,22 @@ void VoxelServerPacketProcessor::processPacket(sockaddr& senderAddress, unsigned
int atByte = numBytesPacketHeader + sizeof(itemNumber);
unsigned char* voxelData = (unsigned char*)&packetData[atByte];
while (atByte < packetLength) {
unsigned char octets = numberOfThreeBitSectionsInCode(voxelData);
int maxSize = packetLength - atByte;
if (debugProcessPacket) {
printf("VoxelServerPacketProcessor::processPacket(() %s packetData=%p packetLength=%ld voxelData=%p atByte=%d maxSize=%d\n",
destructive ? "PACKET_TYPE_SET_VOXEL_DESTRUCTIVE" : "PACKET_TYPE_SET_VOXEL",
packetData, packetLength, voxelData, atByte, maxSize);
}
int octets = numberOfThreeBitSectionsInCode(voxelData, maxSize);
if (octets == OVERFLOWED_OCTCODE_BUFFER) {
printf("WARNING! Got voxel edit record that would overflow buffer in numberOfThreeBitSectionsInCode(), ");
printf("bailing processing of packet!\n");
break;
}
const int COLOR_SIZE_IN_BYTES = 3;
int voxelDataSize = bytesRequiredForCodeLength(octets) + COLOR_SIZE_IN_BYTES;
int voxelCodeSize = bytesRequiredForCodeLength(octets);
@ -65,7 +86,9 @@ void VoxelServerPacketProcessor::processPacket(sockaddr& senderAddress, unsigned
delete[] vertices;
}
_myServer->getServerTree().lockForWrite();
_myServer->getServerTree().readCodeColorBufferToTree(voxelData, destructive);
_myServer->getServerTree().unlock();
// skip to next voxel edit record in the packet
voxelData += voxelDataSize;
@ -76,6 +99,12 @@ void VoxelServerPacketProcessor::processPacket(sockaddr& senderAddress, unsigned
}
}
if (debugProcessPacket) {
printf("VoxelServerPacketProcessor::processPacket(() DONE LOOPING FOR %s packetData=%p packetLength=%ld voxelData=%p atByte=%d\n",
destructive ? "PACKET_TYPE_SET_VOXEL_DESTRUCTIVE" : "PACKET_TYPE_SET_VOXEL",
packetData, packetLength, voxelData, atByte);
}
// Make sure our Node and NodeList knows we've heard from this node.
Node* node = NodeList::getInstance()->nodeWithAddress(&senderAddress);
if (node) {
@ -85,9 +114,9 @@ void VoxelServerPacketProcessor::processPacket(sockaddr& senderAddress, unsigned
} else if (packetData[0] == PACKET_TYPE_ERASE_VOXEL) {
// Send these bits off to the VoxelTree class to process them
_myServer->lockTree();
_myServer->getServerTree().lockForWrite();
_myServer->getServerTree().processRemoveVoxelBitstream((unsigned char*)packetData, packetLength);
_myServer->unlockTree();
_myServer->getServerTree().unlock();
// Make sure our Node and NodeList knows we've heard from this node.
Node* node = NodeList::getInstance()->nodeWithAddress(&senderAddress);

View file

@ -109,8 +109,47 @@ JurisdictionMap::JurisdictionMap(unsigned char* rootOctalCode, const std::vector
init(rootOctalCode, endNodes);
}
void myDebugoutputBits(unsigned char byte, bool withNewLine) {
if (isalnum(byte)) {
printf("[ %d (%c): ", byte, byte);
} else {
printf("[ %d (0x%x): ", byte, byte);
}
for (int i = 0; i < 8; i++) {
printf("%d", byte >> (7 - i) & 1);
}
printf(" ] ");
if (withNewLine) {
printf("\n");
}
}
void myDebugPrintOctalCode(const unsigned char* octalCode, bool withNewLine) {
if (!octalCode) {
printf("NULL");
} else {
for (int i = 0; i < bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(octalCode)); i++) {
myDebugoutputBits(octalCode[i],false);
}
}
if (withNewLine) {
printf("\n");
}
}
JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHexCodes) {
qDebug("JurisdictionMap::JurisdictionMap(const char* rootHexCode=[%p] %s, const char* endNodesHexCodes=[%p] %s)\n",
rootHexCode, rootHexCode, endNodesHexCodes, endNodesHexCodes);
_rootOctalCode = hexStringToOctalCode(QString(rootHexCode));
qDebug("JurisdictionMap::JurisdictionMap() _rootOctalCode=%p octalCode=", _rootOctalCode);
myDebugPrintOctalCode(_rootOctalCode, true);
QString endNodesHexStrings(endNodesHexCodes);
QString delimiterPattern(",");
@ -120,8 +159,16 @@ JurisdictionMap::JurisdictionMap(const char* rootHexCode, const char* endNodesHe
QString endNodeHexString = endNodeList.at(i);
unsigned char* endNodeOctcode = hexStringToOctalCode(endNodeHexString);
qDebug("JurisdictionMap::JurisdictionMap() endNodeList(%d)=%s\n",
i, endNodeHexString.toLocal8Bit().constData());
//printOctalCode(endNodeOctcode);
_endNodes.push_back(endNodeOctcode);
qDebug("JurisdictionMap::JurisdictionMap() endNodeOctcode=%p octalCode=", endNodeOctcode);
myDebugPrintOctalCode(endNodeOctcode, true);
}
}

View file

@ -35,7 +35,7 @@ void JurisdictionSender::processPacket(sockaddr& senderAddress, unsigned char*
if (node) {
QUuid nodeUUID = node->getUUID();
lockRequestingNodes();
_nodesRequestingJurisdictions.insert(nodeUUID);
_nodesRequestingJurisdictions.push(nodeUUID);
unlockRequestingNodes();
}
}
@ -59,18 +59,16 @@ bool JurisdictionSender::process() {
int nodeCount = 0;
lockRequestingNodes();
for (std::set<QUuid>::iterator nodeIterator = _nodesRequestingJurisdictions.begin();
nodeIterator != _nodesRequestingJurisdictions.end(); nodeIterator++) {
while (!_nodesRequestingJurisdictions.empty()) {
QUuid nodeUUID = *nodeIterator;
QUuid nodeUUID = _nodesRequestingJurisdictions.front();
_nodesRequestingJurisdictions.pop();
Node* node = NodeList::getInstance()->nodeWithUUID(nodeUUID);
if (node->getActiveSocket() != NULL) {
sockaddr* nodeAddress = node->getActiveSocket();
queuePacketForSending(*nodeAddress, bufferOut, sizeOut);
nodeCount++;
// remove it from the set
_nodesRequestingJurisdictions.erase(nodeIterator);
}
}
unlockRequestingNodes();

View file

@ -11,7 +11,7 @@
#ifndef __shared__JurisdictionSender__
#define __shared__JurisdictionSender__
#include <set>
#include <queue>
#include <PacketSender.h>
#include <ReceivedPacketProcessor.h>
@ -44,6 +44,6 @@ protected:
private:
pthread_mutex_t _requestingNodeMutex;
JurisdictionMap* _jurisdictionMap;
std::set<QUuid> _nodesRequestingJurisdictions;
std::queue<QUuid> _nodesRequestingJurisdictions;
};
#endif // __shared__JurisdictionSender__

View file

@ -581,7 +581,14 @@ void VoxelTree::processRemoveVoxelBitstream(unsigned char * bitstream, int buffe
int atByte = sizeof(short int) + numBytesForPacketHeader(bitstream);
unsigned char* voxelCode = (unsigned char*)&bitstream[atByte];
while (atByte < bufferSizeBytes) {
int codeLength = numberOfThreeBitSectionsInCode(voxelCode);
int maxSize = bufferSizeBytes - atByte;
int codeLength = numberOfThreeBitSectionsInCode(voxelCode, maxSize);
if (codeLength == OVERFLOWED_OCTCODE_BUFFER) {
printf("WARNING! Got remove voxel bitstream that would overflow buffer in numberOfThreeBitSectionsInCode(), ");
printf("bailing processing of packet!\n");
break;
}
int voxelDataSize = bytesRequiredForCodeLength(codeLength) + SIZE_OF_COLOR_DATA;
if (atByte + voxelDataSize <= bufferSizeBytes) {

View file

@ -21,6 +21,7 @@
#include "VoxelEditPacketSender.h"
#include <QObject>
#include <QReadWriteLock>
// Callback function, for recuseTreeWithOperation
typedef bool (*RecurseVoxelTreeOperation)(VoxelNode* node, void* extraData);
@ -185,6 +186,11 @@ public:
// reads voxels from square image with alpha as a Y-axis
bool readFromSquareARGB32Pixels(const char *filename);
bool readFromSchematicFile(const char* filename);
// VoxelTree does not currently handle its own locking, caller must use these to lock/unlock
void lockForRead() { lock.lockForRead(); }
void lockForWrite() { lock.lockForWrite(); }
void unlock() { lock.unlock(); }
unsigned long getVoxelCount();
@ -266,6 +272,8 @@ private:
static bool nudgeCheck(VoxelNode* node, void* extraData);
void nudgeLeaf(VoxelNode* node, void* extraData);
void chunkifyLeaf(VoxelNode* node);
QReadWriteLock lock;
};
float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeScale);