// // RenderableModelEntityItem.cpp // interface/src // // Created by Brad Hefta-Gaub on 8/6/14. // Copyright 2014 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 "EntityTreeRenderer.h" #include "RenderableModelEntityItem.h" EntityItem* RenderableModelEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return new RenderableModelEntityItem(entityID, properties); } RenderableModelEntityItem::~RenderableModelEntityItem() { assert(_myRenderer || !_model); // if we have a model, we need to know our renderer if (_myRenderer && _model) { _myRenderer->releaseModel(_model); _model = NULL; } } bool RenderableModelEntityItem::setProperties(const EntityItemProperties& properties) { QString oldModelURL = getModelURL(); bool somethingChanged = ModelEntityItem::setProperties(properties); if (somethingChanged && oldModelURL != getModelURL()) { _needsModelReload = true; } return somethingChanged; } int RenderableModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData) { QString oldModelURL = getModelURL(); int bytesRead = ModelEntityItem::readEntitySubclassDataFromBuffer(data, bytesLeftToRead, args, propertyFlags, overwriteLocalData); if (oldModelURL != getModelURL()) { _needsModelReload = true; } return bytesRead; } void RenderableModelEntityItem::remapTextures() { if (!_model) { return; // nothing to do if we don't have a model } if (!_model->isLoadedWithTextures()) { return; // nothing to do if the model has not yet loaded its default textures } if (!_originalTexturesRead && _model->isLoadedWithTextures()) { const QSharedPointer& networkGeometry = _model->getGeometry(); if (networkGeometry) { _originalTextures = networkGeometry->getTextureNames(); _originalTexturesRead = true; } } if (_currentTextures == _textures) { return; // nothing to do if our recently mapped textures match our desired textures } // since we're changing here, we need to run through our current texture map // and any textures in the recently mapped texture, that is not in our desired // textures, we need to "unset" QJsonDocument currentTexturesAsJson = QJsonDocument::fromJson(_currentTextures.toUtf8()); QJsonObject currentTexturesAsJsonObject = currentTexturesAsJson.object(); QVariantMap currentTextureMap = currentTexturesAsJsonObject.toVariantMap(); QJsonDocument texturesAsJson = QJsonDocument::fromJson(_textures.toUtf8()); QJsonObject texturesAsJsonObject = texturesAsJson.object(); QVariantMap textureMap = texturesAsJsonObject.toVariantMap(); foreach(const QString& key, currentTextureMap.keys()) { // if the desired texture map (what we're setting the textures to) doesn't // contain this texture, then remove it by setting the URL to null if (!textureMap.contains(key)) { QUrl noURL; qDebug() << "Removing texture named" << key << "by replacing it with no URL"; _model->setTextureWithNameToURL(key, noURL); } } // here's where we remap any textures if needed... foreach(const QString& key, textureMap.keys()) { QUrl newTextureURL = textureMap[key].toUrl(); qDebug() << "Updating texture named" << key << "to texture at URL" << newTextureURL; _model->setTextureWithNameToURL(key, newTextureURL); } _currentTextures = _textures; } void RenderableModelEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RMEIrender"); assert(getType() == EntityTypes::Model); bool drawAsModel = hasModel(); glm::vec3 position = getPosition(); glm::vec3 dimensions = getDimensions(); float size = glm::length(dimensions); if (drawAsModel) { remapTextures(); glPushMatrix(); { float alpha = getLocalRenderAlpha(); if (!_model || _needsModelReload) { // TODO: this getModel() appears to be about 3% of model render time. We should optimize PerformanceTimer perfTimer("getModel"); EntityTreeRenderer* renderer = static_cast(args->_renderer); getModel(renderer); } if (_model) { // handle animations.. if (hasAnimation()) { if (!jointsMapped()) { QStringList modelJointNames = _model->getJointNames(); mapJoints(modelJointNames); } if (jointsMapped()) { QVector frameData = getAnimationFrame(); for (int i = 0; i < frameData.size(); i++) { _model->setJointState(i, true, frameData[i]); } } } glm::quat rotation = getRotation(); bool movingOrAnimating = isMoving() || isAnimatingSomething(); if ((movingOrAnimating || _needsInitialSimulation) && _model->isActive()) { _model->setScaleToFit(true, dimensions); _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); _model->setRotation(rotation); _model->setTranslation(position); // make sure to simulate so everything gets set up correctly for rendering { PerformanceTimer perfTimer("_model->simulate"); _model->simulate(0.0f); } _needsInitialSimulation = false; } if (_model->isActive()) { // TODO: this is the majority of model render time. And rendering of a cube model vs the basic Box render // is significantly more expensive. Is there a way to call this that doesn't cost us as much? PerformanceTimer perfTimer("model->render"); // filter out if not needed to render if (args && (args->_renderMode == RenderArgs::SHADOW_RENDER_MODE)) { // if (movingOrAnimating) { { _model->renderInScene(alpha, args); } } else { _model->renderInScene(alpha, args); } } else { // if we couldn't get a model, then just draw a cube glm::vec4 color(getColor()[RED_INDEX]/255, getColor()[GREEN_INDEX]/255, getColor()[BLUE_INDEX]/255, 1.0f); glPushMatrix(); glTranslatef(position.x, position.y, position.z); DependencyManager::get()->renderWireCube(size, color); glPopMatrix(); } } else { // if we couldn't get a model, then just draw a cube glm::vec4 color(getColor()[RED_INDEX]/255, getColor()[GREEN_INDEX]/255, getColor()[BLUE_INDEX]/255, 1.0f); glPushMatrix(); glTranslatef(position.x, position.y, position.z); DependencyManager::get()->renderWireCube(size, color); glPopMatrix(); } } glPopMatrix(); } else { glm::vec4 color(getColor()[RED_INDEX]/255, getColor()[GREEN_INDEX]/255, getColor()[BLUE_INDEX]/255, 1.0f); glPushMatrix(); glTranslatef(position.x, position.y, position.z); DependencyManager::get()->renderWireCube(size, color); glPopMatrix(); } } Model* RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { Model* result = NULL; // make sure our renderer is setup if (!_myRenderer) { _myRenderer = renderer; } assert(_myRenderer == renderer); // you should only ever render on one renderer if (QThread::currentThread() != _myRenderer->thread()) { return _model; } _needsModelReload = false; // this is the reload // if we have a URL, then we will want to end up returning a model... if (!getModelURL().isEmpty()) { // if we have a previously allocated model, but its URL doesn't match // then we need to let our renderer update our model for us. if (_model && QUrl(getModelURL()) != _model->getURL()) { result = _model = _myRenderer->updateModel(_model, getModelURL(), getCollisionModelURL()); _needsInitialSimulation = true; } else if (!_model) { // if we don't yet have a model, then we want our renderer to allocate one result = _model = _myRenderer->allocateModel(getModelURL(), getCollisionModelURL()); _needsInitialSimulation = true; } else { // we already have the model we want... result = _model; } } else { // if our desired URL is empty, we may need to delete our existing model if (_model) { _myRenderer->releaseModel(_model); result = _model = NULL; _needsInitialSimulation = true; } } return result; } bool RenderableModelEntityItem::needsToCallUpdate() const { return _needsInitialSimulation || ModelEntityItem::needsToCallUpdate(); } EntityItemProperties RenderableModelEntityItem::getProperties() const { EntityItemProperties properties = ModelEntityItem::getProperties(); // get the properties from our base class if (_originalTexturesRead) { properties.setTextureNames(_originalTextures); } return properties; } bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElement*& element, float& distance, BoxFace& face, void** intersectedObject, bool precisionPicking) const { if (!_model) { return true; } //qDebug() << "RenderableModelEntityItem::findDetailedRayIntersection() precisionPicking:" << precisionPicking; QString extraInfo; return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, extraInfo, precisionPicking); } void RenderableModelEntityItem::setCollisionModelURL(const QString& url) { ModelEntityItem::setCollisionModelURL(url); if (_model) { _model->setCollisionModelURL(QUrl(url)); } } bool RenderableModelEntityItem::hasCollisionModel() const { if (_model) { return ! _model->getCollisionURL().isEmpty(); } else { return !_collisionModelURL.isEmpty(); } } const QString& RenderableModelEntityItem::getCollisionModelURL() const { // assert (!_model || _collisionModelURL == _model->getCollisionURL().toString()); return _collisionModelURL; } bool RenderableModelEntityItem::isReadyToComputeShape() { if (!_model) { return false; // hmm... } if (_model->getCollisionURL().isEmpty()) { // no collision-model url, so we're ready to compute a shape (of type None). return true; } const QSharedPointer collisionNetworkGeometry = _model->getCollisionGeometry(); if (! collisionNetworkGeometry.isNull() && collisionNetworkGeometry->isLoadedWithTextures()) { // we have a _collisionModelURL AND a collisionNetworkGeometry AND it's fully loaded. return true; } // the model is still being downloaded. return false; } void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { if (_model->getCollisionURL().isEmpty()) { info.setParams(getShapeType(), 0.5f * getDimensions()); } else { const QSharedPointer collisionNetworkGeometry = _model->getCollisionGeometry(); const FBXGeometry& collisionGeometry = collisionNetworkGeometry->getFBXGeometry(); const QSharedPointer renderNetworkGeometry = _model->getGeometry(); const FBXGeometry& renderGeometry = renderNetworkGeometry->getFBXGeometry(); _points.clear(); unsigned int i = 0; // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { QVector pointsInPart; // run through all the triangles and (uniquely) add each point to the hull unsigned int triangleCount = meshPart.triangleIndices.size() / 3; for (unsigned int j = 0; j < triangleCount; j++) { unsigned int p0Index = meshPart.triangleIndices[j*3]; unsigned int p1Index = meshPart.triangleIndices[j*3+1]; unsigned int p2Index = meshPart.triangleIndices[j*3+2]; glm::vec3 p0 = mesh.vertices[p0Index]; glm::vec3 p1 = mesh.vertices[p1Index]; glm::vec3 p2 = mesh.vertices[p2Index]; if (!pointsInPart.contains(p0)) { pointsInPart << p0; } if (!pointsInPart.contains(p1)) { pointsInPart << p1; } if (!pointsInPart.contains(p2)) { pointsInPart << p2; } } // run through all the quads and (uniquely) add each point to the hull unsigned int quadCount = meshPart.quadIndices.size() / 4; assert((unsigned int)meshPart.quadIndices.size() == quadCount*4); for (unsigned int j = 0; j < quadCount; j++) { unsigned int p0Index = meshPart.quadIndices[j*4]; unsigned int p1Index = meshPart.quadIndices[j*4+1]; unsigned int p2Index = meshPart.quadIndices[j*4+2]; unsigned int p3Index = meshPart.quadIndices[j*4+3]; glm::vec3 p0 = mesh.vertices[p0Index]; glm::vec3 p1 = mesh.vertices[p1Index]; glm::vec3 p2 = mesh.vertices[p2Index]; glm::vec3 p3 = mesh.vertices[p3Index]; if (!pointsInPart.contains(p0)) { pointsInPart << p0; } if (!pointsInPart.contains(p1)) { pointsInPart << p1; } if (!pointsInPart.contains(p2)) { pointsInPart << p2; } if (!pointsInPart.contains(p3)) { pointsInPart << p3; } } if (pointsInPart.size() == 0) { qDebug() << "Warning -- meshPart has no faces"; continue; } // add next convex hull QVector newMeshPoints; _points << newMeshPoints; // add points to the new convex hull _points[i++] << pointsInPart; } } // We expect that the collision model will have the same units and will be displaced // from its origin in the same way the visual model is. The visual model has // been centered and probably scaled. We take the scaling and offset which were applied // to the visual model and apply them to the collision model (without regard for the // collision model's extents). glm::vec3 scale = _dimensions / renderGeometry.getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine for (int i = 0; i < _points.size(); i++) { for (int j = 0; j < _points[i].size(); j++) { // compensate for registraion _points[i][j] += _model->getOffset(); // scale so the collision points match the model points _points[i][j] *= scale; } } info.setParams(getShapeType(), _dimensions, _collisionModelURL); info.setConvexHulls(_points); } } ShapeType RenderableModelEntityItem::getShapeType() const { // XXX make hull an option in edit.js ? if (!_model || _model->getCollisionURL().isEmpty()) { return _shapeType; } else if (_points.size() == 1) { return SHAPE_TYPE_CONVEX_HULL; } else { return SHAPE_TYPE_COMPOUND; } }