Skip to main content

fission_3d/
render.rs

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        // Construct mesh for primitives
237        let mut vertices: Vec<Vertex> = Vec::new();
238        let mut indices: Vec<u32> = Vec::new();
239
240        // This is a naive tessellator just for demonstration parity.
241        // It maps standard Scene3D primitives into flat TriangleLists.
242        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}