Some base for cutscene
This commit is contained in:
parent
3ba8a2f44b
commit
54cc118df7
73
resources/dialogue/cutscenes.json
Normal file
73
resources/dialogue/cutscenes.json
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"cutscenes": [
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_01",
|
||||||
|
"background": "resources/black.png",
|
||||||
|
"durationMs": 5000,
|
||||||
|
"fadeOutMs": 500,
|
||||||
|
"fadeInMs": 500,
|
||||||
|
"endFadeOutMs": 500,
|
||||||
|
"endFadeInMs": 500,
|
||||||
|
"imageSegments": [
|
||||||
|
{
|
||||||
|
"path": "resources/w/cutscenes/cutscene1/cutscene1_wall_x.png",
|
||||||
|
"startMs": 0,
|
||||||
|
"endMs": 8000,
|
||||||
|
"fadeInMs": 300,
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720,
|
||||||
|
"from": {
|
||||||
|
"centerX": 0.4,
|
||||||
|
"centerY": 0.5,
|
||||||
|
"scale": 1.1
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"centerX": 0.6,
|
||||||
|
"centerY": 0.5,
|
||||||
|
"scale": 1.0
|
||||||
|
},
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "resources/w/cutscenes/cutscene1/cutscene1_aida1_x.png",
|
||||||
|
"startMs": 0,
|
||||||
|
"endMs": 8000,
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720,
|
||||||
|
"from": {
|
||||||
|
|
||||||
|
"centerX": 0.3,
|
||||||
|
"centerY": 0.5,
|
||||||
|
"scale": 1.0
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"centerX": 0.7,
|
||||||
|
"centerY": 0.5,
|
||||||
|
"scale": 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"speaker": "Аида Дженибековна",
|
||||||
|
"portrait": "resources/dialogue/portrait_teacher.png",
|
||||||
|
"text": "Здравствуйте, студенты. Кого я вижу, где вы были весь семестр?",
|
||||||
|
"durationMs": 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speaker": "Аида Дженибековна",
|
||||||
|
"portrait": "resources/dialogue/portrait_teacher.png",
|
||||||
|
"text": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
|
||||||
|
"durationMs": 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speaker": "Аида Дженибековна",
|
||||||
|
"portrait": "resources/dialogue/portrait_teacher.png",
|
||||||
|
"text": "На сегодня лекция завершена. Домашнее задание - к практическому занятию вы должны подготовить презентации, каждый по своей теме.",
|
||||||
|
"durationMs": 2000,
|
||||||
|
"background": "resources/test_cutscene001.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -130,7 +130,7 @@ namespace ZL
|
|||||||
|
|
||||||
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
||||||
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
|
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
|
||||||
dialogueSystem.loadCutsceneDatabase(params.dialoguesJsonPath);
|
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json");
|
||||||
dialogueSystem.setQuestJournal(journal);
|
dialogueSystem.setQuestJournal(journal);
|
||||||
|
|
||||||
npcNameText = std::make_unique<TextRenderer>();
|
npcNameText = std::make_unique<TextRenderer>();
|
||||||
|
|||||||
@ -11,27 +11,18 @@ namespace ZL
|
|||||||
namespace ZL::Cutscene {
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
EasingType CutsceneDatabase::parseEasingType(const std::string& value) {
|
EasingType CutsceneDatabase::parseEasingType(const std::string& value) {
|
||||||
if (value == "EaseInSine") return EasingType::EaseInSine;
|
if (value == "EaseInSine") return EasingType::EaseInSine;
|
||||||
if (value == "EaseOutSine") return EasingType::EaseOutSine;
|
if (value == "EaseOutSine") return EasingType::EaseOutSine;
|
||||||
if (value == "EaseInOutSine") return EasingType::EaseInOutSine;
|
if (value == "EaseInOutSine") return EasingType::EaseInOutSine;
|
||||||
if (value == "EaseInQuad") return EasingType::EaseInQuad;
|
if (value == "EaseInQuad") return EasingType::EaseInQuad;
|
||||||
if (value == "EaseOutQuad") return EasingType::EaseOutQuad;
|
if (value == "EaseOutQuad") return EasingType::EaseOutQuad;
|
||||||
if (value == "EaseInOutQuad") return EasingType::EaseInOutQuad;
|
if (value == "EaseInOutQuad") return EasingType::EaseInOutQuad;
|
||||||
if (value == "EaseInCubic") return EasingType::EaseInCubic;
|
if (value == "EaseInCubic") return EasingType::EaseInCubic;
|
||||||
if (value == "EaseOutCubic") return EasingType::EaseOutCubic;
|
if (value == "EaseOutCubic") return EasingType::EaseOutCubic;
|
||||||
if (value == "EaseInOutCubic") return EasingType::EaseInOutCubic;
|
if (value == "EaseInOutCubic") return EasingType::EaseInOutCubic;
|
||||||
return EasingType::Linear;
|
return EasingType::Linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
CutsceneAnchor CutsceneDatabase::parseCutsceneAnchor(const std::string& value) {
|
|
||||||
if (value == "TopLeft") return CutsceneAnchor::TopLeft;
|
|
||||||
if (value == "TopRight") return CutsceneAnchor::TopRight;
|
|
||||||
if (value == "BottomRight") return CutsceneAnchor::BottomRight;
|
|
||||||
if (value == "BottomLeft") return CutsceneAnchor::BottomLeft;
|
|
||||||
if (value == "Custom") return CutsceneAnchor::Custom;
|
|
||||||
return CutsceneAnchor::Center;
|
|
||||||
}
|
|
||||||
|
|
||||||
CutsceneLine CutsceneDatabase::parseCutsceneLine(const json& j) {
|
CutsceneLine CutsceneDatabase::parseCutsceneLine(const json& j) {
|
||||||
CutsceneLine line;
|
CutsceneLine line;
|
||||||
line.speaker = j.value("speaker", "");
|
line.speaker = j.value("speaker", "");
|
||||||
@ -42,64 +33,50 @@ CutsceneLine CutsceneDatabase::parseCutsceneLine(const json& j) {
|
|||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
CutsceneCameraPose CutsceneDatabase::parseCutsceneCameraPose(const json& j) {
|
CutsceneImagePose CutsceneDatabase::parseCutsceneImagePose(const json& j) {
|
||||||
CutsceneCameraPose pose;
|
CutsceneImagePose pose;
|
||||||
pose.anchor = parseCutsceneAnchor(j.value("anchor", "Center"));
|
pose.centerX = j.value("centerX", 0.5f);
|
||||||
pose.centerX = j.value("centerX", 0.5f);
|
pose.centerY = j.value("centerY", 0.5f);
|
||||||
pose.centerY = j.value("centerY", 0.5f);
|
pose.scale = j.value("scale", 1.0f);
|
||||||
pose.zoom = j.value("zoom", 1.0f);
|
|
||||||
pose.rotationDeg = j.value("rotationDeg", 0.0f);
|
|
||||||
return pose;
|
return pose;
|
||||||
}
|
}
|
||||||
|
|
||||||
CutsceneCameraSegment CutsceneDatabase::parseCutsceneCameraSegment(const json& j) {
|
CutsceneImageSegment CutsceneDatabase::parseCutsceneImageSegment(const json& j) {
|
||||||
CutsceneCameraSegment segment;
|
CutsceneImageSegment seg;
|
||||||
segment.durationMs = j.value("durationMs", 0);
|
seg.path = j.value("path", "");
|
||||||
segment.easing = parseEasingType(j.value("easing", "EaseInOutSine"));
|
seg.startMs = j.value("startMs", 0);
|
||||||
|
seg.endMs = j.value("endMs", 0);
|
||||||
|
seg.fadeInMs = j.value("fadeInMs", 0);
|
||||||
|
seg.fadeOutMs = j.value("fadeOutMs", 0);
|
||||||
|
seg.width = j.value("width", 0);
|
||||||
|
seg.height = j.value("height", 0);
|
||||||
|
seg.easing = parseEasingType(j.value("easing", "Linear"));
|
||||||
if (j.contains("from") && j["from"].is_object()) {
|
if (j.contains("from") && j["from"].is_object()) {
|
||||||
segment.from = parseCutsceneCameraPose(j["from"]);
|
seg.from = parseCutsceneImagePose(j["from"]);
|
||||||
}
|
}
|
||||||
if (j.contains("to") && j["to"].is_object()) {
|
if (j.contains("to") && j["to"].is_object()) {
|
||||||
segment.to = parseCutsceneCameraPose(j["to"]);
|
seg.to = parseCutsceneImagePose(j["to"]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
segment.to = segment.from;
|
seg.to = seg.from;
|
||||||
}
|
}
|
||||||
return segment;
|
return seg;
|
||||||
}
|
|
||||||
|
|
||||||
CutsceneImageCue CutsceneDatabase::parseCutsceneImageCue(const json& j) {
|
|
||||||
CutsceneImageCue cue;
|
|
||||||
cue.path = j.value("path", "");
|
|
||||||
cue.startMs = j.value("startMs", 0);
|
|
||||||
cue.endMs = j.value("endMs", 0);
|
|
||||||
cue.fadeInMs = j.value("fadeInMs", 0);
|
|
||||||
cue.fadeOutMs = j.value("fadeOutMs", 0);
|
|
||||||
return cue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StaticCutsceneDefinition CutsceneDatabase::parseCutscene(const json& j) {
|
StaticCutsceneDefinition CutsceneDatabase::parseCutscene(const json& j) {
|
||||||
StaticCutsceneDefinition cutscene;
|
StaticCutsceneDefinition cutscene;
|
||||||
cutscene.id = j.value("id", "");
|
cutscene.id = j.value("id", "");
|
||||||
cutscene.background = j.value("background", "");
|
cutscene.onFadeInCallback = j.value("onFadeInCallback", "");
|
||||||
cutscene.backgroundWidth = j.value("backgroundWidth", 1280);
|
cutscene.skippable = j.value("skippable", true);
|
||||||
cutscene.backgroundHeight= j.value("backgroundHeight", 720);
|
cutscene.durationMs = j.value("durationMs", 0);
|
||||||
cutscene.skippable = j.value("skippable", true);
|
cutscene.fadeOutMs = j.value("fadeOutMs", 0);
|
||||||
cutscene.durationMs = j.value("durationMs", 0);
|
cutscene.fadeInMs = j.value("fadeInMs", 0);
|
||||||
cutscene.fadeOutMs = j.value("fadeOutMs", 0);
|
cutscene.endFadeOutMs = j.value("endFadeOutMs", 0);
|
||||||
cutscene.fadeInMs = j.value("fadeInMs", 0);
|
cutscene.endFadeInMs = j.value("endFadeInMs", 0);
|
||||||
cutscene.endFadeOutMs = j.value("endFadeOutMs", 0);
|
|
||||||
cutscene.endFadeInMs = j.value("endFadeInMs", 0);
|
|
||||||
cutscene.onFadeInCallback= j.value("onFadeInCallback", "");
|
|
||||||
|
|
||||||
if (j.contains("cameraTrack") && j["cameraTrack"].is_array()) {
|
if (j.contains("imageSegments") && j["imageSegments"].is_array()) {
|
||||||
for (const auto& item : j["cameraTrack"]) {
|
for (const auto& item : j["imageSegments"]) {
|
||||||
cutscene.cameraTrack.push_back(parseCutsceneCameraSegment(item));
|
cutscene.imageSegments.push_back(parseCutsceneImageSegment(item));
|
||||||
}
|
|
||||||
}
|
|
||||||
if (j.contains("images") && j["images"].is_array()) {
|
|
||||||
for (const auto& item : j["images"]) {
|
|
||||||
cutscene.images.push_back(parseCutsceneImageCue(item));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (j.contains("lines") && j["lines"].is_array()) {
|
if (j.contains("lines") && j["lines"].is_array()) {
|
||||||
|
|||||||
@ -19,12 +19,10 @@ private:
|
|||||||
std::unordered_map<std::string, StaticCutsceneDefinition> cutscenes;
|
std::unordered_map<std::string, StaticCutsceneDefinition> cutscenes;
|
||||||
|
|
||||||
static EasingType parseEasingType(const std::string& value);
|
static EasingType parseEasingType(const std::string& value);
|
||||||
static CutsceneAnchor parseCutsceneAnchor(const std::string& value);
|
|
||||||
|
|
||||||
static CutsceneLine parseCutsceneLine(const json& j);
|
static CutsceneLine parseCutsceneLine(const json& j);
|
||||||
static CutsceneCameraPose parseCutsceneCameraPose(const json& j);
|
static CutsceneImagePose parseCutsceneImagePose(const json& j);
|
||||||
static CutsceneCameraSegment parseCutsceneCameraSegment(const json& j);
|
static CutsceneImageSegment parseCutsceneImageSegment(const json& j);
|
||||||
static CutsceneImageCue parseCutsceneImageCue(const json& j);
|
|
||||||
static StaticCutsceneDefinition parseCutscene(const json& j);
|
static StaticCutsceneDefinition parseCutscene(const json& j);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,13 @@ bool CutsceneOverlay::init(Renderer& renderer, const std::string& zipFile) {
|
|||||||
rendererRef = &renderer;
|
rendererRef = &renderer;
|
||||||
zipFilename = zipFile;
|
zipFilename = zipFile;
|
||||||
|
|
||||||
choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile);
|
choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile);
|
||||||
choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile);
|
choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile);
|
||||||
cutsceneSubtitleTexture = renderer.textureManager.LoadFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile);
|
cutsceneSubtitleTexture = renderer.textureManager.LoadFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile);
|
||||||
|
|
||||||
nameRenderer = std::make_unique<TextRenderer>();
|
nameRenderer = std::make_unique<TextRenderer>();
|
||||||
cutsceneRenderer= std::make_unique<TextRenderer>();
|
cutsceneRenderer = std::make_unique<TextRenderer>();
|
||||||
choiceRenderer = std::make_unique<TextRenderer>();
|
choiceRenderer = std::make_unique<TextRenderer>();
|
||||||
|
|
||||||
return
|
return
|
||||||
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
|
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
|
||||||
@ -28,12 +28,12 @@ bool CutsceneOverlay::init(Renderer& renderer, const std::string& zipFile) {
|
|||||||
|
|
||||||
void CutsceneOverlay::update(const ZL::Dialogue::PresentationModel& model, int deltaMs) {
|
void CutsceneOverlay::update(const ZL::Dialogue::PresentationModel& model, int deltaMs) {
|
||||||
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) {
|
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) {
|
||||||
cutsceneSkipHintVisible = false;
|
cutsceneSkipHintVisible = false;
|
||||||
cutsceneSkipArmed = false;
|
cutsceneSkipArmed = false;
|
||||||
cutsceneSkipHolding = false;
|
cutsceneSkipHolding = false;
|
||||||
cutsceneSkipTriggered = false;
|
cutsceneSkipTriggered = false;
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
cutsceneSkipHintRemainingMs = 0;
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,24 +42,70 @@ void CutsceneOverlay::update(const ZL::Dialogue::PresentationModel& model, int d
|
|||||||
if (cutsceneSkipHintVisible) {
|
if (cutsceneSkipHintVisible) {
|
||||||
cutsceneSkipHintRemainingMs -= safeDeltaMs;
|
cutsceneSkipHintRemainingMs -= safeDeltaMs;
|
||||||
if (cutsceneSkipHintRemainingMs <= 0) {
|
if (cutsceneSkipHintRemainingMs <= 0) {
|
||||||
cutsceneSkipHintVisible = false;
|
cutsceneSkipHintVisible = false;
|
||||||
cutsceneSkipArmed = false;
|
cutsceneSkipArmed = false;
|
||||||
cutsceneSkipHolding = false;
|
cutsceneSkipHolding = false;
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
cutsceneSkipHintRemainingMs = 0;
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cutsceneSkipHolding && cutsceneSkipArmed) {
|
if (cutsceneSkipHolding && cutsceneSkipArmed) {
|
||||||
cutsceneSkipHoldElapsedMs += safeDeltaMs;
|
cutsceneSkipHoldElapsedMs += safeDeltaMs;
|
||||||
if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) {
|
if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) {
|
||||||
cutsceneSkipTriggered = true;
|
cutsceneSkipTriggered = true;
|
||||||
cutsceneSkipHolding = false;
|
cutsceneSkipHolding = false;
|
||||||
cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs;
|
cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CutsceneOverlay::buildImageUV(
|
||||||
|
const CutsceneImagePose& pose,
|
||||||
|
float imgW, float imgH,
|
||||||
|
float screenW, float screenH,
|
||||||
|
Eigen::Vector2f& outBL, Eigen::Vector2f& outTL,
|
||||||
|
Eigen::Vector2f& outTR, Eigen::Vector2f& outBR)
|
||||||
|
{
|
||||||
|
const float safeImgW = max(imgW, 1.0f);
|
||||||
|
const float safeImgH = max(imgH, 1.0f);
|
||||||
|
const float safeScrnW = max(screenW, 1.0f);
|
||||||
|
const float safeScrnH = max(screenH, 1.0f);
|
||||||
|
|
||||||
|
const float screenAspect = safeScrnW / safeScrnH;
|
||||||
|
const float imageAspect = safeImgW / safeImgH;
|
||||||
|
|
||||||
|
// Aspect-ratio corrected base viewport at scale = 1: the portion of the image
|
||||||
|
// that fills the screen without stretching.
|
||||||
|
float baseW, baseH;
|
||||||
|
if (imageAspect >= screenAspect) {
|
||||||
|
baseH = safeImgH;
|
||||||
|
baseW = safeImgH * screenAspect;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
baseW = safeImgW;
|
||||||
|
baseH = safeImgW / screenAspect;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scale = max(pose.scale, 0.01f);
|
||||||
|
const float viewportW = baseW / scale;
|
||||||
|
const float viewportH = baseH / scale;
|
||||||
|
const float halfW = viewportW * 0.5f;
|
||||||
|
const float halfH = viewportH * 0.5f;
|
||||||
|
|
||||||
|
// Map normalized pose center to image pixel coords; clamp so viewport stays inside image.
|
||||||
|
const float rawCX = std::clamp(pose.centerX, 0.0f, 1.0f) * safeImgW;
|
||||||
|
const float rawCY = std::clamp(pose.centerY, 0.0f, 1.0f) * safeImgH;
|
||||||
|
const float cx = std::clamp(rawCX, halfW, safeImgW - halfW);
|
||||||
|
const float cy = std::clamp(rawCY, halfH, safeImgH - halfH);
|
||||||
|
|
||||||
|
// Viewport corners in image pixel space, then normalized to UV [0..1].
|
||||||
|
outBL = { (cx - halfW) / safeImgW, (cy - halfH) / safeImgH };
|
||||||
|
outTL = { (cx - halfW) / safeImgW, (cy + halfH) / safeImgH };
|
||||||
|
outTR = { (cx + halfW) / safeImgW, (cy + halfH) / safeImgH };
|
||||||
|
outBR = { (cx + halfW) / safeImgW, (cy - halfH) / safeImgH };
|
||||||
|
}
|
||||||
|
|
||||||
void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationModel& model) {
|
void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationModel& model) {
|
||||||
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return;
|
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return;
|
||||||
|
|
||||||
@ -71,6 +117,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM
|
|||||||
|
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
|
|
||||||
|
// --- Image layers ---
|
||||||
renderer.shaderManager.PushShader("cutsceneFade");
|
renderer.shaderManager.PushShader("cutsceneFade");
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||||
@ -79,54 +126,17 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM
|
|||||||
|
|
||||||
const UiRect screenRect{ 0.0f, 0.0f, W, H };
|
const UiRect screenRect{ 0.0f, 0.0f, W, H };
|
||||||
|
|
||||||
std::vector<ZL::Cutscene::PresentedCutsceneImage> imageLayers = model.cutsceneImages;
|
for (const ZL::Cutscene::PresentedCutsceneImage& layer : 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);
|
const auto texture = loadTextureCached(layer.path);
|
||||||
if (!texture) continue;
|
if (!texture) continue;
|
||||||
|
|
||||||
const float imgW = (model.backgroundWidth > 0) ? static_cast<float>(model.backgroundWidth) : static_cast<float>(texture->getWidth());
|
const float imgW = (layer.width > 0) ? static_cast<float>(layer.width) : static_cast<float>(texture->getWidth());
|
||||||
const float imgH = (model.backgroundHeight > 0) ? static_cast<float>(model.backgroundHeight) : static_cast<float>(texture->getHeight());
|
const float imgH = (layer.height > 0) ? static_cast<float>(layer.height) : static_cast<float>(texture->getHeight());
|
||||||
|
|
||||||
ResolvedViewport layerViewport{};
|
Eigen::Vector2f uvBL, uvTL, uvTR, uvBR;
|
||||||
if (model.cutsceneCamera.active) {
|
buildImageUV(layer.pose, imgW, imgH, W, H, uvBL, uvTL, uvTR, uvBR);
|
||||||
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;
|
backgroundQuad.rebuildWithUV(screenRect, uvBL, uvTL, uvTR, uvBR);
|
||||||
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));
|
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha * model.cutsceneGlobalFadeAlpha, 0.0f, 1.0f));
|
||||||
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
|
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
|
||||||
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
|
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
|
||||||
@ -136,6 +146,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM
|
|||||||
renderer.PopProjectionMatrix();
|
renderer.PopProjectionMatrix();
|
||||||
renderer.shaderManager.PopShader();
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
|
// --- Black fade overlay ---
|
||||||
if (model.cutsceneBlackAlpha > 0.001f) {
|
if (model.cutsceneBlackAlpha > 0.001f) {
|
||||||
renderer.shaderManager.PushShader("cutsceneBlack");
|
renderer.shaderManager.PushShader("cutsceneBlack");
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||||
@ -151,6 +162,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM
|
|||||||
renderer.shaderManager.PopShader();
|
renderer.shaderManager.PopShader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- UI overlay: subtitle panel and skip hint ---
|
||||||
renderer.shaderManager.PushShader(defaultShaderName);
|
renderer.shaderManager.PushShader(defaultShaderName);
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||||
@ -189,6 +201,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM
|
|||||||
renderer.PopProjectionMatrix();
|
renderer.PopProjectionMatrix();
|
||||||
renderer.shaderManager.PopShader();
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
|
// --- Text ---
|
||||||
if (model.showCutsceneSubtitle) {
|
if (model.showCutsceneSubtitle) {
|
||||||
if (!model.speaker.empty()) {
|
if (!model.speaker.empty()) {
|
||||||
nameRenderer->drawText(
|
nameRenderer->drawText(
|
||||||
@ -218,11 +231,11 @@ bool CutsceneOverlay::consumeSkipRequested() {
|
|||||||
const bool result = cutsceneSkipTriggered;
|
const bool result = cutsceneSkipTriggered;
|
||||||
cutsceneSkipTriggered = false;
|
cutsceneSkipTriggered = false;
|
||||||
if (result) {
|
if (result) {
|
||||||
cutsceneSkipHintVisible = false;
|
cutsceneSkipHintVisible = false;
|
||||||
cutsceneSkipArmed = false;
|
cutsceneSkipArmed = false;
|
||||||
cutsceneSkipHolding = false;
|
cutsceneSkipHolding = false;
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
cutsceneSkipHintRemainingMs = 0;
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -232,20 +245,20 @@ void CutsceneOverlay::handlePointerDown(float x, float y, const ZL::Dialogue::Pr
|
|||||||
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) return;
|
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) return;
|
||||||
|
|
||||||
if (!cutsceneSkipArmed) {
|
if (!cutsceneSkipArmed) {
|
||||||
cutsceneSkipHintVisible = true;
|
cutsceneSkipHintVisible = true;
|
||||||
cutsceneSkipArmed = true;
|
cutsceneSkipArmed = true;
|
||||||
cutsceneSkipHolding = false;
|
cutsceneSkipHolding = false;
|
||||||
cutsceneSkipTriggered = false;
|
cutsceneSkipTriggered = false;
|
||||||
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cutsceneSkipHintVisible = true;
|
cutsceneSkipHintVisible = true;
|
||||||
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
||||||
cutsceneSkipHolding = true;
|
cutsceneSkipHolding = true;
|
||||||
cutsceneSkipTriggered = false;
|
cutsceneSkipTriggered = false;
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CutsceneOverlay::handlePointerMoved(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& /*model*/) {
|
void CutsceneOverlay::handlePointerMoved(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& /*model*/) {
|
||||||
@ -254,7 +267,7 @@ void CutsceneOverlay::handlePointerMoved(float /*x*/, float /*y*/, const ZL::Dia
|
|||||||
bool CutsceneOverlay::handlePointerReleased(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 (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return false;
|
||||||
if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
|
if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
|
||||||
cutsceneSkipHolding = false;
|
cutsceneSkipHolding = false;
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -265,103 +278,6 @@ std::shared_ptr<Texture> CutsceneOverlay::loadTextureCached(const std::string& p
|
|||||||
return rendererRef->textureManager.LoadFromPng(path, zipFilename);
|
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(
|
std::string CutsceneOverlay::wrapTextToWidth(
|
||||||
const std::string& input,
|
const std::string& input,
|
||||||
const TextRenderer& textRenderer,
|
const TextRenderer& textRenderer,
|
||||||
@ -395,8 +311,7 @@ std::string CutsceneOverlay::wrapTextToWidth(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (size_t i = 0; i < input.size(); ++i) {
|
for (const char ch : input) {
|
||||||
const char ch = input[i];
|
|
||||||
if (ch == '\n') { pushWord(currentWord); currentWord.clear(); flushLine(); continue; }
|
if (ch == '\n') { pushWord(currentWord); currentWord.clear(); flushLine(); continue; }
|
||||||
if (ch == ' ' || ch == '\t' || ch == '\r') { pushWord(currentWord); currentWord.clear(); continue; }
|
if (ch == ' ' || ch == '\t' || ch == '\r') { pushWord(currentWord); currentWord.clear(); continue; }
|
||||||
currentWord.push_back(ch);
|
currentWord.push_back(ch);
|
||||||
|
|||||||
@ -25,14 +25,6 @@ public:
|
|||||||
bool consumeSkipRequested();
|
bool consumeSkipRequested();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct ResolvedViewport {
|
|
||||||
float centerXPx = 0.0f;
|
|
||||||
float centerYPx = 0.0f;
|
|
||||||
float widthPx = 1.0f;
|
|
||||||
float heightPx = 1.0f;
|
|
||||||
float rotationDeg = 0.0f;
|
|
||||||
};
|
|
||||||
|
|
||||||
Renderer* rendererRef = nullptr;
|
Renderer* rendererRef = nullptr;
|
||||||
std::string zipFilename;
|
std::string zipFilename;
|
||||||
|
|
||||||
@ -41,7 +33,6 @@ private:
|
|||||||
std::shared_ptr<Texture> cutsceneSubtitleTexture;
|
std::shared_ptr<Texture> cutsceneSubtitleTexture;
|
||||||
|
|
||||||
mutable UiRect lastCutsceneAdvanceRect{};
|
mutable UiRect lastCutsceneAdvanceRect{};
|
||||||
mutable UiRect lastCutsceneSkipRect{};
|
|
||||||
|
|
||||||
// Skip UX state
|
// Skip UX state
|
||||||
bool cutsceneSkipHintVisible = false;
|
bool cutsceneSkipHintVisible = false;
|
||||||
@ -65,17 +56,15 @@ private:
|
|||||||
|
|
||||||
std::shared_ptr<Texture> loadTextureCached(const std::string& path);
|
std::shared_ptr<Texture> loadTextureCached(const std::string& path);
|
||||||
|
|
||||||
static float lerpFloat(float a, float b, float t);
|
// Computes UV corners for one image layer given its pose and actual texture size.
|
||||||
static ResolvedViewport resolveViewportPose(
|
static void buildImageUV(
|
||||||
const CutsceneCameraPose& pose,
|
const CutsceneImagePose& pose,
|
||||||
float texW, float texH,
|
float imgW, float imgH,
|
||||||
float screenW, float screenH
|
float screenW, float screenH,
|
||||||
);
|
Eigen::Vector2f& outBL, Eigen::Vector2f& outTL,
|
||||||
static ResolvedViewport blendViewport(
|
Eigen::Vector2f& outTR, Eigen::Vector2f& outBR
|
||||||
const ResolvedViewport& from,
|
|
||||||
const ResolvedViewport& to,
|
|
||||||
float t
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static std::string wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer,
|
static std::string wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer,
|
||||||
float maxWidthPx, float scale);
|
float maxWidthPx, float scale);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -41,21 +41,11 @@ bool CutsceneRuntime::start(const std::string& cutsceneId) {
|
|||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
currentCutsceneLine = def->lines.empty() ? -1 : 0;
|
currentCutsceneLine = def->lines.empty() ? -1 : 0;
|
||||||
|
|
||||||
int imageTrackDurationMs = 0;
|
int maxSegmentEndMs = 0;
|
||||||
for (size_t i = 0; i < def->images.size(); ++i) {
|
for (const CutsceneImageSegment& seg : def->imageSegments) {
|
||||||
const CutsceneImageCue& cue = def->images[i];
|
maxSegmentEndMs = std::max(maxSegmentEndMs, seg.endMs);
|
||||||
int cueEnd = cue.endMs;
|
|
||||||
if (cueEnd <= cue.startMs) {
|
|
||||||
if (i + 1 < def->images.size()) {
|
|
||||||
cueEnd = std::max(def->images[i + 1].startMs, cue.startMs);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cueEnd = cue.startMs + std::max(cue.fadeInMs, 0) + std::max(cue.fadeOutMs, 0) + 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
imageTrackDurationMs = std::max(imageTrackDurationMs, cueEnd);
|
|
||||||
}
|
}
|
||||||
cutsceneContentDurationMs = std::max({ def->durationMs, computeCameraTrackDurationMs(*def), imageTrackDurationMs });
|
cutsceneContentDurationMs = std::max(def->durationMs, maxSegmentEndMs);
|
||||||
if (cutsceneContentDurationMs <= 0 && def->lines.empty()) {
|
if (cutsceneContentDurationMs <= 0 && def->lines.empty()) {
|
||||||
cutsceneContentDurationMs = 3000;
|
cutsceneContentDurationMs = 3000;
|
||||||
}
|
}
|
||||||
@ -155,25 +145,6 @@ bool CutsceneRuntime::canSkip() const {
|
|||||||
|
|
||||||
void CutsceneRuntime::skip() {
|
void CutsceneRuntime::skip() {
|
||||||
if (!canSkip()) return;
|
if (!canSkip()) return;
|
||||||
|
|
||||||
if (!activeCutscene->images.empty()) {
|
|
||||||
int nextImageStartMs = -1;
|
|
||||||
for (const CutsceneImageCue& cue : activeCutscene->images) {
|
|
||||||
if (cue.path.empty()) continue;
|
|
||||||
if (cue.startMs > cutsceneElapsedMs) {
|
|
||||||
if (nextImageStartMs < 0 || cue.startMs < nextImageStartMs) {
|
|
||||||
nextImageStartMs = cue.startMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextImageStartMs >= 0) {
|
|
||||||
cutsceneElapsedMs = nextImageStartMs;
|
|
||||||
syncLineToElapsedTime();
|
|
||||||
refreshPresentation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,15 +186,10 @@ void CutsceneRuntime::syncLineToElapsedTime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CutsceneRuntime::advanceLine() {
|
void CutsceneRuntime::advanceLine() {
|
||||||
if (!activeCutscene) {
|
if (!activeCutscene) { stop(); return; }
|
||||||
stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeCutscene->lines.empty()) return;
|
if (activeCutscene->lines.empty()) return;
|
||||||
|
|
||||||
std::cout << "[CUTSCENE] advance before current=" << currentCutsceneLine << std::endl;
|
|
||||||
++currentCutsceneLine;
|
++currentCutsceneLine;
|
||||||
std::cout << "[CUTSCENE] advance after current=" << currentCutsceneLine << std::endl;
|
|
||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
|
|
||||||
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||||
@ -243,99 +209,43 @@ void CutsceneRuntime::advanceLine() {
|
|||||||
refreshPresentation();
|
refreshPresentation();
|
||||||
}
|
}
|
||||||
|
|
||||||
CutsceneCameraBlendState CutsceneRuntime::evaluateCameraBlend() const {
|
|
||||||
CutsceneCameraBlendState result;
|
|
||||||
result.active = false;
|
|
||||||
result.from = {};
|
|
||||||
result.to = {};
|
|
||||||
result.t = 1.0f;
|
|
||||||
|
|
||||||
if (!activeCutscene || activeCutscene->cameraTrack.empty()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int elapsed = cutsceneElapsedMs;
|
|
||||||
for (const CutsceneCameraSegment& segment : activeCutscene->cameraTrack) {
|
|
||||||
const int durationMs = std::max(segment.durationMs, 1);
|
|
||||||
if (elapsed <= durationMs) {
|
|
||||||
result.active = true;
|
|
||||||
result.from = segment.from;
|
|
||||||
result.to = segment.to;
|
|
||||||
result.t = applyEasing(
|
|
||||||
segment.easing,
|
|
||||||
std::clamp(static_cast<float>(elapsed) / static_cast<float>(durationMs), 0.0f, 1.0f)
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
elapsed -= durationMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.active = true;
|
|
||||||
result.from = activeCutscene->cameraTrack.back().to;
|
|
||||||
result.to = activeCutscene->cameraTrack.back().to;
|
|
||||||
result.t = 1.0f;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<PresentedCutsceneImage> CutsceneRuntime::evaluateImages() const {
|
std::vector<PresentedCutsceneImage> CutsceneRuntime::evaluateImages() const {
|
||||||
std::vector<PresentedCutsceneImage> result;
|
std::vector<PresentedCutsceneImage> result;
|
||||||
if (!activeCutscene) return result;
|
if (!activeCutscene) return result;
|
||||||
|
|
||||||
const std::string& fallbackPath = activeCutscene->background;
|
|
||||||
|
|
||||||
if (activeCutscene->images.empty()) {
|
|
||||||
if (!fallbackPath.empty()) {
|
|
||||||
result.push_back({ fallbackPath, 1.0f });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int effectiveTotalDuration = (cutsceneContentDurationMs > 0)
|
|
||||||
? cutsceneContentDurationMs
|
|
||||||
: std::max(activeCutscene->durationMs, 1);
|
|
||||||
const int now = std::max(cutsceneElapsedMs, 0);
|
const int now = std::max(cutsceneElapsedMs, 0);
|
||||||
|
|
||||||
for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
|
for (const CutsceneImageSegment& seg : activeCutscene->imageSegments) {
|
||||||
const CutsceneImageCue& cue = activeCutscene->images[i];
|
if (seg.path.empty()) continue;
|
||||||
if (cue.path.empty()) continue;
|
if (now < seg.startMs || now > seg.endMs) continue;
|
||||||
|
|
||||||
const int startMs = std::max(cue.startMs, 0);
|
|
||||||
int endMs = cue.endMs;
|
|
||||||
if (endMs <= startMs) {
|
|
||||||
if (i + 1 < activeCutscene->images.size()) {
|
|
||||||
endMs = std::max(activeCutscene->images[i + 1].startMs, startMs + 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
endMs = effectiveTotalDuration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (endMs <= startMs) {
|
|
||||||
endMs = startMs + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now < startMs || now > endMs) continue;
|
|
||||||
|
|
||||||
|
// Fade-in / fade-out alpha
|
||||||
float alpha = 1.0f;
|
float alpha = 1.0f;
|
||||||
if (cue.fadeInMs > 0 && now < startMs + cue.fadeInMs) {
|
if (seg.fadeInMs > 0 && now < seg.startMs + seg.fadeInMs) {
|
||||||
alpha = std::clamp(
|
alpha = std::clamp(
|
||||||
static_cast<float>(now - startMs) / static_cast<float>(cue.fadeInMs),
|
static_cast<float>(now - seg.startMs) / static_cast<float>(seg.fadeInMs),
|
||||||
0.0f, 1.0f
|
0.0f, 1.0f
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (seg.fadeOutMs > 0 && now > seg.endMs - seg.fadeOutMs) {
|
||||||
if (alpha > 0.0f) {
|
const float fadeOutAlpha = std::clamp(
|
||||||
result.push_back({ cue.path, alpha });
|
static_cast<float>(seg.endMs - now) / static_cast<float>(seg.fadeOutMs),
|
||||||
|
0.0f, 1.0f
|
||||||
|
);
|
||||||
|
alpha = std::min(alpha, fadeOutAlpha);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (result.empty() && !fallbackPath.empty()) {
|
// Interpolated pose
|
||||||
result.push_back({ fallbackPath, 1.0f });
|
const float segDuration = static_cast<float>(std::max(seg.endMs - seg.startMs, 1));
|
||||||
}
|
const float rawT = static_cast<float>(now - seg.startMs) / segDuration;
|
||||||
|
const float easedT = applyEasing(seg.easing, std::clamp(rawT, 0.0f, 1.0f));
|
||||||
|
|
||||||
if (!result.empty() && result.front().alpha < 0.999f &&
|
CutsceneImagePose pose;
|
||||||
!fallbackPath.empty() && result.front().path != fallbackPath)
|
pose.centerX = seg.from.centerX + (seg.to.centerX - seg.from.centerX) * easedT;
|
||||||
{
|
pose.centerY = seg.from.centerY + (seg.to.centerY - seg.from.centerY) * easedT;
|
||||||
result.insert(result.begin(), { fallbackPath, 1.0f });
|
pose.scale = seg.from.scale + (seg.to.scale - seg.from.scale) * easedT;
|
||||||
|
|
||||||
|
result.push_back({ seg.path, alpha, pose, seg.width, seg.height });
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -345,12 +255,8 @@ void CutsceneRuntime::refreshPresentation() {
|
|||||||
if (!activeCutscene) return;
|
if (!activeCutscene) return;
|
||||||
|
|
||||||
presentation.mode = ZL::Dialogue::PresentationMode::Cutscene;
|
presentation.mode = ZL::Dialogue::PresentationMode::Cutscene;
|
||||||
presentation.backgroundPath = activeCutscene->background;
|
|
||||||
presentation.backgroundWidth = activeCutscene->backgroundWidth;
|
|
||||||
presentation.backgroundHeight = activeCutscene->backgroundHeight;
|
|
||||||
presentation.cutsceneCamera = evaluateCameraBlend();
|
|
||||||
presentation.cutsceneImages = evaluateImages();
|
|
||||||
presentation.cutsceneSkippable = activeCutscene->skippable;
|
presentation.cutsceneSkippable = activeCutscene->skippable;
|
||||||
|
presentation.cutsceneImages = evaluateImages();
|
||||||
|
|
||||||
const int fadeOutMs = activeCutscene->fadeOutMs;
|
const int fadeOutMs = activeCutscene->fadeOutMs;
|
||||||
const int fadeInMs = activeCutscene->fadeInMs;
|
const int fadeInMs = activeCutscene->fadeInMs;
|
||||||
@ -400,20 +306,14 @@ void CutsceneRuntime::refreshPresentation() {
|
|||||||
presentation.speaker.clear();
|
presentation.speaker.clear();
|
||||||
presentation.fullText.clear();
|
presentation.fullText.clear();
|
||||||
presentation.visibleText.clear();
|
presentation.visibleText.clear();
|
||||||
presentation.portraitPath.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||||
|
|
||||||
presentation.speaker = line.speaker;
|
presentation.speaker = line.speaker;
|
||||||
presentation.fullText = line.text;
|
presentation.fullText = line.text;
|
||||||
presentation.visibleText = line.text;
|
presentation.visibleText = line.text;
|
||||||
presentation.selectedChoice = 0;
|
presentation.selectedChoice = 0;
|
||||||
|
|
||||||
std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
|
|
||||||
<< " current=" << currentCutsceneLine
|
|
||||||
<< std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float CutsceneRuntime::applyEasing(EasingType easing, float t) {
|
float CutsceneRuntime::applyEasing(EasingType easing, float t) {
|
||||||
@ -453,12 +353,4 @@ int CutsceneRuntime::computeFallbackDurationMs(const std::string& text) {
|
|||||||
return std::max(minDuration, calculated + linger);
|
return std::max(minDuration, calculated + linger);
|
||||||
}
|
}
|
||||||
|
|
||||||
int CutsceneRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene) {
|
|
||||||
int total = 0;
|
|
||||||
for (const CutsceneCameraSegment& segment : cutscene.cameraTrack) {
|
|
||||||
total += std::max(segment.durationMs, 0);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZL::Cutscene
|
} // namespace ZL::Cutscene
|
||||||
|
|||||||
@ -50,12 +50,10 @@ private:
|
|||||||
void advanceLine();
|
void advanceLine();
|
||||||
void refreshPresentation();
|
void refreshPresentation();
|
||||||
|
|
||||||
CutsceneCameraBlendState evaluateCameraBlend() const;
|
|
||||||
std::vector<PresentedCutsceneImage> evaluateImages() const;
|
std::vector<PresentedCutsceneImage> evaluateImages() const;
|
||||||
|
|
||||||
static float applyEasing(EasingType easing, float t);
|
static float applyEasing(EasingType easing, float t);
|
||||||
static int computeFallbackDurationMs(const std::string& text);
|
static int computeFallbackDurationMs(const std::string& text);
|
||||||
static int computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL::Cutscene
|
} // namespace ZL::Cutscene
|
||||||
|
|||||||
@ -18,15 +18,6 @@ enum class EasingType {
|
|||||||
EaseInOutCubic
|
EaseInOutCubic
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class CutsceneAnchor {
|
|
||||||
Center,
|
|
||||||
TopLeft,
|
|
||||||
TopRight,
|
|
||||||
BottomRight,
|
|
||||||
BottomLeft,
|
|
||||||
Custom
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CutsceneLine {
|
struct CutsceneLine {
|
||||||
std::string speaker;
|
std::string speaker;
|
||||||
std::string text;
|
std::string text;
|
||||||
@ -35,56 +26,53 @@ struct CutsceneLine {
|
|||||||
bool waitForConfirm = false;
|
bool waitForConfirm = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CutsceneCameraPose {
|
// Describes where/how an image is framed on screen.
|
||||||
CutsceneAnchor anchor = CutsceneAnchor::Center;
|
// centerX/Y: 0..1 normalized over the image; 0.5/0.5 = center of image fills center of screen.
|
||||||
|
// scale: 1 = image fits screen (aspect-corrected), 2 = zoomed in 2x.
|
||||||
|
struct CutsceneImagePose {
|
||||||
float centerX = 0.5f;
|
float centerX = 0.5f;
|
||||||
float centerY = 0.5f;
|
float centerY = 0.5f;
|
||||||
float zoom = 1.0f;
|
float scale = 1.0f;
|
||||||
float rotationDeg = 0.0f;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CutsceneCameraSegment {
|
// One image layer: defines path, active time window, fades, and animated movement.
|
||||||
int durationMs = 0;
|
struct CutsceneImageSegment {
|
||||||
CutsceneCameraPose from;
|
|
||||||
CutsceneCameraPose to;
|
|
||||||
EasingType easing = EasingType::EaseInOutSine;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CutsceneImageCue {
|
|
||||||
std::string path;
|
std::string path;
|
||||||
int startMs = 0;
|
int startMs = 0;
|
||||||
int endMs = 0;
|
int endMs = 0;
|
||||||
int fadeInMs = 0;
|
int fadeInMs = 0;
|
||||||
int fadeOutMs = 0;
|
int fadeOutMs = 0;
|
||||||
|
// Logical size used for all UV/aspect calculations.
|
||||||
|
// 0 = use actual texture pixel dimensions.
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
CutsceneImagePose from;
|
||||||
|
CutsceneImagePose to;
|
||||||
|
EasingType easing = EasingType::Linear;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct StaticCutsceneDefinition {
|
struct StaticCutsceneDefinition {
|
||||||
std::string id;
|
std::string id;
|
||||||
std::string background;
|
|
||||||
int backgroundWidth = 1280;
|
|
||||||
int backgroundHeight = 720;
|
|
||||||
std::string onFadeInCallback;
|
std::string onFadeInCallback;
|
||||||
bool skippable = true;
|
bool skippable = true;
|
||||||
int durationMs = 0;
|
int durationMs = 0;
|
||||||
int fadeOutMs = 0;
|
int fadeOutMs = 0;
|
||||||
int fadeInMs = 0;
|
int fadeInMs = 0;
|
||||||
int endFadeOutMs = 0;
|
int endFadeOutMs = 0;
|
||||||
int endFadeInMs = 0;
|
int endFadeInMs = 0;
|
||||||
std::vector<CutsceneCameraSegment> cameraTrack;
|
std::vector<CutsceneImageSegment> imageSegments;
|
||||||
std::vector<CutsceneImageCue> images;
|
|
||||||
std::vector<CutsceneLine> lines;
|
std::vector<CutsceneLine> lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A single image layer at an evaluated point in time, ready for rendering.
|
||||||
struct PresentedCutsceneImage {
|
struct PresentedCutsceneImage {
|
||||||
std::string path;
|
std::string path;
|
||||||
float alpha = 1.0f;
|
float alpha = 1.0f;
|
||||||
};
|
CutsceneImagePose pose;
|
||||||
|
// Logical size for UV math — mirrors CutsceneImageSegment::width/height.
|
||||||
struct CutsceneCameraBlendState {
|
// 0 = use actual texture pixel dimensions.
|
||||||
bool active = false;
|
int width = 0;
|
||||||
CutsceneCameraPose from;
|
int height = 0;
|
||||||
CutsceneCameraPose to;
|
|
||||||
float t = 1.0f;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL::Cutscene
|
} // namespace ZL::Cutscene
|
||||||
|
|||||||
@ -299,13 +299,13 @@ void DialogueRuntime::presentLine(const Node& node) {
|
|||||||
presentation.fullText = node.text;
|
presentation.fullText = node.text;
|
||||||
presentation.visibleText.clear();
|
presentation.visibleText.clear();
|
||||||
presentation.portraitPath = node.portrait;
|
presentation.portraitPath = node.portrait;
|
||||||
presentation.backgroundPath.clear();
|
|
||||||
presentation.choices.clear();
|
presentation.choices.clear();
|
||||||
presentation.selectedChoice = -1;
|
presentation.selectedChoice = -1;
|
||||||
presentation.revealCompleted = node.text.empty();
|
presentation.revealCompleted = node.text.empty();
|
||||||
presentation.showCutsceneSubtitle = false;
|
presentation.showCutsceneSubtitle = false;
|
||||||
presentation.cutsceneSkippable = false;
|
presentation.cutsceneSkippable = false;
|
||||||
presentation.cutsceneCamera = {};
|
|
||||||
presentation.cutsceneImages.clear();
|
presentation.cutsceneImages.clear();
|
||||||
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
||||||
presentation.cutsceneBlackAlpha = 0.0f;
|
presentation.cutsceneBlackAlpha = 0.0f;
|
||||||
@ -353,12 +353,12 @@ void DialogueRuntime::presentChoices(const Node& node) {
|
|||||||
presentation.fullText = node.text;
|
presentation.fullText = node.text;
|
||||||
presentation.visibleText = node.text;
|
presentation.visibleText = node.text;
|
||||||
presentation.portraitPath = node.portrait;
|
presentation.portraitPath = node.portrait;
|
||||||
presentation.backgroundPath.clear();
|
|
||||||
presentation.selectedChoice = -1;
|
presentation.selectedChoice = -1;
|
||||||
presentation.revealCompleted = true;
|
presentation.revealCompleted = true;
|
||||||
presentation.showCutsceneSubtitle = false;
|
presentation.showCutsceneSubtitle = false;
|
||||||
presentation.cutsceneSkippable = false;
|
presentation.cutsceneSkippable = false;
|
||||||
presentation.cutsceneCamera = {};
|
|
||||||
presentation.cutsceneImages.clear();
|
presentation.cutsceneImages.clear();
|
||||||
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
||||||
presentation.cutsceneBlackAlpha = 0.0f;
|
presentation.cutsceneBlackAlpha = 0.0f;
|
||||||
|
|||||||
@ -115,19 +115,15 @@ struct PresentationModel {
|
|||||||
std::string fullText;
|
std::string fullText;
|
||||||
std::string visibleText;
|
std::string visibleText;
|
||||||
std::string portraitPath;
|
std::string portraitPath;
|
||||||
std::string backgroundPath;
|
|
||||||
std::vector<PresentedChoice> choices;
|
std::vector<PresentedChoice> choices;
|
||||||
int selectedChoice = -1;
|
int selectedChoice = -1;
|
||||||
bool revealCompleted = true;
|
bool revealCompleted = true;
|
||||||
bool showCutsceneSubtitle = false;
|
bool showCutsceneSubtitle = false;
|
||||||
bool cutsceneSkippable = false;
|
bool cutsceneSkippable = false;
|
||||||
|
|
||||||
ZL::Cutscene::CutsceneCameraBlendState cutsceneCamera;
|
|
||||||
std::vector<ZL::Cutscene::PresentedCutsceneImage> cutsceneImages;
|
std::vector<ZL::Cutscene::PresentedCutsceneImage> cutsceneImages;
|
||||||
float cutsceneGlobalFadeAlpha = 1.0f;
|
float cutsceneGlobalFadeAlpha = 1.0f;
|
||||||
float cutsceneBlackAlpha = 0.0f;
|
float cutsceneBlackAlpha = 0.0f;
|
||||||
int backgroundWidth = 1280;
|
|
||||||
int backgroundHeight = 720;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user