SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
LightManager.cpp
Go to the documentation of this file.
2#include <Lighting/Light.hpp>
9#include <Camera/Camera.hpp>
10#include <Core/Window.hpp>
11#include <Core/Application.hpp>
12#include <Core/CommandLine.hpp>
13#include <Math/Matrix.hpp>
14#include <Core/Timer.hpp>
15#include <Core/Logger.hpp>
16#include <cstring>
17#include <cmath>
18
19namespace {
20/// 4x4 row-major matrix inverse via cofactors (Cramer's rule).
21/// Returns false if matrix is singular.
22static bool Invert4x4(const float m[16], float inv[16]) {
23 float inv0 = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10];
24 float inv4 = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10];
25 float inv8 = m[4]*m[9] *m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9];
26 float inv12 = -m[4]*m[9] *m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9];
27
28 float det = m[0]*inv0 + m[1]*inv4 + m[2]*inv8 + m[3]*inv12;
29 if (std::fabsf(det) < 1e-8f) return false;
30 float id = 1.0f / det;
31
32 inv[0] = inv0 * id;
33 inv[4] = inv4 * id;
34 inv[8] = inv8 * id;
35 inv[12] = inv12 * id;
36
37 inv[1] = (-m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10]) * id;
38 inv[5] = ( m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10]) * id;
39 inv[9] = (-m[0]*m[9] *m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9] ) * id;
40 inv[13] = ( m[0]*m[9] *m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9] ) * id;
41
42 inv[2] = ( m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6]) * id;
43 inv[6] = (-m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6]) * id;
44 inv[10] = ( m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5]) * id;
45 inv[14] = (-m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5]) * id;
46
47 inv[3] = (-m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6]) * id;
48 inv[7] = ( m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6]) * id;
49 inv[11] = (-m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5]) * id;
50 inv[15] = ( m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5]) * id;
51
52 return true;
53}
54} // namespace
55
56namespace Sleak {
57
59
61
63 if (!m_lightBuffer) {
68 nullptr));
69 m_lightBuffer->SetSlot(2);
70 }
71}
72
74 if (!light) return;
75 if (m_lights.indexOf(light) != -1) return;
76
77 if (m_lights.GetSize() >= RenderEngine::MAX_LIGHTS) {
79 "Maximum light count ({}) reached, cannot register "
80 "light '{}'",
82 return;
83 }
84
85 m_lights.add(light);
86}
87
89 if (!light) return;
90 int index = m_lights.indexOf(light);
91 if (index != -1) {
92 m_lights.erase(index);
93 }
94}
95
97 if (!m_lightBuffer) return;
98
100
101 // Camera position
102 const auto& camPos = Camera::GetMainCameraPosition();
103 cbData.CameraPosX = camPos.GetX();
104 cbData.CameraPosY = camPos.GetY();
105 cbData.CameraPosZ = camPos.GetZ();
106
107 // Ambient
108 cbData.AmbientR = m_ambientR;
109 cbData.AmbientG = m_ambientG;
110 cbData.AmbientB = m_ambientB;
111 cbData.AmbientIntensity = m_ambientIntensity;
112
113 // Fog — horizon color + sky-zenith blend + exponential height fog.
114 // OpenGL deferred lighting reads its fog parameters from this LightCBData
115 // (binding 2). Vulkan/forward shaders read the same data from
116 // ShadowLightUBO. Keep both blocks in sync.
117 if (m_fogEnabled) {
118 cbData.FogColorR = m_fogR;
119 cbData.FogColorG = m_fogG;
120 cbData.FogColorB = m_fogB;
121 cbData.FogColorA = 1.0f;
122 cbData.FogStart = m_fogStart;
123 cbData.FogEnd = m_fogEnd;
124
125 cbData.FogColorZenith[0] = m_fogZenithR;
126 cbData.FogColorZenith[1] = m_fogZenithG;
127 cbData.FogColorZenith[2] = m_fogZenithB;
128 cbData.FogColorZenith[3] = 1.0f;
129
130 cbData.HeightFogTop = m_heightFogTop;
131 cbData.HeightFogDensity = m_heightFogDensity;
132 cbData.HeightFogFalloff = m_heightFogFalloff;
133 cbData.HeightFogEnabled = m_heightFogEnabled ? 1.0f : 0.0f;
134 } else {
135 cbData.FogStart = 0.0f;
136 cbData.FogEnd = 0.0f;
137 cbData.HeightFogEnabled = 0.0f;
138 }
139
140 // Collect active lights
141 uint32_t count = 0;
142 for (size_t i = 0;
143 i < m_lights.GetSize() && count < RenderEngine::MAX_LIGHTS;
144 ++i) {
145 Light* light = m_lights[i];
146 if (!light || !light->IsEnabled()) continue;
147
148 cbData.Lights[count] = light->BuildGPUData();
149 ++count;
150 }
151 cbData.NumActiveLights = count;
152
153 // Update and bind at slot 2
154 m_lightBuffer->Update(&cbData, sizeof(cbData));
155 m_lightBuffer->Update();
156
157 // Update shadow data for Vulkan renderer
159
160 // Update deferred CB (InvViewProj + screen size) for the lighting pass
162}
163
165 auto* app = Application::GetInstance();
166 if (!app) return;
167 auto* renderer = app->GetRenderer();
168 if (!renderer) return;
169
170 // Find first shadow-casting directional light
171 DirectionalLight* shadowLight = nullptr;
172 for (size_t i = 0; i < m_lights.GetSize(); ++i) {
173 Light* light = m_lights[i];
174 if (!light || !light->IsEnabled() || !light->GetCastShadows()) continue;
175
176 auto* dirLight = dynamic_cast<DirectionalLight*>(light);
177 if (dirLight) {
178 shadowLight = dirLight;
179 break;
180 }
181 }
182
183 // Find first enabled directional light (regardless of shadow casting)
184 // for populating light/ambient data in the UBO
185 DirectionalLight* anyDirLight = nullptr;
186 if (!shadowLight) {
187 for (size_t i = 0; i < m_lights.GetSize(); ++i) {
188 Light* light = m_lights[i];
189 if (!light || !light->IsEnabled()) continue;
190 auto* dirLight = dynamic_cast<DirectionalLight*>(light);
191 if (dirLight) {
192 anyDirLight = dirLight;
193 break;
194 }
195 }
196 }
197
198 // Tell the renderer whether the shadow pass should run
199 renderer->SetShadowPassEnabled(shadowLight != nullptr);
200
201 // Use shadow light if available, otherwise fall back to any directional light
202 DirectionalLight* activeLight = shadowLight ? shadowLight : anyDirLight;
203
204 if (!activeLight) {
205 static bool warned = false;
206 if (!warned) { SLEAK_WARN("UpdateShadowData: No directional light found!"); warned = true; }
207 return;
208 }
209
210 auto dir = activeLight->GetDirection();
211 auto color = activeLight->GetColor();
212 float intensity = activeLight->GetIntensity();
213
215
216 if (shadowLight) {
217 // Compute light view-projection matrix from shadow configuration
218 float frustumSize = shadowLight->GetShadowFrustumSize();
219 float shadowDist = shadowLight->GetShadowDistance();
220 float nearP = shadowLight->GetShadowNearPlane();
221 float farP = shadowLight->GetShadowFarPlane();
222
223 // Light position: follow camera XZ but fix Y at world origin.
224 // Anchoring Y prevents the shadow frustum from shifting vertically
225 // when the player jumps/flies, which causes hard Z-plane cutoff flicker.
226 const auto& camPos = Camera::GetMainCameraPosition();
227 Math::Vector3D lightPos = Math::Vector3D(camPos.GetX(), 0.0f, camPos.GetZ())
228 + dir * (-shadowDist);
229
230 // Convert to Vector<float,3> for Matrix methods
231 Math::Vector<float, 3> lp({lightPos.GetX(), lightPos.GetY(), lightPos.GetZ()});
232 Math::Vector<float, 3> ld({dir.GetX(), dir.GetY(), dir.GetZ()});
233
234 // Avoid degenerate LookTo when light direction is nearly vertical
235 // (cross product with (0,1,0) would be zero → NaN matrix)
236 Math::Vector<float, 3> up = (fabsf(dir.GetY()) > 0.999f)
237 ? Math::Vector<float, 3>({0.0f, 0.0f, 1.0f})
238 : Math::Vector<float, 3>({0.0f, 1.0f, 0.0f});
239
240 Math::Matrix4 lightView = Math::Matrix4::LookTo(lp, ld, up);
241
242 // Build Vulkan-compatible orthographic projection (LH, [0,1] depth range)
243 // Engine stores row-major, GLSL reads column-major (transposed) —
244 // translations go in ROW 3 so they end up in GLSL column 3.
245 float left = -frustumSize, right = frustumSize;
246 float bottom = -frustumSize, top = frustumSize;
248 lightProj(0, 0) = 2.0f / (right - left);
249 lightProj(1, 1) = 2.0f / (top - bottom);
250 lightProj(2, 2) = 1.0f / (farP - nearP);
251 lightProj(3, 0) = -(right + left) / (right - left);
252 lightProj(3, 1) = -(top + bottom) / (top - bottom);
253 lightProj(3, 2) = -nearP / (farP - nearP);
254
255 // ---- Texel snap (DirectX SDK standard technique) ----
256 // Project world origin through the raw lightVP, measure its XY in
257 // shadow-map texel space, snap to nearest texel, apply the delta
258 // back to the projection matrix. This guarantees the sampling grid
259 // is aligned to world-space texel cells so sub-texel camera motion
260 // never shifts which texel a world point lands on → no shimmer.
261 //
262 // Using round() (not floor) — floor flips by a full texel when
263 // the fractional part crosses 0 due to float noise.
264 const float shadowMapSize =
265 static_cast<float>(renderer->GetShadowMapResolution());
266 const float halfShadow = shadowMapSize * 0.5f;
267
268 Math::Matrix4 lightVP_raw = lightView * lightProj;
269 // Anchor the snap on the WORLD ORIGIN (fixed point). The camera is a
270 // constant offset from the light frustum (lightPos follows camXZ), so
271 // projecting the camera yields the SAME texel coords every frame —
272 // constant delta, snap no-ops, world texels crawl while moving. The
273 // origin's projected texel position drifts as the frustum follows the
274 // camera; rounding it quantizes frustum motion to whole texels.
275 // Row-vector convention: (0,0,0,1) * M = row 3.
276 float clipX = lightVP_raw(3, 0);
277 float clipY = lightVP_raw(3, 1);
278 float clipW = lightVP_raw(3, 3);
279 if (clipW != 0.0f) {
280 float ndcX = clipX / clipW;
281 float ndcY = clipY / clipW;
282 float texX = ndcX * halfShadow;
283 float texY = ndcY * halfShadow;
284 float roundedX = std::round(texX);
285 float roundedY = std::round(texY);
286 float offsetNdcX = (roundedX - texX) / halfShadow;
287 float offsetNdcY = (roundedY - texY) / halfShadow;
288 lightProj(3, 0) += offsetNdcX;
289 lightProj(3, 1) += offsetNdcY;
290 }
291
292 // LightVP = View * Projection (row-major convention)
293 lightVP = lightView * lightProj;
294 }
295
296 // DIAG --shadowfreeze: latch the first lightVP forever. If shadows still
297 // shimmer with a frozen frustum, the cause is screen-space, not the
298 // frustum-follow chain.
299 {
300 static const bool s_freeze = CommandLine::HasFlag("--shadowfreeze");
301 static bool s_latched = false;
302 static float s_frozenVP[16];
303 if (s_freeze && shadowLight) {
304 if (!s_latched) {
305 std::memcpy(s_frozenVP, &lightVP(0, 0), sizeof(s_frozenVP));
306 s_latched = true;
307 SLEAK_WARN("shadowfreeze: light frustum latched");
308 } else {
309 std::memcpy(&lightVP(0, 0), s_frozenVP, sizeof(s_frozenVP));
310 }
311 }
312 }
313
314 // Set the light VP matrix on the renderer
315 renderer->SetLightVP(&lightVP(0, 0));
316
317 // Build shadow light UBO
318 const auto& camPos = Camera::GetMainCameraPosition();
320
321 ubo.LightDir[0] = dir.GetX();
322 ubo.LightDir[1] = dir.GetY();
323 ubo.LightDir[2] = dir.GetZ();
324 ubo.LightDir[3] = shadowLight ? shadowLight->GetShadowNormalBias() : 0.0f;
325
326 ubo.LightColor[0] = color.GetX();
327 ubo.LightColor[1] = color.GetY();
328 ubo.LightColor[2] = color.GetZ();
329 ubo.LightColor[3] = intensity;
330
331 ubo.Ambient[0] = m_ambientR;
332 ubo.Ambient[1] = m_ambientG;
333 ubo.Ambient[2] = m_ambientB;
334 ubo.Ambient[3] = m_ambientIntensity;
335
336 // CameraPos.w carries a monotonic scene clock, consumed by shaders that
337 // need animation time (e.g. water waves on the Vulkan backend — the engine
338 // has no MaterialUBO slot in its Vulkan pipeline layout, so there is
339 // nowhere else to stash time). Using a static Timer keeps this
340 // self-contained and independent of Application state.
341 static Sleak::Timer s_sceneClock;
342 ubo.CameraPos[0] = camPos.GetX();
343 ubo.CameraPos[1] = camPos.GetY();
344 ubo.CameraPos[2] = camPos.GetZ();
345 ubo.CameraPos[3] = s_sceneClock.Elapsed();
346
347 // CURRENT lightVP — renderers stage SetLightVP and commit at BeginRender,
348 // so this frame's shadow map IS rendered with this matrix. The old
349 // prev-frame copy lagged sampling one frame behind the map (shadow shake
350 // while the camera moved).
351 std::memcpy(ubo.LightVP, &lightVP(0, 0), sizeof(float) * 16);
352
353 // NdcToShadow = InvViewProj * LightVP composed once on CPU so shadow
354 // coords never round-trip through reconstructed world position (that
355 // per-fragment path shimmers under camera rotation). Uses the same
356 // LightVP the UBO carries (prev frame — matches the bound shadow map).
357 {
360 Math::Matrix4 camVP = camV * camP;
361 float invVP[16];
362 if (Invert4x4(&camVP(0, 0), invVP)) {
363 Math::Matrix4 invVPm, lightVPm;
364 std::memcpy(&invVPm(0, 0), invVP, sizeof(float) * 16);
365 std::memcpy(&lightVPm(0, 0), ubo.LightVP, sizeof(float) * 16);
366 Math::Matrix4 comp = invVPm * lightVPm;
367 std::memcpy(ubo.NdcToShadow, &comp(0, 0), sizeof(float) * 16);
368 } else {
369 std::memcpy(ubo.NdcToShadow, ubo.LightVP, sizeof(float) * 16);
370 }
371 }
372
373 ubo.ShadowBias = shadowLight ? shadowLight->GetShadowBias() : 0.0f;
374 ubo.ShadowStrength = shadowLight ? shadowLight->GetShadowStrength() : 0.0f;
375 ubo.ShadowTexelSize =
376 1.0f / static_cast<float>(renderer->GetShadowMapResolution());
377 ubo.LightSize = shadowLight ? shadowLight->GetLightSize() : 0.0f;
378
379 // Fog — distance gradient (horizon + zenith) and exponential height fog
380 if (m_fogEnabled) {
381 ubo.FogColor[0] = m_fogR;
382 ubo.FogColor[1] = m_fogG;
383 ubo.FogColor[2] = m_fogB;
384 ubo.FogColor[3] = 1.0f;
385 ubo.FogStart = m_fogStart;
386 ubo.FogEnd = m_fogEnd;
387
388 ubo.FogColorZenith[0] = m_fogZenithR;
389 ubo.FogColorZenith[1] = m_fogZenithG;
390 ubo.FogColorZenith[2] = m_fogZenithB;
391 ubo.FogColorZenith[3] = 1.0f;
392
393 ubo.HeightFogTop = m_heightFogTop;
394 ubo.HeightFogDensity = m_heightFogDensity;
395 ubo.HeightFogFalloff = m_heightFogFalloff;
396 ubo.HeightFogEnabled = m_heightFogEnabled ? 1.0f : 0.0f;
397 } else {
398 ubo.FogStart = 0.0f;
399 ubo.FogEnd = 0.0f;
400 ubo.HeightFogEnabled = 0.0f;
401 }
402
403 // Populate extra lights (fill, rim — non-shadow directional lights)
404 ubo.NumExtraLights = 0;
405 for (size_t i = 0; i < m_lights.GetSize() && ubo.NumExtraLights < 3; ++i) {
406 Light* light = m_lights[i];
407 if (!light || !light->IsEnabled()) continue;
408 if (light == activeLight) continue; // already in primary slot
409 auto* dlight = dynamic_cast<DirectionalLight*>(light);
410 if (!dlight) continue;
411 auto eDir = dlight->GetDirection();
412 auto eColor = dlight->GetColor();
413 uint32_t idx = ubo.NumExtraLights;
414 ubo.ExtraLightDir[idx][0] = eDir.GetX();
415 ubo.ExtraLightDir[idx][1] = eDir.GetY();
416 ubo.ExtraLightDir[idx][2] = eDir.GetZ();
417 ubo.ExtraLightDir[idx][3] = 0.0f;
418 ubo.ExtraLightColor[idx][0] = eColor.GetX();
419 ubo.ExtraLightColor[idx][1] = eColor.GetY();
420 ubo.ExtraLightColor[idx][2] = eColor.GetZ();
421 ubo.ExtraLightColor[idx][3] = dlight->GetIntensity();
422 ++ubo.NumExtraLights;
423 }
424
425 renderer->UpdateShadowLightUBO(&ubo, sizeof(ubo));
426}
427
428void LightManager::SetAmbientColor(float r, float g, float b) {
429 m_ambientR = r;
430 m_ambientG = g;
431 m_ambientB = b;
432}
433
435 auto* app = Application::GetInstance();
436 if (!app) return;
437 auto* renderer = app->GetRenderer();
438 if (!renderer) return;
439 auto* ctx = renderer->GetContext();
440 if (!ctx || !ctx->IsDeferredEnabled()) return;
441
442 // ViewProj = View * Proj (row-major engine convention)
445 Math::Matrix4 VP = V * P;
446
448 if (!Invert4x4(&VP(0, 0), cb.InvViewProj)) {
449 // Singular matrix — skip update (can happen during initialization)
450 return;
451 }
452
453 // Screen size from Window static state
454 cb.ScreenWidth = static_cast<float>(app->GetWindow().GetWidth());
455 cb.ScreenHeight = static_cast<float>(app->GetWindow().GetHeight());
456 cb.NearPlane = 0.1f;
457 cb.FarPlane = 2000.0f;
458
459 ctx->UpdateDeferredCB(&cb, sizeof(cb));
460}
461
462} // namespace Sleak
#define SLEAK_WARN(...)
Definition Logger.hpp:21
static Application * GetInstance()
The one Application for this process, or null before construction.
static const Math::Matrix4 & GetMainProjectionMatrix()
Definition Camera.hpp:107
static const Math::Vector3D & GetMainCameraPosition()
Definition Camera.hpp:111
static const Math::Matrix4 & GetMainViewMatrix()
Definition Camera.hpp:103
static bool HasFlag(const std::string &flag)
True if --flag was present on the command line.
Math::Vector3D GetDirection() const
void SetAmbientColor(float r, float g, float b)
void RegisterLight(Light *light)
void UpdateDeferredCB()
Refreshes the deferred-pass constant buffer (fog, ambient) independent of the per-light data.
void Initialize()
Allocates the GPU light buffer; call once before the first UpdateAndBind.
void UpdateShadowData()
Picks the active shadow-casting light and refreshes its shadow-space matrices.
void UpdateAndBind()
Packs every registered light and the fog/ambient parameters into the light buffer and binds it.
void UnregisterLight(Light *light)
virtual RenderEngine::LightGPUEntry BuildGPUData() const =0
Packs this light's parameters into the GPU-side entry used by the lighting constant buffer.
bool GetCastShadows() const
Definition Light.hpp:40
bool IsEnabled() const
Definition Light.hpp:37
float GetIntensity() const
Definition Light.hpp:34
float GetShadowStrength() const
Definition Light.hpp:48
Math::Vector3D GetColor() const
Definition Light.hpp:29
float GetShadowBias() const
Definition Light.hpp:43
float GetShadowNormalBias() const
Definition Light.hpp:51
float GetLightSize() const
Definition Light.hpp:54
static Matrix< float, Rows, Rows > Identity()
Definition Matrix.hpp:203
static Matrix< float, 4, 4 > LookTo(const Vector< float, 3 > &eye, const Vector< float, 3 > &direction, const Vector< float, 3 > &up)
Definition Matrix.hpp:315
float GetY() const
Definition Vector.hpp:361
float GetX() const
Definition Vector.hpp:360
float GetZ() const
Definition Vector.hpp:362
const std::string & GetName() const
Definition Object.hpp:25
static BufferBase * CreateBuffer(BufferType Type, uint32_t Size, void *Data)
Creates a buffer via the currently registered backend factory.
float Elapsed() const
Seconds since construction or the last Reset().
Definition Timer.cpp:13
Matrix< float, 4, 4 > Matrix4
Definition Matrix.hpp:413
static constexpr uint32_t MAX_LIGHTS
Root namespace for everything the engine exposes.
Definition Camera.hpp:10
Per-frame deferred lighting pass CB: inverse view-projection plus screen/near-far metrics.
Full per-frame lighting UBO: camera, ambient, fog, and the packed light array.
LightGPUEntry Lights[MAX_LIGHTS]
Shadow-pass UBO: light/shadow parameters, fog, and extra fill lights (set 2, binding 0).