BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

preview_gl.rs 21.8 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 destroy(&self, gl: &glow::Context) {
37 if let Ok(mut cache) = self.cache.lock() {
38 unsafe { cache.destroy(gl) }
39 }
40 }
41
42 pub fn show(
43 &self,
44 ui: &mut egui::Ui,
45 controls: &mut PreviewControls,
46 character: Option<&PreviewCharacter>,
47 presets: &[&crate::equipment::Preset],
48 ) {
49 ui.horizontal_wrapped(|ui| {
50 if let Some(character) = character {
51 if !character
52 .clips
53 .iter()
54 .any(|clip| clip.name == controls.clip_name)
55 {
56 if let Some(clip) = character.clips.first() {
57 controls.select_clip(&clip.name);
58 }
59 }
60 for clip in &character.clips {
61 if ui
62 .selectable_label(controls.clip_name == clip.name, &clip.name)
63 .clicked()
64 {
65 controls.select_clip(&clip.name);
66 }
67 }
68 }
69 if ui
70 .selectable_label(
71 controls.playing,
72 if controls.playing { "Pauze" } else { "Speel" },
73 )
74 .clicked()
75 {
76 controls.playing = !controls.playing;
77 }
78 if ui.button("Yaw −").clicked() {
79 controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 - 1);
80 }
81 if ui.button("Yaw +").clicked() {
82 controls.snap_yaw_index((controls.yaw_deg / 30.0).round() as i32 + 1);
83 }
84 for (index, pitch) in PAPERDOLL_PITCHES_DEG.iter().enumerate() {
85 if ui.button(format!("pitch {pitch:.0}")).clicked() {
86 controls.snap_pitch_row(index as i32);
87 }
88 }
89 ui.label(format!(
90 "yaw {:.0}° / {} pitch {:.0}°",
91 controls.yaw_deg, PAPERDOLL_YAW_COUNT, controls.pitch_deg
92 ));
93 });
94 ui.label(
95 "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.",
96 );
97 let available = ui.available_rect_before_wrap();
98 let rect = egui::Rect::from_min_size(
99 available.min,
100 egui::vec2(available.width().max(240.0), available.height().max(240.0)),
101 );
102 let response = ui.allocate_rect(rect, egui::Sense::click_and_drag());
103 if response.dragged() {
104 controls.drag(response.drag_delta().x, response.drag_delta().y);
105 }
106 let scroll = ui.input(|input| input.smooth_scroll_delta.y);
107 if response.hovered() && scroll.abs() > 0.0 {
108 controls.zoom(scroll);
109 }
110 let Some(character) = character else {
111 ui.painter()
112 .rect_filled(rect, 4.0, egui::Color32::from_rgb(18, 22, 28));
113 ui.painter().text(
114 rect.center(),
115 egui::Align2::CENTER_CENTER,
116 "Geen glTF geladen. Open een project met een .glb of start met --preview-fixture.",
117 egui::FontId::proportional(14.0),
118 egui::Color32::from_rgb(210, 210, 210),
119 );
120 return;
121 };
122 let duration = clip_duration(character, &controls.clip_name);
123 let dt = ui.input(|input| input.stable_dt);
124 controls.advance(dt, duration);
125 ui.ctx().request_repaint();
126 let bones = sample_skin_matrices(character, &controls.clip_name, controls.time);
127 let aspect = rect.width() / rect.height().max(1.0);
128 let target = [0.0, 0.9, 0.0];
129 let offset = eye_offset(controls.yaw_deg, controls.pitch_deg, controls.distance);
130 let eye = [
131 target[0] + offset[0],
132 target[1] + offset[1],
133 target[2] + offset[2],
134 ];
135 let view_proj = mul(
136 perspective(38.0, aspect, 0.05, 40.0),
137 view_matrix(eye, target),
138 );
139 let mut cache = self.cache.lock().expect("preview gpu lock");
140 let mut diagnostics = Vec::new();
141 let mut equipment = Vec::new();
142 let sex = if controls.sex == crate::preview::PreviewSex::Male {
143 0
144 } else {
145 1
146 };
147 for preset in presets {
148 match crate::equipment::layers_cached(
149 preset,
150 sex,
151 character,
152 controls,
153 &mut cache.texture_cache,
154 ) {
155 Ok(layers) => {
156 for layer in layers {
157 equipment.push(PendingFrame::from_layer(layer, view_proj, eye));
158 }
159 }
160 Err(error) => {
161 diagnostics.push(error);
162 }
163 }
164 }
165 {
166 cache.pending = Some(PendingFrame {
167 generation: character_generation(character),
168 positions: character.positions.clone(),
169 normals: character.normals.clone(),
170 joints: character.joints.clone(),
171 weights: character.weights.clone(),
172 indices: character.indices.clone(),
173 bones,
174 view_proj,
175 color: character.base_color,
176 model: crate::preview::identity(),
177 albedo: None,
178 shiny: None,
179 specular: 0.0,
180 eye,
181 equipment,
182 uvs: Vec::new(),
183 });
184 }
185 drop(cache);
186 let cache = Arc::clone(&self.cache);
187 let callback = CallbackFn::new(move |info, painter| {
188 if let Ok(mut cache) = cache.lock() {
189 unsafe { cache.paint(painter.gl(), &info, true) };
190 }
191 });
192 ui.painter().add(egui::PaintCallback {
193 rect,
194 callback: Arc::new(callback),
195 });
196 if let Ok(cache) = self.cache.lock() {
197 if let Some(error) = &cache.error {
198 diagnostics.push(error.clone());
199 }
200 for layer in &cache.layers {
201 if let Some(error) = &layer.error {
202 diagnostics.push(error.clone());
203 }
204 }
205 }
206 if !diagnostics.is_empty() {
207 ui.painter().text(
208 rect.left_top() + egui::vec2(12., 12.),
209 egui::Align2::LEFT_TOP,
210 diagnostics.join("\n"),
211 egui::FontId::proportional(14.),
212 egui::Color32::LIGHT_RED,
213 );
214 }
215 }
216}
217
218fn character_generation(character: &PreviewCharacter) -> u64 {
219 use std::hash::{Hash, Hasher};
220 let mut hash = std::collections::hash_map::DefaultHasher::new();
221 for point in &character.positions {
222 point.map(f32::to_bits).hash(&mut hash);
223 }
224 for normal in &character.normals {
225 normal.map(f32::to_bits).hash(&mut hash);
226 }
227 for weights in &character.weights {
228 weights.map(f32::to_bits).hash(&mut hash);
229 }
230 character.joints.hash(&mut hash);
231 character.indices.hash(&mut hash);
232 hash.finish()
233}
234
235#[derive(Clone)]
236struct PendingFrame {
237 generation: u64,
238 positions: Vec<[f32; 3]>,
239 normals: Vec<[f32; 3]>,
240 joints: Vec<[u16; 4]>,
241 weights: Vec<[f32; 4]>,
242 indices: Vec<u32>,
243 bones: Vec<crate::preview::Mat4>,
244 view_proj: crate::preview::Mat4,
245 color: [f32; 4],
246 model: crate::preview::Mat4,
247 eye: [f32; 3],
248 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
249 shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
250 specular: f32,
251 equipment: Vec<PendingFrame>,
252 uvs: Vec<[f32; 2]>,
253}
254
255impl PendingFrame {
256 fn from_layer(
257 layer: crate::equipment::Layer,
258 view_proj: crate::preview::Mat4,
259 eye: [f32; 3],
260 ) -> Self {
261 use std::hash::{Hash, Hasher};
262 let mut hash = std::collections::hash_map::DefaultHasher::new();
263 for v in &layer.mesh.positions {
264 v.map(f32::to_bits).hash(&mut hash);
265 }
266 for v in &layer.mesh.normals {
267 v.map(f32::to_bits).hash(&mut hash);
268 }
269 for v in &layer.mesh.uvs {
270 v.map(f32::to_bits).hash(&mut hash);
271 }
272 layer.mesh.indices.hash(&mut hash);
273 if let Some(t) = &layer.albedo {
274 t.content_hash.hash(&mut hash);
275 t.width.hash(&mut hash);
276 t.height.hash(&mut hash);
277 }
278 if let Some(t) = &layer.shiny {
279 t.content_hash.hash(&mut hash);
280 t.width.hash(&mut hash);
281 t.height.hash(&mut hash);
282 }
283 Self {
284 generation: hash.finish(),
285 positions: layer.mesh.positions,
286 normals: layer.mesh.normals,
287 indices: layer.mesh.indices,
288 uvs: layer.mesh.uvs,
289 color: layer.mesh.color,
290 joints: Vec::new(),
291 weights: Vec::new(),
292 bones: Vec::new(),
293 model: layer.model,
294 view_proj,
295 eye,
296 albedo: layer.albedo,
297 shiny: layer.shiny,
298 specular: layer.specular,
299 equipment: Vec::new(),
300 }
301 }
302}
303
304#[derive(Default)]
305struct GpuCache {
306 program: Option<glow::Program>,
307 vao: Option<glow::VertexArray>,
308 vbo: Option<glow::Buffer>,
309 ebo: Option<glow::Buffer>,
310 uploaded: u64,
311 index_count: i32,
312 pending: Option<PendingFrame>,
313 error: Option<String>,
314 textures: [Option<glow::Texture>; 2],
315 layers: Vec<GpuCache>,
316 texture_cache: crate::equipment::TextureCache,
317}
318
319impl GpuCache {
320 unsafe fn paint(&mut self, gl: &glow::Context, info: &egui::PaintCallbackInfo, clear: bool) {
321 let Some(frame) = self.pending.clone() else {
322 return;
323 };
324 if let Err(error) = self.ensure_program(gl) {
325 self.error = Some(error);
326 return;
327 }
328 if self.uploaded != frame.generation {
329 if let Err(error) = self.upload(gl, &frame) {
330 self.error = Some(error);
331 return;
332 }
333 }
334 let Some(program) = self.program else {
335 return;
336 };
337 let viewport = info.viewport_in_pixels();
338 if viewport.width_px <= 0 || viewport.height_px <= 0 {
339 return;
340 }
341 gl.enable(glow::SCISSOR_TEST);
342 gl.scissor(
343 viewport.left_px,
344 viewport.from_bottom_px,
345 viewport.width_px,
346 viewport.height_px,
347 );
348 gl.viewport(
349 viewport.left_px,
350 viewport.from_bottom_px,
351 viewport.width_px,
352 viewport.height_px,
353 );
354 gl.enable(glow::DEPTH_TEST);
355 gl.depth_func(glow::LESS);
356 gl.disable(glow::BLEND);
357 gl.clear_color(0.08, 0.09, 0.12, 1.0);
358 if clear {
359 gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
360 }
361 gl.use_program(Some(program));
362 let view = gl.get_uniform_location(program, "u_view_proj");
363 gl.uniform_matrix_4_f32_slice(view.as_ref(), false, &frame.view_proj);
364 gl.uniform_matrix_4_f32_slice(
365 gl.get_uniform_location(program, "u_model").as_ref(),
366 false,
367 &frame.model,
368 );
369 gl.uniform_3_f32(
370 gl.get_uniform_location(program, "u_eye").as_ref(),
371 frame.eye[0],
372 frame.eye[1],
373 frame.eye[2],
374 );
375 gl.uniform_1_f32(
376 gl.get_uniform_location(program, "u_specular").as_ref(),
377 frame.specular,
378 );
379 for (unit, name, present) in [
380 (0, "u_albedo", frame.albedo.is_some()),
381 (1, "u_shiny", frame.shiny.is_some()),
382 ] {
383 gl.active_texture(glow::TEXTURE0 + unit as u32);
384 gl.bind_texture(glow::TEXTURE_2D, self.textures[unit]);
385 gl.uniform_1_i32(gl.get_uniform_location(program, name).as_ref(), unit as i32);
386 gl.uniform_1_i32(
387 gl.get_uniform_location(program, &format!("{name}_enabled"))
388 .as_ref(),
389 i32::from(present),
390 );
391 }
392 let color = gl.get_uniform_location(program, "u_color");
393 gl.uniform_4_f32(
394 color.as_ref(),
395 frame.color[0],
396 frame.color[1],
397 frame.color[2],
398 frame.color[3],
399 );
400 let mut bones = vec![crate::preview::identity(); MAX_BONES];
401 for (index, bone) in frame.bones.iter().take(MAX_BONES).enumerate() {
402 bones[index] = *bone;
403 }
404 let flat: Vec<f32> = bones.iter().flat_map(|bone| *bone).collect();
405 let bones_loc = gl.get_uniform_location(program, "u_bones");
406 gl.uniform_matrix_4_f32_slice(bones_loc.as_ref(), false, &flat);
407 gl.bind_vertex_array(self.vao);
408 gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0);
409 gl.bind_vertex_array(None);
410 while self.layers.len() > frame.equipment.len() {
411 if let Some(mut layer) = self.layers.pop() {
412 layer.destroy(gl);
413 }
414 }
415 while self.layers.len() < frame.equipment.len() {
416 self.layers.push(GpuCache::default());
417 }
418 for (cache, frame) in self.layers.iter_mut().zip(frame.equipment) {
419 cache.pending = Some(frame);
420 cache.paint(gl, info, false);
421 }
422 gl.active_texture(glow::TEXTURE0);
423 gl.disable(glow::DEPTH_TEST);
424 gl.disable(glow::SCISSOR_TEST);
425 gl.enable(glow::BLEND);
426 }
427
428 unsafe fn destroy(&mut self, gl: &glow::Context) {
429 for layer in &mut self.layers {
430 layer.destroy(gl);
431 }
432 for texture in &mut self.textures {
433 if let Some(t) = texture.take() {
434 gl.delete_texture(t)
435 }
436 }
437 if let Some(v) = self.vao.take() {
438 gl.delete_vertex_array(v)
439 }
440 if let Some(v) = self.vbo.take() {
441 gl.delete_buffer(v)
442 }
443 if let Some(v) = self.ebo.take() {
444 gl.delete_buffer(v)
445 }
446 if let Some(p) = self.program.take() {
447 gl.delete_program(p)
448 }
449 }
450 unsafe fn ensure_program(&mut self, gl: &glow::Context) -> Result<(), String> {
451 if self.program.is_some() {
452 return Ok(());
453 }
454 let program = link(gl, VERTEX, FRAGMENT)?;
455 let vao = gl.create_vertex_array().map_err(|err| err.to_string())?;
456 let vbo = gl.create_buffer().map_err(|err| err.to_string())?;
457 let ebo = gl.create_buffer().map_err(|err| err.to_string())?;
458 self.program = Some(program);
459 self.vao = Some(vao);
460 self.vbo = Some(vbo);
461 self.ebo = Some(ebo);
462 Ok(())
463 }
464
465 unsafe fn upload(&mut self, gl: &glow::Context, frame: &PendingFrame) -> Result<(), String> {
466 let mut interleaved = Vec::with_capacity(frame.positions.len() * 16);
467 for index in 0..frame.positions.len() {
468 let position = frame.positions[index];
469 let normal = frame.normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]);
470 let joint = frame.joints.get(index).copied().unwrap_or([0, 0, 0, 0]);
471 let weight = frame
472 .weights
473 .get(index)
474 .copied()
475 .unwrap_or([1.0, 0.0, 0.0, 0.0]);
476 interleaved.extend_from_slice(&position);
477 interleaved.extend_from_slice(&normal);
478 interleaved.extend([
479 joint[0] as f32,
480 joint[1] as f32,
481 joint[2] as f32,
482 joint[3] as f32,
483 ]);
484 interleaved.extend_from_slice(&weight);
485 interleaved.extend_from_slice(&frame.uvs.get(index).copied().unwrap_or([0.; 2]));
486 }
487 gl.bind_vertex_array(self.vao);
488 gl.bind_buffer(glow::ARRAY_BUFFER, self.vbo);
489 gl.buffer_data_u8_slice(
490 glow::ARRAY_BUFFER,
491 bytemuck_f32(&interleaved),
492 glow::DYNAMIC_DRAW,
493 );
494 gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.ebo);
495 gl.buffer_data_u8_slice(
496 glow::ELEMENT_ARRAY_BUFFER,
497 bytemuck_u32(&frame.indices),
498 glow::DYNAMIC_DRAW,
499 );
500 let stride = 16 * 4;
501 enable_float(gl, 0, 3, stride, 0);
502 enable_float(gl, 1, 3, stride, 3 * 4);
503 enable_float(gl, 2, 4, stride, 6 * 4);
504 enable_float(gl, 3, 4, stride, 10 * 4);
505 enable_float(gl, 4, 2, stride, 14 * 4);
506 for (unit, texture) in [&frame.albedo, &frame.shiny].iter().enumerate() {
507 if let Some(old) = self.textures[unit].take() {
508 gl.delete_texture(old);
509 }
510 if let Some(texture) = texture {
511 let id = gl.create_texture().map_err(|e| e.to_string())?;
512 self.textures[unit] = Some(id);
513 gl.active_texture(glow::TEXTURE0 + unit as u32);
514 gl.bind_texture(glow::TEXTURE_2D, Some(id));
515 gl.tex_parameter_i32(
516 glow::TEXTURE_2D,
517 glow::TEXTURE_MIN_FILTER,
518 glow::NEAREST as i32,
519 );
520 gl.tex_parameter_i32(
521 glow::TEXTURE_2D,
522 glow::TEXTURE_MAG_FILTER,
523 glow::NEAREST as i32,
524 );
525 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
526 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
527 gl.tex_image_2d(
528 glow::TEXTURE_2D,
529 0,
530 glow::RGBA8 as i32,
531 texture.width as i32,
532 texture.height as i32,
533 0,
534 glow::RGBA,
535 glow::UNSIGNED_BYTE,
536 Some(&texture.rgba),
537 );
538 }
539 }
540 self.uploaded = frame.generation;
541 self.index_count = frame.indices.len() as i32;
542 Ok(())
543 }
544}
545
546unsafe fn enable_float(gl: &glow::Context, index: u32, size: i32, stride: i32, offset: i32) {
547 gl.enable_vertex_attrib_array(index);
548 gl.vertex_attrib_pointer_f32(index, size, glow::FLOAT, false, stride, offset);
549}
550
551fn bytemuck_f32(values: &[f32]) -> &[u8] {
552 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
553}
554
555fn bytemuck_u32(values: &[u32]) -> &[u8] {
556 unsafe { std::slice::from_raw_parts(values.as_ptr().cast(), std::mem::size_of_val(values)) }
557}
558
559unsafe fn link(gl: &glow::Context, vertex: &str, fragment: &str) -> Result<glow::Program, String> {
560 let program = gl.create_program().map_err(|err| err.to_string())?;
561 let vs = compile(gl, glow::VERTEX_SHADER, vertex)?;
562 let fs = compile(gl, glow::FRAGMENT_SHADER, fragment)?;
563 gl.attach_shader(program, vs);
564 gl.attach_shader(program, fs);
565 gl.link_program(program);
566 if !gl.get_program_link_status(program) {
567 return Err(gl.get_program_info_log(program));
568 }
569 gl.delete_shader(vs);
570 gl.delete_shader(fs);
571 Ok(program)
572}
573
574unsafe fn compile(gl: &glow::Context, stage: u32, source: &str) -> Result<glow::Shader, String> {
575 let shader = gl.create_shader(stage).map_err(|err| err.to_string())?;
576 gl.shader_source(shader, source);
577 gl.compile_shader(shader);
578 if !gl.get_shader_compile_status(shader) {
579 return Err(gl.get_shader_info_log(shader));
580 }
581 Ok(shader)
582}
583
584const VERTEX: &str = r#"#version 330
585layout(location = 0) in vec3 a_pos;
586layout(location = 1) in vec3 a_normal;
587layout(location = 2) in vec4 a_joints;
588layout(location = 3) in vec4 a_weights;
589layout(location = 4) in vec2 a_uv;
590uniform mat4 u_model;
591out vec2 v_uv;
592out vec3 v_world;
593uniform mat4 u_view_proj;
594uniform mat4 u_bones[128];
595out vec3 v_normal;
596void main() {
597 int j0 = int(a_joints.x);
598 int j1 = int(a_joints.y);
599 int j2 = int(a_joints.z);
600 int j3 = int(a_joints.w);
601 mat4 skin = a_weights.x * u_bones[j0]
602 + a_weights.y * u_bones[j1]
603 + a_weights.z * u_bones[j2]
604 + a_weights.w * u_bones[j3];
605 vec4 world = u_model * skin * vec4(a_pos, 1.0);
606 v_uv = vec2(a_uv.x,1.0-a_uv.y); v_world=world.xyz;
607 v_normal = transpose(inverse(mat3(u_model * skin))) * a_normal;
608 gl_Position = u_view_proj * world;
609}
610"#;
611
612const FRAGMENT: &str = r#"#version 330
613in vec3 v_normal;
614uniform vec4 u_color;
615uniform sampler2D u_albedo;
616uniform sampler2D u_shiny;
617uniform bool u_albedo_enabled;
618uniform bool u_shiny_enabled;
619uniform float u_specular;
620uniform vec3 u_eye;
621in vec2 v_uv;
622in vec3 v_world;
623out vec4 frag;
624void main() {
625 vec3 n = normalize(v_normal);
626 vec3 light = normalize(vec3(0.35, 0.85, 0.4));
627 float shade = 0.28 + 0.72 * max(dot(n, light), 0.0);
628 vec4 color=u_color;
629 if(u_albedo_enabled) color*=texture(u_albedo,v_uv);
630 if(color.a<0.5) discard;
631 float strength=u_specular;
632 if(u_shiny_enabled) strength*=dot(texture(u_shiny,v_uv).rgb,vec3(0.2126,0.7152,0.0722));
633 float highlight=pow(max(dot(reflect(-light,n),normalize(u_eye-v_world)),0.0),32.0)*strength;
634 frag = vec4(clamp(color.rgb * shade + highlight,0.0,1.0), 1.0);
635}
636"#;