|
文礼:专栏文章目录
上一篇我们手动对贴图的格式进行了转换,并且比较了不同格式之间在视觉呈现上的差异。
从压缩比上来说,BC1的压缩比最好,为1/6。BC4的压缩比最差,为1/2。那么很自然地就会想到,如果BC1的品质可以接受,我们能不能将多张单通道图组合在一起,然后再用BC1进行压缩呢?
是的,实际上业界就是这么做的。事实上,我们可以用这个技巧,将原本的5张图(Albedo、Normal、Metal、Rough、AO)合并到3张图当中去。如下:
按通道归并贴图
当然,这种合并方式并非是唯一的,每个游戏都应该根据其自身特点去进行选择。比如是否使用PBR渲染流程,是否对于法线的精度特别敏感,是否需要支持透明半透明物体,等等。
细心的读者可能会留意到在上图的方案当中,缺少了Norm.Z,也就是法线的Z分量。这是因为我这里准备把第三张图压缩成为BC5,也就是双通道图。当然,BC5的压缩比同样只有1/2,相比前一篇的BC1或者BC7都是要大不少的。但是通过前一篇我们知道,对于法线的压缩,在有高反射镜面物体的时候,使用BC1或者BC7容易出现瑕疵。
另外,之前也提到过,法线因为是单位向量,所以如果知道其任意两个分量,我们就可以计算得出第三个分量。因此,我们可以少存储一个分量。这就已经实现了2/3的压缩比。在此基础之上,我们对其进行BC5的压缩,又得到1/2的压缩比。因此总体上我们就实现了1/3的压缩比。虽然不及BC1的1/6、BC7的1/4,但是得到了更好的精度。
对于一些特别苛刻的情况,我们还可以选择不压缩,直接使用双通道法线数据。这样也比原本的3通道节省。
将原本美术制作的贴图,按照事先设计好的方式归并,并进行特定平台相关的压缩,这个过程被称为“烘焙”。
之前我们实现场景图的时候,定义了一个名为SceneObjectMaterial的结构。它对应的是我们的场景描述文件当中的Material数据结构,其中包含了某项材质所需的贴图清单:
Material $material1
{
Name {string {"Red Bricks"}}
Color (attrib = "diffuse") {float[3] {{0.640000066757203, 0.640000066757203, 0.640000066757203}}}
Color (attrib = "specular") {float[3] {{0.5, 0.5, 0.5}}}
Param (attrib = "specular_power") {float {50}}
Texture (attrib = "diffuse")
{
string {"Textures/redbricks2b-albedo.png"}
}
Texture (attrib = "normal")
{
string {"Textures/redbricks2b-normal.png"}
}
Texture (attrib = "metallic")
{
string {"Textures/redbricks2b-metalness.png"}
}
Texture (attrib = "roughness")
{
string {"Textures/redbricks2b-rough.png"}
}
Texture (attrib = "ao")
{
string {"Textures/redbricks2b-ao.png"}
}
}那么,对我们来说,材质的烘焙就是先读入这个清单,然后按照事先规划好的格式逐项去转换(归并、压缩)这些数据的格式,并把结果以某种形式进行存储。
作为第一步,我们首先只是简单地将材质当中的5张图归并为3张图,并各自进行BC压缩。
现在,让我们在Utility目录下面新建一个MaterialBaker.cpp,来实现这个烘焙过程。
Utility/MaterialBacker.cpp
#include <algorithm>
#include <cmath>
#include <fstream>
#include <future>
#include <iostream>
#include <string>
#include <vector>
#include &#34;AssetLoader.hpp&#34;
#include &#34;BaseApplication.hpp&#34;
#include &#34;PVR.hpp&#34;
#include &#34;SceneManager.hpp&#34;
#include &#34;ispc_texcomp.h&#34;
using namespace My;
using namespace std;
template <typename T>
static ostream& operator<<(ostream& out,
unordered_map<string, shared_ptr<T>> map) {
for (auto p : map) {
out << *p.second << endl;
}
return out;
}
void save_as_tga(const rgba_surface& surface, int32_t channels,
const std::string&& filename) {
assert(filename != &#34;&#34;);
// must end in .tga
FILE* file = fopen(filename.c_str(), &#34;wb&#34;);
// misc header information
for (int i = 0; i < 18; i++) {
if (i == 2)
fprintf(file, &#34;%c&#34;, 2);
else if (i == 12)
fprintf(file, &#34;%c&#34;, surface.width % 256);
else if (i == 13)
fprintf(file, &#34;%c&#34;, surface.width / 256);
else if (i == 14)
fprintf(file, &#34;%c&#34;, surface.height % 256);
else if (i == 15)
fprintf(file, &#34;%c&#34;, surface.height / 256);
else if (i == 16)
fprintf(file, &#34;%c&#34;, 32);
else if (i == 17)
fprintf(
file, &#34;%c&#34;,
0x28); // Bit 5: screen origin bit, Bit 3 - 0 alpha bit depth
else
fprintf(file, &#34;%c&#34;, 0);
}
// the data
for (int32_t y = 0; y < surface.height; y++) {
for (int32_t x = 0; x < surface.width; x++) {
// note reversed order: b, g, r, a
fprintf(file, &#34;%c&#34;,
(channels > 2)
? *(surface.ptr + y * surface.stride + x * channels + 2)
: &#39;\0&#39;);
fprintf(file, &#34;%c&#34;,
(channels > 1)
? *(surface.ptr + y * surface.stride + x * channels + 1)
: &#39;\0&#39;);
fprintf(file, &#34;%c&#34;,
(channels > 0)
? *(surface.ptr + y * surface.stride + x * channels + 0)
: &#39;\0&#39;);
fprintf(file, &#34;%c&#34;,
(channels > 3)
? *(surface.ptr + y * surface.stride + x * channels + 3)
: &#39;\1&#39;);
}
}
fclose(file);
}
void save_as_pvr(const rgba_surface& surface, const std::string&& filename,
PVR::PixelFormat compress_format) {
size_t compressed_size = 0;
switch (compress_format) {
case PVR::PixelFormat::BC1:
case PVR::PixelFormat::BC4:
compressed_size = (ALIGN(surface.height, 4) >> 2) *
(ALIGN(surface.width, 4) >> 2) * 8;
break;
case PVR::PixelFormat::BC3:
case PVR::PixelFormat::BC5:
case PVR::PixelFormat::BC6H:
case PVR::PixelFormat::BC7:
compressed_size = (ALIGN(surface.height, 4) >> 2) *
(ALIGN(surface.width, 4) >> 2) * 16;
break;
default:
assert(0);
}
std::vector<uint8_t> _dst_buf(compressed_size);
switch (compress_format) {
case PVR::PixelFormat::BC1:
CompressBlocksBC1(&surface, _dst_buf.data());
break;
case PVR::PixelFormat::BC3:
CompressBlocksBC3(&surface, _dst_buf.data());
break;
case PVR::PixelFormat::BC4:
CompressBlocksBC4(&surface, _dst_buf.data());
break;
case PVR::PixelFormat::BC5:
CompressBlocksBC5(&surface, _dst_buf.data());
break;
case PVR::PixelFormat::BC6H: {
bc6h_enc_settings settings;
GetProfile_bc6h_basic(&settings);
CompressBlocksBC6H(&surface, _dst_buf.data(), &settings);
} break;
case PVR::PixelFormat::BC7: {
bc7_enc_settings settings;
GetProfile_alpha_basic(&settings);
CompressBlocksBC7(&surface, _dst_buf.data(), &settings);
} break;
default:
assert(0);
}
PVR::File compressedFile;
compressedFile.header.flags = PVR::Flags::NoFlag;
compressedFile.header.channel_type = PVR::ChannelType::Unsigned_Byte;
compressedFile.header.height = ALIGN(surface.height, 4);
compressedFile.header.width = ALIGN(surface.width, 4);
compressedFile.header.depth = 1;
compressedFile.header.num_faces = 1;
compressedFile.header.num_surfaces = 1;
compressedFile.header.mipmap_count = 1;
compressedFile.header.metadata_size = 0;
compressedFile.header.pixel_format = compress_format;
compressedFile.pTextureData = _dst_buf.data();
compressedFile.szTextureDataSize = compressed_size;
std::cerr << &#34;generate &#34; << filename << std::endl;
std::ofstream outputFile(filename, std::ios::binary);
outputFile << compressedFile;
outputFile.close();
std::cerr << &#34;finished &#34; << filename << std::endl;
}
int main(int argc, char** argv) {
int error = 0;
BaseApplication app;
AssetLoader assetLoader;
SceneManager sceneManager;
app.RegisterManagerModule(&assetLoader);
app.RegisterManagerModule(&sceneManager);
error = app.Initialize();
if (argc >= 2) {
sceneManager.LoadScene(argv[1]);
} else {
sceneManager.LoadScene(&#34;Materials/splash.mymaterial&#34;);
}
auto& scene = sceneManager.GetSceneForRendering();
std::vector<std::future<void>> tasks;
cerr << &#34;Baking Materials&#34; << endl;
cerr << &#34;---------------------------&#34; << endl;
for (const auto& _it : scene->Materials) {
auto pMaterial = _it.second;
if (pMaterial) {
cerr << pMaterial->GetName() << std::endl;
auto albedo = pMaterial->GetBaseColor().ValueMap;
assert(albedo);
auto albedo_texture = albedo->GetTextureImage();
assert(albedo_texture);
auto albedo_texture_width = albedo_texture->Width;
auto albedo_texture_height = albedo_texture->Height;
auto normal = pMaterial->GetNormal().ValueMap;
assert(normal);
auto normal_texture = normal->GetTextureImage();
assert(normal_texture);
auto normal_texture_width = normal_texture->Width;
auto normal_texture_height = normal_texture->Height;
auto metallic = pMaterial->GetMetallic().ValueMap;
assert(metallic);
auto metallic_texture = metallic->GetTextureImage();
assert(metallic_texture);
auto metallic_texture_width = metallic_texture->Width;
auto metallic_texture_height = metallic_texture->Height;
auto roughness = pMaterial->GetRoughness().ValueMap;
assert(roughness);
auto roughness_texture = roughness->GetTextureImage();
assert(roughness_texture);
auto roughness_texture_width = roughness_texture->Width;
auto roughness_texture_height = roughness_texture->Height;
auto ao = pMaterial->GetAO().ValueMap;
assert(ao);
auto ao_texture = ao->GetTextureImage();
assert(ao_texture);
auto ao_texture_width = ao_texture->Width;
auto ao_texture_height = ao_texture->Height;
auto max_width_1 = albedo_texture_width;
auto max_width_2 =
std::max({metallic_texture_width, roughness_texture_width,
ao_texture_width});
auto max_height_1 = albedo_texture_height;
auto max_height_2 =
std::max({metallic_texture_height, roughness_texture_height,
ao_texture_height});
/*
Now we pack the texture into following format:
+--------+--------+--------+--------+
| R | G | B | A |
+--------+--------+--------+--------+
| Albe.R | Albe.G | Albe.B | Albe.A | surf1
+--------+--------+--------+--------+
| Metal | Rough | AO | | surf2
+--------+--------+--------+--------+
| Norm.X | Norm.Y | - | - | surf3
+--------+--------+--------+--------+
*/
auto combine_textures_1 = [=]() {
// surf
rgba_surface surf;
surf.width = max_width_1;
surf.height = max_height_1;
int32_t channels = 4;
surf.stride = channels * surf.width;
std::vector<uint8_t> buf1(surf.stride * surf.height);
surf.ptr = buf1.data();
float albedo_ratio_x = (float)albedo_texture_width / surf.width;
float albedo_ratio_y =
(float)albedo_texture_height / surf.height;
for (int32_t y = 0; y < surf.height; y++) {
for (int32_t x = 0; x < surf.width; x++) {
*(surf.ptr + y * surf.stride + x * channels) =
albedo_texture->GetR(
std::floor(x * albedo_ratio_x),
std::floor(y * albedo_ratio_y));
*(surf.ptr + y * surf.stride + x * channels + 1) =
albedo_texture->GetG(
std::floor(x * albedo_ratio_x),
std::floor(y * albedo_ratio_y));
*(surf.ptr + y * surf.stride + x * channels + 2) =
albedo_texture->GetB(
std::floor(x * albedo_ratio_x),
std::floor(y * albedo_ratio_y));
*(surf.ptr + y * surf.stride + x * channels + 3) =
albedo_texture->GetA(
std::floor(x * albedo_ratio_x),
std::floor(y * albedo_ratio_y));
}
}
// Now, compress surf with BC7
auto outputFileName = pMaterial->GetName();
if (argc >= 3) {
outputFileName = argv[2];
}
outputFileName += &#34;_1&#34;;
save_as_pvr(surf, outputFileName + &#34;.pvr&#34;,
PVR::PixelFormat::BC7);
save_as_tga(surf, channels, outputFileName + &#34;.tga&#34;);
};
tasks.push_back(std::async(launch::async, combine_textures_1));
auto combine_textures_2 = [=]() {
// surf
rgba_surface surf;
surf.width = max_width_2;
surf.height = max_height_2;
int32_t channels = 4;
surf.stride = channels * surf.width;
std::vector<uint8_t> buf2(surf.stride * surf.height);
surf.ptr = buf2.data();
float metallic_ratio_x =
(float)metallic_texture_width / surf.width;
float metallic_ratio_y =
(float)metallic_texture_height / surf.height;
float roughness_ratio_x =
(float)roughness_texture_width / surf.width;
float roughness_ratio_y =
(float)roughness_texture_height / surf.height;
float ao_ratio_x = (float)ao_texture_width / surf.width;
float ao_ratio_y = (float)ao_texture_height / surf.height;
for (int32_t y = 0; y < surf.height; y++) {
for (int32_t x = 0; x < surf.width; x++) {
*(surf.ptr + y * surf.stride + x * channels) =
metallic_texture->GetX(
std::floor(x * metallic_ratio_x),
std::floor(y * metallic_ratio_y));
*(surf.ptr + y * surf.stride + x * channels + 1) =
roughness_texture->GetX(
std::floor(x * roughness_ratio_x),
std::floor(y * roughness_ratio_y));
*(surf.ptr + y * surf.stride + x * channels + 2) =
ao_texture->GetX(std::floor(x * ao_ratio_x),
std::floor(y * ao_ratio_y));
*(surf.ptr + y * surf.stride + x * channels + 3) = 0;
}
}
// Now, compress surf with BC1
auto outputFileName = pMaterial->GetName();
if (argc >= 3) {
outputFileName = argv[2];
}
outputFileName += &#34;_2&#34;;
save_as_pvr(surf, outputFileName + &#34;.pvr&#34;,
PVR::PixelFormat::BC1);
save_as_tga(surf, channels, outputFileName + &#34;.tga&#34;);
};
tasks.push_back(std::async(launch::async, combine_textures_2));
auto combine_textures_3 = [=]() {
// surf
rgba_surface surf;
int32_t channels = 2;
surf.width = normal_texture_width;
surf.height = normal_texture_height;
surf.stride = channels * surf.width;
std::vector<uint8_t> buf2(surf.stride * surf.height);
surf.ptr = buf2.data();
for (int32_t y = 0; y < surf.height; y++) {
for (int32_t x = 0; x < surf.width; x++) {
*(surf.ptr + y * surf.stride + x * channels) =
normal_texture->GetX(x, y);
*(surf.ptr + y * surf.stride + x * channels + 1) =
normal_texture->GetY(x, y);
}
}
// Now, compress surf with BC5
auto outputFileName = pMaterial->GetName();
if (argc >= 3) {
outputFileName = argv[2];
}
outputFileName += &#34;_3&#34;;
save_as_pvr(surf, outputFileName + &#34;.pvr&#34;,
PVR::PixelFormat::BC5);
save_as_tga(surf, channels, outputFileName + &#34;.tga&#34;);
};
tasks.push_back(std::async(launch::async, combine_textures_3));
}
}
for (auto& task : tasks) {
task.wait();
}
app.Finalize();
return error;
}
我们首先初始化了我们引擎的两个核心模块:AssetLoader和SeneManager。其中,AssetLoader负责从存储读取资源,而SeneManager负责将场景图构建称为内存上的结构化表达。
然后,我们使用SeneManager加载我们的材质定义。这里我自定义了一个新的文件后缀,“.mymaterial”,其实内容就是将场景文件当中的材质定义单独拿出来放在里面。就目前来说,这个文件和场景文件在格式上是共通的,因此实际上直接读取场景文件也是可以的。不过将来,我们会去除场景文件当中的材质定义,将场景和材质进行分离。这是因为在工作流上来说,做材质的和做场景的并不需要是同一批人。
然后就是从场景结构当中获取到Materials这个容器,逐一遍历其成员(材质),并获得5张贴图。
为了合并这些贴图,我们需要获取每张贴图的尺寸,并且按照合并的分组方法,计算出每个合并贴图的尺寸。因为不同的贴图可能会有不同的大小尺寸。
这里同样隐含着一个技术决策。是从一开始就定好各个贴图的大小,让美术同学统一制作呢,还是像我这样按照(每组)最大的贴图尺寸定义合并贴图的尺寸,然后缩放原始贴图呢。各种方法有各种方法的利弊,这同样是每个项目需要根据项目实际去取舍的东西。
在这些准备工作做好之后,我们就可以开始实际的合并于压缩工作了。因为每个组的工作都相对独立,我这里使用了新C++标准当中的future模版以及lambda特性,让每个工作组的工作在单独的线程当中并行执行。当然,我们也可以用新C++标准的线程池对象,或者,通过OpenMP扩展来达到同样的目的。总之就是这里做了一个并列化的实现。
最后,我们将每个合并贴图保存到我们上上篇文章当中定义的PVR图片格式当中,烘焙就完成了。我这里还写了点额外的代码,就是另外存了一份TGA。这主要是为了方便观察调试合并的结果。因为我们的PVR文件目前因为不明原因无法直接用Gimp等绘图软件或者操作系统的图片预览打开。
这也是为什么材质的烘焙往往是在引擎最终输出游戏包的时候才会被执行。毕竟,烘焙过的东西就只能吃了,很难再加工了。
接下来,我们需要修改我们的着色器,使其能够正确提取合并之后的信息,并且重构法线的Z分量。
float3 albedo =
inverse_gamma_correction(diffuseMap.Sample(samp0, texCoords).rgb);
float alpha = diffuseMap.Sample(samp0, texCoords).a;
float meta = metallicMap.Sample(samp0, texCoords).r;
float rough = metallicMap.Sample(samp0, texCoords).g;
float3 tangent_normal;
tangent_normal.xy = normalMap.Sample(samp0, texCoords).rg;
tangent_normal = tangent_normal * 2.0f - 1.00392f;
tangent_normal.z = sqrt(clamp(1.0f - tangent_normal.x * tangent_normal.x -
tangent_normal.y * tangent_normal.y, 0.0f, 1.0f));
float ambientOcc = metallicMap.Sample(samp0, texCoords).b;
这样就好了。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|