BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview_gl.rs

preview_gl.rs

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