BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

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