Skip to content

Commit

Permalink
KTX2/DDS/.basis compressed texture support (bevyengine#3884)
Browse files Browse the repository at this point in the history
# Objective

- Support compressed textures including 'universal' formats (ETC1S, UASTC) and transcoding of them to 
- Support `.dds`, `.ktx2`, and `.basis` files

## Solution

- Fixes bevyengine#3608 Look there for more details.
- Note that the functionality is all enabled through non-default features. If it is desirable to enable some by default, I can do that.
- The `basis-universal` crate, used for `.basis` file support and for transcoding, is built on bindings against a C++ library. It's not feasible to rewrite in Rust in a short amount of time. There are no Rust alternatives of which I am aware and it's specialised code. In its current state it doesn't support the wasm target, but I don't know for sure. However, it is possible to build the upstream C++ library with emscripten, so there is perhaps a way to add support for web too with some shenanigans.
- There's no support for transcoding from BasisLZ/ETC1S in KTX2 files as it was quite non-trivial to implement and didn't feel important given people could use `.basis` files for ETC1S.
  • Loading branch information
superdump authored and ItsDoot committed Feb 1, 2023
1 parent dc808fb commit 0cbfd3c
Show file tree
Hide file tree
Showing 19 changed files with 2,749 additions and 89 deletions.
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ wgpu_trace = ["bevy_internal/wgpu_trace"]
# Image format support for texture loading (PNG and HDR are enabled by default)
hdr = ["bevy_internal/hdr"]
png = ["bevy_internal/png"]
dds = ["bevy_internal/dds"]
tga = ["bevy_internal/tga"]
jpeg = ["bevy_internal/jpeg"]
bmp = ["bevy_internal/bmp"]
basis-universal = ["bevy_internal/basis-universal"]
dds = ["bevy_internal/dds"]
ktx2 = ["bevy_internal/ktx2"]
# For ktx2 supercompression
zlib = ["bevy_internal/zlib"]
zstd = ["bevy_internal/zstd"]

# Audio format support (vorbis is enabled by default)
flac = ["bevy_internal/flac"]
Expand Down
62 changes: 47 additions & 15 deletions crates/bevy_gltf/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use bevy_asset::{
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
};
use bevy_core::Name;
use bevy_ecs::world::World;
use bevy_ecs::{prelude::FromWorld, world::World};
use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder};
use bevy_log::warn;
use bevy_math::{Mat4, Vec3};
Expand All @@ -18,10 +18,9 @@ use bevy_render::{
color::Color,
mesh::{Indices, Mesh, VertexAttributeValues},
primitives::{Aabb, Frustum},
render_resource::{
AddressMode, FilterMode, PrimitiveTopology, SamplerDescriptor, TextureFormat,
},
texture::{Image, ImageType, TextureError},
render_resource::{AddressMode, FilterMode, PrimitiveTopology, SamplerDescriptor},
renderer::RenderDevice,
texture::{CompressedImageFormats, Image, ImageType, TextureError},
view::VisibleEntities,
};
use bevy_scene::Scene;
Expand Down Expand Up @@ -60,27 +59,41 @@ pub enum GltfError {
}

/// Loads glTF files with all of their data as their corresponding bevy representations.
#[derive(Default)]
pub struct GltfLoader;
pub struct GltfLoader {
supported_compressed_formats: CompressedImageFormats,
}

impl AssetLoader for GltfLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<()>> {
Box::pin(async move { Ok(load_gltf(bytes, load_context).await?) })
Box::pin(async move {
Ok(load_gltf(bytes, load_context, self.supported_compressed_formats).await?)
})
}

fn extensions(&self) -> &[&str] {
&["gltf", "glb"]
}
}

impl FromWorld for GltfLoader {
fn from_world(world: &mut World) -> Self {
Self {
supported_compressed_formats: CompressedImageFormats::from_features(
world.resource::<RenderDevice>().features(),
),
}
}
}

