Skip to content

Commit 1649a36

Browse files
bkaradzicCopilot
andcommitted
NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics
loadCubeTexture now accepts a single self-contained cubemap container (all six faces + mips), decoded via bimg::imageParse, and uploads sides 0-5 x mips. ComputeCubeSphericalPolynomial derives the diffuse-IBL spherical harmonics from the top-mip faces (port of CubeMapToSphericalPolynomialTools) and returns the polynomial coefficients to JS. This is done natively because the WebGL upload and cube-readback paths are unimplemented on native and .dds stores no SH. Re-enables 6 prefiltered-environment PBR validation tests. Pairs with BabylonJS/Babylon.js#18560 (native createCubeTexture dispatch for single-URL containers). Depends on a babylonjs dependency bump including it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b6ecf80 commit 1649a36

2 files changed

Lines changed: 274 additions & 12 deletions

File tree

Apps/Playground/Scripts/config.json

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -858,8 +858,6 @@
858858
{
859859
"title": "NMEGLTF",
860860
"playgroundId": "#WGZLGJ#10320",
861-
"excludeFromAutomaticTesting": true,
862-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
863861
"referenceImage": "nmegltf.png"
864862
},
865863
{
@@ -1056,15 +1054,11 @@
10561054
{
10571055
"title": "Anisotropic",
10581056
"playgroundId": "#MAXCNU#1",
1059-
"excludeFromAutomaticTesting": true,
1060-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
10611057
"referenceImage": "anisotropic.png"
10621058
},
10631059
{
10641060
"title": "Clear Coat",
10651061
"playgroundId": "#YACNQS#2",
1066-
"excludeFromAutomaticTesting": true,
1067-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
10681062
"referenceImage": "clearCoat.png"
10691063
},
10701064
{
@@ -1573,22 +1567,16 @@
15731567
{
15741568
"title": "PBRMetallicRoughnessMaterial",
15751569
"playgroundId": "#2FDQT5#13",
1576-
"excludeFromAutomaticTesting": true,
1577-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
15781570
"referenceImage": "PBRMetallicRoughnessMaterial.png"
15791571
},
15801572
{
15811573
"title": "PBRSpecularGlossinessMaterial",
15821574
"playgroundId": "#Z1VL3V#4",
1583-
"excludeFromAutomaticTesting": true,
1584-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
15851575
"referenceImage": "PBRSpecularGlossinessMaterial.png"
15861576
},
15871577
{
15881578
"title": "PBR",
15891579
"playgroundId": "#LCA0Q4#27",
1590-
"excludeFromAutomaticTesting": true,
1591-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
15921580
"referenceImage": "pbr.png"
15931581
},
15941582
{

Plugins/NativeEngine/Source/NativeEngine.cpp

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <cmath>
3232
#include <limits>
3333
#include <optional>
34+
#include <array>
3435

3536
#if defined(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES) && defined(WEBP)
3637
#include <webp/decode.h>
@@ -429,6 +430,241 @@ namespace Babylon
429430
}
430431
}
431432
}
433+
// Parse a single self-contained cubemap container (e.g. .dds / .ktx /
434+
// .ktx2) that already holds all six faces and their mip chain. bimg
435+
// decodes these natively, so there is no need to split into six images
436+
// on the JS side. Unlike ParseImage (which targets single-face 2D images
437+
// and asserts !m_cubeMap), this keeps the container as-is.
438+
bimg::ImageContainer* ParseCubeImage(bx::AllocatorI& allocator, gsl::span<uint8_t> data)
439+
{
440+
bx::ErrorIgnore parseError;
441+
bimg::ImageContainer* image{bimg::imageParse(&allocator, data.data(), static_cast<uint32_t>(data.size()), bimg::TextureFormat::Count, &parseError)};
442+
if (image == nullptr)
443+
{
444+
throw std::runtime_error{"Failed to parse cube image."};
445+
}
446+
447+
if (!image->m_cubeMap)
448+
{
449+
bimg::imageFree(image);
450+
throw std::runtime_error{"Image is not a cubemap."};
451+
}
452+
453+
return image;
454+
}
455+
456+
// Port of Babylon.js CubeMapToSphericalPolynomialTools.ConvertCubeMapToSphericalPolynomial.
457+
// Prefiltered .dds environments need diffuse-IBL spherical harmonics, which Babylon's WebGL
458+
// path computes on the CPU from the top-mip faces. The native engine cannot read cube faces
459+
// back from the GPU (_readTexturePixels throws for cube faces), so we compute the harmonics
460+
// here from the bimg-decoded top mip. Returns the 9x3 polynomial coefficients in
461+
// SphericalPolynomial.FromArray order: x, y, z, xx, yy, zz, yz, zx, xy.
462+
std::array<float, 27> ComputeCubeSphericalPolynomial(bx::AllocatorI& allocator, bimg::ImageContainer* image)
463+
{
464+
std::array<float, 27> result{};
465+
466+
bimg::ImageContainer* f32{bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *image, false)};
467+
if (f32 == nullptr)
468+
{
469+
return result;
470+
}
471+
472+
const uint32_t size{f32->m_width};
473+
constexpr double pi{3.14159265358979323846};
474+
475+
// Face orientations matching Babylon's _FileFaces, indexed by bimg cube side order
476+
// (+X, -X, +Y, -Y, +Z, -Z): worldAxisForNormal, worldAxisForFileX, worldAxisForFileY.
477+
struct FaceAxes
478+
{
479+
double n[3];
480+
double fx[3];
481+
double fy[3];
482+
};
483+
static const FaceAxes faces[6] = {
484+
{{1, 0, 0}, {0, 0, -1}, {0, -1, 0}}, // +X right
485+
{{-1, 0, 0}, {0, 0, 1}, {0, -1, 0}}, // -X left
486+
{{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, // +Y up
487+
{{0, -1, 0}, {1, 0, 0}, {0, 0, -1}}, // -Y down
488+
{{0, 0, 1}, {1, 0, 0}, {0, -1, 0}}, // +Z front
489+
{{0, 0, -1}, {-1, 0, 0}, {0, -1, 0}}, // -Z back
490+
};
491+
492+
const double shConst[9] = {
493+
std::sqrt(1.0 / (4.0 * pi)),
494+
-std::sqrt(3.0 / (4.0 * pi)),
495+
std::sqrt(3.0 / (4.0 * pi)),
496+
-std::sqrt(3.0 / (4.0 * pi)),
497+
std::sqrt(15.0 / (4.0 * pi)),
498+
-std::sqrt(15.0 / (4.0 * pi)),
499+
std::sqrt(5.0 / (16.0 * pi)),
500+
-std::sqrt(15.0 / (4.0 * pi)),
501+
std::sqrt(15.0 / (16.0 * pi)),
502+
};
503+
const double cosKernel[9] = {pi, 2.0 * pi / 3.0, 2.0 * pi / 3.0, 2.0 * pi / 3.0, pi / 4.0, pi / 4.0, pi / 4.0, pi / 4.0, pi / 4.0};
504+
505+
const auto areaElement = [](double x, double y) { return std::atan2(x * y, std::sqrt(x * x + y * y + 1.0)); };
506+
507+
double sh[9][3] = {};
508+
double totalSolidAngle{0.0};
509+
510+
const double du{2.0 / static_cast<double>(size)};
511+
const double halfTexel{0.5 * du};
512+
const double minUV{halfTexel - 1.0};
513+
const double maxHdri{4096.0};
514+
515+
for (uint16_t side = 0; side < 6; ++side)
516+
{
517+
bimg::ImageMip mip{};
518+
if (!bimg::imageGetRawData(*f32, side, 0, f32->m_data, f32->m_size, mip))
519+
{
520+
continue;
521+
}
522+
523+
const float* data{reinterpret_cast<const float*>(mip.m_data)};
524+
const FaceAxes& f{faces[side]};
525+
526+
double v{minUV};
527+
for (uint32_t y = 0; y < size; ++y)
528+
{
529+
double u{minUV};
530+
for (uint32_t x = 0; x < size; ++x)
531+
{
532+
double dir[3] = {
533+
f.fx[0] * u + f.fy[0] * v + f.n[0],
534+
f.fx[1] * u + f.fy[1] * v + f.n[1],
535+
f.fx[2] * u + f.fy[2] * v + f.n[2],
536+
};
537+
const double len{std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2])};
538+
dir[0] /= len;
539+
dir[1] /= len;
540+
dir[2] /= len;
541+
542+
const double deltaSolidAngle{
543+
areaElement(u - halfTexel, v - halfTexel) -
544+
areaElement(u - halfTexel, v + halfTexel) -
545+
areaElement(u + halfTexel, v - halfTexel) +
546+
areaElement(u + halfTexel, v + halfTexel)};
547+
548+
const size_t idx{(static_cast<size_t>(y) * size + x) * 4};
549+
double rgb[3] = {data[idx + 0], data[idx + 1], data[idx + 2]};
550+
for (int c = 0; c < 3; ++c)
551+
{
552+
if (std::isnan(rgb[c]))
553+
{
554+
rgb[c] = 0.0;
555+
}
556+
rgb[c] = rgb[c] < 0.0 ? 0.0 : (rgb[c] > maxHdri ? maxHdri : rgb[c]);
557+
}
558+
559+
const double trig[9] = {
560+
1.0,
561+
dir[1],
562+
dir[2],
563+
dir[0],
564+
dir[0] * dir[1],
565+
dir[1] * dir[2],
566+
3.0 * dir[2] * dir[2] - 1.0,
567+
dir[0] * dir[2],
568+
dir[0] * dir[0] - dir[1] * dir[1],
569+
};
570+
for (int lm = 0; lm < 9; ++lm)
571+
{
572+
const double basis{shConst[lm] * trig[lm] * deltaSolidAngle};
573+
sh[lm][0] += rgb[0] * basis;
574+
sh[lm][1] += rgb[1] * basis;
575+
sh[lm][2] += rgb[2] * basis;
576+
}
577+
totalSolidAngle += deltaSolidAngle;
578+
u += du;
579+
}
580+
v += du;
581+
}
582+
}
583+
584+
bimg::imageFree(f32);
585+
586+
if (totalSolidAngle <= 0.0)
587+
{
588+
return result;
589+
}
590+
591+
// scaleInPlace(correction) + convertIncidentRadianceToIrradiance + convertIrradianceToLambertianRadiance.
592+
const double correction{(4.0 * pi) / totalSolidAngle};
593+
for (int lm = 0; lm < 9; ++lm)
594+
{
595+
const double scale{correction * cosKernel[lm] / pi};
596+
sh[lm][0] *= scale;
597+
sh[lm][1] *= scale;
598+
sh[lm][2] *= scale;
599+
}
600+
601+
// SphericalPolynomial.FromHarmonics (updateFromHarmonics then *1/pi).
602+
for (int c = 0; c < 3; ++c)
603+
{
604+
const double l00{sh[0][c]}, l1_1{sh[1][c]}, l10{sh[2][c]}, l11{sh[3][c]};
605+
const double l2_2{sh[4][c]}, l2_1{sh[5][c]}, l20{sh[6][c]}, l21{sh[7][c]}, l22{sh[8][c]};
606+
const double invPi{1.0 / pi};
607+
result[0 * 3 + c] = static_cast<float>(-1.02333 * l11 * invPi); // x
608+
result[1 * 3 + c] = static_cast<float>(-1.02333 * l1_1 * invPi); // y
609+
result[2 * 3 + c] = static_cast<float>(1.02333 * l10 * invPi); // z
610+
result[3 * 3 + c] = static_cast<float>((0.886277 * l00 - 0.247708 * l20 + 0.429043 * l22) * invPi); // xx
611+
result[4 * 3 + c] = static_cast<float>((0.886277 * l00 - 0.247708 * l20 - 0.429043 * l22) * invPi); // yy
612+
result[5 * 3 + c] = static_cast<float>((0.886277 * l00 + 0.495417 * l20) * invPi); // zz
613+
result[6 * 3 + c] = static_cast<float>(-0.858086 * l2_1 * invPi); // yz
614+
result[7 * 3 + c] = static_cast<float>(-0.858086 * l21 * invPi); // zx
615+
result[8 * 3 + c] = static_cast<float>(0.858086 * l2_2 * invPi); // xy
616+
}
617+
618+
return result;
619+
}
620+
621+
void LoadCubeTextureFromContainer(Graphics::Texture* texture, bimg::ImageContainer* image, bool srgb)
622+
{
623+
assert(image->m_cubeMap);
624+
assert(image->m_width == image->m_height);
625+
const uint32_t size{image->m_width};
626+
627+
if (texture->IsValid())
628+
{
629+
if (texture->Width() != size || texture->Height() != size)
630+
{
631+
bimg::imageFree(image);
632+
throw std::runtime_error{"Cannot update texture from image of different size"};
633+
}
634+
}
635+
else
636+
{
637+
const bool hasMips{image->m_numMips > 1};
638+
const bgfx::TextureFormat::Enum format{Cast(image->m_format)};
639+
const uint64_t flags{srgb ? BGFX_TEXTURE_SRGB : BGFX_TEXTURE_NONE};
640+
texture->CreateCube(static_cast<uint16_t>(size), hasMips, 1, format, flags);
641+
}
642+
643+
// Every (side, mip) view points into the single container's backing
644+
// store, so the allocation is released exactly once, after bgfx has
645+
// consumed the final upload.
646+
const uint8_t numMips{static_cast<uint8_t>(image->m_numMips)};
647+
for (uint8_t side = 0; side < 6; ++side)
648+
{
649+
for (uint8_t mip = 0; mip < numMips; ++mip)
650+
{
651+
bimg::ImageMip imageMip{};
652+
if (bimg::imageGetRawData(*image, side, mip, image->m_data, image->m_size, imageMip))
653+
{
654+
bgfx::ReleaseFn releaseFn{};
655+
if (side == 5 && mip == numMips - 1)
656+
{
657+
releaseFn = [](void*, void* userData) {
658+
bimg::imageFree(static_cast<bimg::ImageContainer*>(userData));
659+
};
660+
}
661+
662+
const bgfx::Memory* mem{bgfx::makeRef(imageMip.m_data, imageMip.m_size, releaseFn, image)};
663+
texture->UpdateCube(0, side, mip, 0, 0, static_cast<uint16_t>(imageMip.m_width), static_cast<uint16_t>(imageMip.m_height), mem);
664+
}
665+
}
666+
}
667+
}
432668
#endif // BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES
433669

