Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 17

Rails

At each bend, a guard rail runs along the outside of the turn: a metal bar half a metre up, on thin posts. It's the first part of the road itself that stands up off the ground.

Step 1,529 of the ride: the road, with its guard rails at the bends ahead.

The rail follows a path on the ground. It runs along the road's outer edge for the last 10 metres before the bend, out to the apex, back to the next segment's outer edge, and on along it for 10 metres more:

runup_points: the last 10 metres before the bend leg_points: out to the apex  turns out leg_points: back to the next segment's edge  turns back runout_points: the first 10 metres after the bend  runs on
The path of a guard rail

From path to quads §

RailSighting finds the rail at every bend in view:

RailSighting.roc
## The quads of the rails at every bend in view: the bends along the road
## ahead, then the bend from the segment behind.
rail_quads_in_view : RoadAhead.RoadView, Geom.Vantage -> List(GuardRail.RailQuad)
rail_quads_in_view = |road, vantage|
	RoadAhead.segments_in_view(road).join_map(
		|in_view|
			match BendInView.bend_after(road, in_view) {
				Ok(bend_in_view) => {
					path_points = rail_points(road, { vantage, bend_in_view })
					GuardRail.quads_along(path_points)
				}
				Err(RoadEndsInView) => []
			},
	)

join_map maps each segment in view to a list, here the quads of its bend's rail, and joins the lists into one. For each segment with a bend after it, rail_points finds the path, and GuardRail.quads_along cuts it into quads. The path joins four lists, one for each box of the diagram:

RailSighting.roc
## The path of the rail at a bend, along the road's outer edge: up to the
## bend, out to where the two outer edges meet, back to the next segment,
## and along it.
rail_points : RoadAhead.RoadView,
{
	vantage : Geom.Vantage,
	bend_in_view : BendInView,
} -> List(RiderView.RiderPoint)
rail_points = |road, { vantage, bend_in_view }| {
	{ end_before, apex, start_after } = bend_in_view.outer_corner_in_view({ road, vantage })
	[
		runup_points(road, { vantage, bend_in_view }),
		leg_points({ from: end_before, to: apex }),
		leg_points({ from: apex, to: start_after }),
		runout_points(road, { vantage, bend_in_view }),
	].join()
}

outer_corner_in_view, from the “The road” chapter, returns the bend's outer corner, and the first line takes that record apart into its three points. A leg between two of them gets a point about every metre:

RailSighting.roc
## A leg of rail from `from` to `to`: a point about every metre, the
## last at `to`, and none at `from`, which ends the leg before it.
leg_points : {
	from : RiderView.RiderPoint,
	to : RiderView.RiderPoint,
} -> List(RiderView.RiderPoint)
leg_points = |{ from, to }| {
	pieces = leg_pieces(RiderView.ground_distance({ from, to }))
	evenly_spaced({ from, to, pieces }).drop_first(1)
}

The points are about a metre apart, and a post will stand at each one. drop_first(1) drops a leg's first point, from, because that's the last point of the leg before it.

Bars and posts §

GuardRail.quads_along turns the path into flat pieces with four corners each: a bar between each two neighbouring points, then a post at each point. A piece is a record:

GuardRail.roc
## A quad of the rail: its corners and its colour. Its low edge runs along
## the path from start to end, and its high edge runs above it.
RailQuad : {
	corners : {
		low_start : RiderView.PointAtHeight,
		low_end : RiderView.PointAtHeight,
		high_end : RiderView.PointAtHeight,
		high_start : RiderView.PointAtHeight,
	},
	color : RgbColor,
}

Its corners are PointAtHeights, points seen from the rider at a height measured from the rider's own level, so a bar's top corners stand higher than its bottom ones. Where the ground has curved away, that height can be below zero.

Each quad goes into the frame's depth sort on its own, at a depth that's the mean distance of its corners, so a tree can stand in front of one post and behind the next. A quad holds only its corners and a color, so quad_rendering makes its rendering directly. The things beside the road go through Pipeline.rendering_of instead:

GuardRail.roc
## A quad as the frame draws it, at its own depth, unless it's wholly
## behind the camera. A rail quad has no kind for `Pipeline` to ask, so it
## renders itself.
quad_rendering : GuardRail.RailQuad, Sprite.LensAndLight -> Try(Rendered, [BehindCamera])
quad_rendering = |quad, { focal_length, darkness }| {
	dimmed_quad = { ..quad, color: Sky.color_at_darkness(quad.color, darkness) }
	quad_shape(dimmed_quad, focal_length).map_ok(
		|shape| Rendered.of(quad_depth(quad), Sprite.from_shapes([shape])),
	)
}

{ ..quad, color: … } is a record update: a new quad, the same as quad but for its color, which is dimmed with the sky. quad itself stays as it was. map_ok works on a Try: it applies its function to an Ok and leaves an Err as it is, so a quad wholly behind the camera stays refused, and Scene drops those with keep_oks.

rail_quads_in_view finds each rail afresh every frame, its bars and posts from the route and the rider, and the depth sort places each one. Next come the radio towers, which go back through the pipeline as things beside the road.