From 8184bc6fc0ac3b86926cb979d28cbdb436b226c0 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 13 Jan 2019 20:53:55 +0000 Subject: [PATCH] Added figures, segments, test .vox files, basic animation test --- common/Cargo.toml | 1 + common/src/comp/mod.rs | 2 +- common/src/figure/cell.rs | 33 ++++++ common/src/figure/mod.rs | 63 ++++++++++ common/src/lib.rs | 1 + common/src/state.rs | 1 - common/src/terrain/block.rs | 1 + common/src/vol.rs | 53 ++++++++- common/src/volumes/chunk.rs | 10 +- common/src/volumes/dyna.rs | 95 +++++++++++++++ common/src/volumes/mod.rs | 1 + voxygen/Cargo.toml | 3 +- .../shaders/{character.frag => figure.frag} | 3 +- .../shaders/{character.vert => figure.vert} | 17 ++- voxygen/src/anim/mod.rs | 63 ++++++++++ voxygen/src/main.rs | 2 + voxygen/src/mesh/mod.rs | 20 ++++ voxygen/src/mesh/segment.rs | 112 ++++++++++++++++++ voxygen/src/render/consts.rs | 8 +- voxygen/src/render/mesh.rs | 17 +++ voxygen/src/render/mod.rs | 11 +- voxygen/src/render/pipelines/character.rs | 47 -------- voxygen/src/render/pipelines/figure.rs | 93 +++++++++++++++ voxygen/src/render/pipelines/mod.rs | 2 +- voxygen/src/render/pipelines/skybox.rs | 2 + voxygen/src/render/renderer.rs | 63 ++++++---- voxygen/src/scene/camera.rs | 4 +- voxygen/src/scene/figure.rs | 79 ++++++++++++ voxygen/src/scene/mod.rs | 83 +++++++++++-- voxygen/test_assets/belt.vox | Bin 0 -> 1544 bytes voxygen/test_assets/chest.vox | Bin 0 -> 2396 bytes voxygen/test_assets/foot.vox | Bin 0 -> 1404 bytes voxygen/test_assets/hand.vox | Bin 0 -> 1272 bytes voxygen/test_assets/head.vox | Bin 0 -> 5232 bytes voxygen/test_assets/knight.vox | Bin 0 -> 9716 bytes voxygen/test_assets/pants.vox | Bin 0 -> 1784 bytes voxygen/test_assets/sword.vox | Bin 0 -> 1568 bytes 37 files changed, 790 insertions(+), 100 deletions(-) create mode 100644 common/src/figure/cell.rs create mode 100644 common/src/figure/mod.rs create mode 100644 common/src/volumes/dyna.rs rename voxygen/shaders/{character.frag => figure.frag} (86%) rename voxygen/shaders/{character.vert => figure.vert} (62%) create mode 100644 voxygen/src/anim/mod.rs create mode 100644 voxygen/src/mesh/mod.rs create mode 100644 voxygen/src/mesh/segment.rs delete mode 100644 voxygen/src/render/pipelines/character.rs create mode 100644 voxygen/src/render/pipelines/figure.rs create mode 100644 voxygen/src/scene/figure.rs create mode 100644 voxygen/test_assets/belt.vox create mode 100644 voxygen/test_assets/chest.vox create mode 100644 voxygen/test_assets/foot.vox create mode 100644 voxygen/test_assets/hand.vox create mode 100644 voxygen/test_assets/head.vox create mode 100644 voxygen/test_assets/knight.vox create mode 100644 voxygen/test_assets/pants.vox create mode 100644 voxygen/test_assets/sword.vox diff --git a/common/Cargo.toml b/common/Cargo.toml index f5d2368a13..d35d5f094a 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -7,3 +7,4 @@ edition = "2018" [dependencies] specs = "0.14" vek = "0.9" +dot_vox = "1.0" diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index cc7bec16b9..c7bb5a311f 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -1,7 +1,7 @@ pub mod phys; // External -use specs::{World as EcsWorld, Builder}; +use specs::World as EcsWorld; pub fn register_local_components(ecs_world: &mut EcsWorld) { ecs_world.register::(); diff --git a/common/src/figure/cell.rs b/common/src/figure/cell.rs new file mode 100644 index 0000000000..c6e1d79cc4 --- /dev/null +++ b/common/src/figure/cell.rs @@ -0,0 +1,33 @@ +// Library +use vek::*; + +/// A type representing a single voxel in a figure +#[derive(Copy, Clone, Debug)] +pub enum Cell { + Filled([u8; 3]), + Empty, +} + +impl Cell { + pub fn empty() -> Self { + Cell::Empty + } + + pub fn new(rgb: Rgb) -> Self { + Cell::Filled(rgb.into_array()) + } + + pub fn is_empty(&self) -> bool { + match self { + Cell::Filled(_) => false, + Cell::Empty => true, + } + } + + pub fn get_color(&self) -> Option> { + match self { + Cell::Filled(col) => Some(Rgb::from(*col)), + Cell::Empty => None, + } + } +} diff --git a/common/src/figure/mod.rs b/common/src/figure/mod.rs new file mode 100644 index 0000000000..cd39161bcf --- /dev/null +++ b/common/src/figure/mod.rs @@ -0,0 +1,63 @@ +pub mod cell; + +// Library +use vek::*; +use dot_vox::DotVoxData; + +// Crate +use crate::{ + vol::WriteVol, + volumes::dyna::Dyna, +}; + +// Local +use self::cell::Cell; + +/// A type representing a single figure bone (e.g: the limb of a character). +#[derive(Copy, Clone)] +pub struct Bone { + origin: Vec3, + offset: Vec3, + ori: Vec3, +} + +/// A type representing a volume that may be part of an animated figure. +/// +/// Figures are used to represent things like characters, NPCs, mobs, etc. +pub type Segment = Dyna; + +impl From for Segment { + fn from(dot_vox_data: DotVoxData) -> Self { + if let Some(model) = dot_vox_data.models.get(0) { + let palette = dot_vox_data + .palette + .iter() + .map(|col| Rgba::from(col.to_ne_bytes()).into()) + .collect::>(); + + let mut segment = Segment::filled( + Vec3::new( + model.size.x, + model.size.y, + model.size.z, + ), + Cell::empty(), + (), + ); + + for voxel in &model.voxels { + if let Some(&color) = palette.get(voxel.i as usize) { + // TODO: Maybe don't ignore this error? + let _ = segment.set( + Vec3::new(voxel.x, voxel.y, voxel.z).map(|e| e as i32), + Cell::new(color), + ); + } + } + + segment + } else { + Segment::filled(Vec3::zero(), Cell::empty(), ()) + } + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 398c59cc87..1aee96ce50 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -2,6 +2,7 @@ pub mod clock; pub mod comp; +pub mod figure; pub mod state; pub mod terrain; pub mod volumes; diff --git a/common/src/state.rs b/common/src/state.rs index 90f46b845f..084b701da6 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -8,7 +8,6 @@ use specs::World as EcsWorld; use crate::{ comp, terrain::TerrainMap, - vol::VolSize, }; /// How much faster should an in-game day be compared to a real day? diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 776c2f7a48..0b45fdcf8a 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -1,3 +1,4 @@ +#[derive(Copy, Clone, Debug)] pub struct Block { kind: u8, color: [u8; 3], diff --git a/common/src/vol.rs b/common/src/vol.rs index 025a3adbf5..bb49a86423 100644 --- a/common/src/vol.rs +++ b/common/src/vol.rs @@ -1,27 +1,76 @@ // Library use vek::*; +/// A volume that contains voxel data. pub trait BaseVol { type Vox; type Err; } -pub trait SizedVol: BaseVol { - const SIZE: Vec3; +// Utility types + +pub struct VoxPosIter { + pos: Vec3, + sz: Vec3, } +impl Iterator for VoxPosIter { + type Item = Vec3; + + fn next(&mut self) -> Option { + let mut old_pos = self.pos; + + if old_pos.z == self.sz.z { + old_pos.z = 0; + old_pos.y += 1; + if old_pos.y == self.sz.y { + old_pos.y = 0; + old_pos.x += 1; + if old_pos.x == self.sz.x { + return None; + } + } + } + + self.pos = old_pos + Vec3::unit_z(); + + Some(old_pos.map(|e| e as i32)) + } +} + +/// A volume that has a finite size. +pub trait SizedVol: BaseVol { + /// Get the size of the volume. + #[inline(always)] + fn get_size(&self) -> Vec3; + + /// Iterate through all potential voxel positions in this volume + fn iter_positions(&self) -> VoxPosIter { + VoxPosIter { + pos: Vec3::zero(), + sz: self.get_size(), + } + } +} + +/// A volume that provided read access to its voxel data. pub trait ReadVol: BaseVol { + /// Get a reference to the voxel at the provided position in the volume. #[inline(always)] fn get(&self, pos: Vec3) -> Result<&Self::Vox, Self::Err>; } +/// A volume that provides write access to its voxel data. pub trait WriteVol: BaseVol { + /// Set the voxel at the provided position in the volume to the provided value. #[inline(always)] fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result<(), Self::Err>; } // Utility traits +/// Used to specify a volume's compile-time size. This exists as a substitute until const generics +/// are implemented. pub trait VolSize { const SIZE: Vec3; } diff --git a/common/src/volumes/chunk.rs b/common/src/volumes/chunk.rs index 1acec192e8..fcd0912873 100644 --- a/common/src/volumes/chunk.rs +++ b/common/src/volumes/chunk.rs @@ -17,6 +17,7 @@ pub enum ChunkErr { OutOfBounds, } +/// A volume with dimensions known at compile-time. // V = Voxel // S = Size (replace when const generics are a thing) // M = Metadata @@ -27,6 +28,8 @@ pub struct Chunk { } impl Chunk { + /// Used to transform a voxel position in the volume into its corresponding index in the voxel + // array. #[inline(always)] fn idx_for(pos: Vec3) -> Option { if @@ -50,7 +53,8 @@ impl BaseVol for Chunk { } impl SizedVol for Chunk { - const SIZE: Vec3 = Vec3 { x: 32, y: 32, z: 32 }; + #[inline(always)] + fn get_size(&self) -> Vec3 { S::SIZE } } impl ReadVol for Chunk { @@ -73,6 +77,8 @@ impl WriteVol for Chunk { } impl Chunk { + /// Create a new `Chunk` with the provided dimensions and all voxels filled with duplicates of + /// the provided voxel. pub fn filled(vox: V, meta: M) -> Self { Self { vox: vec![vox; S::SIZE.product() as usize], @@ -81,10 +87,12 @@ impl Chunk { } } + /// Get a reference to the internal metadata. pub fn metadata(&self) -> &M { &self.meta } + /// Get a mutable reference to the internal metadata. pub fn metadata_mut(&mut self) -> &mut M { &mut self.meta } diff --git a/common/src/volumes/dyna.rs b/common/src/volumes/dyna.rs new file mode 100644 index 0000000000..f975b7e02a --- /dev/null +++ b/common/src/volumes/dyna.rs @@ -0,0 +1,95 @@ +// Library +use vek::*; + +// Local +use crate::vol::{ + BaseVol, + SizedVol, + ReadVol, + WriteVol, +}; + +pub enum DynaErr { + OutOfBounds, +} + +/// A volume with dimensions known only at the creation of the object. +// V = Voxel +// S = Size (replace when const generics are a thing) +// M = Metadata +pub struct Dyna { + vox: Vec, + meta: M, + sz: Vec3, +} + +impl Dyna { + /// Used to transform a voxel position in the volume into its corresponding index in the voxel + // array. + #[inline(always)] + fn idx_for(sz: Vec3, pos: Vec3) -> Option { + if + pos.map(|e| e >= 0).reduce_and() && + pos.map2(sz, |e, lim| e < lim as i32).reduce_and() + { + Some(( + pos.x * sz.y as i32 * sz.z as i32 + + pos.y * sz.z as i32 + + pos.z + ) as usize) + } else { + None + } + } +} + +impl BaseVol for Dyna { + type Vox = V; + type Err = DynaErr; +} + +impl SizedVol for Dyna { + #[inline(always)] + fn get_size(&self) -> Vec3 { self.sz } +} + +impl ReadVol for Dyna { + #[inline(always)] + fn get(&self, pos: Vec3) -> Result<&V, DynaErr> { + Self::idx_for(self.sz, pos) + .and_then(|idx| self.vox.get(idx)) + .ok_or(DynaErr::OutOfBounds) + } +} + +impl WriteVol for Dyna { + #[inline(always)] + fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result<(), DynaErr> { + Self::idx_for(self.sz, pos) + .and_then(|idx| self.vox.get_mut(idx)) + .map(|old_vox| *old_vox = vox) + .ok_or(DynaErr::OutOfBounds) + } +} + +impl Dyna { + /// Create a new `Dyna` with the provided dimensions and all voxels filled with duplicates of + /// the provided voxel. + pub fn filled(sz: Vec3, vox: V, meta: M) -> Self { + Self { + vox: vec![vox; sz.product() as usize], + meta, + sz, + } + } + + /// Get a reference to the internal metadata. + pub fn metadata(&self) -> &M { + &self.meta + } + + /// Get a mutable reference to the internal metadata. + pub fn metadata_mut(&mut self) -> &mut M { + &mut self.meta + } +} diff --git a/common/src/volumes/mod.rs b/common/src/volumes/mod.rs index d1f4568502..8980a5a1ff 100644 --- a/common/src/volumes/mod.rs +++ b/common/src/volumes/mod.rs @@ -1,2 +1,3 @@ +pub mod dyna; pub mod chunk; pub mod vol_map; diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 29c220175d..9535b62ecf 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [features] gl = ["gfx_device_gl"] -default = [] +default = ["gl"] [dependencies] common = { package = "veloren-common", path = "../common" } @@ -28,3 +28,4 @@ failure = "0.1" lazy_static = "1.1" log = "0.4" pretty_env_logger = "0.3" +dot_vox = "1.0" diff --git a/voxygen/shaders/character.frag b/voxygen/shaders/figure.frag similarity index 86% rename from voxygen/shaders/character.frag rename to voxygen/shaders/figure.frag index 6caf3768ba..60b1454ce3 100644 --- a/voxygen/shaders/character.frag +++ b/voxygen/shaders/figure.frag @@ -1,6 +1,7 @@ #version 330 core in vec3 f_pos; +in vec3 f_col; layout (std140) uniform u_locals { @@ -21,5 +22,5 @@ uniform u_globals { out vec4 tgt_color; void main() { - tgt_color = vec4(f_pos, 1.0); + tgt_color = vec4(f_col, 1.0); } diff --git a/voxygen/shaders/character.vert b/voxygen/shaders/figure.vert similarity index 62% rename from voxygen/shaders/character.vert rename to voxygen/shaders/figure.vert index 0fa6bc639b..2a27b94a49 100644 --- a/voxygen/shaders/character.vert +++ b/voxygen/shaders/figure.vert @@ -2,7 +2,7 @@ in vec3 v_pos; in vec3 v_col; -in uint v_bone; +in uint v_bone_idx; layout (std140) uniform u_locals { @@ -20,13 +20,26 @@ uniform u_globals { vec4 time; }; +struct BoneData { + mat4 bone_mat; +}; + +layout (std140) +uniform u_bones { + BoneData bones[16]; +}; + out vec3 f_pos; +out vec3 f_col; void main() { f_pos = v_pos; + f_col = v_col; gl_Position = proj_mat * view_mat * - vec4(0.5 * v_pos + cam_pos.xyz, 1); + model_mat * + bones[v_bone_idx].bone_mat * + vec4(v_pos, 1); } diff --git a/voxygen/src/anim/mod.rs b/voxygen/src/anim/mod.rs new file mode 100644 index 0000000000..afe754cba3 --- /dev/null +++ b/voxygen/src/anim/mod.rs @@ -0,0 +1,63 @@ +// Library +use vek::*; + +// Crate +use crate::render::FigureBoneData; + +#[derive(Copy, Clone)] +pub struct Bone { + parent: Option, // MUST be less than the current bone index + pub offset: Vec3, + pub ori: Quaternion, +} + +impl Bone { + pub fn default() -> Self { + Self { + parent: None, + offset: Vec3::zero(), + ori: Quaternion::identity(), + } + } + + pub fn compute_base_matrix(&self) -> Mat4 { + Mat4::::translation_3d(self.offset) * Mat4::from(self.ori) + } +} + +#[derive(Copy, Clone)] +pub struct Skeleton { + bones: [Bone; 16], +} + +impl Skeleton { + pub fn default() -> Self { + Self { + bones: [Bone::default(); 16], + } + } + + pub fn with_bone(mut self, bone_idx: u8, bone: Bone) -> Self { + self.bones[bone_idx as usize] = bone; + self + } + + pub fn bone(&self, bone_idx: u8) -> &Bone { &self.bones[bone_idx as usize] } + pub fn bone_mut(&mut self, bone_idx: u8) -> &mut Bone { &mut self.bones[bone_idx as usize] } + + pub fn compute_matrices(&self) -> [FigureBoneData; 16] { + let mut bone_data = [FigureBoneData::default(); 16]; + for i in 0..16 { + bone_data[i] = FigureBoneData::new( + self.bones[i].compute_base_matrix() + // * + //if let Some(parent_idx) = self.bones[i].parent { + // bone_data[parent_idx as usize] + //} else { + // Mat4::identity() + //} + ); + } + bone_data + } +} diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index 223534fd77..b0aac99913 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -1,5 +1,7 @@ +pub mod anim; pub mod error; pub mod menu; +pub mod mesh; pub mod render; pub mod scene; pub mod session; diff --git a/voxygen/src/mesh/mod.rs b/voxygen/src/mesh/mod.rs new file mode 100644 index 0000000000..0f432076ae --- /dev/null +++ b/voxygen/src/mesh/mod.rs @@ -0,0 +1,20 @@ +pub mod segment; + +// Library +use vek::*; + +// Crate +use crate::render::{ + self, + Mesh, +}; + +pub trait Meshable { + type Pipeline: render::Pipeline; + + fn generate_mesh(&self) -> Mesh { + self.generate_mesh_with_offset(Vec3::zero()) + } + + fn generate_mesh_with_offset(&self, offs: Vec3) -> Mesh; +} diff --git a/voxygen/src/mesh/segment.rs b/voxygen/src/mesh/segment.rs new file mode 100644 index 0000000000..06a6048e05 --- /dev/null +++ b/voxygen/src/mesh/segment.rs @@ -0,0 +1,112 @@ +// Project +use common::figure::Segment; + +// Library +use vek::*; + +// Project +use common::vol::{ + SizedVol, + ReadVol, +}; + +// Crate +use crate::{ + mesh::Meshable, + render::{ + self, + Mesh, + Quad, + FigurePipeline, + }, +}; + +type FigureVertex = ::Vertex; + +// Utility function +// TODO: Evaluate how useful this is +fn create_quad( + origin: Vec3, + unit_x: Vec3, + unit_y: Vec3, + col: Rgb, + bone: u8, +) -> Quad { + Quad::new( + FigureVertex::new(origin, col, bone), + FigureVertex::new(origin + unit_x, col, bone), + FigureVertex::new(origin + unit_x + unit_y, col, bone), + FigureVertex::new(origin + unit_y, col, bone), + ) +} + +impl Meshable for Segment { + type Pipeline = FigurePipeline; + + fn generate_mesh_with_offset(&self, offs: Vec3) -> Mesh { + let mut mesh = Mesh::new(); + + for pos in self.iter_positions() { + if let Some(col) = self + .get(pos) + .ok() + .and_then(|vox| vox.get_color()) + { + let col = col.map(|e| e as f32 / 255.0); + + // TODO: Face occlusion + + // -x + mesh.push_quad(create_quad( + offs + pos.map(|e| e as f32) + Vec3::unit_y(), + -Vec3::unit_y(), + Vec3::unit_z(), + col, + 0, + )); + // +x + mesh.push_quad(create_quad( + offs + pos.map(|e| e as f32) + Vec3::unit_x(), + Vec3::unit_y(), + Vec3::unit_z(), + col, + 0, + )); + // -y + mesh.push_quad(create_quad( + offs + pos.map(|e| e as f32), + Vec3::unit_x(), + Vec3::unit_z(), + col, + 0, + )); + // +y + mesh.push_quad(create_quad( + offs + pos.map(|e| e as f32) + Vec3::unit_y(), + Vec3::unit_z(), + Vec3::unit_x(), + col, + 0, + )); + // -z + mesh.push_quad(create_quad( + offs + pos.map(|e| e as f32), + Vec3::unit_y(), + Vec3::unit_x(), + col, + 0, + )); + // +z + mesh.push_quad(create_quad( + offs + pos.map(|e| e as f32) + Vec3::unit_z(), + Vec3::unit_x(), + Vec3::unit_y(), + col, + 0, + )); + } + } + + mesh + } +} diff --git a/voxygen/src/render/consts.rs b/voxygen/src/render/consts.rs index 1c15b0362a..08a23824e6 100644 --- a/voxygen/src/render/consts.rs +++ b/voxygen/src/render/consts.rs @@ -19,9 +19,9 @@ pub struct Consts { impl Consts { /// Create a new `Const` - pub fn new(factory: &mut gfx_backend::Factory) -> Self { + pub fn new(factory: &mut gfx_backend::Factory, len: usize) -> Self { Self { - buf: factory.create_constant_buffer(1), + buf: factory.create_constant_buffer(len), } } @@ -29,9 +29,9 @@ impl Consts { pub fn update( &mut self, encoder: &mut gfx::Encoder, - val: T, + vals: &[T], ) -> Result<(), RenderError> { - encoder.update_buffer(&self.buf, &[val], 0) + encoder.update_buffer(&self.buf, vals, 0) .map_err(|err| RenderError::UpdateError(err)) } } diff --git a/voxygen/src/render/mesh.rs b/voxygen/src/render/mesh.rs index 75e317845c..7a91cee1c6 100644 --- a/voxygen/src/render/mesh.rs +++ b/voxygen/src/render/mesh.rs @@ -2,6 +2,7 @@ use super::Pipeline; /// A `Vec`-based mesh structure used to store mesh data on the CPU. +#[derive(Clone)] pub struct Mesh { verts: Vec, } @@ -43,6 +44,22 @@ impl Mesh

