Skip to content

Add ProcessLineAgainstMesh #3178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 165 additions & 145 deletions Client/game_sa/CWorldSA.cpp
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading