Files
2026-10-06 22:25:34 -04:00

386 lines
16 KiB
C++

#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include "Rune Engine/Rune/Rune.h"
#include "Rune Engine/Rune/Rune Engine NULL.h"
#include "Rune Engine/Rune/Scene/Base/RuEntityBase.h"
#include "Rune Engine/Rune/Collision/RuType_Collision.h"
#include "Rune Engine/Rune/Engine/Geometry/RuMeshBase.h"
namespace
{
struct EntitySummary
{
CRuEntity *entity = NULL;
std::string name;
std::string type;
bool hasCollisionObject = false;
std::string collisionType;
bool collisionOptionEnabled = false;
bool collisionOptionPathReachable = false;
int meshCount = 0;
int meshCollisionStructureCount = 0;
int serializedCollisionTriangleCount = 0;
int completeSerializedCollisionTriangleCount = 0;
std::vector<std::string> meshCollisionTypes;
};
void Visit(CRuEntity *entity, bool parentCollisionPathReachable, std::vector<EntitySummary> &entities)
{
if(!entity)
return;
entity->UpdateTransformation();
EntitySummary summary;
summary.entity = entity;
summary.name = entity->GetObjectName() ? entity->GetObjectName() : "";
summary.type = entity->GetType().GetTypeName();
summary.collisionOptionEnabled = entity->GetOption(ruENTITYOPTION_COLLISION) != 0;
summary.collisionOptionPathReachable = parentCollisionPathReachable && summary.collisionOptionEnabled;
if(entity->GetCollisionObject())
{
summary.hasCollisionObject = true;
summary.collisionType = entity->GetCollisionObject()->GetType().GetTypeName();
}
if(entity->GetType().IsSubClassOf(IRuEntity_Renderable::Type()))
{
IRuEntity_Renderable *renderable = static_cast<IRuEntity_Renderable *>(entity);
summary.meshCount = renderable->GetNumMeshes();
for(INT32 i = 0; i < summary.meshCount; ++i)
{
IRuMesh *mesh = NULL;
IRuMaterial *material = NULL;
if(renderable->GetMesh(i, &mesh, &material) && mesh && mesh->GetType().IsSubClassOf(CRuMesh::Type()))
{
IRuCollisionStructure *structure = static_cast<CRuMesh *>(mesh)->GetCollisionStructure();
if(structure)
{
++summary.meshCollisionStructureCount;
summary.meshCollisionTypes.push_back(structure->GetType().GetTypeName());
}
else
{
summary.meshCollisionTypes.push_back("none(serialized); QueryCollision would build default from render mesh");
}
}
else
{
summary.meshCollisionTypes.push_back("unsupported mesh class");
}
}
}
entities.push_back(summary);
CRuEntity *child = entity->GetFirstChild();
while(child)
{
Visit(child, summary.collisionOptionPathReachable, entities);
child = child->GetNextSibling();
}
}
void QueryObject(CRuEntity *entity, IRuCollisionObject *object, CRuCollisionQuery_OBB_TriEnum &query)
{
CRuMatrix4x4 *originalWorldTransform = query.m_worldTransform;
CRuMatrix4x4 *originalInvWorldTransform = query.m_invWorldTransform;
query.m_worldTransform = const_cast<CRuMatrix4x4 *>(&entity->GetWorldTransform());
query.m_invWorldTransform = const_cast<CRuMatrix4x4 *>(&entity->GetInverseWorldTransform());
object->QueryCollision(&query);
query.m_worldTransform = originalWorldTransform;
query.m_invWorldTransform = originalInvWorldTransform;
}
void QuerySerializedDirectOnly(CRuEntity *entity, CRuCollisionQuery_OBB_TriEnum &query)
{
if(!entity)
return;
entity->UpdateTransformation();
if(entity->GetCollisionObject())
{
QueryObject(entity, entity->GetCollisionObject(), query);
return;
}
if(entity->GetType().IsSubClassOf(CRuModel::Type()))
{
CRuModel *model = static_cast<CRuModel *>(entity);
for(INT32 i = 0; i < model->GetNumMeshes(); ++i)
{
IRuMesh *mesh = NULL;
IRuMaterial *material = NULL;
if(model->GetMesh(i, &mesh, &material) && mesh && mesh->GetType().IsSubClassOf(CRuMesh::Type()))
{
IRuCollisionStructure *structure = static_cast<CRuMesh *>(mesh)->GetCollisionStructure();
if(structure)
{
CRuMatrix4x4 *originalWorldTransform = query.m_worldTransform;
CRuMatrix4x4 *originalInvWorldTransform = query.m_invWorldTransform;
query.m_worldTransform = const_cast<CRuMatrix4x4 *>(&entity->GetWorldTransform());
query.m_invWorldTransform = const_cast<CRuMatrix4x4 *>(&entity->GetInverseWorldTransform());
structure->QueryCollision(&query);
query.m_worldTransform = originalWorldTransform;
query.m_invWorldTransform = originalInvWorldTransform;
}
}
}
return;
}
}
void QuerySerializedOnly(CRuEntity *entity, CRuCollisionQuery_OBB_TriEnum &query, bool respectCollisionOptions)
{
if(!entity)
return;
entity->UpdateTransformation();
if(respectCollisionOptions && entity->GetOption(ruENTITYOPTION_COLLISION) == 0)
return;
QuerySerializedDirectOnly(entity, query);
if(entity->GetCollisionObject() || entity->GetType().IsSubClassOf(CRuModel::Type()))
return;
CRuEntity *child = entity->GetFirstChild();
while(child)
{
QuerySerializedOnly(child, query, respectCollisionOptions);
child = child->GetNextSibling();
}
}
bool WriteReport(const std::string &outputPath, const std::string &resourcePath,
const std::string &collisionMode, CRuEntity *root, std::vector<EntitySummary> &entities)
{
const bool respectCollisionOptions = collisionMode == "respect-options";
// A broad source-local box asks the native collision API to enumerate all
// source collision triangles. The selected query also invokes the actual
// engine hierarchy query with the requested entity-option semantics.
CRuCollisionQuery_OBB_TriEnum query;
query.m_ignoreCollisionFlag = respectCollisionOptions ? FALSE : TRUE;
query.m_exclusionMask = 0x00000000;
query.m_inclusionMask = 0x00000000;
query.m_colOBB = CRuOBB(CRuAABB(
CRuVector3(-10000.0f, -10000.0f, -10000.0f),
CRuVector3(10000.0f, 10000.0f, 10000.0f)));
CRuCollisionQuery_OBB_TriEnum serializedQuery;
serializedQuery.m_ignoreCollisionFlag = respectCollisionOptions ? FALSE : TRUE;
serializedQuery.m_colOBB = query.m_colOBB;
QuerySerializedOnly(root, serializedQuery, respectCollisionOptions);
CRuCollisionQuery_OBB_TriEnum completeSerializedQuery;
completeSerializedQuery.m_ignoreCollisionFlag = TRUE;
completeSerializedQuery.m_colOBB = query.m_colOBB;
if(respectCollisionOptions)
QuerySerializedOnly(root, completeSerializedQuery, false);
CRuCollisionQuery_OBB_TriEnum effectiveSerializedQuery;
effectiveSerializedQuery.m_ignoreCollisionFlag = FALSE;
effectiveSerializedQuery.m_colOBB = query.m_colOBB;
if(!respectCollisionOptions)
QuerySerializedOnly(root, effectiveSerializedQuery, true);
root->QueryCollision(&query);
std::ofstream out(outputPath.c_str(), std::ios::out | std::ios::trunc);
if(!out)
return false;
const INT32 vertexCount = query.m_colTriVerts->Count();
const INT32 serializedVertexCount = serializedQuery.m_colTriVerts->Count();
const INT32 completeSerializedVertexCount = respectCollisionOptions ? completeSerializedQuery.m_colTriVerts->Count() : serializedVertexCount;
const INT32 effectiveSerializedVertexCount = respectCollisionOptions ? serializedVertexCount : effectiveSerializedQuery.m_colTriVerts->Count();
for(auto &entity : entities)
{
if(entity.entity)
{
CRuCollisionQuery_OBB_TriEnum perEntityQuery;
perEntityQuery.m_ignoreCollisionFlag = TRUE;
perEntityQuery.m_colOBB = query.m_colOBB;
QuerySerializedDirectOnly(entity.entity, perEntityQuery);
entity.completeSerializedCollisionTriangleCount = perEntityQuery.m_colTriVerts->Count() / 3;
if(entity.collisionOptionPathReachable)
{
CRuCollisionQuery_OBB_TriEnum effectivePerEntityQuery;
effectivePerEntityQuery.m_ignoreCollisionFlag = FALSE;
effectivePerEntityQuery.m_colOBB = query.m_colOBB;
QuerySerializedDirectOnly(entity.entity, effectivePerEntityQuery);
entity.serializedCollisionTriangleCount = effectivePerEntityQuery.m_colTriVerts->Count() / 3;
}
}
}
CRuVector3 minima(0.0f, 0.0f, 0.0f);
CRuVector3 maxima(0.0f, 0.0f, 0.0f);
if(serializedVertexCount > 0)
{
minima = maxima = (*serializedQuery.m_colTriVerts)[0];
for(INT32 i = 1; i < serializedVertexCount; ++i)
{
const CRuVector3 &v = (*serializedQuery.m_colTriVerts)[i];
minima.x = min(minima.x, v.x); minima.y = min(minima.y, v.y); minima.z = min(minima.z, v.z);
maxima.x = max(maxima.x, v.x); maxima.y = max(maxima.y, v.y); maxima.z = max(maxima.z, v.z);
}
}
out << std::setprecision(9);
out << "{\n \"schemaVersion\": 2,\n";
out << " \"sourceModel\": \"" << resourcePath << "\",\n";
out << " \"collisionMode\": \"" << collisionMode << "\",\n";
out << " \"query\": \"CRuEntity::QueryCollision(CRuCollisionQuery_OBB_TriEnum), source-local OBB +/-10000, "
<< (respectCollisionOptions ? "ignoreCollisionFlag=false; ruENTITYOPTION_COLLISION gates respected" : "ignoreCollisionFlag=true; complete collision-option audit") << "\",\n";
out << " \"geometryOutput\": \"" << outputPath << ".obj\",\n";
out << " \"triangleFlagOutput\": \"" << outputPath << ".flags.bin\",\n";
out << " \"queryMasks\": {\"exclusionMask\": 0, \"inclusionMask\": 0, \"ignoreEntityCollisionOption\": "
<< (respectCollisionOptions ? "false" : "true") << "},\n";
out << " \"pathCollisionPolicy\": {\"exclusionMask\": 10485760, \"excludedBits\": {\"water\": 2097152, \"foliage\": 8388608}},\n";
out << " \"entityCount\": " << entities.size() << ",\n";
out << " \"entitiesWithCollisionObject\": ";
int colliderCount = 0;
for(const auto &entity : entities)
colliderCount += entity.hasCollisionObject ? 1 : 0;
out << colliderCount << ",\n";
out << " \"engineQueryTriangleCountIncludingRenderFallbacks\": " << vertexCount / 3 << ",\n";
out << " \"nativeCollisionTriangleCount\": " << serializedVertexCount / 3 << ",\n";
out << " \"nativeCollisionVertexCount\": " << serializedVertexCount << ",\n";
out << " \"completeNativeCollisionTriangleCount\": " << completeSerializedVertexCount / 3 << ",\n";
out << " \"enabledNativeCollisionTriangleCount\": " << effectiveSerializedVertexCount / 3 << ",\n";
out << " \"disabledSubtreeNativeTriangles\": " << (completeSerializedVertexCount - effectiveSerializedVertexCount) / 3 << ",\n";
out << " \"nativeTrianglesOmittedByCollisionOptions\": " << (completeSerializedVertexCount - effectiveSerializedVertexCount) / 3 << ",\n";
out << " \"renderMeshFallbackTriangleCount\": " << (vertexCount - serializedVertexCount) / 3 << ",\n";
std::map<UINT32, INT32> triangleFlagCounts;
for(INT32 i = 0; i < serializedQuery.m_colTriFlags->Count(); ++i)
++triangleFlagCounts[(*serializedQuery.m_colTriFlags)[i]];
out << " \"nativeTriangleFlagCounts\": {";
bool firstFlag = true;
for(const auto &entry : triangleFlagCounts)
{
if(!firstFlag)
out << ", ";
firstFlag = false;
out << "\"" << entry.first << "\": " << entry.second;
}
out << "},\n";
INT32 pathEligibleTriangleCount = 0;
for(INT32 i = 0; i < serializedQuery.m_colTriFlags->Count(); ++i)
{
if(((*serializedQuery.m_colTriFlags)[i] & 0x00A00000) == 0)
++pathEligibleTriangleCount;
}
out << " \"nativeTrianglesAfterTrekkerWaterFoliageExclusion\": " << pathEligibleTriangleCount << ",\n";
INT32 disabledOptionCount = 0;
INT32 unreachableEntityCount = 0;
for(const auto &entity : entities)
{
disabledOptionCount += entity.collisionOptionEnabled ? 0 : 1;
unreachableEntityCount += entity.collisionOptionPathReachable ? 0 : 1;
}
out << " \"disabledCollisionOptionEntityCount\": " << disabledOptionCount << ",\n";
out << " \"disabledEntityCount\": " << disabledOptionCount << ",\n";
out << " \"entityNodesSkippedByCollisionOptionGates\": " << unreachableEntityCount << ",\n";
out << " \"collisionOptionUnreachableEntityCount\": " << unreachableEntityCount << ",\n";
out << " \"nativeCollisionBounds\": {\"min\": [" << minima.x << ", " << minima.y << ", " << minima.z
<< "], \"max\": [" << maxima.x << ", " << maxima.y << ", " << maxima.z << "]},\n";
out << " \"colliderEntities\": [\n";
bool first = true;
for(const auto &entity : entities)
{
if(!entity.hasCollisionObject)
continue;
if(!first)
out << ",\n";
first = false;
out << " {\"name\": \"" << entity.name << "\", \"entityType\": \"" << entity.type
<< "\", \"collisionType\": \"" << entity.collisionType << "\", \"renderMeshCount\": "
<< entity.meshCount << "}";
}
out << "\n ],\n \"entityNodes\": [\n";
for(std::size_t i = 0; i < entities.size(); ++i)
{
const auto &entity = entities[i];
if(i)
out << ",\n";
out << " {\"name\": \"" << entity.name << "\", \"type\": \"" << entity.type
<< "\", \"renderMeshCount\": " << entity.meshCount << ", \"hasCollisionObject\": "
<< (entity.hasCollisionObject ? "true" : "false") << ", \"meshCollisionStructures\": " << entity.meshCollisionStructureCount
<< ", \"collisionOptionEnabled\": " << (entity.collisionOptionEnabled ? "true" : "false")
<< ", \"collisionOptionPathReachable\": " << (entity.collisionOptionPathReachable ? "true" : "false")
<< ", \"serializedCollisionTriangles\": " << entity.serializedCollisionTriangleCount
<< ", \"completeSerializedCollisionTriangles\": " << entity.completeSerializedCollisionTriangleCount
<< ", \"meshCollisionTypes\": [";
for(std::size_t meshIndex = 0; meshIndex < entity.meshCollisionTypes.size(); ++meshIndex)
{
if(meshIndex)
out << ", ";
out << "\"" << entity.meshCollisionTypes[meshIndex] << "\"";
}
out << "]}";
}
out << "\n ]\n}\n";
out.close();
// The optional raw OBJ is diagnostic evidence only. Its positions are the
// native query's model-local, Runewaker left-handed Y-up coordinates.
std::ofstream obj((outputPath + ".obj").c_str(), std::ios::out | std::ios::trunc);
if(!obj)
return false;
obj << "# Native Runewaker CRuCollisionQuery_OBB_TriEnum output; model-local LH Y-up\n";
obj << std::setprecision(9);
for(INT32 i = 0; i < serializedVertexCount; ++i)
{
const CRuVector3 &v = (*serializedQuery.m_colTriVerts)[i];
obj << "v " << v.x << " " << v.y << " " << v.z << "\n";
}
for(INT32 i = 0; i + 2 < serializedVertexCount; i += 3)
obj << "f " << (i + 1) << " " << (i + 2) << " " << (i + 3) << "\n";
std::ofstream flags((outputPath + ".flags.bin").c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
if(!flags)
return false;
for(INT32 i = 0; i < serializedQuery.m_colTriFlags->Count(); ++i)
{
const UINT32 flag = static_cast<UINT32>((*serializedQuery.m_colTriFlags)[i]);
flags.write(reinterpret_cast<const char *>(&flag), sizeof(flag));
}
if(!flags)
return false;
return static_cast<bool>(obj);
}
}
int main(int argc, char **argv)
{
if(argc != 4 && argc != 5)
{
std::cerr << "Usage: native-collision-audit <resource-root> <model-resource-path> <output-json> [complete|respect-options]\n";
return 2;
}
const std::string collisionMode = argc == 5 ? argv[4] : "complete";
if(collisionMode != "complete" && collisionMode != "respect-options")
{
std::cerr << "Collision mode must be 'complete' or 'respect-options'.\n";
return 2;
}
if(RuInitialize_Core() == FALSE || RuInitialize_NULL() == FALSE)
{
std::cerr << "Rune initialization failed.\n";
RuShutdown();
return 3;
}
g_ruResourceManager->SetRootDirectory(argv[1]);
CRuEntity *root = g_ruResourceManager->LoadEntity(argv[2]);
if(!root)
{
std::cerr << "Unable to load ROS entity: " << argv[2] << "\n";
RuShutdown();
return 4;
}
std::vector<EntitySummary> entities;
Visit(root, true, entities);
const bool written = WriteReport(argv[3], argv[2], collisionMode, root, entities);
ruSAFE_RELEASE(root);
RuShutdown();
if(!written)
{
std::cerr << "Unable to write native collision report.\n";
return 5;
}
std::cout << "Wrote native collision query report for " << argv[2] << "\n";
return 0;
}