// // Model.cpp // interface/src/renderer // // Created by Andrzej Kapolka on 10/18/13. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include #include #include "AbstractViewStateInterface.h" #include "MeshPartPayload.h" #include "Model.h" #include "RenderUtilsLogging.h" #include using namespace std; int nakedModelPointerTypeId = qRegisterMetaType(); int weakGeometryResourceBridgePointerTypeId = qRegisterMetaType(); int vec3VectorTypeId = qRegisterMetaType >(); float Model::FAKE_DIMENSION_PLACEHOLDER = -1.0f; #define HTTP_INVALID_COM "http://invalid.com" const int NUM_COLLISION_HULL_COLORS = 24; std::vector _collisionMaterials; void initCollisionMaterials() { // generates bright colors in red, green, blue, yellow, magenta, and cyan spectrums // (no browns, greys, or dark shades) float component[NUM_COLLISION_HULL_COLORS] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.2f, 0.4f, 0.6f, 0.8f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.8f, 0.6f, 0.4f, 0.2f }; _collisionMaterials.reserve(NUM_COLLISION_HULL_COLORS); // each component gets the same cuve // but offset by a multiple of one third the full width int numComponents = 3; int sectionWidth = NUM_COLLISION_HULL_COLORS / numComponents; int greenPhase = sectionWidth; int bluePhase = 2 * sectionWidth; // we stride through the colors to scatter adjacent shades // so they don't tend to group together for large models for (int i = 0; i < sectionWidth; ++i) { for (int j = 0; j < numComponents; ++j) { model::MaterialPointer material; material = std::make_shared(); int index = j * sectionWidth + i; float red = component[index]; float green = component[(index + greenPhase) % NUM_COLLISION_HULL_COLORS]; float blue = component[(index + bluePhase) % NUM_COLLISION_HULL_COLORS]; material->setAlbedo(glm::vec3(red, green, blue)); material->setMetallic(0.02f); material->setRoughness(0.5f); _collisionMaterials.push_back(material); } } } Model::Model(RigPointer rig, QObject* parent, SpatiallyNestable* spatiallyNestableOverride) : QObject(parent), _renderGeometry(), _collisionGeometry(), _renderWatcher(_renderGeometry), _spatiallyNestableOverride(spatiallyNestableOverride), _translation(0.0f), _rotation(), _scale(1.0f, 1.0f, 1.0f), _scaleToFit(false), _scaleToFitDimensions(1.0f), _scaledToFit(false), _snapModelToRegistrationPoint(false), _snappedToRegistrationPoint(false), _url(HTTP_INVALID_COM), _isVisible(true), _blendNumber(0), _appliedBlendNumber(0), _calculatedMeshPartBoxesValid(false), _calculatedMeshBoxesValid(false), _calculatedMeshTrianglesValid(false), _isWireframe(false), _rig(rig) { // we may have been created in the network thread, but we live in the main thread if (_viewState) { moveToThread(_viewState->getMainThread()); } setSnapModelToRegistrationPoint(true, glm::vec3(0.5f)); connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, &Model::loadURLFinished); } Model::~Model() { deleteGeometry(); } AbstractViewStateInterface* Model::_viewState = NULL; bool Model::needsFixupInScene() const { return (_needsFixupInScene || !_addedToScene) && !_needsReload && isLoaded(); } // TODO?: should we combine translation and rotation into single method to avoid double-work? // (figure out where we call these) void Model::setTranslation(const glm::vec3& translation) { _translation = translation; updateRenderItems(); } void Model::setRotation(const glm::quat& rotation) { _rotation = rotation; updateRenderItems(); } Transform Model::getTransform() const { if (_spatiallyNestableOverride) { bool success; Transform transform = _spatiallyNestableOverride->getTransform(success); if (success) { transform.setScale(getScale()); return transform; } } Transform transform; transform.setScale(getScale()); transform.setTranslation(getTranslation()); transform.setRotation(getRotation()); return transform; } void Model::setScale(const glm::vec3& scale) { setScaleInternal(scale); // if anyone sets scale manually, then we are no longer scaled to fit _scaleToFit = false; _scaledToFit = false; } const float SCALE_CHANGE_EPSILON = 0.01f; void Model::setScaleInternal(const glm::vec3& scale) { if (glm::distance(_scale, scale) > SCALE_CHANGE_EPSILON) { _scale = scale; if (_scale.x == 0.0f || _scale.y == 0.0f || _scale.z == 0.0f) { assert(false); } simulate(0.0f, true); } } void Model::setOffset(const glm::vec3& offset) { _offset = offset; // if someone manually sets our offset, then we are no longer snapped to center _snapModelToRegistrationPoint = false; _snappedToRegistrationPoint = false; } void Model::calculateTextureInfo() { if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItems.isEmpty()) { size_t textureSize = 0; int textureCount = 0; bool allTexturesLoaded = true; foreach(auto renderItem, _modelMeshRenderItemsSet) { auto meshPart = renderItem.get(); textureSize += meshPart->getMaterialTextureSize(); textureCount += meshPart->getMaterialTextureCount(); allTexturesLoaded = allTexturesLoaded & meshPart->hasTextureInfo(); } _renderInfoTextureSize = textureSize; _renderInfoTextureCount = textureCount; _hasCalculatedTextureInfo = allTexturesLoaded; // only do this once } } size_t Model::getRenderInfoTextureSize() { calculateTextureInfo(); return _renderInfoTextureSize; } int Model::getRenderInfoTextureCount() { calculateTextureInfo(); return _renderInfoTextureCount; } void Model::updateRenderItems() { if (!_addedToScene) { return; } glm::vec3 scale = getScale(); if (_collisionGeometry) { // _collisionGeometry is already scaled scale = glm::vec3(1.0f); } _needsUpdateClusterMatrices = true; _renderItemsNeedUpdate = false; // queue up this work for later processing, at the end of update and just before rendering. // the application will ensure only the last lambda is actually invoked. void* key = (void*)this; std::weak_ptr weakSelf = shared_from_this(); AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf, scale]() { // do nothing, if the model has already been destroyed. auto self = weakSelf.lock(); if (!self) { return; } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; render::PendingChanges pendingChanges; foreach (auto itemID, self->_modelMeshRenderItems.keys()) { pendingChanges.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->_deleteGeometryCounter) { Transform modelTransform = data._model->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. data._model->updateClusterMatrices(); // update the model transform and bounding box for this render item. const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices); } } }); } // collision mesh does not share the same unit scale as the FBX file's mesh: only apply offset Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); Transform modelTransform = self->getTransform(); foreach (auto itemID, self->_collisionRenderItems.keys()) { pendingChanges.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. data.updateTransform(modelTransform, collisionMeshOffset); }); } scene->enqueuePendingChanges(pendingChanges); }); } void Model::initJointTransforms() { if (isLoaded()) { glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); _rig->setModelOffset(modelOffset); } } void Model::init() { } void Model::reset() { if (isLoaded()) { const FBXGeometry& geometry = getFBXGeometry(); _rig->reset(geometry); } } bool Model::updateGeometry() { bool needFullUpdate = false; if (!isLoaded()) { return false; } _needsReload = false; if (_rig->jointStatesEmpty() && getFBXGeometry().joints.size() > 0) { initJointStates(); assert(_meshStates.empty()); const FBXGeometry& fbxGeometry = getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; state.clusterMatrices.resize(mesh.clusters.size()); _meshStates.append(state); // Note: we add empty buffers for meshes that lack blendshapes so we can access the buffers by index // later in ModelMeshPayload, however the vast majority of meshes will not have them. // TODO? make _blendedVertexBuffers a map instead of vector and only add for meshes with blendshapes? auto buffer = std::make_shared(); if (!mesh.blendshapes.isEmpty()) { buffer->resize((mesh.vertices.size() + mesh.normals.size()) * sizeof(glm::vec3)); buffer->setSubData(0, mesh.vertices.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.vertices.constData()); buffer->setSubData(mesh.vertices.size() * sizeof(glm::vec3), mesh.normals.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.normals.constData()); } _blendedVertexBuffers.push_back(buffer); } needFullUpdate = true; } return needFullUpdate; } // virtual void Model::initJointStates() { const FBXGeometry& geometry = getFBXGeometry(); glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); _rig->initJointStates(geometry, modelOffset); } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo, bool pickAgainstTriangles) { bool intersectedSomething = false; // if we aren't active, we can't ray pick yet... if (!isActive()) { return intersectedSomething; } // extents is the entity relative, scaled, centered extents of the entity glm::vec3 position = _translation; glm::mat4 rotation = glm::mat4_cast(_rotation); glm::mat4 translation = glm::translate(position); glm::mat4 modelToWorldMatrix = translation * rotation; glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); Extents modelExtents = getMeshExtents(); // NOTE: unrotated glm::vec3 dimensions = modelExtents.maximum - modelExtents.minimum; glm::vec3 corner = -(dimensions * _registrationPoint); // since we're going to do the ray picking in the model frame of reference AABox modelFrameBox(corner, dimensions); glm::vec3 modelFrameOrigin = glm::vec3(worldToModelMatrix * glm::vec4(origin, 1.0f)); glm::vec3 modelFrameDirection = glm::vec3(worldToModelMatrix * glm::vec4(direction, 0.0f)); // we can use the AABox's ray intersection by mapping our origin and direction into the model frame // and testing intersection there. if (modelFrameBox.findRayIntersection(modelFrameOrigin, modelFrameDirection, distance, face, surfaceNormal)) { float bestDistance = std::numeric_limits::max(); float distanceToSubMesh; BoxFace subMeshFace; glm::vec3 subMeshSurfaceNormal; int subMeshIndex = 0; const FBXGeometry& geometry = getFBXGeometry(); // If we hit the models box, then consider the submeshes... _mutex.lock(); if (!_calculatedMeshBoxesValid || (pickAgainstTriangles && !_calculatedMeshTrianglesValid)) { recalculateMeshBoxes(pickAgainstTriangles); } for (const auto& subMeshBox : _calculatedMeshBoxes) { if (subMeshBox.findRayIntersection(origin, direction, distanceToSubMesh, subMeshFace, subMeshSurfaceNormal)) { if (distanceToSubMesh < bestDistance) { if (pickAgainstTriangles) { // check our triangles here.... const QVector& meshTriangles = _calculatedMeshTriangles[subMeshIndex]; for(const auto& triangle : meshTriangles) { float thisTriangleDistance; if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { if (thisTriangleDistance < bestDistance) { bestDistance = thisTriangleDistance; intersectedSomething = true; face = subMeshFace; surfaceNormal = triangle.getNormal(); extraInfo = geometry.getModelNameOfMesh(subMeshIndex); } } } } else { // this is the non-triangle picking case... bestDistance = distanceToSubMesh; intersectedSomething = true; face = subMeshFace; surfaceNormal = subMeshSurfaceNormal; extraInfo = geometry.getModelNameOfMesh(subMeshIndex); } } } subMeshIndex++; } _mutex.unlock(); if (intersectedSomething) { distance = bestDistance; } return intersectedSomething; } return intersectedSomething; } bool Model::convexHullContains(glm::vec3 point) { // if we aren't active, we can't compute that yet... if (!isActive()) { return false; } // extents is the entity relative, scaled, centered extents of the entity glm::vec3 position = _translation; glm::mat4 rotation = glm::mat4_cast(_rotation); glm::mat4 translation = glm::translate(position); glm::mat4 modelToWorldMatrix = translation * rotation; glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); Extents modelExtents = getMeshExtents(); // NOTE: unrotated glm::vec3 dimensions = modelExtents.maximum - modelExtents.minimum; glm::vec3 corner = -(dimensions * _registrationPoint); AABox modelFrameBox(corner, dimensions); glm::vec3 modelFramePoint = glm::vec3(worldToModelMatrix * glm::vec4(point, 1.0f)); // we can use the AABox's contains() by mapping our point into the model frame // and testing there. if (modelFrameBox.contains(modelFramePoint)){ _mutex.lock(); if (!_calculatedMeshTrianglesValid) { recalculateMeshBoxes(true); } // If we are inside the models box, then consider the submeshes... int subMeshIndex = 0; foreach(const AABox& subMeshBox, _calculatedMeshBoxes) { if (subMeshBox.contains(point)) { bool insideMesh = true; // To be inside the sub mesh, we need to be behind every triangles' planes const QVector& meshTriangles = _calculatedMeshTriangles[subMeshIndex]; foreach (const Triangle& triangle, meshTriangles) { if (!isPointBehindTrianglesPlane(point, triangle.v0, triangle.v1, triangle.v2)) { // it's not behind at least one so we bail insideMesh = false; break; } } if (insideMesh) { // It's inside this mesh, return true. _mutex.unlock(); return true; } } subMeshIndex++; } _mutex.unlock(); } // It wasn't in any mesh, return false. return false; } // TODO: we seem to call this too often when things haven't actually changed... look into optimizing this // Any script might trigger findRayIntersectionAgainstSubMeshes (and maybe convexHullContains), so these // can occur multiple times. In addition, rendering does it's own ray picking in order to decide which // entity-scripts to call. I think it would be best to do the picking once-per-frame (in cpu, or gpu if possible) // and then the calls use the most recent such result. void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { PROFILE_RANGE(render, __FUNCTION__); bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { const FBXGeometry& geometry = getFBXGeometry(); int numberOfMeshes = geometry.meshes.size(); _calculatedMeshBoxes.resize(numberOfMeshes); _calculatedMeshTriangles.clear(); _calculatedMeshTriangles.resize(numberOfMeshes); _calculatedMeshPartBoxes.clear(); for (int i = 0; i < numberOfMeshes; i++) { const FBXMesh& mesh = geometry.meshes.at(i); Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents, _translation, _rotation); _calculatedMeshBoxes[i] = AABox(scaledMeshExtents); if (pickAgainstTriangles) { QVector thisMeshTriangles; for (int j = 0; j < mesh.parts.size(); j++) { const FBXMeshPart& part = mesh.parts.at(j); bool atLeastOnePointInBounds = false; AABox thisPartBounds; const int INDICES_PER_TRIANGLE = 3; const int INDICES_PER_QUAD = 4; if (part.quadIndices.size() > 0) { int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD; int vIndex = 0; for (int q = 0; q < numberOfQuads; q++) { int i0 = part.quadIndices[vIndex++]; int i1 = part.quadIndices[vIndex++]; int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); atLeastOnePointInBounds = true; } else { thisPartBounds += mv0; } thisPartBounds += mv1; thisPartBounds += mv2; thisPartBounds += mv3; glm::vec3 v0 = calculateScaledOffsetPoint(mv0); glm::vec3 v1 = calculateScaledOffsetPoint(mv1); glm::vec3 v2 = calculateScaledOffsetPoint(mv2); glm::vec3 v3 = calculateScaledOffsetPoint(mv3); // Sam's recommended triangle slices Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; // NOTE: Random guy on the internet's recommended triangle slices //Triangle tri1 = { v0, v1, v2 }; //Triangle tri2 = { v2, v3, v0 }; thisMeshTriangles.push_back(tri1); thisMeshTriangles.push_back(tri2); } } if (part.triangleIndices.size() > 0) { int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE; int vIndex = 0; for (int t = 0; t < numberOfTris; t++) { int i0 = part.triangleIndices[vIndex++]; int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); atLeastOnePointInBounds = true; } else { thisPartBounds += mv0; } thisPartBounds += mv1; thisPartBounds += mv2; glm::vec3 v0 = calculateScaledOffsetPoint(mv0); glm::vec3 v1 = calculateScaledOffsetPoint(mv1); glm::vec3 v2 = calculateScaledOffsetPoint(mv2); Triangle tri = { v0, v1, v2 }; thisMeshTriangles.push_back(tri); } } _calculatedMeshPartBoxes[QPair(i, j)] = thisPartBounds; } _calculatedMeshTriangles[i] = thisMeshTriangles; _calculatedMeshPartBoxesValid = true; } } _calculatedMeshBoxesValid = true; _calculatedMeshTrianglesValid = pickAgainstTriangles; } } void Model::renderSetup(RenderArgs* args) { // set up dilated textures on first render after load/simulate const FBXGeometry& geometry = getFBXGeometry(); if (_dilatedTextures.isEmpty()) { foreach (const FBXMesh& mesh, geometry.meshes) { QVector > dilated; dilated.resize(mesh.parts.size()); _dilatedTextures.append(dilated); } } if (!_addedToScene && isLoaded()) { createRenderItemSet(); } } void Model::setVisibleInScene(bool newValue, std::shared_ptr scene) { if (_isVisible != newValue) { _isVisible = newValue; render::PendingChanges pendingChanges; foreach (auto item, _modelMeshRenderItems.keys()) { pendingChanges.resetItem(item, _modelMeshRenderItems[item]); } foreach (auto item, _collisionRenderItems.keys()) { pendingChanges.resetItem(item, _collisionRenderItems[item]); } scene->enqueuePendingChanges(pendingChanges); } } void Model::setLayeredInFront(bool layered, std::shared_ptr scene) { if (_isLayeredInFront != layered) { _isLayeredInFront = layered; render::PendingChanges pendingChanges; foreach(auto item, _modelMeshRenderItems.keys()) { pendingChanges.resetItem(item, _modelMeshRenderItems[item]); } foreach(auto item, _collisionRenderItems.keys()) { pendingChanges.resetItem(item, _collisionRenderItems[item]); } scene->enqueuePendingChanges(pendingChanges); } } bool Model::addToScene(std::shared_ptr scene, render::PendingChanges& pendingChanges, render::Item::Status::Getters& statusGetters) { bool readyToRender = _collisionGeometry || isLoaded(); if (!_addedToScene && readyToRender) { createRenderItemSet(); } bool somethingAdded = false; if (_collisionGeometry) { if (_collisionRenderItems.empty()) { foreach (auto renderItem, _collisionRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); if (statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } pendingChanges.resetItem(item, renderPayload); _collisionRenderItems.insert(item, renderPayload); } somethingAdded = !_collisionRenderItems.empty(); } } else { if (_modelMeshRenderItems.empty()) { bool hasTransparent = false; size_t verticesCount = 0; foreach(auto renderItem, _modelMeshRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); if (statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } pendingChanges.resetItem(item, renderPayload); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); _modelMeshRenderItems.insert(item, renderPayload); _modelMeshRenderItemIDs.emplace_back(item); } somethingAdded = !_modelMeshRenderItems.empty(); _renderInfoVertexCount = verticesCount; _renderInfoDrawCalls = _modelMeshRenderItems.count(); _renderInfoHasTransparent = hasTransparent; } } if (somethingAdded) { _addedToScene = true; updateRenderItems(); _needsFixupInScene = false; } return somethingAdded; } void Model::removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges) { foreach (auto item, _modelMeshRenderItems.keys()) { pendingChanges.removeItem(item); } _modelMeshRenderItemIDs.clear(); _modelMeshRenderItems.clear(); _modelMeshRenderItemsSet.clear(); foreach (auto item, _collisionRenderItems.keys()) { pendingChanges.removeItem(item); } _collisionRenderItems.clear(); _collisionRenderItemsSet.clear(); _addedToScene = false; _renderInfoVertexCount = 0; _renderInfoDrawCalls = 0; _renderInfoTextureSize = 0; _renderInfoHasTransparent = false; } void Model::renderDebugMeshBoxes(gpu::Batch& batch) { int colorNdx = 0; _mutex.lock(); foreach(AABox box, _calculatedMeshBoxes) { if (_debugMeshBoxesID == GeometryCache::UNKNOWN_ID) { _debugMeshBoxesID = DependencyManager::get()->allocateID(); } QVector points; glm::vec3 brn = box.getCorner(); glm::vec3 bln = brn + glm::vec3(box.getDimensions().x, 0, 0); glm::vec3 brf = brn + glm::vec3(0, 0, box.getDimensions().z); glm::vec3 blf = brn + glm::vec3(box.getDimensions().x, 0, box.getDimensions().z); glm::vec3 trn = brn + glm::vec3(0, box.getDimensions().y, 0); glm::vec3 tln = bln + glm::vec3(0, box.getDimensions().y, 0); glm::vec3 trf = brf + glm::vec3(0, box.getDimensions().y, 0); glm::vec3 tlf = blf + glm::vec3(0, box.getDimensions().y, 0); points << brn << bln; points << brf << blf; points << brn << brf; points << bln << blf; points << trn << tln; points << trf << tlf; points << trn << trf; points << tln << tlf; points << brn << trn; points << brf << trf; points << bln << tln; points << blf << tlf; glm::vec4 color[] = { { 1.0f, 0.0f, 0.0f, 1.0f }, // red { 0.0f, 1.0f, 0.0f, 1.0f }, // green { 0.0f, 0.0f, 1.0f, 1.0f }, // blue { 1.0f, 0.0f, 1.0f, 1.0f }, // purple { 1.0f, 1.0f, 0.0f, 1.0f }, // yellow { 0.0f, 1.0f, 1.0f, 1.0f }, // cyan { 1.0f, 1.0f, 1.0f, 1.0f }, // white { 0.0f, 0.5f, 0.0f, 1.0f }, { 0.0f, 0.0f, 0.5f, 1.0f }, { 0.5f, 0.0f, 0.5f, 1.0f }, { 0.5f, 0.5f, 0.0f, 1.0f }, { 0.0f, 0.5f, 0.5f, 1.0f } }; DependencyManager::get()->updateVertices(_debugMeshBoxesID, points, color[colorNdx]); DependencyManager::get()->renderVertices(batch, gpu::LINES, _debugMeshBoxesID); colorNdx++; } _mutex.unlock(); } Extents Model::getBindExtents() const { if (!isActive()) { return Extents(); } const Extents& bindExtents = getFBXGeometry().bindExtents; Extents scaledExtents = { bindExtents.minimum * _scale, bindExtents.maximum * _scale }; return scaledExtents; } Extents Model::getMeshExtents() const { if (!isActive()) { return Extents(); } const Extents& extents = getFBXGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum * _scale, maximum * _scale }; return scaledExtents; } Extents Model::getUnscaledMeshExtents() const { if (!isActive()) { return Extents(); } const Extents& extents = getFBXGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum, maximum }; return scaledExtents; } Extents Model::calculateScaledOffsetExtents(const Extents& extents, glm::vec3 modelPosition, glm::quat modelOrientation) const { // we need to include any fst scaling, translation, and rotation, which is captured in the offset matrix glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledOffsetExtents = { ((minimum + _offset) * _scale), ((maximum + _offset) * _scale) }; Extents rotatedExtents = scaledOffsetExtents.getRotated(modelOrientation); Extents translatedExtents = { rotatedExtents.minimum + modelPosition, rotatedExtents.maximum + modelPosition }; return translatedExtents; } /// Returns the world space equivalent of some box in model space. AABox Model::calculateScaledOffsetAABox(const AABox& box, glm::vec3 modelPosition, glm::quat modelOrientation) const { return AABox(calculateScaledOffsetExtents(Extents(box), modelPosition, modelOrientation)); } glm::vec3 Model::calculateScaledOffsetPoint(const glm::vec3& point) const { // we need to include any fst scaling, translation, and rotation, which is captured in the offset matrix glm::vec3 offsetPoint = glm::vec3(getFBXGeometry().offset * glm::vec4(point, 1.0f)); glm::vec3 scaledPoint = ((offsetPoint + _offset) * _scale); glm::vec3 rotatedPoint = _rotation * scaledPoint; glm::vec3 translatedPoint = rotatedPoint + _translation; return translatedPoint; } void Model::clearJointState(int index) { _rig->clearJointState(index); } void Model::setJointState(int index, bool valid, const glm::quat& rotation, const glm::vec3& translation, float priority) { _rig->setJointState(index, valid, rotation, translation, priority); } void Model::setJointRotation(int index, bool valid, const glm::quat& rotation, float priority) { _rig->setJointRotation(index, valid, rotation, priority); } void Model::setJointTranslation(int index, bool valid, const glm::vec3& translation, float priority) { _rig->setJointTranslation(index, valid, translation, priority); } int Model::getParentJointIndex(int jointIndex) const { return (isActive() && jointIndex != -1) ? getFBXGeometry().joints.at(jointIndex).parentIndex : -1; } int Model::getLastFreeJointIndex(int jointIndex) const { return (isActive() && jointIndex != -1) ? getFBXGeometry().joints.at(jointIndex).freeLineage.last() : -1; } void Model::setTextures(const QVariantMap& textures) { if (isLoaded()) { _needsUpdateTextures = true; _needsFixupInScene = true; _renderGeometry->setTextures(textures); } else { // FIXME(Huffman): Disconnect previously connected lambdas so we don't set textures multiple // after the geometry has finished loading. connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, [this, textures]() { _renderGeometry->setTextures(textures); }); } } void Model::setURL(const QUrl& url) { // don't recreate the geometry if it's the same URL if (_url == url && _renderWatcher.getURL() == url) { return; } _url = url; { render::PendingChanges pendingChanges; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); if (scene) { removeFromScene(scene, pendingChanges); scene->enqueuePendingChanges(pendingChanges); } else { qCWarning(renderutils) << "Model::setURL(), Unexpected null scene, possibly during application shutdown"; } } _needsReload = true; _needsUpdateTextures = true; _visualGeometryRequestFailed = false; _needsFixupInScene = true; invalidCalculatedMeshBoxes(); deleteGeometry(); auto resource = DependencyManager::get()->getGeometryResource(url); resource->setLoadPriority(this, _loadingPriority); _renderWatcher.setResource(resource); onInvalidate(); } void Model::loadURLFinished(bool success) { if (!success) { _visualGeometryRequestFailed = true; } emit setURLFinished(success); } bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { return _rig->getJointPositionInWorldFrame(jointIndex, position, _translation, _rotation); } bool Model::getJointPosition(int jointIndex, glm::vec3& position) const { return _rig->getJointPosition(jointIndex, position); } bool Model::getJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const { return _rig->getJointRotationInWorldFrame(jointIndex, rotation, _rotation); } bool Model::getJointRotation(int jointIndex, glm::quat& rotation) const { return _rig->getJointRotation(jointIndex, rotation); } bool Model::getJointTranslation(int jointIndex, glm::vec3& translation) const { return _rig->getJointTranslation(jointIndex, translation); } bool Model::getAbsoluteJointRotationInRigFrame(int jointIndex, glm::quat& rotationOut) const { return _rig->getAbsoluteJointRotationInRigFrame(jointIndex, rotationOut); } bool Model::getAbsoluteJointTranslationInRigFrame(int jointIndex, glm::vec3& translationOut) const { return _rig->getAbsoluteJointTranslationInRigFrame(jointIndex, translationOut); } bool Model::getRelativeDefaultJointRotation(int jointIndex, glm::quat& rotationOut) const { return _rig->getRelativeDefaultJointRotation(jointIndex, rotationOut); } bool Model::getRelativeDefaultJointTranslation(int jointIndex, glm::vec3& translationOut) const { return _rig->getRelativeDefaultJointTranslation(jointIndex, translationOut); } bool Model::getJointCombinedRotation(int jointIndex, glm::quat& rotation) const { return _rig->getJointCombinedRotation(jointIndex, rotation, _rotation); } QStringList Model::getJointNames() const { if (QThread::currentThread() != thread()) { QStringList result; QMetaObject::invokeMethod(const_cast(this), "getJointNames", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QStringList, result)); return result; } return isActive() ? getFBXGeometry().getJointNames() : QStringList(); } class Blender : public QRunnable { public: Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& meshes, const QVector& blendshapeCoefficients); virtual void run() override; private: ModelPointer _model; int _blendNumber; Geometry::WeakPointer _geometry; QVector _meshes; QVector _blendshapeCoefficients; }; Blender::Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& meshes, const QVector& blendshapeCoefficients) : _model(model), _blendNumber(blendNumber), _geometry(geometry), _meshes(meshes), _blendshapeCoefficients(blendshapeCoefficients) { } void Blender::run() { PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); QVector vertices, normals; if (_model) { int offset = 0; foreach (const FBXMesh& mesh, _meshes) { if (mesh.blendshapes.isEmpty()) { continue; } vertices += mesh.vertices; normals += mesh.normals; glm::vec3* meshVertices = vertices.data() + offset; glm::vec3* meshNormals = normals.data() + offset; offset += mesh.vertices.size(); const float NORMAL_COEFFICIENT_SCALE = 0.01f; for (int i = 0, n = qMin(_blendshapeCoefficients.size(), mesh.blendshapes.size()); i < n; i++) { float vertexCoefficient = _blendshapeCoefficients.at(i); const float EPSILON = 0.0001f; if (vertexCoefficient < EPSILON) { continue; } float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE; const FBXBlendshape& blendshape = mesh.blendshapes.at(i); for (int j = 0; j < blendshape.indices.size(); j++) { int index = blendshape.indices.at(j); meshVertices[index] += blendshape.vertices.at(j) * vertexCoefficient; meshNormals[index] += blendshape.normals.at(j) * normalCoefficient; } } } } // post the result to the geometry cache, which will dispatch to the model if still alive QMetaObject::invokeMethod(DependencyManager::get().data(), "setBlendedVertices", Q_ARG(ModelPointer, _model), Q_ARG(int, _blendNumber), Q_ARG(const Geometry::WeakPointer&, _geometry), Q_ARG(const QVector&, vertices), Q_ARG(const QVector&, normals)); } void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions) { if (_scaleToFit != scaleToFit || _scaleToFitDimensions != dimensions) { _scaleToFit = scaleToFit; _scaleToFitDimensions = dimensions; _scaledToFit = false; // force rescaling } } void Model::setScaleToFit(bool scaleToFit, float largestDimension, bool forceRescale) { // NOTE: if the model is not active, then it means we don't actually know the true/natural dimensions of the // mesh, and so we can't do the needed calculations for scaling to fit to a single largest dimension. In this // case we will record that we do want to do this, but we will stick our desired single dimension into the // first element of the vec3 for the non-fixed aspect ration dimensions if (!isActive()) { _scaleToFit = scaleToFit; if (scaleToFit) { _scaleToFitDimensions = glm::vec3(largestDimension, FAKE_DIMENSION_PLACEHOLDER, FAKE_DIMENSION_PLACEHOLDER); } return; } if (forceRescale || _scaleToFit != scaleToFit || glm::length(_scaleToFitDimensions) != largestDimension) { _scaleToFit = scaleToFit; // we only need to do this work if we're "turning on" scale to fit. if (scaleToFit) { Extents modelMeshExtents = getUnscaledMeshExtents(); float maxDimension = glm::distance(modelMeshExtents.maximum, modelMeshExtents.minimum); float maxScale = largestDimension / maxDimension; glm::vec3 modelMeshDimensions = modelMeshExtents.maximum - modelMeshExtents.minimum; glm::vec3 dimensions = modelMeshDimensions * maxScale; _scaleToFitDimensions = dimensions; _scaledToFit = false; // force rescaling } } } glm::vec3 Model::getScaleToFitDimensions() const { if (_scaleToFitDimensions.y == FAKE_DIMENSION_PLACEHOLDER && _scaleToFitDimensions.z == FAKE_DIMENSION_PLACEHOLDER) { return glm::vec3(_scaleToFitDimensions.x); } return _scaleToFitDimensions; } void Model::scaleToFit() { // If our _scaleToFitDimensions.y/z are FAKE_DIMENSION_PLACEHOLDER then it means our // user asked to scale us in a fixed aspect ratio to a single largest dimension, but // we didn't yet have an active mesh. We can only enter this scaleToFit() in this state // if we now do have an active mesh, so we take this opportunity to actually determine // the correct scale. if (_scaleToFit && _scaleToFitDimensions.y == FAKE_DIMENSION_PLACEHOLDER && _scaleToFitDimensions.z == FAKE_DIMENSION_PLACEHOLDER) { setScaleToFit(_scaleToFit, _scaleToFitDimensions.x); } Extents modelMeshExtents = getUnscaledMeshExtents(); // size is our "target size in world space" // we need to set our model scale so that the extents of the mesh, fit in a box that size... glm::vec3 meshDimensions = modelMeshExtents.maximum - modelMeshExtents.minimum; glm::vec3 rescaleDimensions = _scaleToFitDimensions / meshDimensions; setScaleInternal(rescaleDimensions); _scaledToFit = true; } void Model::setSnapModelToRegistrationPoint(bool snapModelToRegistrationPoint, const glm::vec3& registrationPoint) { glm::vec3 clampedRegistrationPoint = glm::clamp(registrationPoint, 0.0f, 1.0f); if (_snapModelToRegistrationPoint != snapModelToRegistrationPoint || _registrationPoint != clampedRegistrationPoint) { _snapModelToRegistrationPoint = snapModelToRegistrationPoint; _registrationPoint = clampedRegistrationPoint; _snappedToRegistrationPoint = false; // force re-centering } } void Model::snapToRegistrationPoint() { Extents modelMeshExtents = getUnscaledMeshExtents(); glm::vec3 dimensions = (modelMeshExtents.maximum - modelMeshExtents.minimum); glm::vec3 offset = -modelMeshExtents.minimum - (dimensions * _registrationPoint); _offset = offset; _snappedToRegistrationPoint = true; } void Model::simulate(float deltaTime, bool fullUpdate) { PROFILE_RANGE(simulation_detail, __FUNCTION__); PerformanceTimer perfTimer("Model::simulate"); fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) || (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint); if (isActive() && fullUpdate) { // NOTE: This is overly aggressive and we are invalidating the MeshBoxes when in fact they may not be invalid // they really only become invalid if something about the transform to world space has changed. This is // not too bad at this point, because it doesn't impact rendering. However it does slow down ray picking // because ray picking needs valid boxes to work _calculatedMeshBoxesValid = false; _calculatedMeshTrianglesValid = false; onInvalidate(); // check for scale to fit if (_scaleToFit && !_scaledToFit) { scaleToFit(); } if (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint) { snapToRegistrationPoint(); } // update the world space transforms for all joints glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset); updateRig(deltaTime, parentTransform); computeMeshPartLocalBounds(); } } //virtual void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { _needsUpdateClusterMatrices = true; _rig->updateAnimations(deltaTime, parentTransform); } void Model::computeMeshPartLocalBounds() { for (auto& part : _modelMeshRenderItemsSet) { assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); const Model::MeshState& state = _meshStates.at(part->_meshIndex); part->computeAdjustedLocalBound(state.clusterMatrices); } } // virtual void Model::updateClusterMatrices() { PerformanceTimer perfTimer("Model::updateClusterMatrices"); if (!_needsUpdateClusterMatrices || !isLoaded()) { return; } _needsUpdateClusterMatrices = false; const FBXGeometry& geometry = getFBXGeometry(); for (int i = 0; i < _meshStates.size(); i++) { MeshState& state = _meshStates[i]; const FBXMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); auto jointMatrix = _rig->getJointTransform(cluster.jointIndex); #if GLM_ARCH & GLM_ARCH_SSE2 glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); state.clusterMatrices[j] = out; #else state.clusterMatrices[j] = jointMatrix * cluster.inverseBindMatrix; #endif } // Once computed the cluster matrices, update the buffer(s) if (mesh.clusters.size() > 1) { if (!state.clusterBuffer) { state.clusterBuffer = std::make_shared(state.clusterMatrices.size() * sizeof(glm::mat4), (const gpu::Byte*) state.clusterMatrices.constData()); } else { state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4), (const gpu::Byte*) state.clusterMatrices.constData()); } } } // post the blender if we're not currently waiting for one to finish if (geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { _blendedBlendshapeCoefficients = _blendshapeCoefficients; DependencyManager::get()->noteRequiresBlend(getThisPointer()); } } void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority) { const FBXGeometry& geometry = getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(endIndex).freeLineage; glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset); _rig->inverseKinematics(endIndex, targetPosition, targetRotation, priority, freeLineage, parentTransform); } bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) { const FBXGeometry& geometry = getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; return _rig->restoreJointPosition(jointIndex, fraction, priority, freeLineage); } float Model::getLimbLength(int jointIndex) const { const FBXGeometry& geometry = getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; return _rig->getLimbLength(jointIndex, freeLineage, _scale, geometry.joints); } bool Model::maybeStartBlender() { if (isLoaded()) { const FBXGeometry& fbxGeometry = getFBXGeometry(); if (fbxGeometry.hasBlendedMeshes()) { QThreadPool::globalInstance()->start(new Blender(getThisPointer(), ++_blendNumber, _renderGeometry, fbxGeometry.meshes, _blendshapeCoefficients)); return true; } } return false; } void Model::setBlendedVertices(int blendNumber, const Geometry::WeakPointer& geometry, const QVector& vertices, const QVector& normals) { auto geometryRef = geometry.lock(); if (!geometryRef || _renderGeometry != geometryRef || _blendedVertexBuffers.empty() || blendNumber < _appliedBlendNumber) { return; } _appliedBlendNumber = blendNumber; const FBXGeometry& fbxGeometry = getFBXGeometry(); int index = 0; for (int i = 0; i < fbxGeometry.meshes.size(); i++) { const FBXMesh& mesh = fbxGeometry.meshes.at(i); if (mesh.blendshapes.isEmpty()) { continue; } gpu::BufferPointer& buffer = _blendedVertexBuffers[i]; buffer->setSubData(0, mesh.vertices.size() * sizeof(glm::vec3), (gpu::Byte*) vertices.constData() + index*sizeof(glm::vec3)); buffer->setSubData(mesh.vertices.size() * sizeof(glm::vec3), mesh.normals.size() * sizeof(glm::vec3), (gpu::Byte*) normals.constData() + index*sizeof(glm::vec3)); index += mesh.vertices.size(); } } void Model::deleteGeometry() { _deleteGeometryCounter++; _blendedVertexBuffers.clear(); _meshStates.clear(); _rig->destroyAnimGraph(); _blendedBlendshapeCoefficients.clear(); _renderGeometry.reset(); _collisionGeometry.reset(); } AABox Model::getRenderableMeshBound() const { if (!isLoaded()) { return AABox(); } else { // Build a bound using the last known bound from all the renderItems. AABox totalBound; for (auto& renderItem : _modelMeshRenderItemsSet) { totalBound += renderItem->getBound(); } return totalBound; } } const render::ItemIDs& Model::fetchRenderItemIDs() const { return _modelMeshRenderItemIDs; } void Model::createRenderItemSet() { if (_collisionGeometry) { if (_collisionRenderItemsSet.empty()) { createCollisionRenderItemSet(); } } else { if (_modelMeshRenderItemsSet.empty()) { createVisibleRenderItemSet(); } } }; void Model::createVisibleRenderItemSet() { assert(isLoaded()); const auto& meshes = _renderGeometry->getMeshes(); // all of our mesh vectors must match in size if ((int)meshes.size() != _meshStates.size()) { qCDebug(renderlogging) << "WARNING!!!! Mesh Sizes don't match! We will not segregate mesh groups yet."; return; } // We should not have any existing renderItems if we enter this section of code Q_ASSERT(_modelMeshRenderItemsSet.isEmpty()); _modelMeshRenderItemsSet.clear(); Transform transform; transform.setTranslation(_translation); transform.setRotation(_rotation); Transform offset; offset.setScale(_scale); offset.postTranslate(_offset); // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; uint32_t numMeshes = (uint32_t)meshes.size(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { continue; } // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { _modelMeshRenderItemsSet << std::make_shared(this, i, partIndex, shapeID, transform, offset); shapeID++; } } computeMeshPartLocalBounds(); } void Model::createCollisionRenderItemSet() { assert((bool)_collisionGeometry); if (_collisionMaterials.empty()) { initCollisionMaterials(); } const auto& meshes = _collisionGeometry->getMeshes(); // We should not have any existing renderItems if we enter this section of code Q_ASSERT(_collisionRenderItemsSet.isEmpty()); Transform identity; identity.setIdentity(); Transform offset; offset.postTranslate(_offset); // Run through all of the meshes, and place them into their segregated, but unsorted buckets uint32_t numMeshes = (uint32_t)meshes.size(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { continue; } // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { model::MaterialPointer& material = _collisionMaterials[partIndex % NUM_COLLISION_HULL_COLORS]; auto payload = std::make_shared(mesh, partIndex, material); payload->updateTransform(identity, offset); _collisionRenderItemsSet << payload; } } } bool Model::isRenderable() const { return !_meshStates.isEmpty() || (isLoaded() && _renderGeometry->getMeshes().empty()); } bool Model::initWhenReady(render::ScenePointer scene) { // NOTE: this only called by SkeletonModel if (_addedToScene || !isRenderable()) { return false; } createRenderItemSet(); render::PendingChanges pendingChanges; bool addedPendingChanges = false; if (_collisionGeometry) { foreach (auto renderItem, _collisionRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); _collisionRenderItems.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); } addedPendingChanges = !_collisionRenderItems.empty(); } else { bool hasTransparent = false; size_t verticesCount = 0; foreach (auto renderItem, _modelMeshRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); _modelMeshRenderItems.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); } addedPendingChanges = !_modelMeshRenderItems.empty(); _renderInfoVertexCount = verticesCount; _renderInfoDrawCalls = _modelMeshRenderItems.count(); _renderInfoHasTransparent = hasTransparent; } _addedToScene = addedPendingChanges; if (addedPendingChanges) { scene->enqueuePendingChanges(pendingChanges); // NOTE: updateRender items enqueues identical pendingChanges (using a lambda) // so it looks like we're doing double work here, but I don't want to remove the call // for fear there is some side effect we'll miss. -- Andrew 2016.07.21 // TODO: figure out if we really need this call to updateRenderItems() or not. updateRenderItems(); } return true; } class CollisionRenderGeometry : public Geometry { public: CollisionRenderGeometry(model::MeshPointer mesh) { _fbxGeometry = std::make_shared(); std::shared_ptr meshes = std::make_shared(); meshes->push_back(mesh); _meshes = meshes; _meshParts = std::shared_ptr(); } }; void Model::setCollisionMesh(model::MeshPointer mesh) { if (mesh) { _collisionGeometry = std::make_shared(mesh); } else { _collisionGeometry.reset(); } _needsFixupInScene = true; } ModelBlender::ModelBlender() : _pendingBlenders(0) { } ModelBlender::~ModelBlender() { } void ModelBlender::noteRequiresBlend(ModelPointer model) { if (_pendingBlenders < QThread::idealThreadCount()) { if (model->maybeStartBlender()) { _pendingBlenders++; } return; } { Lock lock(_mutex); _modelsRequiringBlends.insert(model); } } void ModelBlender::setBlendedVertices(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& vertices, const QVector& normals) { if (model) { model->setBlendedVertices(blendNumber, geometry, vertices, normals); } _pendingBlenders--; { Lock lock(_mutex); for (auto i = _modelsRequiringBlends.begin(); i != _modelsRequiringBlends.end();) { auto weakPtr = *i; _modelsRequiringBlends.erase(i++); // remove front of the set ModelPointer nextModel = weakPtr.lock(); if (nextModel && nextModel->maybeStartBlender()) { _pendingBlenders++; return; } } } }