wormhole-engine 0.1.0

A portable, no-editor game engine with Rust core and Crystal scripting
Documentation
// Texture System - Loads and manages textures from JXL files

use wgpu::*;
use std::path::Path;
use std::fs;

pub struct Texture {
    pub texture: wgpu::Texture,
    pub view: TextureView,
    pub sampler: Sampler,
    pub width: u32,
    pub height: u32,
}

impl Texture {
    pub fn from_jxl(
        device: &Device,
        queue: &Queue,
        path: &Path,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        // Read JXL file
        let data = fs::read(path)?;
        
        // Decode JXL using jxl-oxide crate (simpler API than jxl)
        use jxl_oxide::JxlImage;
        
        // Open the JXL image
        let mut image = JxlImage::builder()
            .read(&data[..])
            .map_err(|e| format!("Failed to read JXL header: {}", e))?;
        
        // Get image dimensions from header
        let header = image.image_header();
        let width = header.size.width as u32;
        let height = header.size.height as u32;
        
        // Render the first frame - render_frame returns a Render struct
        let render = image.render_frame(0)
            .map_err(|e| format!("Failed to render frame: {}", e))?;
        
        // Get pixel data - use image_all_channels() to get interleaved data
        let frame_buffer = render.image_all_channels();
        let f32_pixels = frame_buffer.buf();
        
        // Calculate number of channels: buffer_size / (width * height)
        // Error shows 786432 bytes = 256*256*12 = RGB f32 (3 channels * 4 bytes per f32)
        let pixels_per_channel = (width * height) as usize;
        let num_channels = f32_pixels.len() / pixels_per_channel;
        
        // Convert f32 to u8 RGBA
        let mut rgba8_data = Vec::with_capacity(pixels_per_channel * 4);
        
        if num_channels == 3 {
            // RGB - convert to RGBA by adding opaque alpha channel
            for i in 0..pixels_per_channel {
                rgba8_data.push((f32_pixels[i * 3].clamp(0.0, 1.0) * 255.0) as u8); // R
                rgba8_data.push((f32_pixels[i * 3 + 1].clamp(0.0, 1.0) * 255.0) as u8); // G
                rgba8_data.push((f32_pixels[i * 3 + 2].clamp(0.0, 1.0) * 255.0) as u8); // B
                rgba8_data.push(255); // A (opaque)
            }
        } else if num_channels >= 4 {
            // RGBA or more channels - take first 4 channels
            for i in 0..pixels_per_channel {
                rgba8_data.push((f32_pixels[i * num_channels].clamp(0.0, 1.0) * 255.0) as u8); // R
                rgba8_data.push((f32_pixels[i * num_channels + 1].clamp(0.0, 1.0) * 255.0) as u8); // G
                rgba8_data.push((f32_pixels[i * num_channels + 2].clamp(0.0, 1.0) * 255.0) as u8); // B
                rgba8_data.push((f32_pixels[i * num_channels + 3].clamp(0.0, 1.0) * 255.0) as u8); // A
            }
        } else {
            return Err(format!("Unsupported channel count: {} (expected 3 or 4)", num_channels).into());
        }
        
        let pixels = rgba8_data.as_slice();
        
        // Create texture with decoded pixel data
        let texture_size = Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        };
        
        let texture = device.create_texture(&TextureDescriptor {
            label: Some("JXL Texture"),
            size: texture_size,
            mip_level_count: 1,
            sample_count: 1,
            dimension: TextureDimension::D2,
            format: TextureFormat::Rgba8UnormSrgb,
            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
            view_formats: &[],
        });
        
        queue.write_texture(
            ImageCopyTexture {
                texture: &texture,
                mip_level: 0,
                origin: Origin3d::ZERO,
                aspect: TextureAspect::All,
            },
            pixels,
            ImageDataLayout {
                offset: 0,
                bytes_per_row: Some(4 * width),
                rows_per_image: Some(height),
            },
            texture_size,
        );
        
        let view = texture.create_view(&TextureViewDescriptor::default());
        let sampler = device.create_sampler(&SamplerDescriptor {
            label: Some("Texture Sampler"),
            address_mode_u: AddressMode::ClampToEdge,
            address_mode_v: AddressMode::ClampToEdge,
            address_mode_w: AddressMode::ClampToEdge,
            mag_filter: FilterMode::Linear,
            min_filter: FilterMode::Linear,
            mipmap_filter: FilterMode::Nearest,
            ..Default::default()
        });
        
        Ok(Self {
            texture,
            view,
            sampler,
            width,
            height,
        })
    }
}