use crate::assets::{AssetPath, Assets};
use crate::controls::{OrbitControlAction, OrbitControls, PointerEvent, TouchEvent};
use crate::diagnostics::{Diagnostic, LookupError, RenderOutcome};
use crate::picking::{CursorPosition, Hit, Viewport};
use crate::platform::{PlatformSurface, SurfaceEvent};
use crate::render::{Profile, Quality, RenderMode, Renderer, RendererOptions};
use crate::scene::{CameraKey, DirectionalLight, Scene, SceneImport, Vec3};
#[derive(Debug)]
pub struct FirstRender {
assets: Assets,
scene: Scene,
renderer: Renderer,
import: SceneImport,
outcome: RenderOutcome,
diagnostics: Vec<Diagnostic>,
}
#[derive(Debug)]
pub struct HeadlessGltfViewer {
assets: Assets,
scene: Scene,
renderer: Renderer,
import: SceneImport,
}
#[derive(Debug, Clone)]
pub struct HeadlessGltfViewerBuilder {
path: AssetPath,
width: u32,
height: u32,
common: ViewerCommonOptions,
}
#[derive(Debug, Clone)]
struct ViewerCommonOptions {
frame_import: bool,
default_light: bool,
default_environment: bool,
environment_path: Option<AssetPath>,
renderer_options: RendererOptions,
}
impl ViewerCommonOptions {
fn new() -> Self {
Self {
frame_import: true,
default_light: false,
default_environment: false,
environment_path: None,
renderer_options: RendererOptions::default(),
}
}
fn with_environment(mut self, path: impl Into<AssetPath>) -> Self {
self.environment_path = Some(path.into());
self.default_environment = false;
self
}
}
pub fn headless_gltf_viewer(path: impl Into<AssetPath>) -> HeadlessGltfViewerBuilder {
HeadlessGltfViewerBuilder {
path: path.into(),
width: 800,
height: 600,
common: ViewerCommonOptions::new(),
}
}
impl FirstRender {
pub fn assets(&self) -> &Assets {
&self.assets
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn import(&self) -> &SceneImport {
&self.import
}
pub fn outcome(&self) -> &RenderOutcome {
&self.outcome
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
impl HeadlessGltfViewerBuilder {
pub const fn size(mut self, width: u32, height: u32) -> Self {
self.width = width;
self.height = height;
self
}
pub const fn with_default_light(mut self) -> Self {
self.common.default_light = true;
self
}
pub const fn with_default_environment(mut self) -> Self {
self.common.default_environment = true;
self
}
pub fn with_environment(mut self, path: impl Into<AssetPath>) -> Self {
self.common = self.common.with_environment(path);
self
}
pub const fn with_profile(mut self, profile: Profile) -> Self {
self.common.renderer_options = self.common.renderer_options.with_profile(profile);
self
}
pub const fn with_quality(mut self, quality: Quality) -> Self {
self.common.renderer_options = self.common.renderer_options.with_quality(quality);
self
}
pub const fn with_render_mode(mut self, render_mode: RenderMode) -> Self {
self.common.renderer_options = self.common.renderer_options.with_render_mode(render_mode);
self
}
pub const fn on_change(self) -> Self {
self.with_render_mode(RenderMode::OnChange)
}
pub const fn without_framing(mut self) -> Self {
self.common.frame_import = false;
self
}
pub async fn build(self) -> crate::Result<HeadlessGltfViewer> {
let assets = Assets::new();
let scene_asset = assets.load_scene(self.path).await?;
let mut scene = Scene::new();
let import = scene.instantiate(&scene_asset)?;
let camera = scene.add_default_camera()?;
if self.common.frame_import {
scene.frame_import(camera, &import)?;
}
if self.common.default_light {
scene.directional_light(DirectionalLight::default()).add()?;
}
let mut renderer =
Renderer::headless_with_options(self.width, self.height, self.common.renderer_options)?;
if let Some(environment_path) = self.common.environment_path {
let environment = assets.load_environment(environment_path).await?;
renderer.set_environment(environment);
} else if self.common.default_environment {
renderer.set_environment(assets.default_environment());
}
renderer.prepare_with_assets(&mut scene, &assets)?;
Ok(HeadlessGltfViewer {
assets,
scene,
renderer,
import,
})
}
pub async fn render(self) -> crate::Result<FirstRender> {
let mut viewer = self.build().await?;
let outcome = viewer.render_next_frame()?;
let diagnostics = viewer.renderer.diagnostics().to_vec();
let HeadlessGltfViewer {
assets,
scene,
renderer,
import,
} = viewer;
Ok(FirstRender {
assets,
scene,
renderer,
import,
outcome,
diagnostics,
})
}
}
impl HeadlessGltfViewer {
pub fn prepare(&mut self) -> crate::Result<()> {
self.renderer
.prepare_with_assets(&mut self.scene, &self.assets)?;
Ok(())
}
pub fn render_next_frame(&mut self) -> crate::Result<RenderOutcome> {
Ok(self.renderer.render_active(&self.scene)?)
}
pub fn assets(&self) -> &Assets {
&self.assets
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn scene_mut(&mut self) -> &mut Scene {
&mut self.scene
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
pub fn import(&self) -> &SceneImport {
&self.import
}
pub fn snapshot_rgba8(&self) -> &[u8] {
self.renderer.frame_rgba8()
}
pub fn capabilities(&self) -> &crate::Capabilities {
self.renderer.capabilities()
}
}
#[derive(Debug)]
pub struct InteractiveGltfViewer {
assets: Assets,
scene: Scene,
renderer: Renderer,
import: SceneImport,
camera: CameraKey,
orbit_controls: Option<OrbitControls>,
}
#[derive(Debug)]
pub struct InteractiveGltfViewerBuilder {
path: AssetPath,
surface: PlatformSurface,
orbit_controls: bool,
common: ViewerCommonOptions,
}
pub fn interactive_gltf_viewer(
path: impl Into<AssetPath>,
surface: PlatformSurface,
) -> InteractiveGltfViewerBuilder {
InteractiveGltfViewerBuilder {
path: path.into(),
surface,
orbit_controls: false,
common: ViewerCommonOptions::new(),
}
}
impl InteractiveGltfViewerBuilder {
pub const fn with_default_light(mut self) -> Self {
self.common.default_light = true;
self
}
pub const fn with_default_environment(mut self) -> Self {
self.common.default_environment = true;
self
}
pub fn with_environment(mut self, path: impl Into<AssetPath>) -> Self {
self.common = self.common.with_environment(path);
self
}
pub const fn with_orbit_controls(mut self) -> Self {
self.orbit_controls = true;
self
}
pub const fn with_profile(mut self, profile: Profile) -> Self {
self.common.renderer_options = self.common.renderer_options.with_profile(profile);
self
}
pub const fn with_quality(mut self, quality: Quality) -> Self {
self.common.renderer_options = self.common.renderer_options.with_quality(quality);
self
}
pub const fn with_render_mode(mut self, render_mode: RenderMode) -> Self {
self.common.renderer_options = self.common.renderer_options.with_render_mode(render_mode);
self
}
pub const fn on_change(self) -> Self {
self.with_render_mode(RenderMode::OnChange)
}
pub const fn without_framing(mut self) -> Self {
self.common.frame_import = false;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn build(self) -> crate::Result<InteractiveGltfViewer> {
let assets = Assets::new();
let scene_asset = pollster::block_on(assets.load_scene(self.path.clone()))?;
let mut scene = Scene::new();
let import = scene.instantiate(&scene_asset)?;
let camera = scene.add_default_camera()?;
if self.common.frame_import {
scene.frame_import(camera, &import)?;
}
if self.common.default_light {
scene.directional_light(DirectionalLight::default()).add()?;
}
let mut renderer =
Renderer::from_surface_with_options(self.surface, self.common.renderer_options)?;
if let Some(environment_path) = self.common.environment_path {
let environment = pollster::block_on(assets.load_environment(environment_path))?;
renderer.set_environment(environment);
} else if self.common.default_environment {
renderer.set_environment(assets.default_environment());
}
renderer.prepare_with_assets(&mut scene, &assets)?;
let orbit_controls = build_orbit_controls(self.orbit_controls, &scene, &import, camera);
Ok(InteractiveGltfViewer {
assets,
scene,
renderer,
import,
camera,
orbit_controls,
})
}
pub async fn build_async(self) -> crate::Result<InteractiveGltfViewer> {
let assets = Assets::new();
let scene_asset = assets.load_scene(self.path.clone()).await?;
let mut scene = Scene::new();
let import = scene.instantiate(&scene_asset)?;
let camera = scene.add_default_camera()?;
if self.common.frame_import {
scene.frame_import(camera, &import)?;
}
if self.common.default_light {
scene.directional_light(DirectionalLight::default()).add()?;
}
let mut renderer =
Renderer::from_surface_async_with_options(self.surface, self.common.renderer_options)
.await?;
if let Some(environment_path) = self.common.environment_path {
let environment = assets.load_environment(environment_path).await?;
renderer.set_environment(environment);
} else if self.common.default_environment {
renderer.set_environment(assets.default_environment());
}
renderer.prepare_with_assets(&mut scene, &assets)?;
let orbit_controls = build_orbit_controls(self.orbit_controls, &scene, &import, camera);
Ok(InteractiveGltfViewer {
assets,
scene,
renderer,
import,
camera,
orbit_controls,
})
}
}
fn build_orbit_controls(
enabled: bool,
scene: &Scene,
import: &SceneImport,
camera: CameraKey,
) -> Option<OrbitControls> {
if !enabled {
return None;
}
let bounds = import.bounds_world(scene);
let target = bounds.map(|aabb| aabb.center()).unwrap_or(Vec3::ZERO);
let distance = scene
.camera_node(camera)
.and_then(|node| scene.world_transform(node))
.map(|transform| {
let dx = transform.translation.x - target.x;
let dy = transform.translation.y - target.y;
let dz = transform.translation.z - target.z;
(dx * dx + dy * dy + dz * dz).sqrt()
})
.filter(|distance| distance.is_finite() && *distance > 0.0)
.unwrap_or(2.0);
Some(OrbitControls::new(target, distance))
}
impl InteractiveGltfViewer {
pub fn handle_surface_event(&mut self, event: SurfaceEvent) -> crate::Result<()> {
self.renderer.handle_surface_event(event)?;
Ok(())
}
pub fn prepare(&mut self) -> crate::Result<()> {
self.renderer
.prepare_with_assets(&mut self.scene, &self.assets)?;
Ok(())
}
pub fn render_next_frame(&mut self) -> crate::Result<RenderOutcome> {
Ok(self.renderer.render_active(&self.scene)?)
}
pub fn assets(&self) -> &Assets {
&self.assets
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn scene_mut(&mut self) -> &mut Scene {
&mut self.scene
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
pub fn import(&self) -> &SceneImport {
&self.import
}
pub fn camera(&self) -> CameraKey {
self.camera
}
pub fn orbit_controls(&self) -> Option<&OrbitControls> {
self.orbit_controls.as_ref()
}
pub fn diagnostics(&self) -> Vec<Diagnostic> {
self.renderer.diagnostics().to_vec()
}
pub fn snapshot_rgba8(&self) -> &[u8] {
self.renderer.frame_rgba8()
}
pub fn capabilities(&self) -> &crate::Capabilities {
self.renderer.capabilities()
}
pub fn handle_pointer_event(
&mut self,
event: PointerEvent,
) -> Result<OrbitControlAction, LookupError> {
let Some(orbit_controls) = self.orbit_controls.as_mut() else {
return Ok(OrbitControlAction::None);
};
let action = orbit_controls.handle_pointer(event);
if !matches!(action, OrbitControlAction::None) {
orbit_controls.apply_to_scene(&mut self.scene, self.camera)?;
}
Ok(action)
}
pub fn handle_touch_event(
&mut self,
event: TouchEvent,
) -> Result<OrbitControlAction, LookupError> {
let Some(orbit_controls) = self.orbit_controls.as_mut() else {
return Ok(OrbitControlAction::None);
};
let action = orbit_controls.handle_touch(event);
if !matches!(action, OrbitControlAction::None) {
orbit_controls.apply_to_scene(&mut self.scene, self.camera)?;
}
Ok(action)
}
pub fn pick_at(&self, physical_x: f32, physical_y: f32) -> Result<Option<Hit>, LookupError> {
let viewport = self.viewport_for_pick()?;
self.scene.pick_with_assets(
self.camera,
CursorPosition::physical(physical_x, physical_y),
viewport,
&self.assets,
)
}
pub fn pick_and_select_at(
&mut self,
physical_x: f32,
physical_y: f32,
) -> Result<Option<Hit>, LookupError> {
let viewport = self.viewport_for_pick()?;
self.scene.pick_and_select_with_assets(
self.camera,
CursorPosition::physical(physical_x, physical_y),
viewport,
&self.assets,
)
}
pub fn pick_and_hover_at(
&mut self,
physical_x: f32,
physical_y: f32,
) -> Result<Option<Hit>, LookupError> {
let viewport = self.viewport_for_pick()?;
self.scene.pick_and_hover_with_assets(
self.camera,
CursorPosition::physical(physical_x, physical_y),
viewport,
&self.assets,
)
}
fn viewport_for_pick(&self) -> Result<Viewport, LookupError> {
let stats = self.renderer.stats();
Viewport::new(stats.target_width, stats.target_height, 1.0).ok_or(
LookupError::InvalidViewport {
width: stats.target_width,
height: stats.target_height,
},
)
}
}
pub async fn first_render_gltf_headless(
path: impl Into<AssetPath>,
width: u32,
height: u32,
) -> crate::Result<FirstRender> {
headless_gltf_viewer(path)
.size(width, height)
.render()
.await
}