anvilkit_render/
plugin.rs1use anvilkit_ecs::prelude::*;
6use anvilkit_ecs::physics::DeltaTime;
7use anvilkit_input::prelude::InputState;
8use log::info;
9
10use crate::window::WindowConfig;
11use crate::renderer::assets::{MeshHandle, MaterialHandle, RenderAssets};
12use crate::renderer::draw::{ActiveCamera, Aabb, DrawCommand, DrawCommandList, Frustum, SceneLights, MaterialParams};
13use crate::renderer::state::RenderState;
14
15#[derive(Debug, Clone)]
31pub struct RenderPlugin {
32 window_config: WindowConfig,
34}
35
36impl Default for RenderPlugin {
37 fn default() -> Self {
38 Self {
39 window_config: WindowConfig::default(),
40 }
41 }
42}
43
44impl RenderPlugin {
45 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn with_window_config(mut self, config: WindowConfig) -> Self {
72 self.window_config = config;
73 self
74 }
75
76 pub fn window_config(&self) -> &WindowConfig {
88 &self.window_config
89 }
90}
91
92impl Plugin for RenderPlugin {
93 fn build(&self, app: &mut App) {
94 info!("构建渲染插件");
95
96 app.insert_resource(RenderConfig {
98 window_config: self.window_config.clone(),
99 });
100
101 app.init_resource::<ActiveCamera>();
103 app.init_resource::<DrawCommandList>();
104 app.init_resource::<RenderAssets>();
105 app.init_resource::<SceneLights>();
106 app.insert_resource(InputState::new());
107 app.init_resource::<DeltaTime>();
108
109 app.add_systems(
111 AnvilKitSchedule::PostUpdate,
112 (
113 camera_system,
114 render_extract_system.after(camera_system),
115 ),
116 );
117
118 info!("渲染插件构建完成");
119 }
120}
121
122#[derive(Debug, Clone, Resource)]
137pub struct RenderConfig {
138 pub window_config: WindowConfig,
140}
141
142#[derive(Debug, Clone, Component)]
162pub struct CameraComponent {
163 pub fov: f32,
165 pub near: f32,
167 pub far: f32,
169 pub is_active: bool,
171 pub aspect_ratio: f32,
173}
174
175impl Default for CameraComponent {
176 fn default() -> Self {
177 Self {
178 fov: 60.0,
179 near: 0.1,
180 far: 1000.0,
181 is_active: true,
182 aspect_ratio: 16.0 / 9.0,
183 }
184 }
185}
186
187fn camera_system(
195 camera_query: Query<(&CameraComponent, &Transform)>,
196 render_state: Option<Res<RenderState>>,
197 mut active_camera: ResMut<ActiveCamera>,
198) {
199 let Some((camera, transform)) = camera_query.iter().find(|(c, _)| c.is_active) else {
200 return;
201 };
202
203 let aspect = if let Some(ref rs) = render_state {
205 let (w, h) = rs.surface_size;
206 w as f32 / h.max(1) as f32
207 } else {
208 camera.aspect_ratio
209 };
210
211 let eye = transform.translation;
212 let forward = transform.rotation * glam::Vec3::Z;
214 let target = eye + forward;
215
216 let view = glam::Mat4::look_at_lh(eye, target, glam::Vec3::Y);
217 let proj = glam::Mat4::perspective_lh(camera.fov.to_radians(), aspect, camera.near, camera.far);
218
219 active_camera.view_proj = proj * view;
220 active_camera.camera_pos = eye;
221}
222
223fn render_extract_system(
228 query: Query<(&MeshHandle, &MaterialHandle, &Transform, Option<&MaterialParams>, Option<&Aabb>)>,
229 active_camera: Res<ActiveCamera>,
230 mut draw_list: ResMut<DrawCommandList>,
231) {
232 draw_list.clear();
233
234 let frustum = Frustum::from_view_proj(&active_camera.view_proj);
235
236 for (mesh, material, transform, mat_params, aabb) in query.iter() {
237 let model = transform.compute_matrix();
238
239 if let Some(aabb) = aabb {
241 let local_center = aabb.center();
243 let world_center = model.transform_point3(local_center);
244 let scale = transform.scale;
246 let world_half = aabb.half_extents() * scale;
247
248 if !frustum.intersects_aabb(world_center, world_half) {
249 continue; }
251 }
252
253 let default_params = MaterialParams::default();
254 let p = mat_params.unwrap_or(&default_params);
255
256 draw_list.push(DrawCommand {
257 mesh: *mesh,
258 material: *material,
259 model_matrix: model,
260 metallic: p.metallic,
261 roughness: p.roughness,
262 normal_scale: p.normal_scale,
263 emissive_factor: p.emissive_factor,
264 });
265 }
266
267 draw_list.sort_for_batching();
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_render_plugin_creation() {
277 let plugin = RenderPlugin::new();
278 assert_eq!(plugin.window_config().title, "AnvilKit Application");
279 }
280
281 #[test]
282 fn test_render_plugin_with_config() {
283 let config = WindowConfig::new()
284 .with_title("Test Game")
285 .with_size(800, 600);
286
287 let plugin = RenderPlugin::new().with_window_config(config);
288 assert_eq!(plugin.window_config().title, "Test Game");
289 assert_eq!(plugin.window_config().width, 800);
290 assert_eq!(plugin.window_config().height, 600);
291 }
292
293 #[test]
294 fn test_camera_component_default() {
295 let camera = CameraComponent::default();
296 assert_eq!(camera.fov, 60.0);
297 assert_eq!(camera.near, 0.1);
298 assert_eq!(camera.far, 1000.0);
299 assert!(camera.is_active);
300 }
301
302 #[test]
303 fn test_render_plugin_default_config() {
304 let plugin = RenderPlugin::new();
305 assert_eq!(plugin.window_config().title, "AnvilKit Application");
306 assert_eq!(plugin.window_config().width, 1280);
307 }
308
309 #[test]
310 fn test_render_plugin_custom_window() {
311 let config = WindowConfig::new()
312 .with_title("Custom Window")
313 .with_size(800, 600);
314 let plugin = RenderPlugin::new().with_window_config(config);
315
316 assert_eq!(plugin.window_config().title, "Custom Window");
317 assert_eq!(plugin.window_config().width, 800);
318 assert_eq!(plugin.window_config().height, 600);
319 }
320
321 #[test]
322 fn test_render_config_default() {
323 let config = RenderConfig {
324 window_config: WindowConfig::default(),
325 };
326 assert!(config.window_config.vsync);
327 }
328
329 #[test]
330 fn test_camera_component_fields() {
331 let camera = CameraComponent::default();
332 assert!(camera.fov > 0.0);
333 assert!(camera.near > 0.0);
334 assert!(camera.far > camera.near);
335 }
336}