render: implement 2d sprite rendering

This commit is contained in:
SeanOMik 2024-11-02 19:15:35 -04:00 committed by SeanOMik
parent 62adcf2b50
commit 6b9561d9bd
13 changed files with 526 additions and 23 deletions

18
.vscode/launch.json vendored
View File

@ -4,6 +4,24 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug lyra dim_2d example",
"cargo": {
"args": [
"build",
"--manifest-path", "${workspaceFolder}/examples/2d/Cargo.toml"
//"--bin=testbed",
],
"filter": {
"name": "dim_2d",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/examples/2d"
},
{
"type": "lldb",
"request": "launch",

View File

@ -414,12 +414,12 @@ impl Node for MeshPass {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
load: wgpu::LoadOp::Load,/* wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
}), */
store: wgpu::StoreOp::Store,
},
})],
@ -441,9 +441,9 @@ impl Node for MeshPass {
for job in render_meshes.iter() {
// get the mesh (containing vertices) and the buffers from storage
let buffers = mesh_buffers.get(&job.mesh_uuid);
let buffers = mesh_buffers.get(&job.asset_uuid);
if buffers.is_none() {
warn!("Skipping job since its mesh is missing {:?}", job.mesh_uuid);
warn!("Skipping job since its mesh is missing {:?}", job.asset_uuid);
continue;
}
let buffers = buffers.unwrap();

View File

@ -30,3 +30,6 @@ pub use mesh_prepare::*;
mod transform;
pub use transform::*;
mod sprite;
pub use sprite::*;

View File

@ -963,9 +963,9 @@ fn light_shadow_pass_impl<'a>(
for job in render_meshes.iter() {
// get the mesh (containing vertices) and the buffers from storage
let buffers = mesh_buffers.get(&job.mesh_uuid);
let buffers = mesh_buffers.get(&job.asset_uuid);
if buffers.is_none() {
warn!("Skipping job since its mesh is missing {:?}", job.mesh_uuid);
warn!("Skipping job since its mesh is missing {:?}", job.asset_uuid);
continue;
}
let buffers = buffers.unwrap();

View File

@ -0,0 +1,404 @@
use std::{
cell::RefMut,
collections::{HashMap, VecDeque},
rc::Rc,
sync::Arc,
};
use glam::{Vec2, Vec3};
use image::GenericImageView;
use lyra_ecs::{
query::{Entities, ResMut}, AtomicRef, Entity, ResourceData
};
use lyra_game_derive::RenderGraphLabel;
use lyra_resource::{Image, Texture};
use tracing::{info, instrument, warn};
use uuid::Uuid;
use wgpu::util::DeviceExt;
use crate::{
render::{
graph::{Node, NodeDesc, NodeType, SlotAttribute},
render_job::RenderJob,
resource::{
FragmentState, PipelineDescriptor, RenderPipeline, RenderPipelineDescriptor, Shader,
VertexState,
},
texture::{res_filter_to_wgpu, res_wrap_to_wgpu},
transform_buffer_storage::{TransformBuffers, TransformIndex},
vertex::Vertex2D,
},
sprite::Sprite,
};
use super::{BasePassSlots, RenderAssets};
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct SpritePassLabel;
#[derive(Debug, Hash, Clone, PartialEq, RenderGraphLabel)]
pub enum SpritePassSlots {
SpriteTexture,
SpriteTextureView,
SpriteTextureSampler,
}
struct SpriteTexture {
texture: wgpu::Texture,
texture_sampler: wgpu::Sampler,
texture_bg: Arc<wgpu::BindGroup>,
vertex_buffers: wgpu::Buffer,
index_buffers: wgpu::Buffer,
}
#[derive(Default)]
pub struct SpritePass {
pipeline: Option<RenderPipeline>,
texture_bgl: Option<Arc<wgpu::BindGroupLayout>>,
jobs: VecDeque<RenderJob>,
transform_buffers: Option<ResourceData>,
sprite_textures: Option<ResourceData>,
}
impl SpritePass {
pub fn new() -> Self {
Self::default()
}
#[instrument(skip(self, device, sprite))]
fn create_vertex_index_buffers(
&mut self,
device: &wgpu::Device,
sprite: &Sprite,
) -> (wgpu::Buffer, wgpu::Buffer) {
let tex_dims = sprite
.texture
.data_ref()
.map(|t| t.dimensions());
if tex_dims.is_none() {
info!("Sprite texture is not loaded, not rendering it until it is!");
todo!("Wait until texture is loaded");
}
let tex_dims = tex_dims.unwrap();
let vertices = vec![
// top left
Vertex2D::new(Vec3::new(0.0, 0.0, 0.0), Vec2::new(0.0, 1.0)),
// bottom left
Vertex2D::new(Vec3::new(0.0, tex_dims.1 as f32, 0.0), Vec2::new(0.0, 0.0)),
// top right
Vertex2D::new(Vec3::new(tex_dims.0 as f32, 0.0, 0.0), Vec2::new(1.0, 1.0)),
// bottom right
Vertex2D::new(
Vec3::new(tex_dims.0 as f32, tex_dims.1 as f32, 0.0),
Vec2::new(1.0, 0.0),
),
];
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(vertices.as_slice()),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let contents: [u32; 6] = [
//3, 1, 0,
//0, 2, 3
3, 1, 0, // second tri
0, 2, 3, // first tri
//0, 2, 3, // second tri
];
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(&contents),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
(vertex_buffer, index_buffer)
}
fn load_sprite_texture(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
uuid: &Uuid,
image: &Image,
) -> Option<(wgpu::Texture, wgpu::Sampler, wgpu::BindGroup)> {
let uuid_str = uuid.to_string();
let image_dim = image.dimensions();
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("sprite_texture_{}", uuid_str)),
size: wgpu::Extent3d {
width: image_dim.0,
height: image_dim.1,
depth_or_array_layers: 1,
},
mip_level_count: 1, // TODO
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let tex_view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
queue.write_texture(
wgpu::ImageCopyTexture {
aspect: wgpu::TextureAspect::All,
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
},
&image.to_rgba8(),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * image_dim.0),
rows_per_image: Some(image_dim.1),
},
wgpu::Extent3d {
width: image_dim.0,
height: image_dim.1,
depth_or_array_layers: 1,
},
);
let bgl = self.texture_bgl.as_ref().unwrap();
let tex_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("sprite_texture_bg_{}", uuid_str)),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&tex_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
Some((tex, sampler, tex_bg))
}
}
impl Node for SpritePass {
fn desc(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
) -> crate::render::graph::NodeDesc {
let device = &graph.device;
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bgl_sprite_main"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
self.texture_bgl = Some(Arc::new(bgl));
let mut desc = NodeDesc::new(NodeType::Render, None, vec![]);
desc.add_buffer_slot(BasePassSlots::Camera, SlotAttribute::Input, None);
desc
}
fn prepare(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
world: &mut lyra_ecs::World,
_: &mut crate::render::graph::RenderGraphContext,
) {
let device = graph.device();
let vt = graph.view_target();
if self.pipeline.is_none() {
let shader = Rc::new(Shader {
label: Some("sprite_shader".into()),
source: include_str!("../../shaders/2d/sprite_main.wgsl").to_string(),
});
let diffuse_bgl = self.texture_bgl.clone().unwrap();
let camera_bgl = graph.bind_group_layout(BasePassSlots::Camera).clone();
let transforms = world
.get_resource::<TransformBuffers>()
.expect("Missing transform buffers");
let transform_bgl = transforms.bindgroup_layout.clone();
self.pipeline = Some(RenderPipeline::create(
device,
&RenderPipelineDescriptor {
label: Some("sprite_pass".into()),
layouts: vec![diffuse_bgl, transform_bgl, camera_bgl],
push_constant_ranges: vec![],
vertex: VertexState {
module: shader.clone(),
entry_point: "vs_main".into(),
buffers: vec![Vertex2D::desc().into()],
},
fragment: Some(FragmentState {
module: shader,
entry_point: "fs_main".into(),
targets: vec![Some(wgpu::ColorTargetState {
format: vt.format(),
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
depth_stencil: None,
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
multisample: wgpu::MultisampleState::default(),
multiview: None,
},
));
drop(transforms);
world.add_resource_default_if_absent::<RenderAssets<SpriteTexture>>();
let sprite_textures = world
.get_resource_data::<RenderAssets<SpriteTexture>>()
.expect("Missing sprite texture store");
self.sprite_textures = Some(sprite_textures.clone());
let transforms = world
.get_resource_data::<TransformBuffers>()
.expect("Missing transform buffers");
self.transform_buffers = Some(transforms.clone());
}
let queue = &graph.queue;
for (entity, sprite, transform_idx, mut sprite_store) in world
.view::<(
Entities,
&Sprite,
&TransformIndex,
ResMut<RenderAssets<SpriteTexture>>,
)>()
.iter()
{
if let Some(image) = sprite.texture.data_ref() {
let texture_uuid = sprite.texture.uuid();
if !sprite_store.contains_key(&texture_uuid) {
// returns `None` if the Texture image is not loaded.
if let Some((tex, samp, tex_bg)) =
self.load_sprite_texture(device, queue, &texture_uuid, &image)
{
let (vertex, index) = self.create_vertex_index_buffers(device, &sprite);
sprite_store.insert(
texture_uuid,
SpriteTexture {
texture: tex,
texture_sampler: samp,
texture_bg: Arc::new(tex_bg),
vertex_buffers: vertex,
index_buffers: index,
},
);
}
}
self.jobs.push_back(RenderJob {
entity,
shader_id: 0,
asset_uuid: texture_uuid,
transform_id: *transform_idx,
});
}
}
}
fn execute(
&mut self,
graph: &mut crate::render::graph::RenderGraph,
_: &crate::render::graph::NodeDesc,
context: &mut crate::render::graph::RenderGraphContext,
) {
let pipeline = self.pipeline.as_ref().unwrap();
let sprite_store = self.sprite_textures.clone().unwrap();
let sprite_store: AtomicRef<RenderAssets<SpriteTexture>> = sprite_store.get();
let transforms = self.transform_buffers.clone().unwrap();
let transforms: AtomicRef<TransformBuffers> = transforms.get();
let vt = graph.view_target();
let view = vt.render_view();
let camera_bg = graph.bind_group(BasePassSlots::Camera);
{
let encoder = context.encoder.as_mut().unwrap();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("sprite_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline);
while let Some(job) = self.jobs.pop_front() {
let sprite = sprite_store.get(&job.asset_uuid)
.expect("failed to find SpriteTexture for job asset_uuid");
pass.set_bind_group(0, &sprite.texture_bg, &[]);
// Get the bindgroup for job's transform and bind to it using an offset.
let bindgroup = transforms.bind_group(job.transform_id);
let offset = transforms.buffer_offset(job.transform_id);
pass.set_bind_group(1, bindgroup, &[offset]);
pass.set_bind_group(2, camera_bg, &[]);
pass.set_vertex_buffer(
0,
sprite.vertex_buffers.slice(..),
);
pass.set_index_buffer(sprite.index_buffers.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..6, 0, 0..1);
}
}
}
}

View File

@ -32,12 +32,12 @@ pub struct InterpTransform {
#[derive(Default, Debug, Clone, Copy, Hash, RenderGraphLabel)]
pub struct TransformsNodeLabel;
#[derive(Debug)]
pub struct TransformsNode {}
#[derive(Debug, Default)]
pub struct TransformsNode;
impl TransformsNode {
pub fn new() -> Self {
Self {}
Self
}
}

View File

@ -5,7 +5,7 @@ use super::transform_buffer_storage::TransformIndex;
pub struct RenderJob {
pub entity: Entity,
pub shader_id: u64,
pub mesh_uuid: uuid::Uuid,
pub asset_uuid: uuid::Uuid,
pub transform_id: TransformIndex,
}
@ -14,7 +14,7 @@ impl RenderJob {
Self {
entity,
shader_id,
mesh_uuid: mesh_buffer_id,
asset_uuid: mesh_buffer_id,
transform_id
}
}

View File

@ -9,7 +9,7 @@ use lyra_game_derive::RenderGraphLabel;
use tracing::{debug, instrument, warn};
use winit::window::Window;
use crate::render::graph::{BasePass, BasePassLabel, BasePassSlots, FxaaPass, FxaaPassLabel, LightBasePass, LightBasePassLabel, LightCullComputePass, LightCullComputePassLabel, MeshPass, MeshPrepNode, MeshPrepNodeLabel, MeshesPassLabel, PresentPass, PresentPassLabel, RenderGraphLabelValue, RenderTarget, ShadowMapsPass, ShadowMapsPassLabel, SubGraphNode, TransformsNode, TransformsNodeLabel, ViewTarget};
use crate::render::graph::{BasePass, BasePassLabel, BasePassSlots, FxaaPass, FxaaPassLabel, LightBasePass, LightBasePassLabel, LightCullComputePass, LightCullComputePassLabel, MeshPass, MeshPrepNode, MeshPrepNodeLabel, MeshesPassLabel, PresentPass, PresentPassLabel, RenderGraphLabelValue, RenderTarget, ShadowMapsPass, ShadowMapsPassLabel, SpritePass, SpritePassLabel, SubGraphNode, TransformsNode, TransformsNodeLabel, ViewTarget};
use super::graph::RenderGraph;
use super::{resource::RenderPipeline, render_job::RenderJob};
@ -163,6 +163,10 @@ impl BasicRenderer {
forward_plus_graph.add_node(MeshesPassLabel, MeshPass::new(material_bgl));
forward_plus_graph.add_edge(TransformsNodeLabel, MeshPrepNodeLabel);
debug!("Adding sprite pass");
forward_plus_graph.add_node(SpritePassLabel, SpritePass::new());
forward_plus_graph.add_edge(TransformsNodeLabel, SpritePassLabel);
forward_plus_graph.add_edge(LightBasePassLabel, LightCullComputePassLabel);
forward_plus_graph.add_edge(LightCullComputePassLabel, MeshesPassLabel);
forward_plus_graph.add_edge(MeshPrepNodeLabel, MeshesPassLabel);

View File

@ -0,0 +1,60 @@
const ALPHA_CUTOFF = 0.1;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) tex_coords: vec2<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
@location(1) world_position: vec3<f32>,
}
struct TransformData {
transform: mat4x4<f32>,
normal_matrix: mat4x4<f32>,
}
struct CameraUniform {
view: mat4x4<f32>,
inverse_projection: mat4x4<f32>,
view_projection: mat4x4<f32>,
projection: mat4x4<f32>,
position: vec3<f32>,
tile_debug: u32,
}
@group(1) @binding(0)
var<uniform> u_model_transform_data: TransformData;
@group(2) @binding(0)
var<uniform> u_camera: CameraUniform;
@vertex
fn vs_main(
in: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
var world_position: vec4<f32> = u_model_transform_data.transform * vec4<f32>(in.position, 1.0);
out.world_position = world_position.xyz;
out.tex_coords = in.tex_coords;
out.clip_position = u_camera.view_projection * world_position;
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let object_color: vec4<f32> = textureSample(t_diffuse, s_diffuse, in.tex_coords);
if (object_color.a < ALPHA_CUTOFF) {
discard;
}
return object_color;
}

View File

@ -75,7 +75,7 @@ impl Vertex2D {
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
array_stride: std::mem::size_of::<Vertex2D>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {

View File

@ -45,7 +45,7 @@ impl Pivot {
#[derive(Clone, Component, Reflect)]
pub struct Sprite {
pub texture: ResHandle<lyra_resource::Texture>,
pub texture: ResHandle<lyra_resource::Image>,
pub color: Vec3,
pub pivot: Pivot,
}

View File

@ -1,12 +1,10 @@
use lyra_engine::{
assets::ResourceManager, gltf::Gltf, ecs::query::View, game::App, input::{
assets::{Image, ResourceManager, Texture}, ecs::query::View, game::App, gltf::Gltf, input::{
Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource,
InputActionPlugin, KeyCode, LayoutId,
}, math::{self, Transform, Vec3}, render::light::directional::DirectionalLight, scene::{
CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform,
ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN,
ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN,
}, winit::WindowOptions
system_update_world_transforms, CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN
}, sprite::{self, Sprite}, winit::WindowOptions
};
#[async_std::main]
@ -85,7 +83,8 @@ async fn main() {
.with_plugin(setup_scene_plugin)
.with_plugin(action_handler_plugin)
//.with_plugin(camera_debug_plugin)
.with_plugin(FreeFlyCameraPlugin);
.with_plugin(FreeFlyCameraPlugin)
.with_system("system_update_world_transforms", system_update_world_transforms, &[]);
a.run();
}
@ -113,14 +112,28 @@ fn setup_scene_plugin(app: &mut App) {
cube_gltf.wait_recurse_dependencies_load();
let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0];
drop(resman);
let image = resman.request::<Image>("../assets/Egg_item.png")
.unwrap();
image.wait_recurse_dependencies_load();
drop(resman);
world.spawn((
cube_mesh.clone(),
WorldTransform::default(),
Transform::from_xyz(0.0, 0.0, -2.0),
));
world.spawn((
Sprite {
texture: image,
color: Vec3::ONE,
pivot: sprite::Pivot::Center,
},
WorldTransform::default(),
Transform::from_xyz(0.0, 0.0, -20.0),
));
{
let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0);
light_tran.scale = Vec3::new(0.5, 0.5, 0.5);
@ -136,7 +149,7 @@ fn setup_scene_plugin(app: &mut App) {
));
}
let mut camera = CameraComponent::new_2d();
let mut camera = CameraComponent::new_3d();
camera.transform.translation += math::Vec3::new(0.0, 0.0, 5.5);
world.spawn((camera, FreeFlyCamera::default()));
}

1
examples/assets/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
Egg_item.png