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)
|
[README](rust/embgfx/README.md)
|
||||||
|
|
||||||
|
rust/voxel
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Old-school voxel-based landscape renderer.
|
||||||
|
|
||||||
|
[README](rust/voxel/README.md)
|
||||||
|
|
||||||
ROM re'ing
|
ROM re'ing
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ members = [
|
|||||||
"sdlcd",
|
"sdlcd",
|
||||||
"interrupt",
|
"interrupt",
|
||||||
"embgfx",
|
"embgfx",
|
||||||
|
"voxel",
|
||||||
]
|
]
|
||||||
|
|
||||||
[patch.crates-io]
|
[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