If you arrived here wondering, “what is raycasting” then I invite you to scrub the field of view interval below to ‘look around’.
If you see a barchart, then you are correct and very precise. If you see a primitive image of a 3d blocky world (and yes some funny color switches), then you are also correct.
Raycasting is a rendering method used in many iconic and memorable computer games. This implementation - calculated with calculang and rendered to a bar chart, might be one the most naive of all raycasting ever casted.
(or can I say the most retro? :)
Let’s “see” how it works!
👀
The image is generated from:
a 2d representation of a world, aka a ‘level’: a 64x64 grid describing walls and open space
a player position and a ‘field of view’: together these determine the observable world
imaginary ‘rays’ which are ‘casted’ from the player into the observable world
Those rays are casted until they hit a wall. (They will always eventually hit a wall as long as the level is closed by walls and the player stays inside it!)
Then to make the rendering above, we need to calculate the distance each ray travels before hitting a wall. Finally we take the inverse, that’s the easy but no less important piece of the puzzle. The inverse makes rays that travel far get small bars (appearing further away), while rays that are stopped by a wall at short distance get bigger bars (appearing closer!) 📊
So the work in the method is really all about projecting rays (casting) and calculating distances to walls! 🧱🔫📏
🤔 Wouldn’t we expect funny things to happen if the player goes inside a wall?
Well yes, yes we would. And below you can try it if you like! 👾
Where do we even begin to “project rays” and “calculate distances”?
Above we have the level: this is a 2d representation of the world we will raycast - all 64x64 squares divided into empty space and walls.
The dot in the middle is the player, and is also the point from which rays are casted into the observable world. You can move the player using the controls above; both visuals will update to reflect player and field of view movements.
Tick this checkbox to continue and add a field of view visual cue:
Code
viewof observable_world_v = Inputs.checkbox(["overlay field of view"])observable_world = observable_world_v.lengthmd`${observable_world ?`\\o/### casting rays and 📏👀**Now you can cast rays by hovering or clicking in either visual above!** 🔫🧱<br>*Rays are casted in the level view.*Some actual calculations for the ray appear below, but it is much more useful to **scan and compare visually**. Notice that **walls that are near to the player in the level view get large bars which appear closer in the image**, **while walls that are far from the player get small bars which appear further away** - this is all that raycasting does to create playable worlds!There is one other effect included in the renderings I use here: the **opacity** of bars similarly correspond to ray distances, resulting in a fog-like effect.It's a combination of many simple tricks like these that bring computer games to life.`:'<br><br><br>*check the box to continue!*'}`
appendix workings: active ray distance travelled calculation
Code
viewof ray_angle_in = Inputs.range([-4,4], {step:0.01,label:'angle for active ray'})md`The **active ray distance travelled** is calculated as **${main.ray_length({player_x_in,player_y_in,ray_angle_in})} blocks**. ${ray_angle_in <= fov2[1] && ray_angle_in >= fov2[0] ?'':'(outside FOV)'} (30-step projection below)`
Code
md`Its **inverse**, 1 / ${main.ray_length({player_x_in,player_y_in,ray_angle_in})}, is ~ ${Math.round(100*main.inverse_ray_length({player_x_in,player_y_in,ray_angle_in}))/100}. This is the value used in the bar chart for this ray angle.`
Code
md`Color identifier associated with the wall the ray hits is ${main.ray_hit_color({player_x_in,player_y_in,ray_angle_in})}.`
Code
md`The **active ray distance travelled** is derived from a projection (or 'casting') *from the player in the direction of the active ray angle*. We check the level along this projection and the distance given above is the number of steps before hitting a non-zero value (i.e. the ray hits a wall), see ray_value:`
For calculang devtools adjust entrypoint in devtools.
Code
scene.addSignalListener("ray_angle_in", (_, r) => {if (observable_world ==0) return; viewof ray_angle_in.value=Math.round(r.ray_angle_in*100)/100; ray_angle_in.dispatchEvent(newCustomEvent('input'), {bubbles:true});// I might never understand how to make this always work})
Code
level.addSignalListener("xy", (_, r) => { // this didn't improve perf a lot vs event listener: nearly every mousemove is a signal change, I supposeif (observable_world ==0) return; viewof ray_angle_in.value=Math.round(Math.atan2((r.level_y_in[0] - player_y_in) , (r.level_x_in[0] - player_x_in))*100)/100; ray_angle_in.dispatchEvent(newCustomEvent('input'), {bubbles:true});})