Added server-cli, UI tests

Former-commit-id: 93bf5b39138920aa7a4a773a8247d716866c4a05
This commit is contained in:
Joshua Barretto 2019-01-30 12:11:34 +00:00
parent a1617e7b5c
commit c16a257ae3
28 changed files with 779 additions and 15 deletions

View File

@ -3,6 +3,7 @@ members = [
"common",
"client",
"server",
"server-cli",
"voxygen",
"world",
]

3
server-cli/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

12
server-cli/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "veloren-server-cli"
version = "0.1.0"
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
edition = "2018"
[dependencies]
server = { package = "veloren-server", path = "../server" }
common = { package = "veloren-common", path = "../common" }
log = "0.4"
pretty_env_logger = "0.3"

35
server-cli/src/main.rs Normal file
View File

@ -0,0 +1,35 @@
// Standard
use std::time::Duration;
// Library
use log::info;
// Project
use server::{self, Server};
use common::clock::Clock;
const FPS: u64 = 60;
fn main() {
// Init logging
pretty_env_logger::init();
info!("Starting server-cli...");
// Set up an fps clock
let mut clock = Clock::new();
// Create server
let mut server = Server::new();
loop {
server.tick(server::Input {}, clock.get_last_delta())
.expect("Failed to tick server");
// Clean up the server after a tick
server.cleanup();
// Wait for the next tick
clock.tick(Duration::from_millis(1000 / FPS));
}
}

View File

@ -62,4 +62,10 @@ impl Server {
// Finish the tick, pass control back to the frontend (step 6)
Ok(())
}
/// Clean up the server after a tick
pub fn cleanup(&mut self) {
// Cleanup the local state
self.state.cleanup();
}
}

View File

@ -29,3 +29,4 @@ lazy_static = "1.1"
log = "0.4"
pretty_env_logger = "0.3"
dot_vox = "1.0"
image = "0.21"

17
voxygen/shaders/ui.frag Normal file
View File

@ -0,0 +1,17 @@
#version 330 core
in vec3 f_pos;
in vec2 f_uv;
layout (std140)
uniform u_locals {
vec4 bounds;
};
uniform sampler2D u_tex;
out vec4 tgt_color;
void main() {
tgt_color = texture(u_tex, f_uv);
}

22
voxygen/shaders/ui.vert Normal file
View File

@ -0,0 +1,22 @@
#version 330 core
in vec3 v_pos;
in vec2 v_uv;
layout (std140)
uniform u_locals {
vec4 bounds;
};
uniform sampler2D u_tex;
out vec3 f_pos;
out vec2 f_uv;
void main() {
f_uv = v_uv;
f_pos = vec3(vec2(bounds.x, bounds.y) + v_pos.xy * vec2(bounds.z, bounds.w), 0);
f_pos.xy = vec2(f_pos.x * 2.0 - 1.0, f_pos.y * -2.0 + 1.0);
gl_Position = vec4(f_pos, 1);
}

View File

@ -7,6 +7,7 @@ pub mod mesh;
pub mod render;
pub mod scene;
pub mod session;
pub mod ui;
pub mod window;
// Reexports
@ -66,18 +67,20 @@ fn main() {
// Init logging
pretty_env_logger::init();
// Set up the initial play state
let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(TitleState::new())];
states.last().map(|current_state| {
log::info!("Started game with state '{}'", current_state.name())
});
// Set up the global state
let mut global_state = GlobalState {
window: Window::new()
.expect("Failed to create window"),
};
// Set up the initial play state
let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(TitleState::new(
&mut global_state.window.renderer_mut(),
))];
states.last().map(|current_state| {
log::info!("Started game with state '{}'", current_state.name())
});
// What's going on here?
// ---------------------
// The state system used by Voxygen allows for the easy development of stack-based menus.

View File

