1use spine2d::{BlendMode, DrawList};
2use wgpu::util::DeviceExt;
3
4#[repr(C)]
5#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
6struct GpuVertex {
7 position: [f32; 2],
8 uv: [f32; 2],
9 color: [f32; 4],
10 dark_color: [f32; 4],
11}
12
13#[repr(C)]
14#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
15struct Globals {
16 clip_from_world: [[f32; 4]; 4],
17}
18
19pub struct SpineRenderer {
20 pipelines: Pipelines,
21 pipelines_pma: Pipelines,
22 globals_buffer: wgpu::Buffer,
23 globals_bind_group: wgpu::BindGroup,
24 texture_bind_group_layout: wgpu::BindGroupLayout,
25 vertex_buffer: wgpu::Buffer,
26 index_buffer: wgpu::Buffer,
27 vertex_capacity: usize,
28 index_capacity: usize,
29}
30
31struct Pipelines {
32 normal: wgpu::RenderPipeline,
33 additive: wgpu::RenderPipeline,
34 multiply: wgpu::RenderPipeline,
35 screen: wgpu::RenderPipeline,
36}
37
38impl Pipelines {
39 fn by_blend(&self, blend: BlendMode) -> &wgpu::RenderPipeline {
40 match blend {
41 BlendMode::Normal => &self.normal,
42 BlendMode::Additive => &self.additive,
43 BlendMode::Multiply => &self.multiply,
44 BlendMode::Screen => &self.screen,
45 }
46 }
47}
48
49impl SpineRenderer {
50 pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
51 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
52 label: Some("spine2d-wgpu shader"),
53 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
54 });
55
56 let globals_bind_group_layout =
57 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
58 label: Some("globals bind group layout"),
59 entries: &[wgpu::BindGroupLayoutEntry {
60 binding: 0,
61 visibility: wgpu::ShaderStages::VERTEX,
62 ty: wgpu::BindingType::Buffer {
63 ty: wgpu::BufferBindingType::Uniform,
64 has_dynamic_offset: false,
65 min_binding_size: None,
66 },
67 count: None,
68 }],
69 });
70
71 let texture_bind_group_layout =
72 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
73 label: Some("texture bind group layout"),
74 entries: &[
75 wgpu::BindGroupLayoutEntry {
76 binding: 0,
77 visibility: wgpu::ShaderStages::FRAGMENT,
78 ty: wgpu::BindingType::Texture {
79 multisampled: false,
80 view_dimension: wgpu::TextureViewDimension::D2,
81 sample_type: wgpu::TextureSampleType::Float { filterable: true },
82 },
83 count: None,
84 },
85 wgpu::BindGroupLayoutEntry {
86 binding: 1,
87 visibility: wgpu::ShaderStages::FRAGMENT,
88 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
89 count: None,
90 },
91 ],
92 });
93
94 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
95 label: Some("spine2d-wgpu pipeline layout"),
96 bind_group_layouts: &[&globals_bind_group_layout, &texture_bind_group_layout],
97 push_constant_ranges: &[],
98 });
99
100 let pipelines = create_pipelines(device, &pipeline_layout, &shader, color_format, false);
101 let pipelines_pma = create_pipelines(device, &pipeline_layout, &shader, color_format, true);
102
103 let globals = Globals {
104 clip_from_world: [[0.0; 4]; 4],
105 };
106 let globals_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
107 label: Some("globals buffer"),
108 contents: bytemuck::bytes_of(&globals),
109 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
110 });
111 let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
112 label: Some("globals bind group"),
113 layout: &globals_bind_group_layout,
114 entries: &[wgpu::BindGroupEntry {
115 binding: 0,
116 resource: globals_buffer.as_entire_binding(),
117 }],
118 });
119
120 let vertex_capacity = 1024;
121 let index_capacity = 2048;
122 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
123 label: Some("spine2d vertices"),
124 size: (vertex_capacity * std::mem::size_of::<GpuVertex>()) as u64,
125 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
126 mapped_at_creation: false,
127 });
128 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
129 label: Some("spine2d indices"),
130 size: (index_capacity * std::mem::size_of::<u32>()) as u64,
131 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
132 mapped_at_creation: false,
133 });
134
135 Self {
136 pipelines,
137 pipelines_pma,
138 globals_buffer,
139 globals_bind_group,
140 texture_bind_group_layout,
141 vertex_buffer,
142 index_buffer,
143 vertex_capacity,
144 index_capacity,
145 }
146 }
147
148 pub fn texture_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
149 &self.texture_bind_group_layout
150 }
151
152 pub fn update_globals_ortho_centered(&self, queue: &wgpu::Queue, width: f32, height: f32) {
153 let sx = 2.0 / width.max(1.0);
155 let sy = 2.0 / height.max(1.0);
156 let globals = Globals {
157 clip_from_world: [
158 [sx, 0.0, 0.0, 0.0],
159 [0.0, sy, 0.0, 0.0],
160 [0.0, 0.0, 1.0, 0.0],
161 [0.0, 0.0, 0.0, 1.0],
162 ],
163 };
164 queue.write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
165 }
166
167 pub fn update_globals_matrix(&self, queue: &wgpu::Queue, clip_from_world: [[f32; 4]; 4]) {
168 let globals = Globals { clip_from_world };
169 queue.write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
170 }
171
172 pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, draw_list: &DrawList) {
173 let vertices = draw_list
174 .vertices
175 .iter()
176 .map(|v| GpuVertex {
177 position: v.position,
178 uv: v.uv,
179 color: v.color,
180 dark_color: v.dark_color,
181 })
182 .collect::<Vec<_>>();
183
184 self.ensure_buffers(device, vertices.len(), draw_list.indices.len());
185 queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices));
186 queue.write_buffer(
187 &self.index_buffer,
188 0,
189 bytemuck::cast_slice(&draw_list.indices),
190 );
191 }
192
193 pub fn render<'a>(
194 &'a self,
195 pass: &mut wgpu::RenderPass<'a>,
196 draw_list: &'a DrawList,
197 textures: &'a dyn TextureProvider,
198 ) {
199 if draw_list.indices.is_empty() || draw_list.vertices.is_empty() {
200 return;
201 }
202
203 pass.set_bind_group(0, &self.globals_bind_group, &[]);
204 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
205 pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
206
207 for draw in &draw_list.draws {
208 let pipeline = if draw.premultiplied_alpha {
209 self.pipelines_pma.by_blend(draw.blend)
210 } else {
211 self.pipelines.by_blend(draw.blend)
212 };
213 pass.set_pipeline(pipeline);
214 if let Some(bind_group) = textures.bind_group_for(&draw.texture_path) {
215 pass.set_bind_group(1, bind_group, &[]);
216 }
217 let start = draw.first_index as u32;
218 let end = (draw.first_index + draw.index_count) as u32;
219 pass.draw_indexed(start..end, 0, 0..1);
220 }
221 }
222
223 fn ensure_buffers(&mut self, device: &wgpu::Device, vertices: usize, indices: usize) {
224 if vertices > self.vertex_capacity {
225 while self.vertex_capacity < vertices {
226 self.vertex_capacity *= 2;
227 }
228 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
229 label: Some("spine2d vertices"),
230 size: (self.vertex_capacity * std::mem::size_of::<GpuVertex>()) as u64,
231 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
232 mapped_at_creation: false,
233 });
234 }
235 if indices > self.index_capacity {
236 while self.index_capacity < indices {
237 self.index_capacity *= 2;
238 }
239 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
240 label: Some("spine2d indices"),
241 size: (self.index_capacity * std::mem::size_of::<u32>()) as u64,
242 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
243 mapped_at_creation: false,
244 });
245 }
246 }
247}
248
249fn create_pipelines(
250 device: &wgpu::Device,
251 layout: &wgpu::PipelineLayout,
252 shader: &wgpu::ShaderModule,
253 color_format: wgpu::TextureFormat,
254 premultiplied_alpha: bool,
255) -> Pipelines {
256 Pipelines {
257 normal: create_pipeline(
258 device,
259 layout,
260 shader,
261 color_format,
262 BlendMode::Normal,
263 premultiplied_alpha,
264 ),
265 additive: create_pipeline(
266 device,
267 layout,
268 shader,
269 color_format,
270 BlendMode::Additive,
271 premultiplied_alpha,
272 ),
273 multiply: create_pipeline(
274 device,
275 layout,
276 shader,
277 color_format,
278 BlendMode::Multiply,
279 premultiplied_alpha,
280 ),
281 screen: create_pipeline(
282 device,
283 layout,
284 shader,
285 color_format,
286 BlendMode::Screen,
287 premultiplied_alpha,
288 ),
289 }
290}
291
292fn create_pipeline(
293 device: &wgpu::Device,
294 layout: &wgpu::PipelineLayout,
295 shader: &wgpu::ShaderModule,
296 color_format: wgpu::TextureFormat,
297 blend: BlendMode,
298 premultiplied_alpha: bool,
299) -> wgpu::RenderPipeline {
300 let label = match (blend, premultiplied_alpha) {
301 (BlendMode::Normal, false) => "spine2d-wgpu pipeline normal",
302 (BlendMode::Additive, false) => "spine2d-wgpu pipeline additive",
303 (BlendMode::Multiply, false) => "spine2d-wgpu pipeline multiply",
304 (BlendMode::Screen, false) => "spine2d-wgpu pipeline screen",
305 (BlendMode::Normal, true) => "spine2d-wgpu pipeline normal pma",
306 (BlendMode::Additive, true) => "spine2d-wgpu pipeline additive pma",
307 (BlendMode::Multiply, true) => "spine2d-wgpu pipeline multiply pma",
308 (BlendMode::Screen, true) => "spine2d-wgpu pipeline screen pma",
309 };
310
311 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
312 label: Some(label),
313 layout: Some(layout),
314 vertex: wgpu::VertexState {
315 module: shader,
316 entry_point: Some("vs_main"),
317 compilation_options: Default::default(),
318 buffers: &[wgpu::VertexBufferLayout {
319 array_stride: std::mem::size_of::<GpuVertex>() as u64,
320 step_mode: wgpu::VertexStepMode::Vertex,
321 attributes: &wgpu::vertex_attr_array![
322 0 => Float32x2,
323 1 => Float32x2,
324 2 => Float32x4,
325 3 => Float32x4
326 ],
327 }],
328 },
329 fragment: Some(wgpu::FragmentState {
330 module: shader,
331 entry_point: Some("fs_main"),
332 compilation_options: Default::default(),
333 targets: &[Some(wgpu::ColorTargetState {
334 format: color_format,
335 blend: Some(blend_state(blend, premultiplied_alpha)),
336 write_mask: wgpu::ColorWrites::ALL,
337 })],
338 }),
339 primitive: wgpu::PrimitiveState {
340 topology: wgpu::PrimitiveTopology::TriangleList,
341 strip_index_format: None,
342 front_face: wgpu::FrontFace::Ccw,
343 cull_mode: None,
344 ..Default::default()
345 },
346 depth_stencil: None,
347 multisample: wgpu::MultisampleState::default(),
348 multiview: None,
349 cache: None,
350 })
351}
352
353fn blend_state(blend: BlendMode, premultiplied_alpha: bool) -> wgpu::BlendState {
354 use wgpu::{BlendComponent, BlendFactor, BlendOperation};
355
356 let (src_color, dst) = match blend {
360 BlendMode::Normal => (
361 src_color_for_alpha(premultiplied_alpha),
362 BlendFactor::OneMinusSrcAlpha,
363 ),
364 BlendMode::Additive => (src_color_for_alpha(premultiplied_alpha), BlendFactor::One),
365 BlendMode::Multiply => (BlendFactor::Dst, BlendFactor::OneMinusSrcAlpha),
366 BlendMode::Screen => (BlendFactor::One, BlendFactor::OneMinusSrc),
367 };
368
369 wgpu::BlendState {
370 color: BlendComponent {
371 src_factor: src_color,
372 dst_factor: dst,
373 operation: BlendOperation::Add,
374 },
375 alpha: BlendComponent {
376 src_factor: BlendFactor::One,
377 dst_factor: dst,
378 operation: BlendOperation::Add,
379 },
380 }
381}
382
383fn src_color_for_alpha(premultiplied_alpha: bool) -> wgpu::BlendFactor {
384 if premultiplied_alpha {
385 wgpu::BlendFactor::One
386 } else {
387 wgpu::BlendFactor::SrcAlpha
388 }
389}
390
391pub trait TextureProvider {
392 fn bind_group_for(&self, texture_path: &str) -> Option<&wgpu::BindGroup>;
393}
394
395pub struct HashMapTextureProvider {
396 pub bind_groups: std::collections::HashMap<String, wgpu::BindGroup>,
397}
398
399impl TextureProvider for HashMapTextureProvider {
400 fn bind_group_for(&self, texture_path: &str) -> Option<&wgpu::BindGroup> {
401 self.bind_groups.get(texture_path)
402 }
403}
404
405pub fn create_texture_bind_group(
406 device: &wgpu::Device,
407 layout: &wgpu::BindGroupLayout,
408 view: &wgpu::TextureView,
409 sampler: &wgpu::Sampler,
410) -> wgpu::BindGroup {
411 device.create_bind_group(&wgpu::BindGroupDescriptor {
412 label: Some("spine2d texture bind group"),
413 layout,
414 entries: &[
415 wgpu::BindGroupEntry {
416 binding: 0,
417 resource: wgpu::BindingResource::TextureView(view),
418 },
419 wgpu::BindGroupEntry {
420 binding: 1,
421 resource: wgpu::BindingResource::Sampler(sampler),
422 },
423 ],
424 })
425}
426
427pub fn create_sampler_for_atlas_page(
428 device: &wgpu::Device,
429 page: &spine2d::AtlasPage,
430) -> wgpu::Sampler {
431 let (min_filter, mipmap_filter) = to_wgpu_min_mipmap_filter(&page.min_filter);
432 let mag_filter = to_wgpu_mag_filter(&page.mag_filter);
433 let address_mode_u = to_wgpu_address_mode(page.wrap_u);
434 let address_mode_v = to_wgpu_address_mode(page.wrap_v);
435
436 device.create_sampler(&wgpu::SamplerDescriptor {
437 label: Some("spine2d atlas sampler"),
438 mag_filter,
439 min_filter,
440 mipmap_filter,
441 address_mode_u,
442 address_mode_v,
443 ..Default::default()
444 })
445}
446
447fn to_wgpu_address_mode(wrap: spine2d::AtlasWrap) -> wgpu::AddressMode {
448 match wrap {
449 spine2d::AtlasWrap::ClampToEdge => wgpu::AddressMode::ClampToEdge,
450 spine2d::AtlasWrap::Repeat => wgpu::AddressMode::Repeat,
451 }
452}
453
454fn to_wgpu_mag_filter(filter: &spine2d::AtlasFilter) -> wgpu::FilterMode {
455 match filter {
456 spine2d::AtlasFilter::Nearest
457 | spine2d::AtlasFilter::MipMapNearestNearest
458 | spine2d::AtlasFilter::MipMapLinearNearest => wgpu::FilterMode::Nearest,
459 spine2d::AtlasFilter::Linear
460 | spine2d::AtlasFilter::MipMap
461 | spine2d::AtlasFilter::MipMapNearestLinear
462 | spine2d::AtlasFilter::MipMapLinearLinear
463 | spine2d::AtlasFilter::Other(_) => wgpu::FilterMode::Linear,
464 }
465}
466
467fn to_wgpu_min_mipmap_filter(
468 filter: &spine2d::AtlasFilter,
469) -> (wgpu::FilterMode, wgpu::FilterMode) {
470 match filter {
471 spine2d::AtlasFilter::Nearest => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest),
472 spine2d::AtlasFilter::Linear => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
473 spine2d::AtlasFilter::MipMap | spine2d::AtlasFilter::MipMapLinearLinear => {
474 (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear)
475 }
476 spine2d::AtlasFilter::MipMapNearestNearest => {
477 (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest)
478 }
479 spine2d::AtlasFilter::MipMapNearestLinear => {
480 (wgpu::FilterMode::Nearest, wgpu::FilterMode::Linear)
481 }
482 spine2d::AtlasFilter::MipMapLinearNearest => {
483 (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest)
484 }
485 spine2d::AtlasFilter::Other(_) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest),
486 }
487}
488
489const SHADER: &str = r#"
490struct Globals {
491 clip_from_world: mat4x4<f32>,
492};
493
494@group(0) @binding(0)
495var<uniform> globals: Globals;
496
497struct VsIn {
498 @location(0) position: vec2<f32>,
499 @location(1) uv: vec2<f32>,
500 @location(2) light_color: vec4<f32>,
501 @location(3) dark_color: vec4<f32>,
502};
503
504struct VsOut {
505 @builtin(position) position: vec4<f32>,
506 @location(0) uv: vec2<f32>,
507 @location(1) light_color: vec4<f32>,
508 @location(2) dark_color: vec4<f32>,
509};
510
511@vertex
512fn vs_main(in: VsIn) -> VsOut {
513 var out: VsOut;
514 out.position = globals.clip_from_world * vec4<f32>(in.position, 0.0, 1.0);
515 out.uv = in.uv;
516 out.light_color = in.light_color;
517 out.dark_color = in.dark_color;
518 return out;
519}
520
521@group(1) @binding(0)
522var tex: texture_2d<f32>;
523
524@group(1) @binding(1)
525var samp: sampler;
526
527@fragment
528fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
529 let tex_color = textureSample(tex, samp, in.uv);
530 let alpha = tex_color.a * in.light_color.a;
531 let rgb = ((tex_color.a - 1.0) * in.dark_color.a + 1.0 - tex_color.rgb) * in.dark_color.rgb
532 + tex_color.rgb * in.light_color.rgb;
533 return vec4<f32>(rgb, alpha);
534}
535"#;