Hovercraft "Rolling With Environment" Confusion

Heya there borther,

What do I mean by rolling with the environment?

Imagine this:


Wedge here is in your path, and you want to go up it.

Car here is what you use:


You need the car to angle itself so that it is perpendicular with the ground.

So it doesn’t hit the ground,
So it looks realistic,
But it wont work

Here have been my two methods:

  1. Reactive system
    a. Detect when the front of the car is closer to the ground than back of car
    b. Same applies to sides of the car
    c. Roll/Add Angle to make them equal in size
    d. (Optional) Make the speed of the roll scale with the height difference
  2. Proactive System
    a. Get the average of the normals of the rays from the front and back
    b. Same applies to the sides
    c. Then get the vector that is perpendicular to the new average
    d. And set the body gyro’s cframe to look at that ray/vector

Weaknesses:

  1. Reactive: Wobbles like its nodding to some really dumb question
  2. Proactive: Can’t even get it to work

Here is the code I have for the proactive:

local function changeSteer(scal)
	local stabFold = car.StabilityPoints
	local rPoint = stabFold.RPoint
	local lPoint = stabFold.LPoint
	local fPoint = stabFold.FPoint
	local bPoint = stabFold.BPoint
	
	local fronAve
	local sideAve
	
	local _,_, fN = castRay(fPoint, (-1 * fPoint.CFrame.UpVector), hHeight.Value * 2)
	local _,_, bN = castRay(bPoint, (-1 * bPoint.CFrame.UpVector), hHeight.Value * 2)
	fronAve = (fN + bN)/2 --new method, lets see it
	fronAve = Vector3.new(0, fronAve.Y, fronAve.Z).Unit
	
--	local _,_, rN = castRay(rPoint, (-1 * rPoint.CFrame.UpVector), hHeight.Value * 2)
--	local _,_, lN = castRay(lPoint, (-1 * lPoint.CFrame.UpVector), hHeight.Value * 2)
--	sideAve = (rN + lN)/2
--	sideAve = Vector3.new(sideAve.X, sideAve.Y, 0).Unit
	
	local uFronAve = Vector3.new(0, fronAve.Z, -fronAve.Y).Unit
	
	--change that, broth..
	gyro.CFrame = CFrame.new(physRoot.Position, uFronAve)-- * CFrame.Angles(0, (-scal * tSpeed.Value), 0)
	
end

The code is integrated with the steering system but I’m just troubleshooting the stabilizer right now…

1 Like

since each thing you’re casting from is called a point, is there a chance that all the points are an attachment on the car?

All points are parts that are welded to the car. Four total, One for the Front, One for the Right, etc, etc.

2 Likes

What does the function castRay do in detail?

Casts ray using the part you specified as the origin, a specified direction, and a set length the ray can be
Returns the object hit, the position it stopped at, and the normal of the object it hit.

1 Like

Some success!
Not entirely tho.
Got the car to get perpendicular a little!

New code:

local function changeSteer(scal)
	local ch = -scal * tSpeed.Value
	
	local stabFold = car.StabilityPoints
	local rPoint = stabFold.RPoint
	local lPoint = stabFold.LPoint
	local fPoint = stabFold.FPoint
	local bPoint = stabFold.BPoint
	
	local normAve
	
	local _,_, fN = castRay(fPoint, Vector3.new(0, -1, 0), 500000)
	local _,_, bN = castRay(bPoint, Vector3.new(0, -1, 0), 500000)
	local _,_, rN = castRay(rPoint, Vector3.new(0, -1, 0), 500000)
	local _,_, lN = castRay(lPoint, Vector3.new(0, -1, 0), 500000)
	normAve = ((fN + bN + rN + lN)/4).Unit
	
	local angle = math.acos(normAve:Dot(physRoot.CFrame.LookVector))
	
	if angle > 91 then
		normAve = -normAve
	end
	normAve = Vector3.new(0, normAve.Z, -normAve.Y) --perpendicular time
	
	--change that, broth
	gyro.CFrame = CFrame.new(physRoot.Position, (physRoot.Position + normAve)) * CFrame.Angles(0, ch, 0)
end

It seems to get perpendicular, which is very nice. However it is very slow, and it will turn but then when you let go it will return to the original look vector (I’ll probably be able to fix this, but not the first one?)

One way is to do a bunch of raycasts from the underside of the hovercraft a short distance to where the ground is expected to be. Use the surface normal vectors that are returned as the third tuple value from FindPartOnRay(). You’ll probably want to average a bunch of normals to come up with what the “up” should be for the whole craft. How you weight or prioritize them will be where you find the devil in the details. Be aware though, that if you’re hovering over things that are not flat-ish ground, you’ll want to not use their normals. You want to orient to the big flat surfaces, not the sides of any old small parts you hover over. If you sample a lot of points, you could also dot product them with something like the current “up” of the craft and throw away outliers before averaging.

