BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

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