summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTengu712 <genreihoutengu@gmail.com>2026-10-07 00:57:01 +0900
committerTengu712 <genreihoutengu@gmail.com>2026-10-07 00:57:01 +0900
commit3bbe5ebcdb2a5aa6810dd05576c6ec031efdc1c7 (patch)
tree844e4ba5efc755dabf091a51178937a93e1d47ed
parent503486e2ce889858aac619985516374bddeb7a8f (diff)
render imageHEADmain
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock81
-rw-r--r--Cargo.toml1
-rw-r--r--src/graphics.rs53
-rw-r--r--src/graphics/command.rs53
-rw-r--r--src/graphics/font.rs10
-rw-r--r--src/graphics/image.rs51
-rw-r--r--src/main.rs16
-rw-r--r--src/shader.wgsl27
9 files changed, 262 insertions, 32 deletions
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
@@ -19,6 +19,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -306,6 +312,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -325,6 +340,7 @@ dependencies = [
"etagere",
"glam",
"lru",
+ "png",
"pollster",
"ttf-parser",
"wgpu",
@@ -418,12 +434,32 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -752,6 +788,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1236,6 +1292,19 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1510,6 +1579,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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<TextureWrapper>,
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<InstanceData>,
) {
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<InstanceData>,
+) {
+ 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<TextureWrapper> {
+ 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<f32>,
- scale: vec2<f32>,
- uv: vec4<f32>,
+ transform: mat2x2<f32>,
+ translation: vec2<f32>,
+ uv_offset: vec2<f32>,
+ uv_size: vec2<f32>,
+ tex_id: u32,
+ _pad: u32,
}
struct VertexOutput {
- @builtin(position) position: vec4<f32>,
- @location(0) uv: vec2<f32>,
+ @builtin(position) position: vec4<f32>,
+ @location(0) uv: vec2<f32>,
+ @location(1) @interpolate(flat) tex_id: u32,
}
@group(0) @binding(0) var<storage, read> instances: array<Instance>;
@group(1) @binding(0) var tex_sampler: sampler;
@group(1) @binding(1) var font_tex: texture_2d<f32>;
+@group(1) @binding(2) var image_texs: binding_array<texture_2d<f32>, 5>;
@vertex
fn vs_main(
@@ -23,13 +30,17 @@ fn vs_main(
let instance = instances[in_instance_index];
var out: VertexOutput;
- out.position = vec4<f32>(instance.position + (uv - 0.5) * instance.scale * 2.0, 0.0, 1.0);
- out.uv = instance.uv.xy + vec2<f32>(uv.x, 1.0 - uv.y) * instance.uv.zw;
+ out.position = vec4<f32>(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<f32> {
- return vec4<f32>(1.0, 1.0, 1.0, textureSample(font_tex, tex_sampler, in.uv).r);
+ if in.tex_id >= 5u {
+ return vec4<f32>(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);
}