Light detection revamp - Optimized and more accurate

It’s been awhile since i have made any changes to the Light detection module i made.

It was working to some extended but after a while i found numerous bugs and frame drops due to the use of unoptimized functions and events.

I did a few updates as you some of you know. For example i made LightDetectionV2 which did some minor optimizations and added a few new methods. The problem is there is no documentation on how it works (since the base was rescripted). After awhile it broke due to roblox updating.

I was still not satisifed with the results so i chose to make ANOTHER version which at the end i didnt publish just because of how messy it was.

A few months Later and i chose to REVAMP it.

The new version, compared to the latest one (Version 3) is WAY faster

The changes i made are changing the math from linear algebra to using solid geometry.

it works just as the first version with just 1 new method

Anyway enough talking

Code
``````--!nonstrict
local module = {}

local Lighting = game.Lighting
local Run_Service = game:GetService("RunService")

local function SpotLight(x : Vector3,dir : Vector3,p : Vector3,ang : number,range : number) -- Cone
ang = ang > 0 and ang or 1
local A = CFrame.lookAt(x,x + dir) * CFrame.Angles(0,math.rad(ang/2),0) * CFrame.new(0,0,-range)
local B = CFrame.lookAt(x,x + dir) * CFrame.Angles(0,math.rad(-ang/2),0) * CFrame.new(0,0,-range)

local r = (A.Position - B.Position).Magnitude/2
local midPoint = A:Lerp(B,0.5).Position
local h = (midPoint - x).Magnitude

local dist = (p - x):Dot(dir)
local cone_radius = (dist / h) * r

local dist2 = ((p - x) - dist * dir).Magnitude

return (dist2 < cone_radius)
end

local function Side4D(p1,p2,p3,p4,p)
local dp = (p1+p2+p3+p4)/4

local up = (p2 - p1)
local right = (p2 - p4)

local dir = up:Cross(right)

local dir2 = (dp - p)
local dot = dir:Dot(dir2)

return dot >= 1
end

local function SurfaceLight(p1,p2,p3,p4,p5,p6,p7,p8,p)
local x,x1,x2,x3,x4,x5 = Side4D(p1,p2,p3,p4,p) ,Side4D(p1,p5,p6,p2,p) ,Side4D(p2,p6,p7,p3,p) ,Side4D(p3,p7,p8,p4,p) ,Side4D(p4,p8,p5,p1,p) ,Side4D(p5,p8,p7,p6,p)

return x and x1 and x2 and x3 and x4 and x5
end

local function PointLight(x,p,r)
return x:Dot(p) < r^2
end

local function CheckObfuscated(t)
for i,part : BasePart in t do
if part.CastShadow == false then
table.remove(t,i)
end
if part.Transparency >= 0.5 then
table.remove(t,i)
end
end

return t
end

local function GetVectorFromNormal(BasePart : BasePart | Attachment,Normal)
local Dirs = {}

if BasePart:IsA("BasePart") then
Dirs = {
[Enum.NormalId.Top] = BasePart.CFrame.UpVector,
[Enum.NormalId.Bottom] = -BasePart.CFrame.UpVector,
[Enum.NormalId.Right] = BasePart.CFrame.RightVector,
[Enum.NormalId.Left] = -BasePart.CFrame.RightVector,
[Enum.NormalId.Front] = BasePart.CFrame.LookVector,
[Enum.NormalId.Back] = -BasePart.CFrame.LookVector,
}
else
Dirs = {
[Enum.NormalId.Top] = BasePart.WorldCFrame.UpVector,
[Enum.NormalId.Bottom] = -BasePart.WorldCFrame.UpVector,
[Enum.NormalId.Right] = BasePart.WorldCFrame.RightVector,
[Enum.NormalId.Left] = -BasePart.WorldCFrame.RightVector,
[Enum.NormalId.Front] = BasePart.WorldCFrame.LookVector ,
[Enum.NormalId.Back] = -BasePart.WorldCFrame.LookVector,
}
end

return Dirs[Normal]
end

local function Side(v1 : Vector3,v2 : Vector3,v3 : Vector3,v4 : Vector3,p : Vector3)
local normal =  (v2 - v1):Cross(v3 - v1)
local dotV4 = normal:Dot(v4 - v1)
local dotP = normal:Dot(p - v1)
return math.sign(dotV4) == math.sign(dotP)
end

local function SurfaceLightAtt(v1 : Vector3,v2 : Vector3,v3 : Vector3,v4 : Vector3,p : Vector3)
return Side(v1, v2, v3, v4, p) and
Side(v2, v3, v4, v1, p) and
Side(v3, v4, v1, v2, p) and
Side(v4, v1, v2, v3, p)
end

local OVP = OverlapParams.new()
OVP.FilterType = Enum.RaycastFilterType.Blacklist

local function CheckInSunlight(Point : Vector3)
local sunlightDir = Lighting:GetSunDirection()
local sp = Point + sunlightDir * 500
local scf = CFrame.lookAt(sp,sp + sunlightDir)
local ssize = Vector3.new(1,1,1000)

