feat: DDS 纹理加载器(BC 压缩直传 + mip 链,参考 Horizon 实现)
- Assets::Dds 解析器:中性 DdsFormat 枚举(Assets 层禁 Vulkan 头铁律), 支持 legacy fourcc(DXT1/3/5/ATI1/2)+ DX10 头(BC1..BC7/RGBA8/BGRA8) + 未压缩 32bpp + 2D mip 链;数据不解码,直接上传由硬件采样解码 - Texture:按扩展名分发(.dds → InitFromDds);渲染侧负责格式映射与 sRGB 语义(颜色图走 SRGB 变体,BC4/5/6H 数据图保持线性);view/sampler 覆盖完整 mip 链;逐 mip copyBufferToImage(紧凑排布) - Model::GetOrLoadTexture:FBX/DCC 路径反斜杠归一化(否则 Linux 全挂) - DescriptorManager 池 1000 → 4096(SunTemple 1056 材质集打爆过; 材质去重属材质系统任务) - main.cpp 临时挂 SunTemple 场景验证:48 张 DDS 全部加载(2048²、12 mips), 1056 mesh 渲染正常,validation 零警告
This commit is contained in:
+6
-15
@@ -47,7 +47,8 @@ protected:
|
|||||||
EnableImGui();
|
EnableImGui();
|
||||||
|
|
||||||
// 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄)
|
// 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄)
|
||||||
const auto backpack = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
// SunTemple:FBX + DDS 压缩贴图(DdsLoader 验证场景)
|
||||||
|
const auto temple = GetAssets().LoadModel("assets/models/SunTemple/SunTemple.fbx");
|
||||||
|
|
||||||
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
||||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
||||||
@@ -64,23 +65,13 @@ protected:
|
|||||||
camera.primary = true;
|
camera.primary = true;
|
||||||
camera.camera.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
|
camera.camera.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
|
||||||
|
|
||||||
// 背包实体 × 3(悬浮在地面上方)
|
// 神庙实体 × 1( FBX 单位较大,缩放到场景尺度)
|
||||||
auto size = 1;
|
|
||||||
for (int i = 0; i < size; ++i)
|
|
||||||
{
|
{
|
||||||
for (int j = 0; j < size; j++)
|
auto e = scene.CreateEntity("SunTemple");
|
||||||
{
|
|
||||||
auto e = scene.CreateEntity("Backpack" + std::to_string(i));
|
|
||||||
|
|
||||||
e.AddComponent<TransformComponent>()
|
e.AddComponent<TransformComponent>()
|
||||||
.SetPosition(glm::vec3((i - size * 0.5) * 1.5f, 0.0f, (j - size * 0.5) * 1.5f))
|
.SetScale(glm::vec3(0.05f))
|
||||||
.SetScale(glm::vec3(0.5f))
|
|
||||||
.Compose();
|
.Compose();
|
||||||
|
e.AddComponent<MeshRendererComponent>(MeshRendererComponent{temple});
|
||||||
e.AddComponent<MeshRendererComponent>(MeshRendererComponent{backpack});
|
|
||||||
|
|
||||||
m_Spinners.push_back(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 灯光实体(第一个灯 = 阴影光源)
|
// 灯光实体(第一个灯 = 阴影光源)
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/9/23.
|
||||||
|
//
|
||||||
|
// DDS 文件解析(最小实现,不解码压缩块)。
|
||||||
|
//
|
||||||
|
// 设计:GPU 原生支持 BC 压缩格式——DDS 的压缩数据**直接上传**为
|
||||||
|
// BC1/BC2/BC3/BC4/BC5/BC6H/BC7 纹理由硬件采样解码,不做 CPU 解码。
|
||||||
|
// 输出中性 DdsFormat(本层禁 Vulkan 头),vk::Format 映射与 sRGB
|
||||||
|
// 语义由调用方(Texture,渲染侧)决定。
|
||||||
|
//
|
||||||
|
// 支持:
|
||||||
|
// - legacy fourcc:DXT1/DXT3/DXT5/ATI1/BC4U/ATI2/BC5U
|
||||||
|
// - DX10 扩展头:BC1..BC7、R8G8B8A8、B8G8R8A8
|
||||||
|
// - 未压缩 32bpp(DDPF_RGB | DDPF_ALPHAPIXELS)
|
||||||
|
// - 2D + mip 链(cubemap 面标志暂不支持,报错)
|
||||||
|
//
|
||||||
|
// 输出:Mips[r] = 第 r 级完整数据(BC 按块行紧凑排布:
|
||||||
|
// bytes = ceil(w/4)*ceil(h/4)*blockBytes)。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_DDSLOADER_H
|
||||||
|
#define HELLOVULKAN_DDSLOADER_H
|
||||||
|
|
||||||
|
#include <Engine/Core/Types.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Engine::Assets::Dds {
|
||||||
|
|
||||||
|
// 中性格式(Assets 层不感知 Vulkan;映射在渲染侧 Texture)
|
||||||
|
enum class DdsFormat {
|
||||||
|
BC1, BC2, BC3, BC4, BC5, BC6H, BC7,
|
||||||
|
RGBA8,
|
||||||
|
BGRA8, // 按字节序上传后视作 RGBA(近似,触发警告)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DdsData {
|
||||||
|
u32 Width = 0;
|
||||||
|
u32 Height = 0;
|
||||||
|
u32 MipLevels = 1;
|
||||||
|
DdsFormat Format = DdsFormat::RGBA8;
|
||||||
|
std::vector<std::vector<u8>> Mips; // 每级完整数据(紧凑排布)
|
||||||
|
bool BlockCompressed = false; // BC 格式(rowPitch 按块计算)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析 DDS 文件;失败返回 false 并写 errMsg
|
||||||
|
bool Load(const std::string& path, DdsData& out, std::string& errMsg);
|
||||||
|
|
||||||
|
// 指定 mip 级的单行字节跨度(BC = 块行;RGBA8 = w*4)
|
||||||
|
u32 RowPitch(const DdsData& data, u32 mipLevel);
|
||||||
|
|
||||||
|
} // namespace Engine::Assets::Dds
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_DDSLOADER_H
|
||||||
@@ -50,12 +50,12 @@ namespace Engine::ECS {
|
|||||||
// 分量 → world 矩阵(T * R * S)
|
// 分量 → world 矩阵(T * R * S)
|
||||||
TransformComponent& Compose()
|
TransformComponent& Compose()
|
||||||
{
|
{
|
||||||
glm::mat4 t = glm::translate(glm::mat4(1.0f), position);
|
const glm::mat4 t = glm::translate(glm::mat4(1.0f), position);
|
||||||
glm::mat4 r =
|
const glm::mat4 r =
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(0, 1, 0)) *
|
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(0, 1, 0)) *
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(1, 0, 0)) *
|
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(1, 0, 0)) *
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.z), glm::vec3(0, 0, 1));
|
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.z), glm::vec3(0, 0, 1));
|
||||||
glm::mat4 s = glm::scale(glm::mat4(1.0f), scale);
|
const glm::mat4 s = glm::scale(glm::mat4(1.0f), scale);
|
||||||
world = t * r * s;
|
world = t * r * s;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ namespace Engine {
|
|||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
vk::DescriptorPool m_Pool;
|
vk::DescriptorPool m_Pool;
|
||||||
|
|
||||||
static constexpr u32 kMaxSets = 1000;
|
// 描述符池容量:需 ≥ 场景全部材质集合数 + 每帧集合。
|
||||||
|
// SunTemple(984 mesh 材质)把 1000 打爆过;材质去重(同贴图共享
|
||||||
|
// Material)属于材质系统任务,在那之前留足余量
|
||||||
|
static constexpr u32 kMaxSets = 4096;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace Engine {
|
|||||||
|
|
||||||
class Texture {
|
class Texture {
|
||||||
public:
|
public:
|
||||||
// 从磁盘图片加载
|
// 从磁盘图片加载(按扩展名分发:.dds → DdsLoader,其它 → stbi)
|
||||||
Texture(Device& device, MemoryAllocator& allocator, const std::string& path);
|
Texture(Device& device, MemoryAllocator& allocator, const std::string& path);
|
||||||
|
|
||||||
// 程序化纹理:纯色(1x1),用于地面/调试等不依赖磁盘资产的场景
|
// 程序化纹理:纯色(1x1),用于地面/调试等不依赖磁盘资产的场景
|
||||||
@@ -38,6 +38,8 @@ namespace Engine {
|
|||||||
vk::ImageView GetView() const { return m_View; }
|
vk::ImageView GetView() const { return m_View; }
|
||||||
u32 GetWidth() const { return m_Width; }
|
u32 GetWidth() const { return m_Width; }
|
||||||
u32 GetHeight() const { return m_Height; }
|
u32 GetHeight() const { return m_Height; }
|
||||||
|
vk::Format GetFormat() const { return m_Format; }
|
||||||
|
u32 GetMipLevels() const { return m_MipLevels; }
|
||||||
|
|
||||||
vk::Sampler GetSampler() const { return m_Sampler; }
|
vk::Sampler GetSampler() const { return m_Sampler; }
|
||||||
|
|
||||||
@@ -46,13 +48,19 @@ namespace Engine {
|
|||||||
Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
||||||
const std::array<u8, 4>& rgba);
|
const std::array<u8, 4>& rgba);
|
||||||
|
|
||||||
// 公共路径:staging → device local image → view/sampler
|
// 公共路径:staging → device local image → view/sampler(RGBA8 单 mip)
|
||||||
void InitFromPixels(const void* pixels, u32 width, u32 height);
|
void InitFromPixels(const void* pixels, u32 width, u32 height);
|
||||||
|
|
||||||
|
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
|
||||||
|
void InitFromDds(const std::string& path);
|
||||||
|
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
vk::Image m_Image;
|
vk::Image m_Image;
|
||||||
vk::ImageView m_View;
|
vk::ImageView m_View;
|
||||||
|
|
||||||
|
vk::Format m_Format = vk::Format::eUndefined;
|
||||||
|
u32 m_MipLevels = 1;
|
||||||
|
|
||||||
vk::Sampler m_Sampler;
|
vk::Sampler m_Sampler;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/9/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <Engine/Assets/DdsLoader.h>
|
||||||
|
#include <Engine/Core/Log.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
namespace Engine::Assets::Dds
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr u32 kMagic = 0x20534444; // "DDS "
|
||||||
|
|
||||||
|
// 头部偏移(dwSpfFourCC @ 84)
|
||||||
|
constexpr size_t kOffsetHeight = 12;
|
||||||
|
constexpr size_t kOffsetWidth = 16;
|
||||||
|
constexpr size_t kOffsetMips = 28;
|
||||||
|
constexpr size_t kOffsetFourCC = 84;
|
||||||
|
constexpr size_t kHeaderSize = 128;
|
||||||
|
constexpr size_t kDx10Size = 20;
|
||||||
|
|
||||||
|
u32 ReadU32(const u8* p) { return *reinterpret_cast<const u32*>(p); }
|
||||||
|
|
||||||
|
// legacy fourcc → 中性格式
|
||||||
|
bool MapFourCC(u32 fourcc, DdsFormat& out)
|
||||||
|
{
|
||||||
|
switch (fourcc) {
|
||||||
|
case 0x31545844: out = DdsFormat::BC1; return true; // "DXT1"
|
||||||
|
case 0x33545844: out = DdsFormat::BC2; return true; // "DXT3"
|
||||||
|
case 0x35545844: out = DdsFormat::BC3; return true; // "DXT5"
|
||||||
|
case 0x31495441: out = DdsFormat::BC4; return true; // "ATI1"
|
||||||
|
case 0x55344342: out = DdsFormat::BC4; return true; // "BC4U"
|
||||||
|
case 0x32495441: out = DdsFormat::BC5; return true; // "ATI2"
|
||||||
|
case 0x55354342: out = DdsFormat::BC5; return true; // "BC5U"
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DX10 DXGI 值 → 中性格式
|
||||||
|
bool MapDxgi(u32 dxgi, DdsFormat& out)
|
||||||
|
{
|
||||||
|
switch (dxgi) {
|
||||||
|
case 70: case 71: out = DdsFormat::BC1; return true; // BC1
|
||||||
|
case 73: case 74: out = DdsFormat::BC2; return true; // BC2
|
||||||
|
case 77: case 78: out = DdsFormat::BC3; return true; // BC3
|
||||||
|
case 79: case 80: out = DdsFormat::BC4; return true; // BC4
|
||||||
|
case 82: case 83: out = DdsFormat::BC5; return true; // BC5
|
||||||
|
case 94: case 95: out = DdsFormat::BC6H; return true; // BC6H
|
||||||
|
case 97: case 98: out = DdsFormat::BC7; return true; // BC7
|
||||||
|
case 28: case 29: out = DdsFormat::RGBA8; return true; // R8G8B8A8
|
||||||
|
case 87: case 91: out = DdsFormat::BGRA8; return true; // B8G8R8A8(近似)
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 BytesPerBlock(DdsFormat format)
|
||||||
|
{
|
||||||
|
switch (format) {
|
||||||
|
case DdsFormat::BC1:
|
||||||
|
case DdsFormat::BC4:
|
||||||
|
return 8;
|
||||||
|
case DdsFormat::BC2:
|
||||||
|
case DdsFormat::BC3:
|
||||||
|
case DdsFormat::BC5:
|
||||||
|
case DdsFormat::BC6H:
|
||||||
|
case DdsFormat::BC7:
|
||||||
|
return 16;
|
||||||
|
default:
|
||||||
|
return 4; // RGBA8/BGRA8 系
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsBlockCompressed(DdsFormat format)
|
||||||
|
{
|
||||||
|
return format != DdsFormat::RGBA8 && format != DdsFormat::BGRA8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Load(const std::string& path, DdsData& out, std::string& errMsg)
|
||||||
|
{
|
||||||
|
std::ifstream f(path, std::ios::binary);
|
||||||
|
if (!f) {
|
||||||
|
errMsg = "无法打开文件";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 header[kHeaderSize];
|
||||||
|
f.read(reinterpret_cast<char*>(header), kHeaderSize);
|
||||||
|
if (!f || ReadU32(header) != kMagic) {
|
||||||
|
errMsg = "DDS 魔数不符";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Width = ReadU32(header + kOffsetWidth);
|
||||||
|
out.Height = ReadU32(header + kOffsetHeight);
|
||||||
|
const u32 mips = ReadU32(header + kOffsetMips);
|
||||||
|
out.MipLevels = mips ? mips : 1;
|
||||||
|
|
||||||
|
const u32 fourcc = ReadU32(header + kOffsetFourCC);
|
||||||
|
size_t dataOffset = kHeaderSize;
|
||||||
|
|
||||||
|
if (fourcc == 0x30313044) { // "DX10"
|
||||||
|
u8 dx10[kDx10Size];
|
||||||
|
f.read(reinterpret_cast<char*>(dx10), kDx10Size);
|
||||||
|
if (!f) {
|
||||||
|
errMsg = "DX10 头不完整";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const u32 dxgi = ReadU32(dx10);
|
||||||
|
if (!MapDxgi(dxgi, out.Format)) {
|
||||||
|
errMsg = "DX10 DXGI 格式不支持";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dataOffset += kDx10Size;
|
||||||
|
} else if (!MapFourCC(fourcc, out.Format)) {
|
||||||
|
// 未压缩路径:仅支持 32bpp RGBA(DDPF_ALPHAPIXELS(0x1) | DDPF_RGB(0x40))
|
||||||
|
const u32 ddspfFlags = ReadU32(header + 80);
|
||||||
|
if (ddspfFlags & 0x41) {
|
||||||
|
out.Format = DdsFormat::BGRA8; // 常见 32bpp DDS 是 B8G8R8A8 字节序
|
||||||
|
ENGINE_WARN("DDS: 未压缩 32bpp 按 BGRA8 上传 '{}'", path);
|
||||||
|
} else {
|
||||||
|
errMsg = "fourcc 不支持";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.BlockCompressed = IsBlockCompressed(out.Format);
|
||||||
|
f.seekg(dataOffset);
|
||||||
|
|
||||||
|
// 数据量校验 + 逐级读取(紧凑排布)
|
||||||
|
const u32 bpb = BytesPerBlock(out.Format);
|
||||||
|
const u32 cols = out.BlockCompressed ? 4 : 1;
|
||||||
|
out.Mips.resize(out.MipLevels);
|
||||||
|
|
||||||
|
u32 w = out.Width, h = out.Height;
|
||||||
|
for (u32 mip = 0; mip < out.MipLevels; ++mip) {
|
||||||
|
const u32 blocksW = std::max(1u, (w + cols - 1) / cols);
|
||||||
|
const u32 blocksH = std::max(1u, (h + cols - 1) / cols);
|
||||||
|
const size_t bytes = size_t(blocksW) * blocksH * bpb;
|
||||||
|
out.Mips[mip].resize(bytes);
|
||||||
|
f.read(reinterpret_cast<char*>(out.Mips[mip].data()),
|
||||||
|
std::streamsize(bytes));
|
||||||
|
if (!f) {
|
||||||
|
errMsg = "mip 数据不完整";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
w = std::max(1u, w / 2);
|
||||||
|
h = std::max(1u, h / 2);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 RowPitch(const DdsData& data, u32 mipLevel)
|
||||||
|
{
|
||||||
|
u32 w = data.Width;
|
||||||
|
for (u32 m = 0; m < mipLevel; ++m) {
|
||||||
|
w = std::max(1u, w / 2);
|
||||||
|
}
|
||||||
|
const u32 bpb = BytesPerBlock(data.Format);
|
||||||
|
if (data.BlockCompressed) {
|
||||||
|
return std::max(1u, (w + 3) / 4) * bpb;
|
||||||
|
}
|
||||||
|
return w * bpb;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Engine::Assets::Dds
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <assimp/postprocess.h>
|
#include <assimp/postprocess.h>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
@@ -110,8 +111,12 @@ void Model::ProcessNode(aiNode* node, const aiScene* scene)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Texture* Model::GetOrLoadTexture(const std::string& path)
|
Texture* Model::GetOrLoadTexture(const std::string& rawPath)
|
||||||
{
|
{
|
||||||
|
// FBX/DCC 导出的路径常带反斜杠(Windows 语义),统一成正斜杠
|
||||||
|
std::string path = rawPath;
|
||||||
|
std::replace(path.begin(), path.end(), '\\', '/');
|
||||||
|
|
||||||
auto it = m_TextureCache.find(path);
|
auto it = m_TextureCache.find(path);
|
||||||
if (it != m_TextureCache.end()) {
|
if (it != m_TextureCache.end()) {
|
||||||
return it->second.get();
|
return it->second.get();
|
||||||
|
|||||||
@@ -5,10 +5,13 @@
|
|||||||
#include <Engine/Vulkan/Texture.h>
|
#include <Engine/Vulkan/Texture.h>
|
||||||
#include <Engine/Vulkan/Device.h>
|
#include <Engine/Vulkan/Device.h>
|
||||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||||
|
#include <Engine/Assets/DdsLoader.h>
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
|
|
||||||
#include <stb/stb_image.h>
|
#include <stb/stb_image.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include "Engine/Core/Stats.h"
|
#include "Engine/Core/Stats.h"
|
||||||
@@ -18,12 +21,21 @@ namespace Engine
|
|||||||
Texture::Texture(Device& device, MemoryAllocator& allocator, const std::string& path)
|
Texture::Texture(Device& device, MemoryAllocator& allocator, const std::string& path)
|
||||||
: m_Device(device) , m_Allocator(allocator)
|
: m_Device(device) , m_Allocator(allocator)
|
||||||
{
|
{
|
||||||
|
// 按扩展名分发:DDS(BC 压缩直传 + mip 链)vs stbi(无压缩单 mip)
|
||||||
|
const std::string ext = path.size() >= 4
|
||||||
|
? path.substr(path.size() - 4) : "";
|
||||||
|
std::string lower;
|
||||||
|
for (char c : ext) lower += static_cast<char>(std::tolower(c));
|
||||||
|
|
||||||
|
if (lower == ".dds") {
|
||||||
|
InitFromDds(path);
|
||||||
|
} else {
|
||||||
// 1. CPU 加载图片
|
// 1. CPU 加载图片
|
||||||
int width = 0, height = 0, channels = 0;
|
int width = 0, height = 0, channels = 0;
|
||||||
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, STBI_rgb_alpha);
|
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels,
|
||||||
|
STBI_rgb_alpha);
|
||||||
|
|
||||||
if (!pixels)
|
if (!pixels) {
|
||||||
{
|
|
||||||
ENGINE_ERROR("Failed to load texture: {}", path);
|
ENGINE_ERROR("Failed to load texture: {}", path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -34,6 +46,7 @@ namespace Engine
|
|||||||
|
|
||||||
ENGINE_INFO("Texture loaded: {} ({}x{})", path, m_Width, m_Height);
|
ENGINE_INFO("Texture loaded: {} ({}x{})", path, m_Width, m_Height);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Texture::Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
Texture::Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
||||||
const std::array<u8, 4>& rgba)
|
const std::array<u8, 4>& rgba)
|
||||||
@@ -53,6 +66,8 @@ namespace Engine
|
|||||||
{
|
{
|
||||||
m_Width = width;
|
m_Width = width;
|
||||||
m_Height = height;
|
m_Height = height;
|
||||||
|
m_Format = vk::Format::eR8G8B8A8Srgb;
|
||||||
|
m_MipLevels = 1;
|
||||||
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
|
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
|
||||||
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
|
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
|
||||||
|
|
||||||
@@ -87,10 +102,10 @@ namespace Engine
|
|||||||
vk::ImageViewCreateInfo viewInfo;
|
vk::ImageViewCreateInfo viewInfo;
|
||||||
viewInfo.setImage(m_Image)
|
viewInfo.setImage(m_Image)
|
||||||
.setViewType(vk::ImageViewType::e2D)
|
.setViewType(vk::ImageViewType::e2D)
|
||||||
.setFormat(vk::Format::eR8G8B8A8Srgb)
|
.setFormat(m_Format)
|
||||||
.setSubresourceRange({
|
.setSubresourceRange({
|
||||||
vk::ImageAspectFlagBits::eColor,
|
vk::ImageAspectFlagBits::eColor,
|
||||||
0, 1, 0, 1
|
0, m_MipLevels, 0, 1
|
||||||
});
|
});
|
||||||
|
|
||||||
m_View = m_Device.GetDevice().createImageView(viewInfo);
|
m_View = m_Device.GetDevice().createImageView(viewInfo);
|
||||||
@@ -107,7 +122,9 @@ namespace Engine
|
|||||||
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
|
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
|
||||||
.setUnnormalizedCoordinates(VK_FALSE)
|
.setUnnormalizedCoordinates(VK_FALSE)
|
||||||
.setCompareEnable(VK_FALSE)
|
.setCompareEnable(VK_FALSE)
|
||||||
.setMipmapMode(vk::SamplerMipmapMode::eLinear);
|
.setMipmapMode(vk::SamplerMipmapMode::eLinear)
|
||||||
|
.setMinLod(0.0f)
|
||||||
|
.setMaxLod(static_cast<f32>(m_MipLevels));
|
||||||
|
|
||||||
m_Sampler = m_Device.GetDevice().createSampler(samplerInfo);
|
m_Sampler = m_Device.GetDevice().createSampler(samplerInfo);
|
||||||
|
|
||||||
@@ -197,6 +214,186 @@ namespace Engine
|
|||||||
STATS.AddTexture(m_TextureBytes);
|
STATS.AddTexture(m_TextureBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DDS → Vulkan 格式映射(渲染侧职责:sRGB 语义按颜色贴图默认开启;
|
||||||
|
// BC4/BC5/BC6H 是数据图,保持线性)
|
||||||
|
vk::Format ToVkFormat(Assets::Dds::DdsFormat f)
|
||||||
|
{
|
||||||
|
using DF = Assets::Dds::DdsFormat;
|
||||||
|
switch (f) {
|
||||||
|
case DF::BC1: return vk::Format::eBc1RgbaSrgbBlock;
|
||||||
|
case DF::BC2: return vk::Format::eBc2SrgbBlock;
|
||||||
|
case DF::BC3: return vk::Format::eBc3SrgbBlock;
|
||||||
|
case DF::BC4: return vk::Format::eBc4UnormBlock;
|
||||||
|
case DF::BC5: return vk::Format::eBc5UnormBlock;
|
||||||
|
case DF::BC6H: return vk::Format::eBc6HUfloatBlock;
|
||||||
|
case DF::BC7: return vk::Format::eBc7SrgbBlock;
|
||||||
|
case DF::RGBA8: return vk::Format::eR8G8B8A8Srgb;
|
||||||
|
case DF::BGRA8: return vk::Format::eB8G8R8A8Srgb;
|
||||||
|
}
|
||||||
|
return vk::Format::eUndefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 MipDim(u32 v, u32 level)
|
||||||
|
{
|
||||||
|
return std::max(1u, v >> level);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::InitFromDds(const std::string& path)
|
||||||
|
{
|
||||||
|
Assets::Dds::DdsData data;
|
||||||
|
std::string err;
|
||||||
|
if (!Assets::Dds::Load(path, data, err)) {
|
||||||
|
ENGINE_ERROR("DDS: {} ({})", err, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Width = data.Width;
|
||||||
|
m_Height = data.Height;
|
||||||
|
m_MipLevels = data.MipLevels;
|
||||||
|
m_Format = ToVkFormat(data.Format);
|
||||||
|
|
||||||
|
ENGINE_INFO("DDS loaded: {} ({}x{}, {} mips, {})", path,
|
||||||
|
m_Width, m_Height, m_MipLevels, vk::to_string(m_Format));
|
||||||
|
|
||||||
|
// 1. staging:各 mip 紧凑排布,记偏移
|
||||||
|
vk::DeviceSize total = 0;
|
||||||
|
for (const auto& mip : data.Mips) {
|
||||||
|
total += mip.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stagingAlloc = m_Allocator.CreateBuffer(
|
||||||
|
total,
|
||||||
|
vk::BufferUsageFlagBits::eTransferSrc,
|
||||||
|
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||||
|
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||||
|
|
||||||
|
std::vector<vk::DeviceSize> offsets(m_MipLevels);
|
||||||
|
{
|
||||||
|
vk::DeviceSize off = 0;
|
||||||
|
for (u32 mip = 0; mip < m_MipLevels; ++mip) {
|
||||||
|
offsets[mip] = off;
|
||||||
|
std::memcpy(stagingAlloc.MappedPtr + off,
|
||||||
|
data.Mips[mip].data(), data.Mips[mip].size());
|
||||||
|
off += data.Mips[mip].size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. device local image(带完整 mip 链)
|
||||||
|
vk::ImageCreateInfo imageInfo;
|
||||||
|
imageInfo.setImageType(vk::ImageType::e2D)
|
||||||
|
.setFormat(m_Format)
|
||||||
|
.setExtent({m_Width, m_Height, 1})
|
||||||
|
.setMipLevels(m_MipLevels)
|
||||||
|
.setArrayLayers(1)
|
||||||
|
.setSamples(vk::SampleCountFlagBits::e1)
|
||||||
|
.setTiling(vk::ImageTiling::eOptimal)
|
||||||
|
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
|
||||||
|
vk::ImageUsageFlagBits::eSampled)
|
||||||
|
.setSharingMode(vk::SharingMode::eExclusive)
|
||||||
|
.setInitialLayout(vk::ImageLayout::eUndefined);
|
||||||
|
|
||||||
|
m_ImageAllocation = m_Allocator.CreateImage(imageInfo,
|
||||||
|
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
||||||
|
m_Image = m_ImageAllocation.Image;
|
||||||
|
|
||||||
|
// 3. view / sampler(覆盖整个 mip 链)
|
||||||
|
m_View = m_Device.GetDevice().createImageView(
|
||||||
|
vk::ImageViewCreateInfo{}
|
||||||
|
.setImage(m_Image)
|
||||||
|
.setViewType(vk::ImageViewType::e2D)
|
||||||
|
.setFormat(m_Format)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, m_MipLevels, 0, 1}));
|
||||||
|
|
||||||
|
m_Sampler = m_Device.GetDevice().createSampler(
|
||||||
|
vk::SamplerCreateInfo{}
|
||||||
|
.setMagFilter(vk::Filter::eLinear)
|
||||||
|
.setMinFilter(vk::Filter::eLinear)
|
||||||
|
.setMipmapMode(vk::SamplerMipmapMode::eLinear)
|
||||||
|
.setAddressModeU(vk::SamplerAddressMode::eRepeat)
|
||||||
|
.setAddressModeV(vk::SamplerAddressMode::eRepeat)
|
||||||
|
.setAddressModeW(vk::SamplerAddressMode::eRepeat)
|
||||||
|
.setAnisotropyEnable(VK_FALSE)
|
||||||
|
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
|
||||||
|
.setUnnormalizedCoordinates(VK_FALSE)
|
||||||
|
.setCompareEnable(VK_FALSE)
|
||||||
|
.setMinLod(0.0f)
|
||||||
|
.setMaxLod(static_cast<f32>(m_MipLevels)));
|
||||||
|
|
||||||
|
// 4. 一次性命令缓冲:整图 → TransferDst,逐 mip 拷贝 → ShaderRead
|
||||||
|
vk::CommandPoolCreateInfo poolInfo;
|
||||||
|
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
|
||||||
|
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
||||||
|
vk::CommandPool pool = m_Device.GetDevice().createCommandPool(poolInfo);
|
||||||
|
|
||||||
|
vk::CommandBufferAllocateInfo allocInfo;
|
||||||
|
allocInfo.setCommandPool(pool)
|
||||||
|
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||||
|
.setCommandBufferCount(1);
|
||||||
|
vk::CommandBuffer cmd =
|
||||||
|
m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
|
||||||
|
|
||||||
|
cmd.begin(vk::CommandBufferBeginInfo{
|
||||||
|
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
||||||
|
|
||||||
|
vk::ImageMemoryBarrier2 toTransfer;
|
||||||
|
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
||||||
|
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
||||||
|
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
||||||
|
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
||||||
|
.setOldLayout(vk::ImageLayout::eUndefined)
|
||||||
|
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
|
||||||
|
.setImage(m_Image)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, m_MipLevels, 0, 1});
|
||||||
|
cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
|
||||||
|
|
||||||
|
// BC 数据按块紧凑排布 → bufferRowLength/Height 留 0(紧凑)
|
||||||
|
for (u32 mip = 0; mip < m_MipLevels; ++mip) {
|
||||||
|
vk::BufferImageCopy region;
|
||||||
|
region.setBufferOffset(offsets[mip])
|
||||||
|
.setBufferRowLength(0)
|
||||||
|
.setBufferImageHeight(0)
|
||||||
|
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
|
||||||
|
mip, 0, 1})
|
||||||
|
.setImageOffset({0, 0, 0})
|
||||||
|
.setImageExtent({MipDim(m_Width, mip),
|
||||||
|
MipDim(m_Height, mip), 1});
|
||||||
|
cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image,
|
||||||
|
vk::ImageLayout::eTransferDstOptimal, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::ImageMemoryBarrier2 toShader;
|
||||||
|
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
||||||
|
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
||||||
|
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
|
||||||
|
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
||||||
|
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
|
||||||
|
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
||||||
|
.setImage(m_Image)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, m_MipLevels, 0, 1});
|
||||||
|
cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
|
||||||
|
|
||||||
|
cmd.end();
|
||||||
|
|
||||||
|
// 5. 提交并等待,清理 staging
|
||||||
|
vk::Fence fence = m_Device.GetDevice().createFence({});
|
||||||
|
m_Device.GetGraphicsQueue().submit(
|
||||||
|
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
|
||||||
|
if (m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
|
||||||
|
vk::Result::eSuccess) {
|
||||||
|
ENGINE_ERROR("DDS upload: waitForFences failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Device.GetDevice().destroyFence(fence);
|
||||||
|
m_Device.GetDevice().destroyCommandPool(pool);
|
||||||
|
m_Allocator.Destroy(stagingAlloc);
|
||||||
|
|
||||||
|
m_TextureBytes = total;
|
||||||
|
STATS.AddTexture(m_TextureBytes);
|
||||||
|
}
|
||||||
|
|
||||||
Texture::~Texture()
|
Texture::~Texture()
|
||||||
{
|
{
|
||||||
const auto& vkDevice = m_Device.GetDevice();
|
const auto& vkDevice = m_Device.GetDevice();
|
||||||
|
|||||||
Reference in New Issue
Block a user