diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2f6e569f..a66e20ed86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ The dates are in European standard format where date is presented as **YYYY-MM-DD**. TombEngine releases are located in this repository (alongside with Tomb Editor): https://github.com/TombEngine/TombEditorReleases +## [Version 2.0] + +### New features +* Added ease-in and ease-out to flyby camera movement when the "Freeze camera" flag is set. +* Added gamma correction setting. + +### Bug fixes +* Fixed incorrect dynamic range for vertex colors, ambient light, dynamic lights and particle effects. +* Fixed flyby camera jitter by converting the spline type to floating-point. + +### Lua API changes +* Added `GlobalVars` namespace for globally persistent variables across game sessions, including the title level. + ## [Version 1.11.1] ### Bug fixes diff --git a/Scripts/Engine/Achievements/Achievements.lua b/Scripts/Engine/Achievements/Achievements.lua new file mode 100644 index 0000000000..28b65113aa --- /dev/null +++ b/Scripts/Engine/Achievements/Achievements.lua @@ -0,0 +1,222 @@ +--- Achievement system entry point. +-- Loads achievement definitions from a setup file, persists unlock state across +-- levels via GameVars, and exposes a simple API for game scripts. +-- +-- Popup notifications (POSTLOOP) and the list viewer (PREFREEZE) are registered +-- automatically when ImportAchievements() is called. +-- +-- Quick-start (add to LevelFuncs.OnStart in every level that uses achievements): +-- +-- local Achievements = require("Engine.Achievements.Achievements") +-- Achievements.ImportAchievements("AchievementSetup") +-- +-- Unlock an achievement at runtime: +-- Achievements.Unlock("treasure_hunter") +-- +-- Open the full list (e.g. from a key bind or trigger): +-- Achievements.ShowAchievementList() +-- +-- @module Engine.Achievements.Achievements + +local Settings = require("Engine.Achievements.Settings") +local Block = require("Engine.Achievements.Block") -- referenced by sub-modules +local Popup = require("Engine.Achievements.Popup") +local List = require("Engine.Achievements.List") + +LevelFuncs.Engine.Achievements = LevelFuncs.Engine.Achievements or {} + +-- Persist unlock state across levels and save/loads. +GameVars.Engine.Achievements = GameVars.Engine.Achievements or { unlocked = {} } + +local Achievements = {} + +-- Module-level definition tables (runtime only; not persisted). +local Defs = {} -- ordered array of definition tables +local DefMap = {} -- id string -> definition table + +-- Guard: prevents duplicate AddCallback registrations within one Lua session. +local _callbacksRegistered = false + +-- ============================================================================ +-- LevelFuncs callbacks +-- Must live in LevelFuncs so that AddCallback / RemoveCallback can reference +-- them by value after a level reload. +-- ============================================================================ + +LevelFuncs.Engine.Achievements.OnLoop = function() + Popup.Tick() + Popup.Draw() +end + +LevelFuncs.Engine.Achievements.OnFreeze = function() + List.Tick() + List.Draw() +end + +-- ============================================================================ +-- Internal helpers +-- ============================================================================ + +local function PlaySound(soundId) + if soundId and soundId > 0 then + TEN.Sound.PlaySound(soundId) + end +end + +-- ============================================================================ +-- Public API +-- ============================================================================ + +--- Load achievement definitions from an external file and start the system. +-- Can be called from LevelFuncs.OnStart and LevelFuncs.OnLoad; duplicate +-- registrations are guarded internally. +-- @tparam string fileName Name of the Lua file (without extension) in the +-- script folder (e.g. "AchievementSetup"). +function Achievements.ImportAchievements(fileName) + if type(fileName) ~= "string" then + TEN.Util.PrintLog("Achievements.ImportAchievements: 'fileName' must be a string.", + TEN.Util.LogLevel.WARNING) + return + end + + local ok, data = pcall(require, fileName) + if not ok or type(data) ~= "table" then + TEN.Util.PrintLog("Achievements.ImportAchievements: could not load '" .. fileName .. "'.", + TEN.Util.LogLevel.WARNING) + return + end + + Defs = {} + DefMap = {} + + for i, entry in ipairs(data) do + if type(entry.id) ~= "string" then + TEN.Util.PrintLog("Achievements: entry " .. i .. " missing string 'id'. Skipped.", + TEN.Util.LogLevel.WARNING) + elseif type(entry.title) ~= "string" then + TEN.Util.PrintLog("Achievements: entry '" .. entry.id .. "' missing string 'title'. Skipped.", + TEN.Util.LogLevel.WARNING) + elseif type(entry.description) ~= "string" then + TEN.Util.PrintLog("Achievements: entry '" .. entry.id .. "' missing string 'description'. Skipped.", + TEN.Util.LogLevel.WARNING) + elseif type(entry.spriteId) ~= "number" then + TEN.Util.PrintLog("Achievements: entry '" .. entry.id .. "' missing number 'spriteId'. Skipped.", + TEN.Util.LogLevel.WARNING) + else + local def = { + id = entry.id, + title = entry.title, + description = entry.description, + spriteId = entry.spriteId, + hidden = entry.hidden == true, + } + Defs[#Defs + 1] = def + DefMap[entry.id] = def + end + end + + -- Ensure GameVars structure is valid (e.g. after ClearAll or first run). + GameVars.Engine.Achievements = GameVars.Engine.Achievements or {} + GameVars.Engine.Achievements.unlocked = GameVars.Engine.Achievements.unlocked or {} + + -- Inject runtime tables into sub-modules. + Popup.Init(DefMap) + List.Init(Defs) + + Achievements.Status(true) + + TEN.Util.PrintLog("Achievements: loaded " .. #Defs .. " definition(s) from '" .. fileName .. "'.", + TEN.Util.LogLevel.INFO) +end + +--- Enable or disable the achievement callbacks. +-- ImportAchievements() calls this automatically with value = true. +-- @tparam bool value True to activate, false to deactivate. +function Achievements.Status(value) + if value then + if not _callbacksRegistered then + TEN.Logic.AddCallback(TEN.Logic.CallbackPoint.POSTLOOP, + LevelFuncs.Engine.Achievements.OnLoop) + TEN.Logic.AddCallback(TEN.Logic.CallbackPoint.PREFREEZE, + LevelFuncs.Engine.Achievements.OnFreeze) + _callbacksRegistered = true + end + else + TEN.Logic.RemoveCallback(TEN.Logic.CallbackPoint.POSTLOOP, + LevelFuncs.Engine.Achievements.OnLoop) + TEN.Logic.RemoveCallback(TEN.Logic.CallbackPoint.PREFREEZE, + LevelFuncs.Engine.Achievements.OnFreeze) + _callbacksRegistered = false + end +end + +--- Unlock an achievement. +-- Has no effect if the achievement is already unlocked or the ID is unknown. +-- Enqueues a slide-in popup notification automatically. +-- @tparam string id Achievement ID as defined in the setup file. +function Achievements.Unlock(id) + if not DefMap[id] then + TEN.Util.PrintLog("Achievements.Unlock: unknown id '" .. tostring(id) .. "'.", + TEN.Util.LogLevel.WARNING) + return + end + + if GameVars.Engine.Achievements.unlocked[id] then return end + + GameVars.Engine.Achievements.unlocked[id] = true + Popup.Enqueue(id) + + TEN.Util.PrintLog("Achievements: unlocked '" .. id .. "'.", TEN.Util.LogLevel.INFO) +end + +--- Check whether a specific achievement is unlocked. +-- @tparam string id Achievement ID. +-- @treturn bool True if unlocked. +function Achievements.IsUnlocked(id) + return GameVars.Engine.Achievements.unlocked[id] == true +end + +--- Returns true if every loaded achievement has been unlocked. +-- Returns false (not true) when no definitions have been loaded yet. +-- @treturn bool +function Achievements.IsAllUnlocked() + if #Defs == 0 then return false end + for _, def in ipairs(Defs) do + if not GameVars.Engine.Achievements.unlocked[def.id] then + return false + end + end + return true +end + +--- Returns ratio of unlocked achievements. +-- Returns false (not true) when no definitions have been loaded yet. +-- @treturn number|bool Ratio of unlocked achievements (0.0 to 1.0) or false if no definitions. +function Achievements.GetUnlockRatio() + if #Defs == 0 then return false end + local unlockedCount = 0 + for _, def in ipairs(Defs) do + if GameVars.Engine.Achievements.unlocked[def.id] then + unlockedCount = unlockedCount + 1 + end + end + local total = (#Defs > 0) and #Defs or 1 + return unlockedCount / total +end + +--- Clear all unlock state and discard any pending popup notifications. +-- Unlocked achievements will appear locked again the next time the list opens. +function Achievements.ClearAll() + GameVars.Engine.Achievements.unlocked = {} + Popup.ClearQueue() + TEN.Util.PrintLog("Achievements: all achievements cleared.", TEN.Util.LogLevel.INFO) +end + +--- Open the full-screen achievement list (enters FULL freeze mode). +-- The list stays open until the player presses the exit action defined in +-- Settings.List.exitAction (default: Inventory key). +function Achievements.ShowAchievementList() + List.Open() +end + +return Achievements diff --git a/Scripts/Engine/Achievements/Block.lua b/Scripts/Engine/Achievements/Block.lua new file mode 100644 index 0000000000..4711b9910e --- /dev/null +++ b/Scripts/Engine/Achievements/Block.lua @@ -0,0 +1,79 @@ +--- Shared achievement block renderer. +-- Draws a single achievement block (background panel, icon, title, description) +-- at a given center position in screen-percent coordinates. +-- Used by both the popup notification and the list viewer. +-- @module Engine.Achievements.Block +-- @local + +local Settings = require("Engine.Achievements.Settings") + +local Block = {} + +-- ============================================================================ +-- Internal helpers +-- ============================================================================ + +local function DrawSprite(objectId, spriteId, pos, rot, scale, color, layer, alignMode, scaleMode, blendMode) + local sprite = TEN.View.DisplaySprite(objectId, spriteId, pos, rot, scale, color) + sprite:Draw(layer, alignMode, scaleMode, blendMode) +end + +local function DrawText(text, offsetX, offsetY, baseX, baseY, scale, color, options) + local px = TEN.Vec2(TEN.Util.PercentToScreen(baseX + offsetX, baseY + offsetY)) + local str = TEN.Strings.DisplayString(text, px, scale, color, false, options) + TEN.Strings.ShowString(str, 1 / 30) +end + +local function ClampAlpha(a) + return math.floor(math.max(0, math.min(255, a))) +end + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Draw an achievement block centered at (posX, posY) in screen percent. +-- @tparam table def Achievement definition {id, title, description, spriteId, hidden}. +-- @tparam bool isUnlocked Whether the achievement has been unlocked. +-- @tparam number posX Horizontal center of the block in screen percent (0-100). +-- @tparam number posY Vertical center of the block in screen percent (0-100). +-- @tparam number alpha Opacity 0-255 for the entire block. +function Block.Draw(def, isUnlocked, posX, posY, alpha) + local B = Settings.Block + local IC = Settings.Icons + local a = ClampAlpha(alpha) + + -- isLocked: any achievement not yet unlocked → use locked icon + grey title colour + -- isHiddenLocked: hidden AND not unlocked → substitute title text, suppress description + local isLocked = not isUnlocked + local isHiddenLocked = def.hidden and isLocked + + -- Background panel (Vec2 created here from plain numbers) + local bgColor = TEN.Color(B.bgColor.r, B.bgColor.g, B.bgColor.b, B.bgColor.a * a / 255) + DrawSprite(B.bgObjectId, B.bgSpriteId, TEN.Vec2(posX, posY), 0, B.bgSize, bgColor, + 0, B.bgAlignMode, B.bgScaleMode, B.bgBlendMode) + + -- Icon: locked sprite for ANY locked achievement, real sprite when unlocked + local iconSpriteId = isLocked and IC.lockedSpriteId or def.spriteId + local iconPos = TEN.Vec2(posX + B.iconOffset.x, posY + B.iconOffset.y) + local iconColor = TEN.Color(B.iconColor.r, B.iconColor.g, B.iconColor.b, a) + DrawSprite(IC.objectId, iconSpriteId, iconPos, 0, B.iconSize, iconColor, + 1, B.iconAlignMode, B.iconScaleMode, B.iconBlendMode) + + -- Title text: "Locked Achievement" for hidden-locked; real title otherwise + -- Title colour: grey (lockedTitleColor) for any locked achievement; gold when unlocked + local titleText = isHiddenLocked and B.lockedTitle or def.title + local tc = isLocked and B.lockedTitleColor or B.titleColor + local titleColor = TEN.Color(tc.r, tc.g, tc.b, a) + DrawText(titleText, B.titleOffset.x, B.titleOffset.y, posX, posY, + B.titleScale, titleColor, B.titleOptions) + + -- Description: only shown for unlocked achievements + if not isLocked then + local descColor = TEN.Color(B.descColor.r, B.descColor.g, B.descColor.b, a) + DrawText(def.description, B.descOffset.x, B.descOffset.y, posX, posY, + B.descScale, descColor, B.descOptions) + end +end + +return Block diff --git a/Scripts/Engine/Achievements/Input.lua b/Scripts/Engine/Achievements/Input.lua new file mode 100644 index 0000000000..dfa68e9bfa --- /dev/null +++ b/Scripts/Engine/Achievements/Input.lua @@ -0,0 +1,49 @@ +--- Scroll input handler with hold-time acceleration for the achievement list. +-- Call Input.GetScrollDelta() once per frame inside the list tick. +-- Positive delta = scroll down (toward later entries). +-- Negative delta = scroll up (toward earlier entries). +-- @module Engine.Achievements.Input +-- @local + +local Settings = require("Engine.Achievements.Settings") + +local Input = {} + +local holdFramesFwd = 0 +local holdFramesBack = 0 + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Returns the scroll delta (screen %) for this frame and updates hold counters. +-- @treturn number Delta to add to the current scrollOffset. +function Input.GetScrollDelta() + local L = Settings.List + + if TEN.Input.IsKeyHeld(TEN.Input.ActionID.FORWARD) then + holdFramesFwd = holdFramesFwd + 1 + holdFramesBack = 0 + local speed = math.min(L.scrollSpeed + holdFramesFwd * L.scrollAccel, L.maxScrollSpeed) + return -speed -- FORWARD / up = negative (scroll toward top) + + elseif TEN.Input.IsKeyHeld(TEN.Input.ActionID.BACK) then + holdFramesBack = holdFramesBack + 1 + holdFramesFwd = 0 + local speed = math.min(L.scrollSpeed + holdFramesBack * L.scrollAccel, L.maxScrollSpeed) + return speed -- BACK / down = positive (scroll toward bottom) + + else + holdFramesFwd = 0 + holdFramesBack = 0 + return 0 + end +end + +--- Reset all hold counters. Call this when the list closes. +function Input.Reset() + holdFramesFwd = 0 + holdFramesBack = 0 +end + +return Input diff --git a/Scripts/Engine/Achievements/List.lua b/Scripts/Engine/Achievements/List.lua new file mode 100644 index 0000000000..643a7d3bdf --- /dev/null +++ b/Scripts/Engine/Achievements/List.lua @@ -0,0 +1,230 @@ +--- Achievement list viewer. +-- Opens in FULL freeze mode and renders all achievements as scrollable blocks. +-- A progress bar and a percentage label are drawn at the top of the screen. +-- Hidden achievements that are still locked are collected into a single summary +-- line at the bottom of the list rather than shown individually. +-- @module Engine.Achievements.List +-- @local + +local Settings = require("Engine.Achievements.Settings") +local Block = require("Engine.Achievements.Block") +local InputModule = require("Engine.Achievements.Input") + +local List = {} + +-- ============================================================================ +-- State +-- ============================================================================ + +local Defs = nil -- injected via List.Init() +local visible = false +local displayList = {} -- built each time the list opens +local scrollOffset = 0 -- current scroll position in screen percent +local maxScroll = 0 -- maximum scroll position + +-- Arrow alpha state (smoothly fade in/out). +local arrowUpAlpha = 0 +local arrowDownAlpha = 0 + +-- ============================================================================ +-- Internal helpers +-- ============================================================================ + +local function PlaySound(soundId) + if soundId and soundId > 0 then + + if not TEN.Sound.IsSoundPlaying(soundId) then + TEN.Sound.PlaySound(soundId) + end + end +end + +local function CountUnlocked() + local count = 0 + for _, def in ipairs(Defs) do + if GameVars.Engine.Achievements.unlocked[def.id] then + count = count + 1 + end + end + return count +end + +local function BuildDisplayList() + displayList = {} + local hiddenLockedCount = 0 + + for _, def in ipairs(Defs) do + local isUnlocked = GameVars.Engine.Achievements.unlocked[def.id] == true + if def.hidden and not isUnlocked then + hiddenLockedCount = hiddenLockedCount + 1 + else + displayList[#displayList + 1] = { def = def, isUnlocked = isUnlocked } + end + end + + -- Append a summary entry for still-locked hidden achievements. + if hiddenLockedCount > 0 then + displayList[#displayList + 1] = { isSummary = true, count = hiddenLockedCount } + end +end + +local function StepAlpha(current, target, speed) + if current < target then + return math.min(current + speed, target) + else + return math.max(current - speed, target) + end +end + +local function DrawArrow(rot, x, y, alpha) + if alpha <= 0 then return end + local L = Settings.List + local color = TEN.Color(L.arrowColor.r, L.arrowColor.g, L.arrowColor.b, math.floor(alpha)) + local sprite = TEN.View.DisplaySprite(L.arrowObjectId, L.arrowSpriteId, + TEN.Vec2(x, y), rot, L.arrowSize, color) + sprite:Draw(2, L.arrowAlignMode, L.arrowScaleMode, L.arrowBlendMode) +end + +local function DrawProgressBar() + local total = (#Defs > 0) and #Defs or 1 + local unlocked = CountUnlocked() + local progress = unlocked / total -- 0.0 to 1.0 + local pct = math.floor(progress * 100) + + local PB = Settings.ProgressBar + + -- Background sprite + local bgSprite = TEN.View.DisplaySprite(PB.bgObjectId, PB.bgSpriteId, + PB.bgPos, 0, PB.bgSize, PB.bgColor) + bgSprite:Draw(0, PB.bgAlignMode, PB.bgScaleMode, PB.bgBlendMode) + + -- Fill sprite (width scaled by progress) + if progress > 0 then + local fillSize = TEN.Vec2(PB.fillMaxSize.x * progress, PB.fillMaxSize.y) + local fillSprite = TEN.View.DisplaySprite(PB.fillObjectId, PB.fillSpriteId, + PB.fillPos, 0, fillSize, PB.fillColor) + fillSprite:Draw(1, PB.fillAlignMode, PB.fillScaleMode, PB.fillBlendMode) + end + + -- Label text: "3 / 10 (30%)" + local labelText = unlocked .. " / " .. total .. " (" .. pct .. "%)" + local px = TEN.Vec2(TEN.Util.PercentToScreen(PB.labelPos.x, PB.labelPos.y)) + local str = TEN.Strings.DisplayString(labelText, px, + PB.labelScale, PB.labelColor, + false, PB.labelOptions) + TEN.Strings.ShowString(str, 1 / 30) +end + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Inject the definitions array. Called by Achievements.lua after loading the +-- setup file. +-- @tparam table defs Ordered array of achievement definition tables. +function List.Init(defs) + Defs = defs +end + +--- Returns true while the list is open (game is frozen). +-- @treturn bool +function List.IsVisible() + return visible +end + +--- Open the achievement list. Enters FULL freeze mode. +function List.Open() + if visible then return end + if not Defs then return end + + BuildDisplayList() + scrollOffset = 0 + + -- Calculate maximum scroll so the last row is fully visible. + local L = Settings.List + local totalRows = math.ceil(#displayList / 2) + local visibleCount = math.max(1, math.floor((100 - L.startY) / L.blockSpacing)) + maxScroll = math.max(0, (totalRows - visibleCount) * L.blockSpacing) + + InputModule.Reset() + TEN.Input.ClearAllKeys() + TEN.Flow.SetFreezeMode(TEN.Flow.FreezeMode.FULL) + PlaySound(Settings.SoundMap.openList) + visible = true +end + +--- Close the list. Exits freeze mode. +function List.Close() + if not visible then return end + + InputModule.Reset() + arrowUpAlpha = 0 + arrowDownAlpha = 0 + TEN.Input.ClearAllKeys() + PlaySound(Settings.SoundMap.closeList) + visible = false + TEN.Flow.SetFreezeMode(TEN.Flow.FreezeMode.NONE) +end + +--- Update scroll offset and handle exit input. Intended for the PREFREEZE callback. +function List.Tick() + if not visible then return end + + -- Exit + if TEN.Input.IsKeyHit(Settings.List.exitAction) then + List.Close() + return + end + + -- Scroll with acceleration; only play sound when actually moving. + local delta = InputModule.GetScrollDelta() + local prevOffset = scrollOffset + scrollOffset = math.max(0, math.min(maxScroll, scrollOffset + delta)) + if scrollOffset ~= prevOffset then + PlaySound(Settings.SoundMap.scroll) + end + + -- Update arrow alphas. + local L = Settings.List + arrowUpAlpha = StepAlpha(arrowUpAlpha, scrollOffset > 0 and 255 or 0, L.arrowFadeSpeed) + arrowDownAlpha = StepAlpha(arrowDownAlpha, scrollOffset < maxScroll and 255 or 0, L.arrowFadeSpeed) +end + +--- Draw all achievement blocks and the progress bar. Intended for the PREFREEZE callback. +function List.Draw() + if not visible then return end + + local L = Settings.List + + DrawProgressBar() + + -- Scroll arrows (left side of screen, up arrow rotated 180° vs down arrow) + DrawArrow(180, L.arrowUpX, L.arrowUpY, arrowUpAlpha) + DrawArrow(0, L.arrowUpX, L.arrowDownY, arrowDownAlpha) + + for i, entry in ipairs(displayList) do + -- Two-column layout: odd entries go left, even entries go right. + -- Both entries in the same pair share the same row Y. + local row = math.ceil(i / 2) + local col = ((i - 1) % 2) -- 0 = left, 1 = right + local blockX = (col == 0) and L.col1X or L.col2X + local blockY = L.startY + (row - 1) * L.blockSpacing - scrollOffset + + -- Cull rows that are entirely off-screen. + if blockY >= -8 and blockY <= 108 then + if entry.isSummary then + -- Summary spans the full width; centre it. + local text = string.gsub(L.hiddenCountText, "{n}", tostring(entry.count)) + local px = TEN.Vec2(TEN.Util.PercentToScreen((L.col1X + L.col2X) / 2, blockY)) + local str = TEN.Strings.DisplayString(text, px, + L.hiddenTextScale, L.hiddenTextColor, + false, L.hiddenTextOptions) + TEN.Strings.ShowString(str, 1 / 30) + else + Block.Draw(entry.def, entry.isUnlocked, blockX, blockY, 255) + end + end + end +end + +return List diff --git a/Scripts/Engine/Achievements/Popup.lua b/Scripts/Engine/Achievements/Popup.lua new file mode 100644 index 0000000000..f4225d382a --- /dev/null +++ b/Scripts/Engine/Achievements/Popup.lua @@ -0,0 +1,152 @@ +--- Popup notification system for achievements. +-- Maintains a FIFO queue of achievement IDs and animates each one as a block +-- that slides up from below the screen, holds, then slides back down. +-- The popup automatically pauses when the achievement list viewer is open +-- (because POSTLOOP callbacks do not fire during FULL freeze mode). +-- @module Engine.Achievements.Popup +-- @local + +local Settings = require("Engine.Achievements.Settings") +local Block = require("Engine.Achievements.Block") + +local Popup = {} + +-- ============================================================================ +-- State +-- ============================================================================ + +-- Injected via Popup.Init() after definitions are loaded. +local DefMap = nil + +local Queue = {} -- FIFO queue of achievement IDs +local state = "IDLE" -- "IDLE" | "SLIDING_IN" | "HOLDING" | "SLIDING_OUT" +local currentDef = nil -- definition table for the currently displayed popup +local posY = 0 -- current Y position in screen percent +local alpha = 0 -- current alpha 0-255 (float, floored on draw) +local holdTimer = 0 -- elapsed hold time in seconds + +local STATE_IDLE = "IDLE" +local STATE_SLIDING_IN = "SLIDING_IN" +local STATE_HOLDING = "HOLDING" +local STATE_SLIDING_OUT = "SLIDING_OUT" + +-- ============================================================================ +-- Internal +-- ============================================================================ + +local function PlaySound(soundId) + if soundId and soundId > 0 then + TEN.Sound.PlaySound(soundId) + end +end + +local function Lerp(current, target, speed) + return current + (target - current) * speed +end + +local function StartNext() + if #Queue == 0 then + state = STATE_IDLE + currentDef = nil + return + end + + local id = table.remove(Queue, 1) + local def = DefMap and DefMap[id] or nil + + if not def then + -- Unknown ID; skip silently and try the next entry. + StartNext() + return + end + + currentDef = def + posY = Settings.Popup.startPosY + alpha = 0 + holdTimer = 0 + state = STATE_SLIDING_IN + + PlaySound(Settings.Popup.sound) +end + +-- ============================================================================ +-- Public +-- ============================================================================ + +--- Inject definition tables. Called by Achievements.lua after loading the setup file. +-- @tparam table defMap id-to-definition map { [id] = def } +function Popup.Init(defMap) + DefMap = defMap +end + +--- Add an achievement ID to the display queue. +-- If the popup is currently idle the new entry starts immediately. +-- @tparam string id Achievement ID as defined in the setup file. +function Popup.Enqueue(id) + Queue[#Queue + 1] = id + if state == STATE_IDLE then + StartNext() + end +end + +--- Update popup animation. Intended for the POSTLOOP callback. +-- Automatically does nothing when the FULL freeze list is open because +-- POSTLOOP callbacks do not fire during freeze mode. +function Popup.Tick() + if state == STATE_IDLE then return end + + local P = Settings.Popup + + if state == STATE_SLIDING_IN then + posY = Lerp(posY, P.targetPosY, P.slideSpeed) + alpha = Lerp(alpha, 255, P.alphaSpeed) + + if math.abs(posY - P.targetPosY) < 0.15 then + posY = P.targetPosY + alpha = 255 + state = STATE_HOLDING + end + + elseif state == STATE_HOLDING then + holdTimer = holdTimer + 1 / 30 + if holdTimer >= P.holdTime then + state = STATE_SLIDING_OUT + end + + elseif state == STATE_SLIDING_OUT then + posY = Lerp(posY, P.startPosY, P.slideSpeed) + alpha = Lerp(alpha, 0, P.alphaSpeed) + + if math.abs(posY - P.startPosY) < 0.5 then + posY = P.startPosY + alpha = 0 + StartNext() -- advances queue (or goes IDLE) + end + end +end + +--- Draw the current popup block. Intended for the POSTLOOP callback. +function Popup.Draw() + if state == STATE_IDLE then return end + if not currentDef then return end + + local isUnlocked = GameVars.Engine.Achievements.unlocked[currentDef.id] == true + Block.Draw(currentDef, isUnlocked, Settings.Popup.posX, posY, math.floor(alpha)) +end + +--- Returns true while a popup is visible or entries are waiting in the queue. +-- @treturn bool +function Popup.IsActive() + return state ~= STATE_IDLE or #Queue > 0 +end + +--- Discard all queued and in-progress popups (e.g. after ClearAll). +function Popup.ClearQueue() + Queue = {} + state = STATE_IDLE + currentDef = nil + alpha = 0 + posY = Settings.Popup.startPosY +end + +return Popup diff --git a/Scripts/Engine/Achievements/Settings.lua b/Scripts/Engine/Achievements/Settings.lua new file mode 100644 index 0000000000..c607cc40df --- /dev/null +++ b/Scripts/Engine/Achievements/Settings.lua @@ -0,0 +1,181 @@ +--- Settings for the Achievements module. +-- Edit this file to customise the visual appearance and behaviour of achievement +-- blocks, the slide-in popup, the list viewer, the progress bar and sounds. +-- +-- All screen positions and sizes use screen-percent units (0-100). +-- @module Engine.Achievements.Settings +-- @local + +local Settings = {} + +-- ============================================================================ +-- Sounds +-- ============================================================================ +-- Set any value to 0 to disable that sound. + +Settings.SoundMap = +{ + unlock = 114, -- played when Achievements.Unlock() is called + openList = 109, -- played when the list opens + closeList = 109, -- played when the list closes + scroll = 108, -- played on each scroll step (optional; can be noisy) +} + +-- ============================================================================ +-- Achievement Icons +-- ============================================================================ +-- All per-achievement icons and the locked mystery icon come from the +-- ACHIEVEMENT_SPRITES object slot. +-- spriteId == 0 : locked / mystery icon (used for hidden-locked entries) +-- spriteId == 1+ : individual achievement icons, matching 'spriteId' in the +-- setup file (AchievementSetup.lua). + +Settings.Icons = +{ + objectId = TEN.Objects.ObjID.MOTORBOAT_FOAM_SPRITES, + lockedSpriteId = 0, +} + +-- ============================================================================ +-- Achievement Block +-- ============================================================================ +-- The block is the visual unit shared by both the popup and the list. +-- Positions are screen-percent offsets applied to the block's center point. + +Settings.Block = +{ + -- Background panel sprite. + -- Point bgObjectId / bgSpriteId at whichever sprite slot holds your + -- block background graphic (separate from ACHIEVEMENT_SPRITES). + bgObjectId = TEN.Objects.ObjID.MOTORBOAT_FOAM_SPRITES, + bgSpriteId = 5, + bgColor = TEN.Color(128, 128, 128, 128), + bgSize = TEN.Vec2(45, 12), + bgAlignMode = TEN.View.AlignMode.CENTER, + bgScaleMode = TEN.View.ScaleMode.STRETCH, + bgBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Icon (drawn to the left of the text). + iconOffset = TEN.Vec2(-17, 0), + iconSize = TEN.Vec2(10, 10), + iconColor = TEN.Color(255, 255, 255), + iconAlignMode = TEN.View.AlignMode.CENTER, + iconScaleMode = TEN.View.ScaleMode.FIT, + iconBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Title text. + titleOffset = TEN.Vec2(-10, -2), + titleScale = 0.7, + titleColor = TEN.Color(255, 220, 100), -- colour for unlocked achievements + lockedTitleColor = TEN.Color(140, 140, 140), -- grey colour for locked achievements + titleOptions = { TEN.Strings.DisplayStringOption.SHADOW, TEN.Strings.DisplayStringOption.VERTICAL_CENTER }, + + -- Description text (hidden when the achievement is hidden and locked). + descOffset = TEN.Vec2(-10, 2), + descScale = 0.55, + descColor = TEN.Color(200, 200, 200), + descOptions = { TEN.Strings.DisplayStringOption.SHADOW, TEN.Strings.DisplayStringOption.VERTICAL_CENTER }, + + -- Text substituted for the real title on hidden-locked achievements. + lockedTitle = "Locked Achievement", +} + +-- ============================================================================ +-- Popup Notification +-- ============================================================================ +-- The popup block slides up from below the screen at the bottom-right, +-- holds for holdTime seconds, then slides back down. +-- posX / startPosY / targetPosY are in screen percent. + +Settings.Popup = +{ + posX = 75, -- horizontal center of the popup block + startPosY = 115, -- Y when off-screen (below the viewport) + targetPosY = 94, -- Y when fully visible + slideSpeed = 0.12, -- lerp factor per frame (0-1; higher = snappier) + alphaSpeed = 0.20, -- lerp factor per frame for alpha + holdTime = 3.0, -- seconds the popup stays visible before sliding out + sound = Settings.SoundMap.unlock, -- sound ID played on unlock (0 = silence) +} + +-- ============================================================================ +-- Achievement List Viewer +-- ============================================================================ +-- Opened via Achievements.ShowAchievementList(); enters FULL freeze mode. + +Settings.List = +{ + col1X = 27, -- horizontal center of the left column (screen %) + col2X = 73, -- horizontal center of the right column (screen %) + startY = 15, -- Y of the first row center (screen %) + blockSpacing = 15, -- vertical gap between row centers (screen %) + + -- Scroll acceleration: speed starts at scrollSpeed and increases by + -- scrollAccel per frame while the key is held, capped at maxScrollSpeed. + scrollSpeed = 0.5, + scrollAccel = 0.03, + maxScrollSpeed = 3.0, + + -- Action used to close the list. + exitAction = TEN.Input.ActionID.INVENTORY, + + -- Summary line shown at the bottom for still-locked hidden achievements. + -- Use {n} as a placeholder for the count. + hiddenCountText = "{n} Hidden Achievement(s)", + hiddenTextScale = 0.7, + hiddenTextColor = TEN.Color(150, 150, 150), + hiddenTextOptions = { TEN.Strings.DisplayStringOption.SHADOW, + TEN.Strings.DisplayStringOption.CENTER }, + + -- Scroll arrows drawn on the left side of the screen. + -- objectId / spriteId point at whichever sprite slot holds your arrow graphic. + arrowObjectId = TEN.Objects.ObjID.MOTORBOAT_FOAM_SPRITES, + arrowSpriteId = 0, + arrowSize = TEN.Vec2(5, 5), + arrowColor = TEN.Color(255, 255, 255), + arrowAlignMode = TEN.View.AlignMode.CENTER, + arrowScaleMode = TEN.View.ScaleMode.FIT, + arrowBlendMode = TEN.Effects.BlendID.ALPHABLEND, + arrowUpX = 5, -- screen % X for both arrows + arrowUpY = 10, -- screen % Y for the up arrow + arrowDownY = 90, -- screen % Y for the down arrow + arrowFadeSpeed = 20, -- alpha steps per frame (0-255) +} + +-- ============================================================================ +-- Progress Bar +-- ============================================================================ +-- Drawn directly during the freeze frame (PREFREEZE). +-- Background and fill sprites use separate object/sprite references. + +Settings.ProgressBar = +{ + -- Background sprite (full width of the bar). + bgObjectId = TEN.Objects.ObjID.CUSTOM_BAR_GRAPHICS, + bgSpriteId = 0, + bgColor = TEN.Color(60, 60, 60), + bgPos = TEN.Vec2(75, 7), + bgSize = TEN.Vec2(20, 3), + bgAlignMode = TEN.View.AlignMode.CENTER_LEFT, + bgScaleMode = TEN.View.ScaleMode.STRETCH, + bgBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Fill sprite (width scaled by progress fraction 0-1). + fillObjectId = TEN.Objects.ObjID.CUSTOM_BAR_GRAPHICS, + fillSpriteId = 1, + fillColor = TEN.Color(100, 220, 100), + fillPos = TEN.Vec2(75.2, 7), + fillMaxSize = TEN.Vec2(19.6, 2.4), + fillAlignMode = TEN.View.AlignMode.CENTER_LEFT, + fillScaleMode = TEN.View.ScaleMode.STRETCH, + fillBlendMode = TEN.Effects.BlendID.ALPHA_BLEND, + + -- Label drawn above the bar: "3 / 10 (30%)". + labelPos = TEN.Vec2(85, 4), + labelScale = 0.55, + labelColor = TEN.Color(255, 255, 255), + labelOptions = { TEN.Strings.DisplayStringOption.SHADOW, + TEN.Strings.DisplayStringOption.CENTER }, +} + +return Settings diff --git a/Scripts/SystemStrings.lua b/Scripts/SystemStrings.lua index ef1303cfba..794bbca5e7 100644 --- a/Scripts/SystemStrings.lua +++ b/Scripts/SystemStrings.lua @@ -74,6 +74,7 @@ local strings = examine = { "Examine" }, exit_game = { "Exit Game" }, exit_to_title = { "Exit to Title" }, + gamma = { "Gamma Correction" }, general_actions = { "General Actions" }, high = { "High" }, high_framerate = { "High Framerate" }, diff --git a/TombEngine/Game/Hud/DrawItems/DisplayItem.h b/TombEngine/Game/Hud/DrawItems/DisplayItem.h index 12254a66e1..c3f6796697 100644 --- a/TombEngine/Game/Hud/DrawItems/DisplayItem.h +++ b/TombEngine/Game/Hud/DrawItems/DisplayItem.h @@ -1,5 +1,6 @@ #pragma once +#include "Math/Constants.h" #include "Objects/game_object_ids.h" #include "Specific/Structures/BitField.h" @@ -25,7 +26,7 @@ namespace TEN::Hud Vector3 _position = Vector3::Zero; EulerAngles _orientation = EulerAngles::Identity; Vector3 _scale = Vector3::Zero; - Color _color = Vector4::One; + Color _color = NEUTRAL_COLOR; BitField _meshBits = BitField::Default; std::unordered_map _meshOrientations = {}; @@ -36,7 +37,7 @@ namespace TEN::Hud Vector3 _prevPosition = Vector3::Zero; EulerAngles _prevOrientation = EulerAngles::Identity; Vector3 _prevScale = Vector3::Zero; - Color _prevColor = Vector4::One; + Color _prevColor = NEUTRAL_COLOR; std::unordered_map _prevMeshOrientations = {}; public: diff --git a/TombEngine/Game/Hud/DrawItems/DrawItems.cpp b/TombEngine/Game/Hud/DrawItems/DrawItems.cpp index 3a5f12717b..e1dae570fe 100644 --- a/TombEngine/Game/Hud/DrawItems/DrawItems.cpp +++ b/TombEngine/Game/Hud/DrawItems/DrawItems.cpp @@ -135,7 +135,7 @@ namespace TEN::Hud unsigned int DrawItemsController::AddItem(GAME_OBJECT_ID objectID, const Vector3& pos, const EulerAngles& orient, const Vector3& scale, int meshBits) { - // If at capacity, don’t add new item. + // If at capacity, don't add new item. if (_displayItems.size() >= DRAW_ITEM_COUNT_MAX) return 0; @@ -228,7 +228,7 @@ namespace TEN::Hud _cameraPosition = Vector3(0.0f, 0.0f, -BLOCK(1)); _targetPosition = Vector3::Zero; _fov = ANGLE(80.0f); - _ambientLight = Vector4(1.0f, 1.0f, 0.5f, 1.0f); + _ambientLight = AMBIENT_COLOR; } void DrawItemsController::StoreCameraInterpolationData() diff --git a/TombEngine/Game/Hud/DrawItems/DrawItems.h b/TombEngine/Game/Hud/DrawItems/DrawItems.h index 0880bee37f..f9055ec8e0 100644 --- a/TombEngine/Game/Hud/DrawItems/DrawItems.h +++ b/TombEngine/Game/Hud/DrawItems/DrawItems.h @@ -14,11 +14,12 @@ namespace TEN::Hud // Constants static constexpr int DRAW_ITEM_COUNT_MAX = 128; + static constexpr auto AMBIENT_COLOR = Vector4(0.5f, 0.5f, 0.25f, 1.0f); // Fields std::vector _displayItems = {}; - Vector4 _ambientLight = Vector4(1.0f, 1.0f, 0.5f, 1.0f); + Vector4 _ambientLight = AMBIENT_COLOR; float _fov = ANGLE(80.0f); float _prevFov = ANGLE(80.0f); diff --git a/TombEngine/Game/Hud/InteractionHighlighter.cpp b/TombEngine/Game/Hud/InteractionHighlighter.cpp index f57af468d3..caae9f5f61 100644 --- a/TombEngine/Game/Hud/InteractionHighlighter.cpp +++ b/TombEngine/Game/Hud/InteractionHighlighter.cpp @@ -14,8 +14,9 @@ #include "Specific/trutils.h" using namespace TEN::Collision::Los; -using namespace TEN::Math; using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Math; +using namespace TEN::SpotCam; using TEN::Renderer::g_Renderer; namespace TEN::Hud diff --git a/TombEngine/Game/Hud/TargetHighlighter.cpp b/TombEngine/Game/Hud/TargetHighlighter.cpp index 690e6ce2f2..190651f449 100644 --- a/TombEngine/Game/Hud/TargetHighlighter.cpp +++ b/TombEngine/Game/Hud/TargetHighlighter.cpp @@ -14,6 +14,7 @@ using namespace TEN::Effects::DisplaySprite; using namespace TEN::Math; +using namespace TEN::SpotCam; using namespace TEN::Utils; using TEN::Renderer::g_Renderer; diff --git a/TombEngine/Game/Lara/lara_flare.cpp b/TombEngine/Game/Lara/lara_flare.cpp index 42f8478830..c6ed2f1e07 100644 --- a/TombEngine/Game/Lara/lara_flare.cpp +++ b/TombEngine/Game/Lara/lara_flare.cpp @@ -363,7 +363,7 @@ void CreateFlare(ItemInfo& laraItem, GAME_OBJECT_ID objectID, bool isThrown) flareItem.Pose.Orientation.x = 0; flareItem.Pose.Orientation.z = 0; - flareItem.Model.Color = Vector4::One; + flareItem.Model.Color = NEUTRAL_COLOR; if (isThrown) { diff --git a/TombEngine/Game/Lara/lara_one_gun.cpp b/TombEngine/Game/Lara/lara_one_gun.cpp index cc8c2e39d3..d66720cfd3 100644 --- a/TombEngine/Game/Lara/lara_one_gun.cpp +++ b/TombEngine/Game/Lara/lara_one_gun.cpp @@ -623,7 +623,7 @@ bool FireHarpoon(ItemInfo& laraItem, const std::optional& pose) auto& harpoonItem = g_Level.Items[itemNumber]; harpoonItem.ObjectNumber = ID_HARPOON; - harpoonItem.Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + harpoonItem.Model.Color = NEUTRAL_COLOR; if (!ammo.HasInfinite()) ammo--; @@ -729,7 +729,7 @@ bool FireGrenade(ItemInfo& laraItem) auto& grenadeItem = g_Level.Items[itemNumber]; - grenadeItem.Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + grenadeItem.Model.Color = NEUTRAL_COLOR; grenadeItem.ObjectNumber = ID_GRENADE; grenadeItem.RoomNumber = laraItem.RoomNumber; @@ -812,7 +812,7 @@ void GrenadeControl(short itemNumber) { auto& grenadeItem = g_Level.Items[itemNumber]; - grenadeItem.Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + grenadeItem.Model.Color = NEUTRAL_COLOR; // Force grenade to explode if it was activated externally. if ((grenadeItem.Flags & CODE_BITS) == CODE_BITS) @@ -1006,7 +1006,7 @@ void RocketControl(short itemNumber) rocketItem.Pose.Orientation.z += short((rocketItem.Animation.Velocity.z / 4) + 7.0f) * ANGLE(1.0f); } - rocketItem.Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + rocketItem.Model.Color = NEUTRAL_COLOR; // Calculate offset in rocket direction for fire and smoke sparks. auto world = Matrix::CreateTranslation(0, 0, -64) * @@ -1056,7 +1056,7 @@ bool FireCrossbow(ItemInfo& laraItem, const std::optional& pose) auto& boltItem = g_Level.Items[itemNumber]; boltItem.ObjectNumber = ID_CROSSBOW_BOLT; - boltItem.Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + boltItem.Model.Color = NEUTRAL_COLOR; if (!ammo.HasInfinite()) ammo--; @@ -1438,7 +1438,7 @@ bool EmitFromProjectile(ItemInfo& projectile, ProjectileType type) auto& grenadeItem = g_Level.Items[grenadeItemNumber]; - grenadeItem.Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + grenadeItem.Model.Color = NEUTRAL_COLOR; grenadeItem.ObjectNumber = ID_GRENADE; grenadeItem.RoomNumber = projectile.RoomNumber; grenadeItem.Pose.Position = Vector3i( diff --git a/TombEngine/Game/camera.cpp b/TombEngine/Game/camera.cpp index 63358f2e1e..d8520811d8 100644 --- a/TombEngine/Game/camera.cpp +++ b/TombEngine/Game/camera.cpp @@ -31,6 +31,7 @@ using namespace TEN::Effects::Environment; using namespace TEN::Entities::Generic; using namespace TEN::Input; using namespace TEN::Math; +using namespace TEN::SpotCam; using TEN::Renderer::g_Renderer; constexpr auto PARTICLE_FADE_THRESHOLD = BLOCK(14); @@ -1591,7 +1592,7 @@ void UpdateCamera() if (UseSpotCam) { // Draw flyby cameras. - CalculateSpotCameras(); + CalculateSpotCam(); } else { diff --git a/TombEngine/Game/control/control.cpp b/TombEngine/Game/control/control.cpp index c1a669a766..341b6b1246 100644 --- a/TombEngine/Game/control/control.cpp +++ b/TombEngine/Game/control/control.cpp @@ -74,6 +74,7 @@ using namespace TEN::Effects::Drip; using namespace TEN::Effects::Electricity; using namespace TEN::Effects::Environment; using namespace TEN::Effects::Explosion; +using namespace TEN::Effects::Fireflies; using namespace TEN::Effects::Footprint; using namespace TEN::Effects::Hair; using namespace TEN::Effects::Ripple; @@ -82,6 +83,7 @@ using namespace TEN::Effects::Spark; using namespace TEN::Effects::Splash; using namespace TEN::Effects::Streamer; using namespace TEN::Entities::Creatures::TR3; +using namespace TEN::Entities::Effects; using namespace TEN::Entities::Generic; using namespace TEN::Entities::Switches; using namespace TEN::Entities::Traps; @@ -92,9 +94,7 @@ using namespace TEN::Hud; using namespace TEN::Input; using namespace TEN::Math; using namespace TEN::Renderer; -using namespace TEN::Entities::Creatures::TR3; -using namespace TEN::Entities::Effects; -using namespace TEN::Effects::Fireflies; +using namespace TEN::SpotCam; using namespace TEN::Video; constexpr auto DEATH_NO_INPUT_TIMEOUT = 10 * FPS; @@ -581,7 +581,7 @@ void CleanUp() g_Renderer.ClearScene(); g_Renderer.SetPostProcessMode(PostProcessMode::None); g_Renderer.SetPostProcessStrength(1.0f); - g_Renderer.SetPostProcessTint(Vector3::One); + g_Renderer.SetPostProcessTint((Vector3)NEUTRAL_COLOR); // Reset Itemcamera ClearObjCamera(); @@ -639,6 +639,9 @@ void DeInitializeScripting(int levelIndex, GameStatus reason) // If level index is 0, it means we are in a title level and game variables should be cleared. if (levelIndex == 0) g_GameScript->ResetScripts(true); + + // Always save global variables on any script deinit event. + SaveGame::SaveGlobalVars(); } void InitializeOrLoadGame(bool loadGame) diff --git a/TombEngine/Game/control/trigger.cpp b/TombEngine/Game/control/trigger.cpp index b3bcff71c1..24b6cfcf01 100644 --- a/TombEngine/Game/control/trigger.cpp +++ b/TombEngine/Game/control/trigger.cpp @@ -25,11 +25,13 @@ #include "Objects/TR3/Vehicles/kayak.h" #include "Sound/sound.h" #include "Specific/clock.h" +#include "Specific/level.h" #include "Specific/trutils.h" using namespace TEN::Collision::Point; using namespace TEN::Effects::Items; using namespace TEN::Entities::Switches; +using namespace TEN::SpotCam; using namespace TEN::Utils; int TriggerTimer; @@ -431,7 +433,6 @@ void TestTriggers(int x, int y, int z, FloorInfo* floor, Activator activator, bo int flip = NO_VALUE; int newEffect = NO_VALUE; int keyResult = 0; - int spotCamIndex = 0; auto data = GetTriggerIndex(floor, x, y, z); @@ -696,28 +697,24 @@ void TestTriggers(int x, int y, int z, FloorInfo* floor, Activator activator, bo if (triggerType == TRIGGER_TYPES::ANTIPAD || triggerType == TRIGGER_TYPES::ANTITRIGGER || triggerType == TRIGGER_TYPES::HEAVYANTITRIGGER) + { UseSpotCam = false; - else + } + else if (HasSpotCamSequence(value)) { - spotCamIndex = 0; - if (SpotCamRemap[value] != 0) - { - for (int i = 0; i < SpotCamRemap[value]; i++) - { - spotCamIndex += CameraCnt[i]; - } - } + int spotCamIndex = GetSequenceFirstCameraIndex(value); - if (!(SpotCam[spotCamIndex].flags & SCF_CAMERA_ONE_SHOT)) + if (spotCamIndex != NO_VALUE && !(g_Level.SpotCams[spotCamIndex].Flags & SCF_CAMERA_ONE_SHOT)) { if (trigger & ONESHOT) - SpotCam[spotCamIndex].flags |= SCF_CAMERA_ONE_SHOT; + g_Level.SpotCams[spotCamIndex].Flags |= SCF_CAMERA_ONE_SHOT; if (!UseSpotCam || CurrentLevel == 0) { UseSpotCam = true; if (LastSpotCamSequence != value) TrackCameraInit = false; + InitializeSpotCam(value); } } diff --git a/TombEngine/Game/effects/DisplaySprite.cpp b/TombEngine/Game/effects/DisplaySprite.cpp index dd3713d6b3..0b195cf683 100644 --- a/TombEngine/Game/effects/DisplaySprite.cpp +++ b/TombEngine/Game/effects/DisplaySprite.cpp @@ -13,6 +13,30 @@ namespace TEN::Effects::DisplaySprite { std::vector DisplaySprites = {}; + static bool g_hasActiveDisplayScissor = false; + static TEN::Renderer::Structures::RendererRectangle g_activeDisplayScissorRect = {}; + + void SetActiveDisplayScissor(const TEN::Renderer::Structures::RendererRectangle& rect) + { + g_hasActiveDisplayScissor = true; + g_activeDisplayScissorRect = rect; + } + + void ClearActiveDisplayScissor() + { + g_hasActiveDisplayScissor = false; + } + + bool HasActiveDisplayScissor() + { + return g_hasActiveDisplayScissor; + } + + const TEN::Renderer::Structures::RendererRectangle& GetActiveDisplayScissor() + { + return g_activeDisplayScissorRect; + } + void AddDisplaySprite(GAME_OBJECT_ID objectID, int spriteID, const Vector2& pos, short orient, const Vector2& scale, const Vector4& color, int priority, DisplaySpriteAlignMode alignMode, DisplaySpriteScaleMode scaleMode, BlendMode blendMode, DisplaySpritePhase source) @@ -29,6 +53,8 @@ namespace TEN::Effects::DisplaySprite displaySprite.ScaleMode = scaleMode; displaySprite.BlendMode = blendMode; displaySprite.Source = source; + displaySprite.HasScissor = g_hasActiveDisplayScissor; + displaySprite.ScissorRect = g_activeDisplayScissorRect; DisplaySprites.push_back(displaySprite); } diff --git a/TombEngine/Game/effects/DisplaySprite.h b/TombEngine/Game/effects/DisplaySprite.h index cb325ea63b..103310fe52 100644 --- a/TombEngine/Game/effects/DisplaySprite.h +++ b/TombEngine/Game/effects/DisplaySprite.h @@ -2,6 +2,7 @@ #include "Math/Math.h" #include "Objects/game_object_ids.h" #include "Renderer/RendererEnums.h" +#include "Renderer/Structures/RendererRectangle.h" namespace TEN::Effects::DisplaySprite { @@ -47,6 +48,9 @@ namespace TEN::Effects::DisplaySprite BlendMode BlendMode = BlendMode::AlphaBlend; DisplaySpritePhase Source = DisplaySpritePhase::Control; + + bool HasScissor = false; + TEN::Renderer::Structures::RendererRectangle ScissorRect = {}; }; // Result of display sprite layout calculation. @@ -65,6 +69,11 @@ namespace TEN::Effects::DisplaySprite void ClearAllDisplaySprites(); void ClearDrawPhaseDisplaySprites(); + void SetActiveDisplayScissor(const TEN::Renderer::Structures::RendererRectangle& rect); + void ClearActiveDisplayScissor(); + bool HasActiveDisplayScissor(); + const TEN::Renderer::Structures::RendererRectangle& GetActiveDisplayScissor(); + // Calculate complete layout data for a display sprite. // // NOTE: This function is defined inline in the header for performance reasons. diff --git a/TombEngine/Game/effects/Light.cpp b/TombEngine/Game/effects/Light.cpp index ab7a00d60a..bb698f96a6 100644 --- a/TombEngine/Game/effects/Light.cpp +++ b/TombEngine/Game/effects/Light.cpp @@ -17,9 +17,9 @@ namespace TEN::Effects::Light g_Renderer.AddDynamicSpotLight(pos, dir, radius, falloff, dist, color, castShadows, hash); } - void SpawnDynamicLight(int x, int y, int z, short falloff, byte r, byte g, byte b) + void SpawnDynamicLight(int x, int y, int z, short falloff, unsigned char r, unsigned char g, unsigned char b) { - g_Renderer.AddDynamicPointLight(Vector3(x, y, z), float(falloff * UCHAR_MAX), Color(r / (float)CHAR_MAX, g / (float)CHAR_MAX, b / (float)CHAR_MAX), false); + g_Renderer.AddDynamicPointLight(Vector3(x, y, z), float(falloff * UCHAR_MAX), Color(r / (float)UCHAR_MAX, g / (float)UCHAR_MAX, b / (float)UCHAR_MAX), false); } void SpawnDynamicFogBulb(const Vector3& pos, short radius, short density, const Color& color, int hash) diff --git a/TombEngine/Game/effects/Light.h b/TombEngine/Game/effects/Light.h index c716b7b7ce..5bafd1d4f9 100644 --- a/TombEngine/Game/effects/Light.h +++ b/TombEngine/Game/effects/Light.h @@ -7,5 +7,5 @@ namespace TEN::Effects::Light void SpawnDynamicFogBulb(const Vector3& pos, short radius, short density, const Color& color, int hash = 0); // DEPRECATED!!! Use SpawnDynamicPointLight() instead and phase out this legacy function. - void SpawnDynamicLight(int x, int y, int z, short falloff, byte r, byte g, byte b); + void SpawnDynamicLight(int x, int y, int z, short falloff, unsigned char r, unsigned char g, unsigned char b); } diff --git a/TombEngine/Game/effects/effects.cpp b/TombEngine/Game/effects/effects.cpp index 0cda025f15..957fee0d52 100644 --- a/TombEngine/Game/effects/effects.cpp +++ b/TombEngine/Game/effects/effects.cpp @@ -624,10 +624,9 @@ void TriggerGlow(const GameVector& pos, const Vector3& color, int scale) part.xVel = part.yVel = part.zVel = 0; part.gravity = part.friction = part.maxYvel = 0; - // Normalize color from Monty's range - part.sR = part.dR = std::clamp(color.x / 2.0f, 0.0f, 1.0f) * UCHAR_MAX; - part.sG = part.dG = std::clamp(color.y / 2.0f, 0.0f, 1.0f) * UCHAR_MAX; - part.sB = part.dB = std::clamp(color.z / 2.0f, 0.0f, 1.0f) * UCHAR_MAX; + part.sR = part.dR = std::clamp(color.x, 0.0f, 1.0f) * UCHAR_MAX; + part.sG = part.dG = std::clamp(color.y, 0.0f, 1.0f) * UCHAR_MAX; + part.sB = part.dB = std::clamp(color.z, 0.0f, 1.0f) * UCHAR_MAX; part.life = part.sLife = 2; part.colFadeSpeed = 1; @@ -1097,7 +1096,7 @@ void TriggerSuperJetFlame(ItemInfo* item, int yvel, int deadly) if (size < 512) size = 512; - if (item->Model.Color == Vector4::One) + if (item->Model.Color == NEUTRAL_COLOR) { sptr->sR = sptr->sG = (GetRandomControl() & 0x1F) + 48; sptr->sB = (GetRandomControl() & 0x3F) - 64; @@ -1107,8 +1106,8 @@ void TriggerSuperJetFlame(ItemInfo* item, int yvel, int deadly) } else { - auto colorD = item->Model.Color / 2.0f * UCHAR_MAX; - auto luma = Luma((Vector3)item->Model.Color / 2.0f) * 0.85f * UCHAR_MAX; + auto colorD = item->Model.Color * UCHAR_MAX; + auto luma = Luma((Vector3)item->Model.Color) * 0.85f * UCHAR_MAX; auto colorS = Vector3(0.15f * colorD.x + luma, 0.15f * colorD.y + luma, 0.15f * colorD.z + luma); diff --git a/TombEngine/Game/effects/effects.h b/TombEngine/Game/effects/effects.h index 68fa432f93..93e6d1b5fd 100644 --- a/TombEngine/Game/effects/effects.h +++ b/TombEngine/Game/effects/effects.h @@ -21,7 +21,7 @@ constexpr auto MAX_DYNAMICS = 64; constexpr auto MAX_PARTICLES = 8192; constexpr auto MAX_PARTICLE_DYNAMICS = 8; -constexpr auto CREATURE_GUNFLASH_COLOR = Vector4(1.0f, 0.5f, 0.1f, 1.0f); +constexpr auto CREATURE_GUNFLASH_COLOR = Vector4(0.5f, 0.25f, 0.05f, 1.0f); extern int Wibble; diff --git a/TombEngine/Game/effects/spark.cpp b/TombEngine/Game/effects/spark.cpp index 2e04fe489d..e5a75b5988 100644 --- a/TombEngine/Game/effects/spark.cpp +++ b/TombEngine/Game/effects/spark.cpp @@ -74,7 +74,7 @@ namespace TEN::Effects::Spark v += Vector3(GenerateFloat(-64, 64), GenerateFloat(-64, 64), GenerateFloat(-64, 64)); v.Normalize(v); s.velocity = v *GenerateFloat(17,24); - s.sourceColor = Color(1.0f, 1.0f, 1.0f); + s.sourceColor = NEUTRAL_COLOR; s.destinationColor = color; s.active = true; } diff --git a/TombEngine/Game/effects/tomb4fx.cpp b/TombEngine/Game/effects/tomb4fx.cpp index 3f16916677..c5dc2653a6 100644 --- a/TombEngine/Game/effects/tomb4fx.cpp +++ b/TombEngine/Game/effects/tomb4fx.cpp @@ -381,7 +381,7 @@ void UpdateFireProgress() void AddFire(int x, int y, int z, short roomNum, float size, short fade) { - AddFire(Vector3i(x, y, z), roomNum, Vector4::One, size, fade); + AddFire(Vector3i(x, y, z), roomNum, NEUTRAL_COLOR, size, fade); } void AddFire(Vector3i& pos, int roomNumber, Vector4 color, float size, short fade) diff --git a/TombEngine/Game/gui.cpp b/TombEngine/Game/gui.cpp index 2ac0db7fe8..3c30874fe4 100644 --- a/TombEngine/Game/gui.cpp +++ b/TombEngine/Game/gui.cpp @@ -34,6 +34,7 @@ using namespace TEN::Animation; using namespace TEN::Effects::DisplaySprite; using namespace TEN::Input; using namespace TEN::Renderer; +using namespace TEN::SpotCam; using namespace TEN::Utils; using namespace TEN::Video; @@ -489,6 +490,7 @@ namespace TEN::Gui Antialiasing, AmbientOcclusion, HighFramerate, + Gamma, Save, Cancel, @@ -559,6 +561,19 @@ namespace TEN::Gui CurrentSettings.Configuration.EnableHighFramerate = !CurrentSettings.Configuration.EnableHighFramerate; break; + case DisplaySettingsOption::Gamma: + if (CurrentSettings.Configuration.Gamma > GAMMA_MIN) + { + CurrentSettings.Configuration.Gamma -= GAMMA_STEP; + if (CurrentSettings.Configuration.Gamma < GAMMA_MIN) + CurrentSettings.Configuration.Gamma = GAMMA_MIN; + + g_Configuration.Gamma = CurrentSettings.Configuration.Gamma; + g_Renderer.SetGraphicsSettingsChanged(); + SoundEffect(SFX_TR4_MENU_CHOOSE, nullptr, SoundEnvironment::Always); + } + break; + } } @@ -622,6 +637,19 @@ namespace TEN::Gui SoundEffect(SFX_TR4_MENU_CHOOSE, nullptr, SoundEnvironment::Always); CurrentSettings.Configuration.EnableHighFramerate = !CurrentSettings.Configuration.EnableHighFramerate; break; + + case DisplaySettingsOption::Gamma: + if (CurrentSettings.Configuration.Gamma < GAMMA_MAX) + { + CurrentSettings.Configuration.Gamma += GAMMA_STEP; + if (CurrentSettings.Configuration.Gamma > GAMMA_MAX) + CurrentSettings.Configuration.Gamma = GAMMA_MAX; + + g_Configuration.Gamma = CurrentSettings.Configuration.Gamma; + g_Renderer.SetGraphicsSettingsChanged(); + SoundEffect(SFX_TR4_MENU_CHOOSE, nullptr, SoundEnvironment::Always); + } + break; } } @@ -663,6 +691,8 @@ namespace TEN::Gui } else if (SelectedOption == DisplaySettingsOption::Cancel) { + g_Configuration.Gamma = BackupGamma; + g_Renderer.SetGraphicsSettingsChanged(); MenuToDisplay = Menu::Options; SelectedOption = 0; } @@ -901,6 +931,7 @@ namespace TEN::Gui void GuiController::BackupOptions() { CurrentSettings.Configuration = g_Configuration; + BackupGamma = g_Configuration.Gamma; } void GuiController::HandleOptionsInput() diff --git a/TombEngine/Game/gui.h b/TombEngine/Game/gui.h index 74e49b6aac..f559894813 100644 --- a/TombEngine/Game/gui.h +++ b/TombEngine/Game/gui.h @@ -133,6 +133,7 @@ namespace TEN::Gui int SelectedSaveSlot; int TimeInMenu = NO_VALUE; + float BackupGamma = 1.0f; SettingsData CurrentSettings; // Inventory variables diff --git a/TombEngine/Game/items.cpp b/TombEngine/Game/items.cpp index 1e0c6f7681..51575890aa 100644 --- a/TombEngine/Game/items.cpp +++ b/TombEngine/Game/items.cpp @@ -555,7 +555,7 @@ short CreateNewEffect(short roomNumber) room->fxNumber = fxNumber; fx->speed = 0; - fx->color = Vector4::One; + fx->color = NEUTRAL_COLOR; fx->fallspeed = 0; fx->frameNumber = 0; fx->counter = 0; @@ -769,7 +769,7 @@ short SpawnItem(const ItemInfo& item, GAME_OBJECT_ID objectID) newItem.ObjectNumber = objectID; newItem.RoomNumber = item.RoomNumber; newItem.Pose = item.Pose; - newItem.Model.Color = Vector4::One; + newItem.Model.Color = NEUTRAL_COLOR; InitializeItem(itemNumber); diff --git a/TombEngine/Game/missile.cpp b/TombEngine/Game/missile.cpp index dfe90c73bb..59d3a0f9d1 100644 --- a/TombEngine/Game/missile.cpp +++ b/TombEngine/Game/missile.cpp @@ -198,7 +198,7 @@ short ShardGun(int x, int y, int z, short velocity, short yRot, short roomNumber fx.speed = velocity; fx.frameNumber = 0; fx.objectNumber = ID_PROJ_SHARD; - fx.color = Vector4::One; + fx.color = NEUTRAL_COLOR; } return fxNumber; @@ -216,7 +216,7 @@ short BombGun(int x, int y, int z, short velocity, short yRot, short roomNumber) fx.speed = velocity; fx.frameNumber = 0; fx.objectNumber = ID_PROJ_BOMB; - fx.color = Vector4::One; + fx.color = NEUTRAL_COLOR; } return fxNumber; @@ -234,7 +234,7 @@ short HarpoonGun(int x, int y, int z, short velocity, short yRot, short roomNumb fx.speed = velocity; fx.frameNumber = 0; fx.objectNumber = ID_SCUBA_HARPOON; - fx.color = Vector4::One; + fx.color = NEUTRAL_COLOR; } return fxNumber; diff --git a/TombEngine/Game/savegame.cpp b/TombEngine/Game/savegame.cpp index 250a199aab..ad7821fea6 100644 --- a/TombEngine/Game/savegame.cpp +++ b/TombEngine/Game/savegame.cpp @@ -57,14 +57,16 @@ using namespace TEN::Entities::Switches; using namespace TEN::Entities::TR4; using namespace TEN::Gui; using namespace TEN::Renderer; +using namespace TEN::SpotCam; using namespace TEN::Utils; using namespace TEN::Video; namespace Save = TEN::Save; -constexpr auto SAVEGAME_MAX_SLOT = 99; -constexpr auto SAVEGAME_PATH = "Save//"; -constexpr auto SAVEGAME_FILE_MASK = "savegame."; +constexpr auto SAVEGAME_MAX_SLOT = 99; +constexpr auto SAVEGAME_PATH = "Save//"; +constexpr auto SAVEGAME_FILE_MASK = "savegame."; +constexpr auto GLOBAL_VARS_FILENAME = "savegame.global"; GameStats SaveGame::Statistics; SaveGameHeader SaveGame::Infos[SAVEGAME_MAX]; @@ -246,6 +248,202 @@ std::string SaveGame::GetSavegameFilename(int slot) auto vecOffset = vtb.Finish(); \ putDataInVec(UnionType, vecOffset); +static std::vector> SerializeScriptVars(FlatBufferBuilder& fbb, const std::vector& savedVars) +{ + std::vector> varsVec; + for (auto const& s : savedVars) + { + auto putDataInVec = [&varsVec, &fbb](Save::VarUnion type, auto const& offsetVar) + { + Save::UnionTableBuilder ut{ fbb }; + ut.add_u_type(type); + ut.add_u(offsetVar.Union()); + varsVec.push_back(ut.Finish()); + }; + + if (std::holds_alternative(s)) + { + auto strOffset2 = fbb.CreateString(std::get(s)); + Save::stringTableBuilder stb{ fbb }; + stb.add_str(strOffset2); + auto strOffset = stb.Finish(); + + putDataInVec(Save::VarUnion::str, strOffset); + } + else if (std::holds_alternative(s)) + { + Save::doubleTableBuilder dtb{ fbb }; + dtb.add_scalar(std::get(s)); + auto doubleOffset = dtb.Finish(); + + putDataInVec(Save::VarUnion::num, doubleOffset); + } + else if (std::holds_alternative(s)) + { + Save::boolTableBuilder btb{ fbb }; + btb.add_scalar(std::get(s)); + auto boolOffset = btb.Finish(); + + putDataInVec(Save::VarUnion::boolean, boolOffset); + } + else if (std::holds_alternative(s)) + { + std::vector keyValVec; + auto& vec = std::get(s); + for (auto& id : vec) + { + keyValVec.push_back(Save::KeyValPair(id.first, id.second)); + } + + auto vecOffset = fbb.CreateVectorOfStructs(keyValVec); + Save::ScriptTableBuilder stb{ fbb }; + stb.add_keys_vals(vecOffset); + auto scriptTableOffset = stb.Finish(); + + putDataInVec(Save::VarUnion::tab, scriptTableOffset); + } + else if (std::holds_alternative(s)) + { + std::string data = std::get(s).name; + auto strOffset = fbb.CreateString(data); + Save::funcNameTableBuilder ftb{ fbb }; + ftb.add_str(strOffset); + auto funcNameOffset = ftb.Finish(); + + putDataInVec(Save::VarUnion::funcName, funcNameOffset); + } + else + { + switch (SavedVarType(s.index())) + { + case SavedVarType::Vec2: + { + SaveVec(SavedVarType::Vec2, s, Save::vec2TableBuilder, Save::VarUnion::vec2, Save::Vector2, FromVector2); + break; + } + + case SavedVarType::Vec3: + { + SaveVec(SavedVarType::Vec3, s, Save::vec3TableBuilder, Save::VarUnion::vec3, Save::Vector3, FromVector3); + break; + } + + case SavedVarType::Rotation: + { + SaveVec(SavedVarType::Rotation, s, Save::rotationTableBuilder, Save::VarUnion::rotation, Save::Vector3, FromVector3); + break; + } + + case SavedVarType::Time: + { + Save::timeTableBuilder ttb{ fbb }; + ttb.add_scalar(std::get(s)); + auto timeOffset = ttb.Finish(); + + putDataInVec(Save::VarUnion::time, timeOffset); + break; + } + + case SavedVarType::Color: + { + Save::colorTableBuilder ctb{ fbb }; + ctb.add_color(std::get<(int)SavedVarType::Color>(s)); + auto offset = ctb.Finish(); + + putDataInVec(Save::VarUnion::color, offset); + break; + } + } + } + } + + return varsVec; +} + +static std::vector DeserializeScriptVars(const flatbuffers::Vector>& members) +{ + std::vector loadedVars; + + for (const auto& var : members) + { + auto varType = var->u_type(); + switch (varType) + { + case Save::VarUnion::num: + loadedVars.push_back(var->u_as_num()->scalar()); + break; + + case Save::VarUnion::boolean: + loadedVars.push_back(var->u_as_boolean()->scalar()); + break; + + case Save::VarUnion::str: + loadedVars.push_back(var->u_as_str()->str()->str()); + break; + + case Save::VarUnion::tab: + { + auto tab = var->u_as_tab()->keys_vals(); + auto& loadedTab = loadedVars.emplace_back(IndexTable{}); + + for (const auto& pair : *tab) + std::get(loadedTab).push_back(std::make_pair(pair->key(), pair->val())); + + break; + } + + case Save::VarUnion::vec2: + { + auto stored = var->u_as_vec2()->vec(); + SavedVar v; + v.emplace<(int)SavedVarType::Vec2>(ToVector2(stored)); + loadedVars.push_back(v); + break; + } + + case Save::VarUnion::vec3: + { + auto stored = var->u_as_vec3()->vec(); + SavedVar v; + v.emplace<(int)SavedVarType::Vec3>(ToVector3(stored)); + loadedVars.push_back(v); + break; + } + + case Save::VarUnion::rotation: + { + auto stored = var->u_as_rotation()->vec(); + SavedVar v; + v.emplace<(int)SavedVarType::Rotation>(ToVector3(stored)); + loadedVars.push_back(v); + break; + } + + case Save::VarUnion::time: + { + auto stored = var->u_as_time()->scalar(); + SavedVar v; + v.emplace<(int)SavedVarType::Time>(stored); + loadedVars.push_back(v); + break; + } + + case Save::VarUnion::color: + loadedVars.push_back((D3DCOLOR)var->u_as_color()->color()); + break; + + case Save::VarUnion::funcName: + loadedVars.push_back(FuncName{ var->u_as_funcName()->str()->str() }); + break; + + default: + break; + } + } + + return loadedVars; +} + void SaveGame::Init(const std::string& gameDirectory) { FullSaveDirectory = gameDirectory + SAVEGAME_PATH; @@ -1110,10 +1308,10 @@ const std::vector SaveGame::Build() // Flyby cameras std::vector> flybyCameras; - for (int i = 0; i < (int)SpotCam.size(); i++) + for (int i = 0; i < (int)g_Level.SpotCams.size(); i++) { Save::FlyByCameraBuilder flyby{ fbb }; - flyby.add_flags(SpotCam[i].flags); + flyby.add_flags(g_Level.SpotCams[i].Flags); flybyCameras.push_back(flyby.Finish()); } auto flybyCamerasOffset = fbb.CreateVector(flybyCameras); @@ -1479,112 +1677,7 @@ const std::vector SaveGame::Build() g_GameScript->GetVariables(savedVars); - std::vector> varsVec; - for (auto const& s : savedVars) - { - auto putDataInVec = [&varsVec, &fbb](Save::VarUnion type, auto const & offsetVar) - { - Save::UnionTableBuilder ut{ fbb }; - ut.add_u_type(type); - ut.add_u(offsetVar.Union()); - varsVec.push_back(ut.Finish()); - }; - - if (std::holds_alternative(s)) - { - auto strOffset2 = fbb.CreateString(std::get(s)); - Save::stringTableBuilder stb{ fbb }; - stb.add_str(strOffset2); - auto strOffset = stb.Finish(); - - putDataInVec(Save::VarUnion::str, strOffset); - } - else if (std::holds_alternative(s)) - { - Save::doubleTableBuilder dtb{ fbb }; - dtb.add_scalar(std::get(s)); - auto doubleOffset = dtb.Finish(); - - putDataInVec(Save::VarUnion::num, doubleOffset); - } - else if (std::holds_alternative(s)) - { - Save::boolTableBuilder btb{ fbb }; - btb.add_scalar(std::get(s)); - auto boolOffset = btb.Finish(); - - putDataInVec(Save::VarUnion::boolean, boolOffset); - } - else if (std::holds_alternative(s)) - { - std::vector keyValVec; - auto& vec = std::get(s); - for (auto& id : vec) - { - keyValVec.push_back(Save::KeyValPair(id.first, id.second)); - } - - auto vecOffset = fbb.CreateVectorOfStructs(keyValVec); - Save::ScriptTableBuilder stb{ fbb }; - stb.add_keys_vals(vecOffset); - auto scriptTableOffset = stb.Finish(); - - putDataInVec(Save::VarUnion::tab, scriptTableOffset); - } - else if (std::holds_alternative(s)) - { - std::string data = std::get(s).name; - auto strOffset = fbb.CreateString(data); - Save::funcNameTableBuilder ftb{ fbb }; - ftb.add_str(strOffset); - auto funcNameOffset = ftb.Finish(); - - putDataInVec(Save::VarUnion::funcName, funcNameOffset); - } - else - { - switch (SavedVarType(s.index())) - { - case SavedVarType::Vec2: - { - SaveVec(SavedVarType::Vec2, s, Save::vec2TableBuilder, Save::VarUnion::vec2, Save::Vector2, FromVector2); - break; - } - - case SavedVarType::Vec3: - { - SaveVec(SavedVarType::Vec3, s, Save::vec3TableBuilder, Save::VarUnion::vec3, Save::Vector3, FromVector3); - break; - } - - case SavedVarType::Rotation: - { - SaveVec(SavedVarType::Rotation, s, Save::rotationTableBuilder, Save::VarUnion::rotation, Save::Vector3, FromVector3); - break; - } - - case SavedVarType::Time: - { - Save::timeTableBuilder ttb{ fbb }; - ttb.add_scalar(std::get(s)); - auto timeOffset = ttb.Finish(); - - putDataInVec(Save::VarUnion::time, timeOffset); - break; - } - - case SavedVarType::Color: - { - Save::colorTableBuilder ctb{ fbb }; - ctb.add_color(std::get<(int)SavedVarType::Color>(s)); - auto offset = ctb.Finish(); - - putDataInVec(Save::VarUnion::color, offset); - break; - } - } - } - } + auto varsVec = SerializeScriptVars(fbb, savedVars); auto unionVec = fbb.CreateVector(varsVec); Save::UnionVecBuilder uvb{ fbb }; @@ -1991,85 +2084,8 @@ static void ParseLua(const Save::SaveGame* s, bool hubMode) auto loadedVars = std::vector{}; auto unionVec = s->script_vars(); - if (unionVec) - { - for (const auto& var : *(unionVec->members())) - { - auto varType = var->u_type(); - switch (varType) - { - case Save::VarUnion::num: - loadedVars.push_back(var->u_as_num()->scalar()); - break; - - case Save::VarUnion::boolean: - loadedVars.push_back(var->u_as_boolean()->scalar()); - break; - - case Save::VarUnion::str: - loadedVars.push_back(var->u_as_str()->str()->str()); - break; - - case Save::VarUnion::tab: - { - auto tab = var->u_as_tab()->keys_vals(); - auto& loadedTab = loadedVars.emplace_back(IndexTable{}); - - for (const auto& pair : *tab) - std::get(loadedTab).push_back(std::make_pair(pair->key(), pair->val())); - - break; - } - - case Save::VarUnion::vec2: - { - auto stored = var->u_as_vec2()->vec(); - SavedVar var; - var.emplace<(int)SavedVarType::Vec2>(ToVector2(stored)); - loadedVars.push_back(var); - break; - } - - case Save::VarUnion::vec3: - { - auto stored = var->u_as_vec3()->vec(); - SavedVar var; - var.emplace<(int)SavedVarType::Vec3>(ToVector3(stored)); - loadedVars.push_back(var); - break; - } - - case Save::VarUnion::rotation: - { - auto stored = var->u_as_rotation()->vec(); - SavedVar var; - var.emplace<(int)SavedVarType::Rotation>(ToVector3(stored)); - loadedVars.push_back(var); - break; - } - - case Save::VarUnion::time: - { - auto stored = var->u_as_time()->scalar(); - SavedVar var; - var.emplace<(int)SavedVarType::Time>(stored); - loadedVars.push_back(var); - break; - } - - case Save::VarUnion::color: - loadedVars.push_back((D3DCOLOR)var->u_as_color()->color()); - break; - - case Save::VarUnion::funcName: - loadedVars.push_back(FuncName{ var->u_as_funcName()->str()->str() }); - break; - - default: - break; - } - } - } + if (unionVec && unionVec->members()) + loadedVars = DeserializeScriptVars(*(unionVec->members())); g_GameScript->SetVariables(loadedVars, hubMode); @@ -2727,8 +2743,8 @@ static void ParseLevel(const Save::SaveGame* s, bool hubMode) // Flyby cameras for (int i = 0; i < s->flyby_cameras()->size(); i++) { - if (i < (int)SpotCam.size()) - SpotCam[i].flags = s->flyby_cameras()->Get(i)->flags(); + if (i < (int)g_Level.SpotCams.size()) + g_Level.SpotCams[i].Flags = s->flyby_cameras()->Get(i)->flags(); } // Items @@ -3127,6 +3143,122 @@ bool SaveGame::LoadHeader(int slot, SaveGameHeader* header) } } +bool SaveGame::SaveGlobalVars() +{ + if (!g_GameScript) + return false; + + try + { + std::vector savedVars; + g_GameScript->GetGlobalVariables(savedVars); + + if (savedVars.empty()) + return true; + + FlatBufferBuilder fbb{}; + + auto varsVec = SerializeScriptVars(fbb, savedVars); + + auto unionVec = fbb.CreateVector(varsVec); + Save::UnionVecBuilder uvb{ fbb }; + uvb.add_members(unionVec); + auto unionVecOffset = uvb.Finish(); + fbb.Finish(unionVecOffset); + + auto buffer = fbb.GetBufferPointer(); + auto size = fbb.GetSize(); + + if (!std::filesystem::is_directory(FullSaveDirectory)) + std::filesystem::create_directory(FullSaveDirectory); + + auto filename = FullSaveDirectory + GLOBAL_VARS_FILENAME; + + std::ofstream fileOut{}; + fileOut.open(filename, std::ios_base::binary | std::ios_base::out); + + if (!fileOut.is_open()) + { + TENLog("Failed to open file for saving global variables.", LogLevel::Error); + return false; + } + + fileOut.write(reinterpret_cast(buffer), size); + fileOut.close(); + + if (fileOut.fail()) + { + TENLog("Failed to write global variables to file.", LogLevel::Error); + return false; + } + + return true; + } + catch (std::exception& ex) + { + TENLog(fmt::format("Error while saving global variables: {}", ex.what()), LogLevel::Error); + return false; + } +} + +bool SaveGame::LoadGlobalVars() +{ + if (!g_GameScript) + return false; + + auto filename = FullSaveDirectory + GLOBAL_VARS_FILENAME; + + if (!std::filesystem::is_regular_file(filename)) + { + TENLog("No global variables file found. Starting with empty GlobalVars.", LogLevel::Info); + return true; + } + + try + { + auto file = std::ifstream(); + file.open(filename, std::ios_base::binary | std::ios_base::ate); + + if (!file.is_open() || !file.good()) + { + TENLog("Failed to open global variables file.", LogLevel::Error); + return false; + } + + auto size = file.tellg(); + + if (size <= 0) + { + TENLog("Global variables file is empty or unreadable.", LogLevel::Warning); + file.close(); + return false; + } + + file.seekg(0, std::ios::beg); + + auto buffer = std::vector(size); + file.read(reinterpret_cast(buffer.data()), size); + file.close(); + + auto unionVec = flatbuffers::GetRoot(buffer.data()); + if (!unionVec || !unionVec->members()) + { + TENLog("Global variables file is empty or corrupted.", LogLevel::Warning); + return false; + } + + auto loadedVars = DeserializeScriptVars(*(unionVec->members())); + + g_GameScript->SetGlobalVariables(loadedVars); + return true; + } + catch (std::exception& ex) + { + TENLog(fmt::format("Error while loading global variables: {}", ex.what()), LogLevel::Error); + return false; + } +} + void SaveGame::Delete(int slot) { if (!IsSaveGameSlotValid(slot)) diff --git a/TombEngine/Game/savegame.h b/TombEngine/Game/savegame.h index 7f8363f573..e11327ad14 100644 --- a/TombEngine/Game/savegame.h +++ b/TombEngine/Game/savegame.h @@ -58,6 +58,9 @@ class SaveGame static bool Save(int slot); static void Delete(int slot); + static bool SaveGlobalVars(); + static bool LoadGlobalVars(); + static bool DoesSaveGameExist(int slot, bool silent = false); static bool IsLoadGamePossible(); static bool IsSaveGameValid(int slot); diff --git a/TombEngine/Game/spotcam.cpp b/TombEngine/Game/spotcam.cpp index edeb3bbcdd..9d6751476a 100644 --- a/TombEngine/Game/spotcam.cpp +++ b/TombEngine/Game/spotcam.cpp @@ -10,485 +10,673 @@ #include "Game/items.h" #include "Game/Lara/lara.h" #include "Game/Lara/lara_helpers.h" +#include "Math/Utils.h" #include "Specific/Input/Input.h" +#include "Specific/level.h" using namespace TEN::Animation; using namespace TEN::Input; +using namespace TEN::Math; using namespace TEN::Renderer; using namespace TEN::Control::Volumes; using namespace TEN::Collision::Point; -constexpr auto MAX_CAMERA = 18; - -bool TrackCameraInit; -int SpotcamTimer; -bool SpotcamPaused; -int SpotcamLoopCnt; -int CameraFade; -Vector3i LaraFixedPosition; -int InitialCameraRoom; -Vector3i InitialCameraPosition; -Vector3i InitialCameraTarget; -int CurrentSplinePosition; -int SplineToCamera; -int FirstCamera; -int LastCamera; -int CurrentCameraCnt; -int CameraXposition[MAX_CAMERA]; -int CameraYposition[MAX_CAMERA]; -int CameraZposition[MAX_CAMERA]; -int CameraXtarget[MAX_CAMERA]; -int CameraYtarget[MAX_CAMERA]; -int CameraZtarget[MAX_CAMERA]; -int CameraRoll[MAX_CAMERA]; -int CameraFOV[MAX_CAMERA]; -int CameraSpeed[MAX_CAMERA]; -int SplineFromCamera; -bool SpotCamFirstLook; -short CurrentSplineCamera; -int LastSpotCamSequence; -int LaraHealth; -int LaraAir; -int CurrentSpotcamSequence; -std::vector SpotCam; -std::unordered_map SpotCamRemap; -std::vector CameraCnt; - -bool CheckTrigger = false; -bool UseSpotCam = false; -bool SpotcamSwitched = false; -bool SpotcamDontDrawLara = false; -bool SpotcamOverlay = false; - -void ClearSpotCamSequences() +namespace TEN::SpotCam { - UseSpotCam = false; - SpotcamDontDrawLara = false; - SpotcamOverlay = false; - - SpotCam.clear(); - SpotCamRemap.clear(); - CameraCnt.clear(); -} - -void InitializeSpotCamSequences(bool startFirstSequence) -{ - TrackCameraInit = false; - - CameraCnt.clear(); - SpotCamRemap.clear(); - - if (SpotCam.empty()) - return; - - int currentSequence = SpotCam[0].sequence; - int count = 0; - - for (const auto& cam : SpotCam) + enum class PausePhase + { + None, // Normal playback, speed factor = 1. + EaseOut, // Quadratic ease-out, speed factor transitions from 1 to 0. + Hold, // Fully stopped, speed factor = 0. + EaseIn // Quadratic ease-in, speed factor transitions from 0 to 1. + }; + + struct SplineCameraKnots { - if (cam.sequence != currentSequence) + static const int TRACKING_KNOT_COUNT = 3; + static const int SPLINE_KNOT_COUNT = 6; + static const int BLEND_KNOT_COUNT = 5; + + std::vector PosX = {}; + std::vector PosY = {}; + std::vector PosZ = {}; + std::vector TargetX = {}; + std::vector TargetY = {}; + std::vector TargetZ = {}; + std::vector Roll = {}; + std::vector FOV = {}; + std::vector Speed = {}; + + void Resize(int count) { - SpotCamRemap[currentSequence] = (int)CameraCnt.size(); - CameraCnt.push_back(count); - currentSequence = cam.sequence; - count = 0; + PosX.assign(count, 0.0f); + PosY.assign(count, 0.0f); + PosZ.assign(count, 0.0f); + TargetX.assign(count, 0.0f); + TargetY.assign(count, 0.0f); + TargetZ.assign(count, 0.0f); + Roll.assign(count, 0.0f); + FOV.assign(count, 0.0f); + Speed.assign(count, 0.0f); } - - count++; - } - - SpotCamRemap[currentSequence] = (int)CameraCnt.size(); - CameraCnt.push_back(count); - - if (startFirstSequence && SpotCamRemap.count(0)) + + void SetKnot(int index, const SpotCamInfo& cam) + { + PosX[index] = (float)cam.Position.x; + PosY[index] = (float)cam.Position.y; + PosZ[index] = (float)cam.Position.z; + TargetX[index] = (float)cam.Target.x; + TargetY[index] = (float)cam.Target.y; + TargetZ[index] = (float)cam.Target.z; + Roll[index] = (float)cam.Roll; + FOV[index] = (float)cam.FOV; + Speed[index] = (float)cam.Speed; + } + }; + + // Public globals. + + int LastSpotCamSequence = 0; + bool TrackCameraInit = false; + bool UseSpotCam = false; + bool SpotcamSwitched = false; + bool SpotcamDontDrawLara = false; + bool SpotcamOverlay = false; + + // Local state. + + static SplineCameraKnots Knots = {}; + + static std::unordered_map SequenceMap = {}; + static std::vector SequenceCamCount = {}; + + static float SplineAlpha = 0.0f; // Normalized spline position [0, 1]. + + static int FirstCameraIndex = 0; + static int LastCameraIndex = 0; + static int CurrentCameraIndex = 0; + static int FadeCameraIndex = NO_VALUE; + + static int CurrentSequenceID = 0; + static int SequenceCameraCount = 0; + static int LoopCount = 0; + static int SplineFromOffset = 0; // Number of leading knots sourced from initial camera. + + static bool IsFirstLookPress = false; + static bool IsTransitionToGame = false; + static bool RunHeavyTriggers = false; + + + static int SavedCameraRoom = 0; + static Vector3i SavedCameraPos = Vector3i::Zero; + static Vector3i SavedCameraTarget = Vector3i::Zero; + static int SavedLaraHealth = 0; + static int SavedLaraAir = 0; + + // Pause state machine. + + static PausePhase CurrentPausePhase = PausePhase::None; + static float PauseSpeedFactor = 1.0f; // Multiplier applied to per-frame speed advancement. + static float PauseEaseProgress = 0.0f; // Progress through current ease phase [0, 1]. + static float PauseEaseStep = 0.0f; // Per-frame step, derived from segment speed at ease start. + static float PauseEaseStartAlpha = 0.0f; // SplineAlpha when ease-out began. + static int PauseHoldTimer = 0; // Frames remaining in hold phase. + static bool IsPauseComplete = false; // Prevents re-triggering pause for same segment. + + // Resets the pause state machine to idle. + static void InitializePauseState() { - InitializeSpotCam(0); - UseSpotCam = true; + CurrentPausePhase = PausePhase::None; + PauseSpeedFactor = 1.0f; + PauseEaseProgress = 0.0f; + PauseEaseStep = 0.0f; + PauseEaseStartAlpha = 0.0f; + PauseHoldTimer = 0; + IsPauseComplete = false; } -} -void InitializeSpotCam(short Sequence) -{ - if (SpotCam.empty() || SpotCamRemap.find(Sequence) == SpotCamRemap.end()) + bool HasSpotCamSequence(int sequence) { - TENLog(fmt::format("Initializing flyby sequence {} failed, sequence not found.", Sequence), LogLevel::Warning); - return; + return SequenceMap.find(sequence) != SequenceMap.end(); } - if (TrackCameraInit != 0 && LastSpotCamSequence == Sequence) + int GetSequenceFirstCameraIndex(int sequence) { - TrackCameraInit = false; - return; - } - - Lara.Control.Look.OpticRange = 0; - Lara.Control.Look.IsUsingLasersight = false; - - AlterFOV(ANGLE(DEFAULT_FOV), false); - - LaraItem->MeshBits = ALL_JOINT_BITS; - - ResetPlayerFlex(LaraItem); - - Camera.bounce = 0; - - Lara.Inventory.IsBusy = 0; - - CameraFade = NO_VALUE; - LastSpotCamSequence = Sequence; - TrackCameraInit = false; - SpotcamTimer = 0; - SpotcamPaused = false; - SpotcamLoopCnt = 0; - Lara.Control.IsLocked = false; - - LaraAir = Lara.Status.Air; - - InitialCameraPosition.x = Camera.pos.x; - InitialCameraPosition.y = Camera.pos.y; - InitialCameraPosition.z = Camera.pos.z; - - InitialCameraTarget.x = Camera.target.x; - InitialCameraTarget.y = Camera.target.y; - InitialCameraTarget.z = Camera.target.z; - - LaraHealth = LaraItem->HitPoints; - InitialCameraRoom = Camera.pos.RoomNumber; - - LaraFixedPosition.x = LaraItem->Pose.Position.x; - LaraFixedPosition.y = LaraItem->Pose.Position.y; - LaraFixedPosition.z = LaraItem->Pose.Position.z; - - CurrentSpotcamSequence = Sequence; - CurrentSplineCamera = 0; + if (!HasSpotCamSequence(sequence)) + return NO_VALUE; - for (int i = 0; i < SpotCamRemap[Sequence]; i++) - CurrentSplineCamera += CameraCnt[i]; + int index = 0; + for (int i = 0; i < SequenceMap[sequence]; i++) + index += SequenceCamCount[i]; - CurrentSplinePosition = 0; - SplineToCamera = 0; - - FirstCamera = CurrentSplineCamera; + return index; + } - auto* spotcam = &SpotCam[CurrentSplineCamera]; + static int GetSequenceCameraCount(int sequence) + { + if (!HasSpotCamSequence(sequence)) + return 0; - LastCamera = CurrentSplineCamera + (CameraCnt[SpotCamRemap[Sequence]] - 1); - CurrentCameraCnt = CameraCnt[SpotCamRemap[Sequence]]; + return SequenceCamCount[SequenceMap[sequence]]; + } - if ((spotcam->flags & SCF_DISABLE_LARA_CONTROLS)) + void ClearSpotCamSequences() { - Lara.Control.IsLocked = true; - SetCinematicBars(SPOTCAM_CINEMATIC_BARS_HEIGHT, SPOTCAM_CINEMATIC_BARS_SPEED); + UseSpotCam = false; + SpotcamDontDrawLara = false; + SpotcamOverlay = false; + + g_Level.SpotCams.clear(); + SequenceMap.clear(); + SequenceCamCount.clear(); } - if (spotcam->flags & SCF_TRACKING_CAM) + void InitializeSpotCamSequences(bool startFirstSequence) { - CameraXposition[1] = SpotCam[FirstCamera].x; - CameraYposition[1] = SpotCam[FirstCamera].y; - CameraZposition[1] = SpotCam[FirstCamera].z; - CameraXtarget[1] = SpotCam[FirstCamera].tx; - CameraYtarget[1] = SpotCam[FirstCamera].ty; - CameraZtarget[1] = SpotCam[FirstCamera].tz; - CameraRoll[1] = SpotCam[FirstCamera].roll; - CameraFOV[1] = SpotCam[FirstCamera].fov; - CameraSpeed[1] = SpotCam[FirstCamera].speed; + TrackCameraInit = false; + + SequenceCamCount.clear(); + SequenceMap.clear(); - SplineFromCamera = 0; + if (g_Level.SpotCams.empty()) + return; - if (CurrentCameraCnt > 0) + int currentSequence = g_Level.SpotCams[0].Sequence; + int count = 0; + + for (const auto& cam : g_Level.SpotCams) { - spotcam = &SpotCam[FirstCamera]; - - for (int i = 0; i < CurrentCameraCnt; i++, spotcam++) + if (cam.Sequence != currentSequence) { - CameraXposition[i + 2] = spotcam->x; - CameraYposition[i + 2] = spotcam->y; - CameraZposition[i + 2] = spotcam->z; - CameraXtarget[i + 2] = spotcam->tx; - CameraYtarget[i + 2] = spotcam->ty; - CameraZtarget[i + 2] = spotcam->tz; - CameraRoll[i + 2] = spotcam->roll; - CameraFOV[i + 2] = spotcam->fov; - CameraSpeed[i + 2] = spotcam->speed; + SequenceMap[currentSequence] = (int)SequenceCamCount.size(); + SequenceCamCount.push_back(count); + currentSequence = cam.Sequence; + count = 0; } + + count++; } + + SequenceMap[currentSequence] = (int)SequenceCamCount.size(); + SequenceCamCount.push_back(count); - CameraXposition[CurrentCameraCnt + 2] = SpotCam[LastCamera].x; - CameraYposition[CurrentCameraCnt + 2] = SpotCam[LastCamera].y; - CameraZposition[CurrentCameraCnt + 2] = SpotCam[LastCamera].z; - CameraXtarget[CurrentCameraCnt + 2] = SpotCam[LastCamera].tx; - CameraYtarget[CurrentCameraCnt + 2] = SpotCam[LastCamera].ty; - CameraZtarget[CurrentCameraCnt + 2] = SpotCam[LastCamera].tz; - CameraFOV[CurrentCameraCnt + 2] = SpotCam[LastCamera].fov; - CameraRoll[CurrentCameraCnt + 2] = SpotCam[LastCamera].roll; - CameraSpeed[CurrentCameraCnt + 2] = SpotCam[LastCamera].speed; + if (startFirstSequence&& HasSpotCamSequence(0)) + { + InitializeSpotCam(0); + UseSpotCam = true; + } } - else + + void InitializeSpotCam(short sequence) { - int sp = 0; - if ((spotcam->flags & SCF_CUT_PAN)) - { - CameraXposition[1] = SpotCam[CurrentSplineCamera].x; - CameraYposition[1] = SpotCam[CurrentSplineCamera].y; - CameraZposition[1] = SpotCam[CurrentSplineCamera].z; - CameraXtarget[1] = SpotCam[CurrentSplineCamera].tx; - CameraYtarget[1] = SpotCam[CurrentSplineCamera].ty; - CameraZtarget[1] = SpotCam[CurrentSplineCamera].tz; - CameraRoll[1] = SpotCam[CurrentSplineCamera].roll; - CameraFOV[1] = SpotCam[CurrentSplineCamera].fov; - CameraSpeed[1] = SpotCam[CurrentSplineCamera].speed; + if (g_Level.SpotCams.empty() || !HasSpotCamSequence(sequence)) + { + TENLog(fmt::format("Initializing flyby sequence {} failed, sequence not found.", sequence), LogLevel::Warning); + return; + } + + if (TrackCameraInit && LastSpotCamSequence == sequence) + { + TrackCameraInit = false; + return; + } + + // Reset player data. + LaraItem->MeshBits = ALL_JOINT_BITS; + ResetPlayerFlex(LaraItem); + + Lara.Control.Look.OpticRange = 0; + Lara.Control.Look.IsUsingLasersight = false; + Lara.Control.IsLocked = false; + Lara.Inventory.IsBusy = 0; + + AlterFOV(ANGLE(DEFAULT_FOV), false); + Camera.bounce = 0; + + // Reset spotcam state. + FadeCameraIndex = NO_VALUE; + LastSpotCamSequence = sequence; + TrackCameraInit = false; + LoopCount = 0; + InitializePauseState(); + + // Save player state. + SavedLaraAir = Lara.Status.Air; + SavedLaraHealth = LaraItem->HitPoints; + + // Save camera state. + SavedCameraPos = Vector3i(Camera.pos.x, Camera.pos.y, Camera.pos.z); + SavedCameraTarget = Vector3i(Camera.target.x, Camera.target.y, Camera.target.z); + SavedCameraRoom = Camera.pos.RoomNumber; + + // Compute first camera index for this sequence. + CurrentSequenceID = sequence; + CurrentCameraIndex = GetSequenceFirstCameraIndex(sequence); + + if (CurrentCameraIndex == NO_VALUE) + { + TENLog(fmt::format("Can't find proper first camera index for flyby sequence {}.", sequence), LogLevel::Warning); + return; + } - Camera.DisableInterpolation = true; + SplineAlpha = 0.0f; + IsTransitionToGame = false; - SplineFromCamera = 0; + FirstCameraIndex = CurrentCameraIndex; + SequenceCameraCount = GetSequenceCameraCount(sequence); + LastCameraIndex = FirstCameraIndex + SequenceCameraCount - 1; - int cn = CurrentSplineCamera; - while (sp < 4) - { - if (LastCamera < CurrentSplineCamera) - cn = FirstCamera; - - CameraXposition[sp + 2] = SpotCam[cn].x; - CameraYposition[sp + 2] = SpotCam[cn].y; - CameraZposition[sp + 2] = SpotCam[cn].z; - CameraXtarget[sp + 2] = SpotCam[cn].tx; - CameraYtarget[sp + 2] = SpotCam[cn].ty; - CameraZtarget[sp + 2] = SpotCam[cn].tz; - CameraRoll[sp + 2] = SpotCam[cn].roll; - CameraFOV[sp + 2] = SpotCam[cn].fov; - CameraSpeed[sp + 2] = SpotCam[cn].speed; - cn++; - sp++; - } + const auto& firstCam = g_Level.SpotCams[CurrentCameraIndex]; + + if (firstCam.Flags & SCF_DISABLE_LARA_CONTROLS) + { + Lara.Control.IsLocked = true; + SetCinematicBars(SPOTCAM_CINEMATIC_BARS_HEIGHT, SPOTCAM_CINEMATIC_BARS_SPEED); + } + + // Populate spline knot arrays. + if (firstCam.Flags & SCF_TRACKING_CAM) + { + // Tracking camera: pad with first camera, then all cameras, then pad with last. + Knots.Resize(SequenceCameraCount + SplineCameraKnots::TRACKING_KNOT_COUNT); + Knots.SetKnot(1, g_Level.SpotCams[FirstCameraIndex]); + SplineFromOffset = 0; - CurrentSplineCamera++; + for (int i = 0; i < SequenceCameraCount; i++) + Knots.SetKnot(i + 2, g_Level.SpotCams[FirstCameraIndex + i]); - if (CurrentSplineCamera > LastCamera) - CurrentSplineCamera = FirstCamera; + Knots.SetKnot(SequenceCameraCount + 2, g_Level.SpotCams[LastCameraIndex]); + } + else if (firstCam.Flags & SCF_CUT_PAN) + { + // Cut-pan: first knot is current camera, then fill 4 knots from sequence. + Knots.Resize(SplineCameraKnots::SPLINE_KNOT_COUNT); + Knots.SetKnot(1, g_Level.SpotCams[CurrentCameraIndex]); + SplineFromOffset = 0; - if (spotcam->flags & SCF_ACTIVATE_HEAVY_TRIGGERS) - CheckTrigger = true; + Camera.DisableInterpolation = true; + + int camIndex = CurrentCameraIndex; + for (int i = 0; i < 4; i++) + { + if (camIndex > LastCameraIndex) + camIndex = FirstCameraIndex; + + Knots.SetKnot(i + 2, g_Level.SpotCams[camIndex]); + camIndex++; + } - if (spotcam->flags & SCF_HIDE_LARA) + CurrentCameraIndex++; + if (CurrentCameraIndex > LastCameraIndex) + CurrentCameraIndex = FirstCameraIndex; + + if (firstCam.Flags & SCF_ACTIVATE_HEAVY_TRIGGERS) + RunHeavyTriggers = true; + + if (firstCam.Flags & SCF_HIDE_LARA) SpotcamDontDrawLara = true; } else { - int cn = CurrentSplineCamera; - - CameraXposition[1] = InitialCameraPosition.x; - CameraYposition[1] = InitialCameraPosition.y; - CameraZposition[1] = InitialCameraPosition.z; - CameraXtarget[1] = InitialCameraTarget.x; - CameraYtarget[1] = InitialCameraTarget.y; - CameraZtarget[1] = InitialCameraTarget.z; - CameraFOV[1] = CurrentFOV; - CameraRoll[1] = 0; - CameraSpeed[1] = spotcam->speed; - - CameraXposition[2] = InitialCameraPosition.x; - CameraYposition[2] = InitialCameraPosition.y; - CameraZposition[2] = InitialCameraPosition.z; - CameraXtarget[2] = InitialCameraTarget.x; - CameraYtarget[2] = InitialCameraTarget.y; - CameraZtarget[2] = InitialCameraTarget.z; - CameraFOV[2] = CurrentFOV; - CameraRoll[2] = 0; - CameraSpeed[2] = spotcam->speed; - - CameraXposition[3] = SpotCam[CurrentSplineCamera].x; - CameraYposition[3] = SpotCam[CurrentSplineCamera].y; - CameraZposition[3] = SpotCam[CurrentSplineCamera].z; - CameraXtarget[3] = SpotCam[CurrentSplineCamera].tx; - CameraYtarget[3] = SpotCam[CurrentSplineCamera].ty; - CameraZtarget[3] = SpotCam[CurrentSplineCamera].tz; - CameraRoll[3] = SpotCam[CurrentSplineCamera].roll; - CameraFOV[3] = SpotCam[CurrentSplineCamera].fov; - CameraSpeed[3] = SpotCam[CurrentSplineCamera].speed; - - SplineFromCamera = 1; - - cn++; - - if (LastCamera < cn) - cn = FirstCamera; - - CameraXposition[4] = SpotCam[cn].x; - CameraYposition[4] = SpotCam[cn].y; - CameraZposition[4] = SpotCam[cn].z; - - CameraXtarget[4] = SpotCam[cn].tx; - CameraYtarget[4] = SpotCam[cn].ty; - CameraZtarget[4] = SpotCam[cn].tz; - - CameraRoll[4] = SpotCam[cn].roll; - CameraFOV[4] = SpotCam[cn].fov; - CameraSpeed[4] = SpotCam[cn].speed; + // Smooth pan: blend from current camera position to first spotcam (indices 0-4). + Knots.Resize(SplineCameraKnots::BLEND_KNOT_COUNT); + SplineFromOffset = 1; + + // Knots [1] and [2] = current camera position (for smooth approach). + auto setInitialKnot = [&](int index) + { + Knots.PosX[index] = (float)SavedCameraPos.x; + Knots.PosY[index] = (float)SavedCameraPos.y; + Knots.PosZ[index] = (float)SavedCameraPos.z; + Knots.TargetX[index] = (float)SavedCameraTarget.x; + Knots.TargetY[index] = (float)SavedCameraTarget.y; + Knots.TargetZ[index] = (float)SavedCameraTarget.z; + Knots.FOV[index] = (float)CurrentFOV; + Knots.Roll[index] = 0.0f; + Knots.Speed[index] = (float)firstCam.Speed; + }; + + setInitialKnot(1); + setInitialKnot(2); + + // Knot [3] = first spotcam in sequence. + Knots.SetKnot(3, g_Level.SpotCams[CurrentCameraIndex]); + + // Knot [4] = next spotcam (or clamped to last). + int nextIndex = CurrentCameraIndex + 1; + if (nextIndex > LastCameraIndex) + nextIndex = FirstCameraIndex; + + Knots.SetKnot(4, g_Level.SpotCams[nextIndex]); } + + if (firstCam.Flags & SCF_HIDE_LARA) + SpotcamDontDrawLara = true; } - - if (spotcam->flags & SCF_HIDE_LARA) - SpotcamDontDrawLara = true; -} - -void CalculateSpotCameras() -{ - int cpx; // stack offset -96 - int cpy; // stack offset -92 - int cpz; // stack offset -88 - int ctx; // stack offset -84 - int cty; // stack offset -80 - int ctz; // stack offset -76 - int cspeed; // stack offset -72 - int cfov; // stack offset -68 - int croll; // stack offset -64 - SPOTCAM* s; // stack offset -60 - short spline_cnt; // $s3 - int dx; // $v1 - int dy; // $s0 - int dz; // $s1 - - //{ // line 76, offset 0x38114 - int sp; // $s2 - int cp; // $fp - int clen; // $s4 - int tlen; // $v1 - int cx; // $s1 - int cy; // $s0 - int cz; // $v0 - int lx; // stack offset -56 - int lz; // stack offset -52 - int ly; // stack offset -48 - int cn; // $s0 - - CAMERA_INFO backup; - - if (SpotCam.empty() || FirstCamera >= (int)SpotCam.size()) + + // Runs heavy triggers at the camera's current position. + static void TestVolumesAndTriggers() { - UseSpotCam = false; - return; + if (!RunHeavyTriggers) + return; + + auto oldType = Camera.type; + Camera.type = CameraType::Heavy; + + if (CurrentLevel != 0) + { + TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, true); + TestVolumes(&Camera); + } + else + { + TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, false); + TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, true); + TestVolumes(&Camera); + } + + Camera.type = oldType; + RunHeavyTriggers = false; } - - if (Lara.Control.IsLocked) + + // Advances the spline position by the given normalized speed and manages the pause state machine (ease-out, hold, ease-in). + static bool AdvanceOrPauseSequence(float normalizedSpeed) { - LaraItem->HitPoints = LaraHealth; - Lara.Status.Air = LaraAir; + constexpr auto EASE_DISTANCE = 0.15f; + constexpr auto MIN_SPEED = 0.001f; + + // Trigger ease-out when the camera is within PAUSE_EASE_DISTANCE of the segment end and a pause is pending. + if (CurrentPausePhase == PausePhase::None) + { + bool hasPause = g_Level.SpotCams[CurrentCameraIndex].Timer > 0 && (g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_STOP_MOVEMENT) && !IsPauseComplete; + + if (hasPause && (1.0f - SplineAlpha) <= EASE_DISTANCE) + { + IsPauseComplete = true; + PauseEaseStartAlpha = SplineAlpha; + PauseEaseProgress = 0.0f; + + // Derive step so the initial alpha velocity matches the current camera speed. + float remainingAlpha = std::max(1.0f - SplineAlpha, MIN_SPEED); + + // Clamp to a minimum so the pause doesn't stall if speed is near zero. + float clampedSpeed = std::max(normalizedSpeed, MIN_SPEED); + + PauseEaseStep = clampedSpeed / (2.0f * remainingAlpha); + CurrentPausePhase = PausePhase::EaseOut; + } + } + + switch (CurrentPausePhase) + { + case PausePhase::EaseOut: + + PauseEaseProgress = std::min(PauseEaseProgress + PauseEaseStep, 1.0f); + SplineAlpha = PauseEaseStartAlpha + (1.0f - PauseEaseStartAlpha) * PauseEaseProgress * (2.0f - PauseEaseProgress); + + if (PauseEaseProgress >= 1.0f) + { + PauseSpeedFactor = 0.0f; + PauseHoldTimer = g_Level.SpotCams[CurrentCameraIndex].Timer >> 4; + CurrentPausePhase = PausePhase::Hold; + } + return false; + + case PausePhase::Hold: + + PauseHoldTimer--; + + if (PauseHoldTimer <= 0) + { + PauseEaseProgress = 0.0f; + PauseSpeedFactor = 0.0f; + CurrentPausePhase = PausePhase::EaseIn; + return true; // Signal caller to advance to next segment. + } + return false; + + case PausePhase::EaseIn: + + PauseEaseProgress = std::min(PauseEaseProgress + PauseEaseStep, 1.0f); + PauseSpeedFactor = PauseEaseProgress * PauseEaseProgress; + + if (PauseEaseProgress >= 1.0f) + { + PauseSpeedFactor = 1.0f; + PauseEaseProgress = 0.0f; + IsPauseComplete = false; + CurrentPausePhase = PausePhase::None; + } + + // Advance alpha normally using the ramping speed factor. + SplineAlpha = std::min(SplineAlpha + normalizedSpeed * PauseSpeedFactor, 1.0f); + return false; + + default: + // No pause active; normal advance. + SplineAlpha = std::min(SplineAlpha + normalizedSpeed, 1.0f); + return false; + } } - - s = &SpotCam[FirstCamera]; - spline_cnt = 4; - - if (s->flags & SCF_TRACKING_CAM) - spline_cnt = CurrentCameraCnt + 2; - - //loc_37F64 - cpx = Spline(CurrentSplinePosition, &CameraXposition[1], spline_cnt); - cpy = Spline(CurrentSplinePosition, &CameraYposition[1], spline_cnt); - cpz = Spline(CurrentSplinePosition, &CameraZposition[1], spline_cnt); - ctx = Spline(CurrentSplinePosition, &CameraXtarget[1], spline_cnt); - cty = Spline(CurrentSplinePosition, &CameraYtarget[1], spline_cnt); - ctz = Spline(CurrentSplinePosition, &CameraZtarget[1], spline_cnt); - cspeed = Spline(CurrentSplinePosition, &CameraSpeed[1], spline_cnt); - croll = Spline(CurrentSplinePosition, &CameraRoll[1], spline_cnt); - cfov = Spline(CurrentSplinePosition, &CameraFOV[1], spline_cnt); - - if ((SpotCam[CurrentSplineCamera].flags & SCF_SCREEN_FADE_IN) && - CameraFade != CurrentSplineCamera) + + // Ends the spotcam sequence and restores normal camera. + static void EndSequence(const SpotCamInfo& firstCam) { - SetScreenFadeIn(FADE_SCREEN_SPEED); - CameraFade = CurrentSplineCamera; + TestVolumesAndTriggers(); + SetCinematicBars(0.0f, SPOTCAM_CINEMATIC_BARS_SPEED); + + UseSpotCam = false; + RunHeavyTriggers = false; + Lara.Control.IsLocked = false; + Lara.Control.Look.IsUsingBinoculars = false; + Camera.oldType = CameraType::Fixed; + Camera.type = CameraType::Chase; + Camera.speed = 1; + Camera.DisableInterpolation = true; + + if (firstCam.Flags & SCF_CUT_TO_LARA_CAM) + { + Camera.pos.x = SavedCameraPos.x; + Camera.pos.y = SavedCameraPos.y; + Camera.pos.z = SavedCameraPos.z; + Camera.pos.RoomNumber = SavedCameraRoom; + Camera.target.x = SavedCameraTarget.x; + Camera.target.y = SavedCameraTarget.y; + Camera.target.z = SavedCameraTarget.z; + } + + SpotcamOverlay = false; + SpotcamDontDrawLara = false; + AlterFOV(LastFOV); } - - if ((SpotCam[CurrentSplineCamera].flags & SCF_SCREEN_FADE_OUT) && - CameraFade != CurrentSplineCamera) + + // Fills 4 spline knots starting from the given camera index, wrapping or clamping as needed. + static void FillSplineKnots(int startKnotIndex, int startCamIndex, int count, bool loop) { - SetScreenFadeOut(FADE_SCREEN_SPEED); - CameraFade = CurrentSplineCamera; + int camIndex = startCamIndex; + for (int i = 0; i < count; i++) + { + if (loop) + { + if (camIndex > LastCameraIndex) + camIndex = FirstCameraIndex; + } + else + { + if (camIndex > LastCameraIndex) + camIndex = LastCameraIndex; + } + + Knots.SetKnot(startKnotIndex + i, g_Level.SpotCams[camIndex]); + camIndex++; + } } - - sp = 0; - tlen = 0; - clen = 0; - cp = 0; - int temp = 0x2000; - - if (s->flags & SCF_TRACKING_CAM) + + // Tracking camera: finds the closest spline position to Lara using a coarse-to-fine search. + static float FindClosestSplineAlpha(int knotCount) { - lx = LaraItem->Pose.Position.x; - ly = LaraItem->Pose.Position.y; - lz = LaraItem->Pose.Position.z; - - for (int i = 0; i < 8; i++) + auto laraPos = LaraItem->Pose.Position; + float closestAlpha = 0.0f; + float searchStep = 1.0f / 8.0f; + + float searchStart = 0.0f; + + for (int iteration = 0; iteration < 8; iteration++) { - clen = 0x10000; - - for (int j = 0; j < 8; j++) + float closestDist = FLT_MAX; + + for (int sample = 0; sample < 8; sample++) { - cx = Spline(sp, &CameraXposition[1], spline_cnt); - cy = Spline(sp, &CameraYposition[1], spline_cnt); - cz = Spline(sp, &CameraZposition[1], spline_cnt); - - dx = SQUARE(cx - lx); - dy = SQUARE(cy - ly); - dz = SQUARE(cz - lz); - - tlen = sqrt(dx + dy + dz); - - if (tlen <= clen) + float sampleAlpha = searchStart + sample * searchStep; + if (sampleAlpha > 1.0f) + break; + + float cx = Spline(sampleAlpha, &Knots.PosX[1], knotCount); + float cy = Spline(sampleAlpha, &Knots.PosY[1], knotCount); + float cz = Spline(sampleAlpha, &Knots.PosZ[1], knotCount); + + float dist = Vector3::Distance(Vector3(cx, cy, cz), Vector3((float)laraPos.x, (float)laraPos.y, (float)laraPos.z)); + + if (dist <= closestDist) { - cp = sp; - clen = tlen; + closestAlpha = sampleAlpha; + closestDist = dist; } - - sp += temp; - - if (sp > 0x10000) - break; } - - temp >>= 1; - sp = cp - 2 * (temp & 0xFE); // << 2 ? - - if (sp < 0) - sp = 0; + + float halfStep = searchStep / 2.0f; + searchStart = closestAlpha - 2.0f * halfStep; + + if (searchStart < 0.0f) + searchStart = 0.0f; + + searchStep = halfStep; } - - CurrentSplinePosition += (cp - CurrentSplinePosition) >> 5; - - if ((s->flags & SCF_CUT_PAN)) + + return closestAlpha; + } + + void CalculateSpotCam() + { + if (g_Level.SpotCams.empty() || FirstCameraIndex >= (int)g_Level.SpotCams.size()) { - if (abs(cp - CurrentSplinePosition) > 0x8000) - CurrentSplinePosition = cp; + TENLog(fmt::format("Flyby sequence {} refers to a camera {} that does not exist.", CurrentSequenceID, FirstCameraIndex), LogLevel::Warning); + UseSpotCam = false; + return; } + + if (Lara.Control.IsLocked) + { + LaraItem->HitPoints = SavedLaraHealth; + Lara.Status.Air = SavedLaraAir; + } + + const auto& firstCam = g_Level.SpotCams[FirstCameraIndex]; + int knotCount = (firstCam.Flags & SCF_TRACKING_CAM) ? (SequenceCameraCount + 2) : 4; - if (CurrentSplinePosition > 0x10000) - CurrentSplinePosition = 0x10000; - else if (CurrentSplinePosition < 0) - CurrentSplinePosition = 0; - } - else if (!SpotcamTimer) - CurrentSplinePosition += cspeed; - - bool lookPressed = IsHeld(In::Look); - - if (!lookPressed) - SpotCamFirstLook = false; - - if ((s->flags & SCF_DISABLE_BREAKOUT) || !lookPressed) - { - // Disable interpolation if camera traveled too far. - auto origin = Vector3(Camera.pos.x, Camera.pos.y, Camera.pos.z); - auto target = Vector3(cpx, cpy, cpz); - float dist = Vector3::Distance(origin, target); + // Spline() needs at least 4 knots to form a valid segment. + if (knotCount < 4) + { + TENLog(fmt::format("Flyby sequence {} has too few cameras for spline interpolation.", CurrentSequenceID), LogLevel::Warning); + UseSpotCam = false; + return; + } + + // Interpolate all camera properties at current spline position. + float interpPosX = Spline(SplineAlpha, &Knots.PosX[1], knotCount); + float interpPosY = Spline(SplineAlpha, &Knots.PosY[1], knotCount); + float interpPosZ = Spline(SplineAlpha, &Knots.PosZ[1], knotCount); + float interpTargetX = Spline(SplineAlpha, &Knots.TargetX[1], knotCount); + float interpTargetY = Spline(SplineAlpha, &Knots.TargetY[1], knotCount); + float interpTargetZ = Spline(SplineAlpha, &Knots.TargetZ[1], knotCount); + float interpSpeed = Spline(SplineAlpha, &Knots.Speed[1], knotCount); + float interpRoll = Spline(SplineAlpha, &Knots.Roll[1], knotCount); + float interpFOV = Spline(SplineAlpha, &Knots.FOV[1], knotCount); + + // Handle screen fading. + if ((g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_SCREEN_FADE_IN) && + FadeCameraIndex != CurrentCameraIndex) + { + SetScreenFadeIn(FADE_SCREEN_SPEED); + FadeCameraIndex = CurrentCameraIndex; + } + + if ((g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_SCREEN_FADE_OUT) && + FadeCameraIndex != CurrentCameraIndex) + { + SetScreenFadeOut(FADE_SCREEN_SPEED); + FadeCameraIndex = CurrentCameraIndex; + } + + // Advance spline position. + bool advancedToNextSegment = false; + + // Tracking camera: advance spline position to track Lara. + if (firstCam.Flags & SCF_TRACKING_CAM) + { + float closestAlpha = FindClosestSplineAlpha(knotCount); + + // Smoothly approach the closest position. + SplineAlpha += (closestAlpha - SplineAlpha) / 32.0f; + + if ((firstCam.Flags & SCF_CUT_PAN) && std::abs(closestAlpha - SplineAlpha) > 0.5f) + SplineAlpha = closestAlpha; + + SplineAlpha = std::clamp(SplineAlpha, 0.0f, 1.0f); + } + else + { + // Non-tracking: advance alpha and manage pause state machine. + float normalizedSpeed = interpSpeed / (float)USHRT_MAX; + advancedToNextSegment = AdvanceOrPauseSequence(normalizedSpeed); + } + + bool lookPressed = IsHeld(In::Look); + if (!lookPressed) + IsFirstLookPress = false; + + // Handle look-key breakout for non-tracking cameras. + if (!(firstCam.Flags & SCF_DISABLE_BREAKOUT) && lookPressed) + { + if (firstCam.Flags & SCF_TRACKING_CAM) + { + if (!IsFirstLookPress) + { + Camera.oldType = CameraType::Fixed; + IsFirstLookPress = true; + } + + CalculateCamera(LaraCollision); + } + else + { + // Break out of spotcam entirely. + SetScreenFadeIn(FADE_SCREEN_SPEED); + SetCinematicBars(0.0f, SPOTCAM_CINEMATIC_BARS_SPEED); + UseSpotCam = false; + Lara.Control.IsLocked = false; + Camera.speed = 1; + AlterFOV(LastFOV); + CalculateCamera(LaraCollision); + RunHeavyTriggers = false; + } + + return; + } + + // Disable interpolation if camera jumped too far. + auto origin = Vector3((float)Camera.pos.x, (float)Camera.pos.y, (float)Camera.pos.z); + auto target = Vector3(interpPosX, interpPosY, interpPosZ); - if (dist > BLOCK(0.25f)) + if (Vector3::Distance(origin, target) > BLOCK(0.25f)) Camera.DisableInterpolation = true; - - Camera.pos.x = cpx; - Camera.pos.y = cpy; - Camera.pos.z = cpz; - - if ((s->flags & SCF_FOCUS_LARA_HEAD) || (s->flags & SCF_TRACKING_CAM)) + + // Apply interpolated camera position. + Camera.pos.x = (int)interpPosX; + Camera.pos.y = (int)interpPosY; + Camera.pos.z = (int)interpPosZ; + + if ((firstCam.Flags & SCF_FOCUS_LARA_HEAD) || (firstCam.Flags & SCF_TRACKING_CAM)) { Camera.target.x = LaraItem->Pose.Position.x; Camera.target.y = LaraItem->Pose.Position.y; @@ -496,436 +684,290 @@ void CalculateSpotCameras() } else { - Camera.target.x = ctx; - Camera.target.y = cty; - Camera.target.z = ctz; + Camera.target.x = (int)interpTargetX; + Camera.target.y = (int)interpTargetY; + Camera.target.z = (int)interpTargetZ; CalculateBounce(false); } - - int outsideRoom = IsRoomOutside(cpx, cpy, cpz); + + // Resolve camera room number. + int outsideRoom = IsRoomOutside(Camera.pos.x, Camera.pos.y, Camera.pos.z); if (outsideRoom == NO_VALUE) { // HACK: Sometimes actual camera room number desyncs from room number derived using floordata functions. - // If such case is identified, we do a brute-force search for coherrent room number. + // If such case is identified, we do a brute-force search for coherent room number. // This issue is only present in sub-click floor height setups after TE 1.7.0. -- Lwmte, 02.11.2024 - + auto pos = Vector3i(Camera.pos.x, Camera.pos.y, Camera.pos.z); - int collRoomNumber = GetPointCollision(pos, SpotCam[CurrentSplineCamera].roomNumber).GetRoomNumber(); - + int collRoomNumber = GetPointCollision(pos, g_Level.SpotCams[CurrentCameraIndex].RoomNumber).GetRoomNumber(); + if (collRoomNumber != Camera.pos.RoomNumber && !IsPointInRoom(pos, collRoomNumber)) - collRoomNumber = FindRoomNumber(pos, SpotCam[CurrentSplineCamera].roomNumber); - + collRoomNumber = FindRoomNumber(pos, g_Level.SpotCams[CurrentCameraIndex].RoomNumber); + Camera.pos.RoomNumber = collRoomNumber; } else { Camera.pos.RoomNumber = outsideRoom; } - - AlterFOV(cfov, false); - - LookAt(&Camera, croll); + + AlterFOV((short)interpFOV, false); + LookAt(&Camera, (short)interpRoll); UpdateMikePos(*LaraItem); - - if (SpotCam[CurrentSplineCamera].flags & SCF_OVERLAY) + + // Apply per-camera flags. + if (g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_OVERLAY) SpotcamOverlay = true; - if (SpotCam[CurrentSplineCamera].flags & SCF_HIDE_LARA) + if (g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_HIDE_LARA) SpotcamDontDrawLara = true; - if (SpotCam[CurrentSplineCamera].flags & SCF_ACTIVATE_HEAVY_TRIGGERS) - CheckTrigger = true; - - if (CheckTrigger) + if (g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_ACTIVATE_HEAVY_TRIGGERS) + RunHeavyTriggers = true; + + TestVolumesAndTriggers(); + + // Tracking camera just sets init flag and returns. + if (firstCam.Flags & SCF_TRACKING_CAM) { - CameraType oldType = Camera.type; - Camera.type = CameraType::Heavy; - if (CurrentLevel != 0) - { - TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, true); - TestVolumes(&Camera); - } - else - { - TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, false); - TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, true); - TestVolumes(&Camera); - } - - Camera.type = oldType; - CheckTrigger = false; + TrackCameraInit = true; + return; } - - if (s->flags & SCF_TRACKING_CAM) + + // During active pause phases (ease-out, hold, ease-in active), skip + // segment-advance logic unless the hold timer just expired. + if (CurrentPausePhase != PausePhase::None && !advancedToNextSegment) + return; + + // Non-tracking: check if the spline segment is complete. + float normalizedSpeed = interpSpeed / (float)USHRT_MAX; + if (!advancedToNextSegment && SplineAlpha <= 1.0f - normalizedSpeed) + return; + + // Segment complete: advance to next camera. + SplineAlpha = 0.0f; + IsPauseComplete = false; + + int prevCamIndex = (CurrentCameraIndex != FirstCameraIndex) ? (CurrentCameraIndex - 1) : LastCameraIndex; + int knotStartIndex = 1; + + if (SplineFromOffset != 0) { - TrackCameraInit = true; + // First segment was from initial camera; now switch to spotcam-only spline. + SplineFromOffset = 0; + prevCamIndex = FirstCameraIndex - 1; + knotStartIndex = 2; // Leave knot[1] unchanged. } - else if (CurrentSplinePosition > 0x10000 - cspeed) + else { - if (SpotCam[CurrentSplineCamera].timer > 0 && - SpotCam[CurrentSplineCamera].flags & SCF_STOP_MOVEMENT) + if (g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_REENABLE_LARA_CONTROLS) + Lara.Control.IsLocked = false; + + if (g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_DISABLE_LARA_CONTROLS) { - if (!SpotcamTimer && !SpotcamPaused) - SpotcamTimer = SpotCam[CurrentSplineCamera].timer >> 3; + Lara.Control.IsLocked = true; + + if (CurrentLevel) + SetCinematicBars(SPOTCAM_CINEMATIC_BARS_HEIGHT, SPOTCAM_CINEMATIC_BARS_SPEED); } - - if (!SpotcamTimer) + + // Handle cut-to-cam: jump to a specific camera in the sequence. + if (g_Level.SpotCams[CurrentCameraIndex].Flags & SCF_CUT_TO_CAM) { - CurrentSplinePosition = 0; - - if (CurrentSplineCamera != FirstCamera) - cn = CurrentSplineCamera - 1; - else - cn = LastCamera; - - sp = 1; - - if (SplineFromCamera != 0) - { - SplineFromCamera = 0; - cn = FirstCamera - 1; - } - else - { - if (SpotCam[CurrentSplineCamera].flags & SCF_REENABLE_LARA_CONTROLS) - Lara.Control.IsLocked = false; - - if (SpotCam[CurrentSplineCamera].flags & SCF_DISABLE_LARA_CONTROLS) - { - if (CurrentLevel) - SetCinematicBars(SPOTCAM_CINEMATIC_BARS_HEIGHT, SPOTCAM_CINEMATIC_BARS_SPEED); - - Lara.Control.IsLocked = true; - } - - int sp2 = 0; - if (SpotCam[CurrentSplineCamera].flags & SCF_CUT_TO_CAM) - { - cn = FirstCamera + SpotCam[CurrentSplineCamera].timer; - - Camera.DisableInterpolation = true; - - CameraXposition[1] = SpotCam[cn].x; - CameraYposition[1] = SpotCam[cn].y; - CameraZposition[1] = SpotCam[cn].z; - CameraXtarget[1] = SpotCam[cn].tx; - CameraYtarget[1] = SpotCam[cn].ty; - CameraZtarget[1] = SpotCam[cn].tz; - CameraRoll[1] = SpotCam[cn].roll; - CameraFOV[1] = SpotCam[cn].fov; - CameraSpeed[1] = SpotCam[cn].speed; - sp2 = 1; - CurrentSplineCamera = cn; - } - - sp = sp2 + 1; - - CameraXposition[sp] = SpotCam[cn].x; - CameraYposition[sp] = SpotCam[cn].y; - CameraZposition[sp] = SpotCam[cn].z; - CameraXtarget[sp] = SpotCam[cn].tx; - CameraYtarget[sp] = SpotCam[cn].ty; - CameraZtarget[sp] = SpotCam[cn].tz; - CameraRoll[sp] = SpotCam[cn].roll; - CameraFOV[sp] = SpotCam[cn].fov; - CameraSpeed[sp] = SpotCam[cn].speed; - } - - cn++; - if (sp < 4) - { - while (sp < 4) - { - if (s->flags & SCF_LOOP_SEQUENCE) - { - if (LastCamera < cn) - cn = FirstCamera; - } - else - { - if (LastCamera < cn) - cn = LastCamera; - } - - CameraXposition[sp + 1] = SpotCam[cn].x; - CameraYposition[sp + 1] = SpotCam[cn].y; - CameraZposition[sp + 1] = SpotCam[cn].z; - CameraXtarget[sp + 1] = SpotCam[cn].tx; - CameraYtarget[sp + 1] = SpotCam[cn].ty; - CameraZtarget[sp + 1] = SpotCam[cn].tz; - CameraRoll[sp + 1] = SpotCam[cn].roll; - CameraFOV[sp + 1] = SpotCam[cn].fov; - CameraSpeed[sp + 1] = SpotCam[cn].speed; - cn++; - sp++; - } - } + int jumpTarget = FirstCameraIndex + g_Level.SpotCams[CurrentCameraIndex].Timer; - CurrentSplineCamera++; - SpotcamPaused = 0; - - if (LastCamera >= CurrentSplineCamera) - return; - - if (s->flags & SCF_LOOP_SEQUENCE) - { - CurrentSplineCamera = FirstCamera; - SpotcamLoopCnt++; - } - else if (s->flags & SCF_CUT_TO_LARA_CAM || SplineToCamera) + if (jumpTarget < FirstCameraIndex || jumpTarget > LastCameraIndex) { - if (CheckTrigger) - { - CameraType oldType = Camera.type; - Camera.type = CameraType::Heavy; - if (CurrentLevel) - { - TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, true); - TestVolumes(&Camera); - } - else - { - TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, false); - TestTriggers(Camera.pos.x, Camera.pos.y, Camera.pos.z, Camera.pos.RoomNumber, true); - TestVolumes(&Camera); - } - - Camera.type = oldType; - CheckTrigger = false; - } - - SetCinematicBars(0.0f, SPOTCAM_CINEMATIC_BARS_SPEED); - - UseSpotCam = false; - CheckTrigger = false; - Lara.Control.IsLocked = false; - Lara.Control.Look.IsUsingBinoculars = false; - Camera.oldType = CameraType::Fixed; - Camera.type = CameraType::Chase; - Camera.speed = 1; - Camera.DisableInterpolation = true; - - if (s->flags & SCF_CUT_TO_LARA_CAM) - { - Camera.pos.x = InitialCameraPosition.x; - Camera.pos.y = InitialCameraPosition.y; - Camera.pos.z = InitialCameraPosition.z; - Camera.pos.RoomNumber = InitialCameraRoom; - Camera.target.x = InitialCameraTarget.x; - Camera.target.y = InitialCameraTarget.y; - Camera.target.z = InitialCameraTarget.z; - } - - SpotcamOverlay = false; - SpotcamDontDrawLara = false; - AlterFOV(LastFOV); + TENLog(fmt::format("Flyby sequence {} has no camera with index {}. Check the flyby setup in the editor.", CurrentSequenceID, jumpTarget), LogLevel::Warning); + jumpTarget = std::clamp(jumpTarget, FirstCameraIndex, LastCameraIndex); } - else - { - CameraXposition[1] = SpotCam[CurrentSplineCamera - 1].x; - CameraYposition[1] = SpotCam[CurrentSplineCamera - 1].y; - CameraZposition[1] = SpotCam[CurrentSplineCamera - 1].z; - CameraXtarget[1] = SpotCam[CurrentSplineCamera - 1].tx; - CameraYtarget[1] = SpotCam[CurrentSplineCamera - 1].ty; - CameraZtarget[1] = SpotCam[CurrentSplineCamera - 1].tz; - CameraRoll[1] = SpotCam[CurrentSplineCamera - 1].roll; - CameraFOV[1] = SpotCam[CurrentSplineCamera - 1].fov; - CameraSpeed[1] = SpotCam[CurrentSplineCamera - 1].speed; - - CameraXposition[2] = SpotCam[CurrentSplineCamera - 1].x; - CameraYposition[2] = SpotCam[CurrentSplineCamera - 1].y; - CameraZposition[2] = SpotCam[CurrentSplineCamera - 1].z; - CameraXtarget[2] = SpotCam[CurrentSplineCamera - 1].tx; - CameraYtarget[2] = SpotCam[CurrentSplineCamera - 1].ty; - CameraZtarget[2] = SpotCam[CurrentSplineCamera - 1].tz; - CameraRoll[2] = SpotCam[CurrentSplineCamera - 1].roll; - CameraFOV[2] = SpotCam[CurrentSplineCamera - 1].fov; - CameraSpeed[2] = SpotCam[CurrentSplineCamera - 1].speed; - - memcpy((char*)& backup, (char*)& Camera, sizeof(CAMERA_INFO)); - Camera.oldType = CameraType::Fixed; - Camera.type = CameraType::Chase; - Camera.speed = 1; - - int elevation = Camera.targetElevation; - - CalculateCamera(LaraCollision); - - CameraRoll[2] = 0; - CameraRoll[3] = 0; - CameraSpeed[2] = CameraSpeed[1]; - - InitialCameraPosition.x = Camera.pos.x; - InitialCameraPosition.y = Camera.pos.y; - InitialCameraPosition.z = Camera.pos.z; - - InitialCameraTarget.x = Camera.target.x; - InitialCameraTarget.y = Camera.target.y; - InitialCameraTarget.z = Camera.target.z; - - CameraXposition[3] = Camera.pos.x; - CameraYposition[3] = Camera.pos.y; - CameraZposition[3] = Camera.pos.z; - CameraXtarget[3] = Camera.target.x; - CameraYtarget[3] = Camera.target.y; - CameraZtarget[3] = Camera.target.z; - CameraFOV[3] = LastFOV; - CameraSpeed[3] = CameraSpeed[2]; - CameraRoll[3] = 0; - - CameraXposition[4] = Camera.pos.x; - CameraYposition[4] = Camera.pos.y; - CameraZposition[4] = Camera.pos.z; - CameraXtarget[4] = Camera.target.x; - CameraYtarget[4] = Camera.target.y; - CameraZtarget[4] = Camera.target.z; - CameraFOV[4] = LastFOV; - CameraSpeed[4] = CameraSpeed[2] >> 1; - CameraRoll[4] = 0; - - memcpy((char*)& Camera, (char*)& backup, sizeof(CAMERA_INFO)); - - Camera.targetElevation = elevation; + + Knots.SetKnot(1, g_Level.SpotCams[jumpTarget]); + CurrentCameraIndex = jumpTarget; + prevCamIndex = jumpTarget; - LookAt(&Camera, croll); - UpdateMikePos(*LaraItem); - - SplineToCamera = 1; - } - - if (CurrentSplineCamera > LastCamera) - CurrentSplineCamera = LastCamera; - } - else - { - SpotcamTimer--; - if (!SpotcamTimer) - SpotcamPaused = 1; + knotStartIndex = 2; + Camera.DisableInterpolation = true; } + + knotStartIndex++; + Knots.SetKnot(knotStartIndex - 1, g_Level.SpotCams[prevCamIndex]); } - } - else if (s->flags & SCF_TRACKING_CAM) - { - if (!SpotCamFirstLook) + + // Fill remaining knots from subsequent cameras. + int nextCamIndex = prevCamIndex + 1; + bool isLooping = (firstCam.Flags & SCF_LOOP_SEQUENCE) != 0; + FillSplineKnots(knotStartIndex, nextCamIndex, 4 - (knotStartIndex - 1), isLooping); + + CurrentCameraIndex++; + IsPauseComplete = false; + + if (CurrentCameraIndex <= LastCameraIndex) + return; + + // Sequence ended. + if (firstCam.Flags & SCF_LOOP_SEQUENCE) { - Camera.oldType = CameraType::Fixed; - SpotCamFirstLook = true; + CurrentCameraIndex = FirstCameraIndex; + LoopCount++; + return; } - - CalculateCamera(LaraCollision); - } - else - { - SetScreenFadeIn(FADE_SCREEN_SPEED); - SetCinematicBars(0.0f, SPOTCAM_CINEMATIC_BARS_SPEED); - UseSpotCam = false; - Lara.Control.IsLocked = false; + + if ((firstCam.Flags & SCF_CUT_TO_LARA_CAM) || IsTransitionToGame) + { + EndSequence(firstCam); + return; + } + + // No explicit end flag: smoothly blend back to gameplay camera. + Knots.SetKnot(1, g_Level.SpotCams[CurrentCameraIndex - 1]); + Knots.SetKnot(2, g_Level.SpotCams[CurrentCameraIndex - 1]); + + CAMERA_INFO backup; + memcpy(&backup, &Camera, sizeof(CAMERA_INFO)); + + Camera.oldType = CameraType::Fixed; + Camera.type = CameraType::Chase; Camera.speed = 1; - AlterFOV(LastFOV); + + int savedElevation = Camera.targetElevation; CalculateCamera(LaraCollision); - CheckTrigger = false; - } -} - -// Core's version. Proper decompilation by ChocolateFan -// TODO: Replace with float-based version. -int Spline(int x, int* knots, int nk) -{ - int span = x * (nk - 3) >> 16; - if (span >= nk - 3) - span = nk - 4; - - int* k = &knots[span]; - x = x * (nk - 3) - span * 65536; - - int c1 = (k[1] >> 1) - (k[2] >> 1) - k[2] + k[1] + (k[3] >> 1) + ((-k[0] - 1) >> 1); - int c2 = 2 * k[2] - 2 * k[1] - (k[1] >> 1) - (k[3] >> 1) + k[0]; - - return ((__int64)x * (((__int64)x * (((__int64)x * c1 >> 16) + c2) >> 16) + (k[2] >> 1) + ((-k[0] - 1) >> 1)) >> 16) + k[1]; -} - -Pose GetCameraTransform(int sequence, float alpha, bool loop) -{ - constexpr auto BLEND_RANGE = 0.1f; - constexpr auto BLEND_START = BLEND_RANGE; - constexpr auto BLEND_END = 1.0f - BLEND_RANGE; - - alpha = std::clamp(alpha, 0.0f, 1.0f); - - if (sequence < 0 || SpotCamRemap.find(sequence) == SpotCamRemap.end()) - { - TENLog("Wrong flyby sequence number provided for getting camera coordinates.", LogLevel::Warning); - return Pose::Zero; - } - - // Retrieve camera count in sequence. - int cameraCount = CameraCnt[SpotCamRemap[sequence]]; - if (cameraCount < 2) - { - TENLog("Not enough cameras in flyby sequence to calculate the coordinates.", LogLevel::Warning); - return Pose::Zero; - } - - // Find first ID for sequence. - int firstSeqID = 0; - for (int i = 0; i < SpotCamRemap[sequence]; i++) - firstSeqID += CameraCnt[i]; - - // Determine number of spline points and spline position. - int splinePoints = cameraCount + 2; - int splineAlpha = int(alpha * (float)USHRT_MAX); - - // Extract camera properties into separate vectors for interpolation. - std::vector xOrigins, yOrigins, zOrigins, xTargets, yTargets, zTargets, rolls; - for (int i = -1; i < (cameraCount + 1); i++) - { - int seqID = std::clamp(firstSeqID + i, firstSeqID, (firstSeqID + cameraCount) - 1); - - xOrigins.push_back(SpotCam[seqID].x); - yOrigins.push_back(SpotCam[seqID].y); - zOrigins.push_back(SpotCam[seqID].z); - xTargets.push_back(SpotCam[seqID].tx); - yTargets.push_back(SpotCam[seqID].ty); - zTargets.push_back(SpotCam[seqID].tz); - rolls.push_back(SpotCam[seqID].roll); + + Knots.Roll[2] = 0.0f; + Knots.Roll[3] = 0.0f; + Knots.Speed[2] = Knots.Speed[1]; + + SavedCameraPos = Vector3i(Camera.pos.x, Camera.pos.y, Camera.pos.z); + SavedCameraTarget = Vector3i(Camera.target.x, Camera.target.y, Camera.target.z); + + Knots.PosX[3] = (float)Camera.pos.x; + Knots.PosY[3] = (float)Camera.pos.y; + Knots.PosZ[3] = (float)Camera.pos.z; + Knots.TargetX[3] = (float)Camera.target.x; + Knots.TargetY[3] = (float)Camera.target.y; + Knots.TargetZ[3] = (float)Camera.target.z; + Knots.FOV[3] = (float)LastFOV; + Knots.Speed[3] = Knots.Speed[2]; + Knots.Roll[3] = 0.0f; + + Knots.PosX[4] = (float)Camera.pos.x; + Knots.PosY[4] = (float)Camera.pos.y; + Knots.PosZ[4] = (float)Camera.pos.z; + Knots.TargetX[4] = (float)Camera.target.x; + Knots.TargetY[4] = (float)Camera.target.y; + Knots.TargetZ[4] = (float)Camera.target.z; + Knots.FOV[4] = (float)LastFOV; + Knots.Speed[4] = Knots.Speed[2] / 2.0f; + Knots.Roll[4] = 0.0f; + + memcpy(&Camera, &backup, sizeof(CAMERA_INFO)); + Camera.targetElevation = savedElevation; + + LookAt(&Camera, (short)interpRoll); + UpdateMikePos(*LaraItem); + + IsTransitionToGame = true; + + if (CurrentCameraIndex > LastCameraIndex) + CurrentCameraIndex = LastCameraIndex; } - - // Compute spline interpolation of main flyby camera parameters. - auto getInterpolatedPoint = [&](float t, std::vector& x, std::vector& y, std::vector& z) - { - int tAlpha = int(t * (float)USHRT_MAX); - return Vector3(Spline(tAlpha, x.data(), splinePoints), - Spline(tAlpha, y.data(), splinePoints), - Spline(tAlpha, z.data(), splinePoints)); - }; - - auto getInterpolatedRoll = [&](float t) - { - int tAlpha = int(t * (float)USHRT_MAX); - return Spline(tAlpha, rolls.data(), splinePoints); - }; - - auto origin = Vector3::Zero; - auto target = Vector3::Zero; - short orientZ = 0; - - // If loop is enabled and alpha is at sequence start or end, blend between last and first cameras. - if (loop && (alpha < BLEND_START || alpha >= BLEND_END)) + + Pose GetSpotCamSequenceTransform(int sequence, float alpha, bool loop) { - float blendFactor = (alpha < BLEND_START) ? (0.5f + ((alpha / BLEND_RANGE) * 0.5f)) : (((alpha - BLEND_END) / BLEND_START) * 0.5f); + constexpr auto BLEND_RANGE = 0.1f; + constexpr auto BLEND_START = BLEND_RANGE; + constexpr auto BLEND_END = 1.0f - BLEND_RANGE; + + alpha = std::clamp(alpha, 0.0f, 1.0f); + + if (sequence < 0 || !HasSpotCamSequence(sequence)) + { + TENLog(fmt::format("Wrong flyby sequence number {} provided for getting camera coordinates.", sequence), LogLevel::Warning); + return Pose::Zero; + } - origin = Vector3::Lerp(getInterpolatedPoint(BLEND_END, xOrigins, yOrigins, zOrigins), getInterpolatedPoint(BLEND_START, xOrigins, yOrigins, zOrigins), blendFactor); - target = Vector3::Lerp(getInterpolatedPoint(BLEND_END, xTargets, yTargets, zTargets), getInterpolatedPoint(BLEND_START, xTargets, yTargets, zTargets), blendFactor); - orientZ = Lerp(getInterpolatedRoll(BLEND_END), getInterpolatedRoll(BLEND_START), blendFactor); - } - else - { - origin = getInterpolatedPoint(alpha, xOrigins, yOrigins, zOrigins); - target = getInterpolatedPoint(alpha, xTargets, yTargets, zTargets); - orientZ = getInterpolatedRoll(alpha); + int cameraCount = GetSequenceCameraCount(sequence); + if (cameraCount < 2) + { + TENLog(fmt::format("Not enough cameras in flyby sequence {} to calculate the coordinates.", sequence), LogLevel::Warning); + return Pose::Zero; + } + + // Find first camera index for this sequence. + int firstIndex = GetSequenceFirstCameraIndex(sequence); + if (firstIndex == NO_VALUE) + { + TENLog(fmt::format("First camera index is incorrect in flyby sequence {}.", sequence), LogLevel::Warning); + return Pose::Zero; + } + + int splinePoints = cameraCount + 2; + + // Build float arrays for spline interpolation. + std::vector xOrigins, yOrigins, zOrigins, xTargets, yTargets, zTargets, rolls; + xOrigins.reserve(splinePoints); + yOrigins.reserve(splinePoints); + zOrigins.reserve(splinePoints); + xTargets.reserve(splinePoints); + yTargets.reserve(splinePoints); + zTargets.reserve(splinePoints); + rolls.reserve(splinePoints); + + for (int i = -1; i < (cameraCount + 1); i++) + { + int seqID = std::clamp(firstIndex + i, firstIndex, (firstIndex + cameraCount) - 1); + + xOrigins.push_back((float)g_Level.SpotCams[seqID].Position.x); + yOrigins.push_back((float)g_Level.SpotCams[seqID].Position.y); + zOrigins.push_back((float)g_Level.SpotCams[seqID].Position.z); + xTargets.push_back((float)g_Level.SpotCams[seqID].Target.x); + yTargets.push_back((float)g_Level.SpotCams[seqID].Target.y); + zTargets.push_back((float)g_Level.SpotCams[seqID].Target.z); + rolls.push_back((float)g_Level.SpotCams[seqID].Roll); + } + + auto getInterpolatedPoint = [&](float t, std::vector& x, std::vector& y, std::vector& z) + { + return Vector3(Spline(t, x.data(), splinePoints), + Spline(t, y.data(), splinePoints), + Spline(t, z.data(), splinePoints)); + }; + + auto getInterpolatedRoll = [&](float t) + { + return Spline(t, rolls.data(), splinePoints); + }; + + Vector3 originPos, targetPos; + short orientZ = 0; + + // If looping and alpha is near sequence boundaries, blend between end and start. + if (loop && (alpha < BLEND_START || alpha >= BLEND_END)) + { + float blendFactor = (alpha < BLEND_START) ? (0.5f + (alpha / BLEND_RANGE) * 0.5f) : ((alpha - BLEND_END) / BLEND_START) * 0.5f; + + originPos = Vector3::Lerp( + getInterpolatedPoint(BLEND_END, xOrigins, yOrigins, zOrigins), + getInterpolatedPoint(BLEND_START, xOrigins, yOrigins, zOrigins), + blendFactor); + + targetPos = Vector3::Lerp( + getInterpolatedPoint(BLEND_END, xTargets, yTargets, zTargets), + getInterpolatedPoint(BLEND_START, xTargets, yTargets, zTargets), + blendFactor); + + orientZ = (short)Lerp(getInterpolatedRoll(BLEND_END), getInterpolatedRoll(BLEND_START), blendFactor); + } + else + { + originPos = getInterpolatedPoint(alpha, xOrigins, yOrigins, zOrigins); + targetPos = getInterpolatedPoint(alpha, xTargets, yTargets, zTargets); + orientZ = (short)getInterpolatedRoll(alpha); + } + + auto pose = Pose(originPos, EulerAngles(targetPos - originPos)); + pose.Orientation.z = orientZ; + return pose; } - auto pose = Pose(origin, EulerAngles(target - origin)); - pose.Orientation.z = orientZ; - return pose; -} +} // namespace TEN::SpotCam diff --git a/TombEngine/Game/spotcam.h b/TombEngine/Game/spotcam.h index 114b7be843..6d220fb490 100644 --- a/TombEngine/Game/spotcam.h +++ b/TombEngine/Game/spotcam.h @@ -1,69 +1,64 @@ #pragma once -#include - #include "Math/Math.h" #include "Specific/clock.h" -constexpr auto SPOTCAM_CINEMATIC_BARS_HEIGHT = 1.0f / 16; -constexpr auto SPOTCAM_CINEMATIC_BARS_SPEED = 1.0f; - class Pose; -#pragma pack(push, 1) -struct SPOTCAM +namespace TEN::SpotCam { - int x; - int y; - int z; - int tx; - int ty; - int tz; - int sequence; - int camera; - short fov; - short roll; - short timer; - short speed; - short flags; - int roomNumber; -}; -#pragma pack(pop) + constexpr auto SPOTCAM_CINEMATIC_BARS_HEIGHT = 1.0f / 16; + constexpr auto SPOTCAM_CINEMATIC_BARS_SPEED = 1.0f; -enum SPOTCAM_FLAGS -{ - SCF_CUT_PAN = (1 << 0), // Cut without panning smoothly. - SCF_OVERLAY = (1 << 1), // TODO: Add vignette. - SCF_LOOP_SEQUENCE = (1 << 2), - SCF_TRACKING_CAM = (1 << 3), - SCF_HIDE_LARA = (1 << 4), - SCF_FOCUS_LARA_HEAD = (1 << 5), - SCF_CUT_TO_LARA_CAM = (1 << 6), - SCF_CUT_TO_CAM = (1 << 7), - SCF_STOP_MOVEMENT = (1 << 8), // Stop movement for a given time (cf. `Timer` field). - SCF_DISABLE_BREAKOUT = (1 << 9), // Disable breaking out from cutscene using LOOK key. - SCF_DISABLE_LARA_CONTROLS = (1 << 10), // Also add widescreen bars. - SCF_REENABLE_LARA_CONTROLS = (1 << 11), // Used with 0x0400, keeps widescreen bars. - SCF_SCREEN_FADE_IN = (1 << 12), - SCF_SCREEN_FADE_OUT = (1 << 13), - SCF_ACTIVATE_HEAVY_TRIGGERS = (1 << 14), // When camera is moving above heavy trigger sector, it will be activated. - SCF_CAMERA_ONE_SHOT = (1 << 15), -}; + struct SpotCamInfo + { + Vector3i Position = Vector3i::Zero; + Vector3i Target = Vector3i::Zero; + int RoomNumber = 0; + + int Sequence = 0; + int Camera = 0; + + short FOV = 0; + short Roll = 0; + short Timer = 0; + short Speed = 0; + short Flags = 0; + }; + + enum SpotCamFlags + { + SCF_CUT_PAN = (1 << 0), // Cut without panning smoothly. + SCF_OVERLAY = (1 << 1), // TODO: Add vignette. + SCF_LOOP_SEQUENCE = (1 << 2), + SCF_TRACKING_CAM = (1 << 3), + SCF_HIDE_LARA = (1 << 4), + SCF_FOCUS_LARA_HEAD = (1 << 5), + SCF_CUT_TO_LARA_CAM = (1 << 6), + SCF_CUT_TO_CAM = (1 << 7), + SCF_STOP_MOVEMENT = (1 << 8), // Stop movement for a given time (cf. `Timer` field). + SCF_DISABLE_BREAKOUT = (1 << 9), // Disable breaking out from cutscene using LOOK key. + SCF_DISABLE_LARA_CONTROLS = (1 << 10), // Also add widescreen bars. + SCF_REENABLE_LARA_CONTROLS = (1 << 11), // Used with 0x0400, keeps widescreen bars. + SCF_SCREEN_FADE_IN = (1 << 12), + SCF_SCREEN_FADE_OUT = (1 << 13), + SCF_ACTIVATE_HEAVY_TRIGGERS = (1 << 14), // When camera is moving above heavy trigger sector, it will be activated. + SCF_CAMERA_ONE_SHOT = (1 << 15), + }; -extern std::vector SpotCam; -extern std::unordered_map SpotCamRemap; -extern std::vector CameraCnt; -extern int LastSpotCamSequence; -extern bool UseSpotCam; -extern bool SpotcamSwitched; -extern bool SpotcamDontDrawLara; -extern bool SpotcamOverlay; -extern bool TrackCameraInit; + extern int LastSpotCamSequence; + extern bool UseSpotCam; + extern bool SpotcamSwitched; + extern bool SpotcamDontDrawLara; + extern bool SpotcamOverlay; + extern bool TrackCameraInit; -void ClearSpotCamSequences(); -void InitializeSpotCamSequences(bool startFirstSequence); -void InitializeSpotCam(short sequence); -void CalculateSpotCameras(); -int Spline(int x, int* knots, int nk); + bool HasSpotCamSequence(int sequence); + int GetSequenceFirstCameraIndex(int sequence); -Pose GetCameraTransform(int sequence, float alpha, bool loop); + void ClearSpotCamSequences(); + void InitializeSpotCamSequences(bool startFirstSequence); + void InitializeSpotCam(short sequence); + void CalculateSpotCam(); + Pose GetSpotCamSequenceTransform(int sequence, float alpha, bool loop); +} diff --git a/TombEngine/Math/Constants.h b/TombEngine/Math/Constants.h index a43ba00a81..f066e55eb6 100644 --- a/TombEngine/Math/Constants.h +++ b/TombEngine/Math/Constants.h @@ -22,6 +22,13 @@ constexpr auto BOX_EDGE_COUNT = 12; constexpr auto BOX_FACE_COUNT = 6; + // Graphics constants + + constexpr auto NEUTRAL_COLOR = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + constexpr auto GAMMA_MIN = 0.5f; + constexpr auto GAMMA_MAX = 1.5f; + constexpr auto GAMMA_STEP = 0.1f; + // World constants constexpr auto BLOCK_UNIT = 1024; diff --git a/TombEngine/Math/Utils.cpp b/TombEngine/Math/Utils.cpp index 894fa792d5..35560e3e6a 100644 --- a/TombEngine/Math/Utils.cpp +++ b/TombEngine/Math/Utils.cpp @@ -89,6 +89,29 @@ namespace TEN::Math return EaseInOutSine(0.0f, 1.0f, alpha); } + float Spline(float alpha, const float* knots, int knotCount) + { + if (!knots || knotCount < 4) + return 0.0f; + + alpha = std::clamp(alpha, 0.0f, 1.0f); + + int segmentCount = knotCount - 3; + + int segmentIndex = (int)(alpha * segmentCount); + segmentIndex = std::min(segmentIndex, segmentCount - 1); + + const float* knot = &knots[segmentIndex]; + float segmentPos = alpha * segmentCount - (float)segmentIndex; + + float cCube = (-knot[0] + 3.0f * knot[1] - 3.0f * knot[2] + knot[3]) * 0.5f; + float cQuad = knot[0] - 2.5f * knot[1] + 2.0f * knot[2] - 0.5f * knot[3]; + float cLinear = (knot[2] - knot[0]) * 0.5f; + float cConst = knot[1]; + + return segmentPos * (segmentPos * (segmentPos * cCube + cQuad) + cLinear) + cConst; + } + float Luma(const Vector3& color) { constexpr auto RED_COEFF = 0.2126f; @@ -172,11 +195,8 @@ namespace TEN::Math return Vector4(result.x, result.y, result.z, ambient.w * tint.w); } - Vector4 VectorColorToRGBA_TempToVector4(Vector4 c) + unsigned int VectorColorToRGBA(Vector4 c) { - return c; - - /* auto to8 = [](float v) -> unsigned int { float x = std::clamp(v, 0.0f, 1.0f) * 255.0f; return static_cast(std::lround(x)); @@ -187,6 +207,6 @@ namespace TEN::Math unsigned int B = to8(c.z); unsigned int A = to8(c.w); - return (R) | (G << 8) | (B << 16) | (A << 24);*/ + return (R) | (G << 8) | (B << 16) | (A << 24); } } diff --git a/TombEngine/Math/Utils.h b/TombEngine/Math/Utils.h index c2e00bbc5e..b813021597 100644 --- a/TombEngine/Math/Utils.h +++ b/TombEngine/Math/Utils.h @@ -24,14 +24,16 @@ namespace TEN::Math float EaseOutSine(float alpha); float EaseInOutSine(float value0, float value1, float alpha); float EaseInOutSine(float alpha); + float Spline(float alpha, const float* knots, int knotCount); // Color - float Luma(const Vector3& color); - float Chroma(const Vector3& color); - Vector3 Screen(const Vector3& ambient, const Vector3& tint); - Vector4 Screen(const Vector4& ambient, const Vector4& tint); - Vector4 VectorColorToRGBA_TempToVector4(Vector4 c); + float Luma(const Vector3& color); + float Chroma(const Vector3& color); + Vector3 Screen(const Vector3& ambient, const Vector3& tint); + Vector4 Screen(const Vector4& ambient, const Vector4& tint); + unsigned int VectorColorToRGBA(Vector4 c); + std::pair, std::array> GenerateColorShift(Vector3 mainColor, Vector3 secondColor); } diff --git a/TombEngine/Objects/Effects/EmberEmitter.cpp b/TombEngine/Objects/Effects/EmberEmitter.cpp index 850064dfd2..f315f74ff1 100644 --- a/TombEngine/Objects/Effects/EmberEmitter.cpp +++ b/TombEngine/Objects/Effects/EmberEmitter.cpp @@ -21,9 +21,9 @@ namespace TEN::Effects::EmberEmitter unsigned char b = 0; float brightnessShift = Random::GenerateFloat(-0.1f, 0.1f); - r = std::clamp(item.Model.Color.x / 2.0f + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; - g = std::clamp(item.Model.Color.y / 2.0f + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; - b = std::clamp(item.Model.Color.z / 2.0f + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; + r = std::clamp(item.Model.Color.x + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; + g = std::clamp(item.Model.Color.y + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; + b = std::clamp(item.Model.Color.z + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; if (item.TriggerFlags < 0) { diff --git a/TombEngine/Objects/Effects/Fireflies.cpp b/TombEngine/Objects/Effects/Fireflies.cpp index 52a0671db9..29be62440b 100644 --- a/TombEngine/Objects/Effects/Fireflies.cpp +++ b/TombEngine/Objects/Effects/Fireflies.cpp @@ -76,9 +76,9 @@ namespace TEN::Effects::Fireflies if (triggerFlags >= 0) { float brightnessShift = Random::GenerateFloat(-0.1f, 0.1f); - r = std::clamp(item.Model.Color.x / 2.0f + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; - g = std::clamp(item.Model.Color.y / 2.0f + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; - b = std::clamp(item.Model.Color.z / 2.0f + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; + r = std::clamp(item.Model.Color.x + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; + g = std::clamp(item.Model.Color.y + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; + b = std::clamp(item.Model.Color.z + brightnessShift, 0.0f, 1.0f) * UCHAR_MAX; firefly.SpriteSeqID = ID_FIREFLY_SPRITES; firefly.SpriteID = 0; diff --git a/TombEngine/Objects/Effects/enemy_missile.cpp b/TombEngine/Objects/Effects/enemy_missile.cpp index 315ae8d135..129139b0a5 100644 --- a/TombEngine/Objects/Effects/enemy_missile.cpp +++ b/TombEngine/Objects/Effects/enemy_missile.cpp @@ -120,7 +120,7 @@ namespace TEN::Entities::Effects { ShatterItem.yRot = fx->pos.Orientation.y; ShatterItem.meshIndex = fx->frameNumber; - ShatterItem.color = Vector4::One; + ShatterItem.color = NEUTRAL_COLOR; ShatterItem.sphere.Center = fx->pos.Position.ToVector3(); ShatterItem.bit = 0; ShatterItem.flags = fx->flag2 & 0x400; diff --git a/TombEngine/Objects/Effects/flame_emitters.cpp b/TombEngine/Objects/Effects/flame_emitters.cpp index f456bd27da..9a4b4df63b 100644 --- a/TombEngine/Objects/Effects/flame_emitters.cpp +++ b/TombEngine/Objects/Effects/flame_emitters.cpp @@ -81,7 +81,7 @@ namespace TEN::Entities::Effects if (itemPtr->IsLara() && GetLaraInfo(item)->Control.WaterStatus == WaterStatus::FlyCheat) continue; - if (item->Model.Color == Vector4::One) + if (item->Model.Color == NEUTRAL_COLOR) { ItemBurn(itemPtr, itemPtr->IsLara() ? NO_VALUE : FLAME_ITEM_BURN_TIMEOUT); } @@ -132,10 +132,10 @@ namespace TEN::Entities::Effects static Vector4 GetFlameColor(Vector4 sourceColor) { - if (sourceColor == Vector4::One) + if (sourceColor == NEUTRAL_COLOR) return Vector4(1.0f, Random::GenerateFloat(0.3f, 0.4f), 0.1f, 1.0f) * UCHAR_MAX; - return sourceColor / 2.0f * Random::GenerateFloat(0.85f, 1.0f) * UCHAR_MAX; + return sourceColor * Random::GenerateFloat(0.85f, 1.0f) * UCHAR_MAX; } void FlameEmitterControl(short itemNumber) diff --git a/TombEngine/Objects/Generic/Object/burning_torch.cpp b/TombEngine/Objects/Generic/Object/burning_torch.cpp index ec02a286c2..579f22fa44 100644 --- a/TombEngine/Objects/Generic/Object/burning_torch.cpp +++ b/TombEngine/Objects/Generic/Object/burning_torch.cpp @@ -79,18 +79,18 @@ namespace TEN::Entities::Generic static Vector3 GetStartTorchColor(Vector3 sourceColor) { - if (sourceColor == Vector3::One) + if (sourceColor == (Vector3)NEUTRAL_COLOR) return Vector3(1.0f, Random::GenerateFloat(0.3f, 0.4f), 0.1f); - return sourceColor / 2.0f * Random::GenerateFloat(0.85f, 1.0f); + return sourceColor * Random::GenerateFloat(0.85f, 1.0f); } static Vector3 GetEndTorchColor(Vector3 sourceColor) { - if (sourceColor == Vector3::One) + if (sourceColor == (Vector3)NEUTRAL_COLOR) return Vector3(Random::GenerateFloat(-0.25f, -0.10f), Random::GenerateFloat(-0.45f, -0.25f), 0.1f); - return sourceColor / 2.0f * Random::GenerateFloat(0.35f, 0.45f); + return sourceColor * Random::GenerateFloat(0.35f, 0.45f); } void DoFlameTorch() diff --git a/TombEngine/Objects/TR1/Trap/ElectricBall.cpp b/TombEngine/Objects/TR1/Trap/ElectricBall.cpp index 08facf5d0f..d286e18b27 100644 --- a/TombEngine/Objects/TR1/Trap/ElectricBall.cpp +++ b/TombEngine/Objects/TR1/Trap/ElectricBall.cpp @@ -188,6 +188,16 @@ namespace TEN::Entities::Traps constexpr auto SPAWN_RADIUS = BLOCK(0.30f); constexpr auto RAYGUN_SMOKE_LIFE = 16.0f; + auto chargeChannel = [](float value, float step) + { + constexpr auto OVERBRIGHT_MAX = 8.0f; + + float normalizedValue = std::clamp(value / OVERBRIGHT_MAX, 0.0f, 1.0f); + normalizedValue = std::min(normalizedValue + (step / OVERBRIGHT_MAX), 1.0f); + + return normalizedValue * OVERBRIGHT_MAX; + }; + auto offset = Vector3::Zero; auto origin = GameVector(GetJointPosition(&item, bite), item.RoomNumber); auto targetRandom = Vector3i::Zero; @@ -338,34 +348,9 @@ namespace TEN::Entities::Traps if (item.ItemFlags[3] < 30) { - int intensity = 0.01f; - - if (item.Model.Color.x < 4.0f) - { - item.Model.Color.x += 0.05f; - } - else - { - item.Model.Color.x = 4.0f; - } - - if (item.Model.Color.y < 4.0f) - { - item.Model.Color.y += 0.05f; - } - else - { - item.Model.Color.y = 4.0f; - } - - if (item.Model.Color.z < 4.0f) - { - item.Model.Color.z += 0.09f; - } - else - { - item.Model.Color.z = 4.0f; - } + item.Model.Color.x = chargeChannel(item.Model.Color.x, 0.05f); + item.Model.Color.y = chargeChannel(item.Model.Color.y, 0.05f); + item.Model.Color.z = chargeChannel(item.Model.Color.z, 0.09f); SoundEffect(SFX_TR5_GOD_HEAD_CHARGE, &item.Pose); } diff --git a/TombEngine/Objects/TR2/Entity/tr2_knife_thrower.cpp b/TombEngine/Objects/TR2/Entity/tr2_knife_thrower.cpp index 76e701f797..ceaf70053e 100644 --- a/TombEngine/Objects/TR2/Entity/tr2_knife_thrower.cpp +++ b/TombEngine/Objects/TR2/Entity/tr2_knife_thrower.cpp @@ -86,7 +86,7 @@ namespace TEN::Entities::Creatures::TR2 fx.speed = vel; fx.fallspeed = 0; fx.flag2 = KNIFE_PROJECTILE_DAMAGE; - fx.color = Vector4::One; + fx.color = NEUTRAL_COLOR; return fxNumber; } diff --git a/TombEngine/Objects/TR3/Trap/FirePendulum.cpp b/TombEngine/Objects/TR3/Trap/FirePendulum.cpp index 94a28904a6..3012390f3d 100644 --- a/TombEngine/Objects/TR3/Trap/FirePendulum.cpp +++ b/TombEngine/Objects/TR3/Trap/FirePendulum.cpp @@ -166,7 +166,7 @@ namespace TEN::Entities::Traps r += 125 - ((GetRandomControl() / 16) & 4); g += 98 - ((GetRandomControl() / 16) & 8); - auto color = Color(r / (float)CHAR_MAX, g / (float)CHAR_MAX, b / (float)CHAR_MAX); + auto color = Color(r / (float)UCHAR_MAX, g / (float)UCHAR_MAX, b / (float)UCHAR_MAX); if (item.TriggerFlags) SpawnDynamicFogBulb(pos.ToVector3(), PENDULUM_FIRE_FOG_RADIUS, PENDULUM_FIRE_FOG_DENSITY, color); diff --git a/TombEngine/Objects/TR4/Entity/tr4_enemy_jeep.cpp b/TombEngine/Objects/TR4/Entity/tr4_enemy_jeep.cpp index 52b0b71d27..bbe57bce06 100644 --- a/TombEngine/Objects/TR4/Entity/tr4_enemy_jeep.cpp +++ b/TombEngine/Objects/TR4/Entity/tr4_enemy_jeep.cpp @@ -32,7 +32,7 @@ namespace TEN::Entities::TR4 { auto* grenadeItem = &g_Level.Items[grenadeItemNumber]; - grenadeItem->Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + grenadeItem->Model.Color = NEUTRAL_COLOR; grenadeItem->ObjectNumber = ID_GRENADE; grenadeItem->RoomNumber = item->RoomNumber; diff --git a/TombEngine/Objects/TR4/Entity/tr4_sas.cpp b/TombEngine/Objects/TR4/Entity/tr4_sas.cpp index 2db0f9e215..3ef2392d37 100644 --- a/TombEngine/Objects/TR4/Entity/tr4_sas.cpp +++ b/TombEngine/Objects/TR4/Entity/tr4_sas.cpp @@ -658,7 +658,7 @@ namespace TEN::Entities::TR4 auto grenadeItem = &g_Level.Items[itemNumber]; - grenadeItem->Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + grenadeItem->Model.Color = NEUTRAL_COLOR; grenadeItem->ObjectNumber = ID_GRENADE; grenadeItem->RoomNumber = item.RoomNumber; diff --git a/TombEngine/Objects/TR4/Entity/tr4_skeleton.cpp b/TombEngine/Objects/TR4/Entity/tr4_skeleton.cpp index 3a468a56d3..fb18633d2b 100644 --- a/TombEngine/Objects/TR4/Entity/tr4_skeleton.cpp +++ b/TombEngine/Objects/TR4/Entity/tr4_skeleton.cpp @@ -159,7 +159,7 @@ namespace TEN::Entities::TR4 fx->fallspeed = -(GetRandomControl() / 1024); fx->frameNumber = Objects[103].meshIndex; fx->objectNumber = ID_BODY_PART; - fx->color = Vector4::One; + fx->color = NEUTRAL_COLOR; fx->flag2 = 0x601; auto* spark = GetFreeParticle(); diff --git a/TombEngine/Objects/TR5/Emitter/Waterfall.cpp b/TombEngine/Objects/TR5/Emitter/Waterfall.cpp index b069410720..1f550ef967 100644 --- a/TombEngine/Objects/TR5/Emitter/Waterfall.cpp +++ b/TombEngine/Objects/TR5/Emitter/Waterfall.cpp @@ -39,6 +39,7 @@ namespace TEN::Effects::WaterfallEmitter constexpr auto WATERFALL_SPLASH_SPRITE_ID = 0; constexpr auto WATERFALL_STREAM_1_SPRITE_ID = 1; constexpr auto WATERFALL_STREAM_2_SPRITE_ID = 2; + constexpr auto WATERFALL_COLOR_SCALE = 4; void InitializeWaterfall(short itemNumber) { @@ -81,8 +82,7 @@ namespace TEN::Effects::WaterfallEmitter float waterfallWidth = std::max(CLICK(float(item.TriggerFlags)), WATERFALL_DEFAULT_WIDTH); auto vel = item.Pose.Orientation.ToDirection() * BLOCK(customVel); - auto startColor = (item.Model.Color / 4) * SCHAR_MAX; - auto endColor = (item.Model.Color / 8) * UCHAR_MAX; + auto startColor = (item.Model.Color / WATERFALL_COLOR_SCALE) * UCHAR_MAX; auto lastOffset = Vector3(FLT_MAX); auto lastTargetPos = Vector3::Zero; @@ -191,9 +191,9 @@ namespace TEN::Effects::WaterfallEmitter part.sR = std::clamp((int)startColor.x + colorOffset, 0, UCHAR_MAX); part.sG = std::clamp((int)startColor.y + colorOffset, 0, UCHAR_MAX); part.sB = std::clamp((int)startColor.z + colorOffset, 0, UCHAR_MAX); - part.dR = std::clamp((int)endColor.x + colorOffset, 0, UCHAR_MAX); - part.dG = std::clamp((int)endColor.y + colorOffset, 0, UCHAR_MAX); - part.dB = std::clamp((int)endColor.z + colorOffset, 0, UCHAR_MAX); + part.dR = std::clamp((int)startColor.x + colorOffset, 0, UCHAR_MAX); + part.dG = std::clamp((int)startColor.y + colorOffset, 0, UCHAR_MAX); + part.dB = std::clamp((int)startColor.z + colorOffset, 0, UCHAR_MAX); part.roomNumber = part.roomNumber; part.colFadeSpeed = 2; @@ -217,7 +217,6 @@ namespace TEN::Effects::WaterfallEmitter auto colorOffset = Vector3i(40.0f, 40.0f, 40.0f); auto startColor = (Vector3i(color.x, color.y, color.z) + colorOffset); - auto endColor = (Vector3i(color.x, color.y, color.z) + colorOffset); part.on = true; @@ -241,9 +240,9 @@ namespace TEN::Effects::WaterfallEmitter part.sR = std::clamp((int)startColor.x + colorVariation, 0, UCHAR_MAX); part.sG = std::clamp((int)startColor.y + colorVariation, 0, UCHAR_MAX); part.sB = std::clamp((int)startColor.z + colorVariation, 0, UCHAR_MAX); - part.dR = std::clamp((int)endColor.x + colorVariation, 0, UCHAR_MAX); - part.dG = std::clamp((int)endColor.y + colorVariation, 0, UCHAR_MAX); - part.dB = std::clamp((int)endColor.z + colorVariation, 0, UCHAR_MAX); + part.dR = std::clamp((int)startColor.x + colorVariation, 0, UCHAR_MAX); + part.dG = std::clamp((int)startColor.y + colorVariation, 0, UCHAR_MAX); + part.dB = std::clamp((int)startColor.z + colorVariation, 0, UCHAR_MAX); part.colFadeSpeed = 1; part.blendMode = BlendMode::Additive; diff --git a/TombEngine/Objects/TR5/Emitter/tr5_smoke_emitter.cpp b/TombEngine/Objects/TR5/Emitter/tr5_smoke_emitter.cpp index 592af4c305..a618dd83f0 100644 --- a/TombEngine/Objects/TR5/Emitter/tr5_smoke_emitter.cpp +++ b/TombEngine/Objects/TR5/Emitter/tr5_smoke_emitter.cpp @@ -85,9 +85,9 @@ namespace TEN::Effects::SmokeEmitter } else { - unsigned char r = std::clamp(item.Model.Color.x / 2, 0.0f, 1.0f) * UCHAR_MAX; - unsigned char g = std::clamp(item.Model.Color.y / 2, 0.0f, 1.0f) * UCHAR_MAX; - unsigned char b = std::clamp(item.Model.Color.z / 2, 0.0f, 1.0f) * UCHAR_MAX; + unsigned char r = std::clamp(item.Model.Color.x, 0.0f, 1.0f) * UCHAR_MAX; + unsigned char g = std::clamp(item.Model.Color.y, 0.0f, 1.0f) * UCHAR_MAX; + unsigned char b = std::clamp(item.Model.Color.z, 0.0f, 1.0f) * UCHAR_MAX; part.sR = r / 3; part.sG = g / 3; @@ -181,9 +181,9 @@ namespace TEN::Effects::SmokeEmitter } else { - unsigned char r = std::clamp(item.Model.Color.x / 2.0f, 0.0f, 1.0f) * UCHAR_MAX; - unsigned char g = std::clamp(item.Model.Color.y / 2.0f, 0.0f, 1.0f) * UCHAR_MAX; - unsigned char b = std::clamp(item.Model.Color.z / 2.0f, 0.0f, 1.0f) * UCHAR_MAX; + unsigned char r = std::clamp(item.Model.Color.x, 0.0f, 1.0f) * UCHAR_MAX; + unsigned char g = std::clamp(item.Model.Color.y, 0.0f, 1.0f) * UCHAR_MAX; + unsigned char b = std::clamp(item.Model.Color.z, 0.0f, 1.0f) * UCHAR_MAX; part.dR = r; part.dG = g; diff --git a/TombEngine/Objects/TR5/Entity/tr5_imp.cpp b/TombEngine/Objects/TR5/Entity/tr5_imp.cpp index 0192a9c9ce..22840fbdd6 100644 --- a/TombEngine/Objects/TR5/Entity/tr5_imp.cpp +++ b/TombEngine/Objects/TR5/Entity/tr5_imp.cpp @@ -129,7 +129,7 @@ namespace TEN::Entities::Creatures::TR5 fx.speed = BLOCK(0.25f); fx.fallspeed = 0; - fx.color = Vector4::One; + fx.color = NEUTRAL_COLOR; fx.counter = 0; fx.flag1 = 2; fx.flag2 = 0x2000; diff --git a/TombEngine/Objects/TR5/Entity/tr5_roman_statue.cpp b/TombEngine/Objects/TR5/Entity/tr5_roman_statue.cpp index 80dc548941..32acf91b1d 100644 --- a/TombEngine/Objects/TR5/Entity/tr5_roman_statue.cpp +++ b/TombEngine/Objects/TR5/Entity/tr5_roman_statue.cpp @@ -95,7 +95,7 @@ namespace TEN::Entities::Creatures::TR5 fx->speed = 1; fx->fallspeed = 0; fx->objectNumber = ID_BODY_PART; - fx->color = Vector4::One; + fx->color = NEUTRAL_COLOR; fx->flag2 = 9729; fx->frameNumber = Objects[ID_BUBBLES].meshIndex + (GetRandomControl() & 7); fx->counter = 0; diff --git a/TombEngine/Objects/TR5/Entity/tr5_submarine.cpp b/TombEngine/Objects/TR5/Entity/tr5_submarine.cpp index 5cb281a496..ad76c1da59 100644 --- a/TombEngine/Objects/TR5/Entity/tr5_submarine.cpp +++ b/TombEngine/Objects/TR5/Entity/tr5_submarine.cpp @@ -149,7 +149,7 @@ namespace TEN::Entities::Creatures::TR5 SoundEffect(SFX_TR5_UNDERWATER_TORPEDO, &torpedoItem->Pose, SoundEnvironment::Always); torpedoItem->ObjectNumber = ID_TORPEDO; - torpedoItem->Model.Color = Vector4(0.5f, 0.5f, 0.5f, 1.0f); + torpedoItem->Model.Color = NEUTRAL_COLOR; auto pos1 = Vector3i::Zero; auto pos2 = Vector3i::Zero; diff --git a/TombEngine/Objects/TR5/Light/tr5_light.cpp b/TombEngine/Objects/TR5/Light/tr5_light.cpp index 1637ce95f5..b3c1df9b4b 100644 --- a/TombEngine/Objects/TR5/Light/tr5_light.cpp +++ b/TombEngine/Objects/TR5/Light/tr5_light.cpp @@ -37,9 +37,9 @@ void PulseLightControl(short itemNumber) item->Pose.Position.y, item->Pose.Position.z, 24, - (pulse * item->Model.Color.x * SCHAR_MAX) / 512, - (pulse * item->Model.Color.y * SCHAR_MAX) / 512, - (pulse * item->Model.Color.z * SCHAR_MAX) / 512); + (pulse * item->Model.Color.x * UCHAR_MAX) / 512, + (pulse * item->Model.Color.y * UCHAR_MAX) / 512, + (pulse * item->Model.Color.z * UCHAR_MAX) / 512); } } @@ -62,9 +62,9 @@ void StrobeLightControl(short itemNumber) { item->Pose.Orientation.y += ANGLE(16.0f); - byte r = item->Model.Color.x * SCHAR_MAX; - byte g = item->Model.Color.y * SCHAR_MAX; - byte b = item->Model.Color.z * SCHAR_MAX; + byte r = item->Model.Color.x * UCHAR_MAX; + byte g = item->Model.Color.y * UCHAR_MAX; + byte b = item->Model.Color.z * UCHAR_MAX; TriggerAlertLight( item->Pose.Position.x, @@ -95,9 +95,9 @@ void ColorLightControl(short itemNumber) item->Pose.Position.y, item->Pose.Position.z, 24, - item->Model.Color.x * SCHAR_MAX, - item->Model.Color.y * SCHAR_MAX, - item->Model.Color.z * SCHAR_MAX); + item->Model.Color.x * UCHAR_MAX, + item->Model.Color.y * UCHAR_MAX, + item->Model.Color.z * UCHAR_MAX); } } @@ -202,11 +202,11 @@ void ElectricalLightControl(short itemNumber) (intensity * (lightPtr->Color.y / 2)) , (intensity * (lightPtr->Color.z / 2))); - // Set light mesh color. Model.Color max value is 2.0f. + // Set light mesh color. item->Model.Color = Vector4( - ((intensity / 2) * lightPtr->Color.x) / 96, - ((intensity / 2) * lightPtr->Color.y) / 96, - ((intensity / 2) * lightPtr->Color.z) / 96, + (intensity * lightPtr->Color.x) / 96, + (intensity * lightPtr->Color.y) / 96, + (intensity * lightPtr->Color.z) / 96, 1.0f); } @@ -229,9 +229,9 @@ void BlinkingLightControl(short itemNumber) SpawnDynamicLight( pos.x, pos.y, pos.z, 16, - item->Model.Color.x * SCHAR_MAX, - item->Model.Color.y * SCHAR_MAX, - item->Model.Color.z * SCHAR_MAX); + item->Model.Color.x * UCHAR_MAX, + item->Model.Color.y * UCHAR_MAX, + item->Model.Color.z * UCHAR_MAX); item->MeshBits = 2; diff --git a/TombEngine/Objects/TR5/Object/tr5_missile.cpp b/TombEngine/Objects/TR5/Object/tr5_missile.cpp index 8bcffe8d79..3cd96e788f 100644 --- a/TombEngine/Objects/TR5/Object/tr5_missile.cpp +++ b/TombEngine/Objects/TR5/Object/tr5_missile.cpp @@ -208,7 +208,7 @@ void ExplodeFX(FX_INFO* fx, int noXZVel, int bits) { ShatterItem.yRot = fx->pos.Orientation.y; ShatterItem.meshIndex = fx->frameNumber; - ShatterItem.color = Vector4::One; + ShatterItem.color = NEUTRAL_COLOR; ShatterItem.sphere.Center = fx->pos.Position.ToVector3(); ShatterItem.bit = 0; ShatterItem.flags = fx->flag2 & 0x1400; diff --git a/TombEngine/Objects/TR5/Switch/tr5_raisingcog.cpp b/TombEngine/Objects/TR5/Switch/tr5_raisingcog.cpp index 43577bef14..a972c89774 100644 --- a/TombEngine/Objects/TR5/Switch/tr5_raisingcog.cpp +++ b/TombEngine/Objects/TR5/Switch/tr5_raisingcog.cpp @@ -12,6 +12,7 @@ using namespace TEN::Animation; using namespace TEN::Entities::Switches; +using namespace TEN::SpotCam; void InitializeRaisingCog(short itemNumber) { diff --git a/TombEngine/Objects/TR5/Trap/LaserBarrier.cpp b/TombEngine/Objects/TR5/Trap/LaserBarrier.cpp index 3dc5637e13..cb6620e992 100644 --- a/TombEngine/Objects/TR5/Trap/LaserBarrier.cpp +++ b/TombEngine/Objects/TR5/Trap/LaserBarrier.cpp @@ -103,9 +103,9 @@ namespace TEN::Entities::Traps SpawnDynamicLight( item.Pose.Position.x, item.Pose.Position.y, item.Pose.Position.z, 8, - intensityNorm * (item.Model.Color.x / 2), - intensityNorm * (item.Model.Color.y / 2), - intensityNorm * (item.Model.Color.z / 2)); + intensityNorm * (item.Model.Color.x), + intensityNorm * (item.Model.Color.y), + intensityNorm * (item.Model.Color.z)); } void ControlLaserBarrier(short itemNumber) @@ -134,8 +134,7 @@ namespace TEN::Entities::Traps if (barrier.Color.w < 1.0f) barrier.Color.w += 0.02f; - // TODO: Weird. - if (item.Model.Color.w > 8.0f) + if (item.Model.Color.w >= 1.0f) { barrier.Color.w = 0.8f; item.Model.Color.w = 0.8f; diff --git a/TombEngine/Objects/TR5/Trap/LaserBeam.cpp b/TombEngine/Objects/TR5/Trap/LaserBeam.cpp index 72c88f390f..ecdc199118 100644 --- a/TombEngine/Objects/TR5/Trap/LaserBeam.cpp +++ b/TombEngine/Objects/TR5/Trap/LaserBeam.cpp @@ -191,7 +191,7 @@ namespace TEN::Entities::Traps if (beam.IsLethal) SpawnLaserSpark(beam.Target, Random::GenerateAngle(), 6, beam.Color); - SpawnLaserBeamLight(beam.Target.ToVector3(), beam.Target.RoomNumber, item.Model.Color, LASER_BEAM_LIGHT_INTENSITY * item.Model.Color.w, LASER_BEAM_LIGHT_AMPLITUDE_MAX); + SpawnLaserBeamLight(beam.Target.ToVector3(), beam.Target.RoomNumber, beam.Color, LASER_BEAM_LIGHT_INTENSITY * beam.Color.w, LASER_BEAM_LIGHT_AMPLITUDE_MAX); } SoundEffect(SFX_TR5_DOOR_BEAM, &item.Pose); diff --git a/TombEngine/Renderer/ConstantBuffers/CameraMatrixBuffer.h b/TombEngine/Renderer/ConstantBuffers/CameraMatrixBuffer.h index 6c8e568834..cc98ca4cef 100644 --- a/TombEngine/Renderer/ConstantBuffers/CameraMatrixBuffer.h +++ b/TombEngine/Renderer/ConstantBuffers/CameraMatrixBuffer.h @@ -47,7 +47,7 @@ namespace TEN::Renderer::ConstantBuffers int RefreshRate; int NumFogBulbs; float InterpolatedFrame; - float Padding2; + float Gamma; //-- ShaderFogBulb FogBulbs[MAX_FOG_BULBS_DRAW]; }; diff --git a/TombEngine/Renderer/Graphics/Vertices/Vertex.h b/TombEngine/Renderer/Graphics/Vertices/Vertex.h index 2e8eb1ba6d..760febedbd 100644 --- a/TombEngine/Renderer/Graphics/Vertices/Vertex.h +++ b/TombEngine/Renderer/Graphics/Vertices/Vertex.h @@ -5,12 +5,12 @@ namespace TEN::Renderer::Graphics::Vertices { struct Vertex { - Vector3 Position = Vector3::Zero; - unsigned int Normal = 0; - Vector2 UV = Vector2::Zero; - Vector4 Color = Vector4::One; - unsigned int Tangent = 0; - unsigned int FaceNormal = 0; + Vector3 Position = Vector3::Zero; + unsigned int Normal = 0; + Vector2 UV = Vector2::Zero; + unsigned int Color = 0; + unsigned int Tangent = 0; + unsigned int FaceNormal = 0; std::array BoneIndex = { 0, 0, 0, 0 }; std::array BoneWeight = { 255, 0, 0, 0 }; diff --git a/TombEngine/Renderer/Renderer.h b/TombEngine/Renderer/Renderer.h index 030f91b5fb..43692df9c3 100644 --- a/TombEngine/Renderer/Renderer.h +++ b/TombEngine/Renderer/Renderer.h @@ -252,6 +252,7 @@ namespace TEN::Renderer std::vector _lines2DToDraw = {}; std::vector _lines3DToDraw = {}; std::vector _triangles3DToDraw = {}; + std::vector> _debugDisplayRects = {}; // Textures, objects and sprites @@ -356,7 +357,7 @@ namespace TEN::Renderer PostProcessMode _postProcessMode = PostProcessMode::None; float _postProcessStrength = 1.0f; - Vector3 _postProcessTint = Vector3::One; + Vector3 _postProcessTint = (Vector3)NEUTRAL_COLOR; VertexBuffer _fullscreenTriangleVertexBuffer; ComPtr _fullscreenTriangleInputLayout = nullptr; @@ -430,6 +431,10 @@ namespace TEN::Renderer void InitializeMenuBars(int y); void InitializeSky(); void DrawAllStrings(); + void DrawDebugDisplayRects(); + void AddStringInternal(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, + const Color& color, const Vector2& scale, float rotation, int flags, + int priority, BlendMode blendMode); void PrepareDynamicLight(RendererLight& light); void PrepareLaserBarriers(RenderView& view); void PrepareSingleLaserBeam(RenderView& view); @@ -460,6 +465,7 @@ namespace TEN::Renderer void DrawSprites(RenderView& view, RendererPass rendererPass); void DrawDisplaySprites(RenderView& view, bool negativePriority); void DrawDisplayItems(); + void DrawAllDisplayLayers(RenderView& view); void DrawSortedFaces(RenderView& view); void DrawSingleSprite(RendererSortableObject* object, RendererObjectType lastObjectType, RenderView& view); void DrawRoomSorted(RendererSortableObject* objectInfo, RendererObjectType lastObjectType, RenderView& view); @@ -741,7 +747,11 @@ namespace TEN::Renderer void AddString(const std::string& string, const Vector2& pos, const Color& color, float scale, int flags); void AddString(const std::string& string, const Vector2& pos, const Vector2& area, const Color& color, float scale, int flags); void AddString(const std::string& string, const Vector2& currentPos, const Vector2& prevPos, const Vector2& area, const Color& color, float scale, int flags); + void AddString(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, + const Color& color, const Vector2& scale, float rotation, int flags, + int priority = 0, BlendMode blendMode = BlendMode::AlphaBlend); void AddDebugString(const std::string& string, const Vector2& pos, const Color& color, float scale, RendererDebugPage page = RendererDebugPage::None); + Vector2 GetDisplayStringSize(const std::string& text, const Vector2& scale) const; void FreeRendererData(); void AddDynamicPointLight(const Vector3& pos, float radius, const Color& color, bool castShadows, int hash = 0); void AddDynamicFogBulb(const Vector3& pos, float radius, float density, const Color& color, int hash = 0); @@ -769,6 +779,7 @@ namespace TEN::Renderer void AddDebugCylinder(const Vector3& center, const Quaternion& orient, float radius, float length, const Color& color, RendererDebugPage page = RendererDebugPage::None, bool isWireframe = true); void AddDebugSphere(const Vector3& center, float radius, const Color& color, RendererDebugPage page = RendererDebugPage::None, bool isWireframe = true); void AddDebugSphere(const BoundingSphere& sphere, const Color& color, RendererDebugPage page = RendererDebugPage::None, bool isWireframe = true); + void AddDebugDisplayRect(const RendererRectangle& rect, const Vector4& color); void PrintDebugMessage(LPCSTR msg, va_list args); void PrintDebugMessage(LPCSTR msg, ...); diff --git a/TombEngine/Renderer/RendererCompatibility.cpp b/TombEngine/Renderer/RendererCompatibility.cpp index 8402f6c54e..1aaada63da 100644 --- a/TombEngine/Renderer/RendererCompatibility.cpp +++ b/TombEngine/Renderer/RendererCompatibility.cpp @@ -470,7 +470,7 @@ namespace TEN::Renderer vertex->Normal = PackVector3(poly.normals[k]); vertex->UV = poly.textureCoordinates[k]; - vertex->Color = VectorColorToRGBA_TempToVector4(Vector4(room.colors[index].x, room.colors[index].y, room.colors[index].z, 1.0f)); + vertex->Color = VectorColorToRGBA(Vector4(room.colors[index].x, room.colors[index].y, room.colors[index].z, 1.0f)); vertex->Tangent = PackVector3(poly.tangents[k]); vertex->FaceNormal = PackVector3(poly.normal); @@ -1130,7 +1130,7 @@ namespace TEN::Renderer vertex.UV.x = poly->textureCoordinates[k].x; vertex.UV.y = poly->textureCoordinates[k].y; - vertex.Color = VectorColorToRGBA_TempToVector4(Vector4(meshPtr->colors[v].x, meshPtr->colors[v].y, meshPtr->colors[v].z, 1.0f)); + vertex.Color = VectorColorToRGBA(Vector4(meshPtr->colors[v].x, meshPtr->colors[v].y, meshPtr->colors[v].z, 1.0f)); vertex.BoneIndex = meshPtr->boneIndices[v]; vertex.BoneWeight = meshPtr->boneWeights[v]; diff --git a/TombEngine/Renderer/RendererDraw.cpp b/TombEngine/Renderer/RendererDraw.cpp index 34a7743628..cdd939fc6a 100644 --- a/TombEngine/Renderer/RendererDraw.cpp +++ b/TombEngine/Renderer/RendererDraw.cpp @@ -416,13 +416,13 @@ namespace TEN::Renderer vertex0.Position.x = line.Origin.x; vertex0.Position.y = line.Origin.y; vertex0.Position.z = 1.0f; - vertex0.Color = VectorColorToRGBA_TempToVector4(line.Color); + vertex0.Color = VectorColorToRGBA(line.Color); auto vertex1 = Vertex{}; vertex1.Position.x = line.Target.x; vertex1.Position.y = line.Target.y; vertex1.Position.z = 1.0f; - vertex1.Color = VectorColorToRGBA_TempToVector4(line.Color); + vertex1.Color = VectorColorToRGBA(line.Color); vertex0.Position = Vector3::Transform(vertex0.Position, worldMatrix); vertex1.Position = Vector3::Transform(vertex1.Position, worldMatrix); @@ -604,7 +604,7 @@ namespace TEN::Renderer ReflectMatrixOptionally(world); _stInstancedStaticMeshBuffer.StaticMeshes[0].World = world; - _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = _rooms[rat->RoomNumber].AmbientLight; _stInstancedStaticMeshBuffer.StaticMeshes[0].LightMode = (int)moveableObj.ObjectMeshes[0]->LightMode; @@ -722,7 +722,7 @@ namespace TEN::Renderer const auto& mesh = *GetMesh(Objects[ID_FISH_EMITTER].meshIndex + fish.MeshIndex); _stInstancedStaticMeshBuffer.StaticMeshes[0].World = Matrix::Lerp(fish.PrevTransform, fish.Transform, GetInterpolationFactor()); - _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = _rooms[fish.RoomNumber].AmbientLight; _stInstancedStaticMeshBuffer.StaticMeshes[0].LightMode = (int)moveableObj.ObjectMeshes[0]->LightMode; @@ -823,7 +823,7 @@ namespace TEN::Renderer _stInstancedStaticMeshBuffer.StaticMeshes[batCount].World = world; _stInstancedStaticMeshBuffer.StaticMeshes[batCount].Ambient = room.AmbientLight; - _stInstancedStaticMeshBuffer.StaticMeshes[batCount].Color = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[batCount].Color = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[batCount].LightMode = (int)mesh.LightMode; if (rendererPass != RendererPass::GBuffer) @@ -948,7 +948,7 @@ namespace TEN::Renderer _stInstancedStaticMeshBuffer.StaticMeshes[beetleCount].World = world; _stInstancedStaticMeshBuffer.StaticMeshes[beetleCount].Ambient = room.AmbientLight; - _stInstancedStaticMeshBuffer.StaticMeshes[beetleCount].Color = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[beetleCount].Color = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[beetleCount].LightMode = (int)mesh.LightMode; if (rendererPass != RendererPass::GBuffer) @@ -1107,7 +1107,7 @@ namespace TEN::Renderer ReflectMatrixOptionally(world); _stInstancedStaticMeshBuffer.StaticMeshes[0].World = world; - _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = _rooms[locust.RoomNumber].AmbientLight; _stInstancedStaticMeshBuffer.StaticMeshes[0].LightMode = (int)moveableObj.ObjectMeshes[0]->LightMode; @@ -1158,11 +1158,11 @@ namespace TEN::Renderer { auto vertex0 = Vertex{}; vertex0.Position = line.Origin; - vertex0.Color = VectorColorToRGBA_TempToVector4(line.Color); + vertex0.Color = VectorColorToRGBA(line.Color); auto vertex1 = Vertex{}; vertex1.Position = line.Target; - vertex1.Color = VectorColorToRGBA_TempToVector4(line.Color); + vertex1.Color = VectorColorToRGBA(line.Color); _primitiveBatch->DrawLine(vertex0, vertex1); @@ -1197,7 +1197,7 @@ namespace TEN::Renderer { auto rVertex = Vertex{}; rVertex.Position = vertex; - rVertex.Color = VectorColorToRGBA_TempToVector4(tri.Color); + rVertex.Color = VectorColorToRGBA(tri.Color); rVertices.push_back(rVertex); } @@ -1758,6 +1758,7 @@ namespace TEN::Renderer _lines3DToDraw.clear(); _triangles3DToDraw.clear(); _stringsToDraw.clear(); + _debugDisplayRects.clear(); _currentCausticsFrame++; _currentCausticsFrame %= 32; @@ -1905,13 +1906,14 @@ namespace TEN::Renderer ResetScissor(); // Camera constant buffer contains matrices, camera position, fog values, and other things shared for all shaders. - CCameraMatrixBuffer cameraConstantBuffer; + auto cameraConstantBuffer = CCameraMatrixBuffer{}; view.FillConstantBuffer(cameraConstantBuffer); cameraConstantBuffer.Frame = GlobalCounter; cameraConstantBuffer.InterpolatedFrame = (float)GlobalCounter + GetInterpolationFactor(); cameraConstantBuffer.RefreshRate = _refreshRate; cameraConstantBuffer.CameraUnderwater = g_Level.Rooms[cameraConstantBuffer.RoomNumber].flags & ENV_FLAG_WATER; cameraConstantBuffer.DualParaboloidView = Matrix::CreateLookAt(_gameCamera.Camera.WorldPosition, _gameCamera.Camera.WorldPosition + Vector3(0, -1024, 0), Vector3::UnitX); + cameraConstantBuffer.Gamma = g_Configuration.Gamma; if (level.GetFogMaxDistance() > 0) { @@ -2031,11 +2033,10 @@ namespace TEN::Renderer if (renderMode == SceneRenderMode::Full && g_GameFlow->LastGameStatus == GameStatus::Normal) { CollectDisplaySprites(view); - DrawDisplaySprites(view, false); + DrawAllDisplayLayers(view); DrawDebugRenderTargets(view); - DrawAllStrings(); - DrawDisplaySprites(view, true); + DrawDebugDisplayRects(); } time2 = std::chrono::high_resolution_clock::now(); @@ -2094,7 +2095,7 @@ namespace TEN::Renderer auto view = RenderView(&Camera, 0, PI / 2.0f, 32, DEFAULT_FAR_VIEW, ROOM_AMBIENT_MAP_SIZE, ROOM_AMBIENT_MAP_SIZE); - CCameraMatrixBuffer cameraConstantBuffer; + auto cameraConstantBuffer = CCameraMatrixBuffer{}; cameraConstantBuffer.DualParaboloidView = Matrix::CreateLookAt(position, position + Vector3(0, 0, 1024), -Vector3::UnitY); cameraConstantBuffer.Hemisphere = hemisphere; view.FillConstantBuffer(cameraConstantBuffer); @@ -2135,7 +2136,7 @@ namespace TEN::Renderer _stInstancedStaticMeshBuffer.StaticMeshes[0].World = (rotation * translation); _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = weather.SkyColor(s); - _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[0].LightMode = 0; _stInstancedStaticMeshBuffer.StaticMeshes[0].NumLights = 0; _stInstancedStaticMeshBuffer.StaticMeshes[0].ApplyFogBulbs = s == 0 ? 1 : 0; @@ -2157,8 +2158,8 @@ namespace TEN::Renderer const auto& moveableObj = *_moveableObjects[ID_HORIZON]; // FIXME: Replace with same function as in the main pipeline! _stInstancedStaticMeshBuffer.StaticMeshes[0].World = Matrix::CreateTranslation(LaraItem->Pose.Position.ToVector3()); - _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = Vector4::One; - _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = NEUTRAL_COLOR; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[0].LightMode = 0; _stInstancedStaticMeshBuffer.StaticMeshes[0].NumLights = 0; _stInstancedStaticMeshBuffer.StaticMeshes[0].ApplyFogBulbs = 1; @@ -2329,6 +2330,9 @@ namespace TEN::Renderer ClearScene(); _context->ClearState(); + _lastBlendMode = BlendMode::Unknown; + _lastCullMode = CullMode::Unknown; + _lastDepthState = DepthState::Unknown; _swapChain->Present(1, 0); } @@ -3019,7 +3023,7 @@ namespace TEN::Renderer auto view = RenderView(_gameCamera.Camera.WorldPosition, Vector3::UnitX, Vector3::UnitY, ROOM_AMBIENT_MAP_SIZE, ROOM_AMBIENT_MAP_SIZE, 0, 32, DEFAULT_FAR_VIEW, PI / 2.0f); - CCameraMatrixBuffer cameraConstantBuffer; + auto cameraConstantBuffer = CCameraMatrixBuffer{}; cameraConstantBuffer.DualParaboloidView = Matrix::CreateLookAt(_gameCamera.Camera.WorldPosition, _gameCamera.Camera.WorldPosition + Vector3(0, -1024, 0), Vector3::UnitX); cameraConstantBuffer.Hemisphere = -1; cameraConstantBuffer.Frame = GlobalCounter; @@ -3368,6 +3372,9 @@ namespace TEN::Renderer RenderScene(&_backBuffer, _gameCamera); _context->ClearState(); + _lastBlendMode = BlendMode::Unknown; + _lastCullMode = CullMode::Unknown; + _lastDepthState = DepthState::Unknown; _swapChain->Present(1, 0); } @@ -3743,25 +3750,25 @@ namespace TEN::Renderer Vertex v0; v0.Position = Vector3::Transform(p0t, world); v0.UV = uv0; - v0.Color = VectorColorToRGBA_TempToVector4(spr->c1); + v0.Color = VectorColorToRGBA(spr->c1); v0.Effects = 0 << INDEX_IN_POLY_VERTEX_SHIFT; Vertex v1; v1.Position = Vector3::Transform(p1t, world); v1.UV = uv1; - v1.Color = VectorColorToRGBA_TempToVector4(spr->c2); + v1.Color = VectorColorToRGBA(spr->c2); v1.Effects = 1 << INDEX_IN_POLY_VERTEX_SHIFT; Vertex v2; v2.Position = Vector3::Transform(p2t, world); v2.UV = uv2; - v2.Color = VectorColorToRGBA_TempToVector4(spr->c3); + v2.Color = VectorColorToRGBA(spr->c3); v2.Effects = 2 << INDEX_IN_POLY_VERTEX_SHIFT; Vertex v3; v3.Position = Vector3::Transform(p3t, world); v3.UV = uv3; - v3.Color = VectorColorToRGBA_TempToVector4(spr->c4); + v3.Color = VectorColorToRGBA(spr->c4); v3.Effects = 3 << INDEX_IN_POLY_VERTEX_SHIFT; _sortedPolygonsVertices.push_back(v0); @@ -3951,7 +3958,7 @@ namespace TEN::Renderer auto world = objectInfo->World; _stInstancedStaticMeshBuffer.StaticMeshes[0].World = world; - _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = objectInfo->Room->AmbientLight; _stInstancedStaticMeshBuffer.StaticMeshes[0].LightMode = (int)objectInfo->LightMode; BindInstancedStaticLights(objectInfo->Room->LightsToDraw, 0); diff --git a/TombEngine/Renderer/RendererDraw2D.cpp b/TombEngine/Renderer/RendererDraw2D.cpp index 8b67fd15fa..6563e71caa 100644 --- a/TombEngine/Renderer/RendererDraw2D.cpp +++ b/TombEngine/Renderer/RendererDraw2D.cpp @@ -30,6 +30,7 @@ namespace TEN::Renderer using namespace TEN::Effects::DisplaySprite; using namespace TEN::Effects::Environment; using namespace TEN::Math; + using namespace TEN::SpotCam; void Renderer::InitializeGameBars() { @@ -281,28 +282,28 @@ namespace TEN::Renderer vertices[0].Position.z = 0.0f; vertices[0].UV.x = 0.0f; vertices[0].UV.y = 0.0f; - vertices[0].Color = VectorColorToRGBA_TempToVector4(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); + vertices[0].Color = VectorColorToRGBA(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); vertices[1].Position.x = 4.0f / _screenWidth; vertices[1].Position.y = 4.0f / _screenHeight; vertices[1].Position.z = 0.0f; vertices[1].UV.x = 1.0f; vertices[1].UV.y = 0.0f; - vertices[1].Color = VectorColorToRGBA_TempToVector4(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); + vertices[1].Color = VectorColorToRGBA(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); vertices[2].Position.x = 4.0f / _screenWidth; vertices[2].Position.y = -4.0f / _screenHeight; vertices[2].Position.z = 0.0f; vertices[2].UV.x = 1.0f; vertices[2].UV.y = 1.0f; - vertices[2].Color = VectorColorToRGBA_TempToVector4(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); + vertices[2].Color = VectorColorToRGBA(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); vertices[3].Position.x = -4.0f / _screenWidth; vertices[3].Position.y = -4.0f / _screenHeight; vertices[3].Position.z = 0.0f; vertices[3].UV.x = 0.0f; vertices[3].UV.y = 1.0f; - vertices[3].Color = VectorColorToRGBA_TempToVector4(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); + vertices[3].Color = VectorColorToRGBA(Vector4(1.0f, 0.0f, 0.0f, 1.0f)); _shaders.Bind(Shader::FullScreenQuad); @@ -333,6 +334,28 @@ namespace TEN::Renderer DrawFullScreenQuad(texture, Vector3(fade), true); } + void Renderer::AddDebugDisplayRect(const RendererRectangle& rect, const Vector4& color) + { + _debugDisplayRects.push_back({ rect, color }); + } + + void Renderer::DrawDebugDisplayRects() + { + if (_debugDisplayRects.empty()) + return; + + ResetScissor(); + _spriteBatch->Begin(SpriteSortMode_Deferred, _renderStates->NonPremultiplied(), nullptr, nullptr, _cullNoneRasterizerState.Get()); + + for (const auto& [rect, color] : _debugDisplayRects) + { + auto destRect = RECT{ rect.Left, rect.Top, rect.Right, rect.Bottom }; + _spriteBatch->Draw(_whiteTexture.ShaderResourceView.Get(), destRect, DirectX::XMLoadFloat4(&color)); + } + + _spriteBatch->End(); + } + void Renderer::DrawDisplaySprites(RenderView& renderView, bool negativePriority) { constexpr auto VERTEX_COUNT = 4; @@ -341,11 +364,22 @@ namespace TEN::Renderer return; Texture2D* texture2DPtr = nullptr; + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + for (const auto& spriteToDraw : renderView.DisplaySpritesToDraw) { if ((spriteToDraw.Priority >= 0) == negativePriority) continue; + // Handle scissor rect changes. + bool scissorChanged = (spriteToDraw.HasScissor != currentHasScissor) || + (spriteToDraw.HasScissor && + (spriteToDraw.ScissorRect.Left != currentScissor.Left || + spriteToDraw.ScissorRect.Top != currentScissor.Top || + spriteToDraw.ScissorRect.Right != currentScissor.Right || + spriteToDraw.ScissorRect.Bottom != currentScissor.Bottom)); + if (texture2DPtr == nullptr) { _shaders.Bind(Shader::FullScreenQuad); @@ -353,14 +387,32 @@ namespace TEN::Renderer _context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); _context->IASetInputLayout(_inputLayout.Get()); + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + _primitiveBatch->Begin(); BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); SetBlendMode(spriteToDraw.BlendMode); } - else if (texture2DPtr != spriteToDraw.SpritePtr->Texture || _lastBlendMode != spriteToDraw.BlendMode) + else if (texture2DPtr != spriteToDraw.SpritePtr->Texture || _lastBlendMode != spriteToDraw.BlendMode || scissorChanged) { _primitiveBatch->End(); + + if (scissorChanged) + { + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + else + ResetScissor(); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + } + _primitiveBatch->Begin(); BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); @@ -398,7 +450,7 @@ namespace TEN::Renderer { rVertices[i].Position = Vector3(vertices[i]); rVertices[i].UV = spriteToDraw.SpritePtr->UV[i]; - rVertices[i].Color = VectorColorToRGBA_TempToVector4(Vector4(spriteToDraw.Color.x, spriteToDraw.Color.y, spriteToDraw.Color.z, spriteToDraw.Color.w)); + rVertices[i].Color = VectorColorToRGBA(Vector4(spriteToDraw.Color.x, spriteToDraw.Color.y, spriteToDraw.Color.z, spriteToDraw.Color.w)); } _primitiveBatch->DrawQuad(rVertices[0], rVertices[1], rVertices[2], rVertices[3]); @@ -408,6 +460,10 @@ namespace TEN::Renderer if (texture2DPtr != nullptr) _primitiveBatch->End(); + + // Reset scissor if it was active. + if (currentHasScissor) + ResetScissor(); } void Renderer::DrawFullScreenQuad(ID3D11ShaderResourceView* texture, Vector3 color, bool fit, float customAspect) @@ -449,22 +505,22 @@ namespace TEN::Renderer vertices[0].Position = Vector3(-1.0f, 1.0f, 0.0f); vertices[0].UV.x = uvStart.x; vertices[0].UV.y = uvStart.y; - vertices[0].Color = VectorColorToRGBA_TempToVector4(colorVec4); + vertices[0].Color = VectorColorToRGBA(colorVec4); vertices[1].Position = Vector3(1.0f, 1.0f, 0.0f); vertices[1].UV.x = uvEnd.x; vertices[1].UV.y = uvStart.y; - vertices[1].Color = VectorColorToRGBA_TempToVector4(colorVec4); + vertices[1].Color = VectorColorToRGBA(colorVec4); vertices[2].Position = Vector3(1.0f, -1.0f, 0.0f); vertices[2].UV.x = uvEnd.x; vertices[2].UV.y = uvEnd.y; - vertices[2].Color = VectorColorToRGBA_TempToVector4(colorVec4); + vertices[2].Color = VectorColorToRGBA(colorVec4); vertices[3].Position = Vector3(-1.0f, -1.0f, 0.0f); vertices[3].UV.x = uvStart.x; vertices[3].UV.y = uvEnd.y; - vertices[3].Color = VectorColorToRGBA_TempToVector4(colorVec4); + vertices[3].Color = VectorColorToRGBA(colorVec4); _shaders.Bind(Shader::FullScreenQuad); @@ -520,28 +576,28 @@ namespace TEN::Renderer vertices[0].Position.z = 0.0f; vertices[0].UV.x = uvStart.x; vertices[0].UV.y = uvStart.y; - vertices[0].Color = VectorColorToRGBA_TempToVector4(Vector4(color.x, color.y, color.z, 1.0f)); + vertices[0].Color = VectorColorToRGBA(Vector4(color.x, color.y, color.z, 1.0f)); vertices[1].Position.x = 1.0f; vertices[1].Position.y = 1.0f; vertices[1].Position.z = 0.0f; vertices[1].UV.x = uvEnd.x; vertices[1].UV.y = uvStart.y; - vertices[1].Color = VectorColorToRGBA_TempToVector4(Vector4(color.x, color.y, color.z, 1.0f)); + vertices[1].Color = VectorColorToRGBA(Vector4(color.x, color.y, color.z, 1.0f)); vertices[2].Position.x = 1.0f; vertices[2].Position.y = -1.0f; vertices[2].Position.z = 0.0f; vertices[2].UV.x = uvEnd.x; vertices[2].UV.y = uvEnd.y; - vertices[2].Color = VectorColorToRGBA_TempToVector4(Vector4(color.x, color.y, color.z, 1.0f)); + vertices[2].Color = VectorColorToRGBA(Vector4(color.x, color.y, color.z, 1.0f)); vertices[3].Position.x = -1.0f; vertices[3].Position.y = -1.0f; vertices[3].Position.z = 0.0f; vertices[3].UV.x = uvStart.x; vertices[3].UV.y = uvEnd.y; - vertices[3].Color = VectorColorToRGBA_TempToVector4(Vector4(color.x, color.y, color.z, 1.0f)); + vertices[3].Color = VectorColorToRGBA(Vector4(color.x, color.y, color.z, 1.0f)); _shaders.Bind(Shader::FullScreenQuad); @@ -570,10 +626,223 @@ namespace TEN::Renderer spriteToDraw.Priority = priority; spriteToDraw.BlendMode = blendMode; spriteToDraw.AspectCorrection = aspectCorrection; + spriteToDraw.HasScissor = false; renderView.DisplaySpritesToDraw.push_back(spriteToDraw); } + // Draw all display sprites and strings interleaved by priority. + // Sprites at the same priority draw before strings. 3D display items are inserted + // at the transition from negative to non-negative priority. + void Renderer::DrawAllDisplayLayers(RenderView& view) + { + constexpr auto VERTEX_COUNT = 4; + + auto& sprites = view.DisplaySpritesToDraw; + + std::stable_sort(_stringsToDraw.begin(), _stringsToDraw.end(), + [](const auto& a, const auto& b) { return a.Priority < b.Priority; }); + + if (sprites.empty() && _stringsToDraw.empty()) + { + DrawDisplayItems(); + return; + } + + // Gather sorted unique priorities from both lists. + auto priorities = std::vector{}; + priorities.reserve(sprites.size() + _stringsToDraw.size()); + for (const auto& sprite : sprites) priorities.push_back(sprite.Priority); + for (const auto& str : _stringsToDraw) priorities.push_back(str.Priority); + std::sort(priorities.begin(), priorities.end()); + priorities.erase(std::unique(priorities.begin(), priorities.end()), priorities.end()); + + float shadowOffset = 1.5f / (REFERENCE_FONT_SIZE / _gameFont->GetLineSpacing()); + auto shadowColor = (Vector4)g_GameFlow->GetSettings()->UI.ShadowTextColor; + + bool itemsDrawn = false; + int spriteIdx = 0; + int stringIdx = 0; + + for (int priority : priorities) + { + // Insert 3D display items at the transition from negative to non-negative priority. + if (!itemsDrawn && priority >= 0) + { + DrawDisplayItems(); + itemsDrawn = true; + } + + // Draw sprites at this priority level. + { + Texture2D* texture2DPtr = nullptr; + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + + while (spriteIdx < (int)sprites.size() && sprites[spriteIdx].Priority == priority) + { + const auto& spriteToDraw = sprites[spriteIdx++]; + + bool scissorChanged = + (spriteToDraw.HasScissor != currentHasScissor) || + (spriteToDraw.HasScissor && + (spriteToDraw.ScissorRect.Left != currentScissor.Left || + spriteToDraw.ScissorRect.Top != currentScissor.Top || + spriteToDraw.ScissorRect.Right != currentScissor.Right || + spriteToDraw.ScissorRect.Bottom != currentScissor.Bottom)); + + if (texture2DPtr == nullptr) + { + _shaders.Bind(Shader::FullScreenQuad); + _context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + _context->IASetInputLayout(_inputLayout.Get()); + + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + + _primitiveBatch->Begin(); + BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); + SetBlendMode(spriteToDraw.BlendMode); + } + else if (texture2DPtr != spriteToDraw.SpritePtr->Texture || _lastBlendMode != spriteToDraw.BlendMode || scissorChanged) + { + _primitiveBatch->End(); + + if (scissorChanged) + { + if (spriteToDraw.HasScissor) + SetScissor(spriteToDraw.ScissorRect); + else + ResetScissor(); + + currentHasScissor = spriteToDraw.HasScissor; + currentScissor = spriteToDraw.ScissorRect; + } + + _primitiveBatch->Begin(); + BindTexture(TextureRegister::ColorMap, spriteToDraw.SpritePtr->Texture, SamplerStateRegister::AnisotropicClamp); + SetBlendMode(spriteToDraw.BlendMode); + } + + auto vertices = std::array + { + spriteToDraw.Size / 2, + Vector2(-spriteToDraw.Size.x, spriteToDraw.Size.y) / 2, + -spriteToDraw.Size / 2, + Vector2(spriteToDraw.Size.x, -spriteToDraw.Size.y) / 2 + }; + + // NOTE: Must rotate 180 degrees to account for +Y being down. + auto rotMatrix = Matrix::CreateRotationZ(TO_RAD(spriteToDraw.Orientation + ANGLE(180.0f))); + for (auto& vertex : vertices) + { + vertex = Vector2::Transform(vertex, rotMatrix); + vertex *= spriteToDraw.AspectCorrection; + vertex += spriteToDraw.Position; + vertex = TEN::Utils::Convert2DPositionToNDC(vertex); + } + + auto rVertices = std::array{}; + for (int i = 0; i < (int)rVertices.size(); i++) + { + rVertices[i].Position = Vector3(vertices[i]); + rVertices[i].UV = spriteToDraw.SpritePtr->UV[i]; + rVertices[i].Color = VectorColorToRGBA_TempToVector4(Vector4( + spriteToDraw.Color.x, spriteToDraw.Color.y, + spriteToDraw.Color.z, spriteToDraw.Color.w)); + } + + _primitiveBatch->DrawQuad(rVertices[0], rVertices[1], rVertices[2], rVertices[3]); + texture2DPtr = spriteToDraw.SpritePtr->Texture; + } + + if (texture2DPtr != nullptr) + { + _primitiveBatch->End(); + + if (currentHasScissor) + ResetScissor(); + } + } + + // Draw strings at this priority level. + if (stringIdx < (int)_stringsToDraw.size() && _stringsToDraw[stringIdx].Priority == priority) + { + auto currentBlend = BlendMode::AlphaBlend; + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; + + ResetScissor(); + SetBlendMode(currentBlend); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + + while (stringIdx < (int)_stringsToDraw.size() && _stringsToDraw[stringIdx].Priority == priority) + { + const auto& rString = _stringsToDraw[stringIdx++]; + + if (rString.Blend != currentBlend) + { + _spriteBatch->End(); + currentBlend = rString.Blend; + SetBlendMode(currentBlend); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + + bool scissorChanged = + (rString.HasScissor != currentHasScissor) || + (rString.HasScissor && + (rString.ScissorRect.Left != currentScissor.Left || + rString.ScissorRect.Top != currentScissor.Top || + rString.ScissorRect.Right != currentScissor.Right || + rString.ScissorRect.Bottom != currentScissor.Bottom)); + + if (scissorChanged) + { + _spriteBatch->End(); + + if (rString.HasScissor) + SetScissor(rString.ScissorRect); + else + ResetScissor(); + + currentHasScissor = rString.HasScissor; + currentScissor = rString.ScissorRect; + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + + auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor()); + + if (rString.Flags & (int)PrintStringFlags::Outline) + { + auto shadowPos = Vector2(drawPos.x + shadowOffset * rString.Scale.y, drawPos.y + shadowOffset * rString.Scale.y); + _gameFont->DrawString( + _spriteBatch.get(), rString.String.c_str(), + shadowPos, + (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, + rString.Rotation, Vector2::Zero, rString.Scale); + } + + _gameFont->DrawString( + _spriteBatch.get(), rString.String.c_str(), + drawPos, + (rString.Color * rString.Color.w) * ScreenFadeCurrent, + rString.Rotation, Vector2::Zero, rString.Scale); + } + + _spriteBatch->End(); + + if (currentHasScissor) + ResetScissor(); + } + } + + if (!itemsDrawn) + DrawDisplayItems(); + } + void Renderer::CollectDisplaySprites(RenderView& renderView) { constexpr auto DISPLAY_SPACE_ASPECT = DISPLAY_SPACE_RES.x / DISPLAY_SPACE_RES.y; @@ -612,6 +881,14 @@ namespace TEN::Renderer displaySprite.BlendMode, layout.AspectCorrection, renderView); + + // Transfer scissor rect from source sprite. + if (displaySprite.HasScissor) + { + auto& last = renderView.DisplaySpritesToDraw.back(); + last.HasScissor = true; + last.ScissorRect = displaySprite.ScissorRect; + } } std::sort( diff --git a/TombEngine/Renderer/RendererDrawEffect.cpp b/TombEngine/Renderer/RendererDrawEffect.cpp index b81f9c6fa1..74219bb709 100644 --- a/TombEngine/Renderer/RendererDrawEffect.cpp +++ b/TombEngine/Renderer/RendererDrawEffect.cpp @@ -27,6 +27,7 @@ #include "Game/misc.h" #include "Game/Setup.h" #include "Math/Math.h" +#include "Objects/Effects/Fireflies.h" #include "Objects/TR5/Trap/LaserBarrier.h" #include "Objects/TR5/Trap/LaserBeam.h" #include "Objects/Utils/object_helper.h" @@ -35,7 +36,6 @@ #include "Scripting/Include/Flow/ScriptInterfaceFlowHandler.h" #include "Specific/level.h" #include "Structures/RendererSpriteBucket.h" -#include "Objects/Effects/Fireflies.h" using namespace TEN::Animation; using namespace TEN::Effects::Blood; @@ -417,8 +417,8 @@ namespace TEN::Renderer { for (const auto& fire : Fires) { - auto oldFade = fire.PrevFade == 1 ? 1.0f : (float)(255 - fire.PrevFade) / 255.0f; - auto fade = fire.fade == 1 ? 1.0f : (float)(255 - fire.fade) / 255.0f; + auto oldFade = fire.PrevFade == 1 ? 1.0f : (float)(UCHAR_MAX - fire.PrevFade) / (float)UCHAR_MAX; + auto fade = fire.fade == 1 ? 1.0f : (float)(UCHAR_MAX - fire.fade) / (float)UCHAR_MAX; fade = Lerp(oldFade, fade, GetInterpolationFactor()); for (int i = 0; i < MAX_SPARKS_FIRE; i++) @@ -429,22 +429,23 @@ namespace TEN::Renderer // Calculate original flame color. auto color = Vector4::Lerp( Vector4( - spark->PrevColor.x / 255.0f * fade, - spark->PrevColor.y / 255.0f * fade, - spark->PrevColor.z / 255.0f * fade, + spark->PrevColor.x / (float)UCHAR_MAX * fade, + spark->PrevColor.y / (float)UCHAR_MAX * fade, + spark->PrevColor.z / (float)UCHAR_MAX * fade, 1.0f), Vector4( - spark->color.x / 255.0f * fade, - spark->color.y / 255.0f * fade, - spark->color.z / 255.0f * fade, + spark->color.x / (float)UCHAR_MAX * fade, + spark->color.y / (float)UCHAR_MAX * fade, + spark->color.z / (float)UCHAR_MAX * fade, 1.0f), GetInterpolationFactor()); // Influence flame color with object color via chroma modulation. - if (fire.color != Vector4::One) + if (fire.color != NEUTRAL_COLOR) { auto color3 = Vector3(color.x, color.y, color.z); color = Vector4::Lerp(color, fire.color * Luma(color3), Chroma(color3) * 1.5f); + color.w = 1.0f; } AddSpriteBillboard( @@ -1251,7 +1252,7 @@ namespace TEN::Renderer auto* itemPtr = &_items[LaraItem->Index]; // Divide gunflash tint by 2 because tinting uses multiplication and additive color which doesn't look good with overbright color values. - _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = settings.ColorizeMuzzleFlash ? ((Vector4)settings.FlashColor / 2) : Vector4::One; + _stInstancedStaticMeshBuffer.StaticMeshes[0].Color = settings.ColorizeMuzzleFlash ? settings.FlashColor : NEUTRAL_COLOR; _stInstancedStaticMeshBuffer.StaticMeshes[0].Ambient = room.AmbientLight; _stInstancedStaticMeshBuffer.StaticMeshes[0].LightMode = (int)LightMode::Static; BindInstancedStaticLights(itemPtr->LightsToDraw, 0); @@ -1658,19 +1659,19 @@ namespace TEN::Renderer vtx0.Position = Vector3::Transform(deb.mesh.Positions[0], matrix); vtx0.UV = deb.mesh.TextureCoordinates[0]; vtx0.Normal = PackVector3(deb.mesh.Normals[0]); - vtx0.Color = VectorColorToRGBA_TempToVector4(deb.mesh.Colors[0]); + vtx0.Color = VectorColorToRGBA(deb.mesh.Colors[0]); Vertex vtx1; vtx1.Position = Vector3::Transform(deb.mesh.Positions[1], matrix); vtx1.UV = deb.mesh.TextureCoordinates[1]; vtx1.Normal = PackVector3(deb.mesh.Normals[1]); - vtx1.Color = VectorColorToRGBA_TempToVector4(deb.mesh.Colors[1]); + vtx1.Color = VectorColorToRGBA(deb.mesh.Colors[1]); Vertex vtx2; vtx2.Position = Vector3::Transform(deb.mesh.Positions[2], matrix); vtx2.UV = deb.mesh.TextureCoordinates[2]; vtx2.Normal = PackVector3(deb.mesh.Normals[2]); - vtx2.Color = VectorColorToRGBA_TempToVector4(deb.mesh.Colors[2]); + vtx2.Color = VectorColorToRGBA(deb.mesh.Colors[2]); _primitiveBatch->DrawTriangle(vtx0, vtx1, vtx2); diff --git a/TombEngine/Renderer/RendererDrawMenu.cpp b/TombEngine/Renderer/RendererDrawMenu.cpp index c6f47c185e..a39ae96b5e 100644 --- a/TombEngine/Renderer/RendererDrawMenu.cpp +++ b/TombEngine/Renderer/RendererDrawMenu.cpp @@ -49,7 +49,7 @@ namespace TEN::Renderer // Vertical menu positioning templates constexpr auto MenuVerticalControls = 30; - constexpr auto MenuVerticalDisplaySettings = 130; + constexpr auto MenuVerticalDisplaySettings = 110; constexpr auto MenuVerticalOtherSettings = 50; constexpr auto MenuVerticalBottomCenter = 400; constexpr auto MenuVerticalStatisticsTitle = 150; @@ -223,14 +223,19 @@ namespace TEN::Renderer // Enable high framerate AddString(MenuLeftSideEntry, y, g_GameFlow->GetString(STRING_HIGH_FRAMERATE), optionColor, SF(titleOption == 7)); AddString(MenuRightSideEntry, y, Str_Enabled(g_Gui.GetCurrentSettings().Configuration.EnableHighFramerate), plainColor, SF(titleOption == 7)); + GetNextLinePosition(&y); + + // Gamma correction + AddString(MenuLeftSideEntry, y, g_GameFlow->GetString(STRING_GAMMA), optionColor, SF(titleOption == 8)); + AddString(MenuRightSideEntry, y, fmt::format("{:.1f}", g_Gui.GetCurrentSettings().Configuration.Gamma).c_str(), plainColor, SF(titleOption == 8)); GetNextBlockPosition(&y); // Apply - AddString(MenuCenterEntry, y, g_GameFlow->GetString(STRING_APPLY), optionColor, SF_Center(titleOption == 8)); + AddString(MenuCenterEntry, y, g_GameFlow->GetString(STRING_APPLY), optionColor, SF_Center(titleOption == 9)); GetNextLinePosition(&y); // Cancel - AddString(MenuCenterEntry, y, g_GameFlow->GetString(STRING_CANCEL), optionColor, SF_Center(titleOption == 9)); + AddString(MenuCenterEntry, y, g_GameFlow->GetString(STRING_CANCEL), optionColor, SF_Center(titleOption == 10)); break; case Menu::OtherSettings: @@ -534,22 +539,19 @@ namespace TEN::Renderer auto alignment = g_GameFlow->GetSettings()->UI.TitleMenuAlignment.has_value() ? (1 << (int)g_GameFlow->GetSettings()->UI.TitleMenuAlignment.value()) : 0; auto scale = g_GameFlow->GetSettings()->UI.TitleMenuScale; - // HACK: fix for Monty's color range slippage. Should be removed after merging color range fix PR. - auto plainRawColor = Vector4(plainColor.GetR(), plainColor.GetG(), plainColor.GetB(), UCHAR_MAX) / (float)UCHAR_MAX; - switch (menu) { case Menu::Title: // New game - AddString(g_GameFlow->GetString(STRING_NEW_GAME), menuPos.ToVector2(), plainRawColor, scale, SF(titleOption == selectedOption) | alignment); + AddString(g_GameFlow->GetString(STRING_NEW_GAME), menuPos.ToVector2(), plainColor, scale, SF(titleOption == selectedOption) | alignment); GetNextLinePosition(&menuPos.y, scale); selectedOption++; // Home Level if (g_GameFlow->IsHomeLevelEnabled()) { - AddString(g_GameFlow->GetString(STRING_HOME_LEVEL), menuPos.ToVector2(), plainRawColor, scale, SF(titleOption == selectedOption) | alignment); + AddString(g_GameFlow->GetString(STRING_HOME_LEVEL), menuPos.ToVector2(), plainColor, scale, SF(titleOption == selectedOption) | alignment); GetNextLinePosition(&menuPos.y, scale); selectedOption++; } @@ -557,18 +559,18 @@ namespace TEN::Renderer // Load game if (g_GameFlow->IsLoadSaveEnabled()) { - AddString(g_GameFlow->GetString(STRING_LOAD_GAME), menuPos.ToVector2(), plainRawColor, scale, SF(titleOption == selectedOption) | alignment); + AddString(g_GameFlow->GetString(STRING_LOAD_GAME), menuPos.ToVector2(), plainColor, scale, SF(titleOption == selectedOption) | alignment); GetNextLinePosition(&menuPos.y, scale); selectedOption++; } // Options - AddString(g_GameFlow->GetString(STRING_OPTIONS), menuPos.ToVector2(), plainRawColor, scale, SF(titleOption == selectedOption) | alignment); + AddString(g_GameFlow->GetString(STRING_OPTIONS), menuPos.ToVector2(), plainColor, scale, SF(titleOption == selectedOption) | alignment); GetNextLinePosition(&menuPos.y, scale); selectedOption++; // Exit game - AddString(g_GameFlow->GetString(STRING_EXIT_GAME), menuPos.ToVector2(), plainRawColor, scale, SF(titleOption == selectedOption) | alignment); + AddString(g_GameFlow->GetString(STRING_EXIT_GAME), menuPos.ToVector2(), plainColor, scale, SF(titleOption == selectedOption) | alignment); break; case Menu::LoadGame: @@ -851,7 +853,8 @@ namespace TEN::Renderer } auto pos = _viewportToolkit.Unproject(Vector3(pos2D.x, pos2D.y, 1.0f), projMatrix, viewMatrix, Matrix::Identity); - auto color = Vector4(1.0f, 1.0f, 1.0f, opacity); + auto color = NEUTRAL_COLOR; + color.w = opacity; // Set vertex buffer. _context->IASetVertexBuffers(0, 1, _moveablesVertexBuffer.Buffer.GetAddressOf(), &stride, &offset); @@ -865,7 +868,9 @@ namespace TEN::Renderer hudCamera.ViewProjection = viewMatrix * projMatrix; hudCamera.Frame = GlobalCounter; hudCamera.InterpolatedFrame = (float)GlobalCounter + GetInterpolationFactor(); + hudCamera.Gamma = g_Configuration.Gamma; UpdateConstantBuffer(hudCamera, _cbCameraMatrices); + BindConstantBufferPS(ConstantBufferRegister::Camera, _cbCameraMatrices.get()); BindConstantBufferVS(ConstantBufferRegister::Camera, _cbCameraMatrices.get()); _shaders.Bind(Shader::Inventory); @@ -1037,7 +1042,9 @@ namespace TEN::Renderer auto hudCamera = CCameraMatrixBuffer{}; hudCamera.CamDirectionWS = -Vector4::UnitZ; hudCamera.ViewProjection = viewMatrix * projMatrix; + hudCamera.Gamma = g_Configuration.Gamma; _cbCameraMatrices.UpdateData(hudCamera, _context.Get()); + BindConstantBufferPS(ConstantBufferRegister::Camera, _cbCameraMatrices.get()); BindConstantBufferVS(ConstantBufferRegister::Camera, _cbCameraMatrices.get()); _shaders.Bind(Shader::Inventory); @@ -1403,12 +1410,10 @@ namespace TEN::Renderer RenderScene(&_backBuffer, _gameCamera, SceneRenderMode::NoHud); } - // Draw display sprites sorted by priority. + // Draw display sprites and strings sorted by priority. CollectDisplaySprites(_gameCamera); - DrawDisplaySprites(_gameCamera, false); - DrawDisplayItems(); - DrawDisplaySprites(_gameCamera, true); - DrawAllStrings(); + DrawAllDisplayLayers(_gameCamera); + DrawDebugDisplayRects(); if (staticBackground) { @@ -1423,6 +1428,9 @@ namespace TEN::Renderer ClearScene(); _context->ClearState(); + _lastBlendMode = BlendMode::Unknown; + _lastCullMode = CullMode::Unknown; + _lastDepthState = DepthState::Unknown; _swapChain->Present(1, 0); } diff --git a/TombEngine/Renderer/RendererFrame.cpp b/TombEngine/Renderer/RendererFrame.cpp index 33c6d8e00e..b4c5d14a55 100644 --- a/TombEngine/Renderer/RendererFrame.cpp +++ b/TombEngine/Renderer/RendererFrame.cpp @@ -24,6 +24,7 @@ using namespace TEN::Effects::Decal; using namespace TEN::Effects::Environment; using namespace TEN::Entities::Effects; using namespace TEN::Math; +using namespace TEN::SpotCam; using namespace TEN::Utils; namespace TEN::Renderer @@ -840,9 +841,6 @@ namespace TEN::Renderer item->AmbientLight.y = Lerp(prev.y, next.y, item->LightFade); item->AmbientLight.z = Lerp(prev.z, next.z, item->LightFade); } - - // Multiply calculated ambient light by object tint - item->AmbientLight *= nativeItem->Model.Color; } void Renderer::CollectDecalsForRoom(short roomNumber, RenderView& renderView) diff --git a/TombEngine/Renderer/RendererInit.cpp b/TombEngine/Renderer/RendererInit.cpp index 455b718f00..befce5a147 100644 --- a/TombEngine/Renderer/RendererInit.cpp +++ b/TombEngine/Renderer/RendererInit.cpp @@ -46,7 +46,7 @@ namespace TEN::Renderer { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "NORMAL", 0, DXGI_FORMAT_R8G8B8A8_SNORM, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, - { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "TANGENT", 0, DXGI_FORMAT_R8G8B8A8_SNORM, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "NORMAL", 1, DXGI_FORMAT_R8G8B8A8_SNORM, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "BONEINDICES", 0, DXGI_FORMAT_R8G8B8A8_UINT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, @@ -330,7 +330,7 @@ namespace TEN::Renderer normal.Normalize(); quadVertices[0].Normal = PackVector3(normal); quadVertices[0].UV = Vector2(0, 1); - quadVertices[0].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + quadVertices[0].Color = VectorColorToRGBA(NEUTRAL_COLOR); quadVertices[0].Effects = 3 << INDEX_IN_POLY_VERTEX_SHIFT; //Top Left @@ -339,7 +339,7 @@ namespace TEN::Renderer normal.Normalize(); quadVertices[1].Normal = PackVector3(normal); quadVertices[1].UV = Vector2(0, 0); - quadVertices[1].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + quadVertices[1].Color = VectorColorToRGBA(NEUTRAL_COLOR); quadVertices[1].Effects = 0 << INDEX_IN_POLY_VERTEX_SHIFT; //Top Right @@ -348,7 +348,7 @@ namespace TEN::Renderer normal.Normalize(); quadVertices[3].Normal = PackVector3(normal); quadVertices[3].UV = Vector2(1, 0); - quadVertices[3].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + quadVertices[3].Color = VectorColorToRGBA(NEUTRAL_COLOR); quadVertices[3].Effects = 1 << INDEX_IN_POLY_VERTEX_SHIFT; //Bottom Right @@ -357,7 +357,7 @@ namespace TEN::Renderer normal.Normalize(); quadVertices[2].Normal = PackVector3(normal); quadVertices[2].UV = Vector2(1, 1); - quadVertices[2].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + quadVertices[2].Color = VectorColorToRGBA(NEUTRAL_COLOR); quadVertices[2].Effects = 2 << INDEX_IN_POLY_VERTEX_SHIFT; _quadVertexBuffer = VertexBuffer(_device.Get(), 4, quadVertices.data()); @@ -391,7 +391,7 @@ namespace TEN::Renderer vertices[lastVertex].Position.z = -size / 2.0f + (z + 1) * 512.0f; vertices[lastVertex].UV.x = x / 20.0f; vertices[lastVertex].UV.y = (z + 1) / 20.0f; - vertices[lastVertex].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + vertices[lastVertex].Color = VectorColorToRGBA(Vector4::One); lastVertex++; @@ -400,7 +400,7 @@ namespace TEN::Renderer vertices[lastVertex].Position.z = -size / 2.0f + (z + 1) * 512.0f; vertices[lastVertex].UV.x = (x + 1) / 20.0f; vertices[lastVertex].UV.y = (z + 1) / 20.0f; - vertices[lastVertex].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + vertices[lastVertex].Color = VectorColorToRGBA(Vector4::One); lastVertex++; @@ -409,7 +409,7 @@ namespace TEN::Renderer vertices[lastVertex].Position.z = -size / 2.0f + z * 512.0f; vertices[lastVertex].UV.x = (x + 1) / 20.0f; vertices[lastVertex].UV.y = z / 20.0f; - vertices[lastVertex].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + vertices[lastVertex].Color = VectorColorToRGBA(Vector4::One); lastVertex++; @@ -418,7 +418,7 @@ namespace TEN::Renderer vertices[lastVertex].Position.z = -size / 2.0f + z * 512.0f; vertices[lastVertex].UV.x = x / 20.0f; vertices[lastVertex].UV.y = z / 20.0f; - vertices[lastVertex].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + vertices[lastVertex].Color = VectorColorToRGBA(Vector4::One); lastVertex++; } diff --git a/TombEngine/Renderer/RendererSprites.cpp b/TombEngine/Renderer/RendererSprites.cpp index e8aaa08324..aa4a8adc19 100644 --- a/TombEngine/Renderer/RendererSprites.cpp +++ b/TombEngine/Renderer/RendererSprites.cpp @@ -29,11 +29,7 @@ namespace TEN::Renderer spr.Height = size.y; spr.BlendMode = blendMode; spr.SoftParticle = isSoftParticle; - spr.c1 = color; - spr.c2 = color; - spr.c3 = color; - spr.c4 = color; - spr.color = color; + spr.c1 = spr.c2 = spr.c3 = spr.c4 = spr.color = color; spr.renderType = renderType; view.SpritesToDraw.push_back(spr); @@ -61,11 +57,7 @@ namespace TEN::Renderer spr.BlendMode = blendMode; spr.ConstrainAxis = constrainAxis; spr.SoftParticle = isSoftParticle; - spr.c1 = color; - spr.c2 = color; - spr.c3 = color; - spr.c4 = color; - spr.color = color; + spr.c1 = spr.c2 = spr.c3 = spr.c4 = spr.color = color; spr.renderType = renderType; view.SpritesToDraw.push_back(spr); @@ -93,11 +85,7 @@ namespace TEN::Renderer spr.BlendMode = blendMode; spr.LookAtAxis = lookAtAxis; spr.SoftParticle = isSoftParticle; - spr.c1 = color; - spr.c2 = color; - spr.c3 = color; - spr.c4 = color; - spr.color = color; + spr.c1 = spr.c2 = spr.c3 = spr.c4 = spr.color = color; spr.renderType = renderType; view.SpritesToDraw.push_back(spr); @@ -369,7 +357,7 @@ namespace TEN::Renderer auto vertex0 = Vertex{}; vertex0.Position = rDrawSprite.vtx1; vertex0.UV = rDrawSprite.Sprite->UV[0]; - vertex0.Color = VectorColorToRGBA_TempToVector4(rDrawSprite.c1); + vertex0.Color = VectorColorToRGBA(rDrawSprite.c1); vertex0.Effects = 0 << INDEX_IN_POLY_VERTEX_SHIFT; ReflectVectorOptionally(vertex0.Position); @@ -377,7 +365,7 @@ namespace TEN::Renderer auto vertex1 = Vertex{}; vertex1.Position = rDrawSprite.vtx2; vertex1.UV = rDrawSprite.Sprite->UV[1]; - vertex1.Color = VectorColorToRGBA_TempToVector4(rDrawSprite.c2); + vertex1.Color = VectorColorToRGBA(rDrawSprite.c2); vertex1.Effects = 1 << INDEX_IN_POLY_VERTEX_SHIFT; ReflectVectorOptionally(vertex1.Position); @@ -385,7 +373,7 @@ namespace TEN::Renderer auto vertex2 = Vertex{}; vertex2.Position = rDrawSprite.vtx3; vertex2.UV = rDrawSprite.Sprite->UV[2]; - vertex2.Color = VectorColorToRGBA_TempToVector4(rDrawSprite.c3); + vertex2.Color = VectorColorToRGBA(rDrawSprite.c3); vertex2.Effects = 2 << INDEX_IN_POLY_VERTEX_SHIFT; ReflectVectorOptionally(vertex2.Position); @@ -393,7 +381,7 @@ namespace TEN::Renderer auto vertex3 = Vertex{}; vertex3.Position = rDrawSprite.vtx4; vertex3.UV = rDrawSprite.Sprite->UV[3]; - vertex3.Color = VectorColorToRGBA_TempToVector4(rDrawSprite.c4); + vertex3.Color = VectorColorToRGBA(rDrawSprite.c4); vertex3.Effects = 3 << INDEX_IN_POLY_VERTEX_SHIFT; ReflectVectorOptionally(vertex3.Position); @@ -464,25 +452,25 @@ namespace TEN::Renderer auto vertex0 = Vertex{}; vertex0.Position = object->Sprite->vtx1; vertex0.UV = object->Sprite->Sprite->UV[0]; - vertex0.Color = VectorColorToRGBA_TempToVector4(object->Sprite->c1); + vertex0.Color = VectorColorToRGBA(object->Sprite->c1); vertex0.Effects = 0 << INDEX_IN_POLY_VERTEX_SHIFT; auto vertex1 = Vertex{}; vertex1.Position = object->Sprite->vtx2; vertex1.UV = object->Sprite->Sprite->UV[1]; - vertex1.Color = VectorColorToRGBA_TempToVector4(object->Sprite->c2); + vertex1.Color = VectorColorToRGBA(object->Sprite->c2); vertex1.Effects = 1 << INDEX_IN_POLY_VERTEX_SHIFT; auto vertex2 = Vertex{}; vertex2.Position = object->Sprite->vtx3; vertex2.UV = object->Sprite->Sprite->UV[2]; - vertex2.Color = VectorColorToRGBA_TempToVector4(object->Sprite->c3); + vertex2.Color = VectorColorToRGBA(object->Sprite->c3); vertex2.Effects = 2 << INDEX_IN_POLY_VERTEX_SHIFT; auto vertex3 = Vertex{}; vertex3.Position = object->Sprite->vtx4; vertex3.UV = object->Sprite->Sprite->UV[3]; - vertex3.Color = VectorColorToRGBA_TempToVector4(object->Sprite->c4); + vertex3.Color = VectorColorToRGBA(object->Sprite->c4); vertex3.Effects = 3 << INDEX_IN_POLY_VERTEX_SHIFT; _spriteVertices.clear(); diff --git a/TombEngine/Renderer/RendererString.cpp b/TombEngine/Renderer/RendererString.cpp index a064e7c947..a8b31c45b0 100644 --- a/TombEngine/Renderer/RendererString.cpp +++ b/TombEngine/Renderer/RendererString.cpp @@ -1,9 +1,14 @@ #include "framework.h" #include "Renderer/Renderer.h" +#include + +#include "Game/effects/DisplaySprite.h" #include "Scripting/Include/Flow/ScriptInterfaceFlowHandler.h" #include "Specific/trutils.h" +using namespace TEN::Effects::DisplaySprite; + namespace TEN::Renderer { void Renderer::AddDebugString(const std::string& string, const Vector2& pos, const Color& color, float scale, RendererDebugPage page) @@ -24,6 +29,26 @@ namespace TEN::Renderer AddString(string, Vector2(x, y), Color(color), 1.0f, flags); } + Vector2 Renderer::GetDisplayStringSize(const std::string& text, const Vector2& scale) const + { + if (text.empty() || _gameFont == nullptr) + return Vector2::Zero; + + auto screenRes = GetScreenResolution(); + auto factor = Vector2((float)screenRes.x / DISPLAY_SPACE_RES.x, (float)screenRes.y / DISPLAY_SPACE_RES.y); + float uiScale = (screenRes.x > screenRes.y) ? factor.y : factor.x; + float fontSpacing = _gameFont->GetLineSpacing(); + float fontScale = REFERENCE_FONT_SIZE / fontSpacing; + auto stringScale = Vector2(uiScale * fontScale) * scale; + float baseScale = stringScale.y; + + auto wtext = TEN::Utils::ToWString(text); + auto measured = Vector2(_gameFont->MeasureString(wtext.c_str())) * baseScale; + + // Convert pixel size back to display space (800x600 units). + return Vector2(measured.x / factor.x, measured.y / factor.y); + } + void Renderer::AddString(const std::string& string, const Vector2& pos, const Color& color, float scale, int flags) { AddString(string, pos, Vector2::Zero, Color(color), scale, flags); @@ -34,7 +59,21 @@ namespace TEN::Renderer AddString(string, pos, pos, area, color, scale, flags); } + void Renderer::AddString(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, + const Color& color, const Vector2& scale, float rotation, int flags, + int priority, BlendMode blendMode) + { + AddStringInternal(string, pos, prevPos, area, color, scale, rotation, flags, priority, blendMode); + } + void Renderer::AddString(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, const Color& color, float scale, int flags) + { + AddStringInternal(string, pos, prevPos, area, color, Vector2(scale), 0.0f, flags, 0, BlendMode::AlphaBlend); + } + + void Renderer::AddStringInternal(const std::string& string, const Vector2& pos, const Vector2& prevPos, const Vector2& area, + const Color& color, const Vector2& scale, float rotation, int flags, + int priority, BlendMode blendMode) { if (_isLocked) return; @@ -49,8 +88,9 @@ namespace TEN::Renderer float uiScale = (screenRes.x > screenRes.y) ? factor.y : factor.x; float fontSpacing = _gameFont->GetLineSpacing(); float fontScale = REFERENCE_FONT_SIZE / fontSpacing; - float stringScale = (uiScale * fontScale) * scale; - float spaceWidth = Vector3(_gameFont->MeasureString(L" ")).x * stringScale; + auto stringScale = Vector2(uiScale * fontScale) * scale; + float baseScale = stringScale.y; + float spaceWidth = Vector3(_gameFont->MeasureString(L" ")).x * baseScale; std::vector stringLines; @@ -74,7 +114,7 @@ namespace TEN::Renderer for (const auto& word : words) { - float wordWidth = Vector3(_gameFont->MeasureString(word.c_str())).x * stringScale; + float wordWidth = Vector3(_gameFont->MeasureString(word.c_str())).x * baseScale; if (!currentLine.empty() && (currentLineWidth + wordWidth + spaceWidth > area.x * factor.x)) { @@ -107,9 +147,9 @@ namespace TEN::Renderer for (const auto& line : stringLines) { if (line.empty()) - totalHeight += fontSpacing * stringScale; + totalHeight += fontSpacing * baseScale; else - totalHeight += Vector2(_gameFont->MeasureString(line.c_str())).y * stringScale; + totalHeight += Vector2(_gameFont->MeasureString(line.c_str())).y * baseScale; } // Calculate maximum textbox height. @@ -142,9 +182,15 @@ namespace TEN::Renderer rString.Position = Vector2::Zero; rString.Color = color; rString.Scale = stringScale; + rString.Rotation = rotation; + rString.Priority = priority; + rString.Blend = blendMode; + rString.HasScissor = HasActiveDisplayScissor(); + if (rString.HasScissor) + rString.ScissorRect = GetActiveDisplayScissor(); // Measure string. - auto stringSize = line.empty() ? Vector2(0, fontSpacing * rString.Scale) : Vector2(_gameFont->MeasureString(line.c_str())) * rString.Scale; + auto stringSize = line.empty() ? Vector2(0, fontSpacing * baseScale) : Vector2(_gameFont->MeasureString(line.c_str())) * baseScale; // If height clipping enabled, stop drawing when exceeding maxHeight. if (maxHeight > 0.0f && (yOffset + stringSize.y) > maxHeight) @@ -164,7 +210,7 @@ namespace TEN::Renderer else { // Calculate indentation to account for string scaling. - auto indent = line.empty() ? 0 : _gameFont->FindGlyph(line.at(0))->XAdvance * rString.Scale; + auto indent = line.empty() ? 0 : _gameFont->FindGlyph(line.at(0))->XAdvance * baseScale; rString.Position.x = pos.x * factor.x + indent; rString.PrevPosition.x = prevPos.x * factor.x + indent; @@ -194,35 +240,81 @@ namespace TEN::Renderer if (_stringsToDraw.empty()) return; - SetBlendMode(BlendMode::AlphaBlend); + // Sort by priority (lower priority draws first, i.e. behind higher). + std::stable_sort(_stringsToDraw.begin(), _stringsToDraw.end(), + [](const auto& a, const auto& b) { return a.Priority < b.Priority; }); float shadowOffset = 1.5f / (REFERENCE_FONT_SIZE / _gameFont->GetLineSpacing()); auto shadowColor = (Vector4)g_GameFlow->GetSettings()->UI.ShadowTextColor; - _spriteBatch->Begin(); + ResetScissor(); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + + auto currentBlend = BlendMode::AlphaBlend; + SetBlendMode(currentBlend); + + bool currentHasScissor = false; + auto currentScissor = RendererRectangle{}; for (const auto& rString : _stringsToDraw) { + // Switch blend mode per string if needed. + if (rString.Blend != currentBlend) + { + _spriteBatch->End(); + currentBlend = rString.Blend; + SetBlendMode(currentBlend); + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + + // Handle scissor rect changes. + bool scissorChanged = (rString.HasScissor != currentHasScissor) || + (rString.HasScissor && + (rString.ScissorRect.Left != currentScissor.Left || + rString.ScissorRect.Top != currentScissor.Top || + rString.ScissorRect.Right != currentScissor.Right || + rString.ScissorRect.Bottom != currentScissor.Bottom)); + + if (scissorChanged) + { + _spriteBatch->End(); + + if (rString.HasScissor) + SetScissor(rString.ScissorRect); + else + ResetScissor(); + + currentHasScissor = rString.HasScissor; + currentScissor = rString.ScissorRect; + _spriteBatch->Begin(SpriteSortMode_Deferred, nullptr, nullptr, nullptr, _cullNoneRasterizerState.Get()); + } + auto drawPos = Vector2::Lerp(rString.PrevPosition, rString.Position, GetInterpolationFactor()); // Draw shadow. if (rString.Flags & (int)PrintStringFlags::Outline) { + auto shadowPos = Vector2(drawPos.x + shadowOffset * rString.Scale.y, drawPos.y + shadowOffset * rString.Scale.y); + _gameFont->DrawString( _spriteBatch.get(), rString.String.c_str(), - Vector2(drawPos.x + shadowOffset * rString.Scale, drawPos.y + shadowOffset * rString.Scale), + shadowPos, (shadowColor * rString.Color.w * shadowColor.w) * ScreenFadeCurrent, - 0.0f, Vector4::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale); } // Draw string. _gameFont->DrawString( _spriteBatch.get(), rString.String.c_str(), - Vector2(drawPos.x, drawPos.y), + drawPos, (rString.Color * rString.Color.w) * ScreenFadeCurrent, - 0.0f, Vector4::Zero, rString.Scale); + rString.Rotation, Vector2::Zero, rString.Scale); } _spriteBatch->End(); + + // Reset scissor if it was active. + if (currentHasScissor) + ResetScissor(); } } diff --git a/TombEngine/Renderer/Structures/RendererHudBar.h b/TombEngine/Renderer/Structures/RendererHudBar.h index 25c765e4d2..0a70d75f00 100644 --- a/TombEngine/Renderer/Structures/RendererHudBar.h +++ b/TombEngine/Renderer/Structures/RendererHudBar.h @@ -155,7 +155,7 @@ namespace TEN::Renderer::Structures for (int i = 0; i < VERTEX_COUNT; i++) { vertices[i].Position = barVertices[i]; - vertices[i].Color = VectorColorToRGBA_TempToVector4(colors[i]); + vertices[i].Color = VectorColorToRGBA(colors[i]); vertices[i].UV = barUVs[i]; } @@ -166,7 +166,7 @@ namespace TEN::Renderer::Structures for (int i = 0; i < barBorderVertices.size(); i++) { borderVertices[i].Position = barBorderVertices[i]; - borderVertices[i].Color = VectorColorToRGBA_TempToVector4(Vector4::One); + borderVertices[i].Color = VectorColorToRGBA(Vector4::One); borderVertices[i].UV = barBorderUVs[i]; } diff --git a/TombEngine/Renderer/Structures/RendererItem.h b/TombEngine/Renderer/Structures/RendererItem.h index c65d1ea78f..95d837b580 100644 --- a/TombEngine/Renderer/Structures/RendererItem.h +++ b/TombEngine/Renderer/Structures/RendererItem.h @@ -21,8 +21,8 @@ namespace TEN::Renderer::Structures Quaternion BoneOrientations[MAX_BONES]; - Vector4 Color = Vector4::One; - Vector4 AmbientLight = Vector4::One; + Vector4 Color = NEUTRAL_COLOR; + Vector4 AmbientLight = NEUTRAL_COLOR; int SkinIndex = NO_VALUE; std::vector MeshIndex = {}; diff --git a/TombEngine/Renderer/Structures/RendererSprite2D.h b/TombEngine/Renderer/Structures/RendererSprite2D.h index 50c4ecb011..9b1e849844 100644 --- a/TombEngine/Renderer/Structures/RendererSprite2D.h +++ b/TombEngine/Renderer/Structures/RendererSprite2D.h @@ -1,5 +1,6 @@ #pragma once +#include "Renderer/Structures/RendererRectangle.h" #include "Renderer/Structures/RendererSprite.h" #include "Renderer/RendererEnums.h" @@ -18,5 +19,8 @@ namespace TEN::Renderer::Structures BlendMode BlendMode = BlendMode::AlphaBlend; Vector2 AspectCorrection = Vector2::One; + + bool HasScissor = false; + RendererRectangle ScissorRect = {}; }; } diff --git a/TombEngine/Renderer/Structures/RendererStringToDraw.h b/TombEngine/Renderer/Structures/RendererStringToDraw.h index 379dba178d..809142f6a8 100644 --- a/TombEngine/Renderer/Structures/RendererStringToDraw.h +++ b/TombEngine/Renderer/Structures/RendererStringToDraw.h @@ -1,6 +1,9 @@ #pragma once #include +#include "Renderer/RendererEnums.h" +#include "Renderer/Structures/RendererRectangle.h" + namespace TEN::Renderer::Structures { using namespace DirectX::SimpleMath; @@ -12,6 +15,12 @@ namespace TEN::Renderer::Structures int Flags; std::wstring String; Vector4 Color; - float Scale; + Vector2 Scale; + float Rotation = 0.0f; + int Priority = 0; + BlendMode Blend = BlendMode::AlphaBlend; + + bool HasScissor = false; + RendererRectangle ScissorRect = {}; }; } diff --git a/TombEngine/Scripting/Include/ScriptInterfaceGame.h b/TombEngine/Scripting/Include/ScriptInterfaceGame.h index 6f7fa991ae..6c2d5bf882 100644 --- a/TombEngine/Scripting/Include/ScriptInterfaceGame.h +++ b/TombEngine/Scripting/Include/ScriptInterfaceGame.h @@ -75,6 +75,8 @@ class ScriptInterfaceGame virtual void GetVariables(std::vector& vars) = 0; virtual void SetVariables(const std::vector& vars, bool onlyLevelVars) = 0; + virtual void GetGlobalVariables(std::vector& vars) = 0; + virtual void SetGlobalVariables(const std::vector& vars) = 0; virtual void GetCallbackStrings( std::vector& preStart, diff --git a/TombEngine/Scripting/Internal/LanguageScript.h b/TombEngine/Scripting/Internal/LanguageScript.h index 160865271a..ef01af88e3 100644 --- a/TombEngine/Scripting/Internal/LanguageScript.h +++ b/TombEngine/Scripting/Internal/LanguageScript.h @@ -63,6 +63,7 @@ #define STRING_MUSIC_VOLUME "music_volume" #define STRING_SFX_VOLUME "sfx_volume" #define STRING_SCREEN_RESOLUTION "screen_resolution" +#define STRING_GAMMA "gamma" #define STRING_SHADOWS "shadows" #define STRING_SHADOWS_PLAYER "player" #define STRING_SHADOWS_ALL "all" diff --git a/TombEngine/Scripting/Internal/ReservedScriptNames.h b/TombEngine/Scripting/Internal/ReservedScriptNames.h index 2afab69f34..f0812e1cbd 100644 --- a/TombEngine/Scripting/Internal/ReservedScriptNames.h +++ b/TombEngine/Scripting/Internal/ReservedScriptNames.h @@ -52,6 +52,11 @@ static constexpr char ScriptReserved_GetPlayerInteractedMoveable[] = "GetInterac static constexpr char ScriptReserved_DisplaySprite[] = "DisplaySprite"; static constexpr char ScriptReserved_DisplayStringGetObjectID[] = "GetObjectID"; static constexpr char ScriptReserved_DisplayStringGetSpriteID[] = "GetSpriteID"; + +// DisplayString (View) object +static constexpr char ScriptReserved_GetText[] = "GetText"; +static constexpr char ScriptReserved_SetText[] = "SetText"; +static constexpr char ScriptReserved_GetTranslated[] = "GetTranslated"; static constexpr char ScriptReserved_DisplayStringGetPosition[] = "GetPosition"; static constexpr char ScriptReserved_DisplayStringGetRotation[] = "GetRotation"; static constexpr char ScriptReserved_DisplayStringGetScale[] = "GetScale"; @@ -65,6 +70,15 @@ static constexpr char ScriptReserved_DisplayStringSetScale[] = "SetScale"; static constexpr char ScriptReserved_DisplayStringSetColor[] = "SetColor"; static constexpr char ScriptReserved_DisplaySpriteDraw[] = "Draw"; +// DisplayArea (View) object +static constexpr char ScriptReserved_DisplayArea[] = "DisplayArea"; +static constexpr char ScriptReserved_GetSize[] = "GetSize"; +static constexpr char ScriptReserved_SetSize[] = "SetSize"; +static constexpr char ScriptReserved_AddItem[] = "AddItem"; +static constexpr char ScriptReserved_RemoveItem[] = "RemoveItem"; +static constexpr char ScriptReserved_Clear[] = "Clear"; +static constexpr char ScriptReserved_Debug[] = "Debug"; + static constexpr char ScriptReserved_EndReasonDeath[] = "DEATH"; static constexpr char ScriptReserved_EndReasonExitToTitle[] = "EXIT_TO_TITLE"; static constexpr char ScriptReserved_EndReasonLevelComplete[] = "LEVEL_COMPLETE"; @@ -451,6 +465,7 @@ static constexpr char ScriptReserved_FeatherMode[] = "StreamerFeatherMode"; static constexpr char ScriptReserved_LevelVars[] = "LevelVars"; static constexpr char ScriptReserved_GameVars[] = "GameVars"; +static constexpr char ScriptReserved_GlobalVars[] = "GlobalVars"; static constexpr char ScriptReserved_LevelFuncs[] = "LevelFuncs"; static constexpr char ScriptReserved_Engine[] = "Engine"; static constexpr char ScriptReserved_External[] = "External"; diff --git a/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.cpp b/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.cpp index e9a76c7ed4..456ce709d6 100644 --- a/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.cpp +++ b/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.cpp @@ -141,14 +141,7 @@ LogicHandler::LogicHandler(sol::state* lua, sol::table& parent) : _handler{ lua LevelFunc::Register(tableLogic); ResetScripts(true); -} - -void LogicHandler::ResetGameTables() -{ - auto state = _handler.GetState(); - MakeSpecialTable(state, ScriptReserved_GameVars, &GetVariable, &SetVariable); - - (*state)[ScriptReserved_GameVars][ScriptReserved_Engine] = sol::table(*state, sol::create); + ResetGlobalTables(); } /*** Register a function as a callback. @@ -301,6 +294,22 @@ void LogicHandler::ResetLevelTables() (*state)[ScriptReserved_LevelVars][ScriptReserved_Engine] = sol::table{ *state, sol::create }; } +void LogicHandler::ResetGameTables() +{ + auto state = _handler.GetState(); + MakeSpecialTable(state, ScriptReserved_GameVars, &GetVariable, &SetVariable); + + (*state)[ScriptReserved_GameVars][ScriptReserved_Engine] = sol::table(*state, sol::create); +} + +void LogicHandler::ResetGlobalTables() +{ + auto state = _handler.GetState(); + MakeSpecialTable(state, ScriptReserved_GlobalVars, &GetVariable, &SetVariable); + + (*state)[ScriptReserved_GlobalVars][ScriptReserved_Engine] = sol::table(*state, sol::create); +} + sol::object LogicHandler::GetLevelFuncsMember(sol::table tab, const std::string& name) { auto partName = tab.raw_get(strKey); @@ -441,102 +450,6 @@ void LogicHandler::FreeLevelScripts() _handler.GetState()->collect_garbage(); } -// Used when loading. -void LogicHandler::SetVariables(const std::vector& vars, bool onlyLevelVars) -{ - if (!onlyLevelVars) - ResetGameTables(); - - ResetLevelTables(); - - auto solTables = std::unordered_map{}; - - for(int i = 0; i < vars.size(); ++i) - { - if (std::holds_alternative(vars[i])) - { - solTables.try_emplace(i, *_handler.GetState(), sol::create); - auto indexTab = std::get(vars[i]); - for (auto& [first, second] : indexTab) - { - // if we're wanting to reference a table, make sure that table exists - // create it if need be - if (std::holds_alternative(vars[second])) - { - solTables.try_emplace(second, *_handler.GetState(), sol::create); - solTables[i][vars[first]] = solTables[second]; - } - else if (std::holds_alternative(vars[second])) - { - double theNum = std::get(vars[second]); - // If this is representable as an integer use an integer. - // This is to ensure something saved as 1 is not loaded as 1.0 - // which would be confusing for the user. - // todo: should we throw a warning if the user tries to save or load a value - // outside of these bounds? - squidshire 30/04/2022 - if (std::trunc(theNum) == theNum && theNum <= INT64_MAX && theNum >= INT64_MIN) - { - solTables[i][vars[first]] = (int64_t)theNum; - } - else - { - solTables[i][vars[first]] = vars[second]; - } - } - else if (vars[second].index() == (int)SavedVarType::Vec2) - { - auto vec2 = Vec2(std::get<(int)SavedVarType::Vec2>(vars[second])); - solTables[i][vars[first]] = vec2; - } - else if (vars[second].index() == int(SavedVarType::Vec3)) - { - auto vec3 = Vec3(std::get(vars[second])); - solTables[i][vars[first]] = vec3; - } - else if (vars[second].index() == int(SavedVarType::Rotation)) - { - auto vec3 = Rotation(std::get(vars[second])); - solTables[i][vars[first]] = vec3; - } - else if (vars[second].index() == int(SavedVarType::Time)) - { - auto time = Time(std::get(vars[second])); - solTables[i][vars[first]] = time; - } - else if (vars[second].index() == int(SavedVarType::Color)) - { - auto color = D3DCOLOR(std::get(vars[second])); - solTables[i][vars[first]] = ScriptColor{color}; - } - else if (std::holds_alternative(vars[second])) - { - LevelFunc levelFunc; - levelFunc.m_funcName = std::get(vars[second]).name; - levelFunc.m_handler = this; - solTables[i][vars[first]] = levelFunc; - } - else - { - solTables[i][vars[first]] = vars[second]; - } - } - } - } - - auto rootTable = solTables[0]; - - sol::table levelVars = rootTable[ScriptReserved_LevelVars]; - for (auto& [first, second] : levelVars) - (*_handler.GetState())[ScriptReserved_LevelVars][first] = second; - - if (onlyLevelVars) - return; - - sol::table gameVars = rootTable[ScriptReserved_GameVars]; - for (auto& [first, second] : gameVars) - (*_handler.GetState())[ScriptReserved_GameVars][first] = second; -} - template int Handle(TypeFrom& var, MapType& varsMap, size_t& numVars, std::vector& vars) { @@ -603,13 +516,8 @@ std::string LogicHandler::GetRequestedPath() const return path; } -// Used when saving. -void LogicHandler::GetVariables(std::vector& vars) +void LogicHandler::SerializeScriptTable(const sol::table& tab, std::vector& vars) { - auto tab = sol::table(*_handler.GetState(), sol::create); - tab[ScriptReserved_LevelVars] = (*_handler.GetState())[ScriptReserved_LevelVars]; - tab[ScriptReserved_GameVars] = (*_handler.GetState())[ScriptReserved_GameVars]; - auto varsMap = std::unordered_map{}; auto numMap = std::unordered_map{}; auto boolMap = std::unordered_map{}; @@ -668,7 +576,7 @@ void LogicHandler::GetVariables(std::vector& vars) { auto [first, second] = varsMap.insert(std::make_pair(obj.pointer(), (int)varCount)); - if(second) + if (second) { ++varCount; auto id = first->second; @@ -679,9 +587,8 @@ void LogicHandler::GetVariables(std::vector& vars) { bool validKey = true; unsigned int keyIndex = 0; - std::variant key{unsigned int(0)}; + std::variant key{ unsigned int(0) }; - // Strings and numbers can be keys AND values. switch (first.get_type()) { case sol::type::string: @@ -786,6 +693,152 @@ void LogicHandler::GetVariables(std::vector& vars) populate(tab); } +std::unordered_map LogicHandler::DeserializeScriptVars(const std::vector& vars) +{ + auto solTables = std::unordered_map{}; + + for (int i = 0; i < vars.size(); ++i) + { + if (std::holds_alternative(vars[i])) + { + solTables.try_emplace(i, *_handler.GetState(), sol::create); + auto indexTab = std::get(vars[i]); + + for (auto& [first, second] : indexTab) + { + if (first >= vars.size() || second >= vars.size()) + { + TENLog("Corrupted save data: variable index out of range. Skipping entry.", LogLevel::Warning); + continue; + } + + if (std::holds_alternative(vars[second])) + { + solTables.try_emplace(second, *_handler.GetState(), sol::create); + solTables[i][vars[first]] = solTables[second]; + } + else if (std::holds_alternative(vars[second])) + { + double theNum = std::get(vars[second]); + if (std::trunc(theNum) == theNum && theNum <= INT64_MAX && theNum >= INT64_MIN) + { + solTables[i][vars[first]] = (int64_t)theNum; + } + else + { + solTables[i][vars[first]] = vars[second]; + } + } + else if (vars[second].index() == (int)SavedVarType::Vec2) + { + auto vec2 = Vec2(std::get<(int)SavedVarType::Vec2>(vars[second])); + solTables[i][vars[first]] = vec2; + } + else if (vars[second].index() == int(SavedVarType::Vec3)) + { + auto vec3 = Vec3(std::get(vars[second])); + solTables[i][vars[first]] = vec3; + } + else if (vars[second].index() == int(SavedVarType::Rotation)) + { + auto vec3 = Rotation(std::get(vars[second])); + solTables[i][vars[first]] = vec3; + } + else if (vars[second].index() == int(SavedVarType::Time)) + { + auto time = Time(std::get(vars[second])); + solTables[i][vars[first]] = time; + } + else if (vars[second].index() == int(SavedVarType::Color)) + { + auto color = D3DCOLOR(std::get(vars[second])); + solTables[i][vars[first]] = ScriptColor{ color }; + } + else if (std::holds_alternative(vars[second])) + { + LevelFunc levelFunc; + levelFunc.m_funcName = std::get(vars[second]).name; + levelFunc.m_handler = this; + solTables[i][vars[first]] = levelFunc; + } + else + { + solTables[i][vars[first]] = vars[second]; + } + } + } + } + + return solTables; +} + +// Used when saving. +void LogicHandler::GetVariables(std::vector& vars) +{ + auto tab = sol::table(*_handler.GetState(), sol::create); + tab[ScriptReserved_LevelVars] = (*_handler.GetState())[ScriptReserved_LevelVars]; + tab[ScriptReserved_GameVars] = (*_handler.GetState())[ScriptReserved_GameVars]; + + SerializeScriptTable(tab, vars); +} + +// Used when loading. +void LogicHandler::SetVariables(const std::vector& vars, bool onlyLevelVars) +{ + if (!onlyLevelVars) + ResetGameTables(); + + ResetLevelTables(); + + auto solTables = DeserializeScriptVars(vars); + + if (solTables.empty()) + return; + + auto rootTable = solTables[0]; + + sol::table levelVars = rootTable[ScriptReserved_LevelVars]; + for (auto& [first, second] : levelVars) + (*_handler.GetState())[ScriptReserved_LevelVars][first] = second; + + if (onlyLevelVars) + return; + + sol::table gameVars = rootTable[ScriptReserved_GameVars]; + for (auto& [first, second] : gameVars) + (*_handler.GetState())[ScriptReserved_GameVars][first] = second; +} + +// Used when saving global vars to external file. +void LogicHandler::GetGlobalVariables(std::vector& vars) +{ + auto tab = sol::table(*_handler.GetState(), sol::create); + tab[ScriptReserved_GlobalVars] = (*_handler.GetState())[ScriptReserved_GlobalVars]; + + SerializeScriptTable(tab, vars); +} + +// Used when loading global vars from external file. +void LogicHandler::SetGlobalVariables(const std::vector& vars) +{ + ResetGlobalTables(); + + auto solTables = DeserializeScriptVars(vars); + + if (solTables.empty()) + return; + + auto rootTable = solTables[0]; + + sol::object globalVarsObj = rootTable[ScriptReserved_GlobalVars]; + if (globalVarsObj.valid() && globalVarsObj.is()) + { + sol::table globalVars = globalVarsObj; + for (auto& [first, second] : globalVars) + (*_handler.GetState())[ScriptReserved_GlobalVars][first] = second; + } +} + void LogicHandler::GetCallbackStrings( std::vector& preStart, std::vector& postStart, diff --git a/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.h b/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.h index d15c230280..542806886a 100644 --- a/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.h +++ b/TombEngine/Scripting/Internal/TEN/Logic/LogicHandler.h @@ -92,6 +92,10 @@ class LogicHandler : public ScriptInterfaceGame void ResetLevelTables(); void ResetGameTables(); + void ResetGlobalTables(); + + void SerializeScriptTable(const sol::table& tab, std::vector& vars); + std::unordered_map DeserializeScriptVars(const std::vector& vars); public: LogicHandler(sol::state* lua, sol::table& parent); @@ -168,6 +172,8 @@ class LogicHandler : public ScriptInterfaceGame void GetVariables(std::vector& vars) override; void SetVariables(const std::vector& vars, bool onlyLevelVars) override; + void GetGlobalVariables(std::vector& vars) override; + void SetGlobalVariables(const std::vector& vars) override; void ResetVariables(); void SetCallbackStrings(const std::vector& preStart, diff --git a/TombEngine/Scripting/Internal/TEN/Logic/Vars/1_GlobalVars.h b/TombEngine/Scripting/Internal/TEN/Logic/Vars/1_GlobalVars.h new file mode 100644 index 0000000000..d60538edbb --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/Logic/Vars/1_GlobalVars.h @@ -0,0 +1,52 @@ +#pragma once + +// This file is used for documentation of GlobalVars. + +/// A table with global data which persists across game sessions and is saved to an external file. +// @specialtable GlobalVars +// This is for information that should persist across all game sessions and survive engine restarts, +// such as achievements, title screen modifications based on game progress, or unlockable game modes. +// +// For example, you may wish to unlock a new game mode after the player completes the game: +// GlobalVars.gameCompleted = true +// And in the title screen script, you could check: +// if GlobalVars.gameCompleted then +// -- Show "New Game+" option in the menu +// end +// +// GlobalVars are saved to an external file when the engine exits or switches a level, and restored +// when the engine launches. +// +// In GlobalVars, you can only save certain types of variables: +// +// - number (integers and floats) +// - boolean (true and false) +// - @{string} (text) +// - @{table} (tables containing any of these types, including nested tables) +// - @{Vec2} (2D vectors) +// - @{Vec3} (3D vectors) +// - @{Rotation} (rotation values) +// - @{Time} (time values) +// - @{Color} (color values) +// +// If you try to save a variable of an unsupported type (like a function or userdata), an error will be raised. +// +//

Note:

+// +// - GlobalVars is created automatically. Never assign a value to GlobalVars.
+// For example, do not write: +// GlobalVars = {} -- This will break everything! +// or +// GlobalVars = GlobalVars -- not needed, GlobalVars already exists +// Instead, just assign values to members of GlobalVars, like so: +// GlobalVars.someValue = 42 +// GlobalVars.anotherValue = "Hello, world!" +// GlobalVars.aTable = { key1 = "value1", key2 = "value2" } +// GlobalVars.someValue = 11 -- change value of existing variable +// +// - GlobalVars is saved to an external file when the engine exits, and loaded on the next engine launch. +// +// - Unlike @{LevelVars} and @{GameVars}, this table persists across all game sessions, including engine restarts. +// +// - __*GlobalVars.Engine*__ is a reserved table used internally by TombEngine's libs. __Do not modify, overwrite, or add to it.__ +// diff --git a/TombEngine/Scripting/Internal/TEN/Logic/GameVars.h b/TombEngine/Scripting/Internal/TEN/Logic/Vars/2_GameVars.h similarity index 100% rename from TombEngine/Scripting/Internal/TEN/Logic/GameVars.h rename to TombEngine/Scripting/Internal/TEN/Logic/Vars/2_GameVars.h diff --git a/TombEngine/Scripting/Internal/TEN/Logic/LevelVars.h b/TombEngine/Scripting/Internal/TEN/Logic/Vars/3_LevelVars.h similarity index 100% rename from TombEngine/Scripting/Internal/TEN/Logic/LevelVars.h rename to TombEngine/Scripting/Internal/TEN/Logic/Vars/3_LevelVars.h diff --git a/TombEngine/Scripting/Internal/TEN/Objects/Moveable/MoveableObject.cpp b/TombEngine/Scripting/Internal/TEN/Objects/Moveable/MoveableObject.cpp index ca185b479f..7cb6be5a4e 100644 --- a/TombEngine/Scripting/Internal/TEN/Objects/Moveable/MoveableObject.cpp +++ b/TombEngine/Scripting/Internal/TEN/Objects/Moveable/MoveableObject.cpp @@ -119,7 +119,7 @@ static std::unique_ptr Create(GAME_OBJECT_ID objID, const std::string& scriptMov->SetOcb(ValueOr(ocb, 0)); scriptMov->SetAIBits(ValueOr(aiBits, aiBitsType{})); - scriptMov->SetColor(ScriptColor(Vector4::One)); + scriptMov->SetColor(ScriptColor(NEUTRAL_COLOR)); mov.CarriedItem = NO_VALUE; // call this when resetting name too? diff --git a/TombEngine/Scripting/Internal/TEN/Types/Color/Color.cpp b/TombEngine/Scripting/Internal/TEN/Types/Color/Color.cpp index c4b91f034a..558229c03a 100644 --- a/TombEngine/Scripting/Internal/TEN/Types/Color/Color.cpp +++ b/TombEngine/Scripting/Internal/TEN/Types/Color/Color.cpp @@ -11,7 +11,9 @@ namespace TEN::Scripting::Types { void ScriptColor::Register(sol::table& parent) { - using ctors = sol::constructors; + using ctors = sol::constructors; // Register type. parent.new_usertype( @@ -52,12 +54,12 @@ namespace TEN::Scripting::Types _color(128, 128, 128, 255) { } - ScriptColor::ScriptColor(byte r, byte g, byte b) : + ScriptColor::ScriptColor(unsigned char r, unsigned char g, unsigned char b) : _color(r, g, b) { } - ScriptColor::ScriptColor(byte r, byte g, byte b, byte a) : + ScriptColor::ScriptColor(unsigned char r, unsigned char g, unsigned char b, unsigned char a) : ScriptColor(r, g, b) { SetA(a); @@ -78,42 +80,42 @@ namespace TEN::Scripting::Types { } - byte ScriptColor::GetR() const + unsigned char ScriptColor::GetR() const { return _color.GetR(); } - byte ScriptColor::GetG() const + unsigned char ScriptColor::GetG() const { return _color.GetG(); } - byte ScriptColor::GetB() const + unsigned char ScriptColor::GetB() const { return _color.GetB(); } - byte ScriptColor::GetA() const + unsigned char ScriptColor::GetA() const { return _color.GetA(); } - void ScriptColor::SetR(byte value) + void ScriptColor::SetR(unsigned char value) { _color.SetR(value); } - void ScriptColor::SetG(byte value) + void ScriptColor::SetG(unsigned char value) { _color.SetG(value); } - void ScriptColor::SetB(byte value) + void ScriptColor::SetB(unsigned char value) { _color.SetB(value); } - void ScriptColor::SetA(byte value) + void ScriptColor::SetA(unsigned char value) { _color.SetA(value); } diff --git a/TombEngine/Scripting/Internal/TEN/Types/Color/Color.h b/TombEngine/Scripting/Internal/TEN/Types/Color/Color.h index 3d80ac9731..62a615b8e4 100644 --- a/TombEngine/Scripting/Internal/TEN/Types/Color/Color.h +++ b/TombEngine/Scripting/Internal/TEN/Types/Color/Color.h @@ -21,25 +21,25 @@ namespace TEN::Scripting::Types // Constructors ScriptColor(); - ScriptColor(byte r, byte g, byte b); - ScriptColor(byte r, byte g, byte b, byte a); + ScriptColor(unsigned char r, unsigned char g, unsigned char b); + ScriptColor(unsigned char r, unsigned char g, unsigned char b, unsigned char a); ScriptColor(const Vector3& color); ScriptColor(const Vector4& color); ScriptColor(D3DCOLOR); // Getters - byte GetR() const; - byte GetG() const; - byte GetB() const; - byte GetA() const; + unsigned char GetR() const; + unsigned char GetG() const; + unsigned char GetB() const; + unsigned char GetA() const; // Setters - void SetR(byte value); - void SetG(byte value); - void SetB(byte value); - void SetA(byte value); + void SetR(unsigned char value); + void SetG(unsigned char value); + void SetB(unsigned char value); + void SetA(unsigned char value); // Methods diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp new file mode 100644 index 0000000000..d5bebbdda3 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.cpp @@ -0,0 +1,240 @@ +#include "framework.h" +#include "Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h" + +#include "Game/effects/DisplaySprite.h" +#include "Renderer/Renderer.h" +#include "Renderer/Structures/RendererRectangle.h" +#include "Scripting/Internal/LuaHandler.h" +#include "Scripting/Internal/ReservedScriptNames.h" +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" + +using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Renderer::Structures; +using namespace TEN::Scripting::Types; +using TEN::Renderer::g_Renderer; + +/// Represents a clipping area for display items. +// +// @tenclass View.DisplayArea +// @pragma nostrip + +namespace TEN::Scripting::DisplayArea +{ + void ScriptDisplayArea::Register(sol::state& state, sol::table& parent) + { + using ctors = sol::constructors< + ScriptDisplayArea(const Vec2&, const Vec2&), + ScriptDisplayArea(const Vec2&, const Vec2&, sol::table)>; + + // Register type. + parent.new_usertype( + ScriptReserved_DisplayArea, + ctors(), + sol::call_constructor, ctors(), + + ScriptReserved_DisplayStringGetPosition, &ScriptDisplayArea::GetPosition, + ScriptReserved_DisplayStringSetPosition, &ScriptDisplayArea::SetPosition, + ScriptReserved_GetSize, &ScriptDisplayArea::GetSize, + ScriptReserved_SetSize, &ScriptDisplayArea::SetSize, + ScriptReserved_AddItem, &ScriptDisplayArea::AddItem, + ScriptReserved_RemoveItem, &ScriptDisplayArea::RemoveItem, + ScriptReserved_Clear, &ScriptDisplayArea::Clear, + ScriptReserved_Debug, &ScriptDisplayArea::Debug, + ScriptReserved_DisplaySpriteDraw, &ScriptDisplayArea::Draw); + } + + /// Create a DisplayArea object. + // @function DisplayArea + // @tparam Vec2 pos Top-left position of the area in percent. + // @tparam Vec2 size Width and height of the area in percent. + // @tparam[opt] table items Sequence of items. Each entry is either a bare DisplayString or DisplaySprite, + // or a table { item, { priority, alignMode, scaleMode, blendMode } }. + // @treturn DisplayArea A new DisplayArea object. + ScriptDisplayArea::ScriptDisplayArea(const Vec2& pos, const Vec2& size) + { + _position = pos; + _size = size; + } + + ScriptDisplayArea::ScriptDisplayArea(const Vec2& pos, const Vec2& size, sol::table items) + { + _position = pos; + _size = size; + + for (auto& [key, val] : items) + { + // Entry is { item, { args } }. + if (val.is()) + { + auto entry = val.as(); + auto entryItem = entry.get>(1); + auto entryArgs = entry.get>(2); + + if (entryItem.has_value()) + AddItem(entryItem.value(), entryArgs); + } + // Entry is a bare item. + else + { + AddItem(val.as(), sol::nullopt); + } + } + } + + /// Get the top-left position of the area in percent. + // @function DisplayArea:GetPosition + // @treturn Vec2 Top-left position in percent. + Vec2 ScriptDisplayArea::GetPosition() const + { + return _position; + } + + /// Get the size of the area in percent. + // @function DisplayArea:GetSize + // @treturn Vec2 Width and height in percent. + Vec2 ScriptDisplayArea::GetSize() const + { + return _size; + } + + /// Set the top-left position of the area in percent. + // @function DisplayArea:SetPosition + // @tparam Vec2 position New top-left position in percent. + void ScriptDisplayArea::SetPosition(const Vec2& pos) + { + _position = pos; + } + + /// Set the size of the area in percent. + // @function DisplayArea:SetSize + // @tparam Vec2 size New width and height in percent. + void ScriptDisplayArea::SetSize(const Vec2& size) + { + _size = size; + } + + /// Add a display item to the area. + // Items can be View.DisplayString or View.DisplaySprite objects. + // Arguments are forwarded to the item's Draw() method. + // @function DisplayArea:AddItem + // @tparam object item A DisplayString or DisplaySprite to clip within this area. + // @tparam[opt] table args Draw arguments forwarded to the item, e.g. { priority, alignMode, scaleMode, blendMode }. + void ScriptDisplayArea::AddItem(sol::object item, sol::optional args) + { + auto argsObj = args.has_value() ? sol::object(args.value()) : sol::object(sol::lua_nil); + _items.push_back({ item, argsObj }); + } + + /// Remove a display item from the area. + // @function DisplayArea:RemoveItem + // @tparam object item The item to remove. + void ScriptDisplayArea::RemoveItem(sol::object item) + { + for (auto it = _items.begin(); it != _items.end(); ++it) + { + if (it->first == item) + { + _items.erase(it); + return; + } + } + } + + /// Remove all display items from the area. + // @function DisplayArea:Clear + void ScriptDisplayArea::Clear() + { + _items.clear(); + } + + /// Draw all items in the area, clipped to the area bounds. + // @function DisplayArea:Draw + void ScriptDisplayArea::Draw() + { + if (_items.empty()) + return; + + // Convert percent to pixel coordinates. + auto screenRes = g_Renderer.GetScreenResolution(); + float screenWidth = (float)screenRes.x; + float screenHeight = (float)screenRes.y; + + int left = (int)(_position.x * screenWidth / 100.0f); + int top = (int)(_position.y * screenHeight / 100.0f); + int right = (int)((_position.x + _size.x) * screenWidth / 100.0f); + int bottom = (int)((_position.y + _size.y) * screenHeight / 100.0f); + + auto scissorRect = RendererRectangle(left, top, right, bottom); + + // Activate scissor for all items queued during this scope. + SetActiveDisplayScissor(scissorRect); + + for (auto& [item, args] : _items) + { + // Retrieve the Draw method via __index (works for both tables and userdata). + auto* L = item.lua_state(); + item.push(L); // stack: [obj] + lua_getfield(L, -1, ScriptReserved_DisplaySpriteDraw); // stack: [obj, func] + + if (lua_isnil(L, -1)) + { + lua_pop(L, 2); + continue; + } + + // Anchor the function in the registry before clearing the stack. + auto drawFunc = sol::protected_function(L, -1); + lua_pop(L, 2); // stack: [] + + sol::protected_function_result result; + + if (args.is()) + { + auto argsTable = args.as(); + auto argCount = (int)argsTable.size(); + + std::vector argVec; + argVec.reserve(argCount); + for (int i = 1; i <= argCount; i++) + argVec.push_back(argsTable.raw_get(i)); + + result = drawFunc(item, sol::as_args(argVec)); + } + else + { + result = drawFunc(item); + } + + if (!result.valid()) + { + sol::error err = result; + TENLog(std::string("DisplayArea: Error drawing item: ") + err.what(), LogLevel::Warning); + } + } + + ClearActiveDisplayScissor(); + } + + /// Draw a debug overlay showing the area bounds for the current frame. + // @function DisplayArea:Debug + // @tparam[opt=Color(255, 0, 0, 128)] Color color Fill color of the debug overlay. + void ScriptDisplayArea::Debug(sol::optional colorOpt) + { + auto color = colorOpt.value_or(ScriptColor(255, 0, 0, 128)); + + auto screenRes = g_Renderer.GetScreenResolution(); + float screenWidth = (float)screenRes.x; + float screenHeight = (float)screenRes.y; + + int left = (int)(_position.x * screenWidth / 100.0f); + int top = (int)(_position.y * screenHeight / 100.0f); + int right = (int)((_position.x + _size.x) * screenWidth / 100.0f); + int bottom = (int)((_position.y + _size.y) * screenHeight / 100.0f); + + auto rect = RendererRectangle(left, top, right, bottom); + auto rgba = Vector4(color.GetR(), color.GetG(), color.GetB(), color.GetA()) / (float)UCHAR_MAX; + + g_Renderer.AddDebugDisplayRect(rect, rgba); + } +} diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h new file mode 100644 index 0000000000..8cf150f149 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" + +using namespace TEN::Scripting::Types; + +namespace TEN::Scripting::DisplayArea +{ + class ScriptDisplayArea + { + public: + static void Register(sol::state& state, sol::table& parent); + + private: + // Members + Vec2 _position = Vec2(0.0f, 0.0f); + Vec2 _size = Vec2(100.0f, 100.0f); + + // Each entry: { drawable object, optional args table }. + std::vector> _items = {}; + + public: + // Constructors + ScriptDisplayArea(const Vec2& pos, const Vec2& size); + ScriptDisplayArea(const Vec2& pos, const Vec2& size, sol::table items); + + // Getters + Vec2 GetPosition() const; + Vec2 GetSize() const; + + // Setters + void SetPosition(const Vec2& pos); + void SetSize(const Vec2& size); + + // Item management + void AddItem(sol::object item, sol::optional args); + void RemoveItem(sol::object item); + void Clear(); + + // Utilities + void Draw(); + void Debug(sol::optional color); + }; +} diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp new file mode 100644 index 0000000000..ec0ee9fb7f --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.cpp @@ -0,0 +1,411 @@ +#include "framework.h" +#include "Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h" + +#include "Game/effects/DisplaySprite.h" +#include "Game/Setup.h" +#include "Renderer/Renderer.h" +#include "Renderer/RendererEnums.h" +#include "Scripting/Include/Flow/ScriptInterfaceFlowHandler.h" +#include "Scripting/Internal/LuaHandler.h" +#include "Scripting/Internal/ReservedScriptNames.h" +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" +#include "Scripting/Internal/TEN/View/DisplayAnchors/ScriptDisplayAnchors.h" + +using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Scripting::Types; +using TEN::Renderer::g_Renderer; + +/// Represents a display string. +// +// @tenclass View.DisplayString +// @pragma nostrip + +namespace TEN::Scripting::DisplayString +{ + // NOTE: Conversion from 100x100 percent screen space to internal 800x600. + constexpr auto POS_CONVERSION_COEFF = Vector2(DISPLAY_SPACE_RES.x / 100, DISPLAY_SPACE_RES.y / 100); + constexpr auto SCALE_CONVERSION_COEFF = 0.01f; + + static int FlagArrayToFlags(const FlagArray& flags) + { + int result = 0; + + if (flags[(int)DisplayStringOptions::Center]) + result |= (int)PrintStringFlags::Center; + + if (flags[(int)DisplayStringOptions::Right]) + result |= (int)PrintStringFlags::Right; + + if (flags[(int)DisplayStringOptions::Outline]) + result |= (int)PrintStringFlags::Outline; + + if (flags[(int)DisplayStringOptions::Blink]) + result |= (int)PrintStringFlags::Blink; + + if (flags[(int)DisplayStringOptions::VerticalCenter]) + result |= (int)PrintStringFlags::VerticalCenter; + + if (flags[(int)DisplayStringOptions::VerticalBottom]) + result |= (int)PrintStringFlags::VerticalBottom; + + return result; + } + + void ScriptDisplayString::Register(sol::state& state, sol::table& parent) + { + using ctors = sol::constructors< + ScriptDisplayString(const std::string&, const Vec2&, float, const Vec2&, const ScriptColor&, bool), + ScriptDisplayString(const std::string&, const Vec2&, float, const Vec2&, const ScriptColor&), + ScriptDisplayString(const std::string&, const Vec2&, float, const Vec2&), + ScriptDisplayString(const std::string&, const Vec2&)>; + + // Register type. + parent.new_usertype( + ScriptReserved_DisplayString, + ctors(), + sol::call_constructor, ctors(), + + ScriptReserved_GetText, &ScriptDisplayString::GetText, + ScriptReserved_SetText, &ScriptDisplayString::SetText, + ScriptReserved_GetTranslated, &ScriptDisplayString::GetTranslated, + ScriptReserved_SetTranslated, &ScriptDisplayString::SetTranslated, + ScriptReserved_DisplayStringGetPosition, &ScriptDisplayString::GetPosition, + ScriptReserved_DisplayStringSetPosition, &ScriptDisplayString::SetPosition, + ScriptReserved_DisplayStringGetRotation, &ScriptDisplayString::GetRotation, + ScriptReserved_DisplayStringSetRotation, &ScriptDisplayString::SetRotation, + ScriptReserved_DisplayStringGetScale, &ScriptDisplayString::GetScale, + ScriptReserved_DisplayStringSetScale, &ScriptDisplayString::SetScale, + ScriptReserved_DisplayStringGetColor, &ScriptDisplayString::GetColor, + ScriptReserved_DisplayStringSetColor, &ScriptDisplayString::SetColor, + ScriptReserved_GetArea, &ScriptDisplayString::GetArea, + ScriptReserved_SetArea, &ScriptDisplayString::SetArea, + ScriptReserved_GetFlags, &ScriptDisplayString::GetFlags, + ScriptReserved_SetFlags, &ScriptDisplayString::SetFlags, + ScriptReserved_DisplayStringGetAnchors, &ScriptDisplayString::GetAnchors, + ScriptReserved_DisplaySpriteDraw, &ScriptDisplayString::Draw); + } + + /// Create a DisplayString object. + // @function DisplayString + // @tparam string text Text to display, or a translation key if isTranslated is true. + // @tparam Vec2 pos Display position in percent. + // @tparam[opt=0] float rot Rotation in degrees. + // @tparam[opt=Vec2(100, 100)] Vec2 scale Horizontal and vertical scale in percent. + // @tparam[opt=Color(255, 255, 255)] Color color Color. + // @tparam[opt=false] bool isTranslated Whether the text is a translation key. + // @treturn DisplayString A new DisplayString object. + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color, bool isTranslated) + { + _text = text; + _position = pos; + _rotation = rot; + _scale = scale; + _color = color; + _isTranslated = isTranslated; + } + + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color) + { + *this = ScriptDisplayString(text, pos, rot, scale, color, false); + } + + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale) + { + *this = ScriptDisplayString(text, pos, rot, scale, ScriptColor(255, 255, 255, 255), false); + } + + ScriptDisplayString::ScriptDisplayString(const std::string& text, const Vec2& pos) + { + *this = ScriptDisplayString(text, pos, 0.0f, Vec2(100.0f, 100.0f), ScriptColor(255, 255, 255, 255), false); + } + + /// Get the text of the display string. + // @function DisplayString:GetText + // @treturn string Text or translation key. + std::string ScriptDisplayString::GetText() const + { + return _text; + } + + /// Get whether the text is a translation key. + // @function DisplayString:GetTranslated + // @treturn bool True if the text is a translation key. + bool ScriptDisplayString::GetTranslated() const + { + return _isTranslated; + } + + /// Get the display position in percent. + // @function DisplayString:GetPosition + // @treturn Vec2 Display position in percent. + Vec2 ScriptDisplayString::GetPosition() const + { + return _position; + } + + /// Get the rotation in degrees. + // @function DisplayString:GetRotation + // @treturn float Rotation in degrees. + float ScriptDisplayString::GetRotation() const + { + return _rotation; + } + + /// Get the scale in percent. + // @function DisplayString:GetScale + // @treturn Vec2 Horizontal and vertical scale in percent. + Vec2 ScriptDisplayString::GetScale() const + { + return _scale; + } + + /// Get the color. + // @function DisplayString:GetColor + // @treturn Color Color. + ScriptColor ScriptDisplayString::GetColor() const + { + return _color; + } + + /// Get the text wrapping area in percent. + // Vec2(0, 0) means no wrapping. + // @function DisplayString:GetArea + // @treturn Vec2 Wrapping area in percent. + Vec2 ScriptDisplayString::GetArea() const + { + return _area; + } + + /// Get the display string option flags. + // @function DisplayString:GetFlags + // @treturn table Array of DisplayStringOption values. + sol::table ScriptDisplayString::GetFlags(sol::this_state state) const + { + auto table = sol::state_view(state).create_table(); + for (int i = 0; i < (int)_flags.size(); i++) + { + if (_flags[i]) + table.add(i); + } + return table; + } + + /// Set the text of the display string. + // @function DisplayString:SetText + // @tparam string text New text or translation key. + void ScriptDisplayString::SetText(const std::string& text) + { + _text = text; + } + + /// Set whether the text is a translation key. + // @function DisplayString:SetTranslated + // @tparam bool isTranslated True if the text is a translation key. + void ScriptDisplayString::SetTranslated(bool isTranslated) + { + _isTranslated = isTranslated; + } + + /// Set the display position in percent. + // @function DisplayString:SetPosition + // @tparam Vec2 position New display position in percent. + void ScriptDisplayString::SetPosition(const Vec2& pos) + { + _position = pos; + } + + /// Set the rotation in degrees. + // @function DisplayString:SetRotation + // @tparam float rotation New rotation in degrees. + void ScriptDisplayString::SetRotation(float rot) + { + _rotation = rot; + } + + /// Set the scale in percent. + // @function DisplayString:SetScale + // @tparam Vec2 scale New horizontal and vertical scale in percent. + void ScriptDisplayString::SetScale(const Vec2& scale) + { + _scale = scale; + } + + /// Set the color. + // @function DisplayString:SetColor + // @tparam Color color New color. + void ScriptDisplayString::SetColor(const ScriptColor& color) + { + _color = color; + } + + /// Set the text wrapping area in percent. + // Vec2(0, 0) means no wrapping. + // @function DisplayString:SetArea + // @tparam Vec2 area New wrapping area in percent. + void ScriptDisplayString::SetArea(const Vec2& area) + { + _area = area; + } + + /// Set the display string option flags. + // @function DisplayString:SetFlags + // @tparam table flags Array of DisplayStringOption values. + void ScriptDisplayString::SetFlags(const sol::table& flags) + { + _flags = {}; + for (const auto& val : flags) + { + auto i = val.second.as(); + if (i < _flags.size()) + _flags[i] = true; + } + } + + /// Get the anchors of the display string. + // Returns the nine anchor points of the display string bounding box in percent. + // @function DisplayString:GetAnchors + // @tparam[opt=View.AlignMode.Center] View.AlignMode alignMode Alignment mode. + // @tparam[opt=View.ScaleMode.Fit] View.ScaleMode scaleMode Scaling mode. + // @treturn View.DisplayAnchors An object containing the anchor points of the display string bounding box. + ScriptDisplayAnchors ScriptDisplayString::GetAnchors(sol::optional alignModeOpt, sol::optional scaleModeOpt) const + { + constexpr auto DISPLAY_ASPECT = DISPLAY_SPACE_RES.x / DISPLAY_SPACE_RES.y; + constexpr auto DEFAULT_ALIGN_MODE = DisplaySpriteAlignMode::Center; + constexpr auto DEFAULT_SCALE_MODE = DisplaySpriteScaleMode::Fit; + + auto resolvedText = _isTranslated ? std::string(g_GameFlow->GetString(_text.c_str())) : _text; + + // Measure text in display space units (800x600). + auto convertedScale = Vector2(_scale.x, _scale.y) * SCALE_CONVERSION_COEFF; + auto textSize = g_Renderer.GetDisplayStringSize(resolvedText, convertedScale); + + // Text bounding box aspect ratio. + float textAspect = (textSize.y > 0.0f) ? (textSize.x / textSize.y) : 1.0f; + + // Screen aspect data. + auto screenRes = Vector2((float)g_Configuration.ScreenWidth, (float)g_Configuration.ScreenHeight); + float screenAspect = screenRes.x / screenRes.y; + float aspectCorrectionBase = screenAspect / DISPLAY_ASPECT; + + // Convert position and rotation. + auto convertedPos = Vector2(_position.x, _position.y) * POS_CONVERSION_COEFF; + short convertedRot = ANGLE(_rotation); + + // Get modes. + auto alignMode = alignModeOpt.value_or(DEFAULT_ALIGN_MODE); + auto scaleMode = scaleModeOpt.value_or(DEFAULT_SCALE_MODE); + + // Calculate layout using shared helper. + auto layout = CalculateDisplaySpriteLayout( + textAspect, convertedScale, convertedRot, + alignMode, scaleMode, screenAspect, aspectCorrectionBase); + + // Calculate final position and size. + auto position = convertedPos + layout.Offset; + auto size = layout.HalfSize * 2.0f; + + // Build vertices centered around origin (top-left, top-right, bottom-right, bottom-left). + std::array vertices = { + size / 2.0f, + Vector2(-size.x, size.y) / 2.0f, + -size / 2.0f, + Vector2(size.x, -size.y) / 2.0f + }; + + // Apply rotation + aspect correction + position offset. + auto rotMatrix = Matrix::CreateRotationZ(TO_RAD(convertedRot + ANGLE(180.0f))); + for (auto& vertex : vertices) + { + vertex = Vector2::Transform(vertex, rotMatrix); + vertex *= layout.AspectCorrection; + vertex += position; + } + + // Scale to screen resolution. + auto screenScale = screenRes / DISPLAY_SPACE_RES; + for (auto& vertex : vertices) + { + vertex.x *= screenScale.x; + vertex.y *= screenScale.y; + } + + auto toPercent = [&screenRes](const Vector2& pos) -> Vec2 + { + return Vec2(std::round((pos.x / screenRes.x) * 10000.0f) / 100.0f, std::round((pos.y / screenRes.y) * 10000.0f) / 100.0f); + }; + + // Calculate edge midpoints. + auto center = (vertices[0] + vertices[2]) / 2.0f; + auto centerTop = (vertices[0] + vertices[1]) / 2.0f; + auto centerLeft = (vertices[0] + vertices[3]) / 2.0f; + auto centerRight = (vertices[1] + vertices[2]) / 2.0f; + auto centerBottom = (vertices[2] + vertices[3]) / 2.0f; + + // Populate and return anchors. + ScriptDisplayAnchors anchors; + anchors.TOP_LEFT = toPercent(vertices[0]); + anchors.TOP_CENTER = toPercent(centerTop); + anchors.TOP_RIGHT = toPercent(vertices[1]); + anchors.CENTER_LEFT = toPercent(centerLeft); + anchors.CENTER = toPercent(center); + anchors.CENTER_RIGHT = toPercent(centerRight); + anchors.BOTTOM_RIGHT = toPercent(vertices[2]); + anchors.BOTTOM_CENTER = toPercent(centerBottom); + anchors.BOTTOM_LEFT = toPercent(vertices[3]); + + return anchors; + } + + /// Draw the display string in display space for the current frame. + // @function DisplayString:Draw + // @tparam[opt=0] int priority Draw priority. Can be thought of as a layer, with higher values having precedence. + // @tparam[opt=View.AlignMode.CENTER] View.AlignMode alignMode Horizontal alignment mode. Overrides CENTER and RIGHT flags. + // @tparam[opt=Effects.BlendID.ALPHABLEND] Effects.BlendID blendMode Blend mode. + void ScriptDisplayString::Draw(sol::optional priority, sol::optional alignMode, + sol::optional blendMode) + { + // Resolve text. + auto resolvedText = _isTranslated ? std::string(g_GameFlow->GetString(_text.c_str())) : _text; + + if (resolvedText.empty()) + return; + + // Convert percent to display space. + auto convertedPos = Vector2(_position.x, _position.y) * POS_CONVERSION_COEFF; + auto convertedArea = Vector2(_area.x, _area.y) * POS_CONVERSION_COEFF; + auto convertedScale = Vector2(_scale.x, _scale.y) * SCALE_CONVERSION_COEFF; + auto convertedColor = Vector4(_color.GetR(), _color.GetG(), _color.GetB(), _color.GetA()) / (float)UCHAR_MAX; + float convertedRotation = _rotation * (DirectX::XM_PI / 180.0f); + + // Build flags from FlagArray. + int flags = FlagArrayToFlags(_flags); + + // Override alignment with alignMode if provided. + if (alignMode.has_value()) + { + flags &= ~((int)PrintStringFlags::Center | (int)PrintStringFlags::Right); + + switch (alignMode.value()) + { + case (int)DisplaySpriteAlignMode::Center: + case (int)DisplaySpriteAlignMode::CenterTop: + case (int)DisplaySpriteAlignMode::CenterBottom: + flags |= (int)PrintStringFlags::Center; + break; + + case (int)DisplaySpriteAlignMode::CenterRight: + case (int)DisplaySpriteAlignMode::TopRight: + case (int)DisplaySpriteAlignMode::BottomRight: + flags |= (int)PrintStringFlags::Right; + break; + } + } + + g_Renderer.AddString( + resolvedText, convertedPos, convertedPos, convertedArea, + Color(convertedColor), convertedScale, convertedRotation, flags, + priority.value_or(0), + (BlendMode)blendMode.value_or((int)BlendMode::AlphaBlend)); + } +} diff --git a/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h new file mode 100644 index 0000000000..b8b8e4bdd1 --- /dev/null +++ b/TombEngine/Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h @@ -0,0 +1,62 @@ +#pragma once + +#include "Game/effects/DisplaySprite.h" +#include "Scripting/Internal/TEN/Strings/DisplayString/DisplayString.h" +#include "Scripting/Internal/TEN/Types/Color/Color.h" +#include "Scripting/Internal/TEN/Types/Vec2/Vec2.h" +#include "Scripting/Internal/TEN/View/DisplayAnchors/ScriptDisplayAnchors.h" + +using namespace TEN::Effects::DisplaySprite; +using namespace TEN::Scripting::Types; + +namespace TEN::Scripting::DisplayString +{ + class ScriptDisplayString + { + public: + static void Register(sol::state& state, sol::table& parent); + + private: + // Members + std::string _text = {}; + bool _isTranslated = false; + + Vec2 _position = Vec2(0.0f, 0.0f); + float _rotation = 0.0f; + Vec2 _scale = Vec2(100.0f, 100.0f); + ScriptColor _color = ScriptColor(255, 255, 255, 255); + Vec2 _area = Vec2(0.0f, 0.0f); + FlagArray _flags = {}; + + public: + // Constructors + ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color, bool isTranslated); + ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale, const ScriptColor& color); + ScriptDisplayString(const std::string& text, const Vec2& pos, float rot, const Vec2& scale); + ScriptDisplayString(const std::string& text, const Vec2& pos); + + // Getters + std::string GetText() const; + bool GetTranslated() const; + Vec2 GetPosition() const; + float GetRotation() const; + Vec2 GetScale() const; + ScriptColor GetColor() const; + Vec2 GetArea() const; + sol::table GetFlags(sol::this_state state) const; + + // Setters + void SetText(const std::string& text); + void SetTranslated(bool isTranslated); + void SetPosition(const Vec2& pos); + void SetRotation(float rot); + void SetScale(const Vec2& scale); + void SetColor(const ScriptColor& color); + void SetArea(const Vec2& area); + void SetFlags(const sol::table& flags); + + // Utilities + ScriptDisplayAnchors GetAnchors(sol::optional alignModeOpt, sol::optional scaleModeOpt) const; + void Draw(sol::optional priority, sol::optional alignMode, sol::optional blendMode); + }; +} diff --git a/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp b/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp index 4a1b8c4af8..25c08315d4 100644 --- a/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp +++ b/TombEngine/Scripting/Internal/TEN/View/ViewHandler.cpp @@ -17,8 +17,10 @@ #include "Scripting/Internal/TEN/Types/Vec3/Vec3.h" #include "Scripting/Internal/TEN/View/AlignModes.h" #include "Scripting/Internal/TEN/View/CameraTypes.h" +#include "Scripting/Internal/TEN/View/DisplayArea/ScriptDisplayArea.h" #include "Scripting/Internal/TEN/View/DisplayItem/ScriptDisplayItem.h" #include "Scripting/Internal/TEN/View/DisplaySprite/ScriptDisplaySprite.h" +#include "Scripting/Internal/TEN/View/DisplayString/ScriptDisplayString.h" #include "Scripting/Internal/TEN/View/ScaleModes.h" #include "Scripting/Internal/TEN/View/PostProcessEffects.h" #include "Specific/clock.h" @@ -26,9 +28,12 @@ #include "Specific/trutils.h" using namespace TEN::Effects::Environment; +using namespace TEN::Scripting::DisplayArea; using namespace TEN::Scripting::DisplaySprite; +using namespace TEN::Scripting::DisplayString; using namespace TEN::Scripting::DisplayItem; using namespace TEN::Scripting::View; +using namespace TEN::SpotCam; using namespace TEN::Utils; using namespace TEN::Video; @@ -175,14 +180,14 @@ namespace TEN::Scripting::View { constexpr auto PROGRESS_MAX = 100.0f; - return Vec3(GetCameraTransform(seqID, progress / PROGRESS_MAX, ValueOr(loop, false)).Position); + return Vec3(GetSpotCamSequenceTransform(seqID, progress / PROGRESS_MAX, ValueOr(loop, false)).Position); } static Rotation GetFlybyRotation(int seqID, float progress, TypeOrNil loop) { constexpr auto PROGRESS_MAX = 100.0f; - return Rotation(GetCameraTransform(seqID, progress / PROGRESS_MAX, ValueOr(loop, false)).Orientation); + return Rotation(GetSpotCamSequenceTransform(seqID, progress / PROGRESS_MAX, ValueOr(loop, false)).Orientation); } static void FlashScreen(TypeOrNil col, TypeOrNil speed) @@ -369,7 +374,9 @@ namespace TEN::Scripting::View tableView.set_function("PlayFlyBy", &PlayFlyby); // Register types. + ScriptDisplayArea::Register(*state, tableView); ScriptDisplaySprite::Register(*state, tableView); + ScriptDisplayString::Register(*state, tableView); ScriptDisplayItem::Register(*state, tableView); ScriptDisplayAnchors::Register(tableView); diff --git a/TombEngine/Shaders/CBCamera.hlsli b/TombEngine/Shaders/CBCamera.hlsli index eec9357054..50ea84b36e 100644 --- a/TombEngine/Shaders/CBCamera.hlsli +++ b/TombEngine/Shaders/CBCamera.hlsli @@ -44,7 +44,7 @@ cbuffer CBCamera : register(b0) int RefreshRate; int NumFogBulbs; float InterpolatedFrame; - float Padding2; + float Gamma; //-- ShaderFogBulb FogBulbs[MAX_FOG_BULBS]; }; diff --git a/TombEngine/Shaders/InstancedSprites.hlsl b/TombEngine/Shaders/InstancedSprites.hlsl index e8c56c42fd..ce475ba9f1 100644 --- a/TombEngine/Shaders/InstancedSprites.hlsl +++ b/TombEngine/Shaders/InstancedSprites.hlsl @@ -1,136 +1,122 @@ -#include "./CBCamera.hlsli" -#include "./Blending.hlsli" -#include "./VertexInput.hlsli" -#include "./Math.hlsli" -#include "./ShaderLight.hlsli" -#include "./SpriteEffects.hlsli" -#include "./VertexEffects.hlsli" - -// NOTE: This shader is used for all opaque or not sorted transparent sprites, that can be instanced for a faster drawing - -#define INSTANCED_SPRITES_BUCKET_SIZE 512 -#define FADE_FACTOR .789f - -struct PixelShaderInput -{ - float4 Position: SV_POSITION; - float2 UV: TEXCOORD1; - float4 Color: COLOR; - float4 PositionCopy: TEXCOORD2; - float4 FogBulbs : TEXCOORD3; - float DistanceFog : FOG; - uint InstanceID : SV_InstanceID; -}; - -struct InstancedSprite -{ - float4x4 World; - float4 UV[2]; - float4 Color; - float IsBillboard; - float IsSoftParticle; - int RenderType; - int PerVertexColor; -}; - -cbuffer InstancedSpriteBuffer : register(b13) -{ - InstancedSprite Sprites[INSTANCED_SPRITES_BUCKET_SIZE]; -}; - -Texture2D Texture : register(t0); -SamplerState Sampler : register(s0); - -Texture2D DepthTexture : register(t6); -SamplerState DepthSampler : register(s6); - -PixelShaderInput VS(VertexShaderInput input, uint InstanceID : SV_InstanceID) -{ - PixelShaderInput output; - - InstancedSprite sprite = Sprites[InstanceID]; - - float4 worldPosition; - - if (sprite.IsBillboard == 1) - { - worldPosition = mul(float4(input.Position, 1.0f), sprite.World); - output.Position = mul(mul(float4(input.Position, 1.0f), sprite.World), ViewProjection); - } - else - { - worldPosition = float4(input.Position, 1.0f); - output.Position = mul(float4(input.Position, 1.0f), ViewProjection); - } - - int polyIndex = DecodeIndexInPoly(input.Effects); - - output.PositionCopy = output.Position; - output.Color = lerp(sprite.Color, input.Color, saturate((float) sprite.PerVertexColor)); - output.UV = float2(sprite.UV[0][polyIndex], sprite.UV[1][polyIndex]); - output.InstanceID = InstanceID; - - output.FogBulbs = DoFogBulbsForVertex(worldPosition); - output.DistanceFog = DoDistanceFogForVertex(worldPosition); - - return output; -} - -// TODO: From NVIDIA SDK, check if it can be useful instead of linear ramp -float Contrast(float Input, float ContrastPower) -{ -#if 1 - //piecewise contrast function - bool IsAboveHalf = Input > 0.5; - float ToRaise = saturate(2 * (IsAboveHalf ? 1 - Input : Input)); - float Output = 0.5 * pow(ToRaise, ContrastPower); - Output = IsAboveHalf ? 1 - Output : Output; - return Output; -#else - // another solution to create a kind of contrast function - return 1.0 - exp2(-2 * pow(2.0 * saturate(Input), ContrastPower)); -#endif -} - -float4 PS(PixelShaderInput input) : SV_TARGET -{ - float4 output = Texture.Sample(Sampler, input.UV) * input.Color; - - InstancedSprite sprite = Sprites[input.InstanceID]; - - if (sprite.IsSoftParticle == 1) - { - float particleDepth = input.PositionCopy.z / input.PositionCopy.w; - input.PositionCopy.xy /= input.PositionCopy.w; - float2 texCoord = 0.5f * (float2(input.PositionCopy.x, -input.PositionCopy.y) + 1); - float sceneDepth = DepthTexture.Sample(DepthSampler, texCoord).x; - - sceneDepth = LinearizeDepth(sceneDepth, NearPlane, FarPlane); - particleDepth = LinearizeDepth(particleDepth, NearPlane, FarPlane); - - if (particleDepth - sceneDepth > 0.01f) - { - discard; - } - - float fade = (sceneDepth - particleDepth) * 1024.0f; - output.w = min(output.w, fade); - } - - if (sprite.RenderType == 1) - { - output = DoLaserBarrierEffect(input.Position, output, input.UV, FADE_FACTOR, Frame); - } - - if (sprite.RenderType == 2) - { - output = DoLaserBeamEffect(input.Position, output, input.UV, FADE_FACTOR, Frame); - } - - output.xyz *= 1.0f - Luma(input.FogBulbs.xyz); - output.xyz = saturate(output.xyz); - - output = DoDistanceFogForPixel(output, float4(0.0f, 0.0f, 0.0f, 0.0f), input.DistanceFog); - - return output; +#include "./CBCamera.hlsli" +#include "./Blending.hlsli" +#include "./VertexInput.hlsli" +#include "./Math.hlsli" +#include "./ShaderLight.hlsli" +#include "./SpriteEffects.hlsli" +#include "./VertexEffects.hlsli" + +// NOTE: This shader is used for all opaque or not sorted transparent sprites, that can be instanced for a faster drawing + +#define INSTANCED_SPRITES_BUCKET_SIZE 512 +#define FADE_FACTOR .789f + +struct PixelShaderInput +{ + float4 Position: SV_POSITION; + float2 UV: TEXCOORD1; + float4 Color: COLOR; + float4 PositionCopy: TEXCOORD2; + float4 FogBulbs : TEXCOORD3; + float DistanceFog : FOG; + uint InstanceID : SV_InstanceID; +}; + +struct InstancedSprite +{ + float4x4 World; + float4 UV[2]; + float4 Color; + float IsBillboard; + float IsSoftParticle; + int RenderType; + int PerVertexColor; +}; + +cbuffer InstancedSpriteBuffer : register(b13) +{ + InstancedSprite Sprites[INSTANCED_SPRITES_BUCKET_SIZE]; +}; + +Texture2D Texture : register(t0); +SamplerState Sampler : register(s0); + +Texture2D DepthTexture : register(t6); +SamplerState DepthSampler : register(s6); + +PixelShaderInput VS(VertexShaderInput input, uint InstanceID : SV_InstanceID) +{ + PixelShaderInput output; + + InstancedSprite sprite = Sprites[InstanceID]; + + float4 worldPosition; + + if (sprite.IsBillboard == 1) + { + worldPosition = mul(float4(input.Position, 1.0f), sprite.World); + output.Position = mul(mul(float4(input.Position, 1.0f), sprite.World), ViewProjection); + } + else + { + worldPosition = float4(input.Position, 1.0f); + output.Position = mul(float4(input.Position, 1.0f), ViewProjection); + } + + int polyIndex = DecodeIndexInPoly(input.Effects); + + output.PositionCopy = output.Position; + output.Color = lerp(sprite.Color, input.Color, saturate((float)sprite.PerVertexColor)); + output.UV = float2(sprite.UV[0][polyIndex], sprite.UV[1][polyIndex]); + output.InstanceID = InstanceID; + + output.FogBulbs = DoFogBulbsForVertex(worldPosition); + output.DistanceFog = DoDistanceFogForVertex(worldPosition); + + return output; +} + +float4 PS(PixelShaderInput input) : SV_TARGET +{ + float4 output = Texture.Sample(Sampler, input.UV) * input.Color; + + InstancedSprite sprite = Sprites[input.InstanceID]; + + if (sprite.IsSoftParticle == 1) + { + float particleDepth = input.PositionCopy.z / input.PositionCopy.w; + input.PositionCopy.xy /= input.PositionCopy.w; + float2 texCoord = 0.5f * (float2(input.PositionCopy.x, -input.PositionCopy.y) + 1); + float sceneDepth = DepthTexture.Sample(DepthSampler, texCoord).x; + + sceneDepth = LinearizeDepth(sceneDepth, NearPlane, FarPlane); + particleDepth = LinearizeDepth(particleDepth, NearPlane, FarPlane); + + if (particleDepth - sceneDepth > 0.01f) + { + discard; + } + + float fade = (sceneDepth - particleDepth) * 1024.0f; + output.w = min(output.w, fade); + } + + if (sprite.RenderType == 1) + { + float4 rawOutput = Texture.Sample(Sampler, input.UV) * input.Color; + output = DoLaserBarrierEffect(input.Position, float4(ModulateColor(rawOutput.rgb), rawOutput.a), input.UV, FADE_FACTOR, Frame); + } + + if (sprite.RenderType == 2) + { + float4 rawOutput = Texture.Sample(Sampler, input.UV) * input.Color; + output = DoLaserBeamEffect(input.Position, float4(ModulateColor(rawOutput.rgb), rawOutput.a), input.UV, FADE_FACTOR, Frame); + } + + output.xyz *= 1.0f - Luma(input.FogBulbs.xyz); + output.xyz = saturate(output.xyz); + + output = DoDistanceFogForPixel(output, float4(0.0f, 0.0f, 0.0f, 0.0f), input.DistanceFog); + + return output; } \ No newline at end of file diff --git a/TombEngine/Shaders/InstancedStatics.hlsl b/TombEngine/Shaders/InstancedStatics.hlsl index 9aecf4ac9c..43d30cec77 100644 --- a/TombEngine/Shaders/InstancedStatics.hlsl +++ b/TombEngine/Shaders/InstancedStatics.hlsl @@ -1,129 +1,130 @@ -#include "./Math.hlsli" -#include "./CBCamera.hlsli" -#include "./CBInstancedStatics.hlsli" -#include "./ShaderLight.hlsli" -#include "./VertexEffects.hlsli" -#include "./VertexInput.hlsli" -#include "./Blending.hlsli" -#include "./Shadows.hlsli" -#include "./AnimatedTextures.hlsli" -#include "./Materials.hlsli" - -struct PixelShaderInput -{ - float4 Position: SV_POSITION; - float3 WorldPosition: POSITION0; - float3 Normal: NORMAL; - float2 UV: TEXCOORD0; - float4 Color: COLOR; - float Sheen : SHEEN; - float4 PositionCopy : TEXCOORD1; - float4 FogBulbs : TEXCOORD2; - float DistanceFog : FOG; - float3 Tangent: TANGENT; - float3 Binormal : BINORMAL; - float3 FaceNormal : TEXCOORD3; - uint InstanceID : SV_InstanceID; -}; - -struct PixelShaderOutput -{ - float4 Color: SV_TARGET0; -}; - -Texture2D Texture : register(t0); -SamplerState Sampler : register(s0); - -Texture2D NormalTexture : register(t1); -SamplerState NormalTextureSampler : register(s1); - -PixelShaderInput VS(VertexShaderInput input, uint InstanceID : SV_InstanceID) -{ - PixelShaderInput output; - - float wibble = Wibble(input.Effects, DecodeHash(input.AnimationFrameOffsetIndexHash)); - float3 pos = Move(input.Position, input.Effects, wibble); - float3 col = Glow(input.Color.xyz, input.Effects, wibble); - - float4 worldPosition = (mul(float4(pos, 1.0f), StaticMeshes[InstanceID].World)); - - output.Position = mul(worldPosition, ViewProjection); - output.UV = GetUVPossiblyAnimated(input.UV, DecodeIndexInPoly(input.Effects), DecodeAnimationFrameOffset(input.AnimationFrameOffsetIndexHash)); - output.WorldPosition = worldPosition; - output.Color = float4(col, input.Color.w); - output.Color *= StaticMeshes[InstanceID].Color; - output.PositionCopy = output.Position; - output.Sheen = DecodeSheen(input.Effects); - output.InstanceID = InstanceID; - - output.Normal = normalize(mul(input.Normal.xyz, (float3x3) StaticMeshes[InstanceID].World).xyz); - output.Tangent = normalize(mul(input.Tangent.xyz, (float3x3) StaticMeshes[InstanceID].World).xyz); - output.Binormal = SafeNormalize(mul(cross(input.Normal.xyz, input.Tangent.xyz), (float3x3) StaticMeshes[InstanceID].World).xyz); - output.FaceNormal = normalize(mul(input.FaceNormal.xyz, (float3x3) StaticMeshes[InstanceID].World).xyz); - - output.FogBulbs = DoFogBulbsForVertex(worldPosition); - output.DistanceFog = DoDistanceFogForVertex(worldPosition); - - return output; -} - -PixelShaderOutput PS(PixelShaderInput input) -{ - PixelShaderOutput output; - - input.UV = ConvertAnimUV(input.UV); - - // Apply parallax mapping - float3x3 TBNf = float3x3(input.Tangent, input.Binormal, input.FaceNormal); - input.UV = ParallaxOcclusionMapping(TBNf, input.WorldPosition, input.UV); - - float4 ORSH = ConvertAnimOSRH(ORSHTexture.Sample(ORSHSampler, input.UV)); - float ambientOcclusion = ORSH.x; - float roughness = ORSH.y; - float specular = ORSH.z; - - float3 emissive = EmissiveTexture.Sample(EmissiveSampler, input.UV).xyz; - - float3x3 TBN = float3x3(input.Tangent, input.Binormal, input.Normal); - float3 normal = ConvertAnimNormal(UnpackNormalMap(NormalTexture.Sample(NormalTextureSampler, input.UV))); - normal = EnsureNormal(mul(normal, TBN), input.WorldPosition); - - float4 tex = Texture.Sample(Sampler, input.UV); - DoAlphaTest(tex); - - uint mode = StaticMeshes[input.InstanceID].LightInfo.y; - uint numLights = StaticMeshes[input.InstanceID].LightInfo.x; - - // Material effects - tex.xyz = CalculateReflections(input.WorldPosition, tex.xyz, normal, specular); - - // Ambient occlusion - float occlusion = CalculateOcclusion(GetSamplePosition(input.PositionCopy), tex.w); - occlusion *= ambientOcclusion; - - float3 color = (mode == 0) ? - CombineLights( - StaticMeshes[input.InstanceID].AmbientLight.xyz, - input.Color.xyz, - tex.xyz, - input.WorldPosition, - normal, - input.Sheen, - StaticMeshes[input.InstanceID].InstancedStaticLights, - numLights, - input.FogBulbs.w, - emissive, - specular, - roughness) : - StaticLight(input.Color.xyz, tex.xyz, input.FogBulbs.w, emissive); - - color = DoShadow(input.WorldPosition, normal, color, -0.5f); - color = DoBlobShadows(input.WorldPosition, color); - - output.Color = float4(color * occlusion, tex.w); - output.Color = DoFogBulbsForPixel(output.Color, float4(input.FogBulbs.xyz, 1.0f)); - output.Color = DoDistanceFogForPixel(output.Color, FogColor, input.DistanceFog); - output.Color.w *= input.Color.w; - - return output; -} +#include "./Math.hlsli" +#include "./CBCamera.hlsli" +#include "./CBInstancedStatics.hlsli" +#include "./ShaderLight.hlsli" +#include "./VertexEffects.hlsli" +#include "./VertexInput.hlsli" +#include "./Blending.hlsli" +#include "./Shadows.hlsli" +#include "./AnimatedTextures.hlsli" +#include "./Materials.hlsli" + +struct PixelShaderInput +{ + float4 Position: SV_POSITION; + float3 WorldPosition: POSITION0; + float3 Normal: NORMAL; + float2 UV: TEXCOORD0; + float4 Color: COLOR; + float Sheen : SHEEN; + float4 PositionCopy : TEXCOORD1; + float4 FogBulbs : TEXCOORD2; + float DistanceFog : FOG; + float3 Tangent: TANGENT; + float3 Binormal : BINORMAL; + float3 FaceNormal : TEXCOORD3; + uint InstanceID : SV_InstanceID; +}; + +struct PixelShaderOutput +{ + float4 Color: SV_TARGET0; +}; + +Texture2D Texture : register(t0); +SamplerState Sampler : register(s0); + +Texture2D NormalTexture : register(t1); +SamplerState NormalTextureSampler : register(s1); + +PixelShaderInput VS(VertexShaderInput input, uint InstanceID : SV_InstanceID) +{ + PixelShaderInput output; + + float wibble = Wibble(input.Effects, DecodeHash(input.AnimationFrameOffsetIndexHash)); + float3 pos = Move(input.Position, input.Effects, wibble); + float3 col = Glow(input.Color.xyz, input.Effects, wibble); + + float4 worldPosition = (mul(float4(pos, 1.0f), StaticMeshes[InstanceID].World)); + + output.Position = mul(worldPosition, ViewProjection); + output.UV = GetUVPossiblyAnimated(input.UV, DecodeIndexInPoly(input.Effects), DecodeAnimationFrameOffset(input.AnimationFrameOffsetIndexHash)); + output.WorldPosition = worldPosition; + output.Color = float4(col, input.Color.w); + output.Color.w *= StaticMeshes[InstanceID].Color.w; + output.PositionCopy = output.Position; + output.Sheen = DecodeSheen(input.Effects); + output.InstanceID = InstanceID; + + output.Normal = normalize(mul(input.Normal.xyz, (float3x3) StaticMeshes[InstanceID].World).xyz); + output.Tangent = normalize(mul(input.Tangent.xyz, (float3x3) StaticMeshes[InstanceID].World).xyz); + output.Binormal = SafeNormalize(mul(cross(input.Normal.xyz, input.Tangent.xyz), (float3x3) StaticMeshes[InstanceID].World).xyz); + output.FaceNormal = normalize(mul(input.FaceNormal.xyz, (float3x3) StaticMeshes[InstanceID].World).xyz); + + output.FogBulbs = DoFogBulbsForVertex(worldPosition); + output.DistanceFog = DoDistanceFogForVertex(worldPosition); + + return output; +} + +PixelShaderOutput PS(PixelShaderInput input) +{ + PixelShaderOutput output; + + input.UV = ConvertAnimUV(input.UV); + + // Apply parallax mapping + float3x3 TBNf = float3x3(input.Tangent, input.Binormal, input.FaceNormal); + input.UV = ParallaxOcclusionMapping(TBNf, input.WorldPosition, input.UV); + + float4 ORSH = ConvertAnimOSRH(ORSHTexture.Sample(ORSHSampler, input.UV)); + float ambientOcclusion = ORSH.x; + float roughness = ORSH.y; + float specular = ORSH.z; + + float3 emissive = EmissiveTexture.Sample(EmissiveSampler, input.UV).xyz; + + float3x3 TBN = float3x3(input.Tangent, input.Binormal, input.Normal); + float3 normal = ConvertAnimNormal(UnpackNormalMap(NormalTexture.Sample(NormalTextureSampler, input.UV))); + normal = EnsureNormal(mul(normal, TBN), input.WorldPosition); + + float4 tex = Texture.Sample(Sampler, input.UV); + DoAlphaTest(tex); + + uint mode = StaticMeshes[input.InstanceID].LightInfo.y; + uint numLights = StaticMeshes[input.InstanceID].LightInfo.x; + + // Material effects + tex.xyz = CalculateReflections(input.WorldPosition, tex.xyz, normal, specular); + + // Ambient occlusion + float occlusion = CalculateOcclusion(GetSamplePosition(input.PositionCopy), tex.w); + occlusion *= ambientOcclusion; + + float3 staticColor = StaticMeshes[input.InstanceID].Color.xyz; + float3 color = (mode == 0) ? + CombineLights( + ModulateColor(StaticMeshes[input.InstanceID].AmbientLight.xyz), + ModulateColor(input.Color.xyz * staticColor), + tex.xyz, + input.WorldPosition, + normal, + input.Sheen, + StaticMeshes[input.InstanceID].InstancedStaticLights, + numLights, + input.FogBulbs.w, + emissive, + specular, + roughness) : + StaticLight(ModulateColor(input.Color.xyz * staticColor), tex.xyz, input.FogBulbs.w, emissive); + + color = DoShadow(input.WorldPosition, normal, color, -0.5f); + color = DoBlobShadows(input.WorldPosition, color); + + output.Color = float4(color * occlusion, tex.w); + output.Color = DoFogBulbsForPixel(output.Color, float4(input.FogBulbs.xyz, 1.0f)); + output.Color = DoDistanceFogForPixel(output.Color, FogColor, input.DistanceFog); + output.Color.w *= input.Color.w; + + return output; +} \ No newline at end of file diff --git a/TombEngine/Shaders/Inventory.hlsl b/TombEngine/Shaders/Inventory.hlsl index 54d04201f4..f3f2085371 100644 --- a/TombEngine/Shaders/Inventory.hlsl +++ b/TombEngine/Shaders/Inventory.hlsl @@ -60,7 +60,7 @@ PixelShaderOutput PS(PixelShaderInput input) : SV_TARGET PixelShaderOutput output; float4 tex = Texture.Sample(Sampler, input.UV); - float3 baseColor = tex.xyz * Color.xyz; + float3 baseColor = tex.xyz * ModulateColor(Color.xyz); float3 pos = normalize(input.WorldPosition); output.Color = float4(baseColor, tex.w * Color.w); @@ -80,22 +80,23 @@ PixelShaderOutput PS(PixelShaderInput input) : SV_TARGET // Material effects output.Color.xyz = CalculateReflections(input.WorldPosition, output.Color.xyz, normal, specular); - + ShaderLight l; - l.Color = float3(AmbientLight.xyz); l.Intensity = 0.3f; l.Type = LT_SUN; l.Direction = normalize(float3(-1.0f, -0.707f, -0.5f)); + l.Color.xyz = ModulateColor(AmbientLight.xyz); float3 lighting = DoDirectionalLight(pos, normal, l); - lighting += DoSpecularSun(normal, l, input.Sheen, specular, roughness);; + lighting += DoSpecularSun(normal, l, input.Sheen, specular, roughness); lighting += emissive; - + // Emissive material output.Color.xyz += lighting * output.Color.a; output.Color.xyz = saturate(output.Color.xyz); output.Emissive = float4(emissive, 1.0f); + output.Color.xyz = GammaCorrection(output.Color.xyz); return output; } diff --git a/TombEngine/Shaders/Items.hlsl b/TombEngine/Shaders/Items.hlsl index 9cfc734245..1375a0bb22 100644 --- a/TombEngine/Shaders/Items.hlsl +++ b/TombEngine/Shaders/Items.hlsl @@ -1,138 +1,138 @@ -#include "./Math.hlsli" -#include "./CBCamera.hlsli" -#include "./CBItem.hlsli" -#include "./ShaderLight.hlsli" -#include "./VertexEffects.hlsli" -#include "./VertexInput.hlsli" -#include "./Blending.hlsli" -#include "./AnimatedTextures.hlsli" -#include "./Shadows.hlsli" -#include "./Materials.hlsli" - -struct PixelShaderInput -{ - float4 Position: SV_POSITION; - float3 WorldPosition: POSITION0; - float3 Normal: NORMAL; - float2 UV: TEXCOORD0; - float4 Color: COLOR; - float Sheen : SHEEN; - float4 PositionCopy : TEXCOORD1; - float4 FogBulbs : TEXCOORD2; - float DistanceFog : FOG; - float3 Tangent: TANGENT; - float3 Binormal : BINORMAL; - float3 FaceNormal : TEXCOORD3; - unsigned int Bone : BONE; -}; - -struct PixelShaderOutput -{ - float4 Color: SV_TARGET0; -}; - -Texture2D Texture : register(t0); -SamplerState Sampler : register(s0); - -Texture2D NormalTexture : register(t1); -SamplerState NormalTextureSampler : register(s1); - -Texture2D AmbientMapFrontTexture : register(t7); -SamplerState AmbientMapFrontSampler : register(s7); - -Texture2D AmbientMapBackTexture : register(t8); -SamplerState AmbientMapBackSampler : register(s8); - -PixelShaderInput VS(VertexShaderInput input) -{ - PixelShaderInput output; - - // Blend and apply world matrix - float4x4 blended = Skinned ? BlendBoneMatrices(input, Bones, (Skinned == 2)) : Bones[input.BoneIndex[0]]; - float4x4 world = mul(blended, World); - - // Calculate vertex effects - float wibble = Wibble(input.Effects, DecodeHash(input.AnimationFrameOffsetIndexHash)); - float3 pos = Move(input.Position, input.Effects, wibble); - float3 col = Glow(input.Color.xyz, input.Effects, wibble); - float3 worldPosition = mul(float4(pos, 1.0f), world).xyz; - - output.Position = mul(float4(worldPosition, 1.0f), ViewProjection); - output.UV = GetUVPossiblyAnimated(input.UV, DecodeIndexInPoly(input.Effects), DecodeAnimationFrameOffset(input.AnimationFrameOffsetIndexHash)); - output.Color = float4(col, input.Color.w); - output.Color *= Color; - output.PositionCopy = output.Position; - output.Sheen = DecodeSheen(input.Effects); - output.Bone = input.BoneIndex[0]; - output.WorldPosition = worldPosition; - - output.Normal = normalize(mul(input.Normal.xyz, (float3x3) world).xyz); - output.Tangent = normalize(mul(input.Tangent.xyz, (float3x3) world).xyz); - output.Binormal = SafeNormalize(mul(cross(input.Normal.xyz, input.Tangent.xyz), (float3x3) world).xyz); - output.FaceNormal = normalize(mul(input.FaceNormal.xyz, (float3x3) world).xyz); - - output.FogBulbs = DoFogBulbsForVertex(worldPosition); - output.DistanceFog = DoDistanceFogForVertex(worldPosition); - - return output; -} - -PixelShaderOutput PS(PixelShaderInput input) -{ - PixelShaderOutput output; - - input.UV = ConvertAnimUV(input.UV); - - // Apply parallax mapping - float3x3 TBNf = float3x3(input.Tangent, input.Binormal, input.FaceNormal); - input.UV = ParallaxOcclusionMapping(TBNf, input.WorldPosition, input.UV); - - float4 ORSH = ConvertAnimOSRH(ORSHTexture.Sample(ORSHSampler, input.UV)); - float ambientOcclusion = ORSH.x; - float roughness = ORSH.y; - float specular = ORSH.z; - - float3 emissive = EmissiveTexture.Sample(EmissiveSampler, input.UV).xyz; - - float3x3 TBN = float3x3(input.Tangent, input.Binormal, input.Normal); - float3 normal = ConvertAnimNormal(UnpackNormalMap(NormalTexture.Sample(NormalTextureSampler, input.UV))); - normal = EnsureNormal(mul(normal, TBN), input.WorldPosition); - - float4 tex = Texture.Sample(Sampler, input.UV); - DoAlphaTest(tex); - - // Material effects - tex.xyz = CalculateReflections(input.WorldPosition, tex.xyz, normal , specular); - - // Ambient occlusion - float occlusion = CalculateOcclusion(GetSamplePosition(input.PositionCopy), tex.w); - occlusion *= ambientOcclusion; - - float3 color = (BoneLightModes[input.Bone / 4][input.Bone % 4] == 0) ? - CombineLights( - AmbientLight.xyz, - input.Color.xyz, - tex.xyz, - input.WorldPosition, - normal, - input.Sheen, - ItemLights, - NumItemLights, - input.FogBulbs.w, - emissive, - specular, - roughness) : - StaticLight(input.Color.xyz, tex.xyz, input.FogBulbs.w, emissive); - - float shadowable = step(0.5f, float((NumItemLights & SHADOWABLE_MASK) == SHADOWABLE_MASK)); - float3 shadow = DoShadow(input.WorldPosition, normal, color, -0.5f); - shadow = DoBlobShadows(input.WorldPosition, shadow); - color = lerp(color, shadow, shadowable); - - output.Color = saturate(float4(color * occlusion, tex.w)); - output.Color = DoFogBulbsForPixel(output.Color, float4(input.FogBulbs.xyz, 1.0f)); - output.Color = DoDistanceFogForPixel(output.Color, FogColor, input.DistanceFog); - output.Color.w *= input.Color.w; - - return output; -} +#include "./Math.hlsli" +#include "./CBCamera.hlsli" +#include "./CBItem.hlsli" +#include "./ShaderLight.hlsli" +#include "./VertexEffects.hlsli" +#include "./VertexInput.hlsli" +#include "./Blending.hlsli" +#include "./AnimatedTextures.hlsli" +#include "./Shadows.hlsli" +#include "./Materials.hlsli" + +struct PixelShaderInput +{ + float4 Position: SV_POSITION; + float3 WorldPosition: POSITION0; + float3 Normal: NORMAL; + float2 UV: TEXCOORD0; + float4 Color: COLOR; + float Sheen : SHEEN; + float4 PositionCopy : TEXCOORD1; + float4 FogBulbs : TEXCOORD2; + float DistanceFog : FOG; + float3 Tangent: TANGENT; + float3 Binormal : BINORMAL; + float3 FaceNormal : TEXCOORD3; + unsigned int Bone : BONE; +}; + +struct PixelShaderOutput +{ + float4 Color: SV_TARGET0; +}; + +Texture2D Texture : register(t0); +SamplerState Sampler : register(s0); + +Texture2D NormalTexture : register(t1); +SamplerState NormalTextureSampler : register(s1); + +Texture2D AmbientMapFrontTexture : register(t7); +SamplerState AmbientMapFrontSampler : register(s7); + +Texture2D AmbientMapBackTexture : register(t8); +SamplerState AmbientMapBackSampler : register(s8); + +PixelShaderInput VS(VertexShaderInput input) +{ + PixelShaderInput output; + + // Blend and apply world matrix + float4x4 blended = Skinned ? BlendBoneMatrices(input, Bones, (Skinned == 2)) : Bones[input.BoneIndex[0]]; + float4x4 world = mul(blended, World); + + // Calculate vertex effects + float wibble = Wibble(input.Effects, DecodeHash(input.AnimationFrameOffsetIndexHash)); + float3 pos = Move(input.Position, input.Effects, wibble); + float3 col = Glow(input.Color.xyz, input.Effects, wibble); + float3 worldPosition = mul(float4(pos, 1.0f), world).xyz; + + output.Position = mul(float4(worldPosition, 1.0f), ViewProjection); + output.UV = GetUVPossiblyAnimated(input.UV, DecodeIndexInPoly(input.Effects), DecodeAnimationFrameOffset(input.AnimationFrameOffsetIndexHash)); + output.Color = float4(col, input.Color.w); + output.Color.w *= Color.w; + output.PositionCopy = output.Position; + output.Sheen = DecodeSheen(input.Effects); + output.Bone = input.BoneIndex[0]; + output.WorldPosition = worldPosition; + + output.Normal = normalize(mul(input.Normal.xyz, (float3x3) world).xyz); + output.Tangent = normalize(mul(input.Tangent.xyz, (float3x3) world).xyz); + output.Binormal = SafeNormalize(mul(cross(input.Normal.xyz, input.Tangent.xyz), (float3x3) world).xyz); + output.FaceNormal = normalize(mul(input.FaceNormal.xyz, (float3x3) world).xyz); + + output.FogBulbs = DoFogBulbsForVertex(worldPosition); + output.DistanceFog = DoDistanceFogForVertex(worldPosition); + + return output; +} + +PixelShaderOutput PS(PixelShaderInput input) +{ + PixelShaderOutput output; + + input.UV = ConvertAnimUV(input.UV); + + // Apply parallax mapping + float3x3 TBNf = float3x3(input.Tangent, input.Binormal, input.FaceNormal); + input.UV = ParallaxOcclusionMapping(TBNf, input.WorldPosition, input.UV); + + float4 ORSH = ConvertAnimOSRH(ORSHTexture.Sample(ORSHSampler, input.UV)); + float ambientOcclusion = ORSH.x; + float roughness = ORSH.y; + float specular = ORSH.z; + + float3 emissive = EmissiveTexture.Sample(EmissiveSampler, input.UV).xyz; + + float3x3 TBN = float3x3(input.Tangent, input.Binormal, input.Normal); + float3 normal = ConvertAnimNormal(UnpackNormalMap(NormalTexture.Sample(NormalTextureSampler, input.UV))); + normal = EnsureNormal(mul(normal, TBN), input.WorldPosition); + + float4 tex = Texture.Sample(Sampler, input.UV); + DoAlphaTest(tex); + + // Material effects + tex.xyz = CalculateReflections(input.WorldPosition, tex.xyz, normal , specular); + + // Ambient occlusion + float occlusion = CalculateOcclusion(GetSamplePosition(input.PositionCopy), tex.w); + occlusion *= ambientOcclusion; + + float3 color = (BoneLightModes[input.Bone / 4][input.Bone % 4] == 0) ? + CombineLights( + ModulateColor(AmbientLight.xyz), + ModulateColor(input.Color.xyz * Color.xyz), + tex.xyz, + input.WorldPosition, + normal, + input.Sheen, + ItemLights, + NumItemLights, + input.FogBulbs.w, + emissive, + specular, + roughness) : + StaticLight(ModulateColor(input.Color.xyz * Color.xyz), tex.xyz, input.FogBulbs.w, emissive); + + float shadowable = step(0.5f, float((NumItemLights & SHADOWABLE_MASK) == SHADOWABLE_MASK)); + float3 shadow = DoShadow(input.WorldPosition, normal, color, -0.5f); + shadow = DoBlobShadows(input.WorldPosition, shadow); + color = lerp(color, shadow, shadowable); + + output.Color = saturate(float4(color * occlusion, tex.w)); + output.Color = DoFogBulbsForPixel(output.Color, float4(input.FogBulbs.xyz, 1.0f)); + output.Color = DoDistanceFogForPixel(output.Color, FogColor, input.DistanceFog); + output.Color.w *= input.Color.w; + + return output; +} \ No newline at end of file diff --git a/TombEngine/Shaders/PostProcess.hlsl b/TombEngine/Shaders/PostProcess.hlsl index 878c0fac59..e69d185225 100644 --- a/TombEngine/Shaders/PostProcess.hlsl +++ b/TombEngine/Shaders/PostProcess.hlsl @@ -2,6 +2,7 @@ #include "./CBCamera.hlsli" #include "./Materials.hlsli" #include "./Math.hlsli" +#include "./ShaderLight.hlsli" #define MAX_BLUR_RADIUS 100 #define USE_FAST_BILINEAR_BLUR 1 @@ -105,15 +106,12 @@ float4 PSFinalPass(PixelShaderInput input) : SV_TARGET output.w = 1.0f; } - output.xyz = output.xyz * Tint; - + output.xyz = GammaCorrection(ModulateColor(output.xyz * Tint)); return output; } float3 LensFlare(float2 uv, float2 pos) { - float intensity = 0.5f; - float2 main = uv - pos; float2 uvd = uv * length(uv); @@ -170,11 +168,11 @@ float3 LensFlare(float2 uv, float2 pos) float glare = exp(-pow(anamorphicOffset.x * anamorphicScaleX, 2.0f) - pow(anamorphicOffset.y * anamorphicScaleY, 2.0f)); float3 anamorphicGlare = float3(glare * 0.6f, glare * 0.5f, glare * 1.0f) * 0.05f; - // Combine the effects and adjust intensity + // Combine the effects float3 c = saturate(sunflare) * 0.5f + lensflare + anamorphicGlare; c = c * 1.3f - float3(length(uvd) * 0.05f, length(uvd) * 0.05f, length(uvd) * 0.05f); - return c * intensity; + return c; } float3 LensFlareColorCorrection(float3 color, float factor,float factor2) diff --git a/TombEngine/Shaders/Rooms.hlsl b/TombEngine/Shaders/Rooms.hlsl index 82bee7b5a5..2d2ec14346 100644 --- a/TombEngine/Shaders/Rooms.hlsl +++ b/TombEngine/Shaders/Rooms.hlsl @@ -1,222 +1,223 @@ -#include "./CBCamera.hlsli" -#include "./CBRoom.hlsli" -#include "./VertexInput.hlsli" -#include "./VertexEffects.hlsli" -#include "./Blending.hlsli" -#include "./Math.hlsli" -#include "./AnimatedTextures.hlsli" -#include "./Shadows.hlsli" -#include "./ShaderLight.hlsli" -#include "./Materials.hlsli" - -#define ROOM_LIGHT_COEFF 0.7f - -struct PixelShaderInput -{ - float4 Position: SV_POSITION; - float3 WorldPosition: POSITION0; - float3 Normal: NORMAL; - float2 UV: TEXCOORD0; - float4 Color: COLOR; - float Sheen : SHEEN; - float4 PositionCopy : TEXCOORD1; - float4 FogBulbs : TEXCOORD2; - float DistanceFog : FOG; - float3 Tangent: TANGENT; - float3 Binormal : BINORMAL; - float3 FaceNormal : TEXCOORD3; -}; - -Texture2D Texture : register(t0); -SamplerState Sampler : register(s0); - -Texture2D NormalTexture : register(t1); -SamplerState NormalTextureSampler : register(s1); - -Texture2D CausticsTexture : register(t2); -SamplerState CausticsTextureSampler : register(s2); - -struct PixelShaderOutput -{ - float4 Color: SV_TARGET0; -}; - -PixelShaderInput VS(VertexShaderInput input) -{ - PixelShaderInput output; - - // Setting effect weight on TE side prevents portal vertices from moving. - // Here we just read weight and decide if we should apply refraction or movement effect. - float weight = DecodeWeight(input.Effects); - - // Calculate vertex effects - float wibble = Wibble(input.Effects, DecodeHash(input.AnimationFrameOffsetIndexHash)); - float3 pos = Move(input.Position, input.Effects * weight, wibble); - float3 col = Glow(input.Color.xyz, input.Effects, wibble); - - // Refraction - float4 screenPos = mul(float4(pos, 1.0f), ViewProjection); - float2 clipPos = screenPos.xy / screenPos.w; - - if (CameraUnderwater != Water) - { - float factor = (Frame + clipPos.x * 320); - float xOffset = (sin(factor * PI / 20.0f)) * (screenPos.z / 1024) * 4; - float yOffset = (cos(factor * PI / 20.0f)) * (screenPos.z / 1024) * 4; - screenPos.x += xOffset * weight; - screenPos.y += yOffset * weight; - } - - output.Position = screenPos; - output.Normal = input.Normal.xyz; - output.Color = float4(col, input.Color.w); - output.PositionCopy = screenPos; - output.UV = GetUVPossiblyAnimated(input.UV, DecodeIndexInPoly(input.Effects), DecodeAnimationFrameOffset(input.AnimationFrameOffsetIndexHash)); - output.WorldPosition = pos; - output.Tangent = input.Tangent.xyz; - output.Binormal = cross(input.Normal.xyz, input.Tangent.xyz); - output.FaceNormal = input.FaceNormal; - - output.FogBulbs = DoFogBulbsForVertex(output.WorldPosition); - output.DistanceFog = DoDistanceFogForVertex(output.WorldPosition); - - return output; -} - -PixelShaderOutput PS(PixelShaderInput input) -{ - PixelShaderOutput output; - - input.UV = ConvertAnimUV(input.UV); - - // Apply parallax mapping - float3x3 TBNf = float3x3(input.Tangent, input.Binormal, input.FaceNormal); - input.UV = ParallaxOcclusionMapping(TBNf, input.WorldPosition, input.UV); - - float4 ORSH = ConvertAnimOSRH(ORSHTexture.Sample(ORSHSampler, input.UV)); - float ambientOcclusion = ORSH.x; - float roughness = ORSH.y; - float specular = ORSH.z; - - float3 emissive = EmissiveTexture.Sample(EmissiveSampler, input.UV).xyz; - - float3x3 TBN = float3x3(input.Tangent, input.Binormal, input.Normal); - float3 normal = ConvertAnimNormal(UnpackNormalMap(NormalTexture.Sample(NormalTextureSampler, input.UV))); - normal = EnsureNormal(mul(normal, TBN), input.WorldPosition); - - output.Color = Texture.Sample(Sampler, input.UV); - DoAlphaTest(output.Color); - - // Material effects - float3 blendedNormal = normalize(lerp(input.FaceNormal, normal, 0.1f)); // TODO: Make alpha customizable - output.Color.xyz = CalculateReflections(input.WorldPosition, output.Color.xyz, blendedNormal, specular); - - // Ambient occlusion - float occlusion = CalculateOcclusion(GetSamplePosition(input.PositionCopy), output.Color.w); - occlusion *= ambientOcclusion; - - float3 lighting = input.Color.xyz; - - // Shadows - lighting = DoShadow(input.WorldPosition, normal, lighting, -2.5f); - lighting = DoBlobShadows(input.WorldPosition, lighting); - - bool onlyPointLights = (NumRoomLights & ~LT_MASK) == LT_MASK_POINT; - int numLights = NumRoomLights & LT_MASK; - - for (int i = 0; i < numLights; i++) - { - if (onlyPointLights) - { - lighting += DoPointLight(input.WorldPosition, normal, RoomLights[i]) * ROOM_LIGHT_COEFF; - lighting += DoSpecularPoint(input.WorldPosition, normal, RoomLights[i], 0.0f, specular, roughness); - } - else - { - // Room dynamic lights can only be spot or point, so we use simplified function for that. - - float isPoint = step(0.5f, RoomLights[i].Type == LT_POINT); - float isSpot = step(0.5f, RoomLights[i].Type == LT_SPOT); - - float3 pointLight = float3(0.0f, 0.0f, 0.0f); - float3 spotLight = float3(0.0f, 0.0f, 0.0f); - DoPointAndSpotLight(input.WorldPosition, normal, RoomLights[i], specular, roughness, pointLight, spotLight); - - lighting += pointLight * isPoint * ROOM_LIGHT_COEFF + spotLight * isSpot * ROOM_LIGHT_COEFF; - } - } - - // Decals - if (!Animated && NumRoomDecals > 0 && !(MaterialTypeAndFlags & MATERIAL_FLAG_HEIGHTMAP)) - { - float decalMask = 0.0f; - - for (int i = 0; i < NumRoomDecals; i++) - { - float radius = RoomDecals[i].Radius; - float3 pos = input.WorldPosition - RoomDecals[i].Position; - float distance = length(pos); - - if (distance > radius * 1.3f) - continue; - - float2 uv = float2(dot(pos, input.Tangent), dot(pos, input.Binormal)); - uv *= (8.0f / (RoomDecals[i].Pattern + 1) * 2.0f / radius); - - float noiseVal = NebularNoise(uv, 1, 0.5f, 0.3f); - - float noisyRadius = radius * (1.0f + 0.25f * (noiseVal * 2.0f - 1.0f)); - float holeRadius = radius / 4.0f; - - float edge = saturate((noisyRadius - distance) / noisyRadius); - float fade = saturate((radius - distance) / radius); - float hole = saturate((holeRadius - distance) / (holeRadius * 1.3f)) * (1 - RoomDecals[i].Pattern); - - decalMask = max(decalMask, (edge * fade + hole) * RoomDecals[i].Opacity); - } - - lighting *= (1.0 - decalMask); - } - - // Caustics - if (Caustics) - { - float attenuation = saturate(dot(float3(0.0f, -1.0f, 0.0f), normal)); - - float3 blending = abs(normal); - blending = normalize(max(blending, 0.00001f)); - float b = (blending.x + blending.y + blending.z); - blending /= float3(b, b, b); - - float3 p = frac(input.WorldPosition.xyz / 2048.0f); - - float2 uv_x = CausticsStartUV + float2(p.z, p.y) * CausticsSize; - float2 uv_y = CausticsStartUV + float2(p.z, p.x) * CausticsSize; - float2 uv_z = CausticsStartUV + float2(p.y, p.x) * CausticsSize; - - float3 xaxis = CausticsTexture.SampleLevel(CausticsTextureSampler, uv_x, 0).xyz; - float3 yaxis = CausticsTexture.SampleLevel(CausticsTextureSampler, uv_y, 0).xyz; - float3 zaxis = CausticsTexture.SampleLevel(CausticsTextureSampler, uv_z, 0).xyz; - - float3 xc = xaxis * blending.x; - float3 yc = yaxis * blending.y; - float3 zc = zaxis * blending.z; - - float3 caustics = xc + yc + zc; - - lighting += (caustics * attenuation * 2.0f); - } - - // Emissive materials - lighting += emissive; - - // Fog bulbs and final color and light mixing - lighting -= float3(input.FogBulbs.w, input.FogBulbs.w, input.FogBulbs.w); - output.Color.xyz = output.Color.xyz * lighting * occlusion; - output.Color.xyz = saturate(output.Color.xyz); - - output.Color = DoFogBulbsForPixel(output.Color, float4(input.FogBulbs.xyz, 1.0f)); - output.Color = DoDistanceFogForPixel(output.Color, FogColor, input.DistanceFog); - - return output; -} +#include "./CBCamera.hlsli" +#include "./CBRoom.hlsli" +#include "./VertexInput.hlsli" +#include "./VertexEffects.hlsli" +#include "./Blending.hlsli" +#include "./Math.hlsli" +#include "./AnimatedTextures.hlsli" +#include "./Shadows.hlsli" +#include "./ShaderLight.hlsli" +#include "./Materials.hlsli" + +// This value is here for historic reasons, dynamic lights were 0.3 less powerful for rooms +#define ROOM_LIGHT_COEFF 0.7f + +struct PixelShaderInput +{ + float4 Position: SV_POSITION; + float3 WorldPosition: POSITION0; + float3 Normal: NORMAL; + float2 UV: TEXCOORD0; + float4 Color: COLOR; + float Sheen : SHEEN; + float4 PositionCopy : TEXCOORD1; + float4 FogBulbs : TEXCOORD2; + float DistanceFog : FOG; + float3 Tangent: TANGENT; + float3 Binormal : BINORMAL; + float3 FaceNormal : TEXCOORD3; +}; + +Texture2D Texture : register(t0); +SamplerState Sampler : register(s0); + +Texture2D NormalTexture : register(t1); +SamplerState NormalTextureSampler : register(s1); + +Texture2D CausticsTexture : register(t2); +SamplerState CausticsTextureSampler : register(s2); + +struct PixelShaderOutput +{ + float4 Color: SV_TARGET0; +}; + +PixelShaderInput VS(VertexShaderInput input) +{ + PixelShaderInput output; + + // Setting effect weight on TE side prevents portal vertices from moving. + // Here we just read weight and decide if we should apply refraction or movement effect. + float weight = DecodeWeight(input.Effects); + + // Calculate vertex effects + float wibble = Wibble(input.Effects, DecodeHash(input.AnimationFrameOffsetIndexHash)); + float3 pos = Move(input.Position, input.Effects * weight, wibble); + float3 col = Glow(input.Color.xyz, input.Effects, wibble); + + // Refraction + float4 screenPos = mul(float4(pos, 1.0f), ViewProjection); + float2 clipPos = screenPos.xy / screenPos.w; + + if (CameraUnderwater != Water) + { + float factor = (Frame + clipPos.x * 320); + float xOffset = (sin(factor * PI / 20.0f)) * (screenPos.z / 1024) * 4; + float yOffset = (cos(factor * PI / 20.0f)) * (screenPos.z / 1024) * 4; + screenPos.x += xOffset * weight; + screenPos.y += yOffset * weight; + } + + output.Position = screenPos; + output.Normal = input.Normal.xyz; + output.Color = float4(col, input.Color.w); + output.PositionCopy = screenPos; + output.UV = GetUVPossiblyAnimated(input.UV, DecodeIndexInPoly(input.Effects), DecodeAnimationFrameOffset(input.AnimationFrameOffsetIndexHash)); + output.WorldPosition = pos; + output.Tangent = input.Tangent.xyz; + output.Binormal = cross(input.Normal.xyz, input.Tangent.xyz); + output.FaceNormal = input.FaceNormal; + + output.FogBulbs = DoFogBulbsForVertex(output.WorldPosition); + output.DistanceFog = DoDistanceFogForVertex(output.WorldPosition); + + return output; +} + +PixelShaderOutput PS(PixelShaderInput input) +{ + PixelShaderOutput output; + + input.UV = ConvertAnimUV(input.UV); + + // Apply parallax mapping + float3x3 TBNf = float3x3(input.Tangent, input.Binormal, input.FaceNormal); + input.UV = ParallaxOcclusionMapping(TBNf, input.WorldPosition, input.UV); + + float4 ORSH = ConvertAnimOSRH(ORSHTexture.Sample(ORSHSampler, input.UV)); + float ambientOcclusion = ORSH.x; + float roughness = ORSH.y; + float specular = ORSH.z; + + float3 emissive = EmissiveTexture.Sample(EmissiveSampler, input.UV).xyz; + + float3x3 TBN = float3x3(input.Tangent, input.Binormal, input.Normal); + float3 normal = ConvertAnimNormal(UnpackNormalMap(NormalTexture.Sample(NormalTextureSampler, input.UV))); + normal = EnsureNormal(mul(normal, TBN), input.WorldPosition); + + output.Color = Texture.Sample(Sampler, input.UV); + DoAlphaTest(output.Color); + + // Material effects + float3 blendedNormal = normalize(lerp(input.FaceNormal, normal, 0.1f)); // TODO: Make alpha customizable + output.Color.xyz = CalculateReflections(input.WorldPosition, output.Color.xyz, blendedNormal, specular); + + // Ambient occlusion + float occlusion = CalculateOcclusion(GetSamplePosition(input.PositionCopy), output.Color.w); + occlusion *= ambientOcclusion; + + float3 lighting = ModulateColor(input.Color.xyz); + + // Shadows + lighting = DoShadow(input.WorldPosition, normal, lighting, -2.5f); + lighting = DoBlobShadows(input.WorldPosition, lighting); + + bool onlyPointLights = (NumRoomLights & ~LT_MASK) == LT_MASK_POINT; + int numLights = NumRoomLights & LT_MASK; + + for (int i = 0; i < numLights; i++) + { + if (onlyPointLights) + { + lighting += ModulateColor(DoPointLight(input.WorldPosition, normal, RoomLights[i])) * ROOM_LIGHT_COEFF; + lighting += ModulateColor(DoSpecularPoint(input.WorldPosition, normal, RoomLights[i], 0.0f, specular, roughness)); + } + else + { + // Room dynamic lights can only be spot or point, so we use simplified function for that. + + float isPoint = step(0.5f, RoomLights[i].Type == LT_POINT); + float isSpot = step(0.5f, RoomLights[i].Type == LT_SPOT); + + float3 pointLight = float3(0.0f, 0.0f, 0.0f); + float3 spotLight = float3(0.0f, 0.0f, 0.0f); + DoPointAndSpotLight(input.WorldPosition, normal, RoomLights[i], specular, roughness, pointLight, spotLight); + + lighting += ModulateColor(pointLight) * isPoint * ROOM_LIGHT_COEFF + ModulateColor(spotLight) * isSpot * ROOM_LIGHT_COEFF; + } + } + + // Decals + if (!Animated && NumRoomDecals > 0 && !(MaterialTypeAndFlags & MATERIAL_FLAG_HEIGHTMAP)) + { + float decalMask = 0.0f; + + for (int i = 0; i < NumRoomDecals; i++) + { + float radius = RoomDecals[i].Radius; + float3 pos = input.WorldPosition - RoomDecals[i].Position; + float distance = length(pos); + + if (distance > radius * 1.3f) + continue; + + float2 uv = float2(dot(pos, input.Tangent), dot(pos, input.Binormal)); + uv *= (8.0f / (RoomDecals[i].Pattern + 1) * 2.0f / radius); + + float noiseVal = NebularNoise(uv, 1, 0.5f, 0.3f); + + float noisyRadius = radius * (1.0f + 0.25f * (noiseVal * 2.0f - 1.0f)); + float holeRadius = radius / 4.0f; + + float edge = saturate((noisyRadius - distance) / noisyRadius); + float fade = saturate((radius - distance) / radius); + float hole = saturate((holeRadius - distance) / (holeRadius * 1.3f)) * (1 - RoomDecals[i].Pattern); + + decalMask = max(decalMask, (edge * fade + hole) * RoomDecals[i].Opacity); + } + + lighting *= (1.0 - decalMask); + } + + // Caustics + if (Caustics) + { + float attenuation = saturate(dot(float3(0.0f, -1.0f, 0.0f), normal)); + + float3 blending = abs(normal); + blending = normalize(max(blending, 0.00001f)); + float b = (blending.x + blending.y + blending.z); + blending /= float3(b, b, b); + + float3 p = frac(input.WorldPosition.xyz / 2048.0f); + + float2 uv_x = CausticsStartUV + float2(p.z, p.y) * CausticsSize; + float2 uv_y = CausticsStartUV + float2(p.z, p.x) * CausticsSize; + float2 uv_z = CausticsStartUV + float2(p.y, p.x) * CausticsSize; + + float3 xaxis = CausticsTexture.SampleLevel(CausticsTextureSampler, uv_x, 0).xyz; + float3 yaxis = CausticsTexture.SampleLevel(CausticsTextureSampler, uv_y, 0).xyz; + float3 zaxis = CausticsTexture.SampleLevel(CausticsTextureSampler, uv_z, 0).xyz; + + float3 xc = xaxis * blending.x; + float3 yc = yaxis * blending.y; + float3 zc = zaxis * blending.z; + + float3 caustics = xc + yc + zc; + + lighting += (caustics * attenuation * 2.0f); + } + + // Emissive materials + lighting += emissive; + + // Fog bulbs and final color and light mixing + lighting -= float3(input.FogBulbs.w, input.FogBulbs.w, input.FogBulbs.w); + output.Color.xyz = output.Color.xyz * lighting * occlusion; + output.Color.xyz = saturate(output.Color.xyz); + + output.Color = DoFogBulbsForPixel(output.Color, float4(input.FogBulbs.xyz, 1.0f)); + output.Color = DoDistanceFogForPixel(output.Color, FogColor, input.DistanceFog); + + return output; +} diff --git a/TombEngine/Shaders/ShaderLight.hlsli b/TombEngine/Shaders/ShaderLight.hlsli index 49efbcdd71..e589718ab9 100644 --- a/TombEngine/Shaders/ShaderLight.hlsli +++ b/TombEngine/Shaders/ShaderLight.hlsli @@ -4,6 +4,18 @@ #include "./CBCamera.hlsli" #include "./Math.hlsli" +#define COLOR_MODULATION_SCALE 2.0f + +float3 ModulateColor(float3 color) +{ + return (color * COLOR_MODULATION_SCALE); +} + +float3 GammaCorrection(float3 color) +{ + return saturate(pow(color, 1.0f / Gamma)); +} + static float RoughnessToExpMul(float roughness) { float r = saturate(roughness); @@ -43,7 +55,7 @@ float3 DoSpecularPoint(float3 pos, float3 n, ShaderLight light, float strength, float3 DoSpecularSun(float3 n, ShaderLight light, float strength, float specularIntensity, float roughness) { float m = saturate(sign(strength)); - + float3 lightDir = -normalize(light.Direction); float3 reflectDir = reflect(lightDir, n); @@ -99,10 +111,10 @@ float3 DoPointLight(float3 pos, float3 normal, ShaderLight light) float3 lightVec = light.Position.xyz - pos; float distance = length(lightVec); float3 lightDir = normalize(lightVec); - + float attenuation = saturate((light.Out - distance) / (light.Out - light.In)); float d = saturate(dot(normal, lightDir)); - + return saturate(light.Color.xyz * light.Intensity * attenuation * d); } @@ -111,7 +123,7 @@ float3 DoShadowLight(float3 pos, float3 normal, ShaderLight light) float3 lightVec = light.Position.xyz - pos; float distance = length(lightVec); float3 lightDir = normalize(lightVec); - + float attenuation = saturate((light.Out - distance) / (light.Out - light.In)); float d = saturate(dot(normal, lightDir)); @@ -140,7 +152,7 @@ void DoPointAndSpotLight(float3 pos, float3 normal, ShaderLight light, float spe float3 lightVec = light.Position.xyz - pos; float distance = length(lightVec); float3 lightDir = normalize(lightVec); - + float cosine = dot(-lightDir, light.Direction.xyz); float distanceAttenuation = saturate((light.Out - distance) / (light.Out - light.In)); float angleAttenuation = saturate((cosine - light.OutRange) / (light.InRange - light.OutRange)); @@ -148,7 +160,7 @@ void DoPointAndSpotLight(float3 pos, float3 normal, ShaderLight light, float spe float d = saturate(dot(normal, lightDir)); pointOutput = saturate(light.Color.xyz * light.Intensity * distanceAttenuation * d); spotOutput = saturate(light.Color.xyz * light.Intensity * angleAttenuation * distanceAttenuation * d); - + pointOutput += DoSpecularSpot(pos, normal, light, 0.0f, specularIntensity, roughness); spotOutput += DoSpecularSpot(pos, normal, light, 0.0f, specularIntensity, roughness); } @@ -337,7 +349,7 @@ float4 DoFogBulbsForVertex(float3 pos) for (int i = 0; i < NumFogBulbs; i++) { float fogFactor = DoFogBulb(pos, FogBulbs[i]); - float3 fogColor = FogBulbs[i].Color.xyz * fogFactor; + float3 fogColor = ModulateColor(FogBulbs[i].Color.xyz) * fogFactor; fog.xyz += fogColor; fog.w += fogFactor; @@ -355,7 +367,7 @@ float4 DoFogBulbsForSky(float3 pos) for (int i = 0; i < NumFogBulbs; i++) { float fogFactor = DoFogBulbForSky(pos, FogBulbs[i]); - float3 fogColor = FogBulbs[i].Color.xyz * fogFactor; + float3 fogColor = ModulateColor(FogBulbs[i].Color.xyz) * fogFactor; fog.xyz += fogColor; fog.w += fogFactor; @@ -366,7 +378,7 @@ float4 DoFogBulbsForSky(float3 pos) return fog; } -float3 CombineLights(float3 ambient, float3 vertex, float3 tex, float3 pos, float3 normal, float sheen, +float3 CombineLights(float3 ambient, float3 vertex, float3 tex, float3 pos, float3 normal, float sheen, const ShaderLight lights[MAX_LIGHTS_PER_ITEM], int numLights, float fogBulbsDensity, float3 emissive, float specular, float roughness) { float3 diffuse = 0; @@ -375,34 +387,34 @@ float3 CombineLights(float3 ambient, float3 vertex, float3 tex, float3 pos, floa int lightTypeMask = (numLights & ~LT_MASK); numLights = numLights & LT_MASK; - + for (int i = 0; i < numLights; i++) { if (lightTypeMask & LT_MASK_SUN) { float isSun = step(0.5f, float(lights[i].Type == LT_SUN)); - diffuse += isSun * DoDirectionalLight(pos, normal, lights[i]); - spec += isSun * DoSpecularSun(normal, lights[i], sheen, specular, roughness); - } + diffuse += isSun * ModulateColor(DoDirectionalLight(pos, normal, lights[i])); + spec += isSun * ModulateColor(DoSpecularSun(normal, lights[i], sheen, specular, roughness)); + } if (lightTypeMask & LT_MASK_POINT) { float isPoint = step(0.5f, float(lights[i].Type == LT_POINT)); - diffuse += isPoint * DoPointLight(pos, normal, lights[i]); - spec += isPoint * DoSpecularPoint(pos, normal, lights[i], sheen, specular, roughness); - } + diffuse += isPoint * ModulateColor(DoPointLight(pos, normal, lights[i])); + spec += isPoint * ModulateColor(DoSpecularPoint(pos, normal, lights[i], sheen, specular, roughness)); + } if (lightTypeMask & LT_MASK_SPOT) { float isSpot = step(0.5f, float(lights[i].Type == LT_SPOT)); - diffuse += isSpot * DoSpotLight(pos, normal, lights[i]); - spec += isSpot * DoSpecularSpot(pos, normal, lights[i], sheen, specular, roughness); - } - + diffuse += isSpot * ModulateColor(DoSpotLight(pos, normal, lights[i])); + spec += isSpot * ModulateColor(DoSpecularSpot(pos, normal, lights[i], sheen, specular, roughness)); + } + if (lightTypeMask & LT_MASK_SHADOW) { float isShadow = step(0.5f, float(lights[i].Type == LT_SHADOW)); - shadow += isShadow * DoShadowLight(pos, normal, lights[i]); + shadow += isShadow * ModulateColor(DoShadowLight(pos, normal, lights[i])); } } @@ -410,7 +422,7 @@ float3 CombineLights(float3 ambient, float3 vertex, float3 tex, float3 pos, floa diffuse *= tex; float3 ambTex = (ambient - shadow) * tex; - float3 combined = ambTex + diffuse + spec + emissive; + float3 combined = ambTex + diffuse + spec + emissive; combined -= float3(fogBulbsDensity, fogBulbsDensity, fogBulbsDensity); diff --git a/TombEngine/Shaders/SpriteEffects.hlsli b/TombEngine/Shaders/SpriteEffects.hlsli index f4549c25a2..5d0d2be898 100644 --- a/TombEngine/Shaders/SpriteEffects.hlsli +++ b/TombEngine/Shaders/SpriteEffects.hlsli @@ -61,12 +61,13 @@ float4 DoLaserBarrierEffect(float3 input, float4 output, float2 uv, float faceFa color.rgb *= noiseValue2 + 0.6f; color.rgb += noiseValue3; + color.a *= noiseValue + 0.01f; color.rgb -= shadowx + 0.1f; - color.a *= noiseValue2 + 0.9f; - color.a *= noiseValue3 + 2.0f; + color.a *= noiseValue2 + 0.9f; + color.a *= noiseValue3 + 2.0f; float fade0 = faceFactor * max(0.0, 1.0 - dot(float2(BLENDING, BLENDING), float2(gradL, gradT))); float fade1 = faceFactor * max(0.0, 1.0 - dot(float2(BLENDING, BLENDING), float2(gradL, gradB))); @@ -94,10 +95,11 @@ float4 DoLaserBarrierEffect(float3 input, float4 output, float2 uv, float faceFa { decayFactor = (1.0f - uv.y) / 2; } + float fadeMask = scale * decayFactor; color *= decayFactor; - color.rgb = smoothstep(ZERO, EIGHT_FIVE, color.rgb); - return color; + color.rgb = smoothstep(ZERO, EIGHT_FIVE, color.rgb) * 0.8f; + return float4(color.rgb + (output.rgb * 1.5f * fadeMask), color.a); } float4 DoLaserBeamEffect(float3 input, float4 output, float2 uv, float faceFactor, float timeUniform) diff --git a/TombEngine/Shaders/Sprites.hlsl b/TombEngine/Shaders/Sprites.hlsl index fb8eee2682..374bb99cb2 100644 --- a/TombEngine/Shaders/Sprites.hlsl +++ b/TombEngine/Shaders/Sprites.hlsl @@ -74,12 +74,12 @@ float4 PS(PixelShaderInput input) : SV_TARGET if (RenderType == 1) { - output = DoLaserBarrierEffect(input.Position, output, input.UV, FADE_FACTOR, Frame); + output = DoLaserBarrierEffect(input.Position, float4(ModulateColor(output.rgb), output.a), input.UV, FADE_FACTOR, Frame); } if (RenderType == 2) { - output = DoLaserBeamEffect(input.Position, output, input.UV, FADE_FACTOR, Frame); + output = DoLaserBeamEffect(input.Position, float4(ModulateColor(output.rgb), output.a), input.UV, FADE_FACTOR, Frame); } output.xyz *= 1.0f - Luma(input.FogBulbs.xyz); diff --git a/TombEngine/Shaders/VertexEffects.hlsli b/TombEngine/Shaders/VertexEffects.hlsli index 20201d530a..61bd0022b1 100644 --- a/TombEngine/Shaders/VertexEffects.hlsli +++ b/TombEngine/Shaders/VertexEffects.hlsli @@ -55,9 +55,9 @@ float Wibble(uint effect, int hash) float3 Glow(float3 color, uint effect, float wibble) { float glow = DecodeGlow(effect); - + float shouldGlow = step(0.0f, glow); - float intensity = glow * lerp(-0.5f, 1.0f, wibble * 0.5f + 0.5f); + float intensity = glow * lerp(-0.5f, 1.0f, wibble * 0.5f + 0.5f) * 0.5f; float3 glowEffect = float3(intensity, intensity, intensity) * shouldGlow; return color + glowEffect; diff --git a/TombEngine/Specific/RGBAColor8Byte.cpp b/TombEngine/Specific/RGBAColor8Byte.cpp index 858b93a48f..6d4c455f25 100644 --- a/TombEngine/Specific/RGBAColor8Byte.cpp +++ b/TombEngine/Specific/RGBAColor8Byte.cpp @@ -1,17 +1,15 @@ #include "framework.h" #include "Specific/RGBAColor8Byte.h" -static byte FloatComponentToByte(float value) +static unsigned char FloatComponentToByte(float value) { - // TODO: Look into what these actually do and test them to see if they are actually not undefined. - long byteValue = std::lroundf((value / 2.0f) * 255.0f); - return (byte)byteValue; + int byteValue = std::clamp((int)std::lroundf(value * (float)UCHAR_MAX), 0, UCHAR_MAX); + return (unsigned char)byteValue; } -static float ByteComponentToFloat(byte b) +static float ByteComponentToFloat(unsigned char b) { - // TODO: Look into what these actually do and test them to see if they are actually not undefined. - float value = (b / 255.0f) * 2; + float value = (b / 255.0f); return value; } @@ -26,14 +24,14 @@ RGBAColor8Byte::RGBAColor8Byte(D3DCOLOR color) a = color & 0xFF; } -RGBAColor8Byte::RGBAColor8Byte(byte r, byte g, byte b) +RGBAColor8Byte::RGBAColor8Byte(unsigned char r, unsigned char g, unsigned char b) { SetR(r); SetG(g); SetB(b); } -RGBAColor8Byte::RGBAColor8Byte(byte r, byte g, byte b, byte a) : +RGBAColor8Byte::RGBAColor8Byte(unsigned char r, unsigned char g, unsigned char b, unsigned char a) : RGBAColor8Byte(r, g, b) { SetA(a); @@ -51,53 +49,52 @@ RGBAColor8Byte::RGBAColor8Byte(const Vector4& color) r = FloatComponentToByte(color.x); g = FloatComponentToByte(color.y); b = FloatComponentToByte(color.z); - a = FloatComponentToByte(color.w * 2); + a = FloatComponentToByte(color.w); } -byte RGBAColor8Byte::GetR() const +unsigned char RGBAColor8Byte::GetR() const { return r; } -void RGBAColor8Byte::SetR(byte v) +void RGBAColor8Byte::SetR(unsigned char v) { - r = std::clamp(v, 0, 255); + r = std::clamp(v, 0, UCHAR_MAX); } -byte RGBAColor8Byte::GetG() const +unsigned char RGBAColor8Byte::GetG() const { return g; } -void RGBAColor8Byte::SetG(byte v) +void RGBAColor8Byte::SetG(unsigned char v) { - g = std::clamp(v, 0, 255); + g = std::clamp(v, 0, UCHAR_MAX); } -byte RGBAColor8Byte::GetB() const +unsigned char RGBAColor8Byte::GetB() const { return b; } -void RGBAColor8Byte::SetB(byte v) +void RGBAColor8Byte::SetB(unsigned char v) { - b = std::clamp(v, 0, 255); + b = std::clamp(v, 0, UCHAR_MAX); } -byte RGBAColor8Byte::GetA() const +unsigned char RGBAColor8Byte::GetA() const { return a; } -void RGBAColor8Byte::SetA(byte v) +void RGBAColor8Byte::SetA(unsigned char v) { - a = std::clamp(v, 0, 255); + a = std::clamp(v, 0, UCHAR_MAX); } RGBAColor8Byte::operator Color() const { - // Alpha exists on normalized range [0.0f, 1.0f], unlike color components which exist on range [0.0f, 2.0f]. - return Color(ByteComponentToFloat(r), ByteComponentToFloat(g), ByteComponentToFloat(b), ByteComponentToFloat(a) / 2.0f); + return Color(ByteComponentToFloat(r), ByteComponentToFloat(g), ByteComponentToFloat(b), ByteComponentToFloat(a)); } RGBAColor8Byte::operator Vector3() const @@ -107,8 +104,7 @@ RGBAColor8Byte::operator Vector3() const RGBAColor8Byte::operator Vector4() const { - // Alpha exists on normalized range [0.0f, 1.0f], unlike color components which exist on range [0.0f, 2.0f]. - return Vector4(ByteComponentToFloat(r), ByteComponentToFloat(g), ByteComponentToFloat(b), ByteComponentToFloat(a) / 2.0f); + return Vector4(ByteComponentToFloat(r), ByteComponentToFloat(g), ByteComponentToFloat(b), ByteComponentToFloat(a)); } RGBAColor8Byte::operator D3DCOLOR() const diff --git a/TombEngine/Specific/RGBAColor8Byte.h b/TombEngine/Specific/RGBAColor8Byte.h index d59f1ab6d1..114f776e5a 100644 --- a/TombEngine/Specific/RGBAColor8Byte.h +++ b/TombEngine/Specific/RGBAColor8Byte.h @@ -6,31 +6,31 @@ class RGBAColor8Byte { private: // Members - byte r = 0; - byte g = 0; - byte b = 0; - byte a = 255; + unsigned char r = 0; + unsigned char g = 0; + unsigned char b = 0; + unsigned char a = 255; public: // Constructors RGBAColor8Byte() = default; RGBAColor8Byte(D3DCOLOR color); - RGBAColor8Byte(byte r, byte g, byte b); - RGBAColor8Byte(byte r, byte g, byte b, byte a); + RGBAColor8Byte(unsigned char r, unsigned char g, unsigned char b); + RGBAColor8Byte(unsigned char r, unsigned char g, unsigned char b, unsigned char a); RGBAColor8Byte(const Vector3& color); RGBAColor8Byte(const Vector4& color); // Getters - byte GetR() const; - byte GetG() const; - byte GetB() const; - byte GetA() const; + unsigned char GetR() const; + unsigned char GetG() const; + unsigned char GetB() const; + unsigned char GetA() const; // Setters - void SetR(byte value); - void SetG(byte value); - void SetB(byte value); - void SetA(byte value); + void SetR(unsigned char value); + void SetG(unsigned char value); + void SetB(unsigned char value); + void SetA(unsigned char value); // Operators operator Color() const; diff --git a/TombEngine/Specific/configuration.cpp b/TombEngine/Specific/configuration.cpp index e3639ee1e5..4fe100724f 100644 --- a/TombEngine/Specific/configuration.cpp +++ b/TombEngine/Specific/configuration.cpp @@ -190,7 +190,8 @@ bool SaveConfiguration() SetBoolRegKey(graphicsKey, REGKEY_ENABLE_DECALS, g_Configuration.EnableDecals) != ERROR_SUCCESS || SetDWORDRegKey(graphicsKey, REGKEY_ANTIALIASING_MODE, (DWORD)g_Configuration.AntialiasingMode) != ERROR_SUCCESS || SetBoolRegKey(graphicsKey, REGKEY_AMBIENT_OCCLUSION, g_Configuration.EnableAmbientOcclusion) != ERROR_SUCCESS || - SetBoolRegKey(graphicsKey, REGKEY_HIGH_FRAMERATE, g_Configuration.EnableHighFramerate) != ERROR_SUCCESS) + SetBoolRegKey(graphicsKey, REGKEY_HIGH_FRAMERATE, g_Configuration.EnableHighFramerate) != ERROR_SUCCESS || + SetDWORDRegKey(graphicsKey, REGKEY_GAMMA, (int)(RoundToStep(g_Configuration.Gamma, GAMMA_STEP) * 10.0f)) != ERROR_SUCCESS) { RegCloseKey(rootKey); RegCloseKey(graphicsKey); @@ -379,6 +380,7 @@ bool LoadConfiguration() DWORD antialiasingMode = 1; bool enableAmbientOcclusion = false; bool enableHighFramerate = false; + DWORD gammaCorrection = 10; // Load Graphics keys. if (GetDWORDRegKey(graphicsKey, REGKEY_SCREEN_WIDTH, &screenWidth, 0) != ERROR_SUCCESS || @@ -391,7 +393,8 @@ bool LoadConfiguration() GetBoolRegKey(graphicsKey, REGKEY_ENABLE_DECALS, &enableDecals, true) != ERROR_SUCCESS || GetDWORDRegKey(graphicsKey, REGKEY_ANTIALIASING_MODE, &antialiasingMode, true) != ERROR_SUCCESS || GetBoolRegKey(graphicsKey, REGKEY_AMBIENT_OCCLUSION, &enableAmbientOcclusion, false) != ERROR_SUCCESS || - GetBoolRegKey(graphicsKey, REGKEY_HIGH_FRAMERATE, &enableHighFramerate, false) != ERROR_SUCCESS) + GetBoolRegKey(graphicsKey, REGKEY_HIGH_FRAMERATE, &enableHighFramerate, false) != ERROR_SUCCESS || + GetDWORDRegKey(graphicsKey, REGKEY_GAMMA, &gammaCorrection, 10) != ERROR_SUCCESS) { RegCloseKey(rootKey); RegCloseKey(graphicsKey); @@ -531,6 +534,7 @@ bool LoadConfiguration() g_Configuration.ShadowMapSize = shadowMapSize; g_Configuration.EnableAmbientOcclusion = enableAmbientOcclusion; g_Configuration.EnableHighFramerate = enableHighFramerate; + g_Configuration.Gamma = std::clamp(RoundToStep((float)gammaCorrection / 10.0f, GAMMA_STEP), GAMMA_MIN, GAMMA_MAX); g_Configuration.EnableSound = enableSound; g_Configuration.EnableReverb = enableReverb; diff --git a/TombEngine/Specific/configuration.h b/TombEngine/Specific/configuration.h index d7440d4ed4..d33819f72a 100644 --- a/TombEngine/Specific/configuration.h +++ b/TombEngine/Specific/configuration.h @@ -19,6 +19,7 @@ constexpr auto REGKEY_INPUT = "Input"; constexpr auto REGKEY_SCREEN_WIDTH = "ScreenWidth"; constexpr auto REGKEY_SCREEN_HEIGHT = "ScreenHeight"; constexpr auto REGKEY_ENABLE_WINDOWED_MODE = "EnableWindowedMode"; +constexpr auto REGKEY_GAMMA = "GammaCorrection"; constexpr auto REGKEY_SHADOWS = "ShadowsMode"; constexpr auto REGKEY_SHADOW_MAP_SIZE = "ShadowMapSize"; constexpr auto REGKEY_SHADOW_BLOBS_MAX = "ShadowBlobsMax"; @@ -68,6 +69,7 @@ struct GameConfiguration int ScreenWidth = 0; int ScreenHeight = 0; + float Gamma = 1.0f; bool EnableWindowedMode = false; ShadowMode ShadowType = ShadowMode::None; int ShadowMapSize = DEFAULT_SHADOW_MAP_SIZE; diff --git a/TombEngine/Specific/level.cpp b/TombEngine/Specific/level.cpp index f5cb1ffc7d..8eed54cffc 100644 --- a/TombEngine/Specific/level.cpp +++ b/TombEngine/Specific/level.cpp @@ -35,6 +35,7 @@ using TEN::Renderer::g_Renderer; using namespace TEN::Entities::Doors; using namespace TEN::Input; +using namespace TEN::SpotCam; using namespace TEN::Utils; constexpr auto DUMMY_LEVEL_NAME = "dummy.ten"; @@ -606,10 +607,27 @@ void LoadCameras() int numSpotcams = ReadCount(); TENLog("Flyby camera count: " + std::to_string(numSpotcams), LogLevel::Info); - // TODO: Read properly! - SpotCam.resize(numSpotcams); - if (numSpotcams != 0) - ReadBytes(SpotCam.data(), numSpotcams * sizeof(SPOTCAM)); + g_Level.SpotCams.resize(numSpotcams); + for (int i = 0; i < numSpotcams; i++) + { + auto& cam = g_Level.SpotCams[i]; + cam.Position.x = ReadInt32(); + cam.Position.y = ReadInt32(); + cam.Position.z = ReadInt32(); + cam.Target.x = ReadInt32(); + cam.Target.y = ReadInt32(); + cam.Target.z = ReadInt32(); + + cam.Sequence = ReadInt32(); + cam.Camera = ReadInt32(); + + cam.FOV = ReadInt16(); + cam.Roll = ReadInt16(); + cam.Timer = ReadInt16(); + cam.Speed = ReadInt16(); + cam.Flags = ReadInt16(); + cam.RoomNumber = ReadInt32(); + } int sinkCount = ReadCount(); TENLog("Sink count: " + std::to_string(sinkCount), LogLevel::Info); diff --git a/TombEngine/Specific/level.h b/TombEngine/Specific/level.h index 4049ced558..79608f59f5 100644 --- a/TombEngine/Specific/level.h +++ b/TombEngine/Specific/level.h @@ -5,6 +5,7 @@ #include "Game/items.h" #include "Game/itemdata/creature_info.h" #include "Game/room.h" +#include "Game/spotcam.h" #include "Renderer/RendererEnums.h" #include "Sound/sound.h" #include "Specific/IO/ChunkId.h" @@ -16,6 +17,7 @@ using namespace TEN::Animation; using namespace TEN::Control::Volumes; +using namespace TEN::SpotCam; struct ChunkId; struct LEB128; @@ -155,6 +157,7 @@ struct LevelData // Misc. std::vector Cameras = {}; + std::vector SpotCams = {}; std::vector GlobalEventSets = {}; std::vector VolumeEventSets = {}; std::vector LoopedEventSetIndices = {}; diff --git a/TombEngine/Specific/winmain.cpp b/TombEngine/Specific/winmain.cpp index 923032aab5..604cf5f76c 100644 --- a/TombEngine/Specific/winmain.cpp +++ b/TombEngine/Specific/winmain.cpp @@ -654,6 +654,9 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine g_GameScript->ShortenTENCalls(); g_GameFlow->SetGameDir(gameDir); g_GameFlow->LoadFlowScript(); + + // Load global variables from external file. + SaveGame::LoadGlobalVars(); } catch (TENScriptException const& e) { diff --git a/TombEngine/TombEngine.vcxproj b/TombEngine/TombEngine.vcxproj index 91cbb9a3b1..365e70b6af 100644 --- a/TombEngine/TombEngine.vcxproj +++ b/TombEngine/TombEngine.vcxproj @@ -879,9 +879,10 @@ if not exist "%ScriptsDir%\Strings.lua" xcopy /Y "$(SolutionDir)Scripts\Strings. - + + - + @@ -895,6 +896,8 @@ if not exist "%ScriptsDir%\Strings.lua" xcopy /Y "$(SolutionDir)Scripts\Strings. + + @@ -1370,6 +1373,8 @@ if not exist "%ScriptsDir%\Strings.lua" xcopy /Y "$(SolutionDir)Scripts\Strings. + +