summaryrefslogtreecommitdiff
path: root/src
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 /src
parent503486e2ce889858aac619985516374bddeb7a8f (diff)
render imageHEADmain
Diffstat (limited to 'src')
-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
6 files changed, 178 insertions, 32 deletions
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);
}