载入中...
搜索中...
未找到
RenderSystem3D.cpp
浏览该文件的文档.
2#include "common/Capability.h"
3#include "common/Exception.h"
5#include "common/Status.h"
13#include "graphics/Graphics.h"
15#include "graphics/Light.h"
16#include "graphics/Material.h"
17#include "graphics/Mesh.h"
18#include "graphics/Outline.h"
23#include "graphics/Shader.h"
24#include "graphics/Shadow.h"
26#include "graphics/Texture.h"
28
29#include <algorithm>
30#include <cmath>
31#include <glm/gtc/matrix_inverse.hpp>
32#include <glm/gtc/matrix_transform.hpp>
33#include <vector>
34
35namespace eve::graphics {
36
37namespace {
38
39std::vector<RenderSystem3D::GBufferExtraDrawer> g_gbufferDrawers;
40struct ShadowExtraDrawerEntry {
41 uint64_t token = 0;
43};
44std::vector<ShadowExtraDrawerEntry> g_shadowDrawers;
45uint64_t g_nextShadowDrawerToken = 1;
46std::vector<RenderSystem3D::DecalExtraDrawer> g_decalDrawers;
47struct CaptureExtraDrawerEntry {
48 uint64_t token = 0;
49 uint32_t mask = 0;
51};
52std::vector<CaptureExtraDrawerEntry> g_captureDrawers;
53uint64_t g_nextCaptureDrawerToken = 1;
54struct ForwardExtraDrawerEntry {
55 uint64_t token = 0;
57};
58std::vector<ForwardExtraDrawerEntry> g_forwardDrawers;
59uint64_t g_nextForwardExtraDrawerToken = 1;
60struct GpuOpaqueCollectorEntry {
61 uint64_t token = 0;
63};
64std::vector<GpuOpaqueCollectorEntry> g_gpuOpaqueCollectors;
65uint64_t g_nextGpuOpaqueCollectorToken = 1;
66
67glm::vec3 gLightDir = glm::normalize(glm::vec3(0.4f, 1.f, 0.3f));
68glm::vec3 gLightColor = glm::vec3(1.f);
69
70glm::mat4 modelFromTransform(const Renderable3D::Transform3D& t) {
71 glm::mat4 m(1.f);
72 m = glm::translate(m, glm::vec3(t.x, t.y, t.z));
73 m = glm::rotate(m, t.yaw, glm::vec3(0.f, 1.f, 0.f));
74 m = glm::rotate(m, t.pitch, glm::vec3(1.f, 0.f, 0.f));
75 m = glm::rotate(m, t.roll, glm::vec3(0.f, 0.f, 1.f));
76 m = glm::scale(m, glm::vec3(t.sx, t.sy, t.sz));
77 return m;
78}
79
80Camera3D* findDefaultCamera3D() {
81 if (ecs::current()->getManager<Camera3D>() == nullptr) return nullptr;
82 auto camView = ecs::View<Camera3D, Camera3D::Data>();
83 for (auto it = camView.begin(); it != camView.end(); ++it) {
84 auto [data] = *it;
85 if (!data->active || !data->entity) continue;
86 return data->entity;
87 }
88 return nullptr;
89}
90
91struct PackedLight3D {
92 Light3D::Data* data = nullptr;
93 bool isPoint = true;
94};
95
96void collectLights3D(std::vector<PackedLight3D>& out, size_t maxCount) {
97 out.clear();
98 if (ecs::current()->getManager<Light3D>() == nullptr) return;
99 auto view = ecs::View<Light3D, Light3D::Data>();
100 for (auto it = view.begin(); it != view.end(); ++it) {
101 auto [d] = *it;
102 d->shadowLocalSlot = -1; // cleared each frame; selectShadowCasters reassigns
103 if (!d->enabled) continue;
104 if (d->volumetricOnly) continue; // emissive proxies skip surface lighting
105 PackedLight3D pl;
106 pl.data = d;
107 pl.isPoint = (d->type != "dir");
108 out.push_back(pl);
109 }
110 std::stable_sort(out.begin(), out.end(), [](const PackedLight3D& a, const PackedLight3D& b) {
111 if (a.isPoint != b.isPoint) return a.isPoint && !b.isPoint;
112 return a.data->intensity > b.data->intensity;
113 });
114 if (out.size() > maxCount) out.resize(maxCount);
115}
116
119void promoteDirectional(std::vector<PackedLight3D>& packed) {
120 for (size_t i = 0; i < packed.size(); ++i) {
121 if (packed[i].isPoint) continue;
122 if (i != 0) std::swap(packed[0], packed[i]);
123 return;
124 }
125}
126
127void fillLight3DGpu(Light3DGpu& g, const Light3D::Data& d, bool isPoint) {
128 g.color = glm::vec4(d.r * d.intensity, d.g * d.intensity, d.b * d.intensity, 1.f);
129 g.spot = glm::vec4(0.f, 0.f, 0.f, -1.f);
130 if (!isPoint) {
131 glm::vec3 dir(d.dx, d.dy, d.dz);
132 if (glm::length(dir) < 1e-6f)
133 dir = glm::vec3(0.f, 1.f, 0.f);
134 else
135 dir = glm::normalize(dir);
136 g.posRadius = glm::vec4(dir, 0.f);
137 return;
138 }
139 g.posRadius = glm::vec4(d.x, d.y, d.z, d.radius);
140 if (d.type != "spot") return;
141 float cosOuter = 0.f, cosInner = 0.f;
142 lightSpotCosines(d.spotAngleDeg, d.spotSoftness, cosOuter, cosInner);
143 const float scale = 1.f / std::max(cosInner - cosOuter, 1e-4f);
144 const float bias = -cosOuter * scale;
145 glm::vec3 beam(d.dx, d.dy, d.dz);
146 if (glm::length(beam) < 1e-6f)
147 beam = glm::vec3(0.f, -1.f, 0.f);
148 else
149 beam = glm::normalize(beam);
150 // Encode local shadow slot in beam length: |beam| = 1 + (slot+1)/100.
151 // spotAttenuation3D normalizes; shaders decode the slot independently of
152 // packed vs clustered light indices (which diverge after CSM promotion).
153 if (d.shadowLocalSlot >= 0 && d.shadowLocalSlot < ShadowConfig::kLocalSlots)
154 beam *= 1.f + float(d.shadowLocalSlot + 1) * 0.01f;
155 g.spot = glm::vec4(beam, scale);
156 g.color.a = bias;
157}
158
159Lighting3DPack packLights3D(const std::vector<PackedLight3D>& lights, const Camera3D::Data* cam) {
160 Lighting3DPack pack{};
161 if (cam) {
162 pack.ambient = glm::vec4(cam->ambientR, cam->ambientG, cam->ambientB, 0.f);
163 }
164 if (lights.empty()) {
165 pack.count = 1;
166 pack.lights[0].posRadius = glm::vec4(gLightDir, 0.f);
167 pack.lights[0].color = glm::vec4(gLightColor, 1.f);
168 pack.lights[0].spot = glm::vec4(0.f, 0.f, 0.f, -1.f);
169 return pack;
170 }
171 const int n = std::min(int(lights.size()), Lighting3DPack::kMaxLights);
172 pack.count = n;
173 for (int i = 0; i < n; ++i) {
174 const auto* d = lights[size_t(i)].data;
175 fillLight3DGpu(pack.lights[i], *d, lights[size_t(i)].isPoint);
176 }
177 return pack;
178}
179
180void splitLights(const std::vector<PackedLight3D>& packed, std::vector<ClusteredLightGpu>& points,
181 std::vector<ClusteredLightGpu>& dirs) {
182 points.clear();
183 dirs.clear();
184 for (const auto& pl : packed) {
185 const auto* d = pl.data;
187 fillLight3DGpu(g, *d, pl.isPoint);
188 if (pl.isPoint)
189 points.push_back(g);
190 else
191 dirs.push_back(g);
192 }
193}
194
195} // namespace
196
197void Camera3D::setEye(float x, float y, float z) {
198 auto d = data();
199 d->eyeX = x;
200 d->eyeY = y;
201 d->eyeZ = z;
202}
203
204float Camera3D::getEyeX() { return data()->eyeX; }
205float Camera3D::getEyeY() { return data()->eyeY; }
206float Camera3D::getEyeZ() { return data()->eyeZ; }
207
208void Camera3D::setTarget(float x, float y, float z) {
209 auto d = data();
210 d->targetX = x;
211 d->targetY = y;
212 d->targetZ = z;
213}
214
215float Camera3D::getTargetX() { return data()->targetX; }
216float Camera3D::getTargetY() { return data()->targetY; }
217float Camera3D::getTargetZ() { return data()->targetZ; }
218
219void Camera3D::setUp(float x, float y, float z) {
220 auto d = data();
221 d->upX = x;
222 d->upY = y;
223 d->upZ = z;
224}
225
226void Camera3D::setFov(float fovYDeg) { data()->fovYDeg = fovYDeg; }
227
228float Camera3D::getFov() { return data()->fovYDeg; }
229
231 data()->orthographic = true;
232 data()->orthoHeight = std::max(0.001f, height);
233}
234
235void Camera3D::setPerspective() { data()->orthographic = false; }
236
237void Camera3D::setClipPlanes(float nearZ, float farZ) {
238 data()->nearZ = std::max(0.001f, nearZ);
239 data()->farZ = std::max(data()->nearZ + 0.001f, farZ);
240}
241
242float Camera3D::getNearClip(){return data()->nearZ;}
243float Camera3D::getFarClip(){return data()->farZ;}
244void Camera3D::setPhysicalLens(float aperture,float focalLength){
245 EV_PARAM_CHECK(std::isfinite(aperture),"aperture must be positive and finite");
246 EV_PARAM_CHECK(aperture>0.f,"aperture must be positive and finite");
247 EV_PARAM_CHECK(std::isfinite(focalLength),"focal length must be positive and finite");
248 EV_PARAM_CHECK(focalLength>0.f,"focal length must be positive and finite");
249 data()->aperture=aperture;data()->focalLength=focalLength;
250}
251float Camera3D::getAperture(){return data()->aperture;}
252float Camera3D::getFocalLength(){return data()->focalLength;}
254 EV_PARAM_CHECK(layer >= 0, "layer must be in [0,31]");
255 EV_PARAM_CHECK(layer < 32, "layer must be in [0,31]");
256 EV_PARAM_CHECK(std::isfinite(distance), "distance must be finite and non-negative");
257 EV_PARAM_CHECK(distance >= 0.f, "distance must be finite and non-negative");
258 data()->layerCullDistances[size_t(layer)] = distance;
259}
260
262 EV_PARAM_CHECK(layer >= 0, "layer must be in [0,31]");
263 EV_PARAM_CHECK(layer < 32, "layer must be in [0,31]");
264 return data()->layerCullDistances[size_t(layer)];
265}
266
268 EV_PARAM_CHECK(layer >= 0, "layer must be in [0,31]");
269 EV_PARAM_CHECK(layer < 32, "layer must be in [0,31]");
270 EV_PARAM_CHECK(std::isfinite(distance), "distance must be finite and non-negative");
271 EV_PARAM_CHECK(distance >= 0.f, "distance must be finite and non-negative");
272 data()->shadowLayerCullDistances[size_t(layer)] = distance;
273}
274
276 EV_PARAM_CHECK(layer >= 0, "layer must be in [0,31]");
277 EV_PARAM_CHECK(layer < 32, "layer must be in [0,31]");
278 return data()->shadowLayerCullDistances[size_t(layer)];
279}
280
281void Camera3D::setActive(bool active) { data()->active = active; }
282
283void Camera3D::setAmbient(float r, float g, float b) {
284 auto d = data();
285 d->ambientR = r;
286 d->ambientG = g;
287 d->ambientB = b;
288}
289
290void Camera3D::setEnvMap(Texture* cube) { data()->envMap = cube; }
291
292void Camera3D::setEnvIntensity(float intensity) { data()->envIntensity = intensity < 0.f ? 0.f : intensity; }
293
294void Camera3D::setExposure(float ev) { data()->exposureEV = std::clamp(ev, -16.f, 16.f); }
295
296float Camera3D::getExposure() { return data()->exposureEV; }
297
298void Camera3D::setAutoExposure(bool enabled, float minEV, float maxEV) {
299 auto d = data();
300 d->autoExposure = enabled;
301 d->autoExposureMinEV = std::clamp(std::min(minEV, maxEV), -16.f, 16.f);
302 d->autoExposureMaxEV = std::clamp(std::max(minEV, maxEV), -16.f, 16.f);
303}
304
305bool Camera3D::isAutoExposure() { return data()->autoExposure; }
306
307void Camera3D::setBloom(float intensity, float threshold) {
308 auto d = data();
309 d->bloomIntensity = std::clamp(intensity, 0.f, 8.f);
310 d->bloomThreshold = std::clamp(threshold, 0.f, 16.f);
311}
312
313float Camera3D::getBloomIntensity() { return data()->bloomIntensity; }
314float Camera3D::getBloomThreshold() { return data()->bloomThreshold; }
315
316void Camera3D::setDepthOfField(float focusDistance, float maxBlurPx, float focusRange) {
317 auto d = data();
318 d->dofFocusDistance = std::max(0.f, focusDistance);
319 d->dofMaxBlurPx = std::clamp(maxBlurPx, 0.f, 32.f);
320 d->dofFocusRange = std::max(1e-3f, focusRange);
321}
322
323void Camera3D::clearDepthOfField() { data()->dofMaxBlurPx = 0.f; }
324
325float Camera3D::getDofFocusDistance() { return data()->dofFocusDistance; }
326float Camera3D::getDofMaxBlur() { return data()->dofMaxBlurPx; }
327float Camera3D::getDofFocusRange() { return data()->dofFocusRange; }
328
329void Camera3D::setEnvProbe(float centerX, float centerY, float centerZ, float extentX,
330 float extentY, float extentZ) {
331 data()->envProbeCenter = glm::vec3(centerX, centerY, centerZ);
332 data()->envProbeExtent = glm::max(glm::vec3(extentX, extentY, extentZ), glm::vec3(0.f));
333}
334
335void Camera3D::clearEnvProbe() { data()->envProbeExtent = glm::vec3(0.f); }
336
338 const glm::vec3 e = data()->envProbeExtent;
339 return e.x > 0.f && e.y > 0.f && e.z > 0.f;
340}
341float Camera3D::getEnvProbeCenterX() { return data()->envProbeCenter.x; }
342float Camera3D::getEnvProbeCenterY() { return data()->envProbeCenter.y; }
343float Camera3D::getEnvProbeCenterZ() { return data()->envProbeCenter.z; }
344float Camera3D::getEnvProbeExtentX() { return data()->envProbeExtent.x; }
345float Camera3D::getEnvProbeExtentY() { return data()->envProbeExtent.y; }
346float Camera3D::getEnvProbeExtentZ() { return data()->envProbeExtent.z; }
347
348void Camera3D::setReflectionProbe(int slot, Texture* cubemap, float centerX, float centerY, float centerZ,
349 float extentX, float extentY, float extentZ, float intensity, float blendDistance,
350 int priority) {
351 if (slot < 0 || slot >= Data::kMaxReflectionProbes) return;
352 auto& probe = data()->reflectionProbes[static_cast<size_t>(slot)];
353 probe.cubemap = cubemap;
354 probe.center = glm::vec3(centerX, centerY, centerZ);
355 probe.extent = glm::max(glm::vec3(extentX, extentY, extentZ), glm::vec3(0.f));
356 probe.intensity = std::max(intensity, 0.f);
357 probe.blendDistance = std::max(blendDistance, 0.f);
358 probe.priority = priority;
359 probe.enabled = cubemap && glm::all(glm::greaterThan(probe.extent, glm::vec3(0.f))) && probe.intensity > 0.f;
360}
361
363 if (slot < 0 || slot >= Data::kMaxReflectionProbes) return;
364 data()->reflectionProbes[static_cast<size_t>(slot)] = {};
365}
366
368 int count = 0;
369 for (const auto& probe : data()->reflectionProbes)
370 if (probe.enabled) ++count;
371 return count;
372}
373
374static ReflectionProbeUpload selectReflectionProbes(const Camera3D::Data& camera, const glm::vec3& samplePosition) {
375 struct Candidate {
376 int slot = 0;
377 int priority = 0;
378 float distance2 = 0.f;
379 };
380 std::array<Candidate, Camera3D::Data::kMaxReflectionProbes> candidates{};
381 int candidateCount = 0;
382 for (int slot = 0; slot < Camera3D::Data::kMaxReflectionProbes; ++slot) {
383 const auto& probe = camera.reflectionProbes[static_cast<size_t>(slot)];
384 if (!probe.enabled || !probe.cubemap) continue;
385 const glm::vec3 outside = glm::max(glm::abs(samplePosition - probe.center) - probe.extent, glm::vec3(0.f));
386 candidates[static_cast<size_t>(candidateCount++)] = Candidate{slot, probe.priority, glm::dot(outside, outside)};
387 }
388 std::stable_sort(candidates.begin(), candidates.begin() + candidateCount,
389 [](const Candidate& a, const Candidate& b) {
390 if (a.priority != b.priority) return a.priority > b.priority;
391 if (a.distance2 != b.distance2) return a.distance2 < b.distance2;
392 return a.slot < b.slot;
393 });
394
395 ReflectionProbeUpload upload;
396 upload.count = std::min(candidateCount, ReflectionProbeUpload::kMaxProbes);
397 for (int i = 0; i < upload.count; ++i) {
398 const auto& source = camera.reflectionProbes[static_cast<size_t>(candidates[i].slot)];
399 auto& target = upload.probes[i];
400 target.cubemap = source.cubemap;
401 target.center = source.center;
402 target.extent = source.extent;
403 target.intensity = source.intensity;
404 target.blendDistance = source.blendDistance;
405 target.priority = source.priority;
406 }
407 return upload;
408}
409
410void Camera3D::screenToRay(float screenX, float screenY, float viewW, float viewH) {
411 auto d = data();
412 d->screenRayOx = d->eyeX;
413 d->screenRayOy = d->eyeY;
414 d->screenRayOz = d->eyeZ;
415 d->screenRayDx = 0.f;
416 d->screenRayDy = 0.f;
417 d->screenRayDz = -1.f;
418 if (viewW <= 0.f || viewH <= 0.f) return;
419
420 const glm::vec3 eye(d->eyeX, d->eyeY, d->eyeZ);
421 const glm::vec3 target(d->targetX, d->targetY, d->targetZ);
422 const glm::vec3 up(d->upX, d->upY, d->upZ);
423 const glm::mat4 viewM = glm::lookAtRH(eye, target, up);
424 const float aspect = viewW / viewH;
425 const float fovRad = d->fovYDeg * 0.017453292519943295f;
426 const glm::mat4 projM =
427 cameraProjectionVulkanRH_ZO(d->orthographic, fovRad, d->orthoHeight, aspect, d->nearZ, d->farZ);
428 const glm::mat4 invVP = glm::inverse(projM * viewM);
429
430 // Screen pixel → Vulkan NDC (Y-down; matches perspectiveVulkanRH_ZO).
431 const float ndcX = (screenX / viewW) * 2.f - 1.f;
432 const float ndcY = (screenY / viewH) * 2.f - 1.f;
433 auto unproject = [&](float ndcZ) -> glm::vec3 {
434 glm::vec4 w = invVP * glm::vec4(ndcX, ndcY, ndcZ, 1.f);
435 if (std::fabs(w.w) < 1e-8f) return eye;
436 w /= w.w;
437 return glm::vec3(w);
438 };
439 // ZO depth: near = 0, far = 1.
440 const glm::vec3 nearPt = unproject(0.f);
441 const glm::vec3 farPt = unproject(1.f);
442 glm::vec3 dir = farPt - nearPt;
443 const float len = glm::length(dir);
444 if (len > 1e-8f)
445 dir /= len;
446 else
447 dir = glm::normalize(target - eye);
448
449 d->screenRayOx = eye.x;
450 d->screenRayOy = eye.y;
451 d->screenRayOz = eye.z;
452 if (d->orthographic) {
453 d->screenRayOx = nearPt.x;
454 d->screenRayOy = nearPt.y;
455 d->screenRayOz = nearPt.z;
456 }
457 d->screenRayDx = dir.x;
458 d->screenRayDy = dir.y;
459 d->screenRayDz = dir.z;
460}
461
462float Camera3D::getScreenRayOriginX() { return data()->screenRayOx; }
463float Camera3D::getScreenRayOriginY() { return data()->screenRayOy; }
464float Camera3D::getScreenRayOriginZ() { return data()->screenRayOz; }
465float Camera3D::getScreenRayDirX() { return data()->screenRayDx; }
466float Camera3D::getScreenRayDirY() { return data()->screenRayDy; }
467float Camera3D::getScreenRayDirZ() { return data()->screenRayDz; }
468
469void RenderSystem3D::setDirectionalLight(float dx, float dy, float dz, float r, float g, float b) {
470 glm::vec3 d(dx, dy, dz);
471 if (glm::length(d) < 1e-6f) d = glm::vec3(0.f, 1.f, 0.f);
472 gLightDir = glm::normalize(d);
473 gLightColor = glm::vec3(r, g, b);
474}
475
477 if (!drawer) return;
478 g_gbufferDrawers.push_back(std::move(drawer));
479}
480
481uint64_t RenderSystem3D::addCaptureExtraDrawer(uint32_t reflectionCaptureMask, CaptureExtraDrawer drawer) {
482 if (!drawer || reflectionCaptureMask == 0u) return 0;
483 const uint64_t token = g_nextCaptureDrawerToken++;
484 g_captureDrawers.push_back(CaptureExtraDrawerEntry{token, reflectionCaptureMask, std::move(drawer)});
485 return token;
486}
487
489 if (token == 0) return;
490 g_captureDrawers.erase(
491 std::remove_if(g_captureDrawers.begin(), g_captureDrawers.end(),
492 [token](const CaptureExtraDrawerEntry& entry) { return entry.token == token; }),
493 g_captureDrawers.end());
494}
495
497 if (!drawer) return 0;
498 const uint64_t token = g_nextForwardExtraDrawerToken++;
499 g_forwardDrawers.push_back(ForwardExtraDrawerEntry{token, std::move(drawer)});
500 return token;
501}
502
504 if (token == 0) return;
505 g_forwardDrawers.erase(
506 std::remove_if(g_forwardDrawers.begin(), g_forwardDrawers.end(),
507 [token](const ForwardExtraDrawerEntry& entry) { return entry.token == token; }),
508 g_forwardDrawers.end());
509}
510
512 if (!collector) return 0;
513 const uint64_t token = g_nextGpuOpaqueCollectorToken++;
514 g_gpuOpaqueCollectors.push_back(GpuOpaqueCollectorEntry{token, std::move(collector)});
515 return token;
516}
517
519 if (token == 0) return;
520 g_gpuOpaqueCollectors.erase(
521 std::remove_if(g_gpuOpaqueCollectors.begin(), g_gpuOpaqueCollectors.end(),
522 [token](const GpuOpaqueCollectorEntry& entry) { return entry.token == token; }),
523 g_gpuOpaqueCollectors.end());
524}
525
527 if (!drawer) return 0;
528 const uint64_t token = g_nextShadowDrawerToken++;
529 g_shadowDrawers.push_back(ShadowExtraDrawerEntry{token, std::move(drawer)});
530 return token;
531}
532
534 if (token == 0) return;
535 g_shadowDrawers.erase(std::remove_if(g_shadowDrawers.begin(), g_shadowDrawers.end(),
536 [token](const ShadowExtraDrawerEntry& entry) { return entry.token == token; }),
537 g_shadowDrawers.end());
538}
539
541 if (!drawer) return;
542 g_decalDrawers.push_back(std::move(drawer));
543}
544
545namespace {
546
547ShadowPagingView makeShadowPagingView(Camera3D* cam, float aspect) {
549 if (!cam) return view;
550 auto cd = cam->data();
551 view.eye = glm::vec3(cd->eyeX, cd->eyeY, cd->eyeZ);
552 const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ);
553 const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
554 const glm::mat4 v = glm::lookAtRH(view.eye, target, up);
555 const float fov = cd->fovYDeg * 0.017453292519943295f;
556 const glm::mat4 p = perspectiveVulkanRH_ZO(fov, std::max(aspect, 1e-3f), cd->nearZ, cd->farZ);
557 view.viewProj = p * v;
558 view.valid = true;
559 return view;
560}
561
562void selectFrameShadowCasters(const std::vector<PackedLight3D>& packed, const ShadowPagingView& view,
563 Light3D::Data*& directionalCaster, std::vector<LocalShadowSlot>& localSlots) {
564 std::vector<Light3D::Data*> lights;
565 std::vector<bool> isPoint;
566 lights.reserve(packed.size());
567 isPoint.reserve(packed.size());
568 for (const auto& pl : packed) {
569 lights.push_back(pl.data);
570 isPoint.push_back(pl.isPoint);
571 }
572 selectShadowCasters(lights, isPoint, ShadowSchemeSettings::current(), view, directionalCaster, localSlots);
573}
574
575void prioritizeShadowCaster(std::vector<PackedLight3D>& packed, Light3D::Data* caster) {
576 if (!caster || packed.empty()) return;
577 for (size_t i = 0; i < packed.size(); ++i) {
578 if (packed[i].data != caster) continue;
579 if (i != 0) std::swap(packed[0], packed[i]);
580 return;
581 }
582}
583
588struct FrustumPlanes {
589 glm::vec4 p[6]{};
590
591 bool sphereVisible(const glm::vec3& center, float radius) const {
592 for (const auto& pl : p) {
593 const float d = pl.x * center.x + pl.y * center.y + pl.z * center.z + pl.w;
594 if (d < -radius) return false;
595 }
596 return true;
597 }
598};
599
600FrustumPlanes extractFrustum(const glm::mat4& m) {
601 FrustumPlanes f;
602 const glm::vec4 r0(m[0][0], m[1][0], m[2][0], m[3][0]);
603 const glm::vec4 r1(m[0][1], m[1][1], m[2][1], m[3][1]);
604 const glm::vec4 r2(m[0][2], m[1][2], m[2][2], m[3][2]);
605 const glm::vec4 r3(m[0][3], m[1][3], m[2][3], m[3][3]);
606 auto norm = [](glm::vec4& v) {
607 const float l = glm::length(glm::vec3(v));
608 if (l > 1e-8f) v /= l;
609 };
610 f.p[0] = r3 + r0; // left
611 f.p[1] = r3 - r0; // right
612 f.p[2] = r3 + r1; // bottom
613 f.p[3] = r3 - r1; // top
614 // Vulkan clip space uses zero-to-one depth: near plane is z_clip = 0
615 // (plane r2), not the z = -w plane used by OpenGL-style [-1,1] depth.
616 f.p[4] = r2; // near
617 f.p[5] = r3 - r2; // far
618 for (auto& pl : f.p) norm(pl);
619 return f;
620}
621
627struct CameraView {
628 Camera3D::Data* data = nullptr;
629 glm::vec3 eye{0.f};
630 glm::mat4 view{1.f};
631 glm::mat4 proj{1.f};
632 glm::mat4 viewProj{1.f};
633 glm::mat4 cullViewProj{1.f};
634 FrustumPlanes frustum;
635 float fovRad = 1.f;
636 Lighting3DPack lighting{};
637 ClusteredLightingUpload clustered{};
638 bool clusteredValid = false;
640 bool clusteredUploaded = false;
641};
642
643CameraView buildCameraView(Camera3D::Data* cd, const std::vector<PackedLight3D>& packed, bool useClustered,
644 float aspect, const std::vector<ClusteredLightGpu>& clusteredPoints,
645 const std::vector<ClusteredLightGpu>& clusteredDirs, Graphics& gfx,
646 const glm::vec2& jitterNdc) {
647 CameraView cv;
648 cv.data = cd;
649 cv.eye = glm::vec3(cd->eyeX, cd->eyeY, cd->eyeZ);
650 const glm::vec3 look(cd->targetX, cd->targetY, cd->targetZ);
651 const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
652 cv.view = glm::lookAtRH(cv.eye, look, up);
653 cv.fovRad = cd->fovYDeg * 0.017453292519943295f;
654 cv.proj = cameraProjectionVulkanRH_ZO(cd->orthographic, cv.fovRad, cd->orthoHeight, aspect, cd->nearZ, cd->farZ);
655 cv.cullViewProj = cv.proj * cv.view;
656 cv.frustum = extractFrustum(cv.cullViewProj);
657 cv.proj[2][0] += jitterNdc.x;
658 cv.proj[2][1] += jitterNdc.y;
659 cv.viewProj = cv.proj * cv.view;
660 cv.lighting = packLights3D(packed, cd);
661 if (useClustered && !cd->orthographic) {
662 cv.clustered = buildClusteredLighting(clusteredPoints, clusteredDirs, cv.view, cd->nearZ, cd->farZ,
663 gfx.getWidth(), gfx.getHeight(), cv.fovRad,
664 glm::vec4(cd->ambientR, cd->ambientG, cd->ambientB, 0.f));
665 cv.clusteredValid = true;
666 }
667 return cv;
668}
669
675struct CulledItem {
676 Renderable3D::MeshRenderer* mr = nullptr;
677 Mesh* mesh = nullptr;
678 Material* material = nullptr;
679 Shader* shader = nullptr; // effective mesh shader (material or legacy)
680 glm::mat4 model{1.f};
681 glm::vec3 worldC{0.f}; // world-space bounding-sphere center
682 glm::vec2 temporalMotion{0.f}; // object-only previous-UV correction
683 bool receiveShadow = true;
684 bool shadowsOnly = false;
685 bool shadowDoubleSided = true;
689 float lodWeight = 1.f;
690 bool lodFadeReverse = false;
691 bool speedTreeFade = false;
692 float worldR = 0.f; // world-space bounding-sphere radius (0 = unknown → unculled)
693 float distSq = 0.f;
694 float projectedDepth = 0.f;
697 int camIdx = 0;
698 uint32_t cascadeMask = 0; // bit c set when the caster may contribute to cascade c
699 bool inView = false; // inside the item camera's frustum
700 bool inDefaultView = false; // inside the default camera's frustum (G-buffer)
701 bool hair = false;
702 bool xray = false;
703};
704
706 if (count == 1) return SkinInfluenceLimit::One;
707 if (count == 2) return SkinInfluenceLimit::Two;
709}
710
711} // namespace
712
714 eve::debug::RenderPassScope pass3d("RenderSystem3D");
715 Camera3D* defaultCam = findDefaultCamera3D();
716
718 rc->ensureCompiled();
719 const bool doShadow = rc->hasPass("shadow");
720 const bool doGBuffer = rc->hasPass("gbuffer");
721 const bool doDecal = rc->hasPass("decal");
722 const bool doDeferredLighting = rc->hasPass("deferredLighting");
723 const bool doForward = rc->hasPass("forward");
724 const bool doHair = rc->hasPass("hair");
725 const bool allowClustered = rc->isEnabled("clustered");
726
727 std::vector<PackedLight3D> packed;
728 collectLights3D(packed, size_t(ClusteredLightConfig::kMaxLights));
729 promoteDirectional(packed);
730 Light3D::Data* shadowCaster = nullptr;
731 std::vector<LocalShadowSlot> localShadowSlots;
732 const float aspectEarly =
733 (gfx.getHeight() > 0) ? float(gfx.getWidth()) / float(gfx.getHeight()) : 1.f;
734 if (doShadow)
735 selectFrameShadowCasters(packed, makeShadowPagingView(defaultCam, aspectEarly), shadowCaster,
736 localShadowSlots);
737 prioritizeShadowCaster(packed, shadowCaster);
738 // CSM promotion may swap packed indices; refresh local slot → light mapping.
739 for (LocalShadowSlot& slot : localShadowSlots) {
740 slot.lightIndex = -1;
741 if (!slot.light) continue;
742 for (size_t i = 0; i < packed.size() && i < size_t(Lighting3DPack::kMaxLights); ++i) {
743 if (packed[i].data == slot.light) {
744 slot.lightIndex = int(i);
745 break;
746 }
747 }
748 }
749 const bool haveExtraShadowCasters = doShadow && !g_shadowDrawers.empty();
750
751 const float aspect = aspectEarly;
752
753 ShadowUpload shadowUpload{};
754 shadowUpload.active = false;
755 // Per-cascade light frustums for caster culling (sphere vs frustum).
756 FrustumPlanes cascadeFrustums[ShadowConfig::kCascades];
757 if ((shadowCaster || haveExtraShadowCasters) && defaultCam) {
758 auto cd = defaultCam->data();
759 glm::vec3 dir = shadowCaster ? glm::vec3(shadowCaster->dx, shadowCaster->dy, shadowCaster->dz) : gLightDir;
760 if (glm::length(dir) < 1e-6f)
761 dir = glm::vec3(0.f, 1.f, 0.f);
762 else
763 dir = glm::normalize(dir);
764 const float shadowBias = shadowCaster ? shadowCaster->shadowBias : 0.003f;
765 const float shadowStrength = shadowCaster ? shadowCaster->shadowStrength : 1.f;
766 const float fovRad = cd->fovYDeg * 0.017453292519943295f;
767 shadowUpload = buildDirectionalCSM(
768 dir, glm::vec3(cd->eyeX, cd->eyeY, cd->eyeZ), glm::vec3(cd->targetX, cd->targetY, cd->targetZ),
769 glm::vec3(cd->upX, cd->upY, cd->upZ), fovRad, aspect, cd->nearZ, cd->farZ, shadowBias, shadowStrength);
770 for (int c = 0; c < ShadowConfig::kCascades; ++c)
771 cascadeFrustums[c] = extractFrustum(shadowUpload.ubo.lightVP[c]);
772 } else if (!localShadowSlots.empty()) {
773 // Local-only frame: keep CSM inactive but still upload local VPs.
774 shadowUpload.active = true;
775 shadowUpload.ubo.bias = glm::vec4(0.002f, 0.f, 1.f, 0.02f);
776 shadowUpload.ubo.splits.w = 0.f;
777 }
778 if (!localShadowSlots.empty()) {
779 shadowUpload.active = true;
780 shadowUpload.ubo.localMeta =
781 glm::vec4(float(localShadowSlots.size()), 1.f, 0.f, 0.f);
782 shadowUpload.ubo.localSlot01 = glm::vec4(-1.f);
783 shadowUpload.ubo.localSlot23 = glm::vec4(-1.f);
784 for (size_t s = 0; s < localShadowSlots.size(); ++s) {
785 const LocalShadowSlot& slot = localShadowSlots[s];
786 shadowUpload.ubo.localVP[s] = slot.lightVP;
787 if (s < 4) shadowUpload.ubo.localBias[int(s)] = slot.bias;
788 if (slot.lightIndex >= 0 && slot.lightIndex < 8) {
789 glm::vec4& bank = slot.lightIndex < 4 ? shadowUpload.ubo.localSlot01 : shadowUpload.ubo.localSlot23;
790 bank[slot.lightIndex % 4] = float(s);
791 }
792 }
793 }
794 gfx.setMesh3DShadows(shadowUpload);
795
796 // Hybrid always builds the clustered light table so deferred lighting and
797 // transparent Forward+ share one upload. ForwardPlus still gates on light count.
798 const bool useClustered =
799 allowClustered && (doDeferredLighting || packed.size() > size_t(Lighting3DPack::kMaxLights));
800 // Light split / directional promotion is camera-independent: compute once,
801 // then each camera view bakes its own clustered table from these lists.
802 std::vector<ClusteredLightGpu> clusteredPoints;
803 std::vector<ClusteredLightGpu> clusteredDirs;
804 if (useClustered) {
805 splitLights(packed, clusteredPoints, clusteredDirs);
806 if (clusteredDirs.empty()) {
808 d.posRadius = glm::vec4(gLightDir, 0.f);
809 d.color = glm::vec4(gLightColor, 1.f);
810 clusteredDirs.push_back(d);
811 }
812 if (shadowCaster && !clusteredDirs.empty()) {
813 glm::vec3 dir(shadowCaster->dx, shadowCaster->dy, shadowCaster->dz);
814 if (glm::length(dir) < 1e-6f)
815 dir = glm::vec3(0.f, 1.f, 0.f);
816 else
817 dir = glm::normalize(dir);
818 for (size_t i = 0; i < clusteredDirs.size(); ++i) {
819 if (glm::length(glm::vec3(clusteredDirs[i].posRadius) - dir) < 1e-3f) {
820 if (i != 0) std::swap(clusteredDirs[0], clusteredDirs[i]);
821 break;
822 }
823 }
824 }
825 }
826
827 const bool haveManager = ecs::current()->getManager<Renderable3D>() != nullptr;
828 const bool shadowActive = doShadow && shadowUpload.active;
829
830 // Per-camera constants (matrices, frustum, lighting, clustered table).
831 // The default camera is slot 0 so the G-buffer pass reuses its view.
832 std::vector<CameraView> cams;
833 cams.reserve(2);
834 glm::vec2 temporalJitter(0.f);
835 AntiAliasing* temporalAA = nullptr;
836 if (defaultCam && rc->isEnabled("taa")) {
837 auto cd = defaultCam->data();
838 temporalAA = gfx.pipelineAntiAliasing();
839 temporalAA->setTemporalCamera(glm::vec3(cd->eyeX, cd->eyeY, cd->eyeZ),
840 glm::vec3(cd->targetX, cd->targetY, cd->targetZ), cd->fovYDeg);
841 temporalJitter = temporalAA->prepareTemporalJitter(gfx.getWidth(), gfx.getHeight());
842 }
843 if (defaultCam) {
844 cams.push_back(buildCameraView(defaultCam->data().operator->(), packed, useClustered, aspect, clusteredPoints,
845 clusteredDirs, gfx, temporalJitter));
846 if (temporalAA) {
847 auto cd = defaultCam->data();
848 temporalAA->setTemporalViewProjection(cams.front().viewProj, cd->nearZ, cd->farZ);
849 }
850 }
851 auto findOrAddCam = [&](Camera3D* camEnt) -> int {
852 Camera3D::Data* d = camEnt->data().operator->();
853 for (size_t i = 0; i < cams.size(); ++i) {
854 if (cams[i].data == d) return int(i);
855 }
856 cams.push_back(
857 buildCameraView(d, packed, useClustered, aspect, clusteredPoints, clusteredDirs, gfx, temporalJitter));
858 return int(cams.size()) - 1;
859 };
860
861 // Single ECS traversal: one model matrix, one LOD pick, one set of
862 // sphere-vs-frustum tests per part. Passes below only replay the list.
863 std::vector<CulledItem> items;
864 items.reserve(64);
865 if (haveManager) {
866 auto view = ecs::View<Renderable3D, Renderable3D::Transform3D, Renderable3D::MeshRenderer>();
867 for (auto it = view.begin(); it != view.end(); ++it) {
868 auto [xf, mr] = *it;
869 if (!mr->visible) continue;
870 if (mr->instances) {
872 if (!valid) throw eve::Exception("%s", valid.status().describe().c_str());
873 }
874 Camera3D *camEnt = mr->camera ? mr->camera : defaultCam;
875 if (!camEnt) continue;
876 const int camIdx = findOrAddCam(camEnt);
877 const CameraView& cv = cams[size_t(camIdx)];
878 const float dx = xf->x - cv.eye.x;
879 const float dy = xf->y - cv.eye.y;
880 const float dz = xf->z - cv.eye.z;
881 const float distSq = dx * dx + dy * dy + dz * dz;
882 const float dist = std::sqrt(distSq);
883 const float layerDistance = cv.data->layerCullDistances[size_t(mr->layer)];
884 if (layerDistance > 0.f && dist > layerDistance) continue;
885 const glm::mat4 model = modelFromTransform(*xf);
886 const float maxScale = std::max(std::abs(xf->sx), std::max(std::abs(xf->sy), std::abs(xf->sz)));
887 const bool shadowOk = mr->effectiveCastShadow();
888
889 auto pushPart = [&](Mesh* drawMesh, Material* mat, bool asHair, bool castsShadow,
890 const ModelPart* modelPart, int lodLevel, float lodWeight,
891 bool lodFadeReverse = false) {
892 if (!drawMesh) return;
893 CulledItem item;
894 item.mr = mr;
895 item.mesh = drawMesh;
896 item.material = mat;
897 item.shader = mat ? mat->effectiveShader() : mr->shader;
898 item.model = model;
899 const auto* lodState = lodLevel >= 0 && lodLevel < mr->lodCount &&
900 mr->lodRendererStates[lodLevel].configured
901 ? &mr->lodRendererStates[lodLevel]
902 : nullptr;
903 item.receiveShadow = lodState ? lodState->receiveShadows : mr->receiveShadow;
904 item.shadowsOnly = lodState && lodState->shadowCastingMode == 3;
905 item.shadowDoubleSided = lodState ? lodState->shadowCastingMode == 2
906 : (!mat || mat->getDoubleSided());
907 item.useReflectionProbes = !lodState || lodState->reflectionProbeUsage != 0;
908 item.lightProbeUsage = lodState ? lodState->lightProbeUsage : 0;
909 item.skinInfluenceLimit = lodState && lodState->skinQuality != 0 ? lodState->skinQuality : 4;
910 item.lodWeight = std::clamp(lodWeight, 0.f, 1.f);
911 item.lodFadeReverse = lodFadeReverse;
912 item.speedTreeFade = mr->lodFadeMode == 1 && item.lodWeight < 0.9999f && !item.shader;
913 if (temporalAA && (!lodState || lodState->motionVectorMode == 1))
914 item.temporalMotion = temporalAA->prepareTemporalObjectMotion(mr, model);
915 item.distSq = distSq;
916 const glm::vec4 viewCenter = cv.view * glm::vec4(xf->x, xf->y, xf->z, 1.f);
917 item.projectedDepth = -viewCenter.z;
918 item.camIdx = camIdx;
919 item.hair = asHair;
920 item.surfaceMode = mat ? mat->surfaceMode()
921 : (asHair ? SurfaceMode::Transparent
923 item.sortPriority = modelPart && modelPart->hasSortPriority
924 ? modelPart->sortPriority
925 : (mat ? mat->getSortPriority() : 0);
926 item.xray = mr->xrayHighlight;
927 if (mr->instances) {
928 const auto &range = *mr->instances;
929 glm::vec3 center, half;
930 for (int i = 0; i < 3; ++i) {
931 center[i] = range.minimum[i] * .5f + range.maximum[i] * .5f;
932 half[i] = range.maximum[i] * .5f - range.minimum[i] * .5f;
933 }
934 item.worldC = glm::vec3(model * glm::vec4(center, 1));
935 item.worldR = glm::length(half) * maxScale;
936 item.inView = meshInstanceRangeVisible(range, model, cv.cullViewProj, cv.eye);
937 item.inDefaultView =
938 defaultCam ? meshInstanceRangeVisible(range, model, cams[0].cullViewProj, cams[0].eye) : true;
939 } else if (drawMesh->hasBounds()) {
940 const glm::vec4 c4 =
941 model * glm::vec4(drawMesh->boundsCx, drawMesh->boundsCy, drawMesh->boundsCz, 1.f);
942 item.worldC = glm::vec3(c4);
943 item.worldR = drawMesh->boundsRadius * maxScale;
944 item.inView = cv.frustum.sphereVisible(item.worldC, item.worldR);
945 item.inDefaultView = defaultCam ? cams[0].frustum.sphereVisible(item.worldC, item.worldR) : true;
946 const Camera3D::Data* shadowCamera = defaultCam ? defaultCam->data().operator->() : cv.data;
947 const float shadowDistance = shadowCamera->shadowLayerCullDistances[size_t(mr->layer)];
948 if (shadowCaster && shadowActive && castsShadow &&
949 (shadowDistance <= 0.f || dist <= shadowDistance)) {
950 for (int c = 0; c < ShadowConfig::kCascades; ++c) {
951 if (cascadeFrustums[c].sphereVisible(item.worldC, item.worldR))
952 item.cascadeMask |= (1u << c);
953 }
954 }
955 } else {
956 // No bounds (e.g. legacy/imported mesh): never cull.
957 item.inView = true;
958 item.inDefaultView = true;
959 const Camera3D::Data* shadowCamera = defaultCam ? defaultCam->data().operator->() : cv.data;
960 const float shadowDistance = shadowCamera->shadowLayerCullDistances[size_t(mr->layer)];
961 if (shadowCaster && shadowActive && castsShadow &&
962 (shadowDistance <= 0.f || dist <= shadowDistance))
963 item.cascadeMask = (1u << ShadowConfig::kCascades) - 1u;
964 }
965 items.push_back(item);
966 };
967
968 if (mr->usesParts()) {
969 for (int p = 0; p < mr->partCount; ++p) {
970 Material* mat = mr->parts[p].material ? mr->parts[p].material : mr->material;
971 const bool asHair = mat ? mat->isTransparentHair() : mr->isHair;
972 const bool casts = shadowOk && !(mat && !mat->getCastShadow());
973 pushPart(mr->parts[p].mesh, mat, asHair, casts, &mr->parts[p], -1, 1.f);
974 }
975 } else {
976 const float lodDistance = dist / std::max(cv.data->lodBias, 0.001f);
977 int primary=-1,secondary=-1;float secondaryWeight=0.f;
978 mr->lodBlendForDistance(lodDistance,primary,secondary,secondaryWeight);
979 auto meshAt=[&](int level)->Mesh*{
980 if (mr->lodCount <= 0) return level >= 0 ? mr->mesh : nullptr;
981 return level>=0&&level<mr->lodCount?mr->lodMeshes[level]:nullptr;
982 };
983 auto castsAt=[&](int level){return level<0||mr->lodRendererStates[level].shadowCastingMode!=0;};
984 const float primaryWeight=1.f-secondaryWeight;
985 const bool ditherShadows = mr->lodFadeMode == 1 && secondaryWeight > 0.f;
986 pushPart(meshAt(primary),mr->material,mr->effectiveHair(),
987 shadowOk&&castsAt(primary)&&(ditherShadows||primaryWeight>=.5f),
988 nullptr,primary,primaryWeight);
989 if(secondaryWeight>0.f)
990 pushPart(meshAt(secondary),mr->material,mr->effectiveHair(),
991 shadowOk&&castsAt(secondary)&&(ditherShadows||secondaryWeight>.5f),
992 nullptr,secondary,secondaryWeight,true);
993 }
994 }
995 }
996
997 // CSM shadow passes — replay the collected casters, culled per cascade.
998 // Extra drawers may run without a Light3D caster (default gLightDir CSM).
999 if (shadowActive && (shadowCaster || haveExtraShadowCasters) &&
1000 (haveManager || haveExtraShadowCasters)) {
1001 auto cd = defaultCam->data();
1002 for (int c = 0; c < ShadowConfig::kCascades; ++c) {
1003 eve::debug::rtPassBegin("ShadowPass");
1004 gfx.beginShadowPass(c);
1005 for (const auto& item : items) {
1006 if ((item.cascadeMask & (1u << c)) == 0) continue;
1007 eve::debug::rtBind("mesh", "shadowCaster");
1008 eve::debug::rtDraw("drawMeshShadow", "cascade");
1009 Texture* shadowAlbedo = item.material ? item.material->getAlbedoTexture() : item.mr->texture;
1010 const bool doubleSidedShadow = item.shadowDoubleSided ||
1011 (item.material && item.material->getDoubleSided());
1012 if (item.surfaceMode == SurfaceMode::Masked)
1013 gfx.setMesh3DSkinInfluenceLimit(skinInfluenceLimit(item.skinInfluenceLimit));
1014 if (item.surfaceMode == SurfaceMode::Masked)
1015 gfx.drawMeshShadowAlpha(item.mesh, shadowUpload.ubo.lightVP[c] * item.model, shadowAlbedo,
1016 doubleSidedShadow, item.lodWeight, item.lodFadeReverse,
1017 item.speedTreeFade);
1018 else if (item.surfaceMode != SurfaceMode::Transparent) {
1019 gfx.setMesh3DSkinInfluenceLimit(skinInfluenceLimit(item.skinInfluenceLimit));
1020 gfx.drawMeshShadow(item.mesh, shadowUpload.ubo.lightVP[c] * item.model, doubleSidedShadow);
1021 }
1022 }
1023 // Extra shadow casters (billboard/card geometry not in the ECS).
1024 for (const auto& entry : g_shadowDrawers) entry.drawer(gfx, shadowUpload.ubo.lightVP[c], *cd);
1025 gfx.endShadowPass();
1026 eve::debug::rtPassEnd("ShadowPass");
1027 }
1028 }
1029
1030 // Spot (perspective) local shadow slots — layers after the three CSM cascades.
1031 // Paging may keep a slot resident without redrawing when needsUpdate is false.
1032 if (doShadow && !localShadowSlots.empty() && (haveManager || haveExtraShadowCasters) && defaultCam) {
1033 auto cd = defaultCam->data();
1034 for (const LocalShadowSlot& slot : localShadowSlots) {
1035 if (!slot.needsUpdate) continue;
1036 eve::debug::rtPassBegin("LocalShadowPass");
1037 gfx.beginShadowPass(slot.layer);
1038 const FrustumPlanes localFrustum = extractFrustum(slot.lightVP);
1039 for (const auto& item : items) {
1040 if (item.surfaceMode == SurfaceMode::Transparent) continue;
1041 // Approximate: draw every opaque caster inside the spot frustum.
1042 const glm::vec3 center = glm::vec3(item.model[3]);
1043 if (!localFrustum.sphereVisible(center, 2.f)) continue;
1044 eve::debug::rtBind("mesh", "localShadowCaster");
1045 Texture* shadowAlbedo = item.material ? item.material->getAlbedoTexture() : item.mr->texture;
1046 const bool doubleSidedShadow =
1047 item.shadowDoubleSided || (item.material && item.material->getDoubleSided());
1048 gfx.setMesh3DSkinInfluenceLimit(skinInfluenceLimit(item.skinInfluenceLimit));
1049 if (item.surfaceMode == SurfaceMode::Masked)
1050 gfx.drawMeshShadowAlpha(item.mesh, slot.lightVP * item.model, shadowAlbedo, doubleSidedShadow,
1051 item.lodWeight, item.lodFadeReverse, item.speedTreeFade);
1052 else
1053 gfx.drawMeshShadow(item.mesh, slot.lightVP * item.model, doubleSidedShadow);
1054 }
1055 for (const auto& entry : g_shadowDrawers) entry.drawer(gfx, slot.lightVP, *cd);
1056 gfx.endShadowPass();
1057 eve::debug::rtPassEnd("LocalShadowPass");
1058 }
1059 }
1060
1061 // G-buffer fill (sampleable depth/normal) — before the forward swapchain pass.
1062 if (doGBuffer && defaultCam &&
1063 (haveManager || !g_gbufferDrawers.empty() || !g_gpuOpaqueCollectors.empty())) {
1064 eve::debug::rtPassBegin("GBufferPass");
1065 const CameraView& cv = cams[0]; // default camera is slot 0
1066 const int gw = std::max(1, gfx.getPixelWidth() > 0 ? gfx.getPixelWidth() : gfx.getWidth());
1067 const int gh = std::max(1, gfx.getPixelHeight() > 0 ? gfx.getPixelHeight() : gfx.getHeight());
1068 gfx.beginGBufferPass(gw, gh);
1069 for (const auto& item : items) {
1070 // X-ray targets are skipped so their pixels record the occluder depth
1071 // behind them; the X-ray shader samples that to detect occlusion.
1072 if (item.xray || item.mr->instances) continue;
1073 if (item.surfaceMode == SurfaceMode::Transparent) continue;
1074 if (!item.inDefaultView) continue;
1075 Texture* alb = item.material ? item.material->getAlbedoTexture() : item.mr->texture;
1076 const float tr = item.material ? item.material->getTintR() : item.mr->r;
1077 const float tg = item.material ? item.material->getTintG() : item.mr->g;
1078 const float tb = item.material ? item.material->getTintB() : item.mr->b;
1079 const float roughness = item.material ? item.material->getRoughness() : item.mr->roughness;
1080 const float metallic = item.material ? item.material->getMetallic() : item.mr->metallic;
1081 gfx.setMesh3DSkinInfluenceLimit(skinInfluenceLimit(item.skinInfluenceLimit));
1082 eve::debug::rtDraw("drawMeshGBuffer", "gbuffer");
1083 if (item.surfaceMode == SurfaceMode::Masked)
1084 gfx.drawMeshGBufferAlpha(item.mesh, cv.viewProj * item.model, item.model, cv.data->nearZ, cv.data->farZ,
1085 alb, tr, tg, tb, item.temporalMotion.x, item.temporalMotion.y, roughness,
1086 metallic);
1087 else
1088 gfx.drawMeshGBuffer(item.mesh, cv.viewProj * item.model, item.model, cv.data->nearZ, cv.data->farZ, alb,
1089 tr, tg, tb, item.temporalMotion.x, item.temporalMotion.y, roughness, metallic);
1090 }
1091 // Extra G-buffer contributors (billboard/card geometry not in the ECS).
1092 for (const auto& drawer : g_gbufferDrawers) drawer(gfx, *cv.data, cv.viewProj, aspect);
1093 gfx.endGBufferPass();
1094 eve::debug::rtPassEnd("GBufferPass");
1095 } else if (!doGBuffer) {
1096 rc->getGBuffer()->clear();
1097 }
1098
1099 // Screen-space decal layer: box-projected decals read the G-buffer
1100 // depth/normal and write albedo/normal/params targets sampled by
1101 // mesh3d.frag before lighting. Skipped when no decals are registered or
1102 // the backend cannot run the pass (WebGPU).
1103 if (doDecal && defaultCam && !g_decalDrawers.empty() && gfx.supportsDecal()) {
1104 eve::debug::rtPassBegin("DecalPass");
1105 const CameraView& cv = cams[0];
1106 const int dw = std::max(1, gfx.getPixelWidth() > 0 ? gfx.getPixelWidth() : gfx.getWidth());
1107 const int dh = std::max(1, gfx.getPixelHeight() > 0 ? gfx.getPixelHeight() : gfx.getHeight());
1108 gfx.beginDecalPass(dw, dh);
1109 gfx.setDecalCamera(cv.viewProj, cv.data->nearZ, cv.data->farZ);
1110 for (const auto& drawer : g_decalDrawers) drawer(gfx, *cv.data, cv.viewProj, aspect);
1111 gfx.endDecalPass();
1112 eve::debug::rtPassEnd("DecalPass");
1113 }
1114
1115 if (!doForward && !doHair) return;
1116
1117 // GPU-driven opaque is a ForwardPlus path; Hybrid lights core opaque via
1118 // deferredLighting and must not also submit GPU-driven lit draws.
1119 const bool gpuDrivenWanted = !doDeferredLighting && rc->isEnabled("gpuDriven") && gfx.supportsGpuDriven3D();
1120 gfx.gpuDrivenSetEnabled(gpuDrivenWanted);
1121
1122 if (defaultCam) {
1123 auto cd = defaultCam->data();
1124 gfx.setSceneExposure(std::exp2(cd->exposureEV));
1125 gfx.setSceneAutoExposure(cd->autoExposure, cd->autoExposureMinEV, cd->autoExposureMaxEV);
1126 gfx.setSceneBloom(cd->bloomIntensity, cd->bloomThreshold);
1127 gfx.setSceneDepthOfField(cd->dofFocusDistance, cd->dofMaxBlurPx, cd->dofFocusRange,
1128 cd->nearZ, cd->farZ);
1129 }
1130 if (defaultCam) {
1131 const auto& cv = cams.front();
1132 auto prepared = detail::prepareViewResources(gfx, cv.viewProj, cv.eye);
1133 if (!prepared) throw eve::Exception("%s", prepared.status().describe().c_str());
1134 }
1135 gfx.begin3DFrame();
1136 if (!gfx.had3DThisFrame()) return;
1137
1138 // Phase C Hybrid: fullscreen clustered deferred lighting fills scene color
1139 // for core PBR opaque/masked pixels written to the GBuffer. Transparent and
1140 // extended-feature opaques still go through Forward+ below.
1141 if (doDeferredLighting && defaultCam && !cams.empty()) {
1142 eve::debug::rtPassBegin("DeferredLighting");
1143 const CameraView& cv = cams[0];
1144 gfx.setMesh3DViewProj(cv.viewProj);
1145 gfx.setMesh3DView(cv.view);
1146 gfx.setMesh3DClip(cv.data->nearZ, cv.data->farZ);
1147 gfx.setMesh3DCameraPos(cv.eye);
1148 gfx.setMesh3DEnv(cv.data->envMap, cv.data->envIntensity);
1149 gfx.setMesh3DEnvProbe(cv.data->envProbeCenter, cv.data->envProbeExtent);
1150 gfx.setMesh3DLighting(cv.lighting);
1151 if (cv.clusteredValid) {
1152 gfx.setMesh3DClusteredLighting(cv.clustered);
1153 } else {
1154 // Orthographic / empty: drawDeferredLighting synthesizes a valid
1155 // empty cluster table from the frame UBO primary light.
1157 off.active = false;
1159 }
1161 eve::debug::rtPassEnd("DeferredLighting");
1162 }
1163
1164 auto drawPersistentPrimitives = [&]() {
1165 if (cams.empty()) return;
1166 const CameraView& camera = cams.front();
1168 context.view = camera.view;
1169 context.projection = camera.proj;
1170 context.cameraPosition = camera.eye;
1171 context.viewportSize = glm::ivec2(gfx.getPixelWidth() > 0 ? gfx.getPixelWidth() : gfx.getWidth(),
1172 gfx.getPixelHeight() > 0 ? gfx.getPixelHeight() : gfx.getHeight());
1173 context.nearPlane = camera.data->nearZ;
1174 context.farPlane = camera.data->farZ;
1176 gfx.getPrimitiveScene()->render(canvas);
1177 if (!canvas.commands().empty() || !canvas.triangles().empty()) gfx.drawPrimitiveScene(canvas);
1178 };
1179
1180 if (!haveManager && g_forwardDrawers.empty() && g_gpuOpaqueCollectors.empty()) {
1181 drawPersistentPrimitives();
1182 return;
1183 }
1184
1185 // Replay the items collected above: opaque first, hair back-to-front.
1186 std::vector<const CulledItem*> opaque;
1187 std::vector<const CulledItem*> transparentItems;
1188 opaque.reserve(items.size());
1189 transparentItems.reserve(items.size() / 4);
1190 for (const auto& item : items) {
1191 if (!item.inView || item.shadowsOnly) continue;
1192 (item.surfaceMode == SurfaceMode::Transparent ? transparentItems : opaque).push_back(&item);
1193 }
1194 // Opaque: group by (camera, shader, material, mesh) so the backend sees
1195 // long runs of identical pipeline/descriptor state instead of thrashing
1196 // between materials. Hair stays sorted back-to-front by distance below.
1197 std::stable_sort(opaque.begin(), opaque.end(), [](const CulledItem* a, const CulledItem* b) {
1198 if (a->camIdx != b->camIdx) return a->camIdx < b->camIdx;
1199 if (a->shader != b->shader) return a->shader < b->shader;
1200 if (a->material != b->material) return a->material < b->material;
1201 return a->mesh < b->mesh;
1202 });
1203 std::stable_sort(transparentItems.begin(), transparentItems.end(), [](const CulledItem* a, const CulledItem* b) {
1204 if (a->camIdx != b->camIdx) return a->camIdx < b->camIdx;
1205 if (a->sortPriority != b->sortPriority) return a->sortPriority < b->sortPriority;
1206 return a->projectedDepth > b->projectedDepth;
1207 });
1208
1209 // Core metallic-roughness opaque/masked already lit by deferredLighting —
1210 // skip their Forward+ draws. Custom shaders, extended PBR, unlit, instances,
1211 // light probes, and x-ray stay on the forward path.
1212 auto isDeferredCoreOpaque = [&](const CulledItem& item) -> bool {
1213 if (!doDeferredLighting) return false;
1214 if (item.surfaceMode == SurfaceMode::Transparent) return false;
1215 if (item.hair || item.xray || item.speedTreeFade) return false;
1216 if (item.mr->instances || item.lightProbeUsage != 0) return false;
1217 if (item.shader) return false;
1218 Material* mat = item.material;
1219 if (mat) {
1220 if (mat->effectiveShader()) return false;
1221 if (mat->isTransparentHair()) return false;
1222 const std::string model = mat->getShadingModel();
1223 if (model != "pbr") return false;
1224 if (!mat->getReceiveLight()) return false;
1225 if (mat->hasPbrSurface()) {
1226 const PbrSurface s = mat->pbrSurface();
1227 if (s.clearcoatFactor > 1e-4f || s.anisotropyStrength > 1e-4f) return false;
1228 }
1229 } else {
1230 if (item.mr->shader) return false;
1231 if (!item.mr->receiveLight) return false;
1232 }
1233 return true;
1234 };
1235
1236 auto bindLegacyMaterial = [&](Renderable3D::MeshRenderer* mr) {
1238 mr->isHair, 0.5f);
1239 gfx.setMesh3DMaterial(mr->metallic, mr->roughness);
1240 gfx.setMesh3DTexCellBomb(mr->texBombScale, mr->texBombStrength, mr->texBombRot);
1241 gfx.setMesh3DNormalTexture(mr->normalTexture);
1242 gfx.setMesh3DPackedNormalMask(mr->packedNormalMask);
1243 gfx.setMesh3DHeightTexture(mr->heightTexture);
1244 gfx.setMesh3DParallax(mr->parallaxScale, mr->parallaxMinLayers, mr->parallaxMaxLayers);
1245 gfx.setMesh3DShadowReceive(mr->receiveShadow);
1246 };
1247
1248 // Per-camera / per-lighting state. The clustered SSBO table is uploaded at
1249 // most once per camera; afterwards only the cheap active flag toggles.
1250 int curCam = -1;
1251 bool curLit = false;
1252 bool curClustered = false;
1253 int curLightProbeUsage = 0;
1254
1255 auto drawMeshWithMaterial = [&](const CulledItem& item, CameraView& cv) {
1256 auto* mr = item.mr;
1257 Mesh* drawMesh = item.mesh;
1258 Material* mat = item.material;
1259 if (!drawMesh) return;
1260
1261 Texture* albedo = mr->texture;
1262 Color tint(mr->r, mr->g, mr->b, mr->a);
1263 Shader* shader = mr->shader;
1264 if (mat) {
1265 auto bound = mat->bind(gfx);
1266 if (!bound) throw Exception("%s", bound.error()->message().c_str());
1267 albedo = mat->getAlbedoTexture();
1268 tint = Color(mat->getTintR(), mat->getTintG(), mat->getTintB(), mat->getTintA());
1269 shader = mat->effectiveShader();
1270 } else {
1271 auto reset = gfx.setMesh3DPbrSurface(nullptr);
1272 if (!reset) throw Exception("%s", reset.error()->message().c_str());
1273 bindLegacyMaterial(mr);
1274 }
1275 if(item.lodWeight<0.9999f && !item.speedTreeFade)
1277 else if (item.speedTreeFade)
1279 mat && mat->getDoubleSided(), 0.5f, "cutoff");
1280 gfx.setMesh3DShadowReceive(item.receiveShadow && (!mat || mat->getReceiveShadow()));
1281
1282 const bool lit = mat ? mat->getReceiveLight() : mr->receiveLight;
1283 const bool clustered = lit && useClustered && !shader && item.lightProbeUsage == 0 && !item.speedTreeFade;
1284 if (item.camIdx != curCam || lit != curLit || clustered != curClustered ||
1285 (lit && (item.lightProbeUsage != 0 || curLightProbeUsage != 0))) {
1286 if (item.camIdx != curCam) {
1287 gfx.setMesh3DViewProj(cv.viewProj);
1288 gfx.setMesh3DView(cv.view);
1289 gfx.setMesh3DClip(cv.data->nearZ, cv.data->farZ);
1290 gfx.setMesh3DCameraPos(cv.eye);
1291 gfx.setMesh3DEnv(cv.data->envMap, cv.data->envIntensity);
1292 gfx.setMesh3DEnvProbe(cv.data->envProbeCenter, cv.data->envProbeExtent);
1293 curCam = item.camIdx;
1294 }
1295 if (clustered && cv.clusteredValid) {
1296 if (!cv.clusteredUploaded) {
1297 gfx.setMesh3DClusteredLighting(cv.clustered);
1298 cv.clusteredUploaded = true;
1299 } else {
1300 gfx.setMesh3DClusteredActive(true);
1301 }
1302 } else {
1303 gfx.setMesh3DClusteredActive(false);
1304 }
1305 if (lit) {
1306 Lighting3DPack lighting = cv.lighting;
1307 bool sampled = false;
1308 if (item.lightProbeUsage == 4 && mr->customLightProbe) {
1309 lighting.diffuseProbeSh = mr->customLightProbeSh;
1310 sampled = true;
1311 } else if (item.lightProbeUsage == 1) {
1313 item.worldC, 4);
1314 if (sample) {
1315 lighting.diffuseProbeSh = sample.value().coefficients;
1316 sampled = true;
1317 }
1318 } else if (item.lightProbeUsage == 2) {
1320 if (volume) {
1321 lighting.diffuseVolumePosition = volume.value().positions;
1322 lighting.diffuseVolumeExtent = volume.value().extents;
1323 lighting.diffuseVolumeSh = volume.value().coefficients;
1324 lighting.diffuseVolumeProbeCount = volume.value().count;
1325 lighting.diffuseVolumeTrilinearCell = volume.value().trilinearCell;
1326 }
1327 }
1328 lighting.diffuseProbeShEnabled = sampled;
1330 } else {
1331 Lighting3DPack none{};
1332 none.count = 0;
1333 none.ambient = glm::vec4(1.f, 1.f, 1.f, 0.f);
1334 gfx.setMesh3DLighting(none);
1335 }
1336 curLit = lit;
1337 curClustered = clustered;
1338 curLightProbeUsage = item.lightProbeUsage;
1339 }
1340
1341 // Local probes are spatial data. Select at the renderable bounds center
1342 // so adjacent volumes do not leak the camera's nearest probe into every
1343 // object in the frame.
1344 gfx.setMesh3DReflectionProbes(item.useReflectionProbes
1345 ? selectReflectionProbes(*cv.data, item.worldC) : ReflectionProbeUpload{});
1346
1347 const glm::mat4& model = item.model;
1348 if (albedo) eve::debug::rtBind("texture", "albedo");
1349 if (shader) eve::debug::rtBind("shader", item.hair ? "hair" : "mesh");
1350 eve::debug::rtBind("mesh", "renderable3d");
1351 eve::debug::rtDraw("drawMeshShader", shader ? "custom" : "default");
1352 if (mr->instances) {
1353 const auto &range = *mr->instances;
1354 auto drawn = gfx.drawMeshShaderInstances(*drawMesh, *shader, model, tint, range.first, range.count);
1355 if (!drawn) throw eve::Exception("%s", drawn.status().describe().c_str());
1356 } else
1357 gfx.drawMeshShader(drawMesh, model, albedo, tint, shader);
1358
1359 // X-ray second pass: paint only the occluded (behind-building) part over
1360 // the scene. The pipeline runs with depth test/write off + alpha blend and
1361 // the shader discards visible fragments by sampling the G-buffer depth.
1362 if (mr->xrayHighlight && mr->xrayShader) {
1363 float sw = gfx.getPixelWidth() > 0 ? float(gfx.getPixelWidth()) : float(gfx.getWidth());
1364 float shh = gfx.getPixelHeight() > 0 ? float(gfx.getPixelHeight()) : float(gfx.getHeight());
1365 if (mr->xrayShader->hasUniform("screenW")) mr->xrayShader->sendFloat("screenW", sw);
1366 if (mr->xrayShader->hasUniform("screenH")) mr->xrayShader->sendFloat("screenH", shh);
1367 eve::debug::rtBind("shader", "xray");
1368 eve::debug::rtDraw("drawMeshShader", "xray");
1369 gfx.drawMeshShader(drawMesh, model, albedo, tint, mr->xrayShader);
1370 }
1371 };
1372
1373 // Provide the G-buffer depth to X-ray shaders for the occlusion test.
1374 if (doGBuffer && rc->getGBuffer() && rc->getGBuffer()->isValid()) {
1376 }
1377
1378 if (doForward) {
1379 bool gpuDrivenUsed = false;
1380 if (gpuDrivenWanted && !useClustered && defaultCam &&
1381 (!opaque.empty() || !g_gpuOpaqueCollectors.empty())) {
1382 std::vector<const CulledItem*> gpuOpaque;
1383 std::vector<const CulledItem*> cpuOpaque;
1384 gpuOpaque.reserve(opaque.size());
1385 cpuOpaque.reserve(opaque.size());
1386 for (const CulledItem* it : opaque) {
1387 if (it->mesh->hasGpuSkinning() || it->camIdx != 0 || !it->material || it->mr->camera != nullptr ||
1388 it->material->effectiveShader() != nullptr || it->lightProbeUsage != 0 ||
1389 !gfx.gpuDrivenMaterialUsable(it->material))
1390 cpuOpaque.push_back(it);
1391 else
1392 gpuOpaque.push_back(it);
1393 }
1394 if (!gpuOpaque.empty() || !g_gpuOpaqueCollectors.empty()) {
1395 const CameraView& cv = cams[0]; // default camera is slot 0
1396 const Camera3D::Data* cd = cv.data;
1397 const glm::vec3 eye = cv.eye;
1398 gfx.setMesh3DViewProj(cv.viewProj);
1399 gfx.setMesh3DView(cv.view);
1400 gfx.setMesh3DClip(cd->nearZ, cd->farZ);
1401 gfx.setMesh3DCameraPos(cv.eye);
1402 gfx.setMesh3DEnv(cd->envMap, cd->envIntensity);
1406 off.active = false;
1408 gfx.setMesh3DLighting(cv.lighting);
1409
1410 std::vector<eve::graphics::GpuInstance> instances;
1411 instances.reserve(gpuOpaque.size());
1412 bool recordsOk = true;
1413 bool vgAny = false;
1414 const bool resolveWanted = gfx.gpuDrivenResolveWanted();
1415 for (const CulledItem* it : gpuOpaque) {
1416 const ReflectionProbeUpload probes = selectReflectionProbes(*cd, it->worldC);
1417 // Stage 3 VG: meshes with a virtual-geometry asset are culled /
1418 // drawn through the cluster path, not the instance chain.
1419 const uint32_t vgAsset =
1420 resolveWanted ? gfx.gpuDrivenVgAssetId(it->mesh) : eve::graphics::kInvalidGpuDrivenSlot;
1421 if (vgAsset != eve::graphics::kInvalidGpuDrivenSlot && probes.count == 0) {
1422 const uint32_t matId = gfx.gpuDrivenMaterialRecord(it->material);
1424 recordsOk = false;
1425 break;
1426 }
1427 vgAny |= gfx.gpuDrivenVgSetInstance(vgAsset, it->model, matId);
1428 continue;
1429 }
1431 inst.model = it->model;
1432 inst.meshId = gfx.gpuDrivenMeshRecord(it->mesh);
1433 inst.materialId = gfx.gpuDrivenMaterialRecord(it->material);
1434 inst.reflectionProbeSlots.z = uint32_t(probes.count);
1435 for (int probeIndex = 0; probeIndex < probes.count; ++probeIndex) {
1436 const auto& probe = probes.probes[probeIndex];
1437 const uint32_t slot = gfx.gpuDrivenReflectionProbeSlot(probe.cubemap);
1439 recordsOk = false;
1440 break;
1441 }
1442 inst.reflectionProbeSlots[probeIndex] = slot;
1443 inst.reflectionProbeCenter[probeIndex] = glm::vec4(probe.center, probe.intensity);
1444 inst.reflectionProbeExtent[probeIndex] = glm::vec4(probe.extent, probe.blendDistance);
1445 }
1446 if (!recordsOk) break;
1447 if (inst.meshId == eve::graphics::kInvalidGpuDrivenSlot ||
1448 inst.materialId == eve::graphics::kInvalidGpuDrivenSlot) {
1449 recordsOk = false;
1450 break;
1451 }
1452 instances.push_back(inst);
1453 }
1454 if (recordsOk && !g_gpuOpaqueCollectors.empty()) {
1455 const auto collectors = g_gpuOpaqueCollectors;
1456 for (const auto& entry : collectors)
1457 entry.collector(gfx, *cd, cv.viewProj, aspect, instances);
1458 }
1459 if (recordsOk) {
1460 // Stage 2: GPU frustum/HZB cull + GPU-written indirect commands.
1461 if (gfx.gpuDrivenCullEnabled() && !instances.empty() &&
1462 gfx.gpuDrivenCullBegin(instances.data(), uint32_t(instances.size()))) {
1463 gfx.gpuDrivenCullEmit(cv.cullViewProj, eye, cd->fovYDeg, cd->nearZ, cd->farZ);
1464 if (gfx.gpuDrivenResolveWanted()) {
1465 // Stage 3: opaque goes to the GBuffer vis pass, the
1466 // scene color pass runs the fullscreen resolve.
1469 gfx.gpuDrivenResolve();
1470 } else {
1472 gfx.gpuDrivenDrawOpaque();
1473 }
1474 gpuDrivenUsed = true;
1475 } else if (gfx.gpuDrivenCullEnabled() && instances.empty() && vgAny) {
1476 // VG-only frame: no instance chain; the vis pass runs the
1477 // VG cluster cull + draws + resolve.
1478 gfx.gpuDrivenVgComputeSection(cv.cullViewProj, eye, cd->fovYDeg, cd->nearZ, cd->farZ);
1481 gfx.gpuDrivenResolve();
1482 gpuDrivenUsed = true;
1483 } else {
1484 // Deferred scene pass must be open before stage-1 recording.
1486 if (gfx.gpuDrivenSubmitOpaque(instances.data(), uint32_t(instances.size())))
1487 gpuDrivenUsed = true;
1488 }
1489 }
1490 if (gpuDrivenUsed) {
1492 for (const CulledItem* item : cpuOpaque) drawMeshWithMaterial(*item, cams[size_t(item->camIdx)]);
1493 }
1494 }
1495 }
1496 if (!gpuDrivenUsed) {
1498 for (const CulledItem* item : opaque) {
1499 if (isDeferredCoreOpaque(*item)) continue;
1500 drawMeshWithMaterial(*item, cams[size_t(item->camIdx)]);
1501 }
1502 }
1503 if (defaultCam && !g_forwardDrawers.empty()) {
1505 const CameraView& cv = cams.front();
1506 const Camera3D::Data* cd = cv.data;
1507 gfx.setMesh3DViewProj(cv.viewProj);
1508 gfx.setMesh3DView(cv.view);
1509 gfx.setMesh3DClip(cd->nearZ, cd->farZ);
1510 gfx.setMesh3DCameraPos(cv.eye);
1511 gfx.setMesh3DEnv(cd->envMap, cd->envIntensity);
1514 gfx.setMesh3DLighting(cv.lighting);
1515 if (useClustered)
1516 gfx.setMesh3DClusteredLighting(cv.clustered);
1517 else {
1519 off.active = false;
1521 }
1522 const auto drawers = g_forwardDrawers;
1523 for (const auto& entry : drawers) entry.drawer(gfx, *cd, cv.viewProj, aspect);
1524 }
1525 }
1526 if (doForward || doHair) {
1527 // Generic transparent surfaces belong to the forward pass; the legacy
1528 // hair pass remains independently switchable for hair materials.
1530 for (const CulledItem* item : transparentItems) {
1531 if ((item->hair && doHair) || (!item->hair && doForward))
1532 drawMeshWithMaterial(*item, cams[size_t(item->camIdx)]);
1533 }
1534 }
1535
1536 drawPersistentPrimitives();
1537
1538 const bool doAO = rc->isEnabled("ao");
1539 const bool doRTGI = rc->isEnabled("rtgi") || rc->isEnabled("reflectionChain");
1540 const bool doSSR = rc->isEnabled("ssr") || rc->isEnabled("reflectionChain");
1541 const bool doRTX = rc->isEnabled("rtx");
1542 bool aoApplied = false;
1543 auto applyAO = [&]() {
1544 if (!doAO || !gfx.supportsGBufferPost() || !defaultCam || !gfx.had3DThisFrame()) return;
1545 GBuffer* gb = rc->getGBuffer();
1546 if (gb && gb->isValid()) {
1547 auto cd = defaultCam->data();
1548 const float aspectSafe = aspect > 1e-4f ? aspect : 1.f;
1549 auto bindCam = [&](auto* fx) {
1550 fx->setCamera(cd->eyeX, cd->eyeY, cd->eyeZ, cd->targetX, cd->targetY, cd->targetZ, cd->upX, cd->upY,
1551 cd->upZ, cd->fovYDeg, aspectSafe, cd->nearZ, cd->farZ);
1552 };
1554 ao->setQuality("medium");
1555 ao->setIntensity(0.16f);
1556 ao->setPower(1.1f);
1557 ao->setRadius(std::clamp(cd->farZ * 0.006f, 0.18f, 0.35f));
1558 bindCam(ao);
1559 // WebGPU cannot bind a depth-aspect texture to the generic 2D
1560 // post-process layout. Its G-buffer already carries equivalent
1561 // linear depth in a filterable RGBA target.
1562 Texture* depth = gfx.getBackendName() == "webgpu" ? gb->getDepthTexture() : gb->getHwDepthTexture();
1563 if (depth) ao->applyFromGBuffer(&gfx, depth, gb->getNormalTexture());
1564 // Fullscreen SSGI from lit scene color reprints nearby props
1565 // (curtains, planters) onto the floor as multiple swimming ghosts.
1566 // Mesh shaders still add hemispheric sky/ground + wrap fill.
1567 }
1568 aoApplied = true;
1569 };
1570
1571 const bool doReflectionLighting = doRTGI || doSSR || doRTX;
1572 if (doReflectionLighting && defaultCam && gfx.had3DThisFrame()) {
1573 GBuffer* gb = rc->getGBuffer();
1574 if (gb && gb->isValid()) {
1575 Texture* sceneColor = gfx.getSceneColorTexture();
1576 Texture* depth = gfx.getBackendName() == "webgpu" ? gb->getDepthTexture() : gb->getHwDepthTexture();
1577 if (sceneColor && depth) {
1578 DepthPyramid* depthPyramid = gfx.pipelineDepthPyramid();
1579 const std::string reflectionQuality = rc->getReflectionQuality();
1580 const int depthBudget = reflectionQuality == "low" ? 4 : reflectionQuality == "medium" ? 6 : 8;
1581 Texture* depthAtlas = depthPyramid->build(depth, depthBudget);
1582 const int depthLevels = depthPyramid->getLevelCount();
1583 auto cd = defaultCam->data();
1584 const float aspectSafe = aspect > 1e-4f ? aspect : 1.f;
1585 const float dw = gfx.getCanvas() ? float(gfx.getCanvas()->getWidth()) : float(gfx.getWidth());
1586 const float dh = gfx.getCanvas() ? float(gfx.getCanvas()->getHeight()) : float(gfx.getHeight());
1587 auto bindCam = [&](auto* fx) {
1588 fx->setCamera(cd->eyeX, cd->eyeY, cd->eyeZ, cd->targetX, cd->targetY, cd->targetZ, cd->upX, cd->upY,
1589 cd->upZ, cd->fovYDeg, aspectSafe, cd->nearZ, cd->farZ);
1590 if (!cams.empty()) fx->setInvViewProj(glm::inverse(cams.front().viewProj));
1591 };
1592
1593 Texture* rtgiTexture = nullptr;
1594 Texture* ssrTexture = nullptr;
1595
1596 if (doRTGI) {
1598 if (gi->getQuality() != rc->getReflectionQuality()) gi->setQuality(rc->getReflectionQuality());
1602 gi->setDepthPyramid(depthAtlas, depthLevels);
1603 bindCam(gi);
1604 Canvas* rtgiCanvas = gi->getWorkingCanvas();
1605 if (rtgiCanvas) {
1606 gi->applyFromSceneTo(&gfx, sceneColor, depth, rtgiCanvas);
1607 rtgiTexture = gi->getWorkingTexture();
1608 }
1609 }
1610
1611 // Hardware RT reflections (optional module). Prefer RTX when
1612 // available; fall through to SSR on NoOp / failure / absent provider.
1613 if (doRTX) {
1614 if (auto* rt = eve::cap::query<IRayTracing>()) {
1615 if (rt->isAvailable()) {
1617 Canvas* reflCanvas = ssr->getReflectionCanvas();
1618 if (reflCanvas) {
1619 // Switching canvas ends the open scene-color pass so RT
1620 // can sample the finished frame on the present CB.
1621 Canvas* prevCanvas = gfx.getCanvas();
1622 gfx.setCanvas(reflCanvas);
1623
1624 rt->clearScene();
1625 constexpr size_t kMaxRtMeshes = 64;
1626 size_t registered = 0;
1627 for (const CulledItem* item : opaque) {
1628 if (!item || !item->mesh || registered >= kMaxRtMeshes) continue;
1629 auto added = rt->addMesh(item->mesh, item->model);
1630 if (added.ok()) {
1631 ++registered;
1632 (void)added.value();
1633 } else {
1634 added.ignore();
1635 }
1636 }
1637 bool produced = false;
1638 if (registered > 0) {
1639 auto rebuilt = rt->rebuildScene();
1640 if (rebuilt.ok()) {
1641 glm::mat4 viewProj = !cams.empty() ? cams.front().viewProj : glm::mat4(1.f);
1642 glm::mat4 invVP = glm::inverse(viewProj);
1643 glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
1644 auto applied =
1645 rt->applyReflections(&gfx, sceneColor, depth, gb->getNormalTexture(),
1646 reflCanvas, invVP, viewProj, eye);
1647 if (applied.ok() && applied.code() != eve::StatusCode::NoOp) {
1648 ssrTexture = ssr->getReflectionTexture();
1649 produced = true;
1650 } else {
1651 applied.ignore();
1652 }
1653 } else {
1654 rebuilt.ignore();
1655 }
1656 }
1657 (void)produced;
1658 gfx.setCanvas(prevCanvas);
1659 }
1660 }
1661 }
1662 }
1663
1664 // Portable SSR path: explicit ssr/reflectionChain, or RTX requested
1665 // but hardware did not produce a usable reflection texture.
1666 if ((doSSR || doRTX) && !ssrTexture) {
1668 if (ssr->getQuality() != rc->getReflectionQuality()) ssr->setQuality(rc->getReflectionQuality());
1669 ssr->setEnabled(true);
1671 ssr->setDepthPyramid(depthAtlas, depthLevels);
1672 bindCam(ssr);
1673 Canvas* reflCanvas = ssr->getReflectionCanvas();
1674 if (reflCanvas) {
1675 ssr->applyFromSceneTo(&gfx, sceneColor, depth, gb->getNormalTexture(), gb->getAlbedoTexture(),
1676 reflCanvas);
1677 ssrTexture = ssr->getReflectionTexture();
1678 }
1679 }
1680
1681 // All offscreen reflection work is complete. Queue AO and the
1682 // reflection overlays only now, so a later setCanvas() cannot
1683 // force an early swapchain present with pending overlays.
1684 applyAO();
1685 if (rtgiTexture || ssrTexture) {
1686 if (gfx.getBackendName() == "vulkan") {
1687 if (rtgiTexture)
1688 gfx.drawTexturedRectShaderUV(rtgiTexture, nullptr, 0.f, 0.f, dw, dh, 0.f, 0.f, 1.f, 1.f,
1689 Color(1.f, 1.f, 1.f, 1.f), false, BlendMode::Additive);
1690 if (ssrTexture)
1691 gfx.drawTexturedRectShaderUV(ssrTexture, nullptr, 0.f, 0.f, dw, dh, 0.f, 0.f, 1.f, 1.f,
1692 Color(1.f, 1.f, 1.f, 1.f), false, BlendMode::Additive);
1693 } else if (Canvas* composite = gfx.pipelineReflectionComposite(int(dw), int(dh))) {
1694 // Register before setCanvas(): switching away from the scene target
1695 // closes its pass and queues the final resolve immediately.
1696 gfx.setFinalSceneTexture(composite->getTexture());
1697 Canvas* previous = gfx.getCanvas();
1698 gfx.setCanvas(composite);
1699 gfx.drawTexturedRectShaderUV(sceneColor, nullptr, 0.f, 0.f, dw, dh, 0.f, 0.f, 1.f, 1.f,
1700 Color(1.f, 1.f, 1.f, 1.f), false, BlendMode::Opaque);
1701 if (rtgiTexture)
1702 gfx.drawTexturedRectShaderUV(rtgiTexture, nullptr, 0.f, 0.f, dw, dh, 0.f, 0.f, 1.f, 1.f,
1703 Color(1.f, 1.f, 1.f, 1.f), false, BlendMode::Additive);
1704 if (ssrTexture)
1705 gfx.drawTexturedRectShaderUV(ssrTexture, nullptr, 0.f, 0.f, dw, dh, 0.f, 0.f, 1.f, 1.f,
1706 Color(1.f, 1.f, 1.f, 1.f), false, BlendMode::Premultiplied);
1707 gfx.setCanvas(previous);
1708 }
1709 }
1710 }
1711 }
1712 }
1713 if (!aoApplied) applyAO();
1714
1715 const bool doOutline = rc->isEnabled("outline");
1716 if (doOutline && defaultCam && gfx.had3DThisFrame()) {
1717 GBuffer* gb = rc->getGBuffer();
1718 if (gb && gb->isValid()) {
1719 Outline* outline = gfx.pipelineOutline();
1720 auto cd = defaultCam->data();
1721 outline->setClip(cd->nearZ, cd->farZ);
1722 Texture* depth = gfx.getBackendName() == "webgpu" ? gb->getDepthTexture() : gb->getHwDepthTexture();
1723 if (depth) outline->apply(&gfx, depth, gb->getNormalTexture());
1724 }
1725 }
1726}
1727
1728void RenderSystem3D::renderToCanvas(Graphics& gfx, Canvas* target, Camera3D* camera, uint32_t reflectionCaptureMask,
1729 float lodDistanceScale, bool includeTransparent, bool useClusteredLighting,
1730 Texture* skyFaceTexture, Mesh* skyQuad, float skyFaceTextureScale) {
1731 if (!target || !camera) return;
1732 auto cd = camera->data();
1733 const float aspect = target->getWidth() > 0 ? float(target->getWidth()) / float(target->getHeight()) : 1.f;
1734
1735 const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ);
1736 const glm::vec3 look(cd->targetX, cd->targetY, cd->targetZ);
1737 const glm::vec3 up(cd->upX, cd->upY, cd->upZ);
1738 const glm::mat4 viewM = glm::lookAtRH(eye, look, up);
1739 const float fovRad = cd->fovYDeg * 0.017453292519943295f;
1740 const glm::mat4 projM =
1741 cameraProjectionVulkanRH_ZO(cd->orthographic, fovRad, cd->orthoHeight, aspect, cd->nearZ, cd->farZ);
1742 auto prepared = detail::prepareViewResources(gfx, projM * viewM, eye);
1743 if (!prepared) throw eve::Exception("%s", prepared.status().describe().c_str());
1744 // Preview-quality forward pass: no shadow / G-buffer / AO passes.
1746 gfx.setMesh3DViewProj(projM * viewM);
1747 gfx.setMesh3DView(viewM);
1748 gfx.setMesh3DClip(cd->nearZ, cd->farZ);
1750 gfx.setMesh3DEnv(cd->envMap, cd->envIntensity);
1751 gfx.setMesh3DEnvProbe(cd->envProbeCenter, cd->envProbeExtent);
1752 gfx.setMesh3DReflectionProbes(selectReflectionProbes(*cd, eye));
1753 gfx.setSceneExposure(std::exp2(cd->exposureEV));
1754 gfx.setSceneAutoExposure(cd->autoExposure, cd->autoExposureMinEV, cd->autoExposureMaxEV);
1755 gfx.setSceneBloom(cd->bloomIntensity, cd->bloomThreshold);
1756 gfx.setSceneDepthOfField(cd->dofFocusDistance, cd->dofMaxBlurPx, cd->dofFocusRange, cd->nearZ,
1757 cd->farZ);
1758
1759 // Lighting: real lights (if any) + camera ambient; shadows/clustered off.
1760 std::vector<PackedLight3D> packed;
1761 collectLights3D(packed, size_t(ClusteredLightConfig::kMaxLights));
1762 promoteDirectional(packed);
1763 bool clusteredCapture = false;
1764 if (useClusteredLighting && packed.size() > size_t(Lighting3DPack::kMaxLights)) {
1765 std::vector<ClusteredLightGpu> clusteredPoints;
1766 std::vector<ClusteredLightGpu> clusteredDirections;
1767 splitLights(packed, clusteredPoints, clusteredDirections);
1768 if (clusteredDirections.empty()) {
1770 direction.posRadius = glm::vec4(gLightDir, 0.f);
1771 direction.color = glm::vec4(gLightColor, 1.f);
1772 clusteredDirections.push_back(direction);
1773 }
1775 clusteredPoints, clusteredDirections, viewM, cd->nearZ, cd->farZ, target->getWidth(), target->getHeight(),
1776 cd->fovYDeg * 0.017453292519943295f, glm::vec4(cd->ambientR, cd->ambientG, cd->ambientB, 0.f));
1777 clusteredCapture = clustered.active;
1779 } else {
1780 ClusteredLightingUpload noClustered{};
1781 noClustered.active = false;
1782 gfx.setMesh3DClusteredLighting(noClustered);
1783 }
1784 gfx.setMesh3DLighting(packLights3D(packed, cd.operator->()));
1785 ShadowUpload noShadow{};
1786 noShadow.active = false;
1787 gfx.setMesh3DShadows(noShadow);
1788
1789 if (skyFaceTexture && skyQuad) {
1790 const float distance = std::max(cd->farZ * 0.999f, cd->nearZ * 2.f);
1791 glm::mat4 skyModel = glm::inverse(viewM);
1792 skyModel = glm::translate(skyModel, glm::vec3(0.f, 0.f, -distance));
1793 skyModel = glm::scale(skyModel, glm::vec3(distance * aspect * 1.01f, distance * 1.01f, 1.f));
1794 Lighting3DPack unlit{};
1795 unlit.ambient = glm::vec4(1.f, 1.f, 1.f, 0.f);
1796 gfx.setMesh3DClusteredActive(false);
1797 gfx.setMesh3DLighting(unlit);
1798 gfx.setMesh3DEnv(nullptr, 0.f);
1800 gfx.setMesh3DMaterial(0.f, 1.f);
1801 gfx.setMesh3DShadowReceive(false);
1802 const float skyScale = std::max(skyFaceTextureScale, 0.f);
1803 gfx.drawMeshShader(skyQuad, skyModel, skyFaceTexture, Color(skyScale, skyScale, skyScale, 1.f), nullptr);
1804 gfx.setMesh3DEnv(cd->envMap, cd->envIntensity);
1805 gfx.setMesh3DClusteredActive(clusteredCapture);
1806 gfx.setMesh3DLighting(packLights3D(packed, cd.operator->()));
1807 }
1808
1809 const glm::mat4 captureViewProj = projM * viewM;
1810 if (ecs::current()->getManager<Renderable3D>() != nullptr) {
1811 const glm::mat4 captureView =
1812 glm::lookAtRH(eye, glm::vec3(cd->targetX, cd->targetY, cd->targetZ), glm::vec3(cd->upX, cd->upY, cd->upZ));
1813 const glm::mat4 captureProjection =
1814 perspectiveVulkanRH_ZO(cd->fovYDeg * 0.017453292519943295f, aspect, cd->nearZ, cd->farZ);
1815 const FrustumPlanes captureFrustum = extractFrustum(captureProjection * captureView);
1816 struct CanvasItem {
1817 Renderable3D::MeshRenderer* mr = nullptr;
1818 Mesh* mesh = nullptr;
1819 Material* material = nullptr;
1820 glm::mat4 model{1.f};
1821 float distance2 = 0.f;
1822 SurfaceMode surface = SurfaceMode::Opaque;
1823 int sortPriority = 0;
1824 float lodWeight = 1.f;
1825 bool lodFadeReverse = false;
1826 bool speedTreeFade = false;
1827 int skinInfluenceLimit = 4;
1828 int lightProbeUsage = 0;
1829 glm::vec3 worldCenter{0.f};
1830 };
1831 std::vector<CanvasItem> canvasItems;
1832 auto view = ecs::View<Renderable3D, Renderable3D::Transform3D, Renderable3D::MeshRenderer>();
1833 for (auto it = view.begin(); it != view.end(); ++it) {
1834 auto [xf, mr] = *it;
1835 if (!mr->visible || (mr->reflectionCaptureMask & reflectionCaptureMask) == 0u)
1836 continue;
1837 if (mr->instances) {
1839 if (!valid) throw eve::Exception("%s", valid.status().describe().c_str());
1840 }
1841 const glm::mat4 model = modelFromTransform(*xf);
1842 const float maxScale =
1843 std::max(std::abs(xf->sx), std::max(std::abs(xf->sy), std::abs(xf->sz)));
1844 const float dx = xf->x - eye.x;
1845 const float dy = xf->y - eye.y;
1846 const float dz = xf->z - eye.z;
1847 const float distance2 = dx * dx + dy * dy + dz * dz;
1848 auto append = [&](Mesh* mesh, Material* material, bool hair, float lodWeight,
1849 int skinInfluenceLimit = 4, int lightProbeUsage = 0,
1850 bool lodFadeReverse = false) {
1851 if (!mesh) return;
1852 glm::vec3 worldCenter(xf->x, xf->y, xf->z);
1853 if (mr->instances) {
1854 if (!meshInstanceRangeVisible(*mr->instances, model, captureProjection * captureView, eye)) return;
1855 } else if (mesh->hasBounds()) {
1856 const glm::vec4 transformedCenter =
1857 model * glm::vec4(mesh->boundsCx, mesh->boundsCy, mesh->boundsCz, 1.f);
1858 worldCenter = glm::vec3(transformedCenter);
1859 if (!captureFrustum.sphereVisible(worldCenter, mesh->boundsRadius * maxScale)) return;
1860 }
1861 SurfaceMode surface =
1863 lodWeight = std::clamp(lodWeight, 0.f, 1.f);
1864 const bool speedTreeFade = mr->lodFadeMode == 1 && lodWeight < 0.9999f &&
1865 !(material && material->effectiveShader());
1866 if (lodWeight < 0.9999f && !speedTreeFade) surface = SurfaceMode::Transparent;
1867 if (surface == SurfaceMode::Transparent && !includeTransparent) return;
1868 const int sortPriority = material ? material->getSortPriority() : 0;
1869 canvasItems.push_back(
1870 CanvasItem{mr, mesh, material, model, distance2, surface, sortPriority, lodWeight,
1872 worldCenter});
1873 };
1874 if (mr->usesParts()) {
1875 for (int part = 0; part < mr->partCount; ++part) {
1876 Material* material = mr->parts[part].material ? mr->parts[part].material : mr->material;
1877 append(mr->parts[part].mesh, material,
1878 material ? material->isTransparentHair() : mr->isHair, 1.f);
1879 }
1880 } else {
1881 const float distance = std::sqrt(distance2) * std::max(lodDistanceScale, 0.01f);
1882 int primary = -1, secondary = -1;
1883 float secondaryWeight = 0.f;
1884 mr->lodBlendForDistance(distance, primary, secondary, secondaryWeight);
1885 auto meshAt = [&](int level) -> Mesh* {
1886 if (mr->lodCount <= 0) return level >= 0 ? mr->mesh : nullptr;
1887 return level >= 0 && level < mr->lodCount ? mr->lodMeshes[level] : nullptr;
1888 };
1889 const bool hair = mr->material ? mr->material->isTransparentHair() : mr->isHair;
1890 auto qualityAt = [&](int level) {
1891 if (level < 0 || level >= mr->lodCount || !mr->lodRendererStates[level].configured ||
1892 mr->lodRendererStates[level].skinQuality == 0)
1893 return 4;
1894 return mr->lodRendererStates[level].skinQuality;
1895 };
1896 auto lightProbeAt = [&](int level) {
1897 if (level < 0 || level >= mr->lodCount || !mr->lodRendererStates[level].configured) return 0;
1898 return mr->lodRendererStates[level].lightProbeUsage;
1899 };
1900 append(meshAt(primary), mr->material, hair, 1.f - secondaryWeight, qualityAt(primary),
1901 lightProbeAt(primary));
1902 if (secondaryWeight > 0.f)
1903 append(meshAt(secondary), mr->material, hair, secondaryWeight, qualityAt(secondary),
1904 lightProbeAt(secondary), true);
1905 }
1906 }
1907
1908 std::stable_sort(canvasItems.begin(), canvasItems.end(), [](const CanvasItem& a, const CanvasItem& b) {
1909 const bool transparentA = a.surface == SurfaceMode::Transparent;
1910 const bool transparentB = b.surface == SurfaceMode::Transparent;
1911 if (transparentA != transparentB) return !transparentA;
1912 if (transparentA) {
1913 if (a.sortPriority != b.sortPriority) return a.sortPriority < b.sortPriority;
1914 if (a.distance2 != b.distance2) return a.distance2 > b.distance2;
1915 }
1916 if (a.material != b.material) return a.material < b.material;
1917 return a.mesh < b.mesh;
1918 });
1919
1920 bool lightingEnabled = true;
1921 for (const CanvasItem& item : canvasItems) {
1922 Renderable3D::MeshRenderer* mr = item.mr;
1923 Texture* albedo = mr->texture;
1924 Color tint(mr->r, mr->g, mr->b, mr->a);
1925 Shader* shader = mr->shader;
1926 const bool lit = item.material ? item.material->getReceiveLight() : mr->receiveLight;
1927 if (lit) {
1928 Lighting3DPack lighting = packLights3D(packed, cd.operator->());
1929 bool sampled = false;
1930 if (item.lightProbeUsage == 4 && mr->customLightProbe) {
1931 lighting.diffuseProbeSh = mr->customLightProbeSh;
1932 sampled = true;
1933 } else if (item.lightProbeUsage == 1) {
1935 item.worldCenter, 4);
1936 if (sample) {
1937 lighting.diffuseProbeSh = sample.value().coefficients;
1938 sampled = true;
1939 }
1940 } else if (item.lightProbeUsage == 2) {
1941 auto volume = DiffuseLightProbeRegistry::instance().selectVolume(item.worldCenter);
1942 if (volume) {
1943 lighting.diffuseVolumePosition = volume.value().positions;
1944 lighting.diffuseVolumeExtent = volume.value().extents;
1945 lighting.diffuseVolumeSh = volume.value().coefficients;
1946 lighting.diffuseVolumeProbeCount = volume.value().count;
1947 lighting.diffuseVolumeTrilinearCell = volume.value().trilinearCell;
1948 }
1949 }
1950 lighting.diffuseProbeShEnabled = sampled;
1951 gfx.setMesh3DClusteredActive(clusteredCapture && item.lightProbeUsage == 0);
1953 } else if (lightingEnabled) {
1954 gfx.setMesh3DClusteredActive(false);
1955 Lighting3DPack unlit{};
1956 unlit.ambient = glm::vec4(1.f, 1.f, 1.f, 0.f);
1957 gfx.setMesh3DLighting(unlit);
1958 }
1959 lightingEnabled = lit;
1960 if (item.material) {
1961 auto bound = item.material->bind(gfx);
1962 if (!bound) throw Exception("%s", bound.error()->message().c_str());
1963 albedo = item.material->getAlbedoTexture();
1964 tint = Color(item.material->getTintR(), item.material->getTintG(), item.material->getTintB(),
1965 item.material->getTintA());
1966 shader = item.material->effectiveShader();
1967 } else {
1968 gfx.setMesh3DSurface(item.surface, BlendMode::Alpha, false, mr->isHair, 0.5f);
1969 gfx.setMesh3DMaterial(mr->metallic, mr->roughness);
1970 gfx.setMesh3DTexCellBomb(mr->texBombScale, mr->texBombStrength, mr->texBombRot);
1971 gfx.setMesh3DNormalTexture(mr->normalTexture);
1972 gfx.setMesh3DHeightTexture(mr->heightTexture);
1973 gfx.setMesh3DParallax(mr->parallaxScale, mr->parallaxMinLayers, mr->parallaxMaxLayers);
1974 }
1975 if (item.lodWeight < 0.9999f && !item.speedTreeFade)
1977 item.material && item.material->getDoubleSided(), false, 0.5f);
1978 else if (item.speedTreeFade)
1980 item.material && item.material->getDoubleSided(), 0.5f, "cutoff");
1981 gfx.setMesh3DShadowReceive(false);
1982 eve::debug::rtDraw("drawMeshShader", shader ? "custom" : "default");
1983 if (mr->instances) {
1984 const auto &range = *mr->instances;
1985 auto drawn =
1986 gfx.drawMeshShaderInstances(*item.mesh, *shader, item.model, tint, range.first, range.count);
1987 if (!drawn) throw eve::Exception("%s", drawn.status().describe().c_str());
1988 } else
1989 gfx.drawMeshShader(item.mesh, item.model, albedo, tint, shader);
1990 }
1991 }
1992
1993 // Copy the list so a contributor may safely unregister itself while drawing.
1994 const auto captureDrawers = g_captureDrawers;
1995 for (const CaptureExtraDrawerEntry& entry : captureDrawers) {
1996 if ((entry.mask & reflectionCaptureMask) == 0u || !entry.drawer) continue;
1997 entry.drawer(gfx, *cd, captureViewProj, aspect, reflectionCaptureMask);
1998 }
1999
2000 gfx.end3DFrameToCanvas();
2001}
2002
2003} // namespace eve::graphics
LogicalId target
bool & active
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
#define EV_PARAM_CHECK(cond,...)
Validate a function parameter / public API precondition.
Definition Assert.h:30
double volume
const std::string & s
int mask
std::vector< BuildingInstanceSnapshot > instances
int priority
graphics::Texture * albedo
glm::vec4 p[6]
tensor::Graph g
Definition GpuGraph.cpp:7
std::vector< eve::ProcgenProbeDesc > lights
float camera[2]
glm::vec4 tint
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
double r
float v
HexVec3 up
std::int32_t c
int secondary
std::uint32_t height
Range range
std::array< float, 3 > scale
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
float roughness
float tb
float tr
float tg
float metallic
int level
graphics::Canvas * previous
TileLayer * layer
float f
float radius
std::shared_ptr< const std::vector< glm::vec2 > > points
float d
float t
uint32_t cascadeMask
bool lodFadeReverse
bool inView
glm::vec2 temporalMotion
glm::mat4 cullViewProj
RenderSystem3D::ShadowExtraDrawer drawer
float projectedDepth
bool clusteredUploaded
Whether the clustered SSBOs were uploaded for this camera this frame.
bool inDefaultView
Renderable3D::MeshRenderer * mr
glm::vec3 eye
RenderSystem3D::GpuOpaqueCollector collector
float fovRad
FrustumPlanes frustum
bool useReflectionProbes
Mesh * mesh
float lodWeight
ClusteredLightingUpload clustered
int lightProbeUsage
int camIdx
bool receiveShadow
int sortPriority
glm::vec3 worldC
bool clusteredValid
bool speedTreeFade
bool xray
glm::mat4 viewProj
float worldR
Shader * shader
bool hair
Lighting3DPack lighting
bool shadowDoubleSided
uint64_t token
SurfaceMode surfaceMode
float distSq
glm::mat4 view
glm::mat4 model
bool shadowsOnly
Material * material
glm::mat4 proj
int skinInfluenceLimit
bool isPoint
RoadLaneDirection direction
float shadowStrength
float dz
float dy
float dx
float bias
std::uint32_t count
Structured operation status used by the common Result foundation.
V3 dir
Definition TreeMesh.cpp:150
const UnitySourceAsset & source
std::uint32_t depth
const VegetationPresetContext & context
float m[16]
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
RAII pass scope for C++ call sites.
Definition RenderTrace.h:75
Screen-space ambient occlusion.
void setIntensity(float intensity)
Sets the intensity.
void setPower(float power)
Sets the power.
void applyFromGBuffer(Graphics *gfx, Texture *hwDepth, Texture *worldNormal)
Applies from g buffer.
void setQuality(const std::string &quality)
"low" | "medium" | "high" (unknown → medium).
void setRadius(float radius)
Sets the radius.
Classic image-space anti-aliasing.
void setTemporalCamera(const glm::vec3 &eye, const glm::vec3 &target, float fovYDeg)
Detect camera cuts and invalidate incompatible temporal history.
glm::vec2 prepareTemporalJitter(int width, int height)
Return this frame's Halton projection jitter in NDC units.
glm::vec2 prepareTemporalObjectMotion(const void *objectKey, const glm::mat4 &model)
Per-object UV correction relative to static-world camera reprojection.
void setTemporalViewProjection(const glm::mat4 &viewProjection, float nearZ, float farZ)
Set the jittered camera matrix used for depth-based history reprojection.
EVENGINE_API_BACKENDS public API.
void setBloom(float intensity, float threshold=1.f)
Configure HDR bloom. Zero intensity disables it.
float getEyeX()
Camera eye position (world space).
float getDofFocusDistance()
Return DOF focus distance.
float getNearClip()
Return the positive near clipping distance.
float getDofFocusRange()
Return DOF focus range.
void setShadowLayerCullDistance(int layer, float distance)
Set the shadow-caster cull distance for one render layer; zero disables it.
float getAperture()
Return physical-camera aperture.
void setDepthOfField(float focusDistance, float maxBlurPx, float focusRange=8.f)
Configure Gaussian depth-of-field on the final HDR resolve.
float getFocalLength()
Return physical-camera focal length in millimetres.
float getFov()
Vertical field of view in degrees.
void setLayerCullDistance(int layer, float distance)
Set the camera cull distance for one render layer; zero disables the override.
float getDofMaxBlur()
Return DOF max blur in texels (0 when disabled).
void setReflectionProbe(int slot, Texture *cubemap, float centerX, float centerY, float centerZ, float extentX, float extentY, float extentZ, float intensity=1.f, float blendDistance=1.f, int priority=0)
Configure one local reflection probe slot.
bool isAutoExposure()
Return whether automatic exposure is enabled.
float getBloomThreshold()
Return bloom threshold in linear HDR units.
void setPerspective()
Return to perspective projection, retaining the configured field of view.
float getExposure()
Return exposure compensation in photographic stops.
void setUp(float x, float y, float z)
void setOrthographic(float height)
Enable an orthographic projection with the given vertical world-space span.
void clearEnvProbe()
Disable box projection and return to an infinite-distance environment.
void clearReflectionProbe(int slot)
Disable one local reflection probe slot.
void setFov(float fovYDeg)
float getBloomIntensity()
Return bloom intensity.
void setPhysicalLens(float aperture, float focalLength)
Set positive physical-camera aperture and focal length metadata.
void screenToRay(float screenX, float screenY, float viewW, float viewH)
Build a world-space picking ray from a screen pixel. Stores origin (camera eye) and normalized direct...
void setExposure(float ev)
Set exposure compensation in photographic stops. Positive values brighten.
float getLayerCullDistance(int layer)
Return the camera cull-distance override for one render layer.
void setEnvIntensity(float intensity)
void setEnvProbe(float centerX, float centerY, float centerZ, float extentX, float extentY, float extentZ)
Enable box-projected IBL for the camera environment cubemap.
float getFarClip()
Return the far clipping distance.
void setAutoExposure(bool enabled, float minEV=-8.f, float maxEV=8.f)
Enable log-average automatic exposure and set its EV clamp range.
void setTarget(float x, float y, float z)
float getShadowLayerCullDistance(int layer)
Return the shadow-caster cull-distance override for one render layer.
void clearDepthOfField()
Disable depth-of-field while retaining the current focus settings.
void setAmbient(float r, float g, float b)
void setActive(bool active)
int getReflectionProbeCount()
Return the number of enabled local reflection probes.
void setEye(float x, float y, float z)
float getTargetX()
Look-at target (world space).
bool hasEnvProbe()
True when all three box-probe extents are positive.
void setEnvMap(Texture *cube)
Specular IBL cubemap (Graphics::newCubemap). nullptr disables IBL.
void setClipPlanes(float nearZ, float farZ)
Set positive near/far clipping distances; far is kept beyond near.
Canvas public API.
Definition Canvas.h:17
virtual int getWidth() const =0
Returns the width.
virtual int getHeight() const =0
Returns the height.
Cross-backend min/max hardware-depth pyramid packed into one texture atlas.
int getLevelCount() const
Number of valid levels in the last build.
Texture * build(Texture *depth, int maxLevels=8)
Build the min/max hierarchy from a hardware depth texture.
static DiffuseLightProbeRegistry & instance()
Return the process-wide graphics-owned registry.
Result< DiffuseLightProbeVolumeSample > selectVolume(const glm::vec3 &position) const
Select an object-local proxy volume; fragment shading performs the spatial interpolation.
Result< DiffuseLightProbeSample > sample(const glm::vec3 &position, int maxSamples) const
Blend up to maxSamples probes whose influence volumes contain position.
Screen-space buffers for mid/post effects and Hybrid deferred lighting.
Definition GBuffer.h:28
Texture * getAlbedoTexture() const
Returns the albedo texture.
Definition GBuffer.h:57
Texture * getNormalTexture() const
Returns the normal texture.
Definition GBuffer.h:55
Texture * getDepthTexture() const
Returns the depth texture.
Definition GBuffer.h:51
void clear()
Clears .
Definition GBuffer.cpp:43
Texture * getHwDepthTexture() const
Returns the hw depth texture.
Definition GBuffer.h:53
bool isValid() const
True when valid.
Definition GBuffer.cpp:5
Screen-space single-bounce GI (SSGI). @lifetime All texture arguments are borrowed for the duration o...
Canvas * getWorkingCanvas()
Returns the working canvas.
void setTemporalMotionTexture(Texture *motionDepth)
Set packed linear-depth and motion texture used by temporal GI resolve.
void applyFromSceneTo(Graphics *gfx, Texture *color, Texture *hwDepth, Canvas *dest)
Applies from scene to.
Texture * getWorkingTexture()
Temporally resolved GI texture, or the raw working texture before history exists. @lifetime Returned ...
void setQuality(const std::string &quality)
"low" | "medium" | "high" | "ultra" (unknown becomes medium).
std::string getQuality() const
Returns the quality.
void setDepthPyramid(Texture *atlas, int levels)
Set the shared horizontally packed RG min/max depth hierarchy.
void setWorldNormalTexture(Texture *worldNormal)
Set the GBuffer world-normal texture used for guided GI sampling.
void setAlbedoTexture(Texture *albedo)
Set receiver albedo used for energy-conserving diffuse GI response.
ScreenSpaceReflection * pipelineScreenSpaceReflection()
Pipeline screen space reflection.
virtual void beginShadowPass(int cascadeIndex)=0
Depth-only shadow pass for one cascade layer (0..2). Draws are recorded into the next begin3DFrame co...
virtual void setMesh3DEnvProbe(const glm::vec3 &center, const glm::vec3 &extent)=0
Box projection bounds for the active environment; zero extent disables it.
virtual void setMesh3DReflectionProbes(const ReflectionProbeUpload &upload)=0
Upload the two dominant local reflection probes for subsequent mesh draws.
virtual bool gpuDrivenSubmitOpaque(const GpuInstance *instances, uint32_t instanceCount)
Upload + record GPU-driven opaque draws (call inside the open 3D frame). The backend sorts instances ...
Definition Graphics.h:365
virtual std::string getBackendName() const =0
Renderer backend id used by sibling modules (e.g. Gpgpu).
int getPixelHeight() const
Returns the pixel height.
Definition Graphics.h:508
virtual bool gpuDrivenCullEnabled() const
True when the stage-2 GPU cull chain will run this frame.
Definition Graphics.h:388
virtual void setMesh3DShadows(const ShadowUpload &upload)=0
Upload CSM constants for subsequent default mesh draws (active=false disables).
virtual void endGBufferPass()=0
Ends g buffer pass.
virtual void setMesh3DClip(float nearZ, float farZ)=0
Near/far used to pack linear depth into scene color A (SSGI).
virtual void gpuDrivenOpenScenePass()
Open the scene color pass that begin3DFrame deferred (cull path).
Definition Graphics.h:411
virtual void setMesh3DMaterial(float metallic, float roughness)=0
Metallic (0..1) and roughness (0..1) for the next default mesh draw.
virtual bool gpuDrivenVgSetInstance(std::uint32_t vgAssetId, const glm::mat4 &model, std::uint32_t materialId)
Register one instance of a VG asset this frame (model + material). The first instance per asset wins;...
Definition Graphics.h:458
int getWidth() const
Returns the width.
Definition Graphics.h:502
virtual std::uint32_t gpuDrivenVgAssetId(Mesh *mesh) const
VG asset id attached to a mesh (kInvalidGpuDrivenSlot when none).
Definition Graphics.h:440
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 void setMesh3DViewProj(const glm::mat4 &viewProj)=0
Sets the mesh 3 d view proj.
virtual void drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP, bool doubleSided=true)=0
Queue an opaque shadow caster.
virtual void drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo=nullptr, bool doubleSided=true, float lodWeight=1.f, bool lodFadeReverse=false, bool lodDither=false)=0
Shadow pass draw with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): tra...
virtual Result< void > setMesh3DPbrSurface(const PbrSurface *surface)
Copy a validated extended surface for subsequent draws; null resets to legacy shading.
Definition Graphics.h:1192
virtual void setMesh3DCameraPos(const glm::vec3 &eye)=0
Camera eye used by mesh shaders that need view/rim (stored in Mesh3DUBO).
virtual uint32_t gpuDrivenMeshRecord(Mesh *mesh)
GPU mesh-table slot for a mesh (kInvalidGpuDrivenSlot when not uploaded).
Definition Graphics.h:274
virtual void endShadowPass()=0
Ends shadow pass.
virtual void setMesh3DPackedNormalMask(bool enabled)=0
Interpret normal texture RG as tangent XY, B as AO and A as smoothness for terrain surfaces.
GlobalIllumination * pipelineGlobalIllumination()
Pipeline global illumination.
virtual void setMesh3DClusteredActive(bool active)=0
Cheap per-draw toggle for the already-uploaded clustered light table. Unlike setMesh3DClusteredLighti...
virtual void begin3DFrameToCanvas(Canvas *canvas)=0
Open a 3D render pass targeting an offscreen Canvas (color + depth) at the canvas size....
virtual void setMesh3DEnv(Texture *cube, float intensity)=0
Specular IBL environment for subsequent default mesh draws. cube must be from newCubemap (or nullptr ...
virtual bool gpuDrivenMaterialUsable(Material *material)
Whether a material can be shaded by the GPU-driven opaque path. Backends/drivers with descriptor-inde...
Definition Graphics.h:304
virtual void setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount=1.f)=0
Texture cell bombing for the next default mesh draw (breaks tiling). cellScale: cells per UV unit (ty...
virtual void beginDecalPass(int width, int height)=0
Open the decal pass (reads G-buffer hwDepth + normal, writes the screen-space DecalLayer targets)....
virtual Canvas * getCanvas() const =0
Returns the canvas.
virtual void setSceneExposure(float exposure)=0
Set linear exposure multiplier used by the final scene tone-map resolve.
virtual void beginGBufferPass(int width, int height)=0
Depth/normal(/albedo) fill pass for mid/post effects. One-shot submit (like shadow); call before begi...
virtual void setMesh3DClusteredLighting(const ClusteredLightingUpload &upload)=0
Enable clustered forward path for subsequent default mesh draws (SSBO light lists)....
virtual void setMesh3DParallax(float scale, float minLayers=8.f, float maxLayers=32.f)=0
Parallax occlusion mapping for the next default mesh draw. scale: UV displacement strength (0=off)....
virtual void setSceneBloom(float intensity, float threshold)=0
Configure final HDR bloom intensity and linear threshold.
Outline * pipelineOutline()
Pipeline-owned Outline used by RenderSystem3D when the "outline" feature is on.
std::shared_ptr< PrimitiveScene > getPrimitiveScene() const noexcept
Returns the authoritative persistent spatial-primitive scene.
Definition Graphics.h:521
virtual void setMesh3DView(const glm::mat4 &view)=0
Camera view matrix for subsequent drawMesh (view-space depth / CSM select).
virtual bool gpuDrivenScenePassPending() const
Scene pass opening deferred until after the compute cull section.
Definition Graphics.h:391
virtual void gpuDrivenResolve()
Record the fullscreen vis resolve inside the open scene color pass.
Definition Graphics.h:427
virtual void drawMeshGBufferAlpha(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo=nullptr, float tintR=1.f, float tintG=1.f, float tintB=1.f, float motionX=0.f, float motionY=0.f, float roughness=0.45f, float metallic=0.f)=0
GBuffer fill with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): same ou...
virtual void setCanvas(Canvas *canvas)=0
nullptr or this → screen. Switching flushes pending draws to the previous target.
bool had3DThisFrame() const
Had 3 d this frame.
Definition Graphics.h:1491
virtual Result< void > drawMeshShaderInstances(Mesh &mesh, Shader &shader, const glm::mat4 &model, const Color &tint, std::uint32_t first, std::uint32_t count)
Draw a checked range from a resource shader's immutable instance matrix buffer. @ownership Mesh and S...
Definition Graphics.h:1116
virtual void setDecalCamera(const glm::mat4 &viewProj, float nearZ, float farZ)=0
Per-frame camera constants for the decal pass (world-space reconstruction from the G-buffer depth)....
virtual void setMesh3DSurface(SurfaceMode mode, BlendMode blend, bool depthWrite, bool doubleSided, float alphaCutoff, const std::string &alphaTechnique="cutoff")=0
Select pipeline state for subsequent mesh draws.
virtual void setSceneAutoExposure(bool enabled, float minEV, float maxEV)=0
Configure log-average scene auto exposure and its EV clamp range.
AntiAliasing * pipelineAntiAliasing()
Pipeline anti aliasing.
virtual void setMesh3DSceneDepth(Texture *depth)=0
Optional scene hardware depth (G-buffer hwDepth, Vulkan NDC z) bound to mesh3d shader binding 7....
AmbientOcclusion * pipelineAmbientOcclusion()
Pipeline-owned AO / GI / AA used by RenderSystem3D when features "ao" / "gi" / "aa" are enabled....
virtual void gpuDrivenSetEnabled(bool enabled)
Enable/disable the GPU-driven opaque path (no-op when unsupported).
Definition Graphics.h:271
virtual uint32_t gpuDrivenMaterialRecord(Material *material)
GPU material-table slot for a material (lazily created).
Definition Graphics.h:280
virtual void setMesh3DSkinInfluenceLimit(SkinInfluenceLimit count)=0
Limit GPU skinning to the strongest one, two or four vertex influences.
virtual bool gpuDrivenResolveWanted() const
True when the stage-3 vis+resolve path should run this frame.
Definition Graphics.h:421
virtual void gpuDrivenRecordVisPass()
Record the GBuffer vis pass (opaque indirect draws write visID/visBary).
Definition Graphics.h:424
virtual void setMesh3DShadowReceive(bool receive)=0
Per-draw: when false, shadow sampling is forced off for the next mesh draw.
virtual bool supportsGBufferPost() const
Whether gbuffer-based post-process shaders (AO, GI) can be created on this backend....
Definition Graphics.h:238
virtual void begin3DFrame()=0
Begin a 3D frame: shadow/gbuffer (if pending) then a sampleable scene color pass (color+depth)....
void setFinalSceneTexture(Texture *texture)
Override the HDR source consumed by the next backend scene resolve.
Definition Graphics.h:2070
virtual bool supportsGpuDriven3D() const
True when the backend can run GPU-driven opaque draws.
Definition Graphics.h:255
virtual uint32_t gpuDrivenReflectionProbeSlot(Texture *cubemap)
Return/register a bindless cubemap slot for a GPU-driven local probe.
Definition Graphics.h:310
virtual void drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo=nullptr, float tintR=1.f, float tintG=1.f, float tintB=1.f, float motionX=0.f, float motionY=0.f, float roughness=0.45f, float metallic=0.f)=0
Draws mesh g buffer.
DepthPyramid * pipelineDepthPyramid()
Pipeline-owned shared min/max depth hierarchy for screen-space effects. @lifetime Returned effect rem...
virtual void gpuDrivenCullEmit(const glm::mat4 &viewProj, const glm::vec3 &eye, float fovYDeg, float nearZ, float farZ)
Record the cull + emit compute dispatches for the current frame.
Definition Graphics.h:401
virtual void gpuDrivenDrawOpaque()
Draw the opaque geometry with GPU-written indirect commands.
Definition Graphics.h:414
virtual Texture * getSceneColorTexture()
Sampleable 3D color target for the current frame (RGB = lit, A = linear depth). Valid after begin3DFr...
Definition Graphics.h:1047
virtual void drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint, Shader *shader)=0
Draw mesh with an explicit Mesh3D Shader (nullptr = default PBR pipeline).
RenderControl * getRenderControl()
Shared compilable 3D render control (features → pass list + GBuffer). Owned by Graphics; valid for th...
Definition Graphics.cpp:254
virtual void setSceneDepthOfField(float focusDistance, float maxBlurPx, float focusRange, float nearZ, float farZ)=0
Configure final-scene Gaussian depth-of-field.
virtual void gpuDrivenVgComputeSection(const glm::mat4 &viewProj, const glm::vec3 &eye, float fovYDeg, float nearZ, float farZ)
Record the HZB build + cull-params section (VG-only frames).
Definition Graphics.h:467
virtual void endDecalPass()=0
Ends decal pass.
virtual void drawDeferredLighting()
Fullscreen deferred lighting into the open scene-color pass. Samples GBuffer + clustered lights + CSM...
Definition Graphics.h:252
int getPixelWidth() const
Returns the pixel width.
Definition Graphics.h:506
virtual bool supportsDecal() const
True when the backend can render the screen-space decal layer (box-projected decals writing albedo/no...
Definition Graphics.h:1274
int getHeight() const
Returns the height.
Definition Graphics.h:504
Canvas * pipelineReflectionComposite(int width, int height)
Return a reusable HDR target for composing screen-space lighting. @lifetime Returned canvas is Graphi...
virtual void setMesh3DNormalTexture(Texture *normal)=0
Optional normal map for the next drawMesh / drawMeshShader (nullptr = flat).
virtual bool gpuDrivenCullBegin(const GpuInstance *instances, uint32_t instanceCount)
Upload sorted instances + bucket metadata for the cull chain.
Definition Graphics.h:394
virtual void setMesh3DHeightTexture(Texture *height)=0
Optional height map for parallax (R channel; nullptr = flat / off).
virtual void setMesh3DLighting(const Lighting3DPack &pack)=0
Per-frame ambient + up to 8 lights packed into Mesh3DUBO.
virtual void drawPrimitiveScene(const PrimitiveSceneCanvas3D &canvas)=0
Submits an owning frame-local primitive canvas into the active 3D pass.
Packages shading method + surface parameters into one attachable asset.
Definition Material.h:35
bool hasPbrSurface() const
Whether full PBR texture/extension rendering is requested.
Definition Material.h:64
Texture * getAlbedoTexture() const
Get the borrowed material texture. @ownership Graphics factory owns the resource. @lifetime Keep aliv...
Definition Material.h:117
float getTintR() const
Returns the tint r.
Definition Material.h:171
PbrSurface pbrSurface() const
Return an owning parameter snapshot; texture resources remain borrowed.
Definition Material.h:58
bool getReceiveShadow() const
Returns the receive shadow.
Definition Material.h:220
bool isTransparentHair() const
True when this material should go through the hair transparent pass.
Definition Material.cpp:162
float getTintA() const
Returns the tint a.
Definition Material.h:177
bool getDoubleSided() const
Returns the double sided.
Definition Material.h:275
std::string getShadingModel() const
Returns the shading model.
Definition Material.h:50
float getTintB() const
Returns the tint b.
Definition Material.h:175
Result< void > bind(Graphics &gfx) const
Push this material onto Graphics mesh3d state for the next draw. Does not issue the draw itself.
Definition Material.cpp:236
bool getCastShadow() const
Returns the cast shadow.
Definition Material.h:215
float getTintG() const
Returns the tint g.
Definition Material.h:173
bool getReceiveLight() const
Returns the receive light.
Definition Material.h:210
Shader * effectiveShader() const
Return the borrowed effective shader, or nullptr for the default PBR pipeline. @lifetime The pointer ...
Definition Material.cpp:157
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:25
float boundsCx
Model-space bounding sphere used for view/cascade frustum culling. Computed from vertex positions at ...
Definition Mesh.h:59
bool hasBounds() const
True when a valid bounding sphere is available for culling.
Definition Mesh.h:65
float boundsRadius
Definition Mesh.h:62
Screen-space model outline (t3ssel8r-style), computed from the GBuffer hardware depth + world-normal ...
Definition Outline.h:33
void setClip(float nearZ, float farZ)
Near/far used to linearize the hardware depth.
Definition Outline.cpp:110
bool apply(Graphics *gfx, Texture *hwDepth, Texture *worldNormal)
Draw the outline over the currently bound canvas / screen. hwDepth is the D32 GBuffer (Vulkan NDC z),...
Definition Outline.cpp:147
Frame-local scene recorder for 3D line primitives.
const std::vector< TriangleCommand3D > & triangles() const noexcept
Triangles.
const std::vector< PolylineCommand3D > & commands() const noexcept
Commands.
Declarative, compilable 3D render control.
void ensureCompiled()
Ensure compiled; no-op when already clean.
std::string getReflectionQuality() const
Return the shared TAA/RTGI/SSR quality preset.
bool isEnabled(const std::string &feature) const
True when enabled.
GBuffer * getGBuffer()
Returns the g buffer.
bool hasPass(const std::string &name) const
True when pass.
static void setDirectionalLight(float dx, float dy, float dz, float r, float g, float b)
Legacy single directional light used when no enabled Light3D exists.
static void removeShadowExtraDrawer(uint64_t token)
Remove one shadow callback before destroying its captured state.
static void removeForwardExtraDrawer(uint64_t token)
Unregister a main forward-pass contributor.
static void addDecalExtraDrawer(DecalExtraDrawer drawer)
Adds decal extra drawer.
std::function< void(Graphics &gfx, const Camera3D::Data &cam, const glm::mat4 &viewProj, float aspect, std::vector< GpuInstance > &instances)> GpuOpaqueCollector
Collect non-ECS geometry for the normal GPU-driven opaque submission.
static uint64_t addShadowExtraDrawer(ShadowExtraDrawer drawer)
Adds shadow extra drawer.
static void removeCaptureExtraDrawer(uint64_t token)
Unregister an offscreen capture contributor.
static void addGBufferExtraDrawer(GBufferExtraDrawer drawer)
Adds g buffer extra drawer.
static uint64_t addCaptureExtraDrawer(uint32_t reflectionCaptureMask, CaptureExtraDrawer drawer)
Register custom forward geometry for offscreen captures.
static void renderToCanvas(Graphics &gfx, Canvas *target, Camera3D *camera, uint32_t reflectionCaptureMask=0xffffffffu, float lodDistanceScale=1.f, bool includeTransparent=true, bool useClusteredLighting=true, Texture *skyFaceTexture=nullptr, Mesh *skyQuad=nullptr, float skyFaceTextureScale=1.f)
Forward 3D pass into an offscreen canvas using multipart materials, LOD, masked surfaces and sorted t...
static void render(Graphics &gfx)
Renders .
static uint64_t addForwardExtraDrawer(ForwardExtraDrawer drawer)
Register a main forward-pass contributor.
std::function< void(Graphics &gfx, const Camera3D::Data &cam, const glm::mat4 &viewProj, float aspect, uint32_t reflectionCaptureMask)> CaptureExtraDrawer
Draw custom forward geometry into reflection-probe and preview captures.
std::function< void(Graphics &gfx, const glm::mat4 &lightVP, const Camera3D::Data &cam)> ShadowExtraDrawer
Register a callback that casts shadows for geometry outside the Renderable3D ECS (e....
std::function< void(Graphics &gfx, const Camera3D::Data &cam, const glm::mat4 &viewProj, float aspect)> ForwardExtraDrawer
Draw custom opaque geometry after the main scene pass is open. @thread Register, unregister and invok...
static void removeGpuOpaqueCollector(uint64_t token)
Unregister a GPU-driven opaque contributor.
static uint64_t addGpuOpaqueCollector(GpuOpaqueCollector collector)
Register a GPU-driven opaque contributor.
std::function< void(Graphics &gfx, const Camera3D::Data &cam, const glm::mat4 &viewProj, float aspect)> DecalExtraDrawer
Register a callback that draws screen-space decals (box-projected volumes writing the DecalLayer) aft...
std::function< void(Graphics &gfx, const Camera3D::Data &cam, const glm::mat4 &viewProj, float aspect)> GBufferExtraDrawer
Register a callback that fills the G-buffer (depth/normal/albedo) for geometry outside the Renderable...
EVENGINE_API_BACKENDS public API.
Screen-space reflections (SSR) as a fullscreen post pass. @lifetime All Graphics and texture argument...
std::string getQuality() const
Return the active SSR quality preset.
void applyFromSceneTo(Graphics *gfx, Texture *sceneColor, Texture *hwDepth, Texture *worldNormal, Canvas *dest)
Write the SSR result into the currently bound canvas / dest.
void setQuality(const std::string &quality)
Set "low", "medium", "high" or "ultra" SSR quality preset.
Texture * getReflectionTexture()
The reflection texture from the owned canvas, or nullptr before first apply.
void setEnabled(bool enabled)
Enable/disable the pass. When disabled it emits transparent (0 hit).
Canvas * getReflectionCanvas()
Owned reflection canvas (created on first use at the current target size).
void setDepthPyramid(Texture *atlas, int levels)
Set the shared horizontally packed RG min/max depth hierarchy.
void setTemporalMotionTexture(Texture *motionDepth)
Set packed linear-depth and motion texture used by temporal SSR resolve.
Custom GPU program.
Definition Shader.h:39
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
void rtBind(const char *kind, const char *name)
Rt bind.
Definition RenderTrace.h:62
void rtPassBegin(const char *name)
Rt pass begin.
Definition RenderTrace.h:50
void rtDraw(const char *api, const char *detail=nullptr)
Rt draw.
Definition RenderTrace.h:66
void rtPassEnd(const char *name)
Rt pass end.
Definition RenderTrace.h:54
EVENGINE_API_BACKENDS Result< void > prepareViewResources(Graphics &graphics, const glm::mat4 &viewProjection, const glm::vec3 &eye)
Dispatch preparers before opening the destination; no callbacks means success. @thread Graphics threa...
Result< void > validateInstancedRenderable(const Renderable3D::MeshRenderer &mr)
Validate instanced renderable.
卡牌游戏 UI 工具模块:工厂 + 脚本绑定入口。 功能参考 ycarowr/UiCard:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
Definition Animation.h:25
bool meshInstanceRangeVisible(const MeshInstanceRange &range, const glm::mat4 &model, const glm::mat4 &vp, const glm::vec3 &eye)
Test transformed bounds against Vulkan clip planes and horizontal distance.
void selectShadowCasters(const std::vector< Light3D::Data * > &lights, const std::vector< bool > &isPointFlags, const ShadowSchemeSettings &settings, const ShadowPagingView &view, Light3D::Data *&directionalCaster, std::vector< LocalShadowSlot > &localSlots)
Select directional CSM caster + paged local spot slots for one frame.
ClusteredLightingUpload buildClusteredLighting(const std::vector< ClusteredLightGpu > &points, const std::vector< ClusteredLightGpu > &dirs, const glm::mat4 &view, float nearZ, float farZ, int screenW, int screenH, float fovYRad, const glm::vec4 &ambient)
Build clustered tables for point lights in view space.
SurfaceMode
How a 3D surface contributes to depth and color passes.
Definition SurfaceMode.h:6
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.
Definition ClipSpace.h:38
glm::mat4 perspectiveVulkanRH_ZO(float fovyRad, float aspect, float zNear, float zFar)
Right-handed, zero-to-one depth perspective for Vulkan swapchains.
Definition ClipSpace.h:22
ShadowUpload buildDirectionalCSM(const glm::vec3 &lightDirTowardSurface, const glm::vec3 &eye, const glm::vec3 &target, const glm::vec3 &up, float fovYRad, float aspect, float nearZ, float farZ, float bias, float strength)
Build 3 cascade light view-proj matrices for a directional light.
Definition Shadow.cpp:83
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
Light3DGpu ClusteredLightGpu
void lightSpotCosines(float angleDeg, float softness, float &cosOuter, float &cosInner)
Convert spot angle/softness into GPU cosines (outer < inner). Shared by Light2D and Light3D flashligh...
Definition Light.h:28
constexpr uint32_t kInvalidGpuDrivenSlot
GPU-driven rendering shared constants + std430 GPU layouts.
SkinInfluenceLimit
Number of strongest vertex influences retained by GPU skinning.
Definition IGraphics3D.h:19
bool enabled
std::array< float, 32 > shadowLayerCullDistances
static constexpr int kMaxReflectionProbes
CPU-built clustered lighting upload for one frame/camera. Point lights are clustered; directional lig...
Per-instance GPU record (std430). Mirrors GLSL GpuInstance.
GPU light packing for mesh3d / PBR (std140-friendly).
Definition Light.h:161
Data public API.
Definition Light.h:200
Lighting3DPack public API.
Definition Light.h:172
static constexpr int kMaxLights
Definition Light.h:173
std::array< glm::vec4, 9 > diffuseProbeSh
Definition Light.h:177
One allocated local (spot/point-face) shadow slot for the frame. @ownership Ephemeral CPU descriptor;...
One mesh + material slot on a multi-part model (Assimp mesh / body region). When Material* is null,...
Definition Material.h:330
Owning material parameter snapshot; texture pointers remain borrowed. Base color/metallic/roughness c...
Definition PbrSurface.h:218
ReflectionProbeUpload public API.
Definition IGraphics3D.h:26
Immutable camera and viewport facts used to resolve one 3D draw list. Projection uses the engine's RH...
static constexpr int kLocalSlots
Extra depth-array layers for spot (perspective) local shadows.
Definition Shadow.h:26
static constexpr int kCascades
Definition Shadow.h:24
Camera context used to score local shadow candidates for atlas paging. @ownership Ephemeral; valid on...
static ShadowSchemeSettings & current()
Borrow the process-wide settings object.
ShadowUpload public API.
Definition Shadow.h:54