BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

preview_gl.rs 13.6 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
17const MAX_BONES: usize = crate::preview::MAX_JOINTS;
18
19pub struct Viewport {
20 cache: Arc<Mutex<GpuCache>>,
21}
22
23impl Default for Viewport {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl Viewport {
30 pub fn new() -> Self {
31 Self {
32 cache: Arc::new(Mutex::new(GpuCache::default())),
33 }
34 }
35
36 pub fn show(
37 &self,
38 ui: &mut egui::Ui,
39 controls: &mut PreviewControls,
40 character: Option<&PreviewCharacter>,
41 ) {
42 ui.horizontal_wrapped(|ui| {
43 if let Some(character) = character {
44 if !character
45 .clips
46 .iter()
47 .any(|clip| clip.name == controls.clip_name)
48 {
49 if let Some(clip) = character.clips.first() {
50 controls.select_clip(&clip.name);
51 }
52 }
53 for clip in &character.clips {
54 if ui
55 .selectable_label(controls.clip_name == clip.name, &clip.name)
56 .clicked()
57 {
58 controls.select_clip(&clip.name);
59 }
60 }
61 }
62 if ui
63 .selectable_label(
64 controls.playing,
65 if controls.playing { "Pauze" } else { "Speel" },
66 )
67 .clicked()
68 {
69 controls.playing = !controls.playing;
70 }
71 if ui.button("Yaw −").clicked() {
72 controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 - 1);
73 }
74 if ui.button("Yaw +").clicked() {
75 controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 + 1);
76 }
77 for (index, pitch) in PAPERDOLL_PITCHES_DEG.iter().enumerate() {
78 if ui.button(format!("pitch {pitch:.0}")).clicked() {
79 controls.snap_pitch_row(index as i32);
80 }
81 }
82 ui.label(format!(
83 "yaw {:.0}° / {} pitch {:.0}°",
84 controls.yaw_deg, PAPERDOLL_YAW_COUNT, controls.pitch_deg
85 ));
86 });
87 ui.label(
88 "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.",
89 );
90 let available = ui.available_rect_before_wrap();
91 let rect = egui::Rect::from_min_size(
92 available.min,
93 egui::vec2(available.width().max(240.0), available.height().max(240.0)),
94 );
95 let response = ui.allocate_rect(rect, egui::Sense::click_and_drag());
96 if response.dragged() {
97 controls.drag(response.drag_delta().x, response.drag_delta().y);
98 }
99 let scroll = ui.input(|input| input.smooth_scroll_delta.y);
100 if response.hovered() && scroll.abs() > 0.0 {
101 controls.zoom(scroll);
102 }
103 let Some(character) = character else {
104 ui.painter()
105 .rect_filled(rect, 4.0, egui::Color32::from_rgb(18, 22, 28));
106 ui.painter().text(
107 rect.center(),
108 egui::Align2::CENTER_CENTER,
109 "Geen glTF geladen. Open een project met een .glb of start met --preview-fixture.",
110 egui::FontId::proportional(14.0),
111 egui::Color32::from_rgb(210, 210, 210),
112 );
113 return;
114 };
115 let duration = clip_duration(character, &controls.clip_name);
116 let dt = ui.input(|input| input.stable_dt);
117 controls.advance(dt, duration);
118 ui.ctx().request_repaint();
119 let bones = sample_skin_matrices(character, &controls.clip_name, controls.time);
120 let aspect = rect.width() / rect.height().max(1.0);
121 let target = [0.0, 0.9, 0.0];
122 let offset = eye_offset(controls.yaw_deg, controls.pitch_deg, controls.distance);
123 let eye = [
124 target[0] + offset[0],
125 target[1] + offset[1],
126 target[2] + offset[2],
127 ];
128 let view_proj = mul(
129 perspective(38.0, aspect, 0.05, 40.0),
130 view_matrix(eye, target),
131 );
132 {
133 let mut cache = self.cache.lock().expect("preview gpu lock");
134 cache.pending = Some(PendingFrame {
135 generation: character_generation(character),
136 positions: character.positions.clone(),
137 normals: character.normals.clone(),
138 joints: character.joints.clone(),
139 weights: character.weights.clone(),
140 indices: character.indices.clone(),
141 bones,
142 view_proj,
143 color: character.base_color,
144 });
145 }
146 let cache = Arc::clone(&self.cache);
147 let callback = CallbackFn::new(move |info, painter| {
148 if let Ok(mut cache) = cache.lock() {
149 unsafe { cache.paint(painter.gl(), &info) };
150 }
151 });
152 ui.painter().add(egui::PaintCallback {
153 rect,
154 callback: Arc::new(callback),
155 });
156 }
157}
158
159fn character_generation(character: &PreviewCharacter) -> u64 {
160 use std::hash::{Hash, Hasher};
161 let mut hash = std::collections::hash_map::DefaultHasher::new();
162 for point in &character.positions {
163 point.map(f32::to_bits).hash(&mut hash);
164 }
165 for normal in &character.normals {
166 normal.map(f32::to_bits).hash(&mut hash);
167 }
168 for weights in &character.weights {
169 weights.map(f32::to_bits).hash(&mut hash);
170 }
171 character.joints.hash(&mut hash);
172 character.indices.hash(&mut hash);
173 hash.finish()
174}
175
176#[derive(Clone)]
177struct PendingFrame {
178 generation: u64,
179 positions: Vec<[f32; 3]>,
180 normals: Vec<[f32; 3]>,
181 joints: Vec<[u16; 4]>,
182 weights: Vec<[f32; 4]>,
183 indices: Vec<u32>,
184 bones: Vec<crate::preview::Mat4>,
185 view_proj: crate::preview::Mat4,
186 color: [f32; 4],
187}
188
189#[derive(Default)]
190struct GpuCache {
191 program: Option<glow::Program>,
192 vao: Option<glow::VertexArray>,
193 vbo: Option<glow::Buffer>,
194 ebo: Option<glow::Buffer>,
195 uploaded: u64,
196 index_count: i32,
197 pending: Option<PendingFrame>,
198 error: Option<String>,
199}
200
201impl GpuCache {
202 unsafe fn paint(&mut self, gl: &glow::Context, info: &egui::PaintCallbackInfo) {
203 let Some(frame) = self.pending.clone() else {
204 return;
205 };
206 if let Err(error) = self.ensure_program(gl) {
207 self.error = Some(error);
208 return;
209 }
210 if self.uploaded != frame.generation {
211 if let Err(error) = self.upload(gl, &frame) {
212 self.error = Some(error);
213 return;
214 }
215 }
216 let Some(program) = self.program else {
217 return;
218 };
219 let viewport = info.viewport_in_pixels();
220 if viewport.width_px <= 0 || viewport.height_px <= 0 {
221 return;
222 }
223 gl.enable(glow::SCISSOR_TEST);
224 gl.scissor(
225 viewport.left_px,
226 viewport.from_bottom_px,
227 viewport.width_px,
228 viewport.height_px,
229 );
230 gl.viewport(
231 viewport.left_px,
232 viewport.from_bottom_px,
233 viewport.width_px,
234 viewport.height_px,
235 );
236 gl.enable(glow::DEPTH_TEST);
237 gl.depth_func(glow::LESS);
238 gl.disable(glow::BLEND);
239 gl.clear_color(0.08, 0.09, 0.12, 1.0);
240 gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
241 gl.use_program(Some(program));
242 let view = gl.get_uniform_location(program, "u_view_proj");
243 gl.uniform_matrix_4_f32_slice(view.as_ref(), false, &frame.view_proj);
244 let color = gl.get_uniform_location(program, "u_color");
245 gl.uniform_4_f32(
246 color.as_ref(),
247 frame.color[0],
248 frame.color[1],
249 frame.color[2],
250 frame.color[3],
251 );
252 let mut bones = vec![crate::preview::identity(); MAX_BONES];
253 for (index, bone) in frame.bones.iter().take(MAX_BONES).enumerate() {
254 bones[index] = *bone;
255 }
256 let flat: Vec<f32> = bones.iter().flat_map(|bone| *bone).collect();
257 let bones_loc = gl.get_uniform_location(program, "u_bones");
258 gl.uniform_matrix_4_f32_slice(bones_loc.as_ref(), false, &flat);
259 gl.bind_vertex_array(self.vao);
260 gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0);
261 gl.bind_vertex_array(None);
262 gl.disable(glow::DEPTH_TEST);
263 gl.disable(glow::SCISSOR_TEST);
264 gl.enable(glow::BLEND);
265 }
266
267 unsafe fn ensure_program(&mut self, gl: &glow::Context) -> Result<(), String> {
268 if self.program.is_some() {
269 return Ok(());
270 }
271 let program = link(gl, VERTEX, FRAGMENT)?;
272 let vao = gl.create_vertex_array().map_err(|err| err.to_string())?;
273 let vbo = gl.create_buffer().map_err(|err| err.to_string())?;
274 let ebo = gl.create_buffer().map_err(|err| err.to_string())?;
275 self.program = Some(program);
276 self.vao = Some(vao);
277 self.vbo = Some(vbo);
278 self.ebo = Some(ebo);
279 Ok(())
280 }
281
282 unsafe fn upload(&mut self, gl: &glow::Context, frame: &PendingFrame) -> Result<(), String> {
283 let mut interleaved = Vec::with_capacity(frame.positions.len() * 14);
284 for index in 0..frame.positions.len() {
285 let position = frame.positions[index];
286 let normal = frame.normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]);
287 let joint = frame.joints.get(index).copied().unwrap_or([0, 0, 0, 0]);
288 let weight = frame
289 .weights
290 .get(index)
291 .copied()
292 .unwrap_or([1.0, 0.0, 0.0, 0.0]);
293 interleaved.extend_from_slice(&position);
294 interleaved.extend_from_slice(&normal);
295 interleaved.extend([
296 joint[0] as f32,
297 joint[1] as f32,
298 joint[2] as f32,
299 joint[3] as f32,
300 ]);
301 interleaved.extend_from_slice(&weight);
302 }
303 gl.bind_vertex_array(self.vao);
304 gl.bind_buffer(glow::ARRAY_BUFFER, self.vbo);
305 gl.buffer_data_u8_slice(
306 glow::ARRAY_BUFFER,
307 bytemuck_f32(&interleaved),
308 glow::DYNAMIC_DRAW,
309 );
310 gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.ebo);
311 gl.buffer_data_u8_slice(
312 glow::ELEMENT_ARRAY_BUFFER,
313 bytemuck_u32(&frame.indices),
314 glow::DYNAMIC_DRAW,
315 );
316 let stride = 14 * 4;
317 enable_float(gl, 0, 3, stride, 0);
318 enable_float(gl, 1, 3, stride, 3 * 4);
319 enable_float(gl, 2, 4, stride, 6 * 4);
320 enable_float(gl, 3, 4, stride, 10 * 4);
321 self.uploaded = frame.generation;
322 self.index_count = frame.indices.len() as i32;
323 Ok(())
324 }
325}
326
327unsafe fn enable_float(gl: &glow::Context, index: u32, size: i32, stride: i32, offset: i32) {
328 gl.enable_vertex_attrib_array(index);
329 gl.vertex_attrib_pointer_f32(index, size, glow::FLOAT, false, stride, offset);
330}
331
332fn bytemuck_f32(values: &[f32]) -> &[u8] {
333 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
334}
335
336fn bytemuck_u32(values: &[u32]) -> &[u8] {
337 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
338}
339
340unsafe fn link(gl: &glow::Context, vertex: &str, fragment: &str) -> Result<glow::Program, String> {
341 let program = gl.create_program().map_err(|err| err.to_string())?;
342 let vs = compile(gl, glow::VERTEX_SHADER, vertex)?;
343 let fs = compile(gl, glow::FRAGMENT_SHADER, fragment)?;
344 gl.attach_shader(program, vs);
345 gl.attach_shader(program, fs);
346 gl.link_program(program);
347 if !gl.get_program_link_status(program) {
348 return Err(gl.get_program_info_log(program));
349 }
350 gl.delete_shader(vs);
351 gl.delete_shader(fs);
352 Ok(program)
353}
354
355unsafe fn compile(gl: &glow::Context, stage: u32, source: &str) -> Result<glow::Shader, String> {
356 let shader = gl.create_shader(stage).map_err(|err| err.to_string())?;
357 gl.shader_source(shader, source);
358 gl.compile_shader(shader);
359 if !gl.get_shader_compile_status(shader) {
360 return Err(gl.get_shader_info_log(shader));
361 }
362 Ok(shader)
363}
364
365const VERTEX: &str = r#"#version 330
366layout(location = 0) in vec3 a_pos;
367layout(location = 1) in vec3 a_normal;
368layout(location = 2) in vec4 a_joints;
369layout(location = 3) in vec4 a_weights;
370uniform mat4 u_view_proj;
371uniform mat4 u_bones[128];
372out vec3 v_normal;
373void main() {
374 int j0 = int(a_joints.x);
375 int j1 = int(a_joints.y);
376 int j2 = int(a_joints.z);
377 int j3 = int(a_joints.w);
378 mat4 skin = a_weights.x * u_bones[j0]
379 + a_weights.y * u_bones[j1]
380 + a_weights.z * u_bones[j2]
381 + a_weights.w * u_bones[j3];
382 vec4 world = skin * vec4(a_pos, 1.0);
383 v_normal = mat3(skin) * a_normal;
384 gl_Position = u_view_proj * world;
385}
386"#;
387
388const FRAGMENT: &str = r#"#version 330
389in vec3 v_normal;
390uniform vec4 u_color;
391out vec4 frag;
392void main() {
393 vec3 n = normalize(v_normal);
394 vec3 light = normalize(vec3(0.35, 0.85, 0.4));
395 float shade = 0.28 + 0.72 * max(dot(n, light), 0.0);
396 frag = vec4(u_color.rgb * shade, 1.0);
397}
398"#;