Last active
January 28, 2026 14:33
-
-
Save bczhc/4bcead558fc05c5572bda023c2188489 to your computer and use it in GitHub Desktop.
volumeshader_bm native wgpu复刻版
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// Native wgpu version of https://cznull.github.io/vsbm | |
| /// | |
| /// Co-worked with Gemini. | |
| use bytemuck::{Pod, Zeroable}; | |
| use std::sync::Arc; | |
| use std::time::Instant; | |
| use std::{env, iter}; | |
| use winit::application::ApplicationHandler; | |
| use winit::event_loop::{ActiveEventLoop, OwnedDisplayHandle}; | |
| use winit::window::{Window, WindowId}; | |
| use winit::{ | |
| event::*, | |
| event_loop::{ControlFlow, EventLoop}, | |
| }; | |
| // --- Uniform 数据结构 (必须符合 WGSL 的 16 字节对齐) --- | |
| #[repr(C)] | |
| #[derive(Copy, Clone, Debug, Pod, Zeroable)] | |
| struct Uniforms { | |
| origin: [f32; 3], | |
| padding1: f32, | |
| right: [f32; 3], | |
| padding2: f32, | |
| up: [f32; 3], | |
| padding3: f32, | |
| forward: [f32; 3], | |
| padding4: f32, | |
| screen_size: [f32; 2], | |
| len: f32, | |
| padding5: f32, | |
| } | |
| struct State { | |
| surface: wgpu::Surface<'static>, | |
| device: wgpu::Device, | |
| queue: wgpu::Queue, | |
| size: winit::dpi::PhysicalSize<u32>, | |
| render_pipeline: wgpu::RenderPipeline, | |
| uniform_buffer: wgpu::Buffer, | |
| uniform_bind_group: wgpu::BindGroup, | |
| start_time: Instant, | |
| window: Arc<Window>, | |
| surface_format: wgpu::TextureFormat, | |
| } | |
| impl State { | |
| fn window(&self) -> &Window { | |
| &self.window | |
| } | |
| fn configure_surface(&self) { | |
| let surface_config = wgpu::SurfaceConfiguration { | |
| usage: wgpu::TextureUsages::RENDER_ATTACHMENT, | |
| format: self.surface_format, | |
| // Request compatibility with the sRGB-format texture view we‘re going to create later. | |
| view_formats: vec![self.surface_format.add_srgb_suffix()], | |
| alpha_mode: wgpu::CompositeAlphaMode::Auto, | |
| width: self.size.width, | |
| height: self.size.height, | |
| desired_maximum_frame_latency: 2, | |
| present_mode: wgpu::PresentMode::AutoNoVsync, | |
| }; | |
| self.surface.configure(&self.device, &surface_config); | |
| } | |
| async fn new(display: OwnedDisplayHandle, window: Arc<Window>) -> Self { | |
| let saved_window = Arc::clone(&window); | |
| let size = window.inner_size(); | |
| let instance = wgpu::Instance::default(); | |
| let surface = instance.create_surface(window).unwrap(); | |
| let adapter = instance | |
| .request_adapter(&wgpu::RequestAdapterOptions { | |
| power_preference: wgpu::PowerPreference::HighPerformance, | |
| compatible_surface: Some(&surface), | |
| force_fallback_adapter: false, | |
| }) | |
| .await | |
| .unwrap(); | |
| let (device, queue) = adapter | |
| .request_device(&wgpu::DeviceDescriptor::default()) | |
| .await | |
| .unwrap(); | |
| let surface_caps = surface.get_capabilities(&adapter); | |
| let surface_format = surface_caps.formats[0]; | |
| let config = wgpu::SurfaceConfiguration { | |
| usage: wgpu::TextureUsages::RENDER_ATTACHMENT, | |
| format: surface_format, | |
| width: size.width, | |
| height: size.height, | |
| present_mode: surface_caps.present_modes[0], | |
| desired_maximum_frame_latency: 0, | |
| alpha_mode: surface_caps.alpha_modes[0], | |
| view_formats: vec![surface_format.add_srgb_suffix()], | |
| }; | |
| surface.configure(&device, &config); | |
| // --- 核心 WGSL 着色器 --- | |
| let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { | |
| label: Some("Shader"), | |
| source: wgpu::ShaderSource::Wgsl(include_str!("../../vsbm.wgsl").into()), | |
| }); | |
| let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { | |
| label: Some("Uniform Buffer"), | |
| size: std::mem::size_of::<Uniforms>() as u64, | |
| usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, | |
| mapped_at_creation: false, | |
| }); | |
| let uniform_bind_group_layout = | |
| device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { | |
| entries: &[wgpu::BindGroupLayoutEntry { | |
| binding: 0, | |
| visibility: wgpu::ShaderStages::FRAGMENT, | |
| ty: wgpu::BindingType::Buffer { | |
| ty: wgpu::BufferBindingType::Uniform, | |
| has_dynamic_offset: false, | |
| min_binding_size: None, | |
| }, | |
| count: None, | |
| }], | |
| label: Some("uniform_bind_group_layout"), | |
| }); | |
| let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { | |
| layout: &uniform_bind_group_layout, | |
| entries: &[wgpu::BindGroupEntry { | |
| binding: 0, | |
| resource: uniform_buffer.as_entire_binding(), | |
| }], | |
| label: Some("uniform_bind_group"), | |
| }); | |
| let render_pipeline_layout = | |
| device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { | |
| label: Some("Render Pipeline Layout"), | |
| bind_group_layouts: &[&uniform_bind_group_layout], | |
| immediate_size: 0, | |
| }); | |
| let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { | |
| label: Some("Render Pipeline"), | |
| layout: Some(&render_pipeline_layout), | |
| vertex: wgpu::VertexState { | |
| module: &shader, | |
| entry_point: Some("vs_main"), | |
| compilation_options: Default::default(), | |
| buffers: &[], | |
| }, | |
| fragment: Some(wgpu::FragmentState { | |
| module: &shader, | |
| entry_point: Some("fs_main"), | |
| compilation_options: Default::default(), | |
| targets: &[Some(wgpu::ColorTargetState { | |
| format: config.format.add_srgb_suffix(), | |
| blend: None, | |
| write_mask: Default::default(), | |
| })], | |
| }), | |
| multiview_mask: None, | |
| primitive: wgpu::PrimitiveState::default(), | |
| depth_stencil: None, | |
| multisample: wgpu::MultisampleState::default(), | |
| cache: None, | |
| }); | |
| Self { | |
| surface, | |
| device, | |
| queue, | |
| size, | |
| render_pipeline, | |
| uniform_buffer, | |
| uniform_bind_group, | |
| start_time: Instant::now(), | |
| window: saved_window, | |
| surface_format, | |
| } | |
| } | |
| fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) { | |
| self.size = new_size; | |
| // reconfigure the surface | |
| self.configure_surface(); | |
| } | |
| fn update(&mut self) { | |
| let elapsed = self.start_time.elapsed().as_secs_f32(); | |
| let ang1 = 2.8 + elapsed * 0.5; // 自动旋转 | |
| let ang2: f32 = 0.4; | |
| let len = 1.6; | |
| let origin = [ | |
| len * ang1.cos() * ang2.cos(), | |
| len * ang2.sin(), | |
| len * ang1.sin() * ang2.cos(), | |
| ]; | |
| let right = [ang1.sin(), 0.0, -ang1.cos()]; | |
| let up = [ | |
| -ang2.sin() * ang1.cos(), | |
| ang2.cos(), | |
| -ang2.sin() * ang1.sin(), | |
| ]; | |
| let forward = [ | |
| -ang1.cos() * ang2.cos(), | |
| -ang2.sin(), | |
| -ang1.sin() * ang2.cos(), | |
| ]; | |
| let cx = self.size.width as f32; | |
| let cy = self.size.height as f32; | |
| let sx = (cx.min(cy) / cx) * (cx / cx.max(cy)); | |
| let sy = (cy.min(cx) / cy) * (cy / cx.max(cy)); | |
| // 因为使用了 1:1 的 Viewport,这里 screen_size 直接给 1.0 即可 | |
| let uniforms = Uniforms { | |
| origin, | |
| padding1: 0.0, | |
| right, | |
| padding2: 0.0, | |
| up, | |
| padding3: 0.0, | |
| forward, | |
| padding4: 0.0, | |
| screen_size: [1.0, 1.0], | |
| len, | |
| padding5: 0.0, | |
| }; | |
| self.queue | |
| .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms)); | |
| } | |
| fn render(&mut self) -> Result<(), wgpu::SurfaceError> { | |
| let surface_texture = self.surface.get_current_texture()?; | |
| let texture_view = surface_texture | |
| .texture | |
| .create_view(&wgpu::TextureViewDescriptor { | |
| // Without add_srgb_suffix() the image we will be working with | |
| // might not be "gamma correct". | |
| format: Some(self.surface_format.add_srgb_suffix()), | |
| ..Default::default() | |
| }); | |
| let mut encoder = self | |
| .device | |
| .create_command_encoder(&wgpu::CommandEncoderDescriptor { | |
| label: Some("Render Encoder"), | |
| }); | |
| { | |
| let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { | |
| label: Some("Render Pass"), | |
| color_attachments: &[Some(wgpu::RenderPassColorAttachment { | |
| view: &texture_view, | |
| depth_slice: None, | |
| resolve_target: None, | |
| ops: wgpu::Operations { | |
| load: wgpu::LoadOp::Clear(wgpu::Color { | |
| r: 0.07, | |
| g: 0.06, | |
| b: 0.08, | |
| a: 1.0, | |
| }), | |
| store: wgpu::StoreOp::Store, | |
| }, | |
| })], | |
| depth_stencil_attachment: None, | |
| occlusion_query_set: None, | |
| timestamp_writes: None, | |
| multiview_mask: None, | |
| }); | |
| // --- 核心逻辑:设置居中的 1:1 正方形视口 --- | |
| let win_w = self.size.width as f32; | |
| let win_h = self.size.height as f32; | |
| let side = win_w.min(win_h); // 取短边 | |
| let x_offset = (win_w - side) / 2.0; | |
| let y_offset = (win_h - side) / 2.0; | |
| render_pass.set_viewport(x_offset, y_offset, side, side, 0.0, 1.0); | |
| render_pass.set_pipeline(&self.render_pipeline); | |
| render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); | |
| render_pass.draw(0..6, 0..1); | |
| } | |
| self.queue.submit(iter::once(encoder.finish())); | |
| surface_texture.present(); | |
| Ok(()) | |
| } | |
| } | |
| #[derive(Default)] | |
| struct App { | |
| state: Option<State>, | |
| } | |
| impl ApplicationHandler for App { | |
| fn resumed(&mut self, event_loop: &ActiveEventLoop) { | |
| // Create window object | |
| let window = Arc::new( | |
| event_loop | |
| .create_window(Window::default_attributes()) | |
| .unwrap(), | |
| ); | |
| let state = pollster::block_on(State::new( | |
| event_loop.owned_display_handle(), | |
| window.clone(), | |
| )); | |
| self.state = Some(state); | |
| window.request_redraw(); | |
| } | |
| fn window_event( | |
| &mut self, | |
| event_loop: &ActiveEventLoop, | |
| window_id: WindowId, | |
| event: WindowEvent, | |
| ) { | |
| let state = self.state.as_mut().unwrap(); | |
| match event { | |
| WindowEvent::CloseRequested => event_loop.exit(), | |
| WindowEvent::Resized(physical_size) => state.resize(physical_size), | |
| WindowEvent::RedrawRequested => { | |
| state.update(); | |
| match state.render() { | |
| Ok(_) => {} | |
| Err(wgpu::SurfaceError::Lost) => state.resize(state.size), | |
| Err(wgpu::SurfaceError::OutOfMemory) => event_loop.exit(), | |
| Err(e) => eprintln!("{:?}", e), | |
| } | |
| state.window().request_redraw(); | |
| } | |
| _ => {} | |
| } | |
| } | |
| } | |
| pub fn main() { | |
| unsafe { | |
| env::set_var("RUST_LOG", "info"); | |
| } | |
| env_logger::init(); | |
| let event_loop = EventLoop::new().unwrap(); | |
| event_loop.set_control_flow(ControlFlow::Poll); | |
| let mut app = App::default(); | |
| event_loop.run_app(&mut app).unwrap(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| struct Uniforms { | |
| origin: vec3f, | |
| _p1: f32, | |
| right: vec3f, | |
| _p2: f32, | |
| up: vec3f, | |
| _p3: f32, | |
| forward: vec3f, | |
| _p4: f32, | |
| screen_size: vec2f, | |
| len: f32, | |
| _p5: f32, | |
| }; | |
| @group(0) @binding(0) var<uniform> ui: Uniforms; | |
| struct VertexOutput { | |
| @builtin(position) pos: vec4f, | |
| @location(0) uv: vec2f, | |
| }; | |
| @vertex | |
| fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput { | |
| var pos = array<vec2f, 6>( | |
| vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0), | |
| vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0) | |
| ); | |
| var out: VertexOutput; | |
| out.pos = vec4f(pos[idx], 0.0, 1.0); | |
| out.uv = pos[idx]; | |
| return out; | |
| } | |
| fn kernel(ver: vec3f) -> f32 { | |
| var a = ver; | |
| var b: f32; var c: f32; var d: f32; | |
| for(var i: i32 = 0; i < 5; i++) { | |
| b = length(a); | |
| c = atan2(a.y, a.x) * 8.0; | |
| d = acos(a.z / b) * 8.0; | |
| b = pow(b, 8.0); | |
| a = vec3f(b * sin(d) * cos(c), b * sin(d) * sin(c), b * cos(d)) + ver; | |
| if (b > 6.0) { break; } | |
| } | |
| return 4.0 - dot(a, a); | |
| } | |
| @fragment | |
| fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f { | |
| let M_L = 0.381966; | |
| let M_R = 0.618033; | |
| let step_size = 0.002; | |
| let dir = ui.forward + ui.right * uv.x * ui.screen_size.x + ui.up * uv.y * ui.screen_size.y; | |
| let local_dir = normalize(vec3f(uv.x * ui.screen_size.x, uv.y * ui.screen_size.y, -1.0)); | |
| var v1 = kernel(ui.origin + dir * (step_size * ui.len)); | |
| var v2 = kernel(ui.origin); | |
| var sign = 0; | |
| var r3: f32 = 0.0; | |
| for (var k: i32 = 2; k < 1002; k++) { | |
| let ver = ui.origin + dir * (step_size * ui.len * f32(k)); | |
| let v = kernel(ver); | |
| if (v > 0.0 && v1 < 0.0) { | |
| var r1 = step_size * ui.len * f32(k - 1); | |
| var r2 = step_size * ui.len * f32(k); | |
| for (var l = 0; l < 8; l++) { | |
| r3 = r1 * 0.5 + r2 * 0.5; | |
| if (kernel(ui.origin + dir * r3) > 0.0) { r2 = r3; } else { r1 = r3; } | |
| } | |
| if (r3 < 2.0 * ui.len) { sign = 1; break; } | |
| } | |
| if (v < v1 && v1 > v2 && v1 < 0.0 && (v1 * 2.0 > v || v1 * 2.0 > v2)) { | |
| var r1 = step_size * ui.len * f32(k - 2); | |
| var r2 = step_size * ui.len * (f32(k) - 2.0 + 2.0 * M_L); | |
| var r3_g = step_size * ui.len * (f32(k) - 2.0 + 2.0 * M_R); | |
| var r4 = step_size * ui.len * f32(k); | |
| var m2 = kernel(ui.origin + dir * r2); | |
| var m3 = kernel(ui.origin + dir * r3_g); | |
| for (var l = 0; l < 8; l++) { | |
| if (m2 > m3) { | |
| r4 = r3_g; r3_g = r2; r2 = r4 * M_L + r1 * M_R; | |
| m3 = m2; m2 = kernel(ui.origin + dir * r2); | |
| } else { | |
| r1 = r2; r2 = r3_g; r3_g = r4 * M_R + r1 * M_L; | |
| m2 = m3; m3 = kernel(ui.origin + dir * r3_g); | |
| } | |
| } | |
| let target_r = select(r3_g, r2, m2 > 0.0 || m3 > 0.0); | |
| if (kernel(ui.origin + dir * target_r) > 0.0) { | |
| var ra = step_size * ui.len * f32(k - 2); | |
| var rb = target_r; | |
| for (var l = 0; l < 8; l++) { | |
| r3 = ra * 0.5 + rb * 0.5; | |
| if (kernel(ui.origin + dir * r3) > 0.0) { rb = r3; } else { ra = r3; } | |
| } | |
| if (r3 < 2.0 * ui.len && r3 > step_size * ui.len) { sign = 1; break; } | |
| } | |
| } | |
| v2 = v1; v1 = v; | |
| } | |
| var color = vec3f(0.0); | |
| if (sign == 1) { | |
| let hit_pos = ui.origin + dir * r3; | |
| let r_sq = dot(hit_pos, hit_pos); | |
| let eps = r3 * 0.00025; | |
| var n: vec3f; | |
| n.x = kernel(hit_pos - ui.right * eps) - kernel(hit_pos + ui.right * eps); | |
| n.y = kernel(hit_pos - ui.up * eps) - kernel(hit_pos + ui.up * eps); | |
| n.z = kernel(hit_pos + ui.forward * eps) - kernel(hit_pos - ui.forward * eps); | |
| n = normalize(n); | |
| let reflect_v = reflect(local_dir, n); | |
| let light_dir = vec3f(0.276, 0.920, 0.276); | |
| var spec = pow(max(0.0, dot(reflect_v, light_dir)), 4.0); | |
| let diff = max(0.0, dot(n, light_dir)); | |
| let shade = spec * 0.45 + diff * 0.25 + 0.3; | |
| color = (sin(vec3f(r_sq * 10.0, r_sq * 10.0 + 2.05, r_sq * 10.0 - 2.05)) * 0.5 + 0.5) * shade; | |
| } | |
| // return vec4f(color, 1.0); | |
| return vec4f(pow(color, vec3f(2.2)), 1.0); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment