--by pavetr.top/ds local DEBUG = false local DEBUG_SOURCES = { ["IsVisible"] = true, ["IsSelfOccluded"] = true, ["GetEyeTraceEntity"] = true, ["CreateMove"] = true, ["BuildModelHitboxData"] = true, ["Render"] = true, ["General"] = true, } local DEBUG_MESSAGE_FILTERS = { "cmdNum =", "Using cached data", "Skipping duplicate", } local function debugPrint(source, ...) if not DEBUG then return end if not DEBUG_SOURCES[source] then return end local args = { ... } for i = 1, #args do local v = args[i] if type(v) == "userdata" and IsValid(v) and v.GetClass then args[i] = tostring(v) .. " [" .. v:GetClass() .. "]" else args[i] = tostring(v) end end local msg = table.concat(args, " ") for _, filter in ipairs(DEBUG_MESSAGE_FILTERS) do if string.find(msg, filter, 1, true) then return end end MsgC(Color(0, 255, 255), "[DEBUG][" .. source .. "] ", color_white, msg, "\n") end -- === Globals === local math_abs = math.abs local math_max = math.max local math_min = math.min local math_huge = math.huge local table_insert = table.insert local table_pairs = pairs local table_ipairs = ipairs local vector = Vector local angle = Angle local color = Color local util_TraceLine = util.TraceLine local Lerp = Lerp local render_DrawLine = render.DrawLine local render_SetColorMaterial = render.SetColorMaterial local cam_Start3D = cam.Start3D local cam_End3D = cam.End3D local LocalToWorld = LocalToWorld local WorldToLocal = WorldToLocal local MASK_SHOT = MASK_SHOT local angle_zero = angle(0, 0, 0) -- === Metatables === local vec_Meta = FindMetaTable("Vector") local ang_Meta = FindMetaTable("Angle") local entity_Meta = FindMetaTable("Entity") local player_Meta = FindMetaTable("Player") -- === Vector Methods === local vec_DistToSqr = vec_Meta.DistToSqr local vec_GetNormalized = vec_Meta.GetNormalized local vec_Sub = vec_Meta.__sub local vec_Add = vec_Meta.__add local vec_Mul = vec_Meta.__mul local vec_Div = vec_Meta.__div -- === Angle Methods === local ang_Forward = ang_Meta.Forward -- === Entity Methods === local ent_GetBoneMatrix = entity_Meta.GetBoneMatrix local ent_GetHitBoxCount = entity_Meta.GetHitBoxCount local ent_GetHitBoxBone = entity_Meta.GetHitBoxBone local ent_GetHitBoxBounds = entity_Meta.GetHitBoxBounds local ent_GetModel = entity_Meta.GetModel local ent_EntIndex = entity_Meta.EntIndex local ent_IsNPC = entity_Meta.IsNPC local ent_IsNextBot = entity_Meta.IsNextBot local ent_EyePos = entity_Meta.EyePos local ent_EyeAngles = entity_Meta.EyeAngles -- === Player Methods === local ent_IsPlayer = player_Meta.IsPlayer -- === Caches === local visibilityCache = {} local modelCache = {} local debugVisiblePoints = {} local lastCommandNumber = -1 -- === LocalPlayer === local localPlayer = LocalPlayer() -- === Utility === local function VectorsEqual(v1, v2, tolerance) return vec_DistToSqr(v1, v2) < (tolerance or 1e-6) end local function AnglesEqual(a1, a2, tolerance) tolerance = tolerance or 1e-4 return math_abs(a1.p - a2.p) < tolerance and math_abs(a1.y - a2.y) < tolerance and math_abs(a1.r - a2.r) < tolerance end local function IsVisible(point, target) debugPrint("IsVisible", "Checking visibility to target:", target) local eyePos = ent_EyePos(localPlayer) local tr = util_TraceLine({ start = eyePos, endpos = point, mask = MASK_SHOT, filter = localPlayer, }) debugPrint("IsVisible", "Trace result - Hit:", tr.Hit, "Entity:", tr.Entity) return not tr.Hit or tr.Entity == target end local function IsSelfOccluded(ent, eyePos, point, skipHitboxID) debugPrint("IsSelfOccluded", "Checking occlusion for hitbox:", skipHitboxID) local dir = vec_GetNormalized(vec_Sub(point, eyePos)) local maxDist = vec_DistToSqr(eyePos, point) local hitboxSet = 0 local count = ent_GetHitBoxCount(ent, hitboxSet) for hitbox = 0, count - 1 do if hitbox == skipHitboxID then continue end local bone = ent_GetHitBoxBone(ent, hitbox, hitboxSet) local matrix = ent_GetBoneMatrix(ent, bone) if not matrix then continue end local min, max = ent_GetHitBoxBounds(ent, hitbox, hitboxSet) if not min or not max then continue end local origin = matrix:GetTranslation() local angles = matrix:GetAngles() local localStart = WorldToLocal(eyePos, angle_zero, origin, angles) local localDir = vec_Sub(WorldToLocal(vec_Add(eyePos, dir), angle_zero, origin, angles), localStart) local tmin, tmax = 0, math_huge for i = 1, 3 do local start = localStart[i] local direction = localDir[i] local minB = min[i] local maxB = max[i] if math_abs(direction) < 1e-6 then if start < minB or start > maxB then tmin = math_huge break end else local t1 = (minB - start) / direction local t2 = (maxB - start) / direction tmin = math_max(tmin, math_min(t1, t2)) tmax = math_min(tmax, math_max(t1, t2)) end end if tmax >= tmin and tmin > 0 then local hitPoint = vec_Add(eyePos, vec_Mul(dir, tmin)) if vec_DistToSqr(hitPoint, eyePos) < maxDist then debugPrint("IsSelfOccluded", "Occluded by hitbox:", hitbox) return true end end end return false end local function Recurse(min, max, depth, maxDepth, seeds) if depth > maxDepth then return end local center = vec_Div(vec_Add(min, max), 2) table_insert(seeds, center) for x = 0, 1 do for y = 0, 1 do for z = 0, 1 do local subMin = vector( Lerp(x, min.x, center.x), Lerp(y, min.y, center.y), Lerp(z, min.z, center.z) ) local subMax = vector( Lerp(x, center.x, max.x), Lerp(y, center.y, max.y), Lerp(z, center.z, max.z) ) Recurse(subMin, subMax, depth + 1, maxDepth, seeds) end end end end local function GenerateOctreeSeeds(maxDepth) local seeds = {} Recurse(vector(0, 0, 0), vector(1, 1, 1), 1, maxDepth, seeds) return seeds end local function BuildModelHitboxData(ent) local mdl = ent_GetModel(ent) if modelCache[mdl] then return modelCache[mdl] end local hitboxSet = 0 local data = { hitboxes = {}, seeds = GenerateOctreeSeeds(2) } local count = ent_GetHitBoxCount(ent, hitboxSet) for hitbox = 0, count - 1 do local bone = ent_GetHitBoxBone(ent, hitbox, hitboxSet) local min, max = ent_GetHitBoxBounds(ent, hitbox, hitboxSet) if bone and min and max then data.hitboxes[hitbox] = { bone = bone, min = min, max = max } end end modelCache[mdl] = data return data end local function GetEyeTraceEntity() debugPrint("GetEyeTraceEntity", "Tracing from player's eye") local eyePos = ent_EyePos(localPlayer) local eyeDir = ang_Forward(ent_EyeAngles(localPlayer)) local traceEnd = vec_Add(eyePos, vec_Mul(eyeDir, 32768)) local hitEnt = nil local tr = util_TraceLine({ start = eyePos, endpos = traceEnd, mask = MASK_SHOT, filter = function(ent) if ent == localPlayer then return false end local isPlayer = ent_IsPlayer and ent_IsPlayer(ent) local isNPC = ent_IsNPC and ent_IsNPC(ent) local isNextBot = ent_IsNextBot and ent_IsNextBot(ent) if isPlayer or isNPC or isNextBot then hitEnt = ent return true end return false end, }) return hitEnt end -- === CreateMove Hook === hook.Add("CreateMove", "CalculateHitboxVisibility", function(cmd) local cmdNum = cmd:CommandNumber() if cmdNum == lastCommandNumber or cmdNum == 0 then return end lastCommandNumber = cmdNum local ent = GetEyeTraceEntity() -- Replace this with your target player if not IsValid(ent) or ent_GetHitBoxCount(ent, 0) == 0 then debugVisiblePoints = {} return end local modelData = BuildModelHitboxData(ent) local eyePos = ent_EyePos(localPlayer) debugVisiblePoints = {} for hitboxID, hitboxData in table_pairs(modelData.hitboxes) do local bone = hitboxData.bone local matrix = ent_GetBoneMatrix(ent, bone) if not matrix then continue end local pos = matrix:GetTranslation() local ang = matrix:GetAngles() local id = ent_EntIndex(ent) .. "_" .. hitboxID if visibilityCache[id] and VectorsEqual(pos, visibilityCache[id].pos) and AnglesEqual(ang, visibilityCache[id].ang) then debugVisiblePoints[hitboxID] = visibilityCache[id].points continue end local min, max = hitboxData.min, hitboxData.max local centerLocal = vec_Div(vec_Add(min, max), 2) local centerWorld = LocalToWorld(centerLocal, angle_zero, pos, ang) local visiblePoints = {} if IsVisible(centerWorld, ent) and not IsSelfOccluded(ent, eyePos, centerWorld, hitboxID) then table_insert(visiblePoints, centerWorld) else for _, frac in table_ipairs(modelData.seeds) do local localPos = vector( Lerp(frac.x, min.x, max.x), Lerp(frac.y, min.y, max.y), Lerp(frac.z, min.z, max.z) ) local worldPos = LocalToWorld(localPos, angle_zero, pos, ang) if IsVisible(worldPos, ent) and not IsSelfOccluded(ent, eyePos, worldPos, hitboxID) then table_insert(visiblePoints, worldPos) end end end visibilityCache[id] = { pos = pos, ang = ang, points = visiblePoints } debugVisiblePoints[hitboxID] = visiblePoints end end) -- === HUDPaint Hook === local function Draw3DCrosshair(pos, size, clr) size = size or 2 clr = clr or color(255, 255, 255) render_DrawLine(vec_Sub(pos, vector(size, 0, 0)), vec_Add(pos, vector(size, 0, 0)), clr, false) render_DrawLine(vec_Sub(pos, vector(0, size, 0)), vec_Add(pos, vector(0, size, 0)), clr, false) render_DrawLine(vec_Sub(pos, vector(0, 0, size)), vec_Add(pos, vector(0, 0, size)), clr, false) end hook.Add("HUDPaint", "RenderHitboxVisibility", function() if not next(debugVisiblePoints) then return end -- Do a trace from the player's view to get the hit hitbox local tr = util_TraceLine({ start = ent_EyePos(localPlayer), endpos = ent_EyePos(localPlayer) + ent_EyeAngles(localPlayer):Forward() * 32768, mask = MASK_SHOT, filter = localPlayer }) local targetHitbox = nil if tr.Hit and IsValid(tr.Entity) and tr.HitBox then targetHitbox = tr.HitBox end cam_Start3D() for hitboxID, points in pairs(debugVisiblePoints) do -- Use different color if this is the hitbox under the cursor local clr = (hitboxID == targetHitbox) and Color(255, 0, 0) or Color(0, 255, 0) for _, pt in ipairs(points) do render.SetColorMaterial() Draw3DCrosshair(pt, 2, clr) end end cam_End3D() end)