28#include <glm/gtc/matrix_transform.hpp>
37#include <unordered_map>
38#include <unordered_set>
44constexpr float kPi = 3.14159265358979323846f;
46float degToRad(
float d) {
return d * kPi / 180.f; }
48std::string jsonEscape(
const std::string &
s) {
50 out.reserve(
s.size() + 8);
53 case '"': out +=
"\\\"";
break;
54 case '\\': out +=
"\\\\";
break;
55 case '\n': out +=
"\\n";
break;
56 case '\r': out +=
"\\r";
break;
57 case '\t': out +=
"\\t";
break;
64std::string num(
float v) {
66 std::snprintf(buf,
sizeof(buf),
"%g",
static_cast<double>(
v));
70std::string vec3Json(
const glm::vec3 &
v) {
71 return "[" + num(
v.x) +
"," + num(
v.y) +
"," + num(
v.z) +
"]";
76 Graphics *gfx() {
return eve::ModuleManager::getInstance<Graphics>(
"Graphics"); }
79 if (ecs::current()->getManager<Camera3D>() !=
nullptr) {
80 auto view = ecs::View<Camera3D, Camera3D::Data>();
81 for (
auto it =
view.begin(); it !=
view.end(); ++it) {
83 if (data->active && data->entity)
return data->entity;
91 if (
auto *
g =
const_cast<RenderCaptureImpl *
>(
this)->gfx()) {
93 info.height =
g->getHeight();
94 info.pixelWidth =
g->getPixelWidth();
95 info.pixelHeight =
g->getPixelHeight();
96 info.had3DThisFrame =
g->had3DThisFrame();
97 info.readbackEnabled =
g->isScreenReadbackEnabled();
98 info.backend =
g->getBackendName();
103 void setReadbackEnabled(
bool enabled)
override {
104 if (
auto *
g = gfx())
g->setScreenReadbackEnabled(
enabled);
107 bool savePng(
const std::string &
path,
int *outWidth,
int *outHeight,
108 std::string *err)
override {
111 if (err) *err =
"Graphics module not available";
114 g->setScreenReadbackEnabled(
true);
115 image::ImageData *frame =
nullptr;
117 frame =
g->newImageData();
120 *err =
"no presented frame yet: readback was just enabled and the "
121 "copy lands on the next present — call again after one frame "
122 "(screenshots need a running, presented game loop)";
126 if (err) *err =
"no presented frame (enable readback and render a frame first)";
129 filesystem::FileData *png =
nullptr;
131 png = frame->encode(image::ImageData::FormatHandler::ENCODED_PNG,
"frame.png",
false);
134 if (err) *err =
"png encode failed";
139 if (err) *err =
"png encode returned null";
143 std::filesystem::create_directories(std::filesystem::path(
path).parent_path(), ec);
144 std::ofstream out(
path, std::ios::binary);
148 if (err) *err =
"cannot open output file: " +
path;
151 out.write(
static_cast<const char *
>(png->getData()),
152 static_cast<std::streamsize
>(png->getSize()));
153 const bool ok = out.good();
156 if (outWidth) *outWidth =
g->getPixelWidth();
157 if (outHeight) *outHeight =
g->getPixelHeight();
158 if (!
ok && err) *err =
"failed writing: " +
path;
162 std::string capturePngDataUrl()
override {
165 g->setScreenReadbackEnabled(
true);
166 image::ImageData *frame =
nullptr;
168 frame =
g->newImageData();
172 if (!frame)
return {};
173 filesystem::FileData *png =
nullptr;
175 png = frame->encode(image::ImageData::FormatHandler::ENCODED_PNG,
"frame.png",
false);
185 char *b64 =
eve::b64_encode(
static_cast<const char *
>(png->getData()), png->getSize(), 0, len);
186 std::string url =
"data:image/png;base64," + std::string(b64 ? b64 :
"", len);
193 bool ensureCamera()
override {
return camera() !=
nullptr; }
195 bool setCameraPose(
float ex,
float ey,
float ez,
float tx,
float ty,
float tz,
196 float fovYDeg)
override {
198 if (!cam)
return false;
199 cam->setActive(
true);
200 cam->setEye(ex, ey, ez);
201 cam->setTarget(tx, ty, tz);
202 if (fovYDeg > 0.f) cam->setFov(fovYDeg);
206 bool setCameraPoseYawPitch(
float ex,
float ey,
float ez,
float yawDeg,
float pitchDeg,
207 float fovYDeg)
override {
209 if (!cam)
return false;
210 cam->setActive(
true);
211 const float yaw = degToRad(yawDeg);
212 const float pitch = degToRad(pitchDeg);
215 cam->setEye(ex, ey, ez);
216 cam->setTarget(ex +
dir.x * 100.f, ey +
dir.y * 100.f, ez +
dir.z * 100.f);
217 if (fovYDeg > 0.f) cam->setFov(fovYDeg);
221 std::string cameraPoseJson()
override {
223 if (!cam)
return "{\"error\":\"no camera\"}";
224 auto d = cam->data();
225 std::string out =
"{\"eye\":" + vec3Json(glm::vec3(
d->eyeX,
d->eyeY,
d->eyeZ));
226 out +=
",\"target\":" + vec3Json(glm::vec3(
d->targetX,
d->targetY,
d->targetZ));
227 out +=
",\"up\":" + vec3Json(glm::vec3(
d->upX,
d->upY,
d->upZ));
228 out +=
",\"fov\":" + num(
d->fovYDeg);
229 out +=
",\"near\":" + num(
d->nearZ);
230 out +=
",\"far\":" + num(
d->farZ);
231 if (
auto *
g = gfx()) {
232 out +=
",\"viewportWidth\":" + std::to_string(
g->getPixelWidth());
233 out +=
",\"viewportHeight\":" + std::to_string(
g->getPixelHeight());
240 std::uint32_t entityId, std::uint32_t
generation)
override {
241 ecs::EntityHandle
handle{ecs::current(), std::type_index(
typeid(Renderable3D)),
243 auto* renderable =
dynamic_cast<Renderable3D*
>(ecs::try_get(
handle));
247 "Renderable3D handle is missing or stale",
"entity"));
248 auto transform = renderable->transform();
249 auto renderer = renderable->meshRenderer();
252 info.generation = renderable->generation;
253 info.x = transform->x;
254 info.y = transform->y;
255 info.z = transform->z;
256 info.tintR = renderer->r;
257 info.tintG = renderer->g;
258 info.tintB = renderer->b;
259 info.tintA = renderer->a;
260 info.metallic = renderer->metallic;
261 info.roughness = renderer->roughness;
262 info.parallaxScale = renderer->parallaxScale;
263 info.visible = renderer->visible;
264 info.receiveLight = renderer->receiveLight;
265 info.castShadow = renderer->castShadow;
266 info.receiveShadow = renderer->receiveShadow;
267 info.hasPackedMaterial = renderer->material !=
nullptr;
268 info.hasTexture = renderer->texture !=
nullptr || renderer->normalTexture !=
nullptr ||
269 renderer->heightTexture !=
nullptr;
270 info.hasShader = renderer->shader !=
nullptr;
274 std::string visibleEntitiesJson(
float fovYDeg,
bool *
ok)
override {
275 return visibleAt(
nullptr,
nullptr, fovYDeg,
ok);
278 std::string visibleEntitiesJsonAt(
float ex,
float ey,
float ez,
float tx,
float ty,
float tz,
279 float fovYDeg,
bool *
ok)
override {
280 const glm::vec3
eye(ex, ey, ez);
281 const glm::vec3
target(tx, ty, tz);
285 std::string entityIdMaskJson(
int *outWidth,
int *outHeight,
bool *
ok)
override {
288 if (!
g)
return "{\"error\":\"Graphics module not available\"}";
290 if (!cam)
return "{\"error\":\"no camera\"}";
291 auto d = cam->data();
292 const glm::vec3
eye(
d->eyeX,
d->eyeY,
d->eyeZ);
293 const glm::vec3
target(
d->targetX,
d->targetY,
d->targetZ);
294 const glm::vec3
up(
d->upX,
d->upY,
d->upZ);
295 const int vw =
g->getPixelWidth() > 0 ?
g->getPixelWidth() :
g->getWidth();
296 const int vh =
g->getPixelHeight() > 0 ?
g->getPixelHeight() :
g->getHeight();
297 if (outWidth) *outWidth = vw;
298 if (outHeight) *outHeight = vh;
299 if (vw <= 0 || vh <= 0)
return "{\"error\":\"empty viewport\"}";
300 const glm::mat4 viewM = glm::lookAtRH(
eye,
target,
up);
302 d->orthographic, degToRad(
d->fovYDeg),
d->orthoHeight,
303 float(vw) /
float(vh),
d->nearZ,
d->farZ);
304 const glm::mat4
viewProj = projM * viewM;
306 std::vector<Graphics::EntityIdDraw> draws;
310 if (ecs::current()->getManager<scene::SceneHost>() !=
nullptr) {
311 auto hostView = ecs::View<scene::SceneHost, scene::SceneHost::Meta,
312 scene::SceneHost::Tree>();
313 for (
auto it = hostView.begin(); it != hostView.end(); ++it) {
314 auto [meta, tree] = *it;
315 if (!meta || !meta->entity || !tree)
continue;
316 auto *
host = meta->entity;
317 host->walkDepthFirst([&](scene::SceneHost *,
int, scene::SceneNode &
node) {
318 if (!
node.visible ||
node.space !=
"3d")
return;
320 if (!l || !l->target)
return;
321 auto *
r =
static_cast<Renderable3D *
>(l->target);
322 auto mr =
r->meshRenderer();
324 if (!
mesh || !
mesh->gpuHandle)
return;
325 Graphics::EntityIdDraw
draw;
328 const uint32_t packed = uint32_t(
id);
329 draw.idColor = glm::vec4((packed & 255u) / 255.f,
330 ((packed >> 8) & 255u) / 255.f,
331 ((packed >> 16) & 255u) / 255.f, 1.f);
332 draws.push_back(
draw);
334 entities +=
"{\"id\":" + std::to_string(
id) +
341 image::ImageData *img =
g->renderEntityIdMask(draws,
viewProj, vw, vh);
342 if (!img)
return "{\"error\":\"id mask unsupported\"}";
345 return "{\"camera\":" + vec3Json(
eye) +
",\"viewportWidth\":" + std::to_string(vw) +
346 ",\"viewportHeight\":" + std::to_string(vh) +
",\"entities\":[" +
entities +
347 "],\"count\":" + std::to_string(
id - 1) +
"}";
350 bool entityIdMaskPng(
const std::string &
path, std::string *err)
override {
353 if (err) *err =
"Graphics module not available";
358 if (err) *err =
"no camera";
361 auto d = cam->data();
362 const glm::vec3
eye(
d->eyeX,
d->eyeY,
d->eyeZ);
363 const glm::vec3
target(
d->targetX,
d->targetY,
d->targetZ);
364 const glm::vec3
up(
d->upX,
d->upY,
d->upZ);
365 const int vw =
g->getPixelWidth() > 0 ?
g->getPixelWidth() :
g->getWidth();
366 const int vh =
g->getPixelHeight() > 0 ?
g->getPixelHeight() :
g->getHeight();
367 if (vw <= 0 || vh <= 0) {
368 if (err) *err =
"empty viewport";
371 const glm::mat4 viewM = glm::lookAtRH(
eye,
target,
up);
373 d->orthographic, degToRad(
d->fovYDeg),
d->orthoHeight,
374 float(vw) /
float(vh),
d->nearZ,
d->farZ);
375 const glm::mat4
viewProj = projM * viewM;
376 std::vector<Graphics::EntityIdDraw> draws;
379 if (ecs::current()->getManager<scene::SceneHost>() !=
nullptr) {
380 auto hostView = ecs::View<scene::SceneHost, scene::SceneHost::Meta,
381 scene::SceneHost::Tree>();
382 for (
auto it = hostView.begin(); it != hostView.end(); ++it) {
383 auto [meta, tree] = *it;
384 if (!meta || !meta->entity || !tree)
continue;
385 auto *
host = meta->entity;
386 host->walkDepthFirst([&](scene::SceneHost *,
int, scene::SceneNode &
node) {
387 if (!
node.visible ||
node.space !=
"3d")
return;
389 if (!l || !l->target)
return;
390 auto *
r =
static_cast<Renderable3D *
>(l->target);
391 auto mr =
r->meshRenderer();
393 if (!
mesh || !
mesh->gpuHandle)
return;
394 Graphics::EntityIdDraw
draw;
397 const uint32_t packed = uint32_t(
id);
398 draw.idColor = glm::vec4((packed & 255u) / 255.f,
399 ((packed >> 8) & 255u) / 255.f,
400 ((packed >> 16) & 255u) / 255.f, 1.f);
401 draws.push_back(
draw);
406 image::ImageData *img =
g->renderEntityIdMask(draws,
viewProj, vw, vh);
408 if (err) *err =
"id mask unsupported";
411 filesystem::FileData *png =
nullptr;
413 png = img->encode(image::ImageData::FormatHandler::ENCODED_PNG,
"id.png",
false);
416 if (err) *err =
"png encode failed";
422 std::filesystem::create_directories(std::filesystem::path(
path).parent_path(), ec);
423 std::ofstream out(
path, std::ios::binary);
425 out.write(
static_cast<const char *
>(png->getData()),
426 static_cast<std::streamsize
>(png->getSize()));
432 if (!
ok && err) *err =
"failed writing id mask";
436 bool gbufferPng(
const std::string &
name,
const std::string &
path, std::string *err)
override {
439 if (err) *err =
"Graphics module not available";
442 image::ImageData *img =
g->readGBufferToImageData(
name);
444 if (err) *err =
"G-buffer attachment unavailable: " +
name;
447 filesystem::FileData *png =
nullptr;
449 png = img->encode(image::ImageData::FormatHandler::ENCODED_PNG,
"buf.png",
false);
452 if (err) *err =
"png encode failed";
455 const bool ok = png !=
nullptr;
458 std::filesystem::create_directories(std::filesystem::path(
path).parent_path(), ec);
459 std::ofstream out(
path, std::ios::binary);
461 out.write(
static_cast<const char *
>(png->getData()),
462 static_cast<std::streamsize
>(png->getSize()));
467 if (!
ok && err) *err =
"png encode returned null";
472 std::string visibleAt(
const glm::vec3 *
eye,
const glm::vec3 *
target,
float fovYDeg,
478 glm::vec3 camEye(0.f, 1.8f, 3.f);
479 glm::vec3 camTarget(0.f, 1.2f, 0.f);
481 float camNear = 0.1f, camFar = 100.f;
483 auto d = cam->data();
484 camEye = glm::vec3(
d->eyeX,
d->eyeY,
d->eyeZ);
485 camTarget = glm::vec3(
d->targetX,
d->targetY,
d->targetZ);
494 if (fovYDeg > 0.f) camFov = fovYDeg;
496 const int vw =
g ?
g->getPixelWidth() : 1280;
497 const int vh =
g ?
g->getPixelHeight() : 720;
498 const glm::vec3
up(0.f, 1.f, 0.f);
499 const glm::mat4
view = glm::lookAtRH(camEye, camTarget,
up);
504 static const glm::vec3 kLocalCorners[8] = {
505 {-0.5f, -0.5f, -0.5f}, {0.5f, -0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f},
506 {-0.5f, -0.5f, 0.5f}, {0.5f, 0.5f, -0.5f}, {0.5f, -0.5f, 0.5f},
507 {-0.5f, 0.5f, 0.5f}, {0.5f, 0.5f, 0.5f}};
512 if (ecs::current()->getManager<scene::SceneHost>() !=
nullptr) {
513 auto hostView = ecs::View<scene::SceneHost, scene::SceneHost::Meta,
514 scene::SceneHost::Tree>();
515 for (
auto it = hostView.begin(); it != hostView.end(); ++it) {
516 auto [meta, tree] = *it;
517 if (!meta || !meta->entity || !tree)
continue;
518 auto *
host = meta->entity;
519 host->walkDepthFirst([&](scene::SceneHost *,
int, scene::SceneNode &
node) {
520 if (!
node.visible ||
node.space !=
"3d")
return;
521 float minX = 1e30f, minY = 1e30f, minZ = 1e30f;
522 float maxX = -1e30f, maxY = -1e30f, maxZ = -1e30f;
523 float scMinX = 1e30f, scMinY = 1e30f;
524 float scMaxX = -1e30f, scMaxY = -1e30f;
525 bool anyInFront =
false;
526 for (
const auto &
c : kLocalCorners) {
527 const glm::vec4
w =
node.world * glm::vec4(
c, 1.f);
528 minX = std::min(minX,
w.x);
529 minY = std::min(minY,
w.y);
530 minZ = std::min(minZ,
w.z);
531 maxX = std::max(maxX,
w.x);
532 maxY = std::max(maxY,
w.y);
533 maxZ = std::max(maxZ,
w.z);
535 if (
clip.w <= 1e-6f)
continue;
539 const float sx = (ndcX * 0.5f + 0.5f) *
float(vw);
540 const float sy = (ndcY * 0.5f + 0.5f) *
float(vh);
541 scMinX = std::min(scMinX,
sx);
542 scMinY = std::min(scMinY,
sy);
543 scMaxX = std::max(scMaxX,
sx);
544 scMaxY = std::max(scMaxY,
sy);
546 if (!anyInFront)
return;
547 if (scMaxX < 0.f || scMinX >
float(vw) || scMaxY < 0.f ||
554 "\",\"world_aabb\":{\"min\":" + vec3Json(glm::vec3(minX, minY, minZ)) +
555 ",\"max\":" + vec3Json(glm::vec3(maxX, maxY, maxZ)) +
556 "},\"screen_bbox\":{\"x\":" + num(scMinX) +
",\"y\":" + num(scMinY) +
557 ",\"w\":" + num(scMaxX - scMinX) +
",\"h\":" + num(scMaxY - scMinY) +
564 return "{\"camera\":" + vec3Json(camEye) +
",\"cameraTarget\":" + vec3Json(camTarget) +
565 ",\"fov\":" + num(camFov) +
",\"viewportWidth\":" + std::to_string(vw) +
566 ",\"viewportHeight\":" + std::to_string(vh) +
",\"entities\":[" +
entities +
567 "],\"count\":" + std::to_string(
count) +
"}";
575 std::vector<eve::ProcgenProbeDesc>
lights;
578 Result<int> replaceProbeBatch(
const std::string& batchId,
579 const std::vector<eve::ProcgenProbeDesc>& probes)
override {
580 Graphics* graphics = eve::ModuleManager::getInstance<Graphics>(
"Graphics");
581 if (batchId.empty() || !graphics)
583 "procedural probe publication requires a batch id and graphics provider"));
585 std::unordered_set<std::uint64_t> identities;
586 for (
const auto& probe : probes) {
587 if (!probe.sourcePointId || !identities.insert(probe.sourcePointId).second ||
588 probe.resource.empty() || (probe.type != 0 && probe.type != 1) ||
589 !std::isfinite(probe.x) || !std::isfinite(probe.y) || !std::isfinite(probe.z) ||
590 !std::isfinite(probe.extentX) || !std::isfinite(probe.extentY) || !std::isfinite(probe.extentZ) ||
591 probe.extentX <= 0 || probe.extentY <= 0 || probe.extentZ <= 0 ||
592 probe.resolution < 16 || probe.resolution > 2048 ||
593 !std::isfinite(probe.clipDistance) || probe.clipDistance <= 0 ||
594 !std::isfinite(probe.irradianceR) || !std::isfinite(probe.irradianceG) ||
595 !std::isfinite(probe.irradianceB) || probe.irradianceR < 0 || probe.irradianceG < 0 ||
596 probe.irradianceB < 0)
598 "procedural probe publication requires valid unique probe descriptors"));
599 if (probe.hasSphericalHarmonics &&
600 !std::all_of(probe.sphericalHarmonics.begin(), probe.sphericalHarmonics.end(),
601 [](
float coefficient) { return std::isfinite(coefficient); }))
603 "procedural light probe spherical harmonics must be finite"));
604 if (probe.type == 1) {
605 candidate.lights.push_back(probe);
608 auto capture = std::make_unique<ReflectionProbeCapture>(graphics);
609 capture->configure(probe.x, probe.y, probe.z, probe.resolution, 0.1F, probe.clipDistance);
610 capture->configureInfluence(probe.extentX, probe.extentY, probe.extentZ, 1, 1, 0);
611 capture->requestCapture();
612 candidate.reflections.push_back(std::move(capture));
615 for (
auto& probe : candidate.
reflections) registry_->add(probe.get());
616 auto found = batches_.find(batchId);
617 if (
found != batches_.end())
620 batches_.insert_or_assign(batchId, std::move(candidate));
624 Result<int> removeProbeBatch(
const std::string& batchId)
override {
625 auto found = batches_.find(batchId);
626 if (
found == batches_.end())
628 "procedural probe batch was not published", batchId));
629 const int count =
static_cast<int>(
found->second.reflections.size() +
found->second.lights.size());
633 batches_.erase(
found);
637 Result<int> tickProbeBatches(
int faceBudget,
int filterBudget,
int filterSamples)
override {
638 if (faceBudget < 0 || filterBudget < 0 || filterSamples < 8 || filterSamples > 512)
640 "procedural probe tick requires nonnegative budgets and 8..512 filter samples"));
644 int probeCount(
const std::string& batchId)
const override {
645 auto found = batches_.find(batchId);
646 return found == batches_.end() ? 0 :
static_cast<int>(
found->second.reflections.size() +
found->second.lights.size());
648 int reflectionProbeCount(
const std::string& batchId)
const override {
649 auto found = batches_.find(batchId);
650 return found == batches_.end() ? 0 :
static_cast<int>(
found->second.reflections.size());
652 int lightProbeCount(
const std::string& batchId)
const override {
653 auto found = batches_.find(batchId);
654 return found == batches_.end() ? 0 :
static_cast<int>(
found->second.lights.size());
657 void ensureRegistry() {
658 if (!registry_) registry_ = std::make_unique<ReflectionProbeRegistry>();
660 std::unique_ptr<ReflectionProbeRegistry> registry_;
661 std::unordered_map<std::string, Batch> batches_;
667 static RenderCaptureImpl
impl;
668 static ProcgenProbeSinkImpl probeSink;
669 eve::cap::provide<eve::IRenderCapture>(&
impl);
670 eve::cap::provide<eve::IProcgenProbeSink>(&probeSink);
std::vector< eve::ProcgenProbeDesc > lights
std::vector< std::unique_ptr< ReflectionProbeCapture > > reflections
wgpu::PopErrorScopeStatus status
std::vector< ecs::EntityHandle > entities
Renderable3D::MeshRenderer * mr
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.
Optional graphics consumer for complete generated probe batches. Implementations own all runtime reso...
Frame capture + camera + visible-entity inspection (graphics).
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.
static Camera3D * createCamera()
void removeBatch(const std::string &batchId)
Remove every diffuse probe owned by one generated batch.
void replaceBatch(const std::string &batchId, const std::vector< eve::ProcgenProbeDesc > &probes)
Atomically replace the diffuse probes owned by one generated batch.
static DiffuseLightProbeRegistry & instance()
Return the process-wide graphics-owned registry.
卡牌游戏 UI 工具模块:工厂 + 脚本绑定入口。 功能参考 ycarowr/UiCard:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
glm::mat4 cameraProjectionVulkanRH_ZO(bool orthographic, float fovyRad, float orthoHeight, float aspect, float zNear, float zFar)
Camera projection shared by rendering, picking, and diagnostic passes.
glm::mat4 perspectiveVulkanRH_ZO(float fovyRad, float aspect, float zNear, float zFar)
Right-handed, zero-to-one depth perspective for Vulkan swapchains.
void registerGraphicsCapabilities()
Registers graphics capabilities.
int findLinkKind(const char *kind)
Finds link kind.
char * b64_encode(const char *src, size_t srclen, size_t linelen, size_t &dstlen)
Base64-encode data.
Render surface / frame state snapshot (value type).
Generation-qualified snapshot of one live field-backed Renderable3D.