Skip to main content

blade_render/raster/
mod.rs

1use crate::{AssetHub, CameraParams, DummyResources, Object, Shaders, Vertex};
2use blade_graphics as gpu;
3use std::mem;
4
5#[derive(Clone, Copy, Debug)]
6pub struct RasterConfig {
7    pub clear_color: gpu::TextureColor,
8    pub light_dir: mint::Vector3<f32>,
9    pub light_color: mint::Vector3<f32>,
10    pub ambient_color: mint::Vector3<f32>,
11    pub roughness: f32,
12    pub metallic: f32,
13    /// When true, the sky fallback renders pure black instead of a blue gradient.
14    pub space_sky: bool,
15}
16
17impl Default for RasterConfig {
18    fn default() -> Self {
19        Self {
20            clear_color: gpu::TextureColor::OpaqueBlack,
21            light_dir: mint::Vector3 {
22                x: -0.3,
23                y: -1.0,
24                z: -0.2,
25            },
26            light_color: mint::Vector3 {
27                x: 3.0,
28                y: 3.0,
29                z: 3.0,
30            },
31            ambient_color: mint::Vector3 {
32                x: 0.05,
33                y: 0.05,
34                z: 0.05,
35            },
36            roughness: 0.4,
37            metallic: 0.0,
38            space_sky: false,
39        }
40    }
41}
42
43#[repr(C)]
44#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
45struct RasterFrameParams {
46    view_proj: [f32; 16],
47    inv_view_proj: [f32; 16],
48    camera_pos: [f32; 4],
49    light_dir: [f32; 4],
50    light_color: [f32; 4],
51    ambient_color: [f32; 4],
52    material: [f32; 4],
53}
54
55#[repr(C)]
56#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
57struct RasterDrawParams {
58    model: [f32; 16],
59    normal_quat: [f32; 4],
60    base_color_factor: [f32; 4],
61    material: [f32; 4],
62}
63
64#[derive(blade_macros::ShaderData)]
65struct RasterMainData {
66    frame_params: RasterFrameParams,
67    draw_params: RasterDrawParams,
68    vertices: gpu::BufferPiece,
69    samp: gpu::Sampler,
70    base_color_tex: gpu::TextureView,
71    normal_tex: gpu::TextureView,
72}
73
74#[derive(blade_macros::ShaderData)]
75struct RasterSkyData {
76    sky_params: RasterFrameParams,
77    samp: gpu::Sampler,
78    env_map: gpu::TextureView,
79}
80
81struct RasterPipelines {
82    main: gpu::RenderPipeline,
83    sky: gpu::RenderPipeline,
84}
85
86impl RasterPipelines {
87    fn create_main(
88        shader: &gpu::Shader,
89        info: gpu::SurfaceInfo,
90        gpu: &gpu::Context,
91    ) -> gpu::RenderPipeline {
92        shader.check_struct_size::<RasterFrameParams>();
93        shader.check_struct_size::<RasterDrawParams>();
94        let main_layout = <RasterMainData as gpu::ShaderData>::layout();
95        gpu.create_render_pipeline(gpu::RenderPipelineDesc {
96            name: "raster",
97            data_layouts: &[&main_layout],
98            vertex: shader.at("raster_vs"),
99            vertex_fetches: &[],
100            primitive: gpu::PrimitiveState {
101                topology: gpu::PrimitiveTopology::TriangleList,
102                ..Default::default()
103            },
104            depth_stencil: Some(gpu::DepthStencilState {
105                format: gpu::TextureFormat::Depth32Float,
106                depth_write_enabled: true,
107                depth_compare: gpu::CompareFunction::Less,
108                stencil: gpu::StencilState::default(),
109                bias: gpu::DepthBiasState::default(),
110            }),
111            fragment: Some(shader.at("raster_fs")),
112            color_targets: &[info.format.into()],
113            multisample_state: gpu::MultisampleState::default(),
114        })
115    }
116
117    fn create_sky(
118        shader: &gpu::Shader,
119        info: gpu::SurfaceInfo,
120        gpu: &gpu::Context,
121    ) -> gpu::RenderPipeline {
122        shader.check_struct_size::<RasterFrameParams>();
123        let sky_layout = <RasterSkyData as gpu::ShaderData>::layout();
124        gpu.create_render_pipeline(gpu::RenderPipelineDesc {
125            name: "raster-sky",
126            data_layouts: &[&sky_layout],
127            vertex: shader.at("raster_sky_vs"),
128            vertex_fetches: &[],
129            primitive: gpu::PrimitiveState {
130                topology: gpu::PrimitiveTopology::TriangleList,
131                ..Default::default()
132            },
133            depth_stencil: Some(gpu::DepthStencilState {
134                format: gpu::TextureFormat::Depth32Float,
135                depth_write_enabled: false,
136                depth_compare: gpu::CompareFunction::LessEqual,
137                stencil: gpu::StencilState::default(),
138                bias: gpu::DepthBiasState::default(),
139            }),
140            fragment: Some(shader.at("raster_sky_fs")),
141            color_targets: &[info.format.into()],
142            multisample_state: gpu::MultisampleState::default(),
143        })
144    }
145
146    fn init(
147        shaders: &Shaders,
148        config: &crate::render::RenderConfig,
149        gpu: &gpu::Context,
150        shader_man: &blade_asset::AssetManager<crate::shader::Baker>,
151    ) -> Result<Self, &'static str> {
152        let shader = shader_man[shaders.raster].raw.as_ref().unwrap();
153        Ok(Self {
154            main: Self::create_main(shader, config.surface_info, gpu),
155            sky: Self::create_sky(shader, config.surface_info, gpu),
156        })
157    }
158}
159
160pub struct Rasterizer {
161    shaders: Shaders,
162    pipelines: RasterPipelines,
163    sampler_linear: gpu::Sampler,
164    debug: Option<crate::render::DebugRender>,
165    dummy: DummyResources,
166    depth_texture: gpu::Texture,
167    depth_view: gpu::TextureView,
168    surface_size: gpu::Extent,
169    surface_info: gpu::SurfaceInfo,
170}
171
172impl Rasterizer {
173    #[profiling::function]
174    pub fn new(
175        encoder: &mut gpu::CommandEncoder,
176        gpu: &gpu::Context,
177        shaders: Shaders,
178        shader_man: &blade_asset::AssetManager<crate::shader::Baker>,
179        config: &crate::render::RenderConfig,
180    ) -> Self {
181        let pipelines = RasterPipelines::init(&shaders, config, gpu, shader_man).unwrap();
182        #[cfg(target_os = "android")]
183        let debug = None;
184        #[cfg(not(target_os = "android"))]
185        let debug = {
186            let sh_draw = shader_man[shaders.debug_draw].raw.as_ref().unwrap();
187            let sh_blit = shader_man[shaders.debug_blit].raw.as_ref().unwrap();
188            Some(crate::render::DebugRender::init(
189                encoder,
190                gpu,
191                sh_draw,
192                sh_blit,
193                config.max_debug_lines,
194                config.surface_info,
195            ))
196        };
197        let dummy = DummyResources::new(encoder, gpu);
198        let sampler_linear = gpu.create_sampler(gpu::SamplerDesc {
199            name: "raster-linear",
200            address_modes: [gpu::AddressMode::Repeat; 3],
201            mag_filter: gpu::FilterMode::Linear,
202            min_filter: gpu::FilterMode::Linear,
203            mipmap_filter: gpu::FilterMode::Linear,
204            ..Default::default()
205        });
206        let (depth_texture, depth_view) = Self::create_depth_target(config.surface_size, gpu);
207
208        Self {
209            shaders,
210            pipelines,
211            sampler_linear,
212            debug,
213            dummy,
214            depth_texture,
215            depth_view,
216            surface_size: config.surface_size,
217            surface_info: config.surface_info,
218        }
219    }
220
221    pub fn destroy(&mut self, gpu: &gpu::Context) {
222        if let Some(debug) = self.debug.as_mut() {
223            debug.destroy(gpu);
224        }
225        self.dummy.destroy(gpu);
226        gpu.destroy_texture_view(self.depth_view);
227        gpu.destroy_texture(self.depth_texture);
228        gpu.destroy_sampler(self.sampler_linear);
229        gpu.destroy_render_pipeline(&mut self.pipelines.main);
230        gpu.destroy_render_pipeline(&mut self.pipelines.sky);
231    }
232
233    #[profiling::function]
234    pub fn hot_reload(
235        &mut self,
236        asset_hub: &AssetHub,
237        gpu: &gpu::Context,
238        sync_point: &gpu::SyncPoint,
239    ) -> bool {
240        let mut tasks = Vec::new();
241        let old = self.shaders.clone();
242
243        tasks.extend(asset_hub.shaders.hot_reload(&mut self.shaders.raster));
244
245        if tasks.is_empty() {
246            return false;
247        }
248
249        log::info!("Hot reloading raster shaders");
250        let _ = gpu.wait_for(sync_point, !0);
251        for task in tasks {
252            let _ = task.join();
253        }
254
255        if self.shaders.raster != old.raster
256            && let Ok(ref shader) = asset_hub.shaders[self.shaders.raster].raw
257        {
258            self.pipelines.main = RasterPipelines::create_main(shader, self.surface_info, gpu);
259            self.pipelines.sky = RasterPipelines::create_sky(shader, self.surface_info, gpu);
260        }
261
262        true
263    }
264
265    pub fn get_surface_size(&self) -> gpu::Extent {
266        self.surface_size
267    }
268
269    pub fn depth_view(&self) -> gpu::TextureView {
270        self.depth_view
271    }
272
273    pub fn depth_texture(&self) -> gpu::Texture {
274        self.depth_texture
275    }
276
277    pub fn resize_screen(
278        &mut self,
279        size: gpu::Extent,
280        _encoder: &mut gpu::CommandEncoder,
281        gpu: &gpu::Context,
282    ) {
283        if size == self.surface_size {
284            return;
285        }
286        gpu.destroy_texture_view(self.depth_view);
287        gpu.destroy_texture(self.depth_texture);
288        let (depth_texture, depth_view) = Self::create_depth_target(size, gpu);
289        self.depth_texture = depth_texture;
290        self.depth_view = depth_view;
291        self.surface_size = size;
292    }
293
294    #[profiling::function]
295    pub fn render(
296        &mut self,
297        pass: &mut gpu::RenderCommandEncoder,
298        camera: &crate::Camera,
299        objects: &[Object],
300        asset_hub: &AssetHub,
301        environment_map: Option<blade_asset::Handle<crate::Texture>>,
302        config: RasterConfig,
303    ) {
304        let env_map_enabled = environment_map.is_some();
305        let frame_params = self.make_frame_params(camera, config, env_map_enabled);
306        if let mut pc = pass.with(&self.pipelines.main) {
307            for object in objects.iter() {
308                let model = &asset_hub.models[object.model];
309                let object_transform = mat4_transform(&object.transform);
310                let object_normal = object_transform.inverse().transpose();
311
312                for geometry in model.geometries.iter() {
313                    let geometry_transform = mat4_transform(&geometry.transform);
314                    let world_transform = object_transform * geometry_transform;
315                    let normal_transform = object_normal * geometry_transform.inverse().transpose();
316                    let normal_basis = glam::Mat3::from_cols(
317                        normal_transform.x_axis.truncate().normalize_or_zero(),
318                        normal_transform.y_axis.truncate().normalize_or_zero(),
319                        normal_transform.z_axis.truncate().normalize_or_zero(),
320                    );
321                    let normal_quat = glam::Quat::from_mat3(&normal_basis).normalize();
322                    let material = &model.materials[geometry.material_index];
323
324                    let (normal_tex, normal_scale) = match material.normal_texture {
325                        Some(handle) => {
326                            let texture = &asset_hub.textures[handle];
327                            (texture.view, material.normal_scale)
328                        }
329                        None => (self.dummy.white_view, 0.0),
330                    };
331                    let base_color_tex = match material.base_color_texture {
332                        Some(handle) => asset_hub.textures[handle].view,
333                        None => self.dummy.white_view,
334                    };
335
336                    pc.bind(
337                        0,
338                        &RasterMainData {
339                            frame_params,
340                            draw_params: RasterDrawParams {
341                                model: world_transform.to_cols_array(),
342                                normal_quat: normal_quat.to_array(),
343                                base_color_factor: [
344                                    material.base_color_factor[0] * object.color_tint[0],
345                                    material.base_color_factor[1] * object.color_tint[1],
346                                    material.base_color_factor[2] * object.color_tint[2],
347                                    material.base_color_factor[3] * object.color_tint[3],
348                                ],
349                                material: [normal_scale, 0.0, 0.0, 0.0],
350                            },
351                            vertices: model.vertex_buffer.at(0),
352                            samp: self.sampler_linear,
353                            base_color_tex,
354                            normal_tex,
355                        },
356                    );
357
358                    let vertex_count = geometry.vertex_range.end - geometry.vertex_range.start;
359                    let index_count = geometry.triangle_count * 3;
360                    match geometry.index_type {
361                        Some(index_type) => {
362                            pc.draw_indexed(
363                                model.index_buffer.at(geometry.index_offset),
364                                index_type,
365                                index_count,
366                                geometry.vertex_range.start as i32,
367                                0,
368                                1,
369                            );
370                        }
371                        None => {
372                            pc.draw(geometry.vertex_range.start, vertex_count, 0, 1);
373                        }
374                    }
375                }
376            }
377        }
378
379        let env_map = environment_map
380            .map(|handle| asset_hub.textures[handle].view)
381            .unwrap_or(self.dummy.black_view);
382        self.render_sky(pass, frame_params, env_map);
383    }
384
385    pub fn render_debug_lines(
386        &self,
387        pass: &mut gpu::RenderCommandEncoder,
388        camera: &crate::Camera,
389        debug_lines: &[crate::DebugLine],
390    ) {
391        let Some(debug) = self.debug.as_ref() else {
392            return;
393        };
394        if debug_lines.is_empty() {
395            return;
396        }
397        let camera_params = self.make_camera_params(camera);
398        debug.render_lines(debug_lines, camera_params, self.depth_view, pass);
399    }
400
401    /// Render just the sky background (env map or procedural fallback).
402    pub fn render_sky_only(
403        &self,
404        pass: &mut gpu::RenderCommandEncoder,
405        camera: &crate::Camera,
406        environment_map: Option<blade_asset::Handle<crate::Texture>>,
407        asset_hub: &AssetHub,
408        config: RasterConfig,
409    ) {
410        let env_map_enabled = environment_map.is_some();
411        let frame_params = self.make_frame_params(camera, config, env_map_enabled);
412        let env_map = environment_map
413            .map(|handle| asset_hub.textures[handle].view)
414            .unwrap_or(self.dummy.black_view);
415        self.render_sky(pass, frame_params, env_map);
416    }
417
418    fn render_sky(
419        &self,
420        pass: &mut gpu::RenderCommandEncoder,
421        frame_params: RasterFrameParams,
422        env_map: gpu::TextureView,
423    ) {
424        let mut pc = pass.with(&self.pipelines.sky);
425        pc.bind(
426            0,
427            &RasterSkyData {
428                sky_params: frame_params,
429                samp: self.sampler_linear,
430                env_map,
431            },
432        );
433        pc.draw(0, 3, 0, 1);
434    }
435
436    fn create_depth_target(
437        size: gpu::Extent,
438        gpu: &gpu::Context,
439    ) -> (gpu::Texture, gpu::TextureView) {
440        let texture = gpu.create_texture(gpu::TextureDesc {
441            name: "raster depth",
442            size,
443            format: gpu::TextureFormat::Depth32Float,
444            array_layer_count: 1,
445            mip_level_count: 1,
446            sample_count: 1,
447            dimension: gpu::TextureDimension::D2,
448            usage: gpu::TextureUsage::TARGET | gpu::TextureUsage::RESOURCE,
449            external: None,
450        });
451        let view = gpu.create_texture_view(
452            texture,
453            gpu::TextureViewDesc {
454                name: "raster depth",
455                format: gpu::TextureFormat::Depth32Float,
456                dimension: gpu::ViewDimension::D2,
457                subresources: &gpu::TextureSubresources::default(),
458            },
459        );
460        (texture, view)
461    }
462
463    fn make_camera_params(&self, camera: &crate::Camera) -> CameraParams {
464        let fov_x = 2.0
465            * ((camera.fov_y * 0.5).tan() * self.surface_size.width as f32
466                / self.surface_size.height as f32)
467                .atan();
468        CameraParams {
469            position: camera.pos.into(),
470            depth: camera.depth,
471            orientation: camera.rot.into(),
472            fov: [fov_x, camera.fov_y],
473            target_size: [self.surface_size.width, self.surface_size.height],
474        }
475    }
476
477    fn make_frame_params(
478        &self,
479        camera: &crate::Camera,
480        config: RasterConfig,
481        env_map_enabled: bool,
482    ) -> RasterFrameParams {
483        let pos = glam::Vec3::from(camera.pos);
484        let rot = glam::Quat::from(camera.rot);
485        let view = glam::Mat4::from_rotation_translation(rot, pos).inverse();
486        let near = 0.01;
487        let far = camera.depth;
488        let proj = if let Some(fov) = camera.fov {
489            // Asymmetric off-center projection for XR
490            let left = -fov.left.tan() * near;
491            let right = fov.right.tan() * near;
492            let bottom = -fov.down.tan() * near;
493            let top = fov.up.tan() * near;
494            let w = right - left;
495            let h = top - bottom;
496            glam::Mat4::from_cols(
497                glam::Vec4::new(2.0 * near / w, 0.0, 0.0, 0.0),
498                glam::Vec4::new(0.0, 2.0 * near / h, 0.0, 0.0),
499                glam::Vec4::new(
500                    (right + left) / w,
501                    (top + bottom) / h,
502                    far / (near - far),
503                    -1.0,
504                ),
505                glam::Vec4::new(0.0, 0.0, far * near / (near - far), 0.0),
506            )
507        } else {
508            let aspect = self.surface_size.width as f32 / self.surface_size.height.max(1) as f32;
509            glam::Mat4::perspective_rh(camera.fov_y, aspect, near, far)
510        };
511        let view_proj = proj * view;
512        let inv_view_proj = view_proj.inverse();
513        let light_dir = glam::Vec3::from(config.light_dir).normalize_or_zero();
514        RasterFrameParams {
515            view_proj: view_proj.to_cols_array(),
516            inv_view_proj: inv_view_proj.to_cols_array(),
517            camera_pos: [pos.x, pos.y, pos.z, 1.0],
518            light_dir: [light_dir.x, light_dir.y, light_dir.z, 0.0],
519            light_color: {
520                let c = config.light_color;
521                [c.x, c.y, c.z, 0.0]
522            },
523            ambient_color: {
524                let c = config.ambient_color;
525                [c.x, c.y, c.z, config.space_sky as u32 as f32]
526            },
527            material: [
528                config.roughness,
529                config.metallic,
530                env_map_enabled as u32 as f32,
531                0.0,
532            ],
533        }
534    }
535}
536
537impl gpu::Vertex for Vertex {
538    fn layout() -> gpu::VertexLayout {
539        gpu::VertexLayout {
540            attributes: vec![
541                (
542                    "position",
543                    gpu::VertexAttribute {
544                        offset: 0,
545                        format: gpu::VertexFormat::F32Vec3,
546                    },
547                ),
548                (
549                    "bitangent_sign",
550                    gpu::VertexAttribute {
551                        offset: 12,
552                        format: gpu::VertexFormat::F32,
553                    },
554                ),
555                (
556                    "tex_coords",
557                    gpu::VertexAttribute {
558                        offset: 16,
559                        format: gpu::VertexFormat::F32Vec2,
560                    },
561                ),
562                (
563                    "normal",
564                    gpu::VertexAttribute {
565                        offset: 24,
566                        format: gpu::VertexFormat::U32,
567                    },
568                ),
569                (
570                    "tangent",
571                    gpu::VertexAttribute {
572                        offset: 28,
573                        format: gpu::VertexFormat::U32,
574                    },
575                ),
576            ],
577            stride: mem::size_of::<Vertex>() as u32,
578        }
579    }
580}
581
582fn mat4_transform(t: &gpu::Transform) -> glam::Mat4 {
583    glam::Mat4 {
584        x_axis: t.x.into(),
585        y_axis: t.y.into(),
586        z_axis: t.z.into(),
587        w_axis: glam::Vec4::W,
588    }
589    .transpose()
590}