12#include <unordered_map>
14#include <spdlog/spdlog.h>
25 static constexpr float ENV_PI = std::numbers::pi_v<float>;
26 static constexpr float ENV_TWO_PI = 2.0f * ENV_PI;
28 static int calcLevels(
int size)
30 return 1 +
static_cast<int>(std::floor(std::log2(std::max(size, 1))));
37 int EnvLighting::getRequiredSamplesGGX(
int numSamples,
int specularPower)
40 static const std::unordered_map<int, std::unordered_map<int, int>> table = {
41 {16, {{2, 26}, {8, 20}, {32, 17}, {128, 16}, {512, 16}}},
42 {32, {{2, 53}, {8, 40}, {32, 34}, {128, 32}, {512, 32}}},
43 {128, {{2, 214}, {8, 163}, {32, 139}, {128, 130}, {512, 128}}},
44 {1024, {{2, 1722}, {8, 1310}, {32, 1114}, {128, 1041}, {512, 1025}}}
47 auto it = table.find(numSamples);
48 if (it != table.end()) {
49 auto it2 = it->second.find(specularPower);
50 if (it2 != it->second.end()) {
60 std::array<uint8_t, 4> EnvLighting::encodeRGBP(
float r,
float g,
float b)
64 const float sr = std::sqrt(std::max(r, 0.0f));
65 const float sg = std::sqrt(std::max(g, 0.0f));
66 const float sb = std::sqrt(std::max(b, 0.0f));
68 const float maxVal = std::max({sr, sg, sb, 1.0f / 255.0f});
69 const float a = std::clamp((8.0f - maxVal) / 7.0f, 0.0f, 1.0f);
70 const float scale = -a * 7.0f + 8.0f;
73 static_cast<uint8_t
>(std::clamp(sr / scale * 255.0f + 0.5f, 0.0f, 255.0f)),
74 static_cast<uint8_t
>(std::clamp(sg / scale * 255.0f + 0.5f, 0.0f, 255.0f)),
75 static_cast<uint8_t
>(std::clamp(sb / scale * 255.0f + 0.5f, 0.0f, 255.0f)),
76 static_cast<uint8_t
>(std::clamp(a * 255.0f + 0.5f, 0.0f, 255.0f))
84 void EnvLighting::dirToEquirectUv(
float x,
float y,
float z,
float& u,
float& v)
86 const float phi = std::atan2(x, z);
87 const float theta = std::asin(std::clamp(y, -1.0f, 1.0f));
88 u = phi / ENV_TWO_PI + 0.5f;
89 v = 1.0f - (theta / ENV_PI + 0.5f);
92 void EnvLighting::equirectUvToDir(
float u,
float v,
float& x,
float& y,
float& z)
94 const float phi = (u - 0.5f) * ENV_TWO_PI;
95 const float theta = (0.5f - v) * ENV_PI;
96 const float cosTheta = std::cos(theta);
97 x = std::sin(phi) * cosTheta;
99 z = std::cos(phi) * cosTheta;
106 void EnvLighting::dirToFaceUv(
float x,
float y,
float z,
int& face,
float& u,
float& v)
108 const float ax = std::abs(x);
109 const float ay = std::abs(y);
110 const float az = std::abs(z);
113 if (ax >= ay && ax >= az) {
115 if (x > 0) { face = 0; sc = -z; tc = -y; }
116 else { face = 1; sc = z; tc = -y; }
117 }
else if (ay >= ax && ay >= az) {
119 if (y > 0) { face = 2; sc = x; tc = z; }
120 else { face = 3; sc = x; tc = -z; }
123 if (z > 0) { face = 4; sc = x; tc = -y; }
124 else { face = 5; sc = -x; tc = -y; }
127 u = (sc / ma + 1.0f) * 0.5f;
128 v = (tc / ma + 1.0f) * 0.5f;
132 static void faceUvToDir(
int face,
float u,
float v,
float& x,
float& y,
float& z)
135 const float sc = u * 2.0f - 1.0f;
136 const float tc = v * 2.0f - 1.0f;
139 case 0: x = 1.0f; y = -tc; z = -sc;
break;
140 case 1: x = -1.0f; y = -tc; z = sc;
break;
141 case 2: x = sc; y = 1.0f; z = tc;
break;
142 case 3: x = sc; y = -1.0f; z = -tc;
break;
143 case 4: x = sc; y = -tc; z = 1.0f;
break;
144 case 5: x = -sc; y = -tc; z = -1.0f;
break;
145 default: x = y = z = 0.0f;
break;
149 const float len = std::sqrt(x * x + y * y + z * z);
160 EnvLighting::Color3 EnvLighting::sampleFace(
const std::vector<float>& faceData,
int faceSize,
float u,
float v)
163 u = std::clamp(u, 0.0f, 1.0f);
164 v = std::clamp(v, 0.0f, 1.0f);
166 const float fx = u *
static_cast<float>(faceSize - 1);
167 const float fy = v *
static_cast<float>(faceSize - 1);
169 const int x0 = std::clamp(
static_cast<int>(std::floor(fx)), 0, faceSize - 1);
170 const int y0 = std::clamp(
static_cast<int>(std::floor(fy)), 0, faceSize - 1);
171 const int x1 = std::min(x0 + 1, faceSize - 1);
172 const int y1 = std::min(y0 + 1, faceSize - 1);
174 const float sx = fx -
static_cast<float>(x0);
175 const float sy = fy -
static_cast<float>(y0);
177 auto pixel = [&](
int px,
int py) ->
const float* {
178 return &faceData[(py * faceSize + px) * 4];
181 const float* p00 = pixel(x0, y0);
182 const float* p10 = pixel(x1, y0);
183 const float* p01 = pixel(x0, y1);
184 const float* p11 = pixel(x1, y1);
187 for (
int c = 0; c < 3; ++c) {
188 const float top = p00[c] * (1.0f - sx) + p10[c] * sx;
189 const float bot = p01[c] * (1.0f - sx) + p11[c] * sx;
190 (&result.r)[c] = top * (1.0f - sy) + bot * sy;
198 EnvLighting::Color3 EnvLighting::sampleCubemap(
const HdrCubemap& cubemap,
float dirX,
float dirY,
float dirZ,
float mipLevel)
200 mipLevel = std::clamp(mipLevel, 0.0f,
static_cast<float>(cubemap.numLevels - 1));
204 dirToFaceUv(dirX, dirY, dirZ, face, u, v);
206 const int mip0 = std::clamp(
static_cast<int>(std::floor(mipLevel)), 0, cubemap.numLevels - 1);
207 const int mip1 = std::min(mip0 + 1, cubemap.numLevels - 1);
208 const float frac = mipLevel -
static_cast<float>(mip0);
210 const int size0 = std::max(1, cubemap.size >> mip0);
211 const int size1 = std::max(1, cubemap.size >> mip1);
213 Color3 c0 = sampleFace(cubemap.data[mip0][face], size0, u, v);
215 if (mip0 == mip1 || frac < 0.001f) {
219 Color3 c1 = sampleFace(cubemap.data[mip1][face], size1, u, v);
221 c0.r * (1.0f - frac) + c1.r * frac,
222 c0.g * (1.0f - frac) + c1.g * frac,
223 c0.b * (1.0f - frac) + c1.b * frac
230 EnvLighting::HdrCubemap EnvLighting::equirectToCubemap(
Texture* source,
int size)
232 const auto* srcData =
static_cast<const float*
>(source->getLevel(0));
233 const int srcW =
static_cast<int>(source->width());
234 const int srcH =
static_cast<int>(source->height());
235 return equirectToCubemap(srcData, srcW, srcH, size);
238 EnvLighting::HdrCubemap EnvLighting::equirectToCubemap(
const float* srcData,
int srcW,
int srcH,
int size)
242 cubemap.numLevels = 1;
243 cubemap.data.resize(1);
244 cubemap.data[0].resize(6);
246 if (!srcData || srcW == 0 || srcH == 0) {
247 spdlog::error(
"EnvLighting: source texture has no pixel data ({}x{})", srcW, srcH);
252 auto sampleEquirect = [&](
float u,
float v) -> Color3 {
253 u = u - std::floor(u);
254 v = std::clamp(v, 0.0f, 1.0f);
256 const float fx = u *
static_cast<float>(srcW - 1);
257 const float fy = v *
static_cast<float>(srcH - 1);
259 const int x0 = std::clamp(
static_cast<int>(std::floor(fx)), 0, srcW - 1);
260 const int y0 = std::clamp(
static_cast<int>(std::floor(fy)), 0, srcH - 1);
261 const int x1 = (x0 + 1) % srcW;
262 const int y1 = std::min(y0 + 1, srcH - 1);
264 const float sx = fx -
static_cast<float>(x0);
265 const float sy = fy -
static_cast<float>(y0);
267 auto pixel = [&](
int px,
int py) ->
const float* {
268 return &srcData[(py * srcW + px) * 4];
271 const float* p00 = pixel(x0, y0);
272 const float* p10 = pixel(x1, y0);
273 const float* p01 = pixel(x0, y1);
274 const float* p11 = pixel(x1, y1);
277 for (
int c = 0; c < 3; ++c) {
278 const float top = p00[c] * (1.0f - sx) + p10[c] * sx;
279 const float bot = p01[c] * (1.0f - sx) + p11[c] * sx;
280 (&result.r)[c] = top * (1.0f - sy) + bot * sy;
286 for (
int face = 0; face < 6; ++face) {
287 auto& faceData = cubemap.data[0][face];
288 faceData.resize(size * size * 4);
290 for (
int py = 0; py < size; ++py) {
291 for (
int px = 0; px < size; ++px) {
292 const float u = (
static_cast<float>(px) + 0.5f) /
static_cast<float>(size);
293 const float v = (
static_cast<float>(py) + 0.5f) /
static_cast<float>(size);
296 faceUvToDir(face, u, v, dx, dy, dz);
299 dirToEquirectUv(dx, dy, dz, eu, ev);
301 Color3 color = sampleEquirect(eu, ev);
302 const int idx = (py * size + px) * 4;
303 faceData[idx + 0] = color.r;
304 faceData[idx + 1] = color.g;
305 faceData[idx + 2] = color.b;
306 faceData[idx + 3] = 1.0f;
317 void EnvLighting::generateMipmaps(HdrCubemap& cubemap)
319 const int maxLevels = calcLevels(cubemap.size);
320 cubemap.numLevels = maxLevels;
321 cubemap.data.resize(maxLevels);
323 for (
int mip = 1; mip < maxLevels; ++mip) {
324 const int prevSize = std::max(1, cubemap.size >> (mip - 1));
325 const int currSize = std::max(1, cubemap.size >> mip);
327 cubemap.data[mip].resize(6);
329 for (
int face = 0; face < 6; ++face) {
330 auto& prevFace = cubemap.data[mip - 1][face];
331 auto& currFace = cubemap.data[mip][face];
332 currFace.resize(currSize * currSize * 4);
334 for (
int py = 0; py < currSize; ++py) {
335 for (
int px = 0; px < currSize; ++px) {
336 const int sx = px * 2;
337 const int sy = py * 2;
338 const int sx1 = std::min(sx + 1, prevSize - 1);
339 const int sy1 = std::min(sy + 1, prevSize - 1);
341 const int dstIdx = (py * currSize + px) * 4;
343 for (
int c = 0; c < 4; ++c) {
344 const float v00 = prevFace[(sy * prevSize + sx) * 4 + c];
345 const float v10 = prevFace[(sy * prevSize + sx1) * 4 + c];
346 const float v01 = prevFace[(sy1 * prevSize + sx) * 4 + c];
347 const float v11 = prevFace[(sy1 * prevSize + sx1) * 4 + c];
348 currFace[dstIdx + c] = (v00 + v10 + v01 + v11) * 0.25f;
360 void EnvLighting::hemisphereSampleGGX(
float& hx,
float& hy,
float& hz,
float xi1,
float xi2,
float a)
362 const float phi = xi2 * ENV_TWO_PI;
363 const float cosTheta = std::sqrt((1.0f - xi1) / (1.0f + (a * a - 1.0f) * xi1));
364 const float sinTheta = std::sqrt(std::max(0.0f, 1.0f - cosTheta * cosTheta));
365 hx = std::cos(phi) * sinTheta;
366 hy = std::sin(phi) * sinTheta;
369 const float len = std::sqrt(hx * hx + hy * hy + hz * hz);
377 void EnvLighting::hemisphereSampleLambert(
float& hx,
float& hy,
float& hz,
float xi1,
float xi2)
379 const float phi = xi2 * ENV_TWO_PI;
380 const float cosTheta = std::sqrt(1.0f - xi1);
381 const float sinTheta = std::sqrt(xi1);
382 hx = std::cos(phi) * sinTheta;
383 hy = std::sin(phi) * sinTheta;
386 const float len = std::sqrt(hx * hx + hy * hy + hz * hz);
394 float EnvLighting::D_GGX(
float NoH,
float linearRoughness)
396 const float a = NoH * linearRoughness;
397 const float k = linearRoughness / (1.0f - NoH * NoH + a * a);
398 return k * k * (1.0f / ENV_PI);
405 void EnvLighting::writeEquirectRegion(uint8_t* atlas,
int atlasSize,
406 int rx,
int ry,
int rw,
int rh,
407 const HdrCubemap& cubemap,
float mipLevel)
409 const float seamPixels = 1.0f;
410 const float invRw = 1.0f /
static_cast<float>(rw);
411 const float invRh = 1.0f /
static_cast<float>(rh);
413 for (
int py = 0; py < rh; ++py) {
414 for (
int px = 0; px < rw; ++px) {
416 float u = (
static_cast<float>(px) + 0.5f) * invRw;
417 float v = (
static_cast<float>(py) + 0.5f) * invRh;
420 const float innerW =
static_cast<float>(rw) - seamPixels * 2.0f;
421 const float innerH =
static_cast<float>(rh) - seamPixels * 2.0f;
422 if (innerW > 0.0f && innerH > 0.0f) {
423 u = (u *
static_cast<float>(rw) - seamPixels) / innerW;
424 v = (v *
static_cast<float>(rh) - seamPixels) / innerH;
429 equirectUvToDir(u, v, dx, dy, dz);
432 Color3 color = sampleCubemap(cubemap, dx, dy, dz, mipLevel);
435 auto encoded = encodeRGBP(color.r, color.g, color.b);
436 const int atlasIdx = ((ry + py) * atlasSize + (rx + px)) * 4;
437 atlas[atlasIdx + 0] = encoded[0];
438 atlas[atlasIdx + 1] = encoded[1];
439 atlas[atlasIdx + 2] = encoded[2];
440 atlas[atlasIdx + 3] = encoded[3];
450 void EnvLighting::writeEquirectFromSource(uint8_t* atlas,
int atlasSize,
451 int rx,
int ry,
int rw,
int rh,
452 const float* srcData,
int srcW,
int srcH)
454 const float seamPixels = 1.0f;
455 const float invRw = 1.0f /
static_cast<float>(rw);
456 const float invRh = 1.0f /
static_cast<float>(rh);
458 for (
int py = 0; py < rh; ++py) {
459 for (
int px = 0; px < rw; ++px) {
460 float u = (
static_cast<float>(px) + 0.5f) * invRw;
461 float v = (
static_cast<float>(py) + 0.5f) * invRh;
464 const float innerW =
static_cast<float>(rw) - seamPixels * 2.0f;
465 const float innerH =
static_cast<float>(rh) - seamPixels * 2.0f;
466 if (innerW > 0.0f && innerH > 0.0f) {
467 u = (u *
static_cast<float>(rw) - seamPixels) / innerW;
468 v = (v *
static_cast<float>(rh) - seamPixels) / innerH;
475 equirectUvToDir(u, v, dx, dy, dz);
478 dirToEquirectUv(dx, dy, dz, srcU, srcV);
481 srcU = srcU - std::floor(srcU);
482 srcV = std::clamp(srcV, 0.0f, 1.0f);
484 const float fx = srcU *
static_cast<float>(srcW - 1);
485 const float fy = srcV *
static_cast<float>(srcH - 1);
487 const int x0 = std::clamp(
static_cast<int>(std::floor(fx)), 0, srcW - 1);
488 const int y0 = std::clamp(
static_cast<int>(std::floor(fy)), 0, srcH - 1);
489 const int x1 = (x0 + 1) % srcW;
490 const int y1 = std::min(y0 + 1, srcH - 1);
492 const float sx = fx -
static_cast<float>(x0);
493 const float sy = fy -
static_cast<float>(y0);
495 auto pixel = [&](
int ppx,
int ppy) ->
const float* {
496 return &srcData[(ppy * srcW + ppx) * 4];
499 const float* p00 = pixel(x0, y0);
500 const float* p10 = pixel(x1, y0);
501 const float* p01 = pixel(x0, y1);
502 const float* p11 = pixel(x1, y1);
504 float r = 0.0f, g = 0.0f, b = 0.0f;
505 for (
int c = 0; c < 3; ++c) {
506 const float top = p00[c] * (1.0f - sx) + p10[c] * sx;
507 const float bot = p01[c] * (1.0f - sx) + p11[c] * sx;
508 const float val = top * (1.0f - sy) + bot * sy;
510 else if (c == 1) g = val;
514 auto encoded = encodeRGBP(r, g, b);
515 const int atlasIdx = ((ry + py) * atlasSize + (rx + px)) * 4;
516 atlas[atlasIdx + 0] = encoded[0];
517 atlas[atlasIdx + 1] = encoded[1];
518 atlas[atlasIdx + 2] = encoded[2];
519 atlas[atlasIdx + 3] = encoded[3];
528 void EnvLighting::writeGGXRegion(uint8_t* atlas,
int atlasSize,
529 int rx,
int ry,
int rw,
int rh,
530 const HdrCubemap& cubemap,
531 int specularPower,
int numSamples)
533 const float roughness = 1.0f - std::log2(
static_cast<float>(std::max(specularPower, 1))) / 11.0f;
534 const float a = roughness * roughness;
537 const float sourceTotalPixels =
static_cast<float>(cubemap.size * cubemap.size * 6);
538 const float pixelsPerSample = sourceTotalPixels /
static_cast<float>(numSamples);
541 const int requiredSamples = getRequiredSamplesGGX(numSamples, specularPower);
543 struct Sample {
float lx, ly, lz, mipLevel; };
544 std::vector<Sample> samples;
545 samples.reserve(numSamples);
547 for (
int i = 0; i < requiredSamples && static_cast<int>(samples.size()) < numSamples; ++i) {
549 hemisphereSampleGGX(hx, hy, hz,
550 static_cast<float>(i) /
static_cast<float>(requiredSamples),
553 const float NoH = hz;
555 float lx = 2.0f * NoH * hx;
556 float ly = 2.0f * NoH * hy;
557 float lz = 2.0f * NoH * hz - 1.0f;
560 const float pdf = D_GGX(std::min(1.0f, NoH), a) / 4.0f + 0.001f;
561 const float mip = 0.5f * std::log2(pixelsPerSample / pdf);
562 samples.push_back({lx, ly, lz, mip});
567 while (
static_cast<int>(samples.size()) < numSamples) {
568 samples.push_back({0, 0, 0, 0});
571 const float seamPixels = 1.0f;
572 const float invRw = 1.0f /
static_cast<float>(rw);
573 const float invRh = 1.0f /
static_cast<float>(rh);
575 for (
int py = 0; py < rh; ++py) {
576 for (
int px = 0; px < rw; ++px) {
577 float u = (
static_cast<float>(px) + 0.5f) * invRw;
578 float v = (
static_cast<float>(py) + 0.5f) * invRh;
581 const float innerW =
static_cast<float>(rw) - seamPixels * 2.0f;
582 const float innerH =
static_cast<float>(rh) - seamPixels * 2.0f;
583 if (innerW > 0.0f && innerH > 0.0f) {
584 u = (u *
static_cast<float>(rw) - seamPixels) / innerW;
585 v = (v *
static_cast<float>(rh) - seamPixels) / innerH;
590 equirectUvToDir(u, v, nx, ny, nz);
594 float upX = 0.0f, upY = 1.0f, upZ = 0.0f;
595 if (std::abs(ny) > 0.999f) {
596 upX = 1.0f; upY = 0.0f; upZ = 0.0f;
599 float tx = upY * nz - upZ * ny;
600 float ty = upZ * nx - upX * nz;
601 float tz = upX * ny - upY * nx;
602 float tlen = std::sqrt(tx * tx + ty * ty + tz * tz);
603 if (tlen > 0.0f) { tx /= tlen; ty /= tlen; tz /= tlen; }
605 float bx = ny * tz - nz * ty;
606 float by = nz * tx - nx * tz;
607 float bz = nx * ty - ny * tx;
610 float sumR = 0.0f, sumG = 0.0f, sumB = 0.0f;
611 float totalWeight = 0.0f;
613 for (
const auto& s : samples) {
614 if (s.lz <= 0.0f)
continue;
617 const float wx = tx * s.lx + bx * s.ly + nx * s.lz;
618 const float wy = ty * s.lx + by * s.ly + ny * s.lz;
619 const float wz = tz * s.lx + bz * s.ly + nz * s.lz;
621 Color3 color = sampleCubemap(cubemap, wx, wy, wz, s.mipLevel);
628 if (totalWeight > 0.0f) {
634 auto encoded = encodeRGBP(sumR, sumG, sumB);
635 const int atlasIdx = ((ry + py) * atlasSize + (rx + px)) * 4;
636 atlas[atlasIdx + 0] = encoded[0];
637 atlas[atlasIdx + 1] = encoded[1];
638 atlas[atlasIdx + 2] = encoded[2];
639 atlas[atlasIdx + 3] = encoded[3];
648 void EnvLighting::writeLambertRegion(uint8_t* atlas,
int atlasSize,
649 int rx,
int ry,
int rw,
int rh,
650 const HdrCubemap& cubemap,
int numSamples)
653 const float sourceTotalPixels =
static_cast<float>(cubemap.size * cubemap.size * 6);
654 const float pixelsPerSample = sourceTotalPixels /
static_cast<float>(numSamples);
657 struct Sample {
float hx, hy, hz, mipLevel; };
658 std::vector<Sample> samples;
659 samples.reserve(numSamples);
661 for (
int i = 0; i < numSamples; ++i) {
663 hemisphereSampleLambert(hx, hy, hz,
664 static_cast<float>(i) /
static_cast<float>(numSamples),
667 const float pdf = hz / ENV_PI;
668 const float mip = 0.5f * std::log2(pixelsPerSample / std::max(pdf, 0.001f));
669 samples.push_back({hx, hy, hz, mip});
672 const float seamPixels = 1.0f;
673 const float invRw = 1.0f /
static_cast<float>(rw);
674 const float invRh = 1.0f /
static_cast<float>(rh);
676 for (
int py = 0; py < rh; ++py) {
677 for (
int px = 0; px < rw; ++px) {
678 float u = (
static_cast<float>(px) + 0.5f) * invRw;
679 float v = (
static_cast<float>(py) + 0.5f) * invRh;
682 const float innerW =
static_cast<float>(rw) - seamPixels * 2.0f;
683 const float innerH =
static_cast<float>(rh) - seamPixels * 2.0f;
684 if (innerW > 0.0f && innerH > 0.0f) {
685 u = (u *
static_cast<float>(rw) - seamPixels) / innerW;
686 v = (v *
static_cast<float>(rh) - seamPixels) / innerH;
690 equirectUvToDir(u, v, nx, ny, nz);
693 float upX = 0.0f, upY = 1.0f, upZ = 0.0f;
694 if (std::abs(ny) > 0.999f) {
695 upX = 1.0f; upY = 0.0f; upZ = 0.0f;
697 float tx = upY * nz - upZ * ny;
698 float ty = upZ * nx - upX * nz;
699 float tz = upX * ny - upY * nx;
700 float tlen = std::sqrt(tx * tx + ty * ty + tz * tz);
701 if (tlen > 0.0f) { tx /= tlen; ty /= tlen; tz /= tlen; }
702 float bx = ny * tz - nz * ty;
703 float by = nz * tx - nx * tz;
704 float bz = nx * ty - ny * tx;
706 float sumR = 0.0f, sumG = 0.0f, sumB = 0.0f;
707 float totalWeight = 0.0f;
709 for (
const auto& s : samples) {
711 const float wx = tx * s.hx + bx * s.hy + nx * s.hz;
712 const float wy = ty * s.hx + by * s.hy + ny * s.hz;
713 const float wz = tz * s.hx + bz * s.hy + nz * s.hz;
715 Color3 color = sampleCubemap(cubemap, wx, wy, wz, s.mipLevel);
722 if (totalWeight > 0.0f) {
728 auto encoded = encodeRGBP(sumR, sumG, sumB);
729 const int atlasIdx = ((ry + py) * atlasSize + (rx + px)) * 4;
730 atlas[atlasIdx + 0] = encoded[0];
731 atlas[atlasIdx + 1] = encoded[1];
732 atlas[atlasIdx + 2] = encoded[2];
733 atlas[atlasIdx + 3] = encoded[3];
743 int size,
int numReflectionSamples,
int numAmbientSamples)
745 if (!device || !source) {
746 spdlog::error(
"EnvLighting::generateAtlas: null device or source");
750 spdlog::info(
"EnvLighting: generating {}x{} RGBP atlas from {}x{} equirect source",
754 const auto* srcData =
static_cast<const float*
>(source->
getLevel(0));
755 const int srcW =
static_cast<int>(source->
width());
756 const int srcH =
static_cast<int>(source->
height());
762 const int cubemapSize = 256;
763 auto cubemap = equirectToCubemap(source, cubemapSize);
764 spdlog::info(
"EnvLighting: cubemap created ({}x{}, {} faces)", cubemapSize, cubemapSize, 6);
767 generateMipmaps(cubemap);
768 spdlog::info(
"EnvLighting: {} mip levels generated", cubemap.numLevels);
771 std::vector<uint8_t> atlasData(size * size * 4, 0);
780 const int levels = calcLevels(256) - calcLevels(4);
781 int rectX = 0, rectY = 0, rectW = size, rectH = size / 2;
783 for (
int i = 0; i <= levels; ++i) {
784 if (rectW < 1 || rectH < 1)
break;
786 spdlog::debug(
"EnvLighting: mipmap level {} -> rect({}, {}, {}, {})", i, rectX, rectY, rectW, rectH);
788 if (srcData && srcW > 0 && srcH > 0) {
790 writeEquirectFromSource(atlasData.data(), size, rectX, rectY, rectW, rectH,
791 srcData, srcW, srcH);
794 writeEquirectRegion(atlasData.data(), size, rectX, rectY, rectW, rectH,
795 cubemap,
static_cast<float>(i));
800 rectW = std::max(1, rectW / 2);
801 rectH = std::max(1, rectH / 2);
808 int rectX = 0, rectY = size / 2, rectW = size / 2, rectH = size / 4;
810 for (
int i = 1; i <= 6; ++i) {
811 if (rectW < 1 || rectH < 1)
break;
813 const int specularPower = std::max(1, 2048 >> (i * 2));
814 spdlog::info(
"EnvLighting: GGX blur level {} (specularPower={}) -> rect({}, {}, {}, {})",
815 i, specularPower, rectX, rectY, rectW, rectH);
817 writeGGXRegion(atlasData.data(), size, rectX, rectY, rectW, rectH,
818 cubemap, specularPower, numReflectionSamples);
821 rectW = std::max(1, rectW / 2);
822 rectH = std::max(1, rectH / 2);
829 const int rectX = size / 4;
830 const int rectY = size / 2 + size / 4;
831 const int rectW = size / 8;
832 const int rectH = size / 16;
834 spdlog::info(
"EnvLighting: Lambert ambient -> rect({}, {}, {}, {})", rectX, rectY, rectW, rectH);
835 writeLambertRegion(atlasData.data(), size, rectX, rectY, rectW, rectH,
836 cubemap, numAmbientSamples);
841 options.
name =
"envAtlas";
842 options.
width = size;
849 auto* texture =
new Texture(device, options);
851 texture->setLevelData(0, atlasData.data(), atlasData.size());
854 spdlog::info(
"EnvLighting: atlas generated successfully ({}x{}, RGBP)", size, size);
863 int size,
int numReflectionSamples,
int numAmbientSamples)
865 if (!srcData || srcW <= 0 || srcH <= 0) {
866 spdlog::error(
"EnvLighting::generateAtlasRaw: invalid source data ({}x{})", srcW, srcH);
870 spdlog::info(
"EnvLighting: generating {}x{} RGBP atlas from {}x{} equirect source (CPU-only)",
871 size, size, srcW, srcH);
874 const int cubemapSize = 256;
875 auto cubemap = equirectToCubemap(srcData, srcW, srcH, cubemapSize);
876 spdlog::info(
"EnvLighting: cubemap created ({}x{}, {} faces)", cubemapSize, cubemapSize, 6);
879 generateMipmaps(cubemap);
880 spdlog::info(
"EnvLighting: {} mip levels generated", cubemap.numLevels);
883 std::vector<uint8_t> atlasData(size * size * 4, 0);
887 const int levels = calcLevels(256) - calcLevels(4);
888 int rectX = 0, rectY = 0, rectW = size, rectH = size / 2;
890 for (
int i = 0; i <= levels; ++i) {
891 if (rectW < 1 || rectH < 1)
break;
892 spdlog::debug(
"EnvLighting: mipmap level {} -> rect({}, {}, {}, {})", i, rectX, rectY, rectW, rectH);
894 writeEquirectFromSource(atlasData.data(), size, rectX, rectY, rectW, rectH,
895 srcData, srcW, srcH);
899 rectW = std::max(1, rectW / 2);
900 rectH = std::max(1, rectH / 2);
906 int rectX = 0, rectY = size / 2, rectW = size / 2, rectH = size / 4;
908 for (
int i = 1; i <= 6; ++i) {
909 if (rectW < 1 || rectH < 1)
break;
910 const int specularPower = std::max(1, 2048 >> (i * 2));
911 spdlog::info(
"EnvLighting: GGX blur level {} (specularPower={}) -> rect({}, {}, {}, {})",
912 i, specularPower, rectX, rectY, rectW, rectH);
913 writeGGXRegion(atlasData.data(), size, rectX, rectY, rectW, rectH,
914 cubemap, specularPower, numReflectionSamples);
916 rectW = std::max(1, rectW / 2);
917 rectH = std::max(1, rectH / 2);
923 const int rectX = size / 4;
924 const int rectY = size / 2 + size / 4;
925 const int rectW = size / 8;
926 const int rectH = size / 16;
927 spdlog::info(
"EnvLighting: Lambert ambient -> rect({}, {}, {}, {})", rectX, rectY, rectW, rectH);
928 writeLambertRegion(atlasData.data(), size, rectX, rectY, rectW, rectH,
929 cubemap, numAmbientSamples);
932 spdlog::info(
"EnvLighting: atlas generated successfully ({}x{}, RGBP, CPU-only)", size, size);
943 if (!device || !source) {
944 spdlog::warn(
"EnvLighting::generateSkyboxCubemap: null device or source");
948 const auto* srcData =
static_cast<const float*
>(source->
getLevel(0));
950 spdlog::warn(
"EnvLighting::generateSkyboxCubemap: source has no pixel data");
954 const int srcW =
static_cast<int>(source->
width());
955 const int srcH =
static_cast<int>(source->
height());
962 size = std::clamp(size, 4, 4096);
964 spdlog::info(
"EnvLighting: generating {}x{} skybox cubemap from {}x{} equirect source",
965 size, size, srcW, srcH);
967 const size_t facePixels =
static_cast<size_t>(size) *
static_cast<size_t>(size);
968 const size_t faceBytes = facePixels * 4;
973 options.
name =
"skyboxCubemap";
974 options.
width = size;
982 auto* texture =
new Texture(device, options);
985 for (
int face = 0; face < 6; ++face) {
986 std::vector<uint8_t> faceData(faceBytes);
988 for (
int py = 0; py < size; ++py) {
989 for (
int px = 0; px < size; ++px) {
991 const float u = (
static_cast<float>(px) + 0.5f) /
static_cast<float>(size);
992 const float v = (
static_cast<float>(py) + 0.5f) /
static_cast<float>(size);
996 faceUvToDir(face, u, v, dx, dy, dz);
1000 dirToEquirectUv(dx, dy, dz, srcU, srcV);
1003 srcU = srcU - std::floor(srcU);
1004 srcV = std::clamp(srcV, 0.0f, 1.0f);
1007 const float fx = srcU *
static_cast<float>(srcW - 1);
1008 const float fy = srcV *
static_cast<float>(srcH - 1);
1010 const int x0 = std::clamp(
static_cast<int>(std::floor(fx)), 0, srcW - 1);
1011 const int y0 = std::clamp(
static_cast<int>(std::floor(fy)), 0, srcH - 1);
1012 const int x1 = (x0 + 1) % srcW;
1013 const int y1 = std::min(y0 + 1, srcH - 1);
1015 const float sx = fx -
static_cast<float>(x0);
1016 const float sy = fy -
static_cast<float>(y0);
1018 auto pixel = [&](
int ppx,
int ppy) ->
const float* {
1019 return &srcData[(ppy * srcW + ppx) * 4];
1022 const float* p00 = pixel(x0, y0);
1023 const float* p10 = pixel(x1, y0);
1024 const float* p01 = pixel(x0, y1);
1025 const float* p11 = pixel(x1, y1);
1027 float r = 0.0f, g = 0.0f, b = 0.0f;
1028 for (
int c = 0; c < 3; ++c) {
1029 const float top = p00[c] * (1.0f - sx) + p10[c] * sx;
1030 const float bot = p01[c] * (1.0f - sx) + p11[c] * sx;
1031 const float val = top * (1.0f - sy) + bot * sy;
1032 if (c == 0) r = val;
1033 else if (c == 1) g = val;
1042 const auto encoded = encodeRGBP(r, g, b);
1044 const size_t offset = (
static_cast<size_t>(py) * size + px) * 4;
1045 faceData[offset + 0] = encoded[0];
1046 faceData[offset + 1] = encoded[1];
1047 faceData[offset + 2] = encoded[2];
1048 faceData[offset + 3] = encoded[3];
1052 texture->setLevelData(0, faceData.data(), faceData.size(), face);
1057 spdlog::info(
"EnvLighting: skybox cubemap generated successfully ({}x{} per face, RGBP)", size, size);
static Texture * generateAtlas(GraphicsDevice *device, Texture *source, int size=512, int numReflectionSamples=1024, int numAmbientSamples=2048)
static std::vector< uint8_t > generateAtlasRaw(const float *srcData, int srcW, int srcH, int size=512, int numReflectionSamples=1024, int numAmbientSamples=2048)
static Texture * generateSkyboxCubemap(GraphicsDevice *device, Texture *source, int size=0)
Abstract GPU interface for resource creation, state management, and draw submission.
static float radicalInverse(uint32_t i)
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
void * getLevel(uint32_t mipLevel) const