flan/examples/core-3d-picking.fln

131 lines
6.6 KiB
Plaintext

;;;; raylib [core] example - 3d picking
;;;;
;;;; examples/core/core_3d_picking.c. The one `core` example in this batch,
;;;; and it is here for surface and not for category: it is the only example
;;;; anywhere upstream that uses Ray and RayCollision without also loading a
;;;; Model, so it is the only way those two structs get a caller at all.
;;;;
;;;; Picking is the round trip the corpus was missing. core-world-screen.fln
;;;; already runs get-world-to-screen — a point in the scene to a pixel. This
;;;; is the inverse and it is a different shape: a pixel does not name a
;;;; point, it names a *line* through the scene, which is what a Ray is, and
;;;; the answer to "what did I click on" is where that line first meets
;;;; something, which is what a RayCollision is. Two structs the package could
;;;; not describe, for one operation that cannot be written without them.
;;;;
;;;; What was added, all in vendor/raylib/raylib.fln:
;;;;
;;;; - `Ray` — two Vector3s, origin and direction.
;;;; - `RayCollision` — a C `bool`, a float and two Vector3s. This is the
;;;; one of the three new structs a layout check can actually fail:
;;;; `hit` is one byte and `distance` is four, so there are three padding
;;;; bytes between them that a permutation destroys. BoundingBox's two
;;;; Vector3s are interchangeable and nothing would catch swapping them.
;;;; - `BoundingBox` — shared with examples/models-box-collisions.fln.
;;;; - get-screen-to-world-ray and draw-ray, hand-written beside
;;;; get-world-to-screen and the cube draws for the reasons `bindings`
;;;; gives: the first reads the same camera as its inverse, and the second
;;;; is inside a frame.
;;;;
;;;; get-ray-collision-box came free out of the generated half the moment Ray
;;;; and BoundingBox existed, along with the sphere, triangle and quad forms.
;;;; get-ray-collision-mesh is still refused and will stay refused: Mesh owns
;;;; seven arrays and a GPU handle, and describing it is a different job.
;;;;
;;;; raylib 5.5 renamed GetMouseRay to GetScreenToWorldRay and left the old
;;;; name as a #define. A #define is not a symbol, so there is nothing to bind
;;;; it to and nothing lost by not trying.
import rl "vendor:raylib"
const screen-width = 800
const screen-height = 450
once camera: rl/Camera3D
once cube-position: rl/Vector3
once cube-size: rl/Vector3
;; The picking ray, kept between frames because draw-ray draws it every frame
;; whether or not it hit anything — that is how the example shows where the
;; click went.
once ray: rl/Ray
once collision: rl/RayCollision
;; The same centre/size to min/max conversion as in
;; examples/models-box-collisions.fln. Written out here rather than shared
;; because an example is a single file the reader can follow end to end.
;;
;; It used to be eight expressions of field arithmetic; it is the half-extent
;; subtracted and added, which is what the C means, now that the package
;; carries vector arithmetic — see vendor/raylib/vector.fln. That file
;; exists because raymath is `static inline` and has no symbol to bind, so
;; v3-sub and v3-add are Flan and not C.
fn box-around(centre: rl/Vector3, size: rl/Vector3) -> rl/BoundingBox
let half = rl/v3-scale(size, 0.5)
rl/BoundingBox{.min rl/v3-sub(centre, half) .max rl/v3-add(centre, half)}
fn main() -> ()
rl/init-window(screen-width, screen-height,
"raylib [core] example - 3d picking")
defer rl/close-window()
camera = rl/Camera3D{.position rl/Vector3{.x 10.0 .y 10.0 .z 10.0} .target rl/Vector3{.x 0.0 .y 0.0 .z 0.0} .up rl/Vector3{.x 0.0 .y 1.0 .z 0.0} .fovy 45.0 .projection :projection-perspective}
cube-position = rl/Vector3{.x 0.0 .y 1.0 .z 0.0}
cube-size = rl/Vector3{.x 2.0 .y 2.0 .z 2.0}
;; Both start zeroed, which is the C's `= { 0 }`. A zero Ray draws as a
;; degenerate line at the origin and a zero RayCollision has hit false, so
;; neither needs a "not yet" flag beside it.
ray = rl/Ray{.position rl/Vector3{.x 0.0 .y 0.0 .z 0.0} .direction rl/Vector3{.x 0.0 .y 0.0 .z 0.0}}
collision = rl/RayCollision{.hit false .distance 0.0 .point rl/Vector3{.x 0.0 .y 0.0 .z 0.0} .normal rl/Vector3{.x 0.0 .y 0.0 .z 0.0}}
rl/set-target-fps(60)
until rl/window-should-close()
;; Update. The first-person controls only run while the cursor is
;; captured, which is what the right button toggles — otherwise the mouse
;; could not be used to aim at anything.
if rl/is-cursor-hidden()
rl/update-camera(addr(camera), :camera-first-person)
if rl/is-mouse-button-pressed(:mouse-right)
if rl/is-cursor-hidden() then rl/enable-cursor() else rl/disable-cursor()
if rl/is-mouse-button-pressed(:mouse-left)
if not collision.hit
;; The pixel under the pointer, as a line through the scene, tested
;; against the cube's bounds.
ray = rl/get-screen-to-world-ray(rl/get-mouse-position(), camera)
collision = rl/get-ray-collision-box(ray,
box-around(cube-position, cube-size))
;; A second click deselects. Only `hit` is cleared; the ray stays put
;; and keeps being drawn, as in the C.
else
collision.hit = false
;; Draw
rl/with-drawing:
rl/clear-background(rl/raywhite)
rl/with-mode-3d(camera):
if collision.hit
rl/draw-cube(cube-position, cube-size.x, cube-size.y, cube-size.z,
rl/red)
rl/draw-cube-wires(cube-position, cube-size.x, cube-size.y,
cube-size.z, rl/maroon)
;; A slightly larger wireframe around the selection, which is the
;; whole visual feedback of the example.
rl/draw-cube-wires(cube-position, cube-size.x + 0.2,
cube-size.y + 0.2, cube-size.z + 0.2, rl/green)
else
rl/draw-cube(cube-position, cube-size.x, cube-size.y, cube-size.z,
rl/gray)
rl/draw-cube-wires(cube-position, cube-size.x, cube-size.y,
cube-size.z, rl/darkgray)
;; raylib draws the ray a thousand units long, so it reads as a line
;; crossing the scene rather than as a segment with an end.
rl/draw-ray(ray, rl/maroon)
rl/draw-grid(10, 1.0)
rl/draw-text("Try clicking on the box with your mouse!", 240, 10, 20,
rl/darkgray)
if collision.hit
rl/draw-text("BOX SELECTED",
(screen-width - rl/measure-text("BOX SELECTED", 30)) / 2,
i32(f32(screen-height) * 0.1), 30, rl/green)
rl/draw-text("Right click mouse to toggle camera controls", 10, 430, 10,
rl/gray)
rl/draw-fps(10, 10)