411 lines
13 KiB
C++
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
|