scena 1.0.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use web_sys::{WebGl2RenderingContext, WebGlTexture};

use crate::assets::{TextureFilter, TextureSamplerDesc, TextureWrap};

use super::materials::MaterialTextureUpload;

pub(super) fn create_material_texture(
    gl: &WebGl2RenderingContext,
) -> Result<WebGlTexture, wasm_bindgen::JsValue> {
    let texture = gl
        .create_texture()
        .ok_or_else(|| wasm_bindgen::JsValue::from_str("webgl2 texture allocation failed"))?;
    let mut initial_hash = None;
    upload_material_texture_if_dirty(
        gl,
        &texture,
        &mut initial_hash,
        MaterialTextureUpload::from_base_color_texture(None),
    )?;
    Ok(texture)
}

pub(super) fn upload_material_texture_if_dirty(
    gl: &WebGl2RenderingContext,
    texture: &WebGlTexture,
    last_texture_hash: &mut Option<u64>,
    upload: MaterialTextureUpload<'_>,
) -> Result<(), wasm_bindgen::JsValue> {
    let next_hash = material_texture_hash(upload);
    if *last_texture_hash == Some(next_hash) {
        return Ok(());
    }

    gl.active_texture(WebGl2RenderingContext::TEXTURE0);
    gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(texture));
    gl.tex_parameteri(
        WebGl2RenderingContext::TEXTURE_2D,
        WebGl2RenderingContext::TEXTURE_MIN_FILTER,
        webgl2_filter_mode(upload.sampler.min_filter(), true) as i32,
    );
    gl.tex_parameteri(
        WebGl2RenderingContext::TEXTURE_2D,
        WebGl2RenderingContext::TEXTURE_MAG_FILTER,
        webgl2_filter_mode(upload.sampler.mag_filter(), false) as i32,
    );
    gl.tex_parameteri(
        WebGl2RenderingContext::TEXTURE_2D,
        WebGl2RenderingContext::TEXTURE_WRAP_S,
        webgl2_wrap_mode(upload.sampler.wrap_s()) as i32,
    );
    gl.tex_parameteri(
        WebGl2RenderingContext::TEXTURE_2D,
        WebGl2RenderingContext::TEXTURE_WRAP_T,
        webgl2_wrap_mode(upload.sampler.wrap_t()) as i32,
    );
    gl.pixel_storei(WebGl2RenderingContext::UNPACK_ALIGNMENT, 1);
    gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
        WebGl2RenderingContext::TEXTURE_2D,
        0,
        WebGl2RenderingContext::RGBA as i32,
        upload.width as i32,
        upload.height as i32,
        0,
        WebGl2RenderingContext::RGBA,
        WebGl2RenderingContext::UNSIGNED_BYTE,
        Some(upload.rgba8),
    )?;
    if webgl2_filter_uses_mipmaps(upload.sampler.min_filter()) {
        gl.generate_mipmap(WebGl2RenderingContext::TEXTURE_2D);
    }
    *last_texture_hash = Some(next_hash);
    Ok(())
}

fn webgl2_wrap_mode(wrap: TextureWrap) -> u32 {
    match wrap {
        TextureWrap::ClampToEdge => WebGl2RenderingContext::CLAMP_TO_EDGE,
        TextureWrap::MirroredRepeat => WebGl2RenderingContext::MIRRORED_REPEAT,
        TextureWrap::Repeat => WebGl2RenderingContext::REPEAT,
    }
}

fn webgl2_filter_mode(filter: Option<TextureFilter>, allow_mipmaps: bool) -> u32 {
    match filter {
        Some(TextureFilter::Nearest) => WebGl2RenderingContext::NEAREST,
        Some(TextureFilter::Linear) | None => WebGl2RenderingContext::LINEAR,
        Some(TextureFilter::NearestMipmapNearest) if allow_mipmaps => {
            WebGl2RenderingContext::NEAREST_MIPMAP_NEAREST
        }
        Some(TextureFilter::LinearMipmapNearest) if allow_mipmaps => {
            WebGl2RenderingContext::LINEAR_MIPMAP_NEAREST
        }
        Some(TextureFilter::NearestMipmapLinear) if allow_mipmaps => {
            WebGl2RenderingContext::NEAREST_MIPMAP_LINEAR
        }
        Some(TextureFilter::LinearMipmapLinear) if allow_mipmaps => {
            WebGl2RenderingContext::LINEAR_MIPMAP_LINEAR
        }
        Some(TextureFilter::NearestMipmapNearest | TextureFilter::NearestMipmapLinear) => {
            WebGl2RenderingContext::NEAREST
        }
        Some(TextureFilter::LinearMipmapNearest | TextureFilter::LinearMipmapLinear) => {
            WebGl2RenderingContext::LINEAR
        }
    }
}

fn webgl2_filter_uses_mipmaps(filter: Option<TextureFilter>) -> bool {
    matches!(
        filter,
        Some(
            TextureFilter::NearestMipmapNearest
                | TextureFilter::LinearMipmapNearest
                | TextureFilter::NearestMipmapLinear
                | TextureFilter::LinearMipmapLinear
        )
    )
}

fn material_texture_hash(upload: MaterialTextureUpload<'_>) -> u64 {
    let mut hash = 0xcbf2_9ce4_8422_2325u64;
    hash ^= u64::from(upload.width);
    hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    hash ^= u64::from(upload.height);
    hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    hash ^= if upload.uses_decoded_texture { 1 } else { 0 };
    hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    hash_sampler_desc(&mut hash, upload.sampler);
    for byte in upload.rgba8 {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    hash
}

fn hash_sampler_desc(hash: &mut u64, sampler: TextureSamplerDesc) {
    for value in [
        texture_filter_hash(sampler.mag_filter()),
        texture_filter_hash(sampler.min_filter()),
        texture_wrap_hash(sampler.wrap_s()),
        texture_wrap_hash(sampler.wrap_t()),
    ] {
        *hash ^= value;
        *hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
}

fn texture_filter_hash(filter: Option<TextureFilter>) -> u64 {
    match filter {
        None => 0,
        Some(TextureFilter::Nearest) => 1,
        Some(TextureFilter::Linear) => 2,
        Some(TextureFilter::NearestMipmapNearest) => 3,
        Some(TextureFilter::LinearMipmapNearest) => 4,
        Some(TextureFilter::NearestMipmapLinear) => 5,
        Some(TextureFilter::LinearMipmapLinear) => 6,
    }
}

fn texture_wrap_hash(wrap: TextureWrap) -> u64 {
    match wrap {
        TextureWrap::ClampToEdge => 1,
        TextureWrap::MirroredRepeat => 2,
        TextureWrap::Repeat => 3,
    }
}