| /* |
| 32blit Voxel Terrain demo |
| |
| by Jonathan Williamson (lowfatcode) |
| */ |
| |
| #include <cstring> |
| |
| #include "32blit.hpp" |
| #include "map.hpp" |
| |
| using namespace blit; |
| |
| |
| Pen sky_colour = Pen(127, 182, 212, 255); |
| Pen colour_map_palette[256]; |
| |
| uint16_t *tiles[32][32]; |
| std::vector<uint16_t *> free_tiles; |
| |
| constexpr uint16_t tile_cache_size = 120; |
| uint16_t tile_cache[32 * 32 * tile_cache_size]; |
| |
| Vec3 position(64, 64, 100); |
| float angle = 0.0f; |
| float lean = 0.0f; |
| float pitch = 0.0f; |
| |
| uint8_t visited[32][32]; |
| |
| uint32_t terrain_file; |
| uint8_t terrain_index = 1; |
| |
| |
| |
| void load_map() { |
| //close_file(terrain_file); |
| //terrain_file = open_file(std::to_string(terrain_index) + ".map"); |
| |
| // load palette data |
| //if(read_file(terrain_file, 0, 768, (char *)buffer) == 768) { |
| for(uint16_t i = 0; i < 256; i++) { |
| colour_map_palette[i] = Pen(demo_map[i * 3 + 0], demo_map[i * 3 + 1], demo_map[i * 3 + 2]); |
| } |
| //} |
| |
| for(uint8_t y = 0; y < 32; y++) { |
| for(uint8_t x = 0; x < 32; x++) { |
| tiles[x][y] = nullptr; |
| } |
| } |
| |
| free_tiles.clear(); |
| for(uint8_t i = 0; i < tile_cache_size; i++) { |
| free_tiles.push_back(&tile_cache[32 * 32 * i]); |
| } |
| } |
| |
| void init() { |
| set_screen_mode(ScreenMode::lores); |
| load_map(); |
| } |
| |
| void load_tile(int16_t x, int16_t y) { |
| uint32_t tile_size = 32 * 32 * 2; |
| uint32_t offset = 768 + ((x + y * 32) * tile_size); |
| |
| if(free_tiles.size() > 0) { |
| tiles[x][y] = free_tiles.back(); |
| free_tiles.pop_back(); |
| |
| memcpy((char *)tiles[x][y], demo_map + offset, tile_size); |
| |
| //read_file(terrain_file, offset, tile_size, (char *)tiles[x][y]); |
| } |
| } |
| |
| uint16_t get_sample(int16_t x, int16_t y) { |
| // work out the tile coordinates for this sample |
| uint8_t tx = (x >> 5) & 0x1f; |
| uint8_t ty = (y >> 5) & 0x1f; |
| |
| visited[tx][ty] = 1; |
| |
| if(tiles[tx][ty] == nullptr) { |
| return 0; |
| } |
| |
| // work out the coordinates (within the tile) for the sample |
| uint16_t sx = x & 0x1f; |
| uint16_t sy = y & 0x1f; |
| |
| return tiles[tx][ty][sx + sy * 32]; |
| } |
| |
| float deg2rad(float d) { |
| return d * pi / 180.0f; |
| } |
| |
| void draw_world(Vec3 position, float angle, float lean, float horizon, float near, float far) { |
| static int16_t height_buffer[160]; |
| |
| memset(visited, 0, 32 * 32); |
| |
| // reset the height buffer to the bottom of the screen |
| for(auto i = 0; i < screen.bounds.w; i++) { |
| height_buffer[i] = screen.bounds.h; |
| } |
| |
| // convert angle into radians |
| angle = deg2rad(angle); |
| float sina = sinf(angle); |
| float cosa = cosf(angle); |
| |
| // starting z value |
| float z = near; |
| |
| while(z < far) { |
| |
| // calculate the left and right Points for the current sample span |
| Vec2 frustrum_left = Vec2(-cosa * z - sina * z, sina * z - cosa * z); |
| Vec2 frustrum_right = Vec2( cosa * z - sina * z, -sina * z - cosa * z); |
| |
| // calculate the step size along the span for each screen column drawn |
| Vec2 sample_step((frustrum_right.x - frustrum_left.x) / screen.bounds.w, (frustrum_right.y - frustrum_left.y) / screen.bounds.w); |
| float lean_step = lean * 2.0f / screen.bounds.w; |
| |
| // pre-multiply the fog alpha for this z-distance |
| uint8_t fog_blend = (z / far / 1.5f) * 255; // value for "amount of fog" from 0 to 255 |
| Pen fog((sky_colour.r * fog_blend) >> 8, (sky_colour.g * fog_blend) >> 8, (sky_colour.b * fog_blend) >> 8); |
| fog_blend = 255 - fog_blend; |
| |
| // Point being sampled, starts on the left of the view frustrum and is stepped forward each |
| // screen column drawn until it reaches the right of the view frustrum |
| Vec2 sample_point = frustrum_left + Vec2(position.x, position.y); |
| float sample_lean = -lean; |
| |
| float invz = 1.0f / z * 100.0f; |
| |
| // for each column on the screen... |
| for(uint16_t i = 0; i < screen.bounds.w; i++) { |
| |
| uint16_t sample = get_sample(sample_point.x, sample_point.y); |
| uint8_t colour_index = sample >> 8; |
| |
| // convert the height map sample into a y coordinate on screen |
| int16_t height = (position.z - (sample & 0xff)) * invz + float(horizon) + sample_lean; |
| |
| // if the height is smaller (further up the screen) than our current height buffer |
| // value then we need to draw a new vertical strip |
| if(height < height_buffer[i]) { |
| // fetch the colour for this strip from the colour map |
| Pen colour = colour_map_palette[colour_index]; |
| |
| // blend terrain colour with pre-multiplied fog colour |
| colour.r = ((colour.r * fog_blend) >> 8) + fog.r; |
| colour.g = ((colour.g * fog_blend) >> 8) + fog.g; |
| colour.b = ((colour.b * fog_blend) >> 8) + fog.b; |
| |
| // draw the vertical strip |
| screen.pen = colour; |
| screen.v_span(Point(i, height), height_buffer[i] - height); |
| |
| // update the height buffer to save that we've draw this far |
| // up the screen |
| height_buffer[i] = height; |
| } |
| |
| // move to the next sampling coordinate |
| sample_point += sample_step; |
| sample_lean += lean_step; |
| } |
| |
| // move forward (into the distance) in increasingly large steps |
| z *= 1.025f; |
| } |
| } |
| |
| |
| |
| void render(uint32_t time_ms) { |
| |
| // draw the sky |
| screen.pen = sky_colour; |
| screen.clear(); |
| |
| uint32_t ms_start = now(); |
| draw_world( |
| position, // player position |
| angle, // player direction |
| lean, |
| 20.0f + pitch, // horizon position |
| 3.0f, // near distance |
| 300.0f // far distance |
| ); |
| uint32_t ms_end = now(); |
| |
| for(uint8_t y = 0; y < 32; y++) { |
| for(uint8_t x = 0; x < 32; x++) { |
| |
| if(tiles[x][y] != nullptr) { |
| screen.pen = Pen(255, 0, 0, 100); |
| }else{ |
| screen.pen = Pen(0, 0, 0, 100); |
| } |
| screen.pixel(Point(x, y)); |
| |
| if(visited[x][y]) { |
| screen.pen = Pen(255, 255, 0, 100); |
| screen.pixel(Point(x, y)); |
| } |
| } |
| } |
| |
| // work out the tile coordinates for the player position |
| uint8_t tx = (int32_t(position.x) >> 5) & 0x1f; |
| uint8_t ty = (int32_t(position.y) >> 5) & 0x1f; |
| screen.pen = Pen(0, 255, 0); |
| screen.pixel(Point(tx, ty)); |
| |
| // draw FPS meter & watermark |
| screen.watermark(); |
| screen.mask = nullptr; |
| screen.pen = Pen(255, 255, 255); |
| |
| screen.text(std::to_string(ms_end - ms_start), minimal_font, Point(1, 230)); |
| screen.pen = Pen(255, 0, 0); |
| for (int i = 0; i < uint16_t(ms_end - ms_start); i++) { |
| screen.pen = Pen(i * 5, 255 - (i * 5), 0); |
| screen.rectangle(Rect(i * 3 + 1, 237, 2, 2)); |
| } |
| } |
| |
| void update(uint32_t time_ms) { |
| static uint16_t tick = 0; |
| |
| tick++; |
| |
| // update angle of player based on joystick input |
| float old_angle = angle; |
| angle -= joystick.x * 2.0f; |
| |
| lean = (angle - old_angle) * 10.0f; |
| |
| if(buttons & DPAD_UP) { |
| position.z += 1.0f; |
| } |
| |
| if(buttons & DPAD_DOWN) { |
| position.z -= 1.0f; |
| } |
| |
| // move player location if joystick y axis is forward/backwards |
| position.x += sinf(deg2rad(angle)) * joystick.y; |
| position.y += cosf(deg2rad(angle)) * joystick.y; |
| pitch = joystick.y * 20.0f; |
| |
| position.x = position.x < 0.0f ? 1023.0f : position.x; |
| position.y = position.y < 0.0f ? 1023.0f : position.y; |
| |
| position.x = position.x > 1023.0f ? 0.0f : position.x; |
| position.y = position.y > 1023.0f ? 0.0f : position.y; |
| |
| |
| // remove any unused tiles from the cache |
| for(uint8_t y = 0; y < 32; y++) { |
| for(uint8_t x = 0; x < 32; x++) { |
| if(tiles[x][y] != nullptr && !visited[x][y]) { |
| free_tiles.push_back(tiles[x][y]); |
| tiles[x][y] = nullptr; |
| } |
| } |
| } |
| |
| // load the next needed tile into the cache |
| for(uint8_t y = 0; y < 32; y++) { |
| for(uint8_t x = 0; x < 32; x++) { |
| if(tiles[x][y] == nullptr && visited[x][y]) { |
| load_tile(x, y); |
| //return; // quit out early if we load a new tile |
| } |
| } |
| } |
| |
| if(buttons.released & A) { |
| terrain_index++; |
| if(terrain_index > 4) { |
| terrain_index = 1; |
| } |
| load_map(); |
| } |
| } |