/// Loads an entire glTF file.
async fn load_gltf<'a, 'b>(
bytes: &'a [u8],
load_context: &'a mut LoadContext<'b>,
supported_compressed_formats: CompressedImageFormats,
) -> Result<(), GltfError> {
let gltf = gltf::Gltf::from_slice(bytes)?;
let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?;
Expand Down Expand Up @@ -251,8 +264,14 @@ async fn load_gltf<'a, 'b>(
// to avoid https://github.com/bevyengine/bevy/pull/2725
if gltf.textures().len() == 1 || cfg!(target_arch = "wasm32") {
for gltf_texture in gltf.textures() {
let (texture, label) =
load_texture(gltf_texture, &buffer_data, &linear_textures, load_context).await?;
let (texture, label) = load_texture(
gltf_texture,
&buffer_data,
&linear_textures,
load_context,
supported_compressed_formats,
)
.await?;
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
}
} else {
Expand All @@ -265,7 +284,14 @@ async fn load_gltf<'a, 'b>(
let load_context: &LoadContext = load_context;
let buffer_data = &buffer_data;
scope.spawn(async move {
load_texture(gltf_texture, buffer_data, linear_textures, load_context).await
load_texture(
gltf_texture,
buffer_data,
linear_textures,
load_context,
supported_compressed_formats,
)
.await
});
});
})
Expand Down Expand Up @@ -334,13 +360,20 @@ async fn load_texture<'a>(
buffer_data: &[Vec<u8>],
linear_textures: &HashSet<usize>,
load_context: &LoadContext<'a>,
supported_compressed_formats: CompressedImageFormats,
) -> Result<(Image, String), GltfError> {
let is_srgb = !linear_textures.contains(&gltf_texture.index());
let mut texture = match gltf_texture.source().source() {
gltf::image::Source::View { view, mime_type } => {
let start = view.offset() as usize;
let end = (view.offset() + view.length()) as usize;
let buffer = &buffer_data[view.buffer().index()][start..end];
Image::from_buffer(buffer, ImageType::MimeType(mime_type))?
Image::from_buffer(
buffer,
ImageType::MimeType(mime_type),
supported_compressed_formats,
is_srgb,
)?
}
gltf::image::Source::Uri { uri, mime_type } => {
let uri = percent_encoding::percent_decode_str(uri)
Expand All @@ -363,13 +396,12 @@ async fn load_texture<'a>(
Image::from_buffer(
&bytes,
mime_type.map(ImageType::MimeType).unwrap_or(image_type),
supported_compressed_formats,
is_srgb,
)?
}
};
texture.sampler_descriptor = texture_sampler(&gltf_texture);
if (linear_textures).contains(&gltf_texture.index()) {
texture.texture_descriptor.format = TextureFormat::Rgba8Unorm;
}

Ok((texture, texture_label(&gltf_texture)))
}
Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ debug_asset_server = ["bevy_asset/debug_asset_server"]
# Image format support for texture loading (PNG and HDR are enabled by default)
hdr = ["bevy_render/hdr"]
png = ["bevy_render/png"]
dds = ["bevy_render/dds"]
tga = ["bevy_render/tga"]
jpeg = ["bevy_render/jpeg"]
bmp = ["bevy_render/bmp"]
basis-universal = ["bevy_render/basis-universal"]
dds = ["bevy_render/dds"]
ktx2 = ["bevy_render/ktx2"]
# For ktx2 supercompression
zlib = ["bevy_render/zlib"]
zstd = ["bevy_render/zstd"]

