use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use nalgebra::{Matrix4, Orthographic3, Perspective3};
use vulkano::buffer::{
Buffer, BufferContents, BufferCreateInfo, BufferUsage, Subbuffer,
};
use vulkano::command_buffer::allocator::StandardCommandBufferAllocator;
use vulkano::command_buffer::{
AutoCommandBufferBuilder, CommandBufferUsage, RenderPassBeginInfo,
SubpassBeginInfo, SubpassContents,
};
use vulkano::device::Queue;
use vulkano::format::Format;
use vulkano::image::view::ImageView;
use vulkano::image::{Image, ImageCreateInfo, ImageUsage};
use vulkano::memory::allocator::{
AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator,
};
use vulkano::pipeline::graphics::color_blend::{
ColorBlendAttachmentState, ColorBlendState,
};
use vulkano::pipeline::graphics::depth_stencil::{
DepthState, DepthStencilState,
};
use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
use vulkano::pipeline::graphics::multisample::MultisampleState;
use vulkano::pipeline::graphics::rasterization::{
CullMode, FrontFace, RasterizationState,
};
use vulkano::pipeline::graphics::subpass::PipelineSubpassType;
use vulkano::pipeline::graphics::vertex_input::{Vertex, VertexDefinition};
use vulkano::pipeline::graphics::viewport::{Viewport, ViewportState};
use vulkano::pipeline::graphics::GraphicsPipelineCreateInfo;
use vulkano::pipeline::layout::{
PipelineDescriptorSetLayoutCreateInfo, PipelineLayout,
};
use vulkano::pipeline::{
DynamicState, GraphicsPipeline, Pipeline, PipelineShaderStageCreateInfo,
};
use vulkano::render_pass::{
Framebuffer, FramebufferCreateInfo, RenderPass, Subpass,
};
use vulkano::sync::GpuFuture;
use crate::assets::{AssetServer, MeshAsset};
use crate::runtime::{Projection, RenderWorld};
#[derive(Debug)]
pub struct SceneRenderError(String);
impl Display for SceneRenderError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for SceneRenderError {}
#[repr(C)]
#[derive(BufferContents, Vertex, Clone, Copy)]
struct SceneVertex {
#[format(R32G32B32_SFLOAT)]
position: [f32; 3],
#[format(R32G32B32_SFLOAT)]
normal: [f32; 3],
}
#[repr(C)]
#[derive(BufferContents, Clone, Copy)]
struct DrawPushConstants {
mvp: [[f32; 4]; 4],
normal_columns: [[f32; 4]; 3],
color: [f32; 4],
}
struct PreparedMesh {
vertices: Subbuffer<[SceneVertex]>,
indices: Subbuffer<[u32]>,
source_revision: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SceneViewport {
pub offset: [u32; 2],
pub extent: [u32; 2],
}
impl SceneViewport {
#[must_use]
pub const fn full(extent: [u32; 2]) -> Self {
Self {
offset: [0, 0],
extent,
}
}
fn clamped_to(self, target_extent: [u32; 2]) -> Self {
let offset = [
self.offset[0].min(target_extent[0]),
self.offset[1].min(target_extent[1]),
];
Self {
offset,
extent: [
self.extent[0].min(target_extent[0].saturating_sub(offset[0])),
self.extent[1].min(target_extent[1].saturating_sub(offset[1])),
],
}
}
}
pub struct SceneRenderer {
queue: Arc<Queue>,
memory_allocator: Arc<StandardMemoryAllocator>,
command_allocator: Arc<StandardCommandBufferAllocator>,
render_pass: Arc<RenderPass>,
pipeline: Arc<GraphicsPipeline>,
depth: Arc<ImageView>,
depth_extent: [u32; 2],
prepared_meshes: HashMap<u64, PreparedMesh>,
}
impl SceneRenderer {
pub fn new(
queue: Arc<Queue>,
memory_allocator: Arc<StandardMemoryAllocator>,
output_format: Format,
initial_extent: [u32; 2],
) -> Result<Self, SceneRenderError> {
let render_pass = vulkano::single_pass_renderpass!(
queue.device().clone(),
attachments: {
color: {
format: output_format,
samples: 1,
load_op: Clear,
store_op: Store,
},
depth: {
format: Format::D32_SFLOAT,
samples: 1,
load_op: Clear,
store_op: DontCare,
}
},
pass: {
color: [color],
depth_stencil: {depth}
}
)
.map_err(|error| SceneRenderError(error.to_string()))?;
let pipeline = create_pipeline(queue.clone(), render_pass.clone())?;
let depth = create_depth(&memory_allocator, initial_extent)?;
Ok(Self {
command_allocator: Arc::new(StandardCommandBufferAllocator::new(
queue.device().clone(),
Default::default(),
)),
queue,
memory_allocator,
render_pass,
pipeline,
depth,
depth_extent: initial_extent,
prepared_meshes: HashMap::new(),
})
}
pub fn render(
&mut self,
before: Box<dyn GpuFuture>,
target: Arc<ImageView>,
extent: [u32; 2],
viewport: SceneViewport,
render_world: &RenderWorld,
assets: &AssetServer,
) -> Result<Box<dyn GpuFuture>, SceneRenderError> {
let viewport = viewport.clamped_to(extent);
if extent[0] == 0
|| extent[1] == 0
|| viewport.extent[0] == 0
|| viewport.extent[1] == 0
{
return Ok(before);
}
self.ensure_depth(extent)?;
self.prepare_visible_meshes(render_world, assets)?;
let framebuffer = Framebuffer::new(
self.render_pass.clone(),
FramebufferCreateInfo {
attachments: vec![target, self.depth.clone()],
..Default::default()
},
)
.map_err(|error| SceneRenderError(error.to_string()))?;
let mut commands = AutoCommandBufferBuilder::primary(
self.command_allocator.clone(),
self.queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.map_err(|error| SceneRenderError(error.to_string()))?;
commands
.begin_render_pass(
RenderPassBeginInfo {
clear_values: vec![
Some([0.025, 0.04, 0.07, 1.0].into()),
Some(1.0_f32.into()),
],
..RenderPassBeginInfo::framebuffer(framebuffer)
},
SubpassBeginInfo {
contents: SubpassContents::Inline,
..Default::default()
},
)
.map_err(|error| SceneRenderError(error.to_string()))?
.bind_pipeline_graphics(self.pipeline.clone())
.map_err(|error| SceneRenderError(error.to_string()))?
.set_viewport(
0,
[Viewport {
offset: [
viewport.offset[0] as f32,
viewport.offset[1] as f32,
],
extent: [
viewport.extent[0] as f32,
viewport.extent[1] as f32,
],
depth_range: 0.0..=1.0,
}]
.into_iter()
.collect(),
)
.map_err(|error| SceneRenderError(error.to_string()))?;
let view_projection = view_projection(render_world, viewport.extent);
for renderable in &render_world.renderables {
let Some(mesh) = self.prepared_meshes.get(&renderable.mesh.key())
else {
continue;
};
let model = matrix_from_array(renderable.transform.matrix);
let color = assets
.materials
.get(renderable.material)
.map_or([1.0, 0.0, 1.0, 1.0], |material| material.base_color);
commands
.push_constants(
self.pipeline.layout().clone(),
0,
DrawPushConstants {
mvp: (view_projection * model).into(),
normal_columns: normal_columns(model),
color,
},
)
.map_err(|error| SceneRenderError(error.to_string()))?
.bind_vertex_buffers(0, mesh.vertices.clone())
.map_err(|error| SceneRenderError(error.to_string()))?
.bind_index_buffer(mesh.indices.clone())
.map_err(|error| SceneRenderError(error.to_string()))?;
unsafe {
commands
.draw_indexed(mesh.indices.len() as u32, 1, 0, 0, 0)
.map_err(|error| SceneRenderError(error.to_string()))?;
}
}
commands
.end_render_pass(Default::default())
.map_err(|error| SceneRenderError(error.to_string()))?;
let command_buffer = commands
.build()
.map_err(|error| SceneRenderError(error.to_string()))?;
let future = before
.then_execute(self.queue.clone(), command_buffer)
.map_err(|error| SceneRenderError(error.to_string()))?;
Ok(future.boxed())
}
fn prepare_visible_meshes(
&mut self,
render_world: &RenderWorld,
assets: &AssetServer,
) -> Result<(), SceneRenderError> {
for renderable in &render_world.renderables {
let key = renderable.mesh.key();
let revision = assets.meshes.revision(renderable.mesh).unwrap_or(0);
if self
.prepared_meshes
.get(&key)
.is_some_and(|mesh| mesh.source_revision == revision)
{
continue;
}
let mesh = assets
.meshes
.get(renderable.mesh)
.or_else(|| assets.meshes.get(assets.fallback_mesh))
.ok_or_else(|| {
SceneRenderError("fallback mesh is missing".into())
})?;
self.prepared_meshes
.insert(key, self.prepare_mesh(mesh, revision)?);
}
Ok(())
}
fn prepare_mesh(
&self,
mesh: &MeshAsset,
source_revision: u64,
) -> Result<PreparedMesh, SceneRenderError> {
let vertices = Buffer::from_iter(
self.memory_allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
mesh.vertices.iter().map(|vertex| SceneVertex {
position: vertex.position,
normal: vertex.normal,
}),
)
.map_err(|error| SceneRenderError(error.to_string()))?;
let indices = Buffer::from_iter(
self.memory_allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::INDEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
mesh.indices.iter().copied(),
)
.map_err(|error| SceneRenderError(error.to_string()))?;
Ok(PreparedMesh {
vertices,
indices,
source_revision,
})
}
fn ensure_depth(
&mut self,
extent: [u32; 2],
) -> Result<(), SceneRenderError> {
if extent != self.depth_extent {
self.depth = create_depth(&self.memory_allocator, extent)?;
self.depth_extent = extent;
}
Ok(())
}
}
fn create_depth(
allocator: &Arc<StandardMemoryAllocator>,
extent: [u32; 2],
) -> Result<Arc<ImageView>, SceneRenderError> {
let image = Image::new(
allocator.clone(),
ImageCreateInfo {
format: Format::D32_SFLOAT,
extent: [extent[0].max(1), extent[1].max(1), 1],
usage: ImageUsage::DEPTH_STENCIL_ATTACHMENT,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
..Default::default()
},
)
.map_err(|error| SceneRenderError(error.to_string()))?;
ImageView::new_default(image)
.map_err(|error| SceneRenderError(error.to_string()))
}
fn create_pipeline(
queue: Arc<Queue>,
render_pass: Arc<RenderPass>,
) -> Result<Arc<GraphicsPipeline>, SceneRenderError> {
let vertex = vertex_shader::load(queue.device().clone())
.map_err(|error| SceneRenderError(error.to_string()))?
.entry_point("main")
.ok_or_else(|| {
SceneRenderError("scene vertex entry point is missing".into())
})?;
let fragment = fragment_shader::load(queue.device().clone())
.map_err(|error| SceneRenderError(error.to_string()))?
.entry_point("main")
.ok_or_else(|| {
SceneRenderError("scene fragment entry point is missing".into())
})?;
let stages = [
PipelineShaderStageCreateInfo::new(vertex.clone()),
PipelineShaderStageCreateInfo::new(fragment),
];
let layout = PipelineLayout::new(
queue.device().clone(),
PipelineDescriptorSetLayoutCreateInfo::from_stages(&stages)
.into_pipeline_layout_create_info(queue.device().clone())
.map_err(|error| SceneRenderError(error.to_string()))?,
)
.map_err(|error| SceneRenderError(error.to_string()))?;
let subpass = Subpass::from(render_pass, 0)
.ok_or_else(|| SceneRenderError("scene subpass is missing".into()))?;
GraphicsPipeline::new(
queue.device().clone(),
None,
GraphicsPipelineCreateInfo {
stages: stages.into_iter().collect(),
vertex_input_state: Some(
SceneVertex::per_vertex()
.definition(&vertex)
.map_err(|error| SceneRenderError(error.to_string()))?,
),
input_assembly_state: Some(InputAssemblyState::default()),
viewport_state: Some(ViewportState::default()),
rasterization_state: Some(RasterizationState {
cull_mode: CullMode::Back,
front_face: FrontFace::CounterClockwise,
..Default::default()
}),
multisample_state: Some(MultisampleState::default()),
depth_stencil_state: Some(DepthStencilState {
depth: Some(DepthState::simple()),
..Default::default()
}),
color_blend_state: Some(ColorBlendState::with_attachment_states(
1,
ColorBlendAttachmentState::default(),
)),
dynamic_state: [DynamicState::Viewport].into_iter().collect(),
subpass: Some(PipelineSubpassType::BeginRenderPass(subpass)),
..GraphicsPipelineCreateInfo::layout(layout)
},
)
.map_err(|error| SceneRenderError(error.to_string()))
}
fn view_projection(
render_world: &RenderWorld,
extent: [u32; 2],
) -> Matrix4<f32> {
let aspect = extent[0] as f32 / extent[1].max(1) as f32;
if let Some(camera) = render_world.active_camera {
let world_from_camera = matrix_from_array(camera.transform.matrix);
let view = world_from_camera
.try_inverse()
.unwrap_or_else(Matrix4::identity);
let projection = match camera.projection {
Projection::Perspective {
vertical_fov_radians,
near,
far,
} => Perspective3::new(aspect, vertical_fov_radians, near, far)
.to_homogeneous(),
Projection::Orthographic {
vertical_size,
near,
far,
} => Orthographic3::new(
-vertical_size * aspect * 0.5,
vertical_size * aspect * 0.5,
-vertical_size * 0.5,
vertical_size * 0.5,
near,
far,
)
.to_homogeneous(),
};
vulkan_clip_correction() * projection * view
} else {
vulkan_clip_correction()
* Perspective3::new(
aspect,
std::f32::consts::FRAC_PI_3,
0.1,
1_000.0,
)
.to_homogeneous()
}
}
fn vulkan_clip_correction() -> Matrix4<f32> {
Matrix4::new(
1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0,
0.0, 1.0,
)
}
fn matrix_from_array(matrix: [[f32; 4]; 4]) -> Matrix4<f32> {
Matrix4::from_column_slice(&matrix.concat())
}
fn normal_columns(model: Matrix4<f32>) -> [[f32; 4]; 3] {
let linear = model.fixed_view::<3, 3>(0, 0).into_owned();
let normal = linear
.try_inverse()
.map_or_else(nalgebra::Matrix3::identity, |inverse| {
inverse.transpose()
});
[
[normal[(0, 0)], normal[(1, 0)], normal[(2, 0)], 0.0],
[normal[(0, 1)], normal[(1, 1)], normal[(2, 1)], 0.0],
[normal[(0, 2)], normal[(1, 2)], normal[(2, 2)], 0.0],
]
}
#[rustfmt::skip]
mod vertex_shader {
vulkano_shaders::shader! {
ty: "vertex",
src: r"
#version 450
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 0) out vec3 v_normal;
layout(push_constant) uniform DrawPush {
mat4 mvp;
vec4 normal_columns[3];
vec4 color;
} draw;
void main() {
gl_Position = draw.mvp * vec4(position, 1.0);
mat3 normal_matrix = mat3(
draw.normal_columns[0].xyz,
draw.normal_columns[1].xyz,
draw.normal_columns[2].xyz
);
v_normal = normal_matrix * normal;
}
"
}
}
#[rustfmt::skip]
mod fragment_shader {
vulkano_shaders::shader! {
ty: "fragment",
src: r"
#version 450
layout(location = 0) in vec3 v_normal;
layout(location = 0) out vec4 f_color;
layout(push_constant) uniform DrawPush {
mat4 mvp;
vec4 normal_columns[3];
vec4 color;
} draw;
void main() {
vec3 n = normalize(v_normal);
float diffuse = max(dot(n, normalize(vec3(0.4, 0.8, 0.5))), 0.0);
f_color = vec4(draw.color.rgb * (0.22 + diffuse * 0.78), draw.color.a);
}
"
}
}
#[cfg(test)]
mod tests {
use nalgebra::Vector4;
use super::*;
fn perspective(aspect: f32, near: f32, far: f32) -> Matrix4<f32> {
vulkan_clip_correction()
* Perspective3::new(aspect, std::f32::consts::FRAC_PI_3, near, far)
.to_homogeneous()
}
fn ndc(matrix: &Matrix4<f32>, point: Vector4<f32>) -> Vector4<f32> {
let clip = matrix * point;
clip / clip.w
}
#[test]
fn perspective_makes_near_geometry_larger_than_far_geometry() {
let projection = perspective(16.0 / 9.0, 0.1, 100.0);
let near = ndc(&projection, Vector4::new(1.0, 0.0, -2.0, 1.0));
let far = ndc(&projection, Vector4::new(1.0, 0.0, -4.0, 1.0));
assert!(near.x.abs() > far.x.abs());
}
#[test]
fn perspective_maps_depth_to_vulkan_zero_to_one_range() {
let near_plane = 0.1;
let far_plane = 100.0;
let projection = perspective(1.0, near_plane, far_plane);
let near = ndc(&projection, Vector4::new(0.0, 0.0, -near_plane, 1.0));
let far = ndc(&projection, Vector4::new(0.0, 0.0, -far_plane, 1.0));
assert!(near.z.abs() < 0.000_01, "near depth was {}", near.z);
assert!((far.z - 1.0).abs() < 0.000_01, "far depth was {}", far.z);
}
#[test]
fn projection_preserves_square_pixel_aspect() {
let extent = [1000.0, 500.0];
let projection = perspective(extent[0] / extent[1], 0.1, 100.0);
let x = ndc(&projection, Vector4::new(1.0, 0.0, -4.0, 1.0));
let y = ndc(&projection, Vector4::new(0.0, 1.0, -4.0, 1.0));
let horizontal_pixels = x.x.abs() * extent[0];
let vertical_pixels = y.y.abs() * extent[1];
assert!((horizontal_pixels - vertical_pixels).abs() < 0.001);
}
#[test]
fn matrix_upload_round_trip_preserves_columns() {
let matrix =
Matrix4::new_translation(&nalgebra::Vector3::new(2.0, 3.0, 4.0));
let uploaded: [[f32; 4]; 4] = matrix.into();
assert_eq!(matrix_from_array(uploaded), matrix);
}
#[test]
fn draw_constants_fit_vulkan_minimum_push_constant_budget() {
assert_eq!(std::mem::size_of::<DrawPushConstants>(), 128);
}
#[test]
fn normal_matrix_ignores_translation_and_handles_scale() {
let model =
Matrix4::new_translation(&nalgebra::Vector3::new(2.0, 3.0, 4.0))
* Matrix4::new_nonuniform_scaling(&nalgebra::Vector3::new(
2.0, 4.0, 5.0,
));
assert_eq!(
normal_columns(model),
[
[0.5, 0.0, 0.0, 0.0],
[0.0, 0.25, 0.0, 0.0],
[0.0, 0.0, 0.2, 0.0],
]
);
}
}