mirror of
https://github.com/laanwj/k210-sdk-stuff.git
synced 2024-11-22 01:16:20 +04:00
rust: Add voxel rendering demo
This commit is contained in:
parent
d128fdd4a8
commit
e85a074309
@ -235,6 +235,13 @@ Experiments with `embedded-graphics` crate.
|
||||
|
||||
[README](rust/embgfx/README.md)
|
||||
|
||||
rust/voxel
|
||||
-------------
|
||||
|
||||
Old-school voxel-based landscape renderer.
|
||||
|
||||
[README](rust/voxel/README.md)
|
||||
|
||||
ROM re'ing
|
||||
===========
|
||||
|
||||
|
@ -15,6 +15,7 @@ members = [
|
||||
"sdlcd",
|
||||
"interrupt",
|
||||
"embgfx",
|
||||
"voxel",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
|
2
rust/voxel/.gitignore
vendored
Normal file
2
rust/voxel/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
**/*.rs.bk
|
12
rust/voxel/Cargo.toml
Normal file
12
rust/voxel/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "voxel"
|
||||
version = "0.1.0"
|
||||
authors = ["W.J. van der Laan <laanwj@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
riscv-rt = "0.6"
|
||||
k210-hal = "0.2.0"
|
||||
riscv = "0.5"
|
||||
k210-shared = { path = "../k210-shared" }
|
||||
libm = "0.1"
|
7
rust/voxel/README.md
Normal file
7
rust/voxel/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# `voxel`
|
||||
|
||||
Voxel-based landscape rendering.
|
||||
|
||||
Inspired by thie tweet: https://twitter.com/pimoroni/status/1215944540147851264
|
||||
Based on Sebastian Macke's excellent examples in https://github.com/s-macke/VoxelSpace
|
||||
|
BIN
rust/voxel/data/map1.dat
Normal file
BIN
rust/voxel/data/map1.dat
Normal file
Binary file not shown.
BIN
rust/voxel/data/map15.dat
Normal file
BIN
rust/voxel/data/map15.dat
Normal file
Binary file not shown.
47
rust/voxel/scripts/convert.py
Executable file
47
rust/voxel/scripts/convert.py
Executable file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
'''
|
||||
Convert map from set of PNG files to native format
|
||||
for inclusion.
|
||||
|
||||
Example maps can be found here:
|
||||
https://github.com/s-macke/VoxelSpace/tree/master/maps
|
||||
'''
|
||||
import sys
|
||||
from PIL import Image
|
||||
import struct
|
||||
|
||||
def rgb565(r, g, b):
|
||||
'''Truncate RGB888 color to RGB565'''
|
||||
return (((r) >> 3) << 11) | (((g) >> 2) << 5) | ((b) >> 3)
|
||||
|
||||
def convert_palette(pal):
|
||||
'''Convert 8-bit indexed image palette to RGB565'''
|
||||
return [rgb565(pal[i*3+0], pal[i*3+1], pal[i*3+2]) for i in range(256)]
|
||||
|
||||
color_fname = sys.argv[1]
|
||||
depth_fname = sys.argv[2]
|
||||
out_fname = sys.argv[3]
|
||||
|
||||
color_img = Image.open(color_fname)
|
||||
depth_img = Image.open(depth_fname)
|
||||
assert(color_img.size == (1024,1024))
|
||||
assert(depth_img.size == (1024,1024))
|
||||
assert(color_img.getbands() == ('P',))
|
||||
assert(len(color_img.getpalette()) == 768)
|
||||
assert(depth_img.getbands() == ('L',))
|
||||
|
||||
# downsample
|
||||
delta = 4
|
||||
size_out = (color_img.size[0] // delta, color_img.size[1] // delta)
|
||||
|
||||
out = []
|
||||
out += convert_palette(color_img.getpalette())
|
||||
for y in range(0, size_out[1]):
|
||||
for x in range(0, size_out[0]):
|
||||
pos = (x * delta, y * delta)
|
||||
val = color_img.getpixel(pos) | (depth_img.getpixel(pos) << 8)
|
||||
out.append(val)
|
||||
|
||||
with open(out_fname, 'wb') as f:
|
||||
for val in out:
|
||||
f.write(struct.pack('<H', val))
|
274
rust/voxel/src/main.rs
Normal file
274
rust/voxel/src/main.rs
Normal file
@ -0,0 +1,274 @@
|
||||
/*! Voxel renderer.
|
||||
* Based on Sebastian Macke's excellent examples in https://github.com/s-macke/VoxelSpace
|
||||
*/
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use libm::F32Ext;
|
||||
|
||||
use k210_hal::prelude::*;
|
||||
use k210_hal::stdout::Stdout;
|
||||
use k210_hal::Peripherals;
|
||||
use k210_shared::board::def::{io, DISP_HEIGHT, DISP_PIXELS, DISP_WIDTH};
|
||||
use k210_shared::board::lcd::{self, LCD, LCDHL};
|
||||
use k210_shared::board::lcd_colors;
|
||||
use k210_shared::board::lcd_render::ScreenImage;
|
||||
use k210_shared::soc::dmac::{dma_channel, DMACExt};
|
||||
use k210_shared::soc::fpioa;
|
||||
use k210_shared::soc::sleep::usleep;
|
||||
use k210_shared::soc::spi::SPIExt;
|
||||
use k210_shared::soc::sysctl;
|
||||
use riscv_rt::entry;
|
||||
|
||||
mod map_data;
|
||||
|
||||
/** Euclidian modulus. */
|
||||
pub fn mod_euc(a: i32, b: i32) -> i32 {
|
||||
let r = a % b;
|
||||
if r < 0 {
|
||||
r + b
|
||||
} else {
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
/** Minimum of two f32 values. */
|
||||
#[allow(dead_code)]
|
||||
fn fmin(a: f32, b: f32) -> f32 {
|
||||
if a < b {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
/** Maximum of two f32 values. */
|
||||
fn fmax(a: f32, b: f32) -> f32 {
|
||||
if a > b {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/** Connect pins to internal functions */
|
||||
fn io_mux_init() {
|
||||
/* Init SPI IO map and function settings */
|
||||
fpioa::set_function(io::LCD_RST, fpioa::function::gpiohs(lcd::RST_GPIONUM));
|
||||
fpioa::set_io_pull(io::LCD_RST, fpioa::pull::DOWN); // outputs must be pull-down
|
||||
fpioa::set_function(io::LCD_DC, fpioa::function::gpiohs(lcd::DCX_GPIONUM));
|
||||
fpioa::set_io_pull(io::LCD_DC, fpioa::pull::DOWN);
|
||||
fpioa::set_function(io::LCD_CS, fpioa::function::SPI0_SS3);
|
||||
fpioa::set_function(io::LCD_WR, fpioa::function::SPI0_SCLK);
|
||||
|
||||
sysctl::set_spi0_dvp_data(true);
|
||||
}
|
||||
|
||||
/** Set correct voltage for pins */
|
||||
fn io_set_power() {
|
||||
/* Set dvp and spi pin to 1.8V */
|
||||
sysctl::set_power_mode(sysctl::power_bank::BANK6, sysctl::io_power_mode::V18);
|
||||
sysctl::set_power_mode(sysctl::power_bank::BANK7, sysctl::io_power_mode::V18);
|
||||
}
|
||||
|
||||
/** R5G6B65 color value. */
|
||||
pub type Color = u16;
|
||||
|
||||
/** In-memory voxel map. */
|
||||
struct VoxelMap {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub palette: &'static [u16],
|
||||
pub data: &'static [u16],
|
||||
}
|
||||
|
||||
impl VoxelMap {
|
||||
/** Input consists of a 8-bit palette (256 * u16 R5G6B5) followed by two raw interleaved images of size width
|
||||
* by height (width * height * D8P8), as generated by the "convert.py" conversion script.
|
||||
*/
|
||||
pub fn new(width: u32, height: u32, data: &'static [u8]) -> Self {
|
||||
let data =
|
||||
unsafe { core::slice::from_raw_parts_mut(data.as_ptr() as *mut u16, data.len() / 2) };
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
palette: &data[0..256],
|
||||
data: &data[256..],
|
||||
}
|
||||
}
|
||||
|
||||
/** Sample color and depth at coordinates x,y
|
||||
* - wrap around repeat x and y
|
||||
*/
|
||||
pub fn sample(&self, x: f32, y: f32) -> (Color, u8) {
|
||||
// Get into 0..width / height range
|
||||
// TODO: this doesn't work due to lack of fmodf on the platform
|
||||
// let x = x % (self.width as f32);
|
||||
// let y = y % (self.height as f32);
|
||||
let x = mod_euc(x as i32, self.width as i32);
|
||||
let y = mod_euc(y as i32, self.height as i32);
|
||||
// Unpack value
|
||||
let ofs = y as usize * (self.width as usize) + x as usize;
|
||||
let val = self.data[ofs];
|
||||
(self.palette[(val & 0xff) as usize], (val >> 8) as u8)
|
||||
}
|
||||
}
|
||||
|
||||
/** Display image in directly DMA'able u32 per two pixels
|
||||
* format.
|
||||
*/
|
||||
struct Display {
|
||||
pub data: ScreenImage,
|
||||
}
|
||||
|
||||
/** Target for voxel-based rendering. This needs only one operation: draw
|
||||
* a vertical line.
|
||||
*/
|
||||
trait VoxelTarget {
|
||||
/** Draw a vertical line at x coordinate `cx`, from y coordinate `cy1` to `cy2`. */
|
||||
fn dvline(&mut self, cx: i32, cy1: i32, cy2: i32, color: Color);
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: [0; DISP_PIXELS / 2],
|
||||
}
|
||||
}
|
||||
|
||||
/** Image data as mutable [u16] for internal drawing use. */
|
||||
fn data(&mut self) -> &mut [u16] {
|
||||
unsafe { core::slice::from_raw_parts_mut(self.data.as_ptr() as *mut u16, DISP_PIXELS) }
|
||||
}
|
||||
}
|
||||
|
||||
impl VoxelTarget for Display {
|
||||
/** Draw a vertical line at x coordinate `cx`, from y coordinate `cy1` to `cy2`. */
|
||||
fn dvline(&mut self, cx: i32, cy1: i32, cy2: i32, color: Color) {
|
||||
let data = self.data();
|
||||
if cx < 0 || cx >= (DISP_WIDTH as i32) {
|
||||
return;
|
||||
}
|
||||
let xofs = (cx ^ 1) as usize;
|
||||
for y in cy1..cy2 {
|
||||
if y >= 0 && y < (DISP_HEIGHT as i32) {
|
||||
data[(y as usize) * (DISP_WIDTH as usize) + xofs] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
let p = Peripherals::take().unwrap();
|
||||
|
||||
sysctl::pll_set_freq(sysctl::pll::PLL0, 800_000_000).unwrap();
|
||||
sysctl::pll_set_freq(sysctl::pll::PLL1, 300_000_000).unwrap();
|
||||
sysctl::pll_set_freq(sysctl::pll::PLL2, 45_158_400).unwrap();
|
||||
let clocks = k210_hal::clock::Clocks::new();
|
||||
|
||||
usleep(200000);
|
||||
|
||||
// Configure UART
|
||||
let serial = p
|
||||
.UARTHS
|
||||
.configure((p.pins.pin5, p.pins.pin4), 115_200.bps(), &clocks);
|
||||
let (mut tx, _) = serial.split();
|
||||
|
||||
let mut stdout = Stdout(&mut tx);
|
||||
|
||||
io_mux_init();
|
||||
io_set_power();
|
||||
|
||||
writeln!(stdout, "Init DMAC").unwrap();
|
||||
let dmac = p.DMAC.configure();
|
||||
let chan = dma_channel::CHANNEL0;
|
||||
writeln!(
|
||||
stdout,
|
||||
"DMAC: id 0x{:x} version 0x{:x} AXI ID 0x{:x}",
|
||||
dmac.read_id(),
|
||||
dmac.read_version(),
|
||||
dmac.read_channel_id(chan)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let map = VoxelMap::new(map_data::WIDTH, map_data::HEIGHT, map_data::VOXEL_MAP);
|
||||
|
||||
let spi = p.SPI0.constrain();
|
||||
let mut lcd = LCD::new(spi, &dmac, chan);
|
||||
lcd.init();
|
||||
lcd.set_direction(lcd::direction::YX_LRUD);
|
||||
lcd.clear(lcd_colors::PURPLE);
|
||||
|
||||
// Some renderer constants:
|
||||
let sky_color = lcd_colors::rgb565(50, 100, 200); // Color of sky
|
||||
let horizon = (DISP_HEIGHT / 2) as f32; // y coordinate of horizon on screen
|
||||
let scale_height = 50.0; // Scaling factor for mountain heights
|
||||
let distance = 256; // Rendering distance
|
||||
|
||||
writeln!(stdout, "First frame").unwrap();
|
||||
let mut disp = Display::new();
|
||||
// "Player" position
|
||||
let mut p = (0.0, 0.0);
|
||||
// "Player" rotation
|
||||
let mut phi = 0.0f32;
|
||||
loop {
|
||||
// Orientation to variables for 2D rotation matrix.
|
||||
let sinphi = phi.sin();
|
||||
let cosphi = phi.cos();
|
||||
// Derive current player height from landscape height at the current
|
||||
// position.
|
||||
// XXX: it'd be nice to apply some kind of low-pass filter here
|
||||
// to prevent ugly sudden jumps, while not lying into mountains.
|
||||
let (_, p_depth) = map.sample(p.0, p.1);
|
||||
let height = fmax(p_depth as f32 + 20.0, 50.0);
|
||||
// Render landscape!
|
||||
let mut ybuffer = [DISP_HEIGHT as i32; DISP_WIDTH as usize];
|
||||
for z in 1..distance {
|
||||
// Compute both end-points of (rotated) line segment
|
||||
// that represents this horizontal display line on map.
|
||||
let z = z as f32;
|
||||
let mut pleft = (
|
||||
(-cosphi * z - sinphi * z) + p.0,
|
||||
(sinphi * z - cosphi * z) + p.1,
|
||||
);
|
||||
let pright = (
|
||||
(cosphi * z - sinphi * z) + p.0,
|
||||
(-sinphi * z - cosphi * z) + p.1,
|
||||
);
|
||||
// Compute step taken on map for each pixel in the x direction.
|
||||
let delta = (
|
||||
(pright.0 - pleft.0) / DISP_WIDTH as f32,
|
||||
(pright.1 - pleft.1) / DISP_WIDTH as f32,
|
||||
);
|
||||
// Perspective scaling for this distance, given height scaling.
|
||||
let rscale = scale_height / z;
|
||||
|
||||
// Traverse the line segment
|
||||
for i in 0..DISP_WIDTH as i32 {
|
||||
let (color, depth) = map.sample(pleft.0, pleft.1);
|
||||
// Perform perspective projection for height on screen, taking into account
|
||||
// player height and horizon.
|
||||
let height_on_screen = (height - depth as f32) * rscale + horizon;
|
||||
let height_on_screen = height_on_screen as i32;
|
||||
// Clamp against y-buffer to make sure more distant landscape doesn't
|
||||
// render over closer parts that have already been drawn.
|
||||
if height_on_screen < ybuffer[i as usize] {
|
||||
disp.dvline(i, height_on_screen, ybuffer[i as usize], color);
|
||||
ybuffer[i as usize] = height_on_screen;
|
||||
}
|
||||
// Advance.
|
||||
pleft.0 += delta.0;
|
||||
pleft.1 += delta.1;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the remainder of the display with the sky color.
|
||||
for i in 0..DISP_WIDTH as i32 {
|
||||
disp.dvline(i, 0, ybuffer[i as usize], sky_color);
|
||||
}
|
||||
|
||||
lcd.draw_picture(0, 0, DISP_WIDTH, DISP_HEIGHT, &disp.data);
|
||||
|
||||
p.1 -= 1.0;
|
||||
phi += 0.005;
|
||||
}
|
||||
}
|
18
rust/voxel/src/map_data.rs
Normal file
18
rust/voxel/src/map_data.rs
Normal file
@ -0,0 +1,18 @@
|
||||
/** Align an array of bytes to a specified alignment. This struct is generic in Bytes to admit unsizing coercions.
|
||||
* See: https://users.rust-lang.org/t/can-i-conveniently-compile-bytes-into-a-rust-program-with-a-specific-alignment/24049
|
||||
*/
|
||||
#[repr(C)] // guarantee 'bytes' comes after '_align'
|
||||
struct AlignedTo<Align, Bytes: ?Sized> {
|
||||
_align: [Align; 0],
|
||||
bytes: Bytes,
|
||||
}
|
||||
|
||||
/** Dummy static used to create aligned data. */
|
||||
static ALIGNED: &'static AlignedTo<u16, [u8]> = &AlignedTo {
|
||||
_align: [],
|
||||
bytes: *include_bytes!("../data/map15.dat"),
|
||||
};
|
||||
|
||||
pub static WIDTH: u32 = 256;
|
||||
pub static HEIGHT: u32 = 256;
|
||||
pub static VOXEL_MAP: &'static [u8] = &ALIGNED.bytes;
|
Loading…
Reference in New Issue
Block a user