Reflecting rays using raycasting
Want bullets to ricochet? Lasers to bounce? Custom Pong game? Here’s a simple way to reflect a ray when it hits a surface using the surface normal returned by the Raycast method!
Formula
We will be working with this equation:
r = d - 2(d ⋅ n)n
Where r
is the resultant reflection normal, d
is the normal of our ray, and n
is the surface normal that our ray hit (We will get n
from the RaycastResult that the Raycast method returns).
Because we are doing reflections, we will end up casting more than one ray. So instead of basing our “laser” rays on a single ray cast, we can string a bunch of them together. We can limit this to either a certain distance overall or a certain number of raycast iterations. Whichever suits you best.
Casting a ray
First, how do we apply that formula above? Let’s take a look at a single raycast:
local startCFrame = part.CFrame -- Where we derive our position & normal
local normal = startCFrame.LookVector
local position = startCFrame.Position
local result = workspace:Raycast(position, normal * 500) -- Cast ray
Reflecting the ray
Ok, so now we’ve cast our ray outward 500 studs. In this example, I’m using a part’s CFrame property to initialize where the ray will begin. This is practical since you will probably be doing something similar (e.g. the tip of a gun). Next, we need to see if the ray hit anything (if result
is nil
, then it didn’t hit anything). If so, we need to reflect the ray:
if result ~= nil then
-- Get the reflected normal (a 'normal' is a vector that describes a direction)
-- Formula: r = d - 2(d ⋅ n)n
-- Code: r = d - (2 * d:Dot(n) * n)
local reflectedNormal = normal - (2 * normal:Dot(result.Normal) * result.Normal)
-- Override our current normal with the reflected one:
normal = reflectedNormal
end
In that code chunk, we calculated the reflected normal using the first formula shown. Then, we overrode our current ‘normal’ value with the reflection normal. The next step would be to reiterate this process multiple times. The flow should look like this:
- Cast Ray
- Update current position with new position (end of ray)
- If hit, find reflection, override normal with reflected normal
- If not hit, do nothing
- If max iterations/distance/whatever reached, RETURN/STOP
- Else, recurse; go back to 1 and cast again
Example code:
Put script into a Part. This Part will shoot a laser part when Shoot() is called. This is my first write-up of this code. I have written it in preparation of a small game I’m making. Feel free to use it and edit it as you like. I’ll probably heavily edit it myself too.
function Shoot()
local rng = Random.new()
local laser = Instance.new("Part")
laser.Name = "Laser"
laser.TopSurface = Enum.SurfaceType.Smooth
laser.BottomSurface = Enum.SurfaceType.Smooth
laser.Size = Vector3.new(0.2, 0.2, 0.2)
laser.Color = Color3.new(rng:NextNumber(), rng:NextNumber(), rng:NextNumber())
laser.Anchored = true
laser.CanCollide = false
laser.Locked = true
laser.CFrame = script.Parent.CFrame
laser.Parent = workspace
local maxDistance = 200
local curDistance = 0
local stepDistance = 4
local stepWait = 0
local currentPos = script.Parent.Position
local currentNormal = script.Parent.CFrame.LookVector
local function Step(overrideDistance)
-- Cast ray:
local params = RaycastParams.new()
local direction = currentNormal * (overrideDistance or stepDistance)
params.FilterType = Enum.RaycastFilterType.Blacklist
params.FilterDescendantsInstances = {script.Parent}
local result = workspace:Raycast(currentPos, direction)
local pos
if result then
pos = result.Position
else
pos = currentPos + direction
end
-- Update laser position:
laser.Size = Vector3.new(0.4, 0.4, (pos - currentPos).Magnitude)
laser.CFrame = CFrame.new(currentPos:Lerp(pos, 0.5), pos)
local oldPos = currentPos
currentPos = pos
if result then
-- r = d - 2(d DOT n)n
-- Reflect:
local norm = result.Normal
local reflect = (currentNormal - (2 * currentNormal:Dot(norm) * norm))
currentNormal = reflect
Step(stepDistance - (pos - oldPos).Magnitude)
return
end
curDistance = (curDistance + (pos - oldPos).Magnitude)
-- Apply fade effect to laser as it approaches max distance from < 75 studs:
if curDistance > (maxDistance - 75) then
local d = (curDistance - (maxDistance - 75)) / 75
laser.Transparency = d
end
-- Recurse if max distance not reached:
if curDistance < maxDistance then
task.wait(stepWait)
Step()
end
end
Step()
-- Done! Destroy laser:
laser:Destroy()
end
Shoot() -- Fire shot!
Example:
Edit 1:
If I made a mistake or something wasn’t clear, let me know. I’m simply apply a formula here that I’m not super familiar with yet, so I’m sure it’s prone to error. The application of it seems to work properly though as far as I’ve seen.
Edit 2:
Reformatted to fit with new forum.
Edit 3:
Updated to replace deprecated code.
Edit 4:
Replaced FindPartOnRay with newer Raycast method. Also replaced math.random with the newer Random object.
Edit 5:
Use task
library.