@ -1,5 +1,6 @@
// Library
use vek::*;
use image;
// Crate
use crate::{
@ -8,14 +9,28 @@ use crate::{
GlobalState,
window::Event,
session::SessionState,
render::Renderer,
ui::{
Ui,
element::{
Widget,
image::Image,
},
},
};
pub struct TitleState;
pub struct TitleState {
ui: Ui,
}
impl TitleState {
/// Create a new `TitleState`
pub fn new() -> Self {
Self
pub fn new(renderer: &mut Renderer) -> Self {
let img = Image::new(renderer, &image::open(concat!(env!("CARGO_MANIFEST_DIR"), "/test_assets/test.png")).unwrap()).unwrap();
let widget = Widget::new(renderer, img).unwrap();
Self {
ui: Ui::new(renderer, widget).unwrap(),
}
}
}
@ -41,6 +56,12 @@ impl PlayState for TitleState {
// Clear the screen
global_state.window.renderer_mut().clear(BG_COLOR);
// Maintain the UI
self.ui.maintain(global_state.window.renderer_mut());
// Draw the UI to the screen
self.ui.render(global_state.window.renderer_mut());
// Finish the frame
global_state.window.renderer_mut().flush();
global_state.window

View File

@ -3,6 +3,7 @@ pub mod mesh;
pub mod model;
pub mod pipelines;
pub mod renderer;
pub mod texture;
mod util;
// Reexports
@ -10,6 +11,7 @@ pub use self::{
consts::Consts,
mesh::{Mesh, Tri, Quad},
model::Model,
texture::Texture,
renderer::{Renderer, TgtColorFmt, TgtDepthFmt},
pipelines::{
Globals,
@ -27,6 +29,11 @@ pub use self::{
TerrainPipeline,
Locals as TerrainLocals,
},
ui::{
create_quad_mesh as create_ui_quad_mesh,
UiPipeline,
Locals as UiLocals,
},
},
};
@ -41,6 +48,7 @@ use gfx;
pub enum RenderError {
PipelineError(gfx::PipelineStateError<String>),
UpdateError(gfx::UpdateError<usize>),
CombinedError(gfx::CombinedError),
}
/// Used to represent a specific rendering configuration.

View File

@ -1,6 +1,7 @@
pub mod figure;
pub mod skybox;
pub mod terrain;
pub mod ui;
// Library
use gfx::{

View File

@ -0,0 +1,76 @@
// Library
use gfx::{
self,
// Macros
gfx_defines,
gfx_vertex_struct_meta,
gfx_constant_struct_meta,
gfx_impl_struct_meta,
gfx_pipeline,
gfx_pipeline_inner,
};
// Local
use super::{
Globals,
super::{
Pipeline,
TgtColorFmt,
TgtDepthFmt,
Mesh,
Quad,
},
};
gfx_defines! {
vertex Vertex {
pos: [f32; 3] = "v_pos",
uv: [f32; 2] = "v_uv",
}
constant Locals {
bounds: [f32; 4] = "bounds",
}
pipeline pipe {
vbuf: gfx::VertexBuffer<Vertex> = (),
locals: gfx::ConstantBuffer<Locals> = "u_locals",
tex: gfx::TextureSampler<[f32; 4]> = "u_tex",
tgt_color: gfx::RenderTarget<TgtColorFmt> = "tgt_color",
tgt_depth: gfx::DepthTarget<TgtDepthFmt> = gfx::preset::depth::PASS_TEST,
}
}
impl Locals {
pub fn default() -> Self {
Self { bounds: [0.0, 0.0, 1.0, 1.0] }
}
pub fn new(bounds: [f32; 4]) -> Self {
Self {
bounds,
}
}
}
pub struct UiPipeline;
impl Pipeline for UiPipeline {
type Vertex = Vertex;
}
pub fn create_quad_mesh() -> Mesh<UiPipeline> {
let mut mesh = Mesh::new();
#[rustfmt::skip]
mesh.push_quad(Quad::new(
Vertex { pos: [0.0, 0.0, 0.0], uv: [0.0, 0.0] },
Vertex { pos: [0.0, 1.0, 0.0], uv: [0.0, 1.0] },
Vertex { pos: [1.0, 1.0, 0.0], uv: [1.0, 1.0] },
Vertex { pos: [1.0, 0.0, 0.0], uv: [1.0, 0.0] },
));
mesh
}

View File

@ -4,12 +4,14 @@ use gfx::{
self,
traits::{Device, FactoryExt},
};
use image;
// Local
use super::{
consts::Consts,
model::Model,
mesh::Mesh,
model::Model,
texture::Texture,
Pipeline,
RenderError,
gfx_backend,
@ -18,6 +20,7 @@ use super::{
figure,
skybox,
terrain,
ui,
},
};
@ -45,6 +48,7 @@ pub struct Renderer {
skybox_pipeline: GfxPipeline<skybox::pipe::Init<'static>>,
figure_pipeline: GfxPipeline<figure::pipe::Init<'static>>,
terrain_pipeline: GfxPipeline<terrain::pipe::Init<'static>>,
ui_pipeline: GfxPipeline<ui::pipe::Init<'static>>,
}
impl Renderer {
@ -80,6 +84,14 @@ impl Renderer {
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/terrain.frag")),
)?;
// Construct a pipeline for rendering UI elements
let ui_pipeline = create_pipeline(
&mut factory,
ui::pipe::new(),
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/ui.vert")),
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/ui.frag")),
)?;
Ok(Self {
device,
encoder: factory.create_command_buffer().into(),
@ -91,6 +103,7 @@ impl Renderer {
skybox_pipeline,
figure_pipeline,
terrain_pipeline,
ui_pipeline,
})
}
@ -104,6 +117,14 @@ impl Renderer {
(&mut self.tgt_color_view, &mut self.tgt_depth_view)
}
/// Get the resolution of the render target.
pub fn get_resolution(&self) -> Vec2<u16> {
Vec2::new(
self.tgt_color_view.get_dimensions().0,
self.tgt_color_view.get_dimensions().1,
)
}
/// Queue the clearing of the color and depth targets ready for a new frame to be rendered.
/// TODO: Make a version of this that doesn't clear the colour target for speed
pub fn clear(&mut self, col: Rgba<f32>) {
@ -144,6 +165,14 @@ impl Renderer {
))
}
/// Create a new texture from the provided image.
pub fn create_texture<P: Pipeline>(&mut self, image: &image::DynamicImage) -> Result<Texture<P>, RenderError> {
Texture::new(
&mut self.factory,
image,
)
}
/// Queue the rendering of the provided skybox model in the upcoming frame.
pub fn render_skybox(
&mut self,
@ -205,6 +234,26 @@ impl Renderer {
},
);
}
/// Queue the rendering of the provided UI element in the upcoming frame.
pub fn render_ui_element(
&mut self,
model: &Model<ui::UiPipeline>,
locals: &Consts<ui::Locals>,
tex: &Texture<ui::UiPipeline>,
) {
self.encoder.draw(
&model.slice,
&self.ui_pipeline.pso,
&ui::pipe::Data {
vbuf: model.vbuf.clone(),
locals: locals.buf.clone(),
tex: (tex.srv.clone(), tex.sampler.clone()),
tgt_color: self.tgt_color_view.clone(),
tgt_depth: self.tgt_depth_view.clone(),
},
);
}
}
struct GfxPipeline<P: gfx::pso::PipelineInit> {

View File

@ -0,0 +1,58 @@
// Standard
use std::marker::PhantomData;
// Library
use gfx::{
self,
traits::{Factory, FactoryExt},
};
use image::{
DynamicImage,
GenericImageView,
};
// Local
use super::{
RenderError,
mesh::Mesh,
Pipeline,
gfx_backend,
};
type ShaderFormat = (gfx::format::R8_G8_B8_A8, gfx::format::Srgb);
/// Represents an image that has been uploaded to the GPU.
pub struct Texture<P: Pipeline> {
pub tex: gfx::handle::Texture<gfx_backend::Resources, <ShaderFormat as gfx::format::Formatted>::Surface>,
pub srv: gfx::handle::ShaderResourceView<gfx_backend::Resources, <ShaderFormat as gfx::format::Formatted>::View>,
pub sampler: gfx::handle::Sampler<gfx_backend::Resources>,
_phantom: PhantomData<P>,
}
impl<P: Pipeline> Texture<P> {
pub fn new(
factory: &mut gfx_backend::Factory,
image: &DynamicImage,
) -> Result<Self, RenderError> {
let (tex, srv) = factory.create_texture_immutable_u8::<ShaderFormat>(
gfx::texture::Kind::D2(
image.width() as u16,
image.height() as u16,
gfx::texture::AaMode::Single,
),
gfx::texture::Mipmap::Provided,
&[&image.to_rgba().into_raw()],
)
.map_err(|err| RenderError::CombinedError(err))?;
Ok(Self {
tex,
srv,
sampler: factory.create_sampler(gfx::texture::SamplerInfo::new(
gfx::texture::FilterMethod::Scale,
gfx::texture::WrapMode::Clamp,
)),
_phantom: PhantomData,
})
}
}

View File

@ -60,6 +60,12 @@ impl Camera {
.max(-PI / 2.0);
}
/// Zoom the camera by the given delta, limiting the input accordingly.
pub fn zoom_by(&mut self, delta: f32) {
// Clamp camera dist to the 0 <= x <= infinity range
self.dist = (self.dist + delta).max(0.0);
}
/// Get the focus position of the camera.
pub fn get_focus_pos(&self) -> Vec3<f32> { self.focus }
/// Set the focus position of the camera.

View File

@ -112,13 +112,25 @@ impl Scene {
pub fn camera_mut(&mut self) -> &mut Camera { &mut self.camera }
/// Handle an incoming user input event (i.e: cursor moved, key pressed, window closed, etc.).
///
/// If the event is handled, return true
pub fn handle_input_event(&mut self, event: Event) -> bool {
match event {
// When the window is resized, change the camera's aspect ratio
Event::Resize(dims) => {
self.camera.set_aspect_ratio(dims.x as f32 / dims.y as f32);
true
},
// Panning the cursor makes the camera rotate
Event::CursorPan(delta) => {
self.camera.rotate_by(Vec3::from(delta) * CURSOR_PAN_SCALE);
true
},
// Zoom the camera when a zoom event occurs
Event::Zoom(delta) => {
self.camera.zoom_by(delta);
true
},
// All other events are unhandled
_ => false,
}

View File

@ -95,10 +95,6 @@ impl PlayState for SessionState {
for event in global_state.window.fetch_events() {
let _handled = match event {
Event::Close => return PlayStateResult::Shutdown,
// When the window is resized, change the camera's aspect ratio
Event::Resize(dims) => {
self.scene.camera_mut().set_aspect_ratio(dims.x as f32 / dims.y as f32);
},
// When 'q' is pressed, exit the session
Event::Char('q') => return PlayStateResult::Pop,
// Toggle cursor grabbing

View File

@ -0,0 +1,80 @@
// Standard
use std::{
rc::Rc,
cell::RefCell,
};
// Library
use image::DynamicImage;
use vek::*;
// Crate
use crate::render::{
Consts,
UiLocals,
Renderer,
Texture,
UiPipeline,
};
// Local
use super::{
super::{
UiError,
Cache,
},
Element,
Bounds,
SizeRequest,
};
#[derive(Clone)]
pub struct Image {
texture: Rc<Texture<UiPipeline>>,
locals: Consts<UiLocals>,
}
impl Image {
pub fn new(renderer: &mut Renderer, image: &DynamicImage) -> Result<Self, UiError> {
Ok(Self {
texture: Rc::new(
renderer.create_texture(image)
.map_err(|err| UiError::RenderError(err))?
),
locals: renderer.create_consts(&[UiLocals::default()])
.map_err(|err| UiError::RenderError(err))?,
})
}
}
impl Element for Image {
fn get_hsize_request(&self) -> SizeRequest { SizeRequest::indifferent() }
fn get_vsize_request(&self) -> SizeRequest { SizeRequest::indifferent() }
fn maintain(
&mut self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.update_consts(&mut self.locals, &[UiLocals::new(
[bounds.x, bounds.y, bounds.w, bounds.h],
)])
.expect("Could not update UI image consts");
}
fn render(
&self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.render_ui_element(
cache.model(),
&self.locals,
&self.texture,
);
}
}

View File

@ -0,0 +1,181 @@
pub mod image;
// Standard
use std::rc::Rc;
// Library
use vek::*;
// Crate
use crate::render::{
Renderer,
Texture,
Consts,
UiLocals,
UiPipeline,
};
// Local
use super::{
UiError,
Cache,
Span,
SizeRequest,
};
// Bounds
pub type Bounds<T> = Rect<T, T>;
pub trait BoundsExt {
fn relative_to(self, other: Self) -> Self;
}
impl BoundsExt for Bounds<f32> {
fn relative_to(self, other: Self) -> Self {
Self::new(
other.x + self.x * other.w,
other.y + self.y * other.h,
self.w * other.w,
self.h * other.h,
)
}
}
pub trait BoundsSpan {
fn in_resolution(self, resolution: Vec2<f32>) -> Bounds<f32>;
}
impl BoundsSpan for Bounds<Span> {
fn in_resolution(self, resolution: Vec2<f32>) -> Bounds<f32> {
Bounds::new(
self.x.to_rel(resolution.x).rel,
self.y.to_rel(resolution.y).rel,
self.w.to_rel(resolution.x).rel,
self.h.to_rel(resolution.y).rel,
)
}
}
// Element
pub trait Element: 'static {
//fn deep_clone(&self) -> Rc<dyn Element>;
fn get_hsize_request(&self) -> SizeRequest;
fn get_vsize_request(&self) -> SizeRequest;
fn maintain(
&mut self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
);
fn render(
&self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
);
}
// Surface
#[derive(Clone)]
pub enum Surface {
Transparent,
Color(Rgba<f32>),
Texture(Rc<Texture<UiPipeline>>),
Bevel,
}
// Widget
#[derive(Clone)]
pub struct Widget<E: Element> {
inner: Box<E>,
background: Surface,
margin_top: Span,
margin_bottom: Span,
margin_left: Span,
margin_right: Span,
locals: Consts<UiLocals>,
}
impl<E: Element> Widget<E> {
pub fn new(renderer: &mut Renderer, inner: E) -> Result<Box<Self>, UiError> {
Ok(Box::new(Self {
inner: Box::new(inner),
background: Surface::Transparent,
margin_top: Span::rel(0.2),
margin_bottom: Span::rel(0.2),
margin_left: Span::rel(0.2),
margin_right: Span::rel(0.2),
locals: renderer.create_consts(&[UiLocals::default()])
.map_err(|err| UiError::RenderError(err))?,
}))
}
fn get_inner_bounds(&self) -> Bounds<Span> {
Bounds::new(
self.margin_left,
self.margin_top,
Span::full() - self.margin_left - self.margin_right,
Span::full() - self.margin_top - self.margin_bottom,
)
}
}
impl<E: Element> Element for Widget<E> {
fn get_hsize_request(&self) -> SizeRequest {
self.inner.get_hsize_request() + self.margin_left + self.margin_right
}
fn get_vsize_request(&self) -> SizeRequest {
self.inner.get_vsize_request() + self.margin_top + self.margin_bottom
}
fn maintain(
&mut self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.update_consts(&mut self.locals, &[UiLocals::new(
[bounds.x, bounds.y, bounds.w, bounds.h],
)])
.expect("Could not update UI image consts");
let inner_bounds = self
.get_inner_bounds()
.in_resolution(resolution)
.relative_to(bounds);
self.inner.maintain(renderer, cache, inner_bounds, resolution);
}
fn render(
&self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.render_ui_element(
cache.model(),
&self.locals,
&cache.blank_texture(),
);
let inner_bounds = self
.get_inner_bounds()
.in_resolution(resolution)
.relative_to(bounds);
self.inner.render(renderer, cache, inner_bounds, resolution);
}
}

85
voxygen/src/ui/mod.rs Normal file
View File

@ -0,0 +1,85 @@
pub mod element;
pub mod size_request;
pub mod span;
// Reexports
pub use self::{
span::Span,
size_request::SizeRequest,
};
// Library
use image::DynamicImage;
// Crate
use crate::{
Error,
render::{
RenderError,
Renderer,
Model,
Texture,
UiPipeline,
create_ui_quad_mesh,
},
};
// Local
use self::element::{
Element,
Bounds,
};
#[derive(Debug)]
pub enum UiError {
RenderError(RenderError),
}
pub struct Cache {
model: Model<UiPipeline>,
blank_texture: Texture<UiPipeline>,
}
impl Cache {
pub fn new(renderer: &mut Renderer) -> Result<Self, Error> {
Ok(Self {
model: renderer.create_model(&create_ui_quad_mesh())?,
blank_texture: renderer.create_texture(&DynamicImage::new_rgba8(1, 1))?,
})
}
pub fn model(&self) -> &Model<UiPipeline> { &self.model }
pub fn blank_texture(&self) -> &Texture<UiPipeline> { &self.blank_texture }
}
pub struct Ui {
base: Box<dyn Element>,
cache: Cache,
}
impl Ui {
pub fn new<E: Element>(renderer: &mut Renderer, base: Box<E>) -> Result<Self, Error> {
Ok(Self {
base,
cache: Cache::new(renderer)?,
})
}
pub fn maintain(&mut self, renderer: &mut Renderer) {
self.base.maintain(
renderer,
&self.cache,
Bounds::new(0.0, 0.0, 1.0, 1.0),
renderer.get_resolution().map(|e| e as f32),
)
}
pub fn render(&self, renderer: &mut Renderer) {
self.base.render(
renderer,
&self.cache,
Bounds::new(0.0, 0.0, 1.0, 1.0),
renderer.get_resolution().map(|e| e as f32),
);
}
}

View File

@ -0,0 +1,30 @@
// Standard
use std::ops::Add;
// Local
use super::Span;
pub struct SizeRequest {
min: Span,
max: Span,
}
impl SizeRequest {
pub fn indifferent() -> Self {
Self {
min: Span::rel(0.0),
max: Span::rel(std::f32::INFINITY),
}
}
}
impl Add<Span> for SizeRequest {
type Output = Self;
fn add(self, span: Span) -> Self {
Self {
min: self.min + span,
max: self.max + span,
}
}
}

47
voxygen/src/ui/span.rs Normal file
View File

@ -0,0 +1,47 @@
// Standard
use std::ops::{Add, Sub};
#[derive(Copy, Clone)]
pub struct Span {
pub rel: f32,
pub abs: f32,
}
impl Span {
pub fn rel(rel: f32) -> Self { Self { rel, abs: 0.0 } }
pub fn abs(abs: f32) -> Self { Self { rel: 0.0, abs } }
pub fn full() -> Self { Self { rel: 1.0, abs: 0.0 } }
pub fn half() -> Self { Self { rel: 0.5, abs: 0.0 } }
pub fn none() -> Self { Self { rel: 0.0, abs: 0.0 } }
pub fn to_abs(self, res: f32) -> Self {
Self { rel: 0.0, abs: self.rel * res + self.abs }
}
pub fn to_rel(self, res: f32) -> Self {
Self { rel: self.rel + self.abs / res, abs: 0.0 }
}
}
impl Add for Span {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
rel: self.rel + other.rel,
abs: self.abs + other.abs,
}
}
}
impl Sub for Span {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
rel: self.rel - other.rel,
abs: self.abs - other.abs,
}
}
}

View File

@ -97,6 +97,10 @@ impl Window {
glutin::Event::DeviceEvent { event, .. } => match event {
glutin::DeviceEvent::MouseMotion { delta: (dx, dy), .. } if cursor_grabbed =>
events.push(Event::CursorPan(Vec2::new(dx as f32, dy as f32))),
glutin::DeviceEvent::MouseWheel {
delta: glutin::MouseScrollDelta::LineDelta(_x, y),
..
} if cursor_grabbed => events.push(Event::Zoom(y as f32)),
_ => {},
},
_ => {},
@ -136,6 +140,8 @@ pub enum Event {
Char(char),
/// The cursor has been panned across the screen while grabbed.
CursorPan(Vec2<f32>),
/// The camera has been requested to zoom.
Zoom(f32),
/// A key that the game recognises has been pressed down
KeyDown(Key),
/// A key that the game recognises has been released down

1
voxygen/test_assets/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text

BIN
voxygen/test_assets/test.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
voxygen/test_assets/wall.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -32,6 +32,7 @@ impl World {
let air = Block::empty();
let stone = Block::new(1, Rgb::new(200, 220, 255));
let grass = Block::new(2, Rgb::new(50, 255, 0));
let sand = Block::new(3, Rgb::new(180, 150, 50));
let perlin_nz = Perlin::new();
@ -48,7 +49,7 @@ impl World {
if wposf.z < height - 1.0 {
stone
} else {
grass
sand
}
} else {
air