space-game001/src/cutscene/CutsceneOverlay.cpp
2026-06-06 22:04:42 +03:00

411 lines
13 KiB
C++

#include "cutscene/CutsceneOverlay.h"
#include "dialogue/DialogueTypes.h"
#include "Environment.h"
#include "GameConstants.h"
#include <algorithm>
#include <cmath>
namespace ZL::Cutscene {
bool CutsceneOverlay::init(Renderer& renderer, const std::string& zipFile) {
rendererRef = &renderer;
zipFilename = zipFile;
choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile);
choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile);
cutsceneSubtitleTexture = renderer.textureManager.LoadFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile);
nameRenderer = std::make_unique<TextRenderer>();
cutsceneRenderer= std::make_unique<TextRenderer>();
choiceRenderer = std::make_unique<TextRenderer>();
return
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
cutsceneRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile) &&
choiceRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 22, zipFile);
}
void CutsceneOverlay::update(const ZL::Dialogue::PresentationModel& model, int deltaMs) {
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) {
cutsceneSkipHintVisible = false;
cutsceneSkipArmed = false;
cutsceneSkipHolding = false;
cutsceneSkipTriggered = false;
cutsceneSkipHintRemainingMs = 0;
cutsceneSkipHoldElapsedMs = 0;
return;
}
const int safeDeltaMs = max(deltaMs, 0);
if (cutsceneSkipHintVisible) {
cutsceneSkipHintRemainingMs -= safeDeltaMs;
if (cutsceneSkipHintRemainingMs <= 0) {
cutsceneSkipHintVisible = false;
cutsceneSkipArmed = false;
cutsceneSkipHolding = false;
cutsceneSkipHintRemainingMs = 0;
cutsceneSkipHoldElapsedMs = 0;
}
}
if (cutsceneSkipHolding && cutsceneSkipArmed) {
cutsceneSkipHoldElapsedMs += safeDeltaMs;
if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) {
cutsceneSkipTriggered = true;
cutsceneSkipHolding = false;
cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs;
}
}
}
void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationModel& model) {
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return;
const float W = Environment::projectionWidth;
const float H = Environment::projectionHeight;
const UiRect subtitleRect{ W * 0.12f, 22.0f, W * 0.76f, 110.0f };
lastCutsceneAdvanceRect = subtitleRect;
glEnable(GL_BLEND);
renderer.shaderManager.PushShader("cutsceneFade");
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
renderer.PushMatrix();
renderer.LoadIdentity();
const UiRect screenRect{ 0.0f, 0.0f, W, H };
std::vector<ZL::Cutscene::PresentedCutsceneImage> imageLayers = model.cutsceneImages;
if (imageLayers.empty() && !model.backgroundPath.empty()) {
imageLayers.push_back({ model.backgroundPath, 1.0f });
}
for (const ZL::Cutscene::PresentedCutsceneImage& layer : imageLayers) {
const auto texture = loadTextureCached(layer.path);
if (!texture) continue;
const float imgW = (model.backgroundWidth > 0) ? static_cast<float>(model.backgroundWidth) : static_cast<float>(texture->getWidth());
const float imgH = (model.backgroundHeight > 0) ? static_cast<float>(model.backgroundHeight) : static_cast<float>(texture->getHeight());
ResolvedViewport layerViewport{};
if (model.cutsceneCamera.active) {
const ResolvedViewport fromVP = resolveViewportPose(model.cutsceneCamera.from, imgW, imgH, W, H);
const ResolvedViewport toVP = resolveViewportPose(model.cutsceneCamera.to, imgW, imgH, W, H);
layerViewport = blendViewport(fromVP, toVP, std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f));
}
else {
layerViewport = resolveViewportPose(ZL::Cutscene::CutsceneCameraPose{}, imgW, imgH, W, H);
}
const float halfW = layerViewport.widthPx * 0.5f;
const float halfH = layerViewport.heightPx * 0.5f;
const float rotRad = layerViewport.rotationDeg * 3.14159265358979323846f / 180.0f;
const float c = std::cos(rotRad);
const float s = std::sin(rotRad);
auto rotatePoint = [&](float x, float y) -> Eigen::Vector2f {
return {
layerViewport.centerXPx + x * c - y * s,
layerViewport.centerYPx + x * s + y * c
};
};
const Eigen::Vector2f srcBL = rotatePoint(-halfW, -halfH);
const Eigen::Vector2f srcTL = rotatePoint(-halfW, +halfH);
const Eigen::Vector2f srcTR = rotatePoint(+halfW, +halfH);
const Eigen::Vector2f srcBR = rotatePoint(+halfW, -halfH);
auto toUV = [&](const Eigen::Vector2f& p) -> Eigen::Vector2f {
return {
std::clamp(p.x() / max(imgW, 1.0f), 0.0f, 1.0f),
std::clamp(p.y() / max(imgH, 1.0f), 0.0f, 1.0f)
};
};
backgroundQuad.rebuildWithUV(screenRect, toUV(srcBL), toUV(srcTL), toUV(srcTR), toUV(srcBR));
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha * model.cutsceneGlobalFadeAlpha, 0.0f, 1.0f));
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
if (model.cutsceneBlackAlpha > 0.001f) {
renderer.shaderManager.PushShader("cutsceneBlack");
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
renderer.PushMatrix();
renderer.LoadIdentity();
backgroundQuad.rebuild(screenRect);
renderer.RenderUniform1f("uAlpha", model.cutsceneBlackAlpha);
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
renderer.PushMatrix();
renderer.LoadIdentity();
if (model.showCutsceneSubtitle) {
subtitleQuad.rebuild(subtitleRect);
glBindTexture(GL_TEXTURE_2D, cutsceneSubtitleTexture->getTexID());
renderer.DrawVertexRenderStruct(subtitleQuad.mesh);
}
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
const UiRect hintBg{ W - 250.0f, H - 62.0f, 226.0f, 42.0f };
skipHintBgQuad.rebuild(hintBg);
glBindTexture(GL_TEXTURE_2D, choiceOptionalTexture->getTexID());
renderer.DrawVertexRenderStruct(skipHintBgQuad.mesh);
const UiRect progressBg{ W - 232.0f, H - 34.0f, 190.0f, 7.0f };
skipProgressBgQuad.rebuild(progressBg);
renderer.DrawVertexRenderStruct(skipProgressBgQuad.mesh);
if (cutsceneSkipHolding) {
const float progress = std::clamp(
static_cast<float>(cutsceneSkipHoldElapsedMs) / static_cast<float>(CutsceneSkipHoldDurationMs),
0.0f, 1.0f
);
const UiRect progressFill{ progressBg.x, progressBg.y, progressBg.w * progress, progressBg.h };
skipProgressFillQuad.rebuild(progressFill);
glBindTexture(GL_TEXTURE_2D, choiceMainTexture->getTexID());
renderer.DrawVertexRenderStruct(skipProgressFillQuad.mesh);
}
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
if (model.showCutsceneSubtitle) {
if (!model.speaker.empty()) {
nameRenderer->drawText(
model.speaker,
subtitleRect.x + 24.0f,
subtitleRect.y + subtitleRect.h - 32.0f,
1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f }
);
}
const std::string wrapped = wrapTextToWidth(model.visibleText, *cutsceneRenderer, subtitleRect.w - 48.0f, 1.0f);
cutsceneRenderer->drawText(wrapped, subtitleRect.x + 24.0f, subtitleRect.y + 30.0f,
1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
}
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
choiceRenderer->drawText(
cutsceneSkipHolding ? "Hold to skip..." : "Hold LMB to skip",
W - 232.0f, H - 50.0f,
0.85f, false, { 1.0f, 1.0f, 1.0f, 0.95f }
);
}
glDisable(GL_BLEND);
}
bool CutsceneOverlay::consumeSkipRequested() {
const bool result = cutsceneSkipTriggered;
cutsceneSkipTriggered = false;
if (result) {
cutsceneSkipHintVisible = false;
cutsceneSkipArmed = false;
cutsceneSkipHolding = false;
cutsceneSkipHintRemainingMs = 0;
cutsceneSkipHoldElapsedMs = 0;
}
return result;
}
void CutsceneOverlay::handlePointerDown(float x, float y, const ZL::Dialogue::PresentationModel& model) {
(void)x; (void)y;
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) return;
if (!cutsceneSkipArmed) {
cutsceneSkipHintVisible = true;
cutsceneSkipArmed = true;
cutsceneSkipHolding = false;
cutsceneSkipTriggered = false;
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
cutsceneSkipHoldElapsedMs = 0;
return;
}
cutsceneSkipHintVisible = true;
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
cutsceneSkipHolding = true;
cutsceneSkipTriggered = false;
cutsceneSkipHoldElapsedMs = 0;
}
void CutsceneOverlay::handlePointerMoved(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& /*model*/) {
}
bool CutsceneOverlay::handlePointerReleased(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& model) {
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return false;
if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
cutsceneSkipHolding = false;
cutsceneSkipHoldElapsedMs = 0;
}
return true;
}
std::shared_ptr<Texture> CutsceneOverlay::loadTextureCached(const std::string& path) {
if (path.empty()) return nullptr;
return rendererRef->textureManager.LoadFromPng(path, zipFilename);
}
float CutsceneOverlay::lerpFloat(float a, float b, float t) {
return a + (b - a) * t;
}
CutsceneOverlay::ResolvedViewport CutsceneOverlay::resolveViewportPose(
const CutsceneCameraPose& pose,
float texW, float texH,
float screenW, float screenH
) {
ResolvedViewport out{};
const float safeTexW = max(texW, 1.0f);
const float safeTexH = max(texH, 1.0f);
const float safeScreenW = max(screenW, 1.0f);
const float safeScreenH = max(screenH, 1.0f);
const float screenAspect = safeScreenW / safeScreenH;
const float imageAspect = safeTexW / safeTexH;
float baseViewportW = 0.0f;
float baseViewportH = 0.0f;
if (imageAspect >= screenAspect) {
baseViewportH = safeTexH;
baseViewportW = safeTexH * screenAspect;
}
else {
baseViewportW = safeTexW;
baseViewportH = safeTexW / screenAspect;
}
const float zoom = max(pose.zoom, 0.01f);
const float viewportW = baseViewportW / zoom;
const float viewportH = baseViewportH / zoom;
const float rotRad = pose.rotationDeg * 3.14159265358979323846f / 180.0f;
const float c = std::cos(rotRad);
const float s = std::sin(rotRad);
const float halfRotatedW = std::abs((viewportW * 0.5f) * c) + std::abs((viewportH * 0.5f) * s);
const float halfRotatedH = std::abs((viewportW * 0.5f) * s) + std::abs((viewportH * 0.5f) * c);
float centerX = safeTexW * 0.5f;
float centerY = safeTexH * 0.5f;
switch (pose.anchor) {
case CutsceneAnchor::TopLeft:
centerX = halfRotatedW;
centerY = safeTexH - halfRotatedH;
break;
case CutsceneAnchor::TopRight:
centerX = safeTexW - halfRotatedW;
centerY = safeTexH - halfRotatedH;
break;
case CutsceneAnchor::BottomRight:
centerX = safeTexW - halfRotatedW;
centerY = halfRotatedH;
break;
case CutsceneAnchor::BottomLeft:
centerX = halfRotatedW;
centerY = halfRotatedH;
break;
case CutsceneAnchor::Custom:
centerX = std::clamp(pose.centerX, 0.0f, 1.0f) * safeTexW;
centerY = (1.0f - std::clamp(pose.centerY, 0.0f, 1.0f)) * safeTexH;
centerX = std::clamp(centerX, halfRotatedW, safeTexW - halfRotatedW);
centerY = std::clamp(centerY, halfRotatedH, safeTexH - halfRotatedH);
break;
case CutsceneAnchor::Center:
default:
centerX = safeTexW * 0.5f;
centerY = safeTexH * 0.5f;
break;
}
out.centerXPx = centerX;
out.centerYPx = centerY;
out.widthPx = viewportW;
out.heightPx = viewportH;
out.rotationDeg = pose.rotationDeg;
return out;
}
CutsceneOverlay::ResolvedViewport CutsceneOverlay::blendViewport(
const ResolvedViewport& from,
const ResolvedViewport& to,
float t
) {
ResolvedViewport out;
out.centerXPx = lerpFloat(from.centerXPx, to.centerXPx, t);
out.centerYPx = lerpFloat(from.centerYPx, to.centerYPx, t);
out.widthPx = lerpFloat(from.widthPx, to.widthPx, t);
out.heightPx = lerpFloat(from.heightPx, to.heightPx, t);
out.rotationDeg = lerpFloat(from.rotationDeg, to.rotationDeg, t);
return out;
}
std::string CutsceneOverlay::wrapTextToWidth(
const std::string& input,
const TextRenderer& textRenderer,
float maxWidthPx,
float scale)
{
if (input.empty() || maxWidthPx <= 1.0f) return input;
std::string output;
std::string currentLine;
std::string currentWord;
auto flushLine = [&]() {
if (!currentLine.empty()) {
if (!output.empty()) output.push_back('\n');
output += currentLine;
currentLine.clear();
}
};
auto pushWord = [&](const std::string& word) {
if (word.empty()) return;
if (currentLine.empty()) { currentLine = word; return; }
const std::string candidate = currentLine + " " + word;
if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
currentLine = candidate;
}
else {
flushLine();
currentLine = word;
}
};
for (size_t i = 0; i < input.size(); ++i) {
const char ch = input[i];
if (ch == '\n') { pushWord(currentWord); currentWord.clear(); flushLine(); continue; }
if (ch == ' ' || ch == '\t' || ch == '\r') { pushWord(currentWord); currentWord.clear(); continue; }
currentWord.push_back(ch);
}
pushWord(currentWord);
flushLine();
return output;
}
} // namespace ZL::Cutscene