Files
EVEngine/src/modules/graphics/AtmosphereVolume.cpp
T

295 lines
14 KiB
C++

#include "graphics/AtmosphereVolume.h"
#include "graphics/FogVolume.h"
#include "graphics/VolumeDensityGraph.h"
#include "graphics/SparseVolumeTexture.h"
#include <algorithm>
#include <cmath>
#include <glm/common.hpp>
#include <glm/geometric.hpp>
namespace eve::graphics {
void AtmosphereVolume::resize(int width, int height, int depth) {
width_ = std::max(width, 1);
height_ = std::max(height, 1);
depth_ = std::max(depth, 1);
media_.assign(std::size_t(width_) * std::size_t(height_) * std::size_t(depth_), {});
integrated_.assign(media_.size(), glm::vec4(0.f, 0.f, 0.f, 1.f));
}
void AtmosphereVolume::clear() {
std::fill(media_.begin(), media_.end(), FogFroxel{});
std::fill(integrated_.begin(), integrated_.end(), glm::vec4(0.f, 0.f, 0.f, 1.f));
}
void AtmosphereVolume::setDepthRange(float nearDistance, float farDistance) {
nearDistance_ = std::max(nearDistance, 1e-3f);
farDistance_ = std::max(farDistance, nearDistance_ + 1e-3f);
}
float AtmosphereVolume::sliceDistance(int z) const {
if (depth_ <= 0) return nearDistance_;
const float t = (float(std::clamp(z, 0, depth_ - 1)) + 0.5f) / float(depth_);
return nearDistance_ * std::pow(farDistance_ / nearDistance_, t);
}
int AtmosphereVolume::sliceForDistance(float distance) const {
if (depth_ <= 1) return 0;
const float d = std::clamp(distance, nearDistance_, farDistance_);
const float t = std::log(d / nearDistance_) / std::log(farDistance_ / nearDistance_);
return std::clamp(int(std::floor(t * float(depth_))), 0, depth_ - 1);
}
std::size_t AtmosphereVolume::index(int x, int y, int z) const {
x = std::clamp(x, 0, width_ - 1);
y = std::clamp(y, 0, height_ - 1);
z = std::clamp(z, 0, depth_ - 1);
return (std::size_t(z) * std::size_t(height_) + std::size_t(y)) * std::size_t(width_) +
std::size_t(x);
}
FogFroxel &AtmosphereVolume::at(int x, int y, int z) { return media_[index(x, y, z)]; }
const FogFroxel &AtmosphereVolume::at(int x, int y, int z) const { return media_[index(x, y, z)]; }
void AtmosphereVolume::injectHeightFog(float baseExtinction, const glm::vec3 &albedo,
float baseHeight, float heightFalloff, float minWorldY,
float maxWorldY) {
const float sigma = std::max(baseExtinction, 0.f);
const glm::vec3 omega = glm::clamp(albedo, glm::vec3(0.f), glm::vec3(1.f));
const float falloff = std::max(heightFalloff, 0.f);
for (int y = 0; y < height_; ++y) {
const float ty = (float(y) + 0.5f) / float(height_);
const float worldY = minWorldY + (maxWorldY - minWorldY) * ty;
const float extinction = sigma * std::exp(-falloff * std::max(worldY - baseHeight, 0.f));
for (int z = 0; z < depth_; ++z) {
for (int x = 0; x < width_; ++x) {
FogFroxel &f = at(x, y, z);
f.extinction += extinction;
f.scattering += omega * extinction;
}
}
}
}
void AtmosphereVolume::injectLocalVolume(const FogVolume &volume, const glm::vec3 &worldMin,
const glm::vec3 &worldMax) {
for (int z = 0; z < depth_; ++z) {
for (int y = 0; y < height_; ++y) {
for (int x = 0; x < width_; ++x) {
const glm::vec3 uvw((float(x) + 0.5f) / float(width_),
(float(y) + 0.5f) / float(height_),
(float(z) + 0.5f) / float(depth_));
const glm::vec3 world = worldMin + (worldMax - worldMin) * uvw;
const float signedExtinction = volume.sampleExtinction(world);
if (std::fabs(signedExtinction) <= 1e-8f) continue;
FogFroxel &f = at(x, y, z);
const float previous = f.extinction;
f.extinction = std::max(0.f, previous + signedExtinction);
if (signedExtinction > 0.f) {
f.scattering += volume.getAlbedo() * signedExtinction;
f.emissive += volume.getEmissive() * signedExtinction;
f.anisotropy = volume.getAnisotropy();
} else if (previous > 1e-6f) {
const float scale = f.extinction / previous;
f.scattering *= scale;
f.emissive *= scale;
}
}
}
}
}
void AtmosphereVolume::injectLocalVolumeFrustum(const FogVolume &volume,
const glm::mat4 &invViewProj) {
auto unproject = [&](float ndcX, float ndcY, float ndcZ) {
glm::vec4 world = invViewProj * glm::vec4(ndcX, ndcY, ndcZ, 1.f);
return glm::vec3(world) / world.w;
};
for (int y = 0; y < height_; ++y) {
const float v = (float(y) + 0.5f) / float(height_);
for (int x = 0; x < width_; ++x) {
const float u = (float(x) + 0.5f) / float(width_);
const glm::vec3 nearPoint = unproject(u * 2.f - 1.f, v * 2.f - 1.f, 0.f);
const glm::vec3 farPoint = unproject(u * 2.f - 1.f, v * 2.f - 1.f, 1.f);
for (int z = 0; z < depth_; ++z) {
const float depth01 = (sliceDistance(z) - nearDistance_) /
(farDistance_ - nearDistance_);
const glm::vec3 world = glm::mix(nearPoint, farPoint, depth01);
const float signedExtinction = volume.sampleExtinction(world);
if (std::fabs(signedExtinction) <= 1e-8f) continue;
FogFroxel &f = at(x, y, z);
const float previous = f.extinction;
f.extinction = std::max(0.f, previous + signedExtinction);
if (signedExtinction > 0.f) {
f.scattering += volume.getAlbedo() * signedExtinction;
f.emissive += volume.getEmissive() * signedExtinction;
f.anisotropy = volume.getAnisotropy();
} else if (previous > 1e-6f) {
const float scale = f.extinction / previous;
f.scattering *= scale;
f.emissive *= scale;
}
}
}
}
}
void AtmosphereVolume::injectDensityGraph(const VolumeDensityGraph &graph,
const glm::vec3 &worldMin,
const glm::vec3 &worldMax, float extinctionScale,
const glm::vec3 &albedo, float time) {
const glm::vec3 omega = glm::clamp(albedo, glm::vec3(0.f), glm::vec3(1.f));
for (int z = 0; z < depth_; ++z) {
for (int y = 0; y < height_; ++y) {
for (int x = 0; x < width_; ++x) {
const glm::vec3 uvw((float(x) + 0.5f) / float(width_),
(float(y) + 0.5f) / float(height_),
(float(z) + 0.5f) / float(depth_));
const glm::vec3 world = worldMin + (worldMax - worldMin) * uvw;
const float extinction = graph.evaluate(world, time) * extinctionScale;
FogFroxel &f = at(x, y, z);
const float previous = f.extinction;
f.extinction = std::max(0.f, previous + extinction);
if (extinction >= 0.f)
f.scattering += omega * extinction;
else if (previous > 1e-6f)
f.scattering *= f.extinction / previous;
}
}
}
}
void AtmosphereVolume::injectSparseVolume(const SparseVolumeTexture &texture,
float extinctionScale) {
for (int z = 0; z < depth_; ++z) {
for (int y = 0; y < height_; ++y) {
for (int x = 0; x < width_; ++x) {
const FogFroxel source = texture.sample((float(x) + 0.5f) / float(width_),
(float(y) + 0.5f) / float(height_), (float(z) + 0.5f) / float(depth_));
const float scale = std::max(extinctionScale, 0.f);
FogFroxel &target = at(x, y, z);
target.extinction += source.extinction * scale;
target.scattering += source.scattering * scale;
target.emissive += source.emissive * scale;
target.anisotropy = source.anisotropy;
}
}
}
}
void AtmosphereVolume::integrate(const glm::vec3 &lightColor, float phaseScale) {
const glm::vec3 light = glm::max(lightColor, glm::vec3(0.f)) * std::max(phaseScale, 0.f);
for (int y = 0; y < height_; ++y) {
for (int x = 0; x < width_; ++x) {
glm::vec3 radiance(0.f);
float transmittance = 1.f;
float previousDistance = nearDistance_;
for (int z = 0; z < depth_; ++z) {
const float distance = sliceDistance(z);
const float stepLength = std::max(distance - previousDistance, 0.f);
previousDistance = distance;
const FogFroxel &f = at(x, y, z);
const float opticalDepth = f.extinction * stepLength;
const float stepTransmittance = std::exp(-opticalDepth);
const float integral = f.extinction > 1e-6f
? (1.f - stepTransmittance) / f.extinction
: stepLength;
radiance += transmittance *
(f.scattering * light * f.lightVisibility + f.emissive) * integral;
transmittance *= stepTransmittance;
integrated_[index(x, y, z)] = glm::vec4(radiance, transmittance);
}
}
}
}
void AtmosphereVolume::integrateLocalLights(const std::vector<VolumetricLight> &lights,
const glm::vec3 &worldMin,
const glm::vec3 &worldMax,
const glm::vec3 &ambientLight) {
for (int y = 0; y < height_; ++y) {
for (int x = 0; x < width_; ++x) {
glm::vec3 radiance(0.f);
float transmittance = 1.f;
float previousDistance = nearDistance_;
for (int z = 0; z < depth_; ++z) {
const glm::vec3 uvw((float(x) + 0.5f) / float(width_),
(float(y) + 0.5f) / float(height_),
(float(z) + 0.5f) / float(depth_));
const glm::vec3 world = worldMin + (worldMax - worldMin) * uvw;
glm::vec3 incident = glm::max(ambientLight, glm::vec3(0.f));
for (const VolumetricLight &light : lights) {
if (!light.enabled || light.radius <= 0.f || light.intensity <= 0.f) continue;
const float d = glm::length(world - light.position);
if (d >= light.radius) continue;
const float radial = 1.f - d / light.radius;
incident += glm::max(light.color, glm::vec3(0.f)) * light.intensity *
radial * radial;
}
const float distance = sliceDistance(z);
const float stepLength = std::max(distance - previousDistance, 0.f);
previousDistance = distance;
const FogFroxel &f = at(x, y, z);
const float opticalDepth = f.extinction * stepLength;
const float stepTransmittance = std::exp(-opticalDepth);
const float integral = f.extinction > 1e-6f
? (1.f - stepTransmittance) / f.extinction
: stepLength;
radiance += transmittance *
(f.scattering * incident * f.lightVisibility + f.emissive) * integral;
transmittance *= stepTransmittance;
integrated_[index(x, y, z)] = glm::vec4(radiance, transmittance);
}
}
}
}
void AtmosphereVolume::setLightVisibility(int x, int y, int z, float visibility) {
at(x, y, z).lightVisibility = std::clamp(visibility, 0.f, 1.f);
}
std::size_t AtmosphereVolume::blendHistory(const AtmosphereVolume &history, float historyWeight,
float rejectionThreshold) {
if (history.width_ != width_ || history.height_ != height_ || history.depth_ != depth_)
return integrated_.size();
const float baseWeight = std::clamp(historyWeight, 0.f, 1.f);
const float threshold = std::max(rejectionThreshold, 0.f);
std::size_t rejected = 0;
for (std::size_t i = 0; i < integrated_.size(); ++i) {
const glm::vec4 current = integrated_[i];
const glm::vec4 previous = history.integrated_[i];
const float currentLuma = glm::dot(glm::vec3(current), glm::vec3(0.2126f, 0.7152f, 0.0722f));
const float previousLuma = glm::dot(glm::vec3(previous), glm::vec3(0.2126f, 0.7152f, 0.0722f));
const float relativeDelta = std::fabs(currentLuma - previousLuma) /
std::max(std::max(currentLuma, previousLuma), 1e-4f);
const bool reject = relativeDelta > threshold;
if (reject) ++rejected;
const float weight = reject ? 0.f : baseWeight;
integrated_[i] = glm::mix(current, previous, weight);
}
return rejected;
}
const glm::vec4 &AtmosphereVolume::integratedAt(int x, int y, int z) const {
return integrated_[index(x, y, z)];
}
glm::vec4 AtmosphereVolume::sampleIntegrated(float u, float v, float distance) const {
if (integrated_.empty()) return glm::vec4(0.f, 0.f, 0.f, 1.f);
const int x = std::clamp(int(u * float(width_)), 0, width_ - 1);
const int y = std::clamp(int(v * float(height_)), 0, height_ - 1);
return integratedAt(x, y, sliceForDistance(distance));
}
FogFroxel AtmosphereVolume::sampleMedia(float u, float v, float w) const {
if (media_.empty()) return {};
const int x = std::clamp(int(u * float(width_)), 0, width_ - 1);
const int y = std::clamp(int(v * float(height_)), 0, height_ - 1);
const int z = std::clamp(int(w * float(depth_)), 0, depth_ - 1);
return at(x, y, z);
}
} // namespace eve::graphics