diff --git a/Client/game_sa/CWorldSA.cpp b/Client/game_sa/CWorldSA.cpp index 527a8108a1..b946b7e3c1 100644 --- a/Client/game_sa/CWorldSA.cpp +++ b/Client/game_sa/CWorldSA.cpp @@ -254,6 +254,164 @@ void ConvertMatrixToEulerAngles(const CMatrix_Padded& matrixPadded, float& fX, f } } + +auto CWorldSA::ProcessLineAgainstMesh(CEntitySAInterface* targetEntity, CVector start, CVector end) -> SProcessLineOfSightMaterialInfoResult +{ + assert(targetEntity); + + SProcessLineOfSightMaterialInfoResult ret; + + struct Context + { + float minHitDistSq{}; //< [squared] hit distance from the line segment's origin + CVector originOS, endOS, dirOS; //< Line origin, end and dir [in object space] + CMatrix entMat, entInvMat; //< The hit entity's matrix, and it's inverse + RpTriangle* hitTri{}; //< The triangle hit + RpAtomic* hitAtomic{}; //< The atomic of the hit triangle's geometry + RpGeometry* hitGeo{}; //< The geometry of the hit triangle + CVector hitBary{}; //< Barycentric coordinates [on the hit triangle] of the hit + CVector hitPosOS{}; //< Hit position in object space + CEntitySAInterface* entity{}; //< The hit entity + } c = {}; + + c.entity = targetEntity; + + if (!c.entity->m_pRwObject) { + return ret; // isValid will be false in this case + } + + // Get matrix, and it's inverse + c.entity->Placeable.matrix->ConvertToMatrix(c.entMat); + c.entInvMat = c.entMat.Inverse(); + + // ...to transform the line origin and end into object space + c.originOS = c.entInvMat.TransformVector(start); + c.endOS = c.entInvMat.TransformVector(end); + c.dirOS = c.endOS - c.originOS; + c.minHitDistSq = c.dirOS.LengthSquared(); // By setting it to this value we avoid collisions that would be detected after the line segment + // [but are still ont the ray] + + // Do raycast against the DFF to get hit position material UV and name + // This is very slow + // Perhaps we could parallelize it somehow? [OpenMP?] + const auto ProcessOneAtomic = [](RpAtomic* a, void* data) + { + Context* const c = static_cast(data); + RwFrame* const f = RpAtomicGetFrame(a); + + const auto GetFrameCMatrix = [](RwFrame* f) + { + CMatrix out; + pGame->GetRenderWare()->RwMatrixToCMatrix(*RwFrameGetMatrix(f), out); + return out; + }; + + // Atomic not visible + if (!a->renderCallback || !(a->object.object.flags & 0x04 /*rpATOMICRENDER*/)) + { + return true; + } + + // Sometimes atomics have no geometry [I don't think that should be possible, but okay] + RpGeometry* const geo = a->geometry; + if (!geo) + { + return true; + } + + // Calculate transformation by traversing the hierarchy from the bottom (this frame) -> top (root frame) + CMatrix localToObjTransform{}; + for (RwFrame* i = f; i && i != i->root; i = RwFrameGetParent(i)) + { + localToObjTransform = GetFrameCMatrix(i) * localToObjTransform; + } + const CMatrix objToLocalTransform = localToObjTransform.Inverse(); + + const auto ObjectToLocalSpace = [&](const CVector& in) { return objToLocalTransform.TransformVector(in); }; + + // Transform from object space, into local (the frame's) space + const CVector localOrigin = ObjectToLocalSpace(c->originOS); + const CVector localEnd = ObjectToLocalSpace(c->endOS); + +#if 0 + if (!CCollisionSA::TestLineSphere( + CColLineSA{localOrigin, 0.f, localEnd, 0.f}, + reinterpret_cast(*RpAtomicGetBoundingSphere(a)) // Fine for now + )) { + return true; // Line segment doesn't touch bsp + } +#endif + const CVector localDir = localEnd - localOrigin; + + const CVector* const verts = reinterpret_cast(geo->morph_target->verts); // It's fine, trust me bro + for (auto i = geo->triangles_size; i-- > 0;) + { + RpTriangle* const tri = &geo->triangles[i]; + + // Process the line against the triangle + CVector hitBary, hitPos; + if (!localOrigin.IntersectsSegmentTriangle(localDir, verts[tri->verts[0]], verts[tri->verts[1]], verts[tri->verts[2]], &hitPos, &hitBary)) + { + continue; // No intersection at all + } + + // Intersection, check if it's closer than the previous one + const float hitDistSq = (hitPos - localOrigin).LengthSquared(); + if (c->minHitDistSq > hitDistSq) + { + c->minHitDistSq = hitDistSq; + c->hitGeo = geo; + c->hitAtomic = a; + c->hitTri = tri; + c->hitBary = hitBary; + c->hitPosOS = localToObjTransform.TransformVector(hitPos); // Transform back into object space + } + } + + return true; + }; + + if (c.entity->m_pRwObject->object.type == 2 /*rpCLUMP*/) + { + RpClumpForAllAtomics(c.entity->m_pRwObject, ProcessOneAtomic, &c); + } + else + { // Object is a single atomic, so process directly + ProcessOneAtomic(reinterpret_cast(c.entity->m_pRwObject), &c); + } + + if (ret.valid = c.hitGeo != nullptr) + { + // Now, calculate texture UV, etc based on the hit [if we've hit anything at all] + // Since we have the barycentric coords of the hit, calculating it is easy + ret.uv = {}; + for (int i = 0; i < 3; i++) + { + // UV set index - Usually models only use level 0 indices, so let's stick with that + const int uvSetIdx = 0; + + // Vertex's UV position + RwTextureCoordinates* const vtxUV = &c.hitGeo->texcoords[uvSetIdx][c.hitTri->verts[i]]; + + // Now, just interpolate + ret.uv += CVector2D{vtxUV->u, vtxUV->v} * c.hitBary[i]; + } + + // Find out material texture name + // For some reason this is sometimes null + RwTexture* const tex = c.hitGeo->materials.materials[c.hitTri->materialId]->texture; + ret.textureName = tex ? tex->name : nullptr; + + RwFrame* const hitFrame = RpAtomicGetFrame(c.hitAtomic); + ret.frameName = hitFrame ? hitFrame->szName : nullptr; + + // Get hit position in world space + ret.hitPos = c.entMat.TransformVector(c.hitPosOS); + } + + return ret; +} + bool CWorldSA::ProcessLineOfSight(const CVector* vecStart, const CVector* vecEnd, CColPoint** colCollision, CEntity** CollisionEntity, const SLineOfSightFlags flags, SLineOfSightBuildingResult* pBuildingResult, SProcessLineOfSightMaterialInfoResult* outMatInfo) { @@ -354,153 +512,15 @@ bool CWorldSA::ProcessLineOfSight(const CVector* vecStart, const CVector* vecEnd } } - if (outMatInfo && targetEntity && targetEntity->m_pRwObject) + if (outMatInfo) { - struct Context + outMatInfo->valid = false; + if (targetEntity) { - float minHitDistSq{}; //< [squared] hit distance from the line segment's origin - CVector originOS, endOS, dirOS; //< Line origin, end and dir [in object space] - CMatrix entMat, entInvMat; //< The hit entity's matrix, and it's inverse - RpTriangle* hitTri{}; //< The triangle hit - RpAtomic* hitAtomic{}; //< The atomic of the hit triangle's geometry - RpGeometry* hitGeo{}; //< The geometry of the hit triangle - CVector hitBary{}; //< Barycentric coordinates [on the hit triangle] of the hit - CVector hitPosOS{}; //< Hit position in object space - CEntitySAInterface* entity{}; //< The hit entity - } c = {}; - - c.entity = targetEntity; - - // Get matrix, and it's inverse - targetEntity->Placeable.matrix->ConvertToMatrix(c.entMat); - c.entInvMat = c.entMat.Inverse(); - - // ...to transform the line origin and end into object space - c.originOS = c.entInvMat.TransformVector(*vecStart); - c.endOS = c.entInvMat.TransformVector(*vecEnd); - c.dirOS = c.endOS - c.originOS; - c.minHitDistSq = c.dirOS.LengthSquared(); // By setting it to this value we avoid collisions that would be detected after the line segment - // [but are still ont the ray] - - // Do raycast against the DFF to get hit position material UV and name - // This is very slow - // Perhaps we could paralellize it somehow? [OpenMP?] - const auto ProcessOneAtomic = [](RpAtomic* a, void* data) - { - const auto c = (Context*)data; - const auto f = RpAtomicGetFrame(a); - - const auto GetFrameCMatrix = [](RwFrame* f) - { - CMatrix out; - pGame->GetRenderWare()->RwMatrixToCMatrix(*RwFrameGetMatrix(f), out); - return out; - }; - - // Atomic not visible - if (!a->renderCallback || !(a->object.object.flags & 0x04 /*rpATOMICRENDER*/)) - { - return true; - } - - // Sometimes atomics have no geometry [I don't think that should be possible, but okay] - const auto geo = a->geometry; - if (!geo) - { - return true; - } - - // Calculate transformation by traversing the hierarchy from the bottom (this frame) -> top (root frame) - CMatrix localToObjTransform{}; - for (auto i = f; i && i != i->root; i = RwFrameGetParent(i)) - { - localToObjTransform = GetFrameCMatrix(i) * localToObjTransform; - } - const auto objToLocalTransform = localToObjTransform.Inverse(); - - const auto ObjectToLocalSpace = [&](const CVector& in) { return objToLocalTransform.TransformVector(in); }; - - // Transform from object space, into local (the frame's) space - const auto localOrigin = ObjectToLocalSpace(c->originOS); - const auto localEnd = ObjectToLocalSpace(c->endOS); - -#if 0 - if (!CCollisionSA::TestLineSphere( - CColLineSA{localOrigin, 0.f, localEnd, 0.f}, - reinterpret_cast(*RpAtomicGetBoundingSphere(a)) // Fine for now - )) { - return true; // Line segment doesn't touch bsp - } -#endif - const auto localDir = localEnd - localOrigin; - - const auto verts = reinterpret_cast(geo->morph_target->verts); // It's fine, trust me bro - for (auto i = geo->triangles_size; i-- > 0;) - { - const auto tri = &geo->triangles[i]; - - // Process the line against the triangle - CVector hitBary, hitPos; - if (!localOrigin.IntersectsSegmentTriangle(localDir, verts[tri->verts[0]], verts[tri->verts[1]], verts[tri->verts[2]], &hitPos, &hitBary)) - { - continue; // No intersection at all - } - - // Intersection, check if it's closer than the previous one - const auto hitDistSq = (hitPos - localOrigin).LengthSquared(); - if (c->minHitDistSq > hitDistSq) - { - c->minHitDistSq = hitDistSq; - c->hitGeo = geo; - c->hitAtomic = a; - c->hitTri = tri; - c->hitBary = hitBary; - c->hitPosOS = localToObjTransform.TransformVector(hitPos); // Transform back into object space - } - } - - return true; - }; - - if (targetEntity->m_pRwObject->object.type == 2 /*rpCLUMP*/) - { - RpClumpForAllAtomics(targetEntity->m_pRwObject, ProcessOneAtomic, &c); - } - else - { // Object is a single atomic, so process directly - ProcessOneAtomic(reinterpret_cast(targetEntity->m_pRwObject), &c); - } - - // It might be false if the collision model differs from the clump - // This is completely normal as collisions models are meant to be simplified - // compared to the clump's geometry - if (outMatInfo->valid = c.hitGeo != nullptr) - { - // Now, calculate texture UV, etc based on the hit [if we've hit anything at all] - // Since we have the barycentric coords of the hit, calculating it is easy - outMatInfo->uv = {}; - for (auto i = 3u; i-- > 0;) - { - // UV set index - Usually models only use level 0 indices, so let's stick with that - const auto uvSetIdx = 0; - - // Vertex's UV position - const auto vtxUV = &c.hitGeo->texcoords[uvSetIdx][c.hitTri->verts[i]]; - - // Now, just interpolate - outMatInfo->uv += CVector2D{vtxUV->u, vtxUV->v} * c.hitBary[i]; - } - - // Find out material texture name - // For some reason this is sometimes null - const auto tex = c.hitGeo->materials.materials[c.hitTri->materialId]->texture; - outMatInfo->textureName = tex ? tex->name : nullptr; - - const auto frame = RpAtomicGetFrame(c.hitAtomic); // `RpAtomicGetFrame` - outMatInfo->frameName = frame ? frame->szName : nullptr; - - // Get hit position in world space - outMatInfo->hitPos = c.entMat.TransformVector(c.hitPosOS); + // There might not be a texture hit info result as the collision model differs from the mesh itself. + // This is completely normal as collisions models are meant to be simplified + // compared to the mesh + *outMatInfo = ProcessLineAgainstMesh(targetEntity, *vecStart, *vecEnd); } } diff --git a/Client/game_sa/CWorldSA.h b/Client/game_sa/CWorldSA.h index ea5db7b985..5b18c9b35d 100644 --- a/Client/game_sa/CWorldSA.h +++ b/Client/game_sa/CWorldSA.h @@ -48,6 +48,7 @@ class CWorldSA : public CWorld void Remove(CEntity* entity, eDebugCaller CallerId); void Remove(CEntitySAInterface* entityInterface, eDebugCaller CallerId); void RemoveReferencesToDeletedObject(CEntitySAInterface* entity); + auto ProcessLineAgainstMesh(CEntitySAInterface* e, CVector start, CVector end) -> SProcessLineOfSightMaterialInfoResult override; bool ProcessLineOfSight(const CVector* vecStart, const CVector* vecEnd, CColPoint** colCollision, CEntity** CollisionEntity, const SLineOfSightFlags flags, SLineOfSightBuildingResult* pBuildingResult, SProcessLineOfSightMaterialInfoResult* outMatInfo = nullptr); void IgnoreEntity(CEntity* entity); diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.cpp index 1cf83902dd..40f5b345da 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.cpp @@ -21,6 +21,7 @@ void CLuaWorldDefs::LoadFunctions() {"getColorFilter", ArgumentParser}, {"getRoofPosition", GetRoofPosition}, {"getGroundPosition", GetGroundPosition}, + {"processLineAgainstMesh", ArgumentParser}, {"processLineOfSight", ProcessLineOfSight}, {"getWorldFromScreenPosition", GetWorldFromScreenPosition}, {"getScreenFromWorldPosition", GetScreenFromWorldPosition}, @@ -230,6 +231,32 @@ int CLuaWorldDefs::GetRoofPosition(lua_State* luaVM) return 1; } +std::variant> CLuaWorldDefs::ProcessLineAgainstMesh(CClientEntity* e, CVector start, CVector end) { + const auto ge = e->GetGameEntity(); + if (!ge) { + // Element likely not streamed in, and such + // Can't process it. This isn't an error per-se, thus we won't raise anything and treat this as a no-hit scenario + return { false }; + } + const SProcessLineOfSightMaterialInfoResult matInfo{g_pGame->GetWorld()->ProcessLineAgainstMesh(ge->GetInterface(), start, end)}; + if (!matInfo.valid) { + return { false }; // No hit + } + return CLuaMultiReturn{ + true, + + matInfo.uv.fX, + matInfo.uv.fY, + + matInfo.textureName, + matInfo.frameName, + + matInfo.hitPos.fX, + matInfo.hitPos.fY, + matInfo.hitPos.fZ, + }; +} + int CLuaWorldDefs::ProcessLineOfSight(lua_State* L) { // bool float float float element float float float int int int processLineOfSight ( float startX, float startY, float startZ, float endX, float endY, diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.h index c70a9cba5b..bc90a4745e 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.h @@ -15,9 +15,11 @@ class CLuaWorldDefs : public CLuaDefs public: static void LoadFunctions(); + LUA_DECLARE(GetTime); LUA_DECLARE(GetGroundPosition); LUA_DECLARE(GetRoofPosition); + static std::variant> ProcessLineAgainstMesh(CClientEntity* e, CVector start, CVector end); LUA_DECLARE(ProcessLineOfSight); LUA_DECLARE(IsLineOfSightClear); LUA_DECLARE(GetWorldFromScreenPosition); diff --git a/Client/sdk/game/CWorld.h b/Client/sdk/game/CWorld.h index dda88e296e..c2319988d9 100644 --- a/Client/sdk/game/CWorld.h +++ b/Client/sdk/game/CWorld.h @@ -315,6 +315,7 @@ class CWorld virtual void Add(CEntity* entity, eDebugCaller CallerId) = 0; virtual void Remove(CEntity* entity, eDebugCaller CallerId) = 0; virtual void Remove(CEntitySAInterface* entityInterface, eDebugCaller CallerId) = 0; + virtual auto ProcessLineAgainstMesh(CEntitySAInterface* e, CVector start, CVector end) -> SProcessLineOfSightMaterialInfoResult = 0; virtual bool ProcessLineOfSight(const CVector* vecStart, const CVector* vecEnd, CColPoint** colCollision, CEntity** CollisionEntity, const SLineOfSightFlags flags = SLineOfSightFlags(), SLineOfSightBuildingResult* pBuildingResult = NULL, SProcessLineOfSightMaterialInfoResult* outMatInfo = {}) = 0; virtual void IgnoreEntity(CEntity* entity) = 0; diff --git a/Shared/mods/deathmatch/logic/lua/CLuaMultiReturn.h b/Shared/mods/deathmatch/logic/lua/CLuaMultiReturn.h index 218c08f225..47587fac84 100644 --- a/Shared/mods/deathmatch/logic/lua/CLuaMultiReturn.h +++ b/Shared/mods/deathmatch/logic/lua/CLuaMultiReturn.h @@ -16,7 +16,7 @@ struct CLuaMultiReturn { // Note: We use a separate template for the constructor arguments // to allow type conversions. For example: return { "hello", 42 }; - // is a valid statement to construct a LuaMuliReturn + // is a valid statement to construct a CLuaMultiReturn template CLuaMultiReturn(Args... args) : values{args...} {