How to reflect rays on hit

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:

  1. Cast Ray
  2. Update current position with new position (end of ray)
  3. If hit, find reflection, override normal with reflected normal
  4. If not hit, do nothing
  5. If max iterations/distance/whatever reached, RETURN/STOP
  6. 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.

359 Likes

At this point it looks like you’re making lasers.

26 Likes

[quote] At this point it looks like you’re making lasers.
[/quote]

It’s possible that I might be making my own twist of laser tag

6 Likes

The first thing I thought of when I saw this was Terraria’s Shadowbeam Staff. So, I quickly remade it: http://gfycat.com/PolishedJovialBoilweevil Kinda a little crappy, doesn’t do damage, stuff like that. Nonetheless, I thought it was pretty cool to see the staff in 3d.

1 Like

I think it’s funny I made this a day before you did. :slight_smile:

We are connected. Creepy space sound

3 Likes
2 Likes

I posted this on Twitter earlier yesterday

I’ve updated this to be correctly formatted on this new forum.

5 Likes

I just wanted to let you know that your tutorial is still very relevant, and this just help me bring my game from good to great. Thank you for posting this, I can’t wait to implement the math!

6 Likes

3 Years Wow!
Actually this seems handy for a new game I plan to create, bouncing lasers? :open_mouth: never thought of that idea until I saw this thread.

2 Likes

Thats an interesting way of doing that, nice

2 Likes

I have updated this to replace deprecated code and match standard scripting styling:

  • Removed line of code setting FormFactor
  • Changed magnitude to Magnitude
  • Changed cframe.p to cframe.Position
  • Changed cframe.lookVector to cframe.LookVector
  • Changed vector3:lerp to vector3:Lerp
17 Likes

It didn’t work on my end, any help?

1 Like

Hi, sorry for bumping this.

I am making like a Wizard game, where people can shoot things at them, and I am planning to use this.

So my question is, will it create a lot of lag for players if many people are shooting stuff like this against each other?

Like, how much “data” does it use?

(Hope this makes sense)
Thank you for responses!!

Run the rays 100 times and see how performance hard it is yourself
I use the reflections for casting 5-12 particles each time someone hits a wall with an automatic gun and it doesn’t lag (at least not much)

That depends on how you are handling the projectile.
The best way in my opinion is to replicate the ShootOrigin and LookVector via a remote and then render the projectile on each client.
The outcome is this , I shot 1 bullet every 0.01s for a total of 1000 each with ballistics and it ran 60 fps on a 5 year old iPhone.

10 Likes

Wow, thanks for all that!

Yeah, it’s probably best to handle the reflection on the client, and do the first trace on the server.

Thanks a lot!

1 Like

I have updated this post to use the new Raycast method.

4 Likes

How can I make it so it straight curves, like making it so it curves in only X and Z axis. It curves in “Y” axis making this not perfect…

I’ll try to find out how that happens myself meanwhile, but if you’re here, can you solve this for me? :eyes:

1 Like

Okay I solved this one, by making the “norm” variable like this.

local norm = Vector3.new(result.Normal.X,0,result.Normal.Z)
1 Like