Skip to content

Commit

Permalink
Add function processLineAgainstMesh (PR #3178)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pirulax committed Sep 19, 2023
1 parent f382cfb commit acb80a3
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 146 deletions.
310 changes: 165 additions & 145 deletions Client/game_sa/CWorldSA.cpp
Expand Up @@ -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<Context*>(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<CColSphereSA&>(*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<CVector*>(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<RpAtomic*>(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)
{
Expand Down Expand Up @@ -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<CColSphereSA&>(*RpAtomicGetBoundingSphere(a)) // Fine for now
)) {
return true; // Line segment doesn't touch bsp
}
#endif
const auto localDir = localEnd - localOrigin;

const auto verts = reinterpret_cast<CVector*>(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<RpAtomic*>(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);
}
}

Expand Down
1 change: 1 addition & 0 deletions Client/game_sa/CWorldSA.h
Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.cpp
Expand Up @@ -21,6 +21,7 @@ void CLuaWorldDefs::LoadFunctions()
{"getColorFilter", ArgumentParser<GetColorFilter>},
{"getRoofPosition", GetRoofPosition},
{"getGroundPosition", GetGroundPosition},
{"processLineAgainstMesh", ArgumentParser<ProcessLineAgainstMesh>},
{"processLineOfSight", ProcessLineOfSight},
{"getWorldFromScreenPosition", GetWorldFromScreenPosition},
{"getScreenFromWorldPosition", GetScreenFromWorldPosition},
Expand Down Expand Up @@ -230,6 +231,32 @@ int CLuaWorldDefs::GetRoofPosition(lua_State* luaVM)
return 1;
}

std::variant<bool, CLuaMultiReturn<bool, float, float, const char*, const char*, float, float, float>> 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<bool, float, float, const char*, const char*, float, float, float>{
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,
Expand Down
2 changes: 2 additions & 0 deletions Client/mods/deathmatch/logic/luadefs/CLuaWorldDefs.h
Expand Up @@ -15,9 +15,11 @@ class CLuaWorldDefs : public CLuaDefs
public:
static void LoadFunctions();


LUA_DECLARE(GetTime);
LUA_DECLARE(GetGroundPosition);
LUA_DECLARE(GetRoofPosition);
static std::variant<bool, CLuaMultiReturn<bool, float, float, const char*, const char*, float, float, float>> ProcessLineAgainstMesh(CClientEntity* e, CVector start, CVector end);
LUA_DECLARE(ProcessLineOfSight);
LUA_DECLARE(IsLineOfSightClear);
LUA_DECLARE(GetWorldFromScreenPosition);
Expand Down
1 change: 1 addition & 0 deletions Client/sdk/game/CWorld.h
Expand Up @@ -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;
Expand Down

0 comments on commit acb80a3

Please sign in to comment.