// // RenderablePolyVoxEntityItem.cpp // libraries/entities-renderer/src/ // // Created by Seth Alves on 5/19/15. // Copyright 2015 High Fidelity, Inc. // Copyright 2022-2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // #include "RenderablePolyVoxEntityItem.h" #include #include #include #include #include #include #include "ModelScriptingInterface.h" #include #include #include #include #include #include "entities-renderer/ShaderConstants.h" #include #include "EntityTreeRenderer.h" #include #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdouble-promotion" #endif #include #include #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic pop #endif #include #include #include #ifdef _WIN32 #pragma warning(push) #pragma warning( disable : 4267 ) #endif #include #include #include #include #include #ifdef _WIN32 #pragma warning(pop) #endif #include "graphics/Geometry.h" #include "StencilMaskPass.h" #include "EntityTreeRenderer.h" #include "RenderablePolyVoxEntityItem.h" #include "PhysicalEntitySimulation.h" const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; /* A PolyVoxEntity has several interdependent parts: _voxelData -- compressed QByteArray representation of which voxels have which values _volData -- datastructure from the PolyVox library which holds which voxels have which values _mesh -- renderable representation of the voxels _shape -- used for bullet (physics) collisions Each one depends on the one before it, except that _voxelData is set from _volData if a script edits the voxels. There are booleans to indicate that something has been updated and the dependents now need to be updated. _meshReady -- do we have something to give scripts that ask for the mesh? _voxelDataDirty -- do we need to uncompress data and expand it into _volData? _volDataDirty -- does recomputeMesh need to be called? _shapeReady -- are we ready to tell bullet our shape? Here is a simplified diagram of the state machine implemented in RenderablePolyVoxEntityItem::update +-------------------+ | | | | | volDataDirty | voxelDataDirty | +--v--+ | +------+Ready+--------+ | | +-----+ | | | | | +-----v----+ +------v------+ | |BakingMesh| |Uncompressing| | +-----+----+ +------+------+ | | | | | | | +---v-------+ +-------v------------+ | |Compressing| |BakingMeshNoCompress| | +---------+-+ ++-------------------+ | | | | | | | +-v--------v+ | |BakingShape| | +-----+-----+ | | +-------------------+ The work for each step is done on temporary worker threads. The PolyVox entity will update _updateNeeded and enable or disable update calls on the entity, depending on if there is more work to do. From the 'Ready' state, if we receive an update from the network, _voxelDataDirty will be set true. We uncompress the received data, bake the mesh (for the render-engine's benefit), and then compute the shape (for the physics-engine's benefit). This is the right-hand side of the diagram. From the 'Ready' state, if a script changes a voxel, _volDataDirty will be set true. We bake the mesh, compress the voxels into a new _voxelData, and transmit the new _voxelData to the entity-server. We then bake the shape. This is the left-hand side of the diagram. The actual state machine is more complicated than the diagram, because it's possible for _volDataDirty or _voxelDataDirty to be set true while worker threads are attempting to bake meshes or shapes. If this happens, we jump back to a higher point in the diagram to avoid wasting effort. PolyVoxes are designed to seemlessly fit up against neighbors. If voxels go right up to the edge of polyvox, the resulting mesh wont be closed -- the library assumes you'll have another polyvox next to it to continue the mesh. If a polyvox entity is "edged", the voxel space is wrapped in an extra layer of zero-valued voxels. This avoids the previously mentioned gaps along the edges. Non-edged polyvox entities can be told about their neighbors in all 6 cardinal directions. On the positive edges of the polyvox, the values are set from the (negative edge of the) relevant neighbor so that their meshes knit together. This is handled by tellNeighborsToRecopyEdges and copyUpperEdgesFromNeighbors. In these functions, variable names have XP for x-positive, XN x-negative, etc. */ // FIXME move to GLM helpers template void loop3(const T& start, const T& end, F f) { T current; for (current.z = start.z; current.z < end.z; ++current.z) { for (current.y = start.y; current.y < end.y; ++current.y) { for (current.x = start.x; current.x < end.x; ++current.x) { f(current); } } } } EntityItemPointer RenderablePolyVoxEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { std::shared_ptr entity(new RenderablePolyVoxEntityItem(entityID), [](RenderablePolyVoxEntityItem* ptr) { ptr->deleteLater(); }); entity->setProperties(properties); entity->initializePolyVox(); return entity; } RenderablePolyVoxEntityItem::RenderablePolyVoxEntityItem(const EntityItemID& entityItemID) : PolyVoxEntityItem(entityItemID) { } RenderablePolyVoxEntityItem::~RenderablePolyVoxEntityItem() { withWriteLock([&] { _volData.reset(); }); } void RenderablePolyVoxEntityItem::initializePolyVox() { setVoxelVolumeSize(_voxelVolumeSize); } void RenderablePolyVoxEntityItem::setVoxelData(const QByteArray& voxelData) { // accept compressed voxel information from the entity-server bool changed = false; withWriteLock([&] { if (_voxelData != voxelData) { _voxelData = voxelData; _voxelDataDirty = true; changed = true; } }); if (changed) { startUpdates(); } } void RenderablePolyVoxEntityItem::setVoxelSurfaceStyle(uint16_t voxelSurfaceStyle) { // this controls whether the polyvox surface extractor does marching-cubes or makes a cubic mesh. It // also determines if the extra "edged" layer is used. bool volSizeChanged = false; withWriteLock([&] { if (_voxelSurfaceStyle == voxelSurfaceStyle) { return; } // if we are switching to or from "edged" we need to force a resize of _volData. bool wasEdged = isEdged(); bool willBeEdged = isEdged((PolyVoxSurfaceStyle)voxelSurfaceStyle); if (wasEdged != willBeEdged) { _volData.reset(); _voxelDataDirty = true; volSizeChanged = true; } _voxelSurfaceStyle = voxelSurfaceStyle; startUpdates(); }); if (volSizeChanged) { // setVoxelVolumeSize will re-alloc _volData with the right size setVoxelVolumeSize(_voxelVolumeSize); } _updateFromNeighborXEdge = _updateFromNeighborYEdge = _updateFromNeighborZEdge = true; tellNeighborsToRecopyEdges(true); startUpdates(); } bool RenderablePolyVoxEntityItem::setVoxel(const ivec3& v, uint8_t toValue) { if (_locked) { return false; } bool result = false; withWriteLock([&] { result = setVoxelInternal(v, toValue); }); if (result) { startUpdates(); } return result; } void RenderablePolyVoxEntityItem::forEachVoxelValue(const ivec3& voxelSize, std::function thunk) { // a thread-safe way for code outside this class to iterate over a range of voxels withReadLock([&] { loop3(ivec3(0), voxelSize, [&](const ivec3& v) { uint8_t uVoxelValue = getVoxelInternal(v); thunk(v, uVoxelValue); }); }); } QByteArray RenderablePolyVoxEntityItem::volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const { int totalSize = voxelXSize * voxelYSize * voxelZSize; ivec3 voxelSize{ voxelXSize, voxelYSize, voxelZSize }; ivec3 low{ 0 }; int index = 0; QByteArray result = QByteArray(totalSize, '\0'); withReadLock([&] { if (isEdged()) { low += 1; } loop3(ivec3(0), voxelSize, [&](const ivec3& v){ result[index++] = _volData->getVoxelAt(v.x + low.x, v.y + low.y, v.z + low.z); }); }); return result; } bool RenderablePolyVoxEntityItem::setAll(uint8_t toValue) { bool result = false; if (_locked) { return result; } withWriteLock([&] { loop3(ivec3(0), ivec3(_voxelVolumeSize), [&](const ivec3& v) { result |= setVoxelInternal(v, toValue); }); }); if (result) { startUpdates(); } return result; } bool RenderablePolyVoxEntityItem::setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue) { bool result = false; if (_locked) { return result; } ivec3 iLowPosition = ivec3{ glm::round(lowPosition) }; ivec3 iCuboidSize = ivec3{ glm::round(cuboidSize) }; ivec3 iVoxelVolumeSize = ivec3{ glm::round(_voxelVolumeSize) }; ivec3 low = glm::max(glm::min(iLowPosition, iVoxelVolumeSize - 1), ivec3(0)); ivec3 high = glm::max(glm::min(low + iCuboidSize, iVoxelVolumeSize), low); withWriteLock([&] { loop3(low, high, [&] (const ivec3& v){ result |= setVoxelInternal(v, toValue); }); }); if (result) { startUpdates(); } return result; } bool RenderablePolyVoxEntityItem::setVoxelInVolume(const vec3& position, uint8_t toValue) { if (_locked) { return false; } // same as setVoxel but takes a vector rather than 3 floats. return setVoxel(glm::round(position), toValue); } bool RenderablePolyVoxEntityItem::setSphereInVolume(const vec3& center, float radius, uint8_t toValue) { bool result = false; if (_locked) { return result; } float radiusSquared = radius * radius; // This three-level for loop iterates over every voxel in the volume withWriteLock([&] { loop3(ivec3(0), ivec3(_voxelVolumeSize), [&](const ivec3& v) { // Store our current position as a vector... glm::vec3 pos = vec3(v) + 0.5f; // consider voxels cenetered on their coordinates // And compute how far the current position is from the center of the volume float fDistToCenterSquared = glm::distance2(pos, center); // If the current voxel is less than 'radius' units from the center then we set its value if (fDistToCenterSquared <= radiusSquared) { result |= setVoxelInternal(v, toValue); } }); }); if (result) { startUpdates(); } return result; } bool RenderablePolyVoxEntityItem::setSphere(const vec3& centerWorldCoords, float radiusWorldCoords, uint8_t toValue) { bool result = false; if (_locked) { return result; } glm::mat4 vtwMatrix = voxelToWorldMatrix(); glm::mat4 wtvMatrix = glm::inverse(vtwMatrix); glm::vec3 dimensions = getScaledDimensions(); glm::vec3 voxelSize = dimensions / _voxelVolumeSize; float smallestDimensionSize = voxelSize.x; smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.y); smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.z); if (smallestDimensionSize <= 0.0f) { return false; } glm::vec3 maxRadiusInVoxelCoords = glm::vec3(radiusWorldCoords / smallestDimensionSize); glm::vec3 centerInVoxelCoords = wtvMatrix * glm::vec4(centerWorldCoords, 1.0f); glm::vec3 low = glm::floor(centerInVoxelCoords - maxRadiusInVoxelCoords); glm::vec3 high = glm::ceil(centerInVoxelCoords + maxRadiusInVoxelCoords); glm::ivec3 lowI = glm::clamp(low, glm::vec3(0.0f), _voxelVolumeSize); glm::ivec3 highI = glm::clamp(high, glm::vec3(0.0f), _voxelVolumeSize); glm::vec3 radials(radiusWorldCoords / voxelSize.x, radiusWorldCoords / voxelSize.y, radiusWorldCoords / voxelSize.z); // This three-level for loop iterates over every voxel in the volume that might be in the sphere withWriteLock([&] { loop3(lowI, highI, [&](const ivec3& v) { // set voxels whose bounding-box touches the sphere AABox voxelBox(glm::vec3(v) - 0.5f, glm::vec3(1.0f, 1.0f, 1.0f)); if (voxelBox.touchesAAEllipsoid(centerInVoxelCoords, radials)) { result |= setVoxelInternal(v, toValue); } // TODO -- this version only sets voxels which have centers inside the sphere. which is best? // // Store our current position as a vector... // glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates // // convert to world coordinates // glm::vec3 worldPos = glm::vec3(vtwMatrix * pos); // // compute how far the current position is from the center of the volume // float fDistToCenter = glm::distance(worldPos, centerWorldCoords); // // If the current voxel is less than 'radius' units from the center then we set its value // if (fDistToCenter <= radiusWorldCoords) { // result |= setVoxelInternal(x, y, z, toValue); // } }); }); if (result) { startUpdates(); } return result; } bool RenderablePolyVoxEntityItem::setCapsule(const vec3& startWorldCoords, const vec3& endWorldCoords, float radiusWorldCoords, uint8_t toValue) { bool result = false; if (_locked) { return result; } glm::mat4 vtwMatrix = voxelToWorldMatrix(); glm::mat4 wtvMatrix = glm::inverse(vtwMatrix); glm::vec3 dimensions = getScaledDimensions(); glm::vec3 voxelSize = dimensions / _voxelVolumeSize; float smallestDimensionSize = voxelSize.x; smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.y); smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.z); glm::vec3 maxRadiusInVoxelCoords = glm::vec3(radiusWorldCoords / smallestDimensionSize); glm::vec3 startInVoxelCoords = wtvMatrix * glm::vec4(startWorldCoords, 1.0f); glm::vec3 endInVoxelCoords = wtvMatrix * glm::vec4(endWorldCoords, 1.0f); glm::vec3 low = glm::min(glm::floor(startInVoxelCoords - maxRadiusInVoxelCoords), glm::floor(endInVoxelCoords - maxRadiusInVoxelCoords)); glm::vec3 high = glm::max(glm::ceil(startInVoxelCoords + maxRadiusInVoxelCoords), glm::ceil(endInVoxelCoords + maxRadiusInVoxelCoords)); glm::ivec3 lowI = glm::clamp(low, glm::vec3(0.0f), _voxelVolumeSize); glm::ivec3 highI = glm::clamp(high, glm::vec3(0.0f), _voxelVolumeSize); // This three-level for loop iterates over every voxel in the volume that might be in the capsule withWriteLock([&] { loop3(lowI, highI, [&](const ivec3& v) { // Store our current position as a vector... glm::vec4 pos{ vec3(v) + 0.5f, 1.0 }; // consider voxels cenetered on their coordinates // convert to world coordinates glm::vec3 worldPos = glm::vec3(vtwMatrix * pos); if (pointInCapsule(worldPos, startWorldCoords, endWorldCoords, radiusWorldCoords)) { result |= setVoxelInternal(v, toValue); } }); }); if (result) { startUpdates(); } return result; } class RaycastFunctor { public: RaycastFunctor(const std::shared_ptr>& vol) : _result(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)), _vol(vol) { } static bool inBounds(const std::shared_ptr>& vol, const ivec3& v) { // x, y, z are in polyvox volume coords ivec3 volSize{ vol->getWidth(), vol->getHeight(), vol->getDepth() }; return glm::all(glm::greaterThanEqual(v, ivec3(0))) && glm::all(glm::lessThan(v, volSize)); } bool operator()(PolyVox::SimpleVolume::Sampler& sampler) { PolyVox::Vector3DInt32 positionIndex = sampler.getPosition(); ivec3 v{ positionIndex.getX(), positionIndex.getY(), positionIndex.getZ() }; if (!inBounds(_vol, v)) { return true; } if (sampler.getVoxel() == 0) { return true; // keep raycasting } _result = glm::vec4(v, 1.0f); return false; } glm::vec4 _result; const std::shared_ptr> _vol; }; #if 0 class RaycastFunctor { public: RaycastFunctor(PolyVox::SimpleVolume* vol) : _result(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)), _vol(vol) { } static bool inBounds(const PolyVox::SimpleVolume* vol, int x, int y, int z) { // x, y, z are in polyvox volume coords return !(x < 0 || y < 0 || z < 0 || x >= vol->getWidth() || y >= vol->getHeight() || z >= vol->getDepth()); } bool operator()(PolyVox::SimpleVolume::Sampler& sampler) { PolyVox::Vector3DInt32 positionIndex = sampler.getPosition(); int x = positionIndex.getX(); int y = positionIndex.getY(); int z = positionIndex.getZ(); if (!inBounds(_vol, x, y, z)) { return true; } if (sampler.getVoxel() == 0) { return true; // keep raycasting } _result = glm::vec4((float)x, (float)y, (float)z, 1.0f); return false; } glm::vec4 _result; const PolyVox::SimpleVolume* _vol = nullptr; }; #endif bool RenderablePolyVoxEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& viewFrustumPos, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, bool precisionPicking) const { // TODO -- correctly pick against marching-cube generated meshes if (!precisionPicking) { // just intersect with bounding box return true; } glm::mat4 wtvMatrix = worldToVoxelMatrix(true); glm::vec3 normDirection = glm::normalize(direction); // the PolyVox ray intersection code requires a near and far point. // set ray cast length to long enough to cover all of the voxel space float distanceToEntity = glm::distance(origin, getWorldPosition()); glm::vec3 dimensions = getScaledDimensions(); float largestDimension = glm::compMax(dimensions) * 2.0f; glm::vec3 farPoint = origin + normDirection * (distanceToEntity + largestDimension); glm::vec4 originInVoxel = wtvMatrix * glm::vec4(origin, 1.0f); glm::vec4 farInVoxel = wtvMatrix * glm::vec4(farPoint, 1.0f); glm::vec4 result = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f); PolyVox::RaycastResult raycastResult = doRayCast(originInVoxel, farInVoxel, result); if (raycastResult == PolyVox::RaycastResults::Completed) { // the ray completed its path -- nothing was hit. return false; } glm::vec3 result3 = glm::vec3(result); AABox voxelBox; voxelBox += result3 - Vectors::HALF; voxelBox += result3 + Vectors::HALF; glm::vec3 directionInVoxel = vec3(wtvMatrix * glm::vec4(direction, 0.0f)); return voxelBox.findRayIntersection(glm::vec3(originInVoxel), directionInVoxel, 1.0f / directionInVoxel, distance, face, surfaceNormal); } bool RenderablePolyVoxEntityItem::findDetailedParabolaIntersection(const glm::vec3& origin, const glm::vec3& velocity, const glm::vec3& acceleration, const glm::vec3& viewFrustumPos, OctreeElementPointer& element, float& parabolicDistance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, bool precisionPicking) const { // TODO -- correctly pick against marching-cube generated meshes if (!precisionPicking) { // just intersect with bounding box return true; } glm::mat4 wtvMatrix = worldToVoxelMatrix(true); glm::vec4 originInVoxel = wtvMatrix * glm::vec4(origin, 1.0f); glm::vec4 velocityInVoxel = wtvMatrix * glm::vec4(velocity, 0.0f); glm::vec4 accelerationInVoxel = wtvMatrix * glm::vec4(acceleration, 0.0f); // find the first intersection with the voxel bounding box (slightly enlarged so we can catch voxels that touch the sides) bool success; glm::vec3 center = getCenterPosition(success); glm::vec3 dimensions = getScaledDimensions(); const float FIRST_BOX_HALF_SCALE = 0.51f; AABox voxelBox1(wtvMatrix * vec4(center - FIRST_BOX_HALF_SCALE * dimensions, 1.0f), wtvMatrix * vec4(2.0f * FIRST_BOX_HALF_SCALE * dimensions, 0.0f)); bool hit1; float parabolicDistance1; // If we're starting inside the box, our first point is originInVoxel if (voxelBox1.contains(originInVoxel)) { parabolicDistance1 = 0.0f; hit1 = true; } else { BoxFace face1; glm::vec3 surfaceNormal1; hit1 = voxelBox1.findParabolaIntersection(glm::vec3(originInVoxel), glm::vec3(velocityInVoxel), glm::vec3(accelerationInVoxel), parabolicDistance1, face1, surfaceNormal1); } if (hit1) { // find the second intersection, which should be with the inside of the box (use a slightly large box again) const float SECOND_BOX_HALF_SCALE = 0.52f; AABox voxelBox2(wtvMatrix * vec4(center - SECOND_BOX_HALF_SCALE * dimensions, 1.0f), wtvMatrix * vec4(2.0f * SECOND_BOX_HALF_SCALE * dimensions, 0.0f)); glm::vec4 originInVoxel2 = originInVoxel + velocityInVoxel * parabolicDistance1 + 0.5f * accelerationInVoxel * parabolicDistance1 * parabolicDistance1; glm::vec4 velocityInVoxel2 = velocityInVoxel + accelerationInVoxel * parabolicDistance1; glm::vec4 accelerationInVoxel2 = accelerationInVoxel; float parabolicDistance2; BoxFace face2; glm::vec3 surfaceNormal2; // this should always be true if (voxelBox2.findParabolaIntersection(glm::vec3(originInVoxel2), glm::vec3(velocityInVoxel2), glm::vec3(accelerationInVoxel2), parabolicDistance2, face2, surfaceNormal2)) { const int MAX_SECTIONS = 15; PolyVox::RaycastResult raycastResult = PolyVox::RaycastResults::Completed; glm::vec4 result = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f); glm::vec4 segmentStartVoxel = originInVoxel2; for (int i = 0; i < MAX_SECTIONS; i++) { float t = parabolicDistance2 * ((float)(i + 1)) / ((float)MAX_SECTIONS); glm::vec4 segmentEndVoxel = originInVoxel2 + velocityInVoxel2 * t + 0.5f * accelerationInVoxel2 * t * t; raycastResult = doRayCast(segmentStartVoxel, segmentEndVoxel, result); if (raycastResult != PolyVox::RaycastResults::Completed) { // We hit something! break; } segmentStartVoxel = segmentEndVoxel; } if (raycastResult == PolyVox::RaycastResults::Completed) { // the parabola completed its path -- nothing was hit. return false; } glm::vec3 result3 = glm::vec3(result); AABox voxelBox; voxelBox += result3 - Vectors::HALF; voxelBox += result3 + Vectors::HALF; return voxelBox.findParabolaIntersection(glm::vec3(originInVoxel), glm::vec3(velocityInVoxel), glm::vec3(accelerationInVoxel), parabolicDistance, face, surfaceNormal); } } return false; } PolyVox::RaycastResult RenderablePolyVoxEntityItem::doRayCast(glm::vec4 originInVoxel, glm::vec4 farInVoxel, glm::vec4& result) const { PolyVox::Vector3DFloat startPoint(originInVoxel.x, originInVoxel.y, originInVoxel.z); PolyVox::Vector3DFloat endPoint(farInVoxel.x, farInVoxel.y, farInVoxel.z); PolyVox::RaycastResult raycastResult; withReadLock([&] { RaycastFunctor callback(_volData); raycastResult = PolyVox::raycastWithEndpoints(_volData.get(), startPoint, endPoint, callback); // result is in voxel-space coordinates. result = callback._result; }); return raycastResult; } // virtual ShapeType RenderablePolyVoxEntityItem::getShapeType() const { if (_collisionless) { return SHAPE_TYPE_NONE; } return SHAPE_TYPE_COMPOUND; } void RenderablePolyVoxEntityItem::setRegistrationPoint(const glm::vec3& value) { if (value != getRegistrationPoint()) { withWriteLock([&] { _shapeReady = false; }); EntityItem::setRegistrationPoint(value); startUpdates(); } } bool RenderablePolyVoxEntityItem::isReadyToComputeShape() const { ShapeType shapeType = getShapeType(); if (shapeType == SHAPE_TYPE_NONE) { return true; } bool result; withReadLock([&] { result = _shapeReady; }); return result; } void RenderablePolyVoxEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType shapeType = getShapeType(); if (shapeType == SHAPE_TYPE_NONE) { info.setParams(getShapeType(), 0.5f * getScaledDimensions()); return; } withReadLock([&] { info = _shapeInfo; }); } void RenderablePolyVoxEntityItem::changeUpdates(bool value) { if (_updateNeeded != value) { EntityTreePointer entityTree = getTree(); if (entityTree) { EntitySimulationPointer simulation = entityTree->getSimulation(); if (simulation) { _updateNeeded = value; markDirtyFlags(Simulation::DIRTY_UPDATEABLE); simulation->changeEntity(getThisPointer()); } } } } void RenderablePolyVoxEntityItem::startUpdates() { changeUpdates(true); } void RenderablePolyVoxEntityItem::stopUpdates() { changeUpdates(false); } void RenderablePolyVoxEntityItem::update(const quint64& now) { bool doRecomputeMesh { false }; bool doUncompress { false }; bool doCompress { false }; bool doRecomputeShape { false }; withWriteLock([&] { tellNeighborsToRecopyEdges(false); switch (_state) { case PolyVoxState::Ready: { if (_volDataDirty) { _volDataDirty = _voxelDataDirty = false; _state = PolyVoxState::BakingMesh; doRecomputeMesh = true; } else if (_voxelDataDirty) { _voxelDataDirty = false; _state = PolyVoxState::Uncompressing; doUncompress = true; } else { copyUpperEdgesFromNeighbors(); if (!_volDataDirty && !_voxelDataDirty) { // nothing to do stopUpdates(); } } break; } case PolyVoxState::Uncompressing: { break; // wait } case PolyVoxState::UncompressingFinished: { if (_volDataDirty) { _volDataDirty = _voxelDataDirty = false; _state = PolyVoxState::BakingMeshNoCompress; doRecomputeMesh = true; } else if (_voxelDataDirty) { _voxelDataDirty = false; // _voxelData changed while we were uncompressing the previous version, uncompress again _state = PolyVoxState::Uncompressing; doUncompress = true; } else { _state = PolyVoxState::Ready; } break; } case PolyVoxState::BakingMesh: { break; // wait } case PolyVoxState::BakingMeshFinished: { if (_volDataDirty) { _volDataDirty = _voxelDataDirty = false; _state = PolyVoxState::BakingMesh; // a local edit happened while we were baking the mesh. rebake mesh... doRecomputeMesh = true; } else if (_voxelDataDirty) { _voxelDataDirty = false; // we received a change from the wire while baking the mesh. _state = PolyVoxState::Uncompressing; doUncompress = true; } else { _state = PolyVoxState::Compressing; doCompress = true; } break; } case PolyVoxState::BakingMeshNoCompress: { break; // wait } case PolyVoxState::BakingMeshNoCompressFinished: { if (_volDataDirty) { _volDataDirty = _voxelDataDirty = false; _state = PolyVoxState::BakingMesh; // a local edit happened while we were baking the mesh. rebake mesh... doRecomputeMesh = true; } else if (_voxelDataDirty) { _voxelDataDirty = false; // we received a change from the wire while baking the mesh. _state = PolyVoxState::Uncompressing; doUncompress = true; } else { _state = PolyVoxState::BakingShape; doRecomputeShape = true; } break; } case PolyVoxState::Compressing: { break; // wait } case PolyVoxState::CompressingFinished: { _state = PolyVoxState::BakingShape; doRecomputeShape = true; break; } case PolyVoxState::BakingShape: { break; // wait } case PolyVoxState::BakingShapeFinished: { _state = PolyVoxState::Ready; break; } } }); if (doRecomputeMesh) { recomputeMesh(); } if (doUncompress) { uncompressVolumeData(); } if (doCompress) { compressVolumeDataAndSendEditPacket(); } if (doRecomputeShape) { computeShapeInfoWorker(); } } void RenderablePolyVoxEntityItem::setVoxelVolumeSize(const glm::vec3& voxelVolumeSize) { // This controls how many individual voxels are in the entity. This is unrelated to // the dimentions of the entity -- it defines the sizes of the arrays that hold voxel values. // In addition to setting the number of voxels, this is used in a few places for its // side-effect of allocating _volData to be the correct size. withWriteLock([&] { if (_volData && _voxelVolumeSize == voxelVolumeSize) { return; } _voxelDataDirty = true; _voxelVolumeSize = voxelVolumeSize; _volData.reset(); _onCount = 0; _updateFromNeighborXEdge = _updateFromNeighborYEdge = _updateFromNeighborZEdge = true; startUpdates(); static const PolyVox::Vector3DInt32 lowCorner(0, 0, 0); PolyVox::Vector3DInt32 highCorner; if (isEdged()) { // with _EDGED_ we maintain an extra box of voxels around those that the user asked for. This // changes how the surface extractor acts -- it becomes impossible to have holes in the // generated mesh. The non _EDGED_ modes will leave holes in the mesh at the edges of the // voxel space. highCorner = PolyVox::Vector3DInt32(_voxelVolumeSize.x + 1, // corners are inclusive _voxelVolumeSize.y + 1, _voxelVolumeSize.z + 1); } else { // these should each have -1 after them, but if we leave layers on the upper-axis faces, // they act more like I expect. highCorner = PolyVox::Vector3DInt32(_voxelVolumeSize.x, _voxelVolumeSize.y, _voxelVolumeSize.z); } _volData.reset(new PolyVox::SimpleVolume(PolyVox::Region(lowCorner, highCorner))); // having the "outside of voxel-space" value be 255 has helped me notice some problems. _volData->setBorderValue(255); }); tellNeighborsToRecopyEdges(true); } bool inUserBounds(const std::shared_ptr> vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, const ivec3& v) { if (glm::any(glm::lessThan(v, ivec3(0)))) { return false; } ivec3 volume { vol->getWidth(), vol->getHeight(), vol->getDepth() }; // x, y, z are in user voxel-coords, not adjusted-for-edge voxel-coords. if (PolyVoxEntityItem::isEdged(surfaceStyle)) { volume -= 2; } if (glm::any(glm::greaterThanEqual(v, volume))) { return false; } return true; } uint8_t RenderablePolyVoxEntityItem::getVoxel(const ivec3& v) const { uint8_t result; withReadLock([&] { result = getVoxelInternal(v); }); return result; } uint8_t RenderablePolyVoxEntityItem::getVoxelInternal(const ivec3& v) const { if (!inUserBounds(_volData, (PolyVoxSurfaceStyle)_voxelSurfaceStyle, v)) { return 0; } // if _voxelSurfaceStyle is *_EDGED_*, we maintain an extra layer of // voxels all around the requested voxel space. Having the empty voxels around // the edges changes how the surface extractor behaves. if (isEdged()) { return _volData->getVoxelAt(v.x + 1, v.y + 1, v.z + 1); } return _volData->getVoxelAt(v.x, v.y, v.z); } void RenderablePolyVoxEntityItem::setVoxelMarkNeighbors(int x, int y, int z, uint8_t toValue) { _volData->setVoxelAt(x, y, z, toValue); if (x == 0) { _neighborXNeedsUpdate = true; startUpdates(); } if (y == 0) { _neighborYNeedsUpdate = true; startUpdates(); } if (z == 0) { _neighborZNeedsUpdate = true; startUpdates(); } } bool RenderablePolyVoxEntityItem::setVoxelInternal(const ivec3& v, uint8_t toValue) { // set a voxel without recompressing the voxel data. This assumes that the caller has write-locked the entity. bool result = updateOnCount(v, toValue); if (result) { if (isEdged()) { setVoxelMarkNeighbors(v.x + 1, v.y + 1, v.z + 1, toValue); } else { setVoxelMarkNeighbors(v.x, v.y, v.z, toValue); } _volDataDirty = true; } return result; } bool RenderablePolyVoxEntityItem::updateOnCount(const ivec3& v, uint8_t toValue) { // keep _onCount up to date if (!inUserBounds(_volData, (PolyVoxSurfaceStyle)_voxelSurfaceStyle, v)) { return false; } uint8_t uVoxelValue = getVoxelInternal(v); if (toValue != 0) { if (uVoxelValue == 0) { _onCount++; return true; } } else { // toValue == 0 if (uVoxelValue != 0) { _onCount--; assert(_onCount >= 0); return true; } } return false; } void RenderablePolyVoxEntityItem::uncompressVolumeData() { // take compressed data and expand it into _volData. QByteArray voxelData; auto entity = std::static_pointer_cast(getThisPointer()); withReadLock([&] { voxelData = _voxelData; }); QtConcurrent::run([=] { QDataStream reader(voxelData); quint16 voxelXSize, voxelYSize, voxelZSize; reader >> voxelXSize; reader >> voxelYSize; reader >> voxelZSize; if (voxelXSize == 0 || voxelXSize > PolyVoxEntityItem::MAX_VOXEL_DIMENSION || voxelYSize == 0 || voxelYSize > PolyVoxEntityItem::MAX_VOXEL_DIMENSION || voxelZSize == 0 || voxelZSize > PolyVoxEntityItem::MAX_VOXEL_DIMENSION) { qCDebug(entitiesrenderer) << "voxelSize is not reasonable, skipping uncompressions." << voxelXSize << voxelYSize << voxelZSize << getName() << getID(); entity->setVoxelsFromData(QByteArray(1, 0), 1, 1, 1); return; } int rawSize = voxelXSize * voxelYSize * voxelZSize; QByteArray compressedData; reader >> compressedData; QByteArray uncompressedData = qUncompress(compressedData); if (uncompressedData.size() != rawSize) { qCDebug(entitiesrenderer) << "PolyVox uncompress -- size is (" << voxelXSize << voxelYSize << voxelZSize << ")" << "so expected uncompressed length of" << rawSize << "but length is" << uncompressedData.size() << getName() << getID(); entity->setVoxelsFromData(QByteArray(1, 0), 1, 1, 1); return; } entity->setVoxelsFromData(uncompressedData, voxelXSize, voxelYSize, voxelZSize); }); } void RenderablePolyVoxEntityItem::setVoxelsFromData(QByteArray uncompressedData, quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) { // this accepts the payload from uncompressVolumeData ivec3 low{ 0 }; bool result = false; withWriteLock([&] { if (isEdged()) { low += 1; } loop3(ivec3(0), ivec3(voxelXSize, voxelYSize, voxelZSize), [&](const ivec3& v) { int uncompressedIndex = (v.z * (voxelYSize) * (voxelXSize)) + (v.y * (voxelZSize)) + v.x; result |= setVoxelInternal(v, uncompressedData[uncompressedIndex]); }); _state = PolyVoxState::UncompressingFinished; }); if (result) { startUpdates(); } } void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() { // compress the data in _volData and save the results. The compressed form is used during // saves to disk and for transmission over the wire to the entity-server EntityItemPointer entity = getThisPointer(); quint16 voxelXSize; quint16 voxelYSize; quint16 voxelZSize; withReadLock([&] { voxelXSize = _voxelVolumeSize.x; voxelYSize = _voxelVolumeSize.y; voxelZSize = _voxelVolumeSize.z; }); #ifdef WANT_DEBUG qDebug() << "Compressing voxel and sending data packet"; #endif QtConcurrent::run([voxelXSize, voxelYSize, voxelZSize, entity] { auto polyVoxEntity = std::static_pointer_cast(entity); QByteArray uncompressedData = polyVoxEntity->volDataToArray(voxelXSize, voxelYSize, voxelZSize); QByteArray newVoxelData; QDataStream writer(&newVoxelData, QIODevice::WriteOnly | QIODevice::Truncate); writer << voxelXSize << voxelYSize << voxelZSize; QByteArray compressedData = qCompress(uncompressedData, 9); writer << compressedData; // make sure the compressed data can be sent over the wire-protocol if (newVoxelData.size() > 1150) { // HACK -- until we have a way to allow for properties larger than MTU, don't update. // revert the active voxel-space to the last version that fit. qCDebug(entitiesrenderer) << "compressed voxel data is too large" << entity->getName() << entity->getID(); const auto polyVoxEntity = std::static_pointer_cast(entity); polyVoxEntity->compressVolumeDataFinished(QByteArray()); return; } std::static_pointer_cast(entity)->compressVolumeDataFinished(newVoxelData); }); } void RenderablePolyVoxEntityItem::compressVolumeDataFinished(const QByteArray& voxelData) { // compressed voxel information from the entity-server withWriteLock([&] { if (voxelData.size() > 0 && _voxelData != voxelData) { _voxelData = voxelData; } _state = PolyVoxState::CompressingFinished; }); auto now = usecTimestampNow(); setLastEdited(now); setLastBroadcast(now); EntityTreeElementPointer element = getElement(); EntityTreePointer tree = element ? element->getTree() : nullptr; if (tree) { tree->withReadLock([&] { EntityPropertyFlags desiredProperties; desiredProperties.setHasProperty(PROP_VOXEL_DATA); EntityItemProperties properties = getProperties(desiredProperties, false); properties.setVoxelDataDirty(); properties.setLastEdited(now); EntitySimulationPointer simulation = tree ? tree->getSimulation() : nullptr; PhysicalEntitySimulationPointer peSimulation = std::static_pointer_cast(simulation); EntityEditPacketSender* packetSender = peSimulation ? peSimulation->getPacketSender() : nullptr; if (packetSender) { packetSender->queueEditEntityMessage(PacketType::EntityEdit, tree, getID(), properties); } }); } } EntityItemPointer lookUpNeighbor(EntityTreePointer tree, EntityItemID neighborID, EntityItemWeakPointer& currentWP) { EntityItemPointer current = currentWP.lock(); if (!current && neighborID == UNKNOWN_ENTITY_ID) { // no neighbor return nullptr; } if (current && current->getID() == neighborID) { // same neighbor return current; } if (neighborID == UNKNOWN_ENTITY_ID) { currentWP.reset(); return nullptr; } current = tree->findEntityByID(neighborID); if (!current) { return nullptr; } currentWP = current; return current; } void RenderablePolyVoxEntityItem::cacheNeighbors() { // this attempts to turn neighbor entityIDs into neighbor weak-pointers EntityTreeElementPointer element = getElement(); EntityTreePointer tree = element ? element->getTree() : nullptr; if (!tree) { return; } lookUpNeighbor(tree, _xNNeighborID, _xNNeighbor); lookUpNeighbor(tree, _yNNeighborID, _yNNeighbor); lookUpNeighbor(tree, _zNNeighborID, _zNNeighbor); lookUpNeighbor(tree, _xPNeighborID, _xPNeighbor); lookUpNeighbor(tree, _yPNeighborID, _yPNeighbor); lookUpNeighbor(tree, _zPNeighborID, _zPNeighbor); } void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { // fill in our upper edges with a copy of our neighbors lower edges so that the meshes knit together if ((PolyVoxSurfaceStyle)_voxelSurfaceStyle != PolyVoxEntityItem::SURFACE_MARCHING_CUBES) { return; } if (!_updateFromNeighborXEdge && !_updateFromNeighborYEdge && !_updateFromNeighborZEdge) { return; } cacheNeighbors(); if (_updateFromNeighborXEdge) { _updateFromNeighborXEdge = false; auto currentXPNeighbor = getXPNeighbor(); if (currentXPNeighbor && currentXPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { withWriteLock([&] { int x = _volData->getWidth() - 1; for (int y = 0; y < _volData->getHeight(); y++) { for (int z = 0; z < _volData->getDepth(); z++) { uint8_t neighborValue = currentXPNeighbor->getVoxel({ 0, y, z }); uint8_t prevValue = _volData->getVoxelAt(x, y, z); if (prevValue != neighborValue) { _volData->setVoxelAt(x, y, z, neighborValue); _volDataDirty = true; } } } }); } } if (_updateFromNeighborYEdge) { _updateFromNeighborYEdge = false; auto currentYPNeighbor = getYPNeighbor(); if (currentYPNeighbor && currentYPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { withWriteLock([&] { int y = _volData->getHeight() - 1; for (int x = 0; x < _volData->getWidth(); x++) { for (int z = 0; z < _volData->getDepth(); z++) { uint8_t neighborValue = currentYPNeighbor->getVoxel({ x, 0, z }); uint8_t prevValue = _volData->getVoxelAt(x, y, z); if (prevValue != neighborValue) { _volData->setVoxelAt(x, y, z, neighborValue); _volDataDirty = true; } } } }); } } if (_updateFromNeighborZEdge) { _updateFromNeighborZEdge = false; auto currentZPNeighbor = getZPNeighbor(); if (currentZPNeighbor && currentZPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { withWriteLock([&] { int z = _volData->getDepth() - 1; for (int x = 0; x < _volData->getWidth(); x++) { for (int y = 0; y < _volData->getHeight(); y++) { uint8_t neighborValue = currentZPNeighbor->getVoxel({ x, y, 0 }); uint8_t prevValue = _volData->getVoxelAt(x, y, z); if (prevValue != neighborValue) { _volData->setVoxelAt(x, y, z, neighborValue); _volDataDirty = true; } } } }); } } } void RenderablePolyVoxEntityItem::tellNeighborsToRecopyEdges(bool force) { // if this polyvox has changed any of its voxels with a zero coord (in x, y, or z) notify neighbors, if there are any if (force || _neighborXNeedsUpdate || _neighborYNeedsUpdate || _neighborZNeedsUpdate) { cacheNeighbors(); if (force || _neighborXNeedsUpdate) { _neighborXNeedsUpdate = false; auto currentXNNeighbor = getXNNeighbor(); if (currentXNNeighbor) { currentXNNeighbor->neighborXEdgeChanged(); } } if (force || _neighborYNeedsUpdate) { _neighborYNeedsUpdate = false; auto currentYNNeighbor = getYNNeighbor(); if (currentYNNeighbor) { currentYNNeighbor->neighborYEdgeChanged(); } } if (force || _neighborZNeedsUpdate) { _neighborZNeedsUpdate = false; auto currentZNNeighbor = getZNNeighbor(); if (currentZNNeighbor) { currentZNNeighbor->neighborZEdgeChanged(); } } } } void RenderablePolyVoxEntityItem::recomputeMesh() { // use _volData to make a renderable mesh PolyVoxSurfaceStyle voxelSurfaceStyle; withReadLock([&] { voxelSurfaceStyle = (PolyVoxSurfaceStyle)_voxelSurfaceStyle; }); auto entity = std::static_pointer_cast(getThisPointer()); QtConcurrent::run([entity, voxelSurfaceStyle] { graphics::MeshPointer mesh(std::make_shared()); // A mesh object to hold the result of surface extraction PolyVox::SurfaceMesh polyVoxMesh; entity->withReadLock([&] { PolyVox::SimpleVolume* volData = entity->getVolData(); switch (voxelSurfaceStyle) { case PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES: case PolyVoxEntityItem::SURFACE_MARCHING_CUBES: { PolyVox::MarchingCubesSurfaceExtractor> surfaceExtractor (volData, volData->getEnclosingRegion(), &polyVoxMesh); surfaceExtractor.execute(); break; } case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: case PolyVoxEntityItem::SURFACE_CUBIC: { PolyVox::CubicSurfaceExtractorWithNormals> surfaceExtractor (volData, volData->getEnclosingRegion(), &polyVoxMesh); surfaceExtractor.execute(); break; } } }); // convert PolyVox mesh to a Sam mesh const std::vector& vecIndices = polyVoxMesh.getIndices(); auto indexBuffer = std::make_shared(vecIndices.size() * sizeof(uint32_t), (gpu::Byte*)vecIndices.data()); auto indexBufferPtr = gpu::BufferPointer(indexBuffer); gpu::BufferView indexBufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::INDEX)); mesh->setIndexBuffer(indexBufferView); const std::vector& vecVertices = polyVoxMesh.getRawVertexData(); auto vertexBuffer = std::make_shared(vecVertices.size() * sizeof(PolyVox::PositionMaterialNormal), (gpu::Byte*)vecVertices.data()); auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferPtr->getSize(), sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); mesh->setVertexBuffer(vertexBufferView); // TODO -- use 3-byte normals rather than 3-float normals mesh->addAttribute(gpu::Stream::NORMAL, gpu::BufferView(vertexBufferPtr, sizeof(float) * 3, // polyvox mesh is packed: position, normal, material vertexBufferPtr->getSize(), sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ))); std::vector parts; parts.emplace_back(graphics::Mesh::Part((graphics::Index)0, // startIndex (graphics::Index)vecIndices.size(), // numIndices (graphics::Index)0, // baseVertex graphics::Mesh::TRIANGLES)); // topology mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(graphics::Mesh::Part), (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); entity->setMesh(mesh); }); } void RenderablePolyVoxEntityItem::setMesh(graphics::MeshPointer mesh) { // this catches the payload from recomputeMesh withWriteLock([&] { if (!_collisionless) { _flags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } _shapeReady = false; _mesh = mesh; if (_state == PolyVoxState::BakingMeshNoCompress) { _state = PolyVoxState::BakingMeshNoCompressFinished; } else { _state = PolyVoxState::BakingMeshFinished; } _meshReady = true; startUpdates(); }); somethingChangedNotification(); } void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // this creates a collision-shape for the physics engine. The shape comes from // _volData for cubic extractors and from _mesh for marching-cube extractors EntityItemPointer entity = getThisPointer(); PolyVoxSurfaceStyle voxelSurfaceStyle; glm::vec3 voxelVolumeSize; graphics::MeshPointer mesh; withReadLock([&] { voxelSurfaceStyle = (PolyVoxSurfaceStyle)_voxelSurfaceStyle; voxelVolumeSize = _voxelVolumeSize; mesh = _mesh; }); QtConcurrent::run([entity, voxelSurfaceStyle, voxelVolumeSize, mesh] { auto polyVoxEntity = std::static_pointer_cast(entity); QVector> pointCollection; AABox box; glm::mat4 vtoM = std::static_pointer_cast(entity)->voxelToLocalMatrix(); if (voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_MARCHING_CUBES || voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES) { // pull each triangle in the mesh into a polyhedron which can be collided with unsigned int i = 0; const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); const gpu::BufferView& indexBufferView = mesh->getIndexBuffer(); gpu::BufferView::Iterator it = indexBufferView.cbegin(); while (it != indexBufferView.cend()) { uint32_t p0Index = *(it++); uint32_t p1Index = *(it++); uint32_t p2Index = *(it++); const glm::vec3& p0 = vertexBufferView.get(p0Index); const glm::vec3& p1 = vertexBufferView.get(p1Index); const glm::vec3& p2 = vertexBufferView.get(p2Index); glm::vec3 av = (p0 + p1 + p2) / 3.0f; // center of the triangular face glm::vec3 normal = glm::normalize(glm::cross(p1 - p0, p2 - p0)); glm::vec3 p3 = av - normal * MARCHING_CUBE_COLLISION_HULL_OFFSET; glm::vec3 p0Model = glm::vec3(vtoM * glm::vec4(p0, 1.0f)); glm::vec3 p1Model = glm::vec3(vtoM * glm::vec4(p1, 1.0f)); glm::vec3 p2Model = glm::vec3(vtoM * glm::vec4(p2, 1.0f)); glm::vec3 p3Model = glm::vec3(vtoM * glm::vec4(p3, 1.0f)); box += p0Model; box += p1Model; box += p2Model; box += p3Model; QVector pointsInPart; pointsInPart << p0Model; pointsInPart << p1Model; pointsInPart << p2Model; pointsInPart << p3Model; // add next convex hull QVector newMeshPoints; pointCollection << newMeshPoints; // add points to the new convex hull pointCollection[i++] << pointsInPart; } } else { unsigned int i = 0; polyVoxEntity->forEachVoxelValue(voxelVolumeSize, [&](const ivec3& v, uint8_t value) { if (value > 0) { const auto& x = v.x; const auto& y = v.y; const auto& z = v.z; if (glm::all(glm::greaterThan(v, ivec3(0))) && glm::all(glm::lessThan(v, ivec3(voxelVolumeSize) - 1)) && (polyVoxEntity->getVoxelInternal({ x - 1, y, z }) > 0) && (polyVoxEntity->getVoxelInternal({ x, y - 1, z }) > 0) && (polyVoxEntity->getVoxelInternal({ x, y, z - 1 }) > 0) && (polyVoxEntity->getVoxelInternal({ x + 1, y, z }) > 0) && (polyVoxEntity->getVoxelInternal({ x, y + 1, z }) > 0) && (polyVoxEntity->getVoxelInternal({ x, y, z + 1 }) > 0)) { // this voxel has neighbors in every cardinal direction, so there's no need // to include it in the collision hull. return; } QVector pointsInPart; float offL = -0.5f; float offH = 0.5f; if (voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_CUBIC) { offL += 1.0f; offH += 1.0f; } glm::vec3 p000 = glm::vec3(vtoM * glm::vec4(x + offL, y + offL, z + offL, 1.0f)); glm::vec3 p001 = glm::vec3(vtoM * glm::vec4(x + offL, y + offL, z + offH, 1.0f)); glm::vec3 p010 = glm::vec3(vtoM * glm::vec4(x + offL, y + offH, z + offL, 1.0f)); glm::vec3 p011 = glm::vec3(vtoM * glm::vec4(x + offL, y + offH, z + offH, 1.0f)); glm::vec3 p100 = glm::vec3(vtoM * glm::vec4(x + offH, y + offL, z + offL, 1.0f)); glm::vec3 p101 = glm::vec3(vtoM * glm::vec4(x + offH, y + offL, z + offH, 1.0f)); glm::vec3 p110 = glm::vec3(vtoM * glm::vec4(x + offH, y + offH, z + offL, 1.0f)); glm::vec3 p111 = glm::vec3(vtoM * glm::vec4(x + offH, y + offH, z + offH, 1.0f)); box += p000; box += p001; box += p010; box += p011; box += p100; box += p101; box += p110; box += p111; pointsInPart << p000; pointsInPart << p001; pointsInPart << p010; pointsInPart << p011; pointsInPart << p100; pointsInPart << p101; pointsInPart << p110; pointsInPart << p111; // add next convex hull QVector newMeshPoints; pointCollection << newMeshPoints; // add points to the new convex hull pointCollection[i++] << pointsInPart; } }); } polyVoxEntity->setCollisionPoints(pointCollection, box); }); } void RenderablePolyVoxEntityItem::setCollisionPoints(ShapeInfo::PointCollection pointCollection, AABox box) { // this catches the payload from computeShapeInfoWorker if (pointCollection.isEmpty()) { EntityItem::computeShapeInfo(_shapeInfo); withWriteLock([&] { _shapeReady = true; _state = PolyVoxState::BakingShapeFinished; }); return; } glm::vec3 collisionModelDimensions = box.getDimensions(); // include the registrationPoint in the shape key, because the offset is already // included in the points and the shapeManager wont know that the shape has changed. withWriteLock([&] { QString shapeKey = QString(_voxelData.toBase64()) + "," + QString::number(_registrationPoint.x) + "," + QString::number(_registrationPoint.y) + "," + QString::number(_registrationPoint.z); _shapeInfo.setParams(SHAPE_TYPE_COMPOUND, collisionModelDimensions, shapeKey); _shapeInfo.setPointCollection(pointCollection); _shapeReady = true; _state = PolyVoxState::BakingShapeFinished; }); } void RenderablePolyVoxEntityItem::setXNNeighborID(const EntityItemID& xNNeighborID) { if (xNNeighborID == _id) { // TODO loops are still possible return; } if (xNNeighborID != _xNNeighborID) { PolyVoxEntityItem::setXNNeighborID(xNNeighborID); _neighborXNeedsUpdate = true; startUpdates(); } } void RenderablePolyVoxEntityItem::setYNNeighborID(const EntityItemID& yNNeighborID) { if (yNNeighborID == _id) { // TODO loops are still possible return; } if (yNNeighborID != _yNNeighborID) { PolyVoxEntityItem::setYNNeighborID(yNNeighborID); _neighborYNeedsUpdate = true; startUpdates(); } } void RenderablePolyVoxEntityItem::setZNNeighborID(const EntityItemID& zNNeighborID) { if (zNNeighborID == _id) { // TODO loops are still possible return; } if (zNNeighborID != _zNNeighborID) { PolyVoxEntityItem::setZNNeighborID(zNNeighborID); _neighborZNeedsUpdate = true; startUpdates(); } } void RenderablePolyVoxEntityItem::setXPNeighborID(const EntityItemID& xPNeighborID) { if (xPNeighborID == _id) { // TODO loops are still possible return; } if (xPNeighborID != _xPNeighborID) { PolyVoxEntityItem::setXPNeighborID(xPNeighborID); _updateFromNeighborXEdge = true; startUpdates(); } } void RenderablePolyVoxEntityItem::setYPNeighborID(const EntityItemID& yPNeighborID) { if (yPNeighborID == _id) { // TODO loops are still possible return; } if (yPNeighborID != _yPNeighborID) { PolyVoxEntityItem::setYPNeighborID(yPNeighborID); _updateFromNeighborYEdge = true; startUpdates(); } } void RenderablePolyVoxEntityItem::setZPNeighborID(const EntityItemID& zPNeighborID) { if (zPNeighborID == _id) { // TODO loops are still possible return; } if (zPNeighborID != _zPNeighborID) { PolyVoxEntityItem::setZPNeighborID(zPNeighborID); _updateFromNeighborZEdge = true; startUpdates(); } } std::shared_ptr RenderablePolyVoxEntityItem::getXNNeighbor() { return std::dynamic_pointer_cast(_xNNeighbor.lock()); } std::shared_ptr RenderablePolyVoxEntityItem::getYNNeighbor() { return std::dynamic_pointer_cast(_yNNeighbor.lock()); } std::shared_ptr RenderablePolyVoxEntityItem::getZNNeighbor() { return std::dynamic_pointer_cast(_zNNeighbor.lock()); } std::shared_ptr RenderablePolyVoxEntityItem::getXPNeighbor() { return std::dynamic_pointer_cast(_xPNeighbor.lock()); } std::shared_ptr RenderablePolyVoxEntityItem::getYPNeighbor() { return std::dynamic_pointer_cast(_yPNeighbor.lock()); } std::shared_ptr RenderablePolyVoxEntityItem::getZPNeighbor() { return std::dynamic_pointer_cast(_zPNeighbor.lock()); } // deprecated bool RenderablePolyVoxEntityItem::getMeshes(MeshProxyList& result) { bool success = false; if (_mesh) { MeshProxy* meshProxy = nullptr; glm::mat4 transform = voxelToLocalMatrix(); withReadLock([&] { gpu::BufferView::Index numVertices = (gpu::BufferView::Index)_mesh->getNumVertices(); if (!_meshReady) { // we aren't ready to return a mesh. the caller will have to try again later. success = false; } else if (numVertices == 0) { // we are ready, but there are no triangles in the mesh. success = true; } else { success = true; // the mesh will be in voxel-space. transform it into object-space meshProxy = new SimpleMeshProxy( _mesh->map([=](glm::vec3 position) { return glm::vec3(transform * glm::vec4(position, 1.0f)); }, [=](glm::vec3 color) { return color; }, [=](glm::vec3 normal) { return glm::normalize(glm::vec3(transform * glm::vec4(normal, 0.0f))); }, [&](uint32_t index) { return index; })); result << meshProxy; } }); } return success; } scriptable::ScriptableModelBase RenderablePolyVoxEntityItem::getScriptableModel() { if (!_mesh) { return scriptable::ScriptableModelBase(); } bool success = false; glm::mat4 transform = voxelToLocalMatrix(); scriptable::ScriptableModelBase result; result.objectID = getThisPointer()->getID(); withReadLock([&] { gpu::BufferView::Index numVertices = (gpu::BufferView::Index)_mesh->getNumVertices(); if (!_meshReady) { // we aren't ready to return a mesh. the caller will have to try again later. success = false; } else if (numVertices == 0) { // we are ready, but there are no triangles in the mesh. success = true; } else { success = true; // the mesh will be in voxel-space. transform it into object-space result.append(_mesh->map( [=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, [=](glm::vec3 color){ return color; }, [=](glm::vec3 normal){ return glm::normalize(glm::vec3(transform * glm::vec4(normal, 0.0f))); }, [&](uint32_t index){ return index; } )); } }); return result; } using namespace render; using namespace render::entities; static uint8_t CUSTOM_PIPELINE_NUMBER; // forward, shadow, fade, wireframe static std::map, ShapePipelinePointer> _pipelines; static gpu::Stream::FormatPointer _vertexFormat; ShapePipelinePointer shapePipelineFactory(const ShapePlumber& plumber, const ShapeKey& key, RenderArgs* args) { if (_pipelines.empty()) { using namespace shader::entities_renderer::program; // forward, shadow, fade static const std::vector> keys = { std::make_tuple(false, false, false, polyvox), std::make_tuple(true, false, false, polyvox_forward), std::make_tuple(false, true, false, polyvox_shadow), // no such thing as forward + shadow #ifdef POLYVOX_ENTITY_USE_FADE_EFFECT std::make_tuple(false, false, true, polyvox_fade), std::make_tuple(false, true, true, polyvox_shadow_fade), // no such thing as forward + fade/shadow #else std::make_tuple(false, false, true, polyvox), std::make_tuple(false, true, true, polyvox_shadow), // no such thing as forward + fade/shadow #endif }; for (auto& key : keys) { for (int i = 0; i < 2; ++i) { bool wireframe = i != 0; auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); state->setDepthTest(true, true, gpu::LESS_EQUAL); PrepareStencil::testMaskDrawShape(*state); if (wireframe) { state->setFillMode(gpu::State::FILL_LINE); } auto pipeline = gpu::Pipeline::create(gpu::Shader::createProgram(std::get<3>(key)), state); if (!std::get<2>(key)) { _pipelines[std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key), wireframe)] = std::make_shared(pipeline, nullptr, nullptr, nullptr); } else { _pipelines[std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key), wireframe)] = std::make_shared(pipeline, nullptr, FadeEffect::getBatchSetter(), FadeEffect::getItemUniformSetter()); } } } } return _pipelines[std::make_tuple(args->_renderMethod == Args::RenderMethod::FORWARD, args->_renderMode == Args::RenderMode::SHADOW_RENDER_MODE, key.isFaded(), key.isWireframe())]; } PolyVoxEntityRenderer::PolyVoxEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { static std::once_flag once; std::call_once(once, [&] { CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); _vertexFormat = std::make_shared(); _vertexFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); _vertexFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 12); }); _params = std::make_shared(sizeof(glm::vec4), nullptr); } ShapeKey PolyVoxEntityRenderer::getShapeKey() { auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER); if (_primitiveMode == PrimitiveMode::LINES) { builder.withWireframe(); } return builder.build(); } bool PolyVoxEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { if (resultWithReadLock([&] { if (entity->voxelToLocalMatrix() != _lastVoxelToLocalMatrix) { return true; } if (entity->_mesh != _mesh) { return true; } return false; })) { return true; } return Parent::needsRenderUpdateFromTypedEntity(entity); } void PolyVoxEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { #ifdef POLYVOX_ENTITY_USE_FADE_EFFECT if (!_hasTransitioned) { transaction.resetTransitionOnItem(_renderItemID, render::Transition::ELEMENT_ENTER_DOMAIN); _hasTransitioned = true; } #endif } void PolyVoxEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { _lastVoxelToLocalMatrix = entity->voxelToLocalMatrix(); bool success; _position = entity->getCenterPosition(success); _orientation = entity->getBillboardMode() == BillboardMode::NONE ? entity->getWorldOrientation() : entity->getLocalOrientation(); _lastVoxelVolumeSize = entity->getVoxelVolumeSize(); _params->setSubData(0, vec4(_lastVoxelVolumeSize, 0.0)); graphics::MeshPointer newMesh; entity->withReadLock([&] { newMesh = entity->_mesh; }); if (newMesh && newMesh->getIndexBuffer()._buffer) { _mesh = newMesh; } std::array xyzTextureURLs{ { entity->getXTextureURL(), entity->getYTextureURL(), entity->getZTextureURL() } }; for (size_t i = 0; i < xyzTextureURLs.size(); ++i) { auto& texture = _xyzTextures[i]; const auto& textureURL = xyzTextureURLs[i]; if (textureURL.isEmpty()) { texture.reset(); } else if (!texture || texture->getURL() != QUrl(textureURL)) { texture = DependencyManager::get()->getTexture(textureURL); } } } void PolyVoxEntityRenderer::doRender(RenderArgs* args) { if (!_mesh || !_mesh->getIndexBuffer()._buffer) { return; } PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); gpu::Batch& batch = *args->_batch; bool usePrimaryFrustum = args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE || args->_mirrorDepth > 0; glm::mat4 rotation = glm::mat4_cast(BillboardModeHelpers::getBillboardRotation(_position, _orientation, _billboardMode, usePrimaryFrustum ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition())); Transform transform(glm::translate(_position) * rotation * _lastVoxelToLocalMatrix); batch.setModelTransform(transform); batch.setInputFormat(_vertexFormat); batch.setInputBuffer(gpu::Stream::POSITION, _mesh->getVertexBuffer()._buffer, 0, sizeof(PolyVox::PositionMaterialNormal)); batch.setIndexBuffer(gpu::UINT32, _mesh->getIndexBuffer()._buffer, 0); for (size_t i = 0; i < _xyzTextures.size(); ++i) { const auto& texture = _xyzTextures[i]; if (texture) { batch.setResourceTexture((uint32_t)i, texture->getGPUTexture()); } else { batch.setResourceTexture((uint32_t)i, DependencyManager::get()->getWhiteTexture()); } } batch.setUniformBuffer(0, _params); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)_mesh->getNumIndices(), 0); } QDebug operator<<(QDebug debug, PolyVoxState state) { switch (state) { case PolyVoxState::Ready: debug << "Ready"; break; case PolyVoxState::Uncompressing: debug << "Uncompressing"; break; case PolyVoxState::UncompressingFinished: debug << "UncompressingFinished"; break; case PolyVoxState::BakingMesh: debug << "BakingMesh"; break; case PolyVoxState::BakingMeshFinished: debug << "BakingMeshFinished"; break; case PolyVoxState::BakingMeshNoCompress: debug << "BakingMeshNoCompress"; break; case PolyVoxState::BakingMeshNoCompressFinished: debug << "BakingMeshNoCompressFinished"; break; case PolyVoxState::Compressing: debug << "Compressing"; break; case PolyVoxState::CompressingFinished: debug << "CompressingFinished"; break; case PolyVoxState::BakingShape: debug << "BakingShape"; break; case PolyVoxState::BakingShapeFinished: debug << "BakingShapeFinished"; break; } return debug; }