Crafter.Asset/interfaces/Crafter.Asset-Texture.cppm
2026-05-18 22:31:28 +02:00

229 lines
No EOL
9.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Crafter®.Asset
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#define STB_IMAGE_IMPLEMENTATION
#include "../lib/stb_image.h"
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "../lib/stb_image_resize2.h"
export module Crafter.Asset:Texture;
import :Compression;
import std;
import Crafter.Math;
namespace fs = std::filesystem;
export namespace Crafter {
enum class OpaqueType : std::uint8_t {
FullyOpaque, // All pixels have A of 255
SemiOpaque, // All pixels have A of 0 or 255 (no blending needed)
Transparent // Color blending is used
};
struct TextureAssetInfo {
std::uint16_t sizeX;
std::uint16_t sizeY;
OpaqueType opaque;
};
// GDeflate-compressed counterpart of TextureAsset<T>::Save output.
// Single-region blob: the pixel array as one stream.
struct CompressedTextureAsset {
std::uint16_t sizeX = 0;
std::uint16_t sizeY = 0;
OpaqueType opaque = OpaqueType::FullyOpaque;
std::uint32_t pixelStride = 0;
Compression::CompressedBlob blob;
};
namespace TextureAssetFormat {
inline constexpr char magic[4] = {'C', 'G', 'D', 'T'};
inline constexpr std::uint32_t version = 1;
}
inline CompressedTextureAsset LoadCompressedTexture(fs::path path) {
std::ifstream file(path, std::ios::binary);
char magic[4];
file.read(magic, 4);
if (std::memcmp(magic, TextureAssetFormat::magic, 4) != 0) {
Compression::Fatal("LoadCompressedTexture: bad magic on " + path.string());
}
std::uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != TextureAssetFormat::version) {
Compression::Fatal("LoadCompressedTexture: unsupported version on " + path.string());
}
CompressedTextureAsset out;
file.read(reinterpret_cast<char*>(&out.sizeX), sizeof(out.sizeX));
file.read(reinterpret_cast<char*>(&out.sizeY), sizeof(out.sizeY));
file.read(reinterpret_cast<char*>(&out.opaque), sizeof(out.opaque));
file.read(reinterpret_cast<char*>(&out.pixelStride), sizeof(out.pixelStride));
out.blob = Compression::ReadBlob(file);
return out;
}
template <typename T>
struct TextureAsset {
std::uint16_t sizeX;
std::uint16_t sizeY;
OpaqueType opaque;
std::vector<T> pixels;
void Save(fs::path path) {
std::ofstream file(path, std::ios::binary);
file.write(reinterpret_cast<char*>(&sizeX), sizeof(sizeX));
file.write(reinterpret_cast<char*>(&sizeY), sizeof(sizeY));
file.write(reinterpret_cast<char*>(&opaque), sizeof(opaque));
file.write(reinterpret_cast<char*>(pixels.data()), pixels.size() * sizeof(T));
}
void SaveCompressed(fs::path path) const {
std::array<std::span<const std::byte>, 1> streams = {
std::as_bytes(std::span(pixels)),
};
Compression::CompressedBlob blob = Compression::CompressStreams(streams);
std::ofstream file(path, std::ios::binary);
file.write(TextureAssetFormat::magic, 4);
std::uint32_t version = TextureAssetFormat::version;
std::uint32_t stride = static_cast<std::uint32_t>(sizeof(T));
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
file.write(reinterpret_cast<const char*>(&sizeX), sizeof(sizeX));
file.write(reinterpret_cast<const char*>(&sizeY), sizeof(sizeY));
file.write(reinterpret_cast<const char*>(&opaque), sizeof(opaque));
file.write(reinterpret_cast<const char*>(&stride), sizeof(stride));
Compression::WriteBlob(file, blob);
}
static TextureAsset<T> Load(fs::path path) {
TextureAsset<T> tex;
std::ifstream file(path, std::ios::binary);
file.read(reinterpret_cast<char*>(&tex.sizeX), sizeof(tex.sizeX));
file.read(reinterpret_cast<char*>(&tex.sizeY), sizeof(tex.sizeY));
file.read(reinterpret_cast<char*>(&tex.opaque), sizeof(tex.opaque));
tex.pixels.resize(tex.sizeX * tex.sizeY);
file.read(reinterpret_cast<char*>(tex.pixels.data()), tex.sizeX * tex.sizeY * sizeof(T));
return tex;
}
// Bilinear-resize the pixel buffer in-place to `newW × newH`.
// Used to normalize albedos to a uniform size before stacking them
// into a WebGPU texture_2d_array (which requires identical layer
// dimensions). stb_image_resize2 handles RGBA8 directly.
void Resize(std::uint16_t newW, std::uint16_t newH) requires (sizeof(T) == 4) {
if (sizeX == newW && sizeY == newH) return;
std::vector<T> out(static_cast<std::size_t>(newW) * newH);
stbir_resize_uint8_linear(
reinterpret_cast<const std::uint8_t*>(pixels.data()), sizeX, sizeY, 0,
reinterpret_cast<std::uint8_t*>(out.data()), newW, newH, 0,
STBIR_RGBA);
pixels = std::move(out);
sizeX = newW;
sizeY = newH;
}
static TextureAssetInfo LoadInfo(fs::path path) {
TextureAssetInfo info;
std::ifstream file(path, std::ios::binary);
file.read(reinterpret_cast<char*>(&info.sizeX), sizeof(info.sizeX));
file.read(reinterpret_cast<char*>(&info.sizeY), sizeof(info.sizeY));
file.read(reinterpret_cast<char*>(&info.opaque), sizeof(info.opaque));
return info;
}
static void Load(fs::path path, T* pixels, std::uint16_t sizeX, std::uint16_t sizeY) {
std::ifstream file(path, std::ios::binary);
file.seekg(sizeof(std::uint16_t) + sizeof(std::uint16_t) + sizeof(std::uint8_t), std::ios::cur);
file.read(reinterpret_cast<char*>(pixels), sizeX * sizeY * sizeof(T));
}
template <typename TT>
static TextureAsset<T> LoadPNG(fs::path path) {
TextureAsset<T> tex;
std::filesystem::path abs = std::filesystem::absolute(path);
int sizeX;
int sizeY;
unsigned char* data = stbi_load(abs.string().c_str(), &sizeX, &sizeY, nullptr, 4);
tex.sizeX = sizeX;
tex.sizeY = sizeY;
tex.pixels.resize(tex.sizeX*tex.sizeY);
tex.opaque = OpaqueType::FullyOpaque;
if constexpr(
#ifndef __wasm__
std::same_as<TT, _Float16> ||
#endif
std::same_as<TT, float> || std::same_as<TT, double>) {
for(std::uint32_t i = 0; i < sizeX*sizeY; i++) {
tex.pixels[i].r = TT(data[i*4])/255;
tex.pixels[i].g = TT(data[i*4+1])/255;
tex.pixels[i].b = TT(data[i*4+2])/255;
tex.pixels[i].a = TT(data[i*4+3])/255;
}
for(std::uint32_t i = 0; i < tex.sizeX* tex.sizeY; i++) {
if(tex.pixels[i].a != 1) {
tex.opaque = OpaqueType::SemiOpaque;
for(std::uint32_t i2 = i; i2 < tex.sizeX* tex.sizeY; i2++) {
if(tex.pixels[i2].a != 0 && tex.pixels[i2].a != 1) {
tex.opaque = OpaqueType::Transparent;
stbi_image_free(data);
return tex;
}
}
stbi_image_free(data);
return tex;
}
}
} else {
std::memcpy(tex.pixels.data(), data, tex.sizeX * tex.sizeY * 4);
for(std::uint32_t i = 0; i < tex.sizeX* tex.sizeY; i++) {
if(tex.pixels[i].a != 255) {
tex.opaque = OpaqueType::SemiOpaque;
for(std::uint32_t i2 = i; i2 < tex.sizeX* tex.sizeY; i2++) {
if(tex.pixels[i2].a != 0 && tex.pixels[i2].a != 255) {
tex.opaque = OpaqueType::Transparent;
stbi_image_free(data);
return tex;
}
}
stbi_image_free(data);
return tex;
}
}
}
stbi_image_free(data);
return tex;
}
};
}