{ self.verts.push(quad.d); self.verts.push(quad.a); } + + /// Push the vertices of another mesh onto the end of this mesh + pub fn push_mesh(&mut self, other: &Mesh

) { + self.verts.extend_from_slice(other.vertices()); + } + + /// Push the vertices of another mesh onto the end of this mesh + pub fn push_mesh_map P::Vertex>(&mut self, other: &Mesh

, mut f: F) { + // Reserve enough space in our Vec. This isn't necessary, but it tends to reduce the number + // of required (re)allocations. + self.verts.reserve(other.vertices().len()); + + for vert in other.vertices() { + self.verts.push(f(vert.clone())); + } + } } /// Represents a triangle stored on the CPU. diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index 0faf4dc7b0..a61e8ce7fd 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -8,14 +8,15 @@ mod util; // Reexports pub use self::{ consts::Consts, - mesh::{Mesh, Quad}, + mesh::{Mesh, Tri, Quad}, model::Model, renderer::{Renderer, TgtColorFmt, TgtDepthFmt}, pipelines::{ Globals, - character::{ - CharacterPipeline, - Locals as CharacterLocals, + figure::{ + FigurePipeline, + Locals as FigureLocals, + BoneData as FigureBoneData, }, skybox::{ create_mesh as create_skybox_mesh, @@ -47,7 +48,7 @@ pub enum RenderError { /// # Examples /// /// - `SkyboxPipeline` -/// - `CharacterPipeline` +/// - `FigurePipeline` pub trait Pipeline { type Vertex: Clone + diff --git a/voxygen/src/render/pipelines/character.rs b/voxygen/src/render/pipelines/character.rs deleted file mode 100644 index 8b95b86f91..0000000000 --- a/voxygen/src/render/pipelines/character.rs +++ /dev/null @@ -1,47 +0,0 @@ -// 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, - }, -}; - -gfx_defines! { - vertex Vertex { - pos: [f32; 3] = "v_pos", - col: [f32; 3] = "v_col", - bone: u8 = "v_bone", - } - - constant Locals { - model_mat: [[f32; 4]; 4] = "model_mat", - } - - pipeline pipe { - vbuf: gfx::VertexBuffer = (), - locals: gfx::ConstantBuffer = "u_locals", - globals: gfx::ConstantBuffer = "u_globals", - tgt_color: gfx::RenderTarget = "tgt_color", - tgt_depth: gfx::DepthTarget = gfx::preset::depth::LESS_EQUAL_WRITE, - } -} - -pub struct CharacterPipeline; - -impl Pipeline for CharacterPipeline { - type Vertex = Vertex; -} diff --git a/voxygen/src/render/pipelines/figure.rs b/voxygen/src/render/pipelines/figure.rs new file mode 100644 index 0000000000..e7d14f272b --- /dev/null +++ b/voxygen/src/render/pipelines/figure.rs @@ -0,0 +1,93 @@ +// Library +use gfx::{ + self, + // Macros + gfx_defines, + gfx_vertex_struct_meta, + gfx_constant_struct_meta, + gfx_impl_struct_meta, + gfx_pipeline, + gfx_pipeline_inner, +}; +use vek::*; + +// Local +use super::{ + Globals, + super::{ + Pipeline, + TgtColorFmt, + TgtDepthFmt, + util::arr_to_mat, + }, +}; + +gfx_defines! { + vertex Vertex { + pos: [f32; 3] = "v_pos", + col: [f32; 3] = "v_col", + bone_idx: u8 = "v_bone_idx", + } + + constant Locals { + model_mat: [[f32; 4]; 4] = "model_mat", + } + + constant BoneData { + bone_mat: [[f32; 4]; 4] = "bone_mat", + } + + pipeline pipe { + vbuf: gfx::VertexBuffer = (), + + locals: gfx::ConstantBuffer = "u_locals", + globals: gfx::ConstantBuffer = "u_globals", + bones: gfx::ConstantBuffer = "u_bones", + + tgt_color: gfx::RenderTarget = "tgt_color", + tgt_depth: gfx::DepthTarget = gfx::preset::depth::LESS_EQUAL_WRITE, + } +} + +impl Vertex { + pub fn new(pos: Vec3, col: Rgb, bone_idx: u8) -> Self { + Self { + pos: pos.into_array(), + col: col.into_array(), + bone_idx, + } + } + + pub fn with_bone_idx(mut self, bone_idx: u8) -> Self { + self.bone_idx = bone_idx; + self + } +} + +impl Locals { + pub fn default() -> Self { + Self { + model_mat: arr_to_mat(Mat4::identity().into_col_array()), + } + } +} + +impl BoneData { + pub fn new(bone_mat: Mat4) -> Self { + Self { + bone_mat: arr_to_mat(bone_mat.into_col_array()), + } + } + + pub fn default() -> Self { + Self { + bone_mat: arr_to_mat(Mat4::identity().into_col_array()), + } + } +} + +pub struct FigurePipeline; + +impl Pipeline for FigurePipeline { + type Vertex = Vertex; +} diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index e1bd0193c6..663174b70d 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -1,4 +1,4 @@ -pub mod character; +pub mod figure; pub mod skybox; // Library diff --git a/voxygen/src/render/pipelines/skybox.rs b/voxygen/src/render/pipelines/skybox.rs index c39a2b3ce6..25e94dab0b 100644 --- a/voxygen/src/render/pipelines/skybox.rs +++ b/voxygen/src/render/pipelines/skybox.rs @@ -33,8 +33,10 @@ gfx_defines! { pipeline pipe { vbuf: gfx::VertexBuffer = (), + locals: gfx::ConstantBuffer = "u_locals", globals: gfx::ConstantBuffer = "u_globals", + tgt_color: gfx::RenderTarget = "tgt_color", tgt_depth: gfx::DepthTarget = gfx::preset::depth::PASS_TEST, } diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index a46da68e5f..2e23f70f69 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -15,13 +15,13 @@ use super::{ gfx_backend, pipelines::{ Globals, - character, + figure, skybox, }, }; /// Represents the format of the window's color target. -pub type TgtColorFmt = gfx::format::Srgba8; +pub type TgtColorFmt = gfx::format::Rgba8; /// Represents the format of the window's depth target. pub type TgtDepthFmt = gfx::format::DepthStencil; @@ -42,7 +42,7 @@ pub struct Renderer { tgt_depth_view: TgtDepthView, skybox_pipeline: GfxPipeline>, - character_pipeline: GfxPipeline>, + figure_pipeline: GfxPipeline>, } impl Renderer { @@ -62,12 +62,12 @@ impl Renderer { include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/skybox.frag")), )?; - // Construct a pipeline for rendering characters - let character_pipeline = create_pipeline( + // Construct a pipeline for rendering figures + let figure_pipeline = create_pipeline( &mut factory, - character::pipe::new(), - include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/character.vert")), - include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/character.frag")), + figure::pipe::new(), + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/figure.vert")), + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/figure.frag")), )?; Ok(Self { @@ -79,7 +79,7 @@ impl Renderer { tgt_depth_view, skybox_pipeline, - character_pipeline, + figure_pipeline, }) } @@ -96,28 +96,23 @@ impl Renderer { self.device.cleanup(); } - /// Create a new set of constants. - pub fn create_consts(&mut self) -> Result, RenderError> { - Ok(Consts::new(&mut self.factory)) - } - - /// Create a new set of constants with a value. - pub fn create_consts_with( + /// Create a new set of constants with the provided values. + pub fn create_consts( &mut self, - val: T + vals: &[T], ) -> Result, RenderError> { - let mut consts = self.create_consts()?; - consts.update(&mut self.encoder, val)?; + let mut consts = Consts::new(&mut self.factory, vals.len()); + consts.update(&mut self.encoder, vals)?; Ok(consts) } - /// Update a set of constants with a new value. + /// Update a set of constants with the provided values. pub fn update_consts( &mut self, consts: &mut Consts, - val: T + vals: &[T] ) -> Result<(), RenderError> { - consts.update(&mut self.encoder, val) + consts.update(&mut self.encoder, vals) } /// Create a new model from the provided mesh. @@ -132,8 +127,8 @@ impl Renderer { pub fn render_skybox( &mut self, model: &Model, - locals: &Consts, globals: &Consts, + locals: &Consts, ) { self.encoder.draw( &model.slice, @@ -147,6 +142,28 @@ impl Renderer { }, ); } + + /// Queue the rendering of the provided figure model in the upcoming frame. + pub fn render_figure( + &mut self, + model: &Model, + globals: &Consts, + locals: &Consts, + bones: &Consts, + ) { + self.encoder.draw( + &model.slice, + &self.figure_pipeline.pso, + &figure::pipe::Data { + vbuf: model.vbuf.clone(), + locals: locals.buf.clone(), + globals: globals.buf.clone(), + bones: bones.buf.clone(), + tgt_color: self.tgt_color_view.clone(), + tgt_depth: self.tgt_depth_view.clone(), + }, + ); + } } struct GfxPipeline { diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index 5eca5669ce..aad5e5591d 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -19,9 +19,9 @@ impl Camera { /// Create a new `Camera` with default parameters. pub fn new() -> Self { Self { - focus: Vec3::zero(), + focus: Vec3::unit_z() * 10.0, ori: Vec3::zero(), - dist: 5.0, + dist: 40.0, fov: 1.3, aspect: 1.618, } diff --git a/voxygen/src/scene/figure.rs b/voxygen/src/scene/figure.rs new file mode 100644 index 0000000000..92bc610016 --- /dev/null +++ b/voxygen/src/scene/figure.rs @@ -0,0 +1,79 @@ +// Crate +use crate::{ + Error, + render::{ + Consts, + Globals, + Mesh, + Model, + Renderer, + FigurePipeline, + FigureBoneData, + FigureLocals, + }, + anim::Skeleton, +}; + +pub struct Figure { + // GPU data + model: Model, + bone_consts: Consts, + locals: Consts, + + // CPU data + bone_meshes: [Option>; 16], + pub skeleton: Skeleton, +} + +impl Figure { + pub fn new( + renderer: &mut Renderer, + bone_meshes: [Option>; 16] + ) -> Result { + let skeleton = Skeleton::default(); + let mut this = Self { + model: renderer.create_model(&Mesh::new())?, + bone_consts: renderer.create_consts(&skeleton.compute_matrices())?, + locals: renderer.create_consts(&[FigureLocals::default()])?, + + bone_meshes, + skeleton, + }; + this.update_model(renderer)?; + Ok(this) + } + + pub fn update_model(&mut self, renderer: &mut Renderer) -> Result<(), Error> { + let mut mesh = Mesh::new(); + + self.bone_meshes + .iter() + .enumerate() + .filter_map(|(i, bm)| bm.as_ref().map(|bm| (i, bm))) + .for_each(|(i, bone_mesh)| { + mesh.push_mesh_map(bone_mesh, |vert| vert.with_bone_idx(i as u8)) + }); + + self.model = renderer.create_model(&mesh)?; + Ok(()) + } + + pub fn update_skeleton(&mut self, renderer: &mut Renderer) -> Result<(), Error> { + renderer.update_consts(&mut self.bone_consts, &self.skeleton.compute_matrices())?; + Ok(()) + } + + pub fn update_locals(&mut self, renderer: &mut Renderer, locals: FigureLocals) -> Result<(), Error> { + renderer.update_consts(&mut self.locals, &[locals])?; + Ok(()) + } + + pub fn render(&self, renderer: &mut Renderer, globals: &Consts) { + renderer.render_figure( + &self.model, + globals, + &self.locals, + &self.bone_consts, + ); + } +} diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 2ff790d464..127aff17b2 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1,16 +1,19 @@ pub mod camera; +pub mod figure; // Standard use std::time::Duration; // Library use vek::*; +use dot_vox; // Project use client::{ self, Client, }; +use common::figure::Segment; // Crate use crate::{ @@ -22,47 +25,82 @@ use crate::{ Renderer, SkyboxPipeline, SkyboxLocals, + FigureLocals, create_skybox_mesh, }, window::Event, + mesh::Meshable, }; // Local -use self::camera::Camera; +use self::{ + camera::Camera, + figure::Figure, +}; + +// TODO: Don't hard-code this +const CURSOR_PAN_SCALE: f32 = 0.005; struct Skybox { model: Model, locals: Consts, } -// TODO: Don't hard-code this -const CURSOR_PAN_SCALE: f32 = 0.005; - pub struct Scene { camera: Camera, globals: Consts, skybox: Skybox, + test_figure: Figure, + client: Client, } +// TODO: Make a proper asset loading system +fn load_segment(filename: &'static str) -> Segment { + Segment::from(dot_vox::load(&(concat!(env!("CARGO_MANIFEST_DIR"), "/test_assets/").to_string() + filename)).unwrap()) +} + impl Scene { /// Create a new `Scene` with default parameters. pub fn new(renderer: &mut Renderer) -> Self { Self { camera: Camera::new(), globals: renderer - .create_consts_with(Globals::default()) + .create_consts(&[Globals::default()]) .unwrap(), skybox: Skybox { model: renderer .create_model(&create_skybox_mesh()) .unwrap(), locals: renderer - .create_consts_with(SkyboxLocals::default()) + .create_consts(&[SkyboxLocals::default()]) .unwrap(), }, + test_figure: Figure::new( + renderer, + [ + Some(load_segment("head.vox").generate_mesh_with_offset(Vec3::new(-7.0, -5.5, -1.0))), + Some(load_segment("chest.vox").generate_mesh_with_offset(Vec3::new(-6.0, -3.0, 0.0))), + Some(load_segment("belt.vox").generate_mesh_with_offset(Vec3::new(-5.0, -3.0, 0.0))), + Some(load_segment("pants.vox").generate_mesh_with_offset(Vec3::new(-5.0, -3.0, 0.0))), + Some(load_segment("foot.vox").generate_mesh_with_offset(Vec3::new(-2.5, -3.0, 0.0))), + Some(load_segment("foot.vox").generate_mesh_with_offset(Vec3::new(-2.5, -3.0, 0.0))), + Some(load_segment("hand.vox").generate_mesh_with_offset(Vec3::new(-2.0, -2.0, -1.0))), + Some(load_segment("hand.vox").generate_mesh_with_offset(Vec3::new(-2.0, -2.0, -1.0))), + Some(load_segment("sword.vox").generate_mesh_with_offset(Vec3::new(-6.5, -1.0, 0.0))), + None, + None, + None, + None, + None, + None, + None, + ], + ) + .unwrap(), + client: Client::new(), } } @@ -92,7 +130,7 @@ impl Scene { let (view_mat, proj_mat, cam_pos) = self.camera.compute_dependents(); // Update global constants - renderer.update_consts(&mut self.globals, Globals::new( + renderer.update_consts(&mut self.globals, &[Globals::new( view_mat, proj_mat, cam_pos, @@ -100,8 +138,32 @@ impl Scene { 10.0, self.client.state().get_time_of_day(), 0.0, - )) + )]) .expect("Failed to update global constants"); + + // TODO: Don't do this here + let offs = (self.client.state().get_tick() as f32 * 10.0).sin(); + self.test_figure.skeleton.bone_mut(0).offset = Vec3::new(0.0, 0.0, 13.0); + self.test_figure.skeleton.bone_mut(0).ori = Quaternion::rotation_z(offs * 0.3); + // Chest + self.test_figure.skeleton.bone_mut(1).offset = Vec3::new(0.0, 0.0, 9.0); + self.test_figure.skeleton.bone_mut(2).offset = Vec3::new(0.0, 0.0, 7.0); + self.test_figure.skeleton.bone_mut(3).offset = Vec3::new(0.0, 0.0, 4.0); + self.test_figure.skeleton.bone_mut(1).ori = Quaternion::rotation_z(offs * 0.15); + self.test_figure.skeleton.bone_mut(2).ori = Quaternion::rotation_z(offs * 0.15); + self.test_figure.skeleton.bone_mut(3).ori = Quaternion::rotation_z(offs * 0.15); + //Feet + self.test_figure.skeleton.bone_mut(4).offset = Vec3::new(-3.0, -offs * 4.0, 0.0); + self.test_figure.skeleton.bone_mut(5).offset = Vec3::new(3.0, offs * 4.0, 0.0); + // Hands + self.test_figure.skeleton.bone_mut(6).offset = Vec3::new(-8.0, offs * 4.0, 9.0); + self.test_figure.skeleton.bone_mut(7).offset = Vec3::new(8.0, -offs * 4.0, 9.0); + // Sword + self.test_figure.skeleton.bone_mut(8).offset = Vec3::new(-8.0, 5.0, 24.0); + self.test_figure.skeleton.bone_mut(8).ori = Quaternion::rotation_y(2.5); + + self.test_figure.update_locals(renderer, FigureLocals::default()); + self.test_figure.update_skeleton(renderer); } /// Render the scene using the provided `Renderer` @@ -109,8 +171,11 @@ impl Scene { // Render the skybox first (it appears over everything else so must be rendered first) renderer.render_skybox( &self.skybox.model, - &self.skybox.locals, &self.globals, + &self.skybox.locals, ); + + // Render the test figure + self.test_figure.render(renderer, &self.globals); } } diff --git a/voxygen/test_assets/belt.vox b/voxygen/test_assets/belt.vox new file mode 100644 index 0000000000000000000000000000000000000000..6604240bc9b9a68b466bbac328848914bc5df6db GIT binary patch literal 1544 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 literal 0 HcmV?d00001 diff --git a/voxygen/test_assets/foot.vox b/voxygen/test_assets/foot.vox new file mode 100644 index 0000000000000000000000000000000000000000..6cbbf067b25e882559c1938ed9e084c1ce6e696f GIT binary patch literal 1404 zcmc)JZ%CbG7{~GJ`E!&ctJ|Wfo#FW}BR(=!MCUF&Jamh#+K)h~D-hqIchZ$Gr1uedWXz zM@;;Ha^i|3mWnqh7DqhC)n2alaU;K()k^{kxuR)cLHny1F`Iu^4y8jNRv#DGL70pTQtYOYNlE#nx}u zl@ZAz=TL~yiE_eI^;k;)>%SkHei&QMBBR~NK5A?kH6FiYr1cvQ#*IwHSr?4z_H^2g zW)MA@$p;IM&>S;5#*NqJjN>=8wj-DPzCwy)Wz-}igz^S@v$yr^sw@SMpM}6sWS$fjY;J@s&naUDP*Xll!+si#9pdpqPK~q_h07s znLf&kvS_F*V9zt99OjuR4#BT_q%Y zw$sv3!M^9}iN4TC|DnAMy%}Zny?&Cjr&+mjp5(c1mcD$0^z?K#XKo}E3Q-X*q-NV= zJXINH^xZAYeOAcN*UDJEQO-H-nVd1Me{cNyqcPN1%4EEX<%>=H@k=KIXEGT1D#+!V zIjmCfzdwIfRTXDzr`TRG&5q$y!vLdEwMGVC> zRBejPh+&x#)6Qw%D)h~$s9L7RO*3k!ieee+6+;obFWOhkB17vKI@i!TM$}Lh#WK~K z>QzOt4E2hkNI^xt=zdfc!&0>o9i!~u`9f#mK)Uy~wl>n~H1{WjUFVmmas0&}4hQ*s zFLt{azOpK%K_{*Q9z4hD@lUlQp*2WiEi(NCvgD++SEx8FY#bN1ToQ^C!ebesOuh4h z(A-->&*4&H$IJL=;YqsFLSI&RV@^1FN9Xprsfv54O$TVn1*uFr@Q%3&%v95GV>1fho z^+91eBwXDuth_12)E^lT#t&+IMCkGhLv6xHw{TYDea&v-Aup*uACrd~Nx#y{-B)i)g2snv1S2DJ< zj@-~TLLCk4eX*U`zD^Pc_K6*D2;nsJ;gCB%c+{f8W6H6Dn`2FX8lBY@;`^LfL zJ1$nJ{@>5v)YQbMty64km}dLv30xT$H@+0^z9wAo`SAPw4A+eC(ynVaXOtZ$rr7=YBK?<^2?m4Q7m#9wKe3jDd)FTB@1EYT|H|tVEWEQvshxaO O&W^8^D->RRzWfb~B{1{= literal 0 HcmV?d00001 diff --git a/voxygen/test_assets/head.vox b/voxygen/test_assets/head.vox new file mode 100644 index 0000000000000000000000000000000000000000..3aaee9730caa7b84407c61d872fc5d03beaaecb3 GIT binary patch literal 5232 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 literal 0 HcmV?d00001 diff --git a/voxygen/test_assets/sword.vox b/voxygen/test_assets/sword.vox new file mode 100644 index 0000000000000000000000000000000000000000..16fa7b2d177f2239fa7628814974c750922faaf0 GIT binary patch literal 1568 zcmc&zTTEP46y0aw-a9Zn1{j8!0fv!>XuLX^Nc&$cP_63~5Rx4Z+4plr)Voev$ZLjDO2@r++5p*ORR5v-e(m z?S0OzdkVBtaMHX!18Em=0GW5kvTht`q*IUlli81B9mH=5%EX7|J$h zC`(wQ)l@uVaRFV$6WK&EkrB|8cS9Got*MNNsQ7JN*%{B-FqAzMtKt~fR}JNg7%C;C zDUDio+O&{CEzJ_QZ74mgD<^Bx*)KJuD;G6Qu9G#%JU!xgwf!P{%?}DmfVuT`BI})J>*_o4QWMJdC9<@1dX0c)>>h% z+f9pTYdMJ1CuDUa4QbRMeesa#RLHs$ z_Etg0IYHK_;JG^jYo8!-NRUR~c}q~d+AGAWolOT5;7~YqH*@NkL z_);d&{0zoLutfYs{7#Ht6wH4iSeq5x57Dm`Og9R`#|1m@3jFjhb_zz%Fg_rtFB0@r z2?iPj*BNgw_aM-ii^29nOq?x4@Xbn0^wwkTleh54OaLW$P8=%BLCed9INMo?zGlJD zNx{_nf|bt%6(F$mIi=Pw=Tzh=kCoCBdvH+GQozn_0*W(J>EPT@f5 zG-`&gz&+%~y{`oi-xk~|EX4f$JbLp6@Y>P4kfA}O_oriN^2r+pT`qXNUVK9!$ zICJePTs;|h@NEH>{BDf#uKs)fCePG5(Gfv^FK}sc7Mq)!(RWy0Ud9U-hH?1H6k5Jq zLC2jf)YjJGu>fKv@Hb+%1dqOZGT$S9UtL{Yd*=BUM*p*?Su^>xoEhIM7Zu*SUj7AN CVt>2< literal 0 HcmV?d00001