BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

preview_gl.rs 32.7 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, glTF-standaard). 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 // Fixed-camera mode uses the narrow per-sex depth range; clamp, never clip.
443 gl.enable(glow::DEPTH_CLAMP);
444 gl.depth_mask(true);
445 gl.clear_depth_f32(1.0);
446 gl.depth_func(glow::LESS);
447 gl.disable(glow::BLEND);
448 gl.clear_color(0.08, 0.09, 0.12, 1.0);
449 if clear {
450 gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
451 }
452 gl.use_program(Some(program));
453 let view = gl.get_uniform_location(program, "u_view_proj");
454 gl.uniform_matrix_4_f32_slice(view.as_ref(), false, &frame.view_proj);
455 gl.uniform_matrix_4_f32_slice(
456 gl.get_uniform_location(program, "u_model").as_ref(),
457 false,
458 &frame.model,
459 );
460 gl.uniform_3_f32(
461 gl.get_uniform_location(program, "u_eye").as_ref(),
462 frame.eye[0],
463 frame.eye[1],
464 frame.eye[2],
465 );
466 gl.uniform_1_f32(
467 gl.get_uniform_location(program, "u_specular").as_ref(),
468 frame.specular,
469 );
470 for (unit, name, present) in [
471 (0, "u_albedo", frame.albedo.is_some()),
472 (1, "u_shiny", frame.shiny.is_some()),
473 ] {
474 gl.active_texture(glow::TEXTURE0 + unit as u32);
475 gl.bind_texture(glow::TEXTURE_2D, self.textures[unit]);
476 gl.uniform_1_i32(gl.get_uniform_location(program, name).as_ref(), unit as i32);
477 gl.uniform_1_i32(
478 gl.get_uniform_location(program, &format!("{name}_enabled"))
479 .as_ref(),
480 i32::from(present),
481 );
482 }
483 let color = gl.get_uniform_location(program, "u_color");
484 gl.uniform_4_f32(
485 color.as_ref(),
486 frame.color[0],
487 frame.color[1],
488 frame.color[2],
489 frame.color[3],
490 );
491 let mut bones = vec![crate::preview::identity(); MAX_BONES];
492 for (index, bone) in frame.bones.iter().take(MAX_BONES).enumerate() {
493 bones[index] = *bone;
494 }
495 let flat: Vec<f32> = bones.iter().flat_map(|bone| *bone).collect();
496 let bones_loc = gl.get_uniform_location(program, "u_bones");
497 gl.uniform_matrix_4_f32_slice(bones_loc.as_ref(), false, &flat);
498 gl.bind_vertex_array(self.vao);
499 gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0);
500 gl.bind_vertex_array(None);
501 while self.layers.len() > frame.equipment.len() {
502 if let Some(mut layer) = self.layers.pop() {
503 layer.destroy(gl);
504 }
505 }
506 while self.layers.len() < frame.equipment.len() {
507 self.layers.push(GpuCache::default());
508 }
509 for (cache, frame) in self.layers.iter_mut().zip(frame.equipment) {
510 cache.pending = Some(frame);
511 cache.paint(gl, info, false);
512 }
513 gl.active_texture(glow::TEXTURE0);
514 gl.disable(glow::DEPTH_TEST);
515 gl.disable(glow::DEPTH_CLAMP);
516 gl.disable(glow::SCISSOR_TEST);
517 gl.enable(glow::BLEND);
518 }
519
520 unsafe fn destroy(&mut self, gl: &glow::Context) {
521 for layer in &mut self.layers {
522 layer.destroy(gl);
523 }
524 for texture in &mut self.textures {
525 if let Some(t) = texture.take() {
526 gl.delete_texture(t)
527 }
528 }
529 if let Some(v) = self.vao.take() {
530 gl.delete_vertex_array(v)
531 }
532 if let Some(v) = self.vbo.take() {
533 gl.delete_buffer(v)
534 }
535 if let Some(v) = self.ebo.take() {
536 gl.delete_buffer(v)
537 }
538 if let Some(p) = self.program.take() {
539 gl.delete_program(p)
540 }
541 }
542 unsafe fn ensure_program(&mut self, gl: &glow::Context) -> Result<(), String> {
543 if self.program.is_some() {
544 return Ok(());
545 }
546 let program = link(gl, VERTEX, FRAGMENT)?;
547 let vao = gl.create_vertex_array().map_err(|err| err.to_string())?;
548 let vbo = gl.create_buffer().map_err(|err| err.to_string())?;
549 let ebo = gl.create_buffer().map_err(|err| err.to_string())?;
550 self.program = Some(program);
551 self.vao = Some(vao);
552 self.vbo = Some(vbo);
553 self.ebo = Some(ebo);
554 Ok(())
555 }
556
557 unsafe fn upload(&mut self, gl: &glow::Context, frame: &PendingFrame) -> Result<(), String> {
558 let mut interleaved = Vec::with_capacity(frame.positions.len() * 16);
559 for index in 0..frame.positions.len() {
560 let position = frame.positions[index];
561 let normal = frame.normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]);
562 let joint = frame.joints.get(index).copied().unwrap_or([0, 0, 0, 0]);
563 let weight = frame
564 .weights
565 .get(index)
566 .copied()
567 .unwrap_or([1.0, 0.0, 0.0, 0.0]);
568 interleaved.extend_from_slice(&position);
569 interleaved.extend_from_slice(&normal);
570 interleaved.extend([
571 joint[0] as f32,
572 joint[1] as f32,
573 joint[2] as f32,
574 joint[3] as f32,
575 ]);
576 interleaved.extend_from_slice(&weight);
577 interleaved.extend_from_slice(&frame.uvs.get(index).copied().unwrap_or([0.; 2]));
578 }
579 gl.bind_vertex_array(self.vao);
580 gl.bind_buffer(glow::ARRAY_BUFFER, self.vbo);
581 gl.buffer_data_u8_slice(
582 glow::ARRAY_BUFFER,
583 bytemuck_f32(&interleaved),
584 glow::DYNAMIC_DRAW,
585 );
586 gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.ebo);
587 gl.buffer_data_u8_slice(
588 glow::ELEMENT_ARRAY_BUFFER,
589 bytemuck_u32(&frame.indices),
590 glow::DYNAMIC_DRAW,
591 );
592 let stride = 16 * 4;
593 enable_float(gl, 0, 3, stride, 0);
594 enable_float(gl, 1, 3, stride, 3 * 4);
595 enable_float(gl, 2, 4, stride, 6 * 4);
596 enable_float(gl, 3, 4, stride, 10 * 4);
597 enable_float(gl, 4, 2, stride, 14 * 4);
598 for (unit, texture) in [&frame.albedo, &frame.shiny].iter().enumerate() {
599 if let Some(old) = self.textures[unit].take() {
600 gl.delete_texture(old);
601 }
602 if let Some(texture) = texture {
603 let id = gl.create_texture().map_err(|e| e.to_string())?;
604 self.textures[unit] = Some(id);
605 gl.active_texture(glow::TEXTURE0 + unit as u32);
606 gl.bind_texture(glow::TEXTURE_2D, Some(id));
607 gl.tex_parameter_i32(
608 glow::TEXTURE_2D,
609 glow::TEXTURE_MIN_FILTER,
610 glow::NEAREST as i32,
611 );
612 gl.tex_parameter_i32(
613 glow::TEXTURE_2D,
614 glow::TEXTURE_MAG_FILTER,
615 glow::NEAREST as i32,
616 );
617 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
618 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
619 gl.tex_image_2d(
620 glow::TEXTURE_2D,
621 0,
622 glow::RGBA8 as i32,
623 texture.width as i32,
624 texture.height as i32,
625 0,
626 glow::RGBA,
627 glow::UNSIGNED_BYTE,
628 Some(&texture.rgba),
629 );
630 }
631 }
632 self.uploaded = frame.generation;
633 self.index_count = frame.indices.len() as i32;
634 Ok(())
635 }
636}
637
638unsafe fn enable_float(gl: &glow::Context, index: u32, size: i32, stride: i32, offset: i32) {
639 gl.enable_vertex_attrib_array(index);
640 gl.vertex_attrib_pointer_f32(index, size, glow::FLOAT, false, stride, offset);
641}
642
643fn bytemuck_f32(values: &[f32]) -> &[u8] {
644 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
645}
646
647fn bytemuck_u32(values: &[u32]) -> &[u8] {
648 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
649}
650
651unsafe fn link(gl: &glow::Context, vertex: &str, fragment: &str) -> Result<glow::Program, String> {
652 let program = gl.create_program().map_err(|err| err.to_string())?;
653 let vs = compile(gl, glow::VERTEX_SHADER, vertex)?;
654 let fs = compile(gl, glow::FRAGMENT_SHADER, fragment)?;
655 gl.attach_shader(program, vs);
656 gl.attach_shader(program, fs);
657 gl.link_program(program);
658 if !gl.get_program_link_status(program) {
659 return Err(gl.get_program_info_log(program));
660 }
661 gl.delete_shader(vs);
662 gl.delete_shader(fs);
663 Ok(program)
664}
665
666unsafe fn compile(gl: &glow::Context, stage: u32, source: &str) -> Result<glow::Shader, String> {
667 let shader = gl.create_shader(stage).map_err(|err| err.to_string())?;
668 gl.shader_source(shader, source);
669 gl.compile_shader(shader);
670 if !gl.get_shader_compile_status(shader) {
671 return Err(gl.get_shader_info_log(shader));
672 }
673 Ok(shader)
674}
675
676const VERTEX: &str = r#"#version 330
677layout(location = 0) in vec3 a_pos;
678layout(location = 1) in vec3 a_normal;
679layout(location = 2) in vec4 a_joints;
680layout(location = 3) in vec4 a_weights;
681layout(location = 4) in vec2 a_uv;
682uniform mat4 u_model;
683out vec2 v_uv;
684out vec3 v_world;
685uniform mat4 u_view_proj;
686uniform mat4 u_bones[128];
687out vec3 v_normal;
688void main() {
689 int j0 = int(a_joints.x);
690 int j1 = int(a_joints.y);
691 int j2 = int(a_joints.z);
692 int j3 = int(a_joints.w);
693 mat4 skin = a_weights.x * u_bones[j0]
694 + a_weights.y * u_bones[j1]
695 + a_weights.z * u_bones[j2]
696 + a_weights.w * u_bones[j3];
697 vec4 world = u_model * skin * vec4(a_pos, 1.0);
698 v_uv = vec2(a_uv.x,1.0-a_uv.y); v_world=world.xyz;
699 v_normal = transpose(inverse(mat3(u_model * skin))) * a_normal;
700 gl_Position = u_view_proj * world;
701}
702"#;
703
704const FRAGMENT: &str = r#"#version 330
705in vec3 v_normal;
706uniform vec4 u_color;
707uniform sampler2D u_albedo;
708uniform sampler2D u_shiny;
709uniform bool u_albedo_enabled;
710uniform bool u_shiny_enabled;
711uniform float u_specular;
712uniform vec3 u_eye;
713in vec2 v_uv;
714in vec3 v_world;
715out vec4 frag;
716void main() {
717 vec3 n = normalize(v_normal);
718 vec3 light = normalize(vec3(0.35, 0.85, 0.4));
719 float shade = 0.28 + 0.72 * max(dot(n, light), 0.0);
720 vec4 color=u_color;
721 if(u_albedo_enabled) color*=texture(u_albedo,v_uv);
722 if(color.a<0.5) discard;
723 float strength=u_specular;
724 if(u_shiny_enabled) strength*=dot(texture(u_shiny,v_uv).rgb,vec3(0.2126,0.7152,0.0722));
725 float highlight=pow(max(dot(reflect(-light,n),normalize(u_eye-v_world)),0.0),32.0)*strength;
726 frag = vec4(clamp(color.rgb * shade + highlight,0.0,1.0), 1.0);
727}
728"#;
729
730/// Real GPU regression using the same cache/shader/paint path as the callback.
731/// Tests front-first/back-first overlap, depth writes, and the next-frame clear.
732///
733/// # Safety
734/// `gl` must be a current native OpenGL context. Run in a disposable test window;
735/// this probe changes GL state and restores the default framebuffer afterwards.
736pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
737 let native_bits = gl.get_framebuffer_attachment_parameter_i32(
738 glow::FRAMEBUFFER,
739 glow::DEPTH,
740 glow::FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,
741 );
742 if native_bits < 24 {
743 return Err(format!(
744 "native depth attachment ontbreekt: {native_bits} bits"
745 ));
746 }
747 let framebuffer = gl.create_framebuffer()?;
748 let texture = gl.create_texture()?;
749 let depth = gl.create_renderbuffer()?;
750 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));
751 gl.bind_texture(glow::TEXTURE_2D, Some(texture));
752 gl.tex_parameter_i32(
753 glow::TEXTURE_2D,
754 glow::TEXTURE_MIN_FILTER,
755 glow::NEAREST as i32,
756 );
757 gl.tex_parameter_i32(
758 glow::TEXTURE_2D,
759 glow::TEXTURE_MAG_FILTER,
760 glow::NEAREST as i32,
761 );
762 gl.tex_image_2d(
763 glow::TEXTURE_2D,
764 0,
765 glow::RGBA8 as i32,
766 32,
767 32,
768 0,
769 glow::RGBA,
770 glow::UNSIGNED_BYTE,
771 None,
772 );
773 gl.framebuffer_texture_2d(
774 glow::FRAMEBUFFER,
775 glow::COLOR_ATTACHMENT0,
776 glow::TEXTURE_2D,
777 Some(texture),
778 0,
779 );
780 gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth));
781 gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT24, 32, 32);
782 gl.framebuffer_renderbuffer(
783 glow::FRAMEBUFFER,
784 glow::DEPTH_ATTACHMENT,
785 glow::RENDERBUFFER,
786 Some(depth),
787 );
788 gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]);
789 gl.read_buffer(glow::COLOR_ATTACHMENT0);
790 if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {
791 return Err("onvolledig offscreen depth-target".into());
792 }
793 let info = egui::PaintCallbackInfo {
794 viewport: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32., 32.)),
795 clip_rect: egui::Rect::EVERYTHING,
796 pixels_per_point: 1.,
797 screen_size_px: [32, 32],
798 };
799 let make_frame = |indices: Vec<u32>, generation: u64| PendingFrame {
800 generation,
801 positions: vec![
802 [-0.9, -0.9, -0.5],
803 [0.9, -0.9, -0.5],
804 [0., 0.9, -0.5],
805 [-0.9, -0.9, 0.5],
806 [0.9, -0.9, 0.5],
807 [0., 0.9, 0.5],
808 ],
809 normals: vec![
810 [0.35, 0.85, 0.4],
811 [0.35, 0.85, 0.4],
812 [0.35, 0.85, 0.4],
813 [-0.35, -0.85, -0.4],
814 [-0.35, -0.85, -0.4],
815 [-0.35, -0.85, -0.4],
816 ],
817 joints: vec![[0; 4]; 6],
818 weights: vec![[1., 0., 0., 0.]; 6],
819 indices,
820 bones: vec![crate::preview::identity()],
821 view_proj: crate::preview::identity(),
822 color: [1.; 4],
823 model: crate::preview::identity(),
824 eye: [0., 0., -3.],
825 albedo: None,
826 shiny: None,
827 specular: 0.,
828 uvs: vec![[0.; 2]; 6],
829 equipment: Vec::new(),
830 };
831 let mut cache = GpuCache::default();
832 let read = || {
833 let mut rgba = [0u8; 4];
834 gl.read_pixels(
835 16,
836 16,
837 1,
838 1,
839 glow::RGBA,
840 glow::UNSIGNED_BYTE,
841 glow::PixelPackData::Slice(&mut rgba),
842 );
843 rgba
844 };
845 let result = (|| {
846 // Deliberately poison the incoming state: the callback owns its depth setup.
847 gl.depth_mask(false);
848 gl.clear_depth_f32(0.0);
849 gl.depth_func(glow::GREATER);
850 cache.pending = Some(make_frame(vec![0, 1, 2, 3, 4, 5], 1));
851 cache.paint(gl, &info, true);
852 let near = read();
853 if near[..3].iter().any(|c| *c < 250) {
854 return Err(format!("achterste vlak overschrijft voorste: {near:?}"));
855 }
856 cache.pending = Some(make_frame(vec![3, 4, 5], 2));
857 cache.paint(gl, &info, true);
858 let next = read();
859 if next[..3].iter().any(|c| !(69..=73).contains(c)) {
860 return Err(format!("depth niet gewist voor volgende frame: {next:?}"));
861 }
862 cache.pending = Some(make_frame(vec![3, 4, 5, 0, 1, 2], 3));
863 cache.paint(gl, &info, true);
864 let reverse = read();
865 if near != reverse {
866 return Err(format!(
867 "draw-order afhankelijke occlusie: {near:?} / {reverse:?}"
868 ));
869 }
870 let mut body = make_frame(vec![0, 1, 2], 4);
871 let mut behind = make_frame(vec![3, 4, 5], 5);
872 behind.color = [1., 0., 0., 1.];
873 body.equipment.push(behind);
874 cache.pending = Some(body);
875 cache.paint(gl, &info, true);
876 let hidden = read();
877 if hidden != near {
878 return Err(format!("equipment wist body-depth: {hidden:?}"));
879 }
880 let mut body = make_frame(vec![3, 4, 5], 6);
881 let mut front = make_frame(vec![0, 1, 2], 7);
882 front.albedo = Some(Arc::new(crate::equipment::Texture {
883 content_hash: 1,
884 width: 1,
885 height: 1,
886 rgba: vec![255, 0, 0, 255],
887 }));
888 body.equipment.push(front.clone());
889 cache.pending = Some(body.clone());
890 cache.paint(gl, &info, true);
891 let textured = read();
892 if textured[0] < 250 || textured[1] > 2 || textured[2] > 2 {
893 return Err(format!("voorste equipmenttexture ontbreekt: {textured:?}"));
894 }
895 front.generation = 8;
896 front.albedo = Some(Arc::new(crate::equipment::Texture {
897 content_hash: 2,
898 width: 1,
899 height: 1,
900 rgba: vec![255, 0, 0, 0],
901 }));
902 body.equipment = vec![front];
903 cache.pending = Some(body);
904 cache.paint(gl, &info, true);
905 let cutout = read();
906 if cutout != next {
907 return Err(format!("alpha-cutout bedekt body: {cutout:?}"));
908 }
909 let mut body = make_frame(vec![3, 4, 5], 9);
910 let mut front = make_frame(vec![0, 1, 2], 10);
911 front.color = [0., 0., 0., 1.];
912 front.specular = 1.;
913 front.eye = [35000., 85000., 40000.];
914 front.shiny = Some(Arc::new(crate::equipment::Texture {
915 content_hash: 3,
916 width: 1,
917 height: 1,
918 rgba: vec![255, 255, 255, 255],
919 }));
920 body.equipment = vec![front.clone()];
921 cache.pending = Some(body.clone());
922 cache.paint(gl, &info, true);
923 let shiny = read();
924 front.generation = 11;
925 front.shiny = Some(Arc::new(crate::equipment::Texture {
926 content_hash: 4,
927 width: 1,
928 height: 1,
929 rgba: vec![0, 0, 0, 255],
930 }));
931 body.equipment = vec![front];
932 cache.pending = Some(body);
933 cache.paint(gl, &info, true);
934 let matte = read();
935 if shiny[..3].iter().any(|c| *c < 250) || matte[..3] != [0, 0, 0] {
936 return Err(format!(
937 "shinymap beïnvloedt specular niet correct: {shiny:?}/{matte:?}"
938 ));
939 }
940 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:?}");
941 Ok(())
942 })();
943 cache.destroy(gl);
944 gl.bind_framebuffer(glow::FRAMEBUFFER, None);
945 gl.delete_framebuffer(framebuffer);
946 gl.delete_texture(texture);
947 gl.delete_renderbuffer(depth);
948 result
949}