Initial commit.
commit
95fc851525
|
@ -0,0 +1,9 @@
|
|||
# https://EditorConfig.org/
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
|
@ -0,0 +1,13 @@
|
|||
*
|
||||
!*/
|
||||
|
||||
# source code
|
||||
!/src/**/*.rs
|
||||
!/src/**/*.wgsl
|
||||
|
||||
# top-level configuration
|
||||
!/.editorconfig
|
||||
!/.gitignore
|
||||
!/Cargo.lock
|
||||
!/Cargo.toml
|
||||
!/LICENSE.txt
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,110 @@
|
|||
[package]
|
||||
name = "pathland"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = ""
|
||||
repository = "https://github.com/jamestmartin/pathland"
|
||||
license = "0BSD"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
client = []
|
||||
|
||||
server = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
# brotli (compression format)
|
||||
#[dependencies.brotli]
|
||||
#version = "3.3"
|
||||
|
||||
# use Rust structs like C structs for wgpu
|
||||
[dependencies.bytemuck]
|
||||
version = "1.9"
|
||||
features = ["derive"]
|
||||
|
||||
# CBOR (serialization format)
|
||||
#[dependencies.ciborium]
|
||||
#version = "0.2.0"
|
||||
|
||||
# FLAC decoder
|
||||
[dependencies.claxon]
|
||||
version = "0.4.3"
|
||||
|
||||
# audio output
|
||||
[dependencies.cpal]
|
||||
version = "0.13.5"
|
||||
|
||||
# locations of configuration/cache/etc paths
|
||||
#[dependencies.directories]
|
||||
#version = "4.0"
|
||||
|
||||
# logging backend
|
||||
[dependencies.fern]
|
||||
version = "0.6.1"
|
||||
features = ["colored"]
|
||||
|
||||
# text rendering
|
||||
#[dependencies.fontdue]
|
||||
#version = "0.7.2"
|
||||
|
||||
# gamepad input
|
||||
#[dependencies.gilrs]
|
||||
#version = "0.9.0"
|
||||
|
||||
# HTTP client
|
||||
#[dependencies.hyper]
|
||||
#version = "0.14.19"
|
||||
#features = ["http2", "client", "runtime"]
|
||||
|
||||
# images
|
||||
[dependencies.image]
|
||||
version = "0.24.2"
|
||||
default-features = false
|
||||
# for now, no formats; we just use this for manipulating image buffers internally.
|
||||
features = []
|
||||
|
||||
# logging
|
||||
[dependencies.log]
|
||||
version = "0.4.17"
|
||||
features = ["std"]
|
||||
|
||||
# noise functions
|
||||
#[dependencies.noise]
|
||||
#version = "0.7.0"
|
||||
|
||||
# computation parallelism
|
||||
#[dependencies.rayon]
|
||||
#version = "1.5"
|
||||
|
||||
# fast (inverse) fourier transform
|
||||
#[dependencies.rustfft]
|
||||
#version = "6.0.1"
|
||||
|
||||
# serialization
|
||||
#[dependencies.serde]
|
||||
#version = "1.0"
|
||||
|
||||
# async runtime
|
||||
[dependencies.tokio]
|
||||
version = "1.19"
|
||||
# TODO: Is rt-multi-thread faster for our use case?
|
||||
features = ["rt", "macros"]
|
||||
|
||||
# TOML (configuration format)
|
||||
#[dependencies.toml_edit]
|
||||
#version = "0.14.4"
|
||||
|
||||
# graphics API
|
||||
[dependencies.wgpu]
|
||||
version = "0.12.0"
|
||||
|
||||
# window creation
|
||||
[dependencies.winit]
|
||||
version = "0.26.1"
|
||||
features = ["x11", "wayland"]
|
||||
|
||||
[profile.release]
|
||||
strip = "symbols"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
|
@ -0,0 +1,5 @@
|
|||
Copyright (C) 2022 by James Martin <james@jtmar.me>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -0,0 +1,76 @@
|
|||
use cpal::*;
|
||||
use cpal::traits::*;
|
||||
use claxon::*;
|
||||
|
||||
pub struct Audio {
|
||||
device: Device,
|
||||
stream: Stream,
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
pub fn setup() -> Self {
|
||||
let device = default_host().default_output_device()
|
||||
.expect("No audio output device available.");
|
||||
if let Ok(name) = device.name() {
|
||||
log::info!("Using audio output device: {}", name);
|
||||
}
|
||||
let config = device.default_output_config()
|
||||
.expect("Failed to get audio output device default configuration.");
|
||||
log::info!("Using audio output config: {:?}", config);
|
||||
let stream = match config.sample_format() {
|
||||
SampleFormat::F32 => create_output_stream::<f32>(&device, &config.config()),
|
||||
SampleFormat::I16 => create_output_stream::<i16>(&device, &config.config()),
|
||||
SampleFormat::U16 => create_output_stream::<u16>(&device, &config.config())
|
||||
};
|
||||
Self { device, stream }
|
||||
}
|
||||
}
|
||||
|
||||
fn create_output_stream<T: Sample>(device: &Device, config: &StreamConfig) -> Stream {
|
||||
let sample_rate = config.sample_rate.0;
|
||||
let channels = config.channels as usize;
|
||||
let mut clock = 0;
|
||||
|
||||
let music = read_music();
|
||||
|
||||
device.build_output_stream(
|
||||
&config,
|
||||
move |output: &mut [T], _| {
|
||||
for frame in output.chunks_mut(channels) {
|
||||
for sample in frame.iter_mut() {
|
||||
clock += 1;
|
||||
if clock >= music.len() {
|
||||
*sample = Sample::from(&0.0);
|
||||
return;
|
||||
}
|
||||
*sample = Sample::from(&music[clock]);
|
||||
}
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
log::error!("Audio stream error: {}", err);
|
||||
}
|
||||
).expect("Failed to create audio output stream.")
|
||||
}
|
||||
|
||||
fn read_music() -> Box<[f32]> {
|
||||
let mut reader = FlacReader::open("continue.flac").unwrap();
|
||||
if reader.streaminfo().channels != 2 {
|
||||
panic!("Incorrect number of channels in FLAC (must be stereo).");
|
||||
}
|
||||
let mut music = Vec::new();
|
||||
let mut frames = reader.blocks();
|
||||
let mut buffer = Some(Vec::new());
|
||||
while let Some(block) = frames.read_next_or_eof(buffer.take().unwrap()).expect("Error reading FLAC stream.") {
|
||||
for sample in block.stereo_samples() {
|
||||
music.push(convert_sample(sample.0));
|
||||
music.push(convert_sample(sample.1));
|
||||
}
|
||||
buffer = Some(block.into_buffer());
|
||||
}
|
||||
music.into_boxed_slice()
|
||||
}
|
||||
|
||||
fn convert_sample(sample: i32) -> f32 {
|
||||
sample as f32 / i16::MAX as f32
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
use winit::window::Window;
|
||||
use wgpu::*;
|
||||
use wgpu::util::*;
|
||||
|
||||
mod dither;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Vertex {
|
||||
position: [f32; 2],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct PushConstants {
|
||||
dimensions: [f32; 2],
|
||||
field_of_view: f32,
|
||||
}
|
||||
|
||||
const VERTICES: &[Vertex] = &[
|
||||
Vertex { position: [-1.0, 1.0] },
|
||||
Vertex { position: [-1.0, -1.0] },
|
||||
Vertex { position: [1.0, -1.0] },
|
||||
Vertex { position: [1.0, 1.0] },
|
||||
Vertex { position: [-1.0, 1.0] },
|
||||
];
|
||||
|
||||
pub struct Graphics {
|
||||
instance: Instance,
|
||||
window: Window,
|
||||
surface: Surface,
|
||||
adapter: Adapter,
|
||||
surface_format: TextureFormat,
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
shader: ShaderModule,
|
||||
pipeline: RenderPipeline,
|
||||
vertex_buffer: Buffer,
|
||||
surface_stale: bool,
|
||||
desired_size: winit::dpi::PhysicalSize<u32>,
|
||||
dither_bind_group: BindGroup,
|
||||
}
|
||||
|
||||
impl Graphics {
|
||||
pub async fn setup(window: Window) -> Self {
|
||||
// TODO: I don't think there's any reason we can't support ALL, but with ALL it defaults to OpenGL
|
||||
// on my machine for some resason. We should support ALL, so long as the PRIMARY backends
|
||||
// are used by default.
|
||||
let instance = Instance::new(Backends::PRIMARY);
|
||||
let surface = unsafe { instance.create_surface(&window) };
|
||||
let adapter = instance.request_adapter(
|
||||
&RequestAdapterOptionsBase {
|
||||
power_preference: PowerPreference::HighPerformance,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: Some(&surface)
|
||||
}
|
||||
).await.expect("Failed to get wgpu adapter.");
|
||||
let format = surface.get_preferred_format(&adapter).unwrap();
|
||||
let (device, queue) = adapter.request_device(&DeviceDescriptor {
|
||||
label: None,
|
||||
features: Features::PUSH_CONSTANTS,
|
||||
limits: Limits {
|
||||
max_push_constant_size: 128,
|
||||
.. Limits::default()
|
||||
}
|
||||
}, None).await.expect("Failed to get wgpu device.");
|
||||
let shader = device.create_shader_module(&include_wgsl!("shader.wgsl"));
|
||||
let dither_texture = device.create_texture_with_data(
|
||||
&queue,
|
||||
&TextureDescriptor {
|
||||
size: Extent3d {
|
||||
width: u8::MAX as u32,
|
||||
height: u8::MAX as u32,
|
||||
depth_or_array_layers: 1
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba32Float,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
label: Some("dither_texture")
|
||||
},
|
||||
bytemuck::cast_slice(&*dither::bayer_texture())
|
||||
);
|
||||
let dither_texture_view = dither_texture.create_view(&TextureViewDescriptor::default());
|
||||
let dither_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: TextureViewDimension::D2,
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
},
|
||||
count: None,
|
||||
}
|
||||
],
|
||||
label: Some("dither_bind_group_layout")
|
||||
});
|
||||
let dither_bind_group = device.create_bind_group(&BindGroupDescriptor {
|
||||
layout: &dither_bind_group_layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(&dither_texture_view)
|
||||
}
|
||||
],
|
||||
label: Some("dither_bind_group")
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[&dither_bind_group_layout],
|
||||
push_constant_ranges: &[
|
||||
PushConstantRange {
|
||||
stages: ShaderStages::FRAGMENT,
|
||||
range: 0..12,
|
||||
}
|
||||
]
|
||||
})),
|
||||
vertex: VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[
|
||||
VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Vertex>() as BufferAddress,
|
||||
step_mode: VertexStepMode::Vertex,
|
||||
attributes: &vertex_attr_array![0 => Float32x3, 1 => Float32x3]
|
||||
}
|
||||
]
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: &shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[
|
||||
ColorTargetState {
|
||||
format,
|
||||
blend: Some(BlendState::REPLACE),
|
||||
write_mask: ColorWrites::ALL
|
||||
}
|
||||
]
|
||||
}),
|
||||
primitive: PrimitiveState {
|
||||
topology: PrimitiveTopology::TriangleStrip,
|
||||
strip_index_format: None,
|
||||
front_face: FrontFace::Ccw,
|
||||
cull_mode: Some(Face::Back),
|
||||
polygon_mode: PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false
|
||||
},
|
||||
multiview: None
|
||||
});
|
||||
let vertex_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(VERTICES),
|
||||
usage: BufferUsages::VERTEX
|
||||
});
|
||||
let desired_size = window.inner_size();
|
||||
Self {
|
||||
instance,
|
||||
window,
|
||||
surface,
|
||||
surface_format: format,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
shader,
|
||||
pipeline,
|
||||
vertex_buffer,
|
||||
surface_stale: true,
|
||||
desired_size,
|
||||
dither_bind_group
|
||||
}
|
||||
}
|
||||
|
||||
fn reconfigure_surface(&self, size: winit::dpi::PhysicalSize<u32>) {
|
||||
log::debug!("Reconfiguring wgpu surface.");
|
||||
self.surface.configure(&self.device, &SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: self.surface_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::Mailbox
|
||||
});
|
||||
}
|
||||
|
||||
fn reconfigure_surface_if_stale(&mut self) {
|
||||
log::info!("reconfigure");
|
||||
if self.surface_stale {
|
||||
self.reconfigure_surface(self.desired_size);
|
||||
self.surface_stale = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_resized(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
|
||||
self.desired_size = new_size;
|
||||
self.surface_stale = true;
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
log::info!("redraw");
|
||||
self.reconfigure_surface_if_stale();
|
||||
let frame = self.surface.get_current_texture().expect("Failed to get surface texture");
|
||||
let view = frame.texture.create_view(&TextureViewDescriptor::default());
|
||||
let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[
|
||||
RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0
|
||||
}),
|
||||
store: true
|
||||
}
|
||||
}
|
||||
],
|
||||
depth_stencil_attachment: None
|
||||
});
|
||||
render_pass.set_pipeline(&self.pipeline);
|
||||
render_pass.set_push_constants(ShaderStages::FRAGMENT, 0, bytemuck::bytes_of(&PushConstants {
|
||||
dimensions: [self.desired_size.width as f32, self.desired_size.height as f32],
|
||||
field_of_view: std::f32::consts::PI,
|
||||
}));
|
||||
render_pass.set_bind_group(0, &self.dither_bind_group, &[]);
|
||||
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
||||
render_pass.draw(0..VERTICES.len() as u32, 0..1);
|
||||
}
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
frame.present();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
let DITHER_BASE = 16;
|
||||
let BIT_WIDTH = 16;
|
||||
let DITHER_SIZE = 0x10000;
|
||||
|
||||
fn bit_reverse(_x: u32) -> u32 {
|
||||
var x = _x;
|
||||
var hi = u32(1) << (u32(BIT_WIDTH)-u32(1));
|
||||
var lo = u32(1);
|
||||
for (var i = u32(0); i < u32(BIT_WIDTH)/u32(2); i = i + u32(1)) {
|
||||
let bit_hi = x & hi;
|
||||
let bit_lo = x & lo;
|
||||
x = x & ~hi & ~lo;
|
||||
if (bit_hi > u32(0)) { x = x | lo; }
|
||||
if (bit_lo > u32(0)) { x = x | hi; }
|
||||
hi = hi >> u32(1);
|
||||
lo = lo >> u32(1);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
fn bit_interleave(x: u32, y: u32) -> u32 {
|
||||
var mask = u32(1) << (u32(BIT_WIDTH)-u32(1));
|
||||
var acc = u32(0);
|
||||
for (var i = u32(0); i < u32(BIT_WIDTH); i = i + u32(1)) {
|
||||
acc = acc | ((x & mask) << u32(2)*i + u32(1));
|
||||
acc = acc | ((y & mask) << u32(2)*i);
|
||||
mask = mask >> u32(1);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
fn bayer(coord: vec2<u32>) -> f32 {
|
||||
let magic = bit_reverse(bit_interleave(coord.x ^ coord.y, coord.x));
|
||||
return (f32(magic+u32(1)) / (f32(DITHER_SIZE)*f32(DITHER_SIZE))) - 0.5;
|
||||
}
|
||||
|
||||
fn bayer_bias(_pixel: vec2<u32>) -> vec4<f32> {
|
||||
let pixel = _pixel % u32(DITHER_SIZE);
|
||||
return vec4<f32>(
|
||||
bayer(pixel),
|
||||
bayer(vec2<u32>(u32(DITHER_SIZE) - pixel.x - u32(1), pixel.y)),
|
||||
bayer(vec2<u32>(pixel.x, u32(DITHER_SIZE) - pixel.y - u32(1))),
|
||||
bayer(vec2<u32>(u32(DITHER_SIZE) - pixel.x - u32(1), u32(DITHER_SIZE) - pixel.y - u32(1)))
|
||||
);
|
||||
}*
|
|
@ -0,0 +1,55 @@
|
|||
use image::*;
|
||||
|
||||
/// Generate Bayer matrix texture for ordered dithering.
|
||||
|
||||
/// TODO: better/alternative noise-based matrix to avoid grid pattern?
|
||||
|
||||
const DITHER_SIZE: u8 = 1 << 7;
|
||||
|
||||
|
||||
|
||||
/// Bitwise interleave two integers of length BIT_WIDTH into a single
|
||||
/// 2*BIT_WIDTH integer.
|
||||
///
|
||||
/// example interleave:
|
||||
///
|
||||
/// x = 0 1 0 0 1
|
||||
/// y = 1 0 0 1 1
|
||||
/// ----------
|
||||
/// r = 0110000111
|
||||
///
|
||||
/// actually also reverses bits, but I want that anyway.
|
||||
/// (don't try to re-use this function!)
|
||||
fn bit_interleave(mut x: u8, mut y: u8) -> u16 {
|
||||
let mut acc: u16 = 0;
|
||||
for _ in 0..8 {
|
||||
acc <<= 1;
|
||||
acc |= (x & 1) as u16;
|
||||
x >>= 1;
|
||||
acc <<= 1;
|
||||
acc |= (y & 1) as u16;
|
||||
y >>= 1;
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
fn bayer(x: u8, y: u8) -> f32 {
|
||||
// Magic bitwise formula from Wikipedia produces values from 0 to 2^16-1.
|
||||
// FIXME: slight vertical lines when displaying dither texture
|
||||
let magic = bit_interleave(x ^ y, x);
|
||||
(magic as f32 + 1.) / (u8::MAX as u16 * u8::MAX as u16) as f32
|
||||
}
|
||||
|
||||
fn bayer_bias(x: u8, y: u8) -> [f32; 4] {
|
||||
[
|
||||
// TODO: smarter way to re-tile color channels? if this isn't good enough.
|
||||
bayer(x, y),
|
||||
bayer(DITHER_SIZE.overflowing_sub(x).0, y),
|
||||
bayer(x, DITHER_SIZE.overflowing_sub(y).0),
|
||||
bayer(DITHER_SIZE.overflowing_sub(x).0, DITHER_SIZE.overflowing_sub(y).0)
|
||||
]
|
||||
}
|
||||
|
||||
pub fn bayer_texture() -> Rgba32FImage {
|
||||
ImageBuffer::from_fn(u8::MAX as u32, u8::MAX as u32, |x, y| Rgba::from(bayer_bias(x as u8, y as u8)))
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
mod audio;
|
||||
mod graphics;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
setup_logger();
|
||||
|
||||
use winit::event_loop::EventLoop;
|
||||
use winit::platform::unix::WindowBuilderExtUnix;
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let window = winit::window::WindowBuilder::new()
|
||||
// Arbitrarily chosen as the minimum resolution the game is designed to support (for e.g. UI scaling).
|
||||
.with_min_inner_size(winit::dpi::LogicalSize { height: 360, width: 640 })
|
||||
.with_title("Pathland")
|
||||
.with_maximized(true)
|
||||
// TODO: hide window until first frame is drawn (default behavior on wayland)
|
||||
.with_visible(true)
|
||||
.with_decorations(true)
|
||||
.with_class("pathland".to_string(), "pathland".to_string())
|
||||
.with_app_id("pathland".to_string())
|
||||
.build(&event_loop)
|
||||
.expect("Failed to create window.");
|
||||
// TODO: window icon, fullscreen, IME position, cursor grab, cursor visibility
|
||||
let mut graphics = graphics::Graphics::setup(window).await;
|
||||
//let audio = audio::Audio::setup();
|
||||
log::info!("Took {} milliseconds", now.elapsed().as_millis());
|
||||
|
||||
event_loop.run(move |event, target, control_flow| {
|
||||
use winit::event::*;
|
||||
*control_flow = winit::event_loop::ControlFlow::Wait;
|
||||
match event {
|
||||
Event::WindowEvent { window_id, event } => {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
WindowEvent::Destroyed => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
WindowEvent::Focused(focused) => {
|
||||
// TODO: handle focus/unfocus (e.g. pause, resume)
|
||||
},
|
||||
WindowEvent::Resized(new_size) => {
|
||||
graphics.window_resized(new_size)
|
||||
},
|
||||
WindowEvent::ScaleFactorChanged { new_inner_size: new_size, .. } => {
|
||||
graphics.window_resized(*new_size)
|
||||
},
|
||||
// TODO: handle user input
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
Event::DeviceEvent { device_id, event } => {
|
||||
// TODO: handle user input
|
||||
},
|
||||
Event::MainEventsCleared => {
|
||||
// TODO: main event loop. queue simulation calculations, screen redrawing, etc.
|
||||
},
|
||||
Event::RedrawRequested(_) => {
|
||||
graphics.draw();
|
||||
},
|
||||
Event::LoopDestroyed => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
// TODO: What is suspending/resuming? Do I want to support it?
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_logger() {
|
||||
use fern::Dispatch;
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::LevelFilter;
|
||||
|
||||
Dispatch::new()
|
||||
.chain(
|
||||
Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
ColoredLevelConfig::default().color(record.level()),
|
||||
message
|
||||
));
|
||||
})
|
||||
.level(LevelFilter::Warn)
|
||||
.level_for("pathland", LevelFilter::Info)
|
||||
.chain(std::io::stderr()))
|
||||
.chain(
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(LevelFilter::Debug)
|
||||
.level_for("pathland", LevelFilter::Trace)
|
||||
.chain(std::fs::OpenOptions::new().write(true).create(true).truncate(true).open("/tmp/pathland.log").unwrap()))
|
||||
.apply().unwrap();
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
struct VertexInput {
|
||||
[[location(0)]] position: vec2<f32>;
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
[[builtin(position)]] position: vec4<f32>;
|
||||
};
|
||||
|
||||
[[stage(vertex)]]
|
||||
fn vs_main(
|
||||
in: VertexInput,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.position = vec4<f32>(in.position, 0.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
struct PushConstants {
|
||||
dimensions: vec2<f32>;
|
||||
field_of_view: f32;
|
||||
};
|
||||
|
||||
var<push_constant> pc: PushConstants;
|
||||
|
||||
let PI: f32 = 3.14159265358979323846264338327950288; // 3.14159274
|
||||
|
||||
struct Ray {
|
||||
pos: vec3<f32>; // POSition (aka the origin)
|
||||
dir: vec3<f32>; // DIRection (normalized)
|
||||
};
|
||||
|
||||
///
|
||||
/// Convert from pixel coordinates to window-independent square coordinates.
|
||||
///
|
||||
/// Input coordinates:
|
||||
/// x: from 0 (left) to dimensions.x (right)
|
||||
/// y: from 0 (bottom) to dimensions.y (top)
|
||||
///
|
||||
/// Output coordinates:
|
||||
/// x: from -1 (left) to 1 (right)
|
||||
/// y: from -1 (down) to 1 (up)
|
||||
///
|
||||
/// The output coordinates are square and independent of the
|
||||
/// window's dimensions and aspect ratio. Some of the image
|
||||
/// will be cropped if the window's aspect ratio is not square.
|
||||
fn pixel_to_square(pixel: vec2<f32>) -> vec2<f32> {
|
||||
let square = ((pixel / pc.dimensions) - 0.5) * 2.0;
|
||||
|
||||
// Scale the window's smaller aspect ratio to make the coordinates square.
|
||||
// For example, a 16:9 window will have an x coordinate from -1 to 1 and
|
||||
// a y coordinate from -9/16ths to 9/16ths. The rest of the image lying outside
|
||||
// of that range will be cropped out.
|
||||
if (pc.dimensions.x > pc.dimensions.y) {
|
||||
return vec2<f32>(square.x, square.y * pc.dimensions.y / pc.dimensions.x);
|
||||
} else {
|
||||
return vec2<f32>(square.x * pc.dimensions.x / pc.dimensions.y, square.y);
|
||||
}
|
||||
}
|
||||
|
||||
/// Project a coordinate on the unit circle onto the unit hemisphere.
|
||||
/// This is used for curvilinear perspective.
|
||||
///
|
||||
/// Coordinates:
|
||||
/// x: from -1 (90 degrees left) to 1 (90 degrees right)
|
||||
/// y: from -1 (90 degrees down) to 1 (90 degrees up)
|
||||
///
|
||||
/// TODO: add support for the usual, non-curvilinear perspective projection
|
||||
/// (and possibly other projections, just for fun?)
|
||||
fn project(coord_: vec2<f32>) -> vec3<f32> {
|
||||
var coord = coord_;
|
||||
// This projection only supports coordinates within the unit circle
|
||||
// and only projects into the unit hemisphere. Ideally we'd want
|
||||
// some sort of extension which takes points outside the unit circle
|
||||
// and projects them somewhere behind you (with the point at infinity
|
||||
// being directly behind you), but I haven't come up with any reasonable
|
||||
// extension of this perspective system which behaves in that manner.
|
||||
//
|
||||
// What we can do instead is *tile* the projection so that adjacent projections
|
||||
// are a mirrored projection of the unit hemisphere *behind* you.
|
||||
// This is a logical extension because the projection becomes continuous
|
||||
// along the x and y axis (you're just looking around in perfect circles),
|
||||
// and it allows you to view the entire space. The main problem to this approach
|
||||
// is that all of the space between the tiled circles is still undefined,
|
||||
// but this is still the best solution which I'm aware of.
|
||||
|
||||
var dir: f32 = 1.; // the sign of the direction we're facing: 1 forward, -1 backward.
|
||||
// Tile coordinates:
|
||||
// (0-2, 0-2): forward
|
||||
// (2-4, 0-2): backward, left/right mirrored
|
||||
// (0-2, 2-4): backward, up/down mirrored
|
||||
// (2-4, 2-4): forward, left/right and up/down mirrored
|
||||
// FIXME: Use modulus which handles negatives properly so I don't have to arbitrarily add 8.
|
||||
coord = (coord + 1. + 8.) % 4.;
|
||||
// mirror/reverse and map back into 0 to 2 range
|
||||
if (coord.x > 2.) {
|
||||
coord.x = 4. - coord.x;
|
||||
dir = -dir;
|
||||
}
|
||||
if (coord.y > 2.) {
|
||||
coord.y = 4. - coord.y;
|
||||
dir = -dir;
|
||||
}
|
||||
// map back into -1 to 1 range
|
||||
coord = coord - 1.;
|
||||
|
||||
// Avoid NaN because implementations are allowed to assume it won't occur.
|
||||
let preZ = 1. - coord.x*coord.x - coord.y*coord.y;
|
||||
|
||||
// We can "define" the remaining undefined region of the screen
|
||||
// by clamping it to the nearest unit circle. This is sometimes
|
||||
// better than nothing, though it can also be a lot worse because
|
||||
// we still have to actually *render* all of those pixels.
|
||||
|
||||
// TODO: Add an option to allow stretching into a square instead of clamping?
|
||||
// I imagine things could get pretty badly warped, but maybe it could be useful?
|
||||
|
||||
// TODO: Is this clamping behavior correct? It doesn't look like it actually is, tbh.
|
||||
if (preZ < 0.) {
|
||||
return vec3<f32>(normalize(coord), 0.);
|
||||
}
|
||||
return normalize(vec3<f32>(coord, dir*sqrt(preZ)));
|
||||
}
|
||||
|
||||
/// After converting pixel coordinates to screen coordinates, we still have a problem:
|
||||
/// screen coordinates are 2d, but our world is 3d! The camera assigns each screen
|
||||
/// coordinate to a ray in 3d space, indicating the position and angle which
|
||||
/// we will be receiving light from.
|
||||
fn camera_project(square: vec2<f32>) -> Ray {
|
||||
// Our coordinates already range from -1 to 1, corresponding with the
|
||||
// edges of the window, but we want the edges of the window to correspond
|
||||
// with the angle of the FOV instead.
|
||||
let circle = square * pc.field_of_view / PI;
|
||||
let sphere = project(circle);
|
||||
return Ray(vec3<f32>(0.), sphere);
|
||||
}
|
||||
|
||||
[[group(0), binding(0)]]
|
||||
var dither_texture: texture_2d<f32>;
|
||||
|
||||
/// Apply ordered dithering, which reduces color banding and produces the appearance
|
||||
/// of more colors when in a limited color space (e.g. dark colors with a typical
|
||||
/// 8-bit sRGB monitor).
|
||||
// FIXME: document, don't hardcode width/bit depth
|
||||
fn dither(pixel: vec2<u32>, color: vec4<f32>) -> vec4<f32> {
|
||||
// FIXME: issues with bars at edge caused by bad modulus? (should be %256 but pixel rounding incorrect?)
|
||||
let bias = textureLoad(dither_texture, vec2<i32>(i32(pixel.x % u32(255)), i32(pixel.y % u32(255))), 0) - 0.5;
|
||||
// FIXME: hack to avoid srgb issues
|
||||
return color + (bias / 256.);
|
||||
}
|
||||
|
||||
////
|
||||
//// AUTHOR: Sam Hocevar (http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl)
|
||||
////
|
||||
fn rgb2hsv(c: vec3<f32>) -> vec3<f32> {
|
||||
let K = vec4<f32>(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||
let p = mix(vec4<f32>(c.bg, K.wz), vec4<f32>(c.gb, K.xy), step(c.b, c.g));
|
||||
let q = mix(vec4<f32>(p.xyw, c.r), vec4<f32>(c.r, p.yzx), step(p.x, c.r));
|
||||
|
||||
let d = q.x - min(q.w, q.y);
|
||||
let e = 1.0e-10;
|
||||
return vec3<f32>(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
||||
}
|
||||
|
||||
fn hsv2rgb(c: vec3<f32>) -> vec3<f32> {
|
||||
let K = vec4<f32>(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||
let p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||
return c.z * mix(K.xxx, clamp(p - K.xxx, vec3<f32>(0.0), vec3<f32>(1.0)), c.y);
|
||||
}
|
||||
|
||||
/// Given a color which clips outside the color space (some channel is >1.0),
|
||||
/// reduce the brightness (without affecting hue or saturation) until it no
|
||||
/// longer clips. (The default behavior without doing this is just clipping,
|
||||
/// which affects the saturation of the color dramatically, often turning colors
|
||||
/// into 100% white pixels.)
|
||||
fn clamp_value(_color: vec3<f32>) -> vec3<f32> {
|
||||
// TODO: Adjust value directly, without going through HSV conversion.
|
||||
var color = rgb2hsv(_color.rgb);
|
||||
color.z = min(color.z, 1.); // clamp value (brightness) from 0 to 1, preserving saturation and chroma
|
||||
return hsv2rgb(color);
|
||||
}
|
||||
|
||||
[[stage(fragment)]]
|
||||
fn fs_main(in: VertexOutput) -> [[location(0)]] vec4<f32> {
|
||||
let ray = camera_project(pixel_to_square(in.position.xy));
|
||||
var color = ray.dir / 2.0 + 0.5;
|
||||
|
||||
// TODO: Separate postprocessing pass.
|
||||
|
||||
// It is possible for this renderer to emit colors brighter than 1.0,
|
||||
// for example if you use very bright or many light sources. These colors will be
|
||||
// displayed incorrectly, appearing desaturated and having their brightness
|
||||
// clamped to whatever color output is supported.
|
||||
//
|
||||
// This is common in particular if you have very bright lights in a scene,
|
||||
// which is sometimes necessary for objects to be clearly visible. The result
|
||||
// will be you seeing flashes of over-bright white pixels where you should
|
||||
// see color. One way to mitigate this is by increasing the number of samples per
|
||||
// pixel; the average brightness per pixel is generally less than 1.0 when averaged
|
||||
// out with the (more common) black pixels when no light source is encountered.
|
||||
//
|
||||
// Another mitigation approach is to do color correction, where instead of
|
||||
// trying to preserve the brightness by clamping the RGB values and losing saturation,
|
||||
// you try to preserve the saturation by scaling down the brightness until the
|
||||
// full saturation of the colors is visible (or at least part of it).
|
||||
color = clamp_value(color);
|
||||
|
||||
// Dithering after sRGB conversion is slightly worse because the bayer matrix
|
||||
// is linear whereas sRGB is non-linear, but if you do it *before* conversion,
|
||||
// then adjusted colors won't be *quite* close enough to nearest_color that they
|
||||
// should be closest to, which has the potential to create nasty artifacts.
|
||||
//
|
||||
// FIXME: This shader uses linear color space.
|
||||
return dither(
|
||||
vec2<u32>(u32(in.position.x), u32(in.position.y)),
|
||||
vec4<f32>(color, 1.0)
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue