1use std::num::NonZeroU32;
20use std::ffi::CString;
21use std::time::{Duration, Instant};
22
23use glutin::config::ConfigTemplateBuilder;
24use glutin::context::{ContextApi, ContextAttributesBuilder, NotCurrentGlContext,
25 PossiblyCurrentContext, Version};
26use glutin::display::{GetGlDisplay, GlDisplay};
27use glutin::surface::{GlSurface, Surface, WindowSurface};
28use glutin_winit::{DisplayBuilder, GlWindow};
29use glow::HasContext;
30use raw_window_handle::HasWindowHandle;
31use winit::dpi::LogicalSize;
32use winit::event::{ElementState, Event, MouseButton, MouseScrollDelta, WindowEvent};
33use winit::event_loop::EventLoop;
34use winit::keyboard::{KeyCode, PhysicalKey};
35use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
36use winit::window::Window;
37use glam::{Mat4, Vec2, Vec3};
38use bytemuck::cast_slice;
39
40use crate::config::{EngineConfig, RenderConfig};
41use crate::scene::Scene;
42use crate::render::camera::ProofCamera;
43use crate::render::postfx::PostFxPipeline;
44use crate::input::{InputState, Key};
45use crate::glyph::atlas::FontAtlas;
46use crate::glyph::batch::GlyphInstance;
47use crate::render::ui_layer::UiLayer;
48use crate::render::screen_fx::ScreenFx;
49use crate::particle::gpu_density::{GpuDensityEntityData, GpuDensityRenderer};
50
51const VERT_SRC: &str = r#"
54#version 330 core
55
56layout(location = 0) in vec2 v_pos;
57layout(location = 1) in vec2 v_uv;
58
59layout(location = 2) in vec3 i_position;
60layout(location = 3) in vec2 i_scale;
61layout(location = 4) in float i_rotation;
62layout(location = 5) in vec4 i_color;
63layout(location = 6) in float i_emission;
64layout(location = 7) in vec3 i_glow_color;
65layout(location = 8) in float i_glow_radius;
66layout(location = 9) in vec2 i_uv_offset;
67layout(location = 10) in vec2 i_uv_size;
68// x: 1.0 for a fill (ground, panel) that must not cast shadow.
69layout(location = 11) in vec2 i_flags;
70
71uniform mat4 u_view_proj;
72// Oversampling: each base instance is rendered u_n_copies times.
73// vertex_attrib_divisor is set to u_n_copies so all copies share the same
74// instance attributes. gl_InstanceID / u_n_copies selects the base particle;
75// gl_InstanceID % u_n_copies selects the copy and drives the jitter hash.
76uniform uint u_n_copies;
77
78out vec2 f_uv;
79out vec4 f_color;
80out float f_emission;
81out vec3 f_glow_color;
82out float f_glow_radius;
83out float f_fill;
84
85// Hash matching the CPU hf(seed, v) function — same constants, same bit ops.
86float hf(uint seed, uint v) {
87 uint n = seed * 374761393u + v * 668265263u;
88 n ^= (n >> 13u);
89 n *= 0x5851F42Du;
90 n ^= (n >> 16u);
91 return float(n & 0x00FFFFFFu) / float(0x01000000u);
92}
93
94void main() {
95 float c = cos(i_rotation);
96 float s = sin(i_rotation);
97 vec2 rotated = vec2(
98 v_pos.x * c - v_pos.y * s,
99 v_pos.x * s + v_pos.y * c
100 ) * i_scale;
101
102 // Per-copy position jitter — only active when oversampling.
103 // Seeds mirror the CPU oversampling: seed = real_id*17 + copy_id, slots 30/31.
104 vec3 jitter = vec3(0.0);
105 if (u_n_copies > 1u) {
106 uint real_id = uint(gl_InstanceID) / u_n_copies;
107 uint copy_id = uint(gl_InstanceID) % u_n_copies;
108 uint jseed = real_id * 17u + copy_id;
109 jitter = vec3(
110 (hf(jseed, 30u) - 0.5) * i_scale.x * 0.70,
111 (hf(jseed, 31u) - 0.5) * i_scale.y * 0.70,
112 0.0
113 );
114 }
115
116 gl_Position = u_view_proj * vec4(i_position + jitter + vec3(rotated, 0.0), 1.0);
117 gl_Position.y = -gl_Position.y; // FBO renders upside-down relative to screen
118
119 f_uv = i_uv_offset + v_uv * i_uv_size;
120 // Divide alpha and emission by n_copies so n_copies additive contributions
121 // sum to the same luminance as a single unscaled instance.
122 float inv_n = 1.0 / float(u_n_copies);
123 f_color = vec4(i_color.rgb, i_color.a * inv_n);
124 f_emission = i_emission * inv_n;
125 f_glow_color = i_glow_color;
126 f_glow_radius = i_glow_radius;
127 f_fill = i_flags.x;
128}
129"#;
130
131const FRAG_SRC: &str = r#"
139#version 330 core
140
141in vec2 f_uv;
142in vec4 f_color;
143in float f_emission;
144in vec3 f_glow_color;
145in float f_glow_radius;
146in float f_fill;
147
148uniform sampler2D u_atlas;
149
150layout(location = 0) out vec4 o_color;
151layout(location = 1) out vec4 o_emission;
152// Coverage of matter, for the light map's shadows. Fills write none, and
153// cover whatever was under them.
154layout(location = 2) out vec4 o_occluder;
155
156void main() {
157 float dist = texture(u_atlas, f_uv).r;
158
159 // SDF thresholds (in normalized distance, 0.5 = edge)
160 float edge = 0.5;
161 float softness = 0.08; // edge softness (antialiasing)
162
163 // Core glyph alpha (sharp edge with antialiasing)
164 float alpha = smoothstep(edge - softness, edge + softness, dist);
165
166 // Outline: a band just outside the edge
167 float outline_width = 0.06;
168 float outline_alpha = smoothstep(edge - outline_width - softness, edge - outline_width, dist)
169 * (1.0 - smoothstep(edge - softness * 0.5, edge + softness * 0.5, dist));
170 vec3 outline_color = f_glow_color * 0.6;
171
172 // Glow halo: soft falloff outside the glyph
173 float glow_size = 0.15 + f_glow_radius * 0.05;
174 float glow_alpha = smoothstep(edge - glow_size - 0.1, edge - 0.02, dist) * (1.0 - alpha);
175 glow_alpha *= clamp(f_glow_radius * 0.3, 0.0, 0.5);
176
177 // Drop shadow (offset sample)
178 vec2 shadow_offset = vec2(0.003, -0.004);
179 float shadow_dist = texture(u_atlas, f_uv + shadow_offset).r;
180 float shadow_alpha = smoothstep(edge - softness, edge + softness, shadow_dist) * 0.2;
181
182 // Discard if nothing visible
183 float total_alpha = max(max(alpha, outline_alpha), max(glow_alpha, shadow_alpha));
184 if (total_alpha < 0.01) discard;
185
186 // Composite layers
187 float em = clamp(f_emission * 0.5, 0.0, 1.0);
188 vec3 base_col = mix(f_color.rgb, f_glow_color, em);
189
190 // Shadow (darkest layer)
191 vec3 col = vec3(0.0);
192 float a = shadow_alpha * f_color.a * 0.3;
193
194 // Glow halo (behind outline)
195 col = mix(col, f_glow_color * 0.5, glow_alpha);
196 a = max(a, glow_alpha * f_color.a * 0.4);
197
198 // Outline
199 col = mix(col, outline_color, outline_alpha);
200 a = max(a, outline_alpha * f_color.a * 0.7);
201
202 // Core fill (on top)
203 col = mix(col, base_col, alpha);
204 a = max(a, alpha * f_color.a);
205
206 o_color = vec4(col, a);
207
208 // Emission for bloom
209 float bloom_strength = clamp(f_emission - 0.3, 0.0, 1.0);
210 float glow_boost = clamp(f_glow_radius * 0.15, 0.0, 0.8);
211 float em_alpha = max(alpha, glow_alpha * 0.5) * f_color.a;
212 o_emission = vec4(f_glow_color * (bloom_strength + glow_boost), em_alpha);
213 o_occluder = vec4(alpha * f_color.a * (1.0 - f_fill), 0.0, 0.0, a);
214}
215"#;
216
217#[rustfmt::skip]
221const QUAD_VERTS: [f32; 24] = [
222 -0.5, 0.5, 0.0, 1.0,
223 -0.5, -0.5, 0.0, 0.0,
224 0.5, 0.5, 1.0, 1.0,
225 -0.5, -0.5, 0.0, 0.0,
226 0.5, -0.5, 1.0, 0.0,
227 0.5, 0.5, 1.0, 1.0,
228];
229
230#[derive(Clone, Debug, Default)]
234pub struct FrameStats {
235 pub fps: f32,
237 pub dt: f32,
239 pub glyph_count: usize,
241 pub particle_count: usize,
243 pub draw_calls: u32,
245 pub frame_number: u64,
247}
248
249struct FpsCounter {
251 samples: [f32; 60],
252 head: usize,
253 filled: bool,
254}
255
256impl FpsCounter {
257 fn new() -> Self { Self { samples: [0.016; 60], head: 0, filled: false } }
258
259 fn push(&mut self, dt: f32) {
260 self.samples[self.head] = dt.max(f32::EPSILON);
261 self.head = (self.head + 1) % 60;
262 if self.head == 0 { self.filled = true; }
263 }
264
265 fn fps(&self) -> f32 {
266 let count = if self.filled { 60 } else { self.head.max(1) };
267 let avg_dt: f32 = self.samples[..count].iter().sum::<f32>() / count as f32;
268 1.0 / avg_dt
269 }
270}
271
272#[allow(dead_code)]
280pub struct Pipeline {
281 pub width: u32,
283 pub height: u32,
284 pub stats: FrameStats,
285 running: bool,
286
287 render_config: RenderConfig,
289
290 event_loop: EventLoop<()>,
292 window: Window,
293 surface: Surface<WindowSurface>,
294 context: PossiblyCurrentContext,
295
296 gl: glow::Context,
298 program: glow::Program,
299 vao: glow::VertexArray,
300 quad_vbo: glow::Buffer,
301 instance_vbo: glow::Buffer,
302 atlas_tex: glow::Texture,
303 loc_view_proj: glow::UniformLocation,
304 loc_n_copies: Option<glow::UniformLocation>,
305
306 postfx: PostFxPipeline,
308
309 atlas: FontAtlas,
311
312 ui_renderer: super::ui_layer_renderer::UiLayerRenderer,
316 ui_prepared: bool,
319
320 density: Option<GpuDensityRenderer>,
323 density_entities: Vec<GpuDensityEntityData>,
324 density_budget: u32,
325
326 pub svogi: crate::svogi::integration::CascadedSvogi,
328
329 pub fog: crate::volumetric_fog::VolumetricFogPipeline,
331
332 instances: Vec<GlyphInstance>,
334
335 fps_counter: FpsCounter,
337 frame_start: Instant,
338 scene_time: f32,
339
340 mouse_pos: Vec2,
342 mouse_pos_prev: Vec2,
343 mouse_ndc: Vec2,
345
346 pub raw_window_events: Vec<winit::event::WindowEvent>,
349}
350
351impl Pipeline {
352 pub fn init(config: &EngineConfig) -> Self {
354 let event_loop = EventLoop::new().expect("EventLoop::new");
356
357 let window_attrs = Window::default_attributes()
359 .with_title(&config.window_title)
360 .with_inner_size(LogicalSize::new(config.window_width, config.window_height))
361 .with_resizable(true)
362 .with_visible(!crate::capture::hidden_window())
365 .with_active(!crate::capture::hidden_window());
366
367 let template = ConfigTemplateBuilder::new()
369 .with_alpha_size(8)
370 .with_depth_size(0);
371
372 let display_builder = DisplayBuilder::new()
373 .with_window_attributes(Some(window_attrs));
374
375 let (window, gl_config) = display_builder
376 .build(&event_loop, template, |mut configs| {
377 configs.next().expect("no suitable GL config found")
378 })
379 .expect("DisplayBuilder::build failed");
380
381 let window = window.expect("window was not created");
382 let display = gl_config.display();
383
384 let raw_handle = window.window_handle().unwrap().as_raw();
386 let ctx_attrs_43 = ContextAttributesBuilder::new()
387 .with_context_api(ContextApi::OpenGl(Some(Version::new(4, 3))))
388 .build(Some(raw_handle));
389 let ctx_attrs_33 = ContextAttributesBuilder::new()
390 .with_context_api(ContextApi::OpenGl(Some(Version::new(3, 3))))
391 .build(Some(raw_handle));
392
393 let not_current = unsafe {
394 display.create_context(&gl_config, &ctx_attrs_43)
395 .unwrap_or_else(|_| {
396 display.create_context(&gl_config, &ctx_attrs_33)
397 .expect("create_context failed (both GL 4.3 and 3.3)")
398 })
399 };
400
401 let size = window.inner_size();
403 let w = size.width.max(1);
404 let h = size.height.max(1);
405
406 let surface_attrs = window
407 .build_surface_attributes(Default::default())
408 .expect("build_surface_attributes failed");
409
410 let surface = unsafe {
411 display.create_window_surface(&gl_config, &surface_attrs)
412 .expect("create_window_surface failed")
413 };
414
415 let context = not_current.make_current(&surface)
417 .expect("make_current failed");
418 if config.render.vsync {
419 use glutin::surface::SwapInterval;
420 if let Err(e) = surface.set_swap_interval(
421 &context,
422 SwapInterval::Wait(NonZeroU32::new(1).unwrap()),
423 ) {
424 log::warn!("vsync unavailable: {e}");
425 }
426 }
427
428 let gl = unsafe {
430 glow::Context::from_loader_function(|sym| {
431 let sym_c = CString::new(sym).unwrap();
432 display.get_proc_address(sym_c.as_c_str()) as *const _
433 })
434 };
435
436 let program = unsafe { compile_program(&gl, VERT_SRC, FRAG_SRC) };
438 let loc_view_proj = unsafe {
439 gl.get_uniform_location(program, "u_view_proj")
440 .expect("uniform u_view_proj not found")
441 };
442 let loc_n_copies = unsafe {
443 gl.get_uniform_location(program, "u_n_copies")
444 };
445 unsafe {
446 gl.use_program(Some(program));
447 if let Some(loc) = gl.get_uniform_location(program, "u_atlas") {
448 gl.uniform_1_i32(Some(&loc), 0);
449 }
450 if let Some(ref loc) = loc_n_copies {
452 gl.uniform_1_u32(Some(loc), 1u32);
453 }
454 }
455
456 let (vao, quad_vbo, instance_vbo) = unsafe { setup_vao(&gl) };
458
459 let atlas = FontAtlas::build(config.render.font_size as f32);
461 let atlas_tex = unsafe { upload_atlas(&gl, &atlas) };
462
463 let postfx = unsafe { PostFxPipeline::new(&gl, w, h, config.render.render_scale) };
465
466 unsafe {
468 gl.enable(glow::BLEND);
469 gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
470 gl.clear_color(0.02, 0.02, 0.05, 1.0);
471 gl.viewport(0, 0, w as i32, h as i32);
472 }
473
474 log::info!(
475 "Pipeline ready — {}×{} — font atlas {}×{} ({} chars) — PostFxPipeline wired",
476 w, h, atlas.width, atlas.height, atlas.uvs.len()
477 );
478
479 Self {
480 width: w, height: h,
481 stats: FrameStats::default(),
482 running: true,
483 render_config: config.render.clone(),
484 event_loop, window, surface, context,
485 gl, program, vao, quad_vbo, instance_vbo, atlas_tex, loc_view_proj, loc_n_copies,
486 postfx,
487 atlas,
488 ui_renderer: super::ui_layer_renderer::UiLayerRenderer::new(),
489 ui_prepared: false,
490 density: None,
491 density_entities: Vec::new(),
492 density_budget: 0,
493 instances: Vec::with_capacity(8192),
494 fps_counter: FpsCounter::new(),
495 frame_start: Instant::now(),
496 scene_time: 0.0,
497 mouse_pos: Vec2::ZERO,
498 mouse_pos_prev: Vec2::ZERO,
499 mouse_ndc: Vec2::ZERO,
500 raw_window_events: Vec::new(),
501 svogi: crate::svogi::integration::CascadedSvogi::new(3, 64, 50.0),
502 fog: crate::volumetric_fog::VolumetricFogPipeline::new(
503 crate::volumetric_fog::FogPresets::combat()
504 ),
505 }
506 }
507
508 pub fn set_density_entities(&mut self, entities: &[GpuDensityEntityData], budget: u32) {
512 self.density_entities.clear();
513 self.density_entities.extend_from_slice(entities);
514 self.density_budget = budget;
515 if !self.density_entities.is_empty() && self.density.is_none() {
516 self.density = Some(unsafe { GpuDensityRenderer::new(&self.gl) });
517 }
518 }
519
520 pub fn update_render_config(&mut self, config: &RenderConfig) {
523 let old_scale = self.render_config.render_scale;
524 self.render_config = config.clone();
525 if (config.render_scale - old_scale).abs() > 1e-4 {
527 unsafe { self.postfx.resize(&self.gl, self.width, self.height, config.render_scale); }
528 }
529 }
530
531 pub fn poll_events(&mut self, input: &mut InputState) -> bool {
533 input.clear_frame();
534 self.mouse_pos_prev = self.mouse_pos;
535
536 let mut should_exit = false;
537 let mut resize: Option<(u32, u32)> = None;
538 let mut key_events: Vec<(KeyCode, bool)> = Vec::new();
539 let mut mouse_moved: Option<(f64, f64)> = None;
540 let mut mouse_buttons: Vec<(MouseButton, bool)> = Vec::new();
541 let mut scroll_delta: f32 = 0.0;
542
543 self.raw_window_events.clear();
544
545 #[allow(deprecated)]
546 let status = self.event_loop.pump_events(Some(Duration::ZERO), |event, elwt| {
547 match event {
548 Event::WindowEvent { event: we, .. } => match we {
549 WindowEvent::CloseRequested => {
550 should_exit = true;
551 elwt.exit();
552 }
553 WindowEvent::Resized(s) => {
554 resize = Some((s.width, s.height));
555 }
556 WindowEvent::KeyboardInput { event: key_ev, .. } => {
557 if let PhysicalKey::Code(kc) = key_ev.physical_key {
558 let pressed = key_ev.state == ElementState::Pressed;
559 key_events.push((kc, pressed));
560 }
561 }
562 WindowEvent::CursorMoved { position, .. } => {
563 mouse_moved = Some((position.x, position.y));
564 }
565 WindowEvent::MouseInput { button, state, .. } => {
566 let pressed = state == ElementState::Pressed;
567 mouse_buttons.push((button, pressed));
568 }
569 WindowEvent::MouseWheel { delta, .. } => {
570 scroll_delta += match delta {
571 MouseScrollDelta::LineDelta(_, y) => y,
572 MouseScrollDelta::PixelDelta(d) => d.y as f32 / 40.0,
573 };
574 }
575 _ => {}
576 }
577 _ => {}
578 }
579 });
580
581 if let Some((w, h)) = resize {
583 if w > 0 && h > 0 {
584 self.surface.resize(
585 &self.context,
586 NonZeroU32::new(w).unwrap(),
587 NonZeroU32::new(h).unwrap(),
588 );
589 unsafe { self.gl.viewport(0, 0, w as i32, h as i32); }
590 self.width = w;
591 self.height = h;
592 input.window_resized = Some((w, h));
593 unsafe { self.postfx.resize(&self.gl, w, h, self.render_config.render_scale); }
594 }
595 }
596
597 for (kc, pressed) in key_events {
599 if let Some(key) = keycode_to_engine(kc) {
600 if pressed {
601 input.keys_pressed.insert(key);
602 input.keys_just_pressed.insert(key);
603 } else {
604 input.keys_pressed.remove(&key);
605 input.keys_just_released.insert(key);
606 }
607 }
608 }
609
610 if let Some((x, y)) = mouse_moved {
612 self.mouse_pos = Vec2::new(x as f32, y as f32);
613 input.mouse_x = x as f32;
614 input.mouse_y = y as f32;
615 let w = self.width.max(1) as f32;
617 let h = self.height.max(1) as f32;
618 self.mouse_ndc = Vec2::new(
619 (x as f32 / w) * 2.0 - 1.0,
620 1.0 - (y as f32 / h) * 2.0,
621 );
622 input.mouse_ndc = self.mouse_ndc;
623 input.mouse_delta = self.mouse_pos - self.mouse_pos_prev;
624 }
625
626 for (button, pressed) in mouse_buttons {
627 match button {
628 MouseButton::Left => {
629 if pressed { input.mouse_left_just_pressed = true; }
630 else { input.mouse_left_just_released = true; }
631 input.mouse_left = pressed;
632 }
633 MouseButton::Right => {
634 if pressed { input.mouse_right_just_pressed = true; }
635 else { input.mouse_right_just_released = true; }
636 input.mouse_right = pressed;
637 }
638 MouseButton::Middle => {
639 if pressed { input.mouse_middle_just_pressed = true; }
640 input.mouse_middle = pressed;
641 }
642 _ => {}
643 }
644 }
645
646 input.scroll_delta = scroll_delta;
647
648 if should_exit || matches!(status, PumpStatus::Exit(_)) {
650 self.running = false;
651 }
652 self.running
653 }
654
655 pub fn render(&mut self, scene: &Scene, camera: &ProofCamera) {
658 let fx = ScreenFx::default();
659 self.render_frame(scene, camera, None, &fx);
660 }
661
662 pub fn render_frame(
670 &mut self,
671 scene: &Scene,
672 camera: &ProofCamera,
673 ui: Option<&UiLayer>,
674 fx: &ScreenFx,
675 ) {
676 let now = Instant::now();
678 let dt = now.duration_since(self.frame_start).as_secs_f32().min(0.1);
679 self.frame_start = now;
680 self.scene_time = scene.time;
681
682 self.fps_counter.push(dt);
683 self.stats.fps = self.fps_counter.fps();
684 self.stats.dt = dt;
685 self.stats.frame_number += 1;
686
687 let pos = camera.position.position();
689 let tgt = camera.target.position();
690 let fov = camera.fov.position;
691 let aspect = if self.height > 0 { self.width as f32 / self.height as f32 } else { 1.0 };
692 let view = Mat4::look_at_rh(pos, tgt, Vec3::Y);
693 let proj = Mat4::perspective_rh_gl(fov.to_radians(), aspect, camera.near, camera.far);
694 let view_proj = proj * view;
695
696 if self.render_config.global_illumination {
698 use crate::svogi::inject::{LightSource, DirectionalLight, PointLight};
699 let sun = LightSource::Directional(DirectionalLight {
700 direction: Vec3::new(-0.5, 1.0, 0.8).normalize(),
701 color: Vec3::new(0.8, 0.75, 0.65),
702 intensity: 1.0,
703 });
704 let mut all_lights = vec![sun];
705 let mut emissive_count = 0;
707 for (_, glyph) in scene.glyphs.iter() {
708 if glyph.emission > 0.5 && emissive_count < 50 {
709 all_lights.push(LightSource::Point(PointLight {
710 position: glyph.position,
711 color: glyph.glow_color,
712 intensity: glyph.emission * 0.3,
713 radius: glyph.glow_radius * 2.0,
714 }));
715 emissive_count += 1;
716 }
717 }
718 self.svogi.update(dt, &[], &all_lights, &[]);
719 }
720
721 if self.render_config.volumetric_fog {
723 use crate::volumetric_fog::FogLight;
724 let inv_vp = view_proj.inverse();
725 let fog_lights = vec![
726 FogLight::Directional {
727 direction: Vec3::new(-0.3, -0.8, -0.5).normalize(),
728 color: Vec3::new(0.6, 0.55, 0.5),
729 intensity: 0.5,
730 },
731 ];
732 self.fog.update(dt, &inv_vp, pos, &fog_lights, &[]);
733 }
734
735 self.instances.clear();
737 let mut glyph_count = 0;
738 let mut particle_count = 0;
739
740 for (_, glyph) in scene.glyphs.iter() {
742 if !glyph.visible { continue; }
743 let life_scale = if let Some(ref f) = glyph.life_function {
744 f.evaluate(scene.time, 0.0)
745 } else {
746 1.0
747 };
748 let uv = self.atlas.uv_for(glyph.character);
749
750 let dist = (glyph.position - pos).length();
752 let fog_density = 0.003; let fog_factor = (-dist * fog_density).exp(); let fog_color = [0.03f32, 0.04, 0.06]; let mut color = glyph.color.to_array();
756 color[0] = color[0] * fog_factor + fog_color[0] * (1.0 - fog_factor);
757 color[1] = color[1] * fog_factor + fog_color[1] * (1.0 - fog_factor);
758 color[2] = color[2] * fog_factor + fog_color[2] * (1.0 - fog_factor);
759 color[3] *= fog_factor.max(0.1); let emission_fogged = glyph.emission * fog_factor;
761
762 self.instances.push(GlyphInstance {
763 position: glyph.position.to_array(),
764 scale: [glyph.scale.x * life_scale, glyph.scale.y * life_scale],
765 rotation: glyph.rotation,
766 color,
767 emission: emission_fogged,
768 glow_color: glyph.glow_color.to_array(),
769 glow_radius: glyph.glow_radius * fog_factor,
770 uv_offset: uv.offset(),
771 uv_size: uv.size(),
772 _pad: [0.0; 2],
773 });
774 glyph_count += 1;
775 }
776
777 for particle in scene.particles.iter() {
778 let g = &particle.glyph;
779 if !g.visible { continue; }
780 let uv = self.atlas.uv_for(g.character);
781 let dist = (g.position - pos).length();
783 let fog_factor = (-dist * 0.003).exp();
784 let fog_color = [0.03f32, 0.04, 0.06];
785 let mut color = g.color.to_array();
786 color[0] = color[0] * fog_factor + fog_color[0] * (1.0 - fog_factor);
787 color[1] = color[1] * fog_factor + fog_color[1] * (1.0 - fog_factor);
788 color[2] = color[2] * fog_factor + fog_color[2] * (1.0 - fog_factor);
789 color[3] *= fog_factor.max(0.1);
790
791 self.instances.push(GlyphInstance {
792 position: g.position.to_array(),
793 scale: [g.scale.x, g.scale.y],
794 rotation: g.rotation,
795 color,
796 emission: g.emission * fog_factor,
797 glow_color: g.glow_color.to_array(),
798 glow_radius: g.glow_radius * fog_factor,
799 uv_offset: uv.offset(),
800 uv_size: uv.size(),
801 _pad: [0.0; 2],
802 });
803 particle_count += 1;
804 }
805
806 self.stats.glyph_count = glyph_count;
807 self.stats.particle_count = particle_count;
808 self.stats.draw_calls = 0;
809
810 let mut world_proj = None;
816 if let Some(ui) = ui {
817 self.ui_renderer.build_instances(ui, &self.atlas);
818 self.ui_prepared = true;
819 if self.render_config.world_ui_in_scene && self.ui_renderer.world_count() > 0 {
820 let trauma = camera.shake.trauma.clamp(0.0, 1.0);
821 let amp = trauma * trauma * self.render_config.shake_pixels;
822 let t = self.scene_time;
823 let shake = Vec3::new((t * 47.3).sin() * amp, (t * 31.7).cos() * amp, 0.0);
824 world_proj = Some(ui.world_projection() * Mat4::from_translation(shake));
825 }
826 }
827
828 unsafe { self.execute_render_passes(view_proj, world_proj, fx); }
830 }
831
832 pub fn render_ui(&mut self, ui: &super::ui_layer::UiLayer) {
840 let prepared = std::mem::replace(&mut self.ui_prepared, false);
841 if ui.command_count() == 0 {
842 return;
843 }
844 if !prepared {
845 self.ui_renderer.build_instances(ui, &self.atlas);
846 }
847 let proj = ui.projection();
848 if !self.render_config.world_ui_in_scene && self.ui_renderer.world_count() > 0 {
851 unsafe { self.draw_ui_pass(proj, true) };
852 self.stats.draw_calls += 1;
853 }
854 if self.ui_renderer.glyph_count() == 0 {
855 return;
856 }
857 unsafe { self.draw_ui_pass(proj, false) };
858 self.stats.draw_calls += 1;
859 }
860
861 unsafe fn draw_ui_pass(&mut self, proj: Mat4, world: bool) {
865 let gl = &self.gl;
866 let (bytes, count) = if world {
867 (self.ui_renderer.world_bytes(), self.ui_renderer.world_count())
868 } else {
869 (self.ui_renderer.glyph_bytes(), self.ui_renderer.glyph_count())
870 };
871
872 gl.bind_framebuffer(glow::FRAMEBUFFER, None);
874 gl.viewport(0, 0, self.width as i32, self.height as i32);
875
876 gl.disable(glow::DEPTH_TEST);
879 gl.enable(glow::BLEND);
880 gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
881
882 gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_vbo));
883 gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, bytes, glow::DYNAMIC_DRAW);
884
885 gl.use_program(Some(self.program));
886 gl.uniform_matrix_4_f32_slice(Some(&self.loc_view_proj), false, &proj.to_cols_array());
887 gl.uniform_1_u32(self.loc_n_copies.as_ref(), 1);
890 gl.active_texture(glow::TEXTURE0);
891 gl.bind_texture(glow::TEXTURE_2D, Some(self.atlas_tex));
892 gl.bind_vertex_array(Some(self.vao));
893 for loc in 2u32..=11 {
894 gl.vertex_attrib_divisor(loc, 1);
895 }
896 gl.draw_arrays_instanced(glow::TRIANGLES, 0, 6, count as i32);
897
898 gl.enable(glow::DEPTH_TEST);
900 }
901
902 pub fn swap(&mut self) -> bool {
904 if let Err(e) = self.surface.swap_buffers(&self.context) {
905 log::error!("swap_buffers failed: {e}");
906 self.running = false;
907 }
908 self.running
909 }
910
911 pub fn gl(&self) -> &glow::Context {
916 &self.gl
917 }
918
919 pub fn window(&self) -> &Window {
921 &self.window
922 }
923
924 pub fn render_size(&self) -> (u32, u32) {
930 (self.width, self.height)
931 }
932
933 pub fn read_frame(&self) -> (u32, u32, Vec<u8>) {
939 let (w, h) = (self.width, self.height);
940 let mut buf = vec![0u8; (w as usize) * (h as usize) * 4];
941 unsafe {
942 let gl = &self.gl;
943 gl.bind_framebuffer(glow::FRAMEBUFFER, None);
944 gl.read_buffer(glow::BACK);
945 gl.pixel_store_i32(glow::PACK_ALIGNMENT, 1);
946 gl.read_pixels(
947 0,
948 0,
949 w as i32,
950 h as i32,
951 glow::RGBA,
952 glow::UNSIGNED_BYTE,
953 glow::PixelPackData::Slice(Some(&mut buf)),
954 );
955 }
956 (w, h, buf)
957 }
958
959 pub fn window_size(&self) -> (u32, u32) {
961 let size = self.window.inner_size();
962 (size.width, size.height)
963 }
964
965 unsafe fn execute_render_passes(
968 &mut self,
969 view_proj: Mat4,
970 world_proj: Option<Mat4>,
971 fx: &ScreenFx,
972 ) {
973 let gl = &self.gl;
974
975 let (sw, sh) = self.postfx.scene_size();
981 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.postfx.scene_fbo));
982 gl.viewport(0, 0, sw as i32, sh as i32);
983 gl.clear_color(0.02, 0.025, 0.04, 1.0);
984 gl.clear(glow::COLOR_BUFFER_BIT);
985 gl.clear_buffer_f32_slice(glow::COLOR, 2, &[0.0, 0.0, 0.0, 0.0]);
987 gl.enable(glow::BLEND);
988 gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
989
990 if !self.instances.is_empty() {
993 gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_vbo));
995 gl.buffer_data_u8_slice(
996 glow::ARRAY_BUFFER,
997 cast_slice(self.instances.as_slice()),
998 glow::DYNAMIC_DRAW,
999 );
1000
1001 let n_copies = self.render_config.particle_multiplier
1006 .ceil().max(1.0) as u32;
1007
1008 gl.use_program(Some(self.program));
1009 gl.uniform_matrix_4_f32_slice(
1010 Some(&self.loc_view_proj),
1011 false,
1012 &view_proj.to_cols_array(),
1013 );
1014 gl.uniform_1_u32(self.loc_n_copies.as_ref(), n_copies);
1015 gl.active_texture(glow::TEXTURE0);
1016 gl.bind_texture(glow::TEXTURE_2D, Some(self.atlas_tex));
1017 gl.bind_vertex_array(Some(self.vao));
1018 for loc in 2u32..=11 {
1021 gl.vertex_attrib_divisor(loc, n_copies);
1022 }
1023 gl.draw_arrays_instanced(
1024 glow::TRIANGLES, 0, 6,
1025 (self.instances.len() as u32 * n_copies) as i32,
1026 );
1027 self.stats.draw_calls += 1;
1028 }
1029
1030 if let Some(ref mut density) = self.density {
1036 if !self.density_entities.is_empty() {
1037 let draws = density.draw(
1038 gl,
1039 &self.density_entities,
1040 self.density_budget,
1041 &view_proj,
1042 (sw, sh),
1043 self.scene_time,
1044 );
1045 self.stats.draw_calls += draws;
1046 self.stats.particle_count += density.drawn as usize;
1047 }
1048 }
1049
1050 if let Some(wp) = world_proj {
1056 let count = self.ui_renderer.world_count();
1057 if count > 0 {
1058 gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_vbo));
1059 gl.buffer_data_u8_slice(
1060 glow::ARRAY_BUFFER,
1061 self.ui_renderer.world_bytes(),
1062 glow::DYNAMIC_DRAW,
1063 );
1064 gl.use_program(Some(self.program));
1065 gl.uniform_matrix_4_f32_slice(
1066 Some(&self.loc_view_proj),
1067 false,
1068 &wp.to_cols_array(),
1069 );
1070 gl.uniform_1_u32(self.loc_n_copies.as_ref(), 1);
1073 gl.active_texture(glow::TEXTURE0);
1074 gl.bind_texture(glow::TEXTURE_2D, Some(self.atlas_tex));
1075 gl.bind_vertex_array(Some(self.vao));
1076 for loc in 2u32..=11 {
1077 gl.vertex_attrib_divisor(loc, 1);
1078 }
1079 gl.draw_arrays_instanced(glow::TRIANGLES, 0, 6, count as i32);
1080 self.stats.draw_calls += 1;
1081 }
1082 }
1083
1084 let draws = self.postfx.run(
1086 gl, &self.render_config, fx, self.width, self.height, self.scene_time,
1087 );
1088 self.stats.draw_calls += draws;
1089 }
1090}
1091
1092unsafe fn compile_program(gl: &glow::Context, vert_src: &str, frag_src: &str) -> glow::Program {
1096 let vs = gl.create_shader(glow::VERTEX_SHADER).expect("create vertex shader");
1097 gl.shader_source(vs, vert_src);
1098 gl.compile_shader(vs);
1099 if !gl.get_shader_compile_status(vs) {
1100 let log = gl.get_shader_info_log(vs);
1101 panic!("Vertex shader compile error:\n{log}");
1102 }
1103
1104 let fs = gl.create_shader(glow::FRAGMENT_SHADER).expect("create fragment shader");
1105 gl.shader_source(fs, frag_src);
1106 gl.compile_shader(fs);
1107 if !gl.get_shader_compile_status(fs) {
1108 let log = gl.get_shader_info_log(fs);
1109 panic!("Fragment shader compile error:\n{log}");
1110 }
1111
1112 let prog = gl.create_program().expect("create shader program");
1113 gl.attach_shader(prog, vs);
1114 gl.attach_shader(prog, fs);
1115 gl.link_program(prog);
1116 if !gl.get_program_link_status(prog) {
1117 let log = gl.get_program_info_log(prog);
1118 panic!("Shader link error:\n{log}");
1119 }
1120
1121 gl.detach_shader(prog, vs);
1122 gl.detach_shader(prog, fs);
1123 gl.delete_shader(vs);
1124 gl.delete_shader(fs);
1125 prog
1126}
1127
1128unsafe fn setup_vao(gl: &glow::Context) -> (glow::VertexArray, glow::Buffer, glow::Buffer) {
1130 let vao = gl.create_vertex_array().expect("create vao");
1131 gl.bind_vertex_array(Some(vao));
1132
1133 let quad_vbo = gl.create_buffer().expect("create quad_vbo");
1135 gl.bind_buffer(glow::ARRAY_BUFFER, Some(quad_vbo));
1136 gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, cast_slice(&QUAD_VERTS), glow::STATIC_DRAW);
1137 gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 16, 0);
1139 gl.enable_vertex_attrib_array(0);
1140 gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 16, 8);
1142 gl.enable_vertex_attrib_array(1);
1143
1144 let instance_vbo = gl.create_buffer().expect("create instance_vbo");
1146 gl.bind_buffer(glow::ARRAY_BUFFER, Some(instance_vbo));
1147
1148 let stride = std::mem::size_of::<GlyphInstance>() as i32;
1149
1150 macro_rules! inst_attr {
1152 ($loc:expr, $count:expr, $off:expr) => {{
1153 gl.vertex_attrib_pointer_f32($loc, $count, glow::FLOAT, false, stride, $off);
1154 gl.enable_vertex_attrib_array($loc);
1155 gl.vertex_attrib_divisor($loc, 1); }};
1157 }
1158
1159 inst_attr!(2, 3, 0); inst_attr!(3, 2, 12); inst_attr!(4, 1, 20); inst_attr!(5, 4, 24); inst_attr!(6, 1, 40); inst_attr!(7, 3, 44); inst_attr!(8, 1, 56); inst_attr!(9, 2, 60); inst_attr!(10, 2, 68); inst_attr!(11, 2, 76); (vao, quad_vbo, instance_vbo)
1171}
1172
1173unsafe fn upload_atlas(gl: &glow::Context, atlas: &FontAtlas) -> glow::Texture {
1175 let tex = gl.create_texture().expect("create atlas texture");
1176 gl.bind_texture(glow::TEXTURE_2D, Some(tex));
1177 gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
1178 gl.tex_image_2d(
1179 glow::TEXTURE_2D, 0, glow::R8 as i32,
1180 atlas.width as i32, atlas.height as i32,
1181 0, glow::RED, glow::UNSIGNED_BYTE,
1182 glow::PixelUnpackData::Slice(Some(&atlas.pixels)),
1183 );
1184 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
1185 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
1186 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
1187 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
1188 tex
1189}
1190
1191fn keycode_to_engine(kc: KeyCode) -> Option<Key> {
1195 Some(match kc {
1196 KeyCode::KeyA => Key::A, KeyCode::KeyB => Key::B, KeyCode::KeyC => Key::C,
1197 KeyCode::KeyD => Key::D, KeyCode::KeyE => Key::E, KeyCode::KeyF => Key::F,
1198 KeyCode::KeyG => Key::G, KeyCode::KeyH => Key::H, KeyCode::KeyI => Key::I,
1199 KeyCode::KeyJ => Key::J, KeyCode::KeyK => Key::K, KeyCode::KeyL => Key::L,
1200 KeyCode::KeyM => Key::M, KeyCode::KeyN => Key::N, KeyCode::KeyO => Key::O,
1201 KeyCode::KeyP => Key::P, KeyCode::KeyQ => Key::Q, KeyCode::KeyR => Key::R,
1202 KeyCode::KeyS => Key::S, KeyCode::KeyT => Key::T, KeyCode::KeyU => Key::U,
1203 KeyCode::KeyV => Key::V, KeyCode::KeyW => Key::W, KeyCode::KeyX => Key::X,
1204 KeyCode::KeyY => Key::Y, KeyCode::KeyZ => Key::Z,
1205 KeyCode::Digit1 => Key::Num1, KeyCode::Digit2 => Key::Num2,
1206 KeyCode::Digit3 => Key::Num3, KeyCode::Digit4 => Key::Num4,
1207 KeyCode::Digit5 => Key::Num5, KeyCode::Digit6 => Key::Num6,
1208 KeyCode::Digit7 => Key::Num7, KeyCode::Digit8 => Key::Num8,
1209 KeyCode::Digit9 => Key::Num9, KeyCode::Digit0 => Key::Num0,
1210 KeyCode::ArrowUp => Key::Up, KeyCode::ArrowDown => Key::Down,
1211 KeyCode::ArrowLeft => Key::Left, KeyCode::ArrowRight => Key::Right,
1212 KeyCode::Enter | KeyCode::NumpadEnter => Key::Enter,
1213 KeyCode::Escape => Key::Escape,
1214 KeyCode::Space => Key::Space,
1215 KeyCode::Backspace => Key::Backspace,
1216 KeyCode::Tab => Key::Tab,
1217 KeyCode::ShiftLeft => Key::LShift, KeyCode::ShiftRight => Key::RShift,
1218 KeyCode::ControlLeft => Key::LCtrl, KeyCode::ControlRight => Key::RCtrl,
1219 KeyCode::AltLeft => Key::LAlt, KeyCode::AltRight => Key::RAlt,
1220 KeyCode::F1 => Key::F1, KeyCode::F2 => Key::F2, KeyCode::F3 => Key::F3,
1221 KeyCode::F4 => Key::F4, KeyCode::F5 => Key::F5, KeyCode::F6 => Key::F6,
1222 KeyCode::F7 => Key::F7, KeyCode::F8 => Key::F8, KeyCode::F9 => Key::F9,
1223 KeyCode::F10 => Key::F10, KeyCode::F11 => Key::F11, KeyCode::F12 => Key::F12,
1224 KeyCode::Slash => Key::Slash,
1225 KeyCode::Backslash => Key::Backslash,
1226 KeyCode::Period => Key::Period,
1227 KeyCode::Comma => Key::Comma,
1228 KeyCode::Semicolon => Key::Semicolon,
1229 KeyCode::Quote => Key::Quote,
1230 KeyCode::BracketLeft => Key::LBracket,
1231 KeyCode::BracketRight => Key::RBracket,
1232 KeyCode::Minus => Key::Minus,
1233 KeyCode::Equal => Key::Equals,
1234 KeyCode::Backquote => Key::Backtick,
1235 KeyCode::PageUp => Key::PageUp,
1236 KeyCode::PageDown => Key::PageDown,
1237 KeyCode::Home => Key::Home,
1238 KeyCode::End => Key::End,
1239 KeyCode::Insert => Key::Insert,
1240 KeyCode::Delete => Key::Delete,
1241 _ => return None,
1242 })
1243}