v2 / examples / gg / raycaster.v
278 lines · 259 sloc · 7.32 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1// Demonstrates how raycasting works. The left side shows
2// the 2D layout of the walls and player. The green lines
3// represent the field of view of the player.
4//
5// The right side is a simple 3D projection of the field
6// of view.
7//
8// There is no collision detection so yes, you can walk
9// through walls.
10//
11// Watch https://www.youtube.com/watch?v=gYRrGTC7GtA to
12// learn more on how this code works. There are some silly
13// digressons in the video but the tech content is spot on.
14import gg
15import math
16
17const player_size = 8
18const map_x_size = 8
19const map_y_size = 8
20const map_square = 64
21
22struct App {
23mut:
24 ctx &gg.Context = unsafe { nil }
25 player_x f32
26 player_y f32
27 player_dx f32
28 player_dy f32
29 player_angle f32
30 map []int
31}
32
33fn main() {
34 mut app := App{
35 player_x: 230
36 player_y: 320
37 // each number represents an 8x8 square
38 // 1 is a wall cube, 0 is empty space
39 map: [
40 // vfmt off
41 1, 1, 1, 1, 1, 1, 1, 1,
42 1, 0, 0, 0, 0, 0, 0, 1,
43 1, 0, 1, 1, 0, 0, 0, 1,
44 1, 0, 1, 0, 0, 0, 0, 1,
45 1, 0, 0, 0, 0, 0, 0, 1,
46 1, 0, 0, 0, 0, 1, 0, 1,
47 1, 0, 0, 0, 0, 0, 0, 1,
48 1, 1, 1, 1, 1, 1, 1, 1,
49 // vfmt on
50 ]
51 }
52
53 calc_deltas(mut app)
54
55 app.ctx = gg.new_context(
56 user_data: &app
57 window_title: 'Raycaster Demo'
58 width: 1024
59 height: 512
60 bg_color: gg.gray
61 frame_fn: draw
62 event_fn: handle_events
63 )
64
65 app.ctx.run()
66}
67
68fn draw(mut app App) {
69 app.ctx.begin()
70 draw_map_2d(app)
71 draw_player(app)
72 draw_rays_and_walls(app)
73 draw_instructions(app)
74 app.ctx.end()
75}
76
77fn draw_map_2d(app App) {
78 for y := 0; y < map_y_size; y++ {
79 for x := 0; x < map_x_size; x++ {
80 color := if app.map[y * map_x_size + x] == 1 { gg.white } else { gg.black }
81 app.ctx.draw_rect_filled(x * map_square, y * map_square, map_square - 1,
82 map_square - 1, color)
83 }
84 }
85}
86
87fn draw_player(app App) {
88 app.ctx.draw_rect_filled(app.player_x, app.player_y, player_size, player_size, gg.yellow)
89 cx := app.player_x + player_size / 2
90 cy := app.player_y + player_size / 2
91 app.ctx.draw_line(cx, cy, cx + app.player_dx * 5, cy + app.player_dy * 5, gg.yellow)
92}
93
94fn draw_rays_and_walls(app App) {
95 pi2 := math.pi / 2
96 pi3 := 3 * math.pi / 2
97 degree_radian := f32(0.0174533)
98 max_depth_of_field := 8
99 field_of_view := 60 // 60 degrees
100
101 mut distance := f32(0)
102 mut depth_of_field := 0
103 mut ray_x := f32(0)
104 mut ray_y := f32(0)
105 mut offset_x := f32(0)
106 mut offset_y := f32(0)
107 mut map_x := 0
108 mut map_y := 0
109 mut map_pos := 0
110 mut color := gg.red
111 mut ray_angle := clamp_ray_angle(app.player_angle - degree_radian * field_of_view / 2)
112
113 // each step = 1/2 degree
114 steps := field_of_view * 2
115
116 for step := 0; step < steps; step++ {
117 // check horizontal lines
118 mut hd := f32(max_int)
119 mut hx := app.player_x
120 mut hy := app.player_y
121 depth_of_field = 0
122 arc_tan := -1.0 / math.tanf(ray_angle)
123 if ray_angle > math.pi { // looking up
124 ray_y = f32(int(app.player_y) / map_square * map_square) - .0001
125 ray_x = (app.player_y - ray_y) * arc_tan + app.player_x
126 offset_y = -map_square
127 offset_x = -offset_y * arc_tan
128 } else if ray_angle < math.pi { // looking down
129 ray_y = f32(int(app.player_y) / map_square * map_square + map_square)
130 ray_x = (app.player_y - ray_y) * arc_tan + app.player_x
131 offset_y = map_square
132 offset_x = -offset_y * arc_tan
133 } else if ray_angle == 0 || ray_angle == 2 * math.pi { // looking straight left/right
134 ray_x = app.player_x
135 ray_y = app.player_y
136 depth_of_field = max_depth_of_field
137 }
138 for depth_of_field < max_depth_of_field {
139 map_x = int(ray_x) / map_square
140 map_y = int(ray_y) / map_square
141 map_pos = map_y * map_x_size + map_x
142 if app.map[map_pos] or { 0 } == 1 {
143 // hit a wall
144 hx = ray_x
145 hy = ray_y
146 hd = hypotenuse(app.player_x, app.player_y, hx, hy)
147 depth_of_field = max_depth_of_field
148 } else { // go to next line
149 ray_x += offset_x
150 ray_y += offset_y
151 depth_of_field += 1
152 }
153 }
154 // check vertical lines
155 mut vd := f32(max_int)
156 mut vx := app.player_x
157 mut vy := app.player_y
158 depth_of_field = 0
159 neg_tan := -math.tanf(ray_angle)
160 if ray_angle > pi2 && ray_angle < pi3 { // looking left
161 ray_x = f32(int(app.player_x) / map_square * map_square) - .0001
162 ray_y = (app.player_x - ray_x) * neg_tan + app.player_y
163 offset_x = -map_square
164 offset_y = -offset_x * neg_tan
165 } else if ray_angle < pi2 || ray_angle > pi3 { // looking right
166 ray_x = f32(int(app.player_x) / map_square * map_square + map_square)
167 ray_y = (app.player_x - ray_x) * neg_tan + app.player_y
168 offset_x = map_square
169 offset_y = -offset_x * neg_tan
170 } else if ray_angle == 0 || ray_angle == 2 * math.pi { // looking straight up/down
171 ray_x = app.player_x
172 ray_y = app.player_y
173 depth_of_field = max_depth_of_field
174 }
175 for depth_of_field < max_depth_of_field {
176 map_x = int(ray_x) / map_square
177 map_y = int(ray_y) / map_square
178 map_pos = map_y * map_x_size + map_x
179 if app.map[map_pos] or { 0 } == 1 {
180 // hit a wall
181 vx = ray_x
182 vy = ray_y
183 vd = hypotenuse(app.player_x, app.player_y, vx, vy)
184 depth_of_field = max_depth_of_field
185 } else { // go to next line
186 ray_x += offset_x
187 ray_y += offset_y
188 depth_of_field += 1
189 }
190 }
191 // use the shorter of the horizontal and vertical distances to draw rays
192 // use different colors for the two sides of the walls for lighting effect
193 if vd < hd {
194 ray_x = vx
195 ray_y = vy
196 distance = vd
197 color = gg.rgb(0, 100, 0)
198 } else if hd < vd {
199 ray_x = hx
200 ray_y = hy
201 distance = hd
202 color = gg.rgb(0, 120, 0)
203 }
204 // draw ray
205 cx := app.player_x + player_size / 2
206 cy := app.player_y + player_size / 2
207 app.ctx.draw_line(cx, cy, ray_x, ray_y, gg.green)
208 // draw wall section
209 mut ca := clamp_ray_angle(app.player_angle - ray_angle)
210 distance *= math.cosf(ca) // remove fish eye
211 offset_3d_view := 530
212 line_thickeness := 4
213 max_wall_height := 320
214 wall_height := math.min((map_square * max_wall_height) / distance, max_wall_height)
215 wall_offset := max_wall_height / 2 - wall_height / 2
216 app.ctx.draw_line_with_config(step * line_thickeness + offset_3d_view, wall_offset,
217
218 step * line_thickeness + offset_3d_view, wall_offset + wall_height, gg.PenConfig{
219 color: color
220 thickness: line_thickeness
221 })
222 // step to next ray angle
223 ray_angle = clamp_ray_angle(ray_angle + degree_radian / 2)
224 }
225}
226
227fn handle_events(event &gg.Event, mut app App) {
228 if event.typ == .key_down {
229 match event.key_code {
230 .up {
231 app.player_x += app.player_dx
232 app.player_y += app.player_dy
233 }
234 .down {
235 app.player_x -= app.player_dx
236 app.player_y -= app.player_dy
237 }
238 .left {
239 app.player_angle -= 0.1
240 if app.player_angle < 0 {
241 app.player_angle += 2 * math.pi
242 }
243 calc_deltas(mut app)
244 }
245 .right {
246 app.player_angle += 0.1
247 if app.player_angle > 2 * math.pi {
248 app.player_angle -= 2 * math.pi
249 }
250 calc_deltas(mut app)
251 }
252 else {}
253 }
254 }
255}
256
257fn calc_deltas(mut app App) {
258 app.player_dx = math.cosf(app.player_angle) * 5
259 app.player_dy = math.sinf(app.player_angle) * 5
260}
261
262fn hypotenuse(ax f32, ay f32, bx f32, by f32) f32 {
263 a2 := math.square(bx - ax)
264 b2 := math.square(by - ay)
265 return math.sqrtf(a2 + b2)
266}
267
268fn clamp_ray_angle(ra f32) f32 {
269 return match true {
270 ra < 0 { ra + 2 * math.pi }
271 ra > 2 * math.pi { ra - 2 * math.pi }
272 else { ra }
273 }
274}
275
276fn draw_instructions(app App) {
277 app.ctx.draw_text(700, app.ctx.height - 17, 'use arrow keys to move player')
278}
279