# Audio format support (vorbis is enabled by default)
flac = ["bevy_audio/flac"]
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_internal/src/default_plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ impl PluginGroup for DefaultPlugins {
#[cfg(feature = "bevy_pbr")]
group.add(bevy_pbr::PbrPlugin::default());

// NOTE: Load this after renderer initialization so that it knows about the supported
// compressed texture formats
#[cfg(feature = "bevy_gltf")]
group.add(bevy_gltf::GltfPlugin::default());

Expand Down
17 changes: 17 additions & 0 deletions crates/bevy_pbr/src/pbr_material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ bitflags::bitflags! {
const ALPHA_MODE_OPAQUE = (1 << 6);
const ALPHA_MODE_MASK = (1 << 7);
const ALPHA_MODE_BLEND = (1 << 8);
const TWO_COMPONENT_NORMAL_MAP = (1 << 9);
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
Expand Down Expand Up @@ -246,6 +247,22 @@ impl RenderAsset for StandardMaterial {
flags |= StandardMaterialFlags::UNLIT;
}
let has_normal_map = material.normal_map_texture.is_some();
if has_normal_map {
match gpu_images
.get(material.normal_map_texture.as_ref().unwrap())
.unwrap()
.texture_format
{
// All 2-component unorm formats
TextureFormat::Rg8Unorm
| TextureFormat::Rg16Unorm
| TextureFormat::Bc5RgUnorm
| TextureFormat::EacRg11Unorm => {
flags |= StandardMaterialFlags::TWO_COMPONENT_NORMAL_MAP
}
_ => {}
}
}
// NOTE: 0.5 is from the glTF default - do we want this?
let mut alpha_cutoff = 0.5;
match material.alpha_mode {
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_pbr/src/render/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ impl FromWorld for MeshPipeline {
GpuImage {
texture,
texture_view,
texture_format: image.texture_descriptor.format,
sampler,
size: Size::new(
image.texture_descriptor.size.width as f32,
Expand Down
12 changes: 11 additions & 1 deletion crates/bevy_pbr/src/render/pbr.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 64u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u;
let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 512u;

[[group(1), binding(0)]]
var<uniform> material: StandardMaterial;
Expand Down Expand Up @@ -515,7 +516,16 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
#ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP
let TBN = mat3x3<f32>(T, B, N);
N = TBN * normalize(textureSample(normal_map_texture, normal_map_sampler, in.uv).rgb * 2.0 - 1.0);
// Nt is the tangent-space normal.
var Nt: vec3<f32>;
if ((material.flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u) {
// Only use the xy components and derive z for 2-component normal maps.
Nt = vec3<f32>(textureSample(normal_map_texture, normal_map_sampler, in.uv).rg * 2.0 - 1.0, 0.0);
Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y);
} else {
Nt = textureSample(normal_map_texture, normal_map_sampler, in.uv).rgb * 2.0 - 1.0;
}
N = normalize(TBN * Nt);
#endif
#endif

Expand Down
14 changes: 13 additions & 1 deletion crates/bevy_render/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ keywords = ["bevy"]
[features]
png = ["image/png"]
hdr = ["image/hdr"]
dds = ["image/dds"]
tga = ["image/tga"]
jpeg = ["image/jpeg"]
bmp = ["image/bmp"]
dds = ["ddsfile"]

# For ktx2 supercompression
zlib = ["flate2"]
zstd = ["ruzstd"]

trace = []
wgpu_trace = ["wgpu/trace"]
ci_limits = []
Expand Down Expand Up @@ -54,3 +59,10 @@ hexasphere = "7.0.0"
parking_lot = "0.11.0"
regex = "1.5"
copyless = "0.1.5"
ddsfile = { version = "0.5.0", optional = true }
ktx2 = { version = "0.3.0", optional = true }
# For ktx2 supercompression
flate2 = { version = "1.0.22", optional = true }
ruzstd = { version = "0.2.4", optional = true }
# For transcoding of UASTC/ETC1S universal formats, and for .basis file support
basis-universal = { version = "0.2.0", optional = true }
2 changes: 2 additions & 0 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ impl Plugin for RenderPlugin {
.add_plugin(CameraPlugin)
.add_plugin(ViewPlugin)
.add_plugin(MeshPlugin)
// NOTE: Load this after renderer initialization so that it knows about the supported
// compressed texture formats
.add_plugin(ImagePlugin);
}
}
Expand Down
18 changes: 18 additions & 0 deletions crates/bevy_render/src/renderer/render_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use futures_lite::future;
use std::sync::Arc;
use wgpu::util::DeviceExt;

use super::RenderQueue;

/// This GPU device is responsible for the creation of most rendering and compute resources.
#[derive(Clone)]
pub struct RenderDevice {
Expand Down Expand Up @@ -121,6 +123,22 @@ impl RenderDevice {
Buffer::from(wgpu_buffer)
}

/// Creates a new [`Texture`] and initializes it with the specified data.
///
/// `desc` specifies the general format of the texture.
/// `data` is the raw data.
pub fn create_texture_with_data(
&self,
render_queue: &RenderQueue,
desc: &wgpu::TextureDescriptor,
data: &[u8],
) -> Texture {
let wgpu_texture = self
.device
.create_texture_with_data(render_queue.as_ref(), desc, data);
Texture::from(wgpu_texture)
}

/// Creates a new [`Texture`].
///
/// `desc` specifies the general format of the texture.
Expand Down
Loading

0 comments on commit 0cbfd3c

Please sign in to comment.