Formalize cubemap skybox pipeline across backends

This commit is contained in:
2026-04-06 01:37:04 +08:00
parent 66a6818b89
commit f014ae6e6f
24 changed files with 549 additions and 196 deletions

View File

@@ -48,6 +48,9 @@ target_compile_definitions(rendering_integration_skybox_scene PRIVATE
)
add_custom_command(TARGET rendering_integration_skybox_scene POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Res
$<TARGET_FILE_DIR:rendering_integration_skybox_scene>/Res
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/integration/compare_ppm.py
$<TARGET_FILE_DIR:rendering_integration_skybox_scene>/

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,11 @@
Skybox source:
Poly Haven - "farm_field_puresky"
Page: https://polyhaven.com/a/farm_field_puresky
Direct download used for preprocessing:
https://dl.polyhaven.org/file/ph-assets/HDRIs/extra/Tonemapped%20JPG/farm_field_puresky.jpg
License:
CC0 1.0 Universal (public domain), as provided by Poly Haven.
Repository note:
The file committed here is a resized derivative used only for automated rendering tests.

View File

@@ -1,3 +1,6 @@
#define NOMINMAX
#include <windows.h>
#include <gtest/gtest.h>
#include "../RenderingIntegrationMain.h"
@@ -19,12 +22,16 @@
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <XCEngine/Resources/Texture/Texture.h>
#include <XCEngine/Resources/Texture/TextureImportSettings.h>
#include <XCEngine/Resources/Texture/TextureLoader.h>
#include <XCEngine/RHI/RHITexture.h>
#include <XCEngine/Scene/Scene.h>
#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h"
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <memory>
#include <vector>
@@ -42,78 +49,134 @@ constexpr const char* kOpenGLScreenshot = "skybox_scene_opengl.ppm";
constexpr const char* kVulkanScreenshot = "skybox_scene_vulkan.ppm";
constexpr uint32_t kFrameWidth = 1280;
constexpr uint32_t kFrameHeight = 720;
constexpr uint32_t kSkyboxFaceSize = 256;
constexpr float kPi = 3.14159265358979323846f;
Texture* CreatePanoramicSkyboxTexture() {
constexpr uint32_t kTextureWidth = 256;
constexpr uint32_t kTextureHeight = 128;
std::filesystem::path GetExecutableDirectory() {
char exePath[MAX_PATH] = {};
const DWORD length = GetModuleFileNameA(nullptr, exePath, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return std::filesystem::current_path();
}
return std::filesystem::path(exePath).parent_path();
}
std::filesystem::path ResolveRuntimePath(const char* relativePath) {
return GetExecutableDirectory() / relativePath;
}
Vector3 ComputeCubemapDirection(uint32_t faceIndex, float s, float t) {
switch (faceIndex) {
case 0:
return Vector3(1.0f, -t, -s).Normalized();
case 1:
return Vector3(-1.0f, -t, s).Normalized();
case 2:
return Vector3(s, 1.0f, t).Normalized();
case 3:
return Vector3(s, -1.0f, -t).Normalized();
case 4:
return Vector3(s, -t, 1.0f).Normalized();
case 5:
default:
return Vector3(-s, -t, -1.0f).Normalized();
}
}
void SamplePanoramaNearest(
const Texture& panorama,
float u,
float v,
unsigned char outRgba[4]) {
const uint32_t width = panorama.GetWidth();
const uint32_t height = panorama.GetHeight();
const auto* pixels = static_cast<const unsigned char*>(panorama.GetPixelData());
if (pixels == nullptr || width == 0 || height == 0) {
outRgba[0] = 255;
outRgba[1] = 255;
outRgba[2] = 255;
outRgba[3] = 255;
return;
}
u = u - std::floor(u);
v = std::clamp(v, 0.0f, 1.0f);
const uint32_t x = std::min<uint32_t>(
static_cast<uint32_t>(u * static_cast<float>(width - 1) + 0.5f),
width - 1);
const uint32_t y = std::min<uint32_t>(
static_cast<uint32_t>(v * static_cast<float>(height - 1) + 0.5f),
height - 1);
const size_t pixelIndex = (static_cast<size_t>(y) * static_cast<size_t>(width) + x) * 4u;
outRgba[0] = pixels[pixelIndex + 0];
outRgba[1] = pixels[pixelIndex + 1];
outRgba[2] = pixels[pixelIndex + 2];
outRgba[3] = pixels[pixelIndex + 3];
}
Texture* CreateCubemapSkyboxTexture(const std::filesystem::path& panoramaPath) {
TextureLoader loader;
TextureImportSettings settings;
settings.SetSRGB(true);
LoadResult loadResult = loader.Load(panoramaPath.string().c_str(), &settings);
if (!loadResult || loadResult.resource == nullptr) {
return nullptr;
}
std::unique_ptr<Texture> panorama(static_cast<Texture*>(loadResult.resource));
if (!panorama || panorama->GetPixelData() == nullptr || panorama->GetWidth() == 0 || panorama->GetHeight() == 0) {
return nullptr;
}
auto* texture = new Texture();
IResource::ConstructParams params = {};
params.name = "SkyboxPanoramicTexture";
params.path = "Tests/Rendering/SkyboxScene/skybox_panorama.texture";
params.name = "SkyboxCubemapTexture";
params.path = "Tests/Rendering/SkyboxScene/skybox_cubemap.texture";
params.guid = ResourceGUID::Generate(params.path);
texture->Initialize(params);
std::vector<float> pixels(static_cast<size_t>(kTextureWidth) * static_cast<size_t>(kTextureHeight) * 4u, 1.0f);
for (uint32_t y = 0; y < kTextureHeight; ++y) {
for (uint32_t x = 0; x < kTextureWidth; ++x) {
const float u = static_cast<float>(x) / static_cast<float>(kTextureWidth - 1u);
const float v = static_cast<float>(y) / static_cast<float>(kTextureHeight - 1u);
std::vector<unsigned char> pixels(
static_cast<size_t>(kSkyboxFaceSize) * static_cast<size_t>(kSkyboxFaceSize) * 4u * 6u,
255u);
for (uint32_t faceIndex = 0; faceIndex < 6u; ++faceIndex) {
for (uint32_t y = 0; y < kSkyboxFaceSize; ++y) {
for (uint32_t x = 0; x < kSkyboxFaceSize; ++x) {
const float s = (2.0f * (static_cast<float>(x) + 0.5f) / static_cast<float>(kSkyboxFaceSize)) - 1.0f;
const float t = (2.0f * (static_cast<float>(y) + 0.5f) / static_cast<float>(kSkyboxFaceSize)) - 1.0f;
const Vector3 direction = ComputeCubemapDirection(faceIndex, s, t);
Vector3 color = Vector3(0.05f, 0.08f, 0.14f);
if (v < 0.42f) {
const float t = v / 0.42f;
color = Vector3(
0.08f + 0.42f * (1.0f - t),
0.16f + 0.50f * (1.0f - t),
0.34f + 0.58f * (1.0f - t));
} else if (v < 0.58f) {
const float t = (v - 0.42f) / 0.16f;
color = Vector3(
0.92f - 0.24f * t,
0.74f - 0.18f * t,
0.36f - 0.12f * t);
} else {
const float t = (v - 0.58f) / 0.42f;
color = Vector3(
0.12f - 0.06f * t,
0.10f - 0.05f * t,
0.09f - 0.04f * t);
}
const float u = std::atan2(direction.z, direction.x) / (2.0f * kPi) + 0.5f;
const float v = std::acos(std::clamp(direction.y, -1.0f, 1.0f)) / kPi;
const float sunDx = u - 0.22f;
const float sunDy = v - 0.34f;
const float sun = std::exp(-(sunDx * sunDx + sunDy * sunDy) * 1450.0f) * 4.0f;
color += Vector3(1.0f, 0.82f, 0.58f) * sun;
unsigned char rgba[4] = {};
SamplePanoramaNearest(*panorama, u, v, rgba);
if (u > 0.62f && u < 0.68f && v > 0.46f && v < 0.82f) {
color = Vector3(0.10f, 0.52f, 0.96f);
const size_t pixelIndex =
((static_cast<size_t>(faceIndex) * static_cast<size_t>(kSkyboxFaceSize) *
static_cast<size_t>(kSkyboxFaceSize)) +
(static_cast<size_t>(y) * static_cast<size_t>(kSkyboxFaceSize) + x)) * 4u;
pixels[pixelIndex + 0] = rgba[0];
pixels[pixelIndex + 1] = rgba[1];
pixels[pixelIndex + 2] = rgba[2];
pixels[pixelIndex + 3] = 255u;
}
if (u > 0.34f && u < 0.39f && v > 0.50f && v < 0.86f) {
color = Vector3(0.88f, 0.24f, 0.19f);
}
if (u > 0.78f && u < 0.92f && v > 0.60f && v < 0.78f) {
color = Vector3(0.22f, 0.70f, 0.28f);
}
const size_t pixelIndex = (static_cast<size_t>(y) * static_cast<size_t>(kTextureWidth) + x) * 4u;
pixels[pixelIndex + 0] = color.x;
pixels[pixelIndex + 1] = color.y;
pixels[pixelIndex + 2] = color.z;
pixels[pixelIndex + 3] = 1.0f;
}
}
const size_t pixelDataSize = pixels.size() * sizeof(float);
const size_t pixelDataSize = pixels.size();
if (!texture->Create(
kTextureWidth,
kTextureHeight,
kSkyboxFaceSize,
kSkyboxFaceSize,
1,
1,
XCEngine::Resources::TextureType::Texture2D,
XCEngine::Resources::TextureFormat::RGBA32_FLOAT,
XCEngine::Resources::TextureType::TextureCube,
panorama->GetFormat(),
pixels.data(),
pixelDataSize)) {
pixelDataSize,
6)) {
delete texture;
return nullptr;
}
@@ -184,14 +247,14 @@ Material* CreateMaterial(
Material* CreateSkyboxMaterial(Texture* texture) {
auto* material = new Material();
IResource::ConstructParams params = {};
params.name = "SkyboxPanoramicMaterial";
params.path = "Tests/Rendering/SkyboxScene/skybox_panorama.mat";
params.name = "SkyboxCubemapMaterial";
params.path = "Tests/Rendering/SkyboxScene/skybox_cubemap.mat";
params.guid = ResourceGUID::Generate(params.path);
material->Initialize(params);
material->SetShader(ResourceManager::Get().Load<Shader>(GetBuiltinSkyboxShaderPath()));
material->SetTexture("_MainTex", ResourceHandle<Texture>(texture));
material->SetTexture("_Tex", ResourceHandle<Texture>(texture));
material->SetFloat4("_Tint", Vector4(1.0f, 1.0f, 1.0f, 1.0f));
material->SetFloat("_Exposure", 1.05f);
material->SetFloat("_Exposure", 1.0f);
material->SetFloat("_Rotation", 0.0f);
return material;
}
@@ -341,7 +404,7 @@ void SkyboxSceneTest::BuildScene() {
camera->SetFarClipPlane(20.0f);
camera->SetClearColor(XCEngine::Math::Color(0.01f, 0.01f, 0.01f, 1.0f));
camera->SetSkyboxEnabled(true);
Texture* skyboxTexture = CreatePanoramicSkyboxTexture();
Texture* skyboxTexture = CreateCubemapSkyboxTexture(ResolveRuntimePath("Res/skyboxes/farm_field_puresky_1024.jpg"));
ASSERT_NE(skyboxTexture, nullptr);
Material* skyboxMaterial = CreateSkyboxMaterial(skyboxTexture);
ASSERT_NE(skyboxMaterial, nullptr);