/** * @file gltfscenemanager.cpp * @brief Builds menus out of items. * * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2024, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "gltfscenemanager.h" #include "llviewermenufile.h" #include "llappviewer.h" #include "lltinygltfhelper.h" #include "llvertexbuffer.h" #include "llselectmgr.h" #include "llagent.h" #include "llnotificationsutil.h" #include "llvoavatarself.h" #include "llvolumeoctree.h" #include "gltf/asset.h" #include "pipeline.h" #include "llviewershadermgr.h" using namespace LL; // temporary location of LL GLTF Implementation using namespace LL::GLTF; void GLTFSceneManager::load() { LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject(); if (obj) { // Load a scene from disk LLFilePickerReplyThread::startPicker( [](const std::vector& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter) { if (LLAppViewer::instance()->quitRequested()) { return; } if (filenames.size() > 0) { GLTFSceneManager::instance().load(filenames[0]); } }, LLFilePicker::FFLOAD_GLTF, false); } else { LLNotificationsUtil::add("GLTFOpenSelection"); } } void GLTFSceneManager::saveAs() { LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject(); if (obj && obj->mGLTFAsset) { LLFilePickerReplyThread::startPicker( [](const std::vector& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter) { if (LLAppViewer::instance()->quitRequested()) { return; } if (filenames.size() > 0) { GLTFSceneManager::instance().save(filenames[0]); } }, LLFilePicker::FFSAVE_GLTF, "scene.gltf"); } else { LLNotificationsUtil::add("GLTFSaveSelection"); } } void GLTFSceneManager::decomposeSelection() { LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject(); if (obj && obj->mGLTFAsset) { LLFilePickerReplyThread::startPicker( [](const std::vector& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter) { if (LLAppViewer::instance()->quitRequested()) { return; } if (filenames.size() > 0) { GLTFSceneManager::instance().decomposeSelection(filenames[0]); } }, LLFilePicker::FFSAVE_GLTF, "scene.gltf"); } else { LLNotificationsUtil::add("GLTFSaveSelection"); } } void GLTFSceneManager::decomposeSelection(const std::string& filename) { LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject(); if (obj && obj->mGLTFAsset) { // copy asset out for decomposition Asset asset = *obj->mGLTFAsset; // decompose the asset into component parts asset.decompose(filename); // copy decomposed asset into tinygltf for serialization tinygltf::Model model; asset.save(model); LLTinyGLTFHelper::saveModel(filename, model); } } void GLTFSceneManager::save(const std::string& filename) { LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject(); if (obj && obj->mGLTFAsset) { Asset* asset = obj->mGLTFAsset.get(); tinygltf::Model model; asset->save(model); LLTinyGLTFHelper::saveModel(filename, model); } } void GLTFSceneManager::load(const std::string& filename) { tinygltf::Model model; LLTinyGLTFHelper::loadModel(filename, model); std::shared_ptr asset = std::make_shared(); *asset = model; gDebugProgram.bind(); // bind a shader to satisfy LLVertexBuffer assertions asset->allocateGLResources(filename, model); asset->updateTransforms(); // hang the asset off the currently selected object, or off of the avatar if no object is selected LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject(); if (obj) { // assign to self avatar obj->mGLTFAsset = asset; if (std::find(mObjects.begin(), mObjects.end(), obj) == mObjects.end()) { mObjects.push_back(obj); } } } GLTFSceneManager::~GLTFSceneManager() { mObjects.clear(); } void GLTFSceneManager::renderOpaque() { render(true); } void GLTFSceneManager::renderAlpha() { render(false); } void GLTFSceneManager::update() { for (U32 i = 0; i < mObjects.size(); ++i) { if (mObjects[i]->isDead() || mObjects[i]->mGLTFAsset == nullptr) { mObjects.erase(mObjects.begin() + i); --i; continue; } mObjects[i]->mGLTFAsset->update(); } } void GLTFSceneManager::render(bool opaque, bool rigged) { // for debugging, just render the whole scene as opaque // by traversing the whole scenegraph // Assumes camera transform is already set and // appropriate shader is already bound gGL.matrixMode(LLRender::MM_MODELVIEW); for (U32 i = 0; i < mObjects.size(); ++i) { if (mObjects[i]->isDead() || mObjects[i]->mGLTFAsset == nullptr) { mObjects.erase(mObjects.begin() + i); --i; continue; } Asset* asset = mObjects[i]->mGLTFAsset.get(); gGL.pushMatrix(); LLMatrix4a mat = mObjects[i]->getGLTFAssetToAgentTransform(); LLMatrix4a modelview; modelview.loadu(gGLModelView); matMul(mat, modelview, modelview); asset->updateRenderTransforms(modelview); asset->render(opaque, rigged); gGL.popMatrix(); } } LLMatrix4a inverse(const LLMatrix4a& mat) { glh::matrix4f m((F32*)mat.mMatrix); m = m.inverse(); LLMatrix4a ret; ret.loadu(m.m); return ret; } bool GLTFSceneManager::lineSegmentIntersect(LLVOVolume* obj, Asset* asset, const LLVector4a& start, const LLVector4a& end, S32 face, BOOL pick_transparent, BOOL pick_rigged, BOOL pick_unselectable, S32* node_hit, S32* primitive_hit, LLVector4a* intersection, LLVector2* tex_coord, LLVector4a* normal, LLVector4a* tangent) { // line segment intersection test // start and end should be in agent space // volume space and asset space should be the same coordinate frame // results should be transformed back to agent space bool ret = false; LLVector4a local_start; LLVector4a local_end; LLMatrix4a asset_to_agent = obj->getGLTFAssetToAgentTransform(); LLMatrix4a agent_to_asset = inverse(asset_to_agent); agent_to_asset.affineTransform(start, local_start); agent_to_asset.affineTransform(end, local_end); LLVector4a p; LLVector4a n; LLVector2 tc; LLVector4a tn; if (intersection != NULL) { p = *intersection; } if (tex_coord != NULL) { tc = *tex_coord; } if (normal != NULL) { n = *normal; } if (tangent != NULL) { tn = *tangent; } S32 hit_node_index = asset->lineSegmentIntersect(local_start, local_end, &p, &tc, &n, &tn, primitive_hit); if (hit_node_index >= 0) { local_end = p; if (node_hit != NULL) { *node_hit = hit_node_index; } if (intersection != NULL) { asset_to_agent.affineTransform(p, *intersection); } if (normal != NULL) { LLVector3 v_n(n.getF32ptr()); normal->load3(obj->volumeDirectionToAgent(v_n).mV); (*normal).normalize3fast(); } if (tangent != NULL) { LLVector3 v_tn(tn.getF32ptr()); LLVector4a trans_tangent; trans_tangent.load3(obj->volumeDirectionToAgent(v_tn).mV); LLVector4Logical mask; mask.clear(); mask.setElement<3>(); tangent->setSelectWithMask(mask, tn, trans_tangent); (*tangent).normalize3fast(); } if (tex_coord != NULL) { *tex_coord = tc; } ret = true; } return ret; } LLDrawable* GLTFSceneManager::lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end, BOOL pick_transparent, BOOL pick_rigged, BOOL pick_unselectable, BOOL pick_reflection_probe, S32* node_hit, // return the index of the node that was hit S32* primitive_hit, // return the index of the primitive that was hit LLVector4a* intersection, // return the intersection point LLVector2* tex_coord, // return the texture coordinates of the intersection point LLVector4a* normal, // return the surface normal at the intersection point LLVector4a* tangent) // return the surface tangent at the intersection point { LLDrawable* drawable = nullptr; LLVector4a local_end = end; LLVector4a position; for (U32 i = 0; i < mObjects.size(); ++i) { if (mObjects[i]->isDead() || mObjects[i]->mGLTFAsset == nullptr || !mObjects[i]->getVolume()) { mObjects.erase(mObjects.begin() + i); --i; continue; } // temporary debug -- always double check objects that have GLTF scenes hanging off of them even if the ray doesn't intersect the object bounds if (lineSegmentIntersect((LLVOVolume*) mObjects[i].get(), mObjects[i]->mGLTFAsset.get(), start, local_end, -1, pick_transparent, pick_rigged, pick_unselectable, node_hit, primitive_hit, &position, tex_coord, normal, tangent)) { local_end = position; if (intersection) { *intersection = position; } drawable = mObjects[i]->mDrawable; } } return drawable; } void drawBoxOutline(const LLVector4a& pos, const LLVector4a& size); extern LLVector4a gDebugRaycastStart; extern LLVector4a gDebugRaycastEnd; void renderOctreeRaycast(const LLVector4a& start, const LLVector4a& end, const LLVolumeOctree* octree); void renderAssetDebug(LLViewerObject* obj, Asset* asset) { // render debug // assumes appropriate shader is already bound // assumes modelview matrix is already set gGL.pushMatrix(); // get raycast in asset space LLMatrix4a agent_to_asset = obj->getAgentToGLTFAssetTransform(); LLVector4a start; LLVector4a end; agent_to_asset.affineTransform(gDebugRaycastStart, start); agent_to_asset.affineTransform(gDebugRaycastEnd, end); for (auto& node : asset->mNodes) { Mesh& mesh = asset->mMeshes[node.mMesh]; if (node.mMesh != INVALID_INDEX) { gGL.loadMatrix((F32*)node.mRenderMatrix.mMatrix); // draw bounding box of mesh primitives if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_BBOXES)) { gGL.color3f(0.f, 1.f, 1.f); for (auto& primitive : mesh.mPrimitives) { auto* listener = (LLVolumeOctreeListener*) primitive.mOctree->getListener(0); LLVector4a center = listener->mBounds[0]; LLVector4a size = listener->mBounds[1]; drawBoxOutline(center, size); } } #if 0 if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_RAYCAST)) { gGL.flush(); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // convert raycast to node local space LLVector4a local_start; LLVector4a local_end; node.mAssetMatrixInv.affineTransform(start, local_start); node.mAssetMatrixInv.affineTransform(end, local_end); for (auto& primitive : mesh.mPrimitives) { if (primitive.mOctree.notNull()) { renderOctreeRaycast(local_start, local_end, primitive.mOctree); } } gGL.flush(); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } #endif } } gGL.popMatrix(); } void GLTFSceneManager::renderDebug() { if (!gPipeline.hasRenderDebugMask( LLPipeline::RENDER_DEBUG_BBOXES | LLPipeline::RENDER_DEBUG_RAYCAST | LLPipeline::RENDER_DEBUG_NODES)) { return; } gDebugProgram.bind(); LLGLDisable cullface(GL_CULL_FACE); LLGLEnable blend(GL_BLEND); gGL.setSceneBlendType(LLRender::BT_ALPHA); gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); gPipeline.disableLights(); // force update all mRenderMatrix, not just nodes with meshes for (auto& obj : mObjects) { if (obj->isDead() || obj->mGLTFAsset == nullptr) { continue; } LLMatrix4a mat = obj->getGLTFAssetToAgentTransform(); LLMatrix4a modelview; modelview.loadu(gGLModelView); matMul(mat, modelview, modelview); Asset* asset = obj->mGLTFAsset.get(); for (auto& node : asset->mNodes) { matMul(node.mAssetMatrix, modelview, node.mRenderMatrix); } } for (auto& obj : mObjects) { if (obj->isDead() || obj->mGLTFAsset == nullptr) { continue; } Asset* asset = obj->mGLTFAsset.get(); LLMatrix4a mat = obj->getGLTFAssetToAgentTransform(); LLMatrix4a modelview; modelview.loadu(gGLModelView); matMul(mat, modelview, modelview); renderAssetDebug(obj, asset); } if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_NODES)) { //render node hierarchy for (U32 i = 0; i < 2; ++i) { LLGLDepthTest depth(GL_TRUE, i == 0 ? GL_FALSE : GL_TRUE, i == 0 ? GL_GREATER : GL_LEQUAL); LLGLState blend(GL_BLEND, i == 0 ? TRUE : FALSE); gGL.pushMatrix(); for (auto& obj : mObjects) { if (obj->isDead() || obj->mGLTFAsset == nullptr) { continue; } LLMatrix4a mat = obj->getGLTFAssetToAgentTransform(); LLMatrix4a modelview; modelview.loadu(gGLModelView); matMul(mat, modelview, modelview); Asset* asset = obj->mGLTFAsset.get(); for (auto& node : asset->mNodes) { // force update all mRenderMatrix, not just nodes with meshes matMul(node.mAssetMatrix, modelview, node.mRenderMatrix); gGL.loadMatrix(node.mRenderMatrix.getF32ptr()); // render x-axis red, y-axis green, z-axis blue gGL.color4f(1.f, 0.f, 0.f, 0.5f); gGL.begin(LLRender::LINES); gGL.vertex3f(0.f, 0.f, 0.f); gGL.vertex3f(1.f, 0.f, 0.f); gGL.end(); gGL.flush(); gGL.color4f(0.f, 1.f, 0.f, 0.5f); gGL.begin(LLRender::LINES); gGL.vertex3f(0.f, 0.f, 0.f); gGL.vertex3f(0.f, 1.f, 0.f); gGL.end(); gGL.flush(); gGL.begin(LLRender::LINES); gGL.color4f(0.f, 0.f, 1.f, 0.5f); gGL.vertex3f(0.f, 0.f, 0.f); gGL.vertex3f(0.f, 0.f, 1.f); gGL.end(); gGL.flush(); // render path to child nodes cyan gGL.color4f(0.f, 1.f, 1.f, 0.5f); gGL.begin(LLRender::LINES); for (auto& child_idx : node.mChildren) { Node& child = asset->mNodes[child_idx]; gGL.vertex3f(0.f, 0.f, 0.f); gGL.vertex3fv(child.mMatrix.getTranslation().getF32ptr()); } gGL.end(); gGL.flush(); } } gGL.popMatrix(); } } if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_RAYCAST)) { S32 node_hit = -1; S32 primitive_hit = -1; LLVector4a intersection; LLDrawable* drawable = lineSegmentIntersect(gDebugRaycastStart, gDebugRaycastEnd, TRUE, TRUE, TRUE, TRUE, &node_hit, &primitive_hit, &intersection, nullptr, nullptr, nullptr); if (drawable) { gGL.pushMatrix(); Asset* asset = drawable->getVObj()->mGLTFAsset.get(); Node* node = &asset->mNodes[node_hit]; Primitive* primitive = &asset->mMeshes[node->mMesh].mPrimitives[primitive_hit]; gGL.flush(); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); gGL.color3f(1, 0, 1); drawBoxOutline(intersection, LLVector4a(0.1f, 0.1f, 0.1f, 0.f)); gGL.loadMatrix((F32*) node->mRenderMatrix.mMatrix); auto* listener = (LLVolumeOctreeListener*) primitive->mOctree->getListener(0); drawBoxOutline(listener->mBounds[0], listener->mBounds[1]); gGL.flush(); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); gGL.popMatrix(); } } gDebugProgram.unbind(); }