From 3bbe5ebcdb2a5aa6810dd05576c6ec031efdc1c7 Mon Sep 17 00:00:00 2001 From: Tengu712 Date: Wed, 7 Oct 2026 00:57:01 +0900 Subject: render image --- .gitignore | 2 ++ Cargo.lock | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/graphics.rs | 53 ++++++++++++++++++++++++++------ src/graphics/command.rs | 53 +++++++++++++++++++++++++++++--- src/graphics/font.rs | 10 +++--- src/graphics/image.rs | 51 +++++++++++++++++++++++++++++++ src/main.rs | 16 +++++++--- src/shader.wgsl | 27 ++++++++++++----- 9 files changed, 262 insertions(+), 32 deletions(-) create mode 100644 src/graphics/image.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..0beb041 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +*.png diff --git a/Cargo.lock b/Cargo.lock index 85911d7..78cd16d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -305,6 +311,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.23" @@ -325,6 +340,7 @@ dependencies = [ "etagere", "glam", "lru", + "png", "pollster", "ttf-parser", "wgpu", @@ -417,12 +433,32 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + [[package]] name = "foldhash" version = "0.2.0" @@ -751,6 +787,26 @@ dependencies = [ "libc", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "naga" version = "30.0.1" @@ -1235,6 +1291,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "polling" version = "3.11.0" @@ -1509,6 +1578,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -2474,3 +2549,9 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" diff --git a/Cargo.toml b/Cargo.toml index b9f5b9e..6f9991d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ ab_glyph = "0.2.32" etagere = "0.3.0" glam = "0.33.10" lru = "0.18.5" +png = "0.18.1" pollster = "1.0.1" ttf-parser = "0.25.1" winit = "0.30.13" diff --git a/src/graphics.rs b/src/graphics.rs index bab9f05..92defc2 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -1,25 +1,31 @@ -use glam::{Vec2, Vec4}; -use std::{borrow::Cow, mem, slice, sync::Arc}; +use glam::{Mat2, Vec2}; +use std::{borrow::Cow, mem, num::NonZeroU32, slice, sync::Arc}; use wgpu::*; use winit::{dpi::PhysicalSize, event_loop::OwnedDisplayHandle, window::Window}; pub mod command; mod font; +pub mod image; mod texture; use command::RenderCommand; use font::CharAtlas; +use image::ImageId; +use texture::TextureWrapper; pub const INSTANCE_MAX_COUNT: u64 = 128; +const IMAGE_SLOT_COUNT: u32 = 5; #[repr(C)] struct InstanceData { + // 拡縮・回転行列 NDCを考慮していること + transform: Mat2, // 中央原点 左→右[-1, 1] 下→上[-1, 1] - position: Vec2, - // 拡大率 (width, height): [0, 1]^2 - scale: Vec2, - // (u, v, width, height): [0, 1]^4 - uv: Vec4, + translation: Vec2, + uv_offset: Vec2, + uv_size: Vec2, + tex_id: u32, + _pad: u32, } pub enum AcquireSurfaceTextureResult { @@ -40,6 +46,7 @@ pub struct Renderer { instances: Buffer, trans_bind_group: BindGroup, char_atlas: CharAtlas, + images: Vec, fixed_bind_group: BindGroup, } @@ -61,7 +68,15 @@ impl Renderer { .await .map_err(|e| e.to_string())?; let (device, queue) = adapter - .request_device(&DeviceDescriptor::default()) + .request_device(&DeviceDescriptor { + required_features: Features::TEXTURE_BINDING_ARRAY + | Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, + required_limits: Limits { + max_binding_array_elements_per_shader_stage: IMAGE_SLOT_COUNT, + ..Default::default() + }, + ..Default::default() + }) .await .map_err(|e| e.to_string())?; @@ -110,6 +125,16 @@ impl Renderer { }, count: None, }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: NonZeroU32::new(IMAGE_SLOT_COUNT), + }, ], }); let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { @@ -183,6 +208,8 @@ impl Renderer { }); // NOTE: 文字テクスチャアトラスは使い回すので、ここで1個だけ作成すれば良い。 let char_atlas = CharAtlas::new(scale_factor, &device); + // NOTE: 画像テクスチャ配列は使いまわすので、ここで1個だけ作成すればいい。 + let images = image::create_textures(&device, &queue); let fixed_bind_group = device.create_bind_group(&BindGroupDescriptor { label: None, layout: &fixed_bind_group_layout, @@ -195,6 +222,12 @@ impl Renderer { binding: 1, resource: BindingResource::TextureView(&char_atlas.texture.view), }, + BindGroupEntry { + binding: 2, + resource: BindingResource::TextureViewArray( + &[&images[ImageId::Image as usize].view; IMAGE_SLOT_COUNT as usize], + ), + }, ], }); @@ -209,6 +242,7 @@ impl Renderer { instances, trans_bind_group, char_atlas, + images, fixed_bind_group, }; @@ -282,10 +316,9 @@ impl Renderer { window.scale_factor() as f32, &self.queue, &mut self.char_atlas, + &self.images, &mut instances, ); - // TODO: 今はfont_texしか使わないので1ドローコールで済むが、 - // bind groupを変える必要があればドローコールを切る。 } instances.truncate(INSTANCE_MAX_COUNT as usize); let bytes = unsafe { diff --git a/src/graphics/command.rs b/src/graphics/command.rs index f0330cb..6c0b036 100644 --- a/src/graphics/command.rs +++ b/src/graphics/command.rs @@ -1,8 +1,10 @@ use super::{ InstanceData, font::{self, CharAtlas}, + image::ImageId, + texture::TextureWrapper, }; -use glam::Vec2; +use glam::{Mat2, Vec2}; use wgpu::Queue; pub enum HorizontalAlign { @@ -25,8 +27,18 @@ pub struct Text { pub v_align: VerticalAlign, } +pub struct Image { + pub id: ImageId, + pub origin: Vec2, + pub rotation: f32, + pub scale: Vec2, + pub uv_offset: Vec2, + pub uv_size: Vec2, +} + pub enum RenderCommand { Text(Text), + Image(Image), } impl RenderCommand { @@ -37,6 +49,7 @@ impl RenderCommand { /// * scale_factor - 画面拡大率 /// * queue - queue /// * char_atlas - 文字テクスチャアトラス + /// * images - 画像テクスチャ /// * buf - インスタンスバッファ pub fn build( &self, @@ -45,6 +58,7 @@ impl RenderCommand { scale_factor: f32, queue: &Queue, char_atlas: &mut CharAtlas, + images: &[TextureWrapper], buf: &mut Vec, ) { match self { @@ -57,6 +71,9 @@ impl RenderCommand { char_atlas, buf, ), + RenderCommand::Image(image) => { + build_image(image, screen_width, screen_height, images, buf) + } } } } @@ -108,12 +125,18 @@ fn build_text( let center_x = x + ci.x_offset + ci.width / 2.0; let center_y = y + ci.y_offset + ci.height / 2.0; buf.push(InstanceData { - position: Vec2::new( + transform: Mat2::from_diagonal(Vec2::new( + ci.width / screen_width * 2.0, + -ci.height / screen_height * 2.0, + )), + translation: Vec2::new( center_x / screen_width * 2.0 - 1.0, 1.0 - center_y / screen_height * 2.0, ), - scale: Vec2::new(ci.width / screen_width, ci.height / screen_height), - uv: ci.uv, + uv_offset: ci.uv_offset, + uv_size: ci.uv_size, + tex_id: u32::MAX, + _pad: 0, }); ox += font::h_advance(scale, ci.glyph_id); } @@ -128,9 +151,29 @@ fn build_text( let left = (left * scale_factor).round() / scale_factor; for instance in &mut buf[start..] { - instance.position.x += left / screen_width * 2.0; + instance.translation.x += left / screen_width * 2.0; } baseline += line_height; } } + +fn build_image( + image: &Image, + screen_width: f32, + screen_height: f32, + images: &[TextureWrapper], + buf: &mut Vec, +) { + let texture = &images[image.id as usize]; + let size = image.uv_size * Vec2::new(texture.width as f32, texture.height as f32) * image.scale; + let ndc = Vec2::new(2.0 / screen_width, -2.0 / screen_height); + buf.push(InstanceData { + transform: Mat2::from_diagonal(ndc) * Mat2::from_scale_angle(size, image.rotation), + translation: image.origin * ndc + Vec2::new(-1.0, 1.0), + uv_offset: image.uv_offset, + uv_size: image.uv_size, + tex_id: image.id as u32, + _pad: 0, + }); +} diff --git a/src/graphics/font.rs b/src/graphics/font.rs index c20e691..88163cc 100644 --- a/src/graphics/font.rs +++ b/src/graphics/font.rs @@ -1,7 +1,7 @@ use super::texture::TextureWrapper; use ab_glyph::*; use etagere::{AllocId, AtlasAllocator}; -use glam::Vec4; +use glam::Vec2; use lru::LruCache; use std::sync::LazyLock; use ttf_parser::{ @@ -103,7 +103,8 @@ pub struct CharInfo { pub height: f32, pub x_offset: f32, pub y_offset: f32, - pub uv: Vec4, + pub uv_offset: Vec2, + pub uv_size: Vec2, } pub struct CharAtlas { @@ -236,12 +237,11 @@ impl CharAtlas { height: height / self.scale_factor, x_offset: rasterized.x_offset / self.scale_factor, y_offset: rasterized.y_offset / self.scale_factor, - uv: Vec4::new( + uv_offset: Vec2::new( (allocated.rectangle.min.x as f32 + MARGIN as f32) / self.width, (allocated.rectangle.min.y as f32 + MARGIN as f32) / self.height, - width / self.width, - height / self.height, ), + uv_size: Vec2::new(width / self.width, height / self.height), }; self.registereds.push((c, scale.to_bits()), info); } diff --git a/src/graphics/image.rs b/src/graphics/image.rs new file mode 100644 index 0000000..26833c0 --- /dev/null +++ b/src/graphics/image.rs @@ -0,0 +1,51 @@ +// TODO: このファイルは好きな設計ではないので破壊する。 + +use super::texture::TextureWrapper; +use png::{BitDepth, ColorType, Decoder}; +use std::io::Cursor; +use wgpu::{Device, Queue, TextureFormat, TextureUsages}; + +#[derive(Clone, Copy)] +pub enum ImageId { + Image, +} + +static IMAGE_DATAS: [&[u8]; 1] = [include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/image.png" +))]; + +pub fn create_textures(device: &Device, queue: &Queue) -> Vec { + IMAGE_DATAS + .iter() + .map(|data| create_texture(device, queue, data)) + .collect() +} + +fn create_texture(device: &Device, queue: &Queue, data: &[u8]) -> TextureWrapper { + let mut reader = Decoder::new(Cursor::new(data)) + .read_info() + .expect("fatal: failed to load image"); + let mut buf = vec![ + 0x00; + reader + .output_buffer_size() + .expect("fatal: image is too large") + ]; + let info = reader + .next_frame(&mut buf) + .expect("fatal: failed to decode image"); + if info.color_type != ColorType::Rgba || info.bit_depth != BitDepth::Eight { + panic!("fatal: image is not 8-bit RGBA"); + } + + let texture = TextureWrapper::new( + device, + info.width, + info.height, + TextureFormat::Rgba8UnormSrgb, + TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + ); + texture.write(queue, &buf[..info.buffer_size()]); + texture +} diff --git a/src/main.rs b/src/main.rs index 7a8df1d..95457ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,15 +119,23 @@ impl ApplicationHandler for App { renderer.render( window, current_texture, - &[graphics::command::RenderCommand::Text( - graphics::command::Text { + &[ + graphics::command::RenderCommand::Image(graphics::command::Image { + id: graphics::image::ImageId::Image, + origin: glam::Vec2::new(900.0, 400.0), + rotation: 0.2, + scale: glam::Vec2::new(0.25, 0.25), + uv_offset: glam::Vec2::new(0.0, 0.0), + uv_size: glam::Vec2::new(1.0, 1.0), + }), + graphics::command::RenderCommand::Text(graphics::command::Text { text: "君は呪われている!".to_string(), origin: glam::Vec2::new(240.0, 120.0), scale: 48, v_align: graphics::command::VerticalAlign::Top, h_align: graphics::command::HorizontalAlign::Left, - }, - )], + }), + ], ); if !renderer.is_presentation_mode_fifo() { diff --git a/src/shader.wgsl b/src/shader.wgsl index e0b34f4..80df5b0 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,18 +1,25 @@ +enable wgpu_binding_array; + struct Instance { - position: vec2, - scale: vec2, - uv: vec4, + transform: mat2x2, + translation: vec2, + uv_offset: vec2, + uv_size: vec2, + tex_id: u32, + _pad: u32, } struct VertexOutput { - @builtin(position) position: vec4, - @location(0) uv: vec2, + @builtin(position) position: vec4, + @location(0) uv: vec2, + @location(1) @interpolate(flat) tex_id: u32, } @group(0) @binding(0) var instances: array; @group(1) @binding(0) var tex_sampler: sampler; @group(1) @binding(1) var font_tex: texture_2d; +@group(1) @binding(2) var image_texs: binding_array, 5>; @vertex fn vs_main( @@ -23,13 +30,17 @@ fn vs_main( let instance = instances[in_instance_index]; var out: VertexOutput; - out.position = vec4(instance.position + (uv - 0.5) * instance.scale * 2.0, 0.0, 1.0); - out.uv = instance.uv.xy + vec2(uv.x, 1.0 - uv.y) * instance.uv.zw; + out.position = vec4(instance.transform * (uv - 0.5) + instance.translation, 0.0, 1.0); + out.uv = instance.uv_offset + uv * instance.uv_size; + out.tex_id = instance.tex_id; return out; } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(1.0, 1.0, 1.0, textureSample(font_tex, tex_sampler, in.uv).r); + if in.tex_id >= 5u { + return vec4(1.0, 1.0, 1.0, textureSampleLevel(font_tex, tex_sampler, in.uv, 0.0).r); + } + return textureSampleLevel(image_texs[in.tex_id], tex_sampler, in.uv, 0.0); } -- cgit v1.3.1