BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

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