434670
auto RenderTargetSamplesToBgfxMsaaFlag(uint32_t renderTargetSamples)
@@ -1501,6 +1737,44 @@ namespace Babylon
15011737
const auto onSuccess{info[5].As<Napi::Function>()};
15021738
const auto onError{info[6].As<Napi::Function>()};
15031739

1740+
// A single buffer means a self-contained cubemap container (.dds / .ktx /
1741+
// .ktx2) that already holds all six faces and their mip chain; hand it to
1742+
// bimg directly instead of expecting six pre-split face images.
1743+
if (data.Length() == 1)
1744+
{
1745+
const auto typedArray{data[0u].As<Napi::TypedArray>()};
1746+
const auto dataSpan{gsl::make_span(static_cast<uint8_t*>(typedArray.ArrayBuffer().Data()) + typedArray.ByteOffset(), typedArray.ByteLength())};
1747+
auto dataRef{Napi::Persistent(typedArray)};
1748+
arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, [dataSpan]() {
1749+
return ParseCubeImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan);
1750+
})
1751+
.then(arcana::inline_scheduler, *m_cancellationSource, [texture, srgb, cancellationSource{m_cancellationSource}](bimg::ImageContainer* image) {
1752+
// Compute the spherical harmonics from the decoded top mip before the upload
1753+
// hands the container's memory to bgfx.
1754+
auto sphericalPolynomial = ComputeCubeSphericalPolynomial(Graphics::DeviceContext::GetDefaultAllocator(), image);
1755+
LoadCubeTextureFromContainer(texture, image, srgb);
1756+
return sphericalPolynomial;
1757+
})
1758+
.then(m_runtimeScheduler, *m_cancellationSource, [dataRef{std::move(dataRef)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected<std::array<float, 27>, std::exception_ptr> result) {
1759+
if (result.has_error())
1760+
{
1761+
onErrorRef.Call({});
1762+
}
1763+
else
1764+
{
1765+
const auto& sphericalPolynomial{result.value()};
1766+
auto array{Napi::Float32Array::New(onSuccessRef.Env(), sphericalPolynomial.size())};
1767+
float* dst{array.Data()};
1768+
for (size_t i = 0; i < sphericalPolynomial.size(); ++i)
1769+
{
1770+
dst[i] = sphericalPolynomial[i];
1771+
}
1772+
onSuccessRef.Call({array});
1773+
}
1774+
});
1775+
return;
1776+
}
1777+
15041778
std::array<Napi::Reference<Napi::TypedArray>, 6> dataRefs;
15051779
std::array<arcana::task<bimg::ImageContainer*, std::exception_ptr>, 6> tasks;
15061780
for (uint32_t face = 0; face < data.Length(); face++)

0 commit comments

Comments
 (0)