Parabolic arc as aim assist

I wonder what kind of mesh, ray , shader should I use for make effect like in Fortnite when aim Bow, throw Granade.

It’s this blue bent ray, I’m talking about.

Something as in the video.
Edit:
Ok I had bit thought about it.
As raycast looks like be straight only, the different method should be used.
For visual maybe ribon with some semi transparent shader.
Then it is question of math (f(x) = x2 + 3x − 1).[ Parabolic arch - Wikipedia ]
How to interpret it from muzzle to aim and landing point?

The muzzle is known , aim itself is raycast( but this won’t tell me length ), landing/collision will be something to determine.

1 Like

Found the Unity example solution, it quite lengthy the code

I searched around Godot but found only 2D examples which obviously aren’t suitable.

Some ideas?

The math is relatively simple. You have it all here:

It boils down to 2D because the trajectory is always confined to a plane.

Calculate trajectory points in sufficient density and do a series of short raycasts from each point until you hit something.

Draw as a triangle strip. You can additionally expand the vertices in screen space to avoid the flat ribbon look and make it look like a volumetric arc.

1 Like

Thank you for all suggestions.

I found solution inside this project GDQuest project.

this is relevant code fairly long but, I’ll try to break it down


func _update_throw_velocity() -> void:
	var camera := get_viewport().get_camera_3d()
	var up_ratio: float = clamp(max(camera.rotation.x + 0.5, -0.4) * 2, 0.0, 1.0)

	# var throw_direction := camera.quaternion * Vector3.FORWARD
	# If the player's not aiming, the camera's far behind the character, so we increase the ray's
	# length based on how far behind the camera is compared to the character.
	var base_throw_distance: float = lerp(min_throw_distance, max_throw_distance, up_ratio)
	# var camera_forward_distance := camera.global_position.project(throw_direction).distance_to(_launch_point.global_position.project(throw_direction))
	var throw_distance := base_throw_distance #+ camera_forward_distance
	var global_camera_look_position := from_look_position + throw_direction * throw_distance
	_raycast.target_position = global_camera_look_position - _raycast.global_position

	# Snap grenade land position to an enemy the player's aiming at, if applicable
	var to_target := _raycast.target_position

	if _raycast.get_collision_count() != 0:
		var collider := _raycast.get_collider(0)
		var has_target: bool = collider and collider.is_in_group("targeteables")
		_snap_mesh.visible = has_target
		if has_target:
			to_target = collider.global_position - _launch_point.global_position
			_snap_mesh.global_position = _launch_point.global_position + to_target
			_snap_mesh.look_at(_launch_point.global_position)
	else:
		_snap_mesh.visible = false

	# Calculate the initial velocity the grenade needs based on where we want it to land and how
	# high the curve should go.
	var peak_height: float = max(to_target.y + 0.25, _launch_point.position.y + 0.25)

	var motion_up := peak_height
	var time_going_up := sqrt(2.0 * motion_up / gravity)

	var motion_down := to_target.y - peak_height
	var time_going_down := sqrt(-2.0 * motion_down / gravity)

	_time_to_land = time_going_up + time_going_down

	var target_position_xz_plane := Vector3(to_target.x, 0.0, to_target.z)
	var start_position_xz_plane := Vector3(_launch_point.position.x, 0.0, _launch_point.position.z)

	var forward_velocity := (target_position_xz_plane - start_position_xz_plane) / _time_to_land
	var velocity_up := sqrt(2.0 * gravity * motion_up)

	# Caching the found initial_velocity vector so we can use it on the throw() function
	_throw_velocity = Vector3.UP * velocity_up + forward_velocity


func _draw_throw_path() -> void:
	const TIME_STEP := 0.05
	const TRAIL_WIDTH := 0.25

	var forward_direction = Vector3(_throw_velocity.x, 0.0, _throw_velocity.z).normalized()
	var left_direction := Vector3.UP.cross(forward_direction)
	var offset_left = left_direction * TRAIL_WIDTH / 2.0
	var offset_right = -left_direction * TRAIL_WIDTH / 2.0

	var st := SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLES)

	var end_time := _time_to_land + 0.5
	var point_previous = Vector3.ZERO
	var time_current := 0.0
	# We'll create 2 triangles on each iteration, representing the quad of one
	# section of the path
	while time_current < end_time:
		time_current += TIME_STEP
		var point_current := _throw_velocity * time_current + Vector3.DOWN * gravity * 0.5 * time_current * time_current

		# Our point coordinates are at the center of the path, so we need to calculate vertices
		var trail_point_left_end = point_current + offset_left
		var trail_point_right_end = point_current + offset_right
		var trail_point_left_start = point_previous + offset_left
		var trail_point_right_start = point_previous + offset_right

		# UV position goes from 0 to 1, so we normalize the current iteration
		# to get the progress in the UV texture
		var uv_progress_end = time_current / end_time
		var uv_progress_start = uv_progress_end - (TIME_STEP / end_time)

		# Left side on the UV texture is at the top of the texture
		# (Vector2(0,1), or Vector2.DOWN). Right side on the UV texture is at
		# the bottom.
		var uv_value_right_start = (Vector2.RIGHT * uv_progress_start)
		var uv_value_right_end = (Vector2.RIGHT * uv_progress_end)
		var uv_value_left_start = Vector2.DOWN + uv_value_right_start
		var uv_value_left_end = Vector2.DOWN + uv_value_right_end

		point_previous = point_current

		# Both triangles need to be drawn in the same orientation (Godot uses
		# clockwise orientation to determine the face normal)

		# Draw first triangle
		st.set_uv(uv_value_right_end)
		st.add_vertex(trail_point_right_end)
		st.set_uv(uv_value_left_start)
		st.add_vertex(trail_point_left_start)
		st.set_uv(uv_value_left_end)
		st.add_vertex(trail_point_left_end)

		# Draw second triangle
		st.set_uv(uv_value_right_start)
		st.add_vertex(trail_point_right_start)
		st.set_uv(uv_value_left_start)
		st.add_vertex(trail_point_left_start)
		st.set_uv(uv_value_right_end)
		st.add_vertex(trail_point_right_end)

	st.generate_normals()
	_trail_mesh_instance.mesh = st.commit()

