Skip to main content

anvilkit_render/renderer/
draw.rs

1//! # 绘制命令、相机资源和场景灯光
2//!
3//! 提供 ECS 渲染系统的中间表示:绘制命令列表、活动相机信息和场景灯光。
4
5use bevy_ecs::prelude::*;
6use glam::{Mat4, Vec3};
7
8use crate::renderer::assets::{MeshHandle, MaterialHandle};
9
10/// 轴对齐包围盒 (Axis-Aligned Bounding Box)
11///
12/// 用于快速视锥体剔除。附加到实体上表示其局部空间包围盒。
13///
14/// # 示例
15///
16/// ```rust
17/// use anvilkit_render::renderer::draw::Aabb;
18/// use glam::Vec3;
19///
20/// let aabb = Aabb::from_min_max(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
21/// assert_eq!(aabb.center(), Vec3::ZERO);
22/// assert_eq!(aabb.half_extents(), Vec3::ONE);
23/// ```
24#[derive(Debug, Clone, Copy, Component)]
25pub struct Aabb {
26    pub min: Vec3,
27    pub max: Vec3,
28}
29
30impl Aabb {
31    /// 从最小/最大点创建 AABB
32    pub fn from_min_max(min: Vec3, max: Vec3) -> Self {
33        Self { min, max }
34    }
35
36    /// 从顶点位置列表计算 AABB
37    pub fn from_points(points: &[Vec3]) -> Self {
38        let mut min = Vec3::splat(f32::MAX);
39        let mut max = Vec3::splat(f32::MIN);
40        for &p in points {
41            min = min.min(p);
42            max = max.max(p);
43        }
44        Self { min, max }
45    }
46
47    /// 中心点
48    pub fn center(&self) -> Vec3 {
49        (self.min + self.max) * 0.5
50    }
51
52    /// 半尺寸
53    pub fn half_extents(&self) -> Vec3 {
54        (self.max - self.min) * 0.5
55    }
56
57    /// 测试两个 AABB 是否相交
58    pub fn intersects(&self, other: &Aabb) -> bool {
59        self.min.x <= other.max.x && self.max.x >= other.min.x
60            && self.min.y <= other.max.y && self.max.y >= other.min.y
61            && self.min.z <= other.max.z && self.max.z >= other.min.z
62    }
63
64    /// 将 AABB 按偏移量平移
65    pub fn translated(&self, offset: Vec3) -> Aabb {
66        Aabb {
67            min: self.min + offset,
68            max: self.max + offset,
69        }
70    }
71}
72
73impl Default for Aabb {
74    fn default() -> Self {
75        Self {
76            min: Vec3::splat(-0.5),
77            max: Vec3::splat(0.5),
78        }
79    }
80}
81
82/// 视锥体 (6 个裁剪平面)
83///
84/// 从 view-projection 矩阵提取,用于快速剔除不可见物体。
85/// 每个平面以 (normal.xyz, distance) 格式存储,法线指向锥体内部。
86#[derive(Debug, Clone, Copy)]
87pub struct Frustum {
88    /// 6 个裁剪平面: left, right, bottom, top, near, far
89    pub planes: [glam::Vec4; 6],
90}
91
92impl Frustum {
93    /// 从 view-projection 矩阵提取视锥体平面
94    ///
95    /// 使用 Gribb/Hartmann 方法从组合矩阵提取平面。
96    pub fn from_view_proj(vp: &Mat4) -> Self {
97        let m = vp.to_cols_array_2d();
98        let row = |r: usize| -> glam::Vec4 {
99            glam::Vec4::new(m[0][r], m[1][r], m[2][r], m[3][r])
100        };
101        let r0 = row(0);
102        let r1 = row(1);
103        let r2 = row(2);
104        let r3 = row(3);
105
106        let mut planes = [
107            r3 + r0,  // left
108            r3 - r0,  // right
109            r3 + r1,  // bottom
110            r3 - r1,  // top
111            r2,       // near (LH: z >= 0)
112            r3 - r2,  // far
113        ];
114
115        // 归一化每个平面
116        for p in &mut planes {
117            let len = glam::Vec3::new(p.x, p.y, p.z).length();
118            if len > 0.0 {
119                *p /= len;
120            }
121        }
122
123        Self { planes }
124    }
125
126    /// 测试世界空间 AABB 是否与视锥体相交
127    ///
128    /// 使用 AABB 的中心+半尺寸与每个平面的有符号距离测试。
129    /// 如果 AABB 完全在任一平面外侧,返回 false(不可见)。
130    pub fn intersects_aabb(&self, center: Vec3, half_extents: Vec3) -> bool {
131        for plane in &self.planes {
132            let normal = glam::Vec3::new(plane.x, plane.y, plane.z);
133            let d = plane.w;
134            // 计算 AABB 沿平面法线方向的最大投影半径
135            let r = half_extents.x * normal.x.abs()
136                + half_extents.y * normal.y.abs()
137                + half_extents.z * normal.z.abs();
138            // 中心到平面的有符号距离
139            let dist = normal.dot(center) + d;
140            if dist < -r {
141                return false; // 完全在平面外侧
142            }
143        }
144        true
145    }
146}
147
148/// 活动相机资源
149///
150/// 由 camera_system 每帧写入,包含当前激活相机的视图投影矩阵。
151#[derive(Resource)]
152pub struct ActiveCamera {
153    pub view_proj: Mat4,
154    pub camera_pos: Vec3,
155}
156
157impl Default for ActiveCamera {
158    fn default() -> Self {
159        Self {
160            view_proj: Mat4::IDENTITY,
161            camera_pos: Vec3::ZERO,
162        }
163    }
164}
165
166/// 方向光
167#[derive(Debug, Clone)]
168pub struct DirectionalLight {
169    /// 光照方向(从光源指向场景)
170    pub direction: Vec3,
171    /// 光照颜色 (linear RGB)
172    pub color: Vec3,
173    /// 光照强度
174    pub intensity: f32,
175}
176
177impl Default for DirectionalLight {
178    fn default() -> Self {
179        Self {
180            direction: Vec3::new(-0.5, -0.8, 0.3).normalize(),
181            color: Vec3::new(1.0, 0.95, 0.9),
182            intensity: 5.0,
183        }
184    }
185}
186
187/// 点光源
188#[derive(Debug, Clone)]
189pub struct PointLight {
190    /// 世界空间位置
191    pub position: Vec3,
192    /// 光照颜色 (linear RGB)
193    pub color: Vec3,
194    /// 光照强度
195    pub intensity: f32,
196    /// 衰减距离(超出此距离光照为零)
197    pub range: f32,
198}
199
200impl Default for PointLight {
201    fn default() -> Self {
202        Self {
203            position: Vec3::new(0.0, 3.0, 0.0),
204            color: Vec3::ONE,
205            intensity: 5.0,
206            range: 10.0,
207        }
208    }
209}
210
211/// 聚光灯
212#[derive(Debug, Clone)]
213pub struct SpotLight {
214    /// 世界空间位置
215    pub position: Vec3,
216    /// 光照方向(从光源指向场景)
217    pub direction: Vec3,
218    /// 光照颜色 (linear RGB)
219    pub color: Vec3,
220    /// 光照强度
221    pub intensity: f32,
222    /// 衰减距离
223    pub range: f32,
224    /// 内锥角(弧度),全亮区域
225    pub inner_cone_angle: f32,
226    /// 外锥角(弧度),衰减到零
227    pub outer_cone_angle: f32,
228}
229
230impl Default for SpotLight {
231    fn default() -> Self {
232        Self {
233            position: Vec3::new(0.0, 3.0, 0.0),
234            direction: Vec3::new(0.0, -1.0, 0.0),
235            color: Vec3::ONE,
236            intensity: 10.0,
237            range: 15.0,
238            inner_cone_angle: 0.35,  // ~20 degrees
239            outer_cone_angle: 0.52,  // ~30 degrees
240        }
241    }
242}
243
244/// 场景灯光资源
245///
246/// 持有场景中所有灯光信息,最多 8 盏(1 方向光 + 点光/聚光组合)。
247#[derive(Resource)]
248pub struct SceneLights {
249    pub directional: DirectionalLight,
250    pub point_lights: Vec<PointLight>,
251    pub spot_lights: Vec<SpotLight>,
252}
253
254impl Default for SceneLights {
255    fn default() -> Self {
256        Self {
257            directional: DirectionalLight::default(),
258            point_lights: Vec::new(),
259            spot_lights: Vec::new(),
260        }
261    }
262}
263
264/// 材质参数组件
265///
266/// 附加到实体上以控制 PBR 材质参数。
267/// 如果实体没有此组件,render_extract_system 使用默认值 (metallic=0, roughness=0.5, normal_scale=1.0)。
268#[derive(Debug, Clone, Component)]
269pub struct MaterialParams {
270    pub metallic: f32,
271    pub roughness: f32,
272    pub normal_scale: f32,
273    pub emissive_factor: [f32; 3],
274}
275
276impl Default for MaterialParams {
277    fn default() -> Self {
278        Self {
279            metallic: 0.0,
280            roughness: 0.5,
281            normal_scale: 1.0,
282            emissive_factor: [0.0; 3],
283        }
284    }
285}
286
287/// 单个绘制命令
288pub struct DrawCommand {
289    pub mesh: MeshHandle,
290    pub material: MaterialHandle,
291    pub model_matrix: Mat4,
292    pub metallic: f32,
293    pub roughness: f32,
294    pub normal_scale: f32,
295    pub emissive_factor: [f32; 3],
296}
297
298/// 每帧的绘制命令列表
299///
300/// 由 render_extract_system 填充,由 RenderApp::render_ecs() 消费。
301/// 支持按 mesh+material 排序分组以减少管线状态切换。
302#[derive(Resource, Default)]
303pub struct DrawCommandList {
304    pub commands: Vec<DrawCommand>,
305}
306
307impl DrawCommandList {
308    pub fn clear(&mut self) {
309        self.commands.clear();
310    }
311
312    pub fn push(&mut self, cmd: DrawCommand) {
313        self.commands.push(cmd);
314    }
315
316    /// 按 (material, mesh) 排序以实现批处理
317    ///
318    /// 相同 material 的命令排在一起,减少管线状态切换。
319    /// 相同 mesh 的命令排在一起,减少顶点缓冲区切换。
320    pub fn sort_for_batching(&mut self) {
321        self.commands.sort_by(|a, b| {
322            a.material.index().cmp(&b.material.index())
323                .then(a.mesh.index().cmp(&b.mesh.index()))
324        });
325    }
326}
327
328/// GPU 实例数据(per-instance,128 字节)
329///
330/// 包含每个实例的变换和材质参数。
331/// 用于 GPU instancing 时通过 storage buffer 传递。
332#[repr(C)]
333#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
334pub struct InstanceData {
335    pub model: [[f32; 4]; 4],         // 64 bytes
336    pub normal_matrix: [[f32; 4]; 4], // 64 bytes
337}
338
339impl Default for InstanceData {
340    fn default() -> Self {
341        Self {
342            model: glam::Mat4::IDENTITY.to_cols_array_2d(),
343            normal_matrix: glam::Mat4::IDENTITY.to_cols_array_2d(),
344        }
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_directional_light_default() {
354        let light = DirectionalLight::default();
355        assert!(light.direction.length() > 0.99);
356        assert!(light.intensity > 0.0);
357    }
358
359    #[test]
360    fn test_scene_lights_default() {
361        let lights = SceneLights::default();
362        assert!(lights.directional.intensity > 0.0);
363    }
364
365    #[test]
366    fn test_material_params_default() {
367        let params = MaterialParams::default();
368        assert_eq!(params.metallic, 0.0);
369        assert_eq!(params.roughness, 0.5);
370        assert_eq!(params.normal_scale, 1.0);
371    }
372
373    #[test]
374    fn test_aabb_from_points() {
375        let aabb = Aabb::from_points(&[
376            Vec3::new(-1.0, -2.0, -3.0),
377            Vec3::new(4.0, 5.0, 6.0),
378        ]);
379        assert_eq!(aabb.min, Vec3::new(-1.0, -2.0, -3.0));
380        assert_eq!(aabb.max, Vec3::new(4.0, 5.0, 6.0));
381        assert_eq!(aabb.center(), Vec3::new(1.5, 1.5, 1.5));
382        assert_eq!(aabb.half_extents(), Vec3::new(2.5, 3.5, 4.5));
383    }
384
385    #[test]
386    fn test_frustum_contains_origin() {
387        // A simple perspective-like VP that should contain the origin in front of the camera
388        let view = Mat4::look_at_lh(Vec3::new(0.0, 0.0, -5.0), Vec3::ZERO, Vec3::Y);
389        let proj = Mat4::perspective_lh(60.0_f32.to_radians(), 1.0, 0.1, 100.0);
390        let frustum = Frustum::from_view_proj(&(proj * view));
391
392        // Origin should be visible
393        assert!(frustum.intersects_aabb(Vec3::ZERO, Vec3::splat(0.5)));
394
395        // Far behind the camera should not be visible
396        assert!(!frustum.intersects_aabb(Vec3::new(0.0, 0.0, -100.0), Vec3::splat(0.5)));
397    }
398}