载入中...
搜索中...
未找到
VolumetricLights.cpp
浏览该文件的文档.
2
3#include "common/ECS.h"
4#include "common/Exception.h"
6#include "graphics/Canvas.h"
7#include "graphics/Graphics.h"
8#include "graphics/Light.h"
10#include "graphics/Shader.h"
11#include "graphics/Texture.h"
12
13#include <algorithm>
14#include <cmath>
15#include <limits>
16
17#include <glm/gtc/matrix_inverse.hpp>
18#include <glm/common.hpp>
19
20namespace eve::graphics {
21namespace {
22
23float packShadowAnisotropy(float shadowSteps, float anisotropy) {
24 const float steps = std::clamp(std::floor(shadowSteps + 0.5f), 0.f, 32.f);
25 const float g = std::clamp(anisotropy, -0.99f, 0.99f);
26 // fract in [0.005, 0.995] so a pure integer upload still reads as legacy g=0.6.
27 const float frac = (g + 0.99f) / 1.98f;
28 return steps + std::clamp(frac, 0.005f, 0.995f);
29}
30
31bool projectWorldToScreenUV(const glm::mat4 &viewProj, const glm::vec3 &world, float &u,
32 float &v) {
33 const glm::vec4 clip = viewProj * glm::vec4(world, 1.f);
34 // Reject points behind the camera (negative homogeneous w) — mirrored UVs
35 // would otherwise place rear lights as visible shaft origins.
36 if (!(clip.w > 1e-6f)) return false;
37 const float ndcX = clip.x / clip.w;
38 const float ndcY = clip.y / clip.w;
39 // Match Vulkan Y-down UV used by volumetric shaders.
40 u = ndcX * 0.5f + 0.5f;
41 v = 1.f - (ndcY * 0.5f + 0.5f);
42 return std::isfinite(u) && std::isfinite(v);
43}
44
45glm::vec3 safeNormalize(const glm::vec3 &v, const glm::vec3 &fallback) {
46 const float len = glm::length(v);
47 if (len < 1e-6f) return fallback;
48 return v / len;
49}
50
52void frustumWorldBounds(const glm::mat4 &invViewProj, glm::vec3 &worldMin, glm::vec3 &worldMax) {
53 worldMin = glm::vec3(std::numeric_limits<float>::max());
54 worldMax = glm::vec3(std::numeric_limits<float>::lowest());
55 for (float ndcZ : {0.f, 1.f}) {
56 for (float x : {-1.f, 1.f}) {
57 for (float y : {-1.f, 1.f}) {
58 const glm::vec4 h = invViewProj * glm::vec4(x, y, ndcZ, 1.f);
59 if (!(std::fabs(h.w) > 1e-6f)) continue;
60 const glm::vec3 p = glm::vec3(h) / h.w;
61 if (!std::isfinite(p.x) || !std::isfinite(p.y) || !std::isfinite(p.z)) continue;
62 worldMin = glm::min(worldMin, p);
63 worldMax = glm::max(worldMax, p);
64 }
65 }
66 }
67 // Degenerate bounds: give integrateLocalLights a usable default box.
68 if (!(worldMin.x < worldMax.x && worldMin.y < worldMax.y && worldMin.z < worldMax.z)) {
69 worldMin = glm::vec3(-50.f);
70 worldMax = glm::vec3(50.f);
71 }
72}
73
74bool validWorldBounds(const glm::vec3 &worldMin, const glm::vec3 &worldMax) {
75 for (int i = 0; i < 3; ++i) {
76 if (!std::isfinite(worldMin[i]) || !std::isfinite(worldMax[i])) return false;
77 if (!(worldMin[i] < worldMax[i])) return false;
78 }
79 return true;
80}
81
82struct PackedVol2D {
83 Light2D::Data *data = nullptr;
84 float score = 0.f;
85};
86
87struct Cam2D {
88 float x = 0.f;
89 float y = 0.f;
90 float zoom = 1.f;
91 bool valid = false;
92};
93
94Cam2D findCamera2D(Canvas *canvasFilter) {
95 Cam2D cam;
96 if (ecs::current()->getManager<Camera2D>() == nullptr) return cam;
97 auto view = ecs::View<Camera2D, Camera2D::Data>();
98 for (auto it = view.begin(); it != view.end(); ++it) {
99 auto [d] = *it;
100 if (!d->active) continue;
101 if (d->canvas != canvasFilter) continue;
102 cam.valid = true;
103 cam.x = d->x;
104 cam.y = d->y;
105 cam.zoom = d->zoom <= 1e-6f ? 1.f : d->zoom;
106 return cam;
107 }
108 return cam;
109}
110
111void worldToScreen2D(const Cam2D &cam, float wx, float wy, float viewW, float viewH, float &sx,
112 float &sy) {
113 if (!cam.valid) {
114 sx = wx;
115 sy = wy;
116 return;
117 }
118 sx = (wx - cam.x) * cam.zoom + viewW * 0.5f;
119 sy = (wy - cam.y) * cam.zoom + viewH * 0.5f;
120}
121
122void collectVolumetricLights2D(Canvas *canvasFilter, std::vector<PackedVol2D> &out) {
123 out.clear();
124 if (ecs::current()->getManager<Light2D>() == nullptr) return;
125 auto view = ecs::View<Light2D, Light2D::Data>();
126 for (auto it = view.begin(); it != view.end(); ++it) {
127 auto [d] = *it;
128 if (!d->enabled || !d->volumetric) continue;
129 if (d->canvas != canvasFilter) continue;
130 PackedVol2D pl;
131 pl.data = d;
132 pl.score = d->intensity * d->volumetricIntensity *
133 std::max({d->r, d->g, d->b, 0.f});
134 out.push_back(pl);
135 }
136 std::stable_sort(out.begin(), out.end(),
137 [](const PackedVol2D &a, const PackedVol2D &b) { return a.score > b.score; });
138}
139
141void uploadSpotCone2D(Volumetric *vol, const Light2D::Data *d) {
142 if (!vol) return;
143 if (!d || d->type != "spot") {
144 vol->setFloat("spotDx", 0.f);
145 vol->setFloat("spotDy", -1.f);
146 vol->setFloat("spotCosOuter", -2.f);
147 vol->setFloat("spotCosInner", -2.f);
148 return;
149 }
150 float dx = d->dx;
151 float dy = d->dy;
152 const float len = std::sqrt(dx * dx + dy * dy);
153 if (len > 1e-6f) {
154 dx /= len;
155 dy /= len;
156 } else {
157 dx = 0.f;
158 dy = -1.f;
159 }
160 float cosOuter = -2.f;
161 float cosInner = -2.f;
162 light2dSpotCosines(d->spotAngleDeg, d->spotSoftness, cosOuter, cosInner);
163 vol->setFloat("spotDx", dx);
164 vol->setFloat("spotDy", dy);
165 vol->setFloat("spotCosOuter", cosOuter);
166 vol->setFloat("spotCosInner", cosInner);
167}
168
169void sendShadowAnisotropy(Shader *rayShader, float shadowSteps, float anisotropy) {
170 if (!rayShader || !rayShader->hasUniform("shadowAnisoPack")) return;
171 rayShader->sendFloat("shadowAnisoPack", packShadowAnisotropy(shadowSteps, anisotropy));
172}
173
174float lightScore(const VolumetricLight &light) {
175 return light.intensity * std::max({light.color.x, light.color.y, light.color.z, 0.f});
176}
177
178} // namespace
179
180void Volumetric::uploadRayMarchShadowAnisotropy() {
181 if (!rayShader_ || !rayShader_->hasUniform("shadowAnisoPack")) return;
182 float shadowSteps = 8.f;
183 float existing = 0.f;
184 if (rayShader_->getFromVar("shadowAnisoPack", &existing, sizeof(existing)) ==
185 int(sizeof(existing)))
186 shadowSteps = std::floor(existing);
187 sendShadowAnisotropy(rayShader_, shadowSteps, anisotropy_);
188}
189
190void Volumetric::clearPendingEmissiveProxies() { pendingEmissiveProxies_.clear(); }
191
193 anisotropy_ = std::clamp(g, -0.99f, 0.99f);
194 uploadRayMarchShadowAnisotropy();
195}
196
197float Volumetric::getAnisotropy() const { return anisotropy_; }
198
199Result<int> Volumetric::collectSceneLights3D(std::vector<VolumetricLight> &out, int maxCount) {
200 out.clear();
201 if (maxCount < 1)
203 DiagnosticCode::InvalidArgument, "Volumetric.collectSceneLights3D: maxCount must be >= 1"));
204 if (ecs::current()->getManager<Light3D>() == nullptr) return Result<int>::success(0);
205
206 struct Scored {
208 float score = 0.f;
209 };
210 std::vector<Scored> scored;
211 auto view = ecs::View<Light3D, Light3D::Data>();
212 for (auto it = view.begin(); it != view.end(); ++it) {
213 auto [d] = *it;
214 if (!d->enabled || !d->volumetric) continue;
215 if (d->type == "dir") {
216 // Directional lights are handled by driveFromLight3D / uniform integrate;
217 // local froxel lights need a position, so skip dirs here.
218 continue;
219 }
221 light.position = glm::vec3(d->x, d->y, d->z);
222 light.color = glm::vec3(std::max(d->r, 0.f), std::max(d->g, 0.f), std::max(d->b, 0.f));
223 light.radius = std::max(d->radius, 0.f);
224 light.intensity = std::max(d->intensity, 0.f) * std::max(d->volumetricIntensity, 0.f);
225 light.enabled = true;
226 Scored s;
227 s.light = light;
228 s.score = lightScore(light);
229 scored.push_back(s);
230 }
231 std::stable_sort(scored.begin(), scored.end(),
232 [](const Scored &a, const Scored &b) { return a.score > b.score; });
233 const int n = std::min(int(scored.size()), maxCount);
234 out.reserve(std::size_t(n));
235 for (int i = 0; i < n; ++i) out.push_back(scored[std::size_t(i)].light);
236 return Result<int>::success(n);
237}
238
239Result<void> Volumetric::driveFromLight3D(Light3D *light, float viewportW, float viewportH) {
240 if (!light)
242 "Volumetric.driveFromLight3D: null light"));
243 if (!(viewportW > 0.f) || !(viewportH > 0.f) || !std::isfinite(viewportW) ||
244 !std::isfinite(viewportH))
246 DiagnosticCode::InvalidArgument, "Volumetric.driveFromLight3D: viewport must be positive"));
247
248 auto d = light->data();
249 if (!d->enabled)
251 Diagnostic::error(DiagnosticCode::NotFound, "Volumetric.driveFromLight3D: light disabled"));
252
253 const float volScale = std::max(d->volumetricIntensity, 0.f);
254 setShaftColor(std::max(d->r, 0.f), std::max(d->g, 0.f), std::max(d->b, 0.f));
255 setIntensity(std::max(d->intensity, 0.f) * volScale);
256
257 if (d->type == "dir") {
258 const glm::vec3 dir = safeNormalize(glm::vec3(d->dx, d->dy, d->dz), glm::vec3(0.f, 1.f, 0.f));
259 setLightDirection(dir.x, dir.y, dir.z);
260 // Place the SS occlusion target toward the light direction on the far sky.
261 const glm::mat4 viewProj = glm::inverse(invViewProj_);
262 const glm::vec4 eyeH = invViewProj_ * glm::vec4(0.f, 0.f, 0.f, 1.f);
263 const glm::vec3 eye = glm::vec3(eyeH) / std::max(eyeH.w, 1e-6f);
264 const glm::vec3 towardLight = eye + dir * farZ_;
265 float u = 0.7f, v = 0.2f;
266 if (projectWorldToScreenUV(viewProj, towardLight, u, v))
267 setLightScreenUV(std::clamp(u, 0.f, 1.f), std::clamp(v, 0.f, 1.f));
268 return Result<void>::success();
269 }
270
271 const glm::mat4 viewProj = glm::inverse(invViewProj_);
272 const glm::vec4 eyeH = invViewProj_ * glm::vec4(0.f, 0.f, 0.f, 1.f);
273 const glm::vec3 eye = glm::vec3(eyeH) / std::max(eyeH.w, 1e-6f);
274 const glm::vec3 lightPos(d->x, d->y, d->z);
275 const glm::vec3 toLight = lightPos - eye;
276 if (glm::dot(toLight, toLight) > 1e-8f)
277 setLightDirection(toLight.x, toLight.y, toLight.z);
278 float u = 0.5f, v = 0.5f;
279 if (projectWorldToScreenUV(viewProj, lightPos, u, v))
280 setLightScreenUV(std::clamp(u, -0.5f, 1.5f), std::clamp(v, -0.5f, 1.5f));
281 (void)viewportW;
282 (void)viewportH;
283 return Result<void>::success();
284}
285
287 if (ecs::current()->getManager<Light3D>() == nullptr)
289 DiagnosticCode::NotFound, "Volumetric.driveFromPrimarySceneLight3D: no Light3D manager"));
290
291 Light3D *best = nullptr;
292 float bestScore = -1.f;
293 auto view = ecs::View<Light3D, Light3D::Data>();
294 for (auto it = view.begin(); it != view.end(); ++it) {
295 auto [d] = *it;
296 if (!d->enabled || !d->volumetric || !d->entity) continue;
297 const float score = d->intensity * d->volumetricIntensity *
298 std::max({d->r, d->g, d->b, 0.f});
299 if (score > bestScore) {
300 bestScore = score;
301 best = d->entity;
302 }
303 }
304 if (!best)
307 "Volumetric.driveFromPrimarySceneLight3D: no volumetric Light3D"));
308 return driveFromLight3D(best, viewportW, viewportH);
309}
310
312 float ambientB, const glm::vec3 &worldMin,
313 const glm::vec3 &worldMax, int maxLights) {
314 if (!atmosphereVolume_)
316 DiagnosticCode::Failed, "Volumetric.integrateFroxelFromSceneLights: no atmosphere volume"));
317 if (maxLights < 1)
320 "Volumetric.integrateFroxelFromSceneLights: maxLights must be >= 1"));
321 if (!validWorldBounds(worldMin, worldMax))
324 "Volumetric.integrateFroxelFromSceneLights: worldMin must be finite and < worldMax per axis"));
325
326 struct Scored {
328 float score = 0.f;
329 bool fromProxy = false;
330 };
331 std::vector<Scored> scored;
332
333 // Collect all eligible scene lights without applying the budget yet.
334 std::vector<VolumetricLight> scene;
335 auto collected = collectSceneLights3D(scene, std::numeric_limits<int>::max() / 4);
336 if (!collected.ok()) return Result<void>::failure(collected.status());
337 for (const VolumetricLight &light : scene) {
338 Scored s;
339 s.light = light;
340 s.score = lightScore(light);
341 scored.push_back(s);
342 }
343 for (const VolumetricLight &proxy : pendingEmissiveProxies_) {
344 Scored s;
345 s.light = proxy;
346 s.score = lightScore(proxy);
347 s.fromProxy = true;
348 scored.push_back(s);
349 }
350 std::stable_sort(scored.begin(), scored.end(),
351 [](const Scored &a, const Scored &b) { return a.score > b.score; });
352
353 std::vector<VolumetricLight> lights;
354 const int n = std::min(int(scored.size()), maxLights);
355 lights.reserve(std::size_t(n));
356 for (int i = 0; i < n; ++i) lights.push_back(scored[std::size_t(i)].light);
357 // One-shot proxies are consumed by this integrate call once ranked into the pool.
358 clearPendingEmissiveProxies();
359
360 const glm::vec3 ambient(std::max(ambientR, 0.f), std::max(ambientG, 0.f),
361 std::max(ambientB, 0.f));
362 if (lights.empty()) {
363 atmosphereVolume_->integrate(ambient, 1.f);
364 return Result<void>::success();
365 }
366 atmosphereVolume_->integrateLocalLights(lights, worldMin, worldMax, ambient);
367 return Result<void>::success();
368}
369
371 float defaultRadiusPixels) {
372 if (!gfx)
375 "Volumetric.beginOcclusionMapFromSceneLights2D: null graphics"));
376
377 std::vector<PackedVol2D> lights;
378 collectVolumetricLights2D(canvasFilter, lights);
379 gfx->clear(Color(0.f, 0.f, 0.f, 1.f), std::nullopt, std::nullopt);
380 if (lights.empty()) return Result<int>::success(0);
381
382 const float w = gfx->getCanvas() ? float(gfx->getCanvas()->getWidth()) : float(gfx->getWidth());
383 const float h = gfx->getCanvas() ? float(gfx->getCanvas()->getHeight()) : float(gfx->getHeight());
384 const float fallbackR = std::max(defaultRadiusPixels, 1.f);
385 const Cam2D cam = findCamera2D(canvasFilter);
386
387 const auto *primary = lights.front().data;
388 float px = 0.f, py = 0.f;
389 worldToScreen2D(cam, primary->x, primary->y, w, h, px, py);
391 setShaftColor(std::max(primary->r, 0.f), std::max(primary->g, 0.f),
392 std::max(primary->b, 0.f));
393 setIntensity(std::max(primary->intensity, 0.f) * std::max(primary->volumetricIntensity, 0.f));
394 uploadSpotCone2D(this, primary);
395
396 int drawn = 0;
397 for (const PackedVol2D &pl : lights) {
398 const auto *d = pl.data;
399 const float boost = std::clamp(d->volumetricIntensity, 0.25f, 4.f);
400 float sx = 0.f, sy = 0.f;
401 worldToScreen2D(cam, d->x, d->y, w, h, sx, sy);
402 // Prefer the light's configured radius (world units → screen via zoom);
403 // otherwise use defaultRadiusPixels when radius is unset/non-positive.
404 float r = d->radius > 0.f ? d->radius : fallbackR;
405 if (cam.valid) r *= cam.zoom;
406 r = std::max(r * boost, 1.f);
407 const float lum = std::clamp(std::max({d->r, d->g, d->b}) * d->intensity * boost, 0.35f,
408 1.f);
409 gfx->drawSolidRect(sx - r, sy - r, r * 2.f, r * 2.f, Color(lum, lum, lum, 1.f));
410 ++drawn;
411 }
412 return Result<int>::success(drawn);
413}
414
416 Canvas *canvasFilter) {
417 if (!gfx || !occlusion)
420 "Volumetric.scatterFromSceneLights2D: graphics and occlusion required"));
421 // Sampling the active canvas texture while drawing into it is invalid on
422 // Vulkan/WebGPU (same image as color attachment + sampled texture).
423 if (gfx->getCanvas() && gfx->getCanvas()->getTexture() == occlusion)
426 "Volumetric.scatterFromSceneLights2D: occlusion must differ from the active canvas"));
427
428 std::vector<PackedVol2D> lights;
429 collectVolumetricLights2D(canvasFilter, lights);
430 if (lights.empty()) return Result<int>::success(0);
431
432 const float w = gfx->getCanvas() ? float(gfx->getCanvas()->getWidth()) : float(gfx->getWidth());
433 const float h = gfx->getCanvas() ? float(gfx->getCanvas()->getHeight()) : float(gfx->getHeight());
434 const Cam2D cam = findCamera2D(canvasFilter);
435
436 uploadCommon(false);
437 int passes = 0;
438 for (const PackedVol2D &pl : lights) {
439 const auto *d = pl.data;
440 float sx = 0.f, sy = 0.f;
441 worldToScreen2D(cam, d->x, d->y, w, h, sx, sy);
443 setShaftColor(std::max(d->r, 0.f), std::max(d->g, 0.f), std::max(d->b, 0.f));
444 setIntensity(std::max(d->intensity, 0.f) * std::max(d->volumetricIntensity, 0.f));
445 uploadSpotCone2D(this, d);
446 // Additive so overlapping multipass shafts accumulate instead of SrcAlpha-suppressing.
447 gfx->drawTexturedRectShaderUV(occlusion, getShader(), 0.f, 0.f, w, h, 0.f, 0.f, 1.f, 1.f,
448 Color(1.f, 1.f, 1.f, 1.f), false, BlendMode::Additive);
449 ++passes;
450 }
451 return Result<int>::success(passes);
452}
453
454Result<void> Volumetric::injectEmissiveLightProxy(float x, float y, float z, float r, float g,
455 float b, float radius, float intensity) {
456 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || !std::isfinite(r) ||
457 !std::isfinite(g) || !std::isfinite(b) || !std::isfinite(radius) ||
458 !std::isfinite(intensity))
461 "Volumetric.injectEmissiveLightProxy: finite parameters required"));
462 if (radius <= 0.f || intensity < 0.f)
465 "Volumetric.injectEmissiveLightProxy: radius > 0 and intensity >= 0 required"));
466
468 light.position = glm::vec3(x, y, z);
469 light.color = glm::vec3(std::max(r, 0.f), std::max(g, 0.f), std::max(b, 0.f));
470 light.radius = radius;
471 light.intensity = intensity;
472 light.enabled = true;
473 pendingEmissiveProxies_.push_back(light);
474 return Result<void>::success();
475}
476
477} // namespace eve::graphics
double score
Definition Agent.cpp:50
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
const std::string & s
float py
glm::vec4 p[6]
tensor::Graph g
Definition GpuGraph.cpp:7
std::vector< eve::ProcgenProbeDesc > lights
glm::vec4 clip
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
double r
float v
int h
std::vector< Colorf > px
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
World3D * world
float radius
std::weak_ptr< PrimitiveScene > scene
float d
int steps
glm::vec3 eye
glm::mat4 viewProj
glm::mat4 view
float zoom
float ambientG
float ambientR
float ambientB
Light3D::Data * light
float dy
float dx
std::map< Cell, int > best
V3 dir
Definition TreeMesh.cpp:150
float wx
float wy
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.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Canvas public API.
Definition Canvas.h:17
virtual int getWidth() const =0
Returns the width.
virtual void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth)=0
Clears .
virtual Texture * getTexture()=0
Sampleable color buffer; screen Canvas returns nullptr.
virtual int getHeight() const =0
Returns the height.
int getWidth() const
Returns the width.
Definition Graphics.h:502
virtual void drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color, bool rotatedUV=false, BlendMode blend=BlendMode::Alpha)=0
UV draw with an explicit Shader (nullptr = default textured pipeline).
virtual Canvas * getCanvas() const =0
Returns the canvas.
virtual void drawSolidRect(float x, float y, float w, float h, float r, float g, float b, float a=1.f)
RGBA-float overload matching the script-facing drawSolidRect name.
int getHeight() const
Returns the height.
Definition Graphics.h:504
Declarative 3D light. Collected by RenderSystem3D (max 8 per frame). type: "point" | "dir" | "spot" (...
Definition Light.h:191
bool hasUniform(const std::string &name) const
Definition Shader.cpp:59
int getFromVar(const std::string &name, void *data, size_t size) const
Definition Shader.cpp:84
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
Result< void > driveFromLight3D(Light3D *light, float viewportW, float viewportH)
Drive shaft UV/direction/color/intensity from one Light3D using setCamera(). Directional lights set l...
Result< void > driveFromPrimarySceneLight3D(float viewportW, float viewportH)
Drive shafts from the strongest volumetric Light3D currently in ECS.
void setLightScreenPos(float x, float y, float width, float height)
Pixel-space helper (converts with width/height).
Result< int > scatterFromSceneLights2D(Graphics *gfx, Texture *occlusion, Canvas *canvasFilter=nullptr)
Multi-pass screenspace scatter: one radial blur per volumetric Light2D. Uses additive blending so ove...
Result< int > collectSceneLights3D(std::vector< VolumetricLight > &out, int maxCount=32)
Collect enabled Light3D with volumetric=true into froxel light snapshots.
void setLightScreenUV(float u, float v)
Light position in UV (0..1), origin top-left to match 2D UVs.
Shader * getShader() const
Returns the shader.
Definition Volumetric.h:323
void setIntensity(float intensity)
Sets the intensity.
void setAnisotropy(float g)
Henyey–Greenstein anisotropy for raymarch shafts in [-0.99, 0.99]. Positive values produce forward-sc...
void setLightDirection(float dx, float dy, float dz)
World-space direction toward the lit surface (ray march / phase).
Result< void > integrateFroxelFromSceneLights(float ambientR, float ambientG, float ambientB, const glm::vec3 &worldMin, const glm::vec3 &worldMax, int maxLights=16)
Integrate froxels with volumetric Light3D (+ emissive proxies) from ECS. Falls back to uniform ambien...
void setShaftColor(float r, float g, float b)
Sets the shaft color.
Result< void > injectEmissiveLightProxy(float x, float y, float z, float r, float g, float b, float radius, float intensity)
Inject a one-shot emissive glow proxy into the froxel integrate light list. Accumulates until the nex...
Result< int > beginOcclusionMapFromSceneLights2D(Graphics *gfx, Canvas *canvasFilter=nullptr, float defaultRadiusPixels=24.f)
Clear the occlusion canvas and draw bright discs for every volumetric Light2D. Sets the primary (brig...
float getAnisotropy() const
Current raymarch anisotropy.
卡牌游戏 UI 工具模块:工厂 + 脚本绑定入口。 功能参考 ycarowr/UiCard:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
Definition Animation.h:25
void light2dSpotCosines(float angleDeg, float softness, float &cosOuter, float &cosInner)
Backward-compatible alias for lightSpotCosines.
Definition Light.h:40
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
Local light injected into participating media.
glm::vec4 ambient
glm::mat4 invViewProj