use crate::ecs::world::World;
use crate::render::wgpu::brdf_lut;
use crate::render::wgpu::passes;
use crate::render::wgpu::rendergraph::RenderGraph;
use ash::vk::{self, Handle};
use openxr as xr;
#[derive(Clone, Default)]
pub struct XrResources {
pub input: Option<XrInput>,
pub locomotion_enabled: bool,
pub locomotion_speed: f32,
pub initial_player_yaw: Option<f32>,
pub initial_player_position: Option<nalgebra_glm::Vec3>,
pub camera_override: Option<crate::ecs::camera::queries::CameraMatrices>,
pub sync_player_position: Option<nalgebra_glm::Vec3>,
}
#[derive(Clone)]
pub struct XrInput {
pub thumbstick: nalgebra_glm::Vec2,
pub right_thumbstick: nalgebra_glm::Vec2,
pub left_trigger: f32,
pub right_trigger: f32,
pub left_grip: f32,
pub right_grip: f32,
pub a_button: bool,
pub b_button: bool,
pub left_thumbstick_click: bool,
pub left_hand_pose: Option<xr::Posef>,
pub right_hand_pose: Option<xr::Posef>,
pub player_position: nalgebra_glm::Vec3,
pub player_yaw: f32,
pub head_yaw: f32,
pub head_position: nalgebra_glm::Vec3,
pub head_orientation: nalgebra_glm::Quat,
}
impl Default for XrInput {
fn default() -> Self {
Self {
thumbstick: nalgebra_glm::vec2(0.0, 0.0),
right_thumbstick: nalgebra_glm::vec2(0.0, 0.0),
left_trigger: 0.0,
right_trigger: 0.0,
left_grip: 0.0,
right_grip: 0.0,
a_button: false,
b_button: false,
left_thumbstick_click: false,
left_hand_pose: None,
right_hand_pose: None,
player_position: nalgebra_glm::vec3(0.0, 0.0, 0.0),
player_yaw: 0.0,
head_yaw: 0.0,
head_position: nalgebra_glm::vec3(0.0, 0.0, 0.0),
head_orientation: nalgebra_glm::quat_identity(),
}
}
}
impl XrInput {
pub fn left_trigger_pressed(&self) -> bool {
self.left_trigger > 0.5
}
pub fn right_trigger_pressed(&self) -> bool {
self.right_trigger > 0.5
}
pub fn left_grip_pressed(&self) -> bool {
self.left_grip > 0.5
}
pub fn right_grip_pressed(&self) -> bool {
self.right_grip > 0.5
}
pub fn a_button_pressed(&self) -> bool {
self.a_button
}
pub fn b_button_pressed(&self) -> bool {
self.b_button
}
pub fn left_hand_position(&self) -> Option<nalgebra_glm::Vec3> {
self.left_hand_pose.map(|pose| {
let local_x = -pose.position.x;
let local_z = -pose.position.z;
let rotated_x = local_x * self.player_yaw.cos() + local_z * self.player_yaw.sin();
let rotated_z = -local_x * self.player_yaw.sin() + local_z * self.player_yaw.cos();
nalgebra_glm::vec3(
rotated_x + self.player_position.x,
pose.position.y + self.player_position.y,
rotated_z + self.player_position.z,
)
})
}
pub fn right_hand_position(&self) -> Option<nalgebra_glm::Vec3> {
self.right_hand_pose.map(|pose| {
let local_x = -pose.position.x;
let local_z = -pose.position.z;
let rotated_x = local_x * self.player_yaw.cos() + local_z * self.player_yaw.sin();
let rotated_z = -local_x * self.player_yaw.sin() + local_z * self.player_yaw.cos();
nalgebra_glm::vec3(
rotated_x + self.player_position.x,
pose.position.y + self.player_position.y,
rotated_z + self.player_position.z,
)
})
}
pub fn left_hand_rotation(&self) -> Option<nalgebra_glm::Quat> {
self.left_hand_pose.map(|pose| {
let openxr_orientation = pose.orientation;
let openxr_quat = nalgebra_glm::quat(
openxr_orientation.w,
openxr_orientation.z,
openxr_orientation.y,
openxr_orientation.x,
);
let flip_x = nalgebra_glm::quat_angle_axis(
180.0_f32.to_radians(),
&nalgebra_glm::vec3(1.0, 0.0, 0.0),
);
let player_rotation =
nalgebra_glm::quat_angle_axis(self.player_yaw, &nalgebra_glm::vec3(0.0, 1.0, 0.0));
player_rotation * flip_x * openxr_quat
})
}
pub fn right_hand_rotation(&self) -> Option<nalgebra_glm::Quat> {
self.right_hand_pose.map(|pose| {
let openxr_orientation = pose.orientation;
let openxr_quat = nalgebra_glm::quat(
openxr_orientation.w,
openxr_orientation.z,
openxr_orientation.y,
openxr_orientation.x,
);
let flip_x = nalgebra_glm::quat_angle_axis(
180.0_f32.to_radians(),
&nalgebra_glm::vec3(1.0, 0.0, 0.0),
);
let player_rotation =
nalgebra_glm::quat_angle_axis(self.player_yaw, &nalgebra_glm::vec3(0.0, 1.0, 0.0));
player_rotation * flip_x * openxr_quat
})
}
}
use std::ffi::{CString, c_void};
use web_time::Instant;
const VK_TARGET_VERSION: xr::Version = xr::Version::new(1, 1, 0);
const VK_TARGET_VERSION_ASH: u32 = vk::make_api_version(
0,
VK_TARGET_VERSION.major() as u32,
VK_TARGET_VERSION.minor() as u32,
VK_TARGET_VERSION.patch(),
);
pub struct XrContext {
_vk_entry: ash::Entry,
_vk_instance: ash::Instance,
instance: xr::Instance,
_system: xr::SystemId,
session: xr::Session<xr::Vulkan>,
frame_wait: xr::FrameWaiter,
frame_stream: xr::FrameStream<xr::Vulkan>,
stage: xr::Space,
swapchain: xr::Swapchain<xr::Vulkan>,
swapchain_buffers: Vec<wgpu::Texture>,
resolution: (u32, u32),
_views: Vec<xr::ViewConfigurationView>,
action_set: xr::ActionSet,
move_action: xr::Action<xr::Vector2f>,
turn_action: xr::Action<xr::Vector2f>,
_left_hand_action: xr::Action<xr::Posef>,
_right_hand_action: xr::Action<xr::Posef>,
left_trigger_action: xr::Action<f32>,
right_trigger_action: xr::Action<f32>,
left_grip_action: xr::Action<f32>,
right_grip_action: xr::Action<f32>,
a_button_action: xr::Action<bool>,
b_button_action: xr::Action<bool>,
left_thumbstick_click_action: xr::Action<bool>,
left_hand_space: xr::Space,
right_hand_space: xr::Space,
player_position: nalgebra_glm::Vec3,
player_yaw: f32,
session_running: bool,
device: wgpu::Device,
queue: wgpu::Queue,
}
impl XrContext {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let xr_entry = xr::Entry::linked();
let mut required_extensions = xr::ExtensionSet::default();
required_extensions.khr_vulkan_enable2 = true;
let xr_instance = xr_entry.create_instance(
&xr::ApplicationInfo {
application_name: "nightshade-xr",
application_version: 1,
engine_name: "nightshade",
engine_version: 1,
api_version: xr::Version::new(1, 0, 0),
},
&required_extensions,
&[],
)?;
let system = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)?;
let views = xr_instance.enumerate_view_configuration_views(
system,
xr::ViewConfigurationType::PRIMARY_STEREO,
)?;
let resolution = (
views[0].recommended_image_rect_width,
views[0].recommended_image_rect_height,
);
let reqs = xr_instance.graphics_requirements::<xr::Vulkan>(system)?;
if VK_TARGET_VERSION < reqs.min_api_version_supported
|| VK_TARGET_VERSION.major() > reqs.max_api_version_supported.major()
{
return Err(format!(
"OpenXR runtime requires Vulkan version > {}, < {}.0.0",
reqs.min_api_version_supported,
reqs.max_api_version_supported.major() + 1
)
.into());
}
let vk_entry = unsafe { ash::Entry::load()? };
let flags = wgpu::InstanceFlags::empty();
let instance_exts = <wgpu_hal::vulkan::Api as wgpu_hal::Api>::Instance::desired_extensions(
&vk_entry,
VK_TARGET_VERSION_ASH,
flags,
)?;
let vk_instance = unsafe {
let extensions_cchar: Vec<_> = instance_exts.iter().map(|s| s.as_ptr()).collect();
let app_name = CString::new("nightshade-xr")?;
let vk_app_info = vk::ApplicationInfo::default()
.application_name(&app_name)
.application_version(1)
.engine_name(&app_name)
.engine_version(1)
.api_version(VK_TARGET_VERSION_ASH);
let get_instance_proc_addr: unsafe extern "system" fn(
*const c_void,
*const i8,
) -> Option<
unsafe extern "system" fn(),
> = std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr);
let vk_instance = xr_instance
.create_vulkan_instance(
system,
get_instance_proc_addr,
&vk::InstanceCreateInfo::default()
.application_info(&vk_app_info)
.enabled_extension_names(&extensions_cchar) as *const _
as *const _,
)?
.map_err(vk::Result::from_raw)?;
ash::Instance::load(
vk_entry.static_fn(),
vk::Instance::from_raw(vk_instance as _),
)
};
let vk_physical_device = vk::PhysicalDevice::from_raw(unsafe {
xr_instance.vulkan_graphics_device(system, vk_instance.handle().as_raw() as _)? as _
});
let vk_instance_ptr = vk_instance.handle().as_raw() as *const c_void;
let vk_physical_device_ptr = vk_physical_device.as_raw() as *const c_void;
let vk_device_properties =
unsafe { vk_instance.get_physical_device_properties(vk_physical_device) };
if vk_device_properties.api_version < VK_TARGET_VERSION_ASH {
return Err("Vulkan physical device doesn't support version 1.1".into());
}
let wgpu_vk_instance = unsafe {
<wgpu_hal::vulkan::Api as wgpu_hal::Api>::Instance::from_raw(
vk_entry.clone(),
vk_instance.clone(),
vk_device_properties.api_version,
0,
None,
instance_exts,
flags,
wgpu::MemoryBudgetThresholds::default(),
false,
Some(Box::new(|| {})),
)?
};
let wgpu_exposed_adapter = wgpu_vk_instance
.expose_adapter(vk_physical_device)
.ok_or("Failed to expose adapter")?;
let adapter_features = wgpu_exposed_adapter.features;
let desired_features =
wgpu::Features::INDIRECT_FIRST_INSTANCE | wgpu::Features::MULTI_DRAW_INDIRECT_COUNT;
let wgpu_features = adapter_features & desired_features;
let mut enabled_extensions = wgpu_exposed_adapter
.adapter
.required_device_extensions(wgpu_features);
if !enabled_extensions.contains(&ash::khr::swapchain::NAME) {
enabled_extensions.push(ash::khr::swapchain::NAME);
}
let (wgpu_open_device, vk_device_ptr, queue_family_index) = {
let extensions_cchar: Vec<_> = enabled_extensions.iter().map(|s| s.as_ptr()).collect();
let mut enabled_phd_features = wgpu_exposed_adapter
.adapter
.physical_device_features(&enabled_extensions, wgpu_features);
let family_index = 0;
let queue_priorities = [1.0_f32];
let family_info = vk::DeviceQueueCreateInfo::default()
.queue_family_index(family_index)
.queue_priorities(&queue_priorities);
let family_infos = [family_info];
let info = enabled_phd_features
.add_to_device_create(
vk::DeviceCreateInfo::default().queue_create_infos(&family_infos),
)
.enabled_extension_names(&extensions_cchar);
let vk_device = unsafe {
let get_instance_proc_addr: unsafe extern "system" fn(
*const c_void,
*const i8,
) -> Option<
unsafe extern "system" fn(),
> = std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr);
let vk_device = xr_instance
.create_vulkan_device(
system,
get_instance_proc_addr,
vk_physical_device.as_raw() as _,
&info as *const _ as *const _,
)?
.map_err(vk::Result::from_raw)?;
ash::Device::load(vk_instance.fp_v1_0(), vk::Device::from_raw(vk_device as _))
};
let vk_device_ptr = vk_device.handle().as_raw() as *const c_void;
let wgpu_open_device = unsafe {
wgpu_exposed_adapter.adapter.device_from_raw(
vk_device,
Some(Box::new(|| {})),
&enabled_extensions,
wgpu_features,
&wgpu::MemoryHints::Performance,
family_info.queue_family_index,
0,
)
}?;
(
wgpu_open_device,
vk_device_ptr,
family_info.queue_family_index,
)
};
let wgpu_instance =
unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Vulkan>(wgpu_vk_instance) };
let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) };
let adapter_limits = wgpu_adapter.limits();
let limits = adapter_limits;
let (wgpu_device, wgpu_queue) = unsafe {
wgpu_adapter.create_device_from_hal(
wgpu_open_device,
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu_features,
required_limits: limits,
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
},
)
}?;
let (session, frame_wait, frame_stream) = unsafe {
xr_instance.create_session::<xr::Vulkan>(
system,
&xr::vulkan::SessionCreateInfo {
instance: vk_instance_ptr,
physical_device: vk_physical_device_ptr,
device: vk_device_ptr,
queue_family_index,
queue_index: 0,
},
)
}?;
let action_set = xr_instance.create_action_set("gameplay", "Gameplay Actions", 0)?;
let move_action = action_set.create_action::<xr::Vector2f>("move", "Move", &[])?;
let turn_action = action_set.create_action::<xr::Vector2f>("turn", "Turn", &[])?;
let left_hand_action =
action_set.create_action::<xr::Posef>("left_hand_pose", "Left Hand Pose", &[])?;
let right_hand_action =
action_set.create_action::<xr::Posef>("right_hand_pose", "Right Hand Pose", &[])?;
let left_trigger_action =
action_set.create_action::<f32>("left_trigger", "Left Trigger", &[])?;
let right_trigger_action =
action_set.create_action::<f32>("right_trigger", "Right Trigger", &[])?;
let left_grip_action = action_set.create_action::<f32>("left_grip", "Left Grip", &[])?;
let right_grip_action = action_set.create_action::<f32>("right_grip", "Right Grip", &[])?;
let a_button_action = action_set.create_action::<bool>("a_button", "A Button", &[])?;
let b_button_action = action_set.create_action::<bool>("b_button", "B Button", &[])?;
let left_thumbstick_click_action = action_set.create_action::<bool>(
"left_thumbstick_click",
"Left Thumbstick Click",
&[],
)?;
xr_instance.suggest_interaction_profile_bindings(
xr_instance.string_to_path("/interaction_profiles/oculus/touch_controller")?,
&[
xr::Binding::new(
&move_action,
xr_instance.string_to_path("/user/hand/left/input/thumbstick")?,
),
xr::Binding::new(
&turn_action,
xr_instance.string_to_path("/user/hand/right/input/thumbstick")?,
),
xr::Binding::new(
&left_hand_action,
xr_instance.string_to_path("/user/hand/left/input/grip/pose")?,
),
xr::Binding::new(
&right_hand_action,
xr_instance.string_to_path("/user/hand/right/input/grip/pose")?,
),
xr::Binding::new(
&left_trigger_action,
xr_instance.string_to_path("/user/hand/left/input/trigger/value")?,
),
xr::Binding::new(
&right_trigger_action,
xr_instance.string_to_path("/user/hand/right/input/trigger/value")?,
),
xr::Binding::new(
&left_grip_action,
xr_instance.string_to_path("/user/hand/left/input/squeeze/value")?,
),
xr::Binding::new(
&right_grip_action,
xr_instance.string_to_path("/user/hand/right/input/squeeze/value")?,
),
xr::Binding::new(
&a_button_action,
xr_instance.string_to_path("/user/hand/right/input/a/click")?,
),
xr::Binding::new(
&b_button_action,
xr_instance.string_to_path("/user/hand/right/input/b/click")?,
),
xr::Binding::new(
&left_thumbstick_click_action,
xr_instance.string_to_path("/user/hand/left/input/thumbstick/click")?,
),
],
)?;
session.attach_action_sets(&[&action_set])?;
let left_hand_space =
left_hand_action.create_space(&session, xr::Path::NULL, xr::Posef::IDENTITY)?;
let right_hand_space =
right_hand_action.create_space(&session, xr::Path::NULL, xr::Posef::IDENTITY)?;
let stage =
session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?;
let swapchain = session.create_swapchain(&xr::SwapchainCreateInfo {
create_flags: xr::SwapchainCreateFlags::EMPTY,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT
| xr::SwapchainUsageFlags::SAMPLED,
format: vk::Format::R8G8B8A8_SRGB.as_raw() as _,
sample_count: 1,
width: resolution.0,
height: resolution.1,
face_count: 1,
array_size: 2,
mip_count: 1,
})?;
let swapchain_images = swapchain.enumerate_images()?;
let swapchain_buffers: Vec<wgpu::Texture> = swapchain_images
.into_iter()
.map(|color_image| {
let color_image = vk::Image::from_raw(color_image);
let wgpu_hal_texture = unsafe {
let hal_dev = wgpu_device
.as_hal::<wgpu_hal::vulkan::Api>()
.ok_or("Failed to get HAL device")?;
hal_dev.texture_from_raw(
color_image,
&wgpu_hal::TextureDescriptor {
label: Some("VR Swapchain"),
size: wgpu::Extent3d {
width: resolution.0,
height: resolution.1,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUses::COLOR_TARGET | wgpu::TextureUses::COPY_DST,
memory_flags: wgpu_hal::MemoryFlags::empty(),
view_formats: vec![],
},
Some(Box::new(|| {})),
)
};
let texture = unsafe {
wgpu_device.create_texture_from_hal::<wgpu_hal::vulkan::Api>(
wgpu_hal_texture,
&wgpu::TextureDescriptor {
label: Some("VR Swapchain"),
size: wgpu::Extent3d {
width: resolution.0,
height: resolution.1,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
)
};
Ok(texture)
})
.collect::<Result<Vec<_>, Box<dyn std::error::Error>>>()?;
tracing::info!(
"OpenXR session created successfully with resolution {}x{}",
resolution.0,
resolution.1
);
Ok(Self {
_vk_entry: vk_entry,
_vk_instance: vk_instance,
instance: xr_instance,
_system: system,
session,
frame_wait,
frame_stream,
stage,
swapchain,
swapchain_buffers,
resolution,
_views: views,
action_set,
move_action,
turn_action,
_left_hand_action: left_hand_action,
_right_hand_action: right_hand_action,
left_trigger_action,
right_trigger_action,
left_grip_action,
right_grip_action,
a_button_action,
b_button_action,
left_thumbstick_click_action,
left_hand_space,
right_hand_space,
player_position: nalgebra_glm::vec3(0.0, 0.0, 0.0),
player_yaw: 0.0,
session_running: false,
device: wgpu_device,
queue: wgpu_queue,
})
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.queue
}
pub fn resolution(&self) -> (u32, u32) {
self.resolution
}
pub fn surface_format(&self) -> wgpu::TextureFormat {
wgpu::TextureFormat::Rgba8UnormSrgb
}
pub fn set_player_yaw(&mut self, yaw: f32) {
self.player_yaw = yaw;
}
pub fn set_player_position(&mut self, position: nalgebra_glm::Vec3) {
self.player_position = position;
}
pub fn poll_events(&mut self) -> Result<bool, Box<dyn std::error::Error>> {
let mut event_buffer = xr::EventDataBuffer::new();
while let Some(event) = self.instance.poll_event(&mut event_buffer)? {
match event {
xr::Event::SessionStateChanged(state_change) => {
tracing::info!("XR Session state changed to: {:?}", state_change.state());
match state_change.state() {
xr::SessionState::READY => {
self.session
.begin(xr::ViewConfigurationType::PRIMARY_STEREO)?;
self.session_running = true;
tracing::info!("XR Session started");
}
xr::SessionState::STOPPING => {
self.session.end()?;
self.session_running = false;
tracing::info!("XR Session ended");
}
xr::SessionState::EXITING | xr::SessionState::LOSS_PENDING => {
tracing::info!("XR Session exiting");
return Ok(false);
}
_ => {}
}
}
xr::Event::InstanceLossPending(_) => {
tracing::info!("XR Instance loss pending");
return Ok(false);
}
_ => {}
}
}
Ok(true)
}
pub fn wait_frame(&mut self) -> Result<Option<xr::FrameState>, Box<dyn std::error::Error>> {
if !self.session_running {
return Ok(None);
}
Ok(Some(self.frame_wait.wait()?))
}
pub fn update_input(
&mut self,
delta_time: f32,
predicted_display_time: xr::Time,
locomotion_enabled: bool,
locomotion_speed: f32,
) -> Result<XrInput, Box<dyn std::error::Error>> {
self.session.sync_actions(&[(&self.action_set).into()])?;
let move_state = self.move_action.state(&self.session, xr::Path::NULL)?;
let turn_state = self.turn_action.state(&self.session, xr::Path::NULL)?;
let (_, views) = self.session.locate_views(
xr::ViewConfigurationType::PRIMARY_STEREO,
predicted_display_time,
&self.stage,
)?;
let (head_yaw, openxr_quat, head_pose_position) = if !views.is_empty() {
let head_pose = &views[0].pose;
let quat = nalgebra_glm::quat(
head_pose.orientation.w,
head_pose.orientation.z,
head_pose.orientation.y,
head_pose.orientation.x,
);
let head_forward =
nalgebra_glm::quat_rotate_vec3(&quat, &nalgebra_glm::vec3(0.0, 0.0, -1.0));
let yaw = (-head_forward.x).atan2(-head_forward.z);
(yaw, Some(quat), Some(head_pose.position))
} else {
(0.0, None, None)
};
if turn_state.current_state.x.abs() > 0.1 {
let turn_speed = 2.0;
self.player_yaw -= turn_state.current_state.x * turn_speed * delta_time;
}
if locomotion_enabled {
let move_speed = if locomotion_speed > 0.0 {
locomotion_speed
} else {
2.0
};
if move_state.current_state.x.abs() > 0.1 || move_state.current_state.y.abs() > 0.1 {
let combined_yaw = head_yaw + self.player_yaw;
let move_x = move_state.current_state.x;
let move_z = -move_state.current_state.y;
let rotated_x = move_x * combined_yaw.cos() + move_z * combined_yaw.sin();
let rotated_z = -move_x * combined_yaw.sin() + move_z * combined_yaw.cos();
self.player_position.x += rotated_x * move_speed * delta_time;
self.player_position.z += rotated_z * move_speed * delta_time;
}
let left_trigger_value = self
.left_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let right_trigger_value = self
.right_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let vertical_input = right_trigger_value - left_trigger_value;
if vertical_input.abs() > 0.1 {
self.player_position.y += vertical_input * move_speed * delta_time;
}
}
let (head_position, head_orientation) = if let (Some(quat), Some(pos)) =
(openxr_quat, head_pose_position)
{
let local_x = -pos.x;
let local_z = -pos.z;
let rotated_x = local_x * self.player_yaw.cos() + local_z * self.player_yaw.sin();
let rotated_z = -local_x * self.player_yaw.sin() + local_z * self.player_yaw.cos();
let world_head_pos =
self.player_position + nalgebra_glm::vec3(rotated_x, pos.y, rotated_z);
let player_rotation =
nalgebra_glm::quat_angle_axis(self.player_yaw, &nalgebra_glm::vec3(0.0, 1.0, 0.0));
let flip_x = nalgebra_glm::quat_angle_axis(
std::f32::consts::PI,
&nalgebra_glm::vec3(1.0, 0.0, 0.0),
);
let world_head_orientation = player_rotation * flip_x * quat;
(world_head_pos, world_head_orientation)
} else {
(self.player_position, nalgebra_glm::quat_identity())
};
let left_trigger = self
.left_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let right_trigger = self
.right_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let left_grip = self
.left_grip_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let right_grip = self
.right_grip_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let a_button = self
.a_button_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(false);
let b_button = self
.b_button_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(false);
let left_thumbstick_click = self
.left_thumbstick_click_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(false);
let left_hand_pose = self
.left_hand_space
.locate(&self.stage, predicted_display_time)
.ok()
.filter(|loc| {
loc.location_flags.contains(
xr::SpaceLocationFlags::POSITION_VALID
| xr::SpaceLocationFlags::ORIENTATION_VALID,
)
})
.map(|loc| loc.pose);
let right_hand_pose = self
.right_hand_space
.locate(&self.stage, predicted_display_time)
.ok()
.filter(|loc| {
loc.location_flags.contains(
xr::SpaceLocationFlags::POSITION_VALID
| xr::SpaceLocationFlags::ORIENTATION_VALID,
)
})
.map(|loc| loc.pose);
Ok(XrInput {
thumbstick: nalgebra_glm::vec2(move_state.current_state.x, move_state.current_state.y),
right_thumbstick: nalgebra_glm::vec2(
turn_state.current_state.x,
turn_state.current_state.y,
),
left_trigger,
right_trigger,
left_grip,
right_grip,
a_button,
b_button,
left_thumbstick_click,
left_hand_pose,
right_hand_pose,
player_position: self.player_position,
player_yaw: self.player_yaw,
head_yaw,
head_position,
head_orientation,
})
}
pub fn begin_frame(&mut self) -> Result<Option<XrFrameContext>, Box<dyn std::error::Error>> {
let Some(frame_state) = self.wait_frame()? else {
return Ok(None);
};
self.frame_stream.begin()?;
if !frame_state.should_render {
self.frame_stream.end(
frame_state.predicted_display_time,
xr::EnvironmentBlendMode::OPAQUE,
&[],
)?;
return Ok(None);
}
let (view_state_flags, views) = self.session.locate_views(
xr::ViewConfigurationType::PRIMARY_STEREO,
frame_state.predicted_display_time,
&self.stage,
)?;
if !view_state_flags
.contains(xr::ViewStateFlags::POSITION_VALID | xr::ViewStateFlags::ORIENTATION_VALID)
{
self.frame_stream.end(
frame_state.predicted_display_time,
xr::EnvironmentBlendMode::OPAQUE,
&[],
)?;
return Ok(None);
}
let image_index = self.swapchain.acquire_image()?;
self.swapchain.wait_image(xr::Duration::INFINITE)?;
Ok(Some(XrFrameContext {
frame_state,
views,
image_index,
player_position: self.player_position,
player_yaw: self.player_yaw,
}))
}
pub fn get_eye_texture_view(
&self,
frame_ctx: &XrFrameContext,
eye_index: usize,
) -> wgpu::TextureView {
let swapchain_texture = &self.swapchain_buffers[frame_ctx.image_index as usize];
swapchain_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some(&format!("XR Eye {}", eye_index)),
format: Some(wgpu::TextureFormat::Rgba8UnormSrgb),
dimension: Some(wgpu::TextureViewDimension::D2),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: eye_index as u32,
array_layer_count: Some(1),
usage: None,
})
}
pub fn end_frame(
&mut self,
frame_ctx: XrFrameContext,
) -> Result<(), Box<dyn std::error::Error>> {
self.swapchain.release_image()?;
let rect = xr::Rect2Di {
offset: xr::Offset2Di { x: 0, y: 0 },
extent: xr::Extent2Di {
width: self.resolution.0 as i32,
height: self.resolution.1 as i32,
},
};
let sub_images: Vec<_> = frame_ctx
.views
.iter()
.enumerate()
.map(|(view_index, view)| {
xr::CompositionLayerProjectionView::new()
.pose(view.pose)
.fov(view.fov)
.sub_image(
xr::SwapchainSubImage::new()
.swapchain(&self.swapchain)
.image_array_index(view_index as u32)
.image_rect(rect),
)
})
.collect();
let projection_layer = xr::CompositionLayerProjection::new()
.space(&self.stage)
.views(&sub_images);
self.frame_stream.end(
frame_ctx.frame_state.predicted_display_time,
xr::EnvironmentBlendMode::OPAQUE,
&[&projection_layer],
)?;
Ok(())
}
}
pub struct XrFrameContext {
pub frame_state: xr::FrameState,
pub views: Vec<xr::View>,
pub image_index: u32,
pub player_position: nalgebra_glm::Vec3,
pub player_yaw: f32,
}
impl XrFrameContext {
pub fn view_matrix(&self, eye_index: usize) -> nalgebra_glm::Mat4 {
let view = &self.views[eye_index];
let pose = &view.pose;
let rotation = {
let orientation = pose.orientation;
let flip_x = nalgebra_glm::quat_angle_axis(
180.0_f32.to_radians(),
&nalgebra_glm::vec3(1.0, 0.0, 0.0),
);
let openxr_quat =
nalgebra_glm::quat(orientation.w, orientation.z, orientation.y, orientation.x);
let player_rotation =
nalgebra_glm::quat_angle_axis(self.player_yaw, &nalgebra_glm::vec3(0.0, 1.0, 0.0));
player_rotation * flip_x * openxr_quat
};
let local_x = -pose.position.x;
let local_z = -pose.position.z;
let rotated_x = local_x * self.player_yaw.cos() + local_z * self.player_yaw.sin();
let rotated_z = -local_x * self.player_yaw.sin() + local_z * self.player_yaw.cos();
let translation = nalgebra_glm::vec3(rotated_x, pose.position.y, rotated_z);
let eye = translation + self.player_position;
let target =
eye + nalgebra_glm::quat_rotate_vec3(&rotation, &nalgebra_glm::vec3(0.0, 0.0, 1.0));
let up = nalgebra_glm::quat_rotate_vec3(&rotation, &nalgebra_glm::vec3(0.0, 1.0, 0.0));
nalgebra_glm::look_at_rh(&eye, &target, &up)
}
pub fn projection_matrix(&self, eye_index: usize) -> nalgebra_glm::Mat4 {
let view = &self.views[eye_index];
let fov = &view.fov;
let tan_left = fov.angle_left.tan();
let tan_right = fov.angle_right.tan();
let tan_up = fov.angle_up.tan();
let tan_down = fov.angle_down.tan();
let near = 0.1_f32;
let tan_width = tan_right - tan_left;
let tan_height = tan_up - tan_down;
let a11 = 2.0 / tan_width;
let a22 = 2.0 / tan_height;
let a31 = (tan_right + tan_left) / tan_width;
let a32 = (tan_up + tan_down) / tan_height;
let mut proj = nalgebra_glm::Mat4::zeros();
proj[(0, 0)] = a11;
proj[(1, 1)] = a22;
proj[(0, 2)] = a31;
proj[(1, 2)] = a32;
proj[(2, 2)] = 0.0;
proj[(2, 3)] = near;
proj[(3, 2)] = -1.0;
proj
}
pub fn camera_position(&self, eye_index: usize) -> nalgebra_glm::Vec3 {
let view = &self.views[eye_index];
let pose = &view.pose;
let local_x = -pose.position.x;
let local_z = -pose.position.z;
let rotated_x = local_x * self.player_yaw.cos() + local_z * self.player_yaw.sin();
let rotated_z = -local_x * self.player_yaw.sin() + local_z * self.player_yaw.cos();
let translation = nalgebra_glm::vec3(rotated_x, pose.position.y, rotated_z);
translation + self.player_position
}
pub fn eye_count(&self) -> usize {
self.views.len()
}
}
pub struct XrRenderer {
graph: RenderGraph<World>,
_depth_id: crate::render::wgpu::rendergraph::ResourceId,
_scene_color_id: crate::render::wgpu::rendergraph::ResourceId,
swapchain_id: crate::render::wgpu::rendergraph::ResourceId,
_brdf_lut_texture: wgpu::Texture,
brdf_lut_view: wgpu::TextureView,
}
impl XrRenderer {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
resolution: (u32, u32),
surface_format: wgpu::TextureFormat,
) -> Result<Self, Box<dyn std::error::Error>> {
let depth_format = wgpu::TextureFormat::Depth32Float;
let hdr_format = wgpu::TextureFormat::Rgba16Float;
let clear_pass = passes::ClearPass::new();
let sky_pass = passes::SkyPass::new(device, hdr_format, depth_format);
let grid_pass = passes::GridPass::new(device, hdr_format, depth_format);
let scene_pass = passes::ScenePass::new(device, hdr_format, depth_format);
let shadow_depth_pass = passes::ShadowDepthPass::new(device, queue);
let mesh_pass = passes::MeshPass::new(device, queue, hdr_format, depth_format, resolution);
let skinned_mesh_pass =
passes::SkinnedMeshPass::new(device, queue, hdr_format, depth_format);
let water_pass = passes::WaterPass::new(device, hdr_format, depth_format);
let water_mesh_pass = passes::WaterMeshPass::new(device, hdr_format, depth_format);
let lines_pass = passes::LinesPass::new(device, hdr_format);
let particle_pass = passes::ParticlePass::new(device, hdr_format);
let mut graph = RenderGraph::new();
let depth_id = graph
.add_depth_texture("depth")
.size(resolution.0.max(1), resolution.1.max(1))
.clear_depth(0.0)
.transient();
let scene_color_id = graph
.add_color_texture("scene_color")
.format(hdr_format)
.size(resolution.0.max(1), resolution.1.max(1))
.clear_color(wgpu::Color::BLACK)
.transient();
let swapchain_id = graph
.add_color_texture("swapchain")
.format(surface_format)
.external();
let shadow_depth_id = graph
.add_depth_texture("shadow_depth")
.size(2048, 2048)
.format(wgpu::TextureFormat::Depth32Float)
.clear_depth(0.0)
.transient();
let spotlight_shadow_atlas_id = graph
.add_depth_texture("spotlight_shadow_atlas")
.size(2048, 2048)
.format(wgpu::TextureFormat::Depth32Float)
.clear_depth(0.0)
.transient();
let entity_id_id = graph
.add_color_texture("entity_id")
.format(wgpu::TextureFormat::R32Float)
.size(resolution.0.max(1), resolution.1.max(1))
.usage(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
.clear_color(wgpu::Color::TRANSPARENT)
.transient();
let view_normals_id = graph
.add_color_texture("view_normals")
.format(wgpu::TextureFormat::Rgba16Float)
.size(resolution.0.max(1), resolution.1.max(1))
.usage(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
.clear_color(wgpu::Color::TRANSPARENT)
.transient();
graph.add_pass(
Box::new(clear_pass),
&[("color", scene_color_id), ("depth", depth_id)],
)?;
graph.add_pass(Box::new(sky_pass), &[("color", scene_color_id)])?;
graph.add_pass(
Box::new(scene_pass),
&[("color", scene_color_id), ("depth", depth_id)],
)?;
graph.add_pass(
Box::new(shadow_depth_pass),
&[
("shadow_depth", shadow_depth_id),
("spotlight_shadow_atlas", spotlight_shadow_atlas_id),
],
)?;
graph.add_pass(
Box::new(mesh_pass),
&[
("color", scene_color_id),
("depth", depth_id),
("shadow_depth", shadow_depth_id),
("spotlight_shadow_atlas", spotlight_shadow_atlas_id),
("entity_id", entity_id_id),
("view_normals", view_normals_id),
],
)?;
graph.add_pass(
Box::new(skinned_mesh_pass),
&[
("color", scene_color_id),
("depth", depth_id),
("shadow_depth", shadow_depth_id),
("spotlight_shadow_atlas", spotlight_shadow_atlas_id),
],
)?;
graph.add_pass(
Box::new(water_pass),
&[("color", scene_color_id), ("depth", depth_id)],
)?;
graph.add_pass(
Box::new(water_mesh_pass),
&[("color", scene_color_id), ("depth", depth_id)],
)?;
graph.add_pass(
Box::new(grid_pass),
&[("color", scene_color_id), ("depth", depth_id)],
)?;
graph.add_pass(
Box::new(lines_pass),
&[("color", scene_color_id), ("depth", depth_id)],
)?;
graph.add_pass(
Box::new(particle_pass),
&[("color", scene_color_id), ("depth", depth_id)],
)?;
let bloom_width = (resolution.0 / 4).max(1);
let bloom_height = (resolution.1 / 4).max(1);
let bloom_texture_id = graph
.add_color_texture("bloom")
.format(hdr_format)
.size(bloom_width, bloom_height)
.clear_color(wgpu::Color::BLACK)
.transient();
let bloom_pass = passes::BloomPass::new(device, bloom_width * 2, bloom_height * 2);
graph.add_pass(
Box::new(bloom_pass),
&[("hdr", scene_color_id), ("bloom", bloom_texture_id)],
)?;
let ssao_width = (resolution.0 / 2).max(1);
let ssao_height = (resolution.1 / 2).max(1);
let ssao_raw_id = graph
.add_color_texture("ssao_raw")
.format(wgpu::TextureFormat::R8Unorm)
.size(ssao_width, ssao_height)
.usage(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
.clear_color(wgpu::Color::WHITE)
.transient();
let ssao_id = graph
.add_color_texture("ssao")
.format(wgpu::TextureFormat::R8Unorm)
.size(ssao_width, ssao_height)
.usage(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
.clear_color(wgpu::Color::WHITE)
.transient();
let ssao_pass = passes::SsaoPass::new(device);
graph.add_pass(
Box::new(ssao_pass),
&[
("depth", depth_id),
("view_normals", view_normals_id),
("ssao_raw", ssao_raw_id),
],
)?;
let ssao_blur_pass = passes::SsaoBlurPass::new(device);
graph.add_pass(
Box::new(ssao_blur_pass),
&[
("ssao_raw", ssao_raw_id),
("depth", depth_id),
("view_normals", view_normals_id),
("ssao", ssao_id),
],
)?;
let postprocess_pass = passes::PostProcessPass::new(device, surface_format, 0.1);
graph.add_pass(
Box::new(postprocess_pass),
&[
("hdr", scene_color_id),
("bloom", bloom_texture_id),
("ssao", ssao_id),
("output", swapchain_id),
],
)?;
graph.compile()?;
let (brdf_lut_texture, brdf_lut_view) = brdf_lut::generate_brdf_lut(device, queue);
Ok(Self {
graph,
_depth_id: depth_id,
_scene_color_id: scene_color_id,
swapchain_id,
_brdf_lut_texture: brdf_lut_texture,
brdf_lut_view,
})
}
pub fn render_eye(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
world: &mut World,
eye_texture_view: wgpu::TextureView,
resolution: (u32, u32),
) -> Result<(), Box<dyn std::error::Error>> {
self.graph.set_external_texture(
self.swapchain_id,
None,
eye_texture_view,
resolution.0,
resolution.1,
);
let command_buffers = self.graph.execute(device, queue, world)?;
queue.submit(command_buffers);
Ok(())
}
pub fn process_commands(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
world: &mut World,
) {
let mut texture_load_commands = Vec::new();
let mut hdr_skybox_commands = Vec::new();
let mut procedural_ibl_commands = Vec::new();
let mut ibl_snapshot_commands = Vec::new();
for cmd in world.resources.command_queue.iter() {
match cmd {
crate::ecs::world::WorldCommand::LoadTexture {
name,
rgba_data,
width,
height,
} => {
texture_load_commands.push((name.clone(), rgba_data.clone(), *width, *height));
}
crate::ecs::world::WorldCommand::LoadHdrSkybox { hdr_data } => {
hdr_skybox_commands.push(hdr_data.clone());
}
crate::ecs::world::WorldCommand::LoadHdrSkyboxFromPath { path } => {
match std::fs::read(path) {
Ok(bytes) => {
hdr_skybox_commands.push(bytes);
}
Err(error) => {
tracing::error!(
"Failed to read HDR file {}: {}",
path.display(),
error
);
}
}
}
crate::ecs::world::WorldCommand::CaptureProceduralAtmosphereIBL {
atmosphere,
time,
} => {
procedural_ibl_commands.push((*atmosphere, *time));
}
crate::ecs::world::WorldCommand::CaptureIblSnapshots { atmosphere, hours } => {
ibl_snapshot_commands.push((*atmosphere, hours.clone()));
}
_ => {}
}
}
if !texture_load_commands.is_empty() {
for (name, rgba_data, width, height) in texture_load_commands {
if let Err(e) = world.resources.texture_cache.load_texture_from_raw_rgba(
device,
queue,
name.clone(),
&rgba_data,
(width, height),
) {
tracing::error!("Failed to load texture: {}", e);
continue;
}
if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
&& let Some(texture_entry) = world.resources.texture_cache.get(&name)
{
mesh_pass.register_texture(
name.clone(),
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
}
if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::SkinnedMeshPass>()
&& let Some(texture_entry) = world.resources.texture_cache.get(&name)
{
skinned_mesh_pass.register_texture(
name,
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
}
}
world
.resources
.command_queue
.retain(|cmd| !matches!(cmd, crate::ecs::world::WorldCommand::LoadTexture { .. }));
}
if !hdr_skybox_commands.is_empty() {
let mut ibl_views = None;
if let Some(sky_pass) = self.graph.get_pass_mut("sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
{
for hdr_data in hdr_skybox_commands {
ibl_views = Some(sky_pass.load_hdr_skybox(device, queue, &hdr_data));
}
}
if let Some((irradiance_view, prefiltered_view)) = ibl_views {
if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>(
)
{
skinned_mesh_pass.set_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
world.resources.ibl_views.irradiance_view = Some(irradiance_view);
world.resources.ibl_views.prefiltered_view = Some(prefiltered_view);
}
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::LoadHdrSkybox { .. }
| crate::ecs::world::WorldCommand::LoadHdrSkyboxFromPath { .. }
)
});
}
#[cfg(feature = "assets")]
if !procedural_ibl_commands.is_empty() {
let mut ibl_views = None;
if let Some(sky_pass) = self.graph.get_pass_mut("sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
{
for (atmosphere, time) in procedural_ibl_commands {
ibl_views =
sky_pass.capture_procedural_atmosphere(device, queue, atmosphere, time);
}
}
if let Some((irradiance_view, prefiltered_view)) = ibl_views {
if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>(
)
{
skinned_mesh_pass.set_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
world.resources.ibl_views.irradiance_view = Some(irradiance_view);
world.resources.ibl_views.prefiltered_view = Some(prefiltered_view);
}
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::CaptureProceduralAtmosphereIBL { .. }
)
});
}
#[cfg(feature = "assets")]
if !ibl_snapshot_commands.is_empty() {
if let Some(sky_pass) = self.graph.get_pass_mut("sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
{
for (atmosphere, hours) in ibl_snapshot_commands {
sky_pass.capture_ibl_snapshots(device, queue, atmosphere, &hours);
}
}
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::CaptureIblSnapshots { .. }
)
});
}
if let Some(lines_pass) = self.graph.get_pass_mut("lines_pass")
&& let Some(lines_pass) =
(lines_pass as &mut dyn std::any::Any).downcast_mut::<passes::LinesPass>()
{
passes::sync_lines_data(lines_pass, device, queue, world);
}
}
pub fn update_per_frame_passes(&mut self, world: &mut World) {
let point_shadow_view = self
.graph
.get_pass_mut("shadow_depth_pass")
.and_then(|pass| {
(pass as &dyn std::any::Any)
.downcast_ref::<passes::ShadowDepthPass>()
.map(|shadow_pass| shadow_pass.point_light_cubemap_view().clone())
});
if let Some(view) = point_shadow_view {
if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_point_shadow_cubemap(view.clone());
}
if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::SkinnedMeshPass>()
{
skinned_mesh_pass.update_point_shadow_cubemap(view);
}
}
{
let raw_hour = world.resources.graphics.day_night_hour;
let hour = raw_hour - (raw_hour / 24.0).floor() * 24.0;
let world_id = world.resources.world_id;
let mut bracket_views: Option<(
wgpu::TextureView,
wgpu::TextureView,
wgpu::TextureView,
wgpu::TextureView,
f32,
)> = None;
if let Some(sky_pass) = self.graph.get_pass_mut("sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
&& let Some((index_a, index_b, blend)) =
sky_pass.select_ibl_bracket(world.resources.graphics.atmosphere, hour)
{
let irr_a =
sky_pass.ibl_snapshots[index_a]
.1
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let pref_a =
sky_pass.ibl_snapshots[index_a]
.2
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let irr_b =
sky_pass.ibl_snapshots[index_b]
.1
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let pref_b =
sky_pass.ibl_snapshots[index_b]
.2
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
bracket_views = Some((irr_a, pref_a, irr_b, pref_b, blend));
}
if let Some((irr_a, pref_a, irr_b, pref_b, blend)) = bracket_views {
world.resources.graphics.ibl_blend_factor = blend;
if let Some(mesh_pass) = self.graph.get_pass_mut("mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_ibl_textures_blended(
self.brdf_lut_view.clone(),
irr_a.clone(),
pref_a.clone(),
irr_b.clone(),
pref_b.clone(),
blend,
);
}
if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>(
)
{
skinned_mesh_pass.update_ibl_textures_blended_for_world(
world_id,
passes::geometry::BlendedIblViews {
brdf_lut: self.brdf_lut_view.clone(),
irradiance_a: irr_a.clone(),
prefiltered_a: pref_a.clone(),
irradiance_b: irr_b,
prefiltered_b: pref_b,
blend_factor: blend,
},
);
}
world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
world.resources.ibl_views.irradiance_view = Some(irr_a);
world.resources.ibl_views.prefiltered_view = Some(pref_a);
}
}
}
pub fn graph_mut(&mut self) -> &mut RenderGraph<World> {
&mut self.graph
}
}
pub fn launch_xr<S: crate::run::State + 'static>(
mut state: S,
) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(feature = "tracing"))]
tracing_subscriber::fmt::init();
#[cfg(all(feature = "tracing", not(feature = "tracy"), not(feature = "chrome")))]
let _guard = {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let file_appender = tracing_appender::rolling::daily("logs", "nightshade.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer())
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false),
)
.init();
guard
};
#[cfg(all(feature = "tracy", not(feature = "chrome")))]
let _guard = {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let file_appender = tracing_appender::rolling::daily("logs", "nightshade.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer())
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false),
)
.with(tracing_tracy::TracyLayer::default())
.init();
guard
};
#[cfg(all(feature = "chrome", not(feature = "tracy")))]
let _guard = {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let file_appender = tracing_appender::rolling::daily("logs", "nightshade.log");
let (non_blocking, file_guard) = tracing_appender::non_blocking(file_appender);
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let (chrome_layer, chrome_guard) = tracing_chrome::ChromeLayerBuilder::new()
.file("logs/trace.json")
.build();
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer())
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false),
)
.with(chrome_layer)
.init();
(file_guard, chrome_guard)
};
#[cfg(all(feature = "tracy", feature = "chrome"))]
let _guard = {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let file_appender = tracing_appender::rolling::daily("logs", "nightshade.log");
let (non_blocking, file_guard) = tracing_appender::non_blocking(file_appender);
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let (chrome_layer, chrome_guard) = tracing_chrome::ChromeLayerBuilder::new()
.file("logs/trace.json")
.build();
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer())
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false),
)
.with(tracing_tracy::TracyLayer::default())
.with(chrome_layer)
.init();
(file_guard, chrome_guard)
};
tracing::info!("Initializing OpenXR mode: {}", state.title());
let mut xr_context = XrContext::new()?;
let mut world = World::default();
let resolution = xr_context.resolution();
let surface_format = xr_context.surface_format();
tracing::info!(
"Creating XR renderer with resolution {}x{}",
resolution.0,
resolution.1
);
let mut renderer = XrRenderer::new(
xr_context.device(),
xr_context.queue(),
resolution,
surface_format,
)?;
tracing::info!("XR renderer created successfully");
world.resources.xr.locomotion_enabled = true;
world.resources.frame_schedule = crate::schedule::build_default_frame_schedule();
tracing::info!("Calling state.initialize()");
state.initialize(&mut world);
tracing::info!("State initialized successfully");
if let Some(initial_yaw) = world.resources.xr.initial_player_yaw {
xr_context.set_player_yaw(initial_yaw);
}
if let Some(initial_position) = world.resources.xr.initial_player_position {
xr_context.set_player_position(initial_position);
} else {
let character_controllers: Vec<_> = world
.query_entities(
crate::ecs::world::CHARACTER_CONTROLLER | crate::ecs::world::LOCAL_TRANSFORM,
)
.collect();
if let Some(entity) = character_controllers.first()
&& let Some(transform) = world.get_local_transform(*entity)
{
let floor_offset = if let Some(cc) = world.get_character_controller(*entity) {
match &cc.shape {
crate::ecs::physics::ColliderShape::Capsule {
half_height,
radius,
} => half_height + radius,
crate::ecs::physics::ColliderShape::Cylinder { half_height, .. } => {
*half_height
}
crate::ecs::physics::ColliderShape::Cuboid { hy, .. } => *hy,
crate::ecs::physics::ColliderShape::Ball { radius } => *radius,
_ => 1.0,
}
} else {
1.0
};
let floor_position = nalgebra_glm::vec3(
transform.translation.x,
transform.translation.y - floor_offset,
transform.translation.z,
);
xr_context.set_player_position(floor_position);
}
}
renderer.process_commands(xr_context.device(), xr_context.queue(), &mut world);
let mut last_render_time = Instant::now();
let initial_time = Instant::now();
tracing::info!("Starting XR render loop");
loop {
if !xr_context.poll_events()? {
tracing::info!("XR session ended, exiting");
break;
}
let Some(mut frame_ctx) = xr_context.begin_frame()? else {
std::thread::sleep(std::time::Duration::from_millis(10));
continue;
};
let now = Instant::now();
let delta_time = (now - last_render_time).as_secs_f32();
last_render_time = now;
world.resources.window.timing.delta_time = delta_time;
world.resources.window.timing.raw_delta_time = delta_time;
world.resources.window.timing.uptime_milliseconds = (now - initial_time).as_millis() as u64;
world.resources.window.cached_viewport_size = Some(resolution);
let locomotion_enabled = world.resources.xr.locomotion_enabled;
let locomotion_speed = world.resources.xr.locomotion_speed;
let xr_input = xr_context.update_input(
delta_time,
frame_ctx.frame_state.predicted_display_time,
locomotion_enabled,
locomotion_speed,
)?;
frame_ctx.player_yaw = xr_input.player_yaw;
frame_ctx.player_position = xr_input.player_position;
world.resources.xr.input = Some(xr_input);
state.run_systems(&mut world);
#[cfg(all(feature = "mcp", not(target_arch = "wasm32")))]
crate::run::process_mcp_commands_with_state(&mut world, &mut *state);
crate::run::run_frame_systems(&mut world);
let character_controllers: Vec<_> = world
.query_entities(
crate::ecs::world::CHARACTER_CONTROLLER | crate::ecs::world::LOCAL_TRANSFORM,
)
.collect();
if let Some(entity) = character_controllers.first()
&& let Some(transform) = world.get_local_transform(*entity)
{
let floor_offset = if let Some(cc) = world.get_character_controller(*entity) {
match &cc.shape {
crate::ecs::physics::ColliderShape::Capsule {
half_height,
radius,
} => half_height + radius,
crate::ecs::physics::ColliderShape::Cylinder { half_height, .. } => {
*half_height
}
crate::ecs::physics::ColliderShape::Cuboid { hy, .. } => *hy,
crate::ecs::physics::ColliderShape::Ball { radius } => *radius,
_ => 1.0,
}
} else {
1.0
};
let floor_position = nalgebra_glm::vec3(
transform.translation.x,
transform.translation.y - floor_offset,
transform.translation.z,
);
xr_context.set_player_position(floor_position);
}
renderer.process_commands(xr_context.device(), xr_context.queue(), &mut world);
renderer.update_per_frame_passes(&mut world);
for eye_index in 0..frame_ctx.eye_count() {
let view_matrix = frame_ctx.view_matrix(eye_index);
let projection_matrix = frame_ctx.projection_matrix(eye_index);
let camera_position = frame_ctx.camera_position(eye_index);
world.resources.xr.camera_override =
Some(crate::ecs::camera::queries::CameraMatrices {
camera_position,
projection: projection_matrix,
view: view_matrix,
});
let eye_texture_view = xr_context.get_eye_texture_view(&frame_ctx, eye_index);
renderer.render_eye(
xr_context.device(),
xr_context.queue(),
&mut world,
eye_texture_view,
resolution,
)?;
}
world.resources.xr.camera_override = None;
world.resources.xr.input = None;
xr_context.end_frame(frame_ctx)?;
}
Ok(())
}