//! OpenGL viewport for a skinned glTF/GLB or FBX body, a static (unskinned) OBJ/glTF/FBX body, and rigid textured equipment. //! //! The shaded mesh is a placement preview. It is not the unlit paperdoll //! diffuse/depth/normal export. use std::sync::{Arc, Mutex}; use eframe::egui; use egui_glow::CallbackFn; use glow::HasContext; use crate::preview::{ clip_duration, eye_offset, mul, perspective, sample_skin_matrices, view_matrix, PreviewCharacter, PreviewControls, PAPERDOLL_PITCHES_DEG, PAPERDOLL_YAW_COUNT, }; /// egui itself requests no depth buffer. The custom 3D callback needs a real /// 24-bit attachment in the native Glow render target. pub fn native_options() -> eframe::NativeOptions { eframe::NativeOptions { depth_buffer: 24, renderer: eframe::Renderer::Glow, ..Default::default() } } const MAX_BONES: usize = crate::preview::MAX_JOINTS; pub struct Viewport { cache: Arc>, } impl Default for Viewport { fn default() -> Self { Self::new() } } impl Viewport { pub fn new() -> Self { Self { cache: Arc::new(Mutex::new(GpuCache::default())), } } pub fn destroy(&self, gl: &glow::Context) { if let Ok(mut cache) = self.cache.lock() { unsafe { cache.destroy(gl) } } } pub fn show( &self, ui: &mut egui::Ui, controls: &mut PreviewControls, character: Option<&PreviewCharacter>, presets: &[&crate::equipment::Preset], ) { self.show_framed(ui, controls, character, presets, None); } pub fn show_framed( &self, ui: &mut egui::Ui, controls: &mut PreviewControls, character: Option<&PreviewCharacter>, presets: &[&crate::equipment::Preset], framing: Option<(crate::project::CameraFraming, [u32; 2])>, ) { let framing = framing .filter(|(c, r)| c.validate().is_ok() && r[0] > 0 && r[1] > 0) .map(|(c, r)| { ( character .map(|body| crate::camera::resolve(body, c).framing) .unwrap_or(c), r, ) }); ui.horizontal_wrapped(|ui| { if let Some(character) = character { if !character .clips .iter() .any(|clip| clip.name == controls.clip_name) { if let Some(clip) = character.clips.first() { controls.select_clip(&clip.name); } } for clip in &character.clips { if ui .selectable_label(controls.clip_name == clip.name, &clip.name) .clicked() { controls.select_clip(&clip.name); } } if character.clips.is_empty() { let why = if character.is_static() { "Geen animaties: statisch lichaam zonder skeleton (bindpose)" } else { "Geen animaties: bindpose" }; ui.add_enabled(false, egui::Button::new("Animatie")) .on_disabled_hover_text(why); ui.label(why); } } let animated = character.is_some_and(|c| !c.clips.is_empty()); if ui .add_enabled( animated, egui::SelectableLabel::new( controls.playing && animated, if controls.playing { "Pauze" } else { "Speel" }, ), ) .clicked() { controls.playing = !controls.playing; } if ui.button("Yaw −").clicked() { controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 - 1); } if ui.button("Yaw +").clicked() { controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 + 1); } for (index, pitch) in PAPERDOLL_PITCHES_DEG.iter().enumerate() { if ui.button(format!("pitch {pitch:.0}")).clicked() { controls.snap_pitch_row(index as i32); } } ui.label(format!( "yaw {:.0}° / {} pitch {:.0}°", controls.yaw_deg, PAPERDOLL_YAW_COUNT, controls.pitch_deg )); }); ui.label( "Sleep om te draaien, scroll om te zoomen. Yaw 0 kijkt op de voorkant (−Z). De knoppen zetten de vijf pitches en de twaalf yaws.", ); let available = ui.available_rect_before_wrap(); let rect = egui::Rect::from_min_size( available.min, egui::vec2(available.width().max(240.0), available.height().max(240.0)), ); let rect = if let Some((_, size)) = framing { let fit = crate::render_job::fit_preview(size, [rect.width(), rect.height()]); egui::Rect::from_min_size( rect.min + egui::vec2(fit.offset[0], fit.offset[1]), egui::vec2(fit.size[0], fit.size[1]), ) } else { rect }; let response = ui.allocate_rect(rect, egui::Sense::click_and_drag()); if response.dragged() { controls.drag(response.drag_delta().x, response.drag_delta().y); } let scroll = ui.input(|input| input.smooth_scroll_delta.y); if framing.is_none() && response.hovered() && scroll.abs() > 0.0 { controls.zoom(scroll); } let Some(character) = character else { ui.painter() .rect_filled(rect, 4.0, egui::Color32::from_rgb(18, 22, 28)); ui.painter().text( rect.center(), egui::Align2::CENTER_CENTER, "Geen lichaam geladen. Open een project met een .glb/.gltf/.fbx/.obj-lichaam of start met --preview-fixture.", egui::FontId::proportional(14.0), egui::Color32::from_rgb(210, 210, 210), ); return; }; let duration = clip_duration(character, &controls.clip_name); let dt = ui.input(|input| input.stable_dt); controls.advance(dt, duration); ui.ctx().request_repaint(); let bones = sample_skin_matrices(character, &controls.clip_name, controls.time); let aspect = rect.width() / rect.height().max(1.0); let target = [ 0.0, framing .map(|(c, _)| c.pivot_y_milli.unwrap_or(c.center_y_milli) as f32 / 1000.) .unwrap_or(0.9), 0.0, ]; let offset = eye_offset( controls.yaw_deg, controls.pitch_deg, if framing.is_some() { 10. } else { controls.distance }, ); let eye = [ target[0] + offset[0], target[1] + offset[1], target[2] + offset[2], ]; let view_proj = mul( perspective(38.0, aspect, 0.05, 40.0), view_matrix(eye, target), ); let view_proj = if let Some((camera, _)) = framing { let (v, p) = crate::production::camera(camera, controls.yaw_deg, controls.pitch_deg, aspect); mul(p, v) } else { view_proj }; let mut cache = self.cache.lock().expect("preview gpu lock"); let mut diagnostics = Vec::new(); let mut equipment = Vec::new(); let sex = if controls.sex == crate::preview::PreviewSex::Male { 0 } else { 1 }; for preset in presets { match crate::equipment::layers_cached( preset, sex, character, controls, &mut cache.texture_cache, ) { Ok(layers) => { for layer in layers { equipment.push(PendingFrame::from_layer(layer, view_proj, eye)); } } Err(error) => { diagnostics.push(error); } } } { cache.pending = Some(PendingFrame { generation: character_generation(character), positions: character.positions.clone(), normals: character.normals.clone(), joints: character.joints.clone(), weights: character.weights.clone(), indices: character.indices.clone(), bones, view_proj, color: character.base_color, model: crate::preview::identity(), albedo: None, shiny: None, specular: 0.0, eye, equipment, uvs: Vec::new(), }); } drop(cache); let cache = Arc::clone(&self.cache); let callback = CallbackFn::new(move |info, painter| { if let Ok(mut cache) = cache.lock() { unsafe { cache.paint(painter.gl(), &info, true) }; } }); ui.painter().add(egui::PaintCallback { rect, callback: Arc::new(callback), }); if framing.is_some() { ui.painter() .rect_stroke(rect, 0., egui::Stroke::new(2., egui::Color32::YELLOW)); ui.painter().text( rect.left_top() + egui::vec2(5., 5.), egui::Align2::LEFT_TOP, "Vast renderkader", egui::FontId::proportional(12.), egui::Color32::YELLOW, ); } if let Ok(cache) = self.cache.lock() { if let Some(error) = &cache.error { diagnostics.push(error.clone()); } for layer in &cache.layers { if let Some(error) = &layer.error { diagnostics.push(error.clone()); } } } if !diagnostics.is_empty() { ui.painter().text( rect.left_top() + egui::vec2(12., 12.), egui::Align2::LEFT_TOP, diagnostics.join("\n"), egui::FontId::proportional(14.), egui::Color32::LIGHT_RED, ); } } } fn character_generation(character: &PreviewCharacter) -> u64 { use std::hash::{Hash, Hasher}; let mut hash = std::collections::hash_map::DefaultHasher::new(); for point in &character.positions { point.map(f32::to_bits).hash(&mut hash); } for normal in &character.normals { normal.map(f32::to_bits).hash(&mut hash); } for weights in &character.weights { weights.map(f32::to_bits).hash(&mut hash); } character.joints.hash(&mut hash); character.indices.hash(&mut hash); hash.finish() } #[derive(Clone)] struct PendingFrame { generation: u64, positions: Vec<[f32; 3]>, normals: Vec<[f32; 3]>, joints: Vec<[u16; 4]>, weights: Vec<[f32; 4]>, indices: Vec, bones: Vec, view_proj: crate::preview::Mat4, color: [f32; 4], model: crate::preview::Mat4, eye: [f32; 3], albedo: Option>, shiny: Option>, specular: f32, equipment: Vec, uvs: Vec<[f32; 2]>, } impl PendingFrame { fn from_layer( layer: crate::equipment::Layer, view_proj: crate::preview::Mat4, eye: [f32; 3], ) -> Self { use std::hash::{Hash, Hasher}; let mut hash = std::collections::hash_map::DefaultHasher::new(); for v in &layer.mesh.positions { v.map(f32::to_bits).hash(&mut hash); } for v in &layer.mesh.normals { v.map(f32::to_bits).hash(&mut hash); } for v in &layer.mesh.uvs { v.map(f32::to_bits).hash(&mut hash); } layer.mesh.indices.hash(&mut hash); if let Some(t) = &layer.albedo { t.content_hash.hash(&mut hash); t.width.hash(&mut hash); t.height.hash(&mut hash); } if let Some(t) = &layer.shiny { t.content_hash.hash(&mut hash); t.width.hash(&mut hash); t.height.hash(&mut hash); } Self { generation: hash.finish(), positions: layer.mesh.positions, normals: layer.mesh.normals, indices: layer.mesh.indices, uvs: layer.mesh.uvs, color: layer.mesh.color, joints: Vec::new(), weights: Vec::new(), bones: Vec::new(), model: layer.model, view_proj, eye, albedo: layer.albedo, shiny: layer.shiny, specular: layer.specular, equipment: Vec::new(), } } } #[derive(Default)] struct GpuCache { program: Option, vao: Option, vbo: Option, ebo: Option, uploaded: u64, index_count: i32, pending: Option, error: Option, textures: [Option; 2], layers: Vec, texture_cache: crate::equipment::TextureCache, } impl GpuCache { unsafe fn paint(&mut self, gl: &glow::Context, info: &egui::PaintCallbackInfo, clear: bool) { let Some(frame) = self.pending.clone() else { return; }; if let Err(error) = self.ensure_program(gl) { self.error = Some(error); return; } if self.uploaded != frame.generation { if let Err(error) = self.upload(gl, &frame) { self.error = Some(error); return; } } let Some(program) = self.program else { return; }; let viewport = info.viewport_in_pixels(); if viewport.width_px <= 0 || viewport.height_px <= 0 { return; } gl.enable(glow::SCISSOR_TEST); gl.scissor( viewport.left_px, viewport.from_bottom_px, viewport.width_px, viewport.height_px, ); gl.viewport( viewport.left_px, viewport.from_bottom_px, viewport.width_px, viewport.height_px, ); gl.enable(glow::DEPTH_TEST); // Fixed-camera mode uses the narrow per-sex depth range; clamp, never clip. gl.enable(glow::DEPTH_CLAMP); gl.depth_mask(true); gl.clear_depth_f32(1.0); gl.depth_func(glow::LESS); gl.disable(glow::BLEND); gl.clear_color(0.08, 0.09, 0.12, 1.0); if clear { gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT); } gl.use_program(Some(program)); let view = gl.get_uniform_location(program, "u_view_proj"); gl.uniform_matrix_4_f32_slice(view.as_ref(), false, &frame.view_proj); gl.uniform_matrix_4_f32_slice( gl.get_uniform_location(program, "u_model").as_ref(), false, &frame.model, ); gl.uniform_3_f32( gl.get_uniform_location(program, "u_eye").as_ref(), frame.eye[0], frame.eye[1], frame.eye[2], ); gl.uniform_1_f32( gl.get_uniform_location(program, "u_specular").as_ref(), frame.specular, ); for (unit, name, present) in [ (0, "u_albedo", frame.albedo.is_some()), (1, "u_shiny", frame.shiny.is_some()), ] { gl.active_texture(glow::TEXTURE0 + unit as u32); gl.bind_texture(glow::TEXTURE_2D, self.textures[unit]); gl.uniform_1_i32(gl.get_uniform_location(program, name).as_ref(), unit as i32); gl.uniform_1_i32( gl.get_uniform_location(program, &format!("{name}_enabled")) .as_ref(), i32::from(present), ); } let color = gl.get_uniform_location(program, "u_color"); gl.uniform_4_f32( color.as_ref(), frame.color[0], frame.color[1], frame.color[2], frame.color[3], ); let mut bones = vec![crate::preview::identity(); MAX_BONES]; for (index, bone) in frame.bones.iter().take(MAX_BONES).enumerate() { bones[index] = *bone; } let flat: Vec = bones.iter().flat_map(|bone| *bone).collect(); let bones_loc = gl.get_uniform_location(program, "u_bones"); gl.uniform_matrix_4_f32_slice(bones_loc.as_ref(), false, &flat); gl.bind_vertex_array(self.vao); gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0); gl.bind_vertex_array(None); while self.layers.len() > frame.equipment.len() { if let Some(mut layer) = self.layers.pop() { layer.destroy(gl); } } while self.layers.len() < frame.equipment.len() { self.layers.push(GpuCache::default()); } for (cache, frame) in self.layers.iter_mut().zip(frame.equipment) { cache.pending = Some(frame); cache.paint(gl, info, false); } gl.active_texture(glow::TEXTURE0); gl.disable(glow::DEPTH_TEST); gl.disable(glow::DEPTH_CLAMP); gl.disable(glow::SCISSOR_TEST); gl.enable(glow::BLEND); } unsafe fn destroy(&mut self, gl: &glow::Context) { for layer in &mut self.layers { layer.destroy(gl); } for texture in &mut self.textures { if let Some(t) = texture.take() { gl.delete_texture(t) } } if let Some(v) = self.vao.take() { gl.delete_vertex_array(v) } if let Some(v) = self.vbo.take() { gl.delete_buffer(v) } if let Some(v) = self.ebo.take() { gl.delete_buffer(v) } if let Some(p) = self.program.take() { gl.delete_program(p) } } unsafe fn ensure_program(&mut self, gl: &glow::Context) -> Result<(), String> { if self.program.is_some() { return Ok(()); } let program = link(gl, VERTEX, FRAGMENT)?; let vao = gl.create_vertex_array().map_err(|err| err.to_string())?; let vbo = gl.create_buffer().map_err(|err| err.to_string())?; let ebo = gl.create_buffer().map_err(|err| err.to_string())?; self.program = Some(program); self.vao = Some(vao); self.vbo = Some(vbo); self.ebo = Some(ebo); Ok(()) } unsafe fn upload(&mut self, gl: &glow::Context, frame: &PendingFrame) -> Result<(), String> { let mut interleaved = Vec::with_capacity(frame.positions.len() * 16); for index in 0..frame.positions.len() { let position = frame.positions[index]; let normal = frame.normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]); let joint = frame.joints.get(index).copied().unwrap_or([0, 0, 0, 0]); let weight = frame .weights .get(index) .copied() .unwrap_or([1.0, 0.0, 0.0, 0.0]); interleaved.extend_from_slice(&position); interleaved.extend_from_slice(&normal); interleaved.extend([ joint[0] as f32, joint[1] as f32, joint[2] as f32, joint[3] as f32, ]); interleaved.extend_from_slice(&weight); interleaved.extend_from_slice(&frame.uvs.get(index).copied().unwrap_or([0.; 2])); } gl.bind_vertex_array(self.vao); gl.bind_buffer(glow::ARRAY_BUFFER, self.vbo); gl.buffer_data_u8_slice( glow::ARRAY_BUFFER, bytemuck_f32(&interleaved), glow::DYNAMIC_DRAW, ); gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.ebo); gl.buffer_data_u8_slice( glow::ELEMENT_ARRAY_BUFFER, bytemuck_u32(&frame.indices), glow::DYNAMIC_DRAW, ); let stride = 16 * 4; enable_float(gl, 0, 3, stride, 0); enable_float(gl, 1, 3, stride, 3 * 4); enable_float(gl, 2, 4, stride, 6 * 4); enable_float(gl, 3, 4, stride, 10 * 4); enable_float(gl, 4, 2, stride, 14 * 4); for (unit, texture) in [&frame.albedo, &frame.shiny].iter().enumerate() { if let Some(old) = self.textures[unit].take() { gl.delete_texture(old); } if let Some(texture) = texture { let id = gl.create_texture().map_err(|e| e.to_string())?; self.textures[unit] = Some(id); gl.active_texture(glow::TEXTURE0 + unit as u32); gl.bind_texture(glow::TEXTURE_2D, Some(id)); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32, ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32, ); gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32); gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32); gl.tex_image_2d( glow::TEXTURE_2D, 0, glow::RGBA8 as i32, texture.width as i32, texture.height as i32, 0, glow::RGBA, glow::UNSIGNED_BYTE, Some(&texture.rgba), ); } } self.uploaded = frame.generation; self.index_count = frame.indices.len() as i32; Ok(()) } } unsafe fn enable_float(gl: &glow::Context, index: u32, size: i32, stride: i32, offset: i32) { gl.enable_vertex_attrib_array(index); gl.vertex_attrib_pointer_f32(index, size, glow::FLOAT, false, stride, offset); } fn bytemuck_f32(values: &[f32]) -> &[u8] { unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) } } fn bytemuck_u32(values: &[u32]) -> &[u8] { unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) } } unsafe fn link(gl: &glow::Context, vertex: &str, fragment: &str) -> Result { let program = gl.create_program().map_err(|err| err.to_string())?; let vs = compile(gl, glow::VERTEX_SHADER, vertex)?; let fs = compile(gl, glow::FRAGMENT_SHADER, fragment)?; gl.attach_shader(program, vs); gl.attach_shader(program, fs); gl.link_program(program); if !gl.get_program_link_status(program) { return Err(gl.get_program_info_log(program)); } gl.delete_shader(vs); gl.delete_shader(fs); Ok(program) } unsafe fn compile(gl: &glow::Context, stage: u32, source: &str) -> Result { let shader = gl.create_shader(stage).map_err(|err| err.to_string())?; gl.shader_source(shader, source); gl.compile_shader(shader); if !gl.get_shader_compile_status(shader) { return Err(gl.get_shader_info_log(shader)); } Ok(shader) } const VERTEX: &str = r#"#version 330 layout(location = 0) in vec3 a_pos; layout(location = 1) in vec3 a_normal; layout(location = 2) in vec4 a_joints; layout(location = 3) in vec4 a_weights; layout(location = 4) in vec2 a_uv; uniform mat4 u_model; out vec2 v_uv; out vec3 v_world; uniform mat4 u_view_proj; uniform mat4 u_bones[128]; out vec3 v_normal; void main() { int j0 = int(a_joints.x); int j1 = int(a_joints.y); int j2 = int(a_joints.z); int j3 = int(a_joints.w); mat4 skin = a_weights.x * u_bones[j0] + a_weights.y * u_bones[j1] + a_weights.z * u_bones[j2] + a_weights.w * u_bones[j3]; vec4 world = u_model * skin * vec4(a_pos, 1.0); v_uv = vec2(a_uv.x,1.0-a_uv.y); v_world=world.xyz; v_normal = transpose(inverse(mat3(u_model * skin))) * a_normal; gl_Position = u_view_proj * world; } "#; const FRAGMENT: &str = r#"#version 330 in vec3 v_normal; uniform vec4 u_color; uniform sampler2D u_albedo; uniform sampler2D u_shiny; uniform bool u_albedo_enabled; uniform bool u_shiny_enabled; uniform float u_specular; uniform vec3 u_eye; in vec2 v_uv; in vec3 v_world; out vec4 frag; void main() { vec3 n = normalize(v_normal); vec3 light = normalize(vec3(0.35, 0.85, 0.4)); float shade = 0.28 + 0.72 * max(dot(n, light), 0.0); vec4 color=u_color; if(u_albedo_enabled) color*=texture(u_albedo,v_uv); if(color.a<0.5) discard; float strength=u_specular; if(u_shiny_enabled) strength*=dot(texture(u_shiny,v_uv).rgb,vec3(0.2126,0.7152,0.0722)); float highlight=pow(max(dot(reflect(-light,n),normalize(u_eye-v_world)),0.0),32.0)*strength; frag = vec4(clamp(color.rgb * shade + highlight,0.0,1.0), 1.0); } "#; /// Real GPU regression using the same cache/shader/paint path as the callback. /// Tests front-first/back-first overlap, depth writes, and the next-frame clear. /// /// # Safety /// `gl` must be a current native OpenGL context. Run in a disposable test window; /// this probe changes GL state and restores the default framebuffer afterwards. pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> { let native_bits = gl.get_framebuffer_attachment_parameter_i32( glow::FRAMEBUFFER, glow::DEPTH, glow::FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, ); if native_bits < 24 { return Err(format!( "native depth attachment ontbreekt: {native_bits} bits" )); } let framebuffer = gl.create_framebuffer()?; let texture = gl.create_texture()?; let depth = gl.create_renderbuffer()?; gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer)); gl.bind_texture(glow::TEXTURE_2D, Some(texture)); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32, ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32, ); gl.tex_image_2d( glow::TEXTURE_2D, 0, glow::RGBA8 as i32, 32, 32, 0, glow::RGBA, glow::UNSIGNED_BYTE, None, ); gl.framebuffer_texture_2d( glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some(texture), 0, ); gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth)); gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT24, 32, 32); gl.framebuffer_renderbuffer( glow::FRAMEBUFFER, glow::DEPTH_ATTACHMENT, glow::RENDERBUFFER, Some(depth), ); gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]); gl.read_buffer(glow::COLOR_ATTACHMENT0); if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE { return Err("onvolledig offscreen depth-target".into()); } let info = egui::PaintCallbackInfo { viewport: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32., 32.)), clip_rect: egui::Rect::EVERYTHING, pixels_per_point: 1., screen_size_px: [32, 32], }; let make_frame = |indices: Vec, generation: u64| PendingFrame { generation, positions: vec![ [-0.9, -0.9, -0.5], [0.9, -0.9, -0.5], [0., 0.9, -0.5], [-0.9, -0.9, 0.5], [0.9, -0.9, 0.5], [0., 0.9, 0.5], ], normals: vec![ [0.35, 0.85, 0.4], [0.35, 0.85, 0.4], [0.35, 0.85, 0.4], [-0.35, -0.85, -0.4], [-0.35, -0.85, -0.4], [-0.35, -0.85, -0.4], ], joints: vec![[0; 4]; 6], weights: vec![[1., 0., 0., 0.]; 6], indices, bones: vec![crate::preview::identity()], view_proj: crate::preview::identity(), color: [1.; 4], model: crate::preview::identity(), eye: [0., 0., -3.], albedo: None, shiny: None, specular: 0., uvs: vec![[0.; 2]; 6], equipment: Vec::new(), }; let mut cache = GpuCache::default(); let read = || { let mut rgba = [0u8; 4]; gl.read_pixels( 16, 16, 1, 1, glow::RGBA, glow::UNSIGNED_BYTE, glow::PixelPackData::Slice(&mut rgba), ); rgba }; let result = (|| { // Deliberately poison the incoming state: the callback owns its depth setup. gl.depth_mask(false); gl.clear_depth_f32(0.0); gl.depth_func(glow::GREATER); cache.pending = Some(make_frame(vec![0, 1, 2, 3, 4, 5], 1)); cache.paint(gl, &info, true); let near = read(); if near[..3].iter().any(|c| *c < 250) { return Err(format!("achterste vlak overschrijft voorste: {near:?}")); } cache.pending = Some(make_frame(vec![3, 4, 5], 2)); cache.paint(gl, &info, true); let next = read(); if next[..3].iter().any(|c| !(69..=73).contains(c)) { return Err(format!("depth niet gewist voor volgende frame: {next:?}")); } cache.pending = Some(make_frame(vec![3, 4, 5, 0, 1, 2], 3)); cache.paint(gl, &info, true); let reverse = read(); if near != reverse { return Err(format!( "draw-order afhankelijke occlusie: {near:?} / {reverse:?}" )); } let mut body = make_frame(vec![0, 1, 2], 4); let mut behind = make_frame(vec![3, 4, 5], 5); behind.color = [1., 0., 0., 1.]; body.equipment.push(behind); cache.pending = Some(body); cache.paint(gl, &info, true); let hidden = read(); if hidden != near { return Err(format!("equipment wist body-depth: {hidden:?}")); } let mut body = make_frame(vec![3, 4, 5], 6); let mut front = make_frame(vec![0, 1, 2], 7); front.albedo = Some(Arc::new(crate::equipment::Texture { content_hash: 1, width: 1, height: 1, rgba: vec![255, 0, 0, 255], })); body.equipment.push(front.clone()); cache.pending = Some(body.clone()); cache.paint(gl, &info, true); let textured = read(); if textured[0] < 250 || textured[1] > 2 || textured[2] > 2 { return Err(format!("voorste equipmenttexture ontbreekt: {textured:?}")); } front.generation = 8; front.albedo = Some(Arc::new(crate::equipment::Texture { content_hash: 2, width: 1, height: 1, rgba: vec![255, 0, 0, 0], })); body.equipment = vec![front]; cache.pending = Some(body); cache.paint(gl, &info, true); let cutout = read(); if cutout != next { return Err(format!("alpha-cutout bedekt body: {cutout:?}")); } let mut body = make_frame(vec![3, 4, 5], 9); let mut front = make_frame(vec![0, 1, 2], 10); front.color = [0., 0., 0., 1.]; front.specular = 1.; front.eye = [35000., 85000., 40000.]; front.shiny = Some(Arc::new(crate::equipment::Texture { content_hash: 3, width: 1, height: 1, rgba: vec![255, 255, 255, 255], })); body.equipment = vec![front.clone()]; cache.pending = Some(body.clone()); cache.paint(gl, &info, true); let shiny = read(); front.generation = 11; front.shiny = Some(Arc::new(crate::equipment::Texture { content_hash: 4, width: 1, height: 1, rgba: vec![0, 0, 0, 255], })); body.equipment = vec![front]; cache.pending = Some(body); cache.paint(gl, &info, true); let matte = read(); if shiny[..3].iter().any(|c| *c < 250) || matte[..3] != [0, 0, 0] { return Err(format!( "shinymap beïnvloedt specular niet correct: {shiny:?}/{matte:?}" )); } 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:?}"); Ok(()) })(); cache.destroy(gl); gl.bind_framebuffer(glow::FRAMEBUFFER, None); gl.delete_framebuffer(framebuffer); gl.delete_texture(texture); gl.delete_renderbuffer(depth); result }