BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

preview_gl.rs 32.5 KiB Download Raw
1//! OpenGL viewport for a skinned glTF/GLB or FBX body, a static (unskinned) OBJ/glTF/FBX body, and rigid textured equipment.
2//!
3//! The shaded mesh is a placement preview. It is not the unlit paperdoll
4//! diffuse/depth/normal export.
5
6use std::sync::{Arc, Mutex};
7
8use eframe::egui;
9use egui_glow::CallbackFn;
10use glow::HasContext;
11
12use crate::preview::{
13 clip_duration, eye_offset, mul, perspective, sample_skin_matrices, view_matrix,
14 PreviewCharacter, PreviewControls, PAPERDOLL_PITCHES_DEG, PAPERDOLL_YAW_COUNT,
15};
16
17/// egui itself requests no depth buffer. The custom 3D callback needs a real
18/// 24-bit attachment in the native Glow render target.
19pub fn native_options() -> eframe::NativeOptions {
20 eframe::NativeOptions {
21 depth_buffer: 24,
22 renderer: eframe::Renderer::Glow,
23 ..Default::default()
24 }
25}
26
27const MAX_BONES: usize = crate::preview::MAX_JOINTS;
28
29pub struct Viewport {
30 cache: Arc<Mutex<GpuCache>>,
31}
32
33impl Default for Viewport {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl Viewport {
40 pub fn new() -> Self {
41 Self {
42 cache: Arc::new(Mutex::new(GpuCache::default())),
43 }
44 }
45
46 pub fn destroy(&self, gl: &glow::Context) {
47 if let Ok(mut cache) = self.cache.lock() {
48 unsafe { cache.destroy(gl) }
49 }
50 }
51
52 pub fn show(
53 &self,
54 ui: &mut egui::Ui,
55 controls: &mut PreviewControls,
56 character: Option<&PreviewCharacter>,
57 presets: &[&crate::equipment::Preset],
58 ) {
59 self.show_framed(ui, controls, character, presets, None);
60 }
61
62 pub fn show_framed(
63 &self,
64 ui: &mut egui::Ui,
65 controls: &mut PreviewControls,
66 character: Option<&PreviewCharacter>,
67 presets: &[&crate::equipment::Preset],
68 framing: Option<(crate::project::CameraFraming, [u32; 2])>,
69 ) {
70 let framing = framing
71 .filter(|(c, r)| c.validate().is_ok() && r[0] > 0 && r[1] > 0)
72 .map(|(c, r)| {
73 (
74 character
75 .map(|body| crate::camera::resolve(body, c).framing)
76 .unwrap_or(c),
77 r,
78 )
79 });
80 ui.horizontal_wrapped(|ui| {
81 if let Some(character) = character {
82 if !character
83 .clips
84 .iter()
85 .any(|clip| clip.name == controls.clip_name)
86 {
87 if let Some(clip) = character.clips.first() {
88 controls.select_clip(&clip.name);
89 }
90 }
91 for clip in &character.clips {
92 if ui
93 .selectable_label(controls.clip_name == clip.name, &clip.name)
94 .clicked()
95 {
96 controls.select_clip(&clip.name);
97 }
98 }
99 if character.clips.is_empty() {
100 let why = if character.is_static() {
101 "Geen animaties: statisch lichaam zonder skeleton (bindpose)"
102 } else {
103 "Geen animaties: bindpose"
104 };
105 ui.add_enabled(false, egui::Button::new("Animatie"))
106 .on_disabled_hover_text(why);
107 ui.label(why);
108 }
109 }
110 let animated = character.is_some_and(|c| !c.clips.is_empty());
111 if ui
112 .add_enabled(
113 animated,
114 egui::SelectableLabel::new(
115 controls.playing && animated,
116 if controls.playing { "Pauze" } else { "Speel" },
117 ),
118 )
119 .clicked()
120 {
121 controls.playing = !controls.playing;
122 }
123 if ui.button("Yaw −").clicked() {
124 controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 - 1);
125 }
126 if ui.button("Yaw +").clicked() {
127 controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 + 1);
128 }
129 for (index, pitch) in PAPERDOLL_PITCHES_DEG.iter().enumerate() {
130 if ui.button(format!("pitch {pitch:.0}")).clicked() {
131 controls.snap_pitch_row(index as i32);
132 }
133 }
134 ui.label(format!(
135 "yaw {:.0}° / {} pitch {:.0}°",
136 controls.yaw_deg, PAPERDOLL_YAW_COUNT, controls.pitch_deg
137 ));
138 });
139 ui.label(
140 "Sleep om te draaien, scroll om te zoomen. Yaw 0 kijkt op de voorkant (−Z). De knoppen zetten de vijf pitches en de twaalf yaws.",
141 );
142 let available = ui.available_rect_before_wrap();
143 let rect = egui::Rect::from_min_size(
144 available.min,
145 egui::vec2(available.width().max(240.0), available.height().max(240.0)),
146 );
147 let rect = if let Some((_, size)) = framing {
148 let fit = crate::render_job::fit_preview(size, [rect.width(), rect.height()]);
149 egui::Rect::from_min_size(
150 rect.min + egui::vec2(fit.offset[0], fit.offset[1]),
151 egui::vec2(fit.size[0], fit.size[1]),
152 )
153 } else {
154 rect
155 };
156 let response = ui.allocate_rect(rect, egui::Sense::click_and_drag());
157 if response.dragged() {
158 controls.drag(response.drag_delta().x, response.drag_delta().y);
159 }
160 let scroll = ui.input(|input| input.smooth_scroll_delta.y);
161 if framing.is_none() && response.hovered() && scroll.abs() > 0.0 {
162 controls.zoom(scroll);
163 }
164 let Some(character) = character else {
165 ui.painter()
166 .rect_filled(rect, 4.0, egui::Color32::from_rgb(18, 22, 28));
167 ui.painter().text(
168 rect.center(),
169 egui::Align2::CENTER_CENTER,
170 "Geen lichaam geladen. Open een project met een .glb/.gltf/.fbx/.obj-lichaam of start met --preview-fixture.",
171 egui::FontId::proportional(14.0),
172 egui::Color32::from_rgb(210, 210, 210),
173 );
174 return;
175 };
176 let duration = clip_duration(character, &controls.clip_name);
177 let dt = ui.input(|input| input.stable_dt);
178 controls.advance(dt, duration);
179 ui.ctx().request_repaint();
180 let bones = sample_skin_matrices(character, &controls.clip_name, controls.time);
181 let aspect = rect.width() / rect.height().max(1.0);
182 let target = [
183 0.0,
184 framing
185 .map(|(c, _)| c.pivot_y_milli.unwrap_or(c.center_y_milli) as f32 / 1000.)
186 .unwrap_or(0.9),
187 0.0,
188 ];
189 let offset = eye_offset(
190 controls.yaw_deg,
191 controls.pitch_deg,
192 if framing.is_some() {
193 10.
194 } else {
195 controls.distance
196 },
197 );
198 let eye = [
199 target[0] + offset[0],
200 target[1] + offset[1],
201 target[2] + offset[2],
202 ];
203 let view_proj = mul(
204 perspective(38.0, aspect, 0.05, 40.0),
205 view_matrix(eye, target),
206 );
207 let view_proj = if let Some((camera, _)) = framing {
208 let (v, p) =
209 crate::production::camera(camera, controls.yaw_deg, controls.pitch_deg, aspect);
210 mul(p, v)
211 } else {
212 view_proj
213 };
214 let mut cache = self.cache.lock().expect("preview gpu lock");
215 let mut diagnostics = Vec::new();
216 let mut equipment = Vec::new();
217 let sex = if controls.sex == crate::preview::PreviewSex::Male {
218 0
219 } else {
220 1
221 };
222 for preset in presets {
223 match crate::equipment::layers_cached(
224 preset,
225 sex,
226 character,
227 controls,
228 &mut cache.texture_cache,
229 ) {
230 Ok(layers) => {
231 for layer in layers {
232 equipment.push(PendingFrame::from_layer(layer, view_proj, eye));
233 }
234 }
235 Err(error) => {
236 diagnostics.push(error);
237 }
238 }
239 }
240 {
241 cache.pending = Some(PendingFrame {
242 generation: character_generation(character),
243 positions: character.positions.clone(),
244 normals: character.normals.clone(),
245 joints: character.joints.clone(),
246 weights: character.weights.clone(),
247 indices: character.indices.clone(),
248 bones,
249 view_proj,
250 color: character.base_color,
251 model: crate::preview::identity(),
252 albedo: None,
253 shiny: None,
254 specular: 0.0,
255 eye,
256 equipment,
257 uvs: Vec::new(),
258 });
259 }
260 drop(cache);
261 let cache = Arc::clone(&self.cache);
262 let callback = CallbackFn::new(move |info, painter| {
263 if let Ok(mut cache) = cache.lock() {
264 unsafe { cache.paint(painter.gl(), &info, true) };
265 }
266 });
267 ui.painter().add(egui::PaintCallback {
268 rect,
269 callback: Arc::new(callback),
270 });
271 if framing.is_some() {
272 ui.painter()
273 .rect_stroke(rect, 0., egui::Stroke::new(2., egui::Color32::YELLOW));
274 ui.painter().text(
275 rect.left_top() + egui::vec2(5., 5.),
276 egui::Align2::LEFT_TOP,
277 "Vast renderkader",
278 egui::FontId::proportional(12.),
279 egui::Color32::YELLOW,
280 );
281 }
282
283 if let Ok(cache) = self.cache.lock() {
284 if let Some(error) = &cache.error {
285 diagnostics.push(error.clone());
286 }
287 for layer in &cache.layers {
288 if let Some(error) = &layer.error {
289 diagnostics.push(error.clone());
290 }
291 }
292 }
293 if !diagnostics.is_empty() {
294 ui.painter().text(
295 rect.left_top() + egui::vec2(12., 12.),
296 egui::Align2::LEFT_TOP,
297 diagnostics.join("\n"),
298 egui::FontId::proportional(14.),
299 egui::Color32::LIGHT_RED,
300 );
301 }
302 }
303}
304
305fn character_generation(character: &PreviewCharacter) -> u64 {
306 use std::hash::{Hash, Hasher};
307 let mut hash = std::collections::hash_map::DefaultHasher::new();
308 for point in &character.positions {
309 point.map(f32::to_bits).hash(&mut hash);
310 }
311 for normal in &character.normals {
312 normal.map(f32::to_bits).hash(&mut hash);
313 }
314 for weights in &character.weights {
315 weights.map(f32::to_bits).hash(&mut hash);
316 }
317 character.joints.hash(&mut hash);
318 character.indices.hash(&mut hash);
319 hash.finish()
320}
321
322#[derive(Clone)]
323struct PendingFrame {
324 generation: u64,
325 positions: Vec<[f32; 3]>,
326 normals: Vec<[f32; 3]>,
327 joints: Vec<[u16; 4]>,
328 weights: Vec<[f32; 4]>,
329 indices: Vec<u32>,
330 bones: Vec<crate::preview::Mat4>,
331 view_proj: crate::preview::Mat4,
332 color: [f32; 4],
333 model: crate::preview::Mat4,
334 eye: [f32; 3],
335 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
336 shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
337 specular: f32,
338 equipment: Vec<PendingFrame>,
339 uvs: Vec<[f32; 2]>,
340}
341
342impl PendingFrame {
343 fn from_layer(
344 layer: crate::equipment::Layer,
345 view_proj: crate::preview::Mat4,
346 eye: [f32; 3],
347 ) -> Self {
348 use std::hash::{Hash, Hasher};
349 let mut hash = std::collections::hash_map::DefaultHasher::new();
350 for v in &layer.mesh.positions {
351 v.map(f32::to_bits).hash(&mut hash);
352 }
353 for v in &layer.mesh.normals {
354 v.map(f32::to_bits).hash(&mut hash);
355 }
356 for v in &layer.mesh.uvs {
357 v.map(f32::to_bits).hash(&mut hash);
358 }
359 layer.mesh.indices.hash(&mut hash);
360 if let Some(t) = &layer.albedo {
361 t.content_hash.hash(&mut hash);
362 t.width.hash(&mut hash);
363 t.height.hash(&mut hash);
364 }
365 if let Some(t) = &layer.shiny {
366 t.content_hash.hash(&mut hash);
367 t.width.hash(&mut hash);
368 t.height.hash(&mut hash);
369 }
370 Self {
371 generation: hash.finish(),
372 positions: layer.mesh.positions,
373 normals: layer.mesh.normals,
374 indices: layer.mesh.indices,
375 uvs: layer.mesh.uvs,
376 color: layer.mesh.color,
377 joints: Vec::new(),
378 weights: Vec::new(),
379 bones: Vec::new(),
380 model: layer.model,
381 view_proj,
382 eye,
383 albedo: layer.albedo,
384 shiny: layer.shiny,
385 specular: layer.specular,
386 equipment: Vec::new(),
387 }
388 }
389}
390
391#[derive(Default)]
392struct GpuCache {
393 program: Option<glow::Program>,
394 vao: Option<glow::VertexArray>,
395 vbo: Option<glow::Buffer>,
396 ebo: Option<glow::Buffer>,
397 uploaded: u64,
398 index_count: i32,
399 pending: Option<PendingFrame>,
400 error: Option<String>,
401 textures: [Option<glow::Texture>; 2],
402 layers: Vec<GpuCache>,
403 texture_cache: crate::equipment::TextureCache,
404}
405
406impl GpuCache {
407 unsafe fn paint(&mut self, gl: &glow::Context, info: &egui::PaintCallbackInfo, clear: bool) {
408 let Some(frame) = self.pending.clone() else {
409 return;
410 };
411 if let Err(error) = self.ensure_program(gl) {
412 self.error = Some(error);
413 return;
414 }
415 if self.uploaded != frame.generation {
416 if let Err(error) = self.upload(gl, &frame) {
417 self.error = Some(error);
418 return;
419 }
420 }
421 let Some(program) = self.program else {
422 return;
423 };
424 let viewport = info.viewport_in_pixels();
425 if viewport.width_px <= 0 || viewport.height_px <= 0 {
426 return;
427 }
428 gl.enable(glow::SCISSOR_TEST);
429 gl.scissor(
430 viewport.left_px,
431 viewport.from_bottom_px,
432 viewport.width_px,
433 viewport.height_px,
434 );
435 gl.viewport(
436 viewport.left_px,
437 viewport.from_bottom_px,
438 viewport.width_px,
439 viewport.height_px,
440 );
441 gl.enable(glow::DEPTH_TEST);
442 gl.depth_mask(true);
443 gl.clear_depth_f32(1.0);
444 gl.depth_func(glow::LESS);
445 gl.disable(glow::BLEND);
446 gl.clear_color(0.08, 0.09, 0.12, 1.0);
447 if clear {
448 gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
449 }
450 gl.use_program(Some(program));
451 let view = gl.get_uniform_location(program, "u_view_proj");
452 gl.uniform_matrix_4_f32_slice(view.as_ref(), false, &frame.view_proj);
453 gl.uniform_matrix_4_f32_slice(
454 gl.get_uniform_location(program, "u_model").as_ref(),
455 false,
456 &frame.model,
457 );
458 gl.uniform_3_f32(
459 gl.get_uniform_location(program, "u_eye").as_ref(),
460 frame.eye[0],
461 frame.eye[1],
462 frame.eye[2],
463 );
464 gl.uniform_1_f32(
465 gl.get_uniform_location(program, "u_specular").as_ref(),
466 frame.specular,
467 );
468 for (unit, name, present) in [
469 (0, "u_albedo", frame.albedo.is_some()),
470 (1, "u_shiny", frame.shiny.is_some()),
471 ] {
472 gl.active_texture(glow::TEXTURE0 + unit as u32);
473 gl.bind_texture(glow::TEXTURE_2D, self.textures[unit]);
474 gl.uniform_1_i32(gl.get_uniform_location(program, name).as_ref(), unit as i32);
475 gl.uniform_1_i32(
476 gl.get_uniform_location(program, &format!("{name}_enabled"))
477 .as_ref(),
478 i32::from(present),
479 );
480 }
481 let color = gl.get_uniform_location(program, "u_color");
482 gl.uniform_4_f32(
483 color.as_ref(),
484 frame.color[0],
485 frame.color[1],
486 frame.color[2],
487 frame.color[3],
488 );
489 let mut bones = vec![crate::preview::identity(); MAX_BONES];
490 for (index, bone) in frame.bones.iter().take(MAX_BONES).enumerate() {
491 bones[index] = *bone;
492 }
493 let flat: Vec<f32> = bones.iter().flat_map(|bone| *bone).collect();
494 let bones_loc = gl.get_uniform_location(program, "u_bones");
495 gl.uniform_matrix_4_f32_slice(bones_loc.as_ref(), false, &flat);
496 gl.bind_vertex_array(self.vao);
497 gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0);
498 gl.bind_vertex_array(None);
499 while self.layers.len() > frame.equipment.len() {
500 if let Some(mut layer) = self.layers.pop() {
501 layer.destroy(gl);
502 }
503 }
504 while self.layers.len() < frame.equipment.len() {
505 self.layers.push(GpuCache::default());
506 }
507 for (cache, frame) in self.layers.iter_mut().zip(frame.equipment) {
508 cache.pending = Some(frame);
509 cache.paint(gl, info, false);
510 }
511 gl.active_texture(glow::TEXTURE0);
512 gl.disable(glow::DEPTH_TEST);
513 gl.disable(glow::SCISSOR_TEST);
514 gl.enable(glow::BLEND);
515 }
516
517 unsafe fn destroy(&mut self, gl: &glow::Context) {
518 for layer in &mut self.layers {
519 layer.destroy(gl);
520 }
521 for texture in &mut self.textures {
522 if let Some(t) = texture.take() {
523 gl.delete_texture(t)
524 }
525 }
526 if let Some(v) = self.vao.take() {
527 gl.delete_vertex_array(v)
528 }
529 if let Some(v) = self.vbo.take() {
530 gl.delete_buffer(v)
531 }
532 if let Some(v) = self.ebo.take() {
533 gl.delete_buffer(v)
534 }
535 if let Some(p) = self.program.take() {
536 gl.delete_program(p)
537 }
538 }
539 unsafe fn ensure_program(&mut self, gl: &glow::Context) -> Result<(), String> {
540 if self.program.is_some() {
541 return Ok(());
542 }
543 let program = link(gl, VERTEX, FRAGMENT)?;
544 let vao = gl.create_vertex_array().map_err(|err| err.to_string())?;
545 let vbo = gl.create_buffer().map_err(|err| err.to_string())?;
546 let ebo = gl.create_buffer().map_err(|err| err.to_string())?;
547 self.program = Some(program);
548 self.vao = Some(vao);
549 self.vbo = Some(vbo);
550 self.ebo = Some(ebo);
551 Ok(())
552 }
553
554 unsafe fn upload(&mut self, gl: &glow::Context, frame: &PendingFrame) -> Result<(), String> {
555 let mut interleaved = Vec::with_capacity(frame.positions.len() * 16);
556 for index in 0..frame.positions.len() {
557 let position = frame.positions[index];
558 let normal = frame.normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]);
559 let joint = frame.joints.get(index).copied().unwrap_or([0, 0, 0, 0]);
560 let weight = frame
561 .weights
562 .get(index)
563 .copied()
564 .unwrap_or([1.0, 0.0, 0.0, 0.0]);
565 interleaved.extend_from_slice(&position);
566 interleaved.extend_from_slice(&normal);
567 interleaved.extend([
568 joint[0] as f32,
569 joint[1] as f32,
570 joint[2] as f32,
571 joint[3] as f32,
572 ]);
573 interleaved.extend_from_slice(&weight);
574 interleaved.extend_from_slice(&frame.uvs.get(index).copied().unwrap_or([0.; 2]));
575 }
576 gl.bind_vertex_array(self.vao);
577 gl.bind_buffer(glow::ARRAY_BUFFER, self.vbo);
578 gl.buffer_data_u8_slice(
579 glow::ARRAY_BUFFER,
580 bytemuck_f32(&interleaved),
581 glow::DYNAMIC_DRAW,
582 );
583 gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.ebo);
584 gl.buffer_data_u8_slice(
585 glow::ELEMENT_ARRAY_BUFFER,
586 bytemuck_u32(&frame.indices),
587 glow::DYNAMIC_DRAW,
588 );
589 let stride = 16 * 4;
590 enable_float(gl, 0, 3, stride, 0);
591 enable_float(gl, 1, 3, stride, 3 * 4);
592 enable_float(gl, 2, 4, stride, 6 * 4);
593 enable_float(gl, 3, 4, stride, 10 * 4);
594 enable_float(gl, 4, 2, stride, 14 * 4);
595 for (unit, texture) in [&frame.albedo, &frame.shiny].iter().enumerate() {
596 if let Some(old) = self.textures[unit].take() {
597 gl.delete_texture(old);
598 }
599 if let Some(texture) = texture {
600 let id = gl.create_texture().map_err(|e| e.to_string())?;
601 self.textures[unit] = Some(id);
602 gl.active_texture(glow::TEXTURE0 + unit as u32);
603 gl.bind_texture(glow::TEXTURE_2D, Some(id));
604 gl.tex_parameter_i32(
605 glow::TEXTURE_2D,
606 glow::TEXTURE_MIN_FILTER,
607 glow::NEAREST as i32,
608 );
609 gl.tex_parameter_i32(
610 glow::TEXTURE_2D,
611 glow::TEXTURE_MAG_FILTER,
612 glow::NEAREST as i32,
613 );
614 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
615 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
616 gl.tex_image_2d(
617 glow::TEXTURE_2D,
618 0,
619 glow::RGBA8 as i32,
620 texture.width as i32,
621 texture.height as i32,
622 0,
623 glow::RGBA,
624 glow::UNSIGNED_BYTE,
625 Some(&texture.rgba),
626 );
627 }
628 }
629 self.uploaded = frame.generation;
630 self.index_count = frame.indices.len() as i32;
631 Ok(())
632 }
633}
634
635unsafe fn enable_float(gl: &glow::Context, index: u32, size: i32, stride: i32, offset: i32) {
636 gl.enable_vertex_attrib_array(index);
637 gl.vertex_attrib_pointer_f32(index, size, glow::FLOAT, false, stride, offset);
638}
639
640fn bytemuck_f32(values: &[f32]) -> &[u8] {
641 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
642}
643
644fn bytemuck_u32(values: &[u32]) -> &[u8] {
645 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
646}
647
648unsafe fn link(gl: &glow::Context, vertex: &str, fragment: &str) -> Result<glow::Program, String> {
649 let program = gl.create_program().map_err(|err| err.to_string())?;
650 let vs = compile(gl, glow::VERTEX_SHADER, vertex)?;
651 let fs = compile(gl, glow::FRAGMENT_SHADER, fragment)?;
652 gl.attach_shader(program, vs);
653 gl.attach_shader(program, fs);
654 gl.link_program(program);
655 if !gl.get_program_link_status(program) {
656 return Err(gl.get_program_info_log(program));
657 }
658 gl.delete_shader(vs);
659 gl.delete_shader(fs);
660 Ok(program)
661}
662
663unsafe fn compile(gl: &glow::Context, stage: u32, source: &str) -> Result<glow::Shader, String> {
664 let shader = gl.create_shader(stage).map_err(|err| err.to_string())?;
665 gl.shader_source(shader, source);
666 gl.compile_shader(shader);
667 if !gl.get_shader_compile_status(shader) {
668 return Err(gl.get_shader_info_log(shader));
669 }
670 Ok(shader)
671}
672
673const VERTEX: &str = r#"#version 330
674layout(location = 0) in vec3 a_pos;
675layout(location = 1) in vec3 a_normal;
676layout(location = 2) in vec4 a_joints;
677layout(location = 3) in vec4 a_weights;
678layout(location = 4) in vec2 a_uv;
679uniform mat4 u_model;
680out vec2 v_uv;
681out vec3 v_world;
682uniform mat4 u_view_proj;
683uniform mat4 u_bones[128];
684out vec3 v_normal;
685void main() {
686 int j0 = int(a_joints.x);
687 int j1 = int(a_joints.y);
688 int j2 = int(a_joints.z);
689 int j3 = int(a_joints.w);
690 mat4 skin = a_weights.x * u_bones[j0]
691 + a_weights.y * u_bones[j1]
692 + a_weights.z * u_bones[j2]
693 + a_weights.w * u_bones[j3];
694 vec4 world = u_model * skin * vec4(a_pos, 1.0);
695 v_uv = vec2(a_uv.x,1.0-a_uv.y); v_world=world.xyz;
696 v_normal = transpose(inverse(mat3(u_model * skin))) * a_normal;
697 gl_Position = u_view_proj * world;
698}
699"#;
700
701const FRAGMENT: &str = r#"#version 330
702in vec3 v_normal;
703uniform vec4 u_color;
704uniform sampler2D u_albedo;
705uniform sampler2D u_shiny;
706uniform bool u_albedo_enabled;
707uniform bool u_shiny_enabled;
708uniform float u_specular;
709uniform vec3 u_eye;
710in vec2 v_uv;
711in vec3 v_world;
712out vec4 frag;
713void main() {
714 vec3 n = normalize(v_normal);
715 vec3 light = normalize(vec3(0.35, 0.85, 0.4));
716 float shade = 0.28 + 0.72 * max(dot(n, light), 0.0);
717 vec4 color=u_color;
718 if(u_albedo_enabled) color*=texture(u_albedo,v_uv);
719 if(color.a<0.5) discard;
720 float strength=u_specular;
721 if(u_shiny_enabled) strength*=dot(texture(u_shiny,v_uv).rgb,vec3(0.2126,0.7152,0.0722));
722 float highlight=pow(max(dot(reflect(-light,n),normalize(u_eye-v_world)),0.0),32.0)*strength;
723 frag = vec4(clamp(color.rgb * shade + highlight,0.0,1.0), 1.0);
724}
725"#;
726
727/// Real GPU regression using the same cache/shader/paint path as the callback.
728/// Tests front-first/back-first overlap, depth writes, and the next-frame clear.
729///
730/// # Safety
731/// `gl` must be a current native OpenGL context. Run in a disposable test window;
732/// this probe changes GL state and restores the default framebuffer afterwards.
733pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
734 let native_bits = gl.get_framebuffer_attachment_parameter_i32(
735 glow::FRAMEBUFFER,
736 glow::DEPTH,
737 glow::FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,
738 );
739 if native_bits < 24 {
740 return Err(format!(
741 "native depth attachment ontbreekt: {native_bits} bits"
742 ));
743 }
744 let framebuffer = gl.create_framebuffer()?;
745 let texture = gl.create_texture()?;
746 let depth = gl.create_renderbuffer()?;
747 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));
748 gl.bind_texture(glow::TEXTURE_2D, Some(texture));
749 gl.tex_parameter_i32(
750 glow::TEXTURE_2D,
751 glow::TEXTURE_MIN_FILTER,
752 glow::NEAREST as i32,
753 );
754 gl.tex_parameter_i32(
755 glow::TEXTURE_2D,
756 glow::TEXTURE_MAG_FILTER,
757 glow::NEAREST as i32,
758 );
759 gl.tex_image_2d(
760 glow::TEXTURE_2D,
761 0,
762 glow::RGBA8 as i32,
763 32,
764 32,
765 0,
766 glow::RGBA,
767 glow::UNSIGNED_BYTE,
768 None,
769 );
770 gl.framebuffer_texture_2d(
771 glow::FRAMEBUFFER,
772 glow::COLOR_ATTACHMENT0,
773 glow::TEXTURE_2D,
774 Some(texture),
775 0,
776 );
777 gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth));
778 gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT24, 32, 32);
779 gl.framebuffer_renderbuffer(
780 glow::FRAMEBUFFER,
781 glow::DEPTH_ATTACHMENT,
782 glow::RENDERBUFFER,
783 Some(depth),
784 );
785 gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]);
786 gl.read_buffer(glow::COLOR_ATTACHMENT0);
787 if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {
788 return Err("onvolledig offscreen depth-target".into());
789 }
790 let info = egui::PaintCallbackInfo {
791 viewport: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32., 32.)),
792 clip_rect: egui::Rect::EVERYTHING,
793 pixels_per_point: 1.,
794 screen_size_px: [32, 32],
795 };
796 let make_frame = |indices: Vec<u32>, generation: u64| PendingFrame {
797 generation,
798 positions: vec![
799 [-0.9, -0.9, -0.5],
800 [0.9, -0.9, -0.5],
801 [0., 0.9, -0.5],
802 [-0.9, -0.9, 0.5],
803 [0.9, -0.9, 0.5],
804 [0., 0.9, 0.5],
805 ],
806 normals: vec![
807 [0.35, 0.85, 0.4],
808 [0.35, 0.85, 0.4],
809 [0.35, 0.85, 0.4],
810 [-0.35, -0.85, -0.4],
811 [-0.35, -0.85, -0.4],
812 [-0.35, -0.85, -0.4],
813 ],
814 joints: vec![[0; 4]; 6],
815 weights: vec![[1., 0., 0., 0.]; 6],
816 indices,
817 bones: vec![crate::preview::identity()],
818 view_proj: crate::preview::identity(),
819 color: [1.; 4],
820 model: crate::preview::identity(),
821 eye: [0., 0., -3.],
822 albedo: None,
823 shiny: None,
824 specular: 0.,
825 uvs: vec![[0.; 2]; 6],
826 equipment: Vec::new(),
827 };
828 let mut cache = GpuCache::default();
829 let read = || {
830 let mut rgba = [0u8; 4];
831 gl.read_pixels(
832 16,
833 16,
834 1,
835 1,
836 glow::RGBA,
837 glow::UNSIGNED_BYTE,
838 glow::PixelPackData::Slice(&mut rgba),
839 );
840 rgba
841 };
842 let result = (|| {
843 // Deliberately poison the incoming state: the callback owns its depth setup.
844 gl.depth_mask(false);
845 gl.clear_depth_f32(0.0);
846 gl.depth_func(glow::GREATER);
847 cache.pending = Some(make_frame(vec![0, 1, 2, 3, 4, 5], 1));
848 cache.paint(gl, &info, true);
849 let near = read();
850 if near[..3].iter().any(|c| *c < 250) {
851 return Err(format!("achterste vlak overschrijft voorste: {near:?}"));
852 }
853 cache.pending = Some(make_frame(vec![3, 4, 5], 2));
854 cache.paint(gl, &info, true);
855 let next = read();
856 if next[..3].iter().any(|c| !(69..=73).contains(c)) {
857 return Err(format!("depth niet gewist voor volgende frame: {next:?}"));
858 }
859 cache.pending = Some(make_frame(vec![3, 4, 5, 0, 1, 2], 3));
860 cache.paint(gl, &info, true);
861 let reverse = read();
862 if near != reverse {
863 return Err(format!(
864 "draw-order afhankelijke occlusie: {near:?} / {reverse:?}"
865 ));
866 }
867 let mut body = make_frame(vec![0, 1, 2], 4);
868 let mut behind = make_frame(vec![3, 4, 5], 5);
869 behind.color = [1., 0., 0., 1.];
870 body.equipment.push(behind);
871 cache.pending = Some(body);
872 cache.paint(gl, &info, true);
873 let hidden = read();
874 if hidden != near {
875 return Err(format!("equipment wist body-depth: {hidden:?}"));
876 }
877 let mut body = make_frame(vec![3, 4, 5], 6);
878 let mut front = make_frame(vec![0, 1, 2], 7);
879 front.albedo = Some(Arc::new(crate::equipment::Texture {
880 content_hash: 1,
881 width: 1,
882 height: 1,
883 rgba: vec![255, 0, 0, 255],
884 }));
885 body.equipment.push(front.clone());
886 cache.pending = Some(body.clone());
887 cache.paint(gl, &info, true);
888 let textured = read();
889 if textured[0] < 250 || textured[1] > 2 || textured[2] > 2 {
890 return Err(format!("voorste equipmenttexture ontbreekt: {textured:?}"));
891 }
892 front.generation = 8;
893 front.albedo = Some(Arc::new(crate::equipment::Texture {
894 content_hash: 2,
895 width: 1,
896 height: 1,
897 rgba: vec![255, 0, 0, 0],
898 }));
899 body.equipment = vec![front];
900 cache.pending = Some(body);
901 cache.paint(gl, &info, true);
902 let cutout = read();
903 if cutout != next {
904 return Err(format!("alpha-cutout bedekt body: {cutout:?}"));
905 }
906 let mut body = make_frame(vec![3, 4, 5], 9);
907 let mut front = make_frame(vec![0, 1, 2], 10);
908 front.color = [0., 0., 0., 1.];
909 front.specular = 1.;
910 front.eye = [35000., 85000., 40000.];
911 front.shiny = Some(Arc::new(crate::equipment::Texture {
912 content_hash: 3,
913 width: 1,
914 height: 1,
915 rgba: vec![255, 255, 255, 255],
916 }));
917 body.equipment = vec![front.clone()];
918 cache.pending = Some(body.clone());
919 cache.paint(gl, &info, true);
920 let shiny = read();
921 front.generation = 11;
922 front.shiny = Some(Arc::new(crate::equipment::Texture {
923 content_hash: 4,
924 width: 1,
925 height: 1,
926 rgba: vec![0, 0, 0, 255],
927 }));
928 body.equipment = vec![front];
929 cache.pending = Some(body);
930 cache.paint(gl, &info, true);
931 let matte = read();
932 if shiny[..3].iter().any(|c| *c < 250) || matte[..3] != [0, 0, 0] {
933 return Err(format!(
934 "shinymap beïnvloedt specular niet correct: {shiny:?}/{matte:?}"
935 ));
936 }
937 println!("GPU depth PASS: native {native_bits}-bit, front={near:?}, next-frame={next:?}, reversed={reverse:?}, behind-layer={hidden:?}, textured-front={textured:?}, alpha-cutout={cutout:?}, shiny={shiny:?}, matte={matte:?}");
938 Ok(())
939 })();
940 cache.destroy(gl);
941 gl.bind_framebuffer(glow::FRAMEBUFFER, None);
942 gl.delete_framebuffer(framebuffer);
943 gl.delete_texture(texture);
944 gl.delete_renderbuffer(depth);
945 result
946}