full code with rest of logic can be found here godot-4-3d-third-person-controller/player/grenade_launcher.gd at main · gdquest-demos/godot-4-3d-third-person-controller · GitHub

It appears not to be taking obstacles into account.

1 Like

good catch, it does not for Trail.

It only relay on CharacterBody3D logic from Grenade.

func throw(throw_velocity: Vector3) → void:
_velocity = throw_velocity

Could I maybe just fix it by adding short ray’s to cut off rest of projection?

I already described it above. You’d need to raycast each segment between two consecutive trajectory points until you hit something.

1 Like

Keep in mind that if you want to be “closer to reality,” you need to account for whether the projectile will travel upward or downward — take gravity into account — as this changes the length and profile of the trajectory. You also need to factor in the wind, which introduces a third axis. That’s why being a sniper is so difficult in real life.

1 Like

I don’t think the visualization of the trajectory would need to count in the eventual wind. Visualization is often an idealized estimate and wind adds an element of subtle randomness to the actual trajectory. That said, third person games generally don’t have wind as a gameplay element, but even if the wind needs to be accounted for the main trajectory calculation will still basically remain planar. Not that this matters too much outside of mental conceptualization as the third vector component can trivially be added to all equations, and constant wind force applied the same way the gravity is applied.

1 Like
  • The visualization itself — perhaps — shouldn’t show the effect of wind (though that would be interesting), but it’s advisable to factor it in during calculations
  • If (and only if) you want to make it a little more realistic.

The question was about drawing a visualization curve. The actual trajectory will typically be calculated by the physics engine. There could be (minor) discrepancies between the two because pre-calculated trajectory is fully analytic (i.e. exact mathematical curve) while engine integrates in time slices resulting in a linearly segmented approximation, introducing possible inaccuracies depending on the physics time step. The difference shouldn’t be too drastic though. The physics engine could be abused to run the simulation beforehand and deliver the exact points the object will be at each frame but I don’t think doing that is worth the effort. Analytic trajectory is computationally way cheaper and perfectly fine for visualizations.

I always prefer things a bit less realistic which typically results in a bit more fun. Reality is tedious and we already have plenty of it all around us :smiley:

1 Like

Exactly. It’s fun to watch how gusts of wind change a grenade’s flight path.

Well, we’re still a long way from “tedious realism” when using Godot (or any other game engine).

There won’t be any gusts if you don’t put them in. I don’t remember ever seeing a third person run and gun game that has a wind affecting projectile trajectories. It’d be frustrating and generally incompatible with the third person gameplay. “Realistic” wind only makes sense in precision first person shooters… and golf sims. And even there it’s mighty annoying.

Well, that depends on the game developer — whether he’ll include it in the game or not.

  • Wind speed
  • Wind direction

No sane developer would put projectile-affecting wind into a third person game. It wouldn’t make sense from the gameplay standpoint.

What was this discussion about again?

1 Like

That’s up to the developer to decide.

I have no idea what this discussion is about; I just pointed out some aspects that the developer may or may not choose to include and take into account in the game. Maybe he’ll figure out how to “work them into the game” or explore them further.

1 Like

Thanks for interesting side_quest :slight_smile:

I’ll be happy with results of basic parabolic without drag as mentioned in @normalized link inside wikipedia. ( Black one ) .

It rather for grenade, possibly bow which I guess be more like fantasy then real.

Crossbow be rather raycast since velocity should be high enough make it straight.

About bullet and wind, for most even FPS games I guess they aren’t bothered about that unless special Sniper Rifle comes to play.

Inclinedthrow2

1 Like

Something what catching me is why there is GrenadeLauncher assigned as Skeleton for AimSprite and AimSprite2?

Another thing is when you try to recreate it it shows by Godot Editor only Skeleton3D is allowed, so I guess the older version get away with it or is here something else going on?

So far I got this

Current code logic for making and checking points, rest of logic is same as before in GDQuest original.

but this is basically running each time the while time_current < end_time. Would there be maybe better solution to be more accurate? In video you can see it’s not very accurate but it use 41 - 31 points usually.

		time_current += trail_point_density
		var point_current := _throw_velocity * time_current + Vector3.DOWN * gravity * 0.5 * time_current * time_current
	    var global_from := _launch_point.global_position + point_previous
		var global_to := _launch_point.global_position + point_current        
        var query := PhysicsRayQueryParameters3D.create(global_from, global_to)		
        var hit := space_state.intersect_ray(query)

		var stop_after_this_segment := false
		if hit:
			point_current = hit.position - _launch_point.global_position
			stop_after_this_segment = true
		if stop_after_this_segment:
			break

How do you determine how accurate it is? Increasing the number of points should make it smoother.

The mesh should only be rebuilt when any of the projectile parameters change.