You could also raycast down from the 4 corners of the craft and use the hit points rather than the normals. This approach gives you an up direction that is independent of surface normals. Any 3 of the 4 points will define a unique plane, such that a cross product, e.g. (A-B):Cross(C-B) will give you the “up” normal of this plane. You again might end up with different “up” values for the 4 different ways to choose 3 of the 4 points, and once again you need to average them or pick a dominant one, etc. Whichever looks right. What you’ll find with just 4 points, including with your solution so far, is that you’ll want to not just average them per frame, but do a running average to integrate them over time so that your craft does not flip instantly when the up direction changes. A running average (integrator) is a simple case of a low-pass filter, it will smooth away sudden changes.

Once you have an “up” vector, however you choose to get it, you can use it to set the orientation of the craft’s AlignOrientation or BodyGyro.

7 Likes

I do not necessarily think I’ll have to account for any weird shapes that the vehicle is over as the car is meant for roads and generally flat ground. Of course I will add more points so I can average the normal better and help with the overall performance.

I like the idea of having an overall average for what the norm vector should so I will be using that.

Could you explain your second method a bit more? I don’t totally understand the advantage it would bring compared to the normals and how different would it be to the normals, I would still need to get the direction from this new averaged position to the vehicle (The Up Vector to use) which is still ultimately an averaged normal with maybe some slight differences.

The second method is more like the craft has virtual table legs. If the terrain undulates a lot, and the wavelength of the terrain ripples is shorter than the length of the craft, sampling the normals at the corners could tilt your craft significantly off from the perceived overall (macro scale) orientation of the terrain. Like if you were traveling over a wavy surface, the craft could follow the waves rather than float level above them. It should follow waves where the wavelength is much longer than the craft, but not when it’s shorter. So if you deal with just the normals, you need to average a lot of them, effectively doing a vector equivalent of a 2D blur kernel to filter away the small ripples. That’s why I suggested factoring the hit positions in also. the hit positions gives you basically the orientation that a 4-legged table would have. It is sensitive to absolute heights, but not the orientation of the surface at the sample points. As with the normals, more samples is better. Since the raycast gives you both, there is no extra cost to get both values, only to process them.

I suspect some combination of these two measurements would be enough input to make something really stable. It’s common for a control system to use various derivatives of an input signal in some linear combination, and that’s what’s going on when you use both. The “signal” here is ground surface; the sampled hit points are the ground height as a function of X and Z, and the normals are orthogonal to the gradient, so they contain information about the first derivative of the surface. Both can be useful.

Unfortunately, I haven’t made a raycast-based hovering platform, precisely, so I can’t really say for sure what the exact magic formula is. But I do something similar to what I described above in one of my games for characters to walk on differently-oriented surfaces smoothly at any speed, just with some higher-dimensional structures that pack the time and spatial averaging together.

Thank you, but I think I’ll stick with my normals.
I want to ask you how I would rotate my craft now, my auto perpendicular-ization gets in the way and I end up with this:
image

I’m using euclidean rotations and I think i’ll have to go to quaternions :T

I would refer to the equations in this thread:

They are if anything easier to apply to a hovercraft than a car. Basically just simulate a spring damper system at each corner (or add more points around the vehicle for better behaviour in rough strain) using VectorForces, fiddle with the stiffness and damping constants, and the rest of the physics will sort itself out.

That defeats the purpose of a hovercraft. Plus that wouldn’t work for instances where the car is upside down, the car is supposed to hover and follow the curve of the terrain under it. Having invisible wheels is not necessarily my goal.

Finished the final product!
Thank you to all who replied!

Final Code
local function changeSteer(scal)
	local backPerc
	if scal == 1 then
		if turn + tSpeed.Value < math.rad(360) then --because radians, ugh...
			turn = turn + tSpeed.Value
		else
			turn = math.rad(360) - (turn + tSpeed.Value)
		end
	elseif scal == -1 then
		if turn - tSpeed.Value > 0 then
			turn = turn - tSpeed.Value
		else
			turn = (turn - tSpeed.Value) + math.rad(360)
		end
	end
	if turn > math.rad(180) then
		backPerc = (math.rad(180) - (turn - math.rad(180)))/ math.rad(180)
	else
		backPerc = turn/math.rad(180)
	end
	
	--print(turn)
	
	local stabFold = car.StabilityPoints
	
	for _, poin in pairs(stabFold:GetChildren()) do
		local _,_, fN = castRay(poin, Vector3.new(0, -1, 0), 500000)
		normAve = ((normAve.Unit + fN.Unit)/2).Unit
	end
	
	local uAve = Vector3.new(0, normAve.Z, -normAve.Y) --perpendicular time
	
	local dAve = -uAve
	local yDif = dAve.Y - uAve.Y 
	
	--rotate it with the angle of turn
	local px = (uAve.X * math.cos(turn)) - (uAve.Z * math.sin(turn))
	local pz = (uAve.Z * math.cos(turn)) + (uAve.X * math.sin(turn))
	uAve = Vector3.new(px, (uAve.Y + (yDif * backPerc)), pz)
	
	--change that, broth
	gyro.CFrame = CFrame.new(physRoot.Position, (physRoot.Position + uAve))
end

The more backwards you face, the more opposite the y value is with relation to the original perpendicular vector.

Of course I will have to do some rotations for when its moving towards this new y as it will not be totally perpendicular with the ground, but for now I have a success!

3 Likes