1mod depth_visual;
2mod timeline;
3
4use crate::{
5 Scene, SceneConstructor,
6 core::{
7 SealedRanimScene,
8 color::{self, LinearSrgb},
9 store::CoreItemStore,
10 },
11 render::{
12 Renderer,
13 resource::{RenderPool, RenderTextures},
14 utils::WgpuContext,
15 },
16};
17use async_channel::{Receiver, Sender, unbounded};
18use depth_visual::DepthVisualPipeline;
19use eframe::egui;
20use timeline::TimelineState;
21use tracing::{error, info};
22use web_time::Instant;
23
24#[cfg(target_arch = "wasm32")]
25use wasm_bindgen::prelude::*;
26
27pub struct TimelineInfoState {
29 pub ctx: egui::Context,
30 pub canvas: egui::Rect,
31 pub response: egui::Response,
32 pub painter: egui::Painter,
33 pub text_height: f32,
34 pub font_id: egui::FontId,
35}
36
37impl TimelineInfoState {
38 pub fn point_from_ms(&self, state: &TimelineState, ms: i64) -> f32 {
39 let ms = ms as f32;
40 let offset = state.offset_points;
41 let width_sec = state.width_sec as f32;
42 let canvas_width = self.canvas.width();
43
44 let ms_per_pixel = width_sec * 1000.0 / canvas_width;
45 let x = ms / ms_per_pixel;
46 self.canvas.min.x + x - offset
47 }
48}
49
50pub enum RanimPreviewAppCmd {
51 ReloadScene(Scene, Sender<()>),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum ViewMode {
56 Output,
57 Depth,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct Resolution {
62 pub width: u32,
63 pub height: u32,
64}
65
66impl Resolution {
67 pub const fn new(width: u32, height: u32) -> Self {
68 Self { width, height }
69 }
70
71 pub fn ratio(&self) -> f32 {
72 self.width as f32 / self.height as f32
73 }
74
75 pub fn aspect_ratio(&self) -> (u32, u32) {
77 fn gcd(a: u32, b: u32) -> u32 {
78 if b == 0 { a } else { gcd(b, a % b) }
79 }
80 let g = gcd(self.width, self.height);
81 (self.width / g, self.height / g)
82 }
83
84 pub fn aspect_ratio_str(&self) -> String {
85 let (w, h) = self.aspect_ratio();
86 format!("{w}:{h}")
87 }
88}
89
90impl Resolution {
92 pub const HD: Self = Self::new(1280, 720);
94 pub const FHD: Self = Self::new(1920, 1080);
95 pub const QHD: Self = Self::new(2560, 1440);
96 pub const UHD: Self = Self::new(3840, 2160);
97 pub const WXGA: Self = Self::new(1280, 800);
99 pub const WUXGA: Self = Self::new(1920, 1200);
100 pub const SVGA: Self = Self::new(800, 600);
102 pub const XGA: Self = Self::new(1024, 768);
103 pub const SXGA: Self = Self::new(1280, 960);
104 pub const _1K_SQUARE: Self = Self::new(1080, 1080);
106 pub const _2K_SQUARE: Self = Self::new(2160, 2160);
107 pub const UW_QHD: Self = Self::new(3440, 1440);
109}
110
111pub struct RanimPreviewApp {
112 cmd_rx: Receiver<RanimPreviewAppCmd>,
113 pub cmd_tx: Sender<RanimPreviewAppCmd>,
114 #[allow(unused)]
115 title: String,
116 clear_color: wgpu::Color,
117 resolution: Resolution,
118 timeline: SealedRanimScene,
119 need_eval: bool,
120 last_sec: f64,
121 store: CoreItemStore,
122 pool: RenderPool,
123 timeline_state: TimelineState,
124 play_prev_t: Option<Instant>,
125
126 renderer: Option<Renderer>,
128 render_textures: Option<RenderTextures>,
129 texture_id: Option<egui::TextureId>,
130 depth_texture_id: Option<egui::TextureId>,
131 view_mode: ViewMode,
132 wgpu_ctx: Option<WgpuContext>,
133 last_render_time: Option<std::time::Duration>,
134 last_eval_time: Option<std::time::Duration>,
135
136 depth_visual_pipeline: Option<DepthVisualPipeline>,
138 depth_visual_texture: Option<wgpu::Texture>,
139 depth_visual_view: Option<wgpu::TextureView>,
140
141 resolution_dirty: bool,
143}
144
145impl RanimPreviewApp {
146 pub fn new(scene_constructor: impl SceneConstructor, title: String) -> Self {
147 let t = Instant::now();
148 info!("building scene...");
149 let timeline = scene_constructor.build_scene();
150 info!("Scene built, cost: {:?}", t.elapsed());
151
152 info!("Getting timelines info...");
153 let timeline_infos = timeline.get_timeline_infos();
154 info!("Total {} timelines", timeline_infos.len());
155
156 let (cmd_tx, cmd_rx) = unbounded();
157
158 Self {
159 cmd_rx,
160 cmd_tx,
161 title,
162 clear_color: wgpu::Color::TRANSPARENT,
163 resolution: Resolution::QHD,
164 timeline_state: TimelineState::new(timeline.total_secs(), timeline_infos),
165 timeline,
166 need_eval: false,
167 last_sec: -1.0,
168 store: CoreItemStore::default(),
169 pool: RenderPool::new(),
170 play_prev_t: None,
171 renderer: None,
172 render_textures: None,
173 texture_id: None,
174 depth_texture_id: None,
175 view_mode: ViewMode::Output,
176 wgpu_ctx: None,
177 last_render_time: None,
178 last_eval_time: None,
179 depth_visual_pipeline: None,
180 depth_visual_texture: None,
181 depth_visual_view: None,
182 resolution_dirty: false,
183 }
184 }
185
186 pub fn set_clear_color_str(&mut self, color: &str) {
188 let bg = color::try_color(color)
189 .unwrap_or(color::color("#333333ff"))
190 .convert::<LinearSrgb>();
191 let [r, g, b, a] = bg.components.map(|x| x as f64);
192 let clear_color = wgpu::Color { r, g, b, a };
193 self.set_clear_color(clear_color);
194 }
195
196 pub fn set_clear_color(&mut self, color: wgpu::Color) {
198 self.clear_color = color;
199 }
200
201 pub fn set_resolution(&mut self, resolution: Resolution) {
203 if self.resolution != resolution {
204 self.resolution = resolution;
205 self.resolution_dirty = true;
206 }
207 }
208
209 fn calculate_oit_layers(&self, ctx: &WgpuContext, width: u32, height: u32) -> usize {
211 const BYTES_PER_PIXEL_PER_LAYER: usize = 8; const MAX_OIT_LAYERS: usize = 8;
213
214 let limits = ctx.device.limits();
215 let max_buffer_size = limits.max_storage_buffer_binding_size as usize;
216 let pixel_count = (width * height) as usize;
217 let max_layers_by_buffer = max_buffer_size / (pixel_count * BYTES_PER_PIXEL_PER_LAYER);
218 let oit_layers = max_layers_by_buffer.clamp(1, MAX_OIT_LAYERS);
219
220 if oit_layers < MAX_OIT_LAYERS {
221 tracing::warn!(
222 "OIT layers reduced from {} to {} due to GPU buffer size limit ({}MB @ {}x{})",
223 MAX_OIT_LAYERS,
224 oit_layers,
225 max_buffer_size / 1024 / 1024,
226 width,
227 height
228 );
229 }
230
231 oit_layers
232 }
233
234 fn handle_events(&mut self) {
235 if let Ok(cmd) = self.cmd_rx.try_recv() {
236 match cmd {
237 RanimPreviewAppCmd::ReloadScene(scene, tx) => {
238 let timeline = scene.constructor.build_scene();
239 let timeline_infos = timeline.get_timeline_infos();
240 let old_cur_second = self.timeline_state.current_sec;
241 self.timeline_state = TimelineState::new(timeline.total_secs(), timeline_infos);
242 self.timeline_state.current_sec =
243 old_cur_second.clamp(0.0, self.timeline_state.total_sec);
244 self.timeline = timeline;
245 self.store.update(std::iter::empty());
246 self.pool.clean();
247 self.need_eval = true;
248
249 self.set_clear_color_str(&scene.config.clear_color);
250
251 if let Err(err) = tx.try_send(()) {
252 error!("Failed to send reloaded signal: {err:?}");
253 }
254 }
255 }
256 }
257 }
258
259 fn prepare_renderer(&mut self, frame: &eframe::Frame) {
260 let needs_init = self.renderer.is_none();
262 let needs_resize = self.resolution_dirty && self.renderer.is_some();
263
264 if !needs_init && !needs_resize {
265 return;
266 }
267
268 let Some(render_state) = frame.wgpu_render_state() else {
269 tracing::info!("frame.wgpu_render_state() is none");
270 tracing::info!("{:?}", frame.info());
271 return;
272 };
273
274 if needs_init {
275 tracing::info!("preparing renderer...");
276 } else if needs_resize {
277 tracing::info!("recreating renderer for resolution change...");
278 }
279
280 let ctx = WgpuContext {
283 instance: wgpu::Instance::default(), adapter: wgpu::Adapter::clone(&render_state.adapter),
285 device: wgpu::Device::clone(&render_state.device),
286 queue: wgpu::Queue::clone(&render_state.queue),
287 };
288
289 let (width, height) = (self.resolution.width, self.resolution.height);
290 let oit_layers = self.calculate_oit_layers(&ctx, width, height);
291 let renderer = Renderer::new(&ctx, width, height, oit_layers);
292 let render_textures = renderer.new_render_textures(&ctx);
293
294 if self.depth_visual_pipeline.is_none() {
296 self.depth_visual_pipeline = Some(DepthVisualPipeline::new(&ctx));
297 }
298
299 let depth_visual_texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
301 label: Some("Depth Visual Texture"),
302 size: wgpu::Extent3d {
303 width: render_textures.width(),
304 height: render_textures.height(),
305 depth_or_array_layers: 1,
306 },
307 mip_level_count: 1,
308 sample_count: 1,
309 dimension: wgpu::TextureDimension::D2,
310 format: wgpu::TextureFormat::Rgba8Unorm,
311 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
312 view_formats: &[],
313 });
314 let depth_visual_view =
315 depth_visual_texture.create_view(&wgpu::TextureViewDescriptor::default());
316
317 let texture_view = &render_textures.linear_render_view;
319 let texture_id = render_state.renderer.write().register_native_texture(
320 &render_state.device,
321 texture_view,
322 wgpu::FilterMode::Linear,
323 );
324 let depth_id = render_state.renderer.write().register_native_texture(
325 &render_state.device,
326 &depth_visual_view,
327 wgpu::FilterMode::Nearest,
328 );
329
330 self.texture_id = Some(texture_id);
331 self.depth_texture_id = Some(depth_id);
332 self.depth_visual_texture = Some(depth_visual_texture);
333 self.depth_visual_view = Some(depth_visual_view);
334 self.render_textures = Some(render_textures);
335 self.renderer = Some(renderer);
336 self.wgpu_ctx = Some(ctx);
337 self.resolution_dirty = false;
338 self.need_eval = true; }
340
341 fn render_animation(&mut self) {
342 if let (Some(ctx), Some(renderer), Some(render_textures)) = (
343 self.wgpu_ctx.as_ref(),
344 self.renderer.as_mut(),
345 self.render_textures.as_mut(),
346 ) {
347 if self.last_sec == self.timeline_state.current_sec && !self.need_eval {
348 return;
349 }
350 self.need_eval = false;
351 self.last_sec = self.timeline_state.current_sec;
352
353 let start_eval = Instant::now();
354 self.store
355 .update(self.timeline.eval_at_sec(self.timeline_state.current_sec));
356 self.last_eval_time = Some(start_eval.elapsed());
357
358 let start = Instant::now();
359 renderer.render_store_with_pool(
360 ctx,
361 render_textures,
362 self.clear_color,
363 &self.store,
364 &mut self.pool,
365 );
366
367 if let (Some(pipeline), Some(view)) = (
368 self.depth_visual_pipeline.as_ref(),
369 self.depth_visual_view.as_ref(),
370 ) {
371 let mut encoder =
372 ctx.device
373 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
374 label: Some("Depth Visual Encoder"),
375 });
376
377 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
378 label: Some("Depth Visual Bind Group"),
379 layout: &pipeline.bind_group_layout,
380 entries: &[wgpu::BindGroupEntry {
381 binding: 0,
382 resource: wgpu::BindingResource::TextureView(
383 &render_textures.depth_texture_view,
384 ),
385 }],
386 });
387
388 {
389 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
390 label: Some("Depth Visual Pass"),
391 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
392 view,
393 resolve_target: None,
394 depth_slice: None,
395 ops: wgpu::Operations {
396 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
397 store: wgpu::StoreOp::Store,
398 },
399 })],
400 depth_stencil_attachment: None,
401 timestamp_writes: None,
402 occlusion_query_set: None,
403 });
404 rpass.set_pipeline(&pipeline.pipeline);
405 rpass.set_bind_group(0, &bind_group, &[]);
406 rpass.draw(0..3, 0..1);
407 }
408 ctx.queue.submit(Some(encoder.finish()));
409 }
410
411 self.last_render_time = Some(start.elapsed());
412 self.pool.clean();
413 }
414 }
415}
416
417impl eframe::App for RanimPreviewApp {
418 fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
419 self.prepare_renderer(frame);
420 self.handle_events();
421
422 if let Some(play_prev_t) = self.play_prev_t {
423 let elapsed = play_prev_t.elapsed().as_secs_f64();
424 self.timeline_state.current_sec =
425 (self.timeline_state.current_sec + elapsed).min(self.timeline_state.total_sec);
426 if self.timeline_state.current_sec == self.timeline_state.total_sec {
427 self.play_prev_t = None;
428 } else {
429 self.play_prev_t = Some(Instant::now());
430 ctx.request_repaint(); }
432 }
433
434 self.render_animation();
435
436 egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
437 ui.horizontal(|ui| {
438 ui.heading(&self.title);
439
440 {
442 let resolution = self.resolution;
443 egui::ComboBox::from_label("Resolution")
444 .selected_text(format!(
445 "{}x{} ({})",
446 resolution.width,
447 resolution.height,
448 resolution.aspect_ratio_str()
449 ))
450 .show_ui(ui, |ui| {
451 ui.label(egui::RichText::new("16:9").strong());
453 ui.selectable_value(
454 &mut self.resolution,
455 Resolution::HD,
456 "1280x720 (HD)",
457 );
458 ui.selectable_value(
459 &mut self.resolution,
460 Resolution::FHD,
461 "1920x1080 (FHD)",
462 );
463 ui.selectable_value(
464 &mut self.resolution,
465 Resolution::QHD,
466 "2560x1440 (QHD)",
467 );
468 ui.selectable_value(
469 &mut self.resolution,
470 Resolution::UHD,
471 "3840x2160 (UHD)",
472 );
473 ui.separator();
474 ui.label(egui::RichText::new("16:10").strong());
476 ui.selectable_value(
477 &mut self.resolution,
478 Resolution::WXGA,
479 "1280x800 (WXGA)",
480 );
481 ui.selectable_value(
482 &mut self.resolution,
483 Resolution::WUXGA,
484 "1920x1200 (WUXGA)",
485 );
486 ui.separator();
487 ui.label(egui::RichText::new("4:3").strong());
489 ui.selectable_value(
490 &mut self.resolution,
491 Resolution::SVGA,
492 "800x600 (SVGA)",
493 );
494 ui.selectable_value(
495 &mut self.resolution,
496 Resolution::XGA,
497 "1024x768 (XGA)",
498 );
499 ui.selectable_value(
500 &mut self.resolution,
501 Resolution::SXGA,
502 "1280x960 (SXGA)",
503 );
504 ui.separator();
505 ui.label(egui::RichText::new("1:1").strong());
507 ui.selectable_value(
508 &mut self.resolution,
509 Resolution::_1K_SQUARE,
510 "1080x1080",
511 );
512 ui.selectable_value(
513 &mut self.resolution,
514 Resolution::_2K_SQUARE,
515 "2160x2160",
516 );
517 ui.separator();
518 ui.label(egui::RichText::new("21:9").strong());
520 ui.selectable_value(
521 &mut self.resolution,
522 Resolution::UW_QHD,
523 "3440x1440 (UW-QHD)",
524 );
525 });
526 if self.resolution != resolution {
527 self.resolution_dirty = true;
528 }
529 }
530
531 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
532 let dark_mode = ui.visuals().dark_mode;
533 let button_text = if dark_mode { "☀ Light" } else { "🌙 Dark" };
534 if ui.button(button_text).clicked() {
535 if dark_mode {
536 ctx.set_visuals(egui::Visuals::light());
537 } else {
538 ctx.set_visuals(egui::Visuals::dark());
539 }
540 }
541
542 ui.separator();
543 ui.selectable_value(&mut self.view_mode, ViewMode::Output, "Output");
544 ui.selectable_value(&mut self.view_mode, ViewMode::Depth, "Depth");
545 ui.separator();
546
547 if let Some(duration) = self.last_render_time {
548 ui.label(format!("Render: {:.2}ms", duration.as_secs_f64() * 1000.0));
549 ui.separator();
550 }
551 if let Some(duration) = self.last_eval_time {
552 ui.label(format!("Eval: {:.2}ms", duration.as_secs_f64() * 1000.0));
553 ui.separator();
554 }
555 });
556 });
557 });
558
559 egui::TopBottomPanel::bottom("bottom_panel")
560 .resizable(true)
561 .max_height(600.0)
562 .show(ctx, |ui| {
563 ui.label("Timeline");
564
565 ui.horizontal(|ui| {
566 if ui.button("<<").clicked() {
567 self.timeline_state.current_sec = 0.0;
568 }
569 #[allow(clippy::collapsible_else_if)]
570 if self.play_prev_t.is_none() {
571 if ui.button("play").clicked() {
572 self.play_prev_t = Some(Instant::now());
573 }
574 } else {
575 if ui.button("pause").clicked() {
576 self.play_prev_t = None;
577 }
578 }
579 if ui.button(">>").clicked() {
580 self.timeline_state.current_sec = self.timeline_state.total_sec;
581 }
582 ui.style_mut().spacing.slider_width = ui.available_width() - 70.0;
583 ui.add(
584 egui::Slider::new(
585 &mut self.timeline_state.current_sec,
586 0.0..=self.timeline_state.total_sec,
587 )
588 .text("sec"),
589 );
590 });
591
592 self.timeline_state.ui_main_timeline(ui);
593 });
594
595 egui::CentralPanel::default().show(ctx, |ui| {
596 let texture_id = match self.view_mode {
597 ViewMode::Output => self.texture_id,
598 ViewMode::Depth => self.depth_texture_id,
599 };
600
601 if let Some(tid) = texture_id {
602 let available_size = ui.available_size();
605 let aspect_ratio = self
606 .render_textures
607 .as_ref()
608 .map(|rt| rt.ratio())
609 .unwrap_or(1280.0 / 7.0);
610 let mut size = available_size;
611
612 if size.x / size.y > aspect_ratio {
613 size.x = size.y * aspect_ratio;
614 } else {
615 size.y = size.x / aspect_ratio;
616 }
617
618 ui.centered_and_justified(|ui| {
619 ui.image(egui::load::SizedTexture::new(tid, size));
620 });
621 } else {
622 ui.centered_and_justified(|ui| {
623 ui.spinner();
624 });
625 }
626 });
627 }
628}
629
630pub fn run_app(app: RanimPreviewApp, #[cfg(target_arch = "wasm32")] container_id: String) {
631 #[cfg(not(target_arch = "wasm32"))]
632 {
633 let native_options = eframe::NativeOptions {
634 viewport: egui::ViewportBuilder::default()
635 .with_title(&app.title)
636 .with_inner_size([1280.0, 720.0]),
637 renderer: eframe::Renderer::Wgpu,
638 ..Default::default()
639 };
640
641 let title = app.title.clone();
643
644 eframe::run_native(
645 &title,
646 native_options,
647 Box::new(|_cc| {
648 Ok(Box::new(app))
650 }),
651 )
652 .unwrap();
653 }
654
655 #[cfg(target_arch = "wasm32")]
656 {
657 use wasm_bindgen::JsCast;
658 let web_options = eframe::WebOptions {
659 ..Default::default()
660 };
661
662 let document = web_sys::window().unwrap().document().unwrap();
664 let canvas = document
665 .get_element_by_id(&container_id)
666 .and_then(|c| c.dyn_into::<web_sys::HtmlCanvasElement>().ok());
667
668 let canvas = if let Some(canvas) = canvas {
669 canvas
670 } else {
671 let canvas = document.create_element("canvas").unwrap();
672 canvas.set_id(&container_id);
673 document.body().unwrap().append_child(&canvas).unwrap();
674 canvas.dyn_into::<web_sys::HtmlCanvasElement>().unwrap()
675 };
676
677 wasm_bindgen_futures::spawn_local(async {
678 eframe::WebRunner::new()
679 .start(canvas, web_options, Box::new(|_cc| Ok(Box::new(app))))
680 .await
681 .expect("failed to start eframe");
682 });
683 }
684}
685
686pub fn preview_constructor_with_name(scene: impl SceneConstructor, name: &str) {
687 let app = RanimPreviewApp::new(scene, name.to_string());
688 run_app(
689 app,
690 #[cfg(target_arch = "wasm32")]
691 format!("ranim-app-{name}"),
692 );
693}
694
695pub fn preview_scene(scene: &Scene) {
697 preview_scene_with_name(scene, &scene.name);
698}
699
700pub fn preview_scene_with_name(scene: &Scene, name: &str) {
702 let mut app = RanimPreviewApp::new(scene.constructor, name.to_string());
703 app.set_clear_color_str(&scene.config.clear_color);
704 run_app(
705 app,
706 #[cfg(target_arch = "wasm32")]
707 format!("ranim-app-{name}"),
708 );
709}
710
711#[cfg(target_arch = "wasm32")]
713mod wasm {
714 use super::*;
715
716 #[wasm_bindgen(start)]
717 pub async fn wasm_start() {
718 console_error_panic_hook::set_once();
719 wasm_tracing::set_as_global_default();
720 }
721
722 #[wasm_bindgen]
724 pub fn preview_scene(scene: &Scene) {
725 super::preview_scene(scene);
726 }
727}