local Det = workspace:GetPartBoundsInBox(CFrame.new(Point),Vector3.one/10)[1]
OVP.FilterDescendantsInstances = {Det}

local Sunlight = if Lighting.GlobalShadows then CheckObfuscated(workspace:GetPartBoundsInBox(scf,ssize,OVP)) else {}

if Sunlight and 18 > Lighting.ClockTime and Lighting.ClockTime > 8 then
return true
end

return false
end

local AllLights : {Light} = {}

if child:IsA("Light") then
table.insert(AllLights,child)
end
end)

for _,v : Light in workspace:GetDescendants() do
if v:IsA("Light") then
table.insert(AllLights,v)
end
end

export type LightObject = {
LightLevel: number,
Lights: {Light},
Sunlight: boolean,
InLight: boolean
}

function module:DetectLightAtObject(Object: BasePart)
local self = {
Part = Object,
}

function self:Update()
local info =  module:DetectLightAtPoint(Object.Position)
return info
end

return self
end

function module:DetectLightAtPoint(Point: Vector3)
local Detected = {
Lights = {},
Sunlight = false,
InLight = false,
LightLevel = 0,
} :: LightObject

local SunLight = CheckInSunlight(Point)

if SunLight then
Detected.Sunlight = true
Detected.LightLevel = 15
end

for _,Light in AllLights do
local LightParent = Light.Parent
local LightPos = LightParent:IsA("BasePart") and LightParent.Position or (LightParent:IsA("Attachment") and LightParent.WorldPosition or nil)

if not LightPos then continue end
if not Light.Enabled then continue end

local Brightness = Light.Brightness/2
local range = Light.Range

local magnitude = (Point - LightPos).Magnitude
local unit = (LightPos - Point).Unit

local p = Point:Lerp(LightPos,0.5)
local cf = CFrame.lookAt(p,p + unit)
local size = Vector3.new(1,1,magnitude)

local Det = workspace:GetPartBoundsInBox(CFrame.new(Point),Vector3.one/10)[1]
OVP.FilterDescendantsInstances = {Det,LightParent}

local obfuscated = if Light.Shadows then CheckObfuscated(workspace:GetPartBoundsInBox(cf,size,OVP)) else {}

if #obfuscated > 0 then continue end

if Light:IsA("PointLight") then
local InRadius = PointLight(LightPos,Point,Light.Range)

table.insert(Detected.Lights,Light)
Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
end
end

if Light:IsA("SpotLight") then
local dir = GetVectorFromNormal(LightParent,Light.Face)
local InCone = SpotLight(LightPos,dir,Point,Light.Angle,range)

if InCone and magnitude  < range - range/10 then
table.insert(Detected.Lights,Light)
Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
end
end

if Light:IsA("SurfaceLight") then
local dir = LightParent.CFrame * CFrame.new(Vector3.fromNormalId(Light.Face))

if LightParent:IsA("BasePart") then
local ParentSize = LightParent.Size/2

local deg = Light.Angle
local ang = math.rad(deg)

local p1 = (dir * CFrame.new(-ParentSize.X,-ParentSize.Y,0)).Position
local p2 = (dir * CFrame.new(ParentSize.X,-ParentSize.Y,0)).Position

local p3 = (CFrame.new(p2,p2 + dir.LookVector) * CFrame.new(ang*10,0,-range) * CFrame.Angles(0,-ang/2,0)) * CFrame.new(0,-range*ang/2+math.max(0,deg - 45)/30,0)
local p4 = (CFrame.new(p1,p1 + dir.LookVector) * CFrame.new(-ang*10,0,-range) * CFrame.Angles(0,ang/2,0)) * CFrame.new(0,-range*ang/2+math.max(0,deg - 45)/30,0)

local p5 = (dir * CFrame.new(-ParentSize.X,ParentSize.Y,0)).Position
local p6 = (dir * CFrame.new(ParentSize.X,ParentSize.Y,0)).Position

local p7 = p3.Position * Vector3.new(1,-1,1) + Vector3.new(0,p6.Y,0)
local p8 = p4.Position * Vector3.new(1,-1,1) + Vector3.new(0,p5.Y,0)

local InsideCube = SurfaceLight(p1,p2,p3.Position,p4.Position,p5,p6,p7,p8,Point)

if InsideCube and magnitude < range - range/10 then
table.insert(Detected.Lights,Light)
Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
end
else
local dir = GetVectorFromNormal(LightParent,Light.Face)
local InCone = SpotLight(LightPos,dir,Point,Light.Angle,range)

if InCone and magnitude  < range - range/10 then
table.insert(Detected.Lights,Light)
Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
end
end
end
end

Detected.InLight = #Detected.Lights > 0 or Detected.Sunlight

return Detected
end

return module
``````
Code Example
``````local LightDetection = require(game.ReplicatedStorage.LightDetectionRevamp)

