├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── data ├── futcubes │ ├── sonic-texture.png │ └── sonic-texture.xcf └── futdoom │ ├── maps │ ├── corridors.map │ ├── start-only-colors.map │ └── start.map │ ├── misc │ └── poor_gun.png │ └── textures │ ├── bricks.png │ ├── flowers.png │ ├── lines.png │ ├── spiral.png │ ├── squares0.png │ ├── squares1.png │ ├── stars.png │ └── stones.png ├── futcubes.py ├── futdoom.py ├── futdoomlib ├── __init__.py ├── mapper.py ├── resources.py ├── runner.py └── scripts │ └── generate_random_map.py ├── futfly.py ├── futracer.py ├── futracerlib.fut ├── futracerlib ├── .gitignore ├── build_triangles.fut ├── color.fut ├── futhark.pkg ├── misc.fut ├── render.fut ├── render_types.fut └── transformations.fut └── shell.nix /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore non-directories in root, so you can use it as scratch space. 2 | /* 3 | !/*/ 4 | 5 | /futracerlib.py 6 | *.pyc 7 | __pycache__ 8 | .#* 9 | *# 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016, 2017, 2018, 2019, 2020 Niels G. W. Serup 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean run 2 | 3 | all: futracerlib.py 4 | 5 | futracerlib.py: futracerlib.fut futracerlib/*.fut futracerlib/lib 6 | futhark pyopencl --library futracerlib.fut 7 | 8 | futracerlib/lib: futracerlib/futhark.pkg 9 | cd futracerlib && futhark pkg sync 10 | 11 | clean: 12 | rm -f futracerlib.py futracerlib.pyc futracer.pyc 13 | rm -rf __pycache__ 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # futracer 2 | 3 | Race through breathtaking 3-D graphics with Futhark through OpenCL 4 | (*not* OpenGL)! 5 | 6 | Run `make` to build the library, and then run `./futcubes.py`, 7 | `./futfly.py`, or `./futdoom.py` to run the example programs. Use the 8 | `--help` argument to see which settings exist. 9 | 10 | There are three rendering approaches: `segmented` (the default) `chunked`, and 11 | `scatter_bbox`. The `segmented` approach is a rasterizer, while 12 | `chunked` and `scatter_bbox` are raycasters. 13 | 14 | *Click on the image to see a 1-minute video of `futcubes.py` in action.* 15 | [![Video of futcubes](https://hongabar.org/~niels/futracer/futracer-textured-image.jpg)](https://hongabar.org/~niels/futracer/futracer-textured.webm) 16 | 17 | *Click on the image to see a 1-minute video of `futfly.py` in action.* 18 | [![Video of futfly](https://hongabar.org/~niels/futracer/futracer-futfly-image.jpg)](https://hongabar.org/~niels/futracer/futracer-futfly.webm) 19 | 20 | *Click on the image to see a 30-second video of `futdoom.py` in action.* 21 | [![Video of futdoom](https://hongabar.org/~niels/futracer/futracer-futdoom-image.jpg)](https://hongabar.org/~niels/futracer/futracer-futdoom.webm) 22 | 23 | 24 | ## Dependencies 25 | 26 | futracer depends on the programming language Futhark; 27 | see [http://futhark-lang.org/](http://futhark-lang.org/) 28 | and 29 | [https://github.com/HIPERFIT/futhark](https://github.com/HIPERFIT/futhark). 30 | 31 | futracer also depends on PyGame, PyPNG (only `futcubes.py` and 32 | `futdoom.py`), and NumPy. 33 | 34 | 35 | ## Keyboard controls 36 | 37 | Use the arrow keys for now. Use Page Down and Page Up to decrease and 38 | increase the view distance for rendering (fun!). Use 1 and 2 to 39 | decrease and increase the draw distance. 40 | 41 | Use R to switch rendering approaches. 42 | 43 | For the `chunked` rendering approach, use A and D to decrease and 44 | increase the number of draw rectangles on the X axis, and W and S on the 45 | Y axis. Sometime in the future this should be chosen automatically. 46 | Warning: This might slow down the program to a crawl for some reason. 47 | 48 | 49 | ## Scripts 50 | 51 | `futdoom.py` supports custom maps. For an example of a (poorly) 52 | randomly generated map, run: 53 | 54 | ``` 55 | ./futdoomlib/scripts/generate_random_map.py | ./futdoom.py --auto-fps --level - 56 | ``` 57 | -------------------------------------------------------------------------------- /data/futcubes/sonic-texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futcubes/sonic-texture.png -------------------------------------------------------------------------------- /data/futcubes/sonic-texture.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futcubes/sonic-texture.xcf -------------------------------------------------------------------------------- /data/futdoom/maps/corridors.map: -------------------------------------------------------------------------------- 1 | # Aliases 2 | floor=stones 3 | ceiling=flowers 4 | wall=bricks 5 | 6 | # Functions 7 | bN= 8 | floor:floor 9 | ceiling:ceiling 10 | height:N 11 | walls:@standard_walls 12 | wall:wall 13 | number:N 14 | 15 | 16 | b3b3b3 17 | b3b3b3 18 | b3b3b3 19 | b3b3b3 20 | b3b3b3 21 | b3b3b3 22 | b3b3b3b3b3b3b3b3b3b3b3b3 23 | b3b3b3b3b3b3b3b3b3b3b3b3 24 | b3b3b3b3b3b3b3b3b3b3b3b3 25 | b3b3b3 26 | b3b3b3 27 | b3b3b3 28 | b3b3b3 29 | b3b3b3 30 | b3b3b3 31 | b3b3b3b3b3b3b3b3b3b3b3b3 32 | b3b3b3b3b3b3b3b3b3b3b3b3 33 | b3b3b3b3b3b3b3b3b3b3b3b3 34 | b3b3b3 35 | b3b3b3 36 | b3b3b3 37 | b3b3b3 38 | b3b3b3 39 | b3b3b3 40 | b3b3b3b3b3b3b3b3b3b3b3b3 41 | b3b3b3b3b3b3b3b3b3b3b3b3 42 | b3b3b3b3b3b3b3b3b3b3b3b3 43 | b3b3b3 44 | b3b3b3 45 | b3b3b3 46 | b3b3b3 47 | b3b3b3 48 | b3b3b3 49 | b3b3b3b3b3b3b3b3b3b3b3b3 50 | b3b3b3b3b3b3b3b3b3b3b3b3 51 | b3b3b3b3b3b3b3b3b3b3b3b3 52 | b3b3b3 53 | b3b3b3 54 | b3b3b3 55 | b3b3b3 56 | b3b3b3 57 | b3b3b3 58 | b3b3b3b3b3b3b3b3b3b3b3b3 59 | b3b3b3b3b3b3b3b3b3b3b3b3 60 | b3b3b3b3b3b3b3b3b3b3b3b3 61 | b3b3b3 62 | b3b3b3 63 | b3b3b3 64 | b3b3b3 65 | b3b3b3 66 | b3b3b3 67 | b3b3b3b3b3b3b3b3b3b3b3b3 68 | b3b3b3b3b3b3b3b3b3b3b3b3 69 | b3b3b3b3b3b3b3b3b3b3b3b3 70 | -------------------------------------------------------------------------------- /data/futdoom/maps/start-only-colors.map: -------------------------------------------------------------------------------- 1 | # Same as start.map, but with colors instead of textures. 2 | 3 | # Aliases 4 | floor=HSV 14.0 0.5 0.5 5 | ceiling=HSV 173.0 0.5 0.5 6 | wall=HSV 0.0 0.0 0.5 7 | door=HSV 231.0 1.0 1.0 8 | 9 | # Functions 10 | bN= 11 | floor:floor 12 | ceiling:ceiling 13 | height:N 14 | walls:@standard_walls 15 | wall:wall 16 | number:N 17 | 18 | dN= 19 | floor:floor 20 | ceiling:ceiling 21 | height:N 22 | walls:@walls_with_door 23 | wall:wall 24 | number:N 25 | door:door 26 | 27 | 28 | b2b2b2d2b2b2b2 29 | b2b2b2b2b2b2b2 30 | b2b2b2b2b2b2b2 31 | d2b2b2b2b2b2d2 32 | b2b2b2b2b2b2b2 33 | b2b2b2b2b2b2b2 34 | b2b2b2d2b2b2b2 35 | -------------------------------------------------------------------------------- /data/futdoom/maps/start.map: -------------------------------------------------------------------------------- 1 | # Aliases 2 | floor=squares0 3 | ceiling=squares1 4 | wall=stones 5 | door=spiral 6 | 7 | # Functions 8 | bN= 9 | floor:floor 10 | ceiling:ceiling 11 | height:N 12 | walls:@standard_walls 13 | wall:wall 14 | number:N 15 | 16 | dN= 17 | floor:floor 18 | ceiling:ceiling 19 | height:N 20 | walls:@walls_with_door 21 | wall:wall 22 | number:N 23 | door:door 24 | 25 | 26 | b2b2b2d2b2b2b2 27 | b2b2b2b2b2b2b2 28 | b2b2b2b2b2b2b2 29 | d2b2b2b2b2b2d2 30 | b2b2b2b2b2b2b2 31 | b2b2b2b2b2b2b2 32 | b2b2b2d2b2b2b2 33 | -------------------------------------------------------------------------------- /data/futdoom/misc/poor_gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/misc/poor_gun.png -------------------------------------------------------------------------------- /data/futdoom/textures/bricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/bricks.png -------------------------------------------------------------------------------- /data/futdoom/textures/flowers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/flowers.png -------------------------------------------------------------------------------- /data/futdoom/textures/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/lines.png -------------------------------------------------------------------------------- /data/futdoom/textures/spiral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/spiral.png -------------------------------------------------------------------------------- /data/futdoom/textures/squares0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/squares0.png -------------------------------------------------------------------------------- /data/futdoom/textures/squares1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/squares1.png -------------------------------------------------------------------------------- /data/futdoom/textures/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/stars.png -------------------------------------------------------------------------------- /data/futdoom/textures/stones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nqpz/futracer/7c85f478b49b5c3619dcd74e8a9af5d9a99f1d1f/data/futdoom/textures/stones.png -------------------------------------------------------------------------------- /futcubes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os.path 5 | import math 6 | import random 7 | import argparse 8 | import time 9 | 10 | import pygame 11 | 12 | import futracer 13 | 14 | 15 | class FutCubes: 16 | def __init__(self, size=None, n_cubes=None, just_colors=False, 17 | render_approach=None): 18 | if size is None: 19 | size = (800, 600) 20 | self.size = size 21 | if n_cubes is None: 22 | n_cubes = 2000 23 | self.n_cubes = n_cubes 24 | self.just_colors = just_colors 25 | if render_approach is None: 26 | render_approach = 'segmented' 27 | self.render_approach = render_approach 28 | self.view_dist = 600.0 29 | self.draw_dist = 8000.0 30 | if render_approach != 'segmented': 31 | self.draw_dist /= 10 # The others are really that much slower. 32 | self.n_draw_rects = [1, 1] 33 | 34 | def run(self): 35 | # Setup pygame. 36 | pygame.init() 37 | pygame.display.set_caption('futcubes') 38 | self.screen = pygame.display.set_mode(self.size) 39 | self.surface = pygame.Surface(self.size, depth=32) 40 | self.font = pygame.font.Font(None, 36) 41 | self.clock = pygame.time.Clock() 42 | 43 | # Load the library. 44 | self.racer = futracer.FutRacer() 45 | 46 | # Actually run! 47 | return self.loop() 48 | 49 | def message(self, what, where): 50 | text = self.font.render(what, 1, (255, 255, 255)) 51 | self.screen.blit(text, where) 52 | 53 | def random_cubes(self, square_texture=None): 54 | t0 = [(200.0, 100.0, 200.0), 55 | (200.0, 300.0, 200.0), 56 | (400.0, 100.0, 200.0)] 57 | t1 = [(400.0, 300.0, 200.0), 58 | (400.0, 100.0, 200.0), 59 | (200.0, 300.0, 200.0)] 60 | origo = (300.0, 200.0, 300.0) 61 | s0 = [t0, t1] 62 | s1 = [[self.racer.rotate_point((0.0, math.pi / 2, 0.0), origo, p) for p in t] 63 | for t in s0] 64 | s2 = [[self.racer.rotate_point((0.0, -math.pi / 2, 0.0), origo, p) for p in t] 65 | for t in s0] 66 | s3 = [[self.racer.rotate_point((0.0, math.pi, 0.0), origo, p) for p in t] 67 | for t in s0] 68 | s4 = [[self.racer.rotate_point((math.pi / 2, 0.0, 0.0), origo, p) for p in t] 69 | for t in s0] 70 | s5 = [[self.racer.rotate_point((-math.pi / 2, 0.0, 0.0), origo, p) for p in t] 71 | for t in s0] 72 | half_cube_0 = s0 + s1 + s2 + s3 + s4 + s5 73 | 74 | if square_texture is not None: 75 | textures = [square_texture] 76 | else: 77 | textures = [] 78 | 79 | half_cubes = [] 80 | xr = 30000.0 81 | yr = 1000.0 82 | zr = 30000.0 83 | for i in range(self.n_cubes): 84 | xm = random.random() * xr - xr / 2 85 | ym = random.random() * yr - yr / 2 86 | zm = random.random() * zr - zr / 2 87 | ax = random.random() * math.pi 88 | ay = random.random() * math.pi 89 | az = random.random() * math.pi 90 | if square_texture is not None: 91 | def make_surf(i): 92 | surf = (2, (0.0, 0.0, 0.0), i % 2) 93 | return surf 94 | else: 95 | # Use a random color. 96 | hsv = (random.random() * 360.0, 97 | random.random(), 98 | random.random()) 99 | surf = (1, hsv, -1) 100 | make_surf = lambda i: surf 101 | def move_point(p): 102 | return self.racer.rotate_point( 103 | (ax, ay, az), 104 | self.racer.translate_point((xm, ym, zm), origo), 105 | self.racer.translate_point((xm, ym, zm), p)) 106 | half_cube = [[move_point(p) for p in t] + [make_surf(i)] 107 | for t, i in 108 | zip(half_cube_0, range(len(half_cube_0)))] 109 | half_cubes.extend(half_cube) 110 | 111 | return half_cubes, textures 112 | 113 | def loop(self): 114 | if self.just_colors: 115 | triangles, textures = self.random_cubes() 116 | else: 117 | base_dir = os.path.dirname(__file__) 118 | sonic_texture = self.racer.load_double_texture( 119 | os.path.join(base_dir, 'data/futcubes/sonic-texture.png')) 120 | textures_size = (100, 100) 121 | assert (textures_size[0], textures_size[1], 3) == sonic_texture.shape 122 | triangles, textures = self.random_cubes(sonic_texture) 123 | 124 | # The objects will not change (only the camera changes), so we 125 | # preprocess them to save important loop time. 126 | triangles_pre = self.racer.preprocess_triangles(triangles) 127 | textures_pre = self.racer.preprocess_textures(textures) 128 | 129 | camera = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] 130 | 131 | keys_holding = {} 132 | for x in [pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT, 133 | pygame.K_PAGEUP, pygame.K_PAGEDOWN, pygame.K_1, pygame.K_2]: 134 | keys_holding[x] = False 135 | 136 | def inf_range(): 137 | i = 0 138 | while True: 139 | yield i 140 | i += 1 141 | 142 | for i in inf_range(): 143 | fps = self.clock.get_fps() 144 | time_start = time.time() 145 | 146 | dynamic_triangle = [[-300, -300, 500], 147 | [300, -300, 500], 148 | [0, 300, 400], 149 | [1, [240, 1, 1], 0]] 150 | dynamic_origo = [0, 0, 450] 151 | dynamic_angles = [i / 60.0, i / 80.0, i / 100.0] 152 | dynamic_triangle = self.racer.rotate_triangle( 153 | dynamic_angles, dynamic_origo, dynamic_triangle) 154 | 155 | frame = self.racer.render_triangles( 156 | self.size, self.view_dist, self.draw_dist, camera, 157 | [dynamic_triangle], triangles_pre, 158 | None, textures_pre, self.render_approach, self.n_draw_rects) 159 | time_end = time.time() 160 | frame = frame.get() 161 | futhark_dur_ms = (time_end - time_start) * 1000 162 | pygame.surfarray.blit_array(self.surface, frame) 163 | self.screen.blit(self.surface, (0, 0)) 164 | 165 | self.message('FPS: {:.02f}'.format(fps), (10, 10)) 166 | self.message('Futhark: {:.02f} ms'.format(futhark_dur_ms), (10, 40)) 167 | self.message('Draw distance: {:.02f}'.format(self.draw_dist), (10, 70)) 168 | self.message('Rendering approach: {}'.format(self.render_approach), (10, 100)) 169 | if self.render_approach == 'chunked': 170 | self.message('# draw rects: x: {}, y: {}'.format( 171 | *self.n_draw_rects), (10, 130)) 172 | 173 | pygame.display.flip() 174 | 175 | # Check events. 176 | for event in pygame.event.get(): 177 | if event.type == pygame.QUIT: 178 | return 0 179 | 180 | elif event.type == pygame.KEYDOWN: 181 | if event.key == pygame.K_q: 182 | return 0 183 | if event.key in keys_holding.keys(): 184 | keys_holding[event.key] = True 185 | 186 | if event.key == pygame.K_r: 187 | self.render_approach = futracer.next_elem(futracer.render_approaches, 188 | self.render_approach) 189 | 190 | if event.key == pygame.K_a: 191 | self.n_draw_rects[0] = max(1, self.n_draw_rects[0] - 1) 192 | if event.key == pygame.K_d: 193 | self.n_draw_rects[0] = self.n_draw_rects[0] + 1 194 | if event.key == pygame.K_w: 195 | self.n_draw_rects[1] = max(1, self.n_draw_rects[1] - 1) 196 | if event.key == pygame.K_s: 197 | self.n_draw_rects[1] = self.n_draw_rects[1] + 1 198 | 199 | elif event.type == pygame.KEYUP: 200 | if event.key in keys_holding.keys(): 201 | keys_holding[event.key] = False 202 | 203 | if keys_holding[pygame.K_UP]: 204 | p1 = camera[0][:] 205 | p1[2] += 15 206 | p2 = self.racer.rotate_point(camera[1], camera[0], p1) 207 | camera[0] = list(p2) 208 | if keys_holding[pygame.K_DOWN]: 209 | p1 = camera[0][:] 210 | p1[2] -= 15 211 | p2 = self.racer.rotate_point(camera[1], camera[0], p1) 212 | camera[0] = list(p2) 213 | if keys_holding[pygame.K_LEFT]: 214 | camera[1][1] -= 0.04 215 | if keys_holding[pygame.K_RIGHT]: 216 | camera[1][1] += 0.04 217 | 218 | if keys_holding[pygame.K_PAGEUP]: 219 | self.view_dist += 10.0 220 | if keys_holding[pygame.K_PAGEDOWN]: 221 | self.view_dist -= 10.0 222 | if self.view_dist < 1.0: 223 | self.view_dist = 1.0 224 | 225 | if keys_holding[pygame.K_1]: 226 | self.draw_dist -= 10.0 227 | if keys_holding[pygame.K_2]: 228 | self.draw_dist += 10.0 229 | 230 | self.clock.tick() 231 | 232 | def main(args): 233 | def size(s): 234 | return tuple(map(int, s.split('x'))) 235 | 236 | arg_parser = argparse.ArgumentParser(description='Use the arrow keys to move around. Use Page Up and Page Down to adjust the view distance.') 237 | arg_parser.add_argument('--size', type=size, metavar='WIDTHxHEIGHT', 238 | help='set the size of the racing game window') 239 | arg_parser.add_argument('--cubes', type=int, metavar='N', 240 | help='set the number of cubes in the world (defaults to 2000)') 241 | arg_parser.add_argument('--just-colors', action='store_true', 242 | help='use random colors instead of the pretty texture') 243 | arg_parser.add_argument('--render-approach', 244 | choices=futracer.render_approaches, 245 | default='segmented', 246 | help='choose how to render a frame') 247 | 248 | args = arg_parser.parse_args(args) 249 | 250 | cubes = FutCubes(size=args.size, n_cubes=args.cubes, 251 | just_colors=args.just_colors, 252 | render_approach=args.render_approach) 253 | return cubes.run() 254 | 255 | if __name__ == '__main__': 256 | sys.exit(main(sys.argv[1:])) 257 | -------------------------------------------------------------------------------- /futdoom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | import futracer 6 | import futdoomlib 7 | 8 | def main(args): 9 | return futdoomlib.main(futracer, args) 10 | 11 | if __name__ == '__main__': 12 | sys.exit(main(sys.argv[1:])) 13 | -------------------------------------------------------------------------------- /futdoomlib/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import futdoomlib.runner as runner 4 | 5 | def main(futracer, args): 6 | def size(s): 7 | return tuple(map(int, s.split('x'))) 8 | 9 | arg_parser = argparse.ArgumentParser(description='DOOM. Use the arrow keys to move around. Interact with space.') 10 | arg_parser.add_argument('--level-path', 11 | help='play this level from (defaults to "data/futdoom/maps/start.map")') 12 | arg_parser.add_argument('--scale-to', type=size, metavar='WIDTHxHEIGHT', 13 | help='scale the frames to this size when showing them') 14 | arg_parser.add_argument('--render-approach', 15 | choices=futracer.render_approaches, 16 | default='segmented', 17 | help='choose how to render a frame') 18 | arg_parser.add_argument('--auto-fps', 19 | action='store_true', 20 | help='automatically keep the FPS high by dynamically lowering the draw distance (experimental)') 21 | args = arg_parser.parse_args(args) 22 | 23 | doom = runner.Doom(futracer, level_path=args.level_path, 24 | scale_to=args.scale_to, 25 | render_approach=args.render_approach, 26 | auto_fps=args.auto_fps) 27 | return doom.run() 28 | -------------------------------------------------------------------------------- /futdoomlib/mapper.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | GameMapRaw = collections.namedtuple( 4 | 'GameMapRaw', 5 | ['aliases', 'functions', 'cells']) 6 | 7 | GameMapEvald = collections.namedtuple( 8 | 'GameMapEvald', 9 | ['cells']) 10 | 11 | Cell = collections.namedtuple( 12 | 'Cell', 13 | ['floor', 'ceiling', 'height', 'walls']) 14 | 15 | Walls = collections.namedtuple( 16 | 'Walls', 17 | ['north', 'east', 'south', 'west']) 18 | 19 | 20 | def load_map(path): 21 | with open(path) as f: 22 | d = f.read() 23 | d = d.strip() 24 | 25 | raw = parse_map(d) 26 | evald = eval_map(raw) 27 | return evald 28 | 29 | 30 | def parse_map(s): 31 | definitions, textmap = s.split('\n\n\n') 32 | 33 | aliases = {} 34 | functions = {} 35 | 36 | lines = definitions.split('\n') 37 | lines.append('') 38 | lines = [line for line in lines 39 | if not line.strip().startswith('#')] 40 | 41 | def parse_values(name, indentation, i): 42 | values = {} 43 | 44 | while i < len(lines): 45 | line = lines[i] 46 | if len(line) - len(line.lstrip()) != indentation: 47 | return values, i 48 | name, value = line.split(':') 49 | name = name.strip() 50 | if value.startswith('@'): 51 | function_name = value[1:] 52 | call_values, i = parse_values(name, indentation + 1, i + 1) 53 | values[name] = ('call', function_name, call_values) 54 | else: 55 | values[name] = value 56 | i += 1 57 | 58 | i = 0 59 | while i < len(lines): 60 | line = lines[i] 61 | if not line.strip(): 62 | i += 1 63 | continue 64 | 65 | name, value = line.split('=') 66 | value = value.strip() 67 | if value: 68 | aliases[name] = value 69 | i += 1 70 | else: 71 | letter = name[0] 72 | try: 73 | arg = name[1] 74 | except IndexError: 75 | arg = None 76 | values, i = parse_values(name, 1, i + 1) 77 | functions[letter] = (arg, values) 78 | 79 | cells = {} 80 | 81 | lines = textmap.strip().split('\n') 82 | for y in range(len(lines)): 83 | line = lines[y] 84 | for x in range(0, len(line), 2): 85 | val = line[x:x + 2] 86 | if val.strip(): 87 | cells[(x // 2, y)] = val 88 | 89 | raw = GameMapRaw(aliases, functions, cells) 90 | return raw 91 | 92 | 93 | def eval_map(raw): 94 | cells = {} 95 | for pos, value in raw.cells.items(): 96 | cells[pos] = eval_cell(raw, pos, value) 97 | return GameMapEvald(cells) 98 | 99 | def eval_cell(raw, pos, s): 100 | name = s[0] 101 | arg = s[1] 102 | 103 | param, values = raw.functions[name] 104 | if param == 'N': 105 | arg = int(arg) 106 | aliases = raw.aliases.copy() 107 | aliases[param] = arg 108 | values_new = {} 109 | for k, v in values.items(): 110 | if isinstance(v, tuple): 111 | if v[0] == 'call': 112 | function_name = v[1] 113 | args = v[2] 114 | v_new = call_builtin(raw, aliases, pos, function_name, args) 115 | else: 116 | v_new = eval_value(aliases, v) 117 | values_new[k] = v_new 118 | values_new[k] = v_new 119 | cell = Cell(**values_new) 120 | return cell 121 | 122 | def eval_value(aliases, v): 123 | try: 124 | v_new = aliases[v] 125 | except KeyError: 126 | v_new = v 127 | if isinstance(v_new, str) and v_new.startswith('HSV'): 128 | v_new = ('hsv', list(map(float, v_new.split()[1:]))) 129 | return v_new 130 | 131 | def call_builtin(raw, aliases, pos, name, args): 132 | builtins = { 133 | 'standard_walls': builtin_standard_walls, 134 | 'walls_with_door': builtin_walls_with_door 135 | } 136 | return builtins[name](raw, aliases, pos, args) 137 | 138 | def builtin_standard_walls(raw, aliases, pos, kwargs): 139 | wall = eval_value(aliases, kwargs['wall']) 140 | number = eval_value(aliases, kwargs['number']) 141 | 142 | x, y = pos 143 | north = (x, y - 1) 144 | east = (x + 1, y) 145 | south = (x, y + 1) 146 | west = (x - 1, y) 147 | 148 | textures = [] 149 | for i in range(number): 150 | textures.append((i, wall)) 151 | 152 | walls = { 153 | 'north': textures if north not in raw.cells.keys() else [], 154 | 'east': textures if east not in raw.cells.keys() else [], 155 | 'south': textures if south not in raw.cells.keys() else [], 156 | 'west': textures if west not in raw.cells.keys() else [] 157 | } 158 | walls = Walls(**walls) 159 | return walls 160 | 161 | def builtin_walls_with_door(raw, aliases, pos, kwargs): 162 | door = eval_value(aliases, kwargs['door']) 163 | wall = eval_value(aliases, kwargs['wall']) 164 | number = eval_value(aliases, kwargs['number']) 165 | 166 | x, y = pos 167 | north = (x, y - 1) 168 | east = (x + 1, y) 169 | south = (x, y + 1) 170 | west = (x - 1, y) 171 | 172 | textures = [(0, door)] 173 | for i in range(1, number): 174 | textures.append((i, wall)) 175 | 176 | walls = { 177 | 'north': [], 178 | 'east': [], 179 | 'south': [], 180 | 'west': [] 181 | } 182 | 183 | assert sum(int(direc not in raw.cells.keys()) 184 | for direc in (north, east, south, west)) == 1 185 | 186 | if north not in raw.cells.keys(): 187 | walls['north'] = textures 188 | elif east not in raw.cells.keys(): 189 | walls['east'] = textures 190 | elif south not in raw.cells.keys(): 191 | walls['south'] = textures 192 | elif west not in raw.cells.keys(): 193 | walls['west'] = textures 194 | 195 | walls = Walls(**walls) 196 | return walls 197 | -------------------------------------------------------------------------------- /futdoomlib/resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | 4 | def file_paths(path): 5 | return [os.path.join(path, filename) 6 | for filename in os.listdir(path)] 7 | 8 | data_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'data', 'futdoom')) 9 | textures_dir = os.path.join(data_dir, 'textures') 10 | maps_dir = os.path.join(data_dir, 'maps') 11 | misc_dir = os.path.join(data_dir, 'misc') 12 | 13 | textures_paths = file_paths(textures_dir) 14 | maps_paths = file_paths(maps_dir) 15 | misc_paths = file_paths(misc_dir) 16 | -------------------------------------------------------------------------------- /futdoomlib/runner.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import time 4 | 5 | import pygame 6 | 7 | import futdoomlib.resources as resources 8 | import futdoomlib.mapper as mapper 9 | 10 | def square2d_to_triangles2d(square2d): 11 | left_upper, right_lower = square2d 12 | x_lu, y_lu = left_upper 13 | x_rl, y_rl = right_lower 14 | return [ 15 | [(x_lu, y_lu), (x_lu, y_rl), (x_rl, y_lu)], 16 | [(x_rl, y_rl), (x_rl, y_lu), (x_lu, y_rl)] 17 | ] 18 | 19 | class Doom: 20 | def __init__(self, racer_module, level_path, scale_to=None, 21 | render_approach=None, auto_fps=False): 22 | self.racer_module = racer_module 23 | self.level_path = level_path 24 | self.scale_to = scale_to 25 | self.render_approach = render_approach 26 | if render_approach is None: 27 | render_approach = 'segmented' 28 | self.auto_fps = auto_fps 29 | self.size = (640, 360) 30 | self.view_dist = 400.0 31 | self.draw_dist = 20000.0 32 | if render_approach != 'segmented': 33 | self.draw_dist /= 10 # The others are really that much slower. 34 | self.n_draw_rects = [1, 1] 35 | 36 | def run(self): 37 | self.racer = self.racer_module.FutRacer() 38 | self.load_resources() 39 | self.setup_screen() 40 | self.loop() 41 | 42 | def load_resources(self): 43 | textures = {} 44 | textures_list = [] 45 | for path, i in zip(resources.textures_paths, 46 | range(len(resources.textures_paths))): 47 | texture = self.racer.load_double_texture(path) 48 | name = os.path.basename(path).rsplit('.', 1)[0] 49 | textures[name] = (i * 2, texture) 50 | textures_list.append(texture) 51 | self.textures = textures 52 | self.textures_pre = self.racer.preprocess_textures(textures_list) 53 | 54 | if not self.level_path: 55 | self.level_path = os.path.join(resources.maps_dir, 'start.map') 56 | elif self.level_path == '-': 57 | self.level_path = 0 58 | 59 | gamemap = mapper.load_map(self.level_path) 60 | triangles, textures_used = self.make_map_triangles(gamemap) 61 | name = os.path.basename(path).rsplit('.', 1)[0] 62 | self.triangles_pre = self.racer.preprocess_triangles(triangles) 63 | # FIXME: Actually use textures_used to reduce space use. 64 | 65 | pygame.font.init() 66 | self.font = pygame.font.Font(None, 36) 67 | 68 | def make_map_triangles(self, gamemap): 69 | f = 200 70 | 71 | triangles_all = [] 72 | textures_used = set() 73 | 74 | for pos, cell in gamemap.cells.items(): 75 | xp, zp = pos 76 | 77 | # Floor and ceiling. 78 | square2d = [[xp * f - f // 2, zp * f - f // 2], 79 | [xp * f + f // 2, zp * f + f // 2]] 80 | triangles2d = square2d_to_triangles2d(square2d) 81 | for y, texture in ((0, cell.floor), (cell.height, cell.ceiling)): 82 | if isinstance(texture, tuple): 83 | assert texture[0] == 'hsv' 84 | hsv = texture[1] 85 | texfun = lambda _: [1, hsv, 0] 86 | i_base = 0 # Doesn't matter. 87 | else: 88 | i_base, _ = self.textures[texture] 89 | textures_used.add(texture) 90 | texfun = lambda i: [2, [0, 0, 0], i] 91 | triangles = [[[x, (0 - y) * f, z] 92 | for x, z in t] + [texfun(i)] 93 | for t, i in zip(triangles2d, (i_base, i_base + 1))] 94 | triangles_all.extend(triangles) 95 | 96 | # Walls. 97 | direcs = [(cell.walls.north, [[xp * f - f // 2, -1], [xp * f + f // 2, 0]], 98 | zp * f - f // 2, lambda p, n: [p[0], p[1], n]), 99 | (cell.walls.east, [[zp * f - f // 2, -1], [zp * f + f // 2, 0]], 100 | xp * f + f // 2, lambda p, n: [n, p[1], p[0]]), 101 | (cell.walls.south, [[xp * f - f // 2, -1], [xp * f + f // 2, 0]], 102 | zp * f + f // 2, lambda p, n: [p[0], p[1], n]), 103 | (cell.walls.west, [[zp * f - f // 2, -1], [zp * f + f // 2, 0]], 104 | xp * f - f // 2, lambda p, n: [n, p[1], p[0]])] 105 | for textures, square2d, n, fun in direcs: 106 | triangles2d = square2d_to_triangles2d(square2d) 107 | for y_offset, texture in textures: 108 | if isinstance(texture, tuple): 109 | assert texture[0] == 'hsv' 110 | hsv = texture[1] 111 | texfun = lambda _: [1, hsv, 0] 112 | i_base = 0 # Doesn't matter. 113 | else: 114 | i_base, _ = self.textures[texture] 115 | textures_used.add(texture) 116 | texfun = lambda i: [2, [0, 0, 0], i] 117 | triangles = [[(lambda x, y, z: 118 | [x, (y - y_offset) * f, z])(*fun(p, n)) 119 | for p in t] + [texfun(i)] 120 | for t, i in zip(triangles2d, (i_base, i_base + 1))] 121 | triangles_all.extend(triangles) 122 | 123 | return triangles_all, textures_used 124 | 125 | def setup_screen(self): 126 | # Check related input. 127 | if self.scale_to is not None: 128 | if not all(scaled % orig == 0 129 | for scaled, orig 130 | in zip(self.scale_to, self.size)): 131 | print('WARNING: Scale size is not a multiplicative of base size; expect blurriness.', file=sys.stderr) 132 | 133 | # Setup pygame. 134 | pygame.display.init() 135 | pygame.display.set_caption('futdoom') 136 | screen_size = self.scale_to if self.scale_to else self.size 137 | self.screen = pygame.display.set_mode(screen_size) 138 | if self.scale_to: 139 | self.surface_base = pygame.Surface(self.size, depth=32) 140 | self.surface = pygame.Surface(screen_size, depth=32) 141 | self.clock = pygame.time.Clock() 142 | 143 | def message(self, what, where): 144 | text = self.font.render(what, 1, (0, 0, 255)) 145 | self.screen.blit(text, where) 146 | 147 | def loop(self): 148 | camera = [[700.0, -180.0, 700.0], [0.0, 0.0, 0.0]] 149 | 150 | keys_holding = {} 151 | for x in [pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT, 152 | pygame.K_PAGEUP, pygame.K_PAGEDOWN, pygame.K_1, pygame.K_2]: 153 | keys_holding[x] = False 154 | 155 | def inf_range(): 156 | i = 0 157 | while True: 158 | yield i 159 | i += 1 160 | 161 | for i in inf_range(): 162 | fps = self.clock.get_fps() 163 | time_start = time.time() 164 | 165 | frame = self.racer.render_triangles_preprocessed( 166 | self.size, self.view_dist, self.draw_dist, camera, 167 | self.triangles_pre, self.textures_pre, self.render_approach) 168 | frame = frame.get() 169 | if not self.scale_to: 170 | pygame.surfarray.blit_array(self.surface, frame) 171 | else: 172 | pygame.surfarray.blit_array(self.surface_base, frame) 173 | pygame.transform.scale(self.surface_base, self.scale_to, self.surface) 174 | self.screen.blit(self.surface, (0, 0)) 175 | time_end = time.time() 176 | futhark_dur_ms = (time_end - time_start) * 1000 177 | 178 | if self.auto_fps: 179 | if futhark_dur_ms > 20: 180 | self.draw_dist /= 1.1 181 | else: 182 | self.draw_dist += 10.0 183 | 184 | self.message('FPS: {:.02f}'.format(fps), (10, 10)) 185 | self.message('Futhark: {:.02f} ms'.format(futhark_dur_ms), (10, 40)) 186 | self.message('Draw distance: {:.02f}{}'.format( 187 | self.draw_dist, ' (auto)' if self.auto_fps else ''), (10, 70)) 188 | self.message('Rendering approach: {}'.format(self.render_approach), (10, 100)) 189 | if self.render_approach == 'chunked': 190 | self.message('# draw rects: x: {}, y: {}'.format( 191 | *self.n_draw_rects), (10, 130)) 192 | 193 | pygame.display.flip() 194 | 195 | # Check events. 196 | for event in pygame.event.get(): 197 | if event.type == pygame.QUIT: 198 | return 0 199 | 200 | elif event.type == pygame.KEYDOWN: 201 | if event.key == pygame.K_q: 202 | return 0 203 | if event.key in keys_holding.keys(): 204 | keys_holding[event.key] = True 205 | 206 | if event.key == pygame.K_r: 207 | self.render_approach = self.racer_module.next_elem( 208 | self.racer_module.render_approaches, 209 | self.render_approach) 210 | 211 | if event.key == pygame.K_a: 212 | self.n_draw_rects[0] = max(1, self.n_draw_rects[0] - 1) 213 | if event.key == pygame.K_d: 214 | self.n_draw_rects[0] = self.n_draw_rects[0] + 1 215 | if event.key == pygame.K_w: 216 | self.n_draw_rects[1] = max(1, self.n_draw_rects[1] - 1) 217 | if event.key == pygame.K_s: 218 | self.n_draw_rects[1] = self.n_draw_rects[1] + 1 219 | 220 | elif event.type == pygame.KEYUP: 221 | if event.key in keys_holding.keys(): 222 | keys_holding[event.key] = False 223 | 224 | if keys_holding[pygame.K_UP]: 225 | p1 = camera[0][:] 226 | p1[2] += 15 227 | p2 = self.racer.rotate_point(camera[1], camera[0], p1) 228 | camera[0] = list(p2) 229 | if keys_holding[pygame.K_DOWN]: 230 | p1 = camera[0][:] 231 | p1[2] -= 15 232 | p2 = self.racer.rotate_point(camera[1], camera[0], p1) 233 | camera[0] = list(p2) 234 | if keys_holding[pygame.K_LEFT]: 235 | camera[1][1] -= 0.04 236 | if keys_holding[pygame.K_RIGHT]: 237 | camera[1][1] += 0.04 238 | 239 | if keys_holding[pygame.K_PAGEUP]: 240 | self.view_dist += 10.0 241 | if keys_holding[pygame.K_PAGEDOWN]: 242 | self.view_dist -= 10.0 243 | if self.view_dist < 1.0: 244 | self.view_dist = 1.0 245 | 246 | if keys_holding[pygame.K_1]: 247 | self.draw_dist -= 10.0 248 | if keys_holding[pygame.K_2]: 249 | self.draw_dist += 10.0 250 | 251 | self.clock.tick() 252 | -------------------------------------------------------------------------------- /futdoomlib/scripts/generate_random_map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import random 5 | 6 | try: 7 | size = int(sys.argv[1]) 8 | except Exception: 9 | size = 50 10 | 11 | colors_only = len(sys.argv) > 2 12 | 13 | if colors_only: 14 | tiling_textures = ['HSV {} {} {}'.format(random.random() * 360.0, 15 | random.random() / 2.0 + 0.5, 16 | random.random() / 2.0 + 0.5) 17 | for i in range(10)] 18 | else: 19 | tiling_textures = ['stones', 'flowers', 'bricks', 'lines', 'squares0', 'squares1'] 20 | 21 | def t(): 22 | return random.choice(tiling_textures) 23 | 24 | letters = 'abcdefghij' 25 | 26 | for letter in letters: 27 | print(''' 28 | {}N= 29 | floor:{} 30 | ceiling:{} 31 | height:N 32 | walls:@standard_walls 33 | wall:{} 34 | number:N\ 35 | '''.format(letter, t(), t(), t())) 36 | 37 | 38 | m = [[' ' for _ in range(size)] 39 | for _ in range(size)] 40 | 41 | 42 | ns = {} 43 | 44 | p = (random.randrange(size), random.randrange(size)) 45 | for i in range(size * size // 3): 46 | x, y = p 47 | if x >= size: 48 | x = size - 1 49 | if x < 0: 50 | x = 0 51 | if y >= size: 52 | y = size - 1 53 | if y < 0: 54 | y = 0 55 | neighbors = [(x, y - 1), (x, y + 1), (x - 1, y), (x + 1, y)] 56 | n_choices = list(filter(bool, (ns.get(p) for p in neighbors))) 57 | if not n_choices: 58 | n = random.randint(2, 6) 59 | else: 60 | n = random.choice(n_choices) + random.choice([-1, 0, +1]) 61 | if n > 9: 62 | n = 0 63 | if n < 2: 64 | n = 2 65 | ns[p] = n 66 | letter = random.choice(letters) 67 | m[y][x] = '{}{}'.format(letter, n) 68 | neighbors_not_visited = list(filter(lambda p: p not in ns, neighbors)) 69 | if neighbors_not_visited: 70 | p = random.choice(neighbors_not_visited) 71 | else: 72 | p = (random.randrange(size), random.randrange(size)) 73 | 74 | print('\n') 75 | for y in range(size): 76 | for x in range(size): 77 | print(m[y][x], end='') 78 | print('') 79 | -------------------------------------------------------------------------------- /futfly.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os.path 5 | import math 6 | import random 7 | import argparse 8 | import time 9 | import itertools 10 | import functools 11 | 12 | import pygame 13 | 14 | import futracer 15 | 16 | 17 | class FutFly: 18 | def __init__(self, size=None, render_approach=None): 19 | if size is None: 20 | size = (800, 600) 21 | self.size = size 22 | if render_approach is None: 23 | render_approach = 'segmented' 24 | self.render_approach = render_approach 25 | self.view_dist = 600.0 26 | self.draw_dist = 20000.0 27 | if render_approach != 'segmented': 28 | self.draw_dist /= 10 # The others are really that much slower. 29 | self.n_draw_rects = [1, 1] 30 | 31 | def fly(self): 32 | # Setup pygame. 33 | pygame.init() 34 | pygame.display.set_caption('futfly') 35 | self.screen = pygame.display.set_mode(self.size) 36 | self.surface = pygame.Surface(self.size, depth=32) 37 | self.font = pygame.font.Font(None, 36) 38 | self.clock = pygame.time.Clock() 39 | 40 | # Load the library. 41 | self.racer = futracer.FutRacer() 42 | 43 | # Actually run! 44 | return self.loop() 45 | 46 | def message(self, what, where): 47 | text = self.font.render(what, 1, (255, 255, 255)) 48 | self.screen.blit(text, where) 49 | 50 | def terrain(self): 51 | depth = 100 52 | width = 100 53 | size = 300 54 | size_vert = size / 2**0.5 55 | point_rows = [] 56 | 57 | # Make points. 58 | for i in range(depth): 59 | row = [] 60 | indent = (i % 2) * (size / 2) 61 | for j in range(width): 62 | y = 0 63 | x = j * size + indent 64 | z = i * size_vert 65 | row.append([x, y, z]) 66 | point_rows.append(row) 67 | 68 | # Make spikes. 69 | for c in range(int(depth * width)): 70 | i = random.randrange(1, depth - 1) 71 | j = random.randrange(1, width - 1) 72 | fluct = 1000.0 73 | point_rows[j][i][1] = random.random() * fluct - fluct / 2.0 74 | 75 | # Smooth areas. 76 | for c in range(int(depth * width)): 77 | i = random.randrange(1, depth - 1) 78 | j = random.randrange(1, width - 1) 79 | 80 | avg = sum(point_rows[i + io][j + jo][1] for jo, io in 81 | ((-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1))) / 8.0 82 | point_rows[i][j][1] = avg 83 | 84 | # Make triangles. 85 | triangle_rows = [] 86 | for row0, row1, i in zip(point_rows, point_rows[1:], range(depth)): 87 | if i % 2 == 1: 88 | row0, row1 = row1, row0 89 | triangle_row = [] 90 | for i in range(0, width - 1): 91 | for p0, p1, p2 in [(row0[i], row0[i + 1], row1[i]), 92 | (row1[i], row1[i + 1], row0[i + 1])]: 93 | hsv = [random.random() * 360.0, 94 | random.random() * 0.5 + 0.5, 95 | random.random() * 0.5 + 0.5] 96 | surface = [1, hsv, -1] 97 | triangle = [p0, p1, p2, surface] 98 | triangle_row.append(triangle) 99 | triangle_rows.append(triangle_row) 100 | 101 | def h_avg(h0, h1): 102 | h0, h1 = (h0, h1) if h0 < h1 else (h1, h0) 103 | diff_a = h1 - h0 104 | diff_b = h0 + 360.0 - h1 105 | h = h0 + diff_a / 2.0 if diff_a < diff_b else (h1 + diff_b / 2.0) % 360.0 106 | return h 107 | 108 | def hsv_avg(xs): 109 | h = functools.reduce(h_avg, (x[0] for x in xs)) 110 | s = sum(x[1] for x in xs) / len(xs) 111 | v = sum(x[2] for x in xs) / len(xs) 112 | return (h, s, v) 113 | 114 | # Smooth color transitions. 115 | for c in range(int(depth * width * 20)): 116 | i = random.randrange(1, depth - 2) 117 | j = random.randrange(1, width * 2 - 3) 118 | 119 | neigs = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)] 120 | avg = hsv_avg([triangle_rows[i + io][j + jo][3][1] 121 | for io, jo in neigs]) 122 | triangle_rows[i][j][3][1] = avg 123 | 124 | triangles = list(itertools.chain(*triangle_rows)) 125 | return triangles, [] 126 | 127 | def loop(self): 128 | triangles, textures = self.terrain() 129 | 130 | # The objects will not change (only the camera changes), so we 131 | # preprocess them to save important loop time. 132 | triangles_pre = self.racer.preprocess_triangles(triangles) 133 | textures_pre = self.racer.preprocess_textures(textures) 134 | 135 | camera = [[15000.0, -500.0, 0.0], [0.0, 0.0, 0.0]] 136 | 137 | keys_holding = {} 138 | for x in [pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT, 139 | pygame.K_PAGEUP, pygame.K_PAGEDOWN, pygame.K_1, pygame.K_2]: 140 | keys_holding[x] = False 141 | 142 | def inf_range(): 143 | i = 0 144 | while True: 145 | yield i 146 | i += 1 147 | 148 | for i in inf_range(): 149 | fps = self.clock.get_fps() 150 | 151 | time_start = time.time() 152 | frame = self.racer.render_triangles( 153 | self.size, self.view_dist, self.draw_dist, camera, 154 | None, triangles_pre, 155 | None, textures_pre, self.render_approach, self.n_draw_rects) 156 | time_end = time.time() 157 | time_start_get = time.time() 158 | frame = frame.get() 159 | time_end_get = time.time() 160 | 161 | futhark_dur_ms = (time_end - time_start) * 1000 162 | futhark_get_ms = (time_end_get - time_start_get) * 1000 163 | pygame.surfarray.blit_array(self.surface, frame) 164 | self.screen.blit(self.surface, (0, 0)) 165 | 166 | self.message('FPS: {:.02f}'.format(fps), (10, 10)) 167 | self.message('Futhark: {:.02f} ms (memory get: {:.02f} ms)'.format(futhark_dur_ms, futhark_get_ms), (10, 40)) 168 | self.message('Draw distance: {:.02f}'.format(self.draw_dist), (10, 70)) 169 | self.message('Rendering approach: {}'.format(self.render_approach), (10, 100)) 170 | if self.render_approach == 'chunked': 171 | self.message('# draw rects: x: {}, y: {}'.format( 172 | *self.n_draw_rects), (10, 130)) 173 | 174 | pygame.display.flip() 175 | 176 | # Check events. 177 | for event in pygame.event.get(): 178 | if event.type == pygame.QUIT: 179 | return 0 180 | 181 | elif event.type == pygame.KEYDOWN: 182 | if event.key == pygame.K_q: 183 | return 0 184 | if event.key in keys_holding.keys(): 185 | keys_holding[event.key] = True 186 | 187 | if event.key == pygame.K_r: 188 | self.render_approach = futracer.next_elem(futracer.render_approaches, 189 | self.render_approach) 190 | if event.key == pygame.K_a: 191 | self.n_draw_rects[0] = max(1, self.n_draw_rects[0] - 1) 192 | if event.key == pygame.K_d: 193 | self.n_draw_rects[0] = self.n_draw_rects[0] + 1 194 | if event.key == pygame.K_w: 195 | self.n_draw_rects[1] = max(1, self.n_draw_rects[1] - 1) 196 | if event.key == pygame.K_s: 197 | self.n_draw_rects[1] = self.n_draw_rects[1] + 1 198 | 199 | elif event.type == pygame.KEYUP: 200 | if event.key in keys_holding.keys(): 201 | keys_holding[event.key] = False 202 | 203 | if keys_holding[pygame.K_UP]: 204 | camera[1][0] -= 0.02 205 | if keys_holding[pygame.K_DOWN]: 206 | camera[1][0] += 0.02 207 | 208 | # The turning code below will not work without some axis-changing 209 | # math, so I'll just let it stay until someone figures that out. 210 | 211 | # if keys_holding[pygame.K_LEFT]: 212 | # camera[1][1] -= 0.02 213 | # if keys_holding[pygame.K_RIGHT]: 214 | # camera[1][1] += 0.02 215 | 216 | if keys_holding[pygame.K_PAGEUP]: 217 | self.view_dist += 10.0 218 | if keys_holding[pygame.K_PAGEDOWN]: 219 | self.view_dist -= 10.0 220 | if self.view_dist < 1.0: 221 | self.view_dist = 1.0 222 | 223 | if keys_holding[pygame.K_1]: 224 | self.draw_dist -= 10.0 225 | if keys_holding[pygame.K_2]: 226 | self.draw_dist += 10.0 227 | 228 | # Always fly forwards. 229 | p1 = camera[0][:] 230 | p1[2] += 10 231 | p2 = self.racer.rotate_point(camera[1], camera[0], p1) 232 | camera[0] = list(p2) 233 | 234 | self.clock.tick() 235 | 236 | def main(args): 237 | def size(s): 238 | return tuple(map(int, s.split('x'))) 239 | 240 | arg_parser = argparse.ArgumentParser(description='Use the up and down arrow keys to fly. Use Page Up and Page Down to adjust the view distance.') 241 | arg_parser.add_argument('--size', type=size, metavar='WIDTHxHEIGHT', 242 | help='set the size of the racing game window') 243 | arg_parser.add_argument('--render-approach', 244 | choices=futracer.render_approaches, 245 | default='segmented', 246 | help='choose how to render a frame') 247 | 248 | args = arg_parser.parse_args(args) 249 | 250 | fly = FutFly(size=args.size, render_approach=args.render_approach) 251 | return fly.fly() 252 | 253 | if __name__ == '__main__': 254 | sys.exit(main(sys.argv[1:])) 255 | -------------------------------------------------------------------------------- /futracer.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import colorsys 3 | 4 | import numpy 5 | import png 6 | 7 | import futracerlib 8 | 9 | 10 | render_approaches_map = { 11 | 'segmented': 1, 12 | 'chunked': 2, 13 | 'scatter_bbox': 3, 14 | } 15 | render_approaches = list(render_approaches_map.keys()) 16 | 17 | def next_elem(xs, y): 18 | for i in range(len(xs)): 19 | if xs[i] == y: 20 | return xs[(i + 1) % len(xs)] 21 | 22 | class FutRacer: 23 | def __init__(self): 24 | self.futhark = futracerlib.futracerlib() 25 | 26 | def rgb8_to_hsv(self, rgb8): 27 | r8, g8, b8 = rgb8 28 | f = 255.0 29 | r, g, b = r8 / f, g8 / f, b8 / f 30 | h1, s1, v1 = colorsys.rgb_to_hsv(r, g, b) 31 | h360 = h1 * 360.0 32 | return (h360, s1, v1) 33 | 34 | def translate_point(self, move, point): 35 | args = move + point 36 | return self.futhark.translate_point_raw(*args) 37 | 38 | def translate_triangle(self, angles, origo, triangle): 39 | return [self.translate_point(angles, origo, p) 40 | for p in triangle[:3]] + triangle[3:] 41 | 42 | def rotate_point(self, angles, origo, point): 43 | args = angles + origo + point 44 | return self.futhark.rotate_point_raw(*args) 45 | 46 | def rotate_triangle(self, angles, origo, triangle): 47 | return [self.rotate_point(angles, origo, p) 48 | for p in triangle[:3]] + triangle[3:] 49 | 50 | def to_numpy(self, xs, typ): 51 | return numpy.fromiter(xs, dtype=typ) 52 | 53 | def preprocess_triangles(self, triangles): 54 | p0s = [t[0] for t in triangles] 55 | p1s = [t[1] for t in triangles] 56 | p2s = [t[2] for t in triangles] 57 | ss = [t[3] for t in triangles] 58 | 59 | x0s = self.to_numpy((p[0] for p in p0s), 'float32') 60 | y0s = self.to_numpy((p[1] for p in p0s), 'float32') 61 | z0s = self.to_numpy((p[2] for p in p0s), 'float32') 62 | 63 | x1s = self.to_numpy((p[0] for p in p1s), 'float32') 64 | y1s = self.to_numpy((p[1] for p in p1s), 'float32') 65 | z1s = self.to_numpy((p[2] for p in p1s), 'float32') 66 | 67 | x2s = self.to_numpy((p[0] for p in p2s), 'float32') 68 | y2s = self.to_numpy((p[1] for p in p2s), 'float32') 69 | z2s = self.to_numpy((p[2] for p in p2s), 'float32') 70 | 71 | s_types = self.to_numpy((s[0] for s in ss), 'int32') 72 | s_hsv_hs = self.to_numpy((s[1][0] for s in ss), 'float32') 73 | s_hsv_ss = self.to_numpy((s[1][1] for s in ss), 'float32') 74 | s_hsv_vs = self.to_numpy((s[1][2] for s in ss), 'float32') 75 | s_indices = self.to_numpy((s[2] for s in ss), 'int32') 76 | 77 | return (x0s, y0s, z0s, x1s, y1s, z1s, x2s, y2s, z2s, 78 | s_types, s_hsv_hs, s_hsv_ss, s_hsv_vs, s_indices) 79 | 80 | def load_double_texture(self, path): 81 | texture_raw = png.Reader(filename=path) 82 | t_w, t_h, t_pixels, t_meta = texture_raw.asRGB8() 83 | n_channels = 3 84 | rgbs_1d = numpy.fromiter(itertools.chain(*t_pixels), dtype='uint8') 85 | rgbs_2d = numpy.reshape(rgbs_1d, (t_h * t_w, n_channels)) 86 | hsvs_2d = numpy.fromiter(itertools.chain(*map(self.rgb8_to_hsv, rgbs_2d)), dtype='float32') 87 | hsvs_3d = numpy.reshape(hsvs_2d, (t_h, t_w, n_channels)) 88 | return hsvs_3d 89 | 90 | def preprocess_textures(self, textures): 91 | if len(textures) == 0: 92 | # The values will not be used. 93 | texture_w = 0 94 | texture_h = 0 95 | else: 96 | # Assume all textures have the same size. They must have! 97 | # Otherwise something will fail later on. 98 | t = textures[0] 99 | texture_h, texture_w, _channels = t.shape 100 | 101 | if len(textures) > 0: 102 | s_textures = numpy.array(textures) 103 | s_textures_hs = s_textures[:,:,:,0] 104 | s_textures_hs_flat = numpy.reshape( 105 | s_textures_hs, 106 | len(textures) * texture_h * texture_w) 107 | s_textures_hs_flat = self.to_numpy(s_textures_hs_flat, 'float32') 108 | s_textures_ss = s_textures[:,:,:,1] 109 | s_textures_ss_flat = numpy.reshape( 110 | s_textures_ss, 111 | len(textures) * texture_h * texture_w).astype('float32') 112 | s_textures_ss_flat = self.to_numpy(s_textures_ss_flat, 'float32') 113 | s_textures_vs = s_textures[:,:,:,2] 114 | s_textures_vs_flat = numpy.reshape( 115 | s_textures_vs, 116 | len(textures) * texture_h * texture_w).astype('float32') 117 | s_textures_vs_flat = self.to_numpy(s_textures_vs_flat, 'float32') 118 | else: 119 | s_textures_hs_flat = numpy.empty((0,)).astype('float32') 120 | s_textures_ss_flat = numpy.empty((0,)).astype('float32') 121 | s_textures_vs_flat = numpy.empty((0,)).astype('float32') 122 | 123 | return (len(textures), texture_w, texture_h, 124 | s_textures_hs_flat, s_textures_ss_flat, s_textures_vs_flat) 125 | 126 | def render_triangles_preprocessed(self, size, view_dist, draw_dist, camera, 127 | triangles_pre, textures_pre, 128 | render_approach='segmented', 129 | n_draw_rects=(1, 1)): 130 | w, h = size 131 | 132 | ((c_x, c_y, c_z), (c_ax, c_ay, c_az)) = camera 133 | 134 | (x0s, y0s, z0s, x1s, y1s, z1s, x2s, y2s, z2s, 135 | s_types, s_hsv_hs, s_hsv_ss, s_hsv_vs, s_indices) = triangles_pre 136 | 137 | (textures_len, texture_w, texture_h, 138 | s_textures_hs_flat, s_textures_ss_flat, s_textures_vs_flat) = textures_pre 139 | 140 | render_approach = render_approaches_map[render_approach] 141 | 142 | return self.futhark.render_triangles_raw( 143 | render_approach, n_draw_rects[0], n_draw_rects[1], 144 | w, h, view_dist, draw_dist, 145 | x0s, y0s, z0s, x1s, y1s, z1s, x2s, y2s, z2s, 146 | s_types, s_hsv_hs, s_hsv_ss, s_hsv_vs, s_indices, 147 | textures_len, texture_w, texture_h, 148 | s_textures_hs_flat, s_textures_ss_flat, s_textures_vs_flat, 149 | c_x, c_y, c_z, c_ax, c_ay, c_az) 150 | 151 | def render_triangles(self, size, view_dist, draw_dist, camera, 152 | triangles, triangles_pre, 153 | textures, textures_pre, 154 | render_approach='segmented', 155 | n_draw_rects=(1, 1)): 156 | if triangles is None: 157 | triangles_pre1 = triangles_pre 158 | else: 159 | triangles_pre0 = self.preprocess_triangles(triangles) 160 | triangles_pre1 = [numpy.array.concatenate((x, y)) 161 | for x, y in zip(triangles_pre0, triangles_pre)] 162 | if textures is None: 163 | textures_pre1 = textures_pre 164 | else: 165 | textures_pre0 = self.preprocess_triangles(textures) 166 | p0_length, p0_w, p0_h = textures_pre0[:3] 167 | p_length, p_w, p_h = textures_pre0[:3] 168 | length = p0_length + p_length 169 | assert p0_w == p_w 170 | assert p0_h == p_h 171 | textures_pre0_arrays = textures_pre0[3:] 172 | textures_pre_arrays = textures_pre[3:] 173 | triangles_pre1_arrays = [numpy.array.concatenate((x, y)) 174 | for x, y in zip(textures_pre0_arrays, textures_pre_arrays)] 175 | triangles_pre1 = [length, p0_w, p0_h] + triangles_pre1_arrays 176 | return self.render_triangles_preprocessed( 177 | size, view_dist, draw_dist, camera, 178 | triangles_pre1, textures_pre1, 179 | render_approach, n_draw_rects) 180 | -------------------------------------------------------------------------------- /futracerlib.fut: -------------------------------------------------------------------------------- 1 | import "futracerlib/color" 2 | import "futracerlib/transformations" 3 | import "futracerlib/render_types" 4 | import "futracerlib/render" 5 | 6 | entry rotate_point_raw 7 | (angle_x: f32) (angle_y: f32) (angle_z: f32) 8 | (x_origo: f32) (y_origo: f32) (z_origo: f32) 9 | (x: f32) (y: f32) (z: f32): (f32, f32, f32) = 10 | let {x, y, z} = rotate_point {x=angle_x, y=angle_y, z=angle_z} 11 | {x=x_origo, y=y_origo, z=z_origo} 12 | {x=x, y=y, z=z} 13 | in (x, y, z) 14 | 15 | entry translate_point_raw 16 | (x_move: f32) (y_move: f32) (z_move: f32) 17 | (x: f32) (y: f32) (z: f32): (f32, f32, f32) = 18 | let {x, y, z} = translate_point {x=x_move, y=y_move, z=z_move} 19 | {x=x, y=y, z=z} 20 | in (x, y, z) 21 | 22 | entry render_triangles_raw 23 | [n] 24 | (render_approach: render_approach_id) 25 | (n_draw_rects_x: i32) 26 | (n_draw_rects_y: i32) 27 | (w: i64) 28 | (h: i64) 29 | (view_dist: f32) 30 | (draw_dist: f32) 31 | (x0s: [n]f32) 32 | (y0s: [n]f32) 33 | (z0s: [n]f32) 34 | (x1s: [n]f32) 35 | (y1s: [n]f32) 36 | (z1s: [n]f32) 37 | (x2s: [n]f32) 38 | (y2s: [n]f32) 39 | (z2s: [n]f32) 40 | (surface_types: [n]surface_type) 41 | (surface_hsv_hs: [n]f32) 42 | (surface_hsv_ss: [n]f32) 43 | (surface_hsv_vs: [n]f32) 44 | (surface_indices: [n]i32) 45 | (surface_n: i64) 46 | (surface_w: i64) 47 | (surface_h: i64) 48 | (surface_textures_flat_hsv_hs: []f32) 49 | (surface_textures_flat_hsv_ss: []f32) 50 | (surface_textures_flat_hsv_vs: []f32) 51 | (c_x: f32) 52 | (c_y: f32) 53 | (c_z: f32) 54 | (c_ax: f32) 55 | (c_ay: f32) 56 | (c_az: f32): [w][h]pixel = 57 | let n_draw_rects = (n_draw_rects_x, n_draw_rects_y) 58 | let camera = ({x=c_x, y=c_y, z=c_z}, {x=c_ax, y=c_ay, z=c_az}) 59 | let p0s = map3 (\x y z -> {x=x, y=y, z=z}) x0s y0s z0s 60 | let p1s = map3 (\x y z -> {x=x, y=y, z=z}) x1s y1s z1s 61 | let p2s = map3 (\x y z -> {x=x, y=y, z=z}) x2s y2s z2s 62 | let triangles = zip3 p0s p1s p2s 63 | let surface_hsvs = zip3 surface_hsv_hs surface_hsv_ss surface_hsv_vs 64 | let surfaces = zip3 surface_types surface_hsvs surface_indices 65 | let surface_textures = unflatten_3d surface_n surface_h surface_w 66 | (zip3 surface_textures_flat_hsv_hs 67 | surface_textures_flat_hsv_ss 68 | surface_textures_flat_hsv_vs) 69 | let triangles_with_surfaces = zip triangles surfaces 70 | in render_triangles_in_view render_approach n_draw_rects 71 | camera triangles_with_surfaces 72 | surface_textures w h view_dist draw_dist 73 | -------------------------------------------------------------------------------- /futracerlib/.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /futracerlib/build_triangles.fut: -------------------------------------------------------------------------------- 1 | import "render_types" 2 | 3 | import "lib/github.com/diku-dk/segmented/segmented" 4 | 5 | -- All of this is heavily copied from Martin Elsman's 6 | -- https://github.com/melsman/canvas demo. 7 | 8 | let bubble (a: point_projected) (b: point_projected): (point_projected, point_projected) = 9 | if b.y < a.y then (b, a) else (a, b) 10 | 11 | let normalize ((p, q, r): triangle_projected): triangle_projected = 12 | let (p, q) = bubble p q 13 | let (q, r) = bubble q r 14 | let (p, q) = bubble p q 15 | in (p, q, r) 16 | 17 | let lines_in_triangle (((p, _, r), _): (triangle_projected, i64)): i64 = 18 | i64.i32 (r.y - p.y + 1) 19 | 20 | let dxdy (a: point_projected) (b: point_projected): f32 = 21 | let dx = b.x - a.x 22 | let dy = b.y - a.y 23 | in if dy == 0 then f32.i32 0 24 | else f32.i32 dx f32./ f32.i32 dy 25 | 26 | let get_line_in_triangle (((p, q, r), ix): (triangle_projected, i64)) (i: i64): (line, i64) = 27 | let i = i32.i64 i 28 | let y = p.y + i 29 | in if i <= q.y - p.y then -- upper half 30 | let sl1 = dxdy p q 31 | let sl2 = dxdy p r 32 | let x1 = p.x + i32.f32 (sl1 * f32.i32 i) 33 | let x2 = p.x + i32.f32 (sl2 * f32.i32 i) 34 | in (({x=x1, y}, {x=x2,y}), ix) 35 | else -- lower half 36 | let sl1 = dxdy r p 37 | let sl2 = dxdy r q 38 | let dy = (r.y - p.y) - i 39 | let x1 = r.x - i32.f32 (sl1 * f32.i32 dy) 40 | let x2 = r.x - i32.f32 (sl2 * f32.i32 dy) 41 | in (({x=x1, y}, {x=x2, y}), ix) 42 | 43 | let lines_of_triangles [tn] (triangles: [tn]triangle_projected): [](line, i64) = 44 | let triangles' = map normalize triangles 45 | in expand lines_in_triangle get_line_in_triangle 46 | (zip triangles' (0.. v1 then 1 else if v1 > v2 then -1 else 0 52 | 53 | let slo ({x=x1, y=y1}: point) ({x=x2, y=y2}: point): f32 = 54 | if x2 == x1 then if y2 > y1 then r32 1 else r32 (-1) 55 | else r32 (y2 - y1) / r32 (i32.abs (x2 - x1)) 56 | 57 | let points_in_line ((({x=x1, y=y1}, {x=x2, y=y2}), _): (line, i64)): i64 = 58 | i64.i32 (i32.(1 + max (abs (x2 - x1)) (abs (y2 - y1)))) 59 | 60 | let get_point_in_line (((p1, p2), ix): (line, i64)) (i: i64): (point, i64) = 61 | let i = i32.i64 i in 62 | if i32.abs (p1.x - p2.x) > i32.abs (p1.y - p2.y) 63 | then let dir = compare p1.x p2.x 64 | let sl = slo p1 p2 65 | in ({x=p1.x + dir * i, 66 | y=p1.y + t32 (sl * r32 i)}, ix) 67 | else let dir = compare (p1.y) (p2.y) 68 | let sl = slo (swap p1) (swap p2) 69 | in ({x=p1.x + t32 (sl * r32 i), 70 | y=p1.y + i * dir}, ix) 71 | 72 | let points_of_lines (lines: [](line, i64)): [](point, i64) = 73 | expand points_in_line get_point_in_line lines 74 | -------------------------------------------------------------------------------- /futracerlib/color.fut: -------------------------------------------------------------------------------- 1 | import "misc" 2 | 3 | type pixel = u32 4 | type pixel_channel = u32 5 | type rgb = (pixel_channel, pixel_channel, pixel_channel) 6 | type hsv = (f32, f32, f32) 7 | 8 | let pixel_get_r (p: pixel): pixel_channel = 9 | (p >> 16u32) & 255u32 10 | 11 | let pixel_get_g (p: pixel): pixel_channel = 12 | (p >> 8u32) & 255u32 13 | 14 | let pixel_get_b (p: pixel): pixel_channel = 15 | p & 255u32 16 | 17 | let pixel_to_rgb (p: pixel): (pixel_channel, pixel_channel, pixel_channel) = 18 | (pixel_get_r p, pixel_get_g p, pixel_get_b p) 19 | 20 | let rgb_to_pixel (r: pixel_channel, g: pixel_channel, b: pixel_channel): pixel = 21 | (r << 16u32) | (g << 8u32) | b 22 | 23 | let hsv_to_rgb ((h, s, v): hsv): rgb = 24 | let c = v * s 25 | let h' = h / 60.0 26 | let x = c * (1.0 - f32.abs (fmod h' 2.0 - 1.0)) 27 | let (r0, g0, b0) = if 0.0 <= h' && h' < 1.0 28 | then (c, x, 0.0) 29 | else if 1.0 <= h' && h' < 2.0 30 | then (x, c, 0.0) 31 | else if 2.0 <= h' && h' < 3.0 32 | then (0.0, c, x) 33 | else if 3.0 <= h' && h' < 4.0 34 | then (0.0, x, c) 35 | else if 4.0 <= h' && h' < 5.0 36 | then (x, 0.0, c) 37 | else if 5.0 <= h' && h' < 6.0 38 | then (c, 0.0, x) 39 | else (0.0, 0.0, 0.0) 40 | let m = v - c 41 | let (r, g, b) = (r0 + m, g0 + m, b0 + m) 42 | in (u32.f32 (255.0 * r), u32.f32 (255.0 * g), u32.f32 (255.0 * b)) 43 | 44 | let hsv_average 45 | ((h0, s0, v0): hsv) 46 | ((h1, s1, v1): hsv) 47 | : hsv = 48 | let (h0, h1) = if h0 < h1 then (h0, h1) else (h1, h0) 49 | let diff_a = h1 - h0 50 | let diff_b = h0 + 360.0 - h1 51 | let h = if diff_a < diff_b 52 | then h0 + diff_a / 2.0 53 | else fmod (h1 + diff_b / 2.0) 360.0 54 | let s = (s0 + s1) / 2.0 55 | let v = (v0 + v1) / 2.0 56 | in (h, s, v) 57 | -------------------------------------------------------------------------------- /futracerlib/futhark.pkg: -------------------------------------------------------------------------------- 1 | require { 2 | github.com/diku-dk/segmented 0.4.2 #7bad610241494839d3976f6ee4991ba7af8e8c00 3 | } 4 | -------------------------------------------------------------------------------- /futracerlib/misc.fut: -------------------------------------------------------------------------------- 1 | module type racer_num = { 2 | type t 3 | } 4 | 5 | module racer (num: racer_num) = { 6 | type t = num.t 7 | 8 | type point2D = {x: t, y: t} 9 | type point3D = {x: t, y: t, z: t} 10 | type angles = {x: t, y: t, z: t} 11 | } 12 | 13 | module f32racer = racer { 14 | type t = f32 15 | } 16 | 17 | module i32racer = racer { 18 | type t = i32 19 | } 20 | 21 | let fmod (a: f32) (m: f32): f32 = 22 | a - r32 (t32 (a / m)) * m 23 | 24 | let in_range (t: i32) (a: i32) (b: i32): bool = 25 | (a < b && a <= t && t <= b) || (b <= a && b <= t && t <= a) 26 | 27 | let clamp (t: i32) (a: i32) (b: i32): i32 = 28 | i32.min (i32.max t a) b 29 | 30 | let within_bounds 31 | (smallest: i32) (highest: i32) 32 | (n: i32): i32 = 33 | i32.max smallest (i32.min highest n) 34 | -------------------------------------------------------------------------------- /futracerlib/render.fut: -------------------------------------------------------------------------------- 1 | import "misc" 2 | import "color" 3 | import "transformations" 4 | import "render_types" 5 | import "build_triangles" 6 | 7 | let normalize_triangle 8 | ((c, a): camera) 9 | ((p0, p1, p2): triangle) 10 | : triangle = 11 | let normalize_point (pa: f32racer.point3D): f32racer.point3D = 12 | let pb = translate_point {x= -c.x, y= -c.y, z= -c.z} pa 13 | let pc = rotate_point {x= -a.x, y= -a.y, z= -a.z} {x=0.0, y=0.0, z=0.0} pb 14 | in pc 15 | 16 | in (normalize_point p0, 17 | normalize_point p1, 18 | normalize_point p2) 19 | 20 | let project_triangle 21 | (w: i32) (h: i32) 22 | (view_dist: f32) 23 | (triangle: triangle) 24 | : triangle_projected = 25 | 26 | let project_point 27 | ({x, y, z}: f32racer.point3D) 28 | : i32racer.point2D = 29 | let z_ratio = if z >= 0.0 30 | then (view_dist + z) / view_dist 31 | else 1.0 / ((view_dist - z) / view_dist) 32 | let x_projected = x / z_ratio + r32 w / 2.0 33 | let y_projected = y / z_ratio + r32 h / 2.0 34 | in {x=t32 x_projected, y=t32 y_projected} 35 | 36 | let ({x=x0, y=y0, z=z0}, {x=x1, y=y1, z=z1}, {x=x2, y=y2, z=z2}) = triangle 37 | let {x=xp0, y=yp0} = project_point {x=x0, y=y0, z=z0} 38 | let {x=xp1, y=yp1} = project_point {x=x1, y=y1, z=z1} 39 | let {x=xp2, y=yp2} = project_point {x=x2, y=y2, z=z2} 40 | in ({x=xp0, y=yp0, z=z0}, {x=xp1, y=yp1, z=z1}, {x=xp2, y=yp2, z=z2}) 41 | 42 | let barycentric_coordinates 43 | ({x, y}: i32racer.point2D) 44 | (triangle: triangle_projected) 45 | : point_barycentric = 46 | let ({x=xp0, y=yp0, z=_}, {x=xp1, y=yp1, z=_}, {x=xp2, y=yp2, z=_}) = triangle 47 | let factor = (yp1 - yp2) * (xp0 - xp2) + (xp2 - xp1) * (yp0 - yp2) 48 | in if factor != 0 -- Avoid division by zero. 49 | then let a = ((yp1 - yp2) * (x - xp2) + (xp2 - xp1) * (y - yp2)) 50 | let b = ((yp2 - yp0) * (x - xp2) + (xp0 - xp2) * (y - yp2)) 51 | let c = factor - a - b 52 | let factor' = r32 factor 53 | let an = r32 a / factor' 54 | let bn = r32 b / factor' 55 | let cn = 1.0 - an - bn 56 | in (factor, {x=a, y=b, z=c}, {x=an, y=bn, z=cn}) 57 | else (1, {x= -1, y= -1, z= -1}, {x= -1.0, y= -1.0, z= -1.0}) -- Don't draw. 58 | 59 | let is_inside_triangle 60 | ((factor, {x=a, y=b, z=c}, _): point_barycentric) 61 | : bool = 62 | in_range a 0 factor && in_range b 0 factor && in_range c 0 factor 63 | 64 | let interpolate_z 65 | (triangle: triangle_projected) 66 | ((_factor, _, {x=an, y=bn, z=cn}): point_barycentric) 67 | : f32 = 68 | let ({x=_, y=_, z=z0}, {x=_, y=_, z=z1}, {x=_, y=_, z=z2}) = triangle 69 | in an * z0 + bn * z1 + cn * z2 70 | 71 | let color_point 72 | [texture_h][texture_w] 73 | (surface_textures: [][texture_h][texture_w]hsv) 74 | ((s_t, s_hsv, s_ti): surface) 75 | (z: f32) 76 | (bary: point_barycentric) 77 | : hsv = 78 | let (h, s, v) = 79 | if s_t == 1 80 | -- Use the color. 81 | then s_hsv 82 | else if s_t == 2 83 | -- Use the texture index. 84 | then let double_tex = #[unsafe] surface_textures[s_ti / 2] 85 | let ((xn0, yn0), (xn1, yn1), (xn2, yn2)) = 86 | if s_ti & 1 == 0 87 | then ((0.0, 0.0), 88 | (0.0, 1.0), 89 | (1.0, 0.0)) 90 | else ((1.0, 1.0), 91 | (1.0, 0.0), 92 | (0.0, 1.0)) 93 | -- FIXME: This results in a slightly distorted image, as it is based on 94 | -- the projected triangle, not the actual triangle. This is fine for 95 | -- small triangles, but noticable for large triangles. 96 | let {x=an, y=bn, z=cn} = bary.2 97 | let yn = an * yn0 + bn * yn1 + cn * yn2 98 | let xn = an * xn0 + bn * xn1 + cn * xn2 99 | let yi = t32 (yn * f32.i64 texture_h) 100 | let xi = t32 (xn * f32.i64 texture_w) 101 | let yi' = clamp yi 0 (i32.i64 texture_h - 1) 102 | let xi' = clamp xi 0 (i32.i64 texture_w - 1) 103 | in #[unsafe] double_tex[yi', xi'] 104 | else (0.0, 0.0, 0.0) -- unsupported input 105 | let flashlight_brightness = 2.0 * 10.0**6.0 106 | let v_factor = f32.min 1.0 (flashlight_brightness 107 | / (z ** 2.0)) 108 | in (h, s, v * v_factor) 109 | 110 | let render_triangles_chunked 111 | [tn][texture_h][texture_w] 112 | (triangles_projected: [tn]triangle_projected) 113 | (surfaces: [tn]surface) 114 | (surface_textures: [][texture_h][texture_w]hsv) 115 | (w: i64) (h: i64) 116 | ((n_rects_x, n_rects_y): (i32, i32)) 117 | : [w][h]pixel = 118 | let each_pixel 119 | [rtpn] 120 | (rect_triangles_projected: [rtpn]triangle_projected) 121 | (rect_surfaces: []surface) 122 | (pixel_index: i64): pixel = 123 | let p = {x=i32.i64 (pixel_index / h), y=i32.i64 (pixel_index % h)} 124 | let each_triangle 125 | (t: triangle_projected) 126 | (i: i32) 127 | : (bool, f32, i32) = 128 | let bary = barycentric_coordinates p t 129 | let in_triangle = is_inside_triangle bary 130 | let z = interpolate_z t bary 131 | in (in_triangle, z, i) 132 | 133 | let neutral_info = (false, -1.0, -1) 134 | let merge_colors 135 | ((in_triangle0, z0, i0): (bool, f32, i32)) 136 | ((in_triangle1, z1, i1): (bool, f32, i32)) 137 | : (bool, f32, i32) = 138 | if (in_triangle0 && z0 >= 0.0 && 139 | (z1 < 0.0 || !in_triangle1 || z0 < z1)) 140 | then (true, z0, i0) 141 | else if (in_triangle1 && z1 >= 0.0 && 142 | (z0 < 0.0 || !in_triangle0 || z1 < z0)) 143 | then (true, z1, i1) 144 | else if (in_triangle0 && z0 >= 0.0 && 145 | in_triangle1 && z1 >= 0.0 && z0 == z1) 146 | then (true, z0, i0) -- Just pick one of them. 147 | else neutral_info 148 | 149 | let triangles_infos = map2 each_triangle rect_triangles_projected (map i32.i64 (0..= x1b || y1a <= y0b || y0a >= y1b) 163 | 164 | let bounding_box 165 | (({x=x0, y=y0, z=_}, 166 | {x=x1, y=y1, z=_}, 167 | {x=x2, y=y2, z=_}): triangle_projected): rectangle = 168 | ({x=i32.min (i32.min x0 x1) x2, 169 | y=i32.min (i32.min y0 y1) y2}, 170 | {x=i32.max (i32.max x0 x1) x2, 171 | y=i32.max (i32.max y0 y1) y2}) 172 | 173 | -- Does a triangle intersect with a rectangle? FIXME: This might produce 174 | -- false positives (which is not a problem for the renderer, but could be more 175 | -- efficient). 176 | let triangle_in_rect 177 | (rect: rectangle) 178 | (tri: triangle_projected): bool = 179 | let rect1 = bounding_box tri 180 | in rect_in_rect rect1 rect || rect_in_rect rect rect1 181 | 182 | let each_rect 183 | [bn] 184 | (rect: rectangle) 185 | (pixel_indices: [bn]i64): [bn]pixel = 186 | let (rect_triangles_projected, rect_surfaces) = 187 | #[unsafe] unzip (filter (\(t, _) -> triangle_in_rect rect t) (zip triangles_projected surfaces)) 188 | in #[unsafe] map (each_pixel rect_triangles_projected rect_surfaces) pixel_indices 189 | 190 | let rect_pixel_indices (totallen: i64) 191 | (({x=x0, y=y0}, {x=x1, y=y1}): rectangle): [totallen]i64 = 192 | let (xlen, ylen) = (x1 - x0, y1 - y0) 193 | let xs = map (+ x0) (map i32.i64 (iota (i64.i32 xlen))) 194 | let ys = map (+ y0) (map i32.i64 (iota (i64.i32 ylen))) 195 | in flatten (map (\x -> map (\y -> i64.i32 x * h + i64.i32 y) ys) xs) :> [totallen]i64 196 | 197 | in if n_rects_x == 1 && n_rects_y == 1 198 | then 199 | -- Keep it simple. This will be a redomap. 200 | let pixel_indices = iota (w * h) 201 | let pixels = #[unsafe] map (each_pixel triangles_projected surfaces) pixel_indices 202 | in unflatten w h pixels 203 | else 204 | -- Split into rectangles, each with their own triangles, and use scatter in 205 | -- the end. 206 | let x_size = i32.i64 w / n_rects_x + i32.bool (i32.i64 w % n_rects_x > 0) 207 | let y_size = i32.i64 h / n_rects_y + i32.bool (i32.i64 h % n_rects_y > 0) 208 | let n_total = i64.i32 (n_rects_y * n_rects_x) 209 | let n_rects_y' = i64.i32 n_rects_y 210 | let n_rects_x' = i64.i32 n_rects_x 211 | let rects = flatten (map (\x -> map (\y -> 212 | let x0 = i32.i64 x * x_size 213 | let y0 = i32.i64 y * y_size 214 | let x1 = x0 + x_size 215 | let y1 = y0 + y_size 216 | in ({x=x0, y=y0}, {x=x1, y=y1})) 217 | (0.. [n_total]rectangle 219 | 220 | let pixel_indicess = #[unsafe] map (rect_pixel_indices (i64.i32 (x_size * y_size))) rects 221 | let pixelss = map2 each_rect rects pixel_indicess 222 | let pixel_indices = flatten pixel_indicess 223 | let n = length pixel_indices 224 | let pixels = flatten pixelss :> [n]u32 225 | let pixel_indices' = map (\i -> if i < w * h then i else -1) pixel_indices :> [n]i64 226 | let frame = replicate (w * h) 0u32 227 | let frame' = scatter frame pixel_indices' pixels 228 | in unflatten w h frame' 229 | 230 | let render_triangles_scatter_bbox 231 | [tn][texture_w][texture_h] 232 | (triangles_projected: [tn]triangle_projected) 233 | (surfaces: [tn]surface) 234 | (surface_textures: [][texture_h][texture_w]hsv) 235 | (w: i64) (h: i64) 236 | : [w][h]pixel = 237 | let bounding_box 238 | (({x=x0, y=y0, z=_z0}, {x=x1, y=y1, z=_z1}, {x=x2, y=y2, z=_z2}): triangle_projected) 239 | : rectangle = 240 | let (w, h) = (i32.i64 w, i32.i64 h) in 241 | ({x=within_bounds 0i32 (w - 1) (i32.min (i32.min x0 x1) x2), 242 | y=within_bounds 0i32 (h - 1) (i32.min (i32.min y0 y1) y2)}, 243 | {x=within_bounds 0i32 (w - 1) (i32.max (i32.max x0 x1) x2), 244 | y=within_bounds 0i32 (h - 1) (i32.max (i32.max y0 y1) y2)}) 245 | 246 | let merge_colors 247 | (i: i64) 248 | (z_cur: f32) 249 | (p_new: pixel) 250 | (z_new: f32) 251 | (in_triangle_new: bool) 252 | : (i64, pixel, f32) = 253 | if in_triangle_new && z_new >= 0.0 && (z_cur < 0.0 || z_new < z_cur) 254 | then (i, p_new, z_new) 255 | else (-1, 0u32, 0.0f32) 256 | 257 | let pixels_initial = replicate (w * h) 0u32 258 | let z_values_initial = replicate (w * h) f32.inf 259 | let (pixels, _z_values) = 260 | loop (pixels, z_values) = (pixels_initial, z_values_initial) 261 | for i < tn do 262 | let triangle_projected = triangles_projected[i] 263 | let surface = surfaces[i] 264 | 265 | let ({x=x_left, y=y_top}, {x=x_right, y=y_bottom}) = 266 | bounding_box triangle_projected 267 | let x_span = i64.i32 (x_right - x_left + 1) 268 | let y_span = i64.i32 (y_bottom - y_top + 1) 269 | let coordinates = flatten (map (\x -> map (\y -> {x, y}) 270 | (map (+ y_top) (map i32.i64 (iota y_span)))) 271 | (map (+ x_left) (map i32.i64 (iota x_span)))) 272 | let indices = map (\{x, y} -> i64.i32 x * h + i64.i32 y) coordinates 273 | 274 | let z_values_cur = map (\i -> #[unsafe] z_values[i]) indices 275 | 276 | let barys_new = map (\(p: i32racer.point2D): point_barycentric -> 277 | barycentric_coordinates p triangle_projected) 278 | coordinates 279 | 280 | let z_values_new = map (interpolate_z triangle_projected) barys_new 281 | 282 | let colors_new = map2 (color_point surface_textures surface) 283 | z_values_new barys_new 284 | let pixels_new = map (\x -> rgb_to_pixel (hsv_to_rgb x)) colors_new 285 | 286 | let is_insides_new = map is_inside_triangle barys_new 287 | 288 | let colors_merged = map5 merge_colors indices z_values_cur 289 | pixels_new z_values_new is_insides_new 290 | let (indices_merged, pixels_merged, z_values_merged) = unzip3 colors_merged 291 | 292 | let pixels' = scatter pixels indices_merged pixels_merged 293 | let z_values' = scatter z_values indices_merged z_values_merged 294 | in (pixels', z_values') 295 | let frame' = unflatten w h pixels 296 | in frame' 297 | 298 | let encode_loc_and_ix (loc: i32) (ix: i64): i64 = 299 | (i64.i32 loc << 32) | ix -- Let's hope it's within 32 bits. 300 | 301 | let decode_loc_and_ix (code: i64): (i32, i64) = 302 | (i32.i64 (code >> 32), code & 0x00000000ffffffff) 303 | 304 | let render_triangles_segmented 305 | [tn][texture_w][texture_h] 306 | (triangles_projected: [tn]triangle_projected) 307 | (surfaces: [tn]surface) 308 | (surface_textures: [][texture_h][texture_w]hsv) 309 | (w: i64) (h: i64) 310 | : [w][h]pixel = 311 | let (w', h') = (i32.i64 w, i32.i64 h) 312 | let lines = lines_of_triangles triangles_projected 313 | let points = points_of_lines lines 314 | let points' = filter (\({x, y}, _) -> x >= 0 && x < w' && y >=0 && y < h') points 315 | let indices = map (\({x, y}, _) -> i64.i32 x * h + i64.i32 y) points' 316 | let points'' = map (\({x, y}, ix) -> encode_loc_and_ix (x * i32.i64 h + y) ix) points' 317 | let empty_code = encode_loc_and_ix (-1) (-1) 318 | 319 | let update (code_a: i64) (code_b: i64): i64 = 320 | let ((loca, ia), (locb, ib)) = (decode_loc_and_ix code_a, 321 | decode_loc_and_ix code_b) 322 | in if ia == -1 323 | then code_b 324 | else if ib == -1 325 | then code_a 326 | else let (pa, pb) = ({x=loca / h', y=loca % h'}, {x=locb / h', y=locb % h'}) 327 | let (ta, tb) = #[unsafe] (triangles_projected[ia], triangles_projected[ib]) 328 | let (bary_a, bary_b) = (barycentric_coordinates pa ta, 329 | barycentric_coordinates pb tb) 330 | let (z_a, z_b) = (interpolate_z ta bary_a, interpolate_z tb bary_b) 331 | in if z_a < z_b 332 | then code_a 333 | else code_b 334 | 335 | let pixel_color (code: i64): u32 = 336 | let (loc, i) = decode_loc_and_ix code 337 | let p = {x=loc / h', y=loc % h'} 338 | in if i == -1 339 | then 0x00000000 340 | else 341 | let (t, s) = #[unsafe] (triangles_projected[i], surfaces[i]) 342 | let bary = barycentric_coordinates p t 343 | let z = interpolate_z t bary 344 | in let color = color_point surface_textures s z bary 345 | in rgb_to_pixel (hsv_to_rgb color) 346 | 347 | let pixels = replicate (w * h) empty_code 348 | let pixels' = reduce_by_index pixels update empty_code indices points'' 349 | let pixels'' = map pixel_color pixels' 350 | in unflatten w h pixels'' 351 | 352 | let render_triangles_in_view 353 | [texture_h][texture_w] 354 | (render_approach: render_approach_id) 355 | (n_draw_rects: (i32, i32)) 356 | (camera: camera) 357 | (triangles_with_surfaces: []triangle_with_surface) 358 | (surface_textures: [][texture_h][texture_w]hsv) 359 | (w: i64) (h: i64) 360 | (view_dist: f32) 361 | (draw_dist: f32) 362 | : [w][h]pixel = 363 | let (w', h') = (i32.i64 w, i32.i64 h) 364 | let (triangles, surfaces) = unzip triangles_with_surfaces 365 | let triangles_normalized = map (normalize_triangle camera) 366 | triangles 367 | let triangles_projected = map (project_triangle w' h' view_dist) 368 | triangles_normalized 369 | 370 | let close_enough_dist ({x=_, y=_, z}: point_projected): bool = 371 | 0.0 <= z && z < draw_dist 372 | 373 | let close_enough_fully_out_of_frame 374 | (({x=x0, y=y0, z=_z0}, {x=x1, y=y1, z=_z1}, {x=x2, y=y2, z=_z2}): triangle_projected): bool = 375 | (x0 < 0 && x1 < 0 && x2 < 0) || (x0 >= w' && x1 >= w' && x2 >= w') || 376 | (y0 < 0 && y1 < 0 && y2 < 0) || (y0 >= h' && y1 >= h' && y2 >= h') 377 | 378 | let close_enough (triangle: triangle_projected): bool = 379 | (close_enough_dist triangle.0 || 380 | close_enough_dist triangle.1 || 381 | close_enough_dist triangle.2) && 382 | !(close_enough_fully_out_of_frame triangle) 383 | 384 | let triangles_close = filter (close_enough <-< (.0)) 385 | (zip triangles_projected surfaces) 386 | let (triangles_projected', surfaces') = unzip triangles_close 387 | 388 | in if render_approach == 1 389 | then render_triangles_segmented triangles_projected' surfaces' surface_textures w h 390 | else if render_approach == 2 391 | then render_triangles_chunked triangles_projected' surfaces' 392 | surface_textures w h n_draw_rects 393 | else if render_approach == 3 394 | then render_triangles_scatter_bbox triangles_projected' surfaces' surface_textures w h 395 | else replicate w (replicate h 0u32) -- error 396 | -------------------------------------------------------------------------------- /futracerlib/render_types.fut: -------------------------------------------------------------------------------- 1 | import "misc" 2 | import "color" 3 | 4 | type render_approach_id = i32 5 | 6 | type triangle = (f32racer.point3D, f32racer.point3D, f32racer.point3D) 7 | type point_projected = {x: i32, y: i32, z: f32} 8 | type point = i32racer.point2D 9 | type line = (point, point) 10 | type triangle_projected = (point_projected, point_projected, point_projected) 11 | type point_barycentric = (i32, i32racer.point3D, f32racer.point3D) 12 | type camera = (f32racer.point3D, f32racer.angles) 13 | type rectangle = (i32racer.point2D, i32racer.point2D) 14 | 15 | -- If surface_type == 1, use the color in #1 surface. 16 | -- If surface_type == 2, use the surface from the index in #2 surface. 17 | type surface_type = i32 18 | type surface = (surface_type, hsv, i32) 19 | -- A double texture contains two textures: one in the upper left triangle, and 20 | -- one (backwards) in the lower right triangle. Use `texture_index / 2` to 21 | -- refer to the correct double texture. 22 | type~ surface_double_texture = [][]hsv 23 | type triangle_with_surface = (triangle, surface) 24 | -------------------------------------------------------------------------------- /futracerlib/transformations.fut: -------------------------------------------------------------------------------- 1 | import "misc" 2 | 3 | let rotate_point 4 | (angle: f32racer.angles) 5 | (origo: f32racer.point3D) 6 | (p: f32racer.point3D) 7 | : f32racer.point3D = 8 | let (x0, y0, z0) = (p.x - origo.x, p.y - origo.y, p.z - origo.z) 9 | 10 | let (sin_x, cos_x) = (f32.sin angle.x, f32.cos angle.x) 11 | let (sin_y, cos_y) = (f32.sin angle.y, f32.cos angle.y) 12 | let (sin_z, cos_z) = (f32.sin angle.z, f32.cos angle.z) 13 | 14 | -- X axis. 15 | let (x1, y1, z1) = (x0, 16 | y0 * cos_x - z0 * sin_x, 17 | y0 * sin_x + z0 * cos_x) 18 | -- Y axis. 19 | let (x2, y2, z2) = (z1 * sin_y + x1 * cos_y, 20 | y1, 21 | z1 * cos_y - x1 * sin_y) 22 | -- Z axis. 23 | let (x3, y3, z3) = (x2 * cos_z - y2 * sin_z, 24 | x2 * sin_z + y2 * cos_z, 25 | z2) 26 | 27 | let (x', y', z') = (origo.x + x3, origo.y + y3, origo.z + z3) 28 | in {x=x', y=y', z=z'} 29 | 30 | let translate_point 31 | (move: f32racer.point3D) 32 | (p: f32racer.point3D) 33 | : f32racer.point3D = 34 | {x=p.x + move.x, y=p.y + move.y, z=p.z + move.z} 35 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # Use this file with nix-shell or similar tools; see https://nixos.org/ 2 | with import {}; 3 | 4 | mkShell { 5 | buildInputs = [ 6 | pkgconfig 7 | ocl-icd 8 | opencl-headers 9 | SDL2 10 | SDL2_ttf 11 | (python3.withPackages (ppkgs: with ppkgs; [ setuptools numpy pygame pyopencl purepng ])) 12 | ]; 13 | } 14 | --------------------------------------------------------------------------------