BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

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