diff --git a/Components/Hlms/Pbs/src/OgreHlmsPbs.cpp b/Components/Hlms/Pbs/src/OgreHlmsPbs.cpp index 3c1a2292f6d..c5556ad8b05 100644 --- a/Components/Hlms/Pbs/src/OgreHlmsPbs.cpp +++ b/Components/Hlms/Pbs/src/OgreHlmsPbs.cpp @@ -3128,6 +3128,8 @@ namespace Ogre if( OGRE_EXTRACT_HLMS_TYPE_FROM_CACHE_HASH( lastCacheHash ) != mType ) { + if( mCurrentPassBuffer == 0 ) + return 0; // layout(binding = 0) uniform PassBuffer {} pass ConstBufferPacked *passBuffer = mPassBuffers[mCurrentPassBuffer - 1]; *commandBuffer->addCommand() = CbShaderBuffer( diff --git a/Components/MeshLodGenerator/include/OgreLodConfig.h b/Components/MeshLodGenerator/include/OgreLodConfig.h index e011785f293..791051205ef 100644 --- a/Components/MeshLodGenerator/include/OgreLodConfig.h +++ b/Components/MeshLodGenerator/include/OgreLodConfig.h @@ -129,15 +129,27 @@ namespace Ogre struct _OgreLodExport LodConfig { - v1::MeshPtr mesh; ///< The mesh which we want to reduce. + v1::MeshPtr mesh; ///< The v1 mesh which we want to reduce. Null if meshV2 is set. + MeshPtr meshV2; ///< The v2 mesh which we want to reduce. Null if mesh is set. LodStrategy *strategy; ///< Lod strategy to use. typedef vector::type LodLevelList; LodLevelList levels; ///< Info about Lod levels LodConfig( v1::MeshPtr &_mesh, LodStrategy *_strategy = DistanceLodStrategy::getSingletonPtr() ); + /** Constructs a config for generating LOD levels directly against a v2 mesh, + without going through a v1 mesh at any point. + @remarks + Exactly one of mesh / meshV2 must be set; MeshLodGenerator selects the v1 or + v2 code path (LodInputProviderMesh/LodOutputProviderMesh vs. + LodInputProviderMeshV2/LodOutputProviderMeshV2) based on which one is non-null. + */ + LodConfig( MeshPtr &_meshV2, LodStrategy *_strategy = DistanceLodStrategy::getSingletonPtr() ); LodConfig(); + /// True if this config targets a v2 mesh (meshV2 is set) rather than a v1 one. + bool isV2() const { return meshV2 != 0; } + // Helper functions: void createManualLodLevel( Ogre::Real distance, const String &manualMeshName ); void createGeneratedLodLevel( diff --git a/Components/MeshLodGenerator/include/OgreLodInputProvider.h b/Components/MeshLodGenerator/include/OgreLodInputProvider.h index 07ff0bc44cb..a3e531b4214 100644 --- a/Components/MeshLodGenerator/include/OgreLodInputProvider.h +++ b/Components/MeshLodGenerator/include/OgreLodInputProvider.h @@ -1,4 +1,3 @@ - /* * ----------------------------------------------------------------------------- * This source file is part of OGRE-Next @@ -50,7 +49,56 @@ namespace Ogre bool isDuplicateTriangle( LodData::Triangle *triangle, LodData::Triangle *triangle2 ); LodData::Triangle *isDuplicateTriangle( LodData *data, LodData::Triangle *triangle ); static size_t getTriangleCount( OperationType renderOp, size_t indexCount ); + + /// Typedef shared by every concrete provider: maps a raw vertex-buffer index + /// (as found in the source index buffer) to the deduplicated LodData::VertexI + /// produced while reading vertex data. See LodInputProviderMesh::addVertexData / + /// LodInputProviderMeshV2::addVertexData for how this gets populated. + typedef vector::type VertexLookupList; + + /** Builds a LodData::Triangle from three raw vertex-buffer indices (translated + through 'lookup') and registers it with 'data', for both v1 and v2 source + meshes alike -- once vertex/index data has been read into CPU-side arrays, + triangle construction itself does not depend on the source mesh format. + @remarks + Moved here (promoted from LodInputProviderMesh, which used to be the only + concrete provider) so LodInputProviderMeshV2 does not need to duplicate it. + Templated on IndexType since callers read raw indices as either + 'unsigned short' or 'unsigned int' depending on the source index buffer's + element size. + */ + template + void addTriangle( LodData *data, IndexType i0, IndexType i1, IndexType i2, + VertexLookupList &lookup, unsigned submeshID ) + { + LodData::Triangle tri; + tri.vertexID[0] = static_cast( i0 ); + tri.vertexID[1] = static_cast( i1 ); + tri.vertexID[2] = static_cast( i2 ); + tri.vertexi[0] = lookup[i0]; + tri.vertexi[1] = lookup[i1]; + tri.vertexi[2] = lookup[i2]; + // No setter exists for this -- it's a plain public field. isRemoved()/ + // setRemoved() use the all-bits-set sentinel value on this same field, so + // assigning a real submeshID here is also what marks the triangle as + // "not removed" (see LodData::Triangle::isRemoved()). + tri.submeshIDOrRemovedTag = submeshID; + + if( tri.isMalformed() ) + { + // Degenerate after vertex dedup (e.g. two raw indices that pointed at + // distinct, but identically-positioned, vertices). Exclude it from + // collapse calculations the same way isDuplicateTriangle() does. + data->mIndexBufferInfoList[submeshID].indexCount -= 3; + return; + } + + tri.computeNormal( data->mVertexList ); + + data->mTriangleList.push_back( tri ); + addTriangleToEdges( data, &data->mTriangleList.back() ); + } }; } // namespace Ogre -#endif +#endif \ No newline at end of file diff --git a/Components/MeshLodGenerator/include/OgreLodInputProviderMeshV2.h b/Components/MeshLodGenerator/include/OgreLodInputProviderMeshV2.h new file mode 100644 index 00000000000..8b13edd191c --- /dev/null +++ b/Components/MeshLodGenerator/include/OgreLodInputProviderMeshV2.h @@ -0,0 +1,74 @@ +/* + * ----------------------------------------------------------------------------- + * This source file is part of OGRE-Next + * (Object-oriented Graphics Rendering Engine) + * For the latest info, see http://www.ogre3d.org/ + * + * Copyright (c) 2000-2014 Torus Knot Software Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * ----------------------------------------------------------------------------- + */ + +#ifndef _LodInputProviderMeshV2_H__ +#define _LodInputProviderMeshV2_H__ + +#include "OgreLodPrerequisites.h" + +#include "OgreLodInputProvider.h" +#include "OgreSharedPtr.h" + +namespace Ogre +{ + /** Reads geometry directly from a v2 Mesh's SubMesh VAOs (no v1 mesh involved at + any point) and populates a LodData for MeshLodGenerator to collapse. + @remarks + Unlike LodInputProviderMesh, there is no shared-vertex-data concept to handle: + v2 Mesh has no Mesh-level shared vertex buffer, only per-SubMesh VAOs. This + removes the mSharedVertexLookup / useSharedVertexLookup branching entirely. + @par + Reads vertex/index data via VertexArrayObject::readRequests() + + mapAsyncTickets() + unmapAsyncTickets() (the same pattern SubMesh:: + _dearrangeEfficient() already uses elsewhere in this codebase), which works + synchronously regardless of whether the buffers are shadow-copied, so there + is no special buffer-policy requirement on the source mesh. + */ + class _OgreLodExport LodInputProviderMeshV2 : public LodInputProvider + { + public: + LodInputProviderMeshV2( MeshPtr mesh ); + + void initData( LodData *data ) override; + + protected: + MeshPtr mMesh; + + /// Reused across submeshes; cleared at the start of each addVertexData() call. + /// One entry per vertex in the submesh's LOD-0 vertex buffer, mapping its + /// position in that buffer to the deduplicated LodData::VertexI. + VertexLookupList mVertexLookup; + + void tuneContainerSize( LodData *data ); + void initialize( LodData *data ); + void addVertexData( LodData *data, SubMesh *subMesh, unsigned submeshID ); + void addIndexData( LodData *data, SubMesh *subMesh, unsigned submeshID ); + }; + +} // namespace Ogre +#endif diff --git a/Components/MeshLodGenerator/include/OgreLodOutputProviderMeshV2.h b/Components/MeshLodGenerator/include/OgreLodOutputProviderMeshV2.h new file mode 100644 index 00000000000..14b76f71ae7 --- /dev/null +++ b/Components/MeshLodGenerator/include/OgreLodOutputProviderMeshV2.h @@ -0,0 +1,79 @@ +/* + * ----------------------------------------------------------------------------- + * This source file is part of OGRE-Next + * (Object-oriented Graphics Rendering Engine) + * For the latest info, see http://www.ogre3d.org/ + * + * Copyright (c) 2000-2014 Torus Knot Software Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * ----------------------------------------------------------------------------- + */ + +#ifndef _LodOutputProviderMeshV2_H__ +#define _LodOutputProviderMeshV2_H__ + +#include "OgreLodPrerequisites.h" + +#include "OgreLodOutputProvider.h" +#include "OgreSharedPtr.h" + +namespace Ogre +{ + /** Writes generated LOD levels directly onto a v2 Mesh's SubMesh::mVao arrays. + @remarks + Unlike LodOutputProviderMesh (which builds a v1::SubMesh::LODFaceList), this + appends a brand new VertexArrayObject per LOD level directly onto + SubMesh::mVao[VpNormal] (and [VpShadow], see bakeLodLevel()), reusing the + existing LOD-0 vertex buffers untouched -- LOD generation only ever changes + the index buffer, never vertex data, matching the pattern already proven in + SubMesh::importBuffersFromV1()'s own v1-LOD-import loop. + @par + Populating Mesh::_setLodValues() is intentionally NOT done here: just like + v1's LodOutputProviderMesh leaves _configureMeshLodUsage() as a separate step + MeshLodGenerator calls itself (it already has LodConfig in scope, this + provider doesn't need to carry a copy of it), the v2 path has a parallel + MeshLodGenerator::_configureMeshLodUsageV2() called the same way, after + output->finalize(). + @par + Manual (mesh-swap) LOD levels are intentionally not supported here; see + bakeManualLodLevel(). + */ + class _OgreLodExport LodOutputProviderMeshV2 : public LodOutputProvider + { + public: + LodOutputProviderMeshV2( MeshPtr mesh ) : mMesh( mesh ) {} + + void prepare( LodData *data ) override; + void finalize( LodData *data ) override {} + void bakeManualLodLevel( LodData *data, String &manualMeshName, int lodIndex ) override; + void bakeLodLevel( LodData *data, int lodIndex ) override; + + protected: + MeshPtr mMesh; + + /// Builds a new IndexBufferPacked from data's current triangle list for one + /// submesh, exactly mirroring LodOutputProviderMesh::bakeLodLevel's v1 index + /// construction (same "dummy triangle if empty" handling), but returning a + /// v2 IndexBufferPacked instead of writing into a v1::IndexData. + IndexBufferPacked *buildIndexBufferForSubmesh( LodData *data, unsigned submeshID ); + }; + +} // namespace Ogre +#endif diff --git a/Components/MeshLodGenerator/include/OgreMeshLodGenerator.h b/Components/MeshLodGenerator/include/OgreMeshLodGenerator.h index 8326a1652b8..08c6bc8114d 100644 --- a/Components/MeshLodGenerator/include/OgreMeshLodGenerator.h +++ b/Components/MeshLodGenerator/include/OgreMeshLodGenerator.h @@ -80,12 +80,28 @@ namespace Ogre */ void getAutoconfig( v1::MeshPtr &inMesh, LodConfig &outLodConfig ); + /** + * @brief Fills Lod Config with a config, which works on any v2 mesh. + * + * Identical heuristic to the v1 overload above; sets outLodConfig.meshV2 + * instead of outLodConfig.mesh, which is what _resolveComponents() uses to + * select the v2 input/output providers. + * + * @param inMesh Optimize for this mesh. + * @param outLodConfig Lod configuration storing the output. + */ + void getAutoconfig( MeshPtr &inMesh, LodConfig &outLodConfig ); + static void _configureMeshLodUsage( const LodConfig &lodConfig ); + /// v2 equivalent of _configureMeshLodUsage(): populates Mesh::_setLodValues() + /// instead of v1::Mesh's MeshLodUsage list. Called from _process() instead of + /// _configureMeshLodUsage() whenever lodConfig.isV2() is true. + static void _configureMeshLodUsageV2( const LodConfig &lodConfig ); void _resolveComponents( LodConfig &lodConfig, LodCollapseCostPtr &cost, LodDataPtr &data, LodInputProviderPtr &input, LodOutputProviderPtr &output, LodCollapserPtr &collapser ); - void _process( LodConfig &lodConfig, LodCollapseCost *cost, LodData *data, - LodInputProvider *input, LodOutputProvider *output, LodCollapser *collapser ); + void _process( LodConfig &lodConfig, LodCollapseCost *cost, LodData *data, + LodInputProvider *input, LodOutputProvider *output, LodCollapser *collapser ); /// If you only use manual Lod levels, then you don't need to build LodData mesh representation. /// This function will generate manual Lod levels without overhead, but every Lod level needs to @@ -105,4 +121,4 @@ namespace Ogre }; } // namespace Ogre -#endif +#endif \ No newline at end of file diff --git a/Components/MeshLodGenerator/src/OgreLodConfig.cpp b/Components/MeshLodGenerator/src/OgreLodConfig.cpp index 042d790e49d..952c0fa1540 100644 --- a/Components/MeshLodGenerator/src/OgreLodConfig.cpp +++ b/Components/MeshLodGenerator/src/OgreLodConfig.cpp @@ -46,6 +46,13 @@ namespace Ogre { } + LodConfig::LodConfig( MeshPtr &_meshV2, + LodStrategy *_strategy /*= DistanceLodStrategy::getSingletonPtr()*/ ) : + meshV2( _meshV2 ), + strategy( _strategy ) + { + } + LodConfig::LodConfig() {} void LodConfig::createManualLodLevel( Ogre::Real distance, const String &manualMeshName ) diff --git a/Components/MeshLodGenerator/src/OgreLodInputProviderMeshV2.cpp b/Components/MeshLodGenerator/src/OgreLodInputProviderMeshV2.cpp new file mode 100644 index 00000000000..6146a18bdff --- /dev/null +++ b/Components/MeshLodGenerator/src/OgreLodInputProviderMeshV2.cpp @@ -0,0 +1,316 @@ +/* + * ----------------------------------------------------------------------------- + * This source file is part of OGRE-Next + * (Object-oriented Graphics Rendering Engine) + * For the latest info, see http://www.ogre3d.org/ + * + * Copyright (c) 2000-2014 Torus Knot Software Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * ----------------------------------------------------------------------------- + */ + +#include "OgreLodInputProviderMeshV2.h" + +#include "OgreBitwise.h" +#include "OgreLodData.h" +#include "OgreMesh2.h" +#include "OgreSubMesh2.h" +#include "Vao/OgreAsyncTicket.h" +#include "Vao/OgreIndexBufferPacked.h" +#include "Vao/OgreVertexArrayObject.h" +#include "Vao/OgreVertexBufferPacked.h" + +namespace Ogre +{ + LodInputProviderMeshV2::LodInputProviderMeshV2( MeshPtr mesh ) : mMesh( mesh ) {} + + void LodInputProviderMeshV2::initData( LodData *data ) + { + tuneContainerSize( data ); + initialize( data ); + } + + void LodInputProviderMeshV2::tuneContainerSize( LodData *data ) + { + size_t trianglesCount = 0; + size_t vertexCount = 0; + size_t vertexLookupSize = 0; + unsigned submeshCount = mMesh->getNumSubMeshes(); + + for( unsigned i = 0; i < submeshCount; ++i ) + { + const SubMesh *subMesh = mMesh->getSubMesh( i ); + + OgreAssert( !subMesh->mVao[VpNormal].empty(), + "SubMesh has no LOD-0 Vao. Was the mesh actually loaded/built?" ); + + VertexArrayObject *vao = subMesh->mVao[VpNormal][0]; + const size_t vertexCountThisSub = vao->getVertexBuffers()[0]->getNumElements(); + // Non-indexed VAOs have no index buffer at all; in that case every vertex + // is consumed directly by the operation, so the vertex count itself is + // the right triangle-count input (there is no VertexArrayObject:: + // getNumElements() to call here). + const size_t indexCountThisSub = + vao->getIndexBuffer() ? vao->getIndexBuffer()->getNumElements() : vertexCountThisSub; + + trianglesCount += getTriangleCount( vao->getOperationType(), indexCountThisSub ); + vertexLookupSize = std::max( vertexLookupSize, vertexCountThisSub ); + vertexCount += vertexCountThisSub; + } + + // Less than 0.25 item/bucket for low collision rate, same tuning ratio + // LodInputProviderMesh uses for v1 meshes. + data->mUniqueVertexSet.rehash( 4 * vertexCount ); + data->mTriangleList.reserve( trianglesCount ); + data->mVertexList.reserve( vertexCount ); + mVertexLookup.reserve( vertexLookupSize ); + data->mIndexBufferInfoList.resize( submeshCount ); + } + + void LodInputProviderMeshV2::initialize( LodData *data ) + { +#if OGRE_DEBUG_MODE + data->mMeshName = mMesh->getName(); +#endif + data->mMeshBoundingSphereRadius = mMesh->getBoundingSphereRadius(); + unsigned submeshCount = mMesh->getNumSubMeshes(); + + for( unsigned i = 0; i < submeshCount; ++i ) + { + SubMesh *subMesh = mMesh->getSubMesh( i ); + addVertexData( data, subMesh, i ); + + const size_t indexCount = + subMesh->mVao[VpNormal][0]->getIndexBuffer() + ? subMesh->mVao[VpNormal][0]->getIndexBuffer()->getNumElements() + : 0u; + if( indexCount > 0u ) + addIndexData( data, subMesh, i ); + } + + // Only needed for addIndexData() within this submesh's iteration. + mVertexLookup.clear(); + } + + void LodInputProviderMeshV2::addVertexData( LodData *data, SubMesh *subMesh, unsigned submeshID ) + { + OGRE_UNUSED_VAR( submeshID ); + + VertexArrayObject *vao = subMesh->mVao[VpNormal][0]; + const size_t vertexCount = vao->getVertexBuffers()[0]->getNumElements(); + OgreAssert( vertexCount != 0, "" ); + + VertexArrayObject::ReadRequestsVec requests; + requests.push_back( VertexArrayObject::ReadRequests( VES_POSITION ) ); + requests.push_back( VertexArrayObject::ReadRequests( VES_NORMAL ) ); + + vao->readRequests( requests ); + vao->mapAsyncTickets( requests ); + + const bool bPositionIsHalf = requests[0].type == VET_HALF4; + const bool bHasNormal = requests[1].data != 0; + const bool bNormalIsQTangent = bHasNormal && requests[1].type == VET_SHORT4_SNORM; + const bool bNormalIsHalf = bHasNormal && requests[1].type == VET_HALF4; + + data->mUseVertexNormals &= bHasNormal; + + mVertexLookup.clear(); + + for( size_t vi = 0; vi < vertexCount; ++vi ) + { + LodData::VertexI lvi = (LodData::VertexI)data->mVertexList.size(); + { + LodData::Vertex tmp; + tmp.position = Vector3::ZERO; + tmp.normal = Vector3::ZERO; + tmp.seam = false; + data->mVertexList.push_back( tmp ); + } + LodData::Vertex *v = &data->mVertexList.back(); + + if( bPositionIsHalf ) + { + const uint16 *p = reinterpret_cast( requests[0].data ); + v->position.x = Bitwise::halfToFloat( p[0] ); + v->position.y = Bitwise::halfToFloat( p[1] ); + v->position.z = Bitwise::halfToFloat( p[2] ); + } + else + { + const float *p = reinterpret_cast( requests[0].data ); + v->position.x = p[0]; + v->position.y = p[1]; + v->position.z = p[2]; + } + v->collapseToi = LodData::InvalidIndex; + + std::pair ret; + ret = data->mUniqueVertexSet.insert( lvi ); + if( !ret.second ) + { + // Vertex position already exists. + data->mVertexList.pop_back(); + lvi = *ret.first; + v = &data->mVertexList[lvi]; + v->seam = true; + } + else + { +#if OGRE_DEBUG_MODE + v->costHeapPosition = data->mCollapseCostHeap.end(); +#endif + v->seam = false; + } + mVertexLookup.push_back( lvi ); + + if( data->mUseVertexNormals ) + { + Vector3 normal; + if( bNormalIsQTangent ) + { + const int16 *q = reinterpret_cast( requests[1].data ); + Quaternion qTangent; + qTangent.x = Bitwise::snorm16ToFloat( q[0] ); + qTangent.y = Bitwise::snorm16ToFloat( q[1] ); + qTangent.z = Bitwise::snorm16ToFloat( q[2] ); + qTangent.w = Bitwise::snorm16ToFloat( q[3] ); + normal = qTangent.xAxis(); + } + else if( bNormalIsHalf ) + { + const uint16 *n = reinterpret_cast( requests[1].data ); + normal.x = Bitwise::halfToFloat( n[0] ); + normal.y = Bitwise::halfToFloat( n[1] ); + normal.z = Bitwise::halfToFloat( n[2] ); + } + else + { + const float *n = reinterpret_cast( requests[1].data ); + normal.x = n[0]; + normal.y = n[1]; + normal.z = n[2]; + } + + if( !ret.second ) + { + if( v->normal.x != normal.x || v->normal.y != normal.y || v->normal.z != normal.z ) + { + v->normal += normal; + if( v->normal.isZeroLength() ) + v->normal = Vector3( 1.0, 0.0, 0.0 ); + v->normal.normalise(); + } + } + else + { + v->normal = normal; + v->normal.normalise(); + } + } + + requests[0].data += requests[0].vertexBuffer->getBytesPerElement(); + if( bHasNormal ) + requests[1].data += requests[1].vertexBuffer->getBytesPerElement(); + } + + vao->unmapAsyncTickets( requests ); + } + + void LodInputProviderMeshV2::addIndexData( LodData *data, SubMesh *subMesh, unsigned submeshID ) + { + VertexArrayObject *vao = subMesh->mVao[VpNormal][0]; + IndexBufferPacked *ibuf = vao->getIndexBuffer(); + const size_t numIndices = ibuf->getNumElements(); + const bool is32Bit = ibuf->getIndexType() == IndexBufferPacked::IT_32BIT; + + data->mIndexBufferInfoList[submeshID].indexSize = is32Bit ? 4u : 2u; + data->mIndexBufferInfoList[submeshID].indexCount = numIndices; + + if( numIndices == 0 ) + return; + + // Prefer the shadow copy (instant, synchronous) when present; only fall back + // to the async ticket round-trip for non-shadowed (e.g. BT_IMMUTABLE without a + // shadow buffer) index buffers. Mirrors the pattern already used elsewhere in + // this codebase for reading back index data on the CPU. + const void *raw = ibuf->getShadowCopy(); + AsyncTicketPtr ticket; + if( !raw ) + { + ticket = ibuf->readRequest( 0u, numIndices ); + raw = ticket->map(); + } + + const OperationType op = vao->getOperationType(); + + if( is32Bit ) + { + const uint32 *indices = reinterpret_cast( raw ); + switch( op ) + { + case OT_TRIANGLE_LIST: + for( size_t i0 = 0; i0 + 2 < numIndices; i0 += 3 ) + addTriangle( data, indices[i0], indices[i0 + 1], indices[i0 + 2], mVertexLookup, + submeshID ); + break; + case OT_TRIANGLE_STRIP: + for( size_t i0 = 0, i1 = 1, i2 = 2; i2 < numIndices; + ( i2 & 1 ) ? i1 = i2 : i0 = i2, ++i2 ) + addTriangle( data, indices[i0], indices[i1], indices[i2], mVertexLookup, submeshID ); + break; + case OT_TRIANGLE_FAN: + for( size_t i1 = 1; i1 + 1 < numIndices; ++i1 ) + addTriangle( data, indices[0], indices[i1], indices[i1 + 1], mVertexLookup, + submeshID ); + break; + default: + break; + } + } + else + { + const uint16 *indices = reinterpret_cast( raw ); + switch( op ) + { + case OT_TRIANGLE_LIST: + for( size_t i0 = 0; i0 + 2 < numIndices; i0 += 3 ) + addTriangle( data, indices[i0], indices[i0 + 1], indices[i0 + 2], mVertexLookup, + submeshID ); + break; + case OT_TRIANGLE_STRIP: + for( size_t i0 = 0, i1 = 1, i2 = 2; i2 < numIndices; + ( i2 & 1 ) ? i1 = i2 : i0 = i2, ++i2 ) + addTriangle( data, indices[i0], indices[i1], indices[i2], mVertexLookup, submeshID ); + break; + case OT_TRIANGLE_FAN: + for( size_t i1 = 1; i1 + 1 < numIndices; ++i1 ) + addTriangle( data, indices[0], indices[i1], indices[i1 + 1], mVertexLookup, + submeshID ); + break; + default: + break; + } + } + + if( ticket ) + ticket->unmap(); + } + +} // namespace Ogre \ No newline at end of file diff --git a/Components/MeshLodGenerator/src/OgreLodOutputProviderMeshV2.cpp b/Components/MeshLodGenerator/src/OgreLodOutputProviderMeshV2.cpp new file mode 100644 index 00000000000..2a16536ce48 --- /dev/null +++ b/Components/MeshLodGenerator/src/OgreLodOutputProviderMeshV2.cpp @@ -0,0 +1,197 @@ +/* + * ----------------------------------------------------------------------------- + * This source file is part of OGRE-Next + * (Object-oriented Graphics Rendering Engine) + * For the latest info, see http://www.ogre3d.org/ + * + * Copyright (c) 2000-2014 Torus Knot Software Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * ----------------------------------------------------------------------------- + */ + +#include "OgreLodOutputProviderMeshV2.h" + +#include "OgreException.h" +#include "OgreLodData.h" +#include "OgreLogManager.h" +#include "OgreMesh2.h" +#include "OgreStringConverter.h" +#include "OgreSubMesh2.h" +#include "Vao/OgreIndexBufferPacked.h" +#include "Vao/OgreVaoManager.h" +#include "Vao/OgreVertexArrayObject.h" + +namespace Ogre +{ + void LodOutputProviderMeshV2::prepare( LodData *data ) + { + OGRE_UNUSED_VAR( data ); + + // Nothing to pre-size: unlike v1's LODFaceList (which is resized up front), + // we append a new VAO per LOD level directly onto mVao[...] as each level is + // baked. LOD-0 already exists untouched -- it's the mesh as it was loaded. + unsigned submeshCount = mMesh->getNumSubMeshes(); + for( unsigned i = 0; i < submeshCount; ++i ) + { + SubMesh *subMesh = mMesh->getSubMesh( i ); + OgreAssert( !subMesh->mVao[VpNormal].empty(), + "SubMesh has no LOD-0 Vao to base generated LODs on." ); + } + } + + void LodOutputProviderMeshV2::bakeManualLodLevel( LodData *data, String &manualMeshName, + int lodIndex ) + { + OGRE_UNUSED_VAR( data ); + OGRE_UNUSED_VAR( lodIndex ); + + // Manual (mesh-swap) LOD levels are out of scope for this provider: they + // require an entirely separate source mesh's geometry to be merged in as a + // new Vao, which is a distinct feature from collapse-generated LOD and adds + // significant complexity (a different mesh likely has different vertex + // layout, bone assignments, etc). Left as explicit future work rather than + // silently producing an incorrect/empty LOD level. + OGRE_EXCEPT( Exception::ERR_NOT_IMPLEMENTED, + "Manual LOD levels (manualMeshName = '" + manualMeshName + + "') are not supported by LodOutputProviderMeshV2. " + "Use generated (collapse-based) LOD levels only.", + "LodOutputProviderMeshV2::bakeManualLodLevel" ); + } + + IndexBufferPacked *LodOutputProviderMeshV2::buildIndexBufferForSubmesh( LodData *data, + unsigned submeshID ) + { + VaoManager *vaoManager = mMesh->_getVaoManager(); + const size_t indexSize = data->mIndexBufferInfoList[submeshID].indexSize; + size_t indexCount = data->mIndexBufferInfoList[submeshID].indexCount; + + // Mirrors LodOutputProviderMesh::bakeLodLevel's v1 handling: an empty index + // buffer can crash some render systems, so keep a single dummy (zeroed) + // triangle instead of a truly empty buffer. + const bool isDummyTriangle = ( indexCount == 0 ); + if( isDummyTriangle ) + indexCount = 3; + + void *indexDataPtr = OGRE_MALLOC_SIMD( indexCount * indexSize, MEMCATEGORY_GEOMETRY ); + FreeOnDestructor indexDataPtrContainer( indexDataPtr ); + + if( isDummyTriangle ) + { + memset( indexDataPtr, 0, indexCount * indexSize ); + } + else + { + data->mIndexBufferInfoList[submeshID].buf.pshort = + static_cast( indexDataPtr ); + + const size_t triangleCount = data->mTriangleList.size(); + for( size_t i = 0; i < triangleCount; ++i ) + { + if( data->mTriangleList[i].isRemoved() ) + continue; + if( data->mTriangleList[i].submeshID() != submeshID ) + continue; + + if( indexSize == 2u ) + { + for( int m = 0; m < 3; ++m ) + { + *( data->mIndexBufferInfoList[submeshID].buf.pshort++ ) = + static_cast( data->mTriangleList[i].vertexID[m] ); + } + } + else + { + for( int m = 0; m < 3; ++m ) + { + *( data->mIndexBufferInfoList[submeshID].buf.pint++ ) = + static_cast( data->mTriangleList[i].vertexID[m] ); + } + } + } + } + + const IndexBufferPacked::IndexType indexType = + ( indexSize == 2u ) ? IndexBufferPacked::IT_16BIT : IndexBufferPacked::IT_32BIT; + const bool keepAsShadow = mMesh->isIndexBufferShadowed(); + + IndexBufferPacked *indexBuffer = vaoManager->createIndexBuffer( + indexType, indexCount, mMesh->getIndexBufferDefaultType(), indexDataPtr, keepAsShadow ); + + if( keepAsShadow ) // Don't free the pointer ourselves; the buffer now owns it. + indexDataPtrContainer.ptr = 0; + + return indexBuffer; + } + + void LodOutputProviderMeshV2::bakeLodLevel( LodData *data, int lodIndex ) + { + OGRE_UNUSED_VAR( lodIndex ); + + VaoManager *vaoManager = mMesh->_getVaoManager(); + unsigned submeshCount = mMesh->getNumSubMeshes(); + + for( unsigned i = 0; i < submeshCount; ++i ) + { + SubMesh *subMesh = mMesh->getSubMesh( i ); + VertexArrayObject *lod0Vao = subMesh->mVao[VpNormal][0]; + + IndexBufferPacked *newIndexBuffer = buildIndexBufferForSubmesh( data, i ); + + // LOD generation never touches vertex data -- only which triangles + // survive -- so the new VAO reuses LOD-0's vertex buffers untouched, + // exactly like SubMesh::importBuffersFromV1 does for v1-imported LODs. + VertexArrayObject *newVao = vaoManager->createVertexArrayObject( + lod0Vao->getVertexBuffers(), newIndexBuffer, lod0Vao->getOperationType() ); + + subMesh->mVao[VpNormal].push_back( newVao ); + + const bool shadowSharesNormal = + !subMesh->mVao[VpShadow].empty() && subMesh->mVao[VpShadow][0] == lod0Vao; + + if( shadowSharesNormal ) + { + // Common case (prepareForShadowMapping(false), the default): shadow + // and regular rendering already share the same Vaos, so keep sharing + // for this new LOD level too. + subMesh->mVao[VpShadow].push_back( newVao ); + } + else if( !subMesh->mVao[VpShadow].empty() ) + { + // Independent shadow-mapping geometry (prepareForShadowMapping(true)) + // is out of scope here: its vertex buffer may be reordered/deduped + // relative to the regular Vao, so this LOD level's triangle list + // (built against the regular Vao's vertex ordering) cannot be reused + // for it as-is. Call Mesh::prepareForShadowMapping() again after + // generating LODs to rebuild a consistent independent shadow Vao set + // across all LOD levels, rather than silently leaving mVao[VpShadow] + // shorter than mVao[VpNormal] (hasValidShadowMappingVaos() would then + // correctly report false). + LogManager::getSingleton().logMessage( + "WARNING: LodOutputProviderMeshV2::bakeLodLevel: submesh " + + StringConverter::toString( i ) + " of mesh '" + mMesh->getName() + + "' has independent shadow-mapping Vaos. Call " + "Mesh::prepareForShadowMapping() again after LOD generation to " + "rebuild shadow Vaos for the new LOD levels." ); + } + } + } + +} // namespace Ogre \ No newline at end of file diff --git a/Components/MeshLodGenerator/src/OgreMeshLodGenerator.cpp b/Components/MeshLodGenerator/src/OgreMeshLodGenerator.cpp index 85fbbc715c6..eef354a3cc2 100644 --- a/Components/MeshLodGenerator/src/OgreMeshLodGenerator.cpp +++ b/Components/MeshLodGenerator/src/OgreMeshLodGenerator.cpp @@ -37,14 +37,18 @@ #include "OgreLodInputProvider.h" #include "OgreLodInputProviderBuffer.h" #include "OgreLodInputProviderMesh.h" +#include "OgreLodInputProviderMeshV2.h" #include "OgreLodOutputProvider.h" #include "OgreLodOutputProviderBuffer.h" #include "OgreLodOutputProviderCompressedBuffer.h" #include "OgreLodOutputProviderCompressedMesh.h" #include "OgreLodOutputProviderMesh.h" +#include "OgreLodOutputProviderMeshV2.h" #include "OgreLodWorkQueueInjector.h" #include "OgreLodWorkQueueWorker.h" #include "OgreMesh.h" +#include "OgreMesh2.h" +#include "OgreStringConverter.h" #include "OgrePixelCountLodStrategy.h" namespace Ogre @@ -93,6 +97,25 @@ namespace Ogre } } + void MeshLodGenerator::getAutoconfig( MeshPtr &inMesh, LodConfig &outLodConfig ) + { + // Identical heuristic to the v1 overload above -- only the bounding-sphere + // source and which LodConfig field gets set differ. + outLodConfig.meshV2 = inMesh; + outLodConfig.strategy = PixelCountLodStrategy::getSingletonPtr(); + LodLevel lodLevel; + lodLevel.reductionMethod = LodLevel::VRM_COLLAPSE_COST; + Real radius = inMesh->getBoundingSphereRadius(); + for( int i = 2; i < 6; i++ ) + { + Real i4 = (Real)( i * i * i * i ); + Real i5 = i4 * (Real)i; + lodLevel.distance = 3388608.f / i4; + lodLevel.reductionValue = radius / 100000.f * i5; + outLodConfig.levels.push_back( lodLevel ); + } + } + void MeshLodGenerator::generateAutoconfiguredLodLevels( v1::MeshPtr &mesh ) { LodConfig lodConfig; @@ -129,6 +152,72 @@ namespace Ogre lodConfig.mesh->buildEdgeList(); } + void MeshLodGenerator::_configureMeshLodUsageV2( const LodConfig &lodConfig ) + { + // v2 equivalent of _configureMeshLodUsage() above. There is no edge list or + // MeshLodUsage record to maintain on v2 Mesh -- only Mesh::mLodValues, read + // directly off the strategy-transformed distance/pixel value, one entry per + // *kept* (non-outSkipped) level, in the same order LodOutputProviderMeshV2 + // pushed VAOs onto each SubMesh::mVao[...]. + Mesh::LodValueArray lodValues; + lodValues.reserve( lodConfig.levels.size() + 1u ); + + // Placeholder for the base (LOD0) entry. We deliberately do NOT use + // lodConfig.strategy->getBaseValue() here: it is not guaranteed to share the + // same numeric convention as transformUserValue()'s output for every + // strategy (e.g. for a pixel-count-style strategy, "more detail" may mean a + // *larger* raw value, the opposite of distance-style strategies), and lodSet() + // (OgreLodStrategyPrivate.inl) requires this array strictly ascending via + // std::lower_bound. Filled in below once the first real threshold is known. + lodValues.push_back( Real( 0 ) ); + + for( size_t i = 0; i < lodConfig.levels.size(); ++i ) + { + if( !lodConfig.levels[i].outSkipped ) + { + lodValues.push_back( + lodConfig.strategy->transformUserValue( lodConfig.levels[i].distance ) ); + } + } + + if( lodValues.size() > 1u ) + { + // Duplicating the first real threshold into slot 0 is correct regardless + // of the strategy's value convention: lower_bound() behaves identically + // for any query value below this threshold whether slot 0 holds a true + // minimum or an exact copy of slot 1, and equality trivially satisfies + // the ascending requirement. + lodValues[0] = lodValues[1]; + } + else + { + // Degenerate case: every level was outSkipped, so there are no generated + // LOD levels at all (mVao[VpNormal] never grew past LOD-0). Fall back to + // getBaseValue() here since there is no other threshold to copy from -- + // this entry will never actually be compared against a second one. + lodValues[0] = lodConfig.strategy->getBaseValue(); + } + +#if OGRE_DEBUG_MODE + for( size_t i = 1u; i < lodValues.size(); ++i ) + { + if( lodValues[i - 1u] > lodValues[i] ) + { + Ogre::String dump; + for( size_t j = 0; j < lodValues.size(); ++j ) + dump += Ogre::StringConverter::toString( lodValues[j] ) + " "; + LogManager::getSingleton().logMessage( + "ERROR: MeshLodGenerator::_configureMeshLodUsageV2: lodValues not " + "ascending: " + + dump ); + } + } +#endif + + lodConfig.meshV2->setLodStrategyName( lodConfig.strategy->getName() ); + lodConfig.meshV2->_setLodValues( lodValues ); + } + MeshLodGenerator::MeshLodGenerator() : mWQWorker( NULL ), mWQInjector( NULL ) {} MeshLodGenerator::~MeshLodGenerator() @@ -164,6 +253,12 @@ namespace Ogre } if( lodConfig.advanced.useBackgroundQueue ) { + // V2 meshes are not (yet) supported on the background queue path -- the + // existing LodInputProviderBuffer/LodOutputProvider*Buffer classes are + // built around v1::Mesh's WorkQueue-friendly buffer-copy contract. A v2 + // background-queue variant is explicitly out of scope for this change; + // generateLodLevels() rejects useBackgroundQueue + isV2() before reaching + // here (see below) so this branch is unreachable for v2 configs today. if( !input ) { input = LodInputProviderPtr( new LodInputProviderBuffer( lodConfig.mesh ) ); @@ -185,11 +280,24 @@ namespace Ogre { if( !input ) { - input = LodInputProviderPtr( new LodInputProviderMesh( lodConfig.mesh ) ); + if( lodConfig.isV2() ) + input = LodInputProviderPtr( new LodInputProviderMeshV2( lodConfig.meshV2 ) ); + else + input = LodInputProviderPtr( new LodInputProviderMesh( lodConfig.mesh ) ); } if( !output ) { - if( lodConfig.advanced.useCompression ) + if( lodConfig.isV2() ) + { + // Note: compressed output (LodOutputProviderCompressedMesh) has no + // v2 equivalent yet -- v2's Vao-based storage doesn't have the same + // "shared faces with frame shifting" representation that mechanism + // was built around. useCompression is silently ignored for v2 for + // now; flagged here rather than in the PR description alone so it + // isn't missed during review. + output = LodOutputProviderPtr( new LodOutputProviderMeshV2( lodConfig.meshV2 ) ); + } + else if( lodConfig.advanced.useCompression ) { output = LodOutputProviderPtr( new LodOutputProviderCompressedMesh( lodConfig.mesh ) ); @@ -215,7 +323,10 @@ namespace Ogre { // This will be processed in LodWorkQueueInjector if we use background queue. output->inject(); - _configureMeshLodUsage( lodConfig ); + if( lodConfig.isV2() ) + _configureMeshLodUsageV2( lodConfig ); + else + _configureMeshLodUsage( lodConfig ); // lodConfig.mesh->buildEdgeList(); } } @@ -223,6 +334,17 @@ namespace Ogre LodDataPtr data, LodInputProviderPtr input, LodOutputProviderPtr output, LodCollapserPtr collapser ) { + if( lodConfig.isV2() && lodConfig.advanced.useBackgroundQueue ) + { + // See the comment in _resolveComponents(): the WorkQueue-based providers + // are built around v1::Mesh's buffer-copy contract and have no v2 + // equivalent yet. Fail loudly here instead of silently falling through to + // a v1-only code path with a null lodConfig.mesh. + OGRE_EXCEPT( Exception::ERR_NOT_IMPLEMENTED, + "useBackgroundQueue is not yet supported for v2 meshes.", + "MeshLodGenerator::generateLodLevels" ); + } + // If we don't have generated Lod levels, we can use _generateManualLodLevels. bool hasGeneratedLevels = false; for( size_t i = 0; i < lodConfig.levels.size(); i++ ) @@ -254,7 +376,29 @@ namespace Ogre _generateManualLodLevels( lodConfig ); } - lodConfig.mesh->prepareForShadowMapping( false ); + if( lodConfig.isV2() ) + { + // Deliberately NOT calling lodConfig.meshV2->prepareForShadowMapping() + // here (unlike the v1 branch below). That triggers + // VertexShadowMapHelper::optimizeForShadowMapping() when + // Mesh::msOptimizeForShadowMapping is enabled (likely the default), + // which rebuilds mVao[VpShadow] from scratch independently of + // mVao[VpNormal] -- a code path this change has not audited, and one + // that risks breaking the shared-VAO invariant + // (mVao[VpNormal][0] == mVao[VpShadow][0]) that + // SubMesh::destroyShadowMappingVaos() relies on to safely skip + // double-destroying buffers on mesh teardown. LodOutputProviderMeshV2:: + // bakeLodLevel() already keeps mVao[VpShadow] correctly in lockstep + // with mVao[VpNormal] for the common (shared-buffers) case, which is + // exactly the case that safety check handles correctly. Callers who + // specifically need independent/optimized shadow Vaos can call + // Mesh::prepareForShadowMapping(true) themselves afterwards -- see the + // WARNING LodOutputProviderMeshV2::bakeLodLevel() logs for that case. + } + else + { + lodConfig.mesh->prepareForShadowMapping( false ); + } } void MeshLodGenerator::computeLods( LodConfig &lodConfig, LodData *data, LodCollapseCost *cost, @@ -335,6 +479,17 @@ namespace Ogre void MeshLodGenerator::_generateManualLodLevels( LodConfig &lodConfig ) { + if( lodConfig.isV2() ) + { + // Manual LOD levels are not supported for v2 meshes -- see the comment in + // LodOutputProviderMeshV2::bakeManualLodLevel. This path is v1-only + // (constructs a LodOutputProviderMesh directly below), so fail loudly + // here rather than passing a null v1::MeshPtr into it. + OGRE_EXCEPT( Exception::ERR_NOT_IMPLEMENTED, + "All-manual Lod level configs are not supported for v2 meshes.", + "MeshLodGenerator::_generateManualLodLevels" ); + } + LodOutputProviderMesh output( lodConfig.mesh ); output.prepare( NULL ); for( unsigned short curLod = 0; curLod < lodConfig.levels.size(); curLod++ ) @@ -362,4 +517,4 @@ namespace Ogre } } -} // namespace Ogre +} // namespace Ogre \ No newline at end of file diff --git a/OgreMain/include/OgreMesh2.h b/OgreMain/include/OgreMesh2.h index e51f087aa19..7897c784e00 100644 --- a/OgreMain/include/OgreMesh2.h +++ b/OgreMain/include/OgreMesh2.h @@ -357,6 +357,26 @@ namespace Ogre /** Internal methods for loading LOD, do not use. */ // void _setSubMeshLodFaceList(unsigned short subIdx, unsigned short level, IndexData* facedata); + /** Sets the per-LOD-level switch values for this mesh directly. + @remarks + Unlike _setLodInfo() (which historically only carried a level count and + was never completed for v2 meshes -- see its implementation), this sets + the actual transformed LodStrategy values used at render time to pick + which entry of SubMesh::mVao[...] to render (see LodStrategy::lodSet + and RenderQueue::addRenderable). + @param lodValues + One value per LOD level, sorted from most to least detail, starting + with the base (full detail) level. Must have the same number of + entries as every SubMesh::mVao[VpNormal]/[VpShadow] in this mesh, + or rendering will read out of bounds (see the assert in + RenderQueue::addRenderable). + @par + Intended to be called by LOD generators (e.g. MeshLodGenerator's v2 + output provider) once every SubMesh has had its LOD VAOs appended via + SubMesh::mVao[...].push_back(). Not intended for general use. + */ + void _setLodValues( const LodValueArray &lodValues ); + /** Removes all LOD data from this Mesh. */ void removeLodLevels(); diff --git a/OgreMain/src/OgreMesh2.cpp b/OgreMain/src/OgreMesh2.cpp index 4923de57acc..c06771db814 100644 --- a/OgreMain/src/OgreMesh2.cpp +++ b/OgreMain/src/OgreMesh2.cpp @@ -372,6 +372,21 @@ namespace Ogre */ } //--------------------------------------------------------------------- + void Mesh::_setLodValues( const LodValueArray &lodValues ) + { +#if OGRE_DEBUG_MODE + // lodSet() (see OgreLodStrategyPrivate.inl) relies on std::lower_bound over this + // array, which requires it to be sorted from most to least detail (ascending values + // for strategies such as PixelCountLodStrategy where 'value' grows as detail drops). + for( size_t i = 1u; i < lodValues.size(); ++i ) + { + OgreAssert( lodValues[i - 1u] <= lodValues[i], + "Lod values must be sorted from most to least detail" ); + } +#endif + mLodValues = lodValues; + } + //--------------------------------------------------------------------- /*void Mesh::_setSubMeshLodFaceList(unsigned short subIdx, unsigned short level, IndexData* facedata) { @@ -390,24 +405,47 @@ namespace Ogre //-------------------------------------------------------------------- void Mesh::removeLodLevels() { -#if !OGRE_NO_MESHLOD - // Remove data from SubMeshes - /*for( SubMesh *submesh : mSubMeshList ) - submesh->removeLodLevels(); + if( getNumLodLevels() <= 1u ) + return; // Nothing to remove. - freeEdgeList(); - mMeshLodUsageList.clear(); - mLodValues.clear(); + VaoManager *vaoManager = _getVaoManager(); - LodStrategy *lodStrategy = LodStrategyManager::getSingleton().getDefaultStrategy(); + for( SubMesh *subMesh : mSubMeshes ) + { + // Collect every Vao beyond index 0 (the base, full-detail level) that + // needs destroying. mVao[VpShadow] commonly shares pointers with + // mVao[VpNormal] (see SubMesh::destroyShadowMappingVaos's own + // [0]==[0] check) -- dedup so a shared Vao is only destroyed once, + // rather than risking the double-destroy this codebase has already hit + // once during this change (see the GameState MeshPtr lifetime bug). + VertexArrayObjectArray toDestroy; + + for( size_t i = 1u; i < subMesh->mVao[VpNormal].size(); ++i ) + toDestroy.push_back( subMesh->mVao[VpNormal][i] ); + + for( size_t i = 1u; i < subMesh->mVao[VpShadow].size(); ++i ) + { + VertexArrayObject *shadowVao = subMesh->mVao[VpShadow][i]; + bool alreadyQueued = false; + for( size_t j = 0u; j < toDestroy.size() && !alreadyQueued; ++j ) + alreadyQueued = ( toDestroy[j] == shadowVao ); + if( !alreadyQueued ) + toDestroy.push_back( shadowVao ); + } - // Reinitialise - mNumLods = 1; - mMeshLodUsageList.resize(1); - mMeshLodUsageList[0].edgeData = NULL; - // TODO: Shouldn't we rebuild edge lists after freeing them? - mLodValues.push_back( lodStrategy->getBaseValue() );*/ -#endif + if( !toDestroy.empty() ) + SubMesh::destroyVaos( toDestroy, vaoManager, true ); + + if( subMesh->mVao[VpNormal].size() > 1u ) + subMesh->mVao[VpNormal].resize( 1u ); + if( subMesh->mVao[VpShadow].size() > 1u ) + subMesh->mVao[VpShadow].resize( 1u ); + } + + // Empty mLodValues means "always render LOD 0" -- lodSet()'s lower_bound + // over an empty range returns end(), and end() - begin() - 1 clamped to >= 0 + // gives mCurrentMeshLod = 0. No need to push a base value back in. + mLodValues.clear(); } //--------------------------------------------------------------------- void Mesh::_setHashForCaches( const uint64 hash[2] ) @@ -612,4 +650,4 @@ namespace Ogre return independent; } //--------------------------------------------------------------------- -} // namespace Ogre +} // namespace Ogre \ No newline at end of file diff --git a/Samples/2.0/ApiUsage/MeshLodV2/CMakeLists.txt b/Samples/2.0/ApiUsage/MeshLodV2/CMakeLists.txt new file mode 100644 index 00000000000..b0ac29e8692 --- /dev/null +++ b/Samples/2.0/ApiUsage/MeshLodV2/CMakeLists.txt @@ -0,0 +1,26 @@ +#------------------------------------------------------------------- +# This file is part of the CMake build system for OGRE-Next +# (Object-oriented Graphics Rendering Engine) +# For the latest info, see http://www.ogre3d.org/ +# +# The contents of this file are placed in the public domain. Feel +# free to make use of it in any way you like. +#------------------------------------------------------------------- + +macro( add_recursive dir retVal ) + file( GLOB_RECURSE ${retVal} ${dir}/*.h ${dir}/*.cpp ${dir}/*.c ) +endmacro() + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) + +include_directories(${CMAKE_SOURCE_DIR}/Components/Hlms/Common/include) +include_directories(${CMAKE_SOURCE_DIR}/Components/MeshLodGenerator/include) +ogre_add_component_include_dir(Hlms/Pbs) + +add_recursive( ./ SOURCE_FILES ) + +ogre_add_executable(Sample_MeshLodV2 WIN32 MACOSX_BUNDLE ${SOURCE_FILES} ${SAMPLE_COMMON_RESOURCES}) + +target_link_libraries(Sample_MeshLodV2 ${OGRE_LIBRARIES} ${OGRE_MeshLodGenerator_LIBRARIES} ${OGRE_SAMPLES_LIBRARIES}) +ogre_config_sample_lib(Sample_MeshLodV2) +ogre_config_sample_pkg(Sample_MeshLodV2) diff --git a/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2.cpp b/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2.cpp new file mode 100644 index 00000000000..cdf8648f299 --- /dev/null +++ b/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2.cpp @@ -0,0 +1,101 @@ +#include "GraphicsSystem.h" + +#include "MeshLodV2GameState.h" + +#include "Compositor/OgreCompositorManager2.h" +#include "OgreCamera.h" +#include "OgreConfigFile.h" +#include "OgreRoot.h" +#include "OgreSceneManager.h" +#include "OgreWindow.h" + +// Declares WinMain / main +#include "MainEntryPointHelper.h" +#include "System/Android/AndroidSystems.h" +#include "System/MainEntryPoints.h" + +#if OGRE_PLATFORM != OGRE_PLATFORM_ANDROID +# if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 +INT WINAPI WinMainApp( HINSTANCE hInst, HINSTANCE hPrevInstance, LPSTR strCmdLine, INT nCmdShow ) +# else +int mainApp( int argc, const char *argv[] ) +# endif +{ + return Demo::MainEntryPoints::mainAppSingleThreaded( DEMO_MAIN_ENTRY_PARAMS ); +} +#endif + +namespace Demo +{ + class MeshLodV2GraphicsSystem final : public GraphicsSystem + { + void setupResources() override + { + GraphicsSystem::setupResources(); + + Ogre::ConfigFile cf; + cf.load( AndroidSystems::openFile( mResourcePath + "resources2.cfg" ) ); + + Ogre::String dataFolder = cf.getSetting( "DoNotUseAsResource", "Hlms", "" ); + + if( dataFolder.empty() ) + dataFolder = AndroidSystems::isAndroid() ? "/" : "./"; + else if( *( dataFolder.end() - 1 ) != '/' ) + dataFolder += "/"; + + Ogre::String dataFolderSinbad = dataFolder; + + dataFolder += "2.0/scripts/materials/PbsMaterials"; + + addResourceLocation( dataFolder, getMediaReadArchiveType(), "General" ); + + // Sinbad.mesh is shipped in legacy v1 binary format -- loading it as v1 + // is unavoidable (that's the only format the asset exists in on disk). + // The point of this sample is what happens AFTER that load: see + // MeshLodV2GameState::loadAndConvertSinbadToV2() for the v1 -> v2 import + // with NO LOD baked in, followed by native v2 LOD generation. + dataFolderSinbad += "packs/Sinbad.zip"; + addResourceLocation( dataFolderSinbad, "Zip", "General" ); + } + + public: + MeshLodV2GraphicsSystem( GameState *gameState ) : GraphicsSystem( gameState ) {} + }; + + void MainEntryPoints::createSystems( GameState **outGraphicsGameState, + GraphicsSystem **outGraphicsSystem, + GameState **outLogicGameState, LogicSystem **outLogicSystem ) + { + MeshLodV2GameState *gfxGameState = new MeshLodV2GameState( + "Shows how to automatically generate LODs directly against a v2 Mesh,\n" + "compared side-by-side for two different sources:\n" + " - A procedurally-built sphere: never touches v1 at all.\n" + " - Sinbad: loaded as v1 (the only format the asset ships in), imported\n" + " to v2 with NO LOD baked in during that import, then LOD-generated\n" + " directly against the v2 result -- unlike the original MeshLod\n" + " sample, which generates LOD on the v1 mesh BEFORE importing it.\n" + "Both use the same native v2 MeshLodGenerator path once their v2 Mesh\n" + "exists; only how that v2 Mesh first came to exist differs.\n" + "Fly away to see both switch LOD levels (logged to console on change).\n" + "Press F2 to toggle wireframe on both and see the triangle count drop." ); + + GraphicsSystem *graphicsSystem = new MeshLodV2GraphicsSystem( gfxGameState ); + + gfxGameState->_notifyGraphicsSystem( graphicsSystem ); + + *outGraphicsGameState = gfxGameState; + *outGraphicsSystem = graphicsSystem; + } + + void MainEntryPoints::destroySystems( GameState *graphicsGameState, GraphicsSystem *graphicsSystem, + GameState *logicGameState, LogicSystem *logicSystem ) + { + delete graphicsSystem; + delete graphicsGameState; + } + + const char *MainEntryPoints::getWindowTitle() + { + return "Automatic LOD Generation Sample (v2-native)"; + } +} // namespace Demo \ No newline at end of file diff --git a/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2.h b/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2GameState.cpp b/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2GameState.cpp new file mode 100644 index 00000000000..cc0d7e4f60c --- /dev/null +++ b/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2GameState.cpp @@ -0,0 +1,423 @@ +#include "MeshLodV2GameState.h" +#include "CameraController.h" +#include "GraphicsSystem.h" + +#include "OgreItem.h" +#include "OgreSceneManager.h" +#include "OgreSubItem.h" + +#include "OgreMesh.h" +#include "OgreMesh2.h" +#include "OgreMeshManager.h" +#include "OgreMeshManager2.h" +#include "OgreSubMesh2.h" + +#include "OgreCamera.h" +#include "OgreWindow.h" + +#include "OgreHlmsManager.h" +#include "OgreHlmsPbs.h" +#include "OgreHlmsPbsDatablock.h" +#include "OgreRoot.h" + +#include "OgreLodConfig.h" +#include "OgreLodStrategyManager.h" +#include "OgreMeshLodGenerator.h" +#include "OgrePixelCountLodStrategy.h" + +#include "OgreLogManager.h" +#include "OgreStringConverter.h" + +#include "Vao/OgreIndexBufferPacked.h" +#include "Vao/OgreVaoManager.h" +#include "Vao/OgreVertexArrayObject.h" +#include "Vao/OgreVertexBufferPacked.h" + +#include + +using namespace Demo; + +namespace +{ + /// Dummy ManualResourceLoader: the sphere mesh's geometry is filled in directly + /// by createProceduralSphereMeshV2() before load() is ever called, so there is + /// nothing for prepareResource()/loadResource() to actually do. A loader still + /// has to be supplied to createManual(), or Ogre logs a "no manual loader + /// provided" warning. + class ProceduralMeshLoader : public Ogre::ManualResourceLoader + { + public: + void prepareResource( Ogre::Resource * ) override {} + void loadResource( Ogre::Resource * ) override {} + }; + + ProceduralMeshLoader gProceduralMeshLoader; +} // namespace + +namespace Demo +{ + MeshLodV2GameState::MeshLodV2GameState( const Ogre::String &helpDescription ) : + TutorialGameState( helpDescription ), + mSphereItem( 0 ), + mSinbadItem( 0 ), + mSphereDatablock( 0 ), + mSinbadDatablock( 0 ), + mWireframeOn( false ), + mLastLoggedLodSphere( 0 ), + mLastLoggedLodSinbad( 0 ) + { + } + //----------------------------------------------------------------------------------- + Ogre::MeshPtr MeshLodV2GameState::createProceduralSphereMeshV2( const Ogre::String &meshName, + float radius, unsigned numRings, + unsigned numSegments ) + { + Ogre::VaoManager *vaoManager = mGraphicsSystem->getRoot()->getRenderSystem()->getVaoManager(); + + const unsigned rowStride = numSegments + 1u; + const size_t numVertices = ( numRings + 1u ) * rowStride; + const size_t numTriangles = static_cast( numRings ) * numSegments * 2u; + const size_t numIndices = numTriangles * 3u; + + // POS(3) + NORMAL(3) + UV(2) = 8 floats per vertex. No tangent: this sample + // doesn't use a normal map, so we don't need one. + const size_t floatsPerVertex = 8u; + float *vertexData = reinterpret_cast( OGRE_MALLOC_SIMD( + numVertices * floatsPerVertex * sizeof( float ), Ogre::MEMCATEGORY_GEOMETRY ) ); + + size_t vOffset = 0u; + for( unsigned r = 0u; r <= numRings; ++r ) + { + const float v = static_cast( r ) / static_cast( numRings ); + const float phi = v * Ogre::Math::PI; // 0 (north pole) .. PI (south pole) + const float sinPhi = Ogre::Math::Sin( phi ); + const float cosPhi = Ogre::Math::Cos( phi ); + + for( unsigned s = 0u; s <= numSegments; ++s ) + { + const float u = static_cast( s ) / static_cast( numSegments ); + + float nx, ny, nz; + + if( s == numSegments ) + { + // Wrap-around seam column (theta == 2*PI, the same physical + // point as s == 0's theta == 0). Copy the first column's + // position/normal bit-for-bit instead of recomputing via + // sin/cos(2*PI): floating point doesn't guarantee + // sin(2*PI) == sin(0) exactly, and that tiny divergence was + // enough to flip winding/normals on a sliver of triangles right + // at the seam, backface-culling them and showing the background + // through a visible crack (reproducible even at full LOD-0 + // detail, so this was never a LOD-collapse bug). + const size_t firstColumnOffset = + vOffset - static_cast( s ) * floatsPerVertex; + nx = vertexData[firstColumnOffset + 3]; + ny = vertexData[firstColumnOffset + 4]; + nz = vertexData[firstColumnOffset + 5]; + } + else + { + const float theta = u * Ogre::Math::TWO_PI; + const float sinTheta = Ogre::Math::Sin( theta ); + const float cosTheta = Ogre::Math::Cos( theta ); + + nx = sinPhi * cosTheta; + ny = cosPhi; + nz = sinPhi * sinTheta; + } + + vertexData[vOffset + 0] = nx * radius; + vertexData[vOffset + 1] = ny * radius; + vertexData[vOffset + 2] = nz * radius; + vertexData[vOffset + 3] = nx; + vertexData[vOffset + 4] = ny; + vertexData[vOffset + 5] = nz; + vertexData[vOffset + 6] = u; + vertexData[vOffset + 7] = v; + + vOffset += floatsPerVertex; + } + } + + Ogre::uint32 *indexData = reinterpret_cast( + OGRE_MALLOC_SIMD( numIndices * sizeof( Ogre::uint32 ), Ogre::MEMCATEGORY_GEOMETRY ) ); + size_t iOffset = 0u; + for( unsigned r = 0u; r < numRings; ++r ) + { + for( unsigned s = 0u; s < numSegments; ++s ) + { + const Ogre::uint32 a = r * rowStride + s; + const Ogre::uint32 b = a + rowStride; + const Ogre::uint32 c = a + 1u; + const Ogre::uint32 d = b + 1u; + + indexData[iOffset + 0] = a; + indexData[iOffset + 1] = b; + indexData[iOffset + 2] = c; + + indexData[iOffset + 3] = c; + indexData[iOffset + 4] = b; + indexData[iOffset + 5] = d; + + iOffset += 6u; + } + } + + Ogre::VertexElement2Vec vertexElements; + vertexElements.push_back( Ogre::VertexElement2( Ogre::VET_FLOAT3, Ogre::VES_POSITION ) ); + vertexElements.push_back( Ogre::VertexElement2( Ogre::VET_FLOAT3, Ogre::VES_NORMAL ) ); + vertexElements.push_back( + Ogre::VertexElement2( Ogre::VET_FLOAT2, Ogre::VES_TEXTURE_COORDINATES ) ); + + Ogre::VertexBufferPacked *vertexBuffer = vaoManager->createVertexBuffer( + vertexElements, numVertices, Ogre::BT_IMMUTABLE, vertexData, true ); + + Ogre::VertexBufferPackedVec vertexBuffers; + vertexBuffers.push_back( vertexBuffer ); + + Ogre::IndexBufferPacked *indexBuffer = vaoManager->createIndexBuffer( + Ogre::IndexBufferPacked::IT_32BIT, numIndices, Ogre::BT_IMMUTABLE, indexData, true ); + + Ogre::VertexArrayObject *vao = + vaoManager->createVertexArrayObject( vertexBuffers, indexBuffer, Ogre::OT_TRIANGLE_LIST ); + + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createManual( + meshName, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, &gProceduralMeshLoader ); + + Ogre::SubMesh *subMesh = mesh->createSubMesh(); + // Regular rendering and shadow casting share the same Vao (the common case, + // and the only one LodOutputProviderMeshV2 fully supports today -- see its + // bakeLodLevel() comment about independent shadow Vaos). + subMesh->mVao[Ogre::VpNormal].push_back( vao ); + subMesh->mVao[Ogre::VpShadow].push_back( vao ); + + const Ogre::Aabb aabb( Ogre::Vector3::ZERO, Ogre::Vector3( radius, radius, radius ) ); + mesh->_setBounds( aabb, false ); + mesh->_setBoundingSphereRadius( radius ); + + // Geometry is already filled in above; load() just transitions the Resource + // state machine to LOADSTATE_LOADED so Item::_initialise() will accept it + // (it calls mMesh->load() then checks isLoaded()). + mesh->load(); + + return mesh; + } + //----------------------------------------------------------------------------------- + void MeshLodV2GameState::generateLodLevelsV2( const Ogre::MeshPtr &meshV2, + const Ogre::String &logLabel ) + { + Ogre::MeshPtr meshCopy = meshV2; // getAutoconfig takes a non-const MeshPtr& + + Ogre::LodConfig lodConfig; + + Ogre::MeshLodGenerator lodGenerator; + lodGenerator.getAutoconfig( meshCopy, lodConfig ); + + lodConfig.strategy = Ogre::LodStrategyManager::getSingleton().getDefaultStrategy(); + + // getAutoconfig()'s reductionValue/reductionMethod already scale correctly + // with each mesh's own bounding radius, so those are kept as-is. Its + // distance heuristic does NOT scale with mesh size at all though -- it's a + // fixed number, so we replace it with explicit screen-coverage-ratio + // thresholds instead. Since ScreenRatioPixelCountLodStrategy is specifically + // designed to be scale-independent (that's the entire point of "ratio" over + // "absolute distance" or "absolute pixel count"), the SAME four thresholds + // work sensibly for both the sphere and Sinbad here despite their very + // different actual sizes -- which is itself a nice demonstration that this + // approach was the right fix, not just a per-mesh tuning hack. + OgreAssert( lodConfig.levels.size() == 4u, "Expected exactly 4 levels from getAutoconfig" ); + lodConfig.levels[0].distance = 0.35f; + lodConfig.levels[1].distance = 0.15f; + lodConfig.levels[2].distance = 0.05f; + lodConfig.levels[3].distance = 0.015f; + + // This is the entire point of the sample: no v1 mesh, no v1 import, no + // round-trip is needed from THIS point on. LOD levels are generated directly + // against meshV2 and land directly on its SubMesh::mVao[VpNormal]/[VpShadow] + // arrays. + lodGenerator.generateLodLevels( lodConfig ); + + Ogre::LogManager::getSingleton().logMessage( + "[MeshLodV2] Generated " + Ogre::StringConverter::toString( lodConfig.levels.size() ) + + " LOD levels directly against v2 mesh '" + meshV2->getName() + "' (" + logLabel + ")." ); + } + //----------------------------------------------------------------------------------- + Ogre::MeshPtr MeshLodV2GameState::loadAndConvertSinbadToV2() + { + // Sinbad.mesh ships in legacy v1 binary format -- loading it as v1 here is + // unavoidable, that's the only format the asset exists in on disk. + Ogre::v1::MeshPtr meshV1 = Ogre::v1::MeshManager::getSingleton().load( + "Sinbad.mesh", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::v1::HardwareBuffer::HBU_STATIC, Ogre::v1::HardwareBuffer::HBU_STATIC ); + + // Import v1 -> v2 with ZERO LOD levels baked in -- this is the key + // difference from the original MeshLod sample, which runs + // MeshLodGenerator against meshV1 BEFORE this import step (so the import + // carries pre-baked LOD across via SubMesh::importBuffersFromV1's + // mLodFaceList loop). Here, generateLodLevelsV2() runs entirely afterwards, + // directly against the v2 result, through the exact same v2-native + // LodInputProviderMeshV2/LodOutputProviderMeshV2 path the procedural sphere + // uses -- proving the new generator works on a real, skinned, + // multi-submesh mesh, not just a clean procedural one. + Ogre::MeshPtr meshV2 = Ogre::MeshManager::getSingleton().createByImportingV1( + "Sinbad_v2_native_lod.mesh", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + meshV1.get(), true, true, true ); + meshV2->load(); + + // The v1 mesh has done its only job (letting us produce a v2 Mesh at all, + // since that's the format the asset ships in) and is no longer needed -- + // everything from here on operates purely on meshV2. + meshV1->unload(); + Ogre::v1::MeshManager::getSingleton().remove( meshV1 ); + + return meshV2; + } + //----------------------------------------------------------------------------------- + void MeshLodV2GameState::createScene01() + { + Ogre::SceneManager *sceneManager = mGraphicsSystem->getSceneManager(); + + Ogre::SceneNode *rootNode = sceneManager->getRootSceneNode(); + + Ogre::Light *light = sceneManager->createLight(); + Ogre::SceneNode *lightNode = rootNode->createChildSceneNode(); + lightNode->attachObject( light ); + light->setPowerScale( 1.0f ); + light->setType( Ogre::Light::LT_DIRECTIONAL ); + light->setDirection( Ogre::Vector3( -1, -1, -1 ).normalisedCopy() ); + + mLightNodes[0] = lightNode; + + sceneManager->setAmbientLight( Ogre::ColourValue( 0.3f, 0.5f, 0.7f ) * 0.1f * 0.75f, + Ogre::ColourValue( 0.6f, 0.45f, 0.3f ) * 0.065f * 0.75f, + -light->getDirection() + Ogre::Vector3::UNIT_Y * 0.2f ); + + light = sceneManager->createLight(); + lightNode = rootNode->createChildSceneNode(); + lightNode->attachObject( light ); + light->setDiffuseColour( 0.8f, 0.4f, 0.2f ); // Warm + light->setSpecularColour( 0.8f, 0.4f, 0.2f ); + light->setPowerScale( Ogre::Math::PI ); + light->setType( Ogre::Light::LT_SPOTLIGHT ); + lightNode->setPosition( -10.0f, 10.0f, 10.0f ); + light->setDirection( Ogre::Vector3( 1, -1, -1 ).normalisedCopy() ); + light->setAttenuationBasedOnRadius( 10.0f, 0.01f ); + + mLightNodes[1] = lightNode; + + light = sceneManager->createLight(); + lightNode = rootNode->createChildSceneNode(); + lightNode->attachObject( light ); + light->setDiffuseColour( 0.2f, 0.4f, 0.8f ); // Cold + light->setSpecularColour( 0.2f, 0.4f, 0.8f ); + light->setPowerScale( Ogre::Math::PI ); + light->setType( Ogre::Light::LT_SPOTLIGHT ); + lightNode->setPosition( 10.0f, 10.0f, -10.0f ); + light->setDirection( Ogre::Vector3( -1, -1, 1 ).normalisedCopy() ); + light->setAttenuationBasedOnRadius( 10.0f, 0.01f ); + + mLightNodes[2] = lightNode; + + mCameraController = new CameraController( mGraphicsSystem, false ); + + Ogre::LodStrategyManager::getSingleton().setDefaultStrategy( + Ogre::ScreenRatioPixelCountLodStrategy::getSingletonPtr() ); + + // Build the sphere directly as a v2 mesh. High subdivision (96x48 = ~9200 + // triangles) so the LOD reduction is visually unmistakable, especially with + // the F2 wireframe toggle. Deliberately a LOCAL variable (see the comment on + // createProceduralSphereMeshV2()'s declaration) -- once createItem() below + // has its own reference, this local going out of scope at the end of this + // function is correct and expected. + Ogre::MeshPtr sphereMeshV2 = + createProceduralSphereMeshV2( "ProceduralLodSphereV2", 2.0f, 48u, 96u ); + generateLodLevelsV2( sphereMeshV2, "procedurally-built sphere, never touched v1" ); + + Ogre::MeshPtr sinbadMeshV2 = loadAndConvertSinbadToV2(); + generateLodLevelsV2( sinbadMeshV2, + "Sinbad, v1-imported with no LOD then LOD-generated natively on v2" ); + + Ogre::HlmsManager *hlmsManager = mGraphicsSystem->getRoot()->getHlmsManager(); + Ogre::HlmsPbs *hlmsPbs = static_cast( hlmsManager->getHlms( Ogre::HLMS_PBS ) ); + + mSphereDatablock = static_cast( + hlmsPbs->createDatablock( "LodSphereV2", "LodSphereV2", Ogre::HlmsMacroblock(), + Ogre::HlmsBlendblock(), Ogre::HlmsParamVec() ) ); + mSphereDatablock->setDiffuse( Ogre::Vector3( 0.6f, 0.6f, 0.65f ) ); + + mSphereItem = sceneManager->createItem( sphereMeshV2, Ogre::SCENE_DYNAMIC ); + mSphereItem->setDatablock( mSphereDatablock ); + + Ogre::SceneNode *sphereNode = rootNode->createChildSceneNode( Ogre::SCENE_DYNAMIC ); + sphereNode->setPosition( Ogre::Vector3( -3.0f, 2.0f, 0.0f ) ); + sphereNode->attachObject( mSphereItem ); + + mSinbadDatablock = static_cast( + hlmsPbs->createDatablock( "LodSinbadV2", "LodSinbadV2", Ogre::HlmsMacroblock(), + Ogre::HlmsBlendblock(), Ogre::HlmsParamVec() ) ); + mSinbadDatablock->setDiffuse( Ogre::Vector3( 0.65f, 0.55f, 0.45f ) ); + + mSinbadItem = sceneManager->createItem( sinbadMeshV2, Ogre::SCENE_DYNAMIC ); + for( size_t i = 0; i < mSinbadItem->getNumSubItems(); ++i ) + mSinbadItem->getSubItem( i )->setDatablock( mSinbadDatablock ); + + Ogre::SceneNode *sinbadNode = rootNode->createChildSceneNode( Ogre::SCENE_DYNAMIC ); + sinbadNode->setPosition( Ogre::Vector3( 3.0f, 0.0f, 0.0f ) ); + sinbadNode->setScale( Ogre::Vector3( 1.0f ) ); + sinbadNode->attachObject( mSinbadItem ); + + TutorialGameState::createScene01(); + } + //----------------------------------------------------------------------------------- + void MeshLodV2GameState::update( float timeSinceLast ) + { + if( mSphereItem ) + { + const Ogre::uint8 currentLod = mSphereItem->getCurrentMeshLod(); + if( currentLod != mLastLoggedLodSphere ) + { + mLastLoggedLodSphere = currentLod; + Ogre::LogManager::getSingleton().logMessage( + "[MeshLodV2] Sphere switched to LOD level " + + Ogre::StringConverter::toString( static_cast( currentLod ) ) ); + } + } + + if( mSinbadItem ) + { + const Ogre::uint8 currentLod = mSinbadItem->getCurrentMeshLod(); + if( currentLod != mLastLoggedLodSinbad ) + { + mLastLoggedLodSinbad = currentLod; + Ogre::LogManager::getSingleton().logMessage( + "[MeshLodV2] Sinbad switched to LOD level " + + Ogre::StringConverter::toString( static_cast( currentLod ) ) ); + } + } + + TutorialGameState::update( timeSinceLast ); + } + //----------------------------------------------------------------------------------- + void MeshLodV2GameState::keyReleased( const SDL_KeyboardEvent &arg ) + { + if( arg.keysym.sym == SDLK_F2 ) + { + mWireframeOn = !mWireframeOn; + + const Ogre::PolygonMode mode = mWireframeOn ? Ogre::PM_WIREFRAME : Ogre::PM_SOLID; + + Ogre::HlmsMacroblock sphereMacroblock( *mSphereDatablock->getMacroblock() ); + sphereMacroblock.mPolygonMode = mode; + mSphereDatablock->setMacroblock( sphereMacroblock ); + + Ogre::HlmsMacroblock sinbadMacroblock( *mSinbadDatablock->getMacroblock() ); + sinbadMacroblock.mPolygonMode = mode; + mSinbadDatablock->setMacroblock( sinbadMacroblock ); + return; + } + + TutorialGameState::keyReleased( arg ); + } + //----------------------------------------------------------------------------------- +} // namespace Demo \ No newline at end of file diff --git a/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2GameState.h b/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2GameState.h new file mode 100644 index 00000000000..bde7a4aa07f --- /dev/null +++ b/Samples/2.0/ApiUsage/MeshLodV2/MeshLodV2GameState.h @@ -0,0 +1,68 @@ +#ifndef _Demo_MeshLodV2GameState_H_ +#define _Demo_MeshLodV2GameState_H_ + +#include "OgrePrerequisites.h" +#include "TutorialGameState.h" +#include "OgreHlmsPbsDatablock.h" + +namespace Demo +{ + class MeshLodV2GameState : public TutorialGameState + { + Ogre::SceneNode *mLightNodes[3]; + + Ogre::Item *mSphereItem; + + /// Sinbad, imported v1 -> v2 with NO LOD baked in during that import (unlike + /// the original MeshLod sample), then LOD-generated directly against the + /// resulting v2 mesh via the same v2-native code path the sphere uses. A + /// real, skinned, multi-submesh mesh as a comparison/stress case alongside + /// the procedural sphere. + Ogre::Item *mSinbadItem; + + Ogre::HlmsPbsDatablock *mSphereDatablock; + Ogre::HlmsPbsDatablock *mSinbadDatablock; + bool mWireframeOn; + + /// Last LOD level logged per-Item, so update() only logs on an actual change + /// instead of every frame. + Ogre::uint8 mLastLoggedLodSphere; + Ogre::uint8 mLastLoggedLodSinbad; + + /// Builds a UV sphere directly as a v2 Mesh: creates the Mesh via + /// MeshManager::createManual(), fills a SubMesh's mVao[VpNormal]/[VpShadow] + /// directly via VaoManager::createVertexBuffer()/createIndexBuffer()/ + /// createVertexArrayObject(), and sets bounds -- no v1 mesh involved. + /// Returns the MeshPtr as a local value deliberately -- see createScene01(): + /// only the Item created from it should keep it alive long-term, not a + /// GameState member (a GameState-owned MeshPtr outlives GraphicsSystem's own + /// teardown order and crashes on exit reading an already-destroyed + /// VaoManager). + Ogre::MeshPtr createProceduralSphereMeshV2( const Ogre::String &meshName, float radius, + unsigned numRings, unsigned numSegments ); + + /// Loads Sinbad.mesh as v1 (unavoidable -- that's the format the shipped + /// asset is in), imports it straight to v2 with createByImportingV1() while + /// it still has zero LOD levels, then discards the v1 mesh entirely. LOD + /// generation happens afterwards, directly against the v2 result, via + /// generateLodLevelsV2() below -- unlike the original MeshLod sample, which + /// generates LOD on the v1 mesh first and imports the result. + Ogre::MeshPtr loadAndConvertSinbadToV2(); + + /// Builds the LOD levels directly against a v2 mesh via + /// MeshLodGenerator::getAutoconfig()/generateLodLevels() -- the actual point + /// of this sample. Shared between the sphere and Sinbad since both use the + /// same explicit screen-coverage-ratio thresholds (see the comment at the + /// call site for why those thresholds are mesh-size-independent). + void generateLodLevelsV2( const Ogre::MeshPtr &meshV2, const Ogre::String &logLabel ); + + public: + MeshLodV2GameState( const Ogre::String &helpDescription ); + + void createScene01() override; + void update( float timeSinceLast ) override; + void keyReleased( const SDL_KeyboardEvent &arg ) override; + }; +} // namespace Demo + +#endif \ No newline at end of file diff --git a/Samples/2.0/CMakeLists.txt b/Samples/2.0/CMakeLists.txt index c7ed53f7b4d..fea4b91e13f 100644 --- a/Samples/2.0/CMakeLists.txt +++ b/Samples/2.0/CMakeLists.txt @@ -155,6 +155,7 @@ if( OGRE_BUILD_SAMPLES2 AND NOT OGRE_BUILD_SAMPLES2_SKIP ) add_subdirectory(ApiUsage/LocalCubemapsManualProbes) if( OGRE_BUILD_COMPONENT_MESHLODGENERATOR ) add_subdirectory(ApiUsage/MeshLod) + add_subdirectory(ApiUsage/MeshLodV2) endif() add_subdirectory(ApiUsage/MorphAnimations) if( OGRE_BUILD_PLUGIN_PFX ) diff --git a/Tools/MeshTool/src/main.cpp b/Tools/MeshTool/src/main.cpp index 3fa956cb6a3..95af0e8eb47 100644 --- a/Tools/MeshTool/src/main.cpp +++ b/Tools/MeshTool/src/main.cpp @@ -424,24 +424,46 @@ size_t getUniqueVertexCount(v1::MeshPtr mesh) MeshLodGenerator().generateLodLevels(lodConfig); return lodConfig.levels[0].outUniqueVertexCount; } -void buildLod(v1::MeshPtr& mesh) +size_t getUniqueVertexCount(MeshPtr mesh) +{ + // v2 equivalent of the overload above. Identical technique -- a single + // zero-reduction generated Lod level still has to walk every vertex through + // the position-based dedup pass, so outUniqueVertexCount comes out correct -- + // just routed through the v2-native LodInputProviderMeshV2/ + // LodOutputProviderMeshV2 path via LodConfig's v2 constructor. + LodConfig lodConfig(mesh, PixelCountLodStrategy::getSingletonPtr()); + lodConfig.advanced.useBackgroundQueue = false; // Non-threaded + lodConfig.createGeneratedLodLevel(0.0f, 0.0f); + MeshLodGenerator().generateLodLevels(lodConfig); + return lodConfig.levels[0].outUniqueVertexCount; +} +void buildLod(v1::MeshPtr& v1Mesh, MeshPtr& v2Mesh) { String response; + // True v2-native source (no v1 mesh at all) -- e.g. the input file was already + // a v2 .mesh, not something converted from v1 during this run. Previously this + // whole function printed "LOD Generation only works on v1 meshes at the + // moment." and returned immediately for this case; it's now handled by the new + // v2-native LodInputProviderMeshV2/LodOutputProviderMeshV2 path. + const bool isV2Source = ( !v1Mesh && v2Mesh ); + // Prompt for LOD generation? bool genLod = (opts.numLods != 0 || opts.interactive || opts.lodAutoconfigure); bool askLodDtls = opts.interactive; if (genLod) { - if( !mesh ) + if( !v1Mesh && !v2Mesh ) { - cout << "LOD Generation only works on v1 meshes at the moment." << endl; - cout << "Export it as -v1, run the command again, and re-export it to -v2" << endl; + // Nothing loaded at all -- shouldn't normally happen, loadMesh() would + // already have failed earlier. return; } // otherwise only ask if not specified on command line - if (mesh->getNumLodLevels() > 1) + const unsigned short existingLodLevels = + isV2Source ? v2Mesh->getNumLodLevels() : v1Mesh->getNumLodLevels(); + if (existingLodLevels > 1) { do { @@ -456,7 +478,14 @@ void buildLod(v1::MeshPtr& mesh) } else if (response == "d") { - mesh->removeLodLevels(); + // removeLodLevels() used to be a no-op stub on v2 Mesh (its body + // was entirely commented out) -- now implemented for real, see + // OgreMesh2.cpp, specifically because this exact call site needed + // it to actually work rather than silently leave stale LOD data. + if (isV2Source) + v2Mesh->removeLodLevels(); + else + v1Mesh->removeLodLevels(); genLod = false; askLodDtls = false; } @@ -502,7 +531,10 @@ void buildLod(v1::MeshPtr& mesh) int numLod; LodConfig lodConfig; - lodConfig.mesh = mesh; + if (isV2Source) + lodConfig.meshV2 = v2Mesh; + else + lodConfig.mesh = v1Mesh; lodConfig.strategy = DistanceLodStrategy::getSingletonPtr(); if (askLodDtls) { @@ -561,7 +593,7 @@ void buildLod(v1::MeshPtr& mesh) if (response == "f") { lodLevel.reductionMethod = LodLevel::VRM_CONSTANT; - vertexCount = getUniqueVertexCount(mesh); + vertexCount = isV2Source ? getUniqueVertexCount(v2Mesh) : getUniqueVertexCount(v1Mesh); } else if (response == "p") { @@ -738,15 +770,22 @@ void buildLod(v1::MeshPtr& mesh) { // ensure we use correct bounds + v1::MeshPtr nullv1; MeshPtr nullv2; - recalcBounds( mesh, nullv2 ); + if (isV2Source) + recalcBounds( nullv1, v2Mesh ); + else + recalcBounds( v1Mesh, nullv2 ); } MeshLodGenerator gen; if (opts.lodAutoconfigure) { // In this case we ignore other settings - gen.getAutoconfig(mesh, lodConfig); + if (isV2Source) + gen.getAutoconfig(v2Mesh, lodConfig); + else + gen.getAutoconfig(v1Mesh, lodConfig); } printLodConfig(lodConfig); @@ -1316,7 +1355,7 @@ int main(int numargs, char** args) resolveColourAmbiguities(mesh); } - buildLod( v1Mesh ); + buildLod( v1Mesh, v2Mesh ); buildEdgeLists( v1Mesh ); generateTangents( v1Mesh ); @@ -1378,4 +1417,4 @@ int main(int numargs, char** args) return retCode; -} +} \ No newline at end of file