# smooth.ludic — a synthetic 2 km test ground for the terrain renderer. # # The valley scene stands on a real survey (Copernicus GLO-30 resampled to a 4 m grid), # which carries its own resampling lattice and quantisation. This scene carries none: # the height field is an analytic function (see SMOOTH in heightgen.frag), so anything # that still looks like a grid here belongs to the renderer, not the data. # # bin/ludic build examples/rendering/smooth.ludic && ./build/smooth program Smooth { numbers float import "ludic.render3d/r3d.ludic" property Marker { on: int = 1 } model Anchor { Marker } state SmoothState { frame: int = 0 shot_at: int = 40 walk: bool = false spin: bool = false l_blades: Layer = null l_grass_a: Layer = null l_grass_b: Layer = null s_blades: Stream = null s_cards_a: Stream = null s_cards_b: Stream = null l_trees: Layer = null } function rnd() -> float { return float(rng_range(0, 9999)) / 10000.0 } function rnd_range(a: float, b: float) -> float { return Math.lerp(a, b, rnd()) } function smooth(a: float, b: float, x: float) -> float { let t = Math.clamp((x - a) / (b - a), 0.0, 1.0) return t * t * (3.0 - 2.0 * t) } function slope_at(render3d_st: mut Render3dState, x: float, z: float) -> float { let e = 2.0 let dx = terrain_height(render3d_st, x + e, z) - terrain_height(render3d_st, x - e, z) let dz = terrain_height(render3d_st, x, z + e) - terrain_height(render3d_st, x, z - e) let ny = 4.0 / Math.sqrt(dx * dx + dz * dz + 16.0) return 1.0 - ny } function noise01(x: float, z: float, scale: fixed) -> float { let n = Noise.fbm2(fixed(x) * scale, fixed(z) * scale, 0, 4) return float(n * 0.5 + 0.5) } # grass only where the ground is grassy: gentle, above the water, below the mountain function stream_fill(render3d_st: mut Render3dState, s: Stream, cx: int, cz: int, band: int) -> void { seed((cx * 73856093) ^ (cz * 19349663) ^ (band * 83492791) ^ (s.kind * 2654435761)) let size = s.size let x0 = float(cx) * size; let z0 = float(cz) * size # Candidate spacing. These are metres between attempts, so halving one quadruples the # work and the instance count: the first pass here was dense enough to bury the scene # and cost most of the frame. Blades are only placed close to the eye, where they read # as individual grass; past that the cards carry the cover. var step = 1.0 if s.kind == 0 { if band == 0 { step = 0.22 } else if band == 1 { step = 0.45 } else { step = 1.1 } } else { if band == 0 { step = 0.9 } else if band == 1 { step = 1.8 } else { step = 4.0 } } var z = z0 while z < z0 + size { var x = x0 while x < x0 + size { let px = x + rnd() * step let pz = z + rnd() * step let h = terrain_height(render3d_st, px, pz) # Grassy ground only: above the water, off the steep parts, below the rim. Every # test is a smooth ramp — a hard height cut carves the cover into contour rings, # because the cut lands on a line of constant elevation. var keep = smooth(0.2, 1.6, h) # out of the water keep = keep * smooth(90.0, 45.0, h) # below the mountain keep = keep * smooth(0.42, 0.16, slope_at(render3d_st, px, pz)) keep = keep * (0.25 + 0.9 * noise01(px, pz, 0.02)) keep = keep * 0.75 var sc = rnd_range(0.16, 0.4) if s.kind != 0 { sc = rnd_range(1.5, 2.5) * (1.0 + 0.5 * float(band)) } if rnd() < keep { stream_emit(render3d_st, s, px, h - 0.03, pz, sc, rnd() * (2.0 * PI), rnd(), rnd_range(0.6, 1.0)) } x = x + step } z = z + step } } function scene_draw(render3d_st: mut Render3dState) -> void { scatter_draw(render3d_st) } function scene_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void { scatter_draw_casters(render3d_st, light_vp) } handler Boot(render3d_st: mut Render3dState, smooth_st: mut SmoothState) phase Start { spawn Anchor {} r3d_on_draw(render3d_st, fn scene_draw) r3d_on_casters(render3d_st, fn scene_draw_casters) r3d_on_stream_fill(render3d_st, fn stream_fill) # 2 km square, generated analytically render3d_st.TERRAIN_HALF = 1000 render3d_st.ter_smooth = true if not r3d_init(render3d_st, 1920, 1080, "Smooth") { quit() return } smooth_st.walk = Os.has_env("R3D_WALK") smooth_st.spin = Os.has_env("R3D_SPIN") if Os.has_env("R3D_SHOT") { smooth_st.shot_at = Text.to_int(Os.env("R3D_SHOT")) } var cx = 0.0; var cz = 260.0 if Os.has_env("R3D_CAM_X") { cx = float(Text.to_int(Os.env("R3D_CAM_X"))) } if Os.has_env("R3D_CAM_Z") { cz = float(Text.to_int(Os.env("R3D_CAM_Z"))) } var ch = 1.8; var cp = -3.0; var cy = 180.0 if Os.has_env("R3D_CAM_H") { ch = float(Text.to_int(Os.env("R3D_CAM_H"))) } if Os.has_env("R3D_CAM_PITCH") { cp = float(Text.to_int(Os.env("R3D_CAM_PITCH"))) } if Os.has_env("R3D_CAM_YAW") { cy = float(Text.to_int(Os.env("R3D_CAM_YAW"))) } cam_set(render3d_st, cx, terrain_height(render3d_st, cx, cz) + ch, cz, cy, cp) # a small pond in the middle of the meadow — the plane self-clips to the basin, so # its extent only has to cover the hollow, not the map water_init(render3d_st, 4.0, 0.0, 0.0, 200.0, 200.0) sky_set_yaw(render3d_st, Math.deg_to_rad(120.0)) # grass: blades underfoot, cards beyond let dir = render3d_st.r3d_assets + "/models/grass_medium_01" let ga = gltf_load(render3d_st, dir, "grass_medium_01_1k.gltf", "grass_medium_01_tall_a_LOD0") let gb = gltf_load(render3d_st, dir, "grass_medium_01_1k.gltf", "grass_medium_01_mid_a_LOD0") if ga == null or gb == null { print("smooth: no grass models"); return } # blades are the expensive layer: keep them near, and cap them low smooth_st.l_blades = layer_new(render3d_st, model_blade(render3d_st), 400000, true, 3.0, 0.0, 70.0) smooth_st.l_blades.blade = true smooth_st.l_grass_a = layer_cards(render3d_st, ga, 200000, 0.3, 260.0) smooth_st.l_grass_b = layer_cards(render3d_st, gb, 200000, 0.3, 260.0) v3_set(smooth_st.l_grass_a.tint, 0.95, 1.0, 0.85) v3_set(smooth_st.l_grass_b.tint, 0.9, 0.98, 0.8) smooth_st.l_grass_a.rough = 1.6; smooth_st.l_grass_b.rough = 1.6 smooth_st.s_blades = stream_new(render3d_st, smooth_st.l_blades, 8.0, 60.0, 12.0, 28.0, 60.0, 60.0) smooth_st.s_cards_a = stream_new(render3d_st, smooth_st.l_grass_a, 32.0, 240.0, 45.0, 110.0, 240.0, 240.0) smooth_st.s_cards_b = stream_new(render3d_st, smooth_st.l_grass_b, 32.0, 240.0, 45.0, 110.0, 240.0, 240.0) smooth_st.s_blades.kind = 0; smooth_st.s_cards_a.kind = 1; smooth_st.s_cards_b.kind = 2 place_trees(render3d_st, smooth_st) } # scattered firs on the gentle ground, thinning toward the pond and the rim function place_trees(render3d_st: mut Render3dState, smooth_st: mut SmoothState) -> void { let model = gltf_load(render3d_st, render3d_st.r3d_assets + "/models/fir_tree_01", "fir_tree_01_1k.gltf", "fir_tree_01_c_LOD0") if model == null { print("smooth: no fir model"); return } smooth_st.l_trees = layer_new(render3d_st, model, 20000, false, 0.0, 90.0, 0.0) layer_set_impostor(render3d_st, smooth_st.l_trees, impostor_bake(render3d_st, model, 12, 512, 1024)) v3_set(smooth_st.l_trees.tint, 0.9, 1.0, 0.85) seed(4242) var z = -900.0 while z < 900.0 { var x = -900.0 while x < 900.0 { let px = x + rnd() * 14.0; let pz = z + rnd() * 14.0 let h = terrain_height(render3d_st, px, pz) var keep = smooth(7.0, 14.0, h) # above the pond shore keep = keep * smooth(120.0, 60.0, h) # below the rim keep = keep * smooth(0.45, 0.2, slope_at(render3d_st, px, pz)) keep = keep * smooth(0.45, 0.75, noise01(px, pz, 0.004)) if rnd() < keep * 0.5 { let sc = rnd_range(0.7, 1.35) layer_add(smooth_st.l_trees, px, h - 0.2, pz, sc, rnd() * (2.0 * PI), rnd(), 0.0) } x = x + 14.0 } z = z + 14.0 } } handler Fly(render3d_st: mut Render3dState, smooth_st: SmoothState) phase Input { if smooth_st.walk { cam_move(render3d_st, 1.0, 0.0, 0.0, 0.003, 0.0); return } if smooth_st.spin { cam_move(render3d_st, 0.0, 0.0, 0.0, 0.02, 0.0); return } if not is_windowed() { return } Input.poll() var fwd = 0.0; var side = 0.0; var up = 0.0 let speed = 0.6 if Input.key_down(key: 'w') { fwd = speed } if Input.key_down(key: 's') { fwd = -speed } if Input.key_down(key: 'd') { side = speed } if Input.key_down(key: 'a') { side = -speed } if Input.key_down(key: 'e') { up = speed } if Input.key_down(key: 'q') { up = -speed } var dyaw = 0.0; var dpitch = 0.0 if Input.mouse_down(button: 0) { dyaw = float(Input.mouse_dx()) * -0.004 dpitch = float(Input.mouse_dy()) * -0.004 } if fwd != 0 or side != 0 or up != 0 or dyaw != 0 or dpitch != 0 { cam_move(render3d_st, fwd, side, up, dyaw, dpitch) } if Input.key_pressed(key: 'p') { let gy = terrain_height(render3d_st, render3d_st.cam_pos[0], render3d_st.cam_pos[2]) print(`R3D_CAM_X={string(int(render3d_st.cam_pos[0]))} R3D_CAM_Z={string(int(render3d_st.cam_pos[2]))} R3D_CAM_H={string(int(render3d_st.cam_pos[1] - gy))} R3D_CAM_YAW={string(int(render3d_st.cam_yaw * (180.0 / PI)))} R3D_CAM_PITCH={string(int(render3d_st.cam_pitch * (180.0 / PI)))}`) } } handler Draw(render3d_st: mut Render3dState, smooth_st: mut SmoothState) phase Render { r3d_frame(render3d_st, float(Time.elapsed())) smooth_st.frame += 1 if smooth_st.frame == smooth_st.shot_at { Gl.screenshot(path: "build/smooth.ppm") } Gl.swap() } }