diff --git a/crates/re_renderer/shader/composite.wgsl b/crates/re_renderer/shader/composite.wgsl index 22ce9acabc55..6f23c7083161 100644 --- a/crates/re_renderer/shader/composite.wgsl +++ b/crates/re_renderer/shader/composite.wgsl @@ -3,19 +3,57 @@ #import <./global_bindings.wgsl> #import <./screen_triangle_vertex.wgsl> +struct CompositeUniformBuffer { + outline_color_layer_a: Vec4, + outline_color_layer_b: Vec4, + outline_radius_pixel: f32, +}; @group(1) @binding(0) -var input_texture: texture_2d; +var uniforms: CompositeUniformBuffer; + +@group(1) @binding(1) +var color_texture: texture_2d; + +@group(1) @binding(2) +var outline_voronoi_texture: texture_2d; @fragment fn main(in: FragmentInput) -> @location(0) Vec4 { + let resolution = Vec2(textureDimensions(color_texture).xy); + let pixel_coordinates = resolution * in.texcoord; + // Note that we can't use a simple textureLoad using @builtin(position) here despite the lack of filtering. // The issue is that positions provided by @builtin(position) are not dependent on the set viewport, // but are about the location of the texel in the target texture. - var input = textureSample(input_texture, nearest_sampler, in.texcoord).rgb; + var color = textureSample(color_texture, nearest_sampler, in.texcoord).rgb; // TODO(andreas): Do something meaningful with values above 1 - input = clamp(input, ZERO.xyz, ONE.xyz); + color = clamp(color, ZERO.xyz, ONE.xyz); + + // Outlines + { + let closest_positions = textureSample(outline_voronoi_texture, nearest_sampler, in.texcoord); + + let distance_pixel_a = distance(pixel_coordinates, closest_positions.xy); + let distance_pixel_b = distance(pixel_coordinates, closest_positions.zw); + + let sharpness = 1.0; // Fun to play around with, but not exposed yet. + let outline_a = saturate((uniforms.outline_radius_pixel - distance_pixel_a) * sharpness); + let outline_b = saturate((uniforms.outline_radius_pixel - distance_pixel_b) * sharpness); + + let outline_color_a = outline_a * uniforms.outline_color_layer_a; + let outline_color_b = outline_b * uniforms.outline_color_layer_b; + + // Blend outlines with screen color. + color = color * (1.0 - outline_color_a.a) + outline_color_a.rgb; + color = color * (1.0 - outline_color_b.a) + outline_color_b.rgb; + + // Show only the outline. Useful for debugging. + //color = outline_color_a.rgb; + + // Show the raw voronoi texture. Useful for debugging. + //color = Vec3(closest_positions.xy / resolution, 0.0); + } - // Convert to srgb - this is necessary since the final eframe output does *not* have an srgb format. - // Note that the input here is assumed to be linear - if the input texture was an srgb texture it would have been converted on load. - return Vec4(srgb_from_linear(input), 1.0); + // Apply srgb gamma curve - this is necessary since the final eframe output does *not* have an srgb format. + return Vec4(srgb_from_linear(color), 1.0); } diff --git a/crates/re_renderer/shader/outlines/outlines_from_voronoi.wgsl b/crates/re_renderer/shader/outlines/outlines_from_voronoi.wgsl deleted file mode 100644 index 0eb52f5f1972..000000000000 --- a/crates/re_renderer/shader/outlines/outlines_from_voronoi.wgsl +++ /dev/null @@ -1,44 +0,0 @@ -#import <../types.wgsl> -#import <../global_bindings.wgsl> -#import <../screen_triangle_vertex.wgsl> -#import <../utils/srgb.wgsl> - -@group(1) @binding(0) -var voronoi_texture: texture_2d; - -struct OutlineConfigUniformBuffer { - color_layer_a: Vec4, - color_layer_b: Vec4, - outline_radius_pixel: f32, -}; -@group(1) @binding(1) -var uniforms: OutlineConfigUniformBuffer; - -@fragment -fn main(in: FragmentInput) -> @location(0) Vec4 { - let resolution = Vec2(textureDimensions(voronoi_texture).xy); - let pixel_coordinates = resolution * in.texcoord; - let closest_positions = textureSample(voronoi_texture, nearest_sampler, in.texcoord); - - let distance_pixel_a = distance(pixel_coordinates, closest_positions.xy); - let distance_pixel_b = distance(pixel_coordinates, closest_positions.zw); - - let sharpness = 1.0; // Fun to play around with, but not exposed yet. - let outline_a = saturate((uniforms.outline_radius_pixel - distance_pixel_a) * sharpness); - let outline_b = saturate((uniforms.outline_radius_pixel - distance_pixel_b) * sharpness); - - let color_a = outline_a * uniforms.color_layer_a; - let color_b = outline_b * uniforms.color_layer_b; - - // Blend B over A. - let color = color_a * (1.0 - color_b.a) + color_b; - - // We're going directly to an srgb-gamma target without automatic conversion. - return srgba_from_linear(color); - - // Show only the outline. Useful for debugging. - //return Vec4(color.rgb, 1.0); - - // Show the raw voronoi texture. Useful for debugging. - //return Vec4(closest_positions.xy / resolution, 0.0, 1.0); -} diff --git a/crates/re_renderer/src/file_server.rs b/crates/re_renderer/src/file_server.rs index 075476e26ff1..3b42ac0176d2 100644 --- a/crates/re_renderer/src/file_server.rs +++ b/crates/re_renderer/src/file_server.rs @@ -82,6 +82,7 @@ macro_rules! include_file { // Therefore, the in-memory filesystem will actually be able to find this path, // and canonicalize it. use anyhow::Context as _; + use $crate::file_system::FileSystem; $crate::get_filesystem().canonicalize(&path) .with_context(|| format!("include_file!({}) (rooted at {:?}) failed while trying to import virtual path {path:?}", $path, file!())) .unwrap() diff --git a/crates/re_renderer/src/renderer/compositor.rs b/crates/re_renderer/src/renderer/compositor.rs index 5cdbfb25d00a..cbdbac5f4bd5 100644 --- a/crates/re_renderer/src/renderer/compositor.rs +++ b/crates/re_renderer/src/renderer/compositor.rs @@ -1,4 +1,5 @@ use crate::{ + allocator::create_and_fill_uniform_buffer, context::SharedRendererData, include_file, wgpu_resources::{ @@ -6,15 +7,30 @@ use crate::{ GpuRenderPipelineHandle, GpuTexture, PipelineLayoutDesc, RenderPipelineDesc, ShaderModuleDesc, WgpuResourcePools, }, + Rgba, }; use super::{ - screen_triangle_vertex_shader, DrawData, DrawPhase, FileResolver, FileSystem, RenderContext, - Renderer, + screen_triangle_vertex_shader, DrawData, DrawPhase, FileResolver, FileSystem, OutlineConfig, + RenderContext, Renderer, }; use smallvec::smallvec; +mod gpu_data { + use crate::wgpu_buffer_types; + + /// Keep in sync with `composite.wgsl` + #[repr(C, align(256))] + #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] + pub struct CompositeUniformBuffer { + pub outline_color_layer_a: wgpu_buffer_types::Vec4, + pub outline_color_layer_b: wgpu_buffer_types::Vec4, + pub outline_radius_pixel: wgpu_buffer_types::F32RowPadded, + pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 3], + } +} + pub struct Compositor { render_pipeline: GpuRenderPipelineHandle, bind_group_layout: GpuBindGroupLayoutHandle, @@ -32,7 +48,12 @@ impl DrawData for CompositorDrawData { } impl CompositorDrawData { - pub fn new(ctx: &mut RenderContext, target: &GpuTexture) -> Self { + pub fn new( + ctx: &mut RenderContext, + color_texture: &GpuTexture, + outline_final_voronoi: Option<&GpuTexture>, + outline_config: &Option, + ) -> Self { let mut renderers = ctx.renderers.write(); let compositor = renderers.get_or_create::<_, Compositor>( &ctx.shared_renderer_data, @@ -40,13 +61,45 @@ impl CompositorDrawData { &ctx.device, &mut ctx.resolver, ); + + let outline_config = outline_config.clone().unwrap_or(OutlineConfig { + outline_radius_pixel: 0.0, + color_layer_a: Rgba::TRANSPARENT, + color_layer_b: Rgba::TRANSPARENT, + }); + + let uniform_buffer_binding = create_and_fill_uniform_buffer( + ctx, + "CompositorDrawData".into(), + gpu_data::CompositeUniformBuffer { + outline_color_layer_a: outline_config.color_layer_a.into(), + outline_color_layer_b: outline_config.color_layer_b.into(), + outline_radius_pixel: outline_config.outline_radius_pixel.into(), + end_padding: Default::default(), + }, + ); + + let outline_final_voronoi_handle = outline_final_voronoi.map_or_else( + || { + ctx.texture_manager_2d + .get(ctx.texture_manager_2d.white_texture_handle()) + .expect("white fallback texture missing") + .handle + }, + |t| t.handle, + ); + CompositorDrawData { bind_group: ctx.gpu_resources.bind_groups.alloc( &ctx.device, &ctx.gpu_resources, &BindGroupDesc { label: "compositor".into(), - entries: smallvec![BindGroupEntry::DefaultTextureView(target.handle)], + entries: smallvec![ + uniform_buffer_binding, + BindGroupEntry::DefaultTextureView(color_texture.handle), + BindGroupEntry::DefaultTextureView(outline_final_voronoi_handle) + ], layout: compositor.bind_group_layout, }, ), @@ -66,17 +119,43 @@ impl Renderer for Compositor { let bind_group_layout = pools.bind_group_layouts.get_or_create( device, &BindGroupLayoutDesc { - label: "compositor".into(), - entries: vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, + label: "Compositor::bind_group_layout".into(), + entries: vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::< + gpu_data::CompositeUniformBuffer, + >( + ) + as _), + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, }, - count: None, - }], + ], }, ); diff --git a/crates/re_renderer/src/renderer/mod.rs b/crates/re_renderer/src/renderer/mod.rs index 0c07aa20cfab..aa0b40665e5d 100644 --- a/crates/re_renderer/src/renderer/mod.rs +++ b/crates/re_renderer/src/renderer/mod.rs @@ -25,7 +25,8 @@ mod mesh_renderer; pub(crate) use mesh_renderer::MeshRenderer; pub use mesh_renderer::{MeshDrawData, MeshInstance}; -pub mod compositor; +mod compositor; +pub(crate) use compositor::CompositorDrawData; mod outlines; pub(crate) use outlines::OutlineMaskProcessor; diff --git a/crates/re_renderer/src/renderer/outlines.rs b/crates/re_renderer/src/renderer/outlines.rs index e86af2f27dfc..15ac5ac1a224 100644 --- a/crates/re_renderer/src/renderer/outlines.rs +++ b/crates/re_renderer/src/renderer/outlines.rs @@ -1,11 +1,11 @@ //! Outlines as postprocessing effect. //! -//! This module provides the [`OutlineMaskProcessor`] which handles render passes around outlines -//! and [`OutlineCompositor`] which handles compositing the outlines into the final image. +//! This module provides the [`OutlineMaskProcessor`] which handles render passes around outlines. +//! The outlines themselves are evaluated and drawn by the main compositor. //! //! There are two channels (in shader code referred to as A and B) that are handled simultaneously. //! For configuring the look of the outline refer to [`OutlineConfig`]. -//! For setting outlines for an individual primitive from another [`Renderer`]/[`DrawData`], +//! For setting outlines for an individual primitive from another [`super::Renderer`]/[`super::DrawData`], //! check for [`OutlineMaskPreference`] settings on that primitive. //! //! How it works: @@ -43,19 +43,18 @@ //! More details can be found in the respective shader code. //! -use super::{screen_triangle_vertex_shader, DrawData, DrawPhase, Renderer}; +use super::screen_triangle_vertex_shader; use crate::{ - allocator::{create_and_fill_uniform_buffer, create_and_fill_uniform_buffer_batch}, + allocator::create_and_fill_uniform_buffer_batch, config::HardwareTier, - context::SharedRendererData, include_file, view_builder::ViewBuilder, wgpu_resources::{ BindGroupDesc, BindGroupEntry, BindGroupLayoutDesc, GpuBindGroup, GpuBindGroupLayoutHandle, - GpuRenderPipelineHandle, GpuTexture, GpuTextureHandle, PipelineLayoutDesc, PoolError, - RenderPipelineDesc, SamplerDesc, ShaderModuleDesc, WgpuResourcePools, + GpuRenderPipelineHandle, GpuTexture, PipelineLayoutDesc, PoolError, RenderPipelineDesc, + SamplerDesc, ShaderModuleDesc, WgpuResourcePools, }, - DebugLabel, FileResolver, FileSystem, RenderContext, + DebugLabel, RenderContext, }; use smallvec::smallvec; @@ -130,7 +129,6 @@ pub struct OutlineMaskProcessor { bind_group_jumpflooding_init: GpuBindGroup, bind_group_jumpflooding_steps: Vec, - bind_group_draw_outlines: GpuBindGroup, render_pipeline_jumpflooding_init: GpuRenderPipelineHandle, render_pipeline_jumpflooding_step: GpuRenderPipelineHandle, @@ -148,16 +146,6 @@ mod gpu_data { /// All this padding hurts. `step_width` be a PushConstant but they are not widely supported enough! pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 1], } - - /// Keep in sync with `outlines_from_voronoi.wgsl` - #[repr(C, align(256))] - #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] - pub struct OutlineConfigUniformBuffer { - pub color_layer_a: wgpu_buffer_types::Vec4, - pub color_layer_b: wgpu_buffer_types::Vec4, - pub outline_radius_pixel: wgpu_buffer_types::F32RowPadded, - pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 3], - } } impl OutlineMaskProcessor { @@ -274,26 +262,6 @@ impl OutlineMaskProcessor { &voronoi_textures, ); - // Create a bind group for the final compositor pass - it will read the last voronoi texture - let bind_group_draw_outlines = { - let mut renderers = ctx.renderers.write(); - let compositor_renderer = renderers.get_or_create::<_, OutlineCompositor>( - &ctx.shared_renderer_data, - &mut ctx.gpu_resources, - &ctx.device, - &mut ctx.resolver, - ); - - // Point to the last written voronoi texture - // We start writing to voronoi_textures[0] and then do `num_steps` ping-pong rendering. - // Therefore, the last texture is voronoi_textures[num_steps % 2] - compositor_renderer.create_bind_group( - ctx, - voronoi_textures[bind_group_jumpflooding_steps.len() % 2].handle, - config, - ) - }; - // ------------- Render Pipelines ------------- let screen_triangle_vertex_shader = @@ -368,12 +336,18 @@ impl OutlineMaskProcessor { voronoi_textures, bind_group_jumpflooding_init, bind_group_jumpflooding_steps, - bind_group_draw_outlines, render_pipeline_jumpflooding_init, render_pipeline_jumpflooding_step, } } + pub fn final_voronoi_texture(&self) -> &GpuTexture { + // Point to the last written voronoi texture + // We start writing to voronoi_textures[0] and then do `num_steps` ping-pong rendering. + // Therefore, the last texture is voronoi_textures[num_steps % 2] + &self.voronoi_textures[self.bind_group_jumpflooding_steps.len() % 2] + } + pub fn start_mask_render_pass<'a>( &'a self, encoder: &'a mut wgpu::CommandEncoder, @@ -403,7 +377,7 @@ impl OutlineMaskProcessor { self, pools: &WgpuResourcePools, encoder: &mut wgpu::CommandEncoder, - ) -> Result { + ) -> Result<(), PoolError> { let pipelines = &pools.render_pipelines; let ops = wgpu::Operations { @@ -454,9 +428,7 @@ impl OutlineMaskProcessor { jumpflooding_step.draw(0..3, 0..1); } - Ok(OutlineCompositingDrawData { - bind_group: self.bind_group_draw_outlines, - }) + Ok(()) } fn create_bind_group_jumpflooding_init( @@ -593,156 +565,3 @@ impl OutlineMaskProcessor { ) } } - -pub struct OutlineCompositor { - render_pipeline: GpuRenderPipelineHandle, - bind_group_layout: GpuBindGroupLayoutHandle, -} - -#[derive(Clone)] -pub struct OutlineCompositingDrawData { - bind_group: GpuBindGroup, -} - -impl DrawData for OutlineCompositingDrawData { - type Renderer = OutlineCompositor; -} - -impl OutlineCompositor { - fn create_bind_group( - &self, - ctx: &RenderContext, - final_voronoi_texture: GpuTextureHandle, - config: &OutlineConfig, - ) -> GpuBindGroup { - let uniform_buffer_binding = create_and_fill_uniform_buffer( - ctx, - "OutlineCompositingDrawData".into(), - gpu_data::OutlineConfigUniformBuffer { - color_layer_a: config.color_layer_a.into(), - color_layer_b: config.color_layer_b.into(), - outline_radius_pixel: config.outline_radius_pixel.into(), - end_padding: Default::default(), - }, - ); - - ctx.gpu_resources.bind_groups.alloc( - &ctx.device, - &ctx.gpu_resources, - &BindGroupDesc { - label: "OutlineCompositingDrawData".into(), - entries: smallvec![ - BindGroupEntry::DefaultTextureView(final_voronoi_texture), - uniform_buffer_binding - ], - layout: self.bind_group_layout, - }, - ) - } -} - -impl Renderer for OutlineCompositor { - type RendererDrawData = OutlineCompositingDrawData; - - fn participated_phases() -> &'static [DrawPhase] { - &[DrawPhase::Compositing] - } - - fn create_renderer( - shared_data: &SharedRendererData, - pools: &mut WgpuResourcePools, - device: &wgpu::Device, - resolver: &mut FileResolver, - ) -> Self { - let bind_group_layout = pools.bind_group_layouts.get_or_create( - device, - &BindGroupLayoutDesc { - label: "OutlineCompositor::bind_group_layout".into(), - entries: vec![ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::< - gpu_data::OutlineConfigUniformBuffer, - >( - ) - as _), - }, - count: None, - }, - ], - }, - ); - let vertex_handle = screen_triangle_vertex_shader(pools, device, resolver); - let render_pipeline = pools.render_pipelines.get_or_create( - device, - &RenderPipelineDesc { - label: "OutlineCompositor".into(), - pipeline_layout: pools.pipeline_layouts.get_or_create( - device, - &PipelineLayoutDesc { - label: "OutlineCompositor".into(), - entries: vec![shared_data.global_bindings.layout, bind_group_layout], - }, - &pools.bind_group_layouts, - ), - vertex_entrypoint: "main".into(), - vertex_handle, - fragment_entrypoint: "main".into(), - fragment_handle: pools.shader_modules.get_or_create( - device, - resolver, - &ShaderModuleDesc { - label: "outlines_from_voronoi".into(), - source: include_file!("../../shader/outlines/outlines_from_voronoi.wgsl"), - }, - ), - vertex_buffers: smallvec![], - render_targets: smallvec![Some(wgpu::ColorTargetState { - format: shared_data.config.output_format_color, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::all() - })], - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - }, - &pools.pipeline_layouts, - &pools.shader_modules, - ); - - OutlineCompositor { - render_pipeline, - bind_group_layout, - } - } - - fn draw<'a>( - &self, - pools: &'a WgpuResourcePools, - _phase: DrawPhase, - pass: &mut wgpu::RenderPass<'a>, - draw_data: &'a OutlineCompositingDrawData, - ) -> anyhow::Result<()> { - let pipeline = pools.render_pipelines.get_resource(self.render_pipeline)?; - - pass.set_pipeline(pipeline); - pass.set_bind_group(1, &draw_data.bind_group, &[]); - pass.draw(0..3, 0..1); - - Ok(()) - } -} diff --git a/crates/re_renderer/src/view_builder.rs b/crates/re_renderer/src/view_builder.rs index e751fe2b4f76..ef804881c86d 100644 --- a/crates/re_renderer/src/view_builder.rs +++ b/crates/re_renderer/src/view_builder.rs @@ -7,8 +7,7 @@ use crate::{ context::RenderContext, global_bindings::FrameUniformBuffer, renderer::{ - compositor::CompositorDrawData, DrawData, DrawPhase, OutlineConfig, OutlineMaskProcessor, - Renderer, + CompositorDrawData, DrawData, DrawPhase, OutlineConfig, OutlineMaskProcessor, Renderer, }, wgpu_resources::{GpuBindGroup, GpuTexture, TextureDesc}, DebugLabel, Rgba, Size, @@ -278,7 +277,23 @@ impl ViewBuilder { }, ); - self.queue_draw(&CompositorDrawData::new(ctx, &main_target_resolved)); + self.outline_mask_processor = config.outline_config.as_ref().map(|outline_config| { + OutlineMaskProcessor::new( + ctx, + outline_config, + &config.name, + config.resolution_in_pixel, + ) + }); + + self.queue_draw(&CompositorDrawData::new( + ctx, + &main_target_resolved, + self.outline_mask_processor + .as_ref() + .map(|p| p.final_voronoi_texture()), + &config.outline_config, + )); let aspect_ratio = config.resolution_in_pixel[0] as f32 / config.resolution_in_pixel[1] as f32; @@ -421,15 +436,6 @@ impl ViewBuilder { frame_uniform_buffer, ); - self.outline_mask_processor = config.outline_config.map(|outline_config| { - OutlineMaskProcessor::new( - ctx, - &outline_config, - &config.name, - config.resolution_in_pixel, - ) - }); - self.setup = Some(ViewTargetSetup { name: config.name, bind_group_0, @@ -550,9 +556,7 @@ impl ViewBuilder { pass.set_bind_group(0, &setup.bind_group_0, &[]); self.draw_phase(ctx, DrawPhase::OutlineMask, &mut pass); } - self.queue_draw( - &outline_mask_processor.compute_outlines(&ctx.gpu_resources, &mut encoder)?, - ); + outline_mask_processor.compute_outlines(&ctx.gpu_resources, &mut encoder)?; } Ok(encoder.finish()) diff --git a/crates/re_renderer/src/workspace_shaders.rs b/crates/re_renderer/src/workspace_shaders.rs index 3db56b0f8d19..4f1d9095fed2 100644 --- a/crates/re_renderer/src/workspace_shaders.rs +++ b/crates/re_renderer/src/workspace_shaders.rs @@ -85,12 +85,6 @@ pub fn init() { fs.create_file(virtpath, content).unwrap(); } - { - let virtpath = Path::new("shader/outlines/outlines_from_voronoi.wgsl"); - let content = include_str!("../shader/outlines/outlines_from_voronoi.wgsl").into(); - fs.create_file(virtpath, content).unwrap(); - } - { let virtpath = Path::new("shader/point_cloud.wgsl"); let content = include_str!("../shader/point_cloud.wgsl").into();