3#include <glm/gtc/matrix_transform.hpp>
10constexpr size_t kMaxGasParticles = 65536;
11constexpr uint64_t kMaxGasCoveredPixels = 4000000;
12constexpr float kBlurWeights[7] = {.006f, .061f, .242f, .383f, .242f, .061f, .006f};
19 if (!std::isfinite(alpha) || alpha < 0.f || alpha > 1.f)
22 renderVolumeInternal(sim,
false, alpha);
28 if (!std::isfinite(transparency) || transparency < 0.f || transparency > 1.f || !std::isfinite(absorption) ||
29 absorption < 0.f || absorption > 30.f || !std::isfinite(coefficient) || coefficient < -.1f ||
30 coefficient > .1f || downsample < 1 || downsample > 4)
32 "Invalid fluid refraction controls",
33 "fluids.surface.configureRefraction"));
34 refractionTransparency_ = transparency;
35 refractionAbsorption_ = absorption;
36 refractionCoefficient_ = coefficient;
37 refractionDownsample_ = downsample;
50 if (!auxiliaryCurrent_)
52 "Current color frame has no host-visible surface auxiliaries",
53 "fluids.surface.configuredRefraction"));
54 if (sceneColor.size() !=
pixels * 4u)
56 "Refraction scene dimensions are invalid",
57 "fluids.surface.configuredRefraction"));
59 const int targetWidth = std::max(1, (
width + refractionDownsample_ - 1) / refractionDownsample_);
60 const int targetHeight = std::max(1, (
height + refractionDownsample_ - 1) / refractionDownsample_);
61 refractionScratch_.resize(
size_t(targetWidth) *
size_t(targetHeight) * 4u);
62 const auto sample = [](std::span<const uint8_t>
source,
int sourceWidth,
int sourceHeight,
float x,
float y,
64 x = std::clamp(
x, 0.f,
float(sourceWidth - 1));
65 y = std::clamp(
y, 0.f,
float(sourceHeight - 1));
66 const int x0 = int(std::floor(
x)), y0 = int(std::floor(
y));
67 const int x1 = std::min(x0 + 1, sourceWidth - 1), y1 = std::min(y0 + 1, sourceHeight - 1);
68 const float tx =
x - float(x0), ty =
y - float(y0);
69 const auto at = [&](
int px,
int py) {
70 return float(
source[(
size_t(
py) *
size_t(sourceWidth) +
size_t(
px)) * 4u +
size_t(channel)]);
72 return std::lerp(std::lerp(
at(x0, y0),
at(x1, y0), tx), std::lerp(
at(x0, y1),
at(x1, y1), tx), ty);
74 for (
int y = 0;
y < targetHeight; ++
y)
75 for (
int x = 0;
x < targetWidth; ++
x) {
76 const float sourceX = (float(
x) + .5f) *
float(
width) / float(targetWidth) - .5f;
77 const float sourceY = (float(
y) + .5f) *
float(
height) / float(targetHeight) - .5f;
78 const size_t out = (size_t(
y) * size_t(targetWidth) + size_t(
x)) * 4u;
79 for (
int channel = 0; channel < 4; ++channel)
80 refractionScratch_[out +
size_t(channel)] = uint8_t(
81 std::clamp(std::lround(sample(sceneColor,
width,
height,
sourceX, sourceY, channel)), 0l, 255l));
84 const std::span<const uint8_t> reduced(refractionScratch_);
85 for (
size_t i = 0; i <
pixels; ++i) {
86 const size_t rgba = i * 4u;
87 const float coverage = float(color_[rgba + 3u]) / 255.f;
88 if (coverage <= 0.f) {
89 std::copy_n(sceneColor.data() + rgba, 4u, color_.data() + rgba);
92 const int x = int(i %
size_t(
width)),
y = int(i /
size_t(
width));
93 const float u = (float(
x) + .5f) /
float(
width) + normals_[i].x * thickness_[i] * refractionCoefficient_;
94 const float v = (float(
y) + .5f) /
float(
height) - normals_[i].y * thickness_[i] * refractionCoefficient_;
95 const float rx =
u * float(targetWidth) - .5f;
96 const float ry =
v * float(targetHeight) - .5f;
97 for (
int channel = 0; channel < 3; ++channel) {
98 const float liquid = float(color_[rgba +
size_t(channel)]) / 255.f;
99 const float absorbed = std::exp(-refractionAbsorption_ * (1.f - liquid) * std::max(thickness_[i], 0.f));
100 const float refracted = sample(reduced, targetWidth, targetHeight, rx, ry, channel) * absorbed;
101 const float fluid = std::lerp(
float(color_[rgba +
size_t(channel)]), refracted, refractionTransparency_);
102 const float composed = float(sceneColor[rgba +
size_t(channel)]) * (1.f - coverage) + fluid * coverage;
103 color_[rgba + size_t(channel)] = uint8_t(std::clamp(std::lround(composed), 0l, 255l));
105 color_[rgba + 3u] = 255u;
107 residentColorCurrent_ =
false;
111void FluidSurfaceRenderer::renderVolumeInternal(
const VolumeFluid& sim,
bool colorOnly,
float alpha) {
113 residentColorCurrent_ =
false;
114 const auto copied = anisotropyEnabled_
116 particleOrientations_)
118 const float spacing = copied.value();
120 anisotropicFrame_ = anisotropyEnabled_ && !positions_.empty();
121 if (anisotropicFrame_) buildAnisotropicSplats();
122 const bool uniform = !particleBlendConfigured_ && !volumeColors_.empty() &&
123 std::all_of(volumeColors_.begin() + 1, volumeColors_.end(),
124 [&](
const glm::vec4&
color) { return color == volumeColors_.front(); });
125 uniformVolumeColor_ = uniform ? volumeColors_.front() : glm::vec4(0.f);
126 const bool applyDownsampling = !colorOnly;
127 uniformVolumeGpuShade_ = colorOnly && uniform && preferGpu_;
128 multicolorVolumeGpuShade_ = colorOnly && !uniform && !particleBlendConfigured_ && preferGpu_;
129 if (applyDownsampling && surfaceDownsample_ > 1) {
130 ensureReducedRenderer();
131 reducedRenderer_->positions_ = positions_;
132 reducedRenderer_->anisotropicFrame_ = anisotropicFrame_;
133 if (anisotropicFrame_) {
134 reducedRenderer_->particleRadii_ = particleRadii_;
135 reducedRenderer_->particleOrientations_ = particleOrientations_;
136 reducedRenderer_->buildAnisotropicSplats();
138 reducedRenderer_->renderInternal(reducedRenderer_->positions_,
radius);
139 expandReducedOutputs(
false);
141 renderInternal(positions_,
radius);
143 if (applyDownsampling && thicknessDownsample_ != surfaceDownsample_ && !positions_.empty()) {
144 ensureThicknessRenderer();
145 thicknessRenderer_->positions_ = positions_;
146 thicknessRenderer_->anisotropicFrame_ = anisotropicFrame_;
147 if (anisotropicFrame_) {
148 thicknessRenderer_->particleRadii_ = particleRadii_;
149 thicknessRenderer_->particleOrientations_ = particleOrientations_;
150 thicknessRenderer_->buildAnisotropicSplats();
152 thicknessRenderer_->renderInternal(thicknessRenderer_->positions_,
radius);
153 replaceThicknessFromReduced();
155 const bool gpuShadedVolume = (uniformVolumeGpuShade_ || multicolorVolumeGpuShade_) && gpuOk_;
156 uniformVolumeGpuShade_ =
false;
157 multicolorVolumeGpuShade_ =
false;
158 if (gpuShadedVolume) auxiliaryCurrent_ =
false;
159 if (positions_.empty() || gpuShadedVolume)
return;
161 const auto view = glm::lookAtRH(params_.
eye, params_.
target, params_.
up);
162 const float projectionScale =
165 const size_t pixelCount = size_t(
width) * size_t(
height);
171 const glm::vec4 uniformColor = volumeColors_.front();
173 const auto lightDirection = glm::normalize(glm::vec3(0.35f, 0.65f, 0.55f));
174 for (
size_t i = 0; i < pixelCount; ++i) {
175 if (color_[i * 4u + 3u] == 0 || thickness_[i] * 10.f < thicknessCutoff_) {
176 color_[i * 4u + 0
u] = color_[i * 4u + 1u] = color_[i * 4u + 2u] = color_[i * 4u + 3u] = 0;
179 const auto n = normals_[i];
180 const float diff = std::max(0.f, glm::dot(
n, lightDirection));
182 lighting_ ? std::clamp(ambientMultiplier_ + (1.f - std::min(ambientMultiplier_, 1.f)) * diff, 0.f, 6.f)
184 const float exponent = 4.f + 124.f * smoothness_;
185 const float spec = lighting_ ? std::pow(std::max(
n.z, 0.f), exponent) * smoothness_ * .56f : 0.f;
186 const float fresnel = .04f + .96f * std::pow(1.f - std::max(
n.z, 0.f), 5.f);
187 const glm::vec3 reflected = glm::mix(reflectionColor_, glm::vec3(uniformColor), metalness_);
188 const float reflectionStrength = reflectionEnabled_ ? reflection_ : 0.f;
189 const glm::vec3 shaded =
190 glm::vec3(uniformColor) *
light + reflected * fresnel * reflectionStrength + glm::vec3(spec);
191 for (
int c = 0;
c < 3; ++
c) color_[i * 4u +
size_t(
c)] = uint8_t(255.f * std::clamp(shaded[
c], 0.f, 1.f));
192 color_[i * 4u + 3u] = uint8_t(255.f * std::clamp(thickness_[i] * opacity_, 0.f, 1.f) * uniformColor.a);
197 volumeTint_.assign(pixelCount, particleBlendConfigured_ ? glm::vec4(1.f) : glm::vec4(0.f));
198 volumeTintWeights_.assign(pixelCount, 0.f);
199 for (
size_t particle = 0; particle < positions_.size(); ++particle) {
200 const auto v =
view * glm::vec4(positions_[particle], 1.f);
201 const float z = -
v.z;
202 if (z <= params_.nearZ || z >= params_.
farZ)
continue;
204 const float cx = (0.5f + 0.5f *
v.x / (divisor * projectionScale * aspect)) * float(
width);
205 const float cy = (0.5f - 0.5f *
v.y / (divisor * projectionScale)) * float(
height);
206 const float r = std::max(1.f,
radius *
float(
height) / (2.f * divisor * projectionScale));
207 const int x0 = std::max(0,
int(std::floor(
cx -
r))), x1 = std::min(
width - 1,
int(std::ceil(
cx +
r)));
208 const int y0 = std::max(0,
int(std::floor(
cy -
r))), y1 = std::min(
height - 1,
int(std::ceil(
cy +
r)));
209 for (
int y = y0;
y <= y1; ++
y)
210 for (
int x = x0;
x <= x1; ++
x) {
211 const float dx = (float(
x) + 0.5f -
cx) /
r,
dy = (
float(
y) + 0.5f -
cy) /
r;
213 if (
q <= 0.f)
continue;
214 const size_t at = size_t(
y) * size_t(
width) + size_t(
x);
215 const float fragmentDepth =
z -
radius * std::sqrt(
q);
216 if (fragmentDepth > depth_[
at] +
radius)
continue;
217 if (particleBlendConfigured_) {
218 if (particleDepthWrite_ && volumeTintWeights_[
at] > 0.f && fragmentDepth >= volumeTintWeights_[
at])
220 const glm::vec4
source = volumeColors_[particle];
221 const glm::vec4 destination = volumeTint_[
at];
223 glm::clamp(
source * blendFluidColor(particleBlendSource_,
source, destination) +
224 destination * blendFluidColor(particleBlendDestination_,
source, destination),
225 glm::vec4(0.f), glm::vec4(1.f));
226 blended.a = destination.a;
227 volumeTint_[
at] = blended;
228 volumeTintWeights_[
at] = particleDepthWrite_ ? fragmentDepth : 1.f;
230 volumeTint_[
at] += volumeColors_[particle] *
q;
231 volumeTintWeights_[
at] +=
q;
235 for (
size_t i = 0; i < volumeTint_.size(); ++i) {
236 if (volumeTintWeights_[i] <= 0.f || color_[i * 4u + 3u] == 0 || thickness_[i] * 10.f < thicknessCutoff_) {
237 if (thickness_[i] * 10.f < thicknessCutoff_)
238 color_[i * 4u + 0
u] = color_[i * 4u + 1u] = color_[i * 4u + 2u] = color_[i * 4u + 3u] = 0;
241 const auto base = particleBlendConfigured_
243 : glm::clamp(volumeTint_[i] / volumeTintWeights_[i], glm::vec4(0.f), glm::vec4(1.f));
244 const auto n = normals_[i];
245 const float diff = std::max(0.f, glm::dot(
n, glm::normalize(glm::vec3(.35f, .65f, .55f))));
247 lighting_ ? std::clamp(ambientMultiplier_ + (1.f - std::min(ambientMultiplier_, 1.f)) * diff, 0.f, 6.f)
249 const float exponent = 4.f + 124.f * smoothness_;
250 const float spec = lighting_ ? std::pow(std::max(
n.z, 0.f), exponent) * smoothness_ * .56f : 0.f;
251 const float fresnel = .04f + .96f * std::pow(1.f - std::max(
n.z, 0.f), 5.f);
252 const glm::vec3 reflected = glm::mix(reflectionColor_, glm::vec3(base), metalness_);
253 const float reflectionStrength = reflectionEnabled_ ? reflection_ : 0.f;
254 const glm::vec3 shaded = glm::vec3(base) *
light + reflected * fresnel * reflectionStrength + glm::vec3(spec);
255 for (
int c = 0;
c < 3; ++
c) color_[i * 4u +
size_t(
c)] = uint8_t(255.f * std::clamp(shaded[
c], 0.f, 1.f));
256 color_[i * 4u + 3u] = uint8_t(255.f * std::clamp(thickness_[i] * opacity_, 0.f, 1.f) * base.a);
261 return renderVolumeCloud(sim, absorption,
true);
265 return renderVolumeCloud(sim, absorption,
false);
270 renderVolumeInternal(sim,
false, 1.f);
275 residentColorCurrent_ =
false;
276 if (!std::isfinite(absorption) || absorption < .01f || absorption > 30.f)
278 "Volume absorption must be finite and within [0.01,30]",
279 "fluids.surface.volume-cloud"));
282 size_t selectedCount = particles.size();
285 size_t(std::count_if(particles.begin(), particles.end(), [](
const VolumeFluidParticle& particle) {
286 return particle.material.phase == VolumeFluidPhase::Gas;
288 if (selectedCount > kMaxGasParticles)
290 "Volume frame exceeds the 65536-particle render budget",
291 "fluids.surface.volume-cloud"));
293 volumeColors_.clear();
294 positions_.reserve(selectedCount);
295 volumeColors_.reserve(selectedCount);
296 for (
const auto& particle : particles) {
298 positions_.push_back(particle.position);
299 volumeColors_.push_back(particle.color);
305 const int renderWidth = std::max(8, (
width + 3) / 4);
306 const int renderHeight = std::max(8, (
height + 3) / 4);
307 const size_t renderPixelCount = size_t(renderWidth) * size_t(renderHeight);
308 const size_t pixelCount = size_t(
width) * size_t(
height);
310 const float projectionScale =
312 const float aspect = float(renderWidth) / float(renderHeight);
313 const auto view = glm::lookAtRH(params_.
eye, params_.
target, params_.
up);
315 gasSplats_.reserve(positions_.size());
316 uint64_t coveredPixels = 0;
317 for (
const auto&
position : positions_) {
319 const float z = -
v.z;
320 if (z <= params_.nearZ || z >= params_.
farZ) {
321 gasSplats_.push_back({});
327 splat.cx = (.5f + .5f *
v.x / (divisor * projectionScale * aspect)) * float(renderWidth);
328 splat.cy = (.5f - .5f *
v.y / (divisor * projectionScale)) * float(renderHeight);
329 splat.r = std::max(1.f,
radius *
float(renderHeight) / (2.f * divisor * projectionScale));
330 splat.x0 = std::max(0,
int(std::floor(splat.cx - splat.r)));
331 splat.x1 = std::min(renderWidth - 1,
int(std::ceil(splat.cx + splat.r)));
332 splat.y0 = std::max(0,
int(std::floor(splat.cy - splat.r)));
333 splat.y1 = std::min(renderHeight - 1,
int(std::ceil(splat.cy + splat.r)));
334 if (splat.x1 >= splat.x0 && splat.y1 >= splat.y0)
335 coveredPixels += uint64_t(splat.x1 - splat.x0 + 1) * uint64_t(splat.y1 - splat.y0 + 1);
336 if (coveredPixels > kMaxGasCoveredPixels)
338 "Volume frame exceeds the four-million-pixel splat budget",
339 "fluids.surface.volume-cloud"));
340 gasSplats_.push_back(splat);
343 depthScratch_.assign(renderPixelCount, 1e30f);
344 gasDensityScratch_.assign(renderPixelCount, 0.f);
345 gasTintScratch_.assign(renderPixelCount, glm::vec4(0.f));
346 for (
size_t particle = 0; particle < gasSplats_.size(); ++particle) {
347 const auto& splat = gasSplats_[particle];
348 if (splat.x1 < splat.x0 || splat.y1 < splat.y0)
continue;
349 const float r2 = splat.r * splat.r;
350 for (
int y = splat.y0;
y <= splat.y1; ++
y)
351 for (
int x = splat.x0;
x <= splat.x1; ++
x) {
352 const float dx = float(
x) + .5f - splat.cx,
dy = float(
y) + .5f - splat.cy;
353 const float q = 1.f - (
dx *
dx +
dy *
dy) / r2;
354 if (
q <= 0.f)
continue;
355 const size_t at = size_t(
y) * size_t(renderWidth) + size_t(
x);
357 gasDensityScratch_[
at] += chord;
358 gasTintScratch_[
at] += volumeColors_[particle] * chord;
359 depthScratch_[
at] = std::min(depthScratch_[
at], splat.z -
radius * std::sqrt(
q));
363 thickness_.resize(renderPixelCount);
364 volumeTint_.resize(renderPixelCount);
365 auto blur = [&](
bool horizontal,
const std::vector<float>& srcDensity,
const std::vector<glm::vec4>& srcTint,
366 std::vector<float>& dstDensity, std::vector<glm::vec4>& dstTint) {
367 for (
int y = 0;
y < renderHeight; ++
y)
368 for (
int x = 0;
x < renderWidth; ++
x) {
371 for (
int tap = -3; tap <= 3; ++tap) {
372 const int sx = std::clamp(
x + (horizontal ? tap : 0), 0, renderWidth - 1);
373 const int sy = std::clamp(
y + (horizontal ? 0 : tap), 0, renderHeight - 1);
374 const size_t at = size_t(
sy) * size_t(renderWidth) + size_t(
sx);
375 const float weight = kBlurWeights[tap + 3];
379 const size_t at = size_t(
y) * size_t(renderWidth) + size_t(
x);
380 dstDensity[
at] = density;
384 blur(
true, gasDensityScratch_, gasTintScratch_, thickness_, volumeTint_);
385 blur(
false, thickness_, volumeTint_, gasDensityScratch_, gasTintScratch_);
387 depth_.resize(pixelCount);
388 normals_.assign(pixelCount, glm::vec3(0.f));
389 thickness_.resize(pixelCount);
390 volumeTint_.resize(pixelCount);
391 color_.resize(pixelCount * 4u);
392 for (
size_t i = 0; i < pixelCount; ++i) {
393 const int x = int(i %
size_t(
width)),
y = int(i /
size_t(
width));
394 const int sx = std::min(renderWidth - 1,
x * renderWidth /
width);
395 const int sy = std::min(renderHeight - 1,
y * renderHeight /
height);
396 const size_t source = size_t(
sy) * size_t(renderWidth) + size_t(
sx);
397 depth_[i] = depthScratch_[
source];
398 thickness_[i] = gasDensityScratch_[
source];
399 volumeTint_[i] = gasTintScratch_[
source];
400 const float density = thickness_[i];
401 if (density <= 1e-7f) {
402 color_[i * 4u + 0] = color_[i * 4u + 1] = color_[i * 4u + 2] = color_[i * 4u + 3] = 0;
405 const glm::vec4
tint = glm::clamp(volumeTint_[i] / density, glm::vec4(0.f), glm::vec4(1.f));
406 const float alpha = (1.f - std::exp(-absorption * density)) *
tint.a;
407 const float glow = .72f + .28f * std::clamp(density * absorption, 0.f, 1.f);
408 for (
size_t channel = 0; channel < 3; ++channel)
409 color_[i * 4u + channel] = uint8_t(255.f * std::clamp(
410 tint[
static_cast<glm::vec4::length_type
>(channel)] * glow,
412 color_[i * 4u + 3] = uint8_t(255.f * std::clamp(alpha, 0.f, 1.f));
414 auxiliaryCurrent_ =
true;
std::array< double, 10 > q
std::array< float, 3 > position
#define EV_PROFILE_MODULE(module, name)
Profile the enclosing scope, tagged with a module for grouping.
const UnitySourceAsset & source
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
Result< void > renderGasVolume(const VolumeFluid &sim, float absorption=5.f)
Renders gas particles as a blurred, translucent thickness volume.
const std::vector< uint8_t > & color() const
RGBA8 shaded output.
Result< void > configureRefraction(float transparency, float absorption, float coefficient, int downsample)
Configure Fluid3D-compatible scene refraction controls atomically.
Result< void > compositeConfiguredSceneRefraction(std::span< const uint8_t > sceneColor)
Apply the configured Fluid3D-compatible refraction to a matching opaque RGBA8 scene.
void renderVolume(const VolumeFluid &sim)
Reconstructs a volume-fluid snapshot and shades its diffusing particle colors.
Result< void > renderVolumeInterpolated(const VolumeFluid &sim, float alpha)
Reconstructs from fixed-step interpolated volume positions using the existing SSF pipeline.
Result< void > renderVolumeWithoutSurface(const VolumeFluid &sim, float absorption=5.f)
Renders every material phase as a blurred color/thickness volume without reconstructing a surface.
Result< void > configureRefractionEnabled(bool enabled)
Enables or disables Fluid3D's configured refraction stage without losing its controls.
void renderVolumeColorOnly(const VolumeFluid &sim)
Reconstructs only the RGBA output for display, skipping auxiliary GPU readback when color is uniform.
Result< void > renderConfiguredVolume(const VolumeFluid &sim)
Renders through Fluid3D's configured generateSurface policy.
CPU position-based free-volume fluid with a bounded spatial grid. @ownership Owns all particle state;...
std::span< const VolumeFluidParticle > particleView() const
Borrows the current dense particle pool without copying it.
float spacing() const
Returns the rest lattice spacing without copying state.
Result< float > copyInterpolatedRenderData(float alpha, std::vector< glm::vec3 > &positions, std::vector< glm::vec4 > &colors) const
Fuses interpolated positions and colors into one allocation-reusing renderer pass.
Result< float > copyInterpolatedSurfaceRenderData(float alpha, std::vector< glm::vec3 > &positions, std::vector< glm::vec4 > &colors, std::vector< glm::vec3 > &radii, std::vector< glm::vec4 > &orientations) const
Fuses interpolated positions, colors and ellipsoid shape into one renderer pass.
GLSL compute kernels for the GPU surface-flow solver.
Owning particle value with world position and velocity in SI units.