From c9df90042fb663c9c7d37656cb4e99b4bc66461f Mon Sep 17 00:00:00 2001 From: Ben Hansen Date: Tue, 5 Nov 2019 01:02:41 -0700 Subject: [PATCH] textures --- .gitignore | 4 +- code/Cargo.toml | 4 + .../tutorial5-textures/happy-tree.png | Bin 0 -> 2278 bytes code/src/beginner/tutorial5-textures/main.rs | 380 ++++++++++++++++++ .../beginner/tutorial5-textures/shader.frag | 11 + .../beginner/tutorial5-textures/shader.vert | 11 + docs/.vuepress/config.js | 1 + docs/beginner/tutorial5-textures/README.md | 364 +++++++++++++++++ .../tutorial5-textures/happy-tree.png | Bin 0 -> 2278 bytes .../tutorial5-textures/happy-tree.xcf | Bin 0 -> 15703 bytes .../tutorial5-textures/rightside-up.png | Bin 0 -> 13831 bytes .../tutorial5-textures/upside-down.png | Bin 0 -> 12466 bytes docs/todo.md | 11 + 13 files changed, 785 insertions(+), 1 deletion(-) create mode 100644 code/src/beginner/tutorial5-textures/happy-tree.png create mode 100644 code/src/beginner/tutorial5-textures/main.rs create mode 100644 code/src/beginner/tutorial5-textures/shader.frag create mode 100644 code/src/beginner/tutorial5-textures/shader.vert create mode 100644 docs/beginner/tutorial5-textures/README.md create mode 100644 docs/beginner/tutorial5-textures/happy-tree.png create mode 100644 docs/beginner/tutorial5-textures/happy-tree.xcf create mode 100644 docs/beginner/tutorial5-textures/rightside-up.png create mode 100644 docs/beginner/tutorial5-textures/upside-down.png create mode 100644 docs/todo.md diff --git a/.gitignore b/.gitignore index 8c3fe3ed..7326f103 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ target/ -.vscode/ \ No newline at end of file +.vscode/ + +image.png \ No newline at end of file diff --git a/code/Cargo.toml b/code/Cargo.toml index ba07eab2..bc589d3b 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -22,6 +22,10 @@ path = "src/beginner/tutorial3-pipeline/main.rs" name = "tutorial4-buffer" path = "src/beginner/tutorial4-buffer/main.rs" +[[bin]] +name = "tutorial5-textures" +path = "src/beginner/tutorial5-textures/main.rs" + [[bin]] name = "windowless" path = "src/intermediate/windowless/main.rs" diff --git a/code/src/beginner/tutorial5-textures/happy-tree.png b/code/src/beginner/tutorial5-textures/happy-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..ead2f03b84e428c175f41e08d5d6af25e7eb47d0 GIT binary patch literal 2278 zcmYLJ2~<-_7Onpi$R`OQ8FmE;s7JIxVQ7xnEC~df#e+nehR&mV4{vvX8i` z6Solo0P5~;&c^_NFcSiZD%SbMR)u1NtZ){K?apG+xv|lq;SnJKm=>lM+PaC@T3JfI zN8WLRIr>;ZIWa@!bWX+bazxwdes^`|OV88Yca7a+Vzm576bGGql>Kk(zo{c;h;eQ0 z@7;S`JnRnceUh^zoJ^BTSVy!XFyCP}*KWAE`c81);#)PzeW-riVP{5 zeWF?@G`T@{La*%eFcmpniO<*4-22V7WEsJr*=>K3-)@Uovocuw$8mx(bL6r0%5}}C zV>XnyroK-dRO+D0HtPX}1AqR78Z}Y1=E;rbV}wu#_Xkg(Ow+m>)cyPfUngnQ4MOXs zaW4xD_A+0WcHtyv{^RLWCefMpJva5cUB^dUL8Z^`Xf%_>sbluf?vPNPcFm8Je2}Ms z3p-+~+v+QaF=A8$54$=8C0GXoH_xa4iXm!TH=lC=kTka%1a21VVIU#S-P46IPJ$2{ ze12}@9SqWsb2$;mijIs7iHZZP*pPs@kYIX3cw89W)!mcroleokvNyUrJAR+|@A;vY zNV9ewbTmHPiLTNiq-4-3eYMZk5)ZbG(m4k$3tA_*K0e$0^^-67`JX>>TXMaiMBHMS z(7V=9eSb5^5jvGj3?r4 zbX^1x!gVQGof=dq+t8yW`_lfjSW!NAD8=4nrz#@6n=XH~Xjm(v;x3%c;wd6qOL|v6 z>GnbRq083{*bQ*gSg=f@lsFskk4l0$1a`bk>UVx;WY@h8&Tpbqkm=X*QuDG->D1qJ zHaiJTQP5>|<$P&j_^7PAM`8VVy>|IIPzxNl?%G(78cFh5sxwa@S{A@4w>4j}6e?*=R24dAzFV=Aav@OD?dbQ#YB>#ye8r0J4n6-gsKc*$3hIY27H9 zaTMN{uS+(e+F>)XVOd5q3vdb*A-a~iqy{isPXc<^ekLg6;_-m!$^=5I&nzbX_4GXX zS)8&670?!+0n-!`#}%}fLVh)8jM04pz*kzl^*&Iuhoun@RE;kCZ#LNBEaU~=*t#n- zBOwGZVUPx)t)c^fohTDTjy)g{K9Exd0E^dvjeCR*DyZ;=a#bvxqPeZiwxm{QOsG5l zWQZcnw*#;Mux;QWn4=5U`XCel?Pwex%wgJ+nP9=akxc29Fic(9vu?5o7C$#>h&(dI}OIGkz=9*mBD`C}$-AW^s!VAIDwImQI>Mya0M)Ixl(0B1 zwMq5{L=`5L2^J`=!#0hZ8&0qqZZXUN4Z53*OwVXs!T~LH^d6%R0`Fn*jFH3KELFNtkoiFa?ks0h%s(GX=^P=c@s86hT1TRc0j> z0b0ZA*zcVpL#A3suAJg{j#^&xy#SbOk(&dwD8fzzaK3RI^ zD?{ijqqDNpr%DA3-iE-B{OCSwgwnV&arKmQQG#Jlyr-)?wtr4{2ZZpetjw#D`3f4Z zVx{6ic)Nckz&GdTuFnNXQh99;M3EKIs>H`!hh*^VRm|+>-C-(;nOo-UJ8fUGzTHm% z4wL6QBO@DYXj-7icO!)@t`JHNRaZsQWjk!D;YdxNDP8f4U%>n(ID@% zE`T(;G#hpgmo{0+VWx?cGG||IfmuszvHl;7rF3See<^OlbAQx1JbEdSYWjs&Ir*i(^d`q;t%i~_2J_z4C1a5zQBXZOiT;n8I3JHjLVm2C6o*dKsR_>VSbym&oA)* zIf%ZKhCmXGVEd{2!Q}I5=sW|9MDI0c{!dfbcx$>w(d2*^a#28!L(uZYRTGc{U=B59 z9zh`hS;(*AU`~(i4g_sqs6$W|r_j~`M%(iN_c!tOv~n$wr{CcT%mNJ<(8EbrJYJa5 zZmA7O+8xJ1#2O8P9-789st5zyVst?swc|7xlZx~cwu1dFcsu=uCajGjwu@SFs#@M17l9n&|4~jg*hlVp=Z+zi>t382() -> wgpu::VertexBufferDescriptor<'a> { + use std::mem; + wgpu::VertexBufferDescriptor { + stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::InputStepMode::Vertex, + attributes: &[ + wgpu::VertexAttributeDescriptor { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float3, + }, + wgpu::VertexAttributeDescriptor { + offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float2, + }, + ] + } + } +} + +const VERTICES: &[Vertex] = &[ + Vertex { position: [-0.0868241, -0.49240386, 0.0], tex_coords: [0.4131759, 1.0 - 0.99240386], }, // A + Vertex { position: [-0.49513406, -0.06958647, 0.0], tex_coords: [0.0048659444, 1.0 - 0.56958646], }, // B + Vertex { position: [-0.21918549, 0.44939706, 0.0], tex_coords: [0.28081453, 1.0 - 0.050602943], }, // C + Vertex { position: [0.35966998, 0.3473291, 0.0], tex_coords: [0.85967, 1.0 - 0.15267089], }, // D + Vertex { position: [0.44147372, -0.2347359, 0.0], tex_coords: [0.9414737, 1.0 - 0.7347359], }, // E +]; + +const INDICES: &[u16] = &[ + 0, 1, 4, + 1, 2, 4, + 2, 3, 4, +]; + +struct State { + surface: wgpu::Surface, + device: wgpu::Device, + sc_desc: wgpu::SwapChainDescriptor, + swap_chain: wgpu::SwapChain, + + render_pipeline: wgpu::RenderPipeline, + + vertex_buffer: wgpu::Buffer, + index_buffer: wgpu::Buffer, + num_indices: u32, + + diffuse_texture: wgpu::Texture, + diffuse_texture_view: wgpu::TextureView, + diffuse_sampler: wgpu::Sampler, + diffuse_bind_group: wgpu::BindGroup, + + hidpi_factor: f64, + size: winit::dpi::LogicalSize, +} + +impl State { + fn new(window: &Window) -> Self { + let hidpi_factor = window.hidpi_factor(); + let size = window.inner_size(); + let physical_size = size.to_physical(hidpi_factor); + + let instance = wgpu::Instance::new(); + + use raw_window_handle::HasRawWindowHandle as _; + let surface = instance.create_surface(window.raw_window_handle()); + + let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: Default::default(), + }); + + let mut device = adapter.request_device(&wgpu::DeviceDescriptor { + extensions: wgpu::Extensions { + anisotropic_filtering: false, + }, + limits: Default::default(), + }); + + let sc_desc = wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: physical_size.width.round() as u32, + height: physical_size.height.round() as u32, + present_mode: wgpu::PresentMode::Vsync, + }; + let swap_chain = device.create_swap_chain(&surface, &sc_desc); + + let diffuse_bytes = include_bytes!("happy-tree.png"); + let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); + let diffuse_rgba = diffuse_image.as_rgba8().unwrap(); + + use image::GenericImageView; + let dimensions = diffuse_image.dimensions(); + + let size3d = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth: 1, + }; + let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor { + size: size3d, + array_layer_count: 1, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST, + }); + + let diffuse_buffer = device + .create_buffer_mapped(diffuse_rgba.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&diffuse_rgba); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + todo: 0, + }); + + encoder.copy_buffer_to_texture( + wgpu::BufferCopyView { + buffer: &diffuse_buffer, + offset: 0, + row_pitch: 4 * dimensions.0, + image_height: dimensions.1, + }, + wgpu::TextureCopyView { + texture: &diffuse_texture, + mip_level: 0, + array_layer: 0, + origin: wgpu::Origin3d::ZERO, + }, + size3d, + ); + + device.get_queue().submit(&[encoder.finish()]); + + let diffuse_texture_view = diffuse_texture.create_default_view(); + let diffuse_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::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: -100.0, + lod_max_clamp: 100.0, + compare_function: wgpu::CompareFunction::Always, + }); + + let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[ + wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::SampledTexture { + multisampled: false, + dimension: wgpu::TextureViewDimension::D2, + }, + }, + wgpu::BindGroupLayoutBinding { + binding: 1, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler, + }, + ], + }); + + let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &texture_bind_group_layout, + bindings: &[ + wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), + }, + wgpu::Binding { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + } + ], + }); + + let vs_src = include_str!("shader.vert"); + let fs_src = include_str!("shader.frag"); + let vs_spirv = glsl_to_spirv::compile(vs_src, glsl_to_spirv::ShaderType::Vertex).unwrap(); + let fs_spirv = glsl_to_spirv::compile(fs_src, glsl_to_spirv::ShaderType::Fragment).unwrap(); + let vs_data = wgpu::read_spirv(vs_spirv).unwrap(); + let fs_data = wgpu::read_spirv(fs_spirv).unwrap(); + let vs_module = device.create_shader_module(&vs_data); + let fs_module = device.create_shader_module(&fs_data); + + let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&texture_bind_group_layout], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &render_pipeline_layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Ccw, + cull_mode: wgpu::CullMode::Back, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[ + wgpu::ColorStateDescriptor { + format: sc_desc.format, + color_blend: wgpu::BlendDescriptor::REPLACE, + alpha_blend: wgpu::BlendDescriptor::REPLACE, + write_mask: wgpu::ColorWrite::ALL, + }, + ], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[ + Vertex::desc(), + ], + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + let vertex_buffer = device + .create_buffer_mapped(VERTICES.len(), wgpu::BufferUsage::VERTEX) + .fill_from_slice(VERTICES); + let index_buffer = device + .create_buffer_mapped(INDICES.len(), wgpu::BufferUsage::INDEX) + .fill_from_slice(INDICES); + let num_indices = INDICES.len() as u32; + + Self { + surface, + device, + sc_desc, + swap_chain, + render_pipeline, + vertex_buffer, + index_buffer, + num_indices, + diffuse_texture, + diffuse_texture_view, + diffuse_sampler, + diffuse_bind_group, + hidpi_factor, + size, + } + } + + fn update_hidpi_and_resize(&mut self, new_hidpi_factor: f64) { + self.hidpi_factor = new_hidpi_factor; + self.resize(self.size); + } + + fn resize(&mut self, new_size: winit::dpi::LogicalSize) { + let physical_size = new_size.to_physical(self.hidpi_factor); + self.size = new_size; + self.sc_desc.width = physical_size.width.round() as u32; + self.sc_desc.height = physical_size.height.round() as u32; + self.swap_chain = self.device.create_swap_chain(&self.surface, &self.sc_desc); + } + + fn input(&mut self, event: &WindowEvent) -> bool { + false + } + + fn update(&mut self) { + + } + + fn render(&mut self) { + let frame = self.swap_chain.get_next_texture(); + + let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + todo: 0, + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.1, + g: 0.2, + b: 0.3, + a: 1.0, + }, + } + ], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]); + render_pass.set_vertex_buffers(0, &[(&self.vertex_buffer, 0)]); + render_pass.set_index_buffer(&self.index_buffer, 0); + render_pass.draw_indexed(0..self.num_indices, 0, 0..1); + } + + self.device.get_queue().submit(&[ + encoder.finish() + ]); + } +} + +fn main() { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .build(&event_loop) + .unwrap(); + + let mut state = State::new(&window); + + event_loop.run(move |event, _, control_flow| { + match event { + Event::WindowEvent { + ref event, + window_id, + } if window_id == window.id() => if state.input(event) { + *control_flow = ControlFlow::Wait; + } else { + match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + input, + .. + } => { + match input { + KeyboardInput { + state: ElementState::Pressed, + virtual_keycode: Some(VirtualKeyCode::Escape), + .. + } => *control_flow = ControlFlow::Exit, + _ => *control_flow = ControlFlow::Wait, + } + } + WindowEvent::Resized(logical_size) => { + state.resize(*logical_size); + *control_flow = ControlFlow::Wait; + } + WindowEvent::HiDpiFactorChanged(new_hidpi_factor) => { + state.update_hidpi_and_resize(*new_hidpi_factor); + *control_flow = ControlFlow::Wait; + } + _ => *control_flow = ControlFlow::Wait, + } + } + Event::EventsCleared => { + state.update(); + state.render(); + *control_flow = ControlFlow::Wait; + } + _ => *control_flow = ControlFlow::Wait, + } + }); +} \ No newline at end of file diff --git a/code/src/beginner/tutorial5-textures/shader.frag b/code/src/beginner/tutorial5-textures/shader.frag new file mode 100644 index 00000000..9b033114 --- /dev/null +++ b/code/src/beginner/tutorial5-textures/shader.frag @@ -0,0 +1,11 @@ +#version 450 + +layout(location=0) in vec2 v_tex_coords; +layout(location=0) out vec4 f_color; + +layout(set = 0, binding = 0) uniform texture2D t_diffuse; +layout(set = 0, binding = 1) uniform sampler s_diffuse; + +void main() { + f_color = texture(sampler2D(t_diffuse, s_diffuse), v_tex_coords); +} \ No newline at end of file diff --git a/code/src/beginner/tutorial5-textures/shader.vert b/code/src/beginner/tutorial5-textures/shader.vert new file mode 100644 index 00000000..54ce45c7 --- /dev/null +++ b/code/src/beginner/tutorial5-textures/shader.vert @@ -0,0 +1,11 @@ +#version 450 + +layout(location=0) in vec3 a_position; +layout(location=1) in vec2 a_tex_coords; + +layout(location=0) out vec2 v_tex_coords; + +void main() { + v_tex_coords = a_tex_coords; + gl_Position = vec4(a_position, 1.0); +} \ No newline at end of file diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 63eb661f..16bd12e2 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -17,6 +17,7 @@ module.exports = { '/beginner/tutorial2-swapchain', '/beginner/tutorial3-pipeline/', '/beginner/tutorial4-buffer/', + '/beginner/tutorial5-textures/', ], }, { diff --git a/docs/beginner/tutorial5-textures/README.md b/docs/beginner/tutorial5-textures/README.md new file mode 100644 index 00000000..c8f9c0a8 --- /dev/null +++ b/docs/beginner/tutorial5-textures/README.md @@ -0,0 +1,364 @@ +# Textures and bind groups + +Up to this point we have been drawing super simple shapes. While we can make a game with just triangles, but trying to draw highly detailed objects would massively limit what devices could even run our game. We can get around this problem with textures. Textures are images overlayed over a triangle mesh to make the mesh seem more detailed. There are multiple types of textures such as normal maps, bump maps, specular maps, and diffuse maps. We're going to talk about diffuse maps, or in laymens terms, the color texture. + +## Loading an image from a file + +If we want to map an image to our mesh, we first need an image. Let's use this happy little tree. + +![a happy tree](./happy-tree.png) + +We'll use the [image crate](https://crates.io/crates/image) to load our tree. In `State`'s `new()` method add the following just after creating the `swap_chain`: + +```rust +let diffuse_bytes = include_bytes!("happy-tree.png"); +let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); +let diffuse_rgba = diffuse_image.as_rgba8().unwrap(); + +use image::GenericImageView; +let dimensions = diffuse_image.dimensions(); +``` + +Here we just grab the bytes from our image file, and load them into an image, which we then convert into a `Vec` of rgba bytes. We also save the image's dimensions for when we create the actual `Texture`. Speaking of creating the actual `Texture`. + +```rust +let size = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth: 1, +}; +let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor { + // All textures are stored as 3d, we represent our 2d texture + // by setting depth to 1. + size: wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth: 1, + }, + // You can store multiple textures of the same size in one + // Texture object + array_layer_count: 1, + mip_level_count: 1, // We'll talk about this a little later + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + // SAMPLED tells wgpu that we want to use this texture in shaders + // COPY_DST means that we want to copy data to this texture + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST, +}); +``` + +## Getting data into a Texture + +The `Texture` struct has no methods to interact with the data directly. We actually need to load the data into a `Buffer` and copy it into the `Texture`. First we need to create a buffer big enough to hold our texture data. Luckily we have `diffuse_rgba`! + +```rust +let diffuse_buffer = device + .create_buffer_mapped(diffuse_rgba.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&diffuse_rgba); +``` + +We specified our `diffuse_buffer` to be `COPY_SRC` so that we can copy it to our `diffuse_texture`. We preform the copy using a `CommandEncoder`. We'll need to change `device`'s mutablility so we can submit the resulting `CommandBuffer`. + +```rust +let mut device = // ... + +// ... + +let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + todo: 0, +}); + +encoder.copy_buffer_to_texture( + wgpu::BufferCopyView { + buffer: &diffuse_buffer, + offset: 0, + row_pitch: 4 * dimensions.0, // the width of the texture in bytes + image_height: dimensions.1, + }, + wgpu::TextureCopyView { + texture: &diffuse_texture, + mip_level: 0, + array_layer: 0, + origin: wgpu::Origin3d::ZERO, + }, + size, +); + +device.get_queue().submit(&[encoder.finish()]); +``` + +## TextureViews and Samplers + +Now that our texture has data in it, we need a way to use it. This is where a `TextureView` and a `Sampler`. A `TextureView` offers us a *view* into our texture. A `Sampler` controls how the `Texture` is *sampled*. Sampling works similar to the eyedropper tool in Gimp/Photoshop. Our program supplies a coordinate on the texture (known as a texture coordinate), and the sampler then returns a color back based on it's internal parameters. + +Let's define our `diffuse_texture_view` and `diffuse_sampler` now. + +```rust +// We don't need to configure the texture view much, so let's +// let wgpu define it. +let diffuse_texture_view = diffuse_texture.create_default_view(); + +let diffuse_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::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: -100.0, + lod_max_clamp: 100.0, + compare_function: wgpu::CompareFunction::Always, +}); +``` + +The `address_mode_*` parameter's determine what to do if the sampler get's a texture coordinate that's outside of the texture. There's a few that we can use. +* `ClampToEdge`: Any texture coordinates outside the texture will return the color of the nearest pixel on the edges of the texture. +* `Repeat`: The texture will repeat as texture coordinates exceed the textures dimensions. +* `MirrorRepeat`: Similar to `Repeat`, but the image will flip when going over boundaries. + +The `mag_filter` and `min_filter` options describe what to do when a fragment covers multiple pixels, or there are multiple fragments for one pixel respectively. This often comes into play when viewing a surface from up close, or far away. There are 2 options: +* `Linear`: This option will attempt to blend the in-between fragments so that they seem to flow together. +* `Nearest`: In-between fragments will use the color of the nearest pixel. This creates an image that's crisper from far away, but pixelated when view from close up. This can be desirable however if your textures are designed to be pixelated such is in pixel art games, or voxel games like Minecraft. + +Mipmaps are a complex topic, and will require [their own section](/todo). Suffice to say `mipmap_filter` functions similar to `(mag/min)_filter` as it tells the sampler how to blend between mipmaps. + +`lod_(min/max)_clamp` are also related to mipmapping, so will skip over them. + +The `compare_function` is often use in filtering. This is used in techniques such as [shadow mapping](/todo). We don't really care here, but the options are `Never`, `Less`, `Equal`, `LessEqual`, `Greater`, `NotEqual`, `GreaterEqual`, and `Always`. + +All these different resources are nice and all, but they doesn't do us much good if we can't plug them in anywhere. This is where `BindGroup`s and `PipelineLayout`s come in. + +## The BindGroup + +A `BindGroup` describes a set of resources and how they can be accessed by a shader. We create a `BindGroup` using a `BindGroupLayout`. Let's make one of those first. + +```rust +let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[ + wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::SampledTexture { + multisampled: false, + dimension: wgpu::TextureViewDimension::D2, + }, + }, + wgpu::BindGroupLayoutBinding { + binding: 1, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler, + }, + ], +}); +``` + +Our `texture_bind_group_layout` has two bindings: one for a sampled texture at binding 0, and one for a sampler at binding 1. Both of these bindings are visible only to the fragment shader as specified by `FRAGMENT`. The possible values are any bit combination of `NONE`, `VERTEX`, `FRAGMENT`, or `COMPUTE`. Most of the time we'll only use `FRAGMENT` for textures and samplers, but it's good to know what's available. + +With `texture_bind_group_layout`, we can now create our `BindGroup`. + +```rust +let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &texture_bind_group_layout, + bindings: &[ + wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), + }, + wgpu::Binding { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + } + ], +}); +``` + +Looking at this you might get a bit of déjà vu. That's because a `BindGroup` is a more specific declaration of the `BindGroupLayout`. The reason why these are separate is to allow us to swap out `BindGroup`s on the fly, so long as they all share the same `BindGroupLayout`. For each texture and sampler we create, we need to create a `BindGroup`. + +Now that we have our `diffuse_bind_group`, let's add our texture information to the `State` struct. + +```rust +struct State { + // ... + + diffuse_texture: wgpu::Texture, + diffuse_texture_view: wgpu::TextureView, + diffuse_sampler: wgpu::Sampler, + diffuse_bind_group: wgpu::BindGroup, + + // ... +} + +// ... +impl State { + fn new() -> Self { + // ... + Self { + surface, + device, + sc_desc, + swap_chain, + render_pipeline, + vertex_buffer, + index_buffer, + num_indices, + diffuse_texture, + diffuse_texture_view, + diffuse_sampler, + diffuse_bind_group, + hidpi_factor, + size, + } + } +} + +``` + +We actually use the bind group in the `render()` function. + +```rust +// render() +render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]); +``` + +## PipelineLayout + +Remember the `PipelineLayout` we created back in [the pipeline section](/beginner/tutorial3-pipeline#how-do-we-use-the-shaders)? This is finally the time when we get to actually use it. The `PipelineLayout` contains a list of `BindGroupLayout`s that the pipeline can use. Modify `render_pipeline_layout` to use our `texture_bind_group_layout`. + +```rust +let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&texture_bind_group_layout], +}); +``` + +## A change to the VERTICES +There's a few things we need to change about our `Vertex` definition. Up to now we've been using a `color` attribute to dictate the color of our mesh. Now that we're using a texture we want to replace our `color` with `tex_coords`. + +```rust +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct Vertex { + position: [f32; 3], + tex_coords: [f32; 2], +} +``` + +We need to reflect these changes in the `VertexBufferDescriptor`. + +```rust +impl Vertex { + fn desc<'a>() -> wgpu::VertexBufferDescriptor<'a> { + use std::mem; + wgpu::VertexBufferDescriptor { + stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::InputStepMode::Vertex, + attributes: &[ + wgpu::VertexAttributeDescriptor { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float3, + }, + wgpu::VertexAttributeDescriptor { + offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + // We only need to change this to reflect that tex_coords + // is only 2 floats and not 3. It's in the same position + // as color was, so nothing else needs to change + format: wgpu::VertexFormat::Float2, + }, + ] + } + } +} +``` + +Lastly we need to change `VERTICES` itself. + +```rust +const VERTICES: &[Vertex] = &[ + Vertex { position: [-0.0868241, -0.49240386, 0.0], tex_coords: [0.4131759, 0.99240386], }, // A + Vertex { position: [-0.49513406, -0.06958647, 0.0], tex_coords: [0.0048659444, 0.56958646], }, // B + Vertex { position: [-0.21918549, 0.44939706, 0.0], tex_coords: [0.28081453, 0.050602943], }, // C + Vertex { position: [0.35966998, 0.3473291, 0.0], tex_coords: [0.85967, 0.15267089], }, // D + Vertex { position: [0.44147372, -0.2347359, 0.0], tex_coords: [0.9414737, 0.7347359], }, // E +]; +``` + +## Shader time + +Our shaders will need to change inorder to support textures as well. We'll also need to remove any reference to the `color` attribute we used to have. Let's start with the vertex shader. + +```glsl +// shader.vert +#version 450 + +layout(location=0) in vec3 a_position; +// Changed +layout(location=1) in vec2 a_tex_coords; + +// Changed +layout(location=0) out vec2 v_tex_coords; + +void main() { + // Changed + v_tex_coords = a_tex_coords; + gl_Position = vec4(a_position, 1.0); +} +``` + +We need to change the fragment shader to take in `v_tex_coords`. We also need to add a reference to our texture and sampler. + +```glsl +// shader.frag +#version 450 + +// Changed +layout(location=0) in vec2 v_tex_coords; +layout(location=0) out vec4 f_color; + +// New +layout(set = 0, binding = 0) uniform texture2D t_diffuse; +layout(set = 0, binding = 1) uniform sampler s_diffuse; + +void main() { + // Changed + f_color = texture(sampler2D(t_diffuse, s_diffuse), v_tex_coords); +} +``` + +You'll notice that `t_diffuse` and `s_diffuse` are defined with the `uniform` keyword, they don't have `in` nor `out`, and the layout definition uses `set` and `binding` instead of `location`. This is because `t_diffuse` and `s_diffuse` are what we call uniforms. We won't go too deep into what a uniform is, until we talk about uniform buffers in the [cameras section](/todo). What we need to know, for now, is that `set = 0` corresponds to the 1st parameter in `set_bind_group`, `binding = 0` relates the the `binding` specified when we create the `BindGroupLayout` and `BindGroup`. + +## The results + +If we run our program now we should get the following result. + +![an upside down tree on a hexagon](./upside-down.png) + +That's weird, our tree is upside down! This is because wgpu's coordinate system has positive y values going down while texture coords have y as up. We can get our triangle right-side up by inverting the y coord of each texture coord. + +```rust +const VERTICES: &[Vertex] = &[ + Vertex { position: [-0.0868241, -0.49240386, 0.0], tex_coords: [0.4131759, 1.0 - 0.99240386], }, // A + Vertex { position: [-0.49513406, -0.06958647, 0.0], tex_coords: [0.0048659444, 1.0 - 0.56958646], }, // B + Vertex { position: [-0.21918549, 0.44939706, 0.0], tex_coords: [0.28081453, 1.0 - 0.050602943], }, // C + Vertex { position: [0.35966998, 0.3473291, 0.0], tex_coords: [0.85967, 1.0 - 0.15267089], }, // D + Vertex { position: [0.44147372, -0.2347359, 0.0], tex_coords: [0.9414737, 1.0 - 0.7347359], }, // E +]; +``` + +With that in place we now have our tree subscribed right-side up on our hexagon. + +![our happy tree as it should be](./rightside-up.png) + + + diff --git a/docs/beginner/tutorial5-textures/happy-tree.png b/docs/beginner/tutorial5-textures/happy-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..ead2f03b84e428c175f41e08d5d6af25e7eb47d0 GIT binary patch literal 2278 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μ?wo-kVNSJS$38M1-GsnRUmV#SIufGq$G^1kXo2md*JuT-x3N zpj2e+$$uQPuMQ zJXF8@M7t|df^Y}X2r`!tK0tg${`P03*56dT_Fgw!RXx1yzz zh62xkrmubj)Yc2$D5IP4AY;tCPlq5tt9xGn_e%&H^GrawL1sO1T#5!(MuPav%pz?X z;kjnTPHwSrkweooVQFb7-aKCBB1jG*sk~AZw_^DOUt1xxC$YO>K~$vT3+`ntah%Kd z|6iGk4`)0%p<*d8g|pI-zCbkB`J4Y=^*v7-naU}sF3Bzf7**&kV1Hq+l3%_=WHC{RT~s?DS%F0l23oFNw^W`0CX-fSHHLV8rE z^E856{9ik|+~ZMg?)HBc2NffaeHh0Ke1C4Qs`(`ENT8)HADdZEQ!>;Rc1EhL?~F8f z0DP;$8R=;-@~3f(N%DuIb~V?GIbGO7P-D{L)vIMSS!a)8-@=YKuAjwVYg; zj*|+^N3|$>k2+@y^bt);t>|iyHmFs>4dVxWQRg_u|K2DraSs0dj4b=$#b~a_onLOj zAJyA?)@Fe+0-|^Rq@p+=fR_wPgH$K6sPo5nA|@G<<-PJ;`mib~c0ydRl7f{?>FAv- zRcT9ecBmShFt%rg1FGO=e&^=HVAOOpS+LnR-FlVG`g+Rf%=*fV&HlU-g zmUjKIYtyt4Zts?M6&)@6e4T3!U56p7n)@$E*X*ARqr<#lBWH z$FjXn;$sH)YR54Oj}jO)L* zH{NMj89|X5fR{(g-W(VpeX}`2!8Qf`_sV^jnGj6m5&KUYcpX3PU7MvDw6z?cp#9)T za^X2k9;mG-Azr*cIY0D@sw#jDq_PNfP*5QsK<1VhPs^EPX$_}>Ozap$#vD~00B{$< zzx6B<10d?dmjR5Y|MZ}!?zJUWecJUsS7j<&uTlvZTF zbQke!hPj!8>w@W4-o-0_3SyGKqmck)!Xgr7D1PY2W(y?tCs4uX&oNcOv#y%qq>bV( z+c!63g53f`jDI;ki)}CH!-*ev83L5FMQ3q0?PgaZxzboy*5I~^t}bRHHkse331~>e42Y5l6LD{TCits>p|v)5y(@ zNu0R*ea)cVuhM$3(d(MY{+dZE!(I!1qlwZU?UwoM@ja4b zX4biKluV>iSI>&R@vco1Orlh|?6X^nhP*)($DrDwd@C)@PiK9 zYSfJpX{BFZGy9Jp)qIzJdo6p@BWqYQAkd#%Y$Eydaq*)m`g@?R#cb=ndH=rFz%7yM z^jshpU*wqt^LWxy<-vG0GwMz}r4%LVTfb4ZF$=VqJcXFV(|AJ zw{X+FKbMky(EH`8SD#d@JPA1lps=|sjU7N{&%99*a>~xlz3El^0_iC9p6%!+0-+in zq?4l4v~Ggaw4%#lJ{^Rv_kX`(k6;K;yNd-@apLgNFTV_e{B$r`@So!DpWVr8Q^<(U z&TRm8JNh+DR8S~@TTC^2a;z$7+FdhZexYdWI@PFU5NgZ>r8KMh&Rb2>15r1wBYw_3 z&D&fKB z>#FGX>?%LXG2wF#$j%tg*wc|2XF#EE^_yjKP*d7}oLs#UaShn!lXvhY?4j4o80B7a zzXtr#V!m{F5$zbGMV5L!|Mh?2Mb(!KO=<(l44i$wA9?)D4p} zxuS)VV+PGyZWziKaJWbwl|M6ymdRF4nU$5_MX&8zY-`hMkqtafng2pIqG0FJ$kInB z6&BOUqL`yqT|Zx644l(?h+AwN_|PEo=M_&c=C#~Qx;0$Bnl{NeKD%UStdJ8%}h>qAHO?h}!vVavsCPrq4Wd<(K{tp@gW;Or- literal 0 HcmV?d00001 diff --git a/docs/beginner/tutorial5-textures/upside-down.png b/docs/beginner/tutorial5-textures/upside-down.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d32bf22b1a5531921bde7e371ac3cb09ee956d GIT binary patch literal 12466 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 KD^{_4@_zv0#4aBI literal 0 HcmV?d00001 diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 00000000..62789501 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,11 @@ +# Coming Soon! + +This section has not yet been completed. + + + + \ No newline at end of file