Simple Ray Casting with ClojureScript
Ray casting is quite an old technique, that was heavily used in the early days of game development in a lot of games to create an illusion of 3D space. The most known example, and perhaps the first widely successful game that used this technique, was Wolfenstein 3D, made by ID Software in 1992. There were a lot of other games for all kinds of platforms, that used ray casting, for example, I remember playing the labyrinth game on Siemens C35 in around 2001:
There’s probably no reason to use ray casting for rendering pseudo-3D environments today, since this technique is very limited, and modern hardware is heavily optimized for working with triangles, which allows us to create very complex geometry that is impossible to model with ray casting. But I think that ray casting is a kinda cool idea, a simple concept, and definitively was a very smart move in the nineties game development. Additionally, ray casting has a lot more use cases than a simple rendering engine. Also, raycasting seems not to be a real word, but I’m going to refer to this technique like that further on, because it is convenient, and the engine itself can be referred to as raycaster.
For this project, I’ve chosen ClojureScript because I want to get into it, and into Clojure as well. Although my experience with Lisps is not very rich, the main usage right now is for writing functions in Emacs Lisp, but those are rather basic. I’m also going through SICP with GNU Guile and Racket, and though exercises in this book are sometimes brutal, this is not real programming experience. So the real goal of this post is rather not to implement a basic raycaster, but learn some Clojure. What, you thought we’re here for games? Just kidding, we’ll make a simple maze game too, which will be playable on this page.
But first, let’s understand what raycasting is, by going through some basic examples.
Ray casting basics
The general idea behind this technique is that we can imitate a 3D environment based only on 2D information. This is done by casting rays from the point, where the camera stands, and measuring the length of each ray until it hits any of the line segments, that represent walls on a 2D plane. I’ve prepared some pictures to illustrate this idea:
We can look at this as a top-down view of our scene. But how would we convert it to a first-person pseudo-3D view? The answer, as you might expect, is rays. Our camera will shoot a certain amount of rays in front of it, and we will see which rays have intersection points with our red and green walls. Let’s shoot 6 rays:
Now we can find intersections with our walls.
I’ve cut those rays, and calculated their lengths - this is the only information we need to make this 2D plane into a pseudo-3D scene.
Going from left to right, lengths are roughly 23.7
, 24.2
, inf
, 34.8
, 30.3
, 27.5
pixels each:
So, in order to make a 3D scene out of these lengths we, first, have to create an empty scene.
Let’s make it 256
by 224
pixels.
We then divide this scene’s width by amount of rays, to compute the width of each ray, and fill this scene with rectangles of this width, which gives us roughly 42.7
pixels wide rectangles:
This doesn’t look like 3D yet, and you can notice that there are only 5 out of the total 6 rectangles presented. This is due to the fact that one ray had infinity length, since it did not intersect with anything, so we don’t draw anything there. And to make it look 3D we need to change the height of each rectangle accordingly to the length of each ray. General rule – the shorter the ray, the bigger the height:
Now, this looks more like a 3D scene, isn’t it? But the walls don’t exactly look like walls, more like a set of rectangles. We can fix this by increasing our resolution, which we do by casting more rays. Also, let’s add background and remove rectangle outlines:
We can also change the field of view, by keeping the same amount of rays but changing the angle between those:
Which is more convincing, but still not as smooth as it could be. But, we can think about rays as our resolution, so if we would cast 256 rays, one ray for each pixel of image width, we would get something like this:
More advanced raycasting engines can produce various perspective corrections for such rectangles, making things appear at different heights. This is often used to imitate a camera shake while walking. Textures can be mapped to walls and the floor, and very basic lighting can be achieved by changing texture brightness depending on the distance. Even more advanced engines allow changing the vertical angle of the camera to imitate looking up and down. But we will implement pretty basic untextured raycasting, extending it further can be an interesting task, but I don’t feel like doing it with canvas. Maybe later, with WebGL.
Project structure
I’ll be using shadow-cljs to manage my project.
I’ve found this one very straightforward and simple to set up with Leiningen.
To create cljs
project via lein
type this in the command line (I’m using Linux, so) some details on project setup may vary for different operating systems:
$ lein new shadow-cljs raycasting
As a result, it should create the following file structure for us:
raycasting/
├── package.json
├── public
│ ├── css
│ │ └── style.css
│ └── index.html
├── README.md
├── shadow-cljs.edn
└── src
└── raycasting
└── core.cljs
Let’s look at shadow-cljs.edn
:
;; shadow-cljs configuration
{:source-paths ["src"]
:dependencies [[binaryage/devtools "0.9.7"]
[reagent "0.8.0-alpha2"]]
;; set a nrepl port for connection to a REPL.
:nrepl {:port 8777}
:builds {:app {:target :browser
:output-dir "public/js/compiled"
:asset-path "/js/compiled"
:modules {:main {:entries [raycasting.core]}}
:devtools
; before live-reloading, any code calls this function
{:before-load raycasting.core/stop
;; after live-reloading finishes call this function
:after-load raycasting.core/start
;; serve the public directory over http at port 8700
:http-root "public"
:http-port 8700
:preloads [devtools.preload]}}}}
Probably you won’t need to change much here, but I’ve removed reagent
dependency, because I don’t need it, so my shadow-cljs.edn
looks like this:
{:source-paths ["src"]
:dependencies [[binaryage/devtools "0.9.7"]]
:nrepl {:port 8777}
:builds
{:app {:target :browser
:output-dir "public/js/compiled"
:asset-path "/js/compiled"
:compiler-options {:optimizations :advanced}
:modules {:main
{:entries [raycasting.core]}}
:devtools {:http-root "public"
:http-port 8700
:preloads [devtools.preload]}}}}
I’ve also added :compiler-options {:optimizations :advanced}
to produce smaller js
file, and removed :before-load
and :after-load
functions, because I don’t think we need those, and script initialization on page load is sufficient enough.
One last thing - we need a test page to see the results of our code, so let’s create public/index.html
file, where we will add our canvas, and other elements as we go.
With the project setup and ready we now can begin working on ray casting by creating a camera.
Creating a camera
Since we’re going to cast rays out from the camera, we need a camera in the first place.
Camera will be represented as a single point on the plane with x
and y
coordinates, and degree
parameter.
Let’s create a file src/raycasting/camera.cljs
and define camera by using atom
so we could later update it:
(ns raycasting.camera)
(def camera (atom {:x 0.0 :y 0.0 :degree 0.0}))
Our camera
is a hash map with three key-value pairs for each of its parameters.
We can later access these parameters by calling keys on this map like this:
raycasting.camera> (:degree @camera)
0.0
This is quite handy, and we can update our map with assoc
procedure:
raycasting.camera> (assoc @camera :degree 45.0)
{:x 0.0 :y 0.0 :degree 45.0}
Or use merge-with
procedure to change the value currently stored in the map, by using some procedure, which is +
in our case:
raycasting.camera> (merge-with + @camera {:degree 90.0})
{:x 0.0 :y 0.0 :degree 90.0}
You can notice that while I’ve used assoc
to change my :degree
to 45.0
and then used merge-with
with +
to add 90.0
degrees to current :degree
, I’ve still got 90.0
as the result, and not the expected 135.0
.
This is because camera
is a value, and can’t really change, and instead we’re producing a new value based on old one.
And since our old value never changed, it still had 0.0
as :degree
.
To understand how this works we need to understand what is considered a value and why.
You can skip explanations of this if you want, since it mostly has nothing to do with raycasting itself, but this still may explain certain things that we will see later in the code.
In Clojure, we mostly work with values, and there are no variables as presented in other languages. To make it more clear, let’s talk about what is considered a value. For example, in Rust
42
is a value, that we can assign to a variable like this:let mut a = 42;
We’ve created a mutable variable
a
, and put42
into it. We can later mutatea
to hold another value:a -= 15;
The key here is mutate - we’re not magically transforming
42
itself into27
, we’re changing the contents ofa
. Soa
is like a box that contains a value. You can’t really do anything to42
itself – it is and always will be42
, that’s why it is a value. So when we dida -= 15
, we’ve produced a new value based on the old one, and put it intoa
. So values never change, only variables that hold these values change what they are holding.As an example of something that is not a value in Rust, we can look at the Vector collection:
let mut b = vec![1, 2, 3];
This is a dynamic array, that holds three elements. And we can actually change this array by using different methods that it provides, like
push
:b.push(4); // now collection holds these four elements [1, 2, 3, 4]
After this
push
, whenever we refer tob
it will hold four elements. We’ve declaredb
asmut
, but we’re not mutatingb
itself, but the collection thatb
holds - and because of that this is not a value.mut
keyword only specifies that we can mutate collection. But even if we were to drop themut
keyword in both examples, e.g.let a = 42
,let b = vec![1, 2, 3]
, we would not be able to mutate both of these, but it still would not turn[1, 2, 3]
into a value, although we can treat it as a value, underlying semantics of a collection has not changed.(Semi-)real-world example - I give you an apple, and ask you to give me back half of it. You slice the apple and give me back one half. This apple is not a value, because you’ve literally changed it by slicing, and now the original apple is no longer available for both of us. You now hold half of the apple, and I hold another half. This is what happens when you modify a vector or any other collection.
Then I give you an apple made out of a material that once cast, can’t be destroyed, and ask you to give me half of it. You can’t slice it, or do anything to it at all, so you take very precise measures of this apple, cast a new piece out of the same material, and give it back to me. We now have the original apple and an apple slice. Then you can throw this apple in the garbage because you can’t really do anything with it. This is what essentially happens when you do math with numbers - you can’t change
42
, you can take the measure (15
) and produce27
by subtraction. And this is what makes such an apple to be a value.So in Clojure, we work with such type of data, and all Clojure collections are immutable. But the other major aspect of Clojure’s collections is persistence. The apple I’ve described isn’t really persistent, because once you’ve made a new slice, nothing really tells me what its past state was - was it a whole apple or 3/4 of an apple? So in order to make things persistent, we must provide additional information with apple, like a serial number for example. When you cast a new slice, you create a new serial number, that contains information about the old serial number. This way we can look at our piece, and determine what apple was used as a reference.
Now, whenever we create new copies, we encode the previous serial number into the current serial number, and from any point, we are able to trace back to the original state of the apple.
You can argue, that this can be done with an ordinary apple just as with an indestructible one, but the difference is, that an ordinary apple can be changed. Imagine that you’ve made a new apple based on it, added a serial number, that encodes that it was derived from the original apple, and then someone bit off a piece of the original apple. Then any holder of your copy of apple will try to refer to the original one and will find out not what they expected. Or maybe what they expected, since the apple could have been bitten before and you’ve restored the original form of an apple when made it. There’s no way to tell and preserve correct history. So even though we can enforce mutation restrictions in Rust, this does not make the vector into a value.
This is kinda what Clojure does with its collections. But instead of using identifiers, Clojure uses path copying for sharing structure between states. So newer state refers to some point in the old state. There are many different ways to maintain persistence, which are nicely described on this Wikipedia page.
A very simple example of persistent collection we can refer to is a single linked list, that grows at the front. Let’s start with a list of these three items
(1 2 3)
. This can be represented as boxes that hold values, and have pointers to what should be the next value:So
1
refers to2
,2
refers to3
, and3
refers to nothing, which indicates end of the list. Now we add another element to it at the front, which produces a new list(0 1 2 3)
:You can see that all we did is created a box to hold
0
and told it to point to a box that holds1
. The rest thing is our old list, marked with green boxes, and we did not copy anything from it. And old list did not change at all, only a new node was added to it at the front, but the old list knows nothing about it. So if we stored our original list somewhere, and added0
to it, our original list was not changed.Furthermore, if we check the identity of our new list without its first element, it will be the same as in the original list:
user=> (identical? '(1 2 3) '(1 2 3)) false user=> (def a '(1 2 3)) user=> (identical? a (rest (conj a 0))) true
First
identical?
test gives usfalse
because these are two different objects, even though their values are the same. The secondidentical?
test gives ustrue
, because, as I’ve said, by adding a new value to the list, and then removing it, we get back the original list.And all of the collections in Clojure work this way, with a promise of fast change and low memory cost. Although such collections are not as fast as mutable non-persistence collections, they are much more reliable, especially when it comes to concurrent programming, and reliability is much more important than speed most of the time.
But in our raycasting we will have to really change camera position, so how do we do it if collections can’t be changed, and we’re using a hash map to hold camera coordinates and angle?
Moving the camera
When we’ve created camera
we’ve used the atom
procedure to produce a reference type that can be later atomically swapped to hold another value, and the swapping mechanism is concurrently safe.
So, in order to update our map, all we need to do is to call swap!
procedure, and pass our atom and a procedure that will update the value:
raycasting.camera> (swap! camera (fn [c] (assoc c :degree 45.0)))
{:x 0.0 :y 0.0 :degree 45.0}
Now, when we dereference camera
with @
, we can actually see the updated value:
raycasting.camera> @camera
{:x 0.0 :y 0.0 :degree 45.0}
Using swap with anonymous procedures is kinda tedious, so instead, let’s create a set of procedures that we will use to move the camera around:
(def radian (/ Math/PI 180.0)) ➊
(defn move-point [[x y] degree amount] ➋
(let [angle (* degree radian)]
[(+ x (m/three-decimal (* (Math/sin angle) amount)))
(+ y (m/three-decimal (* (Math/cos angle) amount)))]))
(defn move-forward
"Move `camera` forward given `amount` respecting the `degree`."
([camera] (move-forward camera 1))
([camera amount]
(let [{x :x
y :y
degree :degree} camera ➌
[x y] (move-point [x y] degree amount)]
(assoc camera :x x :y y))))
This procedure will move our camera forward relatively to where the camera is pointed at.
We compute ➊ only once, to later convert degrees to radians by multiplication.
The move-point
➋ procedure will later be used in several places of our program, so it is good to have it as a separate procedure, while move-forward
is used exclusively for camera movement.
And inside we have weird construction ➌ which is called Associative Destructuring.
We essentially bind x
, y
, and degree
within our let
block, to the values contained in camera
.
Now let’s try to move our camera around:
raycasting.core> (move-forward (assoc @camera :degree 0.0) 10)
{:x 0, :y 10, :degree 0}
raycasting.core> (move-forward (assoc @camera :degree 90.0) 10)
{:x 10, :y 6.123233995736766e-16, :degree 90}
Uh oh.
Moving forward with 0 degrees was fine, but when we’ve turned 90 degree, we’ve got 6.123233995736766e-16
as our y
coordinate.
This value is very close to 0
, but we don’t really need such precision, and there are some problems with such small values in JavaScript.
Let’s write a macro, that will truncate it to three decimal places, which would be more than enough for our needs:
(defmacro three-decimal
"Truncate `double` to three decimal places."
[num]
`(/ (int (* 1000 ~num)) 1000.0))
(defn move-forward
"Move `camera` forward given `amount` respecting the `degree`."
([camera] (move-forward camera 1))
([camera amount]
(let [rad (* (:degree camera) (/ Math/PI 180.0))]
(merge-with + camera {:x (three-decimal (* (Math/sin rad) amount))
:y (three-decimal (* (Math/cos rad) amount))}))))
This will not work though.
The problem is, that we can’t just define a macro in the same file that we’ve declared procedure.
This is a difference between Clojure and ClojureScript, so we have to create new file src/raycasting/macros.cljc
, and use :require-macros
in our namespace definition for camera.cljs:
(ns raycasting.camera
(:require-macros [raycasting.macros :as m]))
Now we can update move-forward
to refer to macro:
(defn move-forward
"Move `camera` forward given `amount` respecting the `degree`."
([camera] (move-forward camera 1))
([camera amount]
(let [rad (* (:degree camera) (/ Math/PI 180.0))]
(merge-with + camera {:x (m/three-decimal (* (Math/sin rad) amount))
:y (m/three-decimal (* (Math/cos rad) amount))}))))
Let’s try it:
raycasting.core> (move-forward (assoc @camera :degree 0.0) 10)
{:x 0, :y 10, :degree 0}
raycasting.core> (move-forward (assoc @camera :degree 90.0) 10)
{:x 10, :y 0, :degree 90}
Nice. Now, we can do the same thing to rotate the camera:
(defn rotate
"Rotate `camera` by a `degree`."
([camera] (rotate camera 0.5))
([camera degree]
(assoc camera :degree (mod (+ (:degree camera) degree) 360.0))))
And if we call this:
raycasting.core> (rotate @camera 420)
{:x 0.0, :y 0.0, :degree 60.0}
The rotate procedure ensures that our :degree
stays inside 360 degrees, so when we specify a turn that is more than 360 degrees, we wrap around.
These are two main procedures we will later use.
Drawing
Now we can draw our camera on <canvas>
.
Let’s add this to our index.html
:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<script src="js/compiled/main.js"></script>
</head>
<body>
<canvas id="raycaster" width="512" height="224"></canvas>
<script>
window.onload = function() {
raycasting.core.init();
}
</script>
</body>
</html>
We’ve added our <canvas>
and gave it an id
.
We will later be able to refer to this canvas via this id
, so let’s create two vars to hold the canvas object, and its context:
(ns raycasting.core
(:require [raycasting.camera :as cam]))
(defonce *canvas* nil)
(defonce *ctx* nil)
(defn ^:export init []
(when-let [canvas (. js/document getElementById "raycaster")]
(set! *canvas* canvas)
(when (. *canvas* -getContext)
(set! *ctx* (. *canvas* getContext "2d")))))
We use defonce
here, so when we save file, and shadow-cljs hot reloads the code, we will not redefine our *canvas*
and *ctx*
to nil
.
And init
procedure will run on onload
event, and set our *canvas*
and *ctx*
.
Let’s create draw-camera
procedure, that will take current camera
state, and draw it to canvas:
(defn draw-line
([[x1 y1] [x2 y2]]
(draw-line [x1 y1] [x2 y2] "#000000"))
([[x1 y1] [x2 y2] color]
(set! (. ctx -strokeStyle) color)
(doto ctx
(.beginPath)
(.moveTo x1 y1)
(.lineTo x2 y2)
(.stroke))))
(defn draw-camera [camera]
(let [[x y :as pos] [(:x camera) (:y camera)]
pointer (cam/move-point pos (:degree camera) 7)
width 5]
(draw-line pos pointer "#000000")
(set! (. ctx -fillStyle) "#000000")
(. ctx fillRect (- x (/ width 2)) (- y (/ width 2)) width width)))
Now, if we write this to the REPL:
raycasting.core> (draw-camera @cam/camera)
nil
We should see our camera:
Now let’s add some walls to look at.
Creating walls
I need to mention, that many raycasting engines define stages in terms of squares on the grid. This is useful because we can check intersections a lot faster since we only need to perform checks along the ray in fixed points of space. While this is quite fast, I want arbitrarily placed walls in my raycaster. Because of this, our algorithm will not be as optimal, but we will be able to handle more complex wall placement.
Let’s create new file src/raycasting/stage.cljs
.
In this file, we will define our stages.
Each stage is a vector, with line segments, that have three components - beginning coordinate, endpoint, and color.
Let’s define stage, as on our example picture.
(ns raycasting.stage)
(def example-stage
[ ➊
[ ➋
[23.468 126.290] ➌
[106.189 87.527] ➍
"#89a02c"] ➎
[[124.947 40.268] [166.507 95.415] "#916f6f"] ➏])
So what we’re doing here is, we begin by creating a vector ➊. This vector will hold all lines that our stage uses. We define another vector ➋, that will hold beginning point ➌, ending point ➍, and wall color ➎. Lastly, we specify another line ➏, and this process can continue on and on. Bigger stages will require a lot of walls, and this can be a performance hit, but we’re not aiming for the best efficiency.
Now we can implement wall drawing with our draw-line
procedure, that we’ve already used in draw-camera
procedure.
Let’s create draw-stage
procedure, that will walk through our vector of walls and draw each on the canvas:
(defn draw-stage [stage]
(doseq [line stage]
(apply draw-line line)))
doseq
is a great way to iterate on a collection for producing side effects, such as drawing on the screen.
Let’s move our camera to the position 119.5,180
, and rotate it to 180 so it is placed as on our example.
Now if we call this procedure, we should see our stage:
raycasting.core (swap! cam/camera #(assoc % :x 119.5 :y 180 :degree 180))
{:x 119.5 :y 180 :degree 180}
raycasting.core> (draw-camera cam/camera)
nil
raycasting.core> (draw-stage stage/example-stage)
nil
While we are at this, notice how awkward we’ve been setting our position for camera
:
(swap! cam/camera #(➊ assoc ➋ % ➌ :x 119.5 :y 180 :degree 180))
➊ #()
is a shorthand for creating an anonymous procedure.
This procedure will call assoc
➋, and %
➌ will be our cam/camera
.
A more elaborate way to write the same code would be (fn [camera] (assoc camera …))
.
We could simplify this by using reset!
:
(reset! cam/camera {:x 119.5 :y 180 :degree 180})
Which is much simpler, but still very error-prone, because we’ll have to remember what keys should be in the map.
This is not an issue for three key-value pair maps, but for huge maps, it will be better to create some kind of initialization procedure.
Let’s create one in raycasting.camera
namespace:
(defn set-position
"Set position of the `camera` to a certain `x`, `y`, and `degree`.")
➊ ([camera] (set-position camera 0 0 0))
➋ ([camera x y] (set-position camera x y 0))
➌ ([camera x y degree] (assoc camera :x x :y y :degree (mod degree 360.0)))
This is called Multi-arity function, we’ve been using it before, but I’ve never explained what it is.
In Clojure and ClojureScript it is possible to define procedure overloading based on the number of given arguments.
In this example we create three possible ways to call our procedure: ➊ - we only pass our camera
parameter, and it resets its position to 0,0
coordinates, and set’s 0
degree angle, ➋ - we specify coordinates, but we don’t care about our angle, so we reset it, and ➌ - we provide all needed parameters, and set those accordingly with assoc
.
The beauty of multi-arity procedures is that it can call itself with another amount of arguments, so in this case, there’s only one real body, that does the work (➌), and others (➊, ➋) are only calling it with all needed parameters.
With this procedure we can quickly set the desired position for our camera:
raycasting.core> (swap! cam/camera cam/set-position) ;; reset
{:x 0 :y 0 :degree 0}
raycasting.core> (swap! cam/camera cam/set-position 10 20) ;; set coordinates
{:x 10 :y 20 :degree 0}
raycasting.core> (swap! cam/camera cam/set-position 10 20 90) ;; fully specify position
{:x 10 :y 20 :degree 90}
We can use this later when we initialize our stages in the game.
Casting rays
Now, when we have our camera and walls, we can finally cast some rays. Although, we’re not casting rays, but really long line segments because our algorithm will check for intersections between line segments of stage walls, and our rays.
First, we need some linear algebra. In particular, we need to check if two line segments intersect or not. This is done by three procedures. First one checks if a point lies on the line segment:
(defn on-segment?
"Checks if point `[ax ay]` lies on segment defined by two points `[b
c]`, where each point is defined as a vector `[x y]`."
[[ax ay] [[bx by] [cx cy]]]
(and (<= ax (max bx cx))
(>= ax (min bx cx))
(<= ay (max by cy))
(<= ay (min by cy))))
This procedure accepts a point, and a line segment defined as two points and checks whenever the point lies on that line segment. We will need this procedure because when we will check for intersections, we need to ensure, that we’re staying inside line segments.
Next bit is orientation
procedure:
(defn orientation [[ax ay] [bx by] [cx cy]]
(let [orientation (- (* (- by ay) (- cx bx))
(* (- bx ax) (- cy by)))]
(cond (> orientation 0) :cw
(< orientation 0) :cc
:else :colinear)))
This procedure checks the relationship of point a
, and points b
and c
.
These relationships can be :cw
(for clockwise), :cc
(for counter clockwise), and :colinear
:
Now we can check for the intersection itself:
(defn intersect?
"Checks if two line segments intersect. Line segments are defined as
vector of two points `[a1 a2]`, and each point is a vector of two
coordinates `[x y]`."
[[a1 a2] [b1 b2]]
(let [o1 (orientation a1 a2 b1)
o2 (orientation a1 a2 b2)
o3 (orientation b1 b2 a1)
o4 (orientation b1 b2 a2)]
(or
(and (not (= o1 o2))
(not (= o3 o4)))
(and (= o1 :colinear)
(on-segment? b1 [a1 a2]))
(and (= o2 :colinear)
(on-segment? b2 [a1 a2]))
(and (= o3 :colinear)
(on-segment? a1 [b1 b2]))
(and (= o4 :colinear)
(on-segment? a2 [b1 b2])))))
To find the point of the intersection itself, we need to calculate these two formulas:
Let’s write those as intersection
procedure:
(defn intersection [[[x1 y1] [x2 y2] :as ray] [[x3 y3] [x4 y4] :as wall]]
(let [x (/ (- (* (- (* x1 y2) (* y1 x2)) (- x3 x4)) (* (- x1 x2) (- (* x3 y4) (* y3 x4))))
(- (* (- x1 x2) (- y3 y4)) (* (- y1 y2) (- x3 x4))))
y (/ (- (* (- (* x1 y2) (* y1 x2)) (- y3 y4)) (* (- y1 y2) (- (* x3 y4) (* y3 x4))))
(- (* (- x1 x2) (- y3 y4)) (* (- y1 y2) (- x3 x4))))]
[x y]))
I’ve added :as ray
and :as wall
to make it more clear what these points are related to.
The order doesn’t really matter, since we’re computing a point, but I think it gives a slight idea of what this procedure expects.
As a final piece, we need a procedure, that will find all intersections for our ray, and return those in some form, that will be usable for us later.
I’ve created this procedure, that takes our stage, and two points as ray
:
(defn find-intersections [stage [origin end :as ray]]
➊ (->> stage
(map (fn [wall]
(if (intersect? ray wall)
(let [new (intersection ray wall)]
[(distance origin new) new])
[10000000 end])))
(into {})))
Here we’re using thread last macro ➊, which essentially takes the result of our expression, and puts it in the last position in the next expression.
We then map with an anonymous procedure, that takes wall
, from our stage
, and checks if it intersects?
with our ray
.
If it does, we calculate new
intersection point, and return a vector of the distance between ray origin and intersection point, and this new point.
When map finishes, it produces this kind of list ([10 [0 15]] [11 [4.2 22.8]] … [length [x y]])
.
We then convert it into a hash map, where a length will be our key, and a set of coordinates will be our value.
The distance procedure is defined as follows:
(defn distance [[x1 y1] [x2 y2]]
(m/three-decimal
(Math/sqrt (+ (Math/pow (- x1 x2) 2)
(Math/pow (- y1 y2) 2)))))
Now we finally can cast our rays. First, let’s cast a set of rays and draw those without truncation at the intersection point:
(defn cast-rays [camera]
(let [ray-count 16
step (/ fov ray-count)
ray-start [(:x camera) (:y camera)]]
(loop [n (dec ray-count)
degree (:degree (cam/rotate camera (- (/ fov 2.0))))]
(when (>= n 0)
(let [ray-end (cam/move-point ray-start degree 5000)]
(draw-line ray-start ray-end "#ffe680")
(recur (dec n)
(+ degree step)))))))
Now if we call our procedures for drawing rays, camera, and stage like this:
raycasting.core> (draw-stage stages/example-stage)
nil
raycasting.core> (cast-rays @cam/camera)
nil
raycasting.core> (draw-camera @cam/camera)
nil
We should see this result:
Now let’s change this procedure, so it will find ray ends before drawing:
(defn cast-rays [camera stage]
(let [step (/ fov rayCount)
ray-start [(:x camera) (:y camera)]]
(loop [n (dec rayCount)
degree (:degree (cam/rotate camera (- (/ fov 2.0))))]
(when (>= n 0)
(let [ray-end (cam/move-point ray-start degree 5000)]
➊ intersections (find-intersections stage [ray-start ray-end])
➋ shortest-ray (apply min (keys intersections))
➌ ray-end (intersections shortest-ray)
(draw-line ray-start ray-end "#ffe680")
(recur (dec n)
(+ degree step)))))))
First, we compute a map of intersections ➊, then find the shortest intersection length from the map by applying the min
procedure to the list of keys in the map ➋, and lastly get the closest endpoint for given ray ➌.
This way we can find all intersections of the given ray with all walls, but we will draw only the shortest one, like this:
To illustrate this a bit better, let’s create simple animation by making our camera rotate:
Now we can use the cast-rays
procedure to make our pseudo-3D image.
Making a 3D view of our scene
We’ll need to modify our cast-rays
procedure, so it would return a list of rays instead of drawing it:
➊ (defn angle-between [[[x1 y1] [x2 y2]] [[x3 y3] [x4 y4]]]
(let [theta1 (Math/atan2 (- y1 y2) (- x1 x2))
theta2 (Math/atan2 (- y3 y4) (- x3 x4))
diff (Math/abs (- theta1 theta2))]
(min diff (Math/abs (- 180 diff)))))
(defn cast-rays [camera stage]
(let [step (/ fov rayCount)
ray-start [(:x camera) (:y camera)]]
➋ central-ray [ray-start (cam/move-point ray-start (:degree camera) 100)]
(loop [n (dec rayCount)
degree (:degree (cam/rotate camera (- (/ fov 2.0))))
rays ()]
(if (>= n 0)
(let [ray-end (cam/move-point ray-start degree 5000)
intersections (find-intersections stage [ray-start ray-end])
shortest-ray (apply min (keys intersections))
ray-end (intersections shortest-ray)]
➌ ray-angle (angle-between [ray-start ray-end] central-ray)
(recur (dec n)
(+ degree step)
➍ (conj rays [ray-end shortest-ray ray-angle n]))))
➎ rays)))
(defn draw-rays [camera rays]
(let [pos [(:x camera) (:y camera)]]
(doseq [[ray] rays]
(draw-line pos ray "#ffe680"))))
First, we’ve defined an angle-between
➊ procedure, which takes two line segments and computes the angle between those in radians.
We then added one extra computation before we cast rays - we compute central-ray
➋, a direction where the camera is looking.
Then, we compute the angle between the current ray we’re casting and the central ray ➌.
After that we’re conj
ing ➍ vector with endpoint, shortest length, angle, and ray number to a list of rays.
Finally, we return a list of rays ➎ to the caller.
draw-rays
will later use these rays afterward and draw each one ray by ray.
Now, whenever we call this procedure, we get all the useful information that we will later need to produce an image. First, let’s create another procedure, that will render all our elements on screen in a sequence:
(defn render []
(let [height (. *canvas* -height)
width (. *canvas* -width)
camera @cam/camera
rays (cast-rays camera stage)]
(. *ctx* clearRect 0 0 width height)
(draw-rays rays)
(draw-camera camera)
(draw-stage stage)))
➊ (. js/window requestAnimationFrame render)
This is a skeleton, not a fully realized procedure. Note that at ➊, we request another frame to be drawn on our canvas. So this is more like a game loop, though not quite. We will use it as our game loop though.
So, what we do here, is we create a set of bindings on each frame, and we dereference our cam/camera
to get its position and angle, and then cast rays from the camera on our stage.
Later we clear canvas and call draw-rays
, draw-camera
, and draw-stage
.
Calling this procedure should initiate an animation sequence, so if we add (swap! cam/camera cam/rotate 1)
, at some point we will have an animation of the rotating camera.
Now let’s create a 3D view:
(defn draw-3d-stage [rays]
(let [ray-width (/ (/ (. canvas -width) 2) rayCount)
height (. canvas -height)
canvas-width (/ (. canvas -width) 2)])
➊ (set! (. ctx -fillStyle) "#333")
(. ctx fillRect canvas-width 0 canvas-width (/ height 2))
(set! (. ctx -fillStyle) "#666")
(. ctx fillRect canvas-width (/ height 2) canvas-width (/ height 2))
➋ (loop [rays rays]
(when (not (empty? rays))
(let [[_ distance ray-degree n] (first rays)]
➌ wall-height (- height distance)
wall-height (if (< wall-height 0)
0
wall-height)
➍ wall-color wall-height
(set! (. ctx -fillStyle) (str "rgb("wall-color","wall-color","wall-color")")))
➎ (. ctx
fillRect
(+ canvas-width (* n ray-width))
(- (/ height 2) (/ wall-height 2))
(+ ray-width 1) ;; extra pixel for border
wall-height)
(recur (rest rays)))))
This procedure first fills background ➊, then, we loop
through each ray, we calculate wall height by simply subtracting canvas height and distance to the wall ➌, and since our canvas is 256 pixels high, we use this value as our color as well ➍.
Not really a pretty solution, but will do it for now.
Then we fill a rectangle of this height, and width, with an offset of half of the canvas width plus the number of the ray, times its width.
Calling this procedure in our render
procedure gives us such a view:
Sure this gives us the desired 3d view, but something is not right. When we look at our walls we can see that we get some kind of fish eye effect, which we can see in its full form if we look directly at the wall:
There’s a good reason for this, which we can fix, although it requires a few additional calculations.
Fisheye compensation
The reason for this effect is the method we’re using to calculate wall heights. Here’s how we currently view our would:
This curve is our screen, so if we were to display this on a non-flat screen with the opposite curve, our image would look natural. However our screens are flat, so we need to compensate for this. In order to do this, we need to compute distances while taking into account the number of degrees between camera direction and the ray we currently casting.
First, let’s declare angle-between
procedure, that will return the angle between two line segments, one of which will be the direction where the camera is facing, and the other one is the ray we currently casting:
(defn angle-between [[[x1 y1] [x2 y2]] [[x3 y3] [x4 y4]]]
(let [theta1 (Math/atan2 (- y1 y2) (- x1 x2))
theta2 (Math/atan2 (- y3 y4) (- x3 x4))
diff (Math/abs (- theta1 theta2))]
(min diff (Math/abs (- 180 diff)))))
Now we can use it in our cast-rays
procedure:
(defn cast-rays [camera stage]
(let [step (/ *fov* *ray-count*)
ray-start [(:x camera) (:y camera)]
central-ray [ray-start (cam/move-point ray-start (:degree camera) 100)]]
(loop [n (dec *ray-count*)
degree (:degree (cam/rotate camera (- (/ *fov* 2.0))))
rays ()]
(if (>= n 0)
(do (let [ray-end (cam/move-point ray-start degree 5000)
intersections (find-intersections stage [ray-start ray-end])
shortest-ray (apply min (keys intersections))
ray-end (intersections shortest-ray)]
➊ ray-angle (if *compensate-fisheye*
(angle-between [ray-start ray-end] central-ray)
0)
(recur (dec n)
(+ degree step)
➋ (conj rays [ray-end shortest-ray ray-angle n]))))
rays))))
We store calculated angle ➊, and then add it to our ray and conj
it to our rays
collection.
This vector already is quite big, so it would be better to use some associative collection instead.
Let’s change it:
(conj rays {:end ray-end :length shortest-ray :angle ray-angle :n n})
Now, that cast-rays
returns a list of hash maps, we need to update our other procedures that expected a list of vectors.
I’ll omit these steps, as it ain’t much different from what I’ve previously described, and this post is already way longer than I’ve thought, but using hash maps will prove more usable when we will need to incorporate other information into our ray, such as color.
Now we can use angles in draw-3d-scene
procedure.
We’ll change our distance
computation to use Math/cos
procedure, to compute corrected distance:
distance (* distance (m/three-decimal (Math/cos ray-degree)))
This should give us the such result:
This looks correct when we face the wall at a 90-degree angle. But if we’re aligned with the wall, we get this:
Which is not right. Wall is curved for some reason.
Note: To be completely honest, up to this point I did everything without using existing raycasting tutorials. But when I applied a seemingly correct fisheye compensation, and it has gone away only partially, I got stuck for a while. So in order to find a correct way to compensate for this fisheye effect, I’ve read this one, but for some reason the formula there didn’t work for me, so I’ve altered it.
So it seems that we’re missing the projection plane. Let’s create one:
(defn projection-distance [width]
(m/three-decimal (/ (/ *ray-count* 2) (Math/atan (* (/ *fov* 2) cam/radian)))))
It is better to compute this once, but since we can change the ray count, we have to recompute this.
With this distance we can add computation in our wall-height
computation process:
(defn draw-3d-stage-example9 [rays]
(let [ray-width (/ (/ (. *canvas* -width) 2) *ray-count*)
height (. *canvas* -height)
canvas-width (/ (. *canvas* -width) 2)
projection-distance (projection-distance canvas-width)]
(set! (. *ctx* -fillStyle) "#333")
(. *ctx* fillRect canvas-width 0 canvas-width (/ height 2))
(set! (. *ctx* -fillStyle) "#999")
(. *ctx* fillRect canvas-width (/ height 2) canvas-width (/ height 2))
(loop [rays (sort (fn [{l1 :length} {l2 :length}] (> l1 l2)) rays)]
(when-let [ray (first rays)]
(let [{distance :length ray-degree :angle n :n} ray
angle-step (/ *fov* *ray-count*)
distance (if *compensate-fisheye*
(* distance (m/three-decimal (Math/cos ray-degree)))
distance)
wall-height (* (/ (/ height *ray-count*) distance) projection-distance (/ *fov* 4))
wall-height (if (> wall-height height)
height
wall-height)
wall-color (+ wall-height 10)]
(set! (. *ctx* -fillStyle) (str "rgb(" wall-color ", " wall-color "," wall-color ")"))
(. *ctx*
fillRect
(+ canvas-width (* n ray-width))
(- (/ height 2) (/ wall-height 2))
(+ ray-width 1) ;; extra pixel for border
wall-height))
(recur (rest rays))))))
This looks much better in both examples:
Although walls now appear further. I’ve tried to make up constants that will play nice to make the wall occupy the whole height when we near the wall, so I’ll keep it this way. I’m sure I have some misunderstanding of the algorithm, but it works, so I’m ok with that.
Handling input and focus
Now, when we have our camera working, we can make it move by pressing arrow keys. We already have needed procedures for handling camera position and rotation, so the only thing we really need is input handlers, and a way to focus our canvas, so pressing arrows would not scroll the page.
There are some libraries for handling keyboard input, but I’m a purist, so we’ll implement this without dependencies.
All we need is two procedures, that check if a key is pressed, and that it is released.
Let’s create raycasting/input.cljs
:
(ns raycasting.input)
(defonce key-states (atom {}))
(defonce focus false)
We start by defining another atom for our key states.
It will hold a map of keys and their states, like this {"Up" false, "Down" true}
, which means that currently the Down key is pressed down.
Another one is focus
which we will use to determine whether should we handle input or not.
Let’s add these three procedures:
(defn valid-key? [key]
(or (= key "Escape")
(= key "Shift")
(= key "ArrowLeft")
(= key "ArrowUp")
(= key "ArrowRight")
(= key "ArrowDown")))
(defn on-key-press [event]
(when focus
(let [event (if event event (. js/window -event))
key (. event -key)]
(when (valid-key? key)
(. event preventDefault)
(swap! key-states assoc key true)))))
(defn on-key-release [event]
(when focus
(let [event (if event event (. js/window -event))
key (. event -key)]
(when (valid-key? key)
(swap! key-states assoc key false)
(. event preventDefault)))))
We’ll get our keys as strings, and check if this is one of the keys we need.
If it is, and we’re focused, we swap!
our key
state with either true
or false
, so our key-states
atom always holds most recent state.
Now we need to assign these procedures to events:
(set! (. js/window -onkeyup) on-key-release)
(set! (. js/window -onkeydown) on-key-press)
We also have these procedures to handle focus:
(defn on-click [event]
(set! focus true))
(defn release-focus []
(set! focus false))
But we set these in the initialization code:
(defn ^:export init []
(set! *canvas* (. js/document getElementById "raycaster"))
(when *canvas*
(. *canvas* addEventListener "mousedown" input/on-click)
(swap! cam/camera cam/set-position 10 10 45)
(when (. *canvas* -getContext)
(set! *ctx* (. *canvas* getContext "2d"))
(render))))
The final step would be to poll our keys on each frame in our render
procedure.
For this, we create another procedure, but in raycasting.core
this time:
(defn move-camera [stage key-states]
(let [{x :x y :y :as camera} @cam/camera]
(when (key-states "Escape")
(input/release-focus)
(reset! input/key-states {}))
(when (key-states "ArrowRight")
(if (key-states "Shift")
(let [{x' :x y' :y} (cam/strafe camera -1.5)]
(when (not (detect-collision stage [[x y] [x' y']]))
(swap! cam/camera cam/strafe -1)))
(swap! cam/camera cam/rotate -1.5)))
(when (key-states "ArrowLeft")
(if (key-states "Shift")
(let [{x' :x y' :y} (cam/strafe camera 1.5)]
(when (not (detect-collision stage [[x y] [x' y']]))
(swap! cam/camera cam/strafe 1)))
(swap! cam/camera cam/rotate 1.5)))
(when (key-states "ArrowUp")
(let [{x' :x y' :y} (cam/move-forward camera 1.5)]
(when (not (detect-collision stage [[x y] [x' y']]))
(swap! cam/camera cam/move-forward 1.5))))
(when (key-states "ArrowDown")
(let [{x' :x y' :y} (cam/move-forward camera -1.5)]
(when (not (detect-collision stage [[x y] [x' y']]))
(swap! cam/camera cam/move-forward -1.5))))))
We then, call this procedure each frame as early as possible.
You can notice that we’re calling another procedure detect-collision
, it is much like our intersection finding stuff, but it calculates the end position of our camera, and checks if the path intersects with any of our walls.
Not very optimal, but works well.
The last thing I want to add is a way to control the amount of rays, and field of view, as well as a fisheye compensation toggle.
For that we’ll need these <input>
tags:
<div>
<input type="range" id="rayCountSlider"
min="10" max="256" step="1"></input>
<label for="rayCountSlider">Ray Count: </label>
<output name="rayCountValue" id="rayCountOutput"></output>
</div>
<div>
<input type="range" id="fovSlider"
min="10" max="360" step="1"></input>
<label for="fovSlider">Field of View: </label>
<output name="fovSliderValue" id="fovSliderOutput"></output>
</div>
<div>
<input type="checkbox" id="fishEyeCompensation"></input>
<label for="fishEyeCompensation"> FishEye compensation</label><br>
</div>
And we need a way to control those. For this, let’s add these procedures:
(def max-fov 360)
(def max-compensated-fov 180)
(defn update-ray-count [event]
(let [value (.. event -target -value)]
(set! *ray-count* (js/parseInt value))
(when-let [output (. js/document getElementById "rayCountOutput")]
(set! (. output -innerHTML) value))))
(defn update-fov [event]
(let [value (.. event -target -value)]
(set! *fov* (js/parseInt value))
(when-let [output (. js/document getElementById "fovSliderOutput")]
(set! (. output -innerHTML) value))))
(defn update-fish-eye-compensation [event]
(let [value (.. event -target -checked)]
(set! *compensate-fisheye* value)
(when-let [fov-slider (. js/document getElementById "fovSlider")]
(set! (. fov-slider -max) (if value max-compensated-fov max-fov))
(when (and value
(> *fov* max-compensated-fov))
(set! *fov* max-compensated-fov)
(set! (. fov-slider -value) max-compensated-fov)
(when-let [fov-output (. js/document getElementById "fovSliderOutput")]
(set! (. fov-output -innerHTML) *fov*))))))
(defn init-inputs []
(when-let [ray-count-slider (. js/document getElementById "rayCountSlider")]
(set! (. ray-count-slider -value) *ray-count*)
(. ray-count-slider addEventListener "input" update-ray-count))
(when-let [ray-count-output (. js/document getElementById "rayCountOutput")]
(set! (. ray-count-output -innerHTML) *ray-count*))
(when-let [fov-slider (. js/document getElementById "fovSlider")]
(. fov-slider addEventListener "input" update-fov)
(set! (. fov-slider -max) (if *compensate-fisheye* max-compensated-fov max-fov))
(set! (. fov-slider -value) *fov*))
(when-let [ray-count-output (. js/document getElementById "fovSliderOutput")]
(set! (. ray-count-output -innerHTML) *fov*))
(when-let [compensate (. js/document getElementById "fishEyeCompensation")]
(. compensate addEventListener "click" update-fish-eye-compensation)
(set! (. compensate -value) *compensate-fisheye*)))
In short, these are event handlers, which are called when we’re interacting with these inputs.
We’re setting initial values as well, as the handlers in init-input
procedure.
Coloring walls
Up until now, we were creating gray walls, which are fine, but boring.
And you could notice that we’re drawing our lines in color, so walls have color information.
Let’s update our find-intersections
procedure to include color information, and use it to color our walls:
(defn find-intersections [stage [origin end :as ray]]
(->> stage
(map (fn [[_ _ color :as wall]]
(if (intersect? ray wall)
(let [new (intersection ray wall)]
[(distance origin new) {:end new :color (or color color "#eeeeee")}])
[10000000 {:end end :color "#000000"}])))
(into {})))
Now, we return a map that has length as its key, and another map, which holds endpoint and wall color as a value.
We need to update our cast-rays
procedure to use this information:
(defn cast-rays [camera stage]
(let [step (/ *fov* *ray-count*)
ray-start [(:x camera) (:y camera)]
central-ray [ray-start (cam/move-point ray-start (:degree camera) 100)]]
(loop [n (dec *ray-count*)
degree (:degree (cam/rotate camera (- (/ *fov* 2.0))))
rays ()]
(if (>= n 0)
(do (let [ray-end (cam/move-point ray-start degree 5000)
intersections (find-intersections stage [ray-start ray-end])
shortest-ray (apply min (keys intersections))]
➊ {ray-end :end color :color} (intersections shortest-ray)
ray-angle (if *compensate-fisheye*
(angle-between [ray-start ray-end] central-ray)
0)
(recur (dec n)
(+ degree step)
(conj rays {:end ray-end :length shortest-ray :angle ray-angle :n n :color color}))))
rays))))
We destructure our map ➊, and then use its values when we conj
rays.
If we use this color in our draw-3d-stage
procedure, we’ll finally get results similar to what I showed in the very beginning:
But it would be nice if we keep the effect that more distant walls appear darker. To do that we’ll have to parse color, and dim it by some factor:
(defn dim [color amount]
(let [dim-factor (Math/pow 1.1 (/ (inc amount) 7))
color (int (/ color dim-factor))]
(if (< color 25)
25
color)))
(defn dim-color [color distance]
(let [red (js/parseInt (str "0x" (subs color 1 3)))
green (js/parseInt (str "0x" (subs color 3 5)))
blue (js/parseInt (str "0x" (subs color 5)))]
(str "rgb("
(dim red distance)
", "
(dim green distance)
", "
(dim blue distance)
")")))
These two procedures will dim colors encoded as six-digit hex colors, or #ffffff
.
We use our distance as the dim factor with some computations to make it look nice.
Now we can use it in our draw-3d-stage
procedure:
(defn draw-3d-stage [rays]
(let [ray-width (/ (/ (. *canvas* -width) 2) *ray-count*)
height (. *canvas* -height)
canvas-width (/ (. *canvas* -width) 2)
projection-distance (projection-distance canvas-width)
gradient (. *ctx* createLinearGradient 0 0 0 height)]
(doto gradient
(.addColorStop 0 "#424242")
(.addColorStop 0.5 "#111111")
(.addColorStop 1 "#e2e2e2"))
(set! (. *ctx* -fillStyle) gradient)
(. *ctx* fillRect canvas-width 0 canvas-width height)
(loop [rays (sort (fn [{l1 :length} {l2 :length}] (> l1 l2)) rays)]
(when-let [ray (first rays)]
(let [{distance :length ray-degree :angle n :n color :color} ray
angle-step (/ *fov* *ray-count*)
distance (if *compensate-fisheye*
(* distance (m/three-decimal (Math/cos ray-degree)))
distance)
wall-height (* (/ (/ height *ray-count*) distance) projection-distance (/ *fov* 4))
wall-height (if (> wall-height height)
height
wall-height)
color (dim-color color distance)]
(set! (. *ctx* -fillStyle) color)
(. *ctx*
fillRect
(+ canvas-width (* n ray-width))
(- (/ height 2) (/ wall-height 2))
(+ ray-width 1) ;; extra pixel for border
wall-height))
(recur (rest rays))))))
I’ve also added a bit more nice gradient for our background color there. This will be our final version of this procedure.
Labyrinth game
Now we can create a small labyrinth game, but we need a way to determine if the goal was reached. But first, we need a stage to play on, as our current one isn’t really a good one. I’ve come up with this:
Our goal is to reach that colored corridor. To check if we succeeded we’ll simply check if our camera is inside the square. To check that we’ll use our raycasting facilities. The algorithm is pretty easy:
- We cast rays from our point to the right,
- We count with how many walls of a given square we intersect with,
- If the amount of intersections is odd then we’re inside.
We can see that this works for us:
Of course, there are special cases, like when a point lies on either of the rectangle sides, but we can’t intersect with walls, because we detect collisions, so we’ll be fine. Here’s the code:
(defn inside-rectangle [camera [[ax ay] [bx by] [cx cy] [dx dy]]]
(let [walls [[[ax ay] [bx by]]
[[bx by] [cx cy]]
[[cx cy] [dx dy]]
[[dx dy] [ax ay]]]
{x :x y :y} camera
{x' :x y' :y} (cam/move-forward {:x x :y y :degree 90} 1000)]
(->> walls
(map (fn [wall]
(if (intersect? [[x y] [x' y']] wall) 1 0)))
(reduce + 0)
odd?)))
We receive four points that define our goal and move the camera 90 degrees right to some amount.
Then we map intersect?
procedure across our walls, returning either 1
or 0
depending on collision detection, and reduce
results with +
, to finally check if the result is odd?
or not.
Though we need some way to extract goals from our stage, I’ll just define it as a constant because I don’t want to make stage code even more complex than it is now, and we’re not going to change stages. So when we’ve reached our goal, we’ll just teleport the camera back to starting point:
(defn render []
(move-camera stage @input/key-states)
(when (inside-rectangle @cam/camera stage/goal)
(swap! cam/camera cam/set-position 12 195 180))
(let [height (. *canvas* -height)
width (. *canvas* -width)
camera @cam/camera
rays (cast-rays camera stage)]
(. *ctx* clearRect 0 0 width height)
(draw-rays camera rays)
(draw-camera camera)
(draw-stage stage)
(draw-3d-stage rays)
(. js/window requestAnimationFrame render)))
Combining all this stuff should give us this small game:
You can click on this canvas, and use Up, Down, Left, Right to move, you also can strafe to the side while holding Shift, and pressing Esc will return focus to the page so you could scroll it with arrow keys as usual.
What I’ve learned
This project was extremely fun to do in ClojureScript, and Clojure itself is quite an awesome language, although those have some differences. If you’re learning Clojure, I highly recommend Clojure for the Brave and True book, and although I didn’t finish it yet, I’ve borrowed a lot of ideas from there, like using these little Unicode circles (➊) to mark stuff for further explanations. This book has a “Peg Thing” game example, which got me thinking about trying something a bit more complex and real-time with Clojure, but I wasn’t sure how to draw graphics with Java stuff, so I’ve chosen ClojureScript because the canvas was very easy to reach.
After implementing this, I think that while you don’t really need all these class objects like point
, line
, e.t.c.
as long as you’re passing those in meaningful contexts, which I didn’t really do through this post, and I expect that this code will be really hard to read after some point in time.
But certainly, maps provide most of what you need from a class or object and are more generic than objects.
The cleaned-up source code for this project can be found here if you’re interested in looking more closely. I hope you’ve enjoyed the read, and got interested in Clojure or Lisp in particular if you’re not a Lisp hacker already.