mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2024-11-21 13:12:45 +00:00
very wip wgpu renderer
This commit is contained in:
parent
a3a13ffb07
commit
f3b8d6e361
1823
Cargo.lock
generated
1823
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
26
Cargo.toml
26
Cargo.toml
|
@ -37,13 +37,14 @@ category = "Game"
|
|||
osx_minimum_system_version = "10.12"
|
||||
|
||||
[features]
|
||||
default = ["default-base", "backend-sdl", "render-opengl", "exe", "webbrowser", "discord-rpc"]
|
||||
default = ["default-base", "backend-sdl", "render-opengl", "render-wgpu", "exe", "webbrowser", "discord-rpc"]
|
||||
default-base = ["ogg-playback"]
|
||||
ogg-playback = ["lewton"]
|
||||
backend-sdl = ["sdl2", "sdl2-sys"]
|
||||
backend-winit = ["winit", "glutin", "render-opengl"]
|
||||
backend-winit = ["winit", "glutin"]
|
||||
backend-horizon = []
|
||||
render-opengl = []
|
||||
render-wgpu = ["wgpu"]
|
||||
discord-rpc = ["discord-rich-presence"]
|
||||
netplay = ["serde_cbor"]
|
||||
editor = []
|
||||
|
@ -52,7 +53,7 @@ android = []
|
|||
|
||||
[dependencies]
|
||||
#glutin = { path = "./3rdparty/glutin/glutin", optional = true }
|
||||
#winit = { path = "./3rdparty/winit", optional = true, default_features = false, features = ["x11"] }
|
||||
#winit = { path = "./3rdparty/winit", optional = true, default-features = false, features = ["x11"] }
|
||||
#sdl2 = { path = "./3rdparty/rust-sdl2", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
|
||||
#sdl2-sys = { path = "./3rdparty/rust-sdl2/sdl2-sys", optional = true, features = ["bundled", "static-link"] }
|
||||
#cpal = { path = "./3rdparty/cpal" }
|
||||
|
@ -61,13 +62,13 @@ byteorder = "1.4"
|
|||
case_insensitive_hashmap = "1.0.0"
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
cpal = { git = "https://github.com/doukutsu-rs/cpal", rev = "9d269d8724102404e73a61e9def0c0cbc921b676" }
|
||||
directories = "3"
|
||||
directories = "5"
|
||||
discord-rich-presence = { version = "0.2", optional = true }
|
||||
downcast = "0.11"
|
||||
encoding_rs = "0.8.33"
|
||||
glutin = { version = "0.32.0", optional = true }
|
||||
imgui = { git = "https://github.com/imgui-rs/imgui-rs.git", rev = "67f7f11363e62f09aa0e1288a17800e505860486" }
|
||||
image = { version = "0.24", default-features = false, features = ["png", "bmp"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "bmp"] }
|
||||
itertools = "0.13.0"
|
||||
include-flate = "0.3.0"
|
||||
lazy_static = "1.4"
|
||||
|
@ -78,25 +79,26 @@ num-traits = "0.2"
|
|||
open = "3.2"
|
||||
paste = "1.0"
|
||||
pelite = { version = ">=0.9.2", default-features = false, features = ["std"] }
|
||||
sdl2 = { version = "0.37", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
|
||||
sdl2-sys = { version = "0.37", optional = true, features = ["bundled", "static-link"] }
|
||||
rc-box = "1.2.0"
|
||||
pollster = "0.3.0"
|
||||
sdl2 = { version = "0.37", optional = true, features = ["unsafe_textures", "bundled", "static-link", "raw-window-handle", "use-bindgen"] }
|
||||
sdl2-sys = { version = "0.37", optional = true, features = ["bundled", "static-link", "use-bindgen"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_derive = "1"
|
||||
serde_cbor = { version = "0.11", optional = true }
|
||||
serde_json = "1.0"
|
||||
simple_logger = { version = "5.0.0", features = ["colors", "threads"] }
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
# remove and replace when extract_if is in stable
|
||||
vec_mut_scan = "0.4"
|
||||
webbrowser = { version = "1.0.1", optional = true }
|
||||
winit = { version = "0.30.2", optional = true, default_features = false, features = ["x11"] }
|
||||
wgpu = { git = "https://github.com/gfx-rs/wgpu.git", rev = "aadca17885e60ee4527da7559334dacec3b57475", optional = true }
|
||||
winit = { version = "0.30.2", optional = true, default-features = false, features = ["x11"] }
|
||||
xmltree = "0.10"
|
||||
|
||||
#hack to not link SDL_image on Windows(causes a linker error)
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
sdl2 = { version = "0.37", optional = true, features = ["image", "unsafe_textures", "bundled", "static-link"] }
|
||||
sdl2 = { version = "0.37", optional = true, features = ["image", "unsafe_textures", "bundled", "static-link", "raw-window-handle", "use-bindgen"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
|
|
|
@ -38,6 +38,8 @@ use crate::framework::graphics::{BlendMode, SwapMode};
|
|||
use crate::framework::keyboard::ScanCode;
|
||||
#[cfg(feature = "render-opengl")]
|
||||
use crate::framework::render_opengl::{GLContext, OpenGLRenderer};
|
||||
#[cfg(feature = "render-wgpu")]
|
||||
use crate::framework::render_wgpu::WGPURenderer;
|
||||
use crate::framework::ui::init_imgui;
|
||||
use crate::framework::{filesystem, render_opengl};
|
||||
use crate::game::shared_game_state::WindowMode;
|
||||
|
@ -161,6 +163,19 @@ struct SDL2Context {
|
|||
preferred_renderer: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn wm_info() -> sdl2_sys::SDL_SysWMinfo {
|
||||
unsafe {
|
||||
// safety: fields are initialized by SDL_GetWindowWMInfo
|
||||
let mut winfo: sdl2_sys::SDL_SysWMinfo = mem::MaybeUninit::zeroed().assume_init();
|
||||
winfo.version.major = sdl2_sys::SDL_MAJOR_VERSION as _;
|
||||
winfo.version.minor = sdl2_sys::SDL_MINOR_VERSION as _;
|
||||
winfo.version.patch = sdl2_sys::SDL_PATCHLEVEL as _;
|
||||
|
||||
winfo
|
||||
}
|
||||
}
|
||||
|
||||
impl SDL2EventLoop {
|
||||
pub fn new(sdl: &Sdl, size_hint: (u16, u16), ctx: &Context) -> GameResult<Box<dyn BackendEventLoop>> {
|
||||
let event_pump = sdl.event_pump().map_err(GameError::WindowError)?;
|
||||
|
@ -223,11 +238,7 @@ impl SDL2EventLoop {
|
|||
const NSWindowStyleMaskFullSizeContentView: u32 = 1 << 15;
|
||||
|
||||
// safety: fields are initialized by SDL_GetWindowWMInfo
|
||||
let mut winfo: sdl2_sys::SDL_SysWMinfo = mem::MaybeUninit::zeroed().assume_init();
|
||||
winfo.version.major = sdl2_sys::SDL_MAJOR_VERSION as _;
|
||||
winfo.version.minor = sdl2_sys::SDL_MINOR_VERSION as _;
|
||||
winfo.version.patch = sdl2_sys::SDL_PATCHLEVEL as _;
|
||||
|
||||
let mut winfo = wm_info();
|
||||
let mut whandle = self.refs.deref().borrow().window.window().raw();
|
||||
|
||||
if sdl2_sys::SDL_GetWindowWMInfo(whandle, &mut winfo as *mut _) != sdl2_sys::SDL_bool::SDL_FALSE {
|
||||
|
@ -320,9 +331,29 @@ impl SDL2EventLoop {
|
|||
OpenGLRenderer::new(gl_context, imgui)
|
||||
}
|
||||
|
||||
fn try_create_sdl2_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||
#[cfg(feature = "render-wgpu")]
|
||||
fn try_create_wgpu_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||
use std::{ffi::c_ulong, mem::MaybeUninit, num::NonZero, ptr::NonNull};
|
||||
|
||||
use sdl2_sys::{SDL_bool, SDL_SYSWM_TYPE};
|
||||
use wgpu::rwh::{HasDisplayHandle, HasWindowHandle};
|
||||
|
||||
let mut refs = self.refs.borrow_mut();
|
||||
|
||||
let (window_handle, display_handle) = unsafe {
|
||||
let window = refs.window.window();
|
||||
let window_handle = HasWindowHandle::window_handle(window)
|
||||
.map_err(|e| GameError::WindowError("Failed to get raw window handle".to_owned()))?;
|
||||
let display_handle = HasDisplayHandle::display_handle(window)
|
||||
.map_err(|e| GameError::WindowError("Failed to get raw display handle".to_owned()))?;
|
||||
|
||||
(window_handle.as_raw(), display_handle.as_raw())
|
||||
};
|
||||
let imgui = Self::create_imgui()?;
|
||||
|
||||
WGPURenderer::new(imgui, display_handle, window_handle)
|
||||
}
|
||||
|
||||
fn try_create_sdl2_renderer(&self) -> GameResult<Box<dyn BackendRenderer>> {
|
||||
let mut refs = self.refs.borrow_mut();
|
||||
let window = std::mem::take(&mut refs.window);
|
||||
|
@ -517,6 +548,9 @@ impl BackendEventLoop for SDL2EventLoop {
|
|||
let mut renderers = {
|
||||
let mut renderers: Vec<(&'static str, fn(&Self) -> GameResult<Box<dyn BackendRenderer>>)> = Vec::new();
|
||||
|
||||
#[cfg(feature = "render-wgpu")]
|
||||
renderers.push((WGPURenderer::RENDERER_ID, Self::try_create_wgpu_renderer));
|
||||
|
||||
#[cfg(feature = "render-opengl")]
|
||||
renderers.push((OpenGLRenderer::RENDERER_ID, Self::try_create_opengl_renderer));
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ pub mod graphics;
|
|||
pub mod keyboard;
|
||||
#[cfg(feature = "render-opengl")]
|
||||
pub mod render_opengl;
|
||||
#[cfg(feature = "render-wgpu")]
|
||||
pub mod render_wgpu;
|
||||
pub mod ui;
|
||||
pub mod util;
|
||||
pub mod vfs;
|
||||
|
|
|
@ -666,6 +666,8 @@ pub struct OpenGLRenderer {
|
|||
render_data: RenderData,
|
||||
def_matrix: [[f32; 4]; 4],
|
||||
curr_matrix: [[f32; 4]; 4],
|
||||
imgui_display_size: [f32; 2],
|
||||
imgui_display_scale: [f32; 2],
|
||||
}
|
||||
|
||||
impl OpenGLRenderer {
|
||||
|
@ -680,6 +682,9 @@ impl OpenGLRenderer {
|
|||
let mut render_data = RenderData::new();
|
||||
render_data.init(refs.gles2_mode, &mut imgui, &gl);
|
||||
|
||||
let imgui_display_size = imgui.io_mut().display_size;
|
||||
let imgui_display_scale = imgui.io_mut().display_framebuffer_scale;
|
||||
|
||||
Ok(Box::new(OpenGLRenderer {
|
||||
refs,
|
||||
gl,
|
||||
|
@ -687,6 +692,8 @@ impl OpenGLRenderer {
|
|||
render_data,
|
||||
def_matrix: [[0.0; 4]; 4],
|
||||
curr_matrix: [[0.0; 4]; 4],
|
||||
imgui_display_size,
|
||||
imgui_display_scale,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -709,13 +716,6 @@ impl BackendRenderer for OpenGLRenderer {
|
|||
}
|
||||
|
||||
fn present(&mut self) -> GameResult {
|
||||
{
|
||||
let mutex = GAME_SUSPENDED.lock().unwrap();
|
||||
if *mutex {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let gl = &self.gl;
|
||||
gl.gl.BindFramebuffer(gl::FRAMEBUFFER, self.render_data.render_fbo as _);
|
||||
|
@ -807,6 +807,9 @@ impl BackendRenderer for OpenGLRenderer {
|
|||
gl.gl.UseProgram(self.render_data.tex_shader.program_id);
|
||||
gl.gl.Uniform1i(self.render_data.tex_shader.texture, 0);
|
||||
gl.gl.UniformMatrix4fv(self.render_data.tex_shader.proj_mtx, 1, gl::FALSE, self.curr_matrix.as_ptr() as _);
|
||||
|
||||
self.imgui_display_size = self.imgui.borrow().io().display_size;
|
||||
self.imgui_display_scale = self.imgui.borrow().io().display_framebuffer_scale;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -1089,9 +1092,8 @@ impl BackendRenderer for OpenGLRenderer {
|
|||
gl.gl.Disable(gl::DEPTH_TEST);
|
||||
gl.gl.Enable(gl::SCISSOR_TEST);
|
||||
|
||||
let imgui = self.imgui()?;
|
||||
let [width, height] = imgui.borrow().io().display_size;
|
||||
let [scale_w, scale_h] = imgui.borrow().io().display_framebuffer_scale;
|
||||
let [width, height] = self.imgui_display_size;
|
||||
let [scale_w, scale_h] = self.imgui_display_scale;
|
||||
|
||||
let fb_width = width * scale_w;
|
||||
let fb_height = height * scale_h;
|
||||
|
|
735
src/framework/render_wgpu.rs
Normal file
735
src/framework/render_wgpu.rs
Normal file
|
@ -0,0 +1,735 @@
|
|||
use std::{
|
||||
any::Any,
|
||||
borrow::Borrow,
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use imgui::{DrawData, TextureId, Ui};
|
||||
use wgpu::{
|
||||
rwh::{RawDisplayHandle, RawWindowHandle},
|
||||
util::{DeviceExt, TextureDataOrder},
|
||||
AdapterInfo, Device, Extent3d, Instance, PowerPreference, Queue, RenderPass, Surface, SurfaceTargetUnsafe, Texture,
|
||||
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
};
|
||||
|
||||
use crate::common::{Color, Rect};
|
||||
|
||||
use super::{
|
||||
backend::{BackendRenderer, BackendShader, BackendTexture, SpriteBatchCommand, VertexData},
|
||||
error::{GameError, GameResult},
|
||||
graphics::{BlendMode, SwapMode},
|
||||
util::field_offset,
|
||||
};
|
||||
|
||||
const fn convert_swap_mode(swap_mode: SwapMode) -> wgpu::PresentMode {
|
||||
match swap_mode {
|
||||
SwapMode::Immediate => wgpu::PresentMode::AutoNoVsync,
|
||||
SwapMode::VSync => wgpu::PresentMode::AutoVsync,
|
||||
SwapMode::Adaptive => wgpu::PresentMode::Mailbox,
|
||||
}
|
||||
}
|
||||
|
||||
const fn to_wgpu_color(color: Color) -> wgpu::Color {
|
||||
wgpu::Color { r: color.r as _, g: color.g as _, b: color.b as _, a: color.a as _ }
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct VertUBO {
|
||||
proj_mtx: [[f32; 4]; 4],
|
||||
}
|
||||
|
||||
impl Default for VertUBO {
|
||||
fn default() -> Self {
|
||||
VertUBO { proj_mtx: [[0.0; 4]; 4] }
|
||||
}
|
||||
}
|
||||
|
||||
struct WGPUContext {
|
||||
instance: wgpu::Instance,
|
||||
adapter: wgpu::Adapter,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
surface: wgpu::Surface<'static>,
|
||||
surface_texture: RefCell<Option<wgpu::SurfaceTexture>>,
|
||||
swap_mode: Cell<SwapMode>,
|
||||
encoder: RefCell<Option<wgpu::CommandEncoder>>,
|
||||
render_target: RefCell<Option<wgpu::TextureView>>,
|
||||
basic_shader: wgpu::ShaderModule,
|
||||
vbo: wgpu::Buffer,
|
||||
ebo: wgpu::Buffer,
|
||||
curr_mtx: RefCell<VertUBO>,
|
||||
}
|
||||
|
||||
impl Drop for WGPUContext {
|
||||
fn drop(&mut self) {
|
||||
self.surface_texture.replace(None);
|
||||
self.render_target.replace(None);
|
||||
self.encoder.replace(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
|
||||
const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Unorm8x4, 2 => Float32x2];
|
||||
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<VertexData>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &ATTRIBS,
|
||||
}
|
||||
}
|
||||
|
||||
impl WGPUContext {
|
||||
fn get_encoder(&self) -> RefMut<'_, wgpu::CommandEncoder> {
|
||||
{
|
||||
let mut encoder = self.encoder.borrow_mut();
|
||||
if encoder.is_none() {
|
||||
*encoder = Some(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }));
|
||||
}
|
||||
}
|
||||
|
||||
RefMut::map(self.encoder.borrow_mut(), |e| e.as_mut().unwrap())
|
||||
}
|
||||
|
||||
fn finish_encoder_and_present(&self) {
|
||||
let mut encoder = self.encoder.borrow_mut();
|
||||
if let Some(encoder) = encoder.take() {
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
let st = self.surface_texture.take();
|
||||
if let Some(st) = st {
|
||||
st.present();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&self, width: u32, height: u32) -> GameResult {
|
||||
if width == 0 || height == 0 {
|
||||
return Err(GameError::RenderError("Invalid window size".to_owned()));
|
||||
}
|
||||
|
||||
let mut config = self.surface.get_default_config(&self.adapter, width, height);
|
||||
|
||||
if let Some(mut c) = config {
|
||||
c.present_mode = convert_swap_mode(self.swap_mode.get());
|
||||
|
||||
self.render_target.replace(None);
|
||||
self.surface_texture.replace(None);
|
||||
self.surface.configure(&self.device, &c);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(GameError::RenderError("Failed to get default surface configuration".to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_indexed(&self, vertices: &[VertexData], indices: &[u16], texture: &wgpu::Texture) {
|
||||
let mut encoder = self.get_encoder();
|
||||
|
||||
let camera_buffer = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
&self.curr_mtx.borrow().proj_mtx as *const _ as *const u8,
|
||||
std::mem::size_of::<VertUBO>(),
|
||||
)
|
||||
},
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let tex_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let sampler = self.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()
|
||||
});
|
||||
let texture_bind_group_layout = self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: None,
|
||||
});
|
||||
let camera_bind_group_layout = self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
label: None,
|
||||
});
|
||||
let texture_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &texture_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&tex_view) },
|
||||
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
|
||||
],
|
||||
label: None,
|
||||
});
|
||||
let camera_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &camera_bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry { binding: 0, resource: camera_buffer.as_entire_binding() }],
|
||||
label: None,
|
||||
});
|
||||
|
||||
let render_pipeline_layout = self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[&texture_bind_group_layout, &camera_bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let render_pipeline = self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &self.basic_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[vertex_layout()],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Cw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &self.basic_shader,
|
||||
entry_point: Some("fs_main_textured"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: self.render_target.borrow().as_ref().unwrap(),
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
let vbo = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("VBO"),
|
||||
contents: unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
vertices.as_ptr() as *const u8,
|
||||
vertices.len() * std::mem::size_of::<VertexData>(),
|
||||
)
|
||||
},
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let ebo = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("EBO"),
|
||||
contents: unsafe {
|
||||
std::slice::from_raw_parts(indices.as_ptr() as *const u8, indices.len() * std::mem::size_of::<u16>())
|
||||
},
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
rpass.set_pipeline(&render_pipeline);
|
||||
rpass.set_bind_group(0, &texture_bind_group, &[]);
|
||||
rpass.set_bind_group(1, &camera_bind_group, &[]);
|
||||
rpass.set_vertex_buffer(0, vbo.slice(..));
|
||||
rpass.set_index_buffer(ebo.slice(..), wgpu::IndexFormat::Uint16);
|
||||
rpass.draw_indexed(0..indices.len() as u32, 0, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WGPUTexture {
|
||||
ctx: Rc<WGPUContext>,
|
||||
image: Rc<wgpu::Texture>,
|
||||
vertices: Vec<VertexData>,
|
||||
indices: Vec<u16>,
|
||||
uv: (f32, f32),
|
||||
}
|
||||
|
||||
impl WGPUTexture {
|
||||
fn new(ctx: Rc<WGPUContext>, image: wgpu::Texture) -> Self {
|
||||
let (width, height) = (image.size().width, image.size().height);
|
||||
let uv = (1.0 / width as f32, 1.0 / height as f32);
|
||||
let image = Rc::new(image);
|
||||
|
||||
Self { ctx, image, vertices: Vec::new(), indices: Vec::new(), uv }
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendTexture for WGPUTexture {
|
||||
fn dimensions(&self) -> (u16, u16) {
|
||||
let size = self.image.size();
|
||||
(size.width as _, size.height as _)
|
||||
}
|
||||
|
||||
fn add(&mut self, command: SpriteBatchCommand) {
|
||||
let (tex_scale_x, tex_scale_y) = self.uv;
|
||||
|
||||
match command {
|
||||
SpriteBatchCommand::DrawRect(src, dest) => {
|
||||
let vertices = [
|
||||
VertexData {
|
||||
position: (dest.left, dest.bottom),
|
||||
uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.left, dest.top),
|
||||
uv: (src.left * tex_scale_x, src.top * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.top),
|
||||
uv: (src.right * tex_scale_x, src.top * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.bottom),
|
||||
uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
];
|
||||
let idx = self.vertices.len() as u16;
|
||||
self.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
|
||||
self.vertices.extend_from_slice(&vertices);
|
||||
}
|
||||
SpriteBatchCommand::DrawRectFlip(mut src, dest, flip_x, flip_y) => {
|
||||
if flip_x {
|
||||
std::mem::swap(&mut src.left, &mut src.right);
|
||||
}
|
||||
|
||||
if flip_y {
|
||||
std::mem::swap(&mut src.top, &mut src.bottom);
|
||||
}
|
||||
|
||||
let vertices = [
|
||||
VertexData {
|
||||
position: (dest.left, dest.bottom),
|
||||
uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.left, dest.top),
|
||||
uv: (src.left * tex_scale_x, src.top * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.top),
|
||||
uv: (src.right * tex_scale_x, src.top * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.bottom),
|
||||
uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color: (255, 255, 255, 255),
|
||||
},
|
||||
];
|
||||
let idx = self.vertices.len() as u16;
|
||||
self.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
|
||||
self.vertices.extend_from_slice(&vertices);
|
||||
}
|
||||
SpriteBatchCommand::DrawRectTinted(src, dest, color) => {
|
||||
let color = color.to_rgba();
|
||||
let vertices = [
|
||||
VertexData {
|
||||
position: (dest.left, dest.bottom),
|
||||
uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.left, dest.top),
|
||||
uv: (src.left * tex_scale_x, src.top * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.top),
|
||||
uv: (src.right * tex_scale_x, src.top * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.bottom),
|
||||
uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
];
|
||||
let idx = self.vertices.len() as u16;
|
||||
self.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
|
||||
self.vertices.extend_from_slice(&vertices);
|
||||
}
|
||||
SpriteBatchCommand::DrawRectFlipTinted(mut src, dest, flip_x, flip_y, color) => {
|
||||
if flip_x {
|
||||
std::mem::swap(&mut src.left, &mut src.right);
|
||||
}
|
||||
|
||||
if flip_y {
|
||||
std::mem::swap(&mut src.top, &mut src.bottom);
|
||||
}
|
||||
|
||||
let color = color.to_rgba();
|
||||
|
||||
let vertices = [
|
||||
VertexData {
|
||||
position: (dest.left, dest.bottom),
|
||||
uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.left, dest.top),
|
||||
uv: (src.left * tex_scale_x, src.top * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.top),
|
||||
uv: (src.right * tex_scale_x, src.top * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
VertexData {
|
||||
position: (dest.right, dest.bottom),
|
||||
uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
|
||||
color,
|
||||
},
|
||||
];
|
||||
let idx = self.vertices.len() as u16;
|
||||
self.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
|
||||
self.vertices.extend_from_slice(&vertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.vertices.clear();
|
||||
self.indices.clear();
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> GameResult<()> {
|
||||
if self.vertices.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.ctx.draw_indexed(&self.vertices, &self.indices, &self.image);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WGPURenderer {
|
||||
imgui: Rc<RefCell<imgui::Context>>,
|
||||
ctx: Rc<WGPUContext>,
|
||||
size: (u32, u32),
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl WGPURenderer {
|
||||
pub const RENDERER_ID: &'static str = "wgpu";
|
||||
|
||||
pub fn new(
|
||||
mut imgui: imgui::Context,
|
||||
raw_display_handle: RawDisplayHandle,
|
||||
raw_window_handle: RawWindowHandle,
|
||||
) -> GameResult<Box<dyn BackendRenderer>> {
|
||||
pollster::block_on(Self::new_async(imgui, raw_display_handle, raw_window_handle))
|
||||
}
|
||||
|
||||
// grrrr
|
||||
pub async fn new_async(
|
||||
mut imgui: imgui::Context,
|
||||
raw_display_handle: RawDisplayHandle,
|
||||
raw_window_handle: RawWindowHandle,
|
||||
) -> GameResult<Box<dyn BackendRenderer>> {
|
||||
let _ = imgui.fonts().build_alpha8_texture();
|
||||
|
||||
let instance = wgpu::Instance::default();
|
||||
|
||||
let surface = unsafe {
|
||||
instance
|
||||
.create_surface_unsafe(SurfaceTargetUnsafe::RawHandle { raw_display_handle, raw_window_handle })
|
||||
.map_err(|e: wgpu::CreateSurfaceError| {
|
||||
GameError::RenderError(format!("Failed to create WGPU surface: {:?}", e))
|
||||
})?
|
||||
};
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: PowerPreference::HighPerformance,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await;
|
||||
|
||||
let adapter = if let Some(adapter) = adapter {
|
||||
adapter
|
||||
} else {
|
||||
return Err(GameError::RenderError("No WGPU compatible adapter found".to_owned()));
|
||||
};
|
||||
|
||||
let AdapterInfo { name, driver, device_type, backend, .. } = adapter.get_info();
|
||||
log::info!("Selected adapter: {} ({}), type: {:?}", name, driver, device_type);
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
required_features: wgpu::Features::empty(),
|
||||
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
|
||||
required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
|
||||
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| GameError::RenderError(format!("Failed to create WGPU device: {:?}", e)))?;
|
||||
|
||||
let config = surface
|
||||
.get_default_config(&adapter, 640, 480)
|
||||
.ok_or_else(|| GameError::RenderError("Failed to get preferred surface format".to_owned()))?;
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let surface_texture = surface
|
||||
.get_current_texture()
|
||||
.map_err(|e| GameError::RenderError(format!("Failed to get surface texture: {:?}", e)))?;
|
||||
|
||||
let basic_module_desc: wgpu::ShaderModuleDescriptor = wgpu::include_wgsl!("shaders/wgpu/basic.wgsl");
|
||||
let basic_shader = device.create_shader_module(basic_module_desc);
|
||||
|
||||
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("VBO"),
|
||||
contents: &[],
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let ebo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("EBO"),
|
||||
contents: &[],
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
let ctx = Rc::new(WGPUContext {
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
surface_texture: RefCell::new(Some(surface_texture)),
|
||||
encoder: RefCell::new(None),
|
||||
swap_mode: Cell::new(SwapMode::VSync),
|
||||
render_target: RefCell::new(None),
|
||||
basic_shader,
|
||||
vbo,
|
||||
ebo,
|
||||
curr_mtx: RefCell::new(VertUBO::default()),
|
||||
});
|
||||
|
||||
let name = format!("wgpu-rs ({}, {})", backend, name);
|
||||
Ok(Box::new(WGPURenderer { imgui: Rc::new(RefCell::new(imgui)), ctx, name, size: (640, 480) }))
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendRenderer for WGPURenderer {
|
||||
fn renderer_name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn prepare_draw(&mut self, width: f32, height: f32) -> GameResult {
|
||||
let new_size = (width as u32, height as u32);
|
||||
if new_size != self.size {
|
||||
self.size = new_size;
|
||||
self.ctx.resize(self.size.0, self.size.1)?;
|
||||
}
|
||||
|
||||
self.ctx.surface_texture.replace(None);
|
||||
let surface_texture = self.ctx.surface.get_current_texture();
|
||||
let surface_texture = match surface_texture {
|
||||
Ok(surface_texture) => surface_texture,
|
||||
Err(e) => {
|
||||
self.ctx.resize(self.size.0, self.size.1)?;
|
||||
self.ctx
|
||||
.surface
|
||||
.get_current_texture()
|
||||
.map_err(|e| GameError::RenderError(format!("Failed to get surface texture: {:?}", e)))?
|
||||
}
|
||||
};
|
||||
self.ctx.surface_texture.replace(Some(surface_texture));
|
||||
|
||||
self.set_render_target(None)?;
|
||||
|
||||
self.ctx.curr_mtx.borrow_mut().proj_mtx = [
|
||||
[2.0 / width, 0.0, 0.0, 0.0],
|
||||
[0.0, 2.0 / -height, 0.0, 0.0],
|
||||
[0.0, 0.0, -1.0, 0.0],
|
||||
[-1.0, 1.0, 0.0, 1.0],
|
||||
];
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self, color: Color) {
|
||||
let view = self.ctx.render_target.borrow();
|
||||
let view = if let Some(rt) = view.as_ref() {
|
||||
rt
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut encoder = self.ctx.get_encoder();
|
||||
|
||||
let _rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Clear"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
//
|
||||
load: wgpu::LoadOp::Clear(to_wgpu_color(color)),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
});
|
||||
}
|
||||
|
||||
fn present(&mut self) -> GameResult {
|
||||
self.ctx.finish_encoder_and_present();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult<Box<dyn BackendTexture>> {
|
||||
let texture = self.ctx.device.create_texture(&TextureDescriptor {
|
||||
label: None,
|
||||
size: Extent3d { width: width as _, height: height as _, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
Ok(Box::new(WGPUTexture::new(Rc::clone(&self.ctx), texture)))
|
||||
}
|
||||
|
||||
fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult<Box<dyn BackendTexture>> {
|
||||
let texture = self.ctx.device.create_texture_with_data(
|
||||
&self.ctx.queue,
|
||||
&TextureDescriptor {
|
||||
label: None,
|
||||
size: Extent3d { width: width as _, height: height as _, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
TextureDataOrder::LayerMajor,
|
||||
data,
|
||||
);
|
||||
|
||||
Ok(Box::new(WGPUTexture::new(Rc::clone(&self.ctx), texture)))
|
||||
}
|
||||
|
||||
fn set_blend_mode(&mut self, _blend: BlendMode) -> GameResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_render_target(&mut self, texture: Option<&Box<dyn BackendTexture>>) -> GameResult {
|
||||
if let Some(texture) = texture {
|
||||
let wgpu_texture = texture
|
||||
.as_any()
|
||||
.downcast_ref::<WGPUTexture>()
|
||||
.ok_or_else(|| GameError::RenderError("This texture was not created by WGPU backend.".to_string()))?;
|
||||
|
||||
let view = wgpu_texture.image.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
self.ctx.render_target.replace(Some(view));
|
||||
} else {
|
||||
let surf_tex = self.ctx.surface_texture.borrow();
|
||||
if let Some(surf_tex) = surf_tex.as_ref() {
|
||||
let view = surf_tex.texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
self.ctx.render_target.replace(Some(view));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_rect(&mut self, _rect: Rect<isize>, _color: Color) -> GameResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_outline_rect(&mut self, _rect: Rect<isize>, _line_width: usize, _color: Color) -> GameResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_clip_rect(&mut self, _rect: Option<Rect>) -> GameResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn imgui(&self) -> GameResult<Rc<RefCell<imgui::Context>>> {
|
||||
Ok(self.imgui.clone())
|
||||
}
|
||||
|
||||
fn imgui_texture_id(&self, _texture: &Box<dyn BackendTexture>) -> GameResult<TextureId> {
|
||||
Ok(TextureId::from(0))
|
||||
}
|
||||
|
||||
fn prepare_imgui(&mut self, _ui: &Ui) -> GameResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_imgui(&mut self, _draw_data: &DrawData) -> GameResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_triangle_list(
|
||||
&mut self,
|
||||
_vertices: &[VertexData],
|
||||
_texture: Option<&Box<dyn BackendTexture>>,
|
||||
_shader: BackendShader,
|
||||
) -> GameResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
42
src/framework/shaders/wgpu/basic.wgsl
Normal file
42
src/framework/shaders/wgpu/basic.wgsl
Normal file
|
@ -0,0 +1,42 @@
|
|||
struct CameraUniform {
|
||||
proj_mtx: mat4x4<f32>,
|
||||
}
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(2) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(vtx: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.clip_position = u_cam.proj_mtx * vec4<f32>(vtx.position, 0.0, 1.0);
|
||||
out.color = vtx.color;
|
||||
out.tex_coords = vtx.tex_coords;
|
||||
return out;
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var u_texture: texture_2d<f32>;
|
||||
@group(0) @binding(1)
|
||||
var u_sampler: sampler;
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<uniform> u_cam: CameraUniform;
|
||||
|
||||
@fragment
|
||||
fn fs_main_textured(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(u_texture, u_sampler, in.tex_coords) * in.color;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_colored(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return in.color;
|
||||
}
|
Loading…
Reference in a new issue