use std::sync::Arc;
use std::time::Instant;
use winit::{
application::ApplicationHandler,
event::{WindowEvent, DeviceEvent, DeviceId},
event_loop::ActiveEventLoop,
window::{Window, WindowId},
dpi::PhysicalSize,
};
use log::{info, error, debug};
use anvilkit_ecs::app::App;
use anvilkit_ecs::physics::DeltaTime;
use anvilkit_input::prelude::{InputState, KeyCode, MouseButton};
use crate::window::{WindowConfig, WindowState};
use crate::renderer::{RenderDevice, RenderSurface};
use crate::renderer::assets::RenderAssets;
use crate::renderer::draw::{ActiveCamera, DrawCommandList, SceneLights};
use crate::renderer::state::{RenderState, PbrSceneUniform, GpuLight, MAX_LIGHTS};
use crate::renderer::buffer::{
create_uniform_buffer, create_depth_texture_msaa,
create_hdr_render_target, create_hdr_msaa_texture,
create_sampler, create_texture_linear, create_shadow_map, create_shadow_sampler,
Vertex, PbrVertex, SHADOW_MAP_SIZE,
};
use crate::renderer::{RenderPipelineBuilder, DEPTH_FORMAT};
use crate::renderer::ibl::generate_brdf_lut;
use anvilkit_core::error::{AnvilKitError, Result};
pub fn pack_lights(scene_lights: &SceneLights) -> ([GpuLight; MAX_LIGHTS], u32) {
let mut lights = [GpuLight::default(); MAX_LIGHTS];
let mut count = 0u32;
let dir = &scene_lights.directional;
lights[0] = GpuLight {
position_type: [0.0, 0.0, 0.0, 0.0], direction_range: [dir.direction.x, dir.direction.y, dir.direction.z, 0.0],
color_intensity: [dir.color.x, dir.color.y, dir.color.z, dir.intensity],
params: [0.0; 4],
};
count += 1;
for pl in &scene_lights.point_lights {
if count as usize >= MAX_LIGHTS { break; }
lights[count as usize] = GpuLight {
position_type: [pl.position.x, pl.position.y, pl.position.z, 1.0],
direction_range: [0.0, 0.0, 0.0, pl.range],
color_intensity: [pl.color.x, pl.color.y, pl.color.z, pl.intensity],
params: [0.0; 4],
};
count += 1;
}
for sl in &scene_lights.spot_lights {
if count as usize >= MAX_LIGHTS { break; }
lights[count as usize] = GpuLight {
position_type: [sl.position.x, sl.position.y, sl.position.z, 2.0],
direction_range: [sl.direction.x, sl.direction.y, sl.direction.z, sl.range],
color_intensity: [sl.color.x, sl.color.y, sl.color.z, sl.intensity],
params: [sl.inner_cone_angle.cos(), sl.outer_cone_angle.cos(), 0.0, 0.0],
};
count += 1;
}
(lights, count)
}
pub fn compute_light_space_matrix(light_direction: &glam::Vec3) -> glam::Mat4 {
let light_dir = light_direction.normalize();
let light_pos = -light_dir * 15.0;
let light_view = glam::Mat4::look_at_lh(light_pos, glam::Vec3::ZERO, glam::Vec3::Y);
let light_proj = glam::Mat4::orthographic_lh(-10.0, 10.0, -10.0, 10.0, 0.1, 30.0);
light_proj * light_view
}
const SHADOW_SHADER: &str = include_str!("../shaders/shadow.wgsl");
const TONEMAP_SHADER: &str = include_str!("../shaders/tonemap.wgsl");
pub struct RenderApp {
config: WindowConfig,
window: Option<Arc<Window>>,
window_state: WindowState,
render_device: Option<RenderDevice>,
render_surface: Option<RenderSurface>,
exit_requested: bool,
app: Option<App>,
gpu_initialized: bool,
last_frame_time: Instant,
}
impl RenderApp {
pub fn new(config: WindowConfig) -> Self {
info!("创建渲染应用: {}", config.title);
Self {
config,
window: None,
window_state: WindowState::new(),
render_device: None,
render_surface: None,
exit_requested: false,
app: None,
gpu_initialized: false,
last_frame_time: Instant::now(),
}
}
pub fn run(app: App) {
let event_loop = winit::event_loop::EventLoop::new().unwrap();
let window_config = app.world.get_resource::<crate::plugin::RenderConfig>()
.map(|c| c.window_config.clone())
.unwrap_or_default();
let mut render_app = Self::new(window_config);
render_app.app = Some(app);
event_loop.run_app(&mut render_app).unwrap();
}
pub fn config(&self) -> &WindowConfig {
&self.config
}
pub fn window_state(&self) -> &WindowState {
&self.window_state
}
pub fn window(&self) -> Option<&Arc<Window>> {
self.window.as_ref()
}
pub fn request_exit(&mut self) {
info!("请求退出应用");
self.exit_requested = true;
}
pub fn is_exit_requested(&self) -> bool {
self.exit_requested
}
pub fn render_device(&self) -> Option<&RenderDevice> {
self.render_device.as_ref()
}
pub fn surface_format(&self) -> Option<wgpu::TextureFormat> {
self.render_surface.as_ref().map(|s| s.format())
}
pub fn get_current_frame(&self) -> Option<wgpu::SurfaceTexture> {
self.render_surface.as_ref().and_then(|s| s.get_current_frame().ok())
}
fn create_window(&mut self, event_loop: &ActiveEventLoop) -> Result<()> {
if self.window.is_some() {
return Ok(());
}
info!("创建窗口: {} ({}x{})",
self.config.title, self.config.width, self.config.height);
let attributes = self.config.to_window_attributes();
let window = event_loop.create_window(attributes)
.map_err(|e| AnvilKitError::render(format!("创建窗口失败: {}", e)))?;
let size = window.inner_size();
self.window_state.set_size(size.width, size.height);
self.window_state.set_scale_factor(window.scale_factor());
self.window = Some(Arc::new(window));
info!("窗口创建成功");
Ok(())
}
async fn init_render(&mut self) -> Result<()> {
if self.render_device.is_some() {
return Ok(());
}
let window = self.window.as_ref()
.ok_or_else(|| AnvilKitError::render("窗口未创建".to_string()))?;
info!("初始化渲染设备和表面");
let device = RenderDevice::new(window).await?;
let surface = RenderSurface::new_with_vsync(&device, window, self.config.vsync)?;
self.render_device = Some(device);
self.render_surface = Some(surface);
info!("渲染设备和表面初始化成功");
Ok(())
}
fn inject_render_state_to_ecs(&mut self) {
if self.gpu_initialized {
return;
}
let Some(app) = &mut self.app else { return };
let Some(device) = &self.render_device else { return };
let Some(surface) = &self.render_surface else { return };
let format = surface.format();
let (w, h) = self.window_state.size();
let initial_uniform = PbrSceneUniform::default();
let scene_uniform_buffer = create_uniform_buffer(
device,
"ECS Scene Uniform",
bytemuck::bytes_of(&initial_uniform),
);
let scene_bind_group_layout = device.device().create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("ECS Scene BGL"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
},
);
let scene_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ECS Scene BG"),
layout: &scene_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: scene_uniform_buffer.as_entire_binding(),
}],
});
let (_, depth_texture_view) = create_depth_texture_msaa(device, w, h, "ECS Depth MSAA");
let (_, hdr_texture_view) = create_hdr_render_target(device, w, h, "ECS HDR RT");
let (_, hdr_msaa_texture_view) = create_hdr_msaa_texture(device, w, h, "ECS HDR MSAA");
let sampler = create_sampler(device, "ECS Tonemap Sampler");
let tonemap_bind_group_layout = device.device().create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("ECS Tonemap BGL"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
}, count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
},
);
let tonemap_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ECS Tonemap BG"),
layout: &tonemap_bind_group_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&hdr_texture_view) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
],
});
let tonemap_pipeline_bgl = device.device().create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("ECS Tonemap Pipeline BGL"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
}, count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
},
);
let tonemap_pipeline = RenderPipelineBuilder::new()
.with_vertex_shader(TONEMAP_SHADER)
.with_fragment_shader(TONEMAP_SHADER)
.with_format(format)
.with_vertex_layouts(vec![])
.with_bind_group_layouts(vec![tonemap_pipeline_bgl])
.with_label("ECS Tonemap Pipeline")
.build(device)
.expect("创建 Tonemap 管线失败")
.into_pipeline();
let brdf_lut_data = generate_brdf_lut(256);
let (_, brdf_lut_view) = create_texture_linear(device, 256, 256, &brdf_lut_data, "ECS BRDF LUT");
let (_, shadow_map_view) = create_shadow_map(device, SHADOW_MAP_SIZE, "ECS Shadow Map");
let shadow_sampler = create_shadow_sampler(device, "ECS Shadow Sampler");
let ibl_shadow_bind_group_layout = device.device().create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("ECS IBL+Shadow BGL"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
}, count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
}, count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
],
},
);
let ibl_shadow_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ECS IBL+Shadow BG"),
layout: &ibl_shadow_bind_group_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&brdf_lut_view) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&shadow_map_view) },
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&shadow_sampler) },
],
});
let shadow_scene_bgl = device.device().create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("Shadow Scene BGL"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
},
);
let shadow_pipeline = RenderPipelineBuilder::new()
.with_vertex_shader(SHADOW_SHADER)
.with_format(wgpu::TextureFormat::Rgba8Unorm) .with_vertex_layouts(vec![PbrVertex::layout()])
.with_depth_format(DEPTH_FORMAT)
.with_bind_group_layouts(vec![shadow_scene_bgl])
.with_label("ECS Shadow Pipeline")
.build_depth_only(device)
.expect("创建 Shadow 管线失败")
.into_pipeline();
app.insert_resource(RenderState {
surface_format: format,
surface_size: (w, h),
scene_uniform_buffer,
scene_bind_group,
scene_bind_group_layout,
depth_texture_view,
hdr_texture_view,
tonemap_pipeline,
tonemap_bind_group,
tonemap_bind_group_layout,
ibl_shadow_bind_group,
ibl_shadow_bind_group_layout,
shadow_pipeline,
shadow_map_view,
hdr_msaa_texture_view,
});
self.gpu_initialized = true;
info!("RenderState (HDR + IBL + Shadow) 已注入 ECS World");
}
fn handle_resize(&mut self, new_size: PhysicalSize<u32>) {
debug!("窗口大小变化: {}x{}", new_size.width, new_size.height);
self.window_state.set_size(new_size.width, new_size.height);
if let (Some(device), Some(surface)) = (&self.render_device, &mut self.render_surface) {
if let Err(e) = surface.resize(device, new_size.width, new_size.height) {
error!("调整渲染表面大小失败: {}", e);
}
}
if self.gpu_initialized && new_size.width > 0 && new_size.height > 0 {
if let (Some(app), Some(device)) = (&mut self.app, &self.render_device) {
if let Some(mut rs) = app.world.get_resource_mut::<RenderState>() {
rs.surface_size = (new_size.width, new_size.height);
let (_, depth_view) = create_depth_texture_msaa(device, new_size.width, new_size.height, "ECS Depth MSAA");
rs.depth_texture_view = depth_view;
let (_, hdr_view) = create_hdr_render_target(device, new_size.width, new_size.height, "ECS HDR RT");
let (_, hdr_msaa_view) = create_hdr_msaa_texture(device, new_size.width, new_size.height, "ECS HDR MSAA");
let sampler = create_sampler(device, "ECS Sampler");
let new_bg = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ECS Tonemap BG"),
layout: &rs.tonemap_bind_group_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&hdr_view) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
],
});
rs.hdr_texture_view = hdr_view;
rs.hdr_msaa_texture_view = hdr_msaa_view;
rs.tonemap_bind_group = new_bg;
}
}
}
}
fn handle_scale_factor_changed(&mut self, scale_factor: f64) {
debug!("缩放因子变化: {}", scale_factor);
self.window_state.set_scale_factor(scale_factor);
}
fn render_ecs(&mut self) {
let (Some(device), Some(surface)) = (&self.render_device, &self.render_surface) else {
return;
};
let Some(app) = &self.app else { return };
let Some(active_camera) = app.world.get_resource::<ActiveCamera>() else { return };
let Some(draw_list) = app.world.get_resource::<DrawCommandList>() else { return };
let Some(render_assets) = app.world.get_resource::<RenderAssets>() else { return };
let Some(render_state) = app.world.get_resource::<RenderState>() else { return };
if draw_list.commands.is_empty() {
return;
}
let frame = match surface.get_current_frame_with_recovery(device) {
Ok(frame) => frame,
Err(e) => {
error!("获取当前帧失败: {}", e);
return;
}
};
let swapchain_view = frame.texture.create_view(&Default::default());
let view_proj = active_camera.view_proj;
let camera_pos = active_camera.camera_pos;
let default_lights = SceneLights::default();
let scene_lights = app.world.get_resource::<SceneLights>()
.unwrap_or(&default_lights);
let (gpu_lights, light_count) = pack_lights(scene_lights);
let light = &scene_lights.directional;
let shadow_view_proj = compute_light_space_matrix(&light.direction);
let mut encoder = device.device().create_command_encoder(
&wgpu::CommandEncoderDescriptor { label: Some("ECS Frame Encoder") },
);
{
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Shadow Pass"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &render_state.shadow_map_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
rp.set_pipeline(&render_state.shadow_pipeline);
rp.set_bind_group(0, &render_state.scene_bind_group, &[]);
for cmd in draw_list.commands.iter() {
let Some(gpu_mesh) = render_assets.get_mesh(&cmd.mesh) else { continue };
let shadow_uniform = PbrSceneUniform {
model: cmd.model_matrix.to_cols_array_2d(),
view_proj: shadow_view_proj.to_cols_array_2d(),
..Default::default()
};
device.queue().write_buffer(
&render_state.scene_uniform_buffer, 0, bytemuck::bytes_of(&shadow_uniform),
);
rp.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..));
rp.set_index_buffer(gpu_mesh.index_buffer.slice(..), gpu_mesh.index_format);
rp.draw_indexed(0..gpu_mesh.index_count, 0, 0..1);
}
}
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("ECS HDR Scene Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &render_state.hdr_msaa_texture_view,
resolve_target: Some(&render_state.hdr_texture_view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.15, g: 0.3, b: 0.6, a: 1.0 }),
store: wgpu::StoreOp::Discard,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &render_state.depth_texture_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
for cmd in draw_list.commands.iter() {
let Some(gpu_mesh) = render_assets.get_mesh(&cmd.mesh) else { continue };
let Some(gpu_material) = render_assets.get_material(&cmd.material) else { continue };
let model = cmd.model_matrix;
let scale = glam::Vec3::new(
model.x_axis.truncate().length(),
model.y_axis.truncate().length(),
model.z_axis.truncate().length(),
);
let normal_matrix = if (scale.x - scale.y).abs() < 0.001
&& (scale.y - scale.z).abs() < 0.001
{
model.transpose()
} else {
model.inverse().transpose()
};
let uniform = PbrSceneUniform {
model: model.to_cols_array_2d(),
view_proj: view_proj.to_cols_array_2d(),
normal_matrix: normal_matrix.to_cols_array_2d(),
camera_pos: [camera_pos.x, camera_pos.y, camera_pos.z, 0.0],
light_dir: [light.direction.x, light.direction.y, light.direction.z, 0.0],
light_color: [light.color.x, light.color.y, light.color.z, light.intensity],
material_params: [cmd.metallic, cmd.roughness, cmd.normal_scale, light_count as f32],
lights: gpu_lights,
shadow_view_proj: shadow_view_proj.to_cols_array_2d(),
emissive_factor: [cmd.emissive_factor[0], cmd.emissive_factor[1], cmd.emissive_factor[2], 1.0 / SHADOW_MAP_SIZE as f32],
};
device.queue().write_buffer(
&render_state.scene_uniform_buffer, 0, bytemuck::bytes_of(&uniform),
);
let Some(pipeline) = render_assets.get_pipeline(&gpu_material.pipeline_handle) else {
log::error!("材质引用了不存在的管线");
continue;
};
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, &render_state.scene_bind_group, &[]);
render_pass.set_bind_group(1, &gpu_material.bind_group, &[]);
render_pass.set_bind_group(2, &render_state.ibl_shadow_bind_group, &[]);
render_pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(gpu_mesh.index_buffer.slice(..), gpu_mesh.index_format);
render_pass.draw_indexed(0..gpu_mesh.index_count, 0, 0..1);
}
}
{
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("ECS Tonemap Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &swapchain_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rp.set_pipeline(&render_state.tonemap_pipeline);
rp.set_bind_group(0, &render_state.tonemap_bind_group, &[]);
rp.draw(0..3, 0..1); }
device.queue().submit(std::iter::once(encoder.finish()));
frame.present();
}
fn render(&mut self) {
if self.app.is_some() && self.gpu_initialized {
self.render_ecs();
}
}
}
impl ApplicationHandler for RenderApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
info!("应用恢复");
if let Err(e) = self.create_window(event_loop) {
error!("创建窗口失败: {}", e);
event_loop.exit();
return;
}
if let Err(e) = pollster::block_on(self.init_render()) {
error!("初始化渲染失败: {}", e);
event_loop.exit();
return;
}
self.inject_render_state_to_ecs();
if let Some(window) = &self.window {
window.request_redraw();
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
match event {
WindowEvent::CloseRequested => {
info!("收到窗口关闭请求");
self.request_exit();
event_loop.exit();
}
WindowEvent::Resized(new_size) => {
self.handle_resize(new_size);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.handle_scale_factor_changed(scale_factor);
}
WindowEvent::KeyboardInput { event, .. } => {
if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
if let Some(app) = &mut self.app {
if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
if let Some(key) = KeyCode::from_winit(code) {
if event.state.is_pressed() {
input.press_key(key);
} else {
input.release_key(key);
}
}
}
}
}
}
WindowEvent::MouseInput { state, button, .. } => {
if let Some(app) = &mut self.app {
if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
if let Some(btn) = MouseButton::from_winit(button) {
if state.is_pressed() {
input.press_mouse(btn);
} else {
input.release_mouse(btn);
}
}
}
}
}
WindowEvent::CursorMoved { position, .. } => {
if let Some(app) = &mut self.app {
if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
input.set_mouse_position(glam::Vec2::new(position.x as f32, position.y as f32));
}
}
}
WindowEvent::MouseWheel { delta, .. } => {
if let Some(app) = &mut self.app {
if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
let scroll = match delta {
winit::event::MouseScrollDelta::LineDelta(_, y) => y,
winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 120.0,
};
input.add_scroll_delta(scroll);
}
}
}
WindowEvent::Focused(focused) => {
debug!("窗口焦点变化: {}", focused);
self.window_state.set_focused(focused);
}
WindowEvent::Occluded(occluded) => {
debug!("窗口遮挡状态: {}", occluded);
self.window_state.set_minimized(occluded);
}
WindowEvent::RedrawRequested => {
self.render();
}
_ => {}
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: DeviceId,
_event: DeviceEvent,
) {
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(app) = &mut self.app {
let now = Instant::now();
let raw_dt = now.duration_since(self.last_frame_time).as_secs_f32();
self.last_frame_time = now;
let dt = raw_dt.clamp(0.001, 0.1);
app.world.insert_resource(DeltaTime(dt));
app.update();
if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
input.end_frame();
}
}
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_app_creation() {
let config = WindowConfig::new().with_title("Test App");
let app = RenderApp::new(config);
assert_eq!(app.config().title, "Test App");
assert!(app.window().is_none());
assert!(!app.is_exit_requested());
}
#[test]
fn test_exit_request() {
let mut app = RenderApp::new(WindowConfig::default());
assert!(!app.is_exit_requested());
app.request_exit();
assert!(app.is_exit_requested());
}
#[test]
fn test_window_state_updates() {
let mut app = RenderApp::new(WindowConfig::default());
let new_size = PhysicalSize::new(1920, 1080);
app.handle_resize(new_size);
assert_eq!(app.window_state().size(), (1920, 1080));
app.handle_scale_factor_changed(2.0);
assert_eq!(app.window_state().scale_factor(), 2.0);
}
#[test]
fn test_render_app_config() {
let config = WindowConfig::new()
.with_title("Test")
.with_size(640, 480);
let app = RenderApp::new(config);
assert_eq!(app.config().title, "Test");
assert_eq!(app.config().width, 640);
}
#[test]
fn test_render_app_exit_request_toggle() {
let mut app = RenderApp::new(WindowConfig::new());
assert!(!app.is_exit_requested());
app.request_exit();
assert!(app.is_exit_requested());
}
#[test]
fn test_render_app_window_state() {
let config = WindowConfig::new().with_size(1024, 768);
let app = RenderApp::new(config);
let state = app.window_state();
assert_eq!(state.size(), (1280, 720));
}
}