17#include <glm/gtc/matrix_inverse.hpp>
18#include <glm/common.hpp>
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);
27 const float frac = (
g + 0.99f) / 1.98f;
28 return steps + std::clamp(frac, 0.005f, 0.995f);
31bool projectWorldToScreenUV(
const glm::mat4 &
viewProj,
const glm::vec3 &
world,
float &
u,
36 if (!(
clip.w > 1e-6f))
return false;
40 u = ndcX * 0.5f + 0.5f;
41 v = 1.f - (ndcY * 0.5f + 0.5f);
42 return std::isfinite(
u) && std::isfinite(
v);
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;
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}) {
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);
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);
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;
83 Light2D::Data *data =
nullptr;
94Cam2D findCamera2D(Canvas *canvasFilter) {
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) {
100 if (!
d->active)
continue;
101 if (
d->canvas != canvasFilter)
continue;
105 cam.zoom =
d->zoom <= 1e-6f ? 1.f :
d->zoom;
111void worldToScreen2D(
const Cam2D &cam,
float wx,
float wy,
float viewW,
float viewH,
float &
sx,
118 sx = (
wx - cam.x) * cam.zoom + viewW * 0.5f;
119 sy = (
wy - cam.y) * cam.zoom + viewH * 0.5f;
122void collectVolumetricLights2D(Canvas *canvasFilter, std::vector<PackedVol2D> &out) {
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) {
128 if (!
d->enabled || !
d->volumetric)
continue;
129 if (
d->canvas != canvasFilter)
continue;
132 pl.score =
d->intensity *
d->volumetricIntensity *
133 std::max({
d->r,
d->g,
d->b, 0.f});
136 std::stable_sort(out.begin(), out.end(),
137 [](
const PackedVol2D &
a,
const PackedVol2D &
b) { return a.score > b.score; });
141void uploadSpotCone2D(Volumetric *vol,
const Light2D::Data *
d) {
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);
152 const float len = std::sqrt(
dx *
dx +
dy *
dy);
160 float cosOuter = -2.f;
161 float cosInner = -2.f;
163 vol->setFloat(
"spotDx",
dx);
164 vol->setFloat(
"spotDy",
dy);
165 vol->setFloat(
"spotCosOuter", cosOuter);
166 vol->setFloat(
"spotCosInner", cosInner);
169void sendShadowAnisotropy(Shader *rayShader,
float shadowSteps,
float anisotropy) {
170 if (!rayShader || !rayShader->hasUniform(
"shadowAnisoPack"))
return;
171 rayShader->sendFloat(
"shadowAnisoPack", packShadowAnisotropy(shadowSteps, anisotropy));
174float lightScore(
const VolumetricLight &
light) {
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_);
190void Volumetric::clearPendingEmissiveProxies() { pendingEmissiveProxies_.clear(); }
193 anisotropy_ = std::clamp(
g, -0.99f, 0.99f);
194 uploadRayMarchShadowAnisotropy();
210 std::vector<Scored> scored;
211 auto view = ecs::View<Light3D, Light3D::Data>();
212 for (
auto it =
view.begin(); it !=
view.end(); ++it) {
214 if (!
d->enabled || !
d->volumetric)
continue;
215 if (
d->type ==
"dir") {
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;
228 s.score = lightScore(
light);
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);
242 "Volumetric.driveFromLight3D: null light"));
243 if (!(viewportW > 0.f) || !(viewportH > 0.f) || !std::isfinite(viewportW) ||
244 !std::isfinite(viewportH))
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));
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));
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))
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)
278 float u = 0.5f,
v = 0.5f;
279 if (projectWorldToScreenUV(
viewProj, lightPos,
u,
v))
287 if (ecs::current()->getManager<Light3D>() ==
nullptr)
292 float bestScore = -1.f;
293 auto view = ecs::View<Light3D, Light3D::Data>();
294 for (
auto it =
view.begin(); it !=
view.end(); ++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) {
307 "Volumetric.driveFromPrimarySceneLight3D: no volumetric Light3D"));
312 float ambientB,
const glm::vec3 &worldMin,
313 const glm::vec3 &worldMax,
int maxLights) {
314 if (!atmosphereVolume_)
320 "Volumetric.integrateFroxelFromSceneLights: maxLights must be >= 1"));
321 if (!validWorldBounds(worldMin, worldMax))
324 "Volumetric.integrateFroxelFromSceneLights: worldMin must be finite and < worldMax per axis"));
329 bool fromProxy =
false;
331 std::vector<Scored> scored;
334 std::vector<VolumetricLight>
scene;
340 s.score = lightScore(
light);
346 s.score = lightScore(proxy);
350 std::stable_sort(scored.begin(), scored.end(),
351 [](
const Scored &
a,
const Scored &
b) { return a.score > b.score; });
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);
358 clearPendingEmissiveProxies();
363 atmosphereVolume_->integrate(
ambient, 1.f);
366 atmosphereVolume_->integrateLocalLights(
lights, worldMin, worldMax,
ambient);
371 float defaultRadiusPixels) {
375 "Volumetric.beginOcclusionMapFromSceneLights2D: null graphics"));
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);
384 const float fallbackR = std::max(defaultRadiusPixels, 1.f);
385 const Cam2D cam = findCamera2D(canvasFilter);
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);
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);
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,
417 if (!gfx || !occlusion)
420 "Volumetric.scatterFromSceneLights2D: graphics and occlusion required"));
426 "Volumetric.scatterFromSceneLights2D: occlusion must differ from the active canvas"));
428 std::vector<PackedVol2D>
lights;
429 collectVolumetricLights2D(canvasFilter,
lights);
434 const Cam2D cam = findCamera2D(canvasFilter);
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);
447 gfx->
drawTexturedRectShaderUV(occlusion,
getShader(), 0.f, 0.f,
w,
h, 0.f, 0.f, 1.f, 1.f,
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"));
469 light.color = glm::vec3(std::max(
r, 0.f), std::max(
g, 0.f), std::max(
b, 0.f));
471 light.intensity = intensity;
472 light.enabled =
true;
473 pendingEmissiveProxies_.push_back(
light);
std::vector< eve::ProcgenProbeDesc > lights
std::weak_ptr< PrimitiveScene > scene
std::map< Cell, int > best
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.
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.
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.
Declarative 3D light. Collected by RenderSystem3D (max 8 per frame). type: "point" | "dir" | "spot" (...
bool hasUniform(const std::string &name) const
int getFromVar(const std::string &name, void *data, size_t size) const
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
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.
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:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
void light2dSpotCosines(float angleDeg, float softness, float &cosOuter, float &cosInner)
Backward-compatible alias for lightSpotCosines.
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Local light injected into participating media.