local Data = LightDetection:DetectLightAtPoint(Vector3.new(0,10,0)) -- returns table with contents
--[[
{
LightLevel: number,
Lights: {Light},
Sunlight: boolean,
InLight: boolean
}
--]]
-----------------------------------
local LightDetector = LightDetection:DetectLightAtObject(workspace.BasePlate) -- returns table with method :Update()
LightDetector:Update() -- returns the same thing as LightDetection:DetectLightAtPoint()
``````

Link to the module: LightDetectionRevamp

and videos

The math can be optimized so feel free giving suggestions or just straight up making your own version of this module. I havent really tested it so please report here if there are any bugs you encontered

101 Likes

This is pretty cool and all but… How can a developer use it?..

2 Likes

I gave a code example here, you need either a Vector3 or a BasePart to detect the light on.
Thats all, if you want to customize anything you will have to dig in the module.

6 Likes

I feel like this can be used for automatic vehicle lights. I might check it out.

No, I meant in a game. Like for what

Hmmm not quite sure of that one

An example of using this is to make a sighting feature for stealth (horror), or hide and seek games.
Op could have said this at first place to make you understand more but whatever.

Or a game like Lightmatter, where if your not in light you die.

2 Likes

Honestly, the opposite of that would be more realistic. That’s a good game idea.

1 Like

This doesn’t work for me, the part is always green, even when the part is not in light nor in the shadows.

``````local debugPart = workspace:WaitForChild("DebugPart")
local origin = debugPart.Position
local radius = 5
local m = require(script.ModuleScript)
local detection = m:DetectLightAtObject(debugPart)
RunService.Heartbeat:Connect(function(deltaTime)
debug.profilebegin("Move")
local now = time() * 5
local pos = origin + Vector3.new(math.cos(now) * radius, 0, math.sin(now) * radius)
debugPart:PivotTo(CFrame.new(pos))
debug.profileend() -- Move

debug.profilebegin("Scan")
local metadata = detection:Update()
debugPart.Color = if metadata.InLight or metadata.Sunlight then Color3.new(0, 1, 0) else Color3.new(1, 0, 0)
debug.profileend() -- Scan
end)
``````

I also printed the data returned, `InLight` and `Sunlight` are also both `true`
The video attached below, is where the detection also seems to work only in the dark, and it only turns green on the right side of the light from where it’s facing.

Also, when a light is destroyed, errors occur.
Tl;dr every light besides `SurfaceLight` work fine, and sunlight detections do not work correctly.

3 Likes

Dont copy the code from the dev forum, there was a bug that i found a few days after i made this post. The working version is the module

1 Like

Thanks for letting me know, will try

How would i use the module ? i am trying to do a flashligt thats scares away monsters

i only neeed to know how to detect the light

How would I go about using this module?
Say if I wanted to detect if a player is in the light or not.

1 Like

Whoa! This is cool! This would be interesting for something similar to what Minecraft does with mob spawns.

Oh that’s a cool idea!

I will look into if I can use this in a game I am working on! Keep up the good work!

Like @crazygamespp said, how exactly do you detect the light?

Uhh, I’m a but confused. So for this:

``````local LightDetector = LightDetection:DetectLightAtObject(workspace.BasePlate)
LightDetector:Update()
``````

How do I get the light level from that?

And also it doesn’t work at all, I have a part absolutely drenched in light, and the light level is always either 15 or 0.

Most definitely for an NPC, more specifically for a horror game or a story-driven FPS. Let’s say for example a player is hiding from a hostile NPC and suddenly the player walks into the light, logically he would be more visible. Instead of just FOV checks for the NPC, we can now include lighting for more visibility configuration. It’s pretty neat.

i was playing around with the module a few days ago, i might’ve broken it. Get the model from the toolbox
it should work now

Well there’s a new problem with the code. On line 86 it tries to add Point and sunlightDir, but the console was saying that it was trying to add a vector3 and a table, so I printed the point, and it turns out it’s a table. What do I do with point now?

Edit: The first original point from calling the module function from the local script appears to be a table, but in the local script I am calling the module function with a vector3.

Edit: I made a small edit that returns the point as soon as the function is called, and here is the actual value called versus what the function claims what is called.

``````	local vec = Vector3.new(0,10,0)
print(vec)
local data = LightDetection:DetectLightAtPoint(vec)

print(table.unpack(data))
``````
``````  15:14:24.633  0, 10, 0  -  Client - LocalScript:25
15:14:24.633    -  Client - LocalScript:28
``````

nothing I guess?

Edit: roblox be tweakin
I literally just made a module script that its only function is to return what is inputted, and it returned nothing / a table of nothing. What is going on???

Edit: what

``````local module = {}

function module.DetectLightAtPoint(Point)
print(table.unpack(Point))
return Point
end

return module

``````
``````local mds = require(game.ReplicatedStorage.ModuleScript)

local dt = mds:DetectLightAtPoint(vec)
``````
``````  15:22:27.524  0, 10, 0  -  Client - LocalScript:25
15:22:27.525    -  Client - ModuleScript:4
``````

Edit: I put the entire module script onto the local script and called the function from there, and I managed it to print something, but once again, it’s binary. It’s either 15 or 0. No in-between. Doesn’t matter if I should be blinded or not, it’s 15 or 0.