Skip to main content

goud_engine/libs/graphics/
renderer3d.rs

1//! 3D Renderer Module
2//!
3//! Provides a complete 3D rendering system with:
4//! - Primitive creation (cubes, spheres, planes, cylinders)
5//! - Multiple light types (point, directional, spot)
6//! - Camera control with position and rotation
7//! - Grid rendering
8//! - Skybox support
9//!
10//! All GPU operations go through the [`RenderBackend`] trait, enabling
11//! backend-agnostic rendering (OpenGL, wgpu, etc.).
12
13use crate::libs::graphics::backend::{
14    BlendFactor, CullFace, DepthFunc, FrontFace, PrimitiveTopology, RenderBackend, ShaderHandle,
15    VertexAttribute, VertexAttributeType, VertexLayout,
16};
17use cgmath::{perspective, Deg, Matrix, Matrix4, Rad, Vector3, Vector4};
18use std::collections::HashMap;
19
20use super::backend::BufferHandle;
21
22// ============================================================================
23// Constants
24// ============================================================================
25
26/// Maximum number of lights supported
27pub const MAX_LIGHTS: usize = 8;
28
29// ============================================================================
30// Types
31// ============================================================================
32
33/// Type of 3D primitive
34#[repr(C)]
35#[derive(Debug, Clone, Copy, PartialEq)]
36#[allow(missing_docs)]
37pub enum PrimitiveType {
38    /// A cube primitive
39    Cube = 0,
40    /// A sphere primitive
41    Sphere = 1,
42    /// A plane primitive
43    Plane = 2,
44    /// A cylinder primitive
45    Cylinder = 3,
46}
47
48/// Type of light source
49#[repr(C)]
50#[derive(Debug, Clone, Copy, PartialEq)]
51#[allow(missing_docs)]
52pub enum LightType {
53    /// Point light (emits in all directions)
54    Point = 0,
55    /// Directional light (parallel rays)
56    Directional = 1,
57    /// Spot light (cone of light)
58    Spot = 2,
59}
60
61/// Grid render mode
62#[repr(C)]
63#[derive(Debug, Clone, Copy, PartialEq)]
64#[allow(missing_docs)]
65pub enum GridRenderMode {
66    /// Blend grid with scene
67    Blend = 0,
68    /// Overlap grid on scene
69    Overlap = 1,
70}
71
72/// Primitive creation info
73#[repr(C)]
74#[derive(Debug, Clone)]
75#[allow(missing_docs)]
76pub struct PrimitiveCreateInfo {
77    /// Type of primitive to create
78    pub primitive_type: PrimitiveType,
79    /// Width of the primitive
80    pub width: f32,
81    /// Height of the primitive
82    pub height: f32,
83    /// Depth of the primitive
84    pub depth: f32,
85    /// Number of segments (for spheres/cylinders)
86    pub segments: u32,
87    /// Texture ID to apply
88    pub texture_id: u32,
89}
90
91/// A 3D object in the scene
92#[derive(Debug)]
93struct Object3D {
94    buffer: BufferHandle,
95    vertex_count: i32,
96    position: Vector3<f32>,
97    rotation: Vector3<f32>,
98    scale: Vector3<f32>,
99    texture_id: u32,
100}
101
102/// A light in the scene
103#[derive(Debug, Clone)]
104#[allow(missing_docs)]
105pub struct Light {
106    /// Type of light
107    pub light_type: LightType,
108    /// Position in world space
109    pub position: Vector3<f32>,
110    /// Direction (for directional/spot lights)
111    pub direction: Vector3<f32>,
112    /// Light color (RGB)
113    pub color: Vector3<f32>,
114    /// Light intensity
115    pub intensity: f32,
116    /// Light range (for point/spot lights)
117    pub range: f32,
118    /// Spot light angle in degrees
119    pub spot_angle: f32,
120    /// Whether the light is enabled
121    pub enabled: bool,
122}
123
124impl Default for Light {
125    fn default() -> Self {
126        Self {
127            light_type: LightType::Point,
128            position: Vector3::new(0.0, 5.0, 0.0),
129            direction: Vector3::new(0.0, -1.0, 0.0),
130            color: Vector3::new(1.0, 1.0, 1.0),
131            intensity: 1.0,
132            range: 10.0,
133            spot_angle: 45.0,
134            enabled: true,
135        }
136    }
137}
138
139/// Grid configuration
140#[derive(Debug, Clone)]
141#[allow(missing_docs)]
142pub struct GridConfig {
143    /// Whether grid is enabled
144    pub enabled: bool,
145    /// Size of the grid
146    pub size: f32,
147    /// Number of divisions
148    pub divisions: u32,
149    /// Show XZ plane
150    pub show_xz_plane: bool,
151    /// Show XY plane
152    pub show_xy_plane: bool,
153    /// Show YZ plane
154    pub show_yz_plane: bool,
155    /// Render mode
156    pub render_mode: GridRenderMode,
157    /// Line color
158    pub line_color: Vector3<f32>,
159}
160
161impl Default for GridConfig {
162    fn default() -> Self {
163        Self {
164            enabled: true,
165            size: 20.0,
166            divisions: 20,
167            show_xz_plane: true,
168            show_xy_plane: false,
169            show_yz_plane: false,
170            render_mode: GridRenderMode::Blend,
171            line_color: Vector3::new(0.5, 0.5, 0.5),
172        }
173    }
174}
175
176/// Skybox configuration
177#[derive(Debug, Clone)]
178#[allow(missing_docs)]
179pub struct SkyboxConfig {
180    /// Whether skybox is enabled
181    pub enabled: bool,
182    /// Skybox color (RGBA)
183    pub color: Vector4<f32>,
184}
185
186impl Default for SkyboxConfig {
187    fn default() -> Self {
188        Self {
189            enabled: false,
190            color: Vector4::new(0.1, 0.1, 0.2, 1.0),
191        }
192    }
193}
194
195/// Fog configuration
196#[derive(Debug, Clone)]
197#[allow(missing_docs)]
198pub struct FogConfig {
199    /// Whether fog is enabled
200    pub enabled: bool,
201    /// Fog color (RGB)
202    pub color: Vector3<f32>,
203    /// Fog density
204    pub density: f32,
205}
206
207impl Default for FogConfig {
208    fn default() -> Self {
209        Self {
210            enabled: true,
211            color: Vector3::new(0.05, 0.05, 0.1),
212            density: 0.02,
213        }
214    }
215}
216
217/// 3D Camera
218#[derive(Debug, Clone)]
219#[allow(missing_docs)]
220pub struct Camera3D {
221    /// Camera position in world space
222    pub position: Vector3<f32>,
223    /// Camera rotation (pitch, yaw, roll) in degrees
224    pub rotation: Vector3<f32>,
225}
226
227impl Default for Camera3D {
228    fn default() -> Self {
229        Self {
230            position: Vector3::new(0.0, 10.0, -20.0),
231            rotation: Vector3::new(-30.0, 0.0, 0.0),
232        }
233    }
234}
235
236impl Camera3D {
237    /// Get the view matrix
238    pub fn view_matrix(&self) -> Matrix4<f32> {
239        let pitch = Rad::from(Deg(self.rotation.x));
240        let yaw = Rad::from(Deg(self.rotation.y));
241
242        let cos_pitch = pitch.0.cos();
243        let sin_pitch = pitch.0.sin();
244        let cos_yaw = yaw.0.cos();
245        let sin_yaw = yaw.0.sin();
246
247        let forward = Vector3::new(sin_yaw * cos_pitch, sin_pitch, cos_yaw * cos_pitch);
248
249        let target = self.position + forward;
250        let up = Vector3::new(0.0, 1.0, 0.0);
251
252        Matrix4::look_at_rh(
253            cgmath::Point3::new(self.position.x, self.position.y, self.position.z),
254            cgmath::Point3::new(target.x, target.y, target.z),
255            up,
256        )
257    }
258}
259
260// ============================================================================
261// Shaders
262// ============================================================================
263
264const VERTEX_SHADER_3D: &str = r#"
265#version 330 core
266
267layout (location = 0) in vec3 aPos;
268layout (location = 1) in vec3 aNormal;
269layout (location = 2) in vec2 aTexCoord;
270
271out vec3 FragPos;
272out vec3 Normal;
273out vec2 TexCoord;
274
275uniform mat4 model;
276uniform mat4 view;
277uniform mat4 projection;
278
279void main()
280{
281    FragPos = vec3(model * vec4(aPos, 1.0));
282    Normal = mat3(transpose(inverse(model))) * aNormal;
283    TexCoord = aTexCoord;
284    
285    gl_Position = projection * view * vec4(FragPos, 1.0);
286}
287"#;
288
289const FRAGMENT_SHADER_3D: &str = r#"
290#version 330 core
291
292in vec3 FragPos;
293in vec3 Normal;
294in vec2 TexCoord;
295
296out vec4 FragColor;
297
298struct Light {
299    int type;           // 0 = point, 1 = directional, 2 = spot
300    vec3 position;
301    vec3 direction;
302    vec3 color;
303    float intensity;
304    float range;
305    float spotAngle;
306    bool enabled;
307};
308
309uniform sampler2D texture1;
310uniform vec3 viewPos;
311uniform int numLights;
312uniform Light lights[8];
313uniform bool useTexture;
314uniform vec4 objectColor;
315
316vec3 calculatePointLight(Light light, vec3 normal, vec3 fragPos, vec3 viewDir) {
317    vec3 lightDir = normalize(light.position - fragPos);
318    float distance = length(light.position - fragPos);
319    float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance);
320    
321    float diff = max(dot(normal, lightDir), 0.0);
322    vec3 diffuse = diff * light.color;
323    
324    vec3 reflectDir = reflect(-lightDir, normal);
325    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
326    vec3 specular = spec * light.color * 0.5;
327    
328    float rangeAttenuation = 1.0 - smoothstep(0.0, light.range, distance);
329    
330    return (diffuse + specular) * attenuation * rangeAttenuation * light.intensity;
331}
332
333vec3 calculateDirectionalLight(Light light, vec3 normal, vec3 viewDir) {
334    vec3 lightDir = normalize(-light.direction);
335    
336    float diff = max(dot(normal, lightDir), 0.0);
337    vec3 diffuse = diff * light.color;
338    
339    vec3 reflectDir = reflect(-lightDir, normal);
340    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
341    vec3 specular = spec * light.color * 0.5;
342    
343    return (diffuse + specular) * light.intensity;
344}
345
346vec3 calculateSpotLight(Light light, vec3 normal, vec3 fragPos, vec3 viewDir) {
347    vec3 lightDir = normalize(light.position - fragPos);
348    float theta = dot(lightDir, normalize(-light.direction));
349    float epsilon = cos(radians(light.spotAngle)) - cos(radians(light.spotAngle + 5.0));
350    float intensity = clamp((theta - cos(radians(light.spotAngle + 5.0))) / epsilon, 0.0, 1.0);
351    
352    if (intensity > 0.0) {
353        return calculatePointLight(light, normal, fragPos, viewDir) * intensity;
354    }
355    return vec3(0.0);
356}
357
358uniform bool fogEnabled;
359uniform vec3 fogColor;
360uniform float fogDensity;
361
362float calculateFog(float distance) {
363    float fog = exp(-pow(fogDensity * distance, 2.0));
364    return clamp(fog, 0.0, 1.0);
365}
366
367void main() {
368    vec3 norm = normalize(Normal);
369    vec3 viewDir = normalize(viewPos - FragPos);
370    vec3 result = vec3(0.0);
371    
372    vec3 ambient = vec3(0.1);
373    result += ambient;
374    
375    for(int i = 0; i < numLights && i < 8; i++) {
376        if (!lights[i].enabled) continue;
377        
378        vec3 lightContribution;
379        if (lights[i].type == 0) {
380            lightContribution = calculatePointLight(lights[i], norm, FragPos, viewDir);
381        }
382        else if (lights[i].type == 1) {
383            lightContribution = calculateDirectionalLight(lights[i], norm, viewDir);
384        }
385        else if (lights[i].type == 2) {
386            lightContribution = calculateSpotLight(lights[i], norm, FragPos, viewDir);
387        }
388        
389        result += lightContribution;
390    }
391    
392    vec4 baseColor;
393    if (useTexture) {
394        baseColor = texture(texture1, TexCoord);
395    } else {
396        baseColor = objectColor;
397    }
398    
399    vec3 finalColor = result * baseColor.rgb;
400    
401    if (fogEnabled) {
402        float distance = length(FragPos - viewPos);
403        float visibility = calculateFog(distance);
404        finalColor = mix(fogColor, finalColor, visibility);
405    }
406    
407    FragColor = vec4(finalColor, baseColor.a);
408}
409"#;
410
411const GRID_VERTEX_SHADER: &str = r#"
412#version 330 core
413
414layout (location = 0) in vec3 aPos;
415layout (location = 1) in vec3 aColor;
416
417out vec3 vertexColor;
418out vec3 fragPos;
419
420uniform mat4 view;
421uniform mat4 projection;
422
423void main()
424{
425    fragPos = aPos;
426    vertexColor = aColor;
427    gl_Position = projection * view * vec4(aPos, 1.0);
428}
429"#;
430
431const GRID_FRAGMENT_SHADER: &str = r#"
432#version 330 core
433
434in vec3 vertexColor;
435in vec3 fragPos;
436
437out vec4 FragColor;
438
439uniform vec3 viewPos;
440uniform bool fogEnabled;
441uniform vec3 fogColor;
442uniform float fogDensity;
443uniform float alpha;
444
445void main()
446{
447    vec3 color = vertexColor;
448    
449    if (fogEnabled) {
450        float distance = length(viewPos - fragPos);
451        float fogFactor = exp(-fogDensity * distance);
452        fogFactor = clamp(fogFactor, 0.0, 1.0);
453        color = mix(fogColor, color, fogFactor);
454    }
455    
456    FragColor = vec4(color, alpha);
457}
458"#;
459
460// ============================================================================
461// Cached uniform locations
462// ============================================================================
463
464struct LightUniforms {
465    light_type: i32,
466    position: i32,
467    direction: i32,
468    color: i32,
469    intensity: i32,
470    range: i32,
471    spot_angle: i32,
472    enabled: i32,
473}
474
475struct MainUniforms {
476    model: i32,
477    view: i32,
478    projection: i32,
479    view_pos: i32,
480    num_lights: i32,
481    use_texture: i32,
482    object_color: i32,
483    texture1: i32,
484    fog_enabled: i32,
485    fog_color: i32,
486    fog_density: i32,
487    lights: Vec<LightUniforms>,
488}
489
490struct GridUniforms {
491    view: i32,
492    projection: i32,
493    view_pos: i32,
494    fog_enabled: i32,
495    fog_color: i32,
496    fog_density: i32,
497    alpha: i32,
498}
499
500fn resolve_main_uniforms(backend: &dyn RenderBackend, shader: ShaderHandle) -> MainUniforms {
501    let loc = |name: &str| -> i32 { backend.get_uniform_location(shader, name).unwrap_or(-1) };
502
503    let mut lights = Vec::with_capacity(MAX_LIGHTS);
504    for i in 0..MAX_LIGHTS {
505        lights.push(LightUniforms {
506            light_type: loc(&format!("lights[{i}].type")),
507            position: loc(&format!("lights[{i}].position")),
508            direction: loc(&format!("lights[{i}].direction")),
509            color: loc(&format!("lights[{i}].color")),
510            intensity: loc(&format!("lights[{i}].intensity")),
511            range: loc(&format!("lights[{i}].range")),
512            spot_angle: loc(&format!("lights[{i}].spotAngle")),
513            enabled: loc(&format!("lights[{i}].enabled")),
514        });
515    }
516
517    MainUniforms {
518        model: loc("model"),
519        view: loc("view"),
520        projection: loc("projection"),
521        view_pos: loc("viewPos"),
522        num_lights: loc("numLights"),
523        use_texture: loc("useTexture"),
524        object_color: loc("objectColor"),
525        texture1: loc("texture1"),
526        fog_enabled: loc("fogEnabled"),
527        fog_color: loc("fogColor"),
528        fog_density: loc("fogDensity"),
529        lights,
530    }
531}
532
533fn resolve_grid_uniforms(backend: &dyn RenderBackend, shader: ShaderHandle) -> GridUniforms {
534    let loc = |name: &str| -> i32 { backend.get_uniform_location(shader, name).unwrap_or(-1) };
535
536    GridUniforms {
537        view: loc("view"),
538        projection: loc("projection"),
539        view_pos: loc("viewPos"),
540        fog_enabled: loc("fogEnabled"),
541        fog_color: loc("fogColor"),
542        fog_density: loc("fogDensity"),
543        alpha: loc("alpha"),
544    }
545}
546
547// ============================================================================
548// Vertex layouts
549// ============================================================================
550
551fn object_vertex_layout() -> VertexLayout {
552    // position (3 floats) + normal (3 floats) + texcoord (2 floats) = 32 bytes
553    VertexLayout::new(32)
554        .with_attribute(VertexAttribute::new(
555            0,
556            VertexAttributeType::Float3,
557            0,
558            false,
559        ))
560        .with_attribute(VertexAttribute::new(
561            1,
562            VertexAttributeType::Float3,
563            12,
564            false,
565        ))
566        .with_attribute(VertexAttribute::new(
567            2,
568            VertexAttributeType::Float2,
569            24,
570            false,
571        ))
572}
573
574fn grid_vertex_layout() -> VertexLayout {
575    // position (3 floats) + color (3 floats) = 24 bytes
576    VertexLayout::new(24)
577        .with_attribute(VertexAttribute::new(
578            0,
579            VertexAttributeType::Float3,
580            0,
581            false,
582        ))
583        .with_attribute(VertexAttribute::new(
584            1,
585            VertexAttributeType::Float3,
586            12,
587            false,
588        ))
589}
590
591// ============================================================================
592// Renderer3D
593// ============================================================================
594
595/// The main 3D renderer.
596///
597/// Owns a [`RenderBackend`] and manages all GPU resources (shaders, buffers)
598/// through it. No direct graphics API calls are made outside the backend.
599pub struct Renderer3D {
600    backend: Box<dyn RenderBackend>,
601    shader_handle: ShaderHandle,
602    grid_shader_handle: ShaderHandle,
603    grid_buffer: BufferHandle,
604    grid_vertex_count: i32,
605    axis_buffer: BufferHandle,
606    axis_vertex_count: i32,
607    objects: HashMap<u32, Object3D>,
608    lights: HashMap<u32, Light>,
609    next_object_id: u32,
610    next_light_id: u32,
611    camera: Camera3D,
612    window_width: u32,
613    window_height: u32,
614    grid_config: GridConfig,
615    skybox_config: SkyboxConfig,
616    fog_config: FogConfig,
617    uniforms: MainUniforms,
618    grid_uniforms: GridUniforms,
619    object_layout: VertexLayout,
620    grid_layout: VertexLayout,
621}
622
623impl Renderer3D {
624    /// Create a new 3D renderer with the given backend.
625    pub fn new(
626        mut backend: Box<dyn RenderBackend>,
627        window_width: u32,
628        window_height: u32,
629    ) -> Result<Self, String> {
630        let shader_handle = backend
631            .create_shader(VERTEX_SHADER_3D, FRAGMENT_SHADER_3D)
632            .map_err(|e| format!("Main 3D shader: {e}"))?;
633        let uniforms = resolve_main_uniforms(backend.as_ref(), shader_handle);
634
635        let grid_shader_handle = backend
636            .create_shader(GRID_VERTEX_SHADER, GRID_FRAGMENT_SHADER)
637            .map_err(|e| format!("Grid shader: {e}"))?;
638        let grid_uniforms = resolve_grid_uniforms(backend.as_ref(), grid_shader_handle);
639
640        let grid_layout = grid_vertex_layout();
641        let (grid_buffer, grid_vertex_count) = Self::create_grid_mesh(backend.as_mut(), 20.0, 20)?;
642        let (axis_buffer, axis_vertex_count) = Self::create_axis_mesh(backend.as_mut(), 5.0)?;
643
644        Ok(Self {
645            backend,
646            shader_handle,
647            grid_shader_handle,
648            grid_buffer,
649            grid_vertex_count,
650            axis_buffer,
651            axis_vertex_count,
652            objects: HashMap::new(),
653            lights: HashMap::new(),
654            next_object_id: 1,
655            next_light_id: 1,
656            camera: Camera3D::default(),
657            window_width,
658            window_height,
659            grid_config: GridConfig::default(),
660            skybox_config: SkyboxConfig::default(),
661            fog_config: FogConfig::default(),
662            uniforms,
663            grid_uniforms,
664            object_layout: object_vertex_layout(),
665            grid_layout,
666        })
667    }
668
669    // ========================================================================
670    // Mesh creation helpers (data → backend buffer)
671    // ========================================================================
672
673    fn upload_buffer(
674        backend: &mut dyn RenderBackend,
675        vertices: &[f32],
676    ) -> Result<BufferHandle, String> {
677        use crate::libs::graphics::backend::types::{BufferType, BufferUsage};
678        backend
679            .create_buffer(
680                BufferType::Vertex,
681                BufferUsage::Static,
682                bytemuck::cast_slice(vertices),
683            )
684            .map_err(|e| format!("Buffer creation failed: {e}"))
685    }
686
687    fn create_grid_mesh(
688        backend: &mut dyn RenderBackend,
689        size: f32,
690        divisions: u32,
691    ) -> Result<(BufferHandle, i32), String> {
692        let vertices = Self::generate_grid_vertices(size, divisions);
693        let count = (vertices.len() / 6) as i32;
694        let handle = Self::upload_buffer(backend, &vertices)?;
695        Ok((handle, count))
696    }
697
698    fn create_axis_mesh(
699        backend: &mut dyn RenderBackend,
700        size: f32,
701    ) -> Result<(BufferHandle, i32), String> {
702        let vertices = Self::generate_axis_vertices(size);
703        let count = (vertices.len() / 6) as i32;
704        let handle = Self::upload_buffer(backend, &vertices)?;
705        Ok((handle, count))
706    }
707
708    // ========================================================================
709    // Vertex data generation (pure math, no GPU calls)
710    // ========================================================================
711
712    fn generate_grid_vertices(size: f32, divisions: u32) -> Vec<f32> {
713        let mut vertices = Vec::new();
714        let step = size / divisions as f32;
715        let half = size / 2.0;
716
717        let xz_color = [0.3, 0.3, 0.3];
718        let xy_color = [0.2, 0.25, 0.3];
719        let yz_color = [0.3, 0.2, 0.25];
720
721        for i in 0..=divisions {
722            let pos = -half + step * i as f32;
723
724            vertices.extend_from_slice(&[pos, 0.0, -half]);
725            vertices.extend_from_slice(&xz_color);
726            vertices.extend_from_slice(&[pos, 0.0, half]);
727            vertices.extend_from_slice(&xz_color);
728
729            vertices.extend_from_slice(&[-half, 0.0, pos]);
730            vertices.extend_from_slice(&xz_color);
731            vertices.extend_from_slice(&[half, 0.0, pos]);
732            vertices.extend_from_slice(&xz_color);
733        }
734
735        for i in 0..=divisions {
736            let pos = -half + step * i as f32;
737
738            vertices.extend_from_slice(&[pos, -half, 0.0]);
739            vertices.extend_from_slice(&xy_color);
740            vertices.extend_from_slice(&[pos, half, 0.0]);
741            vertices.extend_from_slice(&xy_color);
742
743            vertices.extend_from_slice(&[-half, pos, 0.0]);
744            vertices.extend_from_slice(&xy_color);
745            vertices.extend_from_slice(&[half, pos, 0.0]);
746            vertices.extend_from_slice(&xy_color);
747        }
748
749        for i in 0..=divisions {
750            let pos = -half + step * i as f32;
751
752            vertices.extend_from_slice(&[0.0, pos, -half]);
753            vertices.extend_from_slice(&yz_color);
754            vertices.extend_from_slice(&[0.0, pos, half]);
755            vertices.extend_from_slice(&yz_color);
756
757            vertices.extend_from_slice(&[0.0, -half, pos]);
758            vertices.extend_from_slice(&yz_color);
759            vertices.extend_from_slice(&[0.0, half, pos]);
760            vertices.extend_from_slice(&yz_color);
761        }
762
763        vertices
764    }
765
766    #[rustfmt::skip]
767    fn generate_axis_vertices(size: f32) -> Vec<f32> {
768        vec![
769            // X axis (red)
770            0.0, 0.0, 0.0, 1.0, 0.2, 0.2,  size, 0.0, 0.0, 1.0, 0.2, 0.2,
771            0.0, 0.0, 0.0, 0.5, 0.1, 0.1, -size, 0.0, 0.0, 0.5, 0.1, 0.1,
772            // Y axis (green)
773            0.0, 0.0, 0.0, 0.2, 1.0, 0.2,  0.0, size, 0.0, 0.2, 1.0, 0.2,
774            0.0, 0.0, 0.0, 0.1, 0.5, 0.1,  0.0,-size, 0.0, 0.1, 0.5, 0.1,
775            // Z axis (blue)
776            0.0, 0.0, 0.0, 0.2, 0.2, 1.0,  0.0, 0.0, size, 0.2, 0.2, 1.0,
777            0.0, 0.0, 0.0, 0.1, 0.1, 0.5,  0.0, 0.0,-size, 0.1, 0.1, 0.5,
778            // Origin marker (small cross)
779            -0.2, 0.0, 0.0, 1.0, 1.0, 1.0,  0.2, 0.0, 0.0, 1.0, 1.0, 1.0,
780             0.0,-0.2, 0.0, 1.0, 1.0, 1.0,  0.0, 0.2, 0.0, 1.0, 1.0, 1.0,
781             0.0, 0.0,-0.2, 1.0, 1.0, 1.0,  0.0, 0.0, 0.2, 1.0, 1.0, 1.0,
782        ]
783    }
784
785    fn generate_cube_vertices(width: f32, height: f32, depth: f32) -> Vec<f32> {
786        let w = width / 2.0;
787        let h = height / 2.0;
788        let d = depth / 2.0;
789
790        #[rustfmt::skip]
791        let v = vec![
792            // Front face (z+)
793            -w,-h, d, 0.0, 0.0, 1.0, 0.0, 0.0,  w,-h, d, 0.0, 0.0, 1.0, 1.0, 0.0,
794             w, h, d, 0.0, 0.0, 1.0, 1.0, 1.0,   w, h, d, 0.0, 0.0, 1.0, 1.0, 1.0,
795            -w, h, d, 0.0, 0.0, 1.0, 0.0, 1.0,  -w,-h, d, 0.0, 0.0, 1.0, 0.0, 0.0,
796            // Back face (z-)
797             w,-h,-d, 0.0, 0.0,-1.0, 0.0, 0.0,  -w,-h,-d, 0.0, 0.0,-1.0, 1.0, 0.0,
798            -w, h,-d, 0.0, 0.0,-1.0, 1.0, 1.0,  -w, h,-d, 0.0, 0.0,-1.0, 1.0, 1.0,
799             w, h,-d, 0.0, 0.0,-1.0, 0.0, 1.0,   w,-h,-d, 0.0, 0.0,-1.0, 0.0, 0.0,
800            // Left face (x-)
801            -w,-h,-d,-1.0, 0.0, 0.0, 0.0, 0.0,  -w,-h, d,-1.0, 0.0, 0.0, 1.0, 0.0,
802            -w, h, d,-1.0, 0.0, 0.0, 1.0, 1.0,  -w, h, d,-1.0, 0.0, 0.0, 1.0, 1.0,
803            -w, h,-d,-1.0, 0.0, 0.0, 0.0, 1.0,  -w,-h,-d,-1.0, 0.0, 0.0, 0.0, 0.0,
804            // Right face (x+)
805             w,-h, d, 1.0, 0.0, 0.0, 0.0, 0.0,   w,-h,-d, 1.0, 0.0, 0.0, 1.0, 0.0,
806             w, h,-d, 1.0, 0.0, 0.0, 1.0, 1.0,   w, h,-d, 1.0, 0.0, 0.0, 1.0, 1.0,
807             w, h, d, 1.0, 0.0, 0.0, 0.0, 1.0,   w,-h, d, 1.0, 0.0, 0.0, 0.0, 0.0,
808            // Bottom face (y-)
809            -w,-h,-d, 0.0,-1.0, 0.0, 0.0, 0.0,   w,-h,-d, 0.0,-1.0, 0.0, 1.0, 0.0,
810             w,-h, d, 0.0,-1.0, 0.0, 1.0, 1.0,   w,-h, d, 0.0,-1.0, 0.0, 1.0, 1.0,
811            -w,-h, d, 0.0,-1.0, 0.0, 0.0, 1.0,  -w,-h,-d, 0.0,-1.0, 0.0, 0.0, 0.0,
812            // Top face (y+)
813            -w, h, d, 0.0, 1.0, 0.0, 0.0, 0.0,   w, h, d, 0.0, 1.0, 0.0, 1.0, 0.0,
814             w, h,-d, 0.0, 1.0, 0.0, 1.0, 1.0,   w, h,-d, 0.0, 1.0, 0.0, 1.0, 1.0,
815            -w, h,-d, 0.0, 1.0, 0.0, 0.0, 1.0,  -w, h, d, 0.0, 1.0, 0.0, 0.0, 0.0,
816        ];
817        v
818    }
819
820    fn generate_plane_vertices(width: f32, depth: f32) -> Vec<f32> {
821        let w = width / 2.0;
822        let d = depth / 2.0;
823
824        #[rustfmt::skip]
825        let v = vec![
826            // Top face
827            -w, 0.0, d, 0.0, 1.0, 0.0, 0.0, 1.0,   w, 0.0, d, 0.0, 1.0, 0.0, 1.0, 1.0,
828             w, 0.0,-d, 0.0, 1.0, 0.0, 1.0, 0.0,    w, 0.0,-d, 0.0, 1.0, 0.0, 1.0, 0.0,
829            -w, 0.0,-d, 0.0, 1.0, 0.0, 0.0, 0.0,   -w, 0.0, d, 0.0, 1.0, 0.0, 0.0, 1.0,
830            // Bottom face (double-sided)
831            -w, 0.0,-d, 0.0,-1.0, 0.0, 0.0, 0.0,    w, 0.0,-d, 0.0,-1.0, 0.0, 1.0, 0.0,
832             w, 0.0, d, 0.0,-1.0, 0.0, 1.0, 1.0,    w, 0.0, d, 0.0,-1.0, 0.0, 1.0, 1.0,
833            -w, 0.0, d, 0.0,-1.0, 0.0, 0.0, 1.0,   -w, 0.0,-d, 0.0,-1.0, 0.0, 0.0, 0.0,
834        ];
835        v
836    }
837
838    fn generate_sphere_vertices(radius: f32, segments: u32) -> Vec<f32> {
839        let mut vertices = Vec::new();
840        let segment_count = segments.max(8);
841
842        for i in 0..segment_count {
843            let lat0 = std::f32::consts::PI * (-0.5 + (i as f32) / segment_count as f32);
844            let lat1 = std::f32::consts::PI * (-0.5 + ((i + 1) as f32) / segment_count as f32);
845
846            for j in 0..segment_count {
847                let lng0 = 2.0 * std::f32::consts::PI * (j as f32) / segment_count as f32;
848                let lng1 = 2.0 * std::f32::consts::PI * ((j + 1) as f32) / segment_count as f32;
849
850                let x0 = radius * lat0.cos() * lng0.cos();
851                let y0 = radius * lat0.sin();
852                let z0 = radius * lat0.cos() * lng0.sin();
853                let x1 = radius * lat0.cos() * lng1.cos();
854                let y1 = radius * lat0.sin();
855                let z1 = radius * lat0.cos() * lng1.sin();
856                let x2 = radius * lat1.cos() * lng1.cos();
857                let y2 = radius * lat1.sin();
858                let z2 = radius * lat1.cos() * lng1.sin();
859                let x3 = radius * lat1.cos() * lng0.cos();
860                let y3 = radius * lat1.sin();
861                let z3 = radius * lat1.cos() * lng0.sin();
862
863                let u0 = j as f32 / segment_count as f32;
864                let u1 = (j + 1) as f32 / segment_count as f32;
865                let v0 = i as f32 / segment_count as f32;
866                let v1 = (i + 1) as f32 / segment_count as f32;
867
868                vertices.extend_from_slice(&[
869                    x0,
870                    y0,
871                    z0,
872                    x0 / radius,
873                    y0 / radius,
874                    z0 / radius,
875                    u0,
876                    v0,
877                    x1,
878                    y1,
879                    z1,
880                    x1 / radius,
881                    y1 / radius,
882                    z1 / radius,
883                    u1,
884                    v0,
885                    x2,
886                    y2,
887                    z2,
888                    x2 / radius,
889                    y2 / radius,
890                    z2 / radius,
891                    u1,
892                    v1,
893                    x0,
894                    y0,
895                    z0,
896                    x0 / radius,
897                    y0 / radius,
898                    z0 / radius,
899                    u0,
900                    v0,
901                    x2,
902                    y2,
903                    z2,
904                    x2 / radius,
905                    y2 / radius,
906                    z2 / radius,
907                    u1,
908                    v1,
909                    x3,
910                    y3,
911                    z3,
912                    x3 / radius,
913                    y3 / radius,
914                    z3 / radius,
915                    u0,
916                    v1,
917                ]);
918            }
919        }
920
921        vertices
922    }
923
924    fn generate_cylinder_vertices(radius: f32, height: f32, segments: u32) -> Vec<f32> {
925        let mut vertices = Vec::new();
926        let segment_count = segments.max(8);
927        let h = height / 2.0;
928
929        for i in 0..segment_count {
930            let a0 = 2.0 * std::f32::consts::PI * (i as f32) / segment_count as f32;
931            let a1 = 2.0 * std::f32::consts::PI * ((i + 1) as f32) / segment_count as f32;
932
933            let x0 = radius * a0.cos();
934            let z0 = radius * a0.sin();
935            let x1 = radius * a1.cos();
936            let z1 = radius * a1.sin();
937
938            let u0 = i as f32 / segment_count as f32;
939            let u1 = (i + 1) as f32 / segment_count as f32;
940
941            // Side faces (two triangles)
942            vertices.extend_from_slice(&[
943                x0,
944                -h,
945                z0,
946                x0 / radius,
947                0.0,
948                z0 / radius,
949                u0,
950                0.0,
951                x1,
952                -h,
953                z1,
954                x1 / radius,
955                0.0,
956                z1 / radius,
957                u1,
958                0.0,
959                x1,
960                h,
961                z1,
962                x1 / radius,
963                0.0,
964                z1 / radius,
965                u1,
966                1.0,
967                x0,
968                -h,
969                z0,
970                x0 / radius,
971                0.0,
972                z0 / radius,
973                u0,
974                0.0,
975                x1,
976                h,
977                z1,
978                x1 / radius,
979                0.0,
980                z1 / radius,
981                u1,
982                1.0,
983                x0,
984                h,
985                z0,
986                x0 / radius,
987                0.0,
988                z0 / radius,
989                u0,
990                1.0,
991            ]);
992
993            // Top cap
994            vertices.extend_from_slice(&[
995                0.0,
996                h,
997                0.0,
998                0.0,
999                1.0,
1000                0.0,
1001                0.5,
1002                0.5,
1003                x1,
1004                h,
1005                z1,
1006                0.0,
1007                1.0,
1008                0.0,
1009                0.5 + 0.5 * a1.cos(),
1010                0.5 + 0.5 * a1.sin(),
1011                x0,
1012                h,
1013                z0,
1014                0.0,
1015                1.0,
1016                0.0,
1017                0.5 + 0.5 * a0.cos(),
1018                0.5 + 0.5 * a0.sin(),
1019            ]);
1020
1021            // Bottom cap
1022            vertices.extend_from_slice(&[
1023                0.0,
1024                -h,
1025                0.0,
1026                0.0,
1027                -1.0,
1028                0.0,
1029                0.5,
1030                0.5,
1031                x0,
1032                -h,
1033                z0,
1034                0.0,
1035                -1.0,
1036                0.0,
1037                0.5 + 0.5 * a0.cos(),
1038                0.5 + 0.5 * a0.sin(),
1039                x1,
1040                -h,
1041                z1,
1042                0.0,
1043                -1.0,
1044                0.0,
1045                0.5 + 0.5 * a1.cos(),
1046                0.5 + 0.5 * a1.sin(),
1047            ]);
1048        }
1049
1050        vertices
1051    }
1052
1053    // ========================================================================
1054    // Primitive creation
1055    // ========================================================================
1056
1057    /// Create a primitive object
1058    pub fn create_primitive(&mut self, info: PrimitiveCreateInfo) -> u32 {
1059        let vertices = match info.primitive_type {
1060            PrimitiveType::Cube => {
1061                Self::generate_cube_vertices(info.width, info.height, info.depth)
1062            }
1063            PrimitiveType::Plane => Self::generate_plane_vertices(info.width, info.depth),
1064            PrimitiveType::Sphere => {
1065                Self::generate_sphere_vertices(info.width / 2.0, info.segments)
1066            }
1067            PrimitiveType::Cylinder => {
1068                Self::generate_cylinder_vertices(info.width / 2.0, info.height, info.segments)
1069            }
1070        };
1071
1072        let buffer = match Self::upload_buffer(self.backend.as_mut(), &vertices) {
1073            Ok(h) => h,
1074            Err(e) => {
1075                log::error!("Failed to create primitive buffer: {e}");
1076                return 0;
1077            }
1078        };
1079
1080        let id = self.next_object_id;
1081        self.next_object_id += 1;
1082
1083        self.objects.insert(
1084            id,
1085            Object3D {
1086                buffer,
1087                vertex_count: (vertices.len() / 8) as i32,
1088                position: Vector3::new(0.0, 0.0, 0.0),
1089                rotation: Vector3::new(0.0, 0.0, 0.0),
1090                scale: Vector3::new(1.0, 1.0, 1.0),
1091                texture_id: info.texture_id,
1092            },
1093        );
1094
1095        id
1096    }
1097
1098    // ========================================================================
1099    // Object manipulation
1100    // ========================================================================
1101
1102    /// Set object position
1103    pub fn set_object_position(&mut self, id: u32, x: f32, y: f32, z: f32) -> bool {
1104        if let Some(obj) = self.objects.get_mut(&id) {
1105            obj.position = Vector3::new(x, y, z);
1106            true
1107        } else {
1108            false
1109        }
1110    }
1111
1112    /// Set object rotation (in degrees)
1113    pub fn set_object_rotation(&mut self, id: u32, x: f32, y: f32, z: f32) -> bool {
1114        if let Some(obj) = self.objects.get_mut(&id) {
1115            obj.rotation = Vector3::new(x, y, z);
1116            true
1117        } else {
1118            false
1119        }
1120    }
1121
1122    /// Set object scale
1123    pub fn set_object_scale(&mut self, id: u32, x: f32, y: f32, z: f32) -> bool {
1124        if let Some(obj) = self.objects.get_mut(&id) {
1125            obj.scale = Vector3::new(x, y, z);
1126            true
1127        } else {
1128            false
1129        }
1130    }
1131
1132    /// Remove an object
1133    pub fn remove_object(&mut self, id: u32) -> bool {
1134        if let Some(obj) = self.objects.remove(&id) {
1135            self.backend.destroy_buffer(obj.buffer);
1136            true
1137        } else {
1138            false
1139        }
1140    }
1141
1142    // ========================================================================
1143    // Lighting
1144    // ========================================================================
1145
1146    /// Add a light
1147    pub fn add_light(&mut self, light: Light) -> u32 {
1148        let id = self.next_light_id;
1149        self.next_light_id += 1;
1150        self.lights.insert(id, light);
1151        id
1152    }
1153
1154    /// Update a light
1155    pub fn update_light(&mut self, id: u32, light: Light) -> bool {
1156        use std::collections::hash_map::Entry;
1157        if let Entry::Occupied(mut e) = self.lights.entry(id) {
1158            e.insert(light);
1159            true
1160        } else {
1161            false
1162        }
1163    }
1164
1165    /// Remove a light
1166    pub fn remove_light(&mut self, id: u32) -> bool {
1167        self.lights.remove(&id).is_some()
1168    }
1169
1170    // ========================================================================
1171    // Camera
1172    // ========================================================================
1173
1174    /// Set camera position
1175    pub fn set_camera_position(&mut self, x: f32, y: f32, z: f32) {
1176        self.camera.position = Vector3::new(x, y, z);
1177    }
1178
1179    /// Set camera rotation (pitch, yaw, roll in degrees)
1180    pub fn set_camera_rotation(&mut self, pitch: f32, yaw: f32, roll: f32) {
1181        self.camera.rotation = Vector3::new(pitch, yaw, roll);
1182    }
1183
1184    // ========================================================================
1185    // Grid / Skybox / Fog configuration
1186    // ========================================================================
1187
1188    /// Configure grid
1189    pub fn configure_grid(&mut self, config: GridConfig) {
1190        if config.size != self.grid_config.size || config.divisions != self.grid_config.divisions {
1191            self.backend.destroy_buffer(self.grid_buffer);
1192            if let Ok((buf, count)) =
1193                Self::create_grid_mesh(self.backend.as_mut(), config.size, config.divisions)
1194            {
1195                self.grid_buffer = buf;
1196                self.grid_vertex_count = count;
1197            }
1198        }
1199        self.grid_config = config;
1200    }
1201
1202    /// Set grid enabled state
1203    pub fn set_grid_enabled(&mut self, enabled: bool) {
1204        self.grid_config.enabled = enabled;
1205    }
1206
1207    /// Configure skybox
1208    pub fn configure_skybox(&mut self, config: SkyboxConfig) {
1209        self.skybox_config = config;
1210    }
1211
1212    /// Configure fog
1213    pub fn configure_fog(&mut self, config: FogConfig) {
1214        self.fog_config = config;
1215    }
1216
1217    /// Set fog enabled state
1218    pub fn set_fog_enabled(&mut self, enabled: bool) {
1219        self.fog_config.enabled = enabled;
1220    }
1221
1222    // ========================================================================
1223    // Rendering
1224    // ========================================================================
1225
1226    /// Render the scene
1227    pub fn render(&mut self, texture_manager: Option<&dyn TextureManagerTrait>) {
1228        self.backend.enable_depth_test();
1229        self.backend.set_depth_func(DepthFunc::Less);
1230        self.backend.enable_culling();
1231        self.backend.set_cull_face(CullFace::Back);
1232        self.backend.set_front_face(FrontFace::Ccw);
1233
1234        if self.skybox_config.enabled {
1235            self.backend.set_clear_color(
1236                self.skybox_config.color.x,
1237                self.skybox_config.color.y,
1238                self.skybox_config.color.z,
1239                self.skybox_config.color.w,
1240            );
1241        }
1242        self.backend.clear_depth();
1243
1244        let aspect = self.window_width as f32 / self.window_height as f32;
1245        let projection: Matrix4<f32> = perspective(Deg(45.0), aspect, 0.1, 1000.0);
1246        let view = self.camera.view_matrix();
1247        let view_arr = mat4_to_array(&view);
1248        let proj_arr = mat4_to_array(&projection);
1249
1250        // --- Grid pass ---
1251        if self.grid_config.enabled {
1252            let _ = self.backend.bind_shader(self.grid_shader_handle);
1253
1254            self.backend
1255                .set_uniform_mat4(self.grid_uniforms.view, &view_arr);
1256            self.backend
1257                .set_uniform_mat4(self.grid_uniforms.projection, &proj_arr);
1258            self.backend.set_uniform_vec3(
1259                self.grid_uniforms.view_pos,
1260                self.camera.position.x,
1261                self.camera.position.y,
1262                self.camera.position.z,
1263            );
1264            self.backend
1265                .set_uniform_float(self.grid_uniforms.alpha, 0.4);
1266            self.backend.set_uniform_int(
1267                self.grid_uniforms.fog_enabled,
1268                i32::from(self.fog_config.enabled),
1269            );
1270            self.backend.set_uniform_vec3(
1271                self.grid_uniforms.fog_color,
1272                self.fog_config.color.x,
1273                self.fog_config.color.y,
1274                self.fog_config.color.z,
1275            );
1276            self.backend
1277                .set_uniform_float(self.grid_uniforms.fog_density, self.fog_config.density);
1278
1279            self.backend.enable_blending();
1280            self.backend
1281                .set_blend_func(BlendFactor::SrcAlpha, BlendFactor::OneMinusSrcAlpha);
1282            self.backend.set_depth_mask(false);
1283
1284            let _ = self.backend.bind_buffer(self.grid_buffer);
1285            self.backend.set_vertex_attributes(&self.grid_layout);
1286            let _ = self.backend.draw_arrays(
1287                PrimitiveTopology::Lines,
1288                0,
1289                self.grid_vertex_count as u32,
1290            );
1291
1292            self.backend.set_line_width(3.0);
1293            let _ = self.backend.bind_buffer(self.axis_buffer);
1294            self.backend.set_vertex_attributes(&self.grid_layout);
1295            self.backend
1296                .set_uniform_float(self.grid_uniforms.alpha, 1.0);
1297            let _ = self.backend.draw_arrays(
1298                PrimitiveTopology::Lines,
1299                0,
1300                self.axis_vertex_count as u32,
1301            );
1302            self.backend.set_line_width(1.0);
1303
1304            self.backend.set_depth_mask(true);
1305            self.backend.disable_blending();
1306            self.backend.unbind_shader();
1307        }
1308
1309        // --- Main objects pass ---
1310        let _ = self.backend.bind_shader(self.shader_handle);
1311
1312        self.backend.set_uniform_mat4(self.uniforms.view, &view_arr);
1313        self.backend
1314            .set_uniform_mat4(self.uniforms.projection, &proj_arr);
1315        self.backend.set_uniform_vec3(
1316            self.uniforms.view_pos,
1317            self.camera.position.x,
1318            self.camera.position.y,
1319            self.camera.position.z,
1320        );
1321
1322        self.backend.set_uniform_int(
1323            self.uniforms.fog_enabled,
1324            i32::from(self.fog_config.enabled),
1325        );
1326        self.backend.set_uniform_vec3(
1327            self.uniforms.fog_color,
1328            self.fog_config.color.x,
1329            self.fog_config.color.y,
1330            self.fog_config.color.z,
1331        );
1332        self.backend
1333            .set_uniform_float(self.uniforms.fog_density, self.fog_config.density);
1334
1335        let light_count = self.lights.len().min(MAX_LIGHTS) as i32;
1336        self.backend
1337            .set_uniform_int(self.uniforms.num_lights, light_count);
1338
1339        for (i, (_, light)) in self.lights.iter().enumerate().take(MAX_LIGHTS) {
1340            let lu = &self.uniforms.lights[i];
1341            self.backend
1342                .set_uniform_int(lu.light_type, light.light_type as i32);
1343            self.backend.set_uniform_vec3(
1344                lu.position,
1345                light.position.x,
1346                light.position.y,
1347                light.position.z,
1348            );
1349            self.backend.set_uniform_vec3(
1350                lu.direction,
1351                light.direction.x,
1352                light.direction.y,
1353                light.direction.z,
1354            );
1355            self.backend
1356                .set_uniform_vec3(lu.color, light.color.x, light.color.y, light.color.z);
1357            self.backend
1358                .set_uniform_float(lu.intensity, light.intensity);
1359            self.backend.set_uniform_float(lu.range, light.range);
1360            self.backend
1361                .set_uniform_float(lu.spot_angle, light.spot_angle);
1362            self.backend
1363                .set_uniform_int(lu.enabled, i32::from(light.enabled));
1364        }
1365
1366        self.backend.set_uniform_int(self.uniforms.texture1, 0);
1367
1368        for obj in self.objects.values() {
1369            let model = Self::create_model_matrix(obj.position, obj.rotation, obj.scale);
1370            let model_arr = mat4_to_array(&model);
1371            self.backend
1372                .set_uniform_mat4(self.uniforms.model, &model_arr);
1373
1374            if obj.texture_id > 0 {
1375                if let Some(tm) = texture_manager {
1376                    tm.bind_texture(obj.texture_id, 0);
1377                }
1378                self.backend.set_uniform_int(self.uniforms.use_texture, 1);
1379                self.backend
1380                    .set_uniform_vec4(self.uniforms.object_color, 1.0, 1.0, 1.0, 1.0);
1381            } else {
1382                self.backend.set_uniform_int(self.uniforms.use_texture, 0);
1383                self.backend
1384                    .set_uniform_vec4(self.uniforms.object_color, 0.8, 0.8, 0.8, 1.0);
1385            }
1386
1387            let _ = self.backend.bind_buffer(obj.buffer);
1388            self.backend.set_vertex_attributes(&self.object_layout);
1389            let _ =
1390                self.backend
1391                    .draw_arrays(PrimitiveTopology::Triangles, 0, obj.vertex_count as u32);
1392        }
1393
1394        self.backend.unbind_shader();
1395    }
1396
1397    // ========================================================================
1398    // Utility
1399    // ========================================================================
1400
1401    fn create_model_matrix(
1402        position: Vector3<f32>,
1403        rotation: Vector3<f32>,
1404        scale: Vector3<f32>,
1405    ) -> Matrix4<f32> {
1406        let translation = Matrix4::from_translation(position);
1407        let rot_x = Matrix4::from_angle_x(Deg(rotation.x));
1408        let rot_y = Matrix4::from_angle_y(Deg(rotation.y));
1409        let rot_z = Matrix4::from_angle_z(Deg(rotation.z));
1410        let rotation_matrix = rot_z * rot_y * rot_x;
1411        let scale_matrix = Matrix4::from_nonuniform_scale(scale.x, scale.y, scale.z);
1412        translation * rotation_matrix * scale_matrix
1413    }
1414}
1415
1416impl Drop for Renderer3D {
1417    fn drop(&mut self) {
1418        for obj in self.objects.values() {
1419            self.backend.destroy_buffer(obj.buffer);
1420        }
1421        self.backend.destroy_buffer(self.grid_buffer);
1422        self.backend.destroy_buffer(self.axis_buffer);
1423        self.backend.destroy_shader(self.shader_handle);
1424        self.backend.destroy_shader(self.grid_shader_handle);
1425    }
1426}
1427
1428/// Trait for texture manager integration.
1429///
1430/// This is a legacy bridge: texture binding still happens through the caller's
1431/// GL-backed implementation. A future refactor should route textures through
1432/// [`RenderBackend`] for full backend portability.
1433pub trait TextureManagerTrait {
1434    /// Bind a texture to a slot
1435    fn bind_texture(&self, texture_id: u32, slot: u32);
1436}
1437
1438// ============================================================================
1439// Helpers
1440// ============================================================================
1441
1442fn mat4_to_array(m: &Matrix4<f32>) -> [f32; 16] {
1443    // cgmath matrices are column-major, matching what the backend expects
1444    let ptr = m.as_ptr();
1445    let mut arr = [0.0f32; 16];
1446    // SAFETY: Matrix4<f32> is 16 contiguous floats in column-major order
1447    unsafe {
1448        std::ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 16);
1449    }
1450    arr
1451}