|
31 | 31 | #include <cmath> |
32 | 32 | #include <limits> |
33 | 33 | #include <optional> |
| 34 | +#include <array> |
34 | 35 |
|
35 | 36 | #ifdef BABYLON_NATIVE_NATIVEENGINE_TEST_HOOKS |
36 | 37 | #include <atomic> |
@@ -417,6 +418,241 @@ namespace Babylon |
417 | 418 | } |
418 | 419 | } |
419 | 420 | } |
| 421 | + // Parse a single self-contained cubemap container (e.g. .dds / .ktx / |
| 422 | + // .ktx2) that already holds all six faces and their mip chain. bimg |
| 423 | + // decodes these natively, so there is no need to split into six images |
| 424 | + // on the JS side. Unlike ParseImage (which targets single-face 2D images |
| 425 | + // and asserts !m_cubeMap), this keeps the container as-is. |
| 426 | + bimg::ImageContainer* ParseCubeImage(bx::AllocatorI& allocator, gsl::span<uint8_t> data) |
| 427 | + { |
| 428 | + bx::ErrorIgnore parseError; |
| 429 | + bimg::ImageContainer* image{bimg::imageParse(&allocator, data.data(), static_cast<uint32_t>(data.size()), bimg::TextureFormat::Count, &parseError)}; |
| 430 | + if (image == nullptr) |
| 431 | + { |
| 432 | + throw std::runtime_error{"Failed to parse cube image."}; |
| 433 | + } |
| 434 | + |
| 435 | + if (!image->m_cubeMap) |
| 436 | + { |
| 437 | + bimg::imageFree(image); |
| 438 | + throw std::runtime_error{"Image is not a cubemap."}; |
| 439 | + } |
| 440 | + |
| 441 | + return image; |
| 442 | + } |
| 443 | + |
| 444 | + // Port of Babylon.js CubeMapToSphericalPolynomialTools.ConvertCubeMapToSphericalPolynomial. |
| 445 | + // Prefiltered .dds environments need diffuse-IBL spherical harmonics, which Babylon's WebGL |
| 446 | + // path computes on the CPU from the top-mip faces. The native engine cannot read cube faces |
| 447 | + // back from the GPU (_readTexturePixels throws for cube faces), so we compute the harmonics |
| 448 | + // here from the bimg-decoded top mip. Returns the 9x3 polynomial coefficients in |
| 449 | + // SphericalPolynomial.FromArray order: x, y, z, xx, yy, zz, yz, zx, xy. |
| 450 | + std::array<float, 27> ComputeCubeSphericalPolynomial(bx::AllocatorI& allocator, bimg::ImageContainer* image) |
| 451 | + { |
| 452 | + std::array<float, 27> result{}; |
| 453 | + |
| 454 | + bimg::ImageContainer* f32{bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *image, false)}; |
| 455 | + if (f32 == nullptr) |
| 456 | + { |
| 457 | + return result; |
| 458 | + } |
| 459 | + |
| 460 | + const uint32_t size{f32->m_width}; |
| 461 | + constexpr double pi{3.14159265358979323846}; |
| 462 | + |
| 463 | + // Face orientations matching Babylon's _FileFaces, indexed by bimg cube side order |
| 464 | + // (+X, -X, +Y, -Y, +Z, -Z): worldAxisForNormal, worldAxisForFileX, worldAxisForFileY. |
| 465 | + struct FaceAxes |
| 466 | + { |
| 467 | + double n[3]; |
| 468 | + double fx[3]; |
| 469 | + double fy[3]; |
| 470 | + }; |
| 471 | + static const FaceAxes faces[6] = { |
| 472 | + {{1, 0, 0}, {0, 0, -1}, {0, -1, 0}}, // +X right |
| 473 | + {{-1, 0, 0}, {0, 0, 1}, {0, -1, 0}}, // -X left |
| 474 | + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, // +Y up |
| 475 | + {{0, -1, 0}, {1, 0, 0}, {0, 0, -1}}, // -Y down |
| 476 | + {{0, 0, 1}, {1, 0, 0}, {0, -1, 0}}, // +Z front |
| 477 | + {{0, 0, -1}, {-1, 0, 0}, {0, -1, 0}}, // -Z back |
| 478 | + }; |
| 479 | + |
| 480 | + const double shConst[9] = { |
| 481 | + std::sqrt(1.0 / (4.0 * pi)), |
| 482 | + -std::sqrt(3.0 / (4.0 * pi)), |
| 483 | + std::sqrt(3.0 / (4.0 * pi)), |
| 484 | + -std::sqrt(3.0 / (4.0 * pi)), |
| 485 | + std::sqrt(15.0 / (4.0 * pi)), |
| 486 | + -std::sqrt(15.0 / (4.0 * pi)), |
| 487 | + std::sqrt(5.0 / (16.0 * pi)), |
| 488 | + -std::sqrt(15.0 / (4.0 * pi)), |
| 489 | + std::sqrt(15.0 / (16.0 * pi)), |
| 490 | + }; |
| 491 | + 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}; |
| 492 | + |
| 493 | + const auto areaElement = [](double x, double y) { return std::atan2(x * y, std::sqrt(x * x + y * y + 1.0)); }; |
| 494 | + |
| 495 | + double sh[9][3] = {}; |
| 496 | + double totalSolidAngle{0.0}; |
| 497 | + |
| 498 | + const double du{2.0 / static_cast<double>(size)}; |
| 499 | + const double halfTexel{0.5 * du}; |
| 500 | + const double minUV{halfTexel - 1.0}; |
| 501 | + const double maxHdri{4096.0}; |
| 502 | + |
| 503 | + for (uint16_t side = 0; side < 6; ++side) |
| 504 | + { |
| 505 | + bimg::ImageMip mip{}; |
| 506 | + if (!bimg::imageGetRawData(*f32, side, 0, f32->m_data, f32->m_size, mip)) |
| 507 | + { |
| 508 | + continue; |
| 509 | + } |
| 510 | + |
| 511 | + const float* data{reinterpret_cast<const float*>(mip.m_data)}; |
| 512 | + const FaceAxes& f{faces[side]}; |
| 513 | + |
| 514 | + double v{minUV}; |
| 515 | + for (uint32_t y = 0; y < size; ++y) |
| 516 | + { |
| 517 | + double u{minUV}; |
| 518 | + for (uint32_t x = 0; x < size; ++x) |
| 519 | + { |
| 520 | + double dir[3] = { |
| 521 | + f.fx[0] * u + f.fy[0] * v + f.n[0], |
| 522 | + f.fx[1] * u + f.fy[1] * v + f.n[1], |
| 523 | + f.fx[2] * u + f.fy[2] * v + f.n[2], |
| 524 | + }; |
| 525 | + const double len{std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2])}; |
| 526 | + dir[0] /= len; |
| 527 | + dir[1] /= len; |
| 528 | + dir[2] /= len; |
| 529 | + |
| 530 | + const double deltaSolidAngle{ |
| 531 | + areaElement(u - halfTexel, v - halfTexel) - |
| 532 | + areaElement(u - halfTexel, v + halfTexel) - |
| 533 | + areaElement(u + halfTexel, v - halfTexel) + |
| 534 | + areaElement(u + halfTexel, v + halfTexel)}; |
| 535 | + |
| 536 | + const size_t idx{(static_cast<size_t>(y) * size + x) * 4}; |
| 537 | + double rgb[3] = {data[idx + 0], data[idx + 1], data[idx + 2]}; |
| 538 | + for (int c = 0; c < 3; ++c) |
| 539 | + { |
| 540 | + if (std::isnan(rgb[c])) |
| 541 | + { |
| 542 | + rgb[c] = 0.0; |
| 543 | + } |
| 544 | + rgb[c] = rgb[c] < 0.0 ? 0.0 : (rgb[c] > maxHdri ? maxHdri : rgb[c]); |
| 545 | + } |
| 546 | + |
| 547 | + const double trig[9] = { |
| 548 | + 1.0, |
| 549 | + dir[1], |
| 550 | + dir[2], |
| 551 | + dir[0], |
| 552 | + dir[0] * dir[1], |
| 553 | + dir[1] * dir[2], |
| 554 | + 3.0 * dir[2] * dir[2] - 1.0, |
| 555 | + dir[0] * dir[2], |
| 556 | + dir[0] * dir[0] - dir[1] * dir[1], |
| 557 | + }; |
| 558 | + for (int lm = 0; lm < 9; ++lm) |
| 559 | + { |
| 560 | + const double basis{shConst[lm] * trig[lm] * deltaSolidAngle}; |
| 561 | + sh[lm][0] += rgb[0] * basis; |
| 562 | + sh[lm][1] += rgb[1] * basis; |
| 563 | + sh[lm][2] += rgb[2] * basis; |
| 564 | + } |
| 565 | + totalSolidAngle += deltaSolidAngle; |
| 566 | + u += du; |
| 567 | + } |
| 568 | + v += du; |
| 569 | + } |
| 570 | + } |
| 571 | + |
| 572 | + bimg::imageFree(f32); |
| 573 | + |
| 574 | + if (totalSolidAngle <= 0.0) |
| 575 | + { |
| 576 | + return result; |
| 577 | + } |
| 578 | + |
| 579 | + // scaleInPlace(correction) + convertIncidentRadianceToIrradiance + convertIrradianceToLambertianRadiance. |
| 580 | + const double correction{(4.0 * pi) / totalSolidAngle}; |
| 581 | + for (int lm = 0; lm < 9; ++lm) |
| 582 | + { |
| 583 | + const double scale{correction * cosKernel[lm] / pi}; |
| 584 | + sh[lm][0] *= scale; |
| 585 | + sh[lm][1] *= scale; |
| 586 | + sh[lm][2] *= scale; |
| 587 | + } |
| 588 | + |
| 589 | + // SphericalPolynomial.FromHarmonics (updateFromHarmonics then *1/pi). |
| 590 | + for (int c = 0; c < 3; ++c) |
| 591 | + { |
| 592 | + const double l00{sh[0][c]}, l1_1{sh[1][c]}, l10{sh[2][c]}, l11{sh[3][c]}; |
| 593 | + const double l2_2{sh[4][c]}, l2_1{sh[5][c]}, l20{sh[6][c]}, l21{sh[7][c]}, l22{sh[8][c]}; |
| 594 | + const double invPi{1.0 / pi}; |
| 595 | + result[0 * 3 + c] = static_cast<float>(-1.02333 * l11 * invPi); // x |
| 596 | + result[1 * 3 + c] = static_cast<float>(-1.02333 * l1_1 * invPi); // y |
| 597 | + result[2 * 3 + c] = static_cast<float>(1.02333 * l10 * invPi); // z |
| 598 | + result[3 * 3 + c] = static_cast<float>((0.886277 * l00 - 0.247708 * l20 + 0.429043 * l22) * invPi); // xx |
| 599 | + result[4 * 3 + c] = static_cast<float>((0.886277 * l00 - 0.247708 * l20 - 0.429043 * l22) * invPi); // yy |
| 600 | + result[5 * 3 + c] = static_cast<float>((0.886277 * l00 + 0.495417 * l20) * invPi); // zz |
| 601 | + result[6 * 3 + c] = static_cast<float>(-0.858086 * l2_1 * invPi); // yz |
| 602 | + result[7 * 3 + c] = static_cast<float>(-0.858086 * l21 * invPi); // zx |
| 603 | + result[8 * 3 + c] = static_cast<float>(0.858086 * l2_2 * invPi); // xy |
| 604 | + } |
| 605 | + |
| 606 | + return result; |
| 607 | + } |
| 608 | + |
| 609 | + void LoadCubeTextureFromContainer(Graphics::Texture* texture, bimg::ImageContainer* image, bool srgb) |
| 610 | + { |
| 611 | + assert(image->m_cubeMap); |
| 612 | + assert(image->m_width == image->m_height); |
| 613 | + const uint32_t size{image->m_width}; |
| 614 | + |
| 615 | + if (texture->IsValid()) |
| 616 | + { |
| 617 | + if (texture->Width() != size || texture->Height() != size) |
| 618 | + { |
| 619 | + bimg::imageFree(image); |
| 620 | + throw std::runtime_error{"Cannot update texture from image of different size"}; |
| 621 | + } |
| 622 | + } |
| 623 | + else |
| 624 | + { |
| 625 | + const bool hasMips{image->m_numMips > 1}; |
| 626 | + const bgfx::TextureFormat::Enum format{Cast(image->m_format)}; |
| 627 | + const uint64_t flags{srgb ? BGFX_TEXTURE_SRGB : BGFX_TEXTURE_NONE}; |
| 628 | + texture->CreateCube(static_cast<uint16_t>(size), hasMips, 1, format, flags); |
| 629 | + } |
| 630 | + |
| 631 | + // Every (side, mip) view points into the single container's backing |
| 632 | + // store, so the allocation is released exactly once, after bgfx has |
| 633 | + // consumed the final upload. |
| 634 | + const uint8_t numMips{static_cast<uint8_t>(image->m_numMips)}; |
| 635 | + for (uint8_t side = 0; side < 6; ++side) |
| 636 | + { |
| 637 | + for (uint8_t mip = 0; mip < numMips; ++mip) |
| 638 | + { |
| 639 | + bimg::ImageMip imageMip{}; |
| 640 | + if (bimg::imageGetRawData(*image, side, mip, image->m_data, image->m_size, imageMip)) |
| 641 | + { |
| 642 | + bgfx::ReleaseFn releaseFn{}; |
| 643 | + if (side == 5 && mip == numMips - 1) |
| 644 | + { |
| 645 | + releaseFn = [](void*, void* userData) { |
| 646 | + bimg::imageFree(static_cast<bimg::ImageContainer*>(userData)); |
| 647 | + }; |
| 648 | + } |
| 649 | + |
| 650 | + const bgfx::Memory* mem{bgfx::makeRef(imageMip.m_data, imageMip.m_size, releaseFn, image)}; |
| 651 | + texture->UpdateCube(0, side, mip, 0, 0, static_cast<uint16_t>(imageMip.m_width), static_cast<uint16_t>(imageMip.m_height), mem); |
| 652 | + } |
| 653 | + } |
| 654 | + } |
| 655 | + } |
420 | 656 | #endif // BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES |
421 | 657 |
|
422 | 658 | auto RenderTargetSamplesToBgfxMsaaFlag(uint32_t renderTargetSamples) |
@@ -1533,6 +1769,44 @@ namespace Babylon |
1533 | 1769 | const auto onSuccess{info[5].As<Napi::Function>()}; |
1534 | 1770 | const auto onError{info[6].As<Napi::Function>()}; |
1535 | 1771 |
|
| 1772 | + // A single buffer means a self-contained cubemap container (.dds / .ktx / |
| 1773 | + // .ktx2) that already holds all six faces and their mip chain; hand it to |
| 1774 | + // bimg directly instead of expecting six pre-split face images. |
| 1775 | + if (data.Length() == 1) |
| 1776 | + { |
| 1777 | + const auto typedArray{data[0u].As<Napi::TypedArray>()}; |
| 1778 | + const auto dataSpan{gsl::make_span(static_cast<uint8_t*>(typedArray.ArrayBuffer().Data()) + typedArray.ByteOffset(), typedArray.ByteLength())}; |
| 1779 | + auto dataRef{Napi::Persistent(typedArray)}; |
| 1780 | + arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, [dataSpan]() { |
| 1781 | + return ParseCubeImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan); |
| 1782 | + }) |
| 1783 | + .then(arcana::inline_scheduler, *m_cancellationSource, [texture, srgb, cancellationSource{m_cancellationSource}](bimg::ImageContainer* image) { |
| 1784 | + // Compute the spherical harmonics from the decoded top mip before the upload |
| 1785 | + // hands the container's memory to bgfx. |
| 1786 | + auto sphericalPolynomial = ComputeCubeSphericalPolynomial(Graphics::DeviceContext::GetDefaultAllocator(), image); |
| 1787 | + LoadCubeTextureFromContainer(texture, image, srgb); |
| 1788 | + return sphericalPolynomial; |
| 1789 | + }) |
| 1790 | + .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) { |
| 1791 | + if (result.has_error()) |
| 1792 | + { |
| 1793 | + onErrorRef.Call({}); |
| 1794 | + } |
| 1795 | + else |
| 1796 | + { |
| 1797 | + const auto& sphericalPolynomial{result.value()}; |
| 1798 | + auto array{Napi::Float32Array::New(onSuccessRef.Env(), sphericalPolynomial.size())}; |
| 1799 | + float* dst{array.Data()}; |
| 1800 | + for (size_t i = 0; i < sphericalPolynomial.size(); ++i) |
| 1801 | + { |
| 1802 | + dst[i] = sphericalPolynomial[i]; |
| 1803 | + } |
| 1804 | + onSuccessRef.Call({array}); |
| 1805 | + } |
| 1806 | + }); |
| 1807 | + return; |
| 1808 | + } |
| 1809 | + |
1536 | 1810 | std::array<Napi::Reference<Napi::TypedArray>, 6> dataRefs; |
1537 | 1811 | std::array<arcana::task<bimg::ImageContainer*, std::exception_ptr>, 6> tasks; |
1538 | 1812 | for (uint32_t face = 0; face < data.Length(); face++) |
|
0 commit comments