if (FALSE) { # interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")
# Install the full-altitude Prague data once before rendering:
# skymodelr::download_sky_data(sea_level = FALSE)
if (
requireNamespace("ambient", quietly = TRUE) &&
requireNamespace("tree3d", quietly = TRUE)
) {
# Scene units are kilometres.
# Rounded green hills, with their lower capsule ends buried in the ground.
# The rows are roughly 3-7, 17-26, and 60-85 km from the camera.
# Small foreground hills stay crisp while larger distant hills fade.
hills = data.frame(
x = c(-1.1, 3, -6, -2, 3.5, 8, -27, -17, -6, 7, 22, 35, -45),
z = c(-5.6, -2.2, 9, 15, 11, 17, 55, 63, 69, 58, 66, 60, 62),
radius = c(0.30, 0.72, 1.7, 1.4, 2, 2.2, 6, 5, 5.5, 5, 7, 4, 2),
top = c(0.7, 1.7, 3.8, 3, 4.7, 4.2, 10, 9, 22, 20.5, 31, 30, 34)
)
terrain_mat = diffuse(color = "#469D60")
terrain = xz_rect(xwidth = 160, zwidth = 160, material = terrain_mat)
for (i in seq_len(nrow(hills))) {
h = hills[i, ]
terrain = add_object(
terrain,
csg_object(
csg_capsule(
start = c(h$x, -h$radius, h$z),
end = c(h$x, h$top - h$radius, h$z),
radius = h$radius
),
material = terrain_mat
)
)
}
# A river winds around the capsule footprints and turns out of sight behind
# the distant pair at (-6, 69) and (7, 58). Coordinates and width are in km.
# fmt: skip
river_bends = data.frame(
x = c(0.3, 0.1, 0.7, 0.8, -1.2, -2.6, -3.9, -4.2, 0.2, 2.5, -1.1, 0.7, 1.4, 1.1, -2, -4.5),
z = c(-12, -7, -4, -1, 3, 7, 12, 17, 23, 32, 43, 53, 62, 69, 76, 79)
)
river_curve = stats::splinefun(
river_bends$z,
river_bends$x,
method = "natural"
)
river_z = seq(min(river_bends$z), max(river_bends$z), length.out = 600)
river_center = cbind(x = river_curve(river_z), z = river_z)
# Offset perpendicular to the tangent, keeping the river 1 km wide even
# through bends. Reverse the second bank to make one closed polygon.
river_width = 1
river_slope = river_curve(river_z, deriv = 1)
bank_offset = river_width /
2 *
cbind(1, -river_slope) /
sqrt(1 + river_slope^2)
river_banks = rbind(
river_center + bank_offset,
(river_center - bank_offset)[length(river_z):1, ]
)
# Keep the polygon's world x coordinates and lift its top 1 m above
# ground. A thin extrusion gives the river an upward-facing surface.
terrain = add_object(
terrain,
extruded_polygon(
river_banks,
plane = "xz",
top = 0.001,
bottom = -0.001,
flip_horizontal = TRUE,
material = microfacet(color="#168BC4",transmission=TRUE, roughness=0.2)
)
)
# Redwood-sized trees: 60-100 m tall, in a scene measured in kilometres.
# Generate three solid tree meshes once, then share them across 20,000 instances.
# Crown widths are 12-25 m and trunk diameters are approximately 2.4-5 m.
tree_types = c("pyramidal1", "pyramidal2", "columnar")
tree_colors = c("#245638", "#2B603E", "#305A3B")
tree_models = lapply(seq_along(tree_types), function(i) {
tree3d::tree_mesh(
crown_type = tree_types[i],
solid = TRUE,
resolution = "medium",
tree_height = 0.08,
trunk_height_ratio = c(0.25, 0.3, 0.35)[i],
crown_width = c(0.018, 0.016, 0.020)[i],
trunk_width = c(0.0032, 0.0036, 0.0040)[i],
crown_color = tree_colors[i],
trunk_color = "#794A35",
ambient_intensity = 0
) |>
raymesh_model()
})
# Log-spaced distances give the foreground enough trees to establish scale.
# Candidate positions follow the camera's view across the flat valley floor.
tree_count = 20000
tree_candidates = 4 * tree_count
set.seed(2028)
tree_distance = exp(runif(tree_candidates, log(1.4), log(85)))
tree_positions = data.frame(
x = runif(tree_candidates, -0.65, 0.65) * tree_distance,
z = -8 + tree_distance,
size = runif(tree_candidates, 0.75, 1.25),
angle = runif(tree_candidates, 0, 360),
model = sample(seq_along(tree_models), tree_candidates, replace = TRUE)
)
# Leave enough room for the widest crown along both riverbanks and hills.
# Measure distance to river segments so the exclusion follows every bend.
tree_clearance = 0.015
tree_clear = rep(TRUE, nrow(tree_positions))
for (i in seq_len(nrow(river_center) - 1)) {
dx = river_center[i + 1, 1] - river_center[i, 1]
dz = river_center[i + 1, 2] - river_center[i, 2]
along = pmin(
pmax(
((tree_positions$x - river_center[i, 1]) *
dx +
(tree_positions$z - river_center[i, 2]) * dz) /
(dx^2 + dz^2),
0
),
1
)
river_dx = tree_positions$x - (river_center[i, 1] + along * dx)
river_dz = tree_positions$z - (river_center[i, 2] + along * dz)
tree_clear = tree_clear &
river_dx^2 + river_dz^2 > (river_width / 2 + tree_clearance)^2
}
for (i in seq_len(nrow(hills))) {
tree_clear = tree_clear &
(tree_positions$x - hills$x[i])^2 +
(tree_positions$z - hills$z[i])^2 >
(hills$radius[i] + tree_clearance)^2
}
tree_positions = head(tree_positions[tree_clear, ], tree_count)
# Each group shares one mesh/BVH. Vary height and yaw without copying geometry.
for (i in seq_along(tree_models)) {
grove = tree_positions[tree_positions$model == i, ]
terrain = add_object(
terrain,
create_instances(
tree_models[[i]],
x = grove$x,
z = grove$z,
angle_y = grove$angle,
scale_x = grove$size,
scale_y = grove$size,
scale_z = grove$size
)
)
}
# Billowing Perlin volumes sit above each row of hills. Optical depth sets
# the cloud's own scattering; sky_light() separately supplies clear-air haze.
cloud_rows = data.frame(
z = c(3, 21, 63),
base = c(5, 6, 12.5),
width = c(16, 32, 90),
depth = c(10, 16, 24)
)
landscape = terrain
for (i in seq_len(nrow(cloud_rows))) {
cl = cloud_rows[i, ]
landscape = add_object(
landscape,
cloud(
z = cl$z,
y = cl$base + 1.8 / 2,
width = cl$width,
depth = cl$depth,
height = 1.8,
resolution = 64,
coverage = 0.4,
detail = 0.4,
optical_depth = 4,
g = 0.65,
seed = 41 + i
)
)
}
day = as.POSIXct("2026-06-21 18:00:00", tz = "America/New_York")
sunset = as.POSIXct("2026-06-21 20:35:00", tz = "America/New_York")
render_sky = function(light, iso = 4, caption = "") {
set.seed(2026)
image = landscape |>
add_infinite_light(light) |>
render_scene(
lookfrom = c(0, 0.35, -8),
lookat = c(0, 2.4, 3),
fov = 47,
aperture = 0,
width = 384,
height = 240,
samples = 32,
integrator_type = "nee",
iso = iso,
tonemap = "raw",
plot_scene = FALSE
)
rayimage::render_stack(list(
image,
rayimage::render_text_image(
caption,
size = 14,
font = "sans",
width = dim(image)[2],
height = 34,
just = "center",
check_text_width = FALSE,
check_text_height = FALSE
)
))
}
# Haze changes contrast and color with distance. Hold visibility and ISO fixed.
rayimage::plot_image_grid(
list(
render_sky(
sky_light(
40.7,
-74,
day,
meters_per_unit = 1000,
haze = FALSE
),
caption = "No finite haze"
),
render_sky(
sky_light(40.7, -74, day, meters_per_unit = 1000, visibility = 120),
caption = "Finite haze, 120km"
),
render_sky(
sky_light(40.7, -74, day, meters_per_unit = 1000, visibility = 20),
caption = "Finite haze, 20km"
)
),
dim = c(1, 3)
)
# Isolate altitude-dependent lighting by disabling finite haze in both images.
# The Sun is below the ground horizon, but the elevated cloud can still see it.
rayimage::plot_image_grid(
list(
render_sky(
sky_light(
40.7,
-74,
sunset,
meters_per_unit = 1000,
haze = FALSE,
query_altitude = FALSE
),
iso = 175,
caption = "Fixed observer altitude"
),
render_sky(
sky_light(
40.7,
-74,
sunset,
meters_per_unit = 1000,
haze = FALSE,
query_altitude = TRUE
),
iso = 175,
caption = "Altitude at each interaction"
),
render_sky(
sky_light(
40.7,
-74,
sunset,
meters_per_unit = 1000,
haze = TRUE,
query_altitude = TRUE
),
iso = 175,
caption = "Altitude + haze each interaction"
)
),
dim = c(1, 3)
)
}
}
Run the code above in your browser using DataLab