1use bytemuck::{Pod, Zeroable};
2use wgpu::{
3 DepthStencilState, Device, Extent3d, FragmentState, LoadOp, MultisampleState, Operations,
4 PipelineCompilationOptions, PipelineLayoutDescriptor, PrimitiveState, PrimitiveTopology, Queue,
5 RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPipeline,
6 RenderPipelineDescriptor, Texture, TextureDescriptor, TextureDimension, TextureFormat,
7 TextureUsages, TextureView, TextureViewDescriptor, VertexState,
8};
9
10use crate::{Primitive3D, Scene3D};
11
12#[repr(C)]
13#[derive(Copy, Clone, Debug, Pod, Zeroable)]
14pub struct Vertex {
15 position: [f32; 3],
16 color: [f32; 4],
17}
18
19impl Vertex {
20 pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
21 wgpu::VertexBufferLayout {
22 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
23 step_mode: wgpu::VertexStepMode::Vertex,
24 attributes: &[
25 wgpu::VertexAttribute {
26 offset: 0,
27 shader_location: 0,
28 format: wgpu::VertexFormat::Float32x3,
29 },
30 wgpu::VertexAttribute {
31 offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
32 shader_location: 1,
33 format: wgpu::VertexFormat::Float32x4,
34 },
35 ],
36 }
37 }
38}
39
40pub struct Scene3DRenderer {
41 pipeline: RenderPipeline,
42 uniform_layout: wgpu::BindGroupLayout,
43 depth_texture: Texture,
44 depth_view: TextureView,
45 width: u32,
46 height: u32,
47}
48
49#[repr(C)]
50#[derive(Copy, Clone, Debug, Pod, Zeroable)]
51struct SceneUniforms {
52 aspect: f32,
53 _pad: [f32; 3],
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct Scene3DViewport {
58 pub x: f32,
59 pub y: f32,
60 pub width: f32,
61 pub height: f32,
62}
63
64impl Scene3DRenderer {
65 pub fn new(device: &Device, width: u32, height: u32, target_format: TextureFormat) -> Self {
66 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
67 label: Some("fission-3d shader"),
68 source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
69 });
70
71 let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
72 label: Some("fission-3d uniforms layout"),
73 entries: &[wgpu::BindGroupLayoutEntry {
74 binding: 0,
75 visibility: wgpu::ShaderStages::VERTEX,
76 ty: wgpu::BindingType::Buffer {
77 ty: wgpu::BufferBindingType::Uniform,
78 has_dynamic_offset: false,
79 min_binding_size: None,
80 },
81 count: None,
82 }],
83 });
84
85 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
86 label: Some("fission-3d layout"),
87 bind_group_layouts: &[&uniform_layout],
88 push_constant_ranges: &[],
89 });
90
91 let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
92 label: Some("fission-3d pipeline"),
93 layout: Some(&pipeline_layout),
94 vertex: VertexState {
95 module: &shader,
96 entry_point: Some("vs_main"),
97 buffers: &[Vertex::desc()],
98 compilation_options: PipelineCompilationOptions::default(),
99 },
100 fragment: Some(FragmentState {
101 module: &shader,
102 entry_point: Some("fs_main"),
103 targets: &[Some(wgpu::ColorTargetState {
104 format: target_format,
105 blend: Some(wgpu::BlendState::REPLACE),
106 write_mask: wgpu::ColorWrites::ALL,
107 })],
108 compilation_options: PipelineCompilationOptions::default(),
109 }),
110 primitive: PrimitiveState {
111 topology: PrimitiveTopology::TriangleList,
112 strip_index_format: None,
113 front_face: wgpu::FrontFace::Ccw,
114 cull_mode: None,
115 unclipped_depth: false,
116 polygon_mode: wgpu::PolygonMode::Fill,
117 conservative: false,
118 },
119 depth_stencil: Some(DepthStencilState {
120 format: TextureFormat::Depth32Float,
121 depth_write_enabled: true,
122 depth_compare: wgpu::CompareFunction::Less,
123 stencil: wgpu::StencilState::default(),
124 bias: wgpu::DepthBiasState::default(),
125 }),
126 multisample: MultisampleState {
127 count: 1,
128 mask: !0,
129 alpha_to_coverage_enabled: false,
130 },
131 multiview: None,
132 cache: None,
133 });
134
135 let depth_texture = device.create_texture(&TextureDescriptor {
136 label: Some("fission-3d depth"),
137 size: Extent3d {
138 width: width.max(1),
139 height: height.max(1),
140 depth_or_array_layers: 1,
141 },
142 mip_level_count: 1,
143 sample_count: 1,
144 dimension: TextureDimension::D2,
145 format: TextureFormat::Depth32Float,
146 usage: TextureUsages::RENDER_ATTACHMENT,
147 view_formats: &[],
148 });
149
150 let depth_view = depth_texture.create_view(&TextureViewDescriptor::default());
151
152 Self {
153 pipeline,
154 depth_texture,
155 depth_view,
156 uniform_layout,
157 width,
158 height,
159 }
160 }
161
162 pub fn resize(&mut self, device: &Device, width: u32, height: u32) {
163 if self.width == width && self.height == height {
164 return;
165 }
166 self.width = width;
167 self.height = height;
168
169 self.depth_texture = device.create_texture(&TextureDescriptor {
170 label: Some("fission-3d depth"),
171 size: Extent3d {
172 width: width.max(1),
173 height: height.max(1),
174 depth_or_array_layers: 1,
175 },
176 mip_level_count: 1,
177 sample_count: 1,
178 dimension: TextureDimension::D2,
179 format: TextureFormat::Depth32Float,
180 usage: TextureUsages::RENDER_ATTACHMENT,
181 view_formats: &[],
182 });
183 self.depth_view = self
184 .depth_texture
185 .create_view(&TextureViewDescriptor::default());
186 }
187
188 pub fn render(&mut self, device: &Device, queue: &Queue, view: &TextureView, scene: &Scene3D) {
189 self.render_in_rect(
190 device,
191 queue,
192 view,
193 scene,
194 Scene3DViewport {
195 x: 0.0,
196 y: 0.0,
197 width: self.width as f32,
198 height: self.height as f32,
199 },
200 );
201 }
202
203 pub fn render_in_rect(
204 &mut self,
205 device: &Device,
206 queue: &Queue,
207 view: &TextureView,
208 scene: &Scene3D,
209 viewport: Scene3DViewport,
210 ) {
211 let Some((viewport, scissor)) = clamp_scene3d_viewport(viewport, self.width, self.height)
212 else {
213 return;
214 };
215
216 use wgpu::util::DeviceExt;
217
218 let uniforms = SceneUniforms {
219 aspect: (viewport.width / viewport.height).max(0.01),
220 _pad: [0.0; 3],
221 };
222 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
223 label: Some("fission-3d uniforms"),
224 contents: bytemuck::bytes_of(&uniforms),
225 usage: wgpu::BufferUsages::UNIFORM,
226 });
227 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
228 label: Some("fission-3d uniforms bind group"),
229 layout: &self.uniform_layout,
230 entries: &[wgpu::BindGroupEntry {
231 binding: 0,
232 resource: uniform_buffer.as_entire_binding(),
233 }],
234 });
235
236 let mut vertices: Vec<Vertex> = Vec::new();
238 let mut indices: Vec<u32> = Vec::new();
239
240 for prim in &scene.primitives {
243 match prim {
244 Primitive3D::Cube {
245 center,
246 size,
247 color,
248 } => {
249 let hs = size / 2.0;
250 let (x, y, z) = (center.x, center.y, center.z);
251 let p = [
252 [x - hs, y - hs, z - hs],
253 [x + hs, y - hs, z - hs],
254 [x + hs, y + hs, z - hs],
255 [x - hs, y + hs, z - hs],
256 [x - hs, y - hs, z + hs],
257 [x + hs, y - hs, z + hs],
258 [x + hs, y + hs, z + hs],
259 [x - hs, y + hs, z + hs],
260 ];
261 push_cube(&mut vertices, &mut indices, p, color);
262 }
263 Primitive3D::Sphere {
264 center,
265 radius,
266 color,
267 } => {
268 let base_idx = vertices.len() as u32;
269 let c = [
270 color.r as f32 / 255.0,
271 color.g as f32 / 255.0,
272 color.b as f32 / 255.0,
273 color.a as f32 / 255.0,
274 ];
275 let segments = 16;
276 let rings = 16;
277
278 for i in 0..=rings {
279 let v = i as f32 / rings as f32;
280 let phi = v * std::f32::consts::PI;
281
282 for j in 0..=segments {
283 let u = j as f32 / segments as f32;
284 let theta = u * std::f32::consts::PI * 2.0;
285
286 let x = center.x + radius * phi.sin() * theta.cos();
287 let y = center.y + radius * phi.cos();
288 let z = center.z + radius * phi.sin() * theta.sin();
289
290 vertices.push(Vertex {
291 position: [x, y, z],
292 color: c,
293 });
294 }
295 }
296
297 for i in 0..rings {
298 for j in 0..segments {
299 let first = base_idx + (i * (segments + 1)) as u32 + j as u32;
300 let second = first + segments as u32 + 1;
301
302 indices.push(first);
303 indices.push(second);
304 indices.push(first + 1);
305
306 indices.push(second);
307 indices.push(second + 1);
308 indices.push(first + 1);
309 }
310 }
311 }
312 Primitive3D::Mesh {
313 vertices: v_in,
314 indices: i_in,
315 color,
316 } => {
317 let base_idx = vertices.len() as u32;
318 let c = [
319 color.r as f32 / 255.0,
320 color.g as f32 / 255.0,
321 color.b as f32 / 255.0,
322 color.a as f32 / 255.0,
323 ];
324 for v in v_in {
325 vertices.push(Vertex {
326 position: [v.x, v.y, v.z],
327 color: c,
328 });
329 }
330 for idx in i_in {
331 indices.push(base_idx + *idx);
332 }
333 }
334 }
335 }
336
337 if vertices.is_empty() || indices.is_empty() {
338 return;
339 }
340
341 let v_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
342 label: Some("fission-3d vbuf"),
343 contents: bytemuck::cast_slice(&vertices),
344 usage: wgpu::BufferUsages::VERTEX,
345 });
346 let i_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
347 label: Some("fission-3d ibuf"),
348 contents: bytemuck::cast_slice(&indices),
349 usage: wgpu::BufferUsages::INDEX,
350 });
351
352 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
353 label: Some("fission-3d enc"),
354 });
355
356 {
357 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
358 label: Some("fission-3d pass"),
359 color_attachments: &[Some(RenderPassColorAttachment {
360 view,
361 depth_slice: None,
362 resolve_target: None,
363 ops: Operations {
364 load: LoadOp::Load,
365 store: wgpu::StoreOp::Store,
366 },
367 })],
368 depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
369 view: &self.depth_view,
370 depth_ops: Some(Operations {
371 load: LoadOp::Clear(1.0),
372 store: wgpu::StoreOp::Store,
373 }),
374 stencil_ops: None,
375 }),
376 timestamp_writes: None,
377 occlusion_query_set: None,
378 });
379
380 rpass.set_pipeline(&self.pipeline);
381 rpass.set_bind_group(0, &uniform_bind_group, &[]);
382 rpass.set_viewport(
383 viewport.x,
384 viewport.y,
385 viewport.width,
386 viewport.height,
387 0.0,
388 1.0,
389 );
390 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
391 rpass.set_vertex_buffer(0, v_buf.slice(..));
392 rpass.set_index_buffer(i_buf.slice(..), wgpu::IndexFormat::Uint32);
393 rpass.draw_indexed(0..indices.len() as u32, 0, 0..1);
394 }
395
396 queue.submit(std::iter::once(encoder.finish()));
397 }
398}
399
400fn push_cube(
401 vertices: &mut Vec<Vertex>,
402 indices: &mut Vec<u32>,
403 p: [[f32; 3]; 8],
404 color: &fission_core::op::Color,
405) {
406 push_face(vertices, indices, [p[0], p[1], p[2], p[3]], color, 0.86);
407 push_face(vertices, indices, [p[5], p[4], p[7], p[6]], color, 0.64);
408 push_face(vertices, indices, [p[4], p[0], p[3], p[7]], color, 0.72);
409 push_face(vertices, indices, [p[1], p[5], p[6], p[2]], color, 1.0);
410 push_face(vertices, indices, [p[3], p[2], p[6], p[7]], color, 1.18);
411 push_face(vertices, indices, [p[4], p[5], p[1], p[0]], color, 0.52);
412}
413
414fn push_face(
415 vertices: &mut Vec<Vertex>,
416 indices: &mut Vec<u32>,
417 positions: [[f32; 3]; 4],
418 color: &fission_core::op::Color,
419 shade: f32,
420) {
421 let base_idx = vertices.len() as u32;
422 let color = shaded_color(color, shade);
423 for position in positions {
424 vertices.push(Vertex { position, color });
425 }
426 indices.extend_from_slice(&[
427 base_idx,
428 base_idx + 1,
429 base_idx + 2,
430 base_idx,
431 base_idx + 2,
432 base_idx + 3,
433 ]);
434}
435
436fn shaded_color(color: &fission_core::op::Color, shade: f32) -> [f32; 4] {
437 [
438 ((color.r as f32 / 255.0) * shade).clamp(0.0, 1.0),
439 ((color.g as f32 / 255.0) * shade).clamp(0.0, 1.0),
440 ((color.b as f32 / 255.0) * shade).clamp(0.0, 1.0),
441 color.a as f32 / 255.0,
442 ]
443}
444
445fn clamp_scene3d_viewport(
446 viewport: Scene3DViewport,
447 target_width: u32,
448 target_height: u32,
449) -> Option<(Scene3DViewport, (u32, u32, u32, u32))> {
450 if target_width == 0
451 || target_height == 0
452 || !viewport.x.is_finite()
453 || !viewport.y.is_finite()
454 || !viewport.width.is_finite()
455 || !viewport.height.is_finite()
456 || viewport.width <= 0.0
457 || viewport.height <= 0.0
458 {
459 return None;
460 }
461
462 let target_width_f = target_width as f32;
463 let target_height_f = target_height as f32;
464 let x0 = viewport.x.max(0.0).min(target_width_f);
465 let y0 = viewport.y.max(0.0).min(target_height_f);
466 let x1 = (viewport.x + viewport.width).max(0.0).min(target_width_f);
467 let y1 = (viewport.y + viewport.height).max(0.0).min(target_height_f);
468
469 if x1 <= x0 || y1 <= y0 {
470 return None;
471 }
472
473 let scissor_x = x0.floor() as u32;
474 let scissor_y = y0.floor() as u32;
475 let scissor_right = (x1.ceil() as u32).min(target_width);
476 let scissor_bottom = (y1.ceil() as u32).min(target_height);
477 let scissor_width = scissor_right.saturating_sub(scissor_x);
478 let scissor_height = scissor_bottom.saturating_sub(scissor_y);
479
480 if scissor_width == 0 || scissor_height == 0 {
481 return None;
482 }
483
484 Some((
485 Scene3DViewport {
486 x: x0,
487 y: y0,
488 width: x1 - x0,
489 height: y1 - y0,
490 },
491 (scissor_x, scissor_y, scissor_width, scissor_height),
492 ))
493}
494
495#[cfg(test)]
496mod tests {
497 use super::{clamp_scene3d_viewport, push_cube, Scene3DViewport};
498 use fission_core::op::Color;
499
500 #[test]
501 fn viewport_clamps_to_render_target() {
502 let (viewport, scissor) = clamp_scene3d_viewport(
503 Scene3DViewport {
504 x: -10.0,
505 y: 20.25,
506 width: 130.0,
507 height: 90.0,
508 },
509 100,
510 80,
511 )
512 .expect("viewport should intersect target");
513
514 assert_eq!(
515 viewport,
516 Scene3DViewport {
517 x: 0.0,
518 y: 20.25,
519 width: 100.0,
520 height: 59.75,
521 }
522 );
523 assert_eq!(scissor, (0, 20, 100, 60));
524 }
525
526 #[test]
527 fn viewport_outside_target_is_skipped() {
528 assert!(clamp_scene3d_viewport(
529 Scene3DViewport {
530 x: 120.0,
531 y: 0.0,
532 width: 20.0,
533 height: 20.0,
534 },
535 100,
536 80,
537 )
538 .is_none());
539 }
540
541 #[test]
542 fn cube_mesh_duplicates_faces_with_shading() {
543 let mut vertices = Vec::new();
544 let mut indices = Vec::new();
545 let p = [
546 [-1.0, -1.0, -1.0],
547 [1.0, -1.0, -1.0],
548 [1.0, 1.0, -1.0],
549 [-1.0, 1.0, -1.0],
550 [-1.0, -1.0, 1.0],
551 [1.0, -1.0, 1.0],
552 [1.0, 1.0, 1.0],
553 [-1.0, 1.0, 1.0],
554 ];
555
556 push_cube(
557 &mut vertices,
558 &mut indices,
559 p,
560 &Color {
561 r: 20,
562 g: 184,
563 b: 166,
564 a: 255,
565 },
566 );
567
568 assert_eq!(vertices.len(), 24);
569 assert_eq!(indices.len(), 36);
570 let first_face_color = vertices[0].color;
571 assert!(vertices
572 .iter()
573 .any(|vertex| vertex.color != first_face_